mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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.  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)  --------- 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:
parent
00975ad1a4
commit
34d392b9cd
27 changed files with 605 additions and 59 deletions
|
@ -158,7 +158,7 @@ describe('AlertsGrouping', () => {
|
|||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
'kibana.alert.time_range': {
|
||||
gte: mockDate.from,
|
||||
lte: mockDate.to,
|
||||
},
|
||||
|
|
|
@ -199,7 +199,7 @@ const AlertsGroupingInternal = <T extends BaseAlertsGroupAggregations>(
|
|||
};
|
||||
|
||||
return (
|
||||
<AlertsGroupingLevel
|
||||
<AlertsGroupingLevel<T>
|
||||
{...props}
|
||||
getGrouping={getGrouping}
|
||||
groupingLevel={level}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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]);
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
];
|
||||
};
|
|
@ -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 ?? '--'}
|
||||
|
||||
{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';
|
|
@ -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],
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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/**/*"
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "タグ",
|
||||
|
|
|
@ -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": "标签",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue