[8.0][RAC] 117686 replace alert workflow status in alerts view (#118723)

* Add AlertStatus types

* Add alert status filter component

* Remove Filter in action from the t grid table

* Update group buttons to applied Alert status filter instead of Workflow status

* Keep the Alert status button in sync when typing and first page load

* Fix data test object name and translation keys label

* Add possibility to hide the bulk actions

* Update how hide the bulk actions

* Fix showCheckboxes hardcoded "true". Instead use the leadingControlColumns props

* Hide the leading checkboxes  in the T Grid with the bulk actions

* Update showCheckboxes to false

* Fix test as the leading checkboxes are hidden

* Update tests

* Get back disabledCellActions as it's required by T Grid

* Update tests to skip test related to Workflow action buttons

* Skip workflow tests

* Revert fix showCheckboxes

* Remove unused imports

* Revert the o11y tests as the checkBoxes fix is reverted

* Reactive the tests effected by checkBoxes

* Skip alert workflow status

* [Code review] use predefined types

* Remove unused prop

* Use the alert-data index name in the RegEx

* Detect * in KQL as "show al"l alert filter

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Faisal Kanout 2021-11-29 15:03:33 +03:00 committed by GitHub
parent e570b8783d
commit ee3cb46a68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 89 deletions

View file

@ -7,6 +7,10 @@
import * as t from 'io-ts';
export type Maybe<T> = T | null | undefined;
import {
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
} from '@kbn/rule-data-utils/alerts_as_data_status';
export const alertWorkflowStatusRt = t.keyof({
open: null,
@ -25,3 +29,12 @@ export interface ApmIndicesConfig {
apmAgentConfigurationIndex: string;
apmCustomLinkIndex: string;
}
export type AlertStatusFilterButton =
| typeof ALERT_STATUS_ACTIVE
| typeof ALERT_STATUS_RECOVERED
| '';
export interface AlertStatusFilter {
status: AlertStatusFilterButton;
query: string;
label: string;
}

View file

@ -0,0 +1,79 @@
/*
* 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 { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import {
ALERT_STATUS_ACTIVE,
ALERT_STATUS_RECOVERED,
} from '@kbn/rule-data-utils/alerts_as_data_status';
import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names';
import { AlertStatusFilterButton } from '../../../common/typings';
import { AlertStatusFilter } from '../../../common/typings';
export interface AlertStatusFilterProps {
status: AlertStatusFilterButton;
onChange: (id: string, value: string) => void;
}
export const allAlerts: AlertStatusFilter = {
status: '',
query: '',
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.showAll', {
defaultMessage: 'Show all',
}),
};
export const activeAlerts: AlertStatusFilter = {
status: ALERT_STATUS_ACTIVE,
query: `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`,
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.active', {
defaultMessage: 'Active',
}),
};
export const recoveredAlerts: AlertStatusFilter = {
status: ALERT_STATUS_RECOVERED,
query: `${ALERT_STATUS}: "${ALERT_STATUS_RECOVERED}"`,
label: i18n.translate('xpack.observability.alerts.alertStatusFilter.recovered', {
defaultMessage: 'Recovered',
}),
};
const options: EuiButtonGroupOptionProps[] = [
{
id: allAlerts.status,
label: allAlerts.label,
value: allAlerts.query,
'data-test-subj': 'alert-status-filter-show-all-button',
},
{
id: activeAlerts.status,
label: activeAlerts.label,
value: activeAlerts.query,
'data-test-subj': 'alert-status-filter-active-button',
},
{
id: recoveredAlerts.status,
label: recoveredAlerts.label,
value: recoveredAlerts.query,
'data-test-subj': 'alert-status-filter-recovered-button',
},
];
export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) {
return (
<EuiButtonGroup
legend="Filter by"
color="primary"
options={options}
idSelected={status}
onChange={onChange}
/>
);
}

View file

@ -13,8 +13,6 @@
import {
ALERT_DURATION,
ALERT_REASON,
ALERT_RULE_CONSUMER,
ALERT_RULE_PRODUCER,
ALERT_STATUS,
ALERT_WORKFLOW_STATUS,
TIMESTAMP,
@ -34,11 +32,8 @@ import {
import styled from 'styled-components';
import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { get, pick } from 'lodash';
import {
getAlertsPermissions,
useGetUserAlertsPermissions,
} from '../../hooks/use_alert_permission';
import { pick } from 'lodash';
import { getAlertsPermissions } from '../../hooks/use_alert_permission';
import type {
TimelinesUIStart,
TGridType,
@ -46,13 +41,14 @@ import type {
TGridModel,
SortDirection,
} from '../../../../timelines/public';
import { useStatusBulkActionItems } from '../../../../timelines/public';
import type { TopAlert } from './';
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
import type {
ActionProps,
AlertWorkflowStatus,
ColumnHeaderOptions,
ControlColumnProps,
RowRenderer,
} from '../../../../timelines/common';
@ -60,7 +56,6 @@ import { getRenderCellValue } from './render_cell_value';
import { observabilityAppId, observabilityFeatureId } from '../../../common';
import { useGetUserCasesPermissions } from '../../hooks/use_get_user_cases_permissions';
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';
@ -75,7 +70,6 @@ interface AlertsTableTGridProps {
kuery: string;
workflowStatus: AlertWorkflowStatus;
setRefetch: (ref: () => void) => void;
addToQuery: (value: string) => void;
}
interface ObservabilityActionsProps extends ActionProps {
@ -154,21 +148,21 @@ function ObservabilityActions({
const [openActionsPopoverId, setActionsPopover] = useState(null);
const {
timelines,
application: { capabilities },
application: {},
} = useKibana<CoreStart & { timelines: TimelinesUIStart }>().services;
const parseObservabilityAlert = useMemo(
() => parseAlert(observabilityRuleTypeRegistry),
[observabilityRuleTypeRegistry]
);
const alertDataConsumer = useMemo<string>(
() => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0],
[dataFieldEs]
);
const alertDataProducer = useMemo<string>(
() => get(dataFieldEs, ALERT_RULE_PRODUCER, [''])[0],
[dataFieldEs]
);
// const alertDataConsumer = useMemo<string>(
// () => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0],
// [dataFieldEs]
// );
// const alertDataProducer = useMemo<string>(
// () => get(dataFieldEs, ALERT_RULE_PRODUCER, [''])[0],
// [dataFieldEs]
// );
const alert = parseObservabilityAlert(dataFieldEs);
const { prepend } = core.http.basePath;
@ -194,27 +188,29 @@ function ObservabilityActions({
};
}, [data, eventId, ecsData]);
const onAlertStatusUpdated = useCallback(() => {
setActionsPopover(null);
if (refetch) {
refetch();
}
}, [setActionsPopover, refetch]);
// Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686
const alertPermissions = useGetUserAlertsPermissions(
capabilities,
alertDataConsumer === 'alerts' ? alertDataProducer : alertDataConsumer
);
// const onAlertStatusUpdated = useCallback(() => {
// setActionsPopover(null);
// if (refetch) {
// refetch();
// }
// }, [setActionsPopover, refetch]);
const statusActionItems = useStatusBulkActionItems({
eventIds: [eventId],
currentStatus,
indexName: ecsData._index ?? '',
setEventsLoading,
setEventsDeleted,
onUpdateSuccess: onAlertStatusUpdated,
onUpdateFailure: onAlertStatusUpdated,
});
// const alertPermissions = useGetUserAlertsPermissions(
// capabilities,
// alertDataConsumer === 'alerts' ? alertDataProducer : alertDataConsumer
// );
// const statusActionItems = useStatusBulkActionItems({
// eventIds: [eventId],
// currentStatus,
// indexName: ecsData._index ?? '',
// setEventsLoading,
// setEventsDeleted,
// onUpdateSuccess: onAlertStatusUpdated,
// onUpdateFailure: onAlertStatusUpdated,
// });
const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null;
const linkToRule = ruleId ? prepend(paths.management.ruleDetails(ruleId)) : null;
@ -239,7 +235,8 @@ function ObservabilityActions({
}),
]
: []),
...(alertPermissions.crud ? statusActionItems : []),
// Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686
// ...(alertPermissions.crud ? statusActionItems : []),
...(!!linkToRule
? [
<EuiContextMenuItem
@ -252,15 +249,7 @@ function ObservabilityActions({
]
: []),
];
}, [
afterCaseSelection,
casePermissions,
timelines,
event,
statusActionItems,
alertPermissions,
linkToRule,
]);
}, [afterCaseSelection, casePermissions, timelines, event, linkToRule]);
const actionsToolTip =
actionsMenuItems.length <= 0
@ -320,6 +309,7 @@ function ObservabilityActions({
</>
);
}
// Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686
const FIELDS_WITHOUT_CELL_ACTIONS = [
'@timestamp',
@ -330,7 +320,7 @@ const FIELDS_WITHOUT_CELL_ACTIONS = [
];
export function AlertsTableTGrid(props: AlertsTableTGridProps) {
const { indexNames, rangeFrom, rangeTo, kuery, workflowStatus, setRefetch, addToQuery } = props;
const { indexNames, rangeFrom, rangeTo, kuery, workflowStatus, setRefetch } = props;
const prevWorkflowStatus = usePrevious(workflowStatus);
const {
timelines,
@ -382,7 +372,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
}
}, []);
const leadingControlColumns = useMemo(() => {
const leadingControlColumns: ControlColumnProps[] = useMemo(() => {
return [
{
id: 'expand',
@ -428,7 +418,8 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
type,
columns: tGridState?.columns ?? columns,
deletedEventIds,
defaultCellActions: getDefaultCellActions({ addToQuery }),
// Hide the WorkFlow filter, but keep its code as required in https://github.com/elastic/kibana/issues/117686
// defaultCellActions: getDefaultCellActions({ addToQuery }),
disabledCellActions: FIELDS_WITHOUT_CELL_ACTIONS,
end: rangeTo,
filters: [],
@ -462,7 +453,6 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) {
};
}, [
casePermissions,
addToQuery,
rangeTo,
hasAlertsCrudPermissions,
indexNames,

View file

@ -9,10 +9,13 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { IndexPatternBase } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React, { useCallback, useRef } from 'react';
import React, { useCallback, useRef, useState, useEffect } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { AlertStatus } from '@kbn/rule-data-utils/alerts_as_data_status';
import { ALERT_STATUS } from '@kbn/rule-data-utils/technical_field_names';
import { AlertStatusFilterButton } from '../../../common/typings';
import { ParsedTechnicalFields } from '../../../../rule_registry/common/parse_technical_fields';
import type { AlertWorkflowStatus } from '../../../common/typings';
import { ExperimentalBadge } from '../../components/shared/experimental_badge';
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
import { useFetcher } from '../../hooks/use_fetcher';
@ -26,7 +29,7 @@ import { AlertsSearchBar } from './alerts_search_bar';
import { AlertsTableTGrid } from './alerts_table_t_grid';
import { Provider, alertsPageStateContainer, useAlertsPageStateContainer } from './state_container';
import './styles.scss';
import { WorkflowStatusFilter } from './workflow_status_filter';
import { AlertsStatusFilter } from './alerts_status_filter';
import { AlertsDisclaimer } from './alerts_disclaimer';
export interface TopAlert {
@ -36,25 +39,29 @@ export interface TopAlert {
link?: string;
active: boolean;
}
const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
const NO_INDEX_NAMES: string[] = [];
const NO_INDEX_PATTERNS: IndexPatternBase[] = [];
const BASE_ALERT_REGEX = new RegExp(`\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*"(.*?|\\*?)"`);
const ALERT_STATUS_REGEX = new RegExp(
`\\s*and\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*(".+?"|\\*?)|${regExpEscape(
ALERT_STATUS
)}\\s*:\\s*(".+?"|\\*?)`,
'gm'
);
function AlertsPage() {
const { core, plugins, ObservabilityPageTemplate } = usePluginContext();
const [alertFilterStatus, setAlertFilterStatus] = useState('' as AlertStatusFilterButton);
const { prepend } = core.http.basePath;
const refetch = useRef<() => void>();
const timefilterService = useTimefilterService();
const {
rangeFrom,
setRangeFrom,
rangeTo,
setRangeTo,
kuery,
setKuery,
workflowStatus,
setWorkflowStatus,
} = useAlertsPageStateContainer();
const { rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, workflowStatus } =
useAlertsPageStateContainer();
useEffect(() => {
syncAlertStatusFilterStatus(kuery as string);
}, [kuery]);
useBreadcrumbs([
{
@ -103,36 +110,56 @@ function AlertsPage() {
];
}, [indexNames]);
const setWorkflowStatusFilter = useCallback(
(value: AlertWorkflowStatus) => {
setWorkflowStatus(value);
},
[setWorkflowStatus]
);
// Keep the Workflow status code commented (no delete) as requested: https://github.com/elastic/kibana/issues/117686
// const setWorkflowStatusFilter = useCallback(
// (value: AlertWorkflowStatus) => {
// setWorkflowStatus(value);
// },
// [setWorkflowStatus]
// );
const onQueryChange = useCallback(
({ dateRange, query }) => {
if (rangeFrom === dateRange.from && rangeTo === dateRange.to && kuery === (query ?? '')) {
return refetch.current && refetch.current();
}
timefilterService.setTime(dateRange);
setRangeFrom(dateRange.from);
setRangeTo(dateRange.to);
setKuery(query);
syncAlertStatusFilterStatus(query as string);
},
[rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, timefilterService]
);
const addToQuery = useCallback(
(value: string) => {
let output = value;
if (kuery !== '') {
output = `${kuery} and ${value}`;
const syncAlertStatusFilterStatus = (query: string) => {
const [, alertStatus] = BASE_ALERT_REGEX.exec(query) || [];
if (!alertStatus) {
setAlertFilterStatus('');
return;
}
setAlertFilterStatus(alertStatus.toLowerCase() as AlertStatus);
};
const setAlertStatusFilter = useCallback(
(id: string, query: string) => {
setAlertFilterStatus(id as AlertStatusFilterButton);
// Updating the KQL query bar alongside with user inputs is tricky.
// To avoid issue, this function always remove the AlertFilter and add it
// at the end of the query, each time the filter is added/updated/removed (Show All)
// NOTE: This (query appending) will be changed entirely: https://github.com/elastic/kibana/issues/116135
let output = kuery;
if (kuery === '') {
output = query;
} else {
// console.log(ALERT_STATUS_REGEX);
const queryWithoutAlertFilter = kuery.replace(ALERT_STATUS_REGEX, '');
output = `${queryWithoutAlertFilter} and ${query}`;
}
onQueryChange({
dateRange: { from: rangeFrom, to: rangeTo },
query: output,
// Clean up the kuery from unwanted trailing/ahead ANDs after appending and removing filters.
query: output.replace(/^\s*and\s*|\s*and\s*$/gm, ''),
});
},
[kuery, onQueryChange, rangeFrom, rangeTo]
@ -194,7 +221,9 @@ function AlertsPage() {
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<WorkflowStatusFilter status={workflowStatus} onChange={setWorkflowStatusFilter} />
{/* Keep the Workflow status code commented (no delete) as requested: https://github.com/elastic/kibana/issues/117686*/}
{/* <WorkflowStatusFilter status={workflowStatus} onChange={setWorkflowStatusFilter} /> */}
<AlertsStatusFilter status={alertFilterStatus} onChange={setAlertStatusFilter} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
@ -207,7 +236,6 @@ function AlertsPage() {
kuery={kuery}
workflowStatus={workflowStatus}
setRefetch={setRefetch}
addToQuery={addToQuery}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -186,7 +186,7 @@ export default ({ getService }: FtrProviderContext) => {
});
});
describe('Cell actions', () => {
describe.skip('Cell actions', () => {
beforeEach(async () => {
await retry.try(async () => {
const cells = await observability.alerts.common.getTableCells();

View file

@ -39,7 +39,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await assertAlertsPageState({
kuery: 'kibana.alert.evaluation.threshold > 75',
workflowStatus: 'Closed',
// workflowStatus: 'Closed',
timeRange: '~ a month ago - ~ 10 days ago',
});
});
@ -55,7 +55,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
await assertAlertsPageState({
kuery: '',
workflowStatus: 'Open',
// workflowStatus: 'Open',
timeRange: 'Last 15 minutes',
});
});
@ -77,15 +77,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
async function assertAlertsPageState(expected: {
kuery: string;
workflowStatus: string;
// workflowStatus: string;
timeRange: string;
}) {
expect(await (await observability.alerts.common.getQueryBar()).getVisibleText()).to.be(
expected.kuery
);
expect(await observability.alerts.common.getWorkflowStatusFilterValue()).to.be(
expected.workflowStatus
);
// expect(await observability.alerts.common.getWorkflowStatusFilterValue()).to.be(
// expected.workflowStatus
// );
const timeRange = await observability.alerts.common.getTimeRange();
expect(timeRange).to.be(expected.timeRange);
}

View file

@ -13,7 +13,8 @@ const OPEN_ALERTS_ROWS_COUNT = 33;
export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
describe('alert workflow status', function () {
// Keep the Workflow status code commented (no delete) as requested: https://github.com/elastic/kibana/issues/117686
describe.skip('alert workflow status', function () {
this.tags('includeFirefox');
const observability = getService('observability');