Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [RAC][Observability] Add status update actions in row menu (#108698) #108992

Merged
merged 1 commit into from
Aug 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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