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

[Actionable Observability] Refactor alert search bar #143840

Merged
9 changes: 4 additions & 5 deletions x-pack/plugins/observability/common/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,11 @@ export interface ApmIndicesConfig {
apmAgentConfigurationIndex: string;
apmCustomLinkIndex: string;
}
export type AlertStatusFilterButton =
| typeof ALERT_STATUS_ACTIVE
| typeof ALERT_STATUS_RECOVERED
| '';

export type AlertStatus = typeof ALERT_STATUS_ACTIVE | typeof ALERT_STATUS_RECOVERED | '';

export interface AlertStatusFilter {
status: AlertStatusFilterButton;
status: AlertStatus;
query: string;
label: string;
}
3 changes: 3 additions & 0 deletions x-pack/plugins/observability/public/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
import { CasesUiStart } from '@kbn/cases-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';

export interface ObservabilityAppServices {
application: ApplicationStart;
cases: CasesUiStart;
Expand All @@ -42,5 +44,6 @@ export interface ObservabilityAppServices {
theme: ThemeServiceStart;
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
uiSettings: IUiSettingsClient;
unifiedSearch: UnifiedSearchPublicPluginStart;
isDev?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,27 @@
* 2.0.
*/

import React, { useMemo, useState } from 'react';
import { TimeHistory } from '@kbn/data-plugin/public';
import { SearchBar } from '@kbn/unified-search-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import React, { useState } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import { translations } from '../../../config';
import { ObservabilityAppServices } from '../../../application/types';
import { useAlertDataView } from '../../../hooks/use_alert_data_view';

type QueryLanguageType = 'lucene' | 'kuery';

const NO_INDEX_PATTERNS: DataView[] = [];

export function AlertsSearchBar({
appName,
featureIds,
onQueryChange,
query,
onQueryChange,
rangeFrom,
rangeTo,
}: {
appName: string;
featureIds: ValidFeatureId[];
rangeFrom?: string;
rangeTo?: string;
Expand All @@ -31,28 +35,32 @@ export function AlertsSearchBar({
query?: string;
}) => void;
}) {
const timeHistory = useMemo(() => {
return new TimeHistory(new Storage(localStorage));
}, []);
const {
unifiedSearch: {
ui: { SearchBar },
},
} = useKibana<ObservabilityAppServices>().services;

const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
const { value: dataView, loading, error } = useAlertDataView(featureIds);

return (
<SearchBar
indexPatterns={loading || error ? [] : [dataView!]}
appName={appName}
indexPatterns={loading || error ? NO_INDEX_PATTERNS : [dataView!]}
placeholder={translations.alertsSearchBar.placeholder}
query={{ query: query ?? '', language: queryLanguage }}
timeHistory={timeHistory}
dateRangeFrom={rangeFrom}
dateRangeTo={rangeTo}
displayStyle="inPage"
showFilterBar={false}
onQuerySubmit={({ dateRange, query: nextQuery }) => {
onQueryChange({
dateRange,
query: typeof nextQuery?.query === 'string' ? nextQuery.query : '',
});
setQueryLanguage((nextQuery?.language ?? 'kuery') as QueryLanguageType);
}}
displayStyle="inPage"
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/rule-data-utils';
import { AlertStatusFilterButton } from '../../../../common/typings';
import { AlertStatus } from '../../../../common/typings';
import { AlertStatusFilter } from '../../../../common/typings';

export interface AlertStatusFilterProps {
status: AlertStatusFilterButton;
status: AlertStatus;
onChange: (id: string, value: string) => void;
}

Expand Down Expand Up @@ -41,6 +41,11 @@ export const RECOVERED_ALERTS: AlertStatusFilter = {
}),
};

export const ALERT_STATUS_QUERY = {
[ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query,
[RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query,
};

const options: EuiButtonGroupOptionProps[] = [
{
id: ALL_ALERTS.status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutSize } from '@elastic/eui';

import React, { useCallback, useEffect, useState } from 'react';
import { AnyQuery, BoolQuery } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public';
import { AlertConsumers, AlertStatus } from '@kbn/rule-data-utils';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { AlertStatus } from '../../../../../common/typings';
import { observabilityAlertFeatureIds } from '../../../../config';
import { AlertStatusFilterButton } from '../../../../../common/typings';
import { useGetUserCasesPermissions } from '../../../../hooks/use_get_user_cases_permissions';
import { observabilityFeatureId } from '../../../../../common';
import { useBreadcrumbs } from '../../../../hooks/use_breadcrumbs';
Expand All @@ -28,25 +29,18 @@ import {
useAlertsPageStateContainer,
} from '../state_container';
import './styles.scss';
import { AlertsStatusFilter, AlertsSearchBar, ALL_ALERTS } from '../../components';
import { AlertsStatusFilter, AlertsSearchBar, ALERT_STATUS_QUERY } from '../../components';
import { renderRuleStats } from '../../components/rule_stats';
import { ObservabilityAppServices } from '../../../../application/types';
import {
ALERT_STATUS_REGEX,
ALERTS_PER_PAGE,
ALERTS_TABLE_ID,
BASE_ALERT_REGEX,
} from './constants';
import { ALERTS_PER_PAGE, ALERTS_TABLE_ID } from './constants';
import { RuleStatsState } from './types';

const getAlertStatusQuery = (status: string): AnyQuery[] => {
return status ? [{ query: ALERT_STATUS_QUERY[status], language: 'kuery' }] : [];
};

function AlertsPage() {
const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext();
const [alertFilterStatus, setAlertFilterStatus] = useState(
ALL_ALERTS.query as AlertStatusFilterButton
);
const [refreshNow, setRefreshNow] = useState<number>();
const { rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery } =
useAlertsPageStateContainer();
const {
cases,
docLinks,
Expand All @@ -59,7 +53,6 @@ function AlertsPage() {
},
},
} = useKibana<ObservabilityAppServices>().services;

const [ruleStatsLoading, setRuleStatsLoading] = useState<boolean>(false);
const [ruleStats, setRuleStats] = useState<RuleStatsState>({
total: 0,
Expand All @@ -68,10 +61,19 @@ function AlertsPage() {
error: 0,
snoozed: 0,
});

useEffect(() => {
syncAlertStatusFilterStatus(kuery as string);
}, [kuery]);
const { hasAnyData, isAllRequestsComplete } = useHasData();
const { rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, status, setStatus } =
useAlertsPageStateContainer();
const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(
buildEsQuery(
{
to: rangeTo,
from: rangeFrom,
},
kuery,
getAlertStatusQuery(status)
)
);

useBreadcrumbs([
{
Expand Down Expand Up @@ -123,57 +125,46 @@ function AlertsPage() {

const manageRulesHref = http.basePath.prepend('/app/observability/alerts/rules');

const onRefresh = () => {
setRefreshNow(new Date().getTime());
};
const onStatusChange = useCallback(
(alertStatus: AlertStatus) => {
setEsQuery(
buildEsQuery(
{
to: rangeTo,
from: rangeFrom,
},
kuery,
getAlertStatusQuery(alertStatus)
)
);
},
[kuery, rangeFrom, rangeTo]
);

useEffect(() => {
onStatusChange(status);
}, [onStatusChange, status]);

const onQueryChange = useCallback(
const onSearchBarParamsChange = useCallback(
({ dateRange, query }) => {
if (rangeFrom === dateRange.from && rangeTo === dateRange.to && kuery === (query ?? '')) {
return onRefresh();
}
timeFilterService.setTime(dateRange);
setRangeFrom(dateRange.from);
setRangeTo(dateRange.to);
setKuery(query);
syncAlertStatusFilterStatus(query as string);
},
[rangeFrom, setRangeFrom, rangeTo, setRangeTo, kuery, setKuery, timeFilterService]
);

const syncAlertStatusFilterStatus = (query: string) => {
const [, alertStatus] = BASE_ALERT_REGEX.exec(query) || [];
if (!alertStatus) {
setAlertFilterStatus('');
return;
}
setAlertFilterStatus(alertStatus.toLowerCase() as AlertStatus);
};
const setAlertStatusFilter = useCallback(
(id: string, query: string) => {
setAlertFilterStatus(id as AlertStatusFilterButton);
// Updating the KQL query bar alongside with user inputs is tricky.
// To avoid issue, this function always remove the AlertFilter and add it
// at the end of the query, each time the filter is added/updated/removed (Show All)
// NOTE: This (query appending) will be changed entirely: https://github.com/elastic/kibana/issues/116135
let output;
if (kuery === '') {
output = query;
} else {
const queryWithoutAlertFilter = kuery.replace(ALERT_STATUS_REGEX, '');
output = `${queryWithoutAlertFilter} and ${query}`;
}
onQueryChange({
dateRange: { from: rangeFrom, to: rangeTo },
// Clean up the kuery from unwanted trailing/ahead ANDs after appending and removing filters.
query: output.replace(/^\s*and\s*|\s*and\s*$/gm, ''),
});
Comment on lines -152 to -170
Copy link
Contributor

Choose a reason for hiding this comment

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

👍🏻 so much cleaner

setEsQuery(
buildEsQuery(
{
to: rangeTo,
from: rangeFrom,
},
query,
getAlertStatusQuery(status)
)
);
},
[kuery, onQueryChange, rangeFrom, rangeTo]
[rangeFrom, setRangeFrom, rangeTo, status, setRangeTo, setKuery, timeFilterService]
);

const { hasAnyData, isAllRequestsComplete } = useHasData();

// If there is any data, set hasData to true otherwise we need to wait till all the data is loaded before setting hasData to true or false; undefined indicates the data is still loading.
const hasData = hasAnyData === true || (isAllRequestsComplete === false ? undefined : false);

Expand Down Expand Up @@ -205,18 +196,24 @@ function AlertsPage() {
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<AlertsSearchBar
appName={'observability-alerts'}
featureIds={observabilityAlertFeatureIds}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
query={kuery}
onQueryChange={onQueryChange}
onQueryChange={onSearchBarParamsChange}
/>
</EuiFlexItem>

<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<AlertsStatusFilter status={alertFilterStatus} onChange={setAlertStatusFilter} />
<AlertsStatusFilter
status={status}
onChange={(id) => {
setStatus(id as AlertStatus);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
Expand All @@ -233,16 +230,9 @@ function AlertsPage() {
id={ALERTS_TABLE_ID}
flyoutSize={'s' as EuiFlyoutSize}
featureIds={observabilityAlertFeatureIds}
query={buildEsQuery(
{
to: rangeTo,
from: rangeFrom,
},
kuery
)}
query={esQuery}
showExpandToDetails={false}
pageSize={ALERTS_PER_PAGE}
refreshNow={refreshNow}
/>
</CasesContext>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,6 @@
* 2.0.
*/

import { DataViewBase } from '@kbn/es-query';
import { ALERT_STATUS } from '@kbn/rule-data-utils';

export const ALERTS_PAGE_ID = 'alerts-o11y';
export const ALERTS_PER_PAGE = 50;
export const ALERTS_TABLE_ID = 'xpack.observability.alerts.alert.table';

const regExpEscape = (str: string) => str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
export const NO_INDEX_PATTERNS: DataViewBase[] = [];
export const BASE_ALERT_REGEX = new RegExp(
`\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*"(.*?|\\*?)"`
);
export const ALERT_STATUS_REGEX = new RegExp(
`\\s*and\\s*${regExpEscape(ALERT_STATUS)}\\s*:\\s*(".+?"|\\*?)|${regExpEscape(
ALERT_STATUS
)}\\s*:\\s*(".+?"|\\*?)`,
'gm'
);
Loading