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

Nav unified show timeline #131811

Merged
merged 6 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export enum SecurityPageName {
overview = 'overview',
policies = 'policy',
rules = 'rules',
rulesCreate = 'rules-create',
timelines = 'timelines',
timelinesTemplates = 'timelines-templates',
trustedApps = 'trusted_apps',
Expand All @@ -119,7 +120,7 @@ export enum SecurityPageName {
sessions = 'sessions',
usersEvents = 'users-events',
usersExternalAlerts = 'users-external_alerts',
threatHuntingLanding = 'threat-hunting',
threatHuntingLanding = 'threat_hunting',
dashboardsLanding = 'dashboards',
}

Expand All @@ -134,6 +135,7 @@ export const DETECTION_RESPONSE_PATH = '/detection_response' as const;
export const DETECTIONS_PATH = '/detections' as const;
export const ALERTS_PATH = '/alerts' as const;
export const RULES_PATH = '/rules' as const;
export const RULES_CREATE_PATH = `${RULES_PATH}/create` as const;
export const EXCEPTIONS_PATH = '/exceptions' as const;
export const HOSTS_PATH = '/hosts' as const;
export const USERS_PATH = '/users' as const;
Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/security_solution/public/app/deep_links/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
GETTING_STARTED,
THREAT_HUNTING,
DASHBOARDS,
CREATE_NEW_RULE,
} from '../translations';
import {
OVERVIEW_PATH,
Expand All @@ -59,6 +60,7 @@ import {
THREAT_HUNTING_PATH,
DASHBOARDS_PATH,
MANAGE_PATH,
RULES_CREATE_PATH,
} from '../../../common/constants';
import { ExperimentalFeatures } from '../../../common/experimental_features';

Expand Down Expand Up @@ -183,6 +185,15 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
}),
],
searchable: true,
deepLinks: [
{
id: SecurityPageName.rulesCreate,
title: CREATE_NEW_RULE,
path: RULES_CREATE_PATH,
navLinkStatus: AppNavLinkStatus.hidden,
searchable: false,
},
],
},
{
id: SecurityPageName.exceptions,
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/security_solution/public/app/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export const BLOCKLIST = i18n.translate('xpack.securitySolution.navigation.block
defaultMessage: 'Blocklist',
});

export const CREATE_NEW_RULE = i18n.translate('xpack.securitySolution.navigation.newRuleTitle', {
defaultMessage: 'Create new rule',
});

export const GO_TO_DOCUMENTATION = i18n.translate(
'xpack.securitySolution.goToDocumentationButton',
{
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/security_solution/public/cases/links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ export const getCasesLinkItems = (): LinkItem => {
[SecurityPageName.caseConfigure]: {
features: [FEATURE.casesCrud],
licenseType: 'gold',
hideTimeline: true,
},
[SecurityPageName.caseCreate]: {
features: [FEATURE.casesCrud],
hideTimeline: true,
},
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ export const getDetectionEngineUrl = (search?: string) => `${appendSearch(search

export const getRulesUrl = (search?: string) => `${appendSearch(search)}`;

export const getCreateRuleUrl = (search?: string) => `/create${appendSearch(search)}`;
machadoum marked this conversation as resolved.
Show resolved Hide resolved

export const getRuleDetailsUrl = (detailName: string, search?: string) =>
`/id/${detailName}${appendSearch(search)}`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute
spyState != null && spyState.pageName === SecurityPageName.administration;

const isRulesRoutes = (spyState: RouteSpyState): spyState is AdministrationRouteSpyState =>
spyState != null && spyState.pageName === SecurityPageName.rules;
spyState != null &&
(spyState.pageName === SecurityPageName.rules ||
spyState.pageName === SecurityPageName.rulesCreate);

// eslint-disable-next-line complexity
export const getBreadcrumbsForRoute = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/mod
export const isDetectionsPages = (pageName: string) =>
pageName === SecurityPageName.alerts ||
pageName === SecurityPageName.rules ||
pageName === SecurityPageName.rulesCreate ||
pageName === SecurityPageName.exceptions;

export const decodeRisonUrlState = <T>(value: string | undefined): T | null => {
Expand Down Expand Up @@ -103,7 +104,7 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'network';
} else if (pageName === SecurityPageName.alerts) {
return 'alerts';
} else if (pageName === SecurityPageName.rules) {
} else if (pageName === SecurityPageName.rules || pageName === SecurityPageName.rulesCreate) {
return 'rules';
} else if (pageName === SecurityPageName.exceptions) {
return 'exceptions';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const appLinks: Readonly<LinkItem[]> = Object.freeze([
}),
],
links: [hostsLinks, networkLinks, usersLinks],
skipUrlState: true,
hideTimeline: true,
},
timelinesLinks,
getCasesLinkItems(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ const threatHuntingLinkInfo = {
features: ['siem.show'],
globalNavEnabled: false,
globalSearchKeywords: ['Threat hunting'],
id: 'threat-hunting',
id: 'threat_hunting',
path: '/threat_hunting',
title: 'Threat Hunting',
hideTimeline: true,
skipUrlState: true,
};

const hostsLinkInfo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ const getNormalizedLinks = (
* Normalized indexed version of the global `links` array, referencing the parent by id, instead of having nested links children
*/
const normalizedLinks: Readonly<NormalizedLinks> = Object.freeze(getNormalizedLinks(appLinks));

/**
* Returns the `NormalizedLink` from a link id parameter.
* The object reference is frozen to make sure it is not mutated by the caller.
Expand Down Expand Up @@ -193,3 +192,7 @@ export const getAncestorLinksInfo = (id: SecurityPageName): LinkInfo[] => {
export const needsUrlState = (id: SecurityPageName): boolean => {
return !getNormalizedLink(id).skipUrlState;
};

export const getLinksWithHiddenTimeline = (): LinkInfo[] => {
return Object.values(normalizedLinks).filter((link) => link.hideTimeline);
};
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface LinkItem {
links?: LinkItem[];
path: string;
skipUrlState?: boolean; // defaults to false
hideTimeline?: boolean; // defaults to false
title: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,96 @@ jest.mock('react-router-dom', () => {
};
});

const mockedUseIsGroupedNavigationEnabled = jest.fn();

jest.mock('../../components/navigation/helpers', () => ({
useIsGroupedNavigationEnabled: () => mockedUseIsGroupedNavigationEnabled(),
}));

describe('use show timeline', () => {
it('shows timeline for routes on default', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([true]);
describe('useIsGroupedNavigationEnabled false', () => {
beforeAll(() => {
mockedUseIsGroupedNavigationEnabled.mockReturnValue(false);
});
});
it('hides timeline for blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([false]);

it('shows timeline for routes on default', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([true]);
});
});
});
it('shows timeline for partial blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([true]);

it('hides timeline for blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([false]);
});
});
it('shows timeline for partial blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([true]);
});
});
it('hides timeline for sub blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([false]);
});
});
});
it('hides timeline for sub blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([false]);

describe('useIsGroupedNavigationEnabled true', () => {
beforeAll(() => {
mockedUseIsGroupedNavigationEnabled.mockReturnValue(true);
});

it('shows timeline for routes on default', async () => {
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([true]);
});
});

it('hides timeline for blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules/create' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([false]);
});
});
it('shows timeline for partial blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/rules' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([true]);
});
});
it('hides timeline for sub blacklist routes', async () => {
mockUseLocation.mockReturnValueOnce({ pathname: '/administration/policy' });
await act(async () => {
const { result, waitForNextUpdate } = renderHook(() => useShowTimeline());
await waitForNextUpdate();
const showTimeline = result.current;
expect(showTimeline).toEqual([false]);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import { useState, useEffect } from 'react';
import { matchPath, useLocation } from 'react-router-dom';

const HIDDEN_TIMELINE_ROUTES: readonly string[] = [
import { getLinksWithHiddenTimeline } from '../../links';
import { useIsGroupedNavigationEnabled } from '../../components/navigation/helpers';

const DEPRECATED_HIDDEN_TIMELINE_ROUTES: readonly string[] = [
`/cases/configure`,
'/administration',
'/rules/create',
Expand All @@ -18,17 +21,27 @@ const HIDDEN_TIMELINE_ROUTES: readonly string[] = [
'/manage',
];

const isHiddenTimelinePath = (currentPath: string): boolean => {
return !!HIDDEN_TIMELINE_ROUTES.find((route) => matchPath(currentPath, route));
const isTimelineHidden = (currentPath: string, isGroupedNavigationEnabled: boolean): boolean => {
const groupLinksWithHiddenTimelinePaths = getLinksWithHiddenTimeline().map((l) => l.path);

const hiddenTimelineRoutes = isGroupedNavigationEnabled
? groupLinksWithHiddenTimelinePaths
: DEPRECATED_HIDDEN_TIMELINE_ROUTES;

return !!hiddenTimelineRoutes.find((route) => matchPath(currentPath, route));
};

export const useShowTimeline = () => {
const isGroupedNavigationEnabled = useIsGroupedNavigationEnabled();
const { pathname } = useLocation();
const [showTimeline, setShowTimeline] = useState(!isHiddenTimelinePath(pathname));

const [showTimeline, setShowTimeline] = useState(
!isTimelineHidden(pathname, isGroupedNavigationEnabled)
);

useEffect(() => {
setShowTimeline(!isHiddenTimelinePath(pathname));
}, [pathname]);
setShowTimeline(!isTimelineHidden(pathname, isGroupedNavigationEnabled));
}, [pathname, isGroupedNavigationEnabled]);

return [showTimeline];
};
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ describe('LoadPrebuiltRulesAndTemplatesButton', () => {
await waitFor(() => {
wrapper.update();

expect(
wrapper.find('[data-test-subj="load-prebuilt-rules"] button').props().disabled
).toEqual(true);
expect(wrapper.find('button[data-test-subj="load-prebuilt-rules"]').props().disabled).toEqual(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could we just use [data-test-subj="load-prebuilt-rules"] w/o button as a selector here?

true
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,11 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components';

import { getCreateRuleUrl } from '../../../../common/components/link_to/redirect_to_detection_engine';
import * as i18n from './translations';
import { LinkButton } from '../../../../common/components/links';
import { SecuritySolutionLinkButton } from '../../../../common/components/links';
import { SecurityPageName } from '../../../../app/types';
import { useFormatUrl } from '../../../../common/components/link_to';
import { usePrePackagedRules } from '../../../containers/detection_engine/rules';
import { useUserData } from '../../user_info';
import { useNavigateTo } from '../../../../common/lib/kibana/hooks';

const EmptyPrompt = styled(EuiEmptyPrompt)`
align-self: center; /* Corrects horizontal centering in IE11 */
Expand All @@ -38,16 +35,6 @@ const PrePackagedRulesPromptComponent: React.FC<PrePackagedRulesPromptProps> = (
const handlePreBuiltCreation = useCallback(() => {
createPrePackagedRules();
}, [createPrePackagedRules]);
const { formatUrl } = useFormatUrl(SecurityPageName.rules);
const { navigateTo } = useNavigateTo();

const goToCreateRule = useCallback(
(ev) => {
ev.preventDefault();
navigateTo({ deepLinkId: SecurityPageName.rules, path: getCreateRuleUrl() });
},
[navigateTo]
);

const [{ isSignalIndexExists, isAuthenticated, hasEncryptionKey, canUserCRUD, hasIndexWrite }] =
useUserData();
Expand Down Expand Up @@ -80,14 +67,13 @@ const PrePackagedRulesPromptComponent: React.FC<PrePackagedRulesPromptProps> = (
<EuiFlexGroup justifyContent="center">
<EuiFlexItem grow={false}>{loadPrebuiltRulesAndTemplatesButton}</EuiFlexItem>
<EuiFlexItem grow={false}>
<LinkButton
<SecuritySolutionLinkButton
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A small refactor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks so much cleaner! 🙌 Thank you!

isDisabled={!userHasPermissions}
onClick={goToCreateRule}
href={formatUrl(getCreateRuleUrl())}
iconType="plusInCircle"
deepLinkId={SecurityPageName.rulesCreate}
>
{i18n.CREATE_RULE_ACTION}
</LinkButton>
</SecuritySolutionLinkButton>
</EuiFlexItem>
</EuiFlexGroup>
}
Expand Down
Loading