Skip to content

Commit

Permalink
[Actionable Observability] Integrate alert search bar on rule details…
Browse files Browse the repository at this point in the history
… page (#144718)

Resolves #143962

## 📝 Summary
In this PR, an alerts search bar was added to the rule details page by
syncing its state to the URL. This will enable navigating to the alerts
table for a specific rule with a filtered state based on active or
recovered.
### Notes
- Renamed alert page container to alert search bar container and used it
both in alerts and rule details page (it will be responsible to sync
search bar params to the URL) --> moved to a shared component
- Moved AlertsStatusFilter to be a sub-component of the shared
observability search bar
- Allowed ObservabilityAlertSearchBar to be used both as a stand-alone
component and as a wired component with syncing params to the URL
(ObservabilityAlertSearchBar, ObservabilityAlertSearchbarWithUrlSync)
- Set a minHeight for the Alerts and Execution tab, otherwise, the page
will have extra scroll on the tab change while content is loading (very
annoying!)

## 🎨 Preview

![image](https://user-images.githubusercontent.com/12370520/200547324-d9c4ef3c-8a82-4c16-88bd-f1d4b2bc8006.png)

## 🧪 How to test
- Create a rule and go to the rule details page
- Click on the alerts tab and change the search criteria, you should be
able to see the criteria in the query parameter
- Refresh the page, alerts tab should be selected and you should be able
to see the filters that you applied in the previous step
- As a side test, check alert search bar on alerts page as well, it
should work as before

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
maryam-saeidi and kibanamachine authored Nov 9, 2022
1 parent b1179e7 commit ef7c1a6
Show file tree
Hide file tree
Showing 25 changed files with 549 additions and 292 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import React, { useCallback, useEffect } from 'react';
import { Query } from '@kbn/es-query';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { observabilityAlertFeatureIds } from '../../../config';
import { ObservabilityAppServices } from '../../../application/types';
import { AlertsStatusFilter } from './components';
import { ALERT_STATUS_QUERY, DEFAULT_QUERIES } from './constants';
import { AlertSearchBarProps } from './types';
import { buildEsQuery } from '../../../utils/build_es_query';
import { AlertStatus } from '../../../../common/typings';

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

export function AlertSearchBar({
appName,
rangeFrom,
setRangeFrom,
rangeTo,
setRangeTo,
kuery,
setKuery,
status,
setStatus,
setEsQuery,
queries = DEFAULT_QUERIES,
}: AlertSearchBarProps) {
const {
data: {
query: {
timefilter: { timefilter: timeFilterService },
},
},
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar },
} = useKibana<ObservabilityAppServices>().services;

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

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

const onSearchBarParamsChange = useCallback(
({ dateRange, query }) => {
timeFilterService.setTime(dateRange);
setRangeFrom(dateRange.from);
setRangeTo(dateRange.to);
setKuery(query);
setEsQuery(
buildEsQuery(
{
to: rangeTo,
from: rangeFrom,
},
query,
[...getAlertStatusQuery(status), ...queries]
)
);
},
[
timeFilterService,
setRangeFrom,
setRangeTo,
setKuery,
setEsQuery,
rangeTo,
rangeFrom,
status,
queries,
]
);

return (
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<AlertsSearchBar
appName={appName}
featureIds={observabilityAlertFeatureIds}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
query={kuery}
onQueryChange={onSearchBarParamsChange}
/>
</EuiFlexItem>

<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<AlertsStatusFilter
status={status}
onChange={(id) => {
setStatus(id as AlertStatus);
}}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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 {
alertSearchBarStateContainer,
Provider,
useAlertSearchBarStateContainer,
} from './containers';
import { AlertSearchBar } from './alert_search_bar';
import { AlertSearchBarWithUrlSyncProps } from './types';

function InternalAlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const stateProps = useAlertSearchBarStateContainer();

return <AlertSearchBar {...props} {...stateProps} />;
}

export function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
return (
<Provider value={alertSearchBarStateContainer}>
<InternalAlertSearchbarWithUrlSync {...props} />
</Provider>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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 { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import React from 'react';
import { ALL_ALERTS, ACTIVE_ALERTS, RECOVERED_ALERTS } from '../constants';
import { AlertStatusFilterProps } from '../types';

const options: EuiButtonGroupOptionProps[] = [
{
id: ALL_ALERTS.status,
label: ALL_ALERTS.label,
value: ALL_ALERTS.query,
'data-test-subj': 'alert-status-filter-show-all-button',
},
{
id: ACTIVE_ALERTS.status,
label: ACTIVE_ALERTS.label,
value: ACTIVE_ALERTS.query,
'data-test-subj': 'alert-status-filter-active-button',
},
{
id: RECOVERED_ALERTS.status,
label: RECOVERED_ALERTS.label,
value: RECOVERED_ALERTS.query,
'data-test-subj': 'alert-status-filter-recovered-button',
},
];

export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) {
return (
<EuiButtonGroup
legend="Filter by"
color="primary"
options={options}
idSelected={status}
onChange={onChange}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
* 2.0.
*/

export { Provider, alertsPageStateContainer } from './state_container';
export { useAlertsPageStateContainer } from './use_alerts_page_state_container';
export { AlertsStatusFilter } from './alerts_status_filter';
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,12 @@
* 2.0.
*/

import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, ALERT_STATUS } from '@kbn/rule-data-utils';
import { AlertStatus } from '../../../../common/typings';
import { AlertStatusFilter } from '../../../../common/typings';

export interface AlertStatusFilterProps {
status: AlertStatus;
onChange: (id: string, value: string) => void;
}
export const DEFAULT_QUERIES: Query[] = [];

export const ALL_ALERTS: AlertStatusFilter = {
status: '',
Expand Down Expand Up @@ -45,36 +40,3 @@ export const ALERT_STATUS_QUERY = {
[ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query,
[RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query,
};

const options: EuiButtonGroupOptionProps[] = [
{
id: ALL_ALERTS.status,
label: ALL_ALERTS.label,
value: ALL_ALERTS.query,
'data-test-subj': 'alert-status-filter-show-all-button',
},
{
id: ACTIVE_ALERTS.status,
label: ACTIVE_ALERTS.label,
value: ACTIVE_ALERTS.query,
'data-test-subj': 'alert-status-filter-active-button',
},
{
id: RECOVERED_ALERTS.status,
label: RECOVERED_ALERTS.label,
value: RECOVERED_ALERTS.query,
'data-test-subj': 'alert-status-filter-recovered-button',
},
];

export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) {
return (
<EuiButtonGroup
legend="Filter by"
color="primary"
options={options}
idSelected={status}
onChange={onChange}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export { Provider, alertSearchBarStateContainer } from './state_container';
export { useAlertSearchBarStateContainer } from './use_alert_search_bar_state_container';
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 {
createStateContainer,
createStateContainerReactHelpers,
} 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;
}

interface AlertSearchBarStateTransitions {
setRangeFrom: (
state: AlertSearchBarContainerState
) => (rangeFrom: string) => AlertSearchBarContainerState;
setRangeTo: (
state: AlertSearchBarContainerState
) => (rangeTo: string) => AlertSearchBarContainerState;
setKuery: (
state: AlertSearchBarContainerState
) => (kuery: string) => AlertSearchBarContainerState;
setStatus: (
state: AlertSearchBarContainerState
) => (status: AlertStatus) => AlertSearchBarContainerState;
}

const defaultState: AlertSearchBarContainerState = {
rangeFrom: 'now-15m',
rangeTo: 'now',
kuery: '',
status: ALL_ALERTS.status as AlertStatus,
};

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

const alertSearchBarStateContainer = createStateContainer(defaultState, transitions);

type AlertSearchBarStateContainer = typeof alertSearchBarStateContainer;

const { Provider, useContainer } = createStateContainerReactHelpers<AlertSearchBarStateContainer>();

export { Provider, alertSearchBarStateContainer, useContainer, defaultState };
export type {
AlertSearchBarStateContainer,
AlertSearchBarContainerState,
AlertSearchBarStateTransitions,
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { useTimefilterService } from '../../../../hooks/use_timefilter_service';
import {
useContainer,
defaultState,
AlertsPageStateContainer,
AlertsPageContainerState,
AlertSearchBarStateContainer,
AlertSearchBarContainerState,
} from './state_container';

export function useAlertsPageStateContainer() {
export function useAlertSearchBarStateContainer() {
const stateContainer = useContainer();

useUrlStateSyncEffect(stateContainer);
Expand All @@ -47,7 +47,7 @@ export function useAlertsPageStateContainer() {
};
}

function useUrlStateSyncEffect(stateContainer: AlertsPageStateContainer) {
function useUrlStateSyncEffect(stateContainer: AlertSearchBarStateContainer) {
const history = useHistory();
const timefilterService = useTimefilterService();

Expand All @@ -68,11 +68,11 @@ function useUrlStateSyncEffect(stateContainer: AlertsPageStateContainer) {
}

function setupUrlStateSync(
stateContainer: AlertsPageStateContainer,
stateContainer: AlertSearchBarStateContainer,
stateStorage: IKbnUrlStateStorage
) {
// This handles filling the state when an incomplete URL set is provided
const setWithDefaults = (changedState: Partial<AlertsPageContainerState> | null) => {
const setWithDefaults = (changedState: Partial<AlertSearchBarContainerState> | null) => {
stateContainer.set({ ...defaultState, ...changedState });
};

Expand All @@ -88,10 +88,10 @@ function setupUrlStateSync(

function syncUrlStateWithInitialContainerState(
timefilterService: TimefilterContract,
stateContainer: AlertsPageStateContainer,
stateContainer: AlertSearchBarStateContainer,
urlStateStorage: IKbnUrlStateStorage
) {
const urlState = urlStateStorage.get<Partial<AlertsPageContainerState>>('_a');
const urlState = urlStateStorage.get<Partial<AlertSearchBarContainerState>>('_a');

if (urlState) {
const newState = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export { AlertSearchBar as ObservabilityAlertSearchBar } from './alert_search_bar';
export { AlertSearchbarWithUrlSync as ObservabilityAlertSearchbarWithUrlSync } from './alert_search_bar_with_url_sync';
Loading

0 comments on commit ef7c1a6

Please sign in to comment.