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] Expose ObservabilityAlertSearchBar from Observability plugin #146401

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Contributor

Choose a reason for hiding this comment

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

I added getAlertsSearchBar to the TriggerActionsUI mock over here. So you might not need to reintroduce this here.

Copy link
Member Author

Choose a reason for hiding this comment

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

But this one is mocking useServices not useKibana, and I also want to check the related props, so I defined the mock here.

<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';
Copy link
Contributor

Choose a reason for hiding this comment

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

What benefit does it have to introduce a new useServices which just exports useKibana().services ?

Copy link
Member Author

@maryam-saeidi maryam-saeidi Dec 1, 2022

Choose a reason for hiding this comment

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

Since this component will be used in another plugin, we don't know if they have all the related dependencies defined in their plugin as this shared component needs.
So I used a similar pattern as mentioned in Clint's presentation to make these dependencies explicit and the consumer should make sure to pass the related services to the ObservabilityAlertSearchBarProvider that I defined here.
useServices is a hook that will access the services passed to the provider.

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,
Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed for readability

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);
maryam-saeidi marked this conversation as resolved.
Show resolved Hide resolved
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);
clintandrewhall marked this conversation as resolved.
Show resolved Hide resolved
}
},
[
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