mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Security Solution] expandable flyout - add isolate host panel (#165933)
## Summary
This new expandable flyout is going GA in `8.10`. One feature isn't
working: the `isolate host` from the `take action` button in the right
section footer. The code was added in this
[PR](https://github.com/elastic/kibana/pull/153903) but isolate host
testing must have been overlooked.
This PR adds the functionality to the new expandable flyout, by creating
a new panel, displayed similarly to the right panel is today.
abd99323
-616b-4474-a21c-29ce3c56dd1a
https://github.com/elastic/kibana/pull/165933
### TODO
- [ ] verify logic
- [ ] add unit tests
- [ ] add Cypress tests
### Checklist
Delete any items that are not applicable to this PR.
- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---------
Co-authored-by: Ashokaditya <ashokaditya@elastic.co>
This commit is contained in:
parent
3f18975c04
commit
ed48990395
8 changed files with 322 additions and 9 deletions
|
@ -11,6 +11,9 @@ import {
|
|||
type ExpandableFlyoutProps,
|
||||
ExpandableFlyoutProvider,
|
||||
} from '@kbn/expandable-flyout';
|
||||
import type { IsolateHostPanelProps } from './isolate_host';
|
||||
import { IsolateHostPanel, IsolateHostPanelKey } from './isolate_host';
|
||||
import { IsolateHostPanelProvider } from './isolate_host/context';
|
||||
import type { RightPanelProps } from './right';
|
||||
import { RightPanel, RightPanelKey } from './right';
|
||||
import { RightPanelProvider } from './right/context';
|
||||
|
@ -54,6 +57,14 @@ const expandableFlyoutDocumentsPanels: ExpandableFlyoutProps['registeredPanels']
|
|||
</PreviewPanelProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: IsolateHostPanelKey,
|
||||
component: (props) => (
|
||||
<IsolateHostPanelProvider {...(props as IsolateHostPanelProps).params}>
|
||||
<IsolateHostPanel path={props.path as IsolateHostPanelProps['path']} />
|
||||
</IsolateHostPanelProvider>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const OuterProviders: FC = ({ children }) => {
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import { RightPanelKey } from '../right';
|
||||
import { useBasicDataFromDetailsData } from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import { EndpointIsolateSuccess } from '../../common/components/endpoint/host_isolation';
|
||||
import { useHostIsolationTools } from '../../timelines/components/side_panel/event_details/use_host_isolation_tools';
|
||||
import { useIsolateHostPanelContext } from './context';
|
||||
import { HostIsolationPanel } from '../../detections/components/host_isolation';
|
||||
|
||||
/**
|
||||
* Document details expandable flyout section content for the isolate host component, displaying the form or the success banner
|
||||
*/
|
||||
export const PanelContent: FC = () => {
|
||||
const { openRightPanel } = useExpandableFlyoutContext();
|
||||
const { dataFormattedForFieldBrowser, eventId, scopeId, indexName, isolateAction } =
|
||||
useIsolateHostPanelContext();
|
||||
|
||||
const { isIsolateActionSuccessBannerVisible, handleIsolationActionSuccess } =
|
||||
useHostIsolationTools();
|
||||
|
||||
const { alertId, hostName } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser);
|
||||
|
||||
const showAlertDetails = useCallback(
|
||||
() =>
|
||||
openRightPanel({
|
||||
id: RightPanelKey,
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
},
|
||||
}),
|
||||
[eventId, indexName, scopeId, openRightPanel]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel hasShadow={false} hasBorder={false}>
|
||||
{isIsolateActionSuccessBannerVisible && (
|
||||
<EndpointIsolateSuccess
|
||||
hostName={hostName}
|
||||
alertId={alertId}
|
||||
isolateAction={isolateAction}
|
||||
/>
|
||||
)}
|
||||
<HostIsolationPanel
|
||||
details={dataFormattedForFieldBrowser}
|
||||
cancelCallback={showAlertDetails}
|
||||
successCallback={handleIsolationActionSuccess}
|
||||
isolateAction={isolateAction}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common';
|
||||
import { css } from '@emotion/react';
|
||||
import React, { createContext, memo, useContext, useMemo } from 'react';
|
||||
import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
|
||||
import { useTimelineEventsDetails } from '../../timelines/containers/details';
|
||||
import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers';
|
||||
import { useSpaceId } from '../../common/hooks/use_space_id';
|
||||
import { useRouteSpy } from '../../common/utils/route/use_route_spy';
|
||||
import { SecurityPageName } from '../../../common/constants';
|
||||
import { SourcererScopeName } from '../../common/store/sourcerer/model';
|
||||
import { useSourcererDataView } from '../../common/containers/sourcerer';
|
||||
import type { IsolateHostPanelProps } from '.';
|
||||
|
||||
export interface IsolateHostPanelContext {
|
||||
/**
|
||||
* Id of the document
|
||||
*/
|
||||
eventId: string;
|
||||
/**
|
||||
* Name of the index used in the parent's page
|
||||
*/
|
||||
indexName: string;
|
||||
/**
|
||||
* Maintain backwards compatibility // TODO remove when possible
|
||||
*/
|
||||
scopeId: string;
|
||||
/**
|
||||
* An array of field objects with category and value
|
||||
*/
|
||||
dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null;
|
||||
/**
|
||||
* Isolate action, either 'isolateHost' or 'unisolateHost'
|
||||
*/
|
||||
isolateAction: 'isolateHost' | 'unisolateHost';
|
||||
}
|
||||
|
||||
export const IsolateHostPanelContext = createContext<IsolateHostPanelContext | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export type IsolateHostPanelProviderProps = {
|
||||
/**
|
||||
* React components to render
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
} & Partial<IsolateHostPanelProps['params']>;
|
||||
|
||||
export const IsolateHostPanelProvider = memo(
|
||||
({ id, indexName, scopeId, isolateAction, children }: IsolateHostPanelProviderProps) => {
|
||||
const currentSpaceId = useSpaceId();
|
||||
// TODO Replace getAlertIndexAlias way to retrieving the eventIndex with the GET /_alias
|
||||
// https://github.com/elastic/kibana/issues/113063
|
||||
const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : '';
|
||||
const [{ pageName }] = useRouteSpy();
|
||||
const sourcererScope =
|
||||
pageName === SecurityPageName.detections
|
||||
? SourcererScopeName.detections
|
||||
: SourcererScopeName.default;
|
||||
const sourcererDataView = useSourcererDataView(sourcererScope);
|
||||
const [loading, dataFormattedForFieldBrowser] = useTimelineEventsDetails({
|
||||
indexName: eventIndex,
|
||||
eventId: id ?? '',
|
||||
runtimeMappings: sourcererDataView.runtimeMappings,
|
||||
skip: !id,
|
||||
});
|
||||
|
||||
const contextValue = useMemo(
|
||||
() =>
|
||||
id && indexName && scopeId && isolateAction
|
||||
? {
|
||||
eventId: id,
|
||||
indexName,
|
||||
scopeId,
|
||||
dataFormattedForFieldBrowser,
|
||||
isolateAction,
|
||||
}
|
||||
: undefined,
|
||||
[id, indexName, scopeId, dataFormattedForFieldBrowser, isolateAction]
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<EuiFlexItem
|
||||
css={css`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<EuiLoadingSpinner size="xxl" />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IsolateHostPanelContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</IsolateHostPanelContext.Provider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
IsolateHostPanelProvider.displayName = 'IsolateHostPanelProvider';
|
||||
|
||||
export const useIsolateHostPanelContext = (): IsolateHostPanelContext => {
|
||||
const contextValue = useContext(IsolateHostPanelContext);
|
||||
|
||||
if (!contextValue) {
|
||||
throw new Error(
|
||||
'IsolateHostPanelContext can only be used within IsolateHostPanelContext provider'
|
||||
);
|
||||
}
|
||||
|
||||
return contextValue;
|
||||
};
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
|
||||
import type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import { useIsolateHostPanelContext } from './context';
|
||||
import { FLYOUT_HEADER_TITLE_TEST_ID } from './test_ids';
|
||||
import { PANEL_HEADER_ISOLATE_TITLE, PANEL_HEADER_RELEASE_TITLE } from './translations';
|
||||
|
||||
/**
|
||||
* Document details expandable right section header for the isolate host panel
|
||||
*/
|
||||
export const PanelHeader: FC = () => {
|
||||
const { isolateAction } = useIsolateHostPanelContext();
|
||||
|
||||
const title =
|
||||
isolateAction === 'isolateHost' ? PANEL_HEADER_ISOLATE_TITLE : PANEL_HEADER_RELEASE_TITLE;
|
||||
|
||||
return (
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="s">
|
||||
<h4 data-test-subj={FLYOUT_HEADER_TITLE_TEST_ID}>{title}</h4>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import React from 'react';
|
||||
import type { FlyoutPanelProps } from '@kbn/expandable-flyout';
|
||||
import { PanelContent } from './content';
|
||||
import { PanelHeader } from './header';
|
||||
|
||||
export const IsolateHostPanelKey: IsolateHostPanelProps['key'] = 'document-details-isolate-host';
|
||||
|
||||
export interface IsolateHostPanelProps extends FlyoutPanelProps {
|
||||
key: 'document-details-isolate-host';
|
||||
params?: {
|
||||
id: string;
|
||||
indexName: string;
|
||||
scopeId: string;
|
||||
isolateAction: 'isolateHost' | 'unisolateHost' | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Panel to be displayed right section in the document details expandable flyout when isolate host is clicked in the
|
||||
* take action button
|
||||
*/
|
||||
export const IsolateHostPanel: FC<Partial<IsolateHostPanelProps>> = () => {
|
||||
return (
|
||||
<>
|
||||
<PanelHeader />
|
||||
<PanelContent />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const FLYOUT_HEADER_TITLE_TEST_ID = 'securitySolutionDocumentDetailsFlyoutHeaderTitle';
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 PANEL_HEADER_ISOLATE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.isolateHostPanelHeaderIsolateTitle',
|
||||
{
|
||||
defaultMessage: `Isolate host`,
|
||||
}
|
||||
);
|
||||
|
||||
export const PANEL_HEADER_RELEASE_TITLE = i18n.translate(
|
||||
'xpack.securitySolution.flyout.documentDetails.isolateHostPanelHeaderReleaseTitle',
|
||||
{
|
||||
defaultMessage: `Release host`,
|
||||
}
|
||||
);
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { FC } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useExpandableFlyoutContext } from '@kbn/expandable-flyout';
|
||||
import { FlyoutFooter } from '../../timelines/components/side_panel/event_details/flyout';
|
||||
import { useRightPanelContext } from './context';
|
||||
|
@ -15,13 +15,35 @@ import { useHostIsolationTools } from '../../timelines/components/side_panel/eve
|
|||
/**
|
||||
*
|
||||
*/
|
||||
export const PanelFooter: FC = memo(() => {
|
||||
const { closeFlyout } = useExpandableFlyoutContext();
|
||||
const { dataFormattedForFieldBrowser, dataAsNestedObject, refetchFlyoutData, scopeId } =
|
||||
useRightPanelContext();
|
||||
export const PanelFooter: FC = () => {
|
||||
const { closeFlyout, openRightPanel } = useExpandableFlyoutContext();
|
||||
const {
|
||||
eventId,
|
||||
indexName,
|
||||
dataFormattedForFieldBrowser,
|
||||
dataAsNestedObject,
|
||||
refetchFlyoutData,
|
||||
scopeId,
|
||||
} = useRightPanelContext();
|
||||
|
||||
const { isHostIsolationPanelOpen, showHostIsolationPanel } = useHostIsolationTools();
|
||||
|
||||
const showHostIsolationPanelCallback = useCallback(
|
||||
(action: 'isolateHost' | 'unisolateHost' | undefined) => {
|
||||
showHostIsolationPanel(action);
|
||||
openRightPanel({
|
||||
id: 'document-details-isolate-host',
|
||||
params: {
|
||||
id: eventId,
|
||||
indexName,
|
||||
scopeId,
|
||||
isolateAction: action,
|
||||
},
|
||||
});
|
||||
},
|
||||
[eventId, indexName, openRightPanel, scopeId, showHostIsolationPanel]
|
||||
);
|
||||
|
||||
if (!dataFormattedForFieldBrowser || !dataAsNestedObject) {
|
||||
return null;
|
||||
}
|
||||
|
@ -34,11 +56,9 @@ export const PanelFooter: FC = memo(() => {
|
|||
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
|
||||
isReadOnly={false}
|
||||
loadingEventDetails={false}
|
||||
onAddIsolationStatusClick={showHostIsolationPanel}
|
||||
onAddIsolationStatusClick={showHostIsolationPanelCallback}
|
||||
scopeId={scopeId}
|
||||
refetchFlyoutData={refetchFlyoutData}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
PanelFooter.displayName = 'PanelFooter';
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue