[Security Solution] Add 3rd level breadcrumb to admin page (#71275)

[Endpoint Security] Add 3rd level (hosts / policies) breadcrumb to admin page
This commit is contained in:
Candace Park 2020-07-14 00:00:29 -04:00 committed by GitHub
parent f4091df289
commit b7a6cff74d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 145 additions and 57 deletions

View file

@ -42,7 +42,7 @@ export enum SecurityPageName {
network = 'network',
timelines = 'timelines',
case = 'case',
management = 'management',
administration = 'administration',
}
export const APP_OVERVIEW_PATH = `${APP_PATH}/overview`;

View file

@ -7,7 +7,7 @@ import {
CASES,
DETECTIONS,
HOSTS,
MANAGEMENT,
ADMINISTRATION,
NETWORK,
OVERVIEW,
TIMELINES,
@ -73,7 +73,7 @@ describe('top-level navigation common to all pages in the Security app', () => {
});
it('navigates to the Administration page', () => {
navigateFromHeaderTo(MANAGEMENT);
navigateFromHeaderTo(ADMINISTRATION);
cy.url().should('include', ADMINISTRATION_URL);
});
});

View file

@ -14,7 +14,7 @@ export const HOSTS = '[data-test-subj="navigation-hosts"]';
export const KQL_INPUT = '[data-test-subj="queryInput"]';
export const MANAGEMENT = '[data-test-subj="navigation-management"]';
export const ADMINISTRATION = '[data-test-subj="navigation-administration"]';
export const NETWORK = '[data-test-subj="navigation-network"]';

View file

@ -61,11 +61,11 @@ export const navTabs: SiemNavTab = {
disabled: false,
urlKey: 'case',
},
[SecurityPageName.management]: {
id: SecurityPageName.management,
[SecurityPageName.administration]: {
id: SecurityPageName.administration,
name: i18n.ADMINISTRATION,
href: APP_MANAGEMENT_PATH,
disabled: false,
urlKey: SecurityPageName.management,
urlKey: SecurityPageName.administration,
},
};

View file

@ -15,12 +15,14 @@ import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/p
import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils';
import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils';
import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages';
import { getBreadcrumbs as getAdminBreadcrumbs } from '../../../../management/pages';
import { SecurityPageName } from '../../../../app/types';
import {
RouteSpyState,
HostRouteSpyState,
NetworkRouteSpyState,
TimelineRouteSpyState,
AdministrationRouteSpyState,
} from '../../../utils/route/types';
import { getAppOverviewUrl } from '../../link_to';
@ -61,6 +63,10 @@ const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState =>
const isAlertsRoutes = (spyState: RouteSpyState) =>
spyState != null && spyState.pageName === SecurityPageName.detections;
const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState =>
spyState != null && spyState.pageName === SecurityPageName.administration;
// eslint-disable-next-line complexity
export const getBreadcrumbsForRoute = (
object: RouteSpyState & TabNavigationProps,
getUrlForApp: GetUrlForApp
@ -159,6 +165,27 @@ export const getBreadcrumbsForRoute = (
),
];
}
if (isAdminRoutes(spyState) && object.navTabs) {
const tempNav: SearchNavTab = { urlKey: 'administration', isDetailPage: false };
let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)];
if (spyState.tabName != null) {
urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)];
}
return [
...siemRootBreadcrumb,
...getAdminBreadcrumbs(
spyState,
urlStateKeys.reduce(
(acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)],
[]
),
getUrlForApp
),
];
}
if (
spyState != null &&
object.navTabs &&

View file

@ -106,12 +106,12 @@ describe('SIEM Navigation', () => {
name: 'Cases',
urlKey: 'case',
},
management: {
administration: {
disabled: false,
href: '/app/security/administration',
id: 'management',
id: 'administration',
name: 'Administration',
urlKey: 'management',
urlKey: 'administration',
},
hosts: {
disabled: false,
@ -218,12 +218,12 @@ describe('SIEM Navigation', () => {
name: 'Hosts',
urlKey: 'host',
},
management: {
administration: {
disabled: false,
href: '/app/security/administration',
id: 'management',
id: 'administration',
name: 'Administration',
urlKey: 'management',
urlKey: 'administration',
},
network: {
disabled: false,

View file

@ -48,7 +48,7 @@ export type SiemNavTabKey =
| SecurityPageName.detections
| SecurityPageName.timelines
| SecurityPageName.case
| SecurityPageName.management;
| SecurityPageName.administration;
export type SiemNavTab = Record<SiemNavTabKey, NavTab>;

View file

@ -30,4 +30,4 @@ export type UrlStateType =
| 'network'
| 'overview'
| 'timeline'
| 'management';
| 'administration';

View file

@ -96,6 +96,8 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'timeline';
} else if (pageName === SecurityPageName.case) {
return 'case';
} else if (pageName === SecurityPageName.administration) {
return 'administration';
}
return 'overview';
};

View file

@ -46,7 +46,7 @@ export const URL_STATE_KEYS: Record<UrlStateType, KeyUrlState[]> = {
CONSTANTS.timerange,
CONSTANTS.timeline,
],
management: [],
administration: [],
network: [
CONSTANTS.appQuery,
CONSTANTS.filters,

View file

@ -12,9 +12,10 @@ import { TimelineType } from '../../../../common/types/timeline';
import { HostsTableType } from '../../../hosts/store/model';
import { NetworkRouteType } from '../../../network/pages/navigation/types';
import { AdministrationSubTab as AdministrationType } from '../../../management/types';
import { FlowTarget } from '../../../graphql/types';
export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType;
export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType | AdministrationType;
export interface RouteSpyState {
pageName: string;
detailName: string | undefined;
@ -38,6 +39,10 @@ export interface TimelineRouteSpyState extends RouteSpyState {
tabName: TimelineType | undefined;
}
export interface AdministrationRouteSpyState extends RouteSpyState {
tabName: AdministrationType | undefined;
}
export type RouteSpyAction =
| {
type: 'updateSearch';

View file

@ -3,16 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ManagementStoreGlobalNamespace, ManagementSubTab } from '../types';
import { ManagementStoreGlobalNamespace, AdministrationSubTab } from '../types';
import { APP_ID } from '../../../common/constants';
import { SecurityPageName } from '../../app/types';
// --[ ROUTING ]---------------------------------------------------------------------------
export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.management}`;
export const MANAGEMENT_APP_ID = `${APP_ID}:${SecurityPageName.administration}`;
export const MANAGEMENT_ROUTING_ROOT_PATH = '';
export const MANAGEMENT_ROUTING_HOSTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.hosts})`;
export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${ManagementSubTab.policies})/:policyId`;
export const MANAGEMENT_ROUTING_HOSTS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.hosts})`;
export const MANAGEMENT_ROUTING_POLICIES_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.policies})`;
export const MANAGEMENT_ROUTING_POLICY_DETAILS_PATH = `${MANAGEMENT_ROUTING_ROOT_PATH}/:tabName(${AdministrationSubTab.policies})/:policyId`;
// --[ STORE ]---------------------------------------------------------------------------
/** The SIEM global store namespace where the management state will be mounted */

View file

@ -14,7 +14,7 @@ import {
MANAGEMENT_ROUTING_POLICIES_PATH,
MANAGEMENT_ROUTING_POLICY_DETAILS_PATH,
} from './constants';
import { ManagementSubTab } from '../types';
import { AdministrationSubTab } from '../types';
import { appendSearch } from '../../common/components/link_to/helpers';
import { HostIndexUIQueryParams } from '../pages/endpoint_hosts/types';
@ -47,7 +47,7 @@ export const getHostListPath = (
if (name === 'hostList') {
return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, {
tabName: ManagementSubTab.hosts,
tabName: AdministrationSubTab.hosts,
})}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
}
return `${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
@ -65,17 +65,17 @@ export const getHostDetailsPath = (
const urlSearch = `${urlQueryParams && !isEmpty(search) ? '&' : ''}${search ?? ''}`;
return `${generatePath(MANAGEMENT_ROUTING_HOSTS_PATH, {
tabName: ManagementSubTab.hosts,
tabName: AdministrationSubTab.hosts,
})}${appendSearch(`${urlQueryParams ? `${urlQueryParams}${urlSearch}` : urlSearch}`)}`;
};
export const getPoliciesPath = (search?: string) =>
`${generatePath(MANAGEMENT_ROUTING_POLICIES_PATH, {
tabName: ManagementSubTab.policies,
tabName: AdministrationSubTab.policies,
})}${appendSearch(search)}`;
export const getPolicyDetailPath = (policyId: string, search?: string) =>
`${generatePath(MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, {
tabName: ManagementSubTab.policies,
tabName: AdministrationSubTab.policies,
policyId,
})}${appendSearch(search)}`;

View file

@ -0,0 +1,15 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const HOSTS_TAB = i18n.translate('xpack.securitySolution.hostsTab', {
defaultMessage: 'Hosts',
});
export const POLICIES_TAB = i18n.translate('xpack.securitySolution.policiesTab', {
defaultMessage: 'Policies',
});

View file

@ -8,15 +8,15 @@ import React, { memo, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { useParams } from 'react-router-dom';
import { PageView, PageViewProps } from '../../common/components/endpoint/page_view';
import { ManagementSubTab } from '../types';
import { AdministrationSubTab } from '../types';
import { SecurityPageName } from '../../app/types';
import { useFormatUrl } from '../../common/components/link_to';
import { getHostListPath, getPoliciesPath } from '../common/routing';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) => {
const { formatUrl, search } = useFormatUrl(SecurityPageName.management);
const { tabName } = useParams<{ tabName: ManagementSubTab }>();
const { formatUrl, search } = useFormatUrl(SecurityPageName.administration);
const { tabName } = useParams<{ tabName: AdministrationSubTab }>();
const goToEndpoint = useNavigateByRouterEventHandler(
getHostListPath({ name: 'hostList' }, search)
@ -30,11 +30,11 @@ export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) =>
}
return [
{
name: i18n.translate('xpack.securitySolution.managementTabs.endpoints', {
name: i18n.translate('xpack.securitySolution.managementTabs.hosts', {
defaultMessage: 'Hosts',
}),
id: ManagementSubTab.hosts,
isSelected: tabName === ManagementSubTab.hosts,
id: AdministrationSubTab.hosts,
isSelected: tabName === AdministrationSubTab.hosts,
href: formatUrl(getHostListPath({ name: 'hostList' })),
onClick: goToEndpoint,
},
@ -42,8 +42,8 @@ export const ManagementPageView = memo<Omit<PageViewProps, 'tabs'>>((options) =>
name: i18n.translate('xpack.securitySolution.managementTabs.policies', {
defaultMessage: 'Policies',
}),
id: ManagementSubTab.policies,
isSelected: tabName === ManagementSubTab.policies,
id: AdministrationSubTab.policies,
isSelected: tabName === AdministrationSubTab.policies,
href: formatUrl(getPoliciesPath()),
onClick: goToPolicies,
},

View file

@ -61,7 +61,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
const policyStatus = useHostSelector(
policyResponseStatus
) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR;
const { formatUrl } = useFormatUrl(SecurityPageName.management);
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
const detailsResultsUpper = useMemo(() => {
return [
@ -106,7 +106,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
path: agentDetailsWithFlyoutPath,
state: {
onDoneNavigateTo: [
'securitySolution:management',
'securitySolution:administration',
{
path: getHostDetailsPath({ name: 'hostDetails', selected_host: details.host.id }),
},

View file

@ -118,7 +118,7 @@ const PolicyResponseFlyoutPanel = memo<{
const responseAttentionCount = useHostSelector(policyResponseFailedOrWarningActionCount);
const loading = useHostSelector(policyResponseLoading);
const error = useHostSelector(policyResponseError);
const { formatUrl } = useFormatUrl(SecurityPageName.management);
const { formatUrl } = useFormatUrl(SecurityPageName.administration);
const [detailsUri, detailsRoutePath] = useMemo(
() => [
formatUrl(

View file

@ -89,7 +89,7 @@ export const HostList = () => {
policyItemsLoading,
endpointPackageVersion,
} = useHostSelector(selector);
const { formatUrl, search } = useFormatUrl(SecurityPageName.management);
const { formatUrl, search } = useFormatUrl(SecurityPageName.administration);
const dispatch = useDispatch<(a: HostAction) => void>();
@ -127,12 +127,12 @@ export const HostList = () => {
}`,
state: {
onCancelNavigateTo: [
'securitySolution:management',
'securitySolution:administration',
{ path: getHostListPath({ name: 'hostList' }) },
],
onCancelUrl: formatUrl(getHostListPath({ name: 'hostList' })),
onSaveNavigateTo: [
'securitySolution:management',
'securitySolution:administration',
{ path: getHostListPath({ name: 'hostList' }) },
],
},
@ -145,7 +145,7 @@ export const HostList = () => {
path: `#/configs/${selectedPolicyId}?openEnrollmentFlyout=true`,
state: {
onDoneNavigateTo: [
'securitySolution:management',
'securitySolution:administration',
{ path: getHostListPath({ name: 'hostList' }) },
],
},
@ -422,7 +422,7 @@ export const HostList = () => {
</>
)}
{renderTableOrEmptyState}
<SpyRoute pageName={SecurityPageName.management} />
<SpyRoute pageName={SecurityPageName.administration} />
</ManagementPageView>
);
};

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { isEmpty } from 'lodash/fp';
import React, { memo } from 'react';
import { useHistory, Route, Switch } from 'react-router-dom';
import { ChromeBreadcrumb } from 'kibana/public';
import { EuiText, EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { PolicyContainer } from './policy';
@ -18,10 +20,47 @@ import {
import { NotFoundPage } from '../../app/404';
import { HostsContainer } from './endpoint_hosts';
import { getHostListPath } from '../common/routing';
import { APP_ID, SecurityPageName } from '../../../common/constants';
import { GetUrlForApp } from '../../common/components/navigation/types';
import { AdministrationRouteSpyState } from '../../common/utils/route/types';
import { ADMINISTRATION } from '../../app/home/translations';
import { AdministrationSubTab } from '../types';
import { HOSTS_TAB, POLICIES_TAB } from '../common/translations';
import { SpyRoute } from '../../common/utils/route/spy_routes';
import { SecurityPageName } from '../../app/types';
import { useIngestEnabledCheck } from '../../common/hooks/endpoint/ingest_enabled';
const TabNameMappedToI18nKey: Record<string, string> = {
[AdministrationSubTab.hosts]: HOSTS_TAB,
[AdministrationSubTab.policies]: POLICIES_TAB,
};
export const getBreadcrumbs = (
params: AdministrationRouteSpyState,
search: string[],
getUrlForApp: GetUrlForApp
): ChromeBreadcrumb[] => {
let breadcrumb = [
{
text: ADMINISTRATION,
href: getUrlForApp(`${APP_ID}:${SecurityPageName.administration}`, {
path: !isEmpty(search[0]) ? search[0] : '',
}),
},
];
const tabName = params?.tabName;
if (!tabName) return breadcrumb;
breadcrumb = [
...breadcrumb,
{
text: TabNameMappedToI18nKey[tabName],
href: '',
},
];
return breadcrumb;
};
const NoPermissions = memo(() => {
return (
<>
@ -40,14 +79,14 @@ const NoPermissions = memo(() => {
<p>
<EuiText color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpointManagemnet.noPermissionsSubText"
id="xpack.securitySolution.endpointManagement.noPermissionsSubText"
defaultMessage="It looks like Ingest Manager is disabled. Ingest Manager must be enabled to use this feature. If you do not have permissions to enable Ingest Manager, contact your Kibana administrator."
/>
</EuiText>
</p>
}
/>
<SpyRoute pageName={SecurityPageName.management} />
<SpyRoute pageName={SecurityPageName.administration} />
</>
);
});

View file

@ -172,7 +172,7 @@ describe('Policy Details', () => {
cancelbutton.simulate('click', { button: 0 });
const navigateToAppMockedCalls = coreStart.application.navigateToApp.mock.calls;
expect(navigateToAppMockedCalls[navigateToAppMockedCalls.length - 1]).toEqual([
'securitySolution:management',
'securitySolution:administration',
{ path: policyListPathUrl },
]);
});

View file

@ -55,7 +55,7 @@ export const PolicyDetails = React.memo(() => {
application: { navigateToApp },
},
} = useKibana();
const { formatUrl, search } = useFormatUrl(SecurityPageName.management);
const { formatUrl, search } = useFormatUrl(SecurityPageName.administration);
const { state: locationRouteState } = useLocation<PolicyDetailsRouteState>();
// Store values
@ -149,7 +149,7 @@ export const PolicyDetails = React.memo(() => {
<span data-test-subj="policyDetailsIdNotFoundMessage">{policyApiError?.message}</span>
</EuiCallOut>
) : null}
<SpyRoute pageName={SecurityPageName.management} />
<SpyRoute pageName={SecurityPageName.administration} />
</ManagementPageView>
);
}
@ -251,7 +251,7 @@ export const PolicyDetails = React.memo(() => {
<EuiSpacer size="l" />
<LinuxEvents />
</ManagementPageView>
<SpyRoute pageName={SecurityPageName.management} />
<SpyRoute pageName={SecurityPageName.administration} />
</>
);
});

View file

@ -127,7 +127,7 @@ export const PolicyList = React.memo(() => {
const { services, notifications } = useKibana();
const history = useHistory();
const location = useLocation();
const { formatUrl, search } = useFormatUrl(SecurityPageName.management);
const { formatUrl, search } = useFormatUrl(SecurityPageName.administration);
const [showDelete, setShowDelete] = useState<boolean>(false);
const [policyIdToDelete, setPolicyIdToDelete] = useState<string>('');
@ -477,7 +477,7 @@ export const PolicyList = React.memo(() => {
handleTableChange,
paginationSetup,
])}
<SpyRoute pageName={SecurityPageName.management} />
<SpyRoute pageName={SecurityPageName.administration} />
</ManagementPageView>
</>
);

View file

@ -24,7 +24,7 @@ export type ManagementState = CombinedState<{
/**
* The management list of sub-tabs. Changes to these will impact the Router routes.
*/
export enum ManagementSubTab {
export enum AdministrationSubTab {
hosts = 'hosts',
policies = 'policy',
}
@ -33,8 +33,8 @@ export enum ManagementSubTab {
* The URL route params for the Management Policy List section
*/
export interface ManagementRoutePolicyListParams {
pageName: SecurityPageName.management;
tabName: ManagementSubTab.policies;
pageName: SecurityPageName.administration;
tabName: AdministrationSubTab.policies;
}
/**

View file

@ -281,7 +281,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
});
core.application.register({
id: `${APP_ID}:${SecurityPageName.management}`,
id: `${APP_ID}:${SecurityPageName.administration}`,
title: ADMINISTRATION,
order: 9002,
euiIconType: APP_ICON,

View file

@ -80,7 +80,7 @@ const securitySubPlugins = [
`${APP_ID}:${SecurityPageName.network}`,
`${APP_ID}:${SecurityPageName.timelines}`,
`${APP_ID}:${SecurityPageName.case}`,
`${APP_ID}:${SecurityPageName.management}`,
`${APP_ID}:${SecurityPageName.administration}`,
];
export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, StartPlugins> {