add tabs for endpoint details
This commit is contained in:
Ashokaditya 2021-05-11 16:33:26 +02:00
parent 2d05d9f802
commit c26a7d47b4
6 changed files with 303 additions and 6 deletions

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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} />;

View file

@ -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>
</>
)}

View file

@ -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',
});