From 34d392b9cd7d9a78a35dc8b3b84dd6d81fc9afac Mon Sep 17 00:00:00 2001 From: Maryam Saeidi Date: Thu, 22 Aug 2024 12:20:24 +0200 Subject: [PATCH] Add alert grouping functionality to the observability alerts page (#189958) Closes #190995 ## Summary This PR adds grouping functionality to the alerts page alert table based on @umbopepato's implementation in this [draft PR](https://github.com/elastic/kibana/pull/183114) (basically, he implemented the feature and I adjusted a bit for our use case :D). For now, we only added the **rule** and **source** as default grouping, and I will create a ticket to add tags as well. The challenge with tags is that since it is an array, the value of the alert is joined by a comma as the group, which does not match with what we want for tags. ![image](https://github.com/user-attachments/assets/c08c3cb1-4c6c-4918-8071-3c5913de41f6) Here is how we show the rules that don't have a group by field selected for them: (We used "ungrouped" similar to what we have in SLOs) ![image](https://github.com/user-attachments/assets/280bbd34-6c3b-41c1-803b-dcc6448f6fb4) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: DeDe Morton Co-authored-by: Shahzad --- .../src/components/alerts_grouping.test.tsx | 2 +- .../src/components/alerts_grouping.tsx | 2 +- .../src/components/alerts_grouping_level.tsx | 3 +- .../src/contexts/alerts_grouping_context.tsx | 1 - packages/kbn-alerts-grouping/src/types.ts | 2 +- .../observability/common/typings.ts | 2 + .../components/alert_search_bar/constants.ts | 37 ++++++++ .../get_alerts_page_table_configuration.tsx | 27 +++++- .../alerts/get_persistent_controls.ts | 66 ++++++++++++++ .../get_alerts_page_table_configuration.tsx | 58 +++++++++++++ .../register_alerts_table_configuration.tsx | 23 ++++- .../public/components/alerts_table/types.ts | 36 ++++++++ .../observability/public/components/tags.tsx | 47 ++++++---- .../observability/public/constants.ts | 1 + .../public/pages/alerts/alerts.tsx | 80 ++++++++++++----- .../public/pages/alerts/grouping/constants.ts | 32 +++++++ .../get_aggregations_by_grouping_field.ts | 53 +++++++++++ .../pages/alerts/grouping/get_group_stats.tsx | 57 ++++++++++++ .../alerts/grouping/render_group_panel.tsx | 87 +++++++++++++++++++ .../alerts/helpers/merge_bool_queries.ts | 25 ++++++ .../observability/public/plugin.ts | 10 ++- .../observability/tsconfig.json | 3 + .../server/alert_data_client/alerts_client.ts | 2 +- .../get_alerts_group_aggregations.test.ts | 2 +- .../translations/translations/fr-FR.json | 2 - .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - 27 files changed, 605 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_persistent_controls.ts create mode 100644 x-pack/plugins/observability_solution/observability/public/components/alerts_table/observability/get_alerts_page_table_configuration.tsx create mode 100644 x-pack/plugins/observability_solution/observability/public/components/alerts_table/types.ts create mode 100644 x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/constants.ts create mode 100644 x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_aggregations_by_grouping_field.ts create mode 100644 x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_group_stats.tsx create mode 100644 x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/render_group_panel.tsx create mode 100644 x-pack/plugins/observability_solution/observability/public/pages/alerts/helpers/merge_bool_queries.ts diff --git a/packages/kbn-alerts-grouping/src/components/alerts_grouping.test.tsx b/packages/kbn-alerts-grouping/src/components/alerts_grouping.test.tsx index 87517def778cd9..47e2d5c1b40821 100644 --- a/packages/kbn-alerts-grouping/src/components/alerts_grouping.test.tsx +++ b/packages/kbn-alerts-grouping/src/components/alerts_grouping.test.tsx @@ -158,7 +158,7 @@ describe('AlertsGrouping', () => { }, { range: { - '@timestamp': { + 'kibana.alert.time_range': { gte: mockDate.from, lte: mockDate.to, }, diff --git a/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx b/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx index 5db1ef5a5d0ff3..17a4d35f73e8ae 100644 --- a/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx +++ b/packages/kbn-alerts-grouping/src/components/alerts_grouping.tsx @@ -199,7 +199,7 @@ const AlertsGroupingInternal = ( }; return ( - {...props} getGrouping={getGrouping} groupingLevel={level} diff --git a/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx b/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx index a82818215cbf4d..c0ebf0e6fa2342 100644 --- a/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx +++ b/packages/kbn-alerts-grouping/src/components/alerts_grouping_level.tsx @@ -14,6 +14,7 @@ import { type GroupingAggregation } from '@kbn/grouping'; import { isNoneGroup } from '@kbn/grouping'; import type { DynamicGroupingProps } from '@kbn/grouping/src'; import { parseGroupingQuery } from '@kbn/grouping/src'; +import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils'; import { useGetAlertsGroupAggregationsQuery, UseGetAlertsGroupAggregationsQueryProps, @@ -94,7 +95,7 @@ export const AlertsGroupingLevel = typedMemo( ...filters, { range: { - '@timestamp': { + [ALERT_TIME_RANGE]: { gte: from, lte: to, }, diff --git a/packages/kbn-alerts-grouping/src/contexts/alerts_grouping_context.tsx b/packages/kbn-alerts-grouping/src/contexts/alerts_grouping_context.tsx index cc5e06e652cd46..2d1315e3ece6d4 100644 --- a/packages/kbn-alerts-grouping/src/contexts/alerts_grouping_context.tsx +++ b/packages/kbn-alerts-grouping/src/contexts/alerts_grouping_context.tsx @@ -54,7 +54,6 @@ export const useAlertsGroupingState = (groupingId: string) => { setGroupingState((prevState) => ({ ...prevState, [groupingId]: { - // @ts-expect-error options might not be defined options: [], // @ts-expect-error activeGroups might not be defined activeGroups: initialActiveGroups, diff --git a/packages/kbn-alerts-grouping/src/types.ts b/packages/kbn-alerts-grouping/src/types.ts index 835941e8db95d5..24239364bb6c2e 100644 --- a/packages/kbn-alerts-grouping/src/types.ts +++ b/packages/kbn-alerts-grouping/src/types.ts @@ -22,7 +22,7 @@ import { ReactElement } from 'react'; export interface GroupModel { activeGroups: string[]; - options: Array<{ key: string; label: string }>; + options?: Array<{ key: string; label: string }>; } export interface AlertsGroupingState { diff --git a/x-pack/plugins/observability_solution/observability/common/typings.ts b/x-pack/plugins/observability_solution/observability/common/typings.ts index bfdcd6d5209dc0..03981f5941dc24 100644 --- a/x-pack/plugins/observability_solution/observability/common/typings.ts +++ b/x-pack/plugins/observability_solution/observability/common/typings.ts @@ -11,6 +11,7 @@ import { ALERT_STATUS_RECOVERED, ALERT_STATUS_UNTRACKED, } from '@kbn/rule-data-utils'; +import { Filter } from '@kbn/es-query'; import { ALERT_STATUS_ALL } from './constants'; export type Maybe = T | null | undefined; @@ -39,6 +40,7 @@ export type AlertStatus = export interface AlertStatusFilter { status: AlertStatus; query: string; + filter: Filter[]; label: string; } diff --git a/x-pack/plugins/observability_solution/observability/public/components/alert_search_bar/constants.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_search_bar/constants.ts index 85ea6464d5ac0e..dc6af6316c41c0 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alert_search_bar/constants.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_search_bar/constants.ts @@ -22,6 +22,7 @@ export const DEFAULT_QUERY_STRING = ''; export const ALL_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ALL, query: '', + filter: [], label: i18n.translate('xpack.observability.alerts.alertStatusFilter.showAll', { defaultMessage: 'Show all', }), @@ -30,6 +31,16 @@ export const ALL_ALERTS: AlertStatusFilter = { export const ACTIVE_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ACTIVE, query: `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`, + filter: [ + { + query: { + match_phrase: { + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + }, + }, + meta: {}, + }, + ], label: i18n.translate('xpack.observability.alerts.alertStatusFilter.active', { defaultMessage: 'Active', }), @@ -38,6 +49,16 @@ export const ACTIVE_ALERTS: AlertStatusFilter = { export const RECOVERED_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_RECOVERED, query: `${ALERT_STATUS}: "${ALERT_STATUS_RECOVERED}"`, + filter: [ + { + query: { + match_phrase: { + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + }, + }, + meta: {}, + }, + ], label: i18n.translate('xpack.observability.alerts.alertStatusFilter.recovered', { defaultMessage: 'Recovered', }), @@ -46,6 +67,16 @@ export const RECOVERED_ALERTS: AlertStatusFilter = { export const UNTRACKED_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_UNTRACKED, query: `${ALERT_STATUS}: "${ALERT_STATUS_UNTRACKED}"`, + filter: [ + { + query: { + match_phrase: { + [ALERT_STATUS]: ALERT_STATUS_UNTRACKED, + }, + }, + meta: {}, + }, + ], label: i18n.translate('xpack.observability.alerts.alertStatusFilter.untracked', { defaultMessage: 'Untracked', }), @@ -56,3 +87,9 @@ export const ALERT_STATUS_QUERY = { [RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query, [UNTRACKED_ALERTS.status]: UNTRACKED_ALERTS.query, }; + +export const ALERT_STATUS_FILTER = { + [ACTIVE_ALERTS.status]: ACTIVE_ALERTS.filter, + [RECOVERED_ALERTS.status]: RECOVERED_ALERTS.filter, + [UNTRACKED_ALERTS.status]: UNTRACKED_ALERTS.filter, +}; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx index cabf1d6d6f34e6..30c912b5107436 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_alerts_page_table_configuration.tsx @@ -12,17 +12,29 @@ import { AlertsTableConfigurationRegistry, RenderCustomActionsRowArgs, } from '@kbn/triggers-actions-ui-plugin/public/types'; -import { casesFeatureId, observabilityFeatureId } from '../../../../common'; +import { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types'; +import { HttpSetup } from '@kbn/core-http-browser'; +import { NotificationsStart } from '@kbn/core-notifications-browser'; +import { + casesFeatureId, + observabilityAlertFeatureIds, + observabilityFeatureId, +} from '../../../../common'; import { AlertActions } from '../../../pages/alerts/components/alert_actions'; import { useGetAlertFlyoutComponents } from '../../alerts_flyout/use_get_alert_flyout_components'; import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; +import { ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID } from '../../../constants'; import type { ConfigSchema } from '../../../plugin'; import { getRenderCellValue } from '../common/render_cell_value'; import { getColumns } from '../common/get_columns'; +import { getPersistentControlsHook } from './get_persistent_controls'; export const getAlertsPageTableConfiguration = ( observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry, - config: ConfigSchema + config: ConfigSchema, + dataViews: DataViewsServicePublic, + http: HttpSetup, + notifications: NotificationsStart ): AlertsTableConfigurationRegistry => { const renderCustomActionsRow = (props: RenderCustomActionsRowArgs) => { return ( @@ -34,7 +46,7 @@ export const getAlertsPageTableConfiguration = ( ); }; return { - id: observabilityFeatureId, + id: ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID, cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] }, columns: getColumns({ showRuleName: true }), getRenderCellValue, @@ -53,6 +65,15 @@ export const getAlertsPageTableConfiguration = ( return { header, body, footer }; }, ruleTypeIds: observabilityRuleTypeRegistry.list(), + usePersistentControls: getPersistentControlsHook({ + groupingId: ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID, + featureIds: observabilityAlertFeatureIds, + services: { + dataViews, + http, + notifications, + }, + }), showInspectButton: true, }; }; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_persistent_controls.ts b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_persistent_controls.ts new file mode 100644 index 00000000000000..2141e0fb68d66b --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/alerts/get_persistent_controls.ts @@ -0,0 +1,66 @@ +/* + * 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 { useMemo, useCallback } from 'react'; +import { type AlertsGroupingProps, useAlertsGroupingState } from '@kbn/alerts-grouping'; +import { useAlertsDataView } from '@kbn/alerts-ui-shared/src/common/hooks/use_alerts_data_view'; +import { useGetGroupSelectorStateless } from '@kbn/grouping/src/hooks/use_get_group_selector'; +import { AlertConsumers } from '@kbn/rule-data-utils'; +import { AlertsByGroupingAgg } from '../types'; + +interface GetPersistentControlsParams { + groupingId: string; + featureIds: AlertConsumers[]; + maxGroupingLevels?: number; + services: Pick< + AlertsGroupingProps['services'], + 'dataViews' | 'http' | 'notifications' + >; +} + +export const getPersistentControlsHook = + ({ + groupingId, + featureIds, + maxGroupingLevels = 3, + services: { dataViews, http, notifications }, + }: GetPersistentControlsParams) => + () => { + const { grouping, updateGrouping } = useAlertsGroupingState(groupingId); + + const onGroupChange = useCallback( + (selectedGroups: string[]) => { + updateGrouping({ + activeGroups: + grouping.activeGroups?.filter((g) => g !== 'none').concat(selectedGroups) ?? [], + }); + }, + [grouping, updateGrouping] + ); + + const { dataView } = useAlertsDataView({ + featureIds, + dataViewsService: dataViews, + http, + toasts: notifications.toasts, + }); + + const groupSelector = useGetGroupSelectorStateless({ + groupingId, + onGroupChange, + fields: dataView?.fields ?? [], + defaultGroupingOptions: + grouping.options?.filter((option) => !grouping.activeGroups.includes(option.key)) ?? [], + maxGroupingLevels, + }); + + return useMemo(() => { + return { + right: groupSelector, + }; + }, [groupSelector]); + }; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/observability/get_alerts_page_table_configuration.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/observability/get_alerts_page_table_configuration.tsx new file mode 100644 index 00000000000000..9d761aa87f4cdc --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/observability/get_alerts_page_table_configuration.tsx @@ -0,0 +1,58 @@ +/* + * 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 React from 'react'; +import { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { ALERT_START, AlertConsumers } from '@kbn/rule-data-utils'; +import { + AlertsTableConfigurationRegistry, + RenderCustomActionsRowArgs, +} from '@kbn/triggers-actions-ui-plugin/public/types'; +import { casesFeatureId, observabilityFeatureId } from '../../../../common'; +import { AlertActions } from '../../../pages/alerts/components/alert_actions'; +import { useGetAlertFlyoutComponents } from '../../alerts_flyout/use_get_alert_flyout_components'; +import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observability_rule_type_registry'; +import type { ConfigSchema } from '../../../plugin'; +import { getRenderCellValue } from '../common/render_cell_value'; +import { getColumns } from '../common/get_columns'; + +export const getObservabilityTableConfiguration = ( + observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry, + config: ConfigSchema +): AlertsTableConfigurationRegistry => { + const renderCustomActionsRow = (props: RenderCustomActionsRowArgs) => { + return ( + + ); + }; + return { + id: AlertConsumers.OBSERVABILITY, + cases: { featureId: casesFeatureId, owner: [observabilityFeatureId] }, + columns: getColumns({ showRuleName: true }), + getRenderCellValue, + sort: [ + { + [ALERT_START]: { + order: 'desc' as SortOrder, + }, + }, + ], + useActionsColumn: () => ({ + renderCustomActionsRow, + }), + useInternalFlyout: () => { + const { header, body, footer } = useGetAlertFlyoutComponents(observabilityRuleTypeRegistry); + return { header, body, footer }; + }, + ruleTypeIds: observabilityRuleTypeRegistry.list(), + showInspectButton: true, + }; +}; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/register_alerts_table_configuration.tsx b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/register_alerts_table_configuration.tsx index 1fa574d1dd4026..de687c4dd7944c 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/register_alerts_table_configuration.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/register_alerts_table_configuration.tsx @@ -6,22 +6,39 @@ */ import { AlertTableConfigRegistry } from '@kbn/triggers-actions-ui-plugin/public/application/alert_table_config_registry'; +import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types'; +import { HttpSetup } from '@kbn/core-http-browser'; +import { NotificationsStart } from '@kbn/core-notifications-browser'; import type { ConfigSchema } from '../../plugin'; import { ObservabilityRuleTypeRegistry } from '../..'; import { getAlertsPageTableConfiguration } from './alerts/get_alerts_page_table_configuration'; import { getRuleDetailsTableConfiguration } from './rule_details/get_rule_details_table_configuration'; import { getSloAlertsTableConfiguration } from './slo/get_slo_alerts_table_configuration'; +import { getObservabilityTableConfiguration } from './observability/get_alerts_page_table_configuration'; export const registerAlertsTableConfiguration = ( alertTableConfigRegistry: AlertTableConfigRegistry, observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry, - config: ConfigSchema + config: ConfigSchema, + dataViews: DataViewsServicePublic, + http: HttpSetup, + notifications: NotificationsStart ) => { - // Alert page - const alertsPageAlertsTableConfig = getAlertsPageTableConfiguration( + // Observability table + const observabilityAlertsTableConfig = getObservabilityTableConfiguration( observabilityRuleTypeRegistry, config ); + alertTableConfigRegistry.register(observabilityAlertsTableConfig); + + // Alerts page + const alertsPageAlertsTableConfig = getAlertsPageTableConfiguration( + observabilityRuleTypeRegistry, + config, + dataViews, + http, + notifications + ); alertTableConfigRegistry.register(alertsPageAlertsTableConfig); // Rule details page diff --git a/x-pack/plugins/observability_solution/observability/public/components/alerts_table/types.ts b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/types.ts new file mode 100644 index 00000000000000..477117999a8ca2 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/components/alerts_table/types.ts @@ -0,0 +1,36 @@ +/* + * 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 interface BucketItem { + key: string; + doc_count: number; +} + +export interface AlertsByGroupingAgg extends Record { + groupByFields: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: BucketItem[]; + }; + ruleTags: { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: BucketItem[]; + }; + rulesCountAggregation?: { + value: number; + }; + sourceCountAggregation?: { + value: number; + }; + groupsCount: { + value: number; + }; + unitsCount: { + value: number; + }; +} diff --git a/x-pack/plugins/observability_solution/observability/public/components/tags.tsx b/x-pack/plugins/observability_solution/observability/public/components/tags.tsx index e7059463ef7bd7..015e911c535be5 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/tags.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/tags.tsx @@ -10,38 +10,53 @@ import React, { useState } from 'react'; import { EuiBadge, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -export function Tags({ tags }: { tags: string[] }) { +export function Tags({ + tags, + color, + size = 3, + oneLine = false, +}: { + tags: string[]; + color?: string; + size?: number; + oneLine?: boolean; +}) { const [isMoreTagsOpen, setIsMoreTagsOpen] = useState(false); - const onMoreTagsClick = () => setIsMoreTagsOpen((isPopoverOpen) => !isPopoverOpen); + const onMoreTagsClick = (e: any) => { + e.stopPropagation(); + setIsMoreTagsOpen((isPopoverOpen) => !isPopoverOpen); + }; const closePopover = () => setIsMoreTagsOpen(false); - const moreTags = tags.length > 3 && ( + const moreTags = tags.length > size && ( ); return ( <> - {tags.slice(0, 3).map((tag) => ( - {tag} + {tags.slice(0, size).map((tag) => ( + + {tag} + ))} -
+ {oneLine ? ' ' :
} - {tags.slice(3).map((tag) => ( - {tag} + {tags.slice(size).map((tag) => ( + + {tag} + ))} diff --git a/x-pack/plugins/observability_solution/observability/public/constants.ts b/x-pack/plugins/observability_solution/observability/public/constants.ts index 2da72ff858283a..768094ec8a66b7 100644 --- a/x-pack/plugins/observability_solution/observability/public/constants.ts +++ b/x-pack/plugins/observability_solution/observability/public/constants.ts @@ -8,4 +8,5 @@ export const DEFAULT_INTERVAL = '60s'; export const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm'; +export const ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID = `alerts-page-alerts-table`; export const RULE_DETAILS_ALERTS_TABLE_CONFIG_ID = `rule-details-alerts-table`; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx index 0d3933b6204f4c..c1d14165f5f6e4 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/alerts.tsx @@ -11,20 +11,22 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { BoolQuery } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { AlertsGrouping } from '@kbn/alerts-grouping'; +import { renderGroupPanel } from './grouping/render_group_panel'; import { rulesLocatorID } from '../../../common'; -import { RulesParams } from '../../locators/rules'; -import { useKibana } from '../../utils/kibana_react'; +import { ALERT_STATUS_FILTER } from '../../components/alert_search_bar/constants'; +import { AlertsByGroupingAgg } from '../../components/alerts_table/types'; +import { ObservabilityAlertSearchBar } from '../../components/alert_search_bar/alert_search_bar'; +import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTimeBuckets } from '../../hooks/use_time_buckets'; -import { useGetFilteredRuleTypes } from '../../hooks/use_get_filtered_rule_types'; import { useToasts } from '../../hooks/use_toast'; -import { renderRuleStats, RuleStatsState } from './components/rule_stats'; -import { ObservabilityAlertSearchBar } from '../../components/alert_search_bar/alert_search_bar'; +import { RulesParams } from '../../locators/rules'; +import { useKibana } from '../../utils/kibana_react'; import { alertSearchBarStateContainer, Provider, @@ -34,8 +36,15 @@ import { calculateTimeRangeBucketSize } from '../overview/helpers/calculate_buck import { getAlertSummaryTimeRange } from '../../utils/alert_summary_widget'; import { observabilityAlertFeatureIds } from '../../../common/constants'; import { ALERTS_URL_STORAGE_KEY } from '../../../common/constants'; +import { ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID } from '../../constants'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; import { useGetAvailableRulesWithDescriptions } from '../../hooks/use_get_available_rules_with_descriptions'; +import { buildEsQuery } from '../../utils/build_es_query'; +import { renderRuleStats, RuleStatsState } from './components/rule_stats'; +import { getGroupStats } from './grouping/get_group_stats'; +import { getAggregationsByGroupingField } from './grouping/get_aggregations_by_grouping_field'; +import { DEFAULT_GROUPING_OPTIONS } from './grouping/constants'; +import { mergeBoolQueries } from './helpers/merge_bool_queries'; const ALERTS_SEARCH_BAR_ID = 'alerts-search-bar-o11y'; const ALERTS_PER_PAGE = 50; @@ -48,13 +57,10 @@ function InternalAlertsPage() { const kibanaServices = useKibana().services; const { charts, - data: { - query: { - timefilter: { timefilter: timeFilterService }, - }, - }, + data, http, - notifications: { toasts }, + notifications, + dataViews, observabilityAIAssistant, share: { url: { locators }, @@ -67,6 +73,12 @@ function InternalAlertsPage() { }, uiSettings, } = kibanaServices; + const { toasts } = notifications; + const { + query: { + timefilter: { timefilter: timeFilterService }, + }, + } = data; const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext(); const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, { replace: false, @@ -241,16 +253,42 @@ function InternalAlertsPage() { {esQuery && ( - featureIds={observabilityAlertFeatureIds} - query={esQuery} - showAlertStatusWithFlapping - initialPageSize={ALERTS_PER_PAGE} - cellContext={{ observabilityRuleTypeRegistry }} - /> + defaultFilters={ALERT_STATUS_FILTER[alertSearchBarStateProps.status] ?? []} + from={alertSearchBarStateProps.rangeFrom} + to={alertSearchBarStateProps.rangeTo} + globalFilters={alertSearchBarStateProps.filters} + globalQuery={{ query: alertSearchBarStateProps.kuery, language: 'kuery' }} + groupingId={ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID} + defaultGroupingOptions={DEFAULT_GROUPING_OPTIONS} + getAggregationsByGroupingField={getAggregationsByGroupingField} + renderGroupPanel={renderGroupPanel} + getGroupStats={getGroupStats} + services={{ + notifications, + dataViews, + http, + }} + > + {(groupingFilters) => { + const groupQuery = buildEsQuery({ + filters: groupingFilters, + }); + return ( + + ); + }} + )} diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/constants.ts b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/constants.ts new file mode 100644 index 00000000000000..6bfe2f0febdd53 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/constants.ts @@ -0,0 +1,32 @@ +/* + * 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'; +import { ALERT_RULE_NAME, ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; + +export const ungrouped = i18n.translate('xpack.observability.alert.grouping.ungrouped.label', { + defaultMessage: 'Ungrouped', +}); + +export const ruleName = i18n.translate('xpack.observability.alert.grouping.ruleName.label', { + defaultMessage: 'Rule name', +}); + +export const source = i18n.translate('xpack.observability.alert.grouping.source.label', { + defaultMessage: 'Source', +}); + +export const DEFAULT_GROUPING_OPTIONS = [ + { + label: ruleName, + key: ALERT_RULE_NAME, + }, + { + label: source, + key: ALERT_INSTANCE_ID, + }, +]; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_aggregations_by_grouping_field.ts b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_aggregations_by_grouping_field.ts new file mode 100644 index 00000000000000..e4c8b27225ea54 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_aggregations_by_grouping_field.ts @@ -0,0 +1,53 @@ +/* + * 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 { NamedAggregation } from '@kbn/grouping'; +import { ALERT_INSTANCE_ID, ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; + +export const getAggregationsByGroupingField = (field: string): NamedAggregation[] => { + switch (field) { + case ALERT_RULE_NAME: + return [ + { + sourceCountAggregation: { + cardinality: { + field: ALERT_INSTANCE_ID, + }, + }, + }, + { + ruleTags: { + terms: { + field: 'tags', + }, + }, + }, + ]; + break; + case ALERT_INSTANCE_ID: + return [ + { + rulesCountAggregation: { + cardinality: { + field: ALERT_RULE_UUID, + }, + }, + }, + ]; + break; + default: + return [ + { + rulesCountAggregation: { + cardinality: { + field: ALERT_RULE_UUID, + }, + }, + }, + ]; + } +}; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_group_stats.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_group_stats.tsx new file mode 100644 index 00000000000000..3fe0a6d0068258 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/get_group_stats.tsx @@ -0,0 +1,57 @@ +/* + * 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 { GetGroupStats } from '@kbn/grouping/src'; +import { ALERT_INSTANCE_ID, ALERT_RULE_NAME } from '@kbn/rule-data-utils'; +import { AlertsByGroupingAgg } from '../../../components/alerts_table/types'; + +export const getGroupStats: GetGroupStats = (selectedGroup, bucket) => { + const defaultBadges = [ + { + title: 'Alerts:', + badge: { + value: bucket.doc_count, + width: 50, + }, + }, + ]; + + switch (selectedGroup) { + case ALERT_RULE_NAME: + return [ + { + title: 'Sources:', + badge: { + value: bucket.sourceCountAggregation?.value ?? 0, + width: 50, + }, + }, + ...defaultBadges, + ]; + case ALERT_INSTANCE_ID: + return [ + { + title: 'Rules:', + badge: { + value: bucket.rulesCountAggregation?.value ?? 0, + width: 50, + }, + }, + ...defaultBadges, + ]; + } + return [ + { + title: 'Rules:', + badge: { + value: bucket.rulesCountAggregation?.value ?? 0, + width: 50, + }, + }, + ...defaultBadges, + ]; +}; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/render_group_panel.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/render_group_panel.tsx new file mode 100644 index 00000000000000..17e674eb0a44e5 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/grouping/render_group_panel.tsx @@ -0,0 +1,87 @@ +/* + * 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 React from 'react'; +import { isArray } from 'lodash/fp'; +import { EuiFlexGroup, EuiIconTip, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; +import { firstNonNullValue, GroupPanelRenderer } from '@kbn/grouping/src'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { AlertsByGroupingAgg } from '../../../components/alerts_table/types'; +import { Tags } from '../../../components/tags'; +import { ungrouped } from './constants'; + +export const renderGroupPanel: GroupPanelRenderer = ( + selectedGroup, + bucket +) => { + switch (selectedGroup) { + case 'kibana.alert.rule.name': + return isArray(bucket.key) ? ( + tag.key)} + /> + ) : undefined; + case 'kibana.alert.instance.id': + return ; + } +}; + +const RuleNameGroupContent = React.memo<{ + ruleName: string; + tags?: string[] | undefined; +}>(({ ruleName, tags }) => { + return ( +
+ + + +
{ruleName}
+
+
+
+ + {!!tags && tags.length > 0 && ( + + + + )} +
+ ); +}); +RuleNameGroupContent.displayName = 'RuleNameGroup'; + +const InstanceIdGroupContent = React.memo<{ + instanceId?: string; +}>(({ instanceId }) => { + const isUngrouped = instanceId === '*'; + return ( +
+ + + +
+ {isUngrouped ? ungrouped : instanceId ?? '--'} +   + {isUngrouped && ( + + } + /> + )} +
+
+
+
+
+ ); +}); +InstanceIdGroupContent.displayName = 'InstanceIdGroupContent'; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alerts/helpers/merge_bool_queries.ts b/x-pack/plugins/observability_solution/observability/public/pages/alerts/helpers/merge_bool_queries.ts new file mode 100644 index 00000000000000..bd748f4e5b9283 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alerts/helpers/merge_bool_queries.ts @@ -0,0 +1,25 @@ +/* + * 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 { BoolQuery } from '@kbn/es-query'; + +export const mergeBoolQueries = ( + firstQuery: { bool: BoolQuery }, + secondQuery: { bool: BoolQuery } +): { bool: BoolQuery } => { + const first = firstQuery.bool; + const second = secondQuery.bool; + + return { + bool: { + must: [...first.must, ...second.must], + must_not: [...first.must_not, ...second.must_not], + filter: [...first.filter, ...second.filter], + should: [...first.should, ...second.should], + }, + }; +}; diff --git a/x-pack/plugins/observability_solution/observability/public/plugin.ts b/x-pack/plugins/observability_solution/observability/public/plugin.ts index b2d0e526f3c64c..43967f1339c5cc 100644 --- a/x-pack/plugins/observability_solution/observability/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability/public/plugin.ts @@ -445,14 +445,18 @@ export class Plugin } public start(coreStart: CoreStart, pluginsStart: ObservabilityPublicPluginsStart) { - const { application } = coreStart; + const { application, http, notifications } = coreStart; + const { dataViews, triggersActionsUi } = pluginsStart; const config = this.initContext.config.get(); - const { alertsTableConfigurationRegistry } = pluginsStart.triggersActionsUi; + const { alertsTableConfigurationRegistry } = triggersActionsUi; this.lazyRegisterAlertsTableConfiguration().then(({ registerAlertsTableConfiguration }) => { return registerAlertsTableConfiguration( alertsTableConfigurationRegistry, this.observabilityRuleTypeRegistry, - config + config, + dataViews, + http, + notifications ); }); diff --git a/x-pack/plugins/observability_solution/observability/tsconfig.json b/x-pack/plugins/observability_solution/observability/tsconfig.json index 0a65077d42a1ed..72609a3ada8bd5 100644 --- a/x-pack/plugins/observability_solution/observability/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability/tsconfig.json @@ -112,6 +112,9 @@ "@kbn/core-ui-settings-server-mocks", "@kbn/investigate-plugin", "@kbn/investigation-shared", + "@kbn/grouping", + "@kbn/alerts-grouping", + "@kbn/core-http-browser" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts index 76a39737190ce1..8d61b2b4e5609e 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts @@ -1133,7 +1133,7 @@ export class AlertsClient { script: { source: // When size()==0, emits a uniqueValue as the value to represent this group else join by uniqueValue. - "if (doc[params['selectedGroup']].size()==0) { emit(params['uniqueValue']) }" + + "if (!doc.containsKey(params['selectedGroup']) || doc[params['selectedGroup']].size()==0) { emit(params['uniqueValue']) }" + // Else, join the values with uniqueValue. We cannot simply emit the value like doc[params['selectedGroup']].value, // the runtime field will only return the first value in an array. // The docs advise that if the field has multiple values, "Scripts can call the emit method multiple times to emit multiple values." diff --git a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts index af10edf3723838..c351de1283c2b0 100644 --- a/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts +++ b/x-pack/plugins/rule_registry/server/alert_data_client/tests/get_alerts_group_aggregations.test.ts @@ -127,7 +127,7 @@ describe('getGroupAggregations()', () => { type: 'keyword', script: { source: - "if (doc[params['selectedGroup']].size()==0) { emit(params['uniqueValue']) }" + + "if (!doc.containsKey(params['selectedGroup']) || doc[params['selectedGroup']].size()==0) { emit(params['uniqueValue']) }" + " else { emit(doc[params['selectedGroup']].join(params['uniqueValue']))}", params: { selectedGroup: groupByField, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a0443e37fa617a..1ed13e47986fb5 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -32363,8 +32363,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "Aidez moi à comprendre cette alerte", "xpack.observability.alertDetails.actionsButtonLabel": "Actions", "xpack.observability.alertDetails.addToCase": "Ajouter au cas", - "xpack.observability.alertDetails.alertSummaryField.moreTags": "+{number} de plus", - "xpack.observability.alertDetails.alertSummaryField.moreTags.ariaLabel": "badge plus de balises", "xpack.observability.alertDetails.alertSummaryField.rule": "Règle", "xpack.observability.alertDetails.alertSummaryField.source": "Source", "xpack.observability.alertDetails.alertSummaryField.tags": "Balises", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e6629873082711..b4224908c64a52 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -32347,8 +32347,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "このアラートを理解できるように支援してください", "xpack.observability.alertDetails.actionsButtonLabel": "アクション", "xpack.observability.alertDetails.addToCase": "ケースに追加", - "xpack.observability.alertDetails.alertSummaryField.moreTags": "その他{number}", - "xpack.observability.alertDetails.alertSummaryField.moreTags.ariaLabel": "その他のタグバッジ", "xpack.observability.alertDetails.alertSummaryField.rule": "ルール", "xpack.observability.alertDetails.alertSummaryField.source": "送信元", "xpack.observability.alertDetails.alertSummaryField.tags": "タグ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ef6badee07f440..78ff3faf194f1e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -32387,8 +32387,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "帮助我了解此告警", "xpack.observability.alertDetails.actionsButtonLabel": "操作", "xpack.observability.alertDetails.addToCase": "添加到案例", - "xpack.observability.alertDetails.alertSummaryField.moreTags": "+ 另外 {number} 个", - "xpack.observability.alertDetails.alertSummaryField.moreTags.ariaLabel": "更多标签徽章", "xpack.observability.alertDetails.alertSummaryField.rule": "规则", "xpack.observability.alertDetails.alertSummaryField.source": "源", "xpack.observability.alertDetails.alertSummaryField.tags": "标签",