diff --git a/x-pack/plugins/security_solution/common/endpoint/types.ts b/x-pack/plugins/security_solution/common/endpoint/types.ts index f2b8acb627cc4c..b75d4b2190fe87 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types.ts @@ -4,9 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PackageConfig, NewPackageConfig } from '../../../ingest_manager/common'; +import { ApplicationStart } from 'kibana/public'; +import { NewPackageConfig, PackageConfig } from '../../../ingest_manager/common'; import { ManifestSchema } from './schema/manifest'; +/** + * Supported React-Router state for the Policy Details page + */ +export interface PolicyDetailsRouteState { + /** + * Where the user should be redirected to when the `Save` button is clicked and the update was successful + */ + onSaveNavigateTo?: Parameters; + /** + * Where the user should be redirected to when the `Cancel` button is clicked + */ + onCancelNavigateTo?: Parameters; +} + /** * Object that allows you to maintain stateful information in the location object across navigation events * @@ -17,9 +32,11 @@ export interface AppLocation { search: string; hash: string; key?: string; - state?: { - isTabChange?: boolean; - }; + state?: + | { + isTabChange?: boolean; + } + | PolicyDetailsRouteState; } /** diff --git a/x-pack/plugins/security_solution/public/app/routes.tsx b/x-pack/plugins/security_solution/public/app/routes.tsx index fc0d4e1f4fa625..1d3a59856caa95 100644 --- a/x-pack/plugins/security_solution/public/app/routes.tsx +++ b/x-pack/plugins/security_solution/public/app/routes.tsx @@ -5,34 +5,50 @@ */ import { History } from 'history'; -import React, { FC, memo } from 'react'; +import React, { FC, memo, useEffect } from 'react'; import { Route, Router, Switch } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; import { NotFoundPage } from './404'; import { HomePage } from './home'; import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; import { RouteCapture } from '../common/components/endpoint/route_capture'; +import { AppAction } from '../common/store/actions'; interface RouterProps { children: React.ReactNode; history: History; } -const PageRouterComponent: FC = ({ history, children }) => ( - - - - - - {children} - - - - - - - - -); +const PageRouterComponent: FC = ({ history, children }) => { + const dispatch = useDispatch<(action: AppAction) => void>(); + useEffect(() => { + return () => { + // When app is dismounted via a non-router method (ex. using Kibana's `services.application.navigateToApp()`) + // ensure that one last `userChangedUrl` store action is dispatched, which will help trigger state reset logic + dispatch({ + type: 'userChangedUrl', + payload: { pathname: '', search: '', hash: '' }, + }); + }; + }, [dispatch]); + + return ( + + + + + + {children} + + + + + + + + + ); +}; export const PageRouter = memo(PageRouterComponent); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 7c27acdb51568a..4a870016326be8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -5,17 +5,17 @@ */ import { - PolicyData, + AppLocation, Immutable, MalwareFields, + PolicyData, UIPolicyConfig, - AppLocation, } from '../../../../common/endpoint/types'; import { ServerApiError } from '../../../common/types'; import { GetAgentStatusResponse, - GetPackageConfigsResponse, GetOnePackageConfigResponse, + GetPackageConfigsResponse, GetPackagesResponse, UpdatePackageConfigResponse, } from '../../../../../ingest_manager/common'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx index ebcfd3f1bb209d..67f24977406c69 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiText, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -15,18 +15,39 @@ import { } from '../../../../../../../ingest_manager/public'; import { getPolicyDetailPath } from '../../../../common/routing'; import { MANAGEMENT_APP_ID } from '../../../../common/constants'; +import { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types'; /** * Exports Endpoint-specific package config instructions * for use in the Ingest app create / edit package config */ export const ConfigureEndpointPackageConfig = memo( - ({ from, packageConfigId }: CustomConfigurePackageConfigProps) => { + ({ + from, + packageConfigId, + packageConfig: { config_id: agentConfigId }, + }: CustomConfigurePackageConfigProps) => { let policyUrl = ''; if (from === 'edit' && packageConfigId) { + // Cannot use formalUrl here since the code is called in Ingest, which does not use redux policyUrl = getPolicyDetailPath(packageConfigId); } + const policyDetailRouteState = useMemo((): undefined | PolicyDetailsRouteState => { + if (from !== 'edit') { + return undefined; + } + const navigateTo: PolicyDetailsRouteState['onSaveNavigateTo'] & + PolicyDetailsRouteState['onCancelNavigateTo'] = [ + 'ingestManager', + { path: `#/configs/${agentConfigId}/edit-integration/${packageConfigId}` }, + ]; + return { + onSaveNavigateTo: navigateTo, + onCancelNavigateTo: navigateTo, + }; + }, [agentConfigId, from, packageConfigId]); + return ( <> @@ -63,7 +84,7 @@ export const ConfigureEndpointPackageConfig = memo { ); expect(history.location.pathname).toEqual(policyDetailsPathUrl); cancelbutton.simulate('click', { button: 0 }); - expect(history.location.pathname).toEqual(policyListPathUrl); + const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls; + expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([ + 'securitySolution:management', + { path: policyListPathUrl }, + ]); }); it('should display save button', async () => { await asyncActions; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 6a48ae735180fb..2a4f839a4af1f0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -20,6 +20,8 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { ApplicationStart } from 'kibana/public'; import { usePolicyDetailsSelector } from './policy_hooks'; import { policyDetails, @@ -41,11 +43,20 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes'; import { SecurityPageName } from '../../../../app/types'; import { getPoliciesPath } from '../../../common/routing'; import { useFormatUrl } from '../../../../common/components/link_to'; +import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { MANAGEMENT_APP_ID } from '../../../common/constants'; +import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); - const { notifications } = useKibana(); + const { + notifications, + services: { + application: { navigateToApp }, + }, + } = useKibana(); const { formatUrl, search } = useFormatUrl(SecurityPageName.management); + const { state: locationRouteState } = useLocation(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -56,6 +67,7 @@ export const PolicyDetails = React.memo(() => { // Local state const [showConfirm, setShowConfirm] = useState(false); + const [routeState, setRouteState] = useState(); const policyName = policyItem?.name ?? ''; // Handle showing update statuses @@ -80,6 +92,10 @@ export const PolicyDetails = React.memo(() => { ), }); + + if (routeState && routeState.onSaveNavigateTo) { + navigateToApp(...routeState.onSaveNavigateTo); + } } else { notifications.toasts.danger({ toastLifeTimeMs: 10000, @@ -90,10 +106,15 @@ export const PolicyDetails = React.memo(() => { }); } } - }, [notifications.toasts, policyName, policyUpdateStatus]); + }, [navigateToApp, notifications.toasts, policyName, policyUpdateStatus, routeState]); const handleBackToListOnClick = useNavigateByRouterEventHandler(getPoliciesPath()); + const navigateToAppArguments = useMemo((): Parameters => { + return routeState?.onCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: getPoliciesPath() }]; + }, [routeState?.onCancelNavigateTo]); + const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments); + const handleSaveOnClick = useCallback(() => { setShowConfirm(true); }, []); @@ -109,6 +130,12 @@ export const PolicyDetails = React.memo(() => { setShowConfirm(false); }, []); + useEffect(() => { + if (!routeState && locationRouteState) { + setRouteState(locationRouteState); + } + }, [locationRouteState, routeState]); + // Before proceeding - check if we have a policy data. // If not, and we are still loading, show spinner. // Else, if we have an error, then show error on the page. @@ -159,10 +186,7 @@ export const PolicyDetails = React.memo(() => { - + { let policyInfo: PolicyTestResourceInfo; beforeEach(async () => { @@ -187,13 +186,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await linkToPolicy.click(); await pageObjects.policy.ensureIsOnDetailsPage(); }); - it('should allow the user to navigate, edit and save Policy Details', async () => { + it('should allow the user to navigate, edit, save Policy Details and be redirected back to ingest', async () => { await (await testSubjects.find('editLinkToPolicyDetails')).click(); await pageObjects.policy.ensureIsOnDetailsPage(); await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns'); await pageObjects.policy.confirmAndSave(); await testSubjects.existOrFail('policyDetailsSuccessMessage'); + await pageObjects.ingestManagerCreatePackageConfig.ensureOnEditPageOrFail(); + }); + it('should navigate back to Ingest Configuration Edit package page on click of cancel button', async () => { + await (await testSubjects.find('editLinkToPolicyDetails')).click(); + await (await pageObjects.policy.findCancelButton()).click(); + await pageObjects.ingestManagerCreatePackageConfig.ensureOnEditPageOrFail(); }); }); }); 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 b20e7c7c05e649..8ee42d40263818 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 @@ -69,6 +69,14 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr return await testSubjects.find('policyDetailsSaveButton'); }, + /** + * Finds and returns the Policy Details Page Cancel Button + */ + async findCancelButton() { + await this.ensureIsOnDetailsPage(); + return await testSubjects.find('policyDetailsCancelButton'); + }, + /** * ensures that the Details Page is the currently display view */