[Discover] Enable esQuery alert for adhoc data views (#140885)

## Summary

Closes #142514 #142389

This PR does the following: 
- Enables to create `esQuery` (in KQL or Lucene mode) using adhoc data
views from discover and management pages
- Adds `explore matching indices` button to data view picker in alert
flyout
- Adding adhoc data views from alert flyout should propage them to a
main discover picker


### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))

Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
Dmitry Tomashevich 2022-11-09 18:55:34 +03:00 committed by GitHub
parent 3d7b01e28b
commit a9162f7481
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 812 additions and 401 deletions

View file

@ -1,12 +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 { schema, TypeOf } from '@kbn/config-schema';
export const configSchema = schema.object({});
export type Config = TypeOf<typeof configSchema>;

View file

@ -5,8 +5,4 @@
* 2.0.
*/
// TODO: https://github.com/elastic/kibana/issues/110895
/* eslint-disable @kbn/eslint/no_export_all */
export * from './config';
export { STACK_ALERTS_FEATURE_ID } from './constants';

View file

@ -19,6 +19,8 @@ export interface StackAlertsPublicSetupDeps {
}
export class StackAlertsPublicPlugin implements Plugin<Setup, Start, StackAlertsPublicSetupDeps> {
constructor() {}
public setup(core: CoreSetup, { triggersActionsUi, alerting }: StackAlertsPublicSetupDeps) {
registerRuleTypes({
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry,

View file

@ -10,46 +10,72 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { DataViewSelectPopover, DataViewSelectPopoverProps } from './data_view_select_popover';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import type { DataView } from '@kbn/data-views-plugin/public';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { act } from 'react-dom/test-utils';
const selectedDataView = {
id: 'mock-data-logs-id',
namespaces: ['default'],
title: 'kibana_sample_data_logs',
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'kibana_sample_data_logs',
} as unknown as DataView;
const props: DataViewSelectPopoverProps = {
onSelectDataView: () => {},
dataViewName: 'kibana_sample_data_logs',
dataViewId: 'mock-data-logs-id',
onChangeMetaData: () => {},
dataView: selectedDataView,
};
const dataViewIds = ['mock-data-logs-id', 'mock-ecommerce-id', 'mock-test-id'];
const dataViewOptions = [
{
id: 'mock-data-logs-id',
namespaces: ['default'],
title: 'kibana_sample_data_logs',
},
selectedDataView,
{
id: 'mock-flyghts-id',
namespaces: ['default'],
title: 'kibana_sample_data_flights',
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'kibana_sample_data_flights',
},
{
id: 'mock-ecommerce-id',
namespaces: ['default'],
title: 'kibana_sample_data_ecommerce',
typeMeta: {},
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'kibana_sample_data_ecommerce',
},
{
id: 'mock-test-id',
namespaces: ['default'],
title: 'test',
typeMeta: {},
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'test',
},
];
const mount = () => {
const dataViewsMock = dataViewPluginMocks.createStartContract();
dataViewsMock.getIdsWithTitle.mockImplementation(() => Promise.resolve(dataViewOptions));
dataViewsMock.getIds = jest.fn().mockImplementation(() => Promise.resolve(dataViewIds));
dataViewsMock.get = jest
.fn()
.mockImplementation((id: string) =>
Promise.resolve(dataViewOptions.find((current) => current.id === id))
);
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
return {
wrapper: mountWithIntl(
<KibanaContextProvider services={{ data: { dataViews: dataViewsMock } }}>
<KibanaContextProvider
services={{ dataViews: dataViewsMock, dataViewEditor: dataViewEditorMock }}
>
<DataViewSelectPopover {...props} />
</KibanaContextProvider>
),
@ -66,10 +92,10 @@ describe('DataViewSelectPopover', () => {
wrapper.update();
});
expect(dataViewsMock.getIdsWithTitle).toHaveBeenCalled();
expect(dataViewsMock.getIds).toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="selectDataViewExpression"]').exists()).toBeTruthy();
const getIdsWithTitleResult = await dataViewsMock.getIdsWithTitle.mock.results[0].value;
expect(getIdsWithTitleResult).toBe(dataViewOptions);
const getIdsResult = await dataViewsMock.getIds.mock.results[0].value;
expect(getIdsResult).toBe(dataViewIds);
});
});

View file

@ -14,56 +14,97 @@ import {
EuiExpression,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPopover,
EuiPopoverFooter,
EuiPopoverTitle,
EuiText,
useEuiPaddingCSS,
} from '@elastic/eui';
import { DataViewsList } from '@kbn/unified-search-plugin/public';
import { DataViewListItem } from '@kbn/data-views-plugin/public';
import { useTriggersAndActionsUiDeps } from '../es_query/util';
import type { DataViewListItem, DataView } from '@kbn/data-views-plugin/public';
import { DataViewSelector } from '@kbn/unified-search-plugin/public';
import { useTriggerUiActionServices } from '../es_query/util';
import { EsQueryRuleMetaData } from '../es_query/types';
export interface DataViewSelectPopoverProps {
onSelectDataView: (newDataViewId: string) => void;
dataViewName?: string;
dataViewId?: string;
dataView: DataView;
metadata?: EsQueryRuleMetaData;
onSelectDataView: (selectedDataView: DataView) => void;
onChangeMetaData: (metadata: EsQueryRuleMetaData) => void;
}
const toDataViewListItem = (dataView: DataView): DataViewListItem => {
return {
id: dataView.id!,
title: dataView.title,
name: dataView.name,
};
};
export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopoverProps> = ({
metadata = { adHocDataViewList: [], isManagementPage: true },
dataView,
onSelectDataView,
dataViewName,
dataViewId,
onChangeMetaData,
}) => {
const { data, dataViewEditor } = useTriggersAndActionsUiDeps();
const [dataViewItems, setDataViewsItems] = useState<DataViewListItem[]>();
const { dataViews, dataViewEditor } = useTriggerUiActionServices();
const [dataViewItems, setDataViewsItems] = useState<DataViewListItem[]>([]);
const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false);
const closeDataViewEditor = useRef<() => void | undefined>();
const loadDataViews = useCallback(async () => {
const fetchedDataViewItems = await data.dataViews.getIdsWithTitle();
setDataViewsItems(fetchedDataViewItems);
}, [setDataViewsItems, data.dataViews]);
const allDataViewItems = useMemo(
() => [...dataViewItems, ...metadata.adHocDataViewList.map(toDataViewListItem)],
[dataViewItems, metadata.adHocDataViewList]
);
const closeDataViewPopover = useCallback(() => setDataViewPopoverOpen(false), []);
const onChangeDataView = useCallback(
async (selectedDataViewId: string) => {
const selectedDataView = await dataViews.get(selectedDataViewId);
onSelectDataView(selectedDataView);
closeDataViewPopover();
},
[closeDataViewPopover, dataViews, onSelectDataView]
);
const loadPersistedDataViews = useCallback(async () => {
const ids = await dataViews.getIds();
const dataViewsList = await Promise.all(ids.map((id) => dataViews.get(id)));
setDataViewsItems(dataViewsList.map(toDataViewListItem));
}, [dataViews]);
const onAddAdHocDataView = useCallback(
(adHocDataView: DataView) => {
onChangeMetaData({
...metadata,
adHocDataViewList: [...metadata.adHocDataViewList, adHocDataView],
});
},
[metadata, onChangeMetaData]
);
const createDataView = useMemo(
() =>
dataViewEditor?.userPermissions.editDataView()
dataViewEditor.userPermissions.editDataView()
? () => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (createdDataView) => {
if (createdDataView.id) {
await onSelectDataView(createdDataView.id);
await loadDataViews();
if (!createdDataView.isPersisted()) {
onAddAdHocDataView(createdDataView);
}
await loadPersistedDataViews();
await onChangeDataView(createdDataView.id);
}
},
allowAdHocDataView: true,
});
}
: undefined,
[dataViewEditor, onSelectDataView, loadDataViews]
[dataViewEditor, loadPersistedDataViews, onChangeDataView, onAddAdHocDataView]
);
useEffect(() => {
@ -76,12 +117,25 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
}, []);
useEffect(() => {
loadDataViews();
}, [loadDataViews]);
loadPersistedDataViews();
}, [loadPersistedDataViews]);
const createDataViewButtonPadding = useEuiPaddingCSS('left');
if (!dataViewItems) {
const onCreateDefaultAdHocDataView = useCallback(
async (pattern: string) => {
const newDataView = await dataViews.create({ title: pattern });
if (newDataView.fields.getByName('@timestamp')?.type === 'date') {
newDataView.timeFieldName = '@timestamp';
}
onAddAdHocDataView(newDataView);
onChangeDataView(newDataView.id!);
},
[dataViews, onAddAdHocDataView, onChangeDataView]
);
if (!allDataViewItems) {
return null;
}
@ -96,7 +150,7 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
defaultMessage: 'data view',
})}
value={
dataViewName ??
dataView.getName() ??
i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewPlaceholder', {
defaultMessage: 'Select a data view',
})
@ -105,7 +159,7 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
onClick={() => {
setDataViewPopoverOpen(true);
}}
isInvalid={!dataViewId}
isInvalid={!dataView.id}
/>
}
isOpen={dataViewPopoverOpen}
@ -136,24 +190,14 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
<EuiFormRow
id="indexSelectSearchBox"
fullWidth
css={`
.euiPanel {
padding: 0;
}
`}
>
<DataViewsList
dataViewsList={dataViewItems}
onChangeDataView={(newId) => {
onSelectDataView(newId);
closeDataViewPopover();
}}
currentDataViewId={dataViewId}
/>
</EuiFormRow>
<DataViewSelector
currentDataViewId={dataView.id}
dataViewsList={allDataViewItems}
setPopoverIsOpen={setDataViewPopoverOpen}
onChangeDataView={onChangeDataView}
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
isTextBasedLangSelected={false}
/>
{createDataView ? (
<EuiPopoverFooter paddingSize="none">
<EuiButtonEmpty

View file

@ -159,6 +159,7 @@ describe('EsQueryRuleTypeExpression', () => {
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
onChangeMetaData={() => {}}
/>
);

View file

@ -19,7 +19,7 @@ import { getFields, RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-
import { parseDuration } from '@kbn/alerting-plugin/common';
import { hasExpressionValidationErrors } from '../validation';
import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
import { EsQueryRuleParams, SearchType } from '../types';
import { EsQueryRuleParams, EsQueryRuleMetaData, SearchType } from '../types';
import { IndexSelectPopover } from '../../components/index_select_popover';
import { DEFAULT_VALUES } from '../constants';
import { RuleCommonExpressions } from '../rule_common_expressions';
@ -33,7 +33,7 @@ interface KibanaDeps {
}
export const EsQueryExpression: React.FC<
RuleTypeParamsExpressionProps<EsQueryRuleParams<SearchType.esQuery>>
RuleTypeParamsExpressionProps<EsQueryRuleParams<SearchType.esQuery>, EsQueryRuleMetaData>
> = ({ ruleParams, setRuleParams, setRuleProperty, errors, data }) => {
const {
index,

View file

@ -13,7 +13,7 @@ import { httpServiceMock } from '@kbn/core/public/mocks';
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
import { CommonRuleParams, EsQueryRuleParams, SearchType } from '../types';
import { CommonRuleParams, EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types';
import { EsQueryRuleTypeExpression } from './expression';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { Subject } from 'rxjs';
@ -22,6 +22,7 @@ import { IUiSettingsClient } from '@kbn/core/public';
import { findTestSubject } from '@elastic/eui/lib/test';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { act } from 'react-dom/test-utils';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { ReactWrapper } from 'enzyme';
jest.mock('@kbn/kibana-react-plugin/public', () => {
@ -87,6 +88,8 @@ const searchSourceFieldsMock = {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
title: 'kibana_sample_data_logs',
fields: [],
getName: () => 'kibana_sample_data_logs',
isPersisted: () => true,
},
};
@ -122,11 +125,15 @@ const savedQueryMock = {
};
const dataMock = dataPluginMock.createStartContract();
const dataViewsMock = dataViewPluginMocks.createStartContract();
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
(dataMock.search.searchSource.create as jest.Mock).mockImplementation(() =>
Promise.resolve(searchSourceMock)
);
(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([]));
dataMock.dataViews.getDefaultDataView = jest.fn(() => Promise.resolve(null));
(dataViewsMock.getIds as jest.Mock) = jest.fn().mockImplementation(() => Promise.resolve([]));
dataViewsMock.getDefaultDataView = jest.fn(() => Promise.resolve(null));
dataViewsMock.get = jest.fn();
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
Promise.resolve(savedQueryMock)
);
@ -137,7 +144,8 @@ dataMock.query.savedQueries.findSavedQueries = jest.fn(() =>
const Wrapper: React.FC<{
ruleParams: EsQueryRuleParams<SearchType.searchSource> | EsQueryRuleParams<SearchType.esQuery>;
}> = ({ ruleParams }) => {
metadata?: EsQueryRuleMetaData;
}> = ({ ruleParams, metadata }) => {
const [currentRuleParams, setCurrentRuleParams] = useState<CommonRuleParams>(ruleParams);
const errors = {
index: [],
@ -170,23 +178,29 @@ const Wrapper: React.FC<{
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
metadata={metadata}
onChangeMetaData={jest.fn()}
/>
);
};
const setup = (
ruleParams: EsQueryRuleParams<SearchType.searchSource> | EsQueryRuleParams<SearchType.esQuery>
ruleParams: EsQueryRuleParams<SearchType.searchSource> | EsQueryRuleParams<SearchType.esQuery>,
metadata?: EsQueryRuleMetaData
) => {
return mountWithIntl(
<KibanaContextProvider
services={{
data: dataMock,
dataViews: dataViewsMock,
uiSettings: uiSettingsMock,
docLinks: docLinksMock,
http: httpMock,
unifiedSearch: unifiedSearchMock,
dataViewEditor: dataViewEditorMock,
}}
>
<Wrapper ruleParams={ruleParams} />
<Wrapper ruleParams={ruleParams} metadata={metadata} />
</KibanaContextProvider>
);
};
@ -236,10 +250,10 @@ describe('EsQueryRuleTypeExpression', () => {
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy();
});
test('should render QueryDSL view without the form type chooser if some rule params were passed', async () => {
test('should render QueryDSL view without the form type chooser', async () => {
let wrapper: ReactWrapper;
await act(async () => {
wrapper = setup(defaultEsQueryRuleParams);
wrapper = setup(defaultEsQueryRuleParams, { adHocDataViewList: [], isManagementPage: false });
wrapper = await wrapper.update();
});
expect(findTestSubject(wrapper!, 'queryFormTypeChooserTitle').exists()).toBeFalsy();
@ -247,10 +261,13 @@ describe('EsQueryRuleTypeExpression', () => {
expect(findTestSubject(wrapper!, 'selectIndexExpression').exists()).toBeTruthy();
});
test('should render KQL and Lucene view without the form type chooser if some rule params were passed', async () => {
test('should render KQL and Lucene view without the form type chooser', async () => {
let wrapper: ReactWrapper;
await act(async () => {
wrapper = setup(defaultSearchSourceRuleParams);
wrapper = setup(defaultSearchSourceRuleParams, {
adHocDataViewList: [],
isManagementPage: false,
});
wrapper = await wrapper.update();
});
wrapper = await wrapper!.update();

View file

@ -5,13 +5,13 @@
* 2.0.
*/
import React, { memo, PropsWithChildren, useCallback, useRef } from 'react';
import React, { memo, PropsWithChildren, useCallback } from 'react';
import deepEqual from 'fast-deep-equal';
import 'brace/theme/github';
import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { EsQueryRuleParams, SearchType } from '../types';
import { EsQueryRuleParams, EsQueryRuleMetaData, SearchType } from '../types';
import { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression';
import { EsQueryExpression } from './es_query_expression';
import { QueryFormTypeChooser } from './query_form_type_chooser';
@ -33,11 +33,12 @@ const SearchSourceExpressionMemoized = memo<SearchSourceExpressionProps>(
);
export const EsQueryRuleTypeExpression: React.FunctionComponent<
RuleTypeParamsExpressionProps<EsQueryRuleParams>
RuleTypeParamsExpressionProps<EsQueryRuleParams, EsQueryRuleMetaData>
> = (props) => {
const { ruleParams, errors, setRuleProperty, setRuleParams } = props;
const isSearchSource = isSearchSourceRule(ruleParams);
const isManagementPage = useRef(!Object.keys(ruleParams).length).current;
// metadata provided only when open alert from Discover page
const isManagementPage = props.metadata?.isManagementPage ?? true;
const formTypeSelected = useCallback(
(searchType: SearchType | null) => {

View file

@ -20,6 +20,7 @@ import { IUiSettingsClient } from '@kbn/core/public';
import { findTestSubject } from '@elastic/eui/lib/test';
import { copyToClipboard, EuiLoadingSpinner } from '@elastic/eui';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
import { ReactWrapper } from 'enzyme';
jest.mock('@elastic/eui', () => {
@ -81,6 +82,9 @@ const searchSourceFieldsMock = {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
title: 'kibana_sample_data_logs',
fields: [],
isPersisted: () => true,
getName: () => 'kibana_sample_data_logs',
isTimeBased: () => true,
},
};
@ -182,8 +186,9 @@ const dataMock = dataPluginMock.createStartContract();
(dataMock.search.searchSource.create as jest.Mock).mockImplementation(() =>
Promise.resolve(searchSourceMock)
);
(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([]));
dataMock.dataViews.getDefaultDataView = jest.fn(() => Promise.resolve(null));
(dataViewPluginMock.getIds as jest.Mock) = jest.fn().mockImplementation(() => Promise.resolve([]));
dataViewPluginMock.getDefaultDataView = jest.fn(() => Promise.resolve(null));
dataViewPluginMock.get = jest.fn();
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
Promise.resolve(savedQueryMock)
);
@ -198,9 +203,18 @@ const setup = (alertParams: EsQueryRuleParams<SearchType.searchSource>) => {
timeWindowSize: [],
searchConfiguration: [],
};
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
return mountWithIntl(
<KibanaContextProvider services={{ data: dataMock, uiSettings: uiSettingsMock }}>
<KibanaContextProvider
services={{
dataViews: dataViewPluginMock,
data: dataMock,
uiSettings: uiSettingsMock,
dataViewEditor: dataViewEditorMock,
unifiedSearch: unifiedSearchMock,
}}
>
<SearchSourceExpression
ruleInterval="1m"
ruleThrottle="1m"
@ -215,6 +229,8 @@ const setup = (alertParams: EsQueryRuleParams<SearchType.searchSource>) => {
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
metadata={{ adHocDataViewList: [] }}
onChangeMetaData={jest.fn()}
/>
</KibanaContextProvider>
);

View file

@ -11,13 +11,14 @@ import { EuiSpacer, EuiLoadingSpinner, EuiEmptyPrompt, EuiCallOut } from '@elast
import { ISearchSource } from '@kbn/data-plugin/common';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { SavedQuery } from '@kbn/data-plugin/public';
import { EsQueryRuleParams, SearchType } from '../types';
import { useTriggersAndActionsUiDeps } from '../util';
import { EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types';
import { SearchSourceExpressionForm } from './search_source_expression_form';
import { DEFAULT_VALUES } from '../constants';
import { useTriggerUiActionServices } from '../util';
export type SearchSourceExpressionProps = RuleTypeParamsExpressionProps<
EsQueryRuleParams<SearchType.searchSource>
EsQueryRuleParams<SearchType.searchSource>,
EsQueryRuleMetaData
>;
export const SearchSourceExpression = ({
@ -25,6 +26,8 @@ export const SearchSourceExpression = ({
errors,
setRuleParams,
setRuleProperty,
metadata,
onChangeMetaData,
}: SearchSourceExpressionProps) => {
const {
thresholdComparator,
@ -36,7 +39,7 @@ export const SearchSourceExpression = ({
searchConfiguration,
excludeHitsFromPreviousRun,
} = ruleParams;
const { data } = useTriggersAndActionsUiDeps();
const { data } = useTriggerUiActionServices();
const [searchSource, setSearchSource] = useState<ISearchSource>();
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
@ -112,6 +115,8 @@ export const SearchSourceExpression = ({
errors={errors}
initialSavedQuery={savedQuery}
setParam={setParam}
metadata={metadata}
onChangeMetaData={onChangeMetaData}
/>
);
};

View file

@ -8,21 +8,26 @@
import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import { lastValueFrom } from 'rxjs';
import { Filter } from '@kbn/es-query';
import type { Filter, Query } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { DataView, Query, ISearchSource, getTime } from '@kbn/data-plugin/common';
import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public';
import { SearchBar, SearchBarProps } from '@kbn/unified-search-plugin/public';
import { mapAndFlattenFilters, SavedQuery, TimeHistory } from '@kbn/data-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { CommonRuleParams, EsQueryRuleParams, SearchType } from '../types';
import type { SearchBarProps } from '@kbn/unified-search-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import {
mapAndFlattenFilters,
getTime,
type SavedQuery,
type ISearchSource,
} from '@kbn/data-plugin/public';
import { STACK_ALERTS_FEATURE_ID } from '../../../../common';
import { CommonRuleParams, EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types';
import { DEFAULT_VALUES } from '../constants';
import { DataViewSelectPopover } from '../../components/data_view_select_popover';
import { useTriggersAndActionsUiDeps } from '../util';
import { RuleCommonExpressions } from '../rule_common_expressions';
import { totalHitsToNumber } from '../test_query_row';
import { hasExpressionValidationErrors } from '../validation';
import { useTriggerUiActionServices } from '../util';
const HIDDEN_FILTER_PANEL_OPTIONS: SearchBarProps['hiddenFilterPanelOptions'] = [
'pinFilter',
@ -66,8 +71,10 @@ interface SearchSourceExpressionFormProps {
searchSource: ISearchSource;
ruleParams: EsQueryRuleParams<SearchType.searchSource>;
errors: IErrorObject;
metadata?: EsQueryRuleMetaData;
initialSavedQuery?: SavedQuery;
setParam: (paramField: string, paramValue: unknown) => void;
onChangeMetaData: (metadata: EsQueryRuleMetaData) => void;
}
const isSearchSourceParam = (action: LocalStateAction): action is SearchSourceParamsAction => {
@ -75,12 +82,11 @@ const isSearchSourceParam = (action: LocalStateAction): action is SearchSourcePa
};
export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => {
const { data } = useTriggersAndActionsUiDeps();
const services = useTriggerUiActionServices();
const unifiedSearch = services.unifiedSearch;
const { searchSource, errors, initialSavedQuery, setParam, ruleParams } = props;
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []);
useEffect(() => setSavedQuery(initialSavedQuery), [initialSavedQuery]);
const [ruleConfiguration, dispatch] = useReducer<LocalStateReducer>(
@ -110,11 +116,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
const dataViews = useMemo(() => (dataView ? [dataView] : []), [dataView]);
const onSelectDataView = useCallback(
(newDataViewId) =>
data.dataViews
.get(newDataViewId)
.then((newDataView) => dispatch({ type: 'index', payload: newDataView })),
[data.dataViews]
(newDataView: DataView) => dispatch({ type: 'index', payload: newDataView }),
[]
);
const onUpdateFilters = useCallback((newFilters) => {
@ -228,9 +231,10 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
<EuiSpacer size="s" />
<DataViewSelectPopover
dataViewName={dataView?.getName?.() ?? dataView?.title}
dataViewId={dataView?.id}
dataView={dataView}
metadata={props.metadata}
onSelectDataView={onSelectDataView}
onChangeMetaData={props.onChangeMetaData}
/>
{Boolean(dataView?.id) && (
@ -245,7 +249,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
</h5>
</EuiTitle>
<EuiSpacer size="xs" />
<SearchBar
<unifiedSearch.ui.SearchBar
appName={STACK_ALERTS_FEATURE_ID}
onQuerySubmit={onQueryBarSubmit}
onQueryChange={onChangeQuery}
suggestionsSize="s"
@ -266,7 +271,6 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
showSubmitButton={false}
dateRangeFrom={undefined}
dateRangeTo={undefined}
timeHistory={timeHistory}
hiddenFilterPanelOptions={HIDDEN_FILTER_PANEL_OPTIONS}
/>
</>

View file

@ -8,8 +8,10 @@
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
import { EXPRESSION_ERRORS } from './constants';
export interface Comparator {
@ -32,6 +34,11 @@ export interface CommonRuleParams extends RuleTypeParams {
excludeHitsFromPreviousRun: boolean;
}
export interface EsQueryRuleMetaData {
adHocDataViewList: DataView[];
isManagementPage?: boolean;
}
export type EsQueryRuleParams<T = SearchType> = T extends SearchType.searchSource
? CommonRuleParams & OnlySearchSourceRuleParams
: CommonRuleParams & OnlyEsQueryRuleParams;
@ -55,6 +62,8 @@ export type ExpressionErrors = typeof EXPRESSION_ERRORS;
export type ErrorKey = keyof ExpressionErrors & unknown;
export interface TriggersAndActionsUiDeps {
dataViews: DataViewsPublicPluginStart;
unifiedSearch: UnifiedSearchPublicPluginStart;
data: DataPublicPluginStart;
dataViewEditor: DataViewEditorStart;
}

View file

@ -14,4 +14,4 @@ export const isSearchSourceRule = (
return ruleParams.searchType === 'searchSource';
};
export const useTriggersAndActionsUiDeps = () => useKibana<TriggersAndActionsUiDeps>().services;
export const useTriggerUiActionServices = () => useKibana<TriggersAndActionsUiDeps>().services;

View file

@ -100,6 +100,18 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes
defaultMessage: 'Data view is required.',
})
);
} else if (
typeof ruleParams.searchConfiguration.index === 'object' &&
!Object.hasOwn(ruleParams.searchConfiguration.index, 'timeFieldName')
) {
errors.index.push(
i18n.translate(
'xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewTimeFieldText',
{
defaultMessage: 'Data view should have a time field.',
}
)
);
}
return validationResult;
}

View file

@ -101,6 +101,7 @@ describe('IndexThresholdRuleTypeExpression', () => {
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
onChangeMetaData={() => {}}
/>
);

View file

@ -5,11 +5,15 @@
* 2.0.
*/
import { get } from 'lodash';
import { schema, TypeOf } from '@kbn/config-schema';
import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server';
import { AlertingBuiltinsPlugin } from './plugin';
import { configSchema, Config } from '../common/config';
export { ID as INDEX_THRESHOLD_ID } from './rule_types/index_threshold/rule_type';
export const configSchema = schema.object({});
export type Config = TypeOf<typeof configSchema>;
export const config: PluginConfigDescriptor<Config> = {
exposeToBrowser: {},
schema: configSchema,