Skip to content

Commit

Permalink
[Actionable Observability] Expose ObservabilityAlertSearchBar from Ob…
Browse files Browse the repository at this point in the history
…servability plugin (#146401)

Resolves #146286

## 📝 Summary

In this PR, I exposed ObservabilityAlertSearchBar from the Observability
plugin to be used in other plugins such as APM.
I've added `ObservabilityAlertSearchBarProvider` in order for other
plugins to provide Kibana dependencies to the shared component.

## 🧪 How to test

For testing the implementation, I imported this component in the APM
plugin and used it in the alerts tab, you can do the same locally by
following these steps:
1. Import `ObservabilityAlertSearchBar` in
[APM](https://github.com/elastic/kibana/blob/main/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx)
and define related hook:
```
import {
  ObservabilityAlertSearchBar,
  ObservabilityAlertSearchBarProvider,
} from '@kbn/observability-plugin/public';

export const useToasts = () =>
  useKibana<ApmPluginStartDeps>().services.notifications!.toasts;
```

2. Replace
[AlertsTableStatusFilter](https://github.com/elastic/kibana/blob/main/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx#L74)
with the `ObservabilityAlertSearchBar` component:
```
<ObservabilityAlertSearchBarProvider
  {...services}
  useToasts={useToasts}
>
  <ObservabilityAlertSearchBar
    appName={'apmApp'}
    kuery={''}
    onRangeFromChange={(input) => console.log(input)}
    onRangeToChange={(input) => console.log(input)}
    onKueryChange={(input) => console.log(input)}
    onStatusChange={(input) => console.log(input)}
    onEsQueryChange={(input) => console.log(input)}
    rangeTo={'now'}
    rangeFrom={'now-15m'}
    status={'all'}
  />
</ObservabilityAlertSearchBarProvider>
```
You should see the new search bar in APM alerts tab:


![image](https://user-images.githubusercontent.com/12370520/204302146-c0ff4658-67ab-4639-a955-b75a647f57da.png)
  • Loading branch information
maryam-saeidi authored Dec 7, 2022
1 parent 98412f7 commit 9b828e3
Show file tree
Hide file tree
Showing 13 changed files with 263 additions and 124 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,57 +5,51 @@
* 2.0.
*/

import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks';
import React from 'react';
import { act, waitFor } from '@testing-library/react';
import { AlertSearchBarProps } from './types';
import { waitFor } from '@testing-library/react';
import { timefilterServiceMock } from '@kbn/data-plugin/public/query/timefilter/timefilter_service.mock';
import { useServices } from './services';
import { ObservabilityAlertSearchBarProps } from './types';
import { ObservabilityAlertSearchBar } from './alert_search_bar';
import { observabilityAlertFeatureIds } from '../../../config';
import { useKibana } from '../../../utils/kibana_react';
import { kibanaStartMock } from '../../../utils/kibana_react.mock';
import { render } from '../../../utils/test_helper';

const useKibanaMock = useKibana as jest.Mock;
const useServicesMock = useServices as jest.Mock;
const getAlertsSearchBarMock = jest.fn();
const ALERT_SEARCH_BAR_DATA_TEST_SUBJ = 'alerts-search-bar';
const ACTIVE_BUTTON_DATA_TEST_SUBJ = 'alert-status-filter-active-button';

jest.mock('../../../utils/kibana_react');

const mockKibana = () => {
useKibanaMock.mockReturnValue({
services: {
...kibanaStartMock.startContract().services,
triggersActionsUi: {
...triggersActionsUiMock.createStart(),
getAlertsSearchBar: getAlertsSearchBarMock.mockReturnValue(
<div data-test-subj={ALERT_SEARCH_BAR_DATA_TEST_SUBJ} />
),
},
},

jest.mock('./services');

const mockServices = () => {
useServicesMock.mockReturnValue({
timeFilterService: timefilterServiceMock,
AlertsSearchBar: getAlertsSearchBarMock.mockReturnValue(
<div data-test-subj={ALERT_SEARCH_BAR_DATA_TEST_SUBJ} />
),
useToasts: jest.fn(),
});
};

describe('ObservabilityAlertSearchBar', () => {
const renderComponent = (props: Partial<AlertSearchBarProps> = {}) => {
const alertSearchBarProps: AlertSearchBarProps = {
const renderComponent = (props: Partial<ObservabilityAlertSearchBarProps> = {}) => {
const observabilityAlertSearchBarProps: ObservabilityAlertSearchBarProps = {
appName: 'testAppName',
rangeFrom: 'now-15m',
setRangeFrom: jest.fn(),
rangeTo: 'now',
setRangeTo: jest.fn(),
kuery: '',
setKuery: jest.fn(),
status: 'active',
setStatus: jest.fn(),
setEsQuery: jest.fn(),
onRangeFromChange: jest.fn(),
onRangeToChange: jest.fn(),
onKueryChange: jest.fn(),
onStatusChange: jest.fn(),
onEsQueryChange: jest.fn(),
rangeTo: 'now',
rangeFrom: 'now-15m',
status: 'all',
...props,
};
return render(<ObservabilityAlertSearchBar {...alertSearchBarProps} />);
return render(<ObservabilityAlertSearchBar {...observabilityAlertSearchBarProps} />);
};

beforeAll(() => {
mockKibana();
mockServices();
});

beforeEach(() => {
Expand All @@ -71,9 +65,7 @@ describe('ObservabilityAlertSearchBar', () => {
});

it('should call alert search bar with correct props', () => {
act(() => {
renderComponent();
});
renderComponent();

expect(getAlertsSearchBarMock).toHaveBeenCalledWith(
expect.objectContaining({
Expand All @@ -88,27 +80,71 @@ describe('ObservabilityAlertSearchBar', () => {
});

it('should filter active alerts', async () => {
const mockedSetEsQuery = jest.fn();
const mockedOnEsQueryChange = jest.fn();
const mockedFrom = '2022-11-15T09:38:13.604Z';
const mockedTo = '2022-11-15T09:53:13.604Z';
const { getByTestId } = renderComponent({
setEsQuery: mockedSetEsQuery,

renderComponent({
onEsQueryChange: mockedOnEsQueryChange,
rangeFrom: mockedFrom,
rangeTo: mockedTo,
status: 'active',
});

await act(async () => {
const activeButton = getByTestId(ACTIVE_BUTTON_DATA_TEST_SUBJ);
activeButton.click();
expect(mockedOnEsQueryChange).toHaveBeenCalledWith({
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'kibana.alert.status': 'active' } }],
},
},
{
range: {
'@timestamp': expect.objectContaining({
format: 'strict_date_optional_time',
gte: mockedFrom,
lte: mockedTo,
}),
},
},
],
must: [],
must_not: [],
should: [],
},
});
});

it('should include defaultSearchQueries 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 defaultSearchQueries = [
{
query: 'kibana.alert.rule.uuid: 413a9631-1a29-4344-a8b4-9a1dc23421ee',
language: 'kuery',
},
];

expect(mockedSetEsQuery).toHaveBeenCalledWith({
renderComponent({
onEsQueryChange: mockedOnEsQueryChange,
rangeFrom: mockedFrom,
rangeTo: mockedTo,
defaultSearchQueries,
status: 'all',
});

expect(mockedOnEsQueryChange).toHaveBeenCalledWith({
bool: {
filter: [
{
bool: {
minimum_should_match: 1,
should: [{ match_phrase: { 'kibana.alert.status': 'active' } }],
should: [
{ match: { 'kibana.alert.rule.uuid': '413a9631-1a29-4344-a8b4-9a1dc23421ee' } },
],
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@
*/

import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';

import React, { useCallback, useEffect } from 'react';

import { i18n } from '@kbn/i18n';
import { Query } from '@kbn/es-query';
import { useKibana } from '../../../utils/kibana_react';
import { observabilityAlertFeatureIds } from '../../../config';
import { ObservabilityAppServices } from '../../../application/types';
import { useServices } from './services';
import { AlertsStatusFilter } from './components';
import { observabilityAlertFeatureIds } from '../../../config';
import { ALERT_STATUS_QUERY, DEFAULT_QUERIES, DEFAULT_QUERY_STRING } from './constants';
import { AlertSearchBarProps } from './types';
import { ObservabilityAlertSearchBarProps } from './types';
import { buildEsQuery } from '../../../utils/build_es_query';
import { AlertStatus } from '../../../../common/typings';

Expand All @@ -27,83 +26,79 @@ const getAlertStatusQuery = (status: string): Query[] => {

export function ObservabilityAlertSearchBar({
appName,
defaultSearchQueries = DEFAULT_QUERIES,
onEsQueryChange,
onKueryChange,
onRangeFromChange,
onRangeToChange,
onStatusChange,
kuery,
rangeFrom,
setRangeFrom,
rangeTo,
setRangeTo,
kuery,
setKuery,
status,
setStatus,
setEsQuery,
queries = DEFAULT_QUERIES,
}: AlertSearchBarProps) {
const {
data: {
query: {
timefilter: { timefilter: timeFilterService },
},
},
notifications: { toasts },
triggersActionsUi: { getAlertsSearchBar: AlertsSearchBar },
} = useKibana<ObservabilityAppServices>().services;
}: ObservabilityAlertSearchBarProps) {
const { AlertsSearchBar, timeFilterService, useToasts } = useServices();
const toasts = useToasts();

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

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

const onSearchBarParamsChange = useCallback(
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(
{
to: rangeTo,
from: rangeFrom,
to: dateRange.to,
from: dateRange.from,
},
query,
[...getAlertStatusQuery(status), ...queries]
[...getAlertStatusQuery(status), ...defaultSearchQueries]
);
setKuery(query);
onKueryChange(query);
timeFilterService.setTime(dateRange);
setRangeFrom(dateRange.from);
setRangeTo(dateRange.to);
setEsQuery(esQuery);
onRangeFromChange(dateRange.from);
onRangeToChange(dateRange.to);
onEsQueryChange(esQuery);
} catch (error) {
toasts.addError(error, {
title: i18n.translate('xpack.observability.alerts.searchBar.invalidQueryTitle', {
defaultMessage: 'Invalid query string',
}),
});
setKuery(DEFAULT_QUERY_STRING);
onKueryChange(DEFAULT_QUERY_STRING);
}
},
[
defaultSearchQueries,
timeFilterService,
setRangeFrom,
setRangeTo,
setKuery,
setEsQuery,
rangeTo,
rangeFrom,
onRangeFromChange,
onRangeToChange,
onKueryChange,
onEsQueryChange,
status,
queries,
toasts,
]
);
Expand All @@ -124,15 +119,13 @@ export function ObservabilityAlertSearchBar({
<EuiFlexItem>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<AlertsStatusFilter
status={status}
onChange={(id) => {
setStatus(id as AlertStatus);
}}
/>
<AlertsStatusFilter status={status} onChange={onStatusChange} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
);
}

// eslint-disable-next-line import/no-default-export
export default ObservabilityAlertSearchBar;
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,26 @@ import {
useAlertSearchBarStateContainer,
} from './containers';
import { ObservabilityAlertSearchBar } from './alert_search_bar';
import { ObservabilityAlertSearchBarProvider } from './services';
import { AlertSearchBarWithUrlSyncProps } from './types';
import { useKibana } from '../../../utils/kibana_react';
import { ObservabilityAppServices } from '../../../application/types';
import { useToasts } from '../../../hooks/use_toast';

function AlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
const { urlStorageKey, ...searchBarProps } = props;
const stateProps = useAlertSearchBarStateContainer(urlStorageKey);
const { data, triggersActionsUi } = useKibana<ObservabilityAppServices>().services;

return <ObservabilityAlertSearchBar {...stateProps} {...searchBarProps} />;
return (
<ObservabilityAlertSearchBarProvider
data={data}
triggersActionsUi={triggersActionsUi}
useToasts={useToasts}
>
<ObservabilityAlertSearchBar {...stateProps} {...searchBarProps} />
</ObservabilityAlertSearchBarProvider>
);
}

export function ObservabilityAlertSearchbarWithUrlSync(props: AlertSearchBarWithUrlSyncProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
*/

import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALL_ALERTS, ACTIVE_ALERTS, RECOVERED_ALERTS } from '../constants';
import { AlertStatusFilterProps } from '../types';
import { AlertStatus } from '../../../../../common/typings';

const options: EuiButtonGroupOptionProps[] = [
{
Expand All @@ -34,11 +36,13 @@ const options: EuiButtonGroupOptionProps[] = [
export function AlertsStatusFilter({ status, onChange }: AlertStatusFilterProps) {
return (
<EuiButtonGroup
legend="Filter by"
legend={i18n.translate('xpack.observability.alerts.alertStatusFilter.legend', {
defaultMessage: 'Filter by',
})}
color="primary"
options={options}
idSelected={status}
onChange={onChange}
onChange={(id) => onChange(id as AlertStatus)}
/>
);
}
Loading

0 comments on commit 9b828e3

Please sign in to comment.