[SIEM] Fix Detections page breadcrumbs (#55173) (#55478)

This commit is contained in:
patrykkopycinski 2020-01-22 02:16:55 +01:00 committed by Xavier Mouligneau
parent b1e14ce246
commit e48317fc82
17 changed files with 264 additions and 95 deletions

View file

@ -89,7 +89,8 @@
"**/react": "^16.12.0",
"**/react-test-renderer": "^16.12.0",
"**/deepmerge": "^4.2.2",
"**/serialize-javascript": "^2.1.1"
"**/serialize-javascript": "^2.1.1",
"**/fast-deep-equal": "^3.1.1"
},
"workspaces": {
"packages": [

View file

@ -52,11 +52,15 @@ export const RedirectToEditRulePage = ({ location: { search } }: DetectionEngine
);
};
export const getDetectionEngineUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`;
const baseDetectionEngineUrl = `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`;
export const getDetectionEngineUrl = () => `${baseDetectionEngineUrl}`;
export const getDetectionEngineAlertUrl = () =>
`#/link-to/${DETECTION_ENGINE_PAGE_NAME}/${DetectionEngineTab.alerts}`;
export const getRulesUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules`;
export const getCreateRuleUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/create-rule`;
export const getRuleDetailsUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details`;
export const getEditRuleUrl = () =>
`#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details/edit-rule`;
`${baseDetectionEngineUrl}/${DetectionEngineTab.alerts}`;
export const getDetectionEngineTabUrl = (tabPath: string) => `${baseDetectionEngineUrl}/${tabPath}`;
export const getRulesUrl = () => `${baseDetectionEngineUrl}/rules`;
export const getCreateRuleUrl = () => `${baseDetectionEngineUrl}/rules/create`;
export const getRuleDetailsUrl = (detailName: string) =>
`${baseDetectionEngineUrl}/rules/id/${detailName}`;
export const getEditRuleUrl = (detailName: string) =>
`${baseDetectionEngineUrl}/rules/id/${detailName}/edit`;

View file

@ -10,6 +10,7 @@ import { getOr, omit } from 'lodash/fp';
import { APP_NAME } from '../../../../common/constants';
import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils';
import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details';
import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils';
import { SiemPageName } from '../../../pages/home/types';
import { RouteSpyState, HostRouteSpyState, NetworkRouteSpyState } from '../../../utils/route/types';
import { getOverviewUrl } from '../../link_to';
@ -38,6 +39,9 @@ const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpySt
const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState =>
spyState != null && spyState.pageName === SiemPageName.hosts;
const isDetectionsRoutes = (spyState: RouteSpyState) =>
spyState != null && spyState.pageName === SiemPageName.detections;
export const getBreadcrumbsForRoute = (
object: RouteSpyState & TabNavigationProps
): Breadcrumb[] | null => {
@ -76,6 +80,24 @@ export const getBreadcrumbsForRoute = (
),
];
}
if (isDetectionsRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
}
return [
...siemRootBreadcrumb,
...getDetectionRulesBreadcrumbs(
spyState,
urlStateKeys.reduce(
(acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)],
[]
)
),
];
}
if (
spyState != null &&
object.navTabs &&

View file

@ -187,6 +187,7 @@ describe('SIEM Navigation', () => {
query: { language: 'kuery', query: '' },
savedQuery: undefined,
search: '',
state: undefined,
tabName: 'authentications',
timeline: { id: '', isOpen: false },
timerange: {

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEqual } from 'lodash/fp';
import isEqual from 'lodash/fp/isEqual';
import deepEqual from 'fast-deep-equal';
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
@ -16,67 +17,78 @@ import { setBreadcrumbs } from './breadcrumbs';
import { TabNavigation } from './tab_navigation';
import { SiemNavigationProps, SiemNavigationComponentProps } from './types';
export const SiemNavigationComponent = React.memo<
SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState
>(
({ detailName, display, navTabs, pageName, pathName, search, tabName, urlState, flowTarget }) => {
useEffect(() => {
if (pathName) {
setBreadcrumbs({
query: urlState.query,
detailName,
filters: urlState.filters,
navTabs,
pageName,
pathName,
savedQuery: urlState.savedQuery,
search,
tabName,
flowTarget,
timerange: urlState.timerange,
timeline: urlState.timeline,
});
}
}, [pathName, search, navTabs, urlState]);
export const SiemNavigationComponent: React.FC<SiemNavigationComponentProps &
SiemNavigationProps &
RouteSpyState> = ({
detailName,
display,
navTabs,
pageName,
pathName,
search,
tabName,
urlState,
flowTarget,
state,
}) => {
useEffect(() => {
if (pathName) {
setBreadcrumbs({
query: urlState.query,
detailName,
filters: urlState.filters,
navTabs,
pageName,
pathName,
savedQuery: urlState.savedQuery,
search,
tabName,
flowTarget,
timerange: urlState.timerange,
timeline: urlState.timeline,
state,
});
}
}, [pathName, search, navTabs, urlState, state]);
return (
<TabNavigation
query={urlState.query}
display={display}
filters={urlState.filters}
navTabs={navTabs}
pageName={pageName}
pathName={pathName}
savedQuery={urlState.savedQuery}
tabName={tabName}
timeline={urlState.timeline}
timerange={urlState.timerange}
/>
);
},
(prevProps, nextProps) => {
return (
prevProps.pathName === nextProps.pathName &&
prevProps.search === nextProps.search &&
isEqual(prevProps.navTabs, nextProps.navTabs) &&
isEqual(prevProps.urlState, nextProps.urlState)
);
}
);
SiemNavigationComponent.displayName = 'SiemNavigationComponent';
return (
<TabNavigation
query={urlState.query}
display={display}
filters={urlState.filters}
navTabs={navTabs}
pageName={pageName}
pathName={pathName}
savedQuery={urlState.savedQuery}
tabName={tabName}
timeline={urlState.timeline}
timerange={urlState.timerange}
/>
);
};
export const SiemNavigationRedux = compose<
React.ComponentClass<SiemNavigationProps & RouteSpyState>
>(connect(makeMapStateToProps))(SiemNavigationComponent);
>(connect(makeMapStateToProps))(
React.memo(
SiemNavigationComponent,
(prevProps, nextProps) =>
prevProps.pathName === nextProps.pathName &&
prevProps.search === nextProps.search &&
isEqual(prevProps.navTabs, nextProps.navTabs) &&
isEqual(prevProps.urlState, nextProps.urlState) &&
deepEqual(prevProps.state, nextProps.state)
)
);
export const SiemNavigation = React.memo<SiemNavigationProps>(props => {
const SiemNavigationContainer: React.FC<SiemNavigationProps> = props => {
const [routeProps] = useRouteSpy();
const stateNavReduxProps: RouteSpyState & SiemNavigationProps = {
...routeProps,
...props,
};
return <SiemNavigationRedux {...stateNavReduxProps} />;
});
SiemNavigation.displayName = 'SiemNavigation';
return <SiemNavigationRedux {...stateNavReduxProps} />;
};
export const SiemNavigation = SiemNavigationContainer;

View file

@ -230,8 +230,13 @@ const makeMapStateToProps = () => {
};
};
export const DetectionEngine = connect(makeMapStateToProps, {
const mapDispatchToProps = {
setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
})(DetectionEngineComponent);
};
export const DetectionEngine = connect(
makeMapStateToProps,
mapDispatchToProps
)(DetectionEngineComponent);
DetectionEngine.displayName = 'DetectionEngine';

View file

@ -19,7 +19,7 @@ const detectionEnginePath = `/:pageName(detections)`;
type Props = Partial<RouteComponentProps<{}>> & { url: string };
export const DetectionEngineContainer = React.memo<Props>(() => (
const DetectionEngineContainerComponent: React.FC<Props> = () => (
<ManageUserInfo>
<Switch>
<Route
@ -35,10 +35,10 @@ export const DetectionEngineContainer = React.memo<Props>(() => (
<Route exact path={`${detectionEnginePath}/rules/create`}>
<CreateRuleComponent />
</Route>
<Route exact path={`${detectionEnginePath}/rules/id/:ruleId/`}>
<Route exact path={`${detectionEnginePath}/rules/id/:detailName`}>
<RuleDetails />
</Route>
<Route exact path={`${detectionEnginePath}/rules/id/:ruleId/edit`}>
<Route exact path={`${detectionEnginePath}/rules/id/:detailName/edit`}>
<EditRuleComponent />
</Route>
<Route
@ -49,5 +49,6 @@ export const DetectionEngineContainer = React.memo<Props>(() => (
/>
</Switch>
</ManageUserInfo>
));
DetectionEngineContainer.displayName = 'DetectionEngineContainer';
);
export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent);

View file

@ -109,7 +109,7 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
hasIndexWrite,
signalIndexName,
} = useUserInfo();
const { ruleId } = useParams();
const { detailName: ruleId } = useParams();
const [isLoading, rule] = useRule(ruleId);
// This is used to re-trigger api rule status when user de/activate rule
const [ruleEnabled, setRuleEnabled] = useState<boolean | null>(null);
@ -381,7 +381,7 @@ const RuleDetailsComponent = memo<RuleDetailsComponentProps>(
}}
</WithSource>
<SpyRoute />
<SpyRoute state={{ ruleName: rule?.name }} />
</>
);
}
@ -402,8 +402,10 @@ const makeMapStateToProps = () => {
};
};
export const RuleDetails = connect(makeMapStateToProps, {
const mapDispatchToProps = {
setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker,
})(RuleDetailsComponent);
};
export const RuleDetails = connect(makeMapStateToProps, mapDispatchToProps)(RuleDetailsComponent);
RuleDetails.displayName = 'RuleDetails';

View file

@ -57,7 +57,7 @@ export const EditRuleComponent = memo(() => {
canUserCRUD,
hasManageApiKey,
} = useUserInfo();
const { ruleId } = useParams();
const { detailName: ruleId } = useParams();
const [loading, rule] = useRule(ruleId);
const userHasNoPermissions =
@ -347,7 +347,7 @@ export const EditRuleComponent = memo(() => {
</EuiFlexGroup>
</WrapperPage>
<SpyRoute />
<SpyRoute state={{ ruleName: rule?.name }} />
</>
);
});

View file

@ -25,6 +25,14 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageT
defaultMessage: 'Signal detection rules',
});
export const ADD_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.addPageTitle', {
defaultMessage: 'Create',
});
export const EDIT_PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.editPageTitle', {
defaultMessage: 'Edit',
});
export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', {
defaultMessage: 'Refresh',
});

View file

@ -0,0 +1,98 @@
/*
* 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 { Breadcrumb } from 'ui/chrome';
import { isEmpty } from 'lodash/fp';
import {
getDetectionEngineUrl,
getDetectionEngineTabUrl,
getRulesUrl,
getRuleDetailsUrl,
getCreateRuleUrl,
getEditRuleUrl,
} from '../../../components/link_to/redirect_to_detection_engine';
import * as i18nDetections from '../translations';
import * as i18nRules from './translations';
import { RouteSpyState } from '../../../utils/route/types';
const getTabBreadcrumb = (pathname: string, search: string[]) => {
const tabPath = pathname.split('/')[2];
if (tabPath === 'alerts') {
return {
text: i18nDetections.ALERT,
href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`,
};
}
if (tabPath === 'signals') {
return {
text: i18nDetections.SIGNAL,
href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`,
};
}
if (tabPath === 'rules') {
return {
text: i18nRules.PAGE_TITLE,
href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`,
};
}
};
const isRuleCreatePage = (pathname: string) =>
pathname.includes('/rules') && pathname.includes('/create');
const isRuleEditPage = (pathname: string) =>
pathname.includes('/rules') && pathname.includes('/edit');
export const getBreadcrumbs = (params: RouteSpyState, search: string[]): Breadcrumb[] => {
let breadcrumb = [
{
text: i18nDetections.PAGE_TITLE,
href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`,
},
];
const tabBreadcrumb = getTabBreadcrumb(params.pathName, search);
if (tabBreadcrumb) {
breadcrumb = [...breadcrumb, tabBreadcrumb];
}
if (params.detailName && params.state?.ruleName) {
breadcrumb = [
...breadcrumb,
{
text: params.state.ruleName,
href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`,
},
];
}
if (isRuleCreatePage(params.pathName)) {
breadcrumb = [
...breadcrumb,
{
text: i18nRules.ADD_PAGE_TITLE,
href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`,
},
];
}
if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) {
breadcrumb = [
...breadcrumb,
{
text: i18nRules.EDIT_PAGE_TITLE,
href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`,
},
];
}
return breadcrumb;
};

View file

@ -5,8 +5,8 @@
*/
import { Breadcrumb } from 'ui/chrome';
import { get, isEmpty } from 'lodash/fp';
import { get } from 'lodash/fp';
import { hostsModel } from '../../../store';
import { HostsTableType } from '../../../store/hosts/model';
import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts';
@ -29,7 +29,7 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Bre
let breadcrumb = [
{
text: i18n.PAGE_TITLE,
href: `${getHostsUrl()}${search && search[0] ? search[0] : ''}`,
href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`,
},
];
@ -38,7 +38,7 @@ export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): Bre
...breadcrumb,
{
text: params.detailName,
href: `${getHostDetailsUrl(params.detailName)}${search && search[1] ? search[1] : ''}`,
href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`,
},
];
}

