mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[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:
parent
3d7b01e28b
commit
a9162f7481
57 changed files with 812 additions and 401 deletions
|
@ -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>;
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -159,6 +159,7 @@ describe('EsQueryRuleTypeExpression', () => {
|
|||
defaultActionGroupId=""
|
||||
actionGroups={[]}
|
||||
charts={chartsStartMock}
|
||||
onChangeMetaData={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -14,4 +14,4 @@ export const isSearchSourceRule = (
|
|||
return ruleParams.searchType === 'searchSource';
|
||||
};
|
||||
|
||||
export const useTriggersAndActionsUiDeps = () => useKibana<TriggersAndActionsUiDeps>().services;
|
||||
export const useTriggerUiActionServices = () => useKibana<TriggersAndActionsUiDeps>().services;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ describe('IndexThresholdRuleTypeExpression', () => {
|
|||
defaultActionGroupId=""
|
||||
actionGroups={[]}
|
||||
charts={chartsStartMock}
|
||||
onChangeMetaData={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue