Skip to content

Commit

Permalink
[Security Solution][Endpoint][Policy] Empty page for policy list (#12…
Browse files Browse the repository at this point in the history
  • Loading branch information
parkiino authored Mar 29, 2022
1 parent fa34ec5 commit 0da6141
Show file tree
Hide file tree
Showing 13 changed files with 488 additions and 211 deletions.
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export enum SecurityPageName {
networkHttp = 'network-http',
networkTls = 'network-tls',
overview = 'overview',
policies = 'policies',
policies = 'policy',
rules = 'rules',
timelines = 'timelines',
timelinesTemplates = 'timelines-templates',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
import { getPolicyDetailPath } from '../common/routing';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { useAppUrl } from '../../common/lib/kibana/hooks';
import { PolicyDetailsRouteState } from '../../../common/endpoint/types';

/**
* A policy link (to details) that first checks to see if the policy id exists against
Expand All @@ -20,17 +21,18 @@ export const EndpointPolicyLink = memo<
Omit<EuiLinkAnchorProps, 'href'> & {
policyId: string;
missingPolicies?: Record<string, boolean>;
backLink?: PolicyDetailsRouteState['backLink'];
}
>(({ policyId, children, onClick, missingPolicies = {}, ...otherProps }) => {
>(({ policyId, backLink, children, missingPolicies = {}, ...otherProps }) => {
const { getAppUrl } = useAppUrl();
const { toRoutePath, toRouteUrl } = useMemo(() => {
const path = getPolicyDetailPath(policyId);
return {
toRoutePath: path,
toRoutePath: backLink ? { pathname: path, state: { backLink } } : path,
toRouteUrl: getAppUrl({ path }),
};
}, [policyId, getAppUrl]);
const clickHandler = useNavigateByRouterEventHandler(toRoutePath, onClick);
}, [policyId, getAppUrl, backLink]);
const clickHandler = useNavigateByRouterEventHandler(toRoutePath);

if (missingPolicies[policyId]) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const PolicyEmptyState = React.memo<{
loading: boolean;
onActionClick: (event: MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => void;
actionDisabled?: boolean;
}>(({ loading, onActionClick, actionDisabled }) => {
policyEntryPoint?: boolean;
}>(({ loading, onActionClick, actionDisabled, policyEntryPoint = false }) => {
const docLinks = useKibana().services.docLinks;
return (
<div data-test-subj="emptyPolicyTable">
Expand Down Expand Up @@ -74,10 +75,17 @@ const PolicyEmptyState = React.memo<{
</EuiText>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo"
defaultMessage="From this page, you’ll be able to view and manage the hosts in your environment running Endpoint Security."
/>
{policyEntryPoint ? (
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromPolicyPage"
defaultMessage="From this page, you’ll be able to view and manage the Endpoint Security Integration policies in your environment running Endpoint Security."
/>
) : (
<FormattedMessage
id="xpack.securitySolution.endpoint.policyList.onboardingSectionTwo.fromEndpointPage"
defaultMessage="From this page, you’ll be able to view and manage the hosts in your environment running Endpoint Security."
/>
)}
</EuiText>
<EuiSpacer size="m" />
<EuiText size="s" color="subdued">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export const EndpointList = () => {
metadataTransformStats,
} = useEndpointSelector(selector);
const { search } = useFormatUrl(SecurityPageName.administration);
const { search: searchParams } = useLocation();
const { getAppUrl } = useAppUrl();
const dispatch = useDispatch<(a: EndpointAction) => void>();
// cap ability to page at 10k records. (max_result_window)
Expand All @@ -150,6 +151,7 @@ export const EndpointList = () => {
};
}

// default back button is to the policy list
const policyListPath = getPoliciesPath();

return {
Expand Down Expand Up @@ -241,6 +243,23 @@ export const EndpointList = () => {
}
);

const backToEndpointList: PolicyDetailsRouteState['backLink'] = useMemo(() => {
const endpointListPath = getEndpointListPath({ name: 'endpointList' }, searchParams);

return {
navigateTo: [
APP_UI_ID,
{
path: endpointListPath,
},
],
label: i18n.translate('xpack.securitySolution.endpoint.policy.details.backToListTitle', {
defaultMessage: 'View all endpoints',
}),
href: getAppUrl({ path: endpointListPath }),
};
}, [getAppUrl, searchParams]);

const onRefresh = useCallback(() => {
dispatch({
type: 'appRequestedEndpointList',
Expand Down Expand Up @@ -362,6 +381,7 @@ export const EndpointList = () => {
policyId={policy.id}
className="eui-textTruncate"
data-test-subj="policyNameCellLink"
backLink={backToEndpointList}
>
{policy.name}
</EndpointPolicyLink>
Expand Down Expand Up @@ -504,7 +524,7 @@ export const EndpointList = () => {
],
},
];
}, [queryParams, search, getAppUrl, PAD_LEFT]);
}, [queryParams, search, getAppUrl, backToEndpointList, PAD_LEFT]);

const renderTableOrEmptyState = useMemo(() => {
if (endpointsExist || areEndpointsEnrolling) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,26 @@
*/

import React, { memo, useMemo } from 'react';
import { EuiLink, EuiLinkAnchorProps } from '@elastic/eui';
import { EuiLink, EuiLinkAnchorProps, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useLocation } from 'react-router-dom';
import { useNavigateByRouterEventHandler } from '../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { useAppUrl } from '../../common/lib/kibana/hooks';
import { getEndpointListPath, getPoliciesPath } from '../common/routing';
import { APP_UI_ID } from '../../../common/constants';
import { useAppUrl } from '../../../../../common/lib/kibana';
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
import { getEndpointListPath, getPoliciesPath } from '../../../../common/routing';
import { APP_UI_ID } from '../../../../../../common/constants';

/**
* @param policyId
* @param nonLinkCondition: boolean where the returned component is just text and not a link
*
* Returns a link component that navigates to the endpoint list page filtered by a specific policy
*/
export const PolicyEndpointListLink = memo<
export const PolicyEndpointCount = memo<
Omit<EuiLinkAnchorProps, 'href'> & {
policyId: string;
nonLinkCondition: boolean;
}
>(({ policyId, children, ...otherProps }) => {
>(({ policyId, nonLinkCondition, children, ...otherProps }) => {
const filterByPolicyQuery = `(language:kuery,query:'united.endpoint.Endpoint.policy.applied.id : "${policyId}"')`;
const { search } = useLocation();
const { getAppUrl } = useAppUrl();
Expand Down Expand Up @@ -54,6 +58,9 @@ export const PolicyEndpointListLink = memo<
}, [getAppUrl, filterByPolicyQuery, search]);
const clickHandler = useNavigateByRouterEventHandler(toRoutePathWithBackOptions);

if (nonLinkCondition) {
return <EuiText size="s">{children}</EuiText>;
}
return (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink href={toRouteUrl} onClick={clickHandler} {...otherProps}>
Expand All @@ -62,4 +69,4 @@ export const PolicyEndpointListLink = memo<
);
});

PolicyEndpointListLink.displayName = 'PolicyEndpointListLink';
PolicyEndpointCount.displayName = 'PolicyEndpointCount';
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,25 @@ import { AGENT_API_ROUTES, PACKAGE_POLICY_API_ROOT } from '../../../../../../fle
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
import { useUserPrivileges } from '../../../../common/components/user_privileges';
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
import { getEndpointListPath, getPolicyDetailPath } from '../../../common/routing';
import { getEndpointListPath, getPoliciesPath, getPolicyDetailPath } from '../../../common/routing';
import { getHostIsolationExceptionItems } from '../../host_isolation_exceptions/service';
import { policyListApiPathHandlers } from '../store/test_mock_utils';
import { PolicyDetails } from './policy_details';
import { APP_UI_ID } from '../../../../../common/constants';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';

jest.mock('./policy_forms/components/policy_form_layout');
jest.mock('../../../../common/components/user_privileges');
jest.mock('../../host_isolation_exceptions/service');
jest.mock('../../../../common/hooks/use_experimental_features');

const useUserPrivilegesMock = useUserPrivileges as jest.Mock;
const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock;
const useIsExperimentalFeatureMock = useIsExperimentalFeatureEnabled as jest.Mock;

describe('Policy Details', () => {
const policyDetailsPathUrl = getPolicyDetailPath('1');
const endpointListPath = getEndpointListPath({ name: 'endpointList' });
const policyListPath = getPoliciesPath();
const sleep = (ms = 100) => new Promise((wakeup) => setTimeout(wakeup, ms));
const generator = new EndpointDocGenerator();
let history: AppContextTestRender['history'];
Expand All @@ -51,6 +55,9 @@ describe('Policy Details', () => {
let releaseApiFailure: () => void;

beforeEach(() => {
useIsExperimentalFeatureMock.mockReturnValue({
policyListEnabled: true,
});
http.get.mockImplementation(async () => {
await new Promise((_, reject) => {
releaseApiFailure = reject.bind(null, new Error('policy not found'));
Expand Down Expand Up @@ -125,14 +132,14 @@ describe('Policy Details', () => {
expect(policyView.find('flyoutOverlay')).toHaveLength(0);
});

it('should display back to list button and policy title', async () => {
it('should display back to policy list button and policy title', async () => {
policyView = render();
await asyncActions;
policyView.update();

const backToListLink = policyView.find('BackToExternalAppButton');
expect(backToListLink.prop('backButtonUrl')).toBe(`/app/security${endpointListPath}`);
expect(backToListLink.text()).toBe('View all endpoints');
expect(backToListLink.prop('backButtonUrl')).toBe(`/app/security${policyListPath}`);
expect(backToListLink.text()).toBe('Back to policy list');

const pageTitle = policyView.find('span[data-test-subj="header-page-title"]');
expect(pageTitle).toHaveLength(1);
Expand All @@ -147,7 +154,31 @@ describe('Policy Details', () => {
const backToListLink = policyView.find('a[data-test-subj="policyDetailsBackLink"]');
expect(history.location.pathname).toEqual(policyDetailsPathUrl);
backToListLink.simulate('click', { button: 0 });
expect(history.location.pathname).toEqual(endpointListPath);
expect(history.location.pathname).toEqual(policyListPath);
});

it('should display and navigate to custom back button if non-default backLink state is present', async () => {
const customBackLinkState = {
backLink: {
navigateTo: [
APP_UI_ID,
{
path: getEndpointListPath({ name: 'endpointList' }),
},
],
label: 'View all endpoints',
href: '/app/security/administration/endpoints',
},
};

history.push({ pathname: policyDetailsPathUrl, state: customBackLinkState });
policyView = render();
await asyncActions;
policyView.update();

const backToListLink = policyView.find('BackToExternalAppButton');
expect(backToListLink.prop('backButtonUrl')).toBe(`/app/security/administration/endpoints`);
expect(backToListLink.text()).toBe('View all endpoints');
});

it('should display agent stats', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ import {
BackToExternalAppButtonProps,
} from '../../../components/back_to_external_app_button/back_to_external_app_button';
import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types';
import { getEndpointListPath } from '../../../common/routing';
import { getEndpointListPath, getPoliciesPath } from '../../../common/routing';
import { useAppUrl } from '../../../../common/lib/kibana';
import { APP_UI_ID } from '../../../../../common/constants';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';

export const PolicyDetails = React.memo(() => {
const isPolicyListEnabled = useIsExperimentalFeatureEnabled('policyListEnabled');
const { state: routeState = {} } = useLocation<PolicyDetailsRouteState>();
const { getAppUrl } = useAppUrl();

Expand All @@ -45,24 +47,38 @@ export const PolicyDetails = React.memo(() => {
};
}

const endpointListPath = getEndpointListPath({ name: 'endpointList' });

return {
backButtonLabel: i18n.translate(
'xpack.securitySolution.endpoint.policy.details.backToListTitle',
{
if (isPolicyListEnabled) {
// default is to go back to the policy list
const policyListPath = getPoliciesPath();
return {
backButtonLabel: i18n.translate('xpack.securitySolution.policyDetails.backToPolicyButton', {
defaultMessage: 'Back to policy list',
}),
backButtonUrl: getAppUrl({ path: policyListPath }),
onBackButtonNavigateTo: [
APP_UI_ID,
{
path: policyListPath,
},
],
};
} else {
// remove else block once policy list is not hidden behind feature flag
const endpointListPath = getEndpointListPath({ name: 'endpointList' });
return {
backButtonLabel: i18n.translate('xpack.securitySolution.policyDetails.backToEndpointList', {
defaultMessage: 'View all endpoints',
}
),
backButtonUrl: getAppUrl({ path: endpointListPath }),
onBackButtonNavigateTo: [
APP_UI_ID,
{
path: endpointListPath,
},
],
};
}, [getAppUrl, routeState?.backLink]);
}),
backButtonUrl: getAppUrl({ path: endpointListPath }),
onBackButtonNavigateTo: [
APP_UI_ID,
{
path: endpointListPath,
},
],
};
}
}, [getAppUrl, routeState?.backLink, isPolicyListEnabled]);

const headerRightContent = (
<AgentsSummary
Expand Down
Loading

0 comments on commit 0da6141

Please sign in to comment.