Skip to content

Commit

Permalink
[RAC][Observability] Add status update actions in row menu (#108698)
Browse files Browse the repository at this point in the history
* use rac alerts bulk_update

* cleanup

* adds replace ALERT_STATUS with ALERT_WORKFLOW_STATUS and updates tests and adds logic for switching between signal.status and workflow status when updating alerts in .siem-signals

* allow object and string types in query param, fixed single update api to use WORKFLOW_STATUS instead of ALERT_STATUS

* adds additional integration test for when query is a DSL object in addtion to KQL string

* optionally use fields api in requests if _source does not contain authz properties

* integrate bulk update to all hook calls

* adds fields support, fixes bug where we were writing to 'signals.status' and not { signals: {status }} in alerts client

* clean up and fixes

* fix a bug where we were not waiting for updates to complete when using ids param in alerts bulk update. Adds integration tests for detection engine testing update alerts with new alerts as data client routes

* take index name from ecsData props

* pr suggestions

* some more type fixes

* refactor and type fixes

* snapshot updated

* add status update actions to row context menu

* refactor to use dispatch function in o11y actions

* comment removed

* bring alertConsumer back

* bring indexNames back

* check capabilities to show status update items

Co-authored-by: Devin Hurley <devin.hurley@elastic.co>
  • Loading branch information
semd and dhurley14 committed Aug 17, 2021
1 parent 3e7423a commit b607f42
Show file tree
Hide file tree
Showing 25 changed files with 386 additions and 177 deletions.
61 changes: 61 additions & 0 deletions x-pack/plugins/observability/public/hooks/use_alert_permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 { useEffect, useState } from 'react';
import { RecursiveReadonly } from '@kbn/utility-types';

export interface UseGetUserAlertsPermissionsProps {
crud: boolean;
read: boolean;
loading: boolean;
featureId: string | null;
}

export const useGetUserAlertsPermissions = (
uiCapabilities: RecursiveReadonly<Record<string, any>>,
featureId?: string
): UseGetUserAlertsPermissionsProps => {
const [alertsPermissions, setAlertsPermissions] = useState<UseGetUserAlertsPermissionsProps>({
crud: false,
read: false,
loading: true,
featureId: null,
});

useEffect(() => {
if (!featureId || !uiCapabilities[featureId]) {
setAlertsPermissions({
crud: false,
read: false,
loading: false,
featureId: null,
});
} else {
setAlertsPermissions((currentAlertPermissions) => {
if (currentAlertPermissions.featureId === featureId) {
return currentAlertPermissions;
}
const capabilitiesCanUserCRUD: boolean =
typeof uiCapabilities[featureId].save === 'boolean'
? uiCapabilities[featureId].save
: false;
const capabilitiesCanUserRead: boolean =
typeof uiCapabilities[featureId].show === 'boolean'
? uiCapabilities[featureId].show
: false;
return {
crud: capabilitiesCanUserCRUD,
read: capabilitiesCanUserRead,
loading: false,
featureId,
};
});
}
}, [alertsPermissions.featureId, featureId, uiCapabilities]);

return alertsPermissions;
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
* We have types and code at different imports because we don't want to import the whole package in the resulting webpack bundle for the plugin.
* This way plugins can do targeted imports to reduce the final code bundle
*/
import type {
import {
AlertConsumers as AlertConsumersTyped,
ALERT_DURATION as ALERT_DURATION_TYPED,
ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED,
ALERT_STATUS as ALERT_STATUS_TYPED,
ALERT_RULE_NAME as ALERT_RULE_NAME_TYPED,
ALERT_RULE_CONSUMER,
} from '@kbn/rule-data-utils';
import {
ALERT_DURATION as ALERT_DURATION_NON_TYPED,
Expand All @@ -41,7 +42,10 @@ import { i18n } from '@kbn/i18n';
import styled from 'styled-components';
import React, { Suspense, useMemo, useState, useCallback } from 'react';

import { get } from 'lodash';
import { useGetUserAlertsPermissions } from '../../hooks/use_alert_permission';
import type { TimelinesUIStart, TGridType, SortDirection } from '../../../../timelines/public';
import { useStatusBulkActionItems } from '../../../../timelines/public';
import type { TopAlert } from './';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import type {
Expand All @@ -58,6 +62,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context';
import { getDefaultCellActions } from './default_cell_actions';
import { LazyAlertsFlyout } from '../..';
import { parseAlert } from './parse_alert';
import { CoreStart } from '../../../../../../src/core/public';

const AlertConsumers: typeof AlertConsumersTyped = AlertConsumersNonTyped;
const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED;
Expand All @@ -75,6 +80,7 @@ interface AlertsTableTGridProps {
}

interface ObservabilityActionsProps extends ActionProps {
currentStatus: AlertStatus;
setFlyoutAlert: React.Dispatch<React.SetStateAction<TopAlert | undefined>>;
}

Expand Down Expand Up @@ -161,15 +167,27 @@ function ObservabilityActions({
data,
eventId,
ecsData,
currentStatus,
refetch,
setFlyoutAlert,
setEventsLoading,
setEventsDeleted,
}: ObservabilityActionsProps) {
const { core, observabilityRuleTypeRegistry } = usePluginContext();
const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {});
const [openActionsPopoverId, setActionsPopover] = useState(null);
const { timelines } = useKibana<{ timelines: TimelinesUIStart }>().services;
const {
timelines,
application: { capabilities },
} = useKibana<CoreStart & { timelines: TimelinesUIStart }>().services;

const parseObservabilityAlert = useMemo(() => parseAlert(observabilityRuleTypeRegistry), [
observabilityRuleTypeRegistry,
]);
const alertDataConsumer = useMemo<string>(() => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0], [
dataFieldEs,
]);

const alert = parseObservabilityAlert(dataFieldEs);
const { prepend } = core.http.basePath;

Expand All @@ -181,8 +199,8 @@ function ObservabilityActions({
setActionsPopover(null);
}, []);

const openActionsPopover = useCallback((id) => {
setActionsPopover(id);
const toggleActionsPopover = useCallback((id) => {
setActionsPopover((current) => (current ? null : id));
}, []);
const casePermissions = useGetUserCasesPermissions();
const event = useMemo(() => {
Expand All @@ -193,31 +211,48 @@ function ObservabilityActions({
};
}, [data, eventId, ecsData]);

const onAlertStatusUpdated = useCallback(() => {
setActionsPopover(null);
if (refetch) {
refetch();
}
}, [setActionsPopover, refetch]);

const alertPermissions = useGetUserAlertsPermissions(capabilities, alertDataConsumer);

const statusActionItems = useStatusBulkActionItems({
eventIds: [eventId],
currentStatus,
indexName: ecsData._index ?? '',
setEventsLoading,
setEventsDeleted,
onUpdateSuccess: onAlertStatusUpdated,
onUpdateFailure: onAlertStatusUpdated,
});

const actionsPanels = useMemo(() => {
return [
{
id: 0,
content: [
<>
{timelines.getAddToExistingCaseButton({
event,
casePermissions,
appId: observabilityFeatureId,
onClose: afterCaseSelection,
})}
</>,
<>
{timelines.getAddToNewCaseButton({
event,
casePermissions,
appId: observabilityFeatureId,
onClose: afterCaseSelection,
})}
</>,
timelines.getAddToExistingCaseButton({
event,
casePermissions,
appId: observabilityFeatureId,
onClose: afterCaseSelection,
}),
timelines.getAddToNewCaseButton({
event,
casePermissions,
appId: observabilityFeatureId,
onClose: afterCaseSelection,
}),
...(alertPermissions.crud ? statusActionItems : []),
],
},
];
}, [afterCaseSelection, casePermissions, timelines, event]);
}, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]);

return (
<>
<EuiFlexGroup gutterSize="none" responsive={false}>
Expand Down Expand Up @@ -247,7 +282,7 @@ function ObservabilityActions({
color="text"
iconType="boxesHorizontal"
aria-label="More"
onClick={() => openActionsPopover(eventId)}
onClick={() => toggleActionsPopover(eventId)}
/>
}
isOpen={openActionsPopoverId === eventId}
Expand Down Expand Up @@ -286,11 +321,17 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
);
},
rowCellRender: (actionProps: ActionProps) => {
return <ObservabilityActions {...actionProps} setFlyoutAlert={setFlyoutAlert} />;
return (
<ObservabilityActions
{...actionProps}
currentStatus={status as AlertStatus}
setFlyoutAlert={setFlyoutAlert}
/>
);
},
},
];
}, []);
}, [status]);

