Skip to content

Commit

Permalink
[8.x] [Alert details page] Add related alerts tab to the alert detail…
Browse files Browse the repository at this point in the history
…s page (#193263) (#193583)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Alert details page] Add related alerts tab to the alert details page
(#193263)](#193263)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Maryam
Saeidi","email":"maryam.saeidi@elastic.co"},"sourceCommit":{"committedDate":"2024-09-20T13:40:13Z","message":"[Alert
details page] Add related alerts tab to the alert details page
(#193263)\n\nCloses #188014\r\n\r\n## Summary\r\n\r\nThis PR adds a
related alerts tab to the alert details page and saves\r\nthe filters in
the URL. The logic for filtering alerts:\r\n\r\n- Alerts that have tags
similar to the existing alert (one or more)\r\n- Alerts for similar
group by/source fields (\"or\" logic)\r\n- Either at the root level
(e.g. host.hostname: 'host-1'), or have this\r\ninformation available in
`kibana.alert.group.value`.\r\n\r\nBy default, we filter to only show
active alerts.\r\n\r\n|Alert|Related
alerts|\r\n|---|---|\r\n\r\n|![image](https://github.com/user-attachments/assets/2117d41b-85d6-4d09-836f-d0a0858f0f89)|![image](https://github.com/user-attachments/assets/df3cb061-122e-451c-94e5-1bbcda74cabc)|\r\n\r\n|![image](https://github.com/user-attachments/assets/4308afdf-5940-49b6-80a3-bfe9de23c805)|![image](https://github.com/user-attachments/assets/43011507-1148-4188-aa12-f8692c0e6ccf)|\r\n\r\n####
Follow-up work\r\nI will create some follow-up tickets for these
topics.\r\n- Selecting `Rule name` as the default group by and opening
the first\r\ngroup\r\n- Considering adding an event timeline to this
tab\r\n- Showing the number of active alerts in the
tab","sha":"96dd4c78637cfdff4c33391eebc68587985312b7","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["v9.0.0","release_note:feature","backport:prev-minor","ci:project-deploy-observability","Team:obs-ux-management","v8.16.0"],"title":"[Alert
details page] Add related alerts tab to the alert details
page","number":193263,"url":"#193263
details page] Add related alerts tab to the alert details page
(#193263)\n\nCloses #188014\r\n\r\n## Summary\r\n\r\nThis PR adds a
related alerts tab to the alert details page and saves\r\nthe filters in
the URL. The logic for filtering alerts:\r\n\r\n- Alerts that have tags
similar to the existing alert (one or more)\r\n- Alerts for similar
group by/source fields (\"or\" logic)\r\n- Either at the root level
(e.g. host.hostname: 'host-1'), or have this\r\ninformation available in
`kibana.alert.group.value`.\r\n\r\nBy default, we filter to only show
active alerts.\r\n\r\n|Alert|Related
alerts|\r\n|---|---|\r\n\r\n|![image](https://github.com/user-attachments/assets/2117d41b-85d6-4d09-836f-d0a0858f0f89)|![image](https://github.com/user-attachments/assets/df3cb061-122e-451c-94e5-1bbcda74cabc)|\r\n\r\n|![image](https://github.com/user-attachments/assets/4308afdf-5940-49b6-80a3-bfe9de23c805)|![image](https://github.com/user-attachments/assets/43011507-1148-4188-aa12-f8692c0e6ccf)|\r\n\r\n####
Follow-up work\r\nI will create some follow-up tickets for these
topics.\r\n- Selecting `Rule name` as the default group by and opening
the first\r\ngroup\r\n- Considering adding an event timeline to this
tab\r\n- Showing the number of active alerts in the
tab","sha":"96dd4c78637cfdff4c33391eebc68587985312b7"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"#193263
details page] Add related alerts tab to the alert details page
(#193263)\n\nCloses #188014\r\n\r\n## Summary\r\n\r\nThis PR adds a
related alerts tab to the alert details page and saves\r\nthe filters in
the URL. The logic for filtering alerts:\r\n\r\n- Alerts that have tags
similar to the existing alert (one or more)\r\n- Alerts for similar
group by/source fields (\"or\" logic)\r\n- Either at the root level
(e.g. host.hostname: 'host-1'), or have this\r\ninformation available in
`kibana.alert.group.value`.\r\n\r\nBy default, we filter to only show
active alerts.\r\n\r\n|Alert|Related
alerts|\r\n|---|---|\r\n\r\n|![image](https://github.com/user-attachments/assets/2117d41b-85d6-4d09-836f-d0a0858f0f89)|![image](https://github.com/user-attachments/assets/df3cb061-122e-451c-94e5-1bbcda74cabc)|\r\n\r\n|![image](https://github.com/user-attachments/assets/4308afdf-5940-49b6-80a3-bfe9de23c805)|![image](https://github.com/user-attachments/assets/43011507-1148-4188-aa12-f8692c0e6ccf)|\r\n\r\n####
Follow-up work\r\nI will create some follow-up tickets for these
topics.\r\n- Selecting `Rule name` as the default group by and opening
the first\r\ngroup\r\n- Considering adding an event timeline to this
tab\r\n- Showing the number of active alerts in the
tab","sha":"96dd4c78637cfdff4c33391eebc68587985312b7"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Maryam Saeidi <maryam.saeidi@elastic.co>
  • Loading branch information
kibanamachine and maryam-saeidi committed Sep 20, 2024
1 parent 3c136e9 commit d7f6853
Show file tree
Hide file tree
Showing 16 changed files with 327 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ import {
alertSearchBarStateContainer,
Provider,
useAlertSearchBarStateContainer,
DEFAULT_STATE,
} from './containers';
import { ObservabilityAlertSearchBar } from './alert_search_bar';
import { AlertSearchBarWithUrlSyncProps } from './types';
import { useKibana } from '../../utils/kibana_react';
import { useToasts } from '../../hooks/use_toast';

function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const { urlStorageKey, ...searchBarProps } = props;
const stateProps = useAlertSearchBarStateContainer(urlStorageKey);
const { urlStorageKey, defaultState = DEFAULT_STATE, ...searchBarProps } = props;
const stateProps = useAlertSearchBarStateContainer(urlStorageKey, undefined, defaultState);
const {
data: {
query: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
* 2.0.
*/

export { Provider, alertSearchBarStateContainer } from './state_container';
export { Provider, alertSearchBarStateContainer, DEFAULT_STATE } from './state_container';
export { useAlertSearchBarStateContainer } from './use_alert_search_bar_state_container';
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,7 @@ import {
} from '@kbn/kibana-utils-plugin/public';
import { AlertStatus } from '../../../../common/typings';
import { ALL_ALERTS } from '../constants';

interface AlertSearchBarContainerState {
rangeFrom: string;
rangeTo: string;
kuery: string;
status: AlertStatus;
filters: Filter[];
savedQueryId?: string;
}
import { AlertSearchBarContainerState } from '../types';

interface AlertSearchBarStateTransitions {
setRangeFrom: (
Expand All @@ -43,7 +35,7 @@ interface AlertSearchBarStateTransitions {
) => (savedQueryId?: string) => AlertSearchBarContainerState;
}

const defaultState: AlertSearchBarContainerState = {
const DEFAULT_STATE: AlertSearchBarContainerState = {
rangeFrom: 'now-24h',
rangeTo: 'now',
kuery: '',
Expand All @@ -60,13 +52,13 @@ const transitions: AlertSearchBarStateTransitions = {
setSavedQueryId: (state) => (savedQueryId) => ({ ...state, savedQueryId }),
};

const alertSearchBarStateContainer = createStateContainer(defaultState, transitions);
const alertSearchBarStateContainer = createStateContainer(DEFAULT_STATE, transitions);

type AlertSearchBarStateContainer = typeof alertSearchBarStateContainer;

const { Provider, useContainer } = createStateContainerReactHelpers<AlertSearchBarStateContainer>();

export { Provider, alertSearchBarStateContainer, useContainer, defaultState };
export { Provider, alertSearchBarStateContainer, useContainer, DEFAULT_STATE };
export type {
AlertSearchBarStateContainer,
AlertSearchBarContainerState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { useTimefilterService } from '../../../hooks/use_timefilter_service';

import {
useContainer,
defaultState,
DEFAULT_STATE,
AlertSearchBarStateContainer,
AlertSearchBarContainerState,
} from './state_container';
Expand All @@ -42,12 +42,13 @@ export const alertSearchBarState = t.partial({

export function useAlertSearchBarStateContainer(
urlStorageKey: string,
{ replace }: { replace?: boolean } = {}
{ replace }: { replace?: boolean } = {},
defaultState: AlertSearchBarContainerState = DEFAULT_STATE
) {
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
const stateContainer = useContainer();

useUrlStateSyncEffect(stateContainer, urlStorageKey, replace);
useUrlStateSyncEffect(stateContainer, urlStorageKey, replace, defaultState);

const { setRangeFrom, setRangeTo, setKuery, setStatus, setFilters, setSavedQueryId } =
stateContainer.transitions;
Expand Down Expand Up @@ -105,7 +106,8 @@ export function useAlertSearchBarStateContainer(
function useUrlStateSyncEffect(
stateContainer: AlertSearchBarStateContainer,
urlStorageKey: string,
replace: boolean = true
replace: boolean = true,
defaultState: AlertSearchBarContainerState = DEFAULT_STATE
) {
const history = useHistory();
const timefilterService = useTimefilterService();
Expand All @@ -127,20 +129,23 @@ function useUrlStateSyncEffect(
timefilterService,
stateContainer,
urlStateStorage,
urlStorageKey
urlStorageKey,
defaultState
);

start();

return stop;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stateContainer, history, timefilterService, urlStorageKey, replace]);
}

function setupUrlStateSync(
stateContainer: AlertSearchBarStateContainer,
urlStateStorage: IKbnUrlStateStorage,
urlStorageKey: string,
replace: boolean = true
replace: boolean = true,
defaultState: AlertSearchBarContainerState = DEFAULT_STATE
) {
// This handles filling the state when an incomplete URL set is provided
const setWithDefaults = (changedState: Partial<AlertSearchBarContainerState> | null) => {
Expand All @@ -165,7 +170,8 @@ function initializeUrlAndStateContainer(
timefilterService: TimefilterContract,
stateContainer: AlertSearchBarStateContainer,
urlStateStorage: IKbnUrlStateStorage,
urlStorageKey: string
urlStorageKey: string,
defaultState: AlertSearchBarContainerState
) {
const urlState = alertSearchBarState.decode(
urlStateStorage.get<Partial<AlertSearchBarContainerState>>(urlStorageKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface AlertStatusFilterProps {

export interface AlertSearchBarWithUrlSyncProps extends CommonAlertSearchBarProps {
urlStorageKey: string;
defaultState?: AlertSearchBarContainerState;
}

export interface Dependencies {
Expand Down Expand Up @@ -49,7 +50,7 @@ export interface ObservabilityAlertSearchBarProps
services: Services;
}

interface AlertSearchBarContainerState {
export interface AlertSearchBarContainerState {
rangeFrom: string;
rangeTo: string;
kuery: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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';
import { AlertsByGroupingAgg } from '../types';

export const getGroupStats: GetGroupStats<AlertsByGroupingAgg> = (selectedGroup, bucket) => {
const defaultBadges = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ 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 { AlertsByGroupingAgg } from '../types';
import { Tags } from '../../tags';
import { ungrouped } from './constants';

export const renderGroupPanel: GroupPanelRenderer<AlertsByGroupingAgg> = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ 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`;

export const SEARCH_BAR_URL_STORAGE_KEY = 'searchBarParams';
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/app
import { waitFor } from '@testing-library/react';
import { Chance } from 'chance';
import React, { Fragment } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { from } from 'rxjs';
import { useFetchAlertDetail } from '../../hooks/use_fetch_alert_detail';
import { ConfigSchema } from '../../plugin';
Expand All @@ -30,6 +30,7 @@ jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: jest.fn(),
useLocation: jest.fn(),
useHistory: jest.fn(),
}));

jest.mock('../../utils/kibana_react');
Expand Down Expand Up @@ -85,6 +86,7 @@ jest.mock('@kbn/observability-shared-plugin/public');
const useFetchAlertDetailMock = useFetchAlertDetail as jest.Mock;
const useParamsMock = useParams as jest.Mock;
const useLocationMock = useLocation as jest.Mock;
const useHistoryMock = useHistory as jest.Mock;
const useBreadcrumbsMock = useBreadcrumbs as jest.Mock;

const chance = new Chance();
Expand All @@ -110,6 +112,7 @@ describe('Alert details', () => {
jest.clearAllMocks();
useParamsMock.mockReturnValue(params);
useLocationMock.mockReturnValue({ pathname: '/alerts/uuid', search: '', state: '', hash: '' });
useHistoryMock.mockReturnValue({ replace: jest.fn() });
useBreadcrumbsMock.mockReturnValue([]);
ruleTypeRegistry.list.mockReturnValue([ruleType]);
ruleTypeRegistry.get.mockReturnValue(ruleType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import React, { useEffect, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { useParams } from 'react-router-dom';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import {
EuiEmptyPrompt,
EuiPanel,
Expand All @@ -23,13 +23,17 @@ import {
ALERT_RULE_UUID,
ALERT_STATUS,
ALERT_STATUS_UNTRACKED,
ALERT_GROUP,
} from '@kbn/rule-data-utils';
import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import dedent from 'dedent';
import { AlertFieldsTable } from '@kbn/alerts-ui-shared';
import { css } from '@emotion/react';
import { omit } from 'lodash';
import type { Group } from '../../../common/typings';
import { observabilityFeatureId } from '../../../common';
import { RelatedAlerts } from './components/related_alerts';
import { useKibana } from '../../utils/kibana_react';
import { useFetchRule } from '../../hooks/use_fetch_rule';
import { usePluginContext } from '../../hooks/use_plugin_context';
Expand All @@ -40,7 +44,6 @@ import { AlertSummary, AlertSummaryField } from './components/alert_summary';
import { CenterJustifiedSpinner } from '../../components/center_justified_spinner';
import { getTimeZone } from '../../utils/get_time_zone';
import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enabled';
import { observabilityFeatureId } from '../../../common';
import { paths } from '../../../common/locators/paths';
import { HeaderMenu } from '../overview/components/header_menu/header_menu';
import { AlertOverview } from '../../components/alert_overview/alert_overview';
Expand All @@ -61,6 +64,12 @@ export const LOG_DOCUMENT_COUNT_RULE_TYPE_ID = 'logs.alert.document.count';
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';

const OVERVIEW_TAB_ID = 'overview';
const METADATA_TAB_ID = 'metadata';
const RELATED_ALERTS_TAB_ID = 'related_alerts';
const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId';
type TabId = typeof OVERVIEW_TAB_ID | typeof METADATA_TAB_ID | typeof RELATED_ALERTS_TAB_ID;

export function AlertDetails() {
const {
cases: {
Expand All @@ -73,6 +82,8 @@ export function AlertDetails() {
uiSettings,
} = useKibana().services;

const { search } = useLocation();
const history = useHistory();
const { ObservabilityPageTemplate, config } = usePluginContext();
const { alertId } = useParams<AlertDetailsPathParams>();
const [isLoading, alertDetail] = useFetchAlertDetail(alertId);
Expand All @@ -87,6 +98,27 @@ export function AlertDetails() {
const [alertStatus, setAlertStatus] = useState<AlertStatus>();
const { euiTheme } = useEuiTheme();

const [activeTabId, setActiveTabId] = useState<TabId>(() => {
const searchParams = new URLSearchParams(search);
const urlTabId = searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY);

return urlTabId && [OVERVIEW_TAB_ID, METADATA_TAB_ID, RELATED_ALERTS_TAB_ID].includes(urlTabId)
? (urlTabId as TabId)
: OVERVIEW_TAB_ID;
});
const handleSetTabId = async (tabId: TabId) => {
setActiveTabId(tabId);

let searchParams = new URLSearchParams(search);
if (tabId === RELATED_ALERTS_TAB_ID) {
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
} else {
searchParams = new URLSearchParams();
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
}
history.replace({ search: searchParams.toString() });
};

useEffect(() => {
if (!alertDetail || !observabilityAIAssistant) {
return;
Expand Down Expand Up @@ -162,9 +194,6 @@ export function AlertDetails() {
const AlertDetailsAppSection = ruleTypeModel ? ruleTypeModel.alertDetailsAppSection : null;
const timeZone = getTimeZone(uiSettings);

const OVERVIEW_TAB_ID = 'overview';
const METADATA_TAB_ID = 'metadata';

const overviewTab = alertDetail ? (
AlertDetailsAppSection &&
/*
Expand Down Expand Up @@ -229,6 +258,20 @@ export function AlertDetails() {
'data-test-subj': 'metadataTab',
content: metadataTab,
},
{
id: RELATED_ALERTS_TAB_ID,
name: i18n.translate('xpack.observability.alertDetails.tab.relatedAlertsLabel', {
defaultMessage: 'Related Alerts',
}),
'data-test-subj': 'relatedAlertsTab',
content: (
<RelatedAlerts
alert={alertDetail?.formatted}
tags={alertDetail?.formatted.fields.tags}
groups={alertDetail?.formatted.fields[ALERT_GROUP] as Group[]}
/>
),
},
];

return (
Expand Down Expand Up @@ -266,7 +309,12 @@ export function AlertDetails() {
data-test-subj="alertDetails"
>
<HeaderMenu />
<EuiTabbedContent data-test-subj="alertDetailsTabbedContent" tabs={tabs} />
<EuiTabbedContent
data-test-subj="alertDetailsTabbedContent"
tabs={tabs}
selectedTab={tabs.find((tab) => tab.id === activeTabId)}
onTabClick={(tab) => handleSetTabId(tab.id as TabId)}
/>
</ObservabilityPageTemplate>
);
}
Expand Down
Loading

0 comments on commit d7f6853

Please sign in to comment.