Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] [Alert details page] Add related alerts tab to the alert details page (#193263) #193583

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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