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

[Endpoint] add policy data to Host list UI #69202

Merged
merged 7 commits into from
Jun 17, 2020
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
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down Expand Up @@ -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 [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand All @@ -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',
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -197,6 +230,32 @@ describe('when on the hosts page', () => {
expect(flyout).not.toBeNull();
});
});

it('should display policy name value as a link', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

spacing between the ITs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

spaces added for readability

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');
Expand All @@ -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');
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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(() => {
Expand All @@ -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');
Expand All @@ -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();
Expand All @@ -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 () => {
Expand All @@ -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')
Expand All @@ -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'
Expand All @@ -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(),
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -377,13 +452,15 @@ 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');
expect(subHeaderBackLink.getAttribute('href')).toBe(
'#/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');
Expand Down
Loading