[endpoint] connect policy response ui to api (#66093)

[Endpoint] Connect policy response UI to API
This commit is contained in:
Candace Park 2020-05-12 10:08:15 -04:00 committed by GitHub
parent 0124977830
commit 45790d035b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 150 additions and 67 deletions

View file

@ -32,9 +32,15 @@ interface ServerReturnedHostPolicyResponse {
payload: GetHostPolicyResponse;
}
interface ServerFailedToReturnHostPolicyResponse {
type: 'serverFailedToReturnHostPolicyResponse';
payload: ServerApiError;
}
export type HostAction =
| ServerReturnedHostList
| ServerFailedToReturnHostList
| ServerReturnedHostDetails
| ServerFailedToReturnHostDetails
| ServerReturnedHostPolicyResponse;
| ServerReturnedHostPolicyResponse
| ServerFailedToReturnHostPolicyResponse;

View file

@ -41,6 +41,9 @@ describe('HostList store concerns', () => {
details: undefined,
detailsLoading: false,
detailsError: undefined,
policyResponse: undefined,
policyResponseLoading: false,
policyResponseError: undefined,
location: undefined,
});
});

View file

@ -4,11 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostResultList, HostPolicyResponseActionStatus } from '../../../../../common/types';
import { HostResultList } from '../../../../../common/types';
import { isOnHostPage, hasSelectedHost, uiQueryParams, listData } from './selectors';
import { HostState } from '../../types';
import { ImmutableMiddlewareFactory } from '../../types';
import { HostPolicyResponse } from '../../../../../common/types';
export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = coreStart => {
return ({ getState, dispatch }) => next => async action => {
@ -70,50 +69,28 @@ export const hostMiddlewareFactory: ImmutableMiddlewareFactory<HostState> = core
type: 'serverReturnedHostDetails',
payload: response,
});
dispatch({
type: 'serverReturnedHostPolicyResponse',
payload: {
policy_response: ({
endpoint: {
policy: {
applied: {
version: '1.0.0',
status: HostPolicyResponseActionStatus.success,
id: '17d4b81d-9940-4b64-9de5-3e03ef1fb5cf',
actions: {
download_model: {
status: 'success',
message: 'Model downloaded',
},
ingest_events_config: {
status: 'failure',
message: 'No action taken',
},
},
response: {
configurations: {
malware: {
status: 'success',
concerned_actions: ['download_model'],
},
events: {
status: 'failure',
concerned_actions: ['ingest_events_config'],
},
},
},
},
},
},
} as unknown) as HostPolicyResponse, // Temporary until we get API
},
});
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostDetails',
payload: error,
});
}
// call the policy response api
try {
const policyResponse = await coreStart.http.get(`/api/endpoint/policy_response`, {
query: { hostId: selectedHost },
});
dispatch({
type: 'serverReturnedHostPolicyResponse',
payload: policyResponse,
});
} catch (error) {
dispatch({
type: 'serverFailedToReturnHostPolicyResponse',
payload: error,
});
}
}
};
};

View file

@ -21,6 +21,8 @@ const initialState = (): HostState => {
detailsLoading: false,
detailsError: undefined,
policyResponse: undefined,
policyResponseLoading: false,
policyResponseError: undefined,
location: undefined,
};
};
@ -68,6 +70,14 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
return {
...state,
policyResponse: action.payload.policy_response,
policyResponseLoading: false,
policyResponseError: undefined,
};
} else if (action.type === 'serverFailedToReturnHostPolicyResponse') {
return {
...state,
policyResponseError: action.payload,
policyResponseLoading: false,
};
} else if (action.type === 'userChangedUrl') {
const newState: Immutable<HostState> = {
@ -97,8 +107,10 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
...state,
location: action.payload,
detailsLoading: true,
policyResponseLoading: true,
error: undefined,
detailsError: undefined,
policyResponseError: undefined,
};
} else {
// if previous page was not host list or host details, load both list and details
@ -107,8 +119,10 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
location: action.payload,
loading: true,
detailsLoading: true,
policyResponseLoading: true,
error: undefined,
detailsError: undefined,
policyResponseError: undefined,
};
}
}
@ -118,6 +132,7 @@ export const hostListReducer: ImmutableReducer<HostState, AppAction> = (
location: action.payload,
error: undefined,
detailsError: undefined,
policyResponseError: undefined,
};
}
return state;

View file

@ -88,6 +88,11 @@ export const policyResponseActions: (
}
);
export const policyResponseLoading = (state: Immutable<HostState>): boolean =>
state.policyResponseLoading;
export const policyResponseError = (state: Immutable<HostState>) => state.policyResponseError;
export const isOnHostPage = (state: Immutable<HostState>) =>
state.location ? state.location.pathname === '/hosts' : false;