const tGridProps = useMemo(() => {
const type: TGridType = 'standalone';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const AddEndpointExceptionComponent: React.FC<AddEndpointExceptionProps> = ({
id="addEndpointException"
onClick={onClick}
disabled={disabled}
size="s"
>
{i18n.ACTION_ADD_ENDPOINT_EXCEPTION}
</EuiContextMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const AddExceptionComponent: React.FC<AddExceptionProps> = ({ disabled, onClick
id="addException"
onClick={onClick}
disabled={disabled}
size="s"
>
{i18n.ACTION_ADD_EXCEPTION}
</EuiContextMenuItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
eventId: ecsRowData?._id,
indexName: ecsRowData?._index ?? '',
timelineId,
refetch,
closePopover,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,9 @@
import { useCallback } from 'react';
import { useDispatch } from 'react-redux';

import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { Status } from '../../../../../common/detection_engine/schemas/common/schemas';
import { timelineActions } from '../../../../timelines/store/timeline';
import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types';
import * as i18nCommon from '../../../../common/translations';
import * as i18n from '../translations';

import {
useStateToaster,
displaySuccessToast,
displayErrorToast,
} from '../../../../common/components/toasters';
import { useStatusBulkActionItems } from '../../../../../../timelines/public';

interface Props {
Expand All @@ -28,6 +19,7 @@ interface Props {
eventId: string;
timelineId: string;
indexName: string;
refetch?: () => void;
}

export const useAlertsActions = ({
Expand All @@ -36,59 +28,16 @@ export const useAlertsActions = ({
eventId,
timelineId,
indexName,
refetch,
}: Props) => {
const dispatch = useDispatch();
const [, dispatchToaster] = useStateToaster();

const { addWarning } = useAppToasts();

const onAlertStatusUpdateSuccess = useCallback(
(updated: number, conflicts: number, newStatus: Status) => {
closePopover();
if (conflicts > 0) {
// Partial failure
addWarning({
title: i18nCommon.UPDATE_ALERT_STATUS_FAILED(conflicts),
text: i18nCommon.UPDATE_ALERT_STATUS_FAILED_DETAILED(updated, conflicts),
});
} else {
let title: string;
switch (newStatus) {
case 'closed':
title = i18n.CLOSED_ALERT_SUCCESS_TOAST(updated);
break;
case 'open':
title = i18n.OPENED_ALERT_SUCCESS_TOAST(updated);
break;
case 'in-progress':
title = i18n.IN_PROGRESS_ALERT_SUCCESS_TOAST(updated);
}

displaySuccessToast(title, dispatchToaster);
}
},
[addWarning, closePopover, dispatchToaster]
);

const onAlertStatusUpdateFailure = useCallback(
(newStatus: Status, error: Error) => {
let title: string;
closePopover();

switch (newStatus) {
case 'closed':
title = i18n.CLOSED_ALERT_FAILED_TOAST;
break;
case 'open':
title = i18n.OPENED_ALERT_FAILED_TOAST;
break;
case 'in-progress':
title = i18n.IN_PROGRESS_ALERT_FAILED_TOAST;
}
displayErrorToast(title, [error.message], dispatchToaster);
},
[closePopover, dispatchToaster]
);
const onStatusUpdate = useCallback(() => {
closePopover();
if (refetch) {
refetch();
}
}, [closePopover, refetch]);

const setEventsLoading = useCallback(
({ eventIds, isLoading }: SetEventsLoadingProps) => {
Expand All @@ -110,8 +59,8 @@ export const useAlertsActions = ({
indexName,
setEventsLoading,
setEventsDeleted,
onUpdateSuccess: onAlertStatusUpdateSuccess,
onUpdateFailure: onAlertStatusUpdateFailure,
onUpdateSuccess: onStatusUpdate,
onUpdateFailure: onStatusUpdate,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const TakeActionDropdown = React.memo(
eventId: actionsData.eventId,
indexName,
timelineId,
refetch,
closePopover: closePopoverAndFlyout,
});

Expand Down
Loading

0 comments on commit b607f42

Please sign in to comment.