[Alert controls] Fix refreshing controls and remove extra loading of the alert table (#214612)

## Summary

This PR fixes:
1. refreshing controls when the alert search bar is refreshed
2. the extra initial loading of the alert table in the alerts page
(related to https://github.com/elastic/kibana/issues/183412)
This commit is contained in:
Maryam Saeidi 2025-04-24 14:10:28 +02:00 committed by GitHub
parent 5c41095d1a
commit 2c736f4441
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 145 additions and 81 deletions

View file

@ -22,7 +22,6 @@ import type {
} from '@kbn/controls-plugin/public';
import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import type { Subscription } from 'rxjs';
import { debounce, isEqual, isEqualWith } from 'lodash';
import type { FilterGroupProps, FilterControlConfig } from './types';
import { FilterGroupLoading } from './loading';
@ -62,8 +61,6 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
disableLocalStorageSync = false,
} = props;
const filterChangedSubscription = useRef<Subscription>();
const inputChangedSubscription = useRef<Subscription>();
const [urlStateInitialized, setUrlStateInitialized] = useState(false);
const [controlsFromUrl, setControlsFromUrl] = useState(controlsUrlState ?? []);
@ -121,14 +118,6 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
const urlDataApplied = useRef<boolean>(false);
useEffect(() => {
return () => {
[filterChangedSubscription.current, inputChangedSubscription.current].forEach((sub) => {
if (sub) sub.unsubscribe();
});
};
}, []);
const { filters: validatedFilters, query: validatedQuery } = useMemo(() => {
try {
buildEsQuery(
@ -179,7 +168,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
);
const handleOutputFilterUpdates = useCallback(
(newFilters: Filter[] = []) => {
(newFilters: Filter[] | undefined) => {
if (isEqual(currentFiltersRef.current, newFilters)) return;
if (onFiltersChange) onFiltersChange(newFilters ?? []);
currentFiltersRef.current = newFilters ?? [];
@ -222,37 +211,34 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
}, [controlsUrlState, getStoredControlState, switchToEditMode]);
useEffect(() => {
if (controlsUrlState && !urlStateInitialized) {
initializeUrlState();
if (!controlGroup) {
return;
}
const filterSubscription = controlGroup.filters$.subscribe({ next: debouncedFilterUpdates });
return () => {
filterSubscription.unsubscribe();
};
}, [controlGroup, debouncedFilterUpdates]);
useEffect(() => {
if (!controlGroup) {
return;
}
filterChangedSubscription.current = controlGroup.filters$.subscribe({
next: debouncedFilterUpdates,
});
inputChangedSubscription.current = controlGroup.getInput$().subscribe({
const inputSubscription = controlGroup.getInput$().subscribe({
next: handleStateUpdates,
});
return () => {
[filterChangedSubscription.current, inputChangedSubscription.current].forEach((sub) => {
if (sub) sub.unsubscribe();
});
inputSubscription.unsubscribe();
};
}, [
controlGroup,
controlsUrlState,
debouncedFilterUpdates,
getStoredControlState,
handleStateUpdates,
initializeUrlState,
switchToEditMode,
urlStateInitialized,
]);
}, [controlGroup, handleStateUpdates]);
useEffect(() => {
if (controlsUrlState && !urlStateInitialized) {
initializeUrlState();
}
}, [controlsUrlState, initializeUrlState, urlStateInitialized]);
const onControlGroupLoadHandler = useCallback(
(controlGroupRendererApi: ControlGroupRendererApi | undefined) => {

View file

@ -181,7 +181,8 @@ export const ControlGroupRenderer = ({
},
compressed: compressed ?? true,
})}
onApiAvailable={(controlGroupApi) => {
onApiAvailable={async (controlGroupApi) => {
await controlGroupApi.untilInitialized();
const controlGroupRendererApi: ControlGroupRendererApi = {
...controlGroupApi,
reload: () => reload$.next(),

View file

@ -12,8 +12,9 @@ import { useFetchAlertsIndexNamesQuery } from '@kbn/alerts-ui-shared';
import { ControlGroupRenderer } from '@kbn/controls-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { i18n } from '@kbn/i18n';
import { Filter } from '@kbn/es-query';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { Filter, TimeRange } from '@kbn/es-query';
import { getEsQueryConfig, getTime } from '@kbn/data-plugin/common';
import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils';
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../../../common/constants';
import { DEFAULT_QUERY_STRING, EMPTY_FILTERS } from './constants';
import { ObservabilityAlertSearchBarProps } from './types';
@ -23,6 +24,11 @@ const toastTitle = i18n.translate('xpack.observability.alerts.searchBar.invalidQ
defaultMessage: 'Invalid query string',
});
const getTimeFilter = (timeRange: TimeRange) =>
getTime(undefined, timeRange, {
fieldName: ALERT_TIME_RANGE,
});
export function ObservabilityAlertSearchBar({
appName,
defaultFilters = EMPTY_FILTERS,
@ -37,7 +43,7 @@ export function ObservabilityAlertSearchBar({
showFilterBar = false,
controlConfigs,
filters = EMPTY_FILTERS,
filterControls = EMPTY_FILTERS,
filterControls,
savedQuery,
setSavedQuery,
kuery,
@ -57,6 +63,12 @@ export function ObservabilityAlertSearchBar({
}: ObservabilityAlertSearchBarProps) {
const toasts = useToasts();
const [spaceId, setSpaceId] = useState<string>();
const [timeFilter, setTimeFilter] = useState<Filter | undefined>(
getTimeFilter({
to: rangeTo,
from: rangeFrom,
})
);
const queryFilter = kuery ? { query: kuery, language: 'kuery' } : undefined;
const { data: indexNames } = useFetchAlertsIndexNamesQuery({
http,
@ -73,6 +85,21 @@ export function ObservabilityAlertSearchBar({
[appName, spaceId]
);
const dataViewSpec = useMemo(
() => ({
id: 'observability-unified-alerts-dv',
title: indexNames?.join(',') ?? '',
}),
[indexNames]
);
const aggregatedFilters = useMemo(() => {
const _filters = timeFilter
? [timeFilter, ...filters, ...defaultFilters]
: [...filters, ...defaultFilters];
return _filters.length ? _filters : undefined;
}, [timeFilter, filters, defaultFilters]);
const submitQuery = useCallback(() => {
try {
onEsQueryChange(
@ -82,10 +109,16 @@ export function ObservabilityAlertSearchBar({
from: rangeFrom,
},
kuery,
filters: [...filters, ...filterControls, ...defaultFilters],
filters: [...filters, ...(filterControls ?? []), ...defaultFilters],
config: getEsQueryConfig(uiSettings),
})
);
setTimeFilter(
getTimeFilter({
to: rangeTo,
from: rangeFrom,
})
);
} catch (error) {
toasts.addError(error, {
title: toastTitle,
@ -166,15 +199,12 @@ export function ObservabilityAlertSearchBar({
<EuiFlexItem>
{indexNames && indexNames.length > 0 && (
<AlertFilterControls
dataViewSpec={{
id: 'observability-unified-alerts-dv',
title: indexNames.join(','),
}}
dataViewSpec={dataViewSpec}
spaceId={spaceId}
chainingSystem="HIERARCHICAL"
controlsUrlState={controlConfigs}
setControlsUrlState={onControlConfigsChange}
filters={[...filters, ...defaultFilters]}
filters={aggregatedFilters}
onFiltersChange={onFilterControlsChange}
storageKey={filterControlsStorageKey}
disableLocalStorageSync={disableLocalStorageSync}

View file

@ -5,8 +5,7 @@
* 2.0.
*/
import React, { useState } from 'react';
import { Filter } from '@kbn/es-query';
import React from 'react';
import {
alertSearchBarStateContainer,
Provider,
@ -21,7 +20,6 @@ import { useToasts } from '../../hooks/use_toast';
function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const { urlStorageKey, defaultState = DEFAULT_STATE, ...searchBarProps } = props;
const stateProps = useAlertSearchBarStateContainer(urlStorageKey, undefined, defaultState);
const [filterControls, setFilterControls] = useState<Filter[]>([]);
const {
data,
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar },
@ -41,8 +39,6 @@ function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
<ObservabilityAlertSearchBar
{...stateProps}
{...searchBarProps}
filterControls={filterControls}
onFilterControlsChange={setFilterControls}
showFilterBar
services={{
timeFilterService,

View file

@ -42,7 +42,6 @@ const DEFAULT_STATE: AlertSearchBarContainerState = {
rangeFrom: 'now-24h',
rangeTo: 'now',
kuery: '',
filters: [],
};
const transitions: AlertSearchBarStateTransitions = {

View file

@ -60,8 +60,8 @@ export interface ObservabilityAlertSearchBarProps
AlertSearchBarStateTransitions,
CommonAlertSearchBarProps {
services: Services;
filterControls: Filter[];
onFilterControlsChange: (controlConfigs: Filter[]) => void;
filterControls?: Filter[];
onFilterControlsChange: (filterControls: Filter[]) => void;
savedQuery?: SavedQuery;
showFilterBar?: boolean;
disableLocalStorageSync?: boolean;
@ -91,5 +91,7 @@ interface CommonAlertSearchBarProps {
appName: string;
onEsQueryChange: (query: { bool: BoolQuery }) => void;
defaultFilters?: Filter[];
filterControls?: Filter[];
onFilterControlsChange: (filterControls: Filter[]) => void;
onControlApiAvailable?: (controlGroupHandler: FilterGroupHandler | undefined) => void;
}

View file

@ -8,6 +8,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { BrushEndListener, XYBrushEvent } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import type { FilterGroupHandler } from '@kbn/alerts-ui-shared';
import { BoolQuery, Filter } from '@kbn/es-query';
import { usePerformanceContext } from '@kbn/ebt-tools';
import { i18n } from '@kbn/i18n';
@ -54,6 +55,7 @@ import { buildEsQuery } from '../../utils/build_es_query';
import { renderRuleStats, RuleStatsState } from './components/rule_stats';
import { mergeBoolQueries } from './helpers/merge_bool_queries';
import { GroupingToolbarControls } from '../../components/alerts_table/grouping/grouping_toolbar_controls';
import { AlertsLoader } from './components/alerts_loader';
const ALERTS_SEARCH_BAR_ID = 'alerts-search-bar-o11y';
const ALERTS_PER_PAGE = 50;
@ -61,7 +63,7 @@ const ALERTS_TABLE_ID = 'xpack.observability.alerts.alert.table';
const DEFAULT_INTERVAL = '60s';
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
const DEFAULT_FILTERS: Filter[] = [];
const DEFAULT_EMPTY_FILTERS: Filter[] = [];
const tableColumns = getColumns({ showRuleName: true });
@ -92,12 +94,26 @@ function InternalAlertsPage() {
},
} = data;
const { ObservabilityPageTemplate } = usePluginContext();
const [filterControls, setFilterControls] = useState<Filter[]>([]);
const [filterControls, setFilterControls] = useState<Filter[]>();
const [controlApi, setControlApi] = useState<FilterGroupHandler | undefined>();
const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, {
replace: false,
});
const hasInitialControlLoadingFinished = useMemo(
() => controlApi && Array.isArray(filterControls),
[controlApi, filterControls]
);
const filteredRuleTypes = useGetFilteredRuleTypes();
const themeOverrides = charts.theme.useChartsBaseTheme();
const globalFilters = useMemo(() => {
return [
...(alertSearchBarStateProps.filters ?? DEFAULT_EMPTY_FILTERS),
...(filterControls ?? DEFAULT_EMPTY_FILTERS),
];
}, [alertSearchBarStateProps.filters, filterControls]);
const globalQuery = useMemo(() => {
return { query: alertSearchBarStateProps.kuery, language: 'kuery' };
}, [alertSearchBarStateProps.kuery]);
const { setScreenContext } = observabilityAIAssistant?.service || {};
@ -270,6 +286,7 @@ function InternalAlertsPage() {
onEsQueryChange={setEsQuery}
filterControls={filterControls}
onFilterControlsChange={setFilterControls}
onControlApiAvailable={setControlApi}
showFilterBar
services={{
timeFilterService,
@ -286,30 +303,31 @@ function InternalAlertsPage() {
<EuiSpacer size="s" />
</EuiFlexItem>
<EuiFlexItem>
<AlertSummaryWidget
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
consumers={observabilityAlertFeatureIds}
filter={esQuery}
fullSize
timeRange={alertSummaryTimeRange}
chartProps={{
themeOverrides: charts.theme.useChartsBaseTheme(),
onBrushEnd,
}}
/>
{hasInitialControlLoadingFinished ? (
<AlertSummaryWidget
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
consumers={observabilityAlertFeatureIds}
filter={esQuery}
fullSize
timeRange={alertSummaryTimeRange}
chartProps={{
themeOverrides,
onBrushEnd,
}}
/>
) : (
<AlertsLoader />
)}
</EuiFlexItem>
<EuiFlexItem>
{esQuery && (
{esQuery && hasInitialControlLoadingFinished && (
<AlertsGrouping<AlertsByGroupingAgg>
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
consumers={observabilityAlertFeatureIds}
from={alertSearchBarStateProps.rangeFrom}
to={alertSearchBarStateProps.rangeTo}
globalFilters={[
...(alertSearchBarStateProps.filters ?? DEFAULT_FILTERS),
...filterControls,
]}
globalQuery={{ query: alertSearchBarStateProps.kuery, language: 'kuery' }}
globalFilters={globalFilters}
globalQuery={globalQuery}
groupingId={ALERTS_PAGE_ALERTS_TABLE_CONFIG_ID}
defaultGroupingOptions={DEFAULT_GROUPING_OPTIONS}
getAggregationsByGroupingField={getAggregationsByGroupingField}

View file

@ -0,0 +1,24 @@
/*
* 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 { EuiLoadingChart } from '@elastic/eui';
export function AlertsLoader() {
return (
<div
style={{
minHeight: 238,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<EuiLoadingChart size="l" data-test-subj="alertsLoading" />
</div>
);
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useRef } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
@ -46,6 +46,7 @@ interface Props {
onEsQueryChange: (query: { bool: BoolQuery }) => void;
onSetTabId: (tabId: TabId) => void;
onControlApiAvailable?: (controlGroupHandler: FilterGroupHandler | undefined) => void;
controlApi?: FilterGroupHandler;
}
const tableColumns = getColumns();
@ -60,10 +61,16 @@ export function RuleDetailsTabs({
onSetTabId,
onEsQueryChange,
onControlApiAvailable,
controlApi,
}: Props) {
const {
triggersActionsUi: { getRuleEventLogList: RuleEventLogList },
} = useKibana().services;
const [filterControls, setFilterControls] = useState<Filter[] | undefined>();
const hasInitialControlLoadingFinished = useMemo(
() => controlApi && Array.isArray(filterControls),
[controlApi, filterControls]
);
const ruleFilters = useRef<Filter[]>([
{
@ -93,13 +100,15 @@ export function RuleDetailsTabs({
urlStorageKey={RULE_DETAILS_SEARCH_BAR_URL_STORAGE_KEY}
defaultFilters={ruleFilters.current}
disableLocalStorageSync={true}
filterControls={filterControls}
onFilterControlsChange={setFilterControls}
onControlApiAvailable={onControlApiAvailable}
/>
<EuiSpacer size="s" />
<EuiFlexGroup style={{ minHeight: 450 }} direction={'column'}>
<EuiFlexGroup css={{ minHeight: 450 }} direction={'column'}>
<EuiFlexItem>
{esQuery && ruleTypeIds && (
{esQuery && ruleTypeIds && hasInitialControlLoadingFinished && (
<ObservabilityAlertsTable
id={RULE_DETAILS_PAGE_ID}
ruleTypeIds={ruleTypeIds}
@ -120,7 +129,7 @@ export function RuleDetailsTabs({
}),
'data-test-subj': 'eventLogListTab',
content: (
<EuiFlexGroup style={{ minHeight: 600 }} direction={'column'}>
<EuiFlexGroup css={{ minHeight: 600 }} direction={'column'}>
<EuiFlexItem>
{rule && ruleType ? <RuleEventLogList ruleId={rule.id} ruleType={ruleType} /> : null}
</EuiFlexItem>

View file

@ -106,9 +106,7 @@ export function RuleDetailsPage() {
{ serverless }
);
const [alertFilterControlHandler, setAlertFilterControlHandler] = useState<
FilterGroupHandler | undefined
>();
const [controlApi, setControlApi] = useState<FilterGroupHandler | undefined>();
const [activeTabId, setActiveTabId] = useState<TabId>(() => {
const searchParams = new URLSearchParams(search);
const urlTabId = searchParams.get(RULE_DETAILS_TAB_URL_STORAGE_KEY);
@ -156,7 +154,7 @@ export function RuleDetailsPage() {
const statusControlIndex = getControlIndex(ALERT_STATUS, controlConfigs);
controlConfigs = setStatusOnControlConfigs(status, controlConfigs);
updateSelectedOptions(status, statusControlIndex, alertFilterControlHandler);
updateSelectedOptions(status, statusControlIndex, controlApi);
await locators.get<RuleDetailsLocatorParams>(ruleDetailsLocatorID)?.navigate(
{
@ -234,7 +232,7 @@ export function RuleDetailsPage() {
>
<HeaderMenu />
<EuiFlexGroup wrap gutterSize="m">
<EuiFlexItem style={{ minWidth: 350 }}>
<EuiFlexItem css={{ minWidth: 350 }}>
<RuleStatusPanel
rule={rule}
isEditable={isEditable}
@ -244,7 +242,7 @@ export function RuleDetailsPage() {
/>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: 350 }}>
<EuiFlexItem css={{ minWidth: 350 }}>
<AlertSummaryWidget
ruleTypeIds={OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES}
consumers={observabilityAlertFeatureIds}
@ -281,7 +279,8 @@ export function RuleDetailsPage() {
activeTabId={activeTabId}
onEsQueryChange={setEsQuery}
onSetTabId={handleSetTabId}
onControlApiAvailable={setAlertFilterControlHandler}
onControlApiAvailable={setControlApi}
controlApi={controlApi}
/>
{isEditRuleFlyoutVisible && (