[Actionable Observability] Refactor data view creation in alerts search bar (#142474)

* Refactor creating dataView for alert table

* Add test for use_alert_data_view

* Use [] as default value for indexPatterns
This commit is contained in:
Maryam Saeidi 2022-10-05 10:30:02 +02:00 committed by GitHub
parent 7203e303da
commit 8f50c3b34b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 78 deletions

View file

@ -0,0 +1,16 @@
/*
* 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 { AlertConsumers } from '@kbn/rule-data-utils';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
export const observabilityAlertFeatureIds: ValidFeatureId[] = [
AlertConsumers.APM,
AlertConsumers.INFRASTRUCTURE,
AlertConsumers.LOGS,
AlertConsumers.UPTIME,
];

View file

@ -7,6 +7,7 @@
export { paths } from './paths';
export { translations } from './translations';
export { observabilityAlertFeatureIds } from './alert_feature_ids';
export enum AlertingPages {
alerts = 'alerts',

View file

@ -0,0 +1,100 @@
/*
* 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 { DataView } from '@kbn/data-views-plugin/common';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import { act, renderHook } from '@testing-library/react-hooks';
import { AsyncState } from 'react-use/lib/useAsync';
import { kibanaStartMock } from '../utils/kibana_react.mock';
import { observabilityAlertFeatureIds } from '../config';
import { useAlertDataView } from './use_alert_data_view';
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
jest.mock('@kbn/kibana-react-plugin/public', () => ({
__esModule: true,
useKibana: jest.fn(() => mockUseKibanaReturnValue),
}));
describe('useAlertDataView', () => {
const mockedDataView = 'dataView';
beforeEach(() => {
mockUseKibanaReturnValue.services.http.get.mockImplementation(async () => ({
index_name: [
'.alerts-observability.uptime.alerts-*',
'.alerts-observability.metrics.alerts-*',
'.alerts-observability.logs.alerts-*',
'.alerts-observability.apm.alerts-*',
],
}));
mockUseKibanaReturnValue.services.data.dataViews.create.mockImplementation(
async () => mockedDataView
);
});
afterEach(() => {
jest.clearAllMocks();
});
it('initially is loading and does not have data', async () => {
await act(async () => {
const mockedAsyncDataView = {
loading: true,
};
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], AsyncState<DataView>>(() =>
useAlertDataView(observabilityAlertFeatureIds)
);
await waitForNextUpdate();
expect(result.current).toEqual(mockedAsyncDataView);
});
});
it('returns dataView for the provided featureIds', async () => {
await act(async () => {
const mockedAsyncDataView = {
loading: false,
value: mockedDataView,
};
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], AsyncState<DataView>>(() =>
useAlertDataView(observabilityAlertFeatureIds)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual(mockedAsyncDataView);
});
});
it('returns error with no data when error happens', async () => {
const error = new Error('http error');
mockUseKibanaReturnValue.services.http.get.mockImplementation(async () => {
throw error;
});
await act(async () => {
const mockedAsyncDataView = {
loading: false,
error,
};
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], AsyncState<DataView>>(() =>
useAlertDataView(observabilityAlertFeatureIds)
);
await waitForNextUpdate();
await waitForNextUpdate();
expect(result.current).toEqual(mockedAsyncDataView);
});
});
});

View file

@ -0,0 +1,33 @@
/*
* 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 { DataView } from '@kbn/data-views-plugin/common';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import useAsync from 'react-use/lib/useAsync';
import type { AsyncState } from 'react-use/lib/useAsync';
import { ObservabilityAppServices } from '../application/types';
export function useAlertDataView(featureIds: ValidFeatureId[]): AsyncState<DataView> {
const { http, data: dataService } = useKibana<ObservabilityAppServices>().services;
const features = featureIds.sort().join(',');
const dataView = useAsync(async () => {
const { index_name: indexNames } = await http.get<{ index_name: string[] }>(
`${BASE_RAC_ALERTS_API_PATH}/index`,
{
query: { features },
}
);
return dataService.dataViews.create({ title: indexNames.join(',') });
}, [features]);
return dataView;
}

View file

@ -1,32 +0,0 @@
/*
* 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 { useFetcher } from './use_fetcher';
import { callObservabilityApi } from '../services/call_observability_api';
const NO_INDEX_NAMES: string[] = [];
export function useAlertIndexNames() {
const { data: indexNames = NO_INDEX_NAMES } = useFetcher(({ signal }) => {
return callObservabilityApi('GET /api/observability/rules/alerts/dynamic_index_pattern', {
signal,
params: {
query: {
namespace: 'default',
registrationContexts: [
'observability.apm',
'observability.logs',
'observability.metrics',
'observability.uptime',
],
},
},
});
}, []);
return indexNames;
}

View file

@ -48,13 +48,23 @@ const triggersActionsUiStartMock = {
},
};
const data = {
createStart() {
return {
dataViews: {
create: jest.fn(),
},
};
},
};
export const observabilityPublicPluginsStartMock = {
createStart() {
return {
cases: mockCasesContract(),
embeddable: embeddableStartMock.createStart(),
triggersActionsUi: triggersActionsUiStartMock.createStart(),
data: null,
data: data.createStart(),
lens: null,
discover: null,
};

View file

@ -5,24 +5,24 @@
* 2.0.
*/
import { DataViewBase } from '@kbn/es-query';
import React, { useMemo, useState } from 'react';
import { TimeHistory } from '@kbn/data-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { SearchBar } from '@kbn/unified-search-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { ValidFeatureId } from '@kbn/rule-data-utils';
import { translations } from '../../../config';
import { useAlertDataView } from '../../../hooks/use_alert_data_view';
type QueryLanguageType = 'lucene' | 'kuery';
export function AlertsSearchBar({
dynamicIndexPatterns,
featureIds,
onQueryChange,
query,
rangeFrom,
rangeTo,
}: {
dynamicIndexPatterns: DataViewBase[];
featureIds: ValidFeatureId[];
rangeFrom?: string;
rangeTo?: string;
query?: string;
@ -35,20 +35,11 @@ export function AlertsSearchBar({
return new TimeHistory(new Storage(localStorage));
}, []);
const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
const compatibleIndexPatterns = useMemo(
() =>
dynamicIndexPatterns.map((dynamicIndexPattern) => ({
title: dynamicIndexPattern.title ?? '',
id: dynamicIndexPattern.id ?? '',
fields: dynamicIndexPattern.fields,
})),
[dynamicIndexPatterns]
);
const { value: dataView, loading, error } = useAlertDataView(featureIds);
return (
<SearchBar
indexPatterns={compatibleIndexPatterns as DataView[]}
indexPatterns={loading || error ? [] : [dataView!]}
placeholder={translations.alertsSearchBar.placeholder}
query={{ query: query ?? '', language: queryLanguage }}
timeHistory={timeHistory}

View file

@ -8,17 +8,15 @@
import { EuiFlexGroup, EuiFlexItem, EuiFlyoutSize } from '@elastic/eui';
import React, { useCallback, useEffect, useState } from 'react';
import { DataViewBase } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import useAsync from 'react-use/lib/useAsync';
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 { 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';
import { useAlertIndexNames } from '../../../../hooks/use_alert_index_names';
import { useHasData } from '../../../../hooks/use_has_data';
import { usePluginContext } from '../../../../hooks/use_plugin_context';
import { getNoDataConfig } from '../../../../utils/no_data_config';
@ -38,7 +36,6 @@ import {
ALERTS_PER_PAGE,
ALERTS_TABLE_ID,
BASE_ALERT_REGEX,
NO_INDEX_PATTERNS,
} from './constants';
import { RuleStatsState } from './types';
@ -52,7 +49,6 @@ function AlertsPage() {
useAlertsPageStateContainer();
const {
cases,
dataViews,
docLinks,
http,
notifications: { toasts },
@ -84,7 +80,6 @@ function AlertsPage() {
}),
},
]);
const indexNames = useAlertIndexNames();
async function loadRuleStats() {
setRuleStatsLoading(true);
@ -128,23 +123,6 @@ function AlertsPage() {
const manageRulesHref = http.basePath.prepend('/app/observability/alerts/rules');
const dynamicIndexPatternsAsyncState = useAsync(async (): Promise<DataViewBase[]> => {
if (indexNames.length === 0) {
return [];
}
return [
{
id: 'dynamic-observability-alerts-table-index-pattern',
title: indexNames.join(','),
fields: await dataViews.getFieldsForWildcard({
pattern: indexNames.join(','),
allowNoIndex: true,
}),
},
];
}, [indexNames]);
const onRefresh = () => {
setRefreshNow(new Date().getTime());
};
@ -227,7 +205,7 @@ function AlertsPage() {
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem>
<AlertsSearchBar
dynamicIndexPatterns={dynamicIndexPatternsAsyncState.value ?? NO_INDEX_PATTERNS}
featureIds={observabilityAlertFeatureIds}
rangeFrom={rangeFrom}
rangeTo={rangeTo}
query={kuery}
@ -254,12 +232,7 @@ function AlertsPage() {
configurationId={AlertConsumers.OBSERVABILITY}
id={ALERTS_TABLE_ID}
flyoutSize={'s' as EuiFlyoutSize}
featureIds={[
AlertConsumers.APM,
AlertConsumers.INFRASTRUCTURE,
AlertConsumers.LOGS,
AlertConsumers.UPTIME,
]}
featureIds={observabilityAlertFeatureIds}
query={buildEsQuery(
{
to: rangeTo,