WIP
add tabs for endpoint details
This commit is contained in:
parent
2d05d9f802
commit
c26a7d47b4
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiTabbedContent, EuiTabbedContentTab } from '@elastic/eui';
|
||||
|
||||
export enum EndpointDetailsTabsTypes {
|
||||
overview = 'overview',
|
||||
activityLog = 'activity-log',
|
||||
}
|
||||
|
||||
export type EndpointDetailsTabsId =
|
||||
| EndpointDetailsTabsTypes.overview
|
||||
| EndpointDetailsTabsTypes.activityLog;
|
||||
|
||||
interface EndpointDetailsTabs {
|
||||
id: string;
|
||||
name: string;
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
const StyledEuiTabbedContent = styled(EuiTabbedContent)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
> [role='tabpanel'] {
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 7px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
-webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EndpointDetailsFlyoutTabs = ({ tabs }: { tabs: EndpointDetailsTabs[] }) => {
|
||||
const [selectedTabId, setSelectedTabId] = useState<EndpointDetailsTabsId>(
|
||||
EndpointDetailsTabsTypes.overview
|
||||
);
|
||||
|
||||
const handleTabClick = useCallback(
|
||||
(tab: EuiTabbedContentTab) => setSelectedTabId(tab.id as EndpointDetailsTabsId),
|
||||
[setSelectedTabId]
|
||||
);
|
||||
|
||||
const selectedTab = useMemo(() => tabs.find((tab) => tab.id === selectedTabId), [
|
||||
tabs,
|
||||
selectedTabId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<StyledEuiTabbedContent
|
||||
data-test-subj="endpointDetailsTabs"
|
||||
tabs={tabs}
|
||||
selectedTab={selectedTab}
|
||||
onTabClick={handleTabClick}
|
||||
key="endpoint-details-tabs"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
EndpointDetailsFlyoutTabs.displayName = 'EndpointDetailsFlyoutTabs';
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { EuiComment, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { EndpointAction } from '../../../../../../../common/endpoint/types';
|
||||
|
||||
const TimelineItem = styled(EuiComment)`
|
||||
figure {
|
||||
border: 0;
|
||||
}
|
||||
figcaption.euiCommentEvent__header {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const CommentItem = styled.div`
|
||||
max-width: 85%;
|
||||
> div {
|
||||
display: inline-flex;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimelineEntry = ({ endpointAction }: { endpointAction: EndpointAction }) => {
|
||||
const isIsolated = endpointAction?.data.command === 'isolate';
|
||||
const timelineIcon = isIsolated ? 'lock' : 'lockOpen';
|
||||
const event = `${isIsolated ? 'isolated' : 'unisolated'} the endpoint`;
|
||||
const hasComment = !!endpointAction.data.comment;
|
||||
return (
|
||||
<TimelineItem
|
||||
type="regular"
|
||||
username={endpointAction.user_id}
|
||||
event={event}
|
||||
timelineIcon={timelineIcon}
|
||||
data-test-subj="timelineEntry"
|
||||
>
|
||||
<EuiText size="s">{endpointAction['@timestamp']}</EuiText>
|
||||
<EuiSpacer size="m" />
|
||||
{hasComment ? (
|
||||
<CommentItem>
|
||||
<EuiPanel hasBorder={true} paddingSize="s">
|
||||
<EuiText size="s">
|
||||
<p>{endpointAction.data.comment}</p>
|
||||
</EuiText>
|
||||
</EuiPanel>
|
||||
</CommentItem>
|
||||
) : undefined}
|
||||
</TimelineItem>
|
||||
);
|
||||
};
|
||||
|
||||
TimelineEntry.displayName = 'TimelineEntry';
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { EuiFieldSearch, EuiSpacer } from '@elastic/eui';
|
||||
import { TimelineEntry } from './components/timeline_entry';
|
||||
import { EndpointAction } from '../../../../../../common/endpoint/types';
|
||||
|
||||
export const EndpointActivityLog = ({ endpointActions }: { endpointActions: EndpointAction[] }) => (
|
||||
<>
|
||||
<EuiFieldSearch compressed fullWidth placeholder="Search activity log" />
|
||||
<EuiSpacer size="l" />
|
||||
{endpointActions.map((endpointAction) => (
|
||||
<TimelineEntry key={endpointAction['@timestamp']} endpointAction={endpointAction} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
EndpointActivityLog.displayName = 'EndpointActivityLog';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs';
|
||||
import { EndpointActivityLog } from './endpoint_activity_log';
|
||||
import { EndpointDetailsFlyout } from '.';
|
||||
import { EuiThemeProvider } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
|
||||
import { dummyEndpointActions } from './';
|
||||
|
||||
export default {
|
||||
title: 'Endpoints/Endpoint Details',
|
||||
component: EndpointDetailsFlyout,
|
||||
decorators: [
|
||||
(Story: ComponentType) => (
|
||||
<EuiThemeProvider>
|
||||
<Story />
|
||||
</EuiThemeProvider>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
export const Tabs = () => (
|
||||
<EndpointDetailsFlyoutTabs
|
||||
tabs={[
|
||||
{
|
||||
id: 'overview',
|
||||
name: 'Overview',
|
||||
content: <>{'Endpoint Details'}</>,
|
||||
},
|
||||
{
|
||||
id: 'activity-log',
|
||||
name: 'Activity Log',
|
||||
content: ActivityLog(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ActivityLog = () => <EndpointActivityLog endpointActions={dummyEndpointActions} />;
|
|
@ -6,6 +6,8 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, memo, useMemo } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import {
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
|
@ -16,6 +18,8 @@ import {
|
|||
EuiSpacer,
|
||||
EuiEmptyPrompt,
|
||||
EuiToolTip,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -41,15 +45,65 @@ import {
|
|||
policyResponseAppliedRevision,
|
||||
} from '../../store/selectors';
|
||||
import { EndpointDetails } from './endpoint_details';
|
||||
import { EndpointActivityLog } from './endpoint_activity_log';
|
||||
import { PolicyResponse } from './policy_response';
|
||||
import { HostMetadata } from '../../../../../../common/endpoint/types';
|
||||
import * as i18 from '../translations';
|
||||
import { EndpointAction, HostMetadata } from '../../../../../../common/endpoint/types';
|
||||
import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header';
|
||||
import {
|
||||
EndpointDetailsFlyoutTabs,
|
||||
EndpointDetailsTabsTypes,
|
||||
} from './components/endpoint_details_tabs';
|
||||
import { useNavigateByRouterEventHandler } from '../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler';
|
||||
import { getEndpointListPath } from '../../../../common/routing';
|
||||
import { SecurityPageName } from '../../../../../app/types';
|
||||
import { useFormatUrl } from '../../../../../common/components/link_to';
|
||||
import { PreferenceFormattedDateFromPrimitive } from '../../../../../common/components/formatted_date';
|
||||
|
||||
export const dummyEndpointActions: EndpointAction[] = [
|
||||
{
|
||||
action_id: '1',
|
||||
'@timestamp': moment().subtract(2, 'hours').fromNow().toString(),
|
||||
expiration: moment().add(1, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: ['x', 'y', 'z'],
|
||||
user_id: 'ash',
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment: 'Sem et tortor consequat id porta nibh venenatis cras sed.',
|
||||
},
|
||||
},
|
||||
{
|
||||
action_id: '2',
|
||||
'@timestamp': moment().subtract(4, 'hours').fromNow().toString(),
|
||||
expiration: moment().add(1, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: ['x', 'y', 'z'],
|
||||
user_id: 'someone',
|
||||
data: {
|
||||
command: 'unisolate',
|
||||
comment: 'Turpis egestas pretium aenean pharetra.',
|
||||
},
|
||||
},
|
||||
{
|
||||
action_id: '3',
|
||||
'@timestamp': moment().subtract(1, 'day').fromNow().toString(),
|
||||
expiration: moment().add(3, 'day').fromNow().toString(),
|
||||
type: 'INPUT_ACTION',
|
||||
input_type: 'endpoint',
|
||||
agents: ['x', 'y', 'z'],
|
||||
user_id: 'ash',
|
||||
data: {
|
||||
command: 'isolate',
|
||||
comment:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, \
|
||||
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const EndpointDetailsFlyout = memo(() => {
|
||||
const history = useHistory();
|
||||
const toasts = useToasts();
|
||||
|
@ -88,6 +142,7 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
style={{ zIndex: 4001 }}
|
||||
data-test-subj="endpointDetailsFlyout"
|
||||
size="m"
|
||||
paddingSize="m"
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
{loading ? (
|
||||
|
@ -116,11 +171,30 @@ export const EndpointDetailsFlyout = memo(() => {
|
|||
{show === 'details' && (
|
||||
<>
|
||||
<EuiFlyoutBody data-test-subj="endpointDetailsFlyoutBody">
|
||||
<EndpointDetails
|
||||
details={details}
|
||||
policyInfo={policyInfo}
|
||||
hostStatus={hostStatus}
|
||||
/>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EndpointDetailsFlyoutTabs
|
||||
tabs={[
|
||||
{
|
||||
id: EndpointDetailsTabsTypes.overview,
|
||||
name: i18.OVERVIEW,
|
||||
content: (
|
||||
<EndpointDetails
|
||||
details={details}
|
||||
policyInfo={policyInfo}
|
||||
hostStatus={hostStatus}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: EndpointDetailsTabsTypes.activityLog,
|
||||
name: i18.ACTIVITY_LOG,
|
||||
content: <EndpointActivityLog endpointActions={dummyEndpointActions} />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const OVERVIEW = i18n.translate('xpack.securitySolution.endpointDetails.overview', {
|
||||
defaultMessage: 'Overview',
|
||||
});
|
||||
|
||||
export const ACTIVITY_LOG = i18n.translate('xpack.securitySolution.endpointDetails.activityLog', {
|
||||
defaultMessage: 'Activity Log',
|
||||
});
|
Loading…
Reference in a new issue