Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Endpoint] Support redirect from Policy Details to Ingest when user initiates Edit Policy from Datasource Edit page #70874

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions x-pack/plugins/security_solution/common/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationStart['navigateToApp']>;
/**
* Where the user should be redirected to when the `Cancel` button is clicked
*/
onCancelNavigateTo?: Parameters<ApplicationStart['navigateToApp']>;
}

/**
* Object that allows you to maintain stateful information in the location object across navigation events
*
Expand All @@ -17,9 +32,11 @@ export interface AppLocation {
search: string;
hash: string;
key?: string;
state?: {
isTabChange?: boolean;
};
state?:
| {
isTabChange?: boolean;
}
| PolicyDetailsRouteState;
}

/**
Expand Down
50 changes: 33 additions & 17 deletions x-pack/plugins/security_solution/public/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouterProps> = ({ history, children }) => (
<ManageRoutesSpy>
<Router history={history}>
<RouteCapture>
<Switch>
<Route path="/">
<HomePage>{children}</HomePage>
</Route>
<Route>
<NotFoundPage />
</Route>
</Switch>
</RouteCapture>
</Router>
</ManageRoutesSpy>
);
const PageRouterComponent: FC<RouterProps> = ({ 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 (
<ManageRoutesSpy>
<Router history={history}>
<RouteCapture>
<Switch>
<Route path="/">
<HomePage>{children}</HomePage>
</Route>
<Route>
<NotFoundPage />
</Route>
</Switch>
</RouteCapture>
</Router>
</ManageRoutesSpy>
);
};

export const PageRouter = memo(PageRouterComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<CustomConfigurePackageConfigContent>(
({ 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 (
<>
<EuiTitle size="xs">
Expand Down Expand Up @@ -63,7 +84,7 @@ export const ConfigureEndpointPackageConfig = memo<CustomConfigurePackageConfigC
appId={MANAGEMENT_APP_ID}
className="editLinkToPolicyDetails"
appPath={policyUrl}
// Cannot use formalUrl here since the code is called in Ingest, which does not use redux
appState={policyDetailRouteState}
>
<FormattedMessage
id="xpack.securitySolution.endpoint.ingestManager.editPackageConfig.configurePolicyLink"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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<PolicyDetailsRouteState>();

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

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

// Handle showing update statuses
Expand All @@ -80,6 +92,10 @@ export const PolicyDetails = React.memo(() => {
</span>
),
});

if (routeState && routeState.onSaveNavigateTo) {
navigateToApp(...routeState.onSaveNavigateTo);
}
} else {
notifications.toasts.danger({
toastLifeTimeMs: 10000,
Expand All @@ -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<ApplicationStart['navigateToApp']> => {
return routeState?.onCancelNavigateTo ?? [MANAGEMENT_APP_ID, { path: getPoliciesPath() }];
}, [routeState?.onCancelNavigateTo]);
const handleCancelOnClick = useNavigateToAppEventHandler(...navigateToAppArguments);

const handleSaveOnClick = useCallback(() => {
setShowConfirm(true);
}, []);
Expand All @@ -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.
Expand Down Expand Up @@ -159,10 +186,7 @@ export const PolicyDetails = React.memo(() => {
<VerticalDivider spacing="l" />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={handleBackToListOnClick}
data-test-subj="policyDetailsCancelButton"
>
<EuiButtonEmpty onClick={handleCancelOnClick} data-test-subj="policyDetailsCancelButton">
<FormattedMessage
id="xpack.securitySolution.endpoint.policy.details.cancel"
defaultMessage="Cancel"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
});
});
});

describe('when on Ingest Configurations Edit Package Config page', async () => {
let policyInfo: PolicyTestResourceInfo;
beforeEach(async () => {
Expand All @@ -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();
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down