[Endpoint] add policy data to Host list UI (#69202)
This commit is contained in:
parent
b8e6b54428
commit
1cef65e56f
|
@ -93,13 +93,40 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
|
|||
|
||||
const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseRoutePath);
|
||||
|
||||
const [policyDetailsRoutePath, policyDetailsRouteUrl] = useMemo(() => {
|
||||
return [
|
||||
getManagementUrl({
|
||||
name: 'policyDetails',
|
||||
policyId: details.endpoint.policy.applied.id,
|
||||
excludePrefix: true,
|
||||
}),
|
||||
getManagementUrl({
|
||||
name: 'policyDetails',
|
||||
policyId: details.endpoint.policy.applied.id,
|
||||
}),
|
||||
];
|
||||
}, [details.endpoint.policy.applied.id]);
|
||||
|
||||
const policyDetailsClickHandler = useNavigateByRouterEventHandler(policyDetailsRoutePath);
|
||||
|
||||
const detailsResultsPolicy = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.host.details.policy', {
|
||||
defaultMessage: 'Policy',
|
||||
}),
|
||||
description: details.endpoint.policy.applied.id,
|
||||
description: (
|
||||
<>
|
||||
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
|
||||
<EuiLink
|
||||
data-test-subj="policyDetailsValue"
|
||||
href={policyDetailsRouteUrl}
|
||||
onClick={policyDetailsClickHandler}
|
||||
>
|
||||
{details.endpoint.policy.applied.name}
|
||||
</EuiLink>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.translate('xpack.securitySolution.endpoint.host.details.policyStatus', {
|
||||
|
@ -128,7 +155,14 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
|
|||
),
|
||||
},
|
||||
];
|
||||
}, [details, policyResponseUri, policyStatus, policyStatusClickHandler]);
|
||||
}, [
|
||||
details,
|
||||
policyResponseUri,
|
||||
policyStatus,
|
||||
policyStatusClickHandler,
|
||||
policyDetailsRouteUrl,
|
||||
policyDetailsClickHandler,
|
||||
]);
|
||||
const detailsResultsLower = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { HostStatus, HostPolicyResponseActionStatus } from '../../../../../common/endpoint/types';
|
||||
|
||||
export const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze<
|
||||
|
@ -23,3 +24,17 @@ export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze<
|
|||
warning: 'warning',
|
||||
failure: 'danger',
|
||||
});
|
||||
|
||||
export const POLICY_STATUS_TO_TEXT = Object.freeze<
|
||||
{ [key in keyof typeof HostPolicyResponseActionStatus]: string }
|
||||
>({
|
||||
success: i18n.translate('xpack.securitySolution.policyStatusText.success', {
|
||||
defaultMessage: 'Success',
|
||||
}),
|
||||
warning: i18n.translate('xpack.securitySolution.policyStatusText.warning', {
|
||||
defaultMessage: 'Warning',
|
||||
}),
|
||||
failure: i18n.translate('xpack.securitySolution.policyStatusText.failure', {
|
||||
defaultMessage: 'Failure',
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as reactTestingLibrary from '@testing-library/react';
|
|||
|
||||
import { HostList } from './index';
|
||||
import { mockHostDetailsApiResult, mockHostResultList } from '../store/mock_host_result_list';
|
||||
import { mockPolicyResultList } from '../../policy/store/policy_list/mock_policy_result_list';
|
||||
import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint';
|
||||
import {
|
||||
HostInfo,
|
||||
|
@ -17,6 +18,7 @@ import {
|
|||
} from '../../../../../common/endpoint/types';
|
||||
import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data';
|
||||
import { AppAction } from '../../../../common/store/actions';
|
||||
import { POLICY_STATUS_TO_HEALTH_COLOR, POLICY_STATUS_TO_TEXT } from './host_constants';
|
||||
|
||||
describe('when on the hosts page', () => {
|
||||
const docGenerator = new EndpointDocGenerator();
|
||||
|
@ -47,15 +49,23 @@ describe('when on the hosts page', () => {
|
|||
});
|
||||
});
|
||||
describe('when list data loads', () => {
|
||||
const generatedPolicyStatuses: Array<
|
||||
HostInfo['metadata']['endpoint']['policy']['applied']['status']
|
||||
> = [];
|
||||
let firstPolicyID: string;
|
||||
beforeEach(() => {
|
||||
reactTestingLibrary.act(() => {
|
||||
const hostListData = mockHostResultList({ total: 3 });
|
||||
firstPolicyID = hostListData.hosts[0].metadata.endpoint.policy.applied.id;
|
||||
[HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE].forEach((status, index) => {
|
||||
hostListData.hosts[index] = {
|
||||
metadata: hostListData.hosts[index].metadata,
|
||||
host_status: status,
|
||||
};
|
||||
});
|
||||
hostListData.hosts.forEach((item, index) => {
|
||||
generatedPolicyStatuses[index] = item.metadata.endpoint.policy.applied.status;
|
||||
});
|
||||
const action: AppAction = {
|
||||
type: 'serverReturnedHostList',
|
||||
payload: hostListData,
|
||||
|
@ -92,6 +102,29 @@ describe('when on the hosts page', () => {
|
|||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display correct policy status', async () => {
|
||||
const renderResult = render();
|
||||
const policyStatuses = await renderResult.findAllByTestId('rowPolicyStatus');
|
||||
|
||||
policyStatuses.forEach((status, index) => {
|
||||
expect(status.textContent).toEqual(POLICY_STATUS_TO_TEXT[generatedPolicyStatuses[index]]);
|
||||
expect(
|
||||
status.querySelector(
|
||||
`[data-euiicon-type][color=${
|
||||
POLICY_STATUS_TO_HEALTH_COLOR[generatedPolicyStatuses[index]]
|
||||
}]`
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policy name as a link', async () => {
|
||||
const renderResult = render();
|
||||
const firstPolicyName = (await renderResult.findAllByTestId('policyNameCellLink'))[0];
|
||||
expect(firstPolicyName).not.toBeNull();
|
||||
expect(firstPolicyName.getAttribute('href')).toContain(`policy/${firstPolicyID}`);
|
||||
});
|
||||
|
||||
describe('when the user clicks the first hostname in the table', () => {
|
||||
let renderResult: reactTestingLibrary.RenderResult;
|
||||
beforeEach(async () => {
|
||||
|
@ -197,6 +230,32 @@ describe('when on the hosts page', () => {
|
|||
expect(flyout).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display policy name value as a link', async () => {
|
||||
const renderResult = render();
|
||||
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
|
||||
expect(policyDetailsLink).not.toBeNull();
|
||||
expect(policyDetailsLink.getAttribute('href')).toEqual(
|
||||
`#/management/policy/${hostDetails.metadata.endpoint.policy.applied.id}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should update the URL when policy name link is clicked', async () => {
|
||||
const policyItem = mockPolicyResultList({ total: 1 }).items[0];
|
||||
coreStart.http.get.mockReturnValue(Promise.resolve({ item: policyItem }));
|
||||
|
||||
const renderResult = render();
|
||||
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
reactTestingLibrary.act(() => {
|
||||
reactTestingLibrary.fireEvent.click(policyDetailsLink);
|
||||
});
|
||||
const changedUrlAction = await userChangedUrlChecker;
|
||||
expect(changedUrlAction.payload.pathname).toEqual(
|
||||
`/management/policy/${hostDetails.metadata.endpoint.policy.applied.id}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should display policy status value as a link', async () => {
|
||||
const renderResult = render();
|
||||
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
|
||||
|
@ -205,6 +264,7 @@ describe('when on the hosts page', () => {
|
|||
'#/management/endpoints?page_index=0&page_size=10&selected_host=1&show=policy_response'
|
||||
);
|
||||
});
|
||||
|
||||
it('should update the URL when policy status link is clicked', async () => {
|
||||
const renderResult = render();
|
||||
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
|
||||
|
@ -217,6 +277,7 @@ describe('when on the hosts page', () => {
|
|||
'?page_index=0&page_size=10&selected_host=1&show=policy_response'
|
||||
);
|
||||
});
|
||||
|
||||
it('should display Success overall policy status', async () => {
|
||||
const renderResult = render();
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -230,6 +291,7 @@ describe('when on the hosts page', () => {
|
|||
policyStatusHealth.querySelector('[data-euiicon-type][color="success"]')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display Warning overall policy status', async () => {
|
||||
const renderResult = render();
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -243,6 +305,7 @@ describe('when on the hosts page', () => {
|
|||
policyStatusHealth.querySelector('[data-euiicon-type][color="warning"]')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display Failed overall policy status', async () => {
|
||||
const renderResult = render();
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -256,6 +319,7 @@ describe('when on the hosts page', () => {
|
|||
policyStatusHealth.querySelector('[data-euiicon-type][color="danger"]')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should display Unknown overall policy status', async () => {
|
||||
const renderResult = render();
|
||||
reactTestingLibrary.act(() => {
|
||||
|
@ -269,6 +333,7 @@ describe('when on the hosts page', () => {
|
|||
policyStatusHealth.querySelector('[data-euiicon-type][color="subdued"]')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should include the link to logs', async () => {
|
||||
const renderResult = render();
|
||||
const linkToLogs = await renderResult.findByTestId('hostDetailsLinkToLogs');
|
||||
|
@ -278,6 +343,7 @@ describe('when on the hosts page', () => {
|
|||
"/app/logs/stream?logFilter=(expression:'host.id:1',kind:kuery)"
|
||||
);
|
||||
});
|
||||
|
||||
describe('when link to logs is clicked', () => {
|
||||
beforeEach(async () => {
|
||||
const renderResult = render();
|
||||
|
@ -291,6 +357,7 @@ describe('when on the hosts page', () => {
|
|||
expect(coreStart.application.navigateToApp.mock.calls).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when showing host Policy Response panel', () => {
|
||||
let renderResult: ReturnType<typeof render>;
|
||||
beforeEach(async () => {
|
||||
|
@ -305,10 +372,12 @@ describe('when on the hosts page', () => {
|
|||
dispatchServerReturnedHostPolicyResponse();
|
||||
});
|
||||
});
|
||||
|
||||
it('should hide the host details panel', async () => {
|
||||
const hostDetailsFlyout = await renderResult.queryByTestId('hostDetailsFlyoutBody');
|
||||
expect(hostDetailsFlyout).toBeNull();
|
||||
});
|
||||
|
||||
it('should display policy response sub-panel', async () => {
|
||||
expect(
|
||||
await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutHeader')
|
||||
|
@ -317,17 +386,20 @@ describe('when on the hosts page', () => {
|
|||
await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutBody')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should include the sub-panel title', async () => {
|
||||
expect(
|
||||
(await renderResult.findByTestId('hostDetailsPolicyResponseFlyoutTitle')).textContent
|
||||
).toBe('Policy Response');
|
||||
});
|
||||
|
||||
it('should show a configuration section for each protection', async () => {
|
||||
const configAccordions = await renderResult.findAllByTestId(
|
||||
'hostDetailsPolicyResponseConfigAccordion'
|
||||
);
|
||||
expect(configAccordions).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should show an actions section for each configuration', async () => {
|
||||
const actionAccordions = await renderResult.findAllByTestId(
|
||||
'hostDetailsPolicyResponseActionsAccordion'
|
||||
|
@ -340,6 +412,7 @@ describe('when on the hosts page', () => {
|
|||
expect(statusHealth).not.toBeNull();
|
||||
expect(message).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should not show any numbered badges if all actions are successful', () => {
|
||||
const policyResponse = docGenerator.generatePolicyResponse(
|
||||
new Date().getTime(),
|
||||
|
@ -359,6 +432,7 @@ describe('when on the hosts page', () => {
|
|||
expect(e).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show a numbered badge if at least one action failed', () => {
|
||||
reactTestingLibrary.act(() => {
|
||||
dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure);
|
||||
|
@ -368,6 +442,7 @@ describe('when on the hosts page', () => {
|
|||
);
|
||||
expect(attentionBadge).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should show a numbered badge if at least one action has a warning', () => {
|
||||
reactTestingLibrary.act(() => {
|
||||
dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning);
|
||||
|
@ -377,6 +452,7 @@ describe('when on the hosts page', () => {
|
|||
);
|
||||
expect(attentionBadge).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should include the back to details link', async () => {
|
||||
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
|
||||
expect(subHeaderBackLink.textContent).toBe('Endpoint Details');
|
||||
|
@ -384,6 +460,7 @@ describe('when on the hosts page', () => {
|
|||
'#/management/endpoints?page_index=0&page_size=10&selected_host=1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should update URL when back to details link is clicked', async () => {
|
||||
const subHeaderBackLink = await renderResult.findByTestId('flyoutSubHeaderBackButton');
|
||||
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
|
||||
|
|
|
@ -22,7 +22,11 @@ import { createStructuredSelector } from 'reselect';
|
|||
import { HostDetailsFlyout } from './details';
|
||||
import * as selectors from '../store/selectors';
|
||||
import { useHostSelector } from './hooks';
|
||||
import { HOST_STATUS_TO_HEALTH_COLOR } from './host_constants';
|
||||
import {
|
||||
HOST_STATUS_TO_HEALTH_COLOR,
|
||||
POLICY_STATUS_TO_HEALTH_COLOR,
|
||||
POLICY_STATUS_TO_TEXT,
|
||||
} from './host_constants';
|
||||
import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
|
||||
import { CreateStructuredSelector } from '../../../../common/store';
|
||||
import { Immutable, HostInfo } from '../../../../../common/endpoint/types';
|
||||
|
@ -31,17 +35,18 @@ import { ManagementPageView } from '../../../components/management_page_view';
|
|||
import { getManagementUrl } from '../../..';
|
||||
import { FormattedDate } from '../../../../common/components/formatted_date';
|
||||
|
||||
const HostLink = memo<{
|
||||
const HostListNavLink = memo<{
|
||||
name: string;
|
||||
href: string;
|
||||
route: string;
|
||||
}>(({ name, href, route }) => {
|
||||
dataTestSubj: string;
|
||||
}>(({ name, href, route, dataTestSubj }) => {
|
||||
const clickHandler = useNavigateByRouterEventHandler(route);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @elastic/eui/href-or-on-click
|
||||
<EuiLink
|
||||
data-test-subj="hostnameCellLink"
|
||||
data-test-subj={dataTestSubj}
|
||||
className="eui-textTruncate"
|
||||
href={href}
|
||||
onClick={clickHandler}
|
||||
|
@ -50,7 +55,7 @@ const HostLink = memo<{
|
|||
</EuiLink>
|
||||
);
|
||||
});
|
||||
HostLink.displayName = 'HostLink';
|
||||
HostListNavLink.displayName = 'HostListNavLink';
|
||||
|
||||
const selector = (createStructuredSelector as CreateStructuredSelector)(selectors);
|
||||
export const HostList = () => {
|
||||
|
@ -116,7 +121,14 @@ export const HostList = () => {
|
|||
name: 'endpointDetails',
|
||||
selected_host: id,
|
||||
});
|
||||
return <HostLink name={hostname} href={toRouteUrl} route={toRoutePath} />;
|
||||
return (
|
||||
<HostListNavLink
|
||||
name={hostname}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="hostnameCellLink"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -142,41 +154,62 @@ export const HostList = () => {
|
|||
},
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
field: 'metadata.endpoint.policy.applied',
|
||||
name: i18n.translate('xpack.securitySolution.endpointList.policy', {
|
||||
defaultMessage: 'Policy',
|
||||
}),
|
||||
truncateText: true,
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: () => {
|
||||
return <span className="eui-textTruncate">{'Policy Name'}</span>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
name: i18n.translate('xpack.securitySolution.endpointList.policyStatus', {
|
||||
defaultMessage: 'Policy Status',
|
||||
}),
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: () => {
|
||||
render: (policy: HostInfo['metadata']['endpoint']['policy']['applied']) => {
|
||||
const toRoutePath = getManagementUrl({
|
||||
name: 'policyDetails',
|
||||
policyId: policy.id,
|
||||
excludePrefix: true,
|
||||
});
|
||||
const toRouteUrl = getManagementUrl({
|
||||
name: 'policyDetails',
|
||||
policyId: policy.id,
|
||||
});
|
||||
return (
|
||||
<EuiHealth color="success" className="eui-textTruncate">
|
||||
<FormattedMessage
|
||||
id="xpack.securitySolution.endpointList.policyStatus"
|
||||
defaultMessage="Policy Status"
|
||||
/>
|
||||
</EuiHealth>
|
||||
<HostListNavLink
|
||||
name={policy.name}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="policyNameCellLink"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: '',
|
||||
name: i18n.translate('xpack.securitySolution.endpointList.alerts', {
|
||||
defaultMessage: 'Alerts',
|
||||
field: 'metadata.endpoint.policy.applied',
|
||||
name: i18n.translate('xpack.securitySolution.endpointList.policyStatus', {
|
||||
defaultMessage: 'Policy Status',
|
||||
}),
|
||||
dataType: 'number',
|
||||
render: () => {
|
||||
return '0';
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (policy: HostInfo['metadata']['endpoint']['policy']['applied'], item: HostInfo) => {
|
||||
const toRoutePath = getManagementUrl({
|
||||
name: 'endpointPolicyResponse',
|
||||
selected_host: item.metadata.host.id,
|
||||
excludePrefix: true,
|
||||
});
|
||||
const toRouteUrl = getManagementUrl({
|
||||
name: 'endpointPolicyResponse',
|
||||
selected_host: item.metadata.host.id,
|
||||
});
|
||||
return (
|
||||
<EuiHealth
|
||||
color={POLICY_STATUS_TO_HEALTH_COLOR[policy.status]}
|
||||
className="eui-textTruncate"
|
||||
data-test-subj="rowPolicyStatus"
|
||||
>
|
||||
<HostListNavLink
|
||||
name={POLICY_STATUS_TO_TEXT[policy.status]}
|
||||
href={toRouteUrl}
|
||||
route={toRoutePath}
|
||||
dataTestSubj="policyStatusCellLink"
|
||||
/>
|
||||
</EuiHealth>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue