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 <dede.morton@elastic.co>
Co-authored-by: Shahzad <shahzad31comp@gmail.com>
This commit is contained in:
Maryam Saeidi 2024-08-22 12:20:24 +02:00 committed by GitHub
parent 00975ad1a4
commit 34d392b9cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 605 additions and 59 deletions

View file

@ -158,7 +158,7 @@ describe('AlertsGrouping', () => {
},
{
range: {
'@timestamp': {
'kibana.alert.time_range': {
gte: mockDate.from,
lte: mockDate.to,
},

View file

@ -199,7 +199,7 @@ const AlertsGroupingInternal = <T extends BaseAlertsGroupAggregations>(
};
return (
<AlertsGroupingLevel
<AlertsGroupingLevel<T>
{...props}
getGrouping={getGrouping}
groupingLevel={level}

View file

@ -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,
},

View file

@ -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,

View file

@ -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 {

View file

@ -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> = T | null | undefined;
@ -39,6 +40,7 @@ export type AlertStatus =
export interface AlertStatusFilter {
status: AlertStatus;
query: string;
filter: Filter[];
label: string;
}

View file

@ -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,
};

View file

@ -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,
};
};

View file

@ -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<AlertsByGroupingAgg>['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]);
};

View file

@ -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 (
<AlertActions
{...props}
config={config}
observabilityRuleTypeRegistry={observabilityRuleTypeRegistry}
/>
);
};
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,
};
};

View file

@ -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

View file

@ -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<string, unknown> {
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;
};
}

View file

@ -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 && (
<EuiBadge
key="more"
onClick={onMoreTagsClick}
onClickAriaLabel={i18n.translate(
'xpack.observability.alertDetails.alertSummaryField.moreTags.ariaLabel',
{
defaultMessage: 'more tags badge',
}
)}
onClickAriaLabel={i18n.translate('xpack.observability.component.tags.moreTags.ariaLabel', {
defaultMessage: 'more tags badge',
})}
color={color}
>
<FormattedMessage
id="xpack.observability.alertDetails.alertSummaryField.moreTags"
id="xpack.observability.component.tags.moreTags"
defaultMessage="+{number} more"
values={{ number: tags.length - 3 }}
values={{ number: tags.length - size }}
/>
</EuiBadge>
);
return (
<>
{tags.slice(0, 3).map((tag) => (
<EuiBadge key={tag}>{tag}</EuiBadge>
{tags.slice(0, size).map((tag) => (
<EuiBadge key={tag} color={color}>
{tag}
</EuiBadge>
))}
<br />
{oneLine ? ' ' : <br />}
<EuiPopover button={moreTags} isOpen={isMoreTagsOpen} closePopover={closePopover}>
{tags.slice(3).map((tag) => (
<EuiBadge key={tag}>{tag}</EuiBadge>
{tags.slice(size).map((tag) => (
<EuiBadge key={tag} color={color}>
{tag}
</EuiBadge>
))}
</EuiPopover>
</>

View file

@ -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`;

View file

@ -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() {
</EuiFlexItem>
<EuiFlexItem>
{esQuery && (
<AlertsStateTable
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
configurationId={AlertConsumers.OBSERVABILITY}
id={ALERTS_TABLE_ID}
<AlertsGrouping<AlertsByGroupingAgg>
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 (
<AlertsStateTable
id={ALERTS_TABLE_ID}
featureIds={observabilityAlertFeatureIds}
configurationId={ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID}
query={mergeBoolQueries(esQuery, groupQuery)}
showAlertStatusWithFlapping
initialPageSize={ALERTS_PER_PAGE}
cellContext={{ observabilityRuleTypeRegistry }}
alertsTableConfigurationRegistry={alertsTableConfigurationRegistry}
/>
);
}}
</AlertsGrouping>
)}
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -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,
},
];

View file

@ -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,
},
},
},
];
}
};

View file

@ -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<AlertsByGroupingAgg> = (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,
];
};

View file

@ -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<AlertsByGroupingAgg> = (
selectedGroup,
bucket
) => {
switch (selectedGroup) {
case 'kibana.alert.rule.name':
return isArray(bucket.key) ? (
<RuleNameGroupContent
ruleName={bucket.key[0]}
tags={bucket.ruleTags?.buckets.map((tag: any) => tag.key)}
/>
) : undefined;
case 'kibana.alert.instance.id':
return <InstanceIdGroupContent instanceId={firstNonNullValue(bucket.key)} />;
}
};
const RuleNameGroupContent = React.memo<{
ruleName: string;
tags?: string[] | undefined;
}>(({ ruleName, tags }) => {
return (
<div style={{ display: 'table', tableLayout: 'fixed', width: '100%' }}>
<EuiFlexGroup data-test-subj="rule-name-group-renderer" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false} style={{ display: 'contents' }}>
<EuiTitle size="xs">
<h5 className="eui-textTruncate">{ruleName}</h5>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
{!!tags && tags.length > 0 && (
<EuiText size="s">
<Tags tags={tags} color="hollow" size={5} oneLine />
</EuiText>
)}
</div>
);
});
RuleNameGroupContent.displayName = 'RuleNameGroup';
const InstanceIdGroupContent = React.memo<{
instanceId?: string;
}>(({ instanceId }) => {
const isUngrouped = instanceId === '*';
return (
<div style={{ display: 'table', tableLayout: 'fixed', width: '100%' }}>
<EuiFlexGroup data-test-subj="rule-name-group-renderer" gutterSize="m" alignItems="center">
<EuiFlexItem grow={false} style={{ display: 'contents' }}>
<EuiTitle size="xs">
<h5 className="eui-textTruncate">
{isUngrouped ? ungrouped : instanceId ?? '--'}
&nbsp;
{isUngrouped && (
<EuiIconTip
content={
<FormattedMessage
id="xpack.observability.alert.grouping.ungrouped.info"
defaultMessage='There is no "group by" field selected in the rule definition.'
/>
}
/>
)}
</h5>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
});
InstanceIdGroupContent.displayName = 'InstanceIdGroupContent';

View file

@ -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],
},
};
};

View file

@ -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
);
});

View file

@ -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/**/*"

View file

@ -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."

View file

@ -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,

View file

@ -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",

View file

@ -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": "タグ",

View file

@ -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": "标签",