View file

@ -110,6 +110,10 @@ export interface HostState {
detailsError?: ServerApiError;
/** Holds the Policy Response for the Host currently being displayed in the details */
policyResponse?: HostPolicyResponse;
/** policyResponse is being retrieved */
policyResponseLoading: boolean;
/** api error from retrieving the policy response */
policyResponseError?: ServerApiError;
/** current location info */
location?: Immutable<EndpointAppLocation>;
}

View file

@ -23,7 +23,7 @@ import { useHostSelector, useHostLogsUrl } from '../hooks';
import { urlFromQueryParams } from '../url_from_query_params';
import { policyResponseStatus, uiQueryParams } from '../../../store/hosts/selectors';
import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler';
import { POLICY_STATUS_TO_HEALTH_COLOR } from './host_constants';
import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants';
const HostIds = styled(EuiListGroupItem)`
margin-top: 0;

View file

@ -13,6 +13,7 @@ import {
EuiTitle,
EuiText,
EuiSpacer,
EuiEmptyPrompt,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n/react';
@ -29,6 +30,8 @@ import {
policyResponseConfigurations,
policyResponseActions,
policyResponseFailedOrWarningActionCount,
policyResponseError,
policyResponseLoading,
} from '../../../store/hosts/selectors';
import { HostDetails } from './host_details';
import { PolicyResponse } from './policy_response';
@ -108,6 +111,8 @@ const PolicyResponseFlyoutPanel = memo<{
const responseConfig = useHostSelector(policyResponseConfigurations);
const responseActionStatus = useHostSelector(policyResponseActions);
const responseAttentionCount = useHostSelector(policyResponseFailedOrWarningActionCount);
const loading = useHostSelector(policyResponseLoading);
const error = useHostSelector(policyResponseError);
const detailsUri = useMemo(
() =>
urlFromQueryParams({
@ -142,17 +147,24 @@ const PolicyResponseFlyoutPanel = memo<{
/>
</h4>
</EuiText>
{responseConfig !== undefined && responseActionStatus !== undefined ? (
{error && (
<EuiEmptyPrompt
title={
<FormattedMessage
id="xpack.endpoint.hostDetails.noPolicyResponse"
defaultMessage="No policy response available"
/>
}
/>
)}
{loading && <EuiLoadingContent lines={3} />}
{responseConfig !== undefined && responseActionStatus !== undefined && (
<PolicyResponse
responseConfig={responseConfig}
responseActionStatus={responseActionStatus}
responseAttentionCount={responseAttentionCount}
/>
) : (
<FormattedMessage
id="xpack.endpoint.hostDetails.noPolicyResponse"
defaultMessage="No Policy Response Available"
/>
)}
</EuiFlyoutBody>
</>

View file

@ -14,7 +14,7 @@ import {
Immutable,
} from '../../../../../../common/types';
import { formatResponse } from './policy_response_friendly_names';
import { POLICY_STATUS_TO_HEALTH_COLOR } from './host_constants';
import { POLICY_STATUS_TO_HEALTH_COLOR } from '../host_constants';
/**
* Nested accordion in the policy response detailing any concerned
@ -43,6 +43,17 @@ const PolicyResponseConfigAccordion = styled(EuiAccordion)`
:hover:not(.euiAccordion-isOpen) {
background-color: ${props => props.theme.eui.euiColorLightestShade};
}
.policyResponseActionsAccordion {
svg {
height: ${props => props.theme.eui.euiIconSizes.small};
width: ${props => props.theme.eui.euiIconSizes.small};
}
}
.policyResponseStatusHealth {
width: 100px;
}
`;
const ResponseActions = memo(
@ -65,8 +76,13 @@ const ResponseActions = memo(
id={action + index}
key={action + index}
data-test-subj="hostDetailsPolicyResponseActionsAccordion"
className="policyResponseActionsAccordion"
buttonContent={
<EuiText size="xs" data-test-subj="policyResponseAction">
<EuiText
size="xs"
className="eui-textTruncate"
data-test-subj="policyResponseAction"
>
<h4>{formatResponse(action)}</h4>
</EuiText>
}
@ -75,6 +91,7 @@ const ResponseActions = memo(
<EuiHealth
color={POLICY_STATUS_TO_HEALTH_COLOR[statuses.status]}
data-test-subj="policyResponseStatusHealth"
className="policyResponseStatusHealth"
>
<EuiText size="xs">
<p>{formatResponse(statuses.status)}</p>

View file

@ -25,6 +25,18 @@ responseMap.set(
defaultMessage: 'Failed',
})
);
responseMap.set(
'logging',
i18n.translate('xpack.endpoint.hostDetails.policyResponse.logging', {
defaultMessage: 'Logging',
})
);
responseMap.set(
'streaming',
i18n.translate('xpack.endpoint.hostDetails.policyResponse.streaming', {
defaultMessage: 'Streaming',
})
);
responseMap.set(
'malware',
i18n.translate('xpack.endpoint.hostDetails.policyResponse.malware', {

View file

@ -4,7 +4,17 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { HostPolicyResponseActionStatus } from '../../../../../../common/types';
import { HostPolicyResponseActionStatus, HostStatus } from '../../../../../common/types';
export const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze<
{
[key in HostStatus]: string;
}
>({
[HostStatus.ERROR]: 'danger',
[HostStatus.ONLINE]: 'success',
[HostStatus.OFFLINE]: 'subdued',
});
export const POLICY_STATUS_TO_HEALTH_COLOR = Object.freeze<
{ [key in keyof typeof HostPolicyResponseActionStatus]: string }

View file

@ -5,7 +5,14 @@
*/
import React, { useMemo, useCallback, memo } from 'react';
import { EuiHorizontalRule, EuiBasicTable, EuiText, EuiLink, EuiHealth } from '@elastic/eui';
import {
EuiHorizontalRule,
EuiBasicTable,
EuiText,
EuiLink,
EuiHealth,
EuiToolTip,
} from '@elastic/eui';
import { useHistory } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
@ -16,19 +23,10 @@ import * as selectors from '../../store/hosts/selectors';
import { useHostSelector } from './hooks';
import { CreateStructuredSelector } from '../../types';
import { urlFromQueryParams } from './url_from_query_params';
import { HostInfo, HostStatus, Immutable } from '../../../../../common/types';
import { HostInfo, Immutable } from '../../../../../common/types';
import { PageView } from '../components/page_view';
import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler';
const HOST_STATUS_TO_HEALTH_COLOR = Object.freeze<
{
[key in HostStatus]: string;
}
>({
[HostStatus.ERROR]: 'danger',
[HostStatus.ONLINE]: 'success',
[HostStatus.OFFLINE]: 'subdued',
});
import { HOST_STATUS_TO_HEALTH_COLOR } from './host_constants';
const HostLink = memo<{
name: string;
@ -39,7 +37,12 @@ const HostLink = memo<{
return (
// eslint-disable-next-line @elastic/eui/href-or-on-click
<EuiLink data-test-subj="hostnameCellLink" href={href} onClick={clickHandler}>
<EuiLink
data-test-subj="hostnameCellLink"
className="eui-textTruncate"
href={href}
onClick={clickHandler}
>
{name}
</EuiLink>
);
@ -107,6 +110,7 @@ export const HostList = () => {
<EuiHealth
color={HOST_STATUS_TO_HEALTH_COLOR[hostStatus]}
data-test-subj="rowHostStatus"
className="eui-textTruncate"
>
<FormattedMessage
id="xpack.endpoint.host.list.hostStatusValue"
@ -124,7 +128,7 @@ export const HostList = () => {
}),
truncateText: true,
render: () => {
return 'Policy Name';
return <span className="eui-textTruncate">Policy Name</span>;
},
},
{
@ -133,7 +137,14 @@ export const HostList = () => {
defaultMessage: 'Policy Status',
}),
render: () => {
return <EuiHealth color="success">Policy Status</EuiHealth>;
return (
<EuiHealth color="success" className="eui-textTruncate">
<FormattedMessage
id="xpack.endpoint.host.list.policyStatus"
defaultMessage="Policy Status"
/>
</EuiHealth>
);
},
},
{
@ -151,13 +162,24 @@ export const HostList = () => {
name: i18n.translate('xpack.endpoint.host.list.os', {
defaultMessage: 'Operating System',
}),
truncateText: true,
},
{
field: 'metadata.host.ip',
name: i18n.translate('xpack.endpoint.host.list.ip', {
defaultMessage: 'IP Address',
}),
truncateText: true,
render: (ip: string[]) => {
return (
<EuiToolTip content={ip.toString().replace(',', ', ')} anchorClassName="eui-fullWidth">
<EuiText size="s" className="eui-fullWidth">
<span className="eui-textTruncate eui-fullWidth">
{ip.toString().replace(',', ', ')}
</span>
</EuiText>
</EuiToolTip>
);
},
},
{
field: 'metadata.agent.version',