Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ebd88ab
start query
baileycash-elastic Jun 11, 2025
3549b3a
add callout with alert count
baileycash-elastic Jun 11, 2025
ed4f074
route users to related alerts
baileycash-elastic Jun 11, 2025
7f84fa8
fix docs
baileycash-elastic Jun 11, 2025
1371d9b
Merge branch 'main' into alerting-213020
baileycash-elastic Jun 11, 2025
ed5d632
reposition callout
baileycash-elastic Jun 12, 2025
ccf976c
improvements, tab behavior fixes
baileycash-elastic Jun 12, 2025
321464c
fix zero conditional
baileycash-elastic Jun 12, 2025
34ad95c
add tests for callout
baileycash-elastic Jun 12, 2025
55bc18d
consolidate queries
baileycash-elastic Jun 12, 2025
09c05ba
rename query builder file
baileycash-elastic Jun 12, 2025
005f509
add ui filter control
baileycash-elastic Jun 12, 2025
0572a26
Merge branch 'main' into alerting-213020
baileycash-elastic Jun 13, 2025
74a1e77
cleanup
baileycash-elastic Jun 13, 2025
c000c24
merge in main
baileycash-elastic Jun 13, 2025
48d9a7f
error resolution
baileycash-elastic Jun 13, 2025
5e84b43
update copy
baileycash-elastic Jun 13, 2025
d8db503
update copy for filter, separate filter into its own component
baileycash-elastic Jun 13, 2025
881a390
Merge branch 'main' into alerting-213020
baileycash-elastic Jun 16, 2025
8649528
Merge branch 'main' into alerting-213020
baileycash-elastic Jun 21, 2025
e6e5574
fix errors from merge conflict
baileycash-elastic Jun 22, 2025
10a7ddc
change boolean name from useDefaultContext to skipReactQueryContext
baileycash-elastic Jun 23, 2025
30e69a7
merge main into alerting-213020
baileycash-elastic Jun 23, 2025
7c47b55
simplify tab switching based on recent changes from main
baileycash-elastic Jun 23, 2025
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 @@ -68,6 +68,10 @@ export interface SearchAlertsParams {
* The page size to fetch
*/
pageSize: number;
/**
* Force using the default context, otherwise use the AlertQueryContext
*/
skipAlertsQueryContext?: boolean;
/**
* The minimum score to apply to the query
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export const queryKeyPrefix = ['alerts', searchAlerts.name];
* When testing components that depend on this hook, prefer mocking the {@link searchAlerts} function instead of the hook itself.
* @external https://tanstack.com/query/v4/docs/framework/react/guides/testing
*/
export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryParams) => {
export const useSearchAlertsQuery = ({
data,
skipAlertsQueryContext,
...params
}: UseSearchAlertsQueryParams) => {
const {
ruleTypeIds,
consumers,
Expand Down Expand Up @@ -64,7 +68,7 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa
trackScores,
}),
refetchOnWindowFocus: false,
context: AlertsQueryContext,
context: skipAlertsQueryContext ? undefined : AlertsQueryContext,
enabled: ruleTypeIds.length > 0,
// To avoid flash of empty state with pagination, see https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata
keepPreviousData: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ describe('Alert details', () => {
expect(alertDetails.queryByTestId('alert-summary-container')).toBeFalsy();
expect(alertDetails.queryByTestId('overviewTab')).toBeTruthy();
expect(alertDetails.queryByTestId('metadataTab')).toBeTruthy();
expect(alertDetails.queryByTestId('relatedAlertsTab')).toBeTruthy();
});

it('should show Metadata tab', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React, { useCallback, useEffect, useState } from 'react';
import { useHistory, useLocation, useParams } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
Expand Down Expand Up @@ -59,6 +59,8 @@ import StaleAlert from './components/stale_alert';
import { RelatedDashboards } from './components/related_dashboards';
import { getAlertTitle } from '../../utils/format_alert_title';
import { AlertSubtitle } from './components/alert_subtitle';
import { ProximalAlertsCallout } from './proximal_alerts_callout';
import { useTabId } from './hooks/use_tab_id';
import { useRelatedDashboards } from './hooks/use_related_dashboards';

interface AlertDetailsPathParams {
Expand All @@ -73,7 +75,6 @@ const defaultBreadcrumb = i18n.translate('xpack.observability.breadcrumbs.alertD
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 ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId';

const TAB_IDS = [
'overview',
Expand All @@ -90,6 +91,7 @@ const isTabId = (value: string): value is TabId => {
};

export function AlertDetails() {
const { services } = useKibana();
const {
cases: {
helpers: { canUseCases },
Expand All @@ -100,12 +102,11 @@ export function AlertDetails() {
observabilityAIAssistant,
uiSettings,
serverless,
} = useKibana().services;

const { search } = useLocation();
const history = useHistory();
} = services;
const { ObservabilityPageTemplate, config } = usePluginContext();
const { alertId } = useParams<AlertDetailsPathParams>();
const { getUrlTabId, setUrlTabId } = useTabId();
const urlTabId = getUrlTabId();
const {
isLoadingRelatedDashboards,
suggestedDashboards,
Expand Down Expand Up @@ -134,17 +135,17 @@ export function AlertDetails() {
const { euiTheme } = useEuiTheme();
const [sources, setSources] = useState<AlertDetailsSource[]>();
const [activeTabId, setActiveTabId] = useState<TabId>();

const handleSetTabId = async (tabId: TabId) => {
setActiveTabId(tabId);

let searchParams = new URLSearchParams(search);
if (tabId === 'related_alerts') {
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
setUrlTabId(tabId, true, {
filterProximal: 'true',
});
} else {
searchParams = new URLSearchParams();
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);
setUrlTabId(tabId, true);
}
history.replace({ search: searchParams.toString() });
};

useEffect(() => {
Expand All @@ -170,11 +171,9 @@ export function AlertDetails() {
if (alertDetail) {
setRuleTypeModel(ruleTypeRegistry.get(alertDetail?.formatted.fields[ALERT_RULE_TYPE_ID]!));
setAlertStatus(alertDetail?.formatted?.fields[ALERT_STATUS] as AlertStatus);
const searchParams = new URLSearchParams(search);
const urlTabId = searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY);
setActiveTabId(urlTabId && isTabId(urlTabId) ? urlTabId : 'overview');
}
}, [alertDetail, ruleTypeRegistry, search]);
}, [alertDetail, ruleTypeRegistry, urlTabId]);

useBreadcrumbs(
[
Expand All @@ -198,6 +197,10 @@ export function AlertDetails() {
setAlertStatus(ALERT_STATUS_UNTRACKED);
}, []);

const showRelatedAlertsFromCallout = () => {
handleSetTabId('related_alerts');
};

usePageReady({
isRefreshing: isLoading,
isReady: !isLoading && !!alertDetail && activeTabId === 'overview',
Expand Down Expand Up @@ -252,6 +255,10 @@ export function AlertDetails() {

<EuiSpacer size="m" />
<EuiFlexGroup direction="column" gutterSize="m">
<ProximalAlertsCallout
alertDetail={alertDetail}
switchTabs={showRelatedAlertsFromCallout}
/>
<SourceBar alert={alertDetail.formatted} sources={sources} />
<AlertDetailContextualInsights alert={alertDetail} />
{rule && alertDetail.formatted && (
Expand All @@ -272,6 +279,11 @@ export function AlertDetails() {
</>
) : (
<EuiPanel hasShadow={false} data-test-subj="overviewTabPanel" paddingSize="none">
<EuiSpacer size="l" />
<ProximalAlertsCallout
alertDetail={alertDetail}
switchTabs={showRelatedAlertsFromCallout}
/>
<EuiSpacer size="l" />
<AlertDetailContextualInsights alert={alertDetail} />
<EuiSpacer size="l" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ALERT_START, ALERT_UUID } from '@kbn/rule-data-utils';
import { AlertsTable } from '@kbn/response-ops-alerts-table';
import { SortOrder } from '@elastic/elasticsearch/lib/api/types';
import { getRelatedColumns } from './get_related_columns';
import { useBuildRelatedAlertsQuery } from '../../hooks/related_alerts/use_build_related_alerts_query';
import { getBuildRelatedAlertsQuery } from '../../hooks/related_alerts/get_build_related_alerts_query';
import { AlertData } from '../../../../hooks/use_fetch_alert_detail';
import {
GetObservabilityAlertsTableProp,
Expand All @@ -25,6 +25,8 @@ import { AlertsFlyoutFooter } from '../../../../components/alerts_flyout/alerts_
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../../../common/constants';
import { AlertsTableCellValue } from '../../../../components/alerts_table/common/cell_value';
import { casesFeatureIdV2 } from '../../../../../common';
import { useFilterProximalParam } from '../../hooks/use_filter_proximal_param';
import { RelatedAlertsTableFilter } from './related_alerts_table_filter';

interface Props {
alertData: AlertData;
Expand Down Expand Up @@ -52,14 +54,15 @@ const RELATED_ALERTS_TABLE_ID = 'xpack.observability.alerts.relatedAlerts';

export function RelatedAlertsTable({ alertData }: Props) {
const { formatted: alert } = alertData;
const esQuery = useBuildRelatedAlertsQuery({ alert });
const { filterProximal } = useFilterProximalParam();
const esQuery = getBuildRelatedAlertsQuery({ alert, filterProximal });
const { observabilityRuleTypeRegistry, config } = usePluginContext();

const services = useKibana().services;

return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiSpacer size="s" />
<RelatedAlertsTableFilter />
<AlertsTable<ObservabilityAlertsTableContext>
id={RELATED_ALERTS_TABLE_ID}
query={esQuery}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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 { EuiCheckbox, EuiFlexGroup, EuiFormRow, EuiPanel, EuiText } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { useFilterProximalParam } from '../../hooks/use_filter_proximal_param';

export function RelatedAlertsTableFilter() {
const { filterProximal, setProximalFilterParam } = useFilterProximalParam();

return (
<EuiPanel paddingSize="m" hasShadow={false} color="subdued">
<EuiFlexGroup direction="row" alignItems="center" justifyContent="flexStart">
<EuiText size="s">
<strong>
{i18n.translate('xpack.observability.alerts.relatedAlerts.filtersLabel', {
defaultMessage: 'Filters',
})}
</strong>
</EuiText>
<EuiFormRow fullWidth>
<EuiCheckbox
label={i18n.translate(
'xpack.observability.alerts.relatedAlerts.proximityCheckboxLabel',
{
defaultMessage: 'Created around the same time',
}
)}
checked={filterProximal}
onChange={(event) => {
setProximalFilterParam(event.target.checked);
}}
id={'proximal-alerts-checkbox'}
data-test-subj="proximal-alerts-checkbox"
/>
</EuiFormRow>
</EuiFlexGroup>
</EuiPanel>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ import { TopAlert } from '../../../../typings/alerts';

interface Props {
alert: TopAlert<ObservabilityFields>;
filterProximal: boolean;
}

export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryContainer {
export function getBuildRelatedAlertsQuery({
alert,
filterProximal,
}: Props): QueryDslQueryContainer {
const groups = alert.fields[ALERT_GROUP];
const shouldGroups: QueryDslQueryContainer[] = [];
groups?.forEach(({ field, value }) => {
Expand Down Expand Up @@ -58,14 +62,22 @@ export function useBuildRelatedAlertsQuery({ alert }: Props): QueryDslQueryConta
const tags = alert.fields[ALERT_RULE_TAGS] ?? [];
const instanceId = alert.fields[ALERT_INSTANCE_ID]?.split(',') ?? [];

const range = filterProximal ? [30, 'minutes'] : [1, 'days'];

return {
bool: {
filter: [
{
range: {
[ALERT_START]: {
gte: startDate.clone().subtract(1, 'days').toISOString(),
lte: startDate.clone().add(1, 'days').toISOString(),
gte: startDate
.clone()
.subtract(...range)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Destructuring an array so it becomes the exact arguments expected by the function feels brittle to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I felt this was cleaner than having a conditional for the "same time" filter bool * (startDate + endDate) combinations

.toISOString(),
lte: startDate
.clone()
.add(...range)
.toISOString(),
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { useHistory, useLocation } from 'react-router-dom';

export const useFilterProximalParam = () => {
const { search } = useLocation();
const searchParams = new URLSearchParams(search);
const history = useHistory();

const setProximalFilterParam = (proximalFilter: boolean) => {
searchParams.set('filterProximal', String(proximalFilter));
history.replace({ search: searchParams.toString() });
};

const filterProximal = searchParams.get('filterProximal') === 'true';

return { filterProximal, setProximalFilterParam };
};
Original file line number Diff line number Diff line change
@@ -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 { useSearchAlertsQuery } from '@kbn/alerts-ui-shared/src/common/hooks/use_search_alerts_query';
import {
OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
observabilityAlertFeatureIds,
} from '../../../../common/constants';
import { AlertData } from '../../../hooks/use_fetch_alert_detail';
import { useKibana } from '../../../utils/kibana_react';
import { getBuildRelatedAlertsQuery } from './related_alerts/get_build_related_alerts_query';

export const useFindProximalAlerts = (alertDetail: AlertData) => {
const { services } = useKibana();

const esQuery = getBuildRelatedAlertsQuery({
alert: alertDetail.formatted,
filterProximal: true,
});

return useSearchAlertsQuery({
data: services.data,
ruleTypeIds: OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES,
consumers: observabilityAlertFeatureIds,
query: esQuery,
skipAlertsQueryContext: true,
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { useHistory, useLocation } from 'react-router-dom';

const ALERT_DETAILS_TAB_URL_STORAGE_KEY = 'tabId';

export const useTabId = () => {
const { search } = useLocation();
const history = useHistory();

const getUrlTabId = () => {
const searchParams = new URLSearchParams(search);
return searchParams.get(ALERT_DETAILS_TAB_URL_STORAGE_KEY);
};

const setUrlTabId = (
tabId: string,
overrideSearchState?: boolean,
newSearchState?: Record<string, string>
) => {
const searchParams = new URLSearchParams(overrideSearchState ? undefined : search);
searchParams.set(ALERT_DETAILS_TAB_URL_STORAGE_KEY, tabId);

if (newSearchState) {
Object.entries(newSearchState).forEach(([key, value]) => {
searchParams.set(key, value);
});
}

history.replace({ search: searchParams.toString() });
};

return {
getUrlTabId,
setUrlTabId,
};
};
Loading