Skip to content

Commit 6579009

Browse files
authored
[Endpoint] Support redirect from Policy Details to Ingest when user initiates Edit Policy from Datasource Edit page (#70874) (#71015)
* allow Policy Details to support route state * Functional Tests cases that cover ingest navigation
1 parent 6d41149 commit 6579009

File tree

8 files changed

+132
-37
lines changed

8 files changed

+132
-37
lines changed

x-pack/plugins/security_solution/common/endpoint/types.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,24 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import { PackageConfig, NewPackageConfig } from '../../../ingest_manager/common';
7+
import { ApplicationStart } from 'kibana/public';
8+
import { NewPackageConfig, PackageConfig } from '../../../ingest_manager/common';
89
import { ManifestSchema } from './schema/manifest';
910

11+
/**
12+
* Supported React-Router state for the Policy Details page
13+
*/
14+
export interface PolicyDetailsRouteState {
15+
/**
16+
* Where the user should be redirected to when the `Save` button is clicked and the update was successful
17+
*/
18+
onSaveNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
19+
/**
20+
* Where the user should be redirected to when the `Cancel` button is clicked
21+
*/
22+
onCancelNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
23+
}
24+
1025
/**
1126
* Object that allows you to maintain stateful information in the location object across navigation events
1227
*
@@ -17,9 +32,11 @@ export interface AppLocation {
1732
search: string;
1833
hash: string;
1934
key?: string;
20-
state?: {
21-
isTabChange?: boolean;
22-
};
35+
state?:
36+
| {
37+
isTabChange?: boolean;
38+
}
39+
| PolicyDetailsRouteState;
2340
}
2441

2542
/**

x-pack/plugins/security_solution/public/app/routes.tsx

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,50 @@
55
*/
66

77
import { History } from 'history';
8-
import React, { FC, memo } from 'react';
8+
import React, { FC, memo, useEffect } from 'react';
99
import { Route, Router, Switch } from 'react-router-dom';
1010

11+
import { useDispatch } from 'react-redux';
1112
import { NotFoundPage } from './404';
1213
import { HomePage } from './home';
1314
import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes';
1415
import { RouteCapture } from '../common/components/endpoint/route_capture';
16+
import { AppAction } from '../common/store/actions';
1517

1618
interface RouterProps {
1719
children: React.ReactNode;
1820
history: History;
1921
}
2022

21-
const PageRouterComponent: FC<RouterProps> = ({ history, children }) => (
22-
<ManageRoutesSpy>
23-
<Router history={history}>
24-
<RouteCapture>
25-
<Switch>
26-
<Route path="/">
27-
<HomePage>{children}</HomePage>
28-
</Route>
29-
<Route>
30-
<NotFoundPage />
31-
</Route>
32-
</Switch>
33-
</RouteCapture>
34-
</Router>
35-
</ManageRoutesSpy>
36-
);
23+
const PageRouterComponent: FC<RouterProps> = ({ history, children }) => {
24+
const dispatch = useDispatch<(action: AppAction) => void>();
25+
useEffect(() => {
26+
return () => {
27+
// When app is dismounted via a non-router method (ex. using Kibana's `services.application.navigateToApp()`)
28+
// ensure that one last `userChangedUrl` store action is dispatched, which will help trigger state reset logic
29+
dispatch({
30+
type: 'userChangedUrl',
31+
payload: { pathname: '', search: '', hash: '' },
32+
});
33+
};
34+
}, [dispatch]);
35+
36+
return (
37+
<ManageRoutesSpy>
38+
<Router history={history}>
39+
<RouteCapture>
40+
<Switch>
41+
<Route path="/">
42+
<HomePage>{children}</HomePage>
43+
</Route>
44+
<Route>
45+
<NotFoundPage />
46+
</Route>
47+
</Switch>
48+
</RouteCapture>
49+
</Router>
50+
</ManageRoutesSpy>
51+
);
52+
};
3753

3854
export const PageRouter = memo(PageRouterComponent);

x-pack/plugins/security_solution/public/management/pages/policy/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
*/
66

77
import {
8-
PolicyData,
8+
AppLocation,
99
Immutable,
1010
MalwareFields,
11+
PolicyData,
1112
UIPolicyConfig,
12-
AppLocation,
1313
} from '../../../../common/endpoint/types';
1414
import { ServerApiError } from '../../../common/types';
1515
import {
1616
GetAgentStatusResponse,
17-
GetPackageConfigsResponse,
1817
GetOnePackageConfigResponse,
18+
GetPackageConfigsResponse,
1919
GetPackagesResponse,
2020
UpdatePackageConfigResponse,
2121
} from '../../../../../ingest_manager/common';

x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/configure_package_config.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { memo } from 'react';
7+
import React, { memo, useMemo } from 'react';
88
import { FormattedMessage } from '@kbn/i18n/react';
99
import { EuiCallOut, EuiText, EuiTitle, EuiSpacer } from '@elastic/eui';
1010
import { i18n } from '@kbn/i18n';
@@ -15,18 +15,39 @@ import {
1515
} from '../../../../../../../ingest_manager/public';
1616
import { getPolicyDetailPath } from '../../../../common/routing';
1717
import { MANAGEMENT_APP_ID } from '../../../../common/constants';
18+
import { PolicyDetailsRouteState } from '../../../../../../common/endpoint/types';
1819

1920
/**
2021
* Exports Endpoint-specific package config instructions
2122
* for use in the Ingest app create / edit package config
2223
*/
2324
export const ConfigureEndpointPackageConfig = memo<CustomConfigurePackageConfigContent>(
24-
({ from, packageConfigId }: CustomConfigurePackageConfigProps) => {
25+
({
26+
from,
27+
packageConfigId,
28+
packageConfig: { config_id: agentConfigId },
29+
}: CustomConfigurePackageConfigProps) => {
2530
let policyUrl = '';
2631
if (from === 'edit' && packageConfigId) {
32+
// Cannot use formalUrl here since the code is called in Ingest, which does not use redux
2733
policyUrl = getPolicyDetailPath(packageConfigId);
2834
}
2935

36+
const policyDetailRouteState = useMemo((): undefined | PolicyDetailsRouteState => {
37+
if (from !== 'edit') {
38+
return undefined;
39+
}
40+
const navigateTo: PolicyDetailsRouteState['onSaveNavigateTo'] &
41+
PolicyDetailsRouteState['onCancelNavigateTo'] = [
42+
'ingestManager',
43+
{ path: `#/configs/${agentConfigId}/edit-integration/${packageConfigId}` },
44+
];
45+
return {
46+
onSaveNavigateTo: navigateTo,
47+
onCancelNavigateTo: navigateTo,
48+
};
49+
}, [agentConfigId, from, packageConfigId]);
50+
3051
return (
3152
<>
3253
<EuiTitle size="xs">
@@ -63,7 +84,7 @@ export const ConfigureEndpointPackageConfig = memo<CustomConfigurePackageConfigC
6384
appId={MANAGEMENT_APP_ID}
6485
className="editLinkToPolicyDetails"
6586
appPath={policyUrl}
66-
// Cannot use formalUrl here since the code is called in Ingest, which does not use redux
87+
appState={policyDetailRouteState}
6788
>
6889
<FormattedMessage
6990
id="xpack.securitySolution.endpoint.ingestManager.editPackageConfig.configurePolicyLink"

x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,11 @@ describe('Policy Details', () => {
170170
);
171171
expect(history.location.pathname).toEqual(policyDetailsPathUrl);
172172
cancelbutton.simulate('click', { button: 0 });
173-
expect(history.location.pathname).toEqual(policyListPathUrl);
173+
const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls;
174+
expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([
175+
'securitySolution:management',
176+
{ path: policyListPathUrl },
177+
]);
174178
});
175179
it('should display save button', async () => {
176180
await asyncActions;

x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
import React, { useCallback, useEffect, useState } from 'react';
7+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
88
import {
99
EuiFlexGroup,
1010
EuiFlexItem,
@@ -20,6 +20,8 @@ import {
2020
import { FormattedMessage } from '@kbn/i18n/react';
2121
import { i18n } from '@kbn/i18n';
2222
import { useDispatch } from 'react-redux';
23+
import { useLocation } from 'react-router-dom';
24+
import { ApplicationStart } from 'kibana/public';
2325
import { usePolicyDetailsSelector } from './policy_hooks';
2426
import {
2527
policyDetails,
@@ -41,11 +43,20 @@ import { SpyRoute } from '../../../../common/utils/route/spy_routes';
4143
import { SecurityPageName } from '../../../../app/types';
4244
import { getPoliciesPath } from '../../../common/routing';
4345
import { useFormatUrl } from '../../../../common/components/link_to';
46+
import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler';
47+
import { MANAGEMENT_APP_ID } from '../../../common/constants';
48+
import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types';
4449

4550
export const PolicyDetails = React.memo(() => {
4651
const dispatch = useDispatch<(action: AppAction) => void>();
47-
const { notifications } = useKibana();
52+
const {
53+
notifications,
54+
services: {
55+
application: { navigateToApp },
56+
},
57+
} = useKibana();
4858
const { formatUrl, search } = useFormatUrl(SecurityPageName.management);
59+
const { state: locationRouteState } = useLocation<PolicyDetailsRouteState>();
4960

5061
// Store values
5162
const policyItem = usePolicyDetailsSelector(policyDetails);
@@ -56,6 +67,7 @@ export const PolicyDetails = React.memo(() => {
5667

5768
// Local state
5869
const [showConfirm, setShowConfirm] = useState<boolean>(false);
70+
const [routeState, setRouteState] = useState<PolicyDetailsRouteState>();
5971
const policyName = policyItem?.name ?? '';
6072

6173
// Handle showing update statuses
@@ -80,6 +92,10 @@ export const PolicyDetails = React.memo(() => {
8092
</span>
8193
),
8294
});
95+
96+
if (routeState && routeState.onSaveNavigateTo) {
97+
navigateToApp(...routeState.onSaveNavigateTo);
98+
}
8399
} else {
84100
notifications.toasts.danger({
85101
toastLifeTimeMs: 10000,
@@ -90,10 +106,15 @@ export const PolicyDetails = React.memo(() => {
90106
});
91107
}
92108
}
93-
}, [notifications.toasts, policyName, policyUpdateStatus]);
109+
}, [navigateToApp, notifications.toasts, policyName, policyUpdateStatus, routeState]);
94110

95111
const handleBackToListOnClick = useNavigateByRouterEventHandler(getPoliciesPath());
96112

113+
const navigateToAppArguments = useMemo((): Parameters<ApplicationStart['navigateToApp']> => {
114+
return routeState?.onCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: getPoliciesPath() }];
115+
}, [routeState?.onCancelNavigateTo]);
116+
const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments);
117+
97118
const handleSaveOnClick = useCallback(() => {
98119
setShowConfirm(true);
99120
}, []);
@@ -109,6 +130,12 @@ export const PolicyDetails = React.memo(() => {
109130
setShowConfirm(false);
110131
}, []);
111132

133+
useEffect(() => {
134+
if (!routeState && locationRouteState) {
135+
setRouteState(locationRouteState);
136+
}
137+
}, [locationRouteState, routeState]);
138+
112139
// Before proceeding - check if we have a policy data.
113140
// If not, and we are still loading, show spinner.
114141
// Else, if we have an error, then show error on the page.
@@ -159,10 +186,7 @@ export const PolicyDetails = React.memo(() => {
159186
<VerticalDivider spacing="l" />
160187
</EuiFlexItem>
161188
<EuiFlexItem grow={false}>
162-
<EuiButtonEmpty
163-
onClick={handleBackToListOnClick}
164-
data-test-subj="policyDetailsCancelButton"
165-
>
189+
<EuiButtonEmpty onClick={handleCancelOnClick} data-test-subj="policyDetailsCancelButton">
166190
<FormattedMessage
167191
id="xpack.securitySolution.endpoint.policy.details.cancel"
168192
defaultMessage="Cancel"

x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
163163
});
164164
});
165165
});
166-
167166
describe('when on Ingest Configurations Edit Package Config page', async () => {
168167
let policyInfo: PolicyTestResourceInfo;
169168
beforeEach(async () => {
@@ -187,13 +186,19 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
187186
await linkToPolicy.click();
188187
await pageObjects.policy.ensureIsOnDetailsPage();
189188
});
190-
it('should allow the user to navigate, edit and save Policy Details', async () => {
189+
it('should allow the user to navigate, edit, save Policy Details and be redirected back to ingest', async () => {
191190
await (await testSubjects.find('editLinkToPolicyDetails')).click();
192191
await pageObjects.policy.ensureIsOnDetailsPage();
193192
await pageObjects.endpointPageUtils.clickOnEuiCheckbox('policyWindowsEvent_dns');
194193
await pageObjects.policy.confirmAndSave();
195194

196195
await testSubjects.existOrFail('policyDetailsSuccessMessage');
196+
await pageObjects.ingestManagerCreatePackageConfig.ensureOnEditPageOrFail();
197+
});
198+
it('should navigate back to Ingest Configuration Edit package page on click of cancel button', async () => {
199+
await (await testSubjects.find('editLinkToPolicyDetails')).click();
200+
await (await pageObjects.policy.findCancelButton()).click();
201+
await pageObjects.ingestManagerCreatePackageConfig.ensureOnEditPageOrFail();
197202
});
198203
});
199204
});

x-pack/test/security_solution_endpoint/page_objects/policy_page.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ export function EndpointPolicyPageProvider({ getService, getPageObjects }: FtrPr
6969
return await testSubjects.find('policyDetailsSaveButton');
7070
},
7171

72+
/**
73+
* Finds and returns the Policy Details Page Cancel Button
74+
*/
75+
async findCancelButton() {
76+
await this.ensureIsOnDetailsPage();
77+
return await testSubjects.find('policyDetailsCancelButton');
78+
},
79+
7280
/**
7381
* ensures that the Details Page is the currently display view
7482
*/

0 commit comments

Comments
 (0)