View file

@ -5,8 +5,8 @@
*/
import { Breadcrumb } from 'ui/chrome';
import { get, isEmpty } from 'lodash/fp';
import { get } from 'lodash/fp';
import { decodeIpv6 } from '../../../lib/helpers';
import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network';
import { networkModel } from '../../../store/network';
@ -28,7 +28,7 @@ export const getBreadcrumbs = (params: NetworkRouteSpyState, search: string[]):
let breadcrumb = [
{
text: i18n.PAGE_TITLE,
href: `${getNetworkUrl()}${search && search[0] ? search[0] : ''}`,
href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`,
},
];
if (params.detailName != null) {
@ -37,7 +37,7 @@ export const getBreadcrumbs = (params: NetworkRouteSpyState, search: string[]):
{
text: decodeIpv6(params.detailName),
href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${
search && search[1] ? search[1] : ''
!isEmpty(search[1]) ? search[1] : ''
}`,
},
];

View file

@ -15,6 +15,7 @@ export const initRouteSpy: RouteSpyState = {
tabName: undefined,
search: '',
pathName: '/',
state: undefined,
};
export const RouterSpyStateContext = createContext<[RouteSpyState, Dispatch<RouteSpyAction>]>([

View file

@ -8,6 +8,7 @@ import * as H from 'history';
import { isEqual } from 'lodash/fp';
import { memo, useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom';
import deepEqual from 'fast-deep-equal';
import { SpyRouteProps } from './types';
import { useRouteSpy } from './use_route_spy';
@ -19,6 +20,7 @@ export const SpyRouteComponent = memo<SpyRouteProps & { location: H.Location }>(
match: {
params: { pageName, detailName, tabName, flowTarget },
},
state,
}) => {
const [isInitializing, setIsInitializing] = useState(true);
const [route, dispatch] = useRouteSpy();
@ -61,8 +63,24 @@ export const SpyRouteComponent = memo<SpyRouteProps & { location: H.Location }>(
},
});
}
} else {
if (pageName && !deepEqual(state, route.state)) {
dispatch({
type: 'updateRoute',
route: {
pageName,
detailName,
tabName,
search,
pathName: pathname,
history,
flowTarget,
state,
},
});
}
}
}, [pathname, search, pageName, detailName, tabName, flowTarget]);
}, [pathname, search, pageName, detailName, tabName, flowTarget, state]);
return null;
}
);

View file

@ -21,6 +21,7 @@ export interface RouteSpyState {
pathName: string;
history?: H.History;
flowTarget?: FlowTarget;
state?: Record<string, string | undefined>;
}
export interface HostRouteSpyState extends RouteSpyState {
@ -38,7 +39,10 @@ export type RouteSpyAction =
}
| {
type: 'updateRouteWithOutSearch';
route: Pick<RouteSpyState, 'pageName' & 'detailName' & 'tabName' & 'pathName' & 'history'>;
route: Pick<
RouteSpyState,
'pageName' & 'detailName' & 'tabName' & 'pathName' & 'history' & 'state'
>;
}
| {
type: 'updateRoute';
@ -55,4 +59,6 @@ export type SpyRouteProps = RouteComponentProps<{
tabName: HostsTableType | undefined;
search: string;
flowTarget: FlowTarget | undefined;
}>;
}> & {
state?: Record<string, string | undefined>;
};

View file

@ -12827,17 +12827,7 @@ fancy-log@^1.3.2:
color-support "^1.1.3"
time-stamp "^1.0.0"
fast-deep-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
integrity sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=
fast-deep-equal@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
fast-deep-equal@^3.1.1:
fast-deep-equal@^1.0.0, fast-deep-equal@^2.0.1, fast-deep-equal@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==