Skip to content

Commit

Permalink
Enable filters for the alert search bar on the Observability Alerts p…
Browse files Browse the repository at this point in the history
…age (#178886)

Fixes #158400
Resolves #177431

## Summary

This PR adds the filter option to the alert search bar.


https://github.com/elastic/kibana/assets/12370520/d476dec4-e540-4379-b2cf-076fd27c13a0


### 🧪 How to test
- Go to the Observability > Alerts page
- Use filter and saved query functionalities, they should work as
expected.
- APM > Alerts tab should not show the filters, we can enable them
separately if we want to.

Inspired by what @umbopepato implemented for the global alerts page!
  • Loading branch information
maryam-saeidi authored Mar 28, 2024
1 parent 8eed206 commit 716b6f8
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,14 @@ describe('ObservabilityAlertSearchBar', () => {
const observabilityAlertSearchBarProps: ObservabilityAlertSearchBarProps = {
appName: 'testAppName',
kuery: '',
filters: [],
onRangeFromChange: jest.fn(),
onRangeToChange: jest.fn(),
onKueryChange: jest.fn(),
onStatusChange: jest.fn(),
onEsQueryChange: jest.fn(),
onFiltersChange: jest.fn(),
setSavedQuery: jest.fn(),
rangeTo: 'now',
rangeFrom: 'now-15m',
status: 'all',
Expand Down Expand Up @@ -160,6 +163,53 @@ describe('ObservabilityAlertSearchBar', () => {
});
});

it('should include filters in es query', async () => {
const mockedOnEsQueryChange = jest.fn();
const mockedFrom = '2022-11-15T09:38:13.604Z';
const mockedTo = '2022-11-15T09:53:13.604Z';
const filters = [
{
meta: {},
query: {
'service.name': {
value: 'synth-node-0',
},
},
},
];

renderComponent({
onEsQueryChange: mockedOnEsQueryChange,
rangeFrom: mockedFrom,
rangeTo: mockedTo,
filters,
});

expect(mockedOnEsQueryChange).toHaveBeenCalledWith({
bool: {
filter: [
{
range: {
'kibana.alert.time_range': expect.objectContaining({
format: 'strict_date_optional_time',
gte: mockedFrom,
lte: mockedTo,
}),
},
},
{
'service.name': {
value: 'synth-node-0',
},
},
],
must: [],
must_not: [],
should: [],
},
});
});

it('should show error in a toast', async () => {
const error = new Error('something is wrong in esQueryChange');
const mockedOnEsQueryChange = jest.fn().mockImplementation(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useCallback, useEffect } from 'react';

import { i18n } from '@kbn/i18n';
import { Query } from '@kbn/es-query';
import { Filter, Query } from '@kbn/es-query';
import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { AlertsStatusFilter } from './components';
import { observabilityAlertFeatureIds } from '../../../common/constants';
Expand All @@ -26,6 +26,7 @@ const getAlertStatusQuery = (status: string): Query[] => {
const toastTitle = i18n.translate('xpack.observability.alerts.searchBar.invalidQueryTitle', {
defaultMessage: 'Invalid query string',
});
const defaultFilters: Filter[] = [];

export function ObservabilityAlertSearchBar({
appName,
Expand All @@ -35,6 +36,11 @@ export function ObservabilityAlertSearchBar({
onRangeFromChange,
onRangeToChange,
onStatusChange,
onFiltersChange,
showFilterBar = false,
filters = defaultFilters,
savedQuery,
setSavedQuery,
kuery,
rangeFrom,
rangeTo,
Expand All @@ -43,6 +49,10 @@ export function ObservabilityAlertSearchBar({
}: ObservabilityAlertSearchBarProps) {
const toasts = useToasts();

const clearSavedQuery = useCallback(
() => (setSavedQuery ? setSavedQuery(undefined) : null),
[setSavedQuery]
);
const onAlertStatusChange = useCallback(
(alertStatus: AlertStatus) => {
try {
Expand Down Expand Up @@ -80,47 +90,48 @@ export function ObservabilityAlertSearchBar({
onAlertStatusChange(status);
}, [onAlertStatusChange, status]);

useEffect(() => {
try {
onEsQueryChange(
buildEsQuery({
timeRange: {
to: rangeTo,
from: rangeFrom,
},
kuery,
filters,
})
);
} catch (error) {
toasts.addError(error, {
title: toastTitle,
});
onKueryChange(DEFAULT_QUERY_STRING);
}
}, [filters, kuery, onEsQueryChange, onKueryChange, rangeFrom, rangeTo, toasts]);

const onSearchBarParamsChange = useCallback<
(query: {
dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' };
query?: string;
}) => void
>(
({ dateRange, query }) => {
try {
// First try to create es query to make sure query is valid, then save it in state
const esQuery = buildEsQuery({
timeRange: {
to: dateRange.to,
from: dateRange.from,
},
kuery: query,
queries: [...getAlertStatusQuery(status), ...defaultSearchQueries],
config: getEsQueryConfig(uiSettings),
});
if (query) onKueryChange(query);
timeFilterService.setTime(dateRange);
onRangeFromChange(dateRange.from);
onRangeToChange(dateRange.to);
onEsQueryChange(esQuery);
} catch (error) {
toasts.addError(error, {
title: toastTitle,
});
onKueryChange(DEFAULT_QUERY_STRING);
}
clearSavedQuery();
onKueryChange(query ?? '');
timeFilterService.setTime(dateRange);
onRangeFromChange(dateRange.from);
onRangeToChange(dateRange.to);
},
[
status,
defaultSearchQueries,
uiSettings,
onKueryChange,
timeFilterService,
onRangeFromChange,
onRangeToChange,
onEsQueryChange,
toasts,
]
[onKueryChange, timeFilterService, clearSavedQuery, onRangeFromChange, onRangeToChange]
);

const onFilterUpdated = useCallback<(filters: Filter[]) => void>(
(updatedFilters) => {
clearSavedQuery();
onFiltersChange?.(updatedFilters);
},
[clearSavedQuery, onFiltersChange]
);

return (
Expand All @@ -131,6 +142,12 @@ export function ObservabilityAlertSearchBar({
featureIds={observabilityAlertFeatureIds}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
showFilterBar={showFilterBar}
filters={filters}
onFiltersUpdated={onFilterUpdated}
savedQuery={savedQuery}
onSavedQueryUpdated={setSavedQuery}
onClearSavedQuery={clearSavedQuery}
query={kuery}
onQuerySubmit={onSearchBarParamsChange}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
<ObservabilityAlertSearchBar
{...stateProps}
{...searchBarProps}
showFilterBar
services={{ timeFilterService, AlertsSearchBar, useToasts, uiSettings }}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { Filter } from '@kbn/es-query';
import {
createStateContainer,
createStateContainerReactHelpers,
Expand All @@ -17,6 +18,8 @@ interface AlertSearchBarContainerState {
rangeTo: string;
kuery: string;
status: AlertStatus;
filters: Filter[];
savedQueryId?: string;
}

interface AlertSearchBarStateTransitions {
Expand All @@ -32,20 +35,29 @@ interface AlertSearchBarStateTransitions {
setStatus: (
state: AlertSearchBarContainerState
) => (status: AlertStatus) => AlertSearchBarContainerState;
setFilters: (
state: AlertSearchBarContainerState
) => (filters: Filter[]) => AlertSearchBarContainerState;
setSavedQueryId: (
state: AlertSearchBarContainerState
) => (savedQueryId?: string) => AlertSearchBarContainerState;
}

const defaultState: AlertSearchBarContainerState = {
rangeFrom: 'now-24h',
rangeTo: 'now',
kuery: '',
status: ALL_ALERTS.status,
filters: [],
};

const transitions: AlertSearchBarStateTransitions = {
setRangeFrom: (state) => (rangeFrom) => ({ ...state, rangeFrom }),
setRangeTo: (state) => (rangeTo) => ({ ...state, rangeTo }),
setKuery: (state) => (kuery) => ({ ...state, kuery }),
setStatus: (state) => (status) => ({ ...state, status }),
setFilters: (state) => (filters) => ({ ...state, filters }),
setSavedQueryId: (state) => (savedQueryId) => ({ ...state, savedQueryId }),
};

const alertSearchBarStateContainer = createStateContainer(defaultState, transitions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import { isRight } from 'fp-ts/Either';
import { pipe } from 'fp-ts/pipeable';
import * as t from 'io-ts';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils';
import { TimefilterContract } from '@kbn/data-plugin/public';
import { SavedQuery, TimefilterContract } from '@kbn/data-plugin/public';
import {
createKbnUrlStateStorage,
syncState,
Expand Down Expand Up @@ -44,25 +44,61 @@ export function useAlertSearchBarStateContainer(
urlStorageKey: string,
{ replace }: { replace?: boolean } = {}
) {
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
const stateContainer = useContainer();

useUrlStateSyncEffect(stateContainer, urlStorageKey, replace);

const { setRangeFrom, setRangeTo, setKuery, setStatus } = stateContainer.transitions;
const { rangeFrom, rangeTo, kuery, status } = useContainerSelector(
const { setRangeFrom, setRangeTo, setKuery, setStatus, setFilters, setSavedQueryId } =
stateContainer.transitions;
const { rangeFrom, rangeTo, kuery, status, filters, savedQueryId } = useContainerSelector(
stateContainer,
(state) => state
);

useEffect(() => {
if (!savedQuery) {
setSavedQueryId(undefined);
return;
}
if (savedQuery.id !== savedQueryId) {
setSavedQueryId(savedQuery.id);
if (typeof savedQuery.attributes.query.query === 'string') {
setKuery(savedQuery.attributes.query.query);
}
if (savedQuery.attributes.filters?.length) {
setFilters(savedQuery.attributes.filters);
}
if (savedQuery.attributes.timefilter?.from) {
setRangeFrom(savedQuery.attributes.timefilter.from);
}
if (savedQuery.attributes.timefilter?.to) {
setRangeFrom(savedQuery.attributes.timefilter.to);
}
}
}, [
savedQuery,
savedQueryId,
setFilters,
setKuery,
setRangeFrom,
setSavedQueryId,
stateContainer,
]);

return {
kuery,
onKueryChange: setKuery,
onRangeFromChange: setRangeFrom,
onRangeToChange: setRangeTo,
onStatusChange: setStatus,
onFiltersChange: setFilters,
filters,
rangeFrom,
rangeTo,
status,
savedQuery,
setSavedQuery,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import { ReactElement } from 'react';
import { ToastsStart } from '@kbn/core-notifications-browser';
import { TimefilterContract } from '@kbn/data-plugin/public';
import { type SavedQuery, TimefilterContract } from '@kbn/data-plugin/public';
import { AlertsSearchBarProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_search_bar';
import { BoolQuery, Query } from '@kbn/es-query';
import { BoolQuery, Filter, Query } from '@kbn/es-query';
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import { AlertStatus } from '../../../common/typings';
export interface AlertStatusFilterProps {
Expand Down Expand Up @@ -44,6 +44,8 @@ export interface ObservabilityAlertSearchBarProps
extends AlertSearchBarContainerState,
AlertSearchBarStateTransitions,
CommonAlertSearchBarProps {
showFilterBar?: boolean;
savedQuery?: SavedQuery;
services: Services;
}

Expand All @@ -52,13 +54,17 @@ interface AlertSearchBarContainerState {
rangeTo: string;
kuery: string;
status: AlertStatus;
filters?: Filter[];
savedQueryId?: string;
}

interface AlertSearchBarStateTransitions {
onRangeFromChange: (rangeFrom: string) => void;
onRangeToChange: (rangeTo: string) => void;
onKueryChange: (kuery: string) => void;
onStatusChange: (status: AlertStatus) => void;
onFiltersChange?: (filters: Filter[]) => void;
setSavedQuery?: (savedQueryId?: SavedQuery) => void;
}

interface CommonAlertSearchBarProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,5 @@ export const getAlertsPageTableConfiguration = (
return { header, body, footer };
},
ruleTypeIds: observabilityRuleTypeRegistry.list(),
showInspectButton: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ function InternalAlertsPage() {
{...alertSearchBarStateProps}
appName={ALERTS_SEARCH_BAR_ID}
onEsQueryChange={setEsQuery}
showFilterBar
services={{ timeFilterService, AlertsSearchBar, useToasts, uiSettings }}
/>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ export default ({ getService }: FtrProviderContext) => {

it('Correctly applies date picker selections', async () => {
await retry.try(async () => {
await observability.alerts.common.submitQuery('kibana.alert.status: recovered');
await (await testSubjects.find('superDatePickerToggleQuickMenuButton')).click();
// We shouldn't expect any data for the last 15 minutes
// We shouldn't expect any recovered alert for the last 15 minutes
await (await testSubjects.find('superDatePickerCommonlyUsed_Last_15 minutes')).click();
await observability.alerts.common.getNoDataStateOrFail();
});
Expand Down

0 comments on commit 716b6f8

Please sign in to comment.