[Cloud Posture] Implement breadcrumbs for security solution (#136821)

This commit is contained in:
Ari Aviran 2022-07-24 11:03:40 +03:00 committed by GitHub
parent 770802b1cb
commit b61625cec6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 91 additions and 63 deletions

View file

@ -26,3 +26,8 @@ export type CloudSecurityPosturePageId =
| 'cloud_security_posture-findings'
| 'cloud_security_posture-benchmarks'
| 'cloud_security_posture-rules';
export interface BreadcrumbEntry {
readonly name: string;
readonly path: string;
}

View file

@ -4,21 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { MouseEvent } from 'react';
import type { ChromeBreadcrumb, CoreStart } from '@kbn/core/public';
import { useEffect } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { type RouteProps, useRouteMatch, useHistory } from 'react-router-dom';
import { type RouteProps, useRouteMatch } from 'react-router-dom';
import type { EuiBreadcrumb } from '@elastic/eui';
import { string } from 'io-ts';
import { i18n } from '@kbn/i18n';
import { CLOUD_SECURITY_POSTURE_BASE_PATH } from '../..';
import type { CspNavigationItem } from './types';
import useObservable from 'react-use/lib/useObservable';
import type { BreadcrumbEntry } from './types';
const getClickableBreadcrumb = (
routeMatch: RouteProps['path'],
breadcrumbPath: CspNavigationItem['path']
) => {
const getClickableBreadcrumb = (routeMatch: RouteProps['path'], breadcrumbPath: string) => {
const hasParams = breadcrumbPath.includes(':');
if (hasParams) return;
@ -27,15 +23,20 @@ const getClickableBreadcrumb = (
}
};
export const useCspBreadcrumbs = (breadcrumbs: CspNavigationItem[]) => {
export const useCspBreadcrumbs = (breadcrumbs: BreadcrumbEntry[]) => {
const {
services: {
chrome: { setBreadcrumbs, docTitle },
application: { getUrlForApp },
application: { currentAppId$, applications$, navigateToApp },
},
} = useKibana<CoreStart>();
const match = useRouteMatch();
const history = useHistory();
const appId = useObservable(currentAppId$);
const applications = useObservable(applications$);
const application = appId ? applications?.get(appId) : undefined;
const appTitle = application?.title;
useEffect(() => {
const additionalBreadCrumbs: ChromeBreadcrumb[] = breadcrumbs.map((breadcrumb) => {
@ -43,31 +44,32 @@ export const useCspBreadcrumbs = (breadcrumbs: CspNavigationItem[]) => {
return {
text: breadcrumb.name,
...(clickableLink && {
onClick: (e) => {
e.preventDefault();
history.push(clickableLink);
},
}),
...(clickableLink &&
appId && {
onClick: (e) => {
e.preventDefault();
void navigateToApp(appId, { path: clickableLink });
},
}),
};
});
const nextBreadcrumbs = [
{
text: i18n.translate('xpack.csp.navigation.cloudPostureBreadcrumbLabel', {
defaultMessage: 'Cloud Posture',
text: appTitle,
...(appId && {
onClick: (e: MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
void navigateToApp(appId);
},
}),
onClick: (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
history.push(CLOUD_SECURITY_POSTURE_BASE_PATH);
},
},
...additionalBreadCrumbs,
];
setBreadcrumbs(nextBreadcrumbs);
docTitle.change(getTextBreadcrumbs(nextBreadcrumbs));
}, [match.path, getUrlForApp, setBreadcrumbs, breadcrumbs, history, docTitle]);
}, [match.path, setBreadcrumbs, breadcrumbs, docTitle, appTitle, appId, navigateToApp]);
};
const getTextBreadcrumbs = (breadcrumbs: EuiBreadcrumb[]) =>

View file

@ -7,6 +7,7 @@
import React from 'react';
import type { UseQueryResult } from 'react-query';
import { Redirect, Switch, Route, useLocation } from 'react-router-dom';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { useFindingsEsPit } from './es_pit/use_findings_es_pit';
import { FindingsEsPitContext } from './es_pit/findings_es_pit_context';
@ -68,8 +69,14 @@ export const FindingsNoPageTemplate = () => {
);
};
export const Findings = () => (
<CspPageTemplate paddingSize="none">
<FindingsNoPageTemplate />
</CspPageTemplate>
);
const FINDINGS_BREADCRUMBS = [cloudPosturePages.findings];
export const Findings = () => {
useCspBreadcrumbs(FINDINGS_BREADCRUMBS);
return (
<CspPageTemplate paddingSize="none">
<FindingsNoPageTemplate />
</CspPageTemplate>
);
};

View file

@ -26,8 +26,6 @@ import {
} from '../utils';
import { PageWrapper, PageTitle, PageTitleText } from '../layout/findings_layout';
import { FindingsGroupBySelector } from '../layout/findings_group_by_selector';
import { useCspBreadcrumbs } from '../../../common/navigation/use_csp_breadcrumbs';
import { findingsNavigation } from '../../../common/navigation/constants';
import { useUrlQuery } from '../../../common/hooks/use_url_query';
import { ErrorCallout } from '../layout/error_callout';
@ -43,8 +41,6 @@ export const getDefaultQuery = ({
});
export const LatestFindingsContainer = ({ dataView }: FindingsBaseProps) => {
useCspBreadcrumbs([findingsNavigation.findings_default]);
const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery);
const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery);

View file

@ -26,7 +26,6 @@ import {
import { PageTitle, PageTitleText, PageWrapper } from '../layout/findings_layout';
import { FindingsGroupBySelector } from '../layout/findings_group_by_selector';
import { findingsNavigation } from '../../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../../common/navigation/use_csp_breadcrumbs';
import { ResourceFindings } from './resource_findings/resource_findings_container';
import { ErrorCallout } from '../layout/error_callout';
import { FindingsDistributionBar } from '../layout/findings_distribution_bar';
@ -57,8 +56,6 @@ export const FindingsByResourceContainer = ({ dataView }: FindingsBaseProps) =>
);
const LatestFindingsByResource = ({ dataView }: FindingsBaseProps) => {
useCspBreadcrumbs([findingsNavigation.findings_by_resource]);
const getPersistedDefaultQuery = usePersistedQuery(getDefaultQuery);
const { urlQuery, setUrlQuery } = useUrlQuery(getPersistedDefaultQuery);

View file

@ -13,7 +13,6 @@ import { generatePath } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import * as TEST_SUBJECTS from '../../test_subjects';
import { PageWrapper, PageTitle, PageTitleText } from '../../layout/findings_layout';
import { useCspBreadcrumbs } from '../../../../common/navigation/use_csp_breadcrumbs';
import { findingsNavigation } from '../../../../common/navigation/constants';
import { ResourceFindingsQuery, useResourceFindings } from './use_resource_findings';
import { useUrlQuery } from '../../../../common/hooks/use_url_query';
@ -54,7 +53,6 @@ const BackToResourcesButton = () => (
);
export const ResourceFindings = ({ dataView }: FindingsBaseProps) => {
useCspBreadcrumbs([findingsNavigation.findings_default]);
const { euiTheme } = useEuiTheme();
const params = useParams<{ resourceId: string }>();

View file

@ -5,31 +5,56 @@
* 2.0.
*/
import React, { useMemo } from 'react';
import React, { useContext, useMemo } from 'react';
import { generatePath, Link, type RouteComponentProps } from 'react-router-dom';
import { EuiTextColor, EuiButtonEmpty, EuiFlexGroup, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { pagePathGetters } from '@kbn/fleet-plugin/public';
import type { BreadcrumbEntry } from '../../common/navigation/types';
import { RulesContainer, type PageUrlParams } from './rules_container';
import { cloudPosturePages } from '../../common/navigation/constants';
import { useCspBreadcrumbs } from '../../common/navigation/use_csp_breadcrumbs';
import type { CspPageNavigationItem } from '../../common/navigation/types';
import { useCspIntegrationInfo } from './use_csp_integration';
import { CspPageTemplate } from '../../components/csp_page_template';
import { useKibana } from '../../common/hooks/use_kibana';
import { CloudPosturePage } from '../../components/cloud_posture_page';
import { SecuritySolutionContext } from '../../application/security_solution_context';
const getRulesBreadcrumbs = (name?: string): CspPageNavigationItem[] =>
[cloudPosturePages.benchmarks, { ...cloudPosturePages.rules, name }].filter(
(breadcrumb): breadcrumb is CspPageNavigationItem => !!breadcrumb.name
);
const getRulesBreadcrumbs = (
name?: string,
manageBreadcrumb?: BreadcrumbEntry
): BreadcrumbEntry[] => {
const breadCrumbs: BreadcrumbEntry[] = [];
if (manageBreadcrumb) {
breadCrumbs.push(manageBreadcrumb);
}
breadCrumbs.push(cloudPosturePages.benchmarks);
if (name) {
breadCrumbs.push({ ...cloudPosturePages.rules, name });
} else {
breadCrumbs.push(cloudPosturePages.rules);
}
return breadCrumbs;
};
export const RulesNoPageTemplate = ({ match: { params } }: RouteComponentProps<PageUrlParams>) => {
const { http } = useKibana().services;
const integrationInfo = useCspIntegrationInfo(params);
const securitySolutionContext = useContext(SecuritySolutionContext);
const [packageInfo, agentInfo] = integrationInfo.data || [];
const breadcrumbs = useMemo(
() =>
getRulesBreadcrumbs(packageInfo?.name, securitySolutionContext?.getManageBreadcrumbEntry()),
[packageInfo?.name, securitySolutionContext]
);
useCspBreadcrumbs(breadcrumbs);
return (
<CloudPosturePage query={integrationInfo}>
<EuiPageHeader
@ -89,19 +114,6 @@ export const RulesNoPageTemplate = ({ match: { params } }: RouteComponentProps<P
};
export const Rules = (props: RouteComponentProps<PageUrlParams>) => {
const { params } = props.match;
const integrationInfo = useCspIntegrationInfo(params);
const [packageInfo] = integrationInfo.data || [];
const breadcrumbs = useMemo(
// TODO: make benchmark breadcrumb navigable
() => getRulesBreadcrumbs(packageInfo?.name),
[packageInfo?.name]
);
useCspBreadcrumbs(breadcrumbs);
return (
<CspPageTemplate>
<RulesNoPageTemplate {...props} />

View file

@ -10,7 +10,7 @@ import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/
import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ChartsPluginStart } from '@kbn/charts-plugin/public';
import type { DiscoverStart } from '@kbn/discover-plugin/public';
import type { CloudSecurityPosturePageId } from './common/navigation/types';
import type { BreadcrumbEntry, CloudSecurityPosturePageId } from './common/navigation/types';
/**
* The cloud security posture's public plugin setup interface.
@ -52,4 +52,6 @@ export interface CspSecuritySolutionContext {
getFiltersGlobalComponent: () => ComponentType<{ children: ReactNode }>;
/** Gets the `SpyRoute` component for navigation highlighting and breadcrumbs. */
getSpyRouteComponent: () => ComponentType<{ pageName?: CloudSecurityPosturePageId }>;
/** Gets the `Manage` breadcrumb entry. */
getManageBreadcrumbEntry: () => BreadcrumbEntry;
}

View file

@ -12,11 +12,13 @@ import {
type CspSecuritySolutionContext,
} from '@kbn/cloud-security-posture-plugin/public';
import { TrackApplicationView } from '@kbn/usage-collection-plugin/public';
import { MANAGE_PATH } from '../../common/constants';
import type { SecurityPageName, SecuritySubPluginRoutes } from '../app/types';
import { useKibana } from '../common/lib/kibana';
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
import { SpyRoute } from '../common/utils/route/spy_routes';
import { FiltersGlobal } from '../common/components/filters_global';
import { MANAGE } from '../app/translations';
// This exists only for the type signature cast
const CloudPostureSpyRoute = ({ pageName }: { pageName?: CloudSecurityPosturePageId }) => (
@ -29,6 +31,7 @@ const CloudSecurityPosture = memo(() => {
const securitySolutionContext: CspSecuritySolutionContext = {
getFiltersGlobalComponent: () => FiltersGlobal,
getSpyRouteComponent: () => CloudPostureSpyRoute,
getManageBreadcrumbEntry: () => ({ name: MANAGE, path: MANAGE_PATH }),
};
return (

View file

@ -80,7 +80,13 @@ export const getBreadcrumbsForRoute = (
): ChromeBreadcrumb[] | null => {
const spyState: RouteSpyState = omit('navTabs', object);
if (!spyState || !object.navTabs || !spyState.pageName || isCaseRoutes(spyState)) {
if (
!spyState ||
!object.navTabs ||
!spyState.pageName ||
isCaseRoutes(spyState) ||
isCloudSecurityPostureManagedRoutes(spyState)
) {
return null;
}
@ -164,6 +170,9 @@ const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute
spyState.pageName === SecurityPageName.rules ||
spyState.pageName === SecurityPageName.rulesCreate;
const isCloudSecurityPostureManagedRoutes = (spyState: RouteSpyState) =>
spyState.pageName === SecurityPageName.cloudSecurityPostureRules;
const emptyLastBreadcrumbUrl = (breadcrumbs: ChromeBreadcrumb[]) => {
const leadingBreadCrumbs = breadcrumbs.slice(0, -1);
const lastBreadcrumb = last(breadcrumbs);

View file

@ -10429,7 +10429,6 @@
"xpack.csp.findings.groupBySelector.groupByResourceIdLabel": "Ressource",
"xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "Retour à la vue de regroupement par ressource",
"xpack.csp.findings.searchBar.searchPlaceholder": "Rechercher dans les résultats (par ex. rule.section.keyword : \"serveur d'API\")",
"xpack.csp.navigation.cloudPostureBreadcrumbLabel": "Niveau du cloud",
"xpack.csp.rules.activateAllButtonLabel": "Activer {count, plural, one {# règle} other {# règles}}",
"xpack.csp.rules.clearSelectionButtonLabel": "Effacer la sélection",
"xpack.csp.rules.deactivateAllButtonLabel": "Désactiver {count, plural, one {# règle} other {# règles}}",

View file

@ -10421,7 +10421,6 @@
"xpack.csp.findings.groupBySelector.groupByResourceIdLabel": "リソース",
"xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "リソース別グループビューに戻る",
"xpack.csp.findings.searchBar.searchPlaceholder": "検索結果rule.section.keyword\"API Server\"",
"xpack.csp.navigation.cloudPostureBreadcrumbLabel": "クラウド態勢",
"xpack.csp.rules.activateAllButtonLabel": "{count, plural, other {#個のルール}}をアクティブ化",
"xpack.csp.rules.clearSelectionButtonLabel": "選択した項目をクリア",
"xpack.csp.rules.deactivateAllButtonLabel": "{count, plural, other {#個のルール}}を非アクティブ化",

View file

@ -10436,7 +10436,6 @@
"xpack.csp.findings.groupBySelector.groupByResourceIdLabel": "资源",
"xpack.csp.findings.resourceFindings.backToResourcesPageButtonLabel": "返回到按资源视图分组",
"xpack.csp.findings.searchBar.searchPlaceholder": "搜索结果例如rule.section.keyword“APM 服务器”)",
"xpack.csp.navigation.cloudPostureBreadcrumbLabel": "云态势",
"xpack.csp.rules.activateAllButtonLabel": "激活 {count, plural, other {# 个规则}}",
"xpack.csp.rules.clearSelectionButtonLabel": "清除所选内容",
"xpack.csp.rules.deactivateAllButtonLabel": "停用 {count, plural, other {# 个规则}}",