Skip to content

Commit

Permalink
extract take action button
Browse files Browse the repository at this point in the history
  • Loading branch information
christineweng committed Dec 16, 2024
1 parent f88716e commit 176e4c2
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 230 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { mockFlyoutApi } from '../shared/mocks/mock_flyout_context';
import { mockContextValue } from '../shared/mocks/mock_context';
import { DocumentDetailsContext } from '../shared/context';
import { PreviewPanelFooter } from './footer';
import { PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids';
import { FLYOUT_FOOTER_TEST_ID, FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from '../right/test_ids';
import { PREVIEW_FOOTER_TEST_ID, PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids';
import { FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from '../shared/components/test_ids';
import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock';
import { useKibana } from '../../../common/lib/kibana';
import { useAlertExceptionActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions';
Expand Down Expand Up @@ -56,6 +56,18 @@ describe('<PreviewPanelFooter />', () => {
(useAddToCaseActions as jest.Mock).mockReturnValue({ addToCaseActionItems: [] });
});

it('should not render the take action dropdown if preview mode', () => {
const { queryByTestId } = render(
<TestProviders>
<DocumentDetailsContext.Provider value={{ ...mockContextValue, isPreview: true }}>
<PreviewPanelFooter />
</DocumentDetailsContext.Provider>
</TestProviders>
);

expect(queryByTestId(PREVIEW_FOOTER_TEST_ID)).not.toBeInTheDocument();
});

it('should render footer for alert', () => {
const { getByTestId } = render(
<TestProviders>
Expand All @@ -64,7 +76,7 @@ describe('<PreviewPanelFooter />', () => {
</DocumentDetailsContext.Provider>
</TestProviders>
);
expect(getByTestId(FLYOUT_FOOTER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(PREVIEW_FOOTER_TEST_ID)).toBeInTheDocument();
expect(getByTestId(PREVIEW_FOOTER_LINK_TEST_ID)).toHaveTextContent('Show full alert details');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,24 @@
* 2.0.
*/

import { EuiLink } from '@elastic/eui';
import type { FC } from 'react';
import React, { useCallback, useMemo } from 'react';
import { EuiLink, EuiFlyoutFooter, EuiPanel, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { TakeActionButton } from '../shared/components/take_action_button';
import { getField } from '../shared/utils';
import { EventKind } from '../shared/constants/event_kinds';
import { DocumentDetailsRightPanelKey } from '../shared/constants/panel_keys';
import { useDocumentDetailsContext } from '../shared/context';
import { PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids';
import { PREVIEW_FOOTER_TEST_ID, PREVIEW_FOOTER_LINK_TEST_ID } from './test_ids';
import { useKibana } from '../../../common/lib/kibana';
import { DocumentEventTypes } from '../../../common/lib/telemetry';
import { PanelFooter } from '../right/footer';

/**
* Footer at the bottom of preview panel with a link to open document details flyout
*/
export const PreviewPanelFooter = () => {
export const PreviewPanelFooter: FC = () => {
const { eventId, indexName, scopeId, getFieldsData, isPreview } = useDocumentDetailsContext();
const { openFlyout } = useExpandableFlyoutApi();
const { telemetry } = useKibana().services;
Expand All @@ -48,7 +49,7 @@ export const PreviewPanelFooter = () => {
});
}, [openFlyout, eventId, indexName, scopeId, telemetry]);

const additionalActions = useMemo(
const fullDetailsLink = useMemo(
() => (
<EuiLink
onClick={openDocumentFlyout}
Expand All @@ -65,5 +66,19 @@ export const PreviewPanelFooter = () => {
),
[isAlert, openDocumentFlyout]
);
return <PanelFooter isPreview={isPreview} additionalActions={additionalActions} />;

if (isPreview) return null;

return (
<EuiFlyoutFooter data-test-subj={PREVIEW_FOOTER_TEST_ID}>
<EuiPanel color="transparent">
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexItem grow={false}>{fullDetailsLink}</EuiFlexItem>
<EuiFlexItem grow={false}>
<TakeActionButton />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlyoutFooter>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { PanelFooter } from './footer';
import { TestProviders } from '../../../common/mock';
import { mockContextValue } from '../shared/mocks/mock_context';
import { DocumentDetailsContext } from '../shared/context';
import { FLYOUT_FOOTER_TEST_ID, FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from './test_ids';
import { FLYOUT_FOOTER_TEST_ID } from './test_ids';
import { FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID } from '../shared/components/test_ids';
import { useKibana } from '../../../common/lib/kibana';
import { useAlertExceptionActions } from '../../../detections/components/alerts_table/timeline_actions/use_add_exception_actions';
import { useInvestigateInTimeline } from '../../../detections/components/alerts_table/timeline_actions/use_investigate_in_timeline';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,237 +6,34 @@
*/

import type { FC } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { EuiFlexGroup, EuiFlexItem, useEuiTheme, EuiFlyoutFooter, EuiPanel } from '@elastic/eui';
import { find } from 'lodash/fp';
import React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutFooter, EuiPanel } from '@elastic/eui';
import { FLYOUT_FOOTER_TEST_ID } from './test_ids';
import type { Status } from '../../../../common/api/detection_engine';
import { getAlertDetailsFieldValue } from '../../../common/lib/endpoint/utils/get_event_details_field_values';
import { TakeActionDropdown } from './components/take_action_dropdown';
import { AddExceptionFlyoutWrapper } from '../../../detections/components/alerts_table/timeline_actions/alert_context_menu';
import { EventFiltersFlyout } from '../../../management/pages/event_filters/view/components/event_filters_flyout';
import { OsqueryFlyout } from '../../../detections/components/osquery/osquery_flyout';
import { useDocumentDetailsContext } from '../shared/context';
import { useHostIsolation } from '../shared/hooks/use_host_isolation';
import { DocumentDetailsIsolateHostPanelKey } from '../shared/constants/panel_keys';
import { useRefetchByScope } from './hooks/use_refetch_by_scope';
import { useExceptionFlyout } from '../../../detections/components/alerts_table/timeline_actions/use_add_exception_flyout';
import { isActiveTimeline } from '../../../helpers';
import { useEventFilterModal } from '../../../detections/components/alerts_table/timeline_actions/use_event_filter_modal';

interface AlertSummaryData {
/**
* Status of the alert (open, closed...)
*/
alertStatus: Status;
/**
* Id of the document
*/
eventId: string;
/**
* Id of the rule
*/
ruleId: string;
/**
* Property ruleId on the rule
*/
ruleRuleId: string;
/**
* Name of the rule
*/
ruleName: string;
}
import { TakeActionButton } from '../shared/components/take_action_button';

interface PanelFooterProps {
/**
* Boolean that indicates whether flyout is in preview and action should be hidden
*/
isPreview: boolean;
/**
* Additional actions to be displayed in the footer
*/
additionalActions?: React.ReactNode;
}

/**
* Bottom section of the flyout that contains the take action button
*/
export const PanelFooter: FC<PanelFooterProps> = ({ isPreview, additionalActions }) => {
const { euiTheme } = useEuiTheme();
// we need this flyout to be above the timeline flyout (which has a z-index of 1002)
const flyoutZIndex = useMemo(
() => ({ style: `z-index: ${(euiTheme.levels.flyout as number) + 3}` }),
[euiTheme]
);

const { closeFlyout, openRightPanel } = useExpandableFlyoutApi();
const {
eventId,
indexName,
dataFormattedForFieldBrowser,
dataAsNestedObject,
refetchFlyoutData,
scopeId,
} = useDocumentDetailsContext();

// host isolation interaction
const { isHostIsolationPanelOpen, showHostIsolationPanel } = useHostIsolation();
const showHostIsolationPanelCallback = useCallback(
(action: 'isolateHost' | 'unisolateHost' | undefined) => {
showHostIsolationPanel(action);
openRightPanel({
id: DocumentDetailsIsolateHostPanelKey,
params: {
id: eventId,
indexName,
scopeId,
isolateAction: action,
},
});
},
[eventId, indexName, openRightPanel, scopeId, showHostIsolationPanel]
);

const { refetch: refetchAll } = useRefetchByScope({ scopeId });

// exception interaction
const ruleIndexRaw = useMemo(
() =>
find({ category: 'signal', field: 'signal.rule.index' }, dataFormattedForFieldBrowser)
?.values ??
find(
{ category: 'kibana', field: 'kibana.alert.rule.parameters.index' },
dataFormattedForFieldBrowser
)?.values,
[dataFormattedForFieldBrowser]
);
const ruleIndex = useMemo(
(): string[] | undefined => (Array.isArray(ruleIndexRaw) ? ruleIndexRaw : undefined),
[ruleIndexRaw]
);
const ruleDataViewIdRaw = useMemo(
() =>
find({ category: 'signal', field: 'signal.rule.data_view_id' }, dataFormattedForFieldBrowser)
?.values ??
find(
{ category: 'kibana', field: 'kibana.alert.rule.parameters.data_view_id' },
dataFormattedForFieldBrowser
)?.values,
[dataFormattedForFieldBrowser]
);
const ruleDataViewId = useMemo(
(): string | undefined => (Array.isArray(ruleDataViewIdRaw) ? ruleDataViewIdRaw[0] : undefined),
[ruleDataViewIdRaw]
);
const alertSummaryData = useMemo(
() =>
[
{ category: 'signal', field: 'signal.rule.id', name: 'ruleId' },
{ category: 'signal', field: 'signal.rule.rule_id', name: 'ruleRuleId' },
{ category: 'signal', field: 'signal.rule.name', name: 'ruleName' },
{ category: 'signal', field: 'kibana.alert.workflow_status', name: 'alertStatus' },
{ category: '_id', field: '_id', name: 'eventId' },
].reduce<AlertSummaryData>(
(acc, curr) => ({
...acc,
[curr.name]: getAlertDetailsFieldValue(
{ category: curr.category, field: curr.field },
dataFormattedForFieldBrowser
),
}),
{} as AlertSummaryData
),
[dataFormattedForFieldBrowser]
);
const {
exceptionFlyoutType,
openAddExceptionFlyout,
onAddExceptionTypeClick,
onAddExceptionCancel,
onAddExceptionConfirm,
} = useExceptionFlyout({
refetch: refetchAll,
isActiveTimelines: isActiveTimeline(scopeId),
});

// event filter interaction
const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } =
useEventFilterModal();

// osquery interaction
const [isOsqueryFlyoutOpenWithAgentId, setOsqueryFlyoutOpenWithAgentId] = useState<null | string>(
null
);
const closeOsqueryFlyout = useCallback(() => {
setOsqueryFlyoutOpenWithAgentId(null);
}, [setOsqueryFlyoutOpenWithAgentId]);
const alertId = useMemo(
() => (dataAsNestedObject?.kibana?.alert ? dataAsNestedObject?._id : null),
[dataAsNestedObject?._id, dataAsNestedObject?.kibana?.alert]
);

export const PanelFooter: FC<PanelFooterProps> = ({ isPreview }) => {
if (isPreview) return null;

return (
<>
<EuiFlyoutFooter data-test-subj={FLYOUT_FOOTER_TEST_ID}>
<EuiPanel color="transparent">
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
{additionalActions && <EuiFlexItem>{additionalActions}</EuiFlexItem>}
<EuiFlexItem grow={false}>
{dataAsNestedObject && (
<TakeActionDropdown
dataFormattedForFieldBrowser={dataFormattedForFieldBrowser}
dataAsNestedObject={dataAsNestedObject}
handleOnEventClosed={closeFlyout}
isHostIsolationPanelOpen={isHostIsolationPanelOpen}
onAddEventFilterClick={onAddEventFilterClick}
onAddExceptionTypeClick={onAddExceptionTypeClick}
onAddIsolationStatusClick={showHostIsolationPanelCallback}
refetchFlyoutData={refetchFlyoutData}
refetch={refetchAll}
scopeId={scopeId}
onOsqueryClick={setOsqueryFlyoutOpenWithAgentId}
/>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlyoutFooter>

{openAddExceptionFlyout &&
alertSummaryData.ruleId != null &&
alertSummaryData.ruleRuleId != null &&
alertSummaryData.eventId != null && (
<AddExceptionFlyoutWrapper
{...alertSummaryData}
ruleIndices={ruleIndex}
ruleDataViewId={ruleDataViewId}
exceptionListType={exceptionFlyoutType}
onCancel={onAddExceptionCancel}
onConfirm={onAddExceptionConfirm}
/>
)}

{isAddEventFilterModalOpen && dataAsNestedObject != null && (
<EventFiltersFlyout
data={dataAsNestedObject}
onCancel={closeAddEventFilterModal}
// EUI TODO: This z-index override of EuiOverlayMask is a workaround, and ideally should be resolved with a cleaner UI/UX flow long-term
maskProps={flyoutZIndex} // we need this flyout to be above the timeline flyout (which has a z-index of 1002)
/>
)}

{isOsqueryFlyoutOpenWithAgentId && dataAsNestedObject != null && (
<OsqueryFlyout
agentId={isOsqueryFlyoutOpenWithAgentId}
defaultValues={alertId ? { alertIds: [alertId] } : undefined}
onClose={closeOsqueryFlyout}
ecsData={dataAsNestedObject}
/>
)}
</>
<EuiFlyoutFooter data-test-subj={FLYOUT_FOOTER_TEST_ID}>
<EuiPanel color="transparent">
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
<EuiFlexItem grow={false}>
<TakeActionButton />
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</EuiFlyoutFooter>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { PREFIX } from '../../shared/test_ids';

export const FLYOUT_BODY_TEST_ID = `${PREFIX}Body` as const;
export const FLYOUT_FOOTER_TEST_ID = `${PREFIX}Footer` as const;
export const FLYOUT_FOOTER_DROPDOWN_BUTTON_TEST_ID =
`${FLYOUT_FOOTER_TEST_ID}DropdownButton` as const;
export const OVERVIEW_TAB_TEST_ID = `${PREFIX}OverviewTab` as const;
export const TABLE_TAB_TEST_ID = `${PREFIX}TableTab` as const;
export const JSON_TAB_TEST_ID = `${PREFIX}JsonTab` as const;
Loading

0 comments on commit 176e4c2

Please sign in to comment.