From 5cb8448182fd5edbdb5b7c2fb93349c56ccaef11 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Thu, 11 Jun 2020 15:18:25 -0400 Subject: [PATCH 01/12] Add Create button to Polices List header --- .../use_navigate_to_app_event_handler.ts | 2 +- .../pages/policy/view/policy_list.tsx | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts index 759d72419e42e..2436ae967d3a7 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts @@ -6,7 +6,7 @@ import { MouseEventHandler, useCallback } from 'react'; import { ApplicationStart } from 'kibana/public'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useKibana } from '../../lib/kibana'; type NavigateToAppHandlerProps = Parameters; type EventHandlerCallback = MouseEventHandler; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index d34a6d2d93893..ae9d292609c7b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -17,6 +17,7 @@ import { EuiContextMenuItem, EuiButtonIcon, EuiContextMenuPanel, + EuiButton, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -35,6 +36,7 @@ import { ManagementPageView } from '../../../components/management_page_view'; import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { getManagementUrl } from '../../../common/routing'; import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time'; +import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -116,6 +118,15 @@ export const PolicyList = React.memo(() => { selectApiError: apiError, } = usePolicyListSelector(selector); + const handleCreatePolicyClick = useNavigateToAppEventHandler('ingestManager', { + path: '#/integrations/endpoint-0.3.0/add-datasource', + state: { + onCancelNavigateTo: [], + onCancelUrl: [], + onSaveNavigateTo: [], + }, + }); + useEffect(() => { if (apiError) { notifications.toasts.danger({ @@ -266,6 +277,14 @@ export const PolicyList = React.memo(() => { headerLeft={i18n.translate('xpack.securitySolution.endpoint.policyList.viewTitle', { defaultMessage: 'Policies', })} + headerRight={ + + + + } bodyHeader={ Date: Thu, 11 Jun 2020 16:04:34 -0400 Subject: [PATCH 02/12] Add Context Provider to bridge History from Kibana to Hash router --- .../hooks/use_intra_app_state.tsx | 62 ++++ .../applications/ingest_manager/index.tsx | 301 +++++++++--------- 2 files changed, 218 insertions(+), 145 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx new file mode 100644 index 0000000000000..18fdf50631b1d --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useContext, useMemo } from 'react'; +import { ApplicationStart, AppMountParameters } from 'kibana/public'; +import { useLocation } from 'react-router-dom'; + +interface IntraAppState { + forRoute: string; + routeState?: {}; +} + +const IntraAppStateContext = React.createContext({ forRoute: '' }); +const wasHandled = new WeakSet(); + +/** + * Provides a bridget between Kibana's ScopedHistory instance (normally used with BrowserRouter) + * and the Hash router used within the app in order to enable state to be used between kibana + * apps + */ +export const IntraAppStateProvider = memo<{ + kibanaScopedHistory: AppMountParameters['history']; + children: React.ReactNode; +}>(({ kibanaScopedHistory, children }) => { + const internalAppToAppState = useMemo(() => { + return { + forRoute: kibanaScopedHistory.location.hash.substr(1), + routeState: (kibanaScopedHistory.location.state || undefined) as Parameters< + ApplicationStart['navigateToApp'] + >, + }; + }, [kibanaScopedHistory.location.hash, kibanaScopedHistory.location.state]); + return ( + + {children} + + ); +}); + +/** + * Retrieve UI Route state from the React Router History for the current URL location + */ +export const useIntraAppState = (): IntraAppState | undefined => { + const location = useLocation(); + const intraAppState = useContext(IntraAppStateContext); + if (!intraAppState) { + throw new Error('Hook called outside of IntraAppStateContext'); + } + return useMemo((): IntraAppState | undefined => { + // Due to the use of HashRouter in Ingest, we only want state to be returned + // once so that it does not impact navigation to the page from within the + // ingest app. side affect is that the browser back button would not work + // consistently either. + if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) { + wasHandled.add(intraAppState); + return intraAppState; + } + }, [intraAppState, location.pathname]); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx index ed5a75ce6c991..623df428b7dd9 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useState } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import ReactDOM from 'react-dom'; import { useObservable } from 'react-use'; import { HashRouter as Router, Redirect, Switch, Route, RouteProps } from 'react-router-dom'; @@ -28,6 +28,7 @@ import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks'; import { FleetStatusProvider } from './hooks/use_fleet_status'; import './index.scss'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; +import { IntraAppStateProvider } from './hooks/use_intra_app_state'; export interface ProtectedRouteProps extends RouteProps { isAllowed?: boolean; @@ -56,163 +57,170 @@ const ErrorLayout = ({ children }: { children: JSX.Element }) => ( ); -const IngestManagerRoutes = ({ ...rest }) => { - const { epm, fleet } = useConfig(); - const { notifications } = useCore(); +const IngestManagerRoutes = memo<{ history: AppMountParameters['history']; basepath: string }>( + ({ history, ...rest }) => { + const { epm, fleet } = useConfig(); + const { notifications } = useCore(); - const [isPermissionsLoading, setIsPermissionsLoading] = useState(false); - const [permissionsError, setPermissionsError] = useState(); - const [isInitialized, setIsInitialized] = useState(false); - const [initializationError, setInitializationError] = useState(null); + const [isPermissionsLoading, setIsPermissionsLoading] = useState(false); + const [permissionsError, setPermissionsError] = useState(); + const [isInitialized, setIsInitialized] = useState(false); + const [initializationError, setInitializationError] = useState(null); - useEffect(() => { - (async () => { - setIsPermissionsLoading(false); - setPermissionsError(undefined); - setIsInitialized(false); - setInitializationError(null); - try { - setIsPermissionsLoading(true); - const permissionsResponse = await sendGetPermissionsCheck(); + useEffect(() => { + (async () => { setIsPermissionsLoading(false); - if (permissionsResponse.data?.success) { - try { - const setupResponse = await sendSetup(); - if (setupResponse.error) { - setInitializationError(setupResponse.error); + setPermissionsError(undefined); + setIsInitialized(false); + setInitializationError(null); + try { + setIsPermissionsLoading(true); + const permissionsResponse = await sendGetPermissionsCheck(); + setIsPermissionsLoading(false); + if (permissionsResponse.data?.success) { + try { + const setupResponse = await sendSetup(); + if (setupResponse.error) { + setInitializationError(setupResponse.error); + } + } catch (err) { + setInitializationError(err); } - } catch (err) { - setInitializationError(err); + setIsInitialized(true); + } else { + setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR'); } - setIsInitialized(true); - } else { - setPermissionsError(permissionsResponse.data?.error || 'REQUEST_ERROR'); + } catch (err) { + setPermissionsError('REQUEST_ERROR'); } - } catch (err) { - setPermissionsError('REQUEST_ERROR'); - } - })(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + })(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - if (isPermissionsLoading || permissionsError) { - return ( - - {isPermissionsLoading ? ( - - ) : permissionsError === 'REQUEST_ERROR' ? ( - - } - error={i18n.translate('xpack.ingestManager.permissionsRequestErrorMessageDescription', { - defaultMessage: 'There was a problem checking Ingest Manager permissions', - })} - /> - ) : ( - - + {isPermissionsLoading ? ( + + ) : permissionsError === 'REQUEST_ERROR' ? ( + - {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( - - ) : ( - - )} - + } - body={ -

- {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( - superuser }} - /> - ) : ( - - )} -

+ error={i18n.translate( + 'xpack.ingestManager.permissionsRequestErrorMessageDescription', + { + defaultMessage: 'There was a problem checking Ingest Manager permissions', + } + )} + /> + ) : ( + + + {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( + + ) : ( + + )} + + } + body={ +

+ {permissionsError === 'MISSING_SUPERUSER_ROLE' ? ( + superuser }} + /> + ) : ( + + )} +

+ } + /> +
+ )} +
+ ); + } + + if (!isInitialized || initializationError) { + return ( + + {initializationError ? ( + } + error={initializationError} /> - - )} - - ); - } + ) : ( + + )} + + ); + } - if (!isInitialized || initializationError) { return ( - - {initializationError ? ( - - } - error={initializationError} - /> - ) : ( - - )} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; +); const IngestManagerApp = ({ basepath, @@ -220,12 +228,14 @@ const IngestManagerApp = ({ setupDeps, startDeps, config, + history, }: { basepath: string; coreStart: CoreStart; setupDeps: IngestManagerSetupDeps; startDeps: IngestManagerStartDeps; config: IngestManagerConfigType; + history: AppMountParameters['history']; }) => { const isDarkMode = useObservable(coreStart.uiSettings.get$('theme:darkMode')); return ( @@ -234,7 +244,7 @@ const IngestManagerApp = ({ - + @@ -245,7 +255,7 @@ const IngestManagerApp = ({ export function renderApp( coreStart: CoreStart, - { element, appBasePath }: AppMountParameters, + { element, appBasePath, history }: AppMountParameters, setupDeps: IngestManagerSetupDeps, startDeps: IngestManagerStartDeps, config: IngestManagerConfigType @@ -258,6 +268,7 @@ export function renderApp( setupDeps={setupDeps} startDeps={startDeps} config={config} + history={history} />, element ); From 91bda2e7a0a99b0a7a9ad7b46edbdf8c23db4f14 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 12 Jun 2020 12:03:39 -0400 Subject: [PATCH 03/12] Added support for passing of state from endpoint to ingest on policy --- .../hooks/use_intra_app_state.tsx | 24 ++++++---- .../create_datasource_page/index.tsx | 48 +++++++++++++++---- .../ingest_manager/types/index.ts | 2 + .../types/intra_app_route_state.ts | 27 +++++++++++ x-pack/plugins/ingest_manager/public/index.ts | 1 + .../use_navigate_to_app_event_handler.ts | 15 +++--- .../pages/policy/view/policy_list.tsx | 25 ++++++---- 7 files changed, 109 insertions(+), 33 deletions(-) create mode 100644 x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/intra_app_route_state.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx index 18fdf50631b1d..b392e5fb3323c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx @@ -5,13 +5,17 @@ */ import React, { memo, useContext, useMemo } from 'react'; -import { ApplicationStart, AppMountParameters } from 'kibana/public'; +import { AppMountParameters } from 'kibana/public'; import { useLocation } from 'react-router-dom'; +import { AnyIntraAppRouteState } from '../types'; -interface IntraAppState { +interface IntraAppState { forRoute: string; - routeState?: {}; + routeState?: S; } +type UseIntraAppStateHook = () => + | IntraAppState['routeState'] + | undefined; const IntraAppStateContext = React.createContext({ forRoute: '' }); const wasHandled = new WeakSet(); @@ -28,9 +32,7 @@ export const IntraAppStateProvider = memo<{ const internalAppToAppState = useMemo(() => { return { forRoute: kibanaScopedHistory.location.hash.substr(1), - routeState: (kibanaScopedHistory.location.state || undefined) as Parameters< - ApplicationStart['navigateToApp'] - >, + routeState: kibanaScopedHistory.location.state as AnyIntraAppRouteState, }; }, [kibanaScopedHistory.location.hash, kibanaScopedHistory.location.state]); return ( @@ -41,22 +43,24 @@ export const IntraAppStateProvider = memo<{ }); /** - * Retrieve UI Route state from the React Router History for the current URL location + * Retrieve UI Route state from the React Router History for the current URL location. + * This state can be used by other Kibana Apps to influence certain behaviours in Ingest, for example, + * redirecting back to an given Application after a craete action. */ -export const useIntraAppState = (): IntraAppState | undefined => { +export const useIntraAppState: UseIntraAppStateHook = () => { const location = useLocation(); const intraAppState = useContext(IntraAppStateContext); if (!intraAppState) { throw new Error('Hook called outside of IntraAppStateContext'); } - return useMemo((): IntraAppState | undefined => { + return useMemo(() => { // Due to the use of HashRouter in Ingest, we only want state to be returned // once so that it does not impact navigation to the page from within the // ingest app. side affect is that the browser back button would not work // consistently either. if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) { wasHandled.add(intraAppState); - return intraAppState; + return intraAppState.routeState; } }, [intraAppState, location.pathname]); }; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 577f08cdc3313..93918bb20c325 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useMemo, useCallback, ReactEventHandler } from 'react'; import { useRouteMatch, useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -17,7 +17,12 @@ import { EuiSpacer, } from '@elastic/eui'; import { EuiStepProps } from '@elastic/eui/src/components/steps/step'; -import { AgentConfig, PackageInfo, NewDatasource } from '../../../types'; +import { + AgentConfig, + PackageInfo, + NewDatasource, + CreateDatasourceRouteState, +} from '../../../types'; import { useLink, useBreadcrumbs, @@ -34,12 +39,14 @@ import { StepSelectPackage } from './step_select_package'; import { StepSelectConfig } from './step_select_config'; import { StepConfigureDatasource } from './step_configure_datasource'; import { StepDefineDatasource } from './step_define_datasource'; +import { useIntraAppState } from '../../../hooks/use_intra_app_state'; export const CreateDatasourcePage: React.FunctionComponent = () => { const { notifications, chrome: { getIsNavDrawerLocked$ }, uiSettings, + application: { navigateToApp }, } = useCore(); const { fleet: { enabled: isFleetEnabled }, @@ -49,6 +56,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { } = useRouteMatch(); const { getHref, getPath } = useLink(); const history = useHistory(); + const routeState = useIntraAppState(); const from: CreateDatasourceFrom = configId ? 'config' : 'package'; const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); @@ -171,10 +179,24 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { }; // Cancel path - const cancelUrl = - from === 'config' - ? getHref('configuration_details', { configId: agentConfig?.id || configId }) + const cancelUrl = useMemo(() => { + if (routeState && routeState.onCancelUrl) { + return routeState.onCancelUrl; + } + return from === 'config' + ? getHref('configuration_details', { configId: agentConfigId || configId }) : getHref('integration_details', { pkgkey }); + }, [agentConfigId, configId, from, getHref, pkgkey, routeState]); + + const cancelClickHandler: ReactEventHandler = useCallback( + (ev) => { + if (routeState && routeState.onCancelNavigateTo) { + ev.preventDefault(); + navigateToApp(...routeState.onCancelNavigateTo); + } + }, + [routeState, navigateToApp] + ); // Save datasource const saveDatasource = async () => { @@ -193,9 +215,18 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { setFormState('CONFIRM'); return; } - const { error } = await saveDatasource(); + const { error, data } = await saveDatasource(); if (!error) { - history.push(getPath('configuration_details', { configId: agentConfig?.id || configId })); + if (routeState && routeState.onSaveNavigateTo) { + navigateToApp( + ...(typeof routeState.onSaveNavigateTo === 'function' + ? routeState.onSaveNavigateTo(data!.item) + : routeState.onSaveNavigateTo) + ); + } else { + history.push(getPath('configuration_details', { configId: agentConfig?.id || configId })); + } + notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.createDatasource.addedNotificationTitle', { defaultMessage: `Successfully added '{datasourceName}'`, @@ -334,7 +365,8 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { > - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + + | ((newDatasource: Datasource) => Parameters); + /** On cancel, navigate to the given app */ + onCancelNavigateTo?: Parameters; + /** Url to be used on cancel links */ + onCancelUrl?: string; +} + +/** + * All possible Route states. + */ +export type AnyIntraAppRouteState = CreateDatasourceRouteState; diff --git a/x-pack/plugins/ingest_manager/public/index.ts b/x-pack/plugins/ingest_manager/public/index.ts index e26f310b6d9c6..9f4893ac6e499 100644 --- a/x-pack/plugins/ingest_manager/public/index.ts +++ b/x-pack/plugins/ingest_manager/public/index.ts @@ -19,3 +19,4 @@ export { } from './applications/ingest_manager/sections/agent_config/create_datasource_page/components/custom_configure_datasource'; export { NewDatasource } from './applications/ingest_manager/types'; +export * from './applications/ingest_manager/types/intra_app_route_state'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts index 2436ae967d3a7..cb4de29802e54 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_navigate_to_app_event_handler.ts @@ -5,10 +5,13 @@ */ import { MouseEventHandler, useCallback } from 'react'; -import { ApplicationStart } from 'kibana/public'; +import { ApplicationStart, NavigateToAppOptions } from 'kibana/public'; import { useKibana } from '../../lib/kibana'; -type NavigateToAppHandlerProps = Parameters; +type NavigateToAppHandlerOptions = NavigateToAppOptions & { + state?: S; + onClick?: EventHandlerCallback; +}; type EventHandlerCallback = MouseEventHandler; /** @@ -25,14 +28,12 @@ type EventHandlerCallback = MouseEventHandlerSee configs */ -export const useNavigateToAppEventHandler = ( +export const useNavigateToAppEventHandler = ( /** the app id - normally the value of the `id` in that plugin's `kibana.json` */ - appId: NavigateToAppHandlerProps[0], + appId: Parameters[0], /** Options, some of which are passed along to the app route */ - options?: NavigateToAppHandlerProps[1] & { - onClick?: EventHandlerCallback; - } + options?: NavigateToAppHandlerOptions ): EventHandlerCallback => { const { services } = useKibana(); const { path, state, onClick } = options || {}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index ae9d292609c7b..0f7b9c1959a0e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -37,6 +37,7 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { getManagementUrl } from '../../../common/routing'; import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { CreateDatasourceRouteState } from '../../../../../../ingest_manager/public'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -118,14 +119,22 @@ export const PolicyList = React.memo(() => { selectApiError: apiError, } = usePolicyListSelector(selector); - const handleCreatePolicyClick = useNavigateToAppEventHandler('ingestManager', { - path: '#/integrations/endpoint-0.3.0/add-datasource', - state: { - onCancelNavigateTo: [], - onCancelUrl: [], - onSaveNavigateTo: [], - }, - }); + const handleCreatePolicyClick = useNavigateToAppEventHandler( + 'ingestManager', + { + path: '#/integrations/endpoint-0.3.0/add-datasource', + state: { + onCancelNavigateTo: [ + 'securitySolution', + { path: getManagementUrl({ name: 'policyList' }) }, + ], + onCancelUrl: services.application?.getUrlForApp('securitySolution', { + path: getManagementUrl({ name: 'policyList' }), + }), + onSaveNavigateTo: ['securitySolution', { path: getManagementUrl({ name: 'policyList' }) }], + }, + } + ); useEffect(() => { if (apiError) { From b67c0486ad60f8171394a206cf4eb49bf81fa73b Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 12 Jun 2020 12:22:15 -0400 Subject: [PATCH 04/12] Fixed up types ++ Added cancel on click handler to datasource page layout --- .../ingest_manager/hooks/use_intra_app_state.tsx | 11 +++++------ .../create_datasource_page/components/layout.tsx | 12 ++++++++++-- .../agent_config/create_datasource_page/index.tsx | 1 + 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx index b392e5fb3323c..565c5b364893c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_intra_app_state.tsx @@ -13,9 +13,6 @@ interface IntraAppState forRoute: string; routeState?: S; } -type UseIntraAppStateHook = () => - | IntraAppState['routeState'] - | undefined; const IntraAppStateContext = React.createContext({ forRoute: '' }); const wasHandled = new WeakSet(); @@ -47,7 +44,9 @@ export const IntraAppStateProvider = memo<{ * This state can be used by other Kibana Apps to influence certain behaviours in Ingest, for example, * redirecting back to an given Application after a craete action. */ -export const useIntraAppState: UseIntraAppStateHook = () => { +export function useIntraAppState(): + | IntraAppState['routeState'] + | undefined { const location = useLocation(); const intraAppState = useContext(IntraAppStateContext); if (!intraAppState) { @@ -60,7 +59,7 @@ export const useIntraAppState: UseIntraAppStateHook = () => { // consistently either. if (location.pathname === intraAppState.forRoute && !wasHandled.has(intraAppState)) { wasHandled.add(intraAppState); - return intraAppState.routeState; + return intraAppState.routeState as S; } }, [intraAppState, location.pathname]); -}; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index ccd2bc75fe223..4fec57ec79f69 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -23,13 +23,21 @@ import { CreateDatasourceFrom } from '../types'; export const CreateDatasourcePageLayout: React.FunctionComponent<{ from: CreateDatasourceFrom; cancelUrl: string; + cancelOnClick?: React.ReactEventHandler; agentConfig?: AgentConfig; packageInfo?: PackageInfo; -}> = ({ from, cancelUrl, agentConfig, packageInfo, children }) => { +}> = ({ from, cancelUrl, cancelOnClick, agentConfig, packageInfo, children }) => { const leftColumn = ( - + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { const layoutProps = { from, cancelUrl, + cancelOnClick: cancelClickHandler, agentConfig, packageInfo, }; From a97958d4ce5e47f2ba4a596980ebf83f686f6570 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Fri, 12 Jun 2020 15:56:32 -0400 Subject: [PATCH 05/12] Added hook that provide package information --- .../store/policy_list/services/ingest.ts | 19 ++++++++ .../pages/policy/view/ingest_hooks.ts | 44 +++++++++++++++++++ .../pages/policy/view/policy_list.tsx | 15 ++++++- 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts index db482e2a6bdb6..96ed5b232b931 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_list/services/ingest.ts @@ -9,6 +9,7 @@ import { GetDatasourcesRequest, GetAgentStatusResponse, DATASOURCE_SAVED_OBJECT_TYPE, + GetPackagesResponse, } from '../../../../../../../../ingest_manager/common'; import { GetPolicyListResponse, GetPolicyResponse, UpdatePolicyResponse } from '../../../types'; import { NewPolicyData } from '../../../../../../../common/endpoint/types'; @@ -17,6 +18,7 @@ const INGEST_API_ROOT = `/api/ingest_manager`; export const INGEST_API_DATASOURCES = `${INGEST_API_ROOT}/datasources`; const INGEST_API_FLEET = `${INGEST_API_ROOT}/fleet`; const INGEST_API_FLEET_AGENT_STATUS = `${INGEST_API_FLEET}/agent-status`; +const INGEST_API_EPM_PACKAGES = `${INGEST_API_ROOT}/epm/packages`; /** * Retrieves a list of endpoint specific datasources (those created with a `package.name` of @@ -93,3 +95,20 @@ export const sendGetFleetAgentStatusForConfig = ( }, }); }; + +/** + * Get Endpoint Security Package information + */ +export const sendGetEndpointSecurityPackage = async ( + http: HttpStart +): Promise => { + const options = { query: { category: 'security' } }; + const securityPackages = await http.get(INGEST_API_EPM_PACKAGES, options); + const endpointPackageInfo = securityPackages.response.find( + (epmPackage) => epmPackage.name === 'endpoint' + ); + if (!endpointPackageInfo) { + throw new Error('Endpoint package was not found.'); + } + return endpointPackageInfo; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts new file mode 100644 index 0000000000000..75e1556ff0bb0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_hooks.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; +import { Immutable } from '../../../../../common/endpoint/types'; +import { GetPackagesResponse } from '../../../../../../ingest_manager/common/types/rest_spec'; +import { sendGetEndpointSecurityPackage } from '../store/policy_list/services/ingest'; +import { useKibana } from '../../../../common/lib/kibana'; + +type UseEndpointPackageInfo = [ + /** The Package Info. will be undefined while it is being fetched */ + Immutable | undefined, + /** Boolean indicating if fetching is underway */ + boolean, + /** Any error encountered during fetch */ + Error | undefined +]; + +/** + * Hook that fetches the endpoint package info + * + * @example + * const [packageInfo, isFetching, fetchError] = useEndpointPackageInfo(); + */ +export const useEndpointPackageInfo = (): UseEndpointPackageInfo => { + const { + services: { http }, + } = useKibana(); + const [endpointPackage, setEndpointPackage] = useState(); + const [isFetching, setIsFetching] = useState(true); + const [error, setError] = useState(); + + useEffect(() => { + sendGetEndpointSecurityPackage(http) + .then((packageInfo) => setEndpointPackage(packageInfo)) + .catch((apiError) => setError(apiError)) + .finally(() => setIsFetching(false)); + }, [http]); + + return [endpointPackage, isFetching, error]; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 0f7b9c1959a0e..28fd784aed184 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -38,6 +38,7 @@ import { getManagementUrl } from '../../../common/routing'; import { FormattedDateAndTime } from '../../../../common/components/endpoint/formatted_date_time'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { CreateDatasourceRouteState } from '../../../../../../ingest_manager/public'; +import { useEndpointPackageInfo } from './ingest_hooks'; interface TableChangeCallbackArguments { page: { index: number; size: number }; @@ -110,6 +111,7 @@ export const PolicyList = React.memo(() => { const location = useLocation(); const dispatch = useDispatch<(action: PolicyListAction) => void>(); + const [packageInfo, isFetchingPackageInfo] = useEndpointPackageInfo(); const { selectPolicyItems: policyItems, selectPageIndex: pageIndex, @@ -122,7 +124,12 @@ export const PolicyList = React.memo(() => { const handleCreatePolicyClick = useNavigateToAppEventHandler( 'ingestManager', { - path: '#/integrations/endpoint-0.3.0/add-datasource', + // We redirect to Ingest's Integaration page if we can't get the package version, and + // to the Integration Endpoint Package Add Datasource if we have package information. + // Also, + // We pass along soem state information so that the Ingest page can change the behaviour + // of the cancel and submit buttons and redirect the user back to endpoint policy + path: `#/integrations${packageInfo ? `/endpoint-${packageInfo.version}/add-datasource` : ''}`, state: { onCancelNavigateTo: [ 'securitySolution', @@ -287,7 +294,11 @@ export const PolicyList = React.memo(() => { defaultMessage: 'Policies', })} headerRight={ - + Date: Fri, 12 Jun 2020 16:32:19 -0400 Subject: [PATCH 06/12] add data-test-subj prop support to Ingest components --- .../ingest_manager/components/header.tsx | 4 +++- .../ingest_manager/layouts/with_header.tsx | 13 +++++++++++-- .../create_datasource_page/components/layout.tsx | 12 +++++++++++- .../agent_config/create_datasource_page/index.tsx | 2 +- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx index 9354d94f46801..e0623108e7d39 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/header.tsx @@ -36,6 +36,7 @@ export interface HeaderProps { rightColumn?: JSX.Element; rightColumnGrow?: EuiFlexItemProps['grow']; tabs?: EuiTabProps[]; + 'data-test-subj'?: string; } const HeaderColumns: React.FC> = memo( @@ -53,8 +54,9 @@ export const Header: React.FC = ({ rightColumnGrow, tabs, maxWidth, + 'data-test-subj': dataTestSubj, }) => ( - + = ({ restrictWidth, restrictHeaderWidth, children, + 'data-test-subj': dataTestSubj, ...rest }) => ( -
- +
+ {children} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 4fec57ec79f69..0035d067e5915 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -26,7 +26,16 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ cancelOnClick?: React.ReactEventHandler; agentConfig?: AgentConfig; packageInfo?: PackageInfo; -}> = ({ from, cancelUrl, cancelOnClick, agentConfig, packageInfo, children }) => { + 'data-test-subj'?: string; +}> = ({ + from, + cancelUrl, + cancelOnClick, + agentConfig, + packageInfo, + children, + 'data-test-subj': dataTestSubj, +}) => { const leftColumn = ( @@ -138,6 +147,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ leftColumn={leftColumn} rightColumn={rightColumn} rightColumnGrow={false} + data-test-subj={dataTestSubj} > {children} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 4f5418336a6f8..d4e10c3015180 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -333,7 +333,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { ]; return ( - + {formState === 'CONFIRM' && agentConfig && ( Date: Fri, 12 Jun 2020 16:57:31 -0400 Subject: [PATCH 07/12] Start work on Functional tests --- .../pages/policy/view/policy_list.tsx | 1 + .../apps/endpoint/policy_list.ts | 26 ++++++++++++++++++- .../page_objects/index.ts | 2 ++ .../page_objects/ingest_manager.ts | 26 +++++++++++++++++++ .../page_objects/policy_page.ts | 11 ++++++++ 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx index 28fd784aed184..c89d59be620ab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.tsx @@ -298,6 +298,7 @@ export const PolicyList = React.memo(() => { iconType="plusInCircle" onClick={handleCreatePolicyClick} isDisabled={isFetchingPackageInfo} + data-test-subj="headerCreateNewPolicyButton" > { + const createButtonTitle = await testSubjects.getVisibleText('headerCreateNewPolicyButton'); + expect(createButtonTitle).to.equal('Create new policy'); + }); it('shows policy count total', async () => { const policyTotal = await testSubjects.getVisibleText('policyTotalCount'); expect(policyTotal).to.equal('0 Policies'); @@ -89,5 +99,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { ); }); }); + + describe('and user clicks on page header create button', () => { + beforeEach(async () => { + await pageObjects.policy.navigateToPolicyList(); + }); + + it('should redirect to ingest management integrations add datasource', async () => { + const headerCreateButton = await pageObjects.policy.findHeaderCreateNewButton(); + await headerCreateButton.click(); + await pageObjects.ingestManager.ensureDatasourceCratePageOrFail(); + }); + it('should redirect user back to Policy List if Cancel button is clicked', async () => {}); + it('should redirect user back to Policy List after a successful save', async () => {}); + }); }); } diff --git a/x-pack/test/security_solution_endpoint/page_objects/index.ts b/x-pack/test/security_solution_endpoint/page_objects/index.ts index 5b550bea5b55d..10d4eb6cb42c0 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/index.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/index.ts @@ -9,6 +9,7 @@ import { EndpointPageProvider } from './endpoint_page'; import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; import { EndpointPolicyPageProvider } from './policy_page'; import { EndpointPageUtils } from './page_utils'; +import { IngestManager } from './ingest_manager'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -16,4 +17,5 @@ export const pageObjects = { policy: EndpointPolicyPageProvider, endpointPageUtils: EndpointPageUtils, endpointAlerts: EndpointAlertsPageProvider, + ingestManager: IngestManager, }; diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts new file mode 100644 index 0000000000000..a19fdad18e575 --- /dev/null +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function IngestManager({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + /** + * Validates that the page shown is the Datasource Craete Page + */ + async ensureDatasourceCratePageOrFail() { + await testSubjects.existOrFail('createDataSource_header'); + }, + + async findDatasourceCreateCancelButton() {}, + + async findDatasourceCreateBackLink() {}, + + async findDatasourceCreateSaveButton() {}, + }; +} diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index aa9c5361de846..92313d4662ecd 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -55,5 +55,16 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr await testSubjects.existOrFail('policyDetailsConfirmModal'); await pageObjects.common.clickConfirmOnModal(); }, + + /** + * Finds and returns the Create New policy Policy button displayed on the List page + */ + async findHeaderCreateNewButton() { + // The Create button is initially disabled because we need to first make a call to Ingest + // to retrieve the package version, so that the redirect works as expected. So, we wait + // for that to occur here a well. + await testSubjects.waitForEnabled('headerCreateNewPolicyButton'); + return await testSubjects.find('headerCreateNewPolicyButton'); + }, }; } From f25fdc6e979a67db9b35a2362b362ce164ecdccb Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Sun, 14 Jun 2020 11:31:15 -0400 Subject: [PATCH 08/12] Rename ingest pageObject file + added methods to get multiple form elements --- .../components/layout.tsx | 1 + .../create_datasource_page/index.tsx | 8 +++- .../apps/endpoint/policy_list.ts | 13 +++--- .../page_objects/index.ts | 4 +- .../page_objects/ingest_manager.ts | 26 ----------- .../ingest_manager_create_datasource_page.ts | 43 +++++++++++++++++++ 6 files changed, 61 insertions(+), 34 deletions(-) delete mode 100644 x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts create mode 100644 x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx index 0035d067e5915..7939feed80143 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/components/layout.tsx @@ -46,6 +46,7 @@ export const CreateDatasourcePageLayout: React.FunctionComponent<{ flush="left" href={cancelUrl} onClick={cancelOnClick} + data-test-subj={`${dataTestSubj}_cancelBackLink`} > { {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - + { iconType="save" color="primary" fill + data-test-subj="createDatasourceSaveButton" > { beforeEach(async () => { await pageObjects.policy.navigateToPolicyList(); + await (await pageObjects.policy.findHeaderCreateNewButton()).click(); }); it('should redirect to ingest management integrations add datasource', async () => { - const headerCreateButton = await pageObjects.policy.findHeaderCreateNewButton(); - await headerCreateButton.click(); - await pageObjects.ingestManager.ensureDatasourceCratePageOrFail(); + await pageObjects.ingestManagerCreateDatasource.ensureOnCreatePageOrFail(); }); - it('should redirect user back to Policy List if Cancel button is clicked', async () => {}); + it('should redirect user back to Policy List if Cancel button is clicked', async () => { + await (await pageObjects.ingestManagerCreateDatasource.findCancelButton()).click(); + // FIXME: need method to chec that is on policy list page. + }); + it('should redirect user back to Policy List if Back link is clicked', async () => {}); it('should redirect user back to Policy List after a successful save', async () => {}); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/index.ts b/x-pack/test/security_solution_endpoint/page_objects/index.ts index 10d4eb6cb42c0..96e2a47e7803e 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/index.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/index.ts @@ -9,7 +9,7 @@ import { EndpointPageProvider } from './endpoint_page'; import { EndpointAlertsPageProvider } from './endpoint_alerts_page'; import { EndpointPolicyPageProvider } from './policy_page'; import { EndpointPageUtils } from './page_utils'; -import { IngestManager } from './ingest_manager'; +import { IngestManagerCreateDatasource } from './ingest_manager_create_datasource_page'; export const pageObjects = { ...xpackFunctionalPageObjects, @@ -17,5 +17,5 @@ export const pageObjects = { policy: EndpointPolicyPageProvider, endpointPageUtils: EndpointPageUtils, endpointAlerts: EndpointAlertsPageProvider, - ingestManager: IngestManager, + ingestManagerCreateDatasource: IngestManagerCreateDatasource, }; diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts deleted file mode 100644 index a19fdad18e575..0000000000000 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FtrProviderContext } from '../ftr_provider_context'; - -export function IngestManager({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - - return { - /** - * Validates that the page shown is the Datasource Craete Page - */ - async ensureDatasourceCratePageOrFail() { - await testSubjects.existOrFail('createDataSource_header'); - }, - - async findDatasourceCreateCancelButton() {}, - - async findDatasourceCreateBackLink() {}, - - async findDatasourceCreateSaveButton() {}, - }; -} diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts new file mode 100644 index 0000000000000..e8b69921db88a --- /dev/null +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function IngestManagerCreateDatasource({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + return { + /** + * Validates that the page shown is the Datasource Craete Page + */ + async ensureOnCreatePageOrFail() { + await testSubjects.existOrFail('createDataSource_header'); + }, + + /** + * Finds and returns the Cancel button on the sticky bottom bar + */ + async findCancelButton() { + return await testSubjects.find('createDatasourceCancelButton'); + }, + + /** + * Finds and returns the Cancel back link at the top of the create page + */ + async findBackLink() { + return await testSubjects.find('createDataSource_cancelBackLink'); + }, + + async selectAgentConfig(name?: string) {}, + + /** + * Finds and returns the save button on the sticky bottom bar + */ + async findDSaveButton() { + return await testSubjects.find('createDatasourceSaveButton'); + }, + }; +} From 3c59c79dcb9188fcb5a50febf3ca1ec0c37570ed Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Sun, 14 Jun 2020 11:57:41 -0400 Subject: [PATCH 09/12] Add test subj + pageObject for endpoint custom datasource configuration --- .../ingest_manager_integration/configure_datasource.tsx | 1 + .../page_objects/policy_page.ts | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx index db5196bfc4eb4..03539e5637900 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_datasource.tsx @@ -32,6 +32,7 @@ export const ConfigureEndpointDatasource = memo

diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index 92313d4662ecd..fc617c683854e 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -66,5 +66,14 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr await testSubjects.waitForEnabled('headerCreateNewPolicyButton'); return await testSubjects.find('headerCreateNewPolicyButton'); }, + + /** + * Used when looking a the Ingest create/edit datasource pages. Finds the endpoint + * custom configuaration component + * @param onEditPage + */ + async findDatasourceEndpointCustomConfiguration(onEditPage: boolean = false) { + return await testSubjects.find(`endpointDatasourceConfig_${onEditPage ? 'edit' : 'create'}`); + }, }; } From 61f519d0ae975da29ceb3a31372e0dfceca3c742 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Sun, 14 Jun 2020 18:47:20 -0400 Subject: [PATCH 10/12] More test stuff --- .../apps/endpoint/policy_list.ts | 19 ++++++++++++-- .../ingest_manager_create_datasource_page.ts | 26 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index d50d8548ed88c..3420e975afe91 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -111,10 +111,25 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should redirect user back to Policy List if Cancel button is clicked', async () => { await (await pageObjects.ingestManagerCreateDatasource.findCancelButton()).click(); + + // FIXME: need method to chec that is on policy list page. + }); + it('should redirect user back to Policy List if Back link is clicked', async () => { + await (await pageObjects.ingestManagerCreateDatasource.findBackLink()).click(); + // FIXME: need method to chec that is on policy list page. + // TODO test case + expect(true).to.be(false); + }); + it('should display custom endpoint configuration message', async () => { + await pageObjects.ingestManagerCreateDatasource.selectAgentConfig(); + const endpointConfig = await pageObjects.policy.findDatasourceEndpointCustomConfiguration(); + expect(endpointConfig).not.to.be(undefined); + }); + it('should redirect user back to Policy List after a successful save', async () => { + // TODO test case + expect(true).to.be(false); }); - it('should redirect user back to Policy List if Back link is clicked', async () => {}); - it('should redirect user back to Policy List after a successful save', async () => {}); }); }); } diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts index e8b69921db88a..7072540c69e8a 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts @@ -8,6 +8,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function IngestManagerCreateDatasource({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const find = getService('find'); return { /** @@ -31,13 +32,34 @@ export function IngestManagerCreateDatasource({ getService }: FtrProviderContext return await testSubjects.find('createDataSource_cancelBackLink'); }, - async selectAgentConfig(name?: string) {}, - /** * Finds and returns the save button on the sticky bottom bar */ async findDSaveButton() { return await testSubjects.find('createDatasourceSaveButton'); }, + + /** + * Selects an agent configuration on the form + * @param name + * Visual name of the configuration. if one is not provided, the first agent + * configuration on the list will be chosen + */ + async selectAgentConfig(name?: string) { + // if we have a name, then find the button with that `title` set. + if (name) { + // FIXME implement + } + // Else, just select the first agent configuration that is present + else { + await (await find.byCssSelector('button[]')).click(); + } + }, + + /** + * Set the name of the datasource on the input field + * @param name + */ + async setDatasourceName(name: string) {}, }; } From 8bfac6e59024cfbf80229a31f3bd01f5e0d00b91 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 15 Jun 2020 10:20:59 -0400 Subject: [PATCH 11/12] Additional test subj ids to Ingest ++ complete tests for integration --- .../step_define_datasource.tsx | 1 + .../step_select_config.tsx | 1 + .../apps/endpoint/policy_list.ts | 18 ++++++++++-------- .../ingest_manager_create_datasource_page.ts | 10 +++++++--- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx index 3b22756409330..2651615b458f7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_define_datasource.tsx @@ -88,6 +88,7 @@ export const StepDefineDatasource: React.FunctionComponent<{ name: e.target.value, }) } + data-test-subj="datasourceNameInput" /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx index 22cb219f911f6..5f556a46e518d 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/step_select_config.tsx @@ -113,6 +113,7 @@ export const StepSelectConfig: React.FunctionComponent<{ label: name, key: id, checked: selectedConfigId === id ? 'on' : undefined, + 'data-test-subj': 'agentConfigItem', }; })} renderOption={(option) => ( diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index dab9f46ac2aab..8945e414914fd 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -121,26 +121,28 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should redirect to ingest management integrations add datasource', async () => { await pageObjects.ingestManagerCreateDatasource.ensureOnCreatePageOrFail(); }); + it('should redirect user back to Policy List if Cancel button is clicked', async () => { await (await pageObjects.ingestManagerCreateDatasource.findCancelButton()).click(); - - // FIXME: need method to chec that is on policy list page. + await pageObjects.policy.ensureIsOnPolicyPage(); }); + it('should redirect user back to Policy List if Back link is clicked', async () => { await (await pageObjects.ingestManagerCreateDatasource.findBackLink()).click(); - - // FIXME: need method to chec that is on policy list page. - // TODO test case - expect(true).to.be(false); + await pageObjects.policy.ensureIsOnPolicyPage(); }); + it('should display custom endpoint configuration message', async () => { await pageObjects.ingestManagerCreateDatasource.selectAgentConfig(); const endpointConfig = await pageObjects.policy.findDatasourceEndpointCustomConfiguration(); expect(endpointConfig).not.to.be(undefined); }); + it('should redirect user back to Policy List after a successful save', async () => { - // TODO test case - expect(true).to.be(false); + await pageObjects.ingestManagerCreateDatasource.selectAgentConfig(); + await pageObjects.ingestManagerCreateDatasource.setDatasourceName('endpoint policy 1'); + await (await pageObjects.ingestManagerCreateDatasource.findDSaveButton()).click(); + await pageObjects.policy.ensureIsOnPolicyPage(); }); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts index 7072540c69e8a..efb84d826d5b1 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts @@ -48,11 +48,13 @@ export function IngestManagerCreateDatasource({ getService }: FtrProviderContext async selectAgentConfig(name?: string) { // if we have a name, then find the button with that `title` set. if (name) { - // FIXME implement + await ( + await find.byCssSelector(`[data-test-subj="agentConfigItem"][title="${name}"]`) + ).click(); } // Else, just select the first agent configuration that is present else { - await (await find.byCssSelector('button[]')).click(); + await (await testSubjects.find('agentConfigItem')).click(); } }, @@ -60,6 +62,8 @@ export function IngestManagerCreateDatasource({ getService }: FtrProviderContext * Set the name of the datasource on the input field * @param name */ - async setDatasourceName(name: string) {}, + async setDatasourceName(name: string) { + await testSubjects.setValue('datasourceNameInput', name); + }, }; } From 1e3c5b96c9f83ad462b39cee292fa07176fd4f59 Mon Sep 17 00:00:00 2001 From: Paul Tavares Date: Mon, 15 Jun 2020 11:45:52 -0400 Subject: [PATCH 12/12] Ensure test deletes datasource created via ui --- .../create_datasource_page/index.tsx | 2 + .../apps/endpoint/policy_list.ts | 5 ++- .../ingest_manager_create_datasource_page.ts | 10 +++++ .../page_objects/policy_page.ts | 2 +- .../services/endpoint_policy.ts | 45 +++++++++++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx index 288ef599d72b1..876383770aa71 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_datasource_page/index.tsx @@ -243,6 +243,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { }, }) : undefined, + 'data-test-subj': 'datasourceCreateSuccessToast', }); } else { notifications.toasts.addError(error, { @@ -319,6 +320,7 @@ export const CreateDatasourcePage: React.FunctionComponent = () => { defaultMessage: 'Select the data you want to collect', }), status: !packageInfo || !agentConfig ? 'disabled' : undefined, + 'data-test-subj': 'dataCollectionSetupStep', children: agentConfig && packageInfo ? ( { + const newPolicyName = `endpoint policy ${Date.now()}`; await pageObjects.ingestManagerCreateDatasource.selectAgentConfig(); - await pageObjects.ingestManagerCreateDatasource.setDatasourceName('endpoint policy 1'); + await pageObjects.ingestManagerCreateDatasource.setDatasourceName(newPolicyName); await (await pageObjects.ingestManagerCreateDatasource.findDSaveButton()).click(); + await pageObjects.ingestManagerCreateDatasource.waitForSaveSuccessNotification(); await pageObjects.policy.ensureIsOnPolicyPage(); + await policyTestResources.deletePolicyByName(newPolicyName); }); }); }); diff --git a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts index efb84d826d5b1..f50cde6285be7 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/ingest_manager_create_datasource_page.ts @@ -63,7 +63,17 @@ export function IngestManagerCreateDatasource({ getService }: FtrProviderContext * @param name */ async setDatasourceName(name: string) { + // Because of the bottom sticky bar, we need to scroll section 2 into view + // so that `setValue()` enters the data on the input field. + await testSubjects.scrollIntoView('dataCollectionSetupStep'); await testSubjects.setValue('datasourceNameInput', name); }, + + /** + * Waits for the save Notification toast to be visible + */ + async waitForSaveSuccessNotification() { + await testSubjects.existOrFail('datasourceCreateSuccessToast'); + }, }; } diff --git a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts index d6d70543c72ef..a2b0f9a671039 100644 --- a/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts +++ b/x-pack/test/security_solution_endpoint/page_objects/policy_page.ts @@ -42,7 +42,7 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr * ensures that the Policy Page is the currently display view */ async ensureIsOnPolicyPage() { - await testSubjects.existOrFail('policyTable'); + await testSubjects.existOrFail('policyListPage'); }, /** diff --git a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts index cfba9e803be7d..fbed7dcc663ec 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint_policy.ts @@ -10,8 +10,10 @@ import { CreateAgentConfigResponse, CreateDatasourceRequest, CreateDatasourceResponse, + DATASOURCE_SAVED_OBJECT_TYPE, DeleteAgentConfigRequest, DeleteDatasourcesRequest, + GetDatasourcesResponse, GetFullAgentConfigResponse, GetPackagesResponse, } from '../../../plugins/ingest_manager/common'; @@ -223,5 +225,48 @@ export function EndpointPolicyTestResourcesProvider({ getService }: FtrProviderC }, }; }, + + /** + * Deletes a policy (Datasource) by using the policy name + * @param name + */ + async deletePolicyByName(name: string) { + let datasourceList: GetDatasourcesResponse['items']; + try { + const { body: datasourcesResponse }: { body: GetDatasourcesResponse } = await supertest + .get(INGEST_API_DATASOURCES) + .set('kbn-xsrf', 'xxx') + .query({ kuery: `${DATASOURCE_SAVED_OBJECT_TYPE}.name: ${name}` }) + .send() + .expect(200); + datasourceList = datasourcesResponse.items; + } catch (error) { + return logSupertestApiErrorAndThrow( + `Unable to get list of datasources with name=${name}`, + error + ); + } + + if (datasourceList.length === 0) { + throw new Error(`Policy named '${name}' was not found!`); + } + + if (datasourceList.length > 1) { + throw new Error(`Found ${datasourceList.length} Policies - was expecting only one!`); + } + + try { + const deleteDatasourceData: DeleteDatasourcesRequest['body'] = { + datasourceIds: [datasourceList[0].id], + }; + await supertest + .post(INGEST_API_DATASOURCES_DELETE) + .set('kbn-xsrf', 'xxx') + .send(deleteDatasourceData) + .expect(200); + } catch (error) { + logSupertestApiErrorAndThrow('Unable to delete Datasource via Ingest!', error); + } + }, }; }