From e2a288edbaad453a48c7b2f8893c4d43238a244c Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Tue, 16 Feb 2021 21:34:48 -0500 Subject: [PATCH] [Security Solution][Endpoint][Admin] Endpoint Details UX Enhancements (#90870) --- .../pages/endpoint_hosts/store/reducer.ts | 2 + .../pages/endpoint_hosts/store/selectors.ts | 11 +++ .../management/pages/endpoint_hosts/types.ts | 4 + .../view/details/endpoint_details.tsx | 99 +++++++++++++------ .../endpoint_hosts/view/details/index.tsx | 28 +++++- .../view/details/policy_response.tsx | 2 +- .../endpoint_hosts/view/host_constants.ts | 9 ++ .../pages/endpoint_hosts/view/index.test.tsx | 48 ++++----- 8 files changed, 137 insertions(+), 66 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 0998809425665..4547ae3b34243 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -43,6 +43,7 @@ export const initialEndpointListState: Immutable = { endpointsTotalError: undefined, queryStrategyVersion: undefined, policyVersionInfo: undefined, + hostStatus: undefined, }; /* eslint-disable-next-line complexity */ @@ -109,6 +110,7 @@ export const endpointListReducer: ImmutableReducer = ( ...state, details: action.payload.metadata, policyVersionInfo: action.payload.policy_info, + hostStatus: action.payload.host_status, detailsLoading: false, detailsError: undefined, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 313ef3ed403b2..17ce24e7cda7f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -16,6 +16,7 @@ import { HostPolicyResponseConfiguration, HostPolicyResponseActionStatus, MetadataQueryStrategyVersions, + HostStatus, } from '../../../../../common/endpoint/types'; import { EndpointState, EndpointIndexUIQueryParams } from '../types'; import { extractListPaginationParams } from '../../../common/routing'; @@ -224,6 +225,16 @@ export const showView: (state: EndpointState) => 'policy_response' | 'details' = } ); +/** + * Returns the Host Status which is connected the fleet agent + */ +export const hostStatusInfo: (state: Immutable) => HostStatus = createSelector( + (state) => state.hostStatus, + (hostStatus) => { + return hostStatus ? hostStatus : HostStatus.ERROR; + } +); + /** * Returns the Policy Response overall status */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 104c17d332bd3..7e989276edeb6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -13,6 +13,7 @@ import { AppLocation, PolicyData, MetadataQueryStrategyVersions, + HostStatus, } from '../../../../common/endpoint/types'; import { ServerApiError } from '../../../common/types'; import { GetPackagesResponse } from '../../../../../fleet/common'; @@ -79,6 +80,9 @@ export interface EndpointState { queryStrategyVersion?: MetadataQueryStrategyVersions; /** The policy IDs and revision number of the corresponding agent, and endpoint. May be more recent than what's running */ policyVersionInfo?: HostInfo['policy_info']; + /** The status of the host, which is mapped to the Elastic Agent status in Fleet + */ + hostStatus?: HostStatus; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx index ce16391206ec1..eb3e534ba427f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_details.tsx @@ -10,23 +10,27 @@ import { EuiDescriptionList, EuiHealth, EuiHorizontalRule, - EuiLink, EuiListGroup, EuiListGroupItem, EuiIcon, EuiText, EuiFlexGroup, EuiFlexItem, + EuiBadge, } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { isPolicyOutOfDate } from '../../utils'; -import { HostInfo, HostMetadata } from '../../../../../../common/endpoint/types'; +import { HostInfo, HostMetadata, HostStatus } from '../../../../../../common/endpoint/types'; import { useEndpointSelector, useAgentDetailsIngestUrl } from '../hooks'; import { useNavigateToAppEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; import { policyResponseStatus, uiQueryParams } from '../../store/selectors'; -import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants'; +import { + POLICY_STATUS_TO_HEALTH_COLOR, + POLICY_STATUS_TO_BADGE_COLOR, + HOST_STATUS_TO_HEALTH_COLOR, +} from '../host_constants'; import { FormattedDateAndTime } from '../../../../../common/components/endpoint/formatted_date_time'; import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { LinkToApp } from '../../../../../common/components/endpoint/link_to_app'; @@ -48,6 +52,7 @@ const LinkToExternalApp = styled.div` margin-top: ${(props) => props.theme.eui.ruleMargins.marginMedium}; .linkToAppIcon { margin-right: ${(props) => props.theme.eui.ruleMargins.marginXSmall}; + vertical-align: top; } .linkToAppPopoutIcon { margin-left: ${(props) => props.theme.eui.ruleMargins.marginXSmall}; @@ -57,7 +62,15 @@ const LinkToExternalApp = styled.div` const openReassignFlyoutSearch = '?openReassignFlyout=true'; export const EndpointDetails = memo( - ({ details, policyInfo }: { details: HostMetadata; policyInfo?: HostInfo['policy_info'] }) => { + ({ + details, + policyInfo, + hostStatus, + }: { + details: HostMetadata; + policyInfo?: HostInfo['policy_info']; + hostStatus: HostStatus; + }) => { const agentId = details.elastic.agent.id; const { url: agentDetailsUrl, @@ -78,6 +91,25 @@ export const EndpointDetails = memo( }), description: details.host.os.full, }, + { + title: i18n.translate('xpack.securitySolution.endpoint.details.agentStatus', { + defaultMessage: 'Agent Status', + }), + description: ( + + + + + + ), + }, { title: i18n.translate('xpack.securitySolution.endpoint.details.lastSeen', { defaultMessage: 'Last Seen', @@ -85,7 +117,7 @@ export const EndpointDetails = memo( description: , }, ]; - }, [details]); + }, [details, hostStatus]); const [policyResponseUri, policyResponseRoutePath] = useMemo(() => { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -135,13 +167,15 @@ export const EndpointDetails = memo( defaultMessage: 'Integration Policy', }), description: ( - <> - - {details.Endpoint.policy.applied.name} - + + + + {details.Endpoint.policy.applied.name} + + {details.Endpoint.policy.applied.endpoint_policy_version && ( @@ -167,7 +201,7 @@ export const EndpointDetails = memo( )} - + ), }, { @@ -175,25 +209,26 @@ export const EndpointDetails = memo( defaultMessage: 'Policy Response', }), description: ( - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - - - - - - + + + + ), }, ]; @@ -248,7 +283,7 @@ export const EndpointDetails = memo( onClick={handleReassignEndpointsClick} data-test-subj="endpointDetailsLinkToIngest" > - + { } = queryParams; const details = useEndpointSelector(detailsData); const policyInfo = useEndpointSelector(policyVersionInfo); + const hostStatus = useEndpointSelector(hostStatusInfo); const loading = useEndpointSelector(detailsLoading); const error = useEndpointSelector(detailsError); const show = useEndpointSelector(showView); @@ -83,7 +86,7 @@ export const EndpointDetailsFlyout = memo(() => { onClose={handleFlyoutClose} style={{ zIndex: 4001 }} data-test-subj="endpointDetailsFlyout" - size="s" + size="m" > {loading ? ( @@ -112,7 +115,11 @@ export const EndpointDetailsFlyout = memo(() => { {show === 'details' && ( <> - + )} @@ -125,6 +132,14 @@ export const EndpointDetailsFlyout = memo(() => { EndpointDetailsFlyout.displayName = 'EndpointDetailsFlyout'; +const PolicyResponseFlyout = styled.div` + .endpointDetailsPolicyResponseFlyoutBody { + .euiFlyoutBody__overflowContent { + padding-top: 0; + } + } +`; + const PolicyResponseFlyoutPanel = memo<{ hostMeta: HostMetadata; }>(({ hostMeta }) => { @@ -165,12 +180,15 @@ const PolicyResponseFlyoutPanel = memo<{ }, [backToDetailsClickHandler, detailsUri]); return ( - <> + - +

)} - + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx index 5c883e9affe1d..b6c6be673da60 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/policy_response.tsx @@ -29,7 +29,7 @@ import { */ const PolicyResponseConfigAccordion = styled(EuiAccordion)` .euiAccordion__triggerWrapper { - padding: ${(props) => props.theme.eui.paddingSizes.s}; + padding: ${(props) => props.theme.eui.paddingSizes.xs}; } &.euiAccordion-isOpen { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts index 4745cd9de249d..71f6d78caea7e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/host_constants.ts @@ -28,6 +28,15 @@ export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze< unsupported: 'subdued', }); +export const POLICY_STATUS_TO_BADGE_COLOR = Object.freeze< + { [key in keyof typeof HostPolicyResponseActionStatus]: string } +>({ + success: 'secondary', + warning: 'warning', + failure: 'danger', + unsupported: 'default', +}); + export const POLICY_STATUS_TO_TEXT = Object.freeze< { [key in keyof typeof HostPolicyResponseActionStatus]: string } >({ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index 99c313220e068..9925b35616c91 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -645,49 +645,41 @@ describe('when on the list page', () => { it('should display Success overall policy status', async () => { const renderResult = await renderAndWaitForData(); - const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); - expect(policyStatusLink.textContent).toEqual('Success'); - - const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); - expect( - policyStatusHealth.querySelector('[data-euiicon-type][color="success"]') - ).not.toBeNull(); + const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusBadge.textContent).toEqual('Success'); + expect(policyStatusBadge.getAttribute('style')).toMatch( + /background-color\: rgb\(109\, 204\, 177\)\;/ + ); }); it('should display Warning overall policy status', async () => { mockEndpointListApi(createPolicyResponse(HostPolicyResponseActionStatus.warning)); const renderResult = await renderAndWaitForData(); - const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); - expect(policyStatusLink.textContent).toEqual('Warning'); - - const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); - expect( - policyStatusHealth.querySelector('[data-euiicon-type][color="warning"]') - ).not.toBeNull(); + const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusBadge.textContent).toEqual('Warning'); + expect(policyStatusBadge.getAttribute('style')).toMatch( + /background-color\: rgb\(241\, 216\, 111\)\;/ + ); }); it('should display Failed overall policy status', async () => { mockEndpointListApi(createPolicyResponse(HostPolicyResponseActionStatus.failure)); const renderResult = await renderAndWaitForData(); - const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); - expect(policyStatusLink.textContent).toEqual('Failed'); - - const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); - expect( - policyStatusHealth.querySelector('[data-euiicon-type][color="danger"]') - ).not.toBeNull(); + const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusBadge.textContent).toEqual('Failed'); + expect(policyStatusBadge.getAttribute('style')).toMatch( + /background-color\: rgb\(255\, 126\, 98\)\;/ + ); }); it('should display Unknown overall policy status', async () => { mockEndpointListApi(createPolicyResponse('' as HostPolicyResponseActionStatus)); const renderResult = await renderAndWaitForData(); - const policyStatusLink = await renderResult.findByTestId('policyStatusValue'); - expect(policyStatusLink.textContent).toEqual('Unknown'); - - const policyStatusHealth = await renderResult.findByTestId('policyStatusHealth'); - expect( - policyStatusHealth.querySelector('[data-euiicon-type][color="subdued"]') - ).not.toBeNull(); + const policyStatusBadge = await renderResult.findByTestId('policyStatusValue'); + expect(policyStatusBadge.textContent).toEqual('Unknown'); + expect(policyStatusBadge.getAttribute('style')).toMatch( + /background-color\: rgb\(211\, 218\, 230\)\;/ + ); }); it('should include the link to reassignment in Ingest', async () => {