[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

@ -983,7 +983,11 @@ describe('SearchSource', () => {
}, },
]; ];
const indexPattern123 = { id: '123', isPersisted: () => true } as DataView; const indexPattern123 = {
id: '123',
isPersisted: jest.fn(() => true),
toSpec: jest.fn(),
} as unknown as DataView;
test('should return serialized fields', () => { test('should return serialized fields', () => {
searchSource.setField('index', indexPattern123); searchSource.setField('index', indexPattern123);
@ -991,6 +995,7 @@ describe('SearchSource', () => {
return filter; return filter;
}); });
const serializedFields = searchSource.getSerializedFields(); const serializedFields = searchSource.getSerializedFields();
expect(indexPattern123.toSpec).toHaveBeenCalledTimes(0);
expect(serializedFields).toMatchSnapshot(); expect(serializedFields).toMatchSnapshot();
}); });
@ -1000,11 +1005,19 @@ describe('SearchSource', () => {
const childSearchSource = searchSource.createChild(); const childSearchSource = searchSource.createChild();
childSearchSource.setField('timeout', '100'); childSearchSource.setField('timeout', '100');
const serializedFields = childSearchSource.getSerializedFields(true); const serializedFields = childSearchSource.getSerializedFields(true);
expect(indexPattern123.toSpec).toHaveBeenCalledTimes(0);
expect(serializedFields).toMatchObject({ expect(serializedFields).toMatchObject({
timeout: '100', timeout: '100',
parent: { index: '123', from: 123 }, parent: { index: '123', from: 123 },
}); });
}); });
test('should use spec', () => {
indexPattern123.isPersisted = jest.fn(() => false);
searchSource.setField('index', indexPattern123);
searchSource.getSerializedFields(true, false);
expect(indexPattern123.toSpec).toHaveBeenCalledWith(false);
});
}); });
describe('fetch$', () => { describe('fetch$', () => {

View file

@ -923,7 +923,7 @@ export class SearchSource {
/** /**
* serializes search source fields (which can later be passed to {@link ISearchStartSearchSource}) * serializes search source fields (which can later be passed to {@link ISearchStartSearchSource})
*/ */
public getSerializedFields(recurse = false): SerializedSearchSourceFields { public getSerializedFields(recurse = false, includeFields = true): SerializedSearchSourceFields {
const { const {
filter: originalFilters, filter: originalFilters,
aggs: searchSourceAggs, aggs: searchSourceAggs,
@ -938,7 +938,9 @@ export class SearchSource {
...searchSourceFields, ...searchSourceFields,
}; };
if (index) { if (index) {
serializedSearchSourceFields.index = index.isPersisted() ? index.id : index.toSpec(); serializedSearchSourceFields.index = index.isPersisted()
? index.id
: index.toSpec(includeFields);
} }
if (sort) { if (sort) {
serializedSearchSourceFields.sort = !Array.isArray(sort) ? [sort] : sort; serializedSearchSourceFields.sort = !Array.isArray(sort) ? [sort] : sort;

View file

@ -188,6 +188,8 @@ async function mountComponent(
persistDataView: jest.fn(), persistDataView: jest.fn(),
updateAdHocDataViewId: jest.fn(), updateAdHocDataViewId: jest.fn(),
adHocDataViewList: [], adHocDataViewList: [],
savedDataViewList: [],
updateDataViewList: jest.fn(),
}; };
const component = mountWithIntl( const component = mountWithIntl(

View file

@ -72,6 +72,8 @@ export function DiscoverLayout({
persistDataView, persistDataView,
updateAdHocDataViewId, updateAdHocDataViewId,
adHocDataViewList, adHocDataViewList,
savedDataViewList,
updateDataViewList,
}: DiscoverLayoutProps) { }: DiscoverLayoutProps) {
const { const {
trackUiMetric, trackUiMetric,
@ -233,6 +235,8 @@ export function DiscoverLayout({
persistDataView={persistDataView} persistDataView={persistDataView}
updateAdHocDataViewId={updateAdHocDataViewId} updateAdHocDataViewId={updateAdHocDataViewId}
adHocDataViewList={adHocDataViewList} adHocDataViewList={adHocDataViewList}
savedDataViewList={savedDataViewList}
updateDataViewList={updateDataViewList}
/> />
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle"> <EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
<SavedSearchURLConflictCallout <SavedSearchURLConflictCallout

View file

@ -8,7 +8,7 @@
import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query'; import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public';
import { DataViewListItem, ISearchSource } from '@kbn/data-plugin/public'; import type { DataViewListItem, ISearchSource } from '@kbn/data-plugin/public';
import { RequestAdapter } from '@kbn/inspector-plugin/common'; import { RequestAdapter } from '@kbn/inspector-plugin/common';
import { SavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch } from '@kbn/saved-search-plugin/public';
import { DataTableRecord } from '../../../../types'; import { DataTableRecord } from '../../../../types';
@ -35,6 +35,8 @@ export interface DiscoverLayoutProps {
state: AppState; state: AppState;
stateContainer: GetStateReturn; stateContainer: GetStateReturn;
persistDataView: (dataView: DataView) => Promise<DataView | undefined>; persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
updateDataViewList: (dataViews: DataView[]) => Promise<void>;
updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>; updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>;
adHocDataViewList: DataView[]; adHocDataViewList: DataView[];
savedDataViewList: DataViewListItem[];
} }

View file

@ -47,6 +47,8 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
persistDataView: jest.fn(), persistDataView: jest.fn(),
updateAdHocDataViewId: jest.fn(), updateAdHocDataViewId: jest.fn(),
adHocDataViewList: [], adHocDataViewList: [],
savedDataViewList: [],
updateDataViewList: jest.fn(),
}; };
} }

View file

@ -8,7 +8,7 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query'; import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
import { DataViewType, type DataView } from '@kbn/data-views-plugin/public'; import { DataViewListItem, DataViewType, type DataView } from '@kbn/data-views-plugin/public';
import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public'; import type { DataViewPickerProps } from '@kbn/unified-search-plugin/public';
import { ENABLE_SQL } from '../../../../../common'; import { ENABLE_SQL } from '../../../../../common';
import { useDiscoverServices } from '../../../../hooks/use_discover_services'; import { useDiscoverServices } from '../../../../hooks/use_discover_services';
@ -38,6 +38,8 @@ export type DiscoverTopNavProps = Pick<
persistDataView: (dataView: DataView) => Promise<DataView | undefined>; persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>; updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>;
adHocDataViewList: DataView[]; adHocDataViewList: DataView[];
savedDataViewList: DataViewListItem[];
updateDataViewList: (DataViewEditorStart: DataView[]) => Promise<void>;
}; };
export const DiscoverTopNav = ({ export const DiscoverTopNav = ({
@ -58,6 +60,8 @@ export const DiscoverTopNav = ({
persistDataView, persistDataView,
updateAdHocDataViewId, updateAdHocDataViewId,
adHocDataViewList, adHocDataViewList,
savedDataViewList,
updateDataViewList,
}: DiscoverTopNavProps) => { }: DiscoverTopNavProps) => {
const history = useHistory(); const history = useHistory();
@ -161,6 +165,8 @@ export const DiscoverTopNav = ({
searchSource, searchSource,
onOpenSavedSearch, onOpenSavedSearch,
isPlainRecord, isPlainRecord,
adHocDataViews: adHocDataViewList,
updateDataViewList,
persistDataView, persistDataView,
updateAdHocDataViewId, updateAdHocDataViewId,
}), }),
@ -174,8 +180,10 @@ export const DiscoverTopNav = ({
searchSource, searchSource,
onOpenSavedSearch, onOpenSavedSearch,
isPlainRecord, isPlainRecord,
adHocDataViewList,
persistDataView, persistDataView,
updateAdHocDataViewId, updateAdHocDataViewId,
updateDataViewList,
] ]
); );
@ -213,6 +221,7 @@ export const DiscoverTopNav = ({
onChangeDataView, onChangeDataView,
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'], textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
adHocDataViews: adHocDataViewList, adHocDataViews: adHocDataViewList,
savedDataViewList,
}; };
const onTextBasedSavedAndExit = useCallback( const onTextBasedSavedAndExit = useCallback(

View file

@ -38,6 +38,8 @@ test('getTopNavLinks result', () => {
onOpenSavedSearch: () => {}, onOpenSavedSearch: () => {},
isPlainRecord: false, isPlainRecord: false,
persistDataView: jest.fn(), persistDataView: jest.fn(),
updateDataViewList: jest.fn(),
adHocDataViews: [],
updateAdHocDataViewId: jest.fn(), updateAdHocDataViewId: jest.fn(),
}); });
expect(topNavLinks).toMatchInlineSnapshot(` expect(topNavLinks).toMatchInlineSnapshot(`
@ -102,6 +104,8 @@ test('getTopNavLinks result for sql mode', () => {
onOpenSavedSearch: () => {}, onOpenSavedSearch: () => {},
isPlainRecord: true, isPlainRecord: true,
persistDataView: jest.fn(), persistDataView: jest.fn(),
updateDataViewList: jest.fn(),
adHocDataViews: [],
updateAdHocDataViewId: jest.fn(), updateAdHocDataViewId: jest.fn(),
}); });
expect(topNavLinks).toMatchInlineSnapshot(` expect(topNavLinks).toMatchInlineSnapshot(`

View file

@ -34,6 +34,8 @@ export const getTopNavLinks = ({
onOpenSavedSearch, onOpenSavedSearch,
isPlainRecord, isPlainRecord,
persistDataView, persistDataView,
adHocDataViews,
updateDataViewList,
updateAdHocDataViewId, updateAdHocDataViewId,
}: { }: {
dataView: DataView; dataView: DataView;
@ -45,6 +47,8 @@ export const getTopNavLinks = ({
searchSource: ISearchSource; searchSource: ISearchSource;
onOpenSavedSearch: (id: string) => void; onOpenSavedSearch: (id: string) => void;
isPlainRecord: boolean; isPlainRecord: boolean;
adHocDataViews: DataView[];
updateDataViewList: (dataView: DataView[]) => Promise<void>;
persistDataView: (dataView: DataView) => Promise<DataView | undefined>; persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>; updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>;
}): TopNavMenuData[] => { }): TopNavMenuData[] => {
@ -75,16 +79,15 @@ export const getTopNavLinks = ({
defaultMessage: 'Alerts', defaultMessage: 'Alerts',
}), }),
run: async (anchorElement: HTMLElement) => { run: async (anchorElement: HTMLElement) => {
const updatedDataView = await persistDataView(dataView); openAlertsPopover({
if (updatedDataView) { I18nContext: services.core.i18n.Context,
openAlertsPopover({ anchorElement,
I18nContext: services.core.i18n.Context, searchSource: savedSearch.searchSource,
anchorElement, services,
searchSource: savedSearch.searchSource, adHocDataViews,
services, updateDataViewList,
savedQueryId: state.appStateContainer.getState().savedQuery, savedQueryId: state.appStateContainer.getState().savedQuery,
}); });
}
}, },
testId: 'discoverAlertsButton', testId: 'discoverAlertsButton',
}; };

View file

@ -6,7 +6,7 @@
* Side Public License, v 1. * Side Public License, v 1.
*/ */
import React from 'react'; import React, { ReactNode } from 'react';
import { mountWithIntl } from '@kbn/test-jest-helpers'; import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test'; import { findTestSubject } from '@elastic/eui/lib/test';
import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks'; import { createSearchSourceMock } from '@kbn/data-plugin/public/mocks';
@ -16,6 +16,8 @@ import { discoverServiceMock } from '../../../../__mocks__/services';
import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield'; import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
import { dataViewMock } from '../../../../__mocks__/data_view'; import { dataViewMock } from '../../../../__mocks__/data_view';
const Context = ({ children }: { children: ReactNode }) => <>{children}</>;
const mount = (dataView = dataViewMock) => const mount = (dataView = dataViewMock) =>
mountWithIntl( mountWithIntl(
<KibanaContextProvider services={discoverServiceMock}> <KibanaContextProvider services={discoverServiceMock}>
@ -23,7 +25,11 @@ const mount = (dataView = dataViewMock) =>
searchSource={createSearchSourceMock({ index: dataView })} searchSource={createSearchSourceMock({ index: dataView })}
anchorElement={document.createElement('div')} anchorElement={document.createElement('div')}
savedQueryId={undefined} savedQueryId={undefined}
onClose={() => {}} adHocDataViews={[]}
services={discoverServiceMock}
updateDataViewList={jest.fn()}
onClose={jest.fn()}
I18nContext={Context}
/> />
</KibanaContextProvider> </KibanaContextProvider>
); );

View file

@ -11,11 +11,10 @@ import ReactDOM from 'react-dom';
import { I18nStart } from '@kbn/core/public'; import { I18nStart } from '@kbn/core/public';
import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui'; import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { ISearchSource } from '@kbn/data-plugin/common'; import type { DataView, ISearchSource } from '@kbn/data-plugin/common';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { DiscoverServices } from '../../../../build_services'; import { DiscoverServices } from '../../../../build_services';
import { updateSearchSource } from '../../utils/update_search_source'; import { updateSearchSource } from '../../utils/update_search_source';
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
const container = document.createElement('div'); const container = document.createElement('div');
let isOpen = false; let isOpen = false;
@ -27,16 +26,27 @@ interface AlertsPopoverProps {
anchorElement: HTMLElement; anchorElement: HTMLElement;
searchSource: ISearchSource; searchSource: ISearchSource;
savedQueryId?: string; savedQueryId?: string;
adHocDataViews: DataView[];
I18nContext: I18nStart['Context'];
services: DiscoverServices;
updateDataViewList: (dataViews: DataView[]) => Promise<void>;
}
interface EsQueryAlertMetaData {
isManagementPage?: boolean;
adHocDataViewList: DataView[];
} }
export function AlertsPopover({ export function AlertsPopover({
searchSource, searchSource,
anchorElement, anchorElement,
savedQueryId, savedQueryId,
adHocDataViews,
services,
onClose: originalOnClose, onClose: originalOnClose,
updateDataViewList,
}: AlertsPopoverProps) { }: AlertsPopoverProps) {
const dataView = searchSource.getField('index')!; const dataView = searchSource.getField('index')!;
const services = useDiscoverServices();
const { triggersActionsUi } = services; const { triggersActionsUi } = services;
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false); const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false);
const onClose = useCallback(() => { const onClose = useCallback(() => {
@ -63,20 +73,45 @@ export function AlertsPopover({
}; };
}, [savedQueryId, searchSource, services]); }, [savedQueryId, searchSource, services]);
const discoverMetadata: EsQueryAlertMetaData = useMemo(
() => ({
isManagementPage: false,
adHocDataViewList: adHocDataViews,
}),
[adHocDataViews]
);
const SearchThresholdAlertFlyout = useMemo(() => { const SearchThresholdAlertFlyout = useMemo(() => {
if (!alertFlyoutVisible) { if (!alertFlyoutVisible) {
return; return;
} }
const onFinishFlyoutInteraction = (metadata: EsQueryAlertMetaData) => {
updateDataViewList(metadata.adHocDataViewList);
};
return triggersActionsUi?.getAddAlertFlyout({ return triggersActionsUi?.getAddAlertFlyout({
metadata: discoverMetadata,
consumer: 'discover', consumer: 'discover',
onClose, onClose: (_, metadata) => {
onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData);
onClose();
},
onSave: async (metadata) => {
onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData);
},
canChangeTrigger: false, canChangeTrigger: false,
ruleTypeId: ALERT_TYPE_ID, ruleTypeId: ALERT_TYPE_ID,
initialValues: { initialValues: { params: getParams() },
params: getParams(),
},
}); });
}, [getParams, onClose, triggersActionsUi, alertFlyoutVisible]); }, [
alertFlyoutVisible,
triggersActionsUi,
discoverMetadata,
getParams,
updateDataViewList,
onClose,
]);
const hasTimeFieldName = dataView.timeFieldName; const hasTimeFieldName = dataView.timeFieldName;
const panels = [ const panels = [
@ -145,13 +180,17 @@ export function openAlertsPopover({
anchorElement, anchorElement,
searchSource, searchSource,
services, services,
adHocDataViews,
savedQueryId, savedQueryId,
updateDataViewList,
}: { }: {
I18nContext: I18nStart['Context']; I18nContext: I18nStart['Context'];
anchorElement: HTMLElement; anchorElement: HTMLElement;
searchSource: ISearchSource; searchSource: ISearchSource;
services: DiscoverServices; services: DiscoverServices;
adHocDataViews: DataView[];
savedQueryId?: string; savedQueryId?: string;
updateDataViewList: (dataViews: DataView[]) => Promise<void>;
}) { }) {
if (isOpen) { if (isOpen) {
closeAlertsPopover(); closeAlertsPopover();
@ -169,6 +208,10 @@ export function openAlertsPopover({
anchorElement={anchorElement} anchorElement={anchorElement}
searchSource={searchSource} searchSource={searchSource}
savedQueryId={savedQueryId} savedQueryId={savedQueryId}
adHocDataViews={adHocDataViews}
I18nContext={I18nContext}
services={services}
updateDataViewList={updateDataViewList}
/> />
</KibanaContextProvider> </KibanaContextProvider>
</I18nContext> </I18nContext>

View file

@ -56,12 +56,14 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
onUpdateQuery, onUpdateQuery,
persistDataView, persistDataView,
updateAdHocDataViewId, updateAdHocDataViewId,
updateDataViewList,
refetch$, refetch$,
resetSavedSearch, resetSavedSearch,
searchSource, searchSource,
state, state,
stateContainer, stateContainer,
adHocDataViewList, adHocDataViewList,
savedDataViewList,
} = useDiscoverState({ } = useDiscoverState({
services, services,
history: usedHistory, history: usedHistory,
@ -120,7 +122,9 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
stateContainer={stateContainer} stateContainer={stateContainer}
persistDataView={persistDataView} persistDataView={persistDataView}
updateAdHocDataViewId={updateAdHocDataViewId} updateAdHocDataViewId={updateAdHocDataViewId}
updateDataViewList={updateDataViewList}
adHocDataViewList={adHocDataViewList} adHocDataViewList={adHocDataViewList}
savedDataViewList={savedDataViewList}
/> />
</DiscoverAppStateProvider> </DiscoverAppStateProvider>
); );

View file

@ -14,20 +14,20 @@ import { discoverServiceMock } from '../../__mocks__/services';
import { DiscoverMainRoute } from './discover_main_route'; import { DiscoverMainRoute } from './discover_main_route';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { dataViewMock } from '../../__mocks__/data_view'; import { dataViewMock } from '../../__mocks__/data_view';
import { SavedObject, ScopedHistory } from '@kbn/core/public'; import { SavedObject } from '@kbn/core/public';
import { DataViewSavedObjectAttrs } from '@kbn/data-views-plugin/common'; import { DataViewSavedObjectAttrs } from '@kbn/data-views-plugin/common';
import { DiscoverMainApp } from './discover_main_app'; import { DiscoverMainApp } from './discover_main_app';
import { SearchSource } from '@kbn/data-plugin/common'; import { SearchSource } from '@kbn/data-plugin/common';
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks'; import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
import { findTestSubject } from '@elastic/eui/lib/test'; import { findTestSubject } from '@elastic/eui/lib/test';
import { scopedHistoryMock } from '@kbn/core/public/mocks';
jest.mock('./discover_main_app', () => { jest.mock('./discover_main_app', () => {
return { return {
DiscoverMainApp: jest.fn().mockReturnValue(<></>), DiscoverMainApp: jest.fn().mockReturnValue(<></>),
}; };
}); });
setScopedHistory({ location: {} } as ScopedHistory); setScopedHistory(scopedHistoryMock.create());
describe('DiscoverMainRoute', () => { describe('DiscoverMainRoute', () => {
test('renders the main app when hasESData=true & hasUserDataView=true ', async () => { test('renders the main app when hasESData=true & hasUserDataView=true ', async () => {
const component = mountComponent(true, true); const component = mountComponent(true, true);

View file

@ -69,11 +69,11 @@ export const useAdHocDataViews = ({
prev.filter((d) => d.id && dataViewToUpdate.id && d.id !== dataViewToUpdate.id) prev.filter((d) => d.id && dataViewToUpdate.id && d.id !== dataViewToUpdate.id)
); );
// update filters references
const uiActions = await getUiActions(); const uiActions = await getUiActions();
const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER); const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER);
const action = uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION); const action = uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION);
// execute shouldn't be awaited, this is important for pending history push cancellation
action?.execute({ action?.execute({
trigger, trigger,
fromDataView: dataViewToUpdate.id, fromDataView: dataViewToUpdate.id,
@ -81,11 +81,12 @@ export const useAdHocDataViews = ({
usedDataViews: [], usedDataViews: [],
} as ActionExecutionContext); } as ActionExecutionContext);
savedSearch.searchSource.setField('index', newDataView);
stateContainer.replaceUrlAppState({ index: newDataView.id }); stateContainer.replaceUrlAppState({ index: newDataView.id });
setUrlTracking(newDataView); setUrlTracking(newDataView);
return newDataView; return newDataView;
}, },
[dataViews, setUrlTracking, stateContainer] [dataViews, setUrlTracking, stateContainer, savedSearch.searchSource]
); );
const { openConfirmSavePrompt, updateSavedSearch } = const { openConfirmSavePrompt, updateSavedSearch } =
@ -105,5 +106,19 @@ export const useAdHocDataViews = ({
return currentDataView; return currentDataView;
}, [stateContainer, openConfirmSavePrompt, savedSearch, updateSavedSearch]); }, [stateContainer, openConfirmSavePrompt, savedSearch, updateSavedSearch]);
return { adHocDataViewList, persistDataView, updateAdHocDataViewId }; const onAddAdHocDataViews = useCallback((newDataViews: DataView[]) => {
setAdHocDataViewList((prev) => {
const newAdHocDataViews = newDataViews.filter(
(newDataView) => !prev.find((d) => d.id === newDataView.id)
);
return [...prev, ...newAdHocDataViews];
});
}, []);
return {
adHocDataViewList,
persistDataView,
updateAdHocDataViewId,
onAddAdHocDataViews,
};
}; };

View file

@ -8,7 +8,7 @@
import { useMemo, useEffect, useState, useCallback } from 'react'; import { useMemo, useEffect, useState, useCallback } from 'react';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { History } from 'history'; import { History } from 'history';
import { DataViewListItem, DataViewType } from '@kbn/data-views-plugin/public'; import { type DataViewListItem, type DataView, DataViewType } from '@kbn/data-views-plugin/public';
import { SavedSearch, getSavedSearch } from '@kbn/saved-search-plugin/public'; import { SavedSearch, getSavedSearch } from '@kbn/saved-search-plugin/public';
import type { SortOrder } from '@kbn/saved-search-plugin/public'; import type { SortOrder } from '@kbn/saved-search-plugin/public';
import { useTextBasedQueryLanguage } from './use_text_based_query_language'; import { useTextBasedQueryLanguage } from './use_text_based_query_language';
@ -36,7 +36,7 @@ export function useDiscoverState({
history, history,
savedSearch, savedSearch,
setExpandedDoc, setExpandedDoc,
dataViewList, dataViewList: initialDataViewList,
}: { }: {
services: DiscoverServices; services: DiscoverServices;
savedSearch: SavedSearch; savedSearch: SavedSearch;
@ -124,15 +124,29 @@ export function useDiscoverState({
/** /**
* Adhoc data views functionality * Adhoc data views functionality
*/ */
const { adHocDataViewList, persistDataView, updateAdHocDataViewId } = useAdHocDataViews({ const { adHocDataViewList, persistDataView, updateAdHocDataViewId, onAddAdHocDataViews } =
dataView, useAdHocDataViews({
stateContainer, dataView,
savedSearch, dataViews,
setUrlTracking, stateContainer,
dataViews, savedSearch,
toastNotifications, setUrlTracking,
filterManager, filterManager,
}); toastNotifications,
});
const [savedDataViewList, setSavedDataViewList] = useState(initialDataViewList);
/**
* Updates data views selector state
*/
const updateDataViewList = useCallback(
async (newAdHocDataViews: DataView[]) => {
setSavedDataViewList(await data.dataViews.getIdsWithTitle());
onAddAdHocDataViews(newAdHocDataViews);
},
[data.dataViews, onAddAdHocDataViews]
);
/** /**
* Data fetching logic * Data fetching logic
@ -153,7 +167,7 @@ export function useDiscoverState({
documents$: data$.documents$, documents$: data$.documents$,
dataViews, dataViews,
stateContainer, stateContainer,
dataViewList, dataViewList: savedDataViewList,
savedSearch, savedSearch,
}); });
@ -306,7 +320,9 @@ export function useDiscoverState({
state, state,
stateContainer, stateContainer,
adHocDataViewList, adHocDataViewList,
savedDataViewList,
persistDataView, persistDataView,
updateAdHocDataViewId, updateAdHocDataViewId,
updateDataViewList,
}; };
} }

View file

@ -78,23 +78,11 @@ export function ViewAlertRoute() {
history.push(DISCOVER_MAIN_ROUTE); history.push(DISCOVER_MAIN_ROUTE);
return; return;
} }
const calculatedChecksum = getCurrentChecksum(fetchedAlert.params);
// rule params changed
if (openActualAlert && calculatedChecksum !== queryParams.checksum) {
displayRuleChangedWarn();
}
// documents might be updated or deleted
else if (openActualAlert && calculatedChecksum === queryParams.checksum) {
displayPossibleDocsDiffInfoAlert();
}
const fetchedSearchSource = await fetchSearchSource(fetchedAlert); const fetchedSearchSource = await fetchSearchSource(fetchedAlert);
if (!fetchedSearchSource) { if (!fetchedSearchSource) {
history.push(DISCOVER_MAIN_ROUTE); history.push(DISCOVER_MAIN_ROUTE);
return; return;
} }
const dataView = fetchedSearchSource.getField('index'); const dataView = fetchedSearchSource.getField('index');
const timeFieldName = dataView?.timeFieldName; const timeFieldName = dataView?.timeFieldName;
// data view fetch error // data view fetch error
@ -104,15 +92,30 @@ export function ViewAlertRoute() {
return; return;
} }
const dataViewSavedObject = await core.savedObjects.client.get('index-pattern', dataView.id!); if (dataView.isPersisted()) {
const alertUpdatedAt = fetchedAlert.updatedAt; const dataViewSavedObject = await core.savedObjects.client.get(
const dataViewUpdatedAt = dataViewSavedObject.updatedAt!; 'index-pattern',
// data view updated after the last update of the alert rule dataView.id!
if ( );
openActualAlert &&
new Date(dataViewUpdatedAt).valueOf() > new Date(alertUpdatedAt).valueOf() const alertUpdatedAt = fetchedAlert.updatedAt;
) { const dataViewUpdatedAt = dataViewSavedObject.updatedAt!;
showDataViewUpdatedWarning(); // data view updated after the last update of the alert rule
if (
openActualAlert &&
new Date(dataViewUpdatedAt).valueOf() > new Date(alertUpdatedAt).valueOf()
) {
showDataViewUpdatedWarning();
}
}
const calculatedChecksum = getCurrentChecksum(fetchedAlert.params);
// rule params changed
if (openActualAlert && calculatedChecksum !== queryParams.checksum) {
displayRuleChangedWarn();
} else if (openActualAlert && calculatedChecksum === queryParams.checksum) {
// documents might be updated or deleted
displayPossibleDocsDiffInfoAlert();
} }
const timeRange = openActualAlert const timeRange = openActualAlert
@ -120,7 +123,7 @@ export function ViewAlertRoute() {
: buildTimeRangeFilter(dataView, fetchedAlert, timeFieldName); : buildTimeRangeFilter(dataView, fetchedAlert, timeFieldName);
const state: DiscoverAppLocatorParams = { const state: DiscoverAppLocatorParams = {
query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(), query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(),
dataViewId: dataView.id, dataViewSpec: dataView.toSpec(false),
timeRange, timeRange,
}; };

View file

@ -52,6 +52,9 @@ import { DiscoverStartPlugins } from './plugin';
import { DiscoverContextAppLocator } from './application/context/services/locator'; import { DiscoverContextAppLocator } from './application/context/services/locator';
import { DiscoverSingleDocLocator } from './application/doc/locator'; import { DiscoverSingleDocLocator } from './application/doc/locator';
/**
* Location state of internal Discover history instance
*/
export interface HistoryLocationState { export interface HistoryLocationState {
referrer: string; referrer: string;
} }

View file

@ -7,7 +7,7 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { import {
EuiPopover, EuiPopover,
@ -24,16 +24,16 @@ import {
EuiFlexItem, EuiFlexItem,
EuiButtonEmpty, EuiButtonEmpty,
EuiToolTip, EuiToolTip,
EuiSpacer,
} from '@elastic/eui'; } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { IUnifiedSearchPluginServices } from '../types'; import type { IUnifiedSearchPluginServices } from '../types';
import type { DataViewPickerPropsExtended } from '.'; import type { DataViewPickerPropsExtended } from '.';
import { type DataViewListItemEnhanced, DataViewsList } from './dataview_list'; import type { DataViewListItemEnhanced } from './dataview_list';
import type { TextBasedLanguagesListProps } from './text_languages_list'; import type { TextBasedLanguagesListProps } from './text_languages_list';
import type { TextBasedLanguagesTransitionModalProps } from './text_languages_transition_modal'; import type { TextBasedLanguagesTransitionModalProps } from './text_languages_transition_modal';
import adhoc from './assets/adhoc.svg'; import adhoc from './assets/adhoc.svg';
import { changeDataViewStyles } from './change_dataview.styles'; import { changeDataViewStyles } from './change_dataview.styles';
import { DataViewSelector } from './data_view_selector';
// local storage key for the text based languages transition modal // local storage key for the text based languages transition modal
const TEXT_LANG_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal'; const TEXT_LANG_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal';
@ -62,6 +62,7 @@ export function ChangeDataView({
isMissingCurrent, isMissingCurrent,
currentDataViewId, currentDataViewId,
adHocDataViews, adHocDataViews,
savedDataViews,
onChangeDataView, onChangeDataView,
onAddField, onAddField,
onDataViewCreated, onDataViewCreated,
@ -77,9 +78,6 @@ export function ChangeDataView({
}: DataViewPickerPropsExtended) { }: DataViewPickerPropsExtended) {
const { euiTheme } = useEuiTheme(); const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setPopoverIsOpen] = useState(false); const [isPopoverOpen, setPopoverIsOpen] = useState(false);
const [noDataViewMatches, setNoDataViewMatches] = useState(false);
const [dataViewSearchString, setDataViewSearchString] = useState('');
const [indexMatches, setIndexMatches] = useState(0);
const [dataViewsList, setDataViewsList] = useState<DataViewListItemEnhanced[]>([]); const [dataViewsList, setDataViewsList] = useState<DataViewListItemEnhanced[]>([]);
const [triggerLabel, setTriggerLabel] = useState(''); const [triggerLabel, setTriggerLabel] = useState('');
const [isTextBasedLangSelected, setIsTextBasedLangSelected] = useState( const [isTextBasedLangSelected, setIsTextBasedLangSelected] = useState(
@ -100,7 +98,9 @@ export function ChangeDataView({
useEffect(() => { useEffect(() => {
const fetchDataViews = async () => { const fetchDataViews = async () => {
const dataViewsRefs: DataViewListItemEnhanced[] = await data.dataViews.getIdsWithTitle(); const dataViewsRefs: DataViewListItemEnhanced[] = savedDataViews
? savedDataViews
: await data.dataViews.getIdsWithTitle();
if (adHocDataViews?.length) { if (adHocDataViews?.length) {
adHocDataViews.forEach((adHocDataView) => { adHocDataViews.forEach((adHocDataView) => {
if (adHocDataView.id) { if (adHocDataView.id) {
@ -116,25 +116,7 @@ export function ChangeDataView({
setDataViewsList(dataViewsRefs); setDataViewsList(dataViewsRefs);
}; };
fetchDataViews(); fetchDataViews();
}, [data, currentDataViewId, adHocDataViews]); }, [data, currentDataViewId, adHocDataViews, savedDataViews]);
const pendingIndexMatch = useRef<undefined | NodeJS.Timeout>();
useEffect(() => {
async function checkIndices() {
if (dataViewSearchString !== '' && noDataViewMatches) {
const matches = await kibana.services.dataViews.getIndices({
pattern: dataViewSearchString,
isRollupIndex: () => false,
showAllIndices: false,
});
setIndexMatches(matches.length);
}
}
if (pendingIndexMatch.current) {
clearTimeout(pendingIndexMatch.current);
}
pendingIndexMatch.current = setTimeout(checkIndices, 250);
}, [dataViewSearchString, kibana.services.dataViews, noDataViewMatches]);
useEffect(() => { useEffect(() => {
if (trigger.label) { if (trigger.label) {
@ -313,9 +295,13 @@ export function ChangeDataView({
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>
)} )}
<DataViewSelector
<DataViewsList currentDataViewId={currentDataViewId}
searchListInputId={searchListInputId}
dataViewsList={dataViewsList} dataViewsList={dataViewsList}
selectableProps={selectableProps}
isTextBasedLangSelected={isTextBasedLangSelected}
setPopoverIsOpen={setPopoverIsOpen}
onChangeDataView={async (newId) => { onChangeDataView={async (newId) => {
const dataView = await data.dataViews.get(newId); const dataView = await data.dataViews.get(newId);
await data.dataViews.refreshFields(dataView); await data.dataViews.refreshFields(dataView);
@ -336,58 +322,8 @@ export function ChangeDataView({
onChangeDataView(newId); onChangeDataView(newId);
} }
}} }}
currentDataViewId={currentDataViewId} onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
selectableProps={{
...(selectableProps || {}),
// @ts-expect-error Some EUI weirdness
searchProps: {
...(selectableProps?.searchProps || {}),
onChange: (value, matches) => {
selectableProps?.searchProps?.onChange?.(value, matches);
setNoDataViewMatches(matches.length === 0 && dataViewsList.length > 0);
setDataViewSearchString(value);
},
},
}}
searchListInputId={searchListInputId}
isTextBasedLangSelected={isTextBasedLangSelected}
/> />
{onCreateDefaultAdHocDataView && noDataViewMatches && indexMatches > 0 && (
<EuiFlexGroup
alignItems="center"
gutterSize="none"
justifyContent="spaceBetween"
data-test-subj="select-text-based-language-panel"
css={css`
margin: ${euiTheme.size.s};
margin-bottom: 0;
`}
>
<EuiFlexItem grow={true}>
<EuiButton
fullWidth
size="s"
onClick={() => {
setPopoverIsOpen(false);
onCreateDefaultAdHocDataView(dataViewSearchString);
}}
>
{i18n.translate(
'unifiedSearch.query.queryBar.indexPattern.createForMatchingIndices',
{
defaultMessage: `Explore {indicesLength, plural,
one {# matching index}
other {# matching indices}}`,
values: {
indicesLength: indexMatches,
},
}
)}
</EuiButton>
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
)}
</> </>
); );

View file

@ -0,0 +1,97 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { Fragment, useEffect, useRef, useState } from 'react';
import type { EuiSelectableProps } from '@elastic/eui';
import type { DataViewListItem } from '@kbn/data-views-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { DataViewsList } from './dataview_list';
import { IUnifiedSearchPluginServices } from '../types';
import { ExploreMatchingButton } from './explore_matching_button';
interface DataViewSelectorProps {
currentDataViewId?: string;
searchListInputId?: string;
dataViewsList: DataViewListItem[];
selectableProps?: EuiSelectableProps;
isTextBasedLangSelected: boolean;
setPopoverIsOpen: (isOpen: boolean) => void;
onChangeDataView: (dataViewId: string) => void;
onCreateDefaultAdHocDataView?: (pattern: string) => void;
}
export const DataViewSelector = ({
currentDataViewId,
searchListInputId,
dataViewsList,
selectableProps,
isTextBasedLangSelected,
setPopoverIsOpen,
onChangeDataView,
onCreateDefaultAdHocDataView,
}: DataViewSelectorProps) => {
const kibana = useKibana<IUnifiedSearchPluginServices>();
const { dataViews } = kibana.services;
const [noDataViewMatches, setNoDataViewMatches] = useState(false);
const [dataViewSearchString, setDataViewSearchString] = useState('');
const [indexMatches, setIndexMatches] = useState(0);
const pendingIndexMatch = useRef<undefined | NodeJS.Timeout>();
useEffect(() => {
async function checkIndices() {
if (dataViewSearchString !== '' && noDataViewMatches) {
const matches = await dataViews.getIndices({
pattern: dataViewSearchString,
isRollupIndex: () => false,
showAllIndices: false,
});
setIndexMatches(matches.length);
}
}
pendingIndexMatch.current = setTimeout(checkIndices, 250);
return () => {
if (pendingIndexMatch.current) {
clearTimeout(pendingIndexMatch.current);
}
};
}, [dataViewSearchString, dataViews, noDataViewMatches]);
return (
<Fragment>
<DataViewsList
dataViewsList={dataViewsList}
onChangeDataView={onChangeDataView}
currentDataViewId={currentDataViewId}
selectableProps={{
...(selectableProps || {}),
// @ts-expect-error Some EUI weirdness
searchProps: {
...(selectableProps?.searchProps || {}),
onChange: (value, matches) => {
selectableProps?.searchProps?.onChange?.(value, matches);
setNoDataViewMatches(matches.length === 0 && dataViewsList.length > 0);
setDataViewSearchString(value);
},
},
}}
searchListInputId={searchListInputId}
isTextBasedLangSelected={isTextBasedLangSelected}
/>
<ExploreMatchingButton
noDataViewMatches={noDataViewMatches}
indexMatches={indexMatches}
dataViewSearchString={dataViewSearchString}
setPopoverIsOpen={setPopoverIsOpen}
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
/>
</Fragment>
);
};

View file

@ -0,0 +1,67 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, useEuiTheme } from '@elastic/eui';
import { css } from '@emotion/css';
import { i18n } from '@kbn/i18n';
interface ExploreMatchingButtonProps {
noDataViewMatches: boolean;
indexMatches: number;
dataViewSearchString: string;
onCreateDefaultAdHocDataView?: (pattern: string) => void;
setPopoverIsOpen: (isOpen: boolean) => void;
}
export const ExploreMatchingButton = ({
noDataViewMatches,
indexMatches,
dataViewSearchString,
setPopoverIsOpen,
onCreateDefaultAdHocDataView,
}: ExploreMatchingButtonProps) => {
const { euiTheme } = useEuiTheme();
if (onCreateDefaultAdHocDataView && noDataViewMatches && indexMatches > 0) {
return (
<EuiFlexGroup
alignItems="center"
gutterSize="none"
justifyContent="spaceBetween"
data-test-subj="select-text-based-language-panel"
css={css`
margin: ${euiTheme.size.s};
margin-bottom: 0;
`}
>
<EuiFlexItem grow={true}>
<EuiButton
fullWidth
size="s"
onClick={() => {
setPopoverIsOpen(false);
onCreateDefaultAdHocDataView(dataViewSearchString);
}}
>
{i18n.translate('unifiedSearch.query.queryBar.indexPattern.createForMatchingIndices', {
defaultMessage: `Explore {indicesLength, plural,
one {# matching index}
other {# matching indices}}`,
values: {
indicesLength: indexMatches,
},
})}
</EuiButton>
<EuiSpacer size="s" />
</EuiFlexItem>
</EuiFlexGroup>
);
}
return null;
};

View file

@ -8,7 +8,7 @@
import React from 'react'; import React from 'react';
import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui'; import type { EuiButtonProps, EuiSelectableProps } from '@elastic/eui';
import type { DataView } from '@kbn/data-views-plugin/public'; import type { DataView, DataViewListItem } from '@kbn/data-views-plugin/public';
import type { AggregateQuery, Query } from '@kbn/es-query'; import type { AggregateQuery, Query } from '@kbn/es-query';
import { ChangeDataView } from './change_dataview'; import { ChangeDataView } from './change_dataview';
@ -54,6 +54,10 @@ export interface DataViewPickerProps {
* The adHocDataviews. * The adHocDataviews.
*/ */
adHocDataViews?: DataView[]; adHocDataViews?: DataView[];
/**
* Saved data views
*/
savedDataViews?: DataViewListItem[];
/** /**
* EuiSelectable properties. * EuiSelectable properties.
*/ */
@ -102,6 +106,7 @@ export const DataViewPicker = ({
isMissingCurrent, isMissingCurrent,
currentDataViewId, currentDataViewId,
adHocDataViews, adHocDataViews,
savedDataViews,
onChangeDataView, onChangeDataView,
onEditDataView, onEditDataView,
onAddField, onAddField,
@ -126,6 +131,7 @@ export const DataViewPicker = ({
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView} onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
trigger={trigger} trigger={trigger}
adHocDataViews={adHocDataViews} adHocDataViews={adHocDataViews}
savedDataViews={savedDataViews}
selectableProps={selectableProps} selectableProps={selectableProps}
textBasedLanguages={textBasedLanguages} textBasedLanguages={textBasedLanguages}
onSaveTextLanguageQuery={onSaveTextLanguageQuery} onSaveTextLanguageQuery={onSaveTextLanguageQuery}

View file

@ -21,6 +21,7 @@ export { SearchBar } from './search_bar';
export type { FilterItemsProps } from './filter_bar'; export type { FilterItemsProps } from './filter_bar';
export { FilterLabel, FilterItem, FilterItems } from './filter_bar'; export { FilterLabel, FilterItem, FilterItems } from './filter_bar';
export { DataViewsList } from './dataview_picker/dataview_list'; export { DataViewsList } from './dataview_picker/dataview_list';
export { DataViewSelector } from './dataview_picker/data_view_selector';
export { DataViewPicker } from './dataview_picker'; export { DataViewPicker } from './dataview_picker';
export type { DataViewPickerProps } from './dataview_picker'; export type { DataViewPickerProps } from './dataview_picker';

View file

@ -38,12 +38,20 @@ export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> =
useDefaultBehaviors?: boolean; useDefaultBehaviors?: boolean;
savedQueryId?: string; savedQueryId?: string;
onSavedQueryIdChange?: (savedQueryId?: string) => void; onSavedQueryIdChange?: (savedQueryId?: string) => void;
onFiltersUpdated?: (filters: Filter[]) => void;
}; };
// Respond to user changing the filters // Respond to user changing the filters
const defaultFiltersUpdated = (queryService: QueryStart) => { const defaultFiltersUpdated = (
queryService: QueryStart,
onFiltersUpdated?: (filters: Filter[]) => void
) => {
return (filters: Filter[]) => { return (filters: Filter[]) => {
queryService.filterManager.setFilters(filters); if (onFiltersUpdated) {
onFiltersUpdated(filters);
} else {
queryService.filterManager.setFilters(filters);
}
}; };
}; };
@ -206,7 +214,7 @@ export function createSearchBar({
isRefreshPaused={refreshInterval.pause} isRefreshPaused={refreshInterval.pause}
filters={filters} filters={filters}
query={query} query={query}
onFiltersUpdated={defaultFiltersUpdated(data.query)} onFiltersUpdated={defaultFiltersUpdated(data.query, props.onFiltersUpdated)}
onRefreshChange={defaultOnRefreshChange(data.query)} onRefreshChange={defaultOnRefreshChange(data.query)}
savedQuery={savedQuery} savedQuery={savedQuery}
onQuerySubmit={defaultOnQuerySubmit(props, data.query, query)} onQuerySubmit={defaultOnQuerySubmit(props, data.query, query)}

View file

@ -14,8 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const toasts = getService('toasts'); const toasts = getService('toasts');
const esArchiver = getService('esArchiver'); const esArchiver = getService('esArchiver');
const filterBar = getService('filterBar'); const filterBar = getService('filterBar');
const dashboardAddPanel = getService('dashboardAddPanel');
const fieldEditor = getService('fieldEditor'); const fieldEditor = getService('fieldEditor');
const dashboardAddPanel = getService('dashboardAddPanel');
const kibanaServer = getService('kibanaServer'); const kibanaServer = getService('kibanaServer');
const retry = getService('retry'); const retry = getService('retry');
const queryBar = getService('queryBar'); const queryBar = getService('queryBar');
@ -132,7 +132,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should update data view id when saving data view from hoc one', async () => { it('should update data view id when saving data view from hoc one', async () => {
const prevDataViewId = await PageObjects.discover.getCurrentDataViewId(); const prevDataViewId = await PageObjects.discover.getCurrentDataViewId();
await testSubjects.click('discoverAlertsButton'); await testSubjects.click('shareTopNavButton');
await testSubjects.click('confirmModalConfirmButton'); await testSubjects.click('confirmModalConfirmButton');
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -67,6 +67,11 @@ export class ToastsService extends FtrService {
return await list.findByCssSelector(`.euiToast:nth-child(${index})`); return await list.findByCssSelector(`.euiToast:nth-child(${index})`);
} }
public async getToastContent(index: number) {
const elem = await this.getToastElement(index);
return await elem.getVisibleText();
}
public async getAllToastElements() { public async getAllToastElements() {
const list = await this.getGlobalToastList(); const list = await this.getGlobalToastList();
return await list.findAllByCssSelector(`.euiToast`); return await list.findAllByCssSelector(`.euiToast`);

View file

@ -117,7 +117,6 @@ export const SearchPanel: FC<Props> = ({
displayStyle={'inPage'} displayStyle={'inPage'}
isClearable={true} isClearable={true}
customSubmitButton={<div />} customSubmitButton={<div />}
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })} onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
/> />
</EuiFlexItem> </EuiFlexItem>

View file

@ -10,6 +10,7 @@ import { EuiThemeComputed, useEuiTheme } from '@elastic/eui';
import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public';
import type { DataView } from '@kbn/data-plugin/common'; import type { DataView } from '@kbn/data-plugin/common';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import type { Filter } from '@kbn/es-query';
import { SecuritySolutionContext } from '../../../application/security_solution_context'; import { SecuritySolutionContext } from '../../../application/security_solution_context';
import * as TEST_SUBJECTS from '../test_subjects'; import * as TEST_SUBJECTS from '../test_subjects';
import type { FindingsBaseURLQuery } from '../types'; import type { FindingsBaseURLQuery } from '../types';
@ -49,7 +50,6 @@ export const FindingsSearchBar = ({
isLoading={loading} isLoading={loading}
indexPatterns={[dataView]} indexPatterns={[dataView]}
onQuerySubmit={setQuery} onQuerySubmit={setQuery}
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
onFiltersUpdated={(value: Filter[]) => setQuery({ filters: value })} onFiltersUpdated={(value: Filter[]) => setQuery({ filters: value })}
placeholder={i18n.translate('xpack.csp.findings.searchBar.searchPlaceholder', { placeholder={i18n.translate('xpack.csp.findings.searchBar.searchPlaceholder', {
defaultMessage: 'Search findings (eg. rule.section : "API Server" )', defaultMessage: 'Search findings (eg. rule.section : "API Server" )',

View file

@ -136,7 +136,6 @@ export const SearchPanel: FC<Props> = ({
onQuerySubmit={(params: { dateRange: TimeRange; query?: Query | undefined }) => onQuerySubmit={(params: { dateRange: TimeRange; query?: Query | undefined }) =>
searchHandler({ query: params.query }) searchHandler({ query: params.query })
} }
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })} onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
indexPatterns={[dataView]} indexPatterns={[dataView]}
placeholder={i18n.translate('xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', { placeholder={i18n.translate('xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', {

View file

@ -86,7 +86,7 @@ type Props = Omit<
}, },
AlertContextMeta AlertContextMeta
>, >,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' 'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
>; >;
export const defaultExpression = { export const defaultExpression = {

View file

@ -39,7 +39,7 @@ type AlertParams = RuleTypeParams &
type Props = Omit< type Props = Omit<
RuleTypeParamsExpressionProps<AlertParams, AlertContextMeta>, RuleTypeParamsExpressionProps<AlertParams, AlertContextMeta>,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' 'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
>; >;
export const defaultExpression = { export const defaultExpression = {

View file

@ -42,7 +42,7 @@ const FILTER_TYPING_DEBOUNCE_MS = 500;
type Props = Omit< type Props = Omit<
RuleTypeParamsExpressionProps<RuleTypeParams & AlertParams, AlertContextMeta>, RuleTypeParamsExpressionProps<RuleTypeParams & AlertParams, AlertContextMeta>,
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' 'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
>; >;
const defaultExpression = { const defaultExpression = {

View file

@ -70,7 +70,6 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
onClearSavedQuery={onClearSavedQuery} onClearSavedQuery={onClearSavedQuery}
showSaveQuery showSaveQuery
showQueryInput showQueryInput
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
onFiltersUpdated={onFilterChange} onFiltersUpdated={onFilterChange}
/> />
); );

View file

@ -143,6 +143,7 @@ describe('alert_form', () => {
operation="create" operation="create"
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry} ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={() => {}}
/> />
</KibanaReactContext.Provider> </KibanaReactContext.Provider>
</I18nProvider> </I18nProvider>

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. * 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'; export { STACK_ALERTS_FEATURE_ID } from './constants';

View file

@ -19,6 +19,8 @@ export interface StackAlertsPublicSetupDeps {
} }
export class StackAlertsPublicPlugin implements Plugin<Setup, Start, StackAlertsPublicSetupDeps> { export class StackAlertsPublicPlugin implements Plugin<Setup, Start, StackAlertsPublicSetupDeps> {
constructor() {}
public setup(core: CoreSetup, { triggersActionsUi, alerting }: StackAlertsPublicSetupDeps) { public setup(core: CoreSetup, { triggersActionsUi, alerting }: StackAlertsPublicSetupDeps) {
registerRuleTypes({ registerRuleTypes({
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry, 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 { DataViewSelectPopover, DataViewSelectPopoverProps } from './data_view_select_popover';
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; 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'; 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 = { const props: DataViewSelectPopoverProps = {
onSelectDataView: () => {}, onSelectDataView: () => {},
dataViewName: 'kibana_sample_data_logs', onChangeMetaData: () => {},
dataViewId: 'mock-data-logs-id', dataView: selectedDataView,
}; };
const dataViewIds = ['mock-data-logs-id', 'mock-ecommerce-id', 'mock-test-id'];
const dataViewOptions = [ const dataViewOptions = [
{ selectedDataView,
id: 'mock-data-logs-id',
namespaces: ['default'],
title: 'kibana_sample_data_logs',
},
{ {
id: 'mock-flyghts-id', id: 'mock-flyghts-id',
namespaces: ['default'], namespaces: ['default'],
title: 'kibana_sample_data_flights', title: 'kibana_sample_data_flights',
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'kibana_sample_data_flights',
}, },
{ {
id: 'mock-ecommerce-id', id: 'mock-ecommerce-id',
namespaces: ['default'], namespaces: ['default'],
title: 'kibana_sample_data_ecommerce', title: 'kibana_sample_data_ecommerce',
typeMeta: {}, typeMeta: {},
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'kibana_sample_data_ecommerce',
}, },
{ {
id: 'mock-test-id', id: 'mock-test-id',
namespaces: ['default'], namespaces: ['default'],
title: 'test', title: 'test',
typeMeta: {}, typeMeta: {},
isTimeBased: jest.fn(),
isPersisted: jest.fn(() => true),
getName: () => 'test',
}, },
]; ];
const mount = () => { const mount = () => {
const dataViewsMock = dataViewPluginMocks.createStartContract(); 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 { return {
wrapper: mountWithIntl( wrapper: mountWithIntl(
<KibanaContextProvider services={{ data: { dataViews: dataViewsMock } }}> <KibanaContextProvider
services={{ dataViews: dataViewsMock, dataViewEditor: dataViewEditorMock }}
>
<DataViewSelectPopover {...props} /> <DataViewSelectPopover {...props} />
</KibanaContextProvider> </KibanaContextProvider>
), ),
@ -66,10 +92,10 @@ describe('DataViewSelectPopover', () => {
wrapper.update(); wrapper.update();
}); });
expect(dataViewsMock.getIdsWithTitle).toHaveBeenCalled(); expect(dataViewsMock.getIds).toHaveBeenCalled();
expect(wrapper.find('[data-test-subj="selectDataViewExpression"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="selectDataViewExpression"]').exists()).toBeTruthy();
const getIdsWithTitleResult = await dataViewsMock.getIdsWithTitle.mock.results[0].value; const getIdsResult = await dataViewsMock.getIds.mock.results[0].value;
expect(getIdsWithTitleResult).toBe(dataViewOptions); expect(getIdsResult).toBe(dataViewIds);
}); });
}); });

View file

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

View file

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

View file

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

View file

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

View file

@ -5,13 +5,13 @@
* 2.0. * 2.0.
*/ */
import React, { memo, PropsWithChildren, useCallback, useRef } from 'react'; import React, { memo, PropsWithChildren, useCallback } from 'react';
import deepEqual from 'fast-deep-equal'; import deepEqual from 'fast-deep-equal';
import 'brace/theme/github'; import 'brace/theme/github';
import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public'; 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 { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression';
import { EsQueryExpression } from './es_query_expression'; import { EsQueryExpression } from './es_query_expression';
import { QueryFormTypeChooser } from './query_form_type_chooser'; import { QueryFormTypeChooser } from './query_form_type_chooser';
@ -33,11 +33,12 @@ const SearchSourceExpressionMemoized = memo<SearchSourceExpressionProps>(
); );
export const EsQueryRuleTypeExpression: React.FunctionComponent< export const EsQueryRuleTypeExpression: React.FunctionComponent<
RuleTypeParamsExpressionProps<EsQueryRuleParams> RuleTypeParamsExpressionProps<EsQueryRuleParams, EsQueryRuleMetaData>
> = (props) => { > = (props) => {
const { ruleParams, errors, setRuleProperty, setRuleParams } = props; const { ruleParams, errors, setRuleProperty, setRuleParams } = props;
const isSearchSource = isSearchSourceRule(ruleParams); 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( const formTypeSelected = useCallback(
(searchType: SearchType | null) => { (searchType: SearchType | null) => {

View file

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

View file

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

View file

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

View file

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

View file

@ -14,4 +14,4 @@ export const isSearchSourceRule = (
return ruleParams.searchType === 'searchSource'; 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.', 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; return validationResult;
} }

View file

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

View file

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

View file

@ -207,7 +207,10 @@ describe('rule_add', () => {
expect(wrapper.find('[data-test-subj="saveRuleButton"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="saveRuleButton"]').exists()).toBeTruthy();
wrapper.find('[data-test-subj="cancelSaveRuleButton"]').last().simulate('click'); wrapper.find('[data-test-subj="cancelSaveRuleButton"]').last().simulate('click');
expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.CANCELED); expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.CANCELED, {
fields: ['test'],
test: 'some value',
});
}); });
it('renders a confirm close modal if the flyout is closed after inputs have changed', async () => { it('renders a confirm close modal if the flyout is closed after inputs have changed', async () => {
@ -301,7 +304,10 @@ describe('rule_add', () => {
wrapper.update(); wrapper.update();
}); });
expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED); expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED, {
test: 'some value',
fields: ['test'],
});
}); });
it('should enforce any default interval', async () => { it('should enforce any default interval', async () => {

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import React, { useReducer, useMemo, useState, useEffect } from 'react'; import React, { useReducer, useMemo, useState, useEffect, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui'; import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
@ -47,11 +47,13 @@ const RuleAdd = ({
initialValues, initialValues,
reloadRules, reloadRules,
onSave, onSave,
metadata, metadata: initialMetadata,
filteredRuleTypes, filteredRuleTypes,
...props ...props
}: RuleAddProps) => { }: RuleAddProps) => {
const onSaveHandler = onSave ?? reloadRules; const onSaveHandler = onSave ?? reloadRules;
const [metadata, setMetadata] = useState(initialMetadata);
const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []);
const initialRule: InitialRule = useMemo(() => { const initialRule: InitialRule = useMemo(() => {
return { return {
@ -177,7 +179,7 @@ const RuleAdd = ({
) { ) {
setIsConfirmRuleCloseModalOpen(true); setIsConfirmRuleCloseModalOpen(true);
} else { } else {
onClose(RuleFlyoutCloseReason.CANCELED); onClose(RuleFlyoutCloseReason.CANCELED, metadata);
} }
}; };
@ -185,9 +187,9 @@ const RuleAdd = ({
const savedRule = await onSaveRule(); const savedRule = await onSaveRule();
setIsSaving(false); setIsSaving(false);
if (savedRule) { if (savedRule) {
onClose(RuleFlyoutCloseReason.SAVED); onClose(RuleFlyoutCloseReason.SAVED, metadata);
if (onSaveHandler) { if (onSaveHandler) {
onSaveHandler(); onSaveHandler(metadata);
} }
} }
}; };
@ -263,6 +265,7 @@ const RuleAdd = ({
ruleTypeRegistry={ruleTypeRegistry} ruleTypeRegistry={ruleTypeRegistry}
metadata={metadata} metadata={metadata}
filteredRuleTypes={filteredRuleTypes} filteredRuleTypes={filteredRuleTypes}
onChangeMetaData={onChangeMetaData}
/> />
</EuiFlyoutBody> </EuiFlyoutBody>
<RuleAddFooter <RuleAddFooter
@ -308,7 +311,7 @@ const RuleAdd = ({
<ConfirmRuleClose <ConfirmRuleClose
onConfirm={() => { onConfirm={() => {
setIsConfirmRuleCloseModalOpen(false); setIsConfirmRuleCloseModalOpen(false);
onClose(RuleFlyoutCloseReason.CANCELED); onClose(RuleFlyoutCloseReason.CANCELED, metadata);
}} }}
onCancel={() => { onCancel={() => {
setIsConfirmRuleCloseModalOpen(false); setIsConfirmRuleCloseModalOpen(false);

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import React, { useReducer, useState, useEffect } from 'react'; import React, { useReducer, useState, useEffect, useCallback } from 'react';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import { import {
EuiTitle, EuiTitle,
@ -52,7 +52,7 @@ export const RuleEdit = ({
onSave, onSave,
ruleTypeRegistry, ruleTypeRegistry,
actionTypeRegistry, actionTypeRegistry,
metadata, metadata: initialMetadata,
...props ...props
}: RuleEditProps) => { }: RuleEditProps) => {
const onSaveHandler = onSave ?? reloadRules; const onSaveHandler = onSave ?? reloadRules;
@ -71,6 +71,9 @@ export const RuleEdit = ({
); );
const [config, setConfig] = useState<TriggersActionsUiConfig>({ isUsingSecurity: false }); const [config, setConfig] = useState<TriggersActionsUiConfig>({ isUsingSecurity: false });
const [metadata, setMetadata] = useState(initialMetadata);
const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []);
const { const {
http, http,
notifications: { toasts }, notifications: { toasts },
@ -119,7 +122,7 @@ export const RuleEdit = ({
if (hasRuleChanged(rule, initialRule, true)) { if (hasRuleChanged(rule, initialRule, true)) {
setIsConfirmRuleCloseModalOpen(true); setIsConfirmRuleCloseModalOpen(true);
} else { } else {
onClose(RuleFlyoutCloseReason.CANCELED); onClose(RuleFlyoutCloseReason.CANCELED, metadata);
} }
}; };
@ -140,9 +143,9 @@ export const RuleEdit = ({
}, },
}) })
); );
onClose(RuleFlyoutCloseReason.SAVED); onClose(RuleFlyoutCloseReason.SAVED, metadata);
if (onSaveHandler) { if (onSaveHandler) {
onSaveHandler(); onSaveHandler(metadata);
} }
} else { } else {
setRule( setRule(
@ -214,6 +217,7 @@ export const RuleEdit = ({
} }
)} )}
metadata={metadata} metadata={metadata}
onChangeMetaData={onChangeMetaData}
/> />
</EuiFlyoutBody> </EuiFlyoutBody>
<EuiFlyoutFooter> <EuiFlyoutFooter>
@ -280,7 +284,7 @@ export const RuleEdit = ({
<ConfirmRuleClose <ConfirmRuleClose
onConfirm={() => { onConfirm={() => {
setIsConfirmRuleCloseModalOpen(false); setIsConfirmRuleCloseModalOpen(false);
onClose(RuleFlyoutCloseReason.CANCELED); onClose(RuleFlyoutCloseReason.CANCELED, metadata);
}} }}
onCancel={() => { onCancel={() => {
setIsConfirmRuleCloseModalOpen(false); setIsConfirmRuleCloseModalOpen(false);

View file

@ -211,6 +211,7 @@ describe('rule_form', () => {
operation="create" operation="create"
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry} ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={jest.fn()}
/> />
); );
@ -334,6 +335,7 @@ describe('rule_form', () => {
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry} ruleTypeRegistry={ruleTypeRegistry}
connectorFeatureId={featureId} connectorFeatureId={featureId}
onChangeMetaData={jest.fn()}
/> />
); );
@ -578,6 +580,7 @@ describe('rule_form', () => {
operation="create" operation="create"
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry} ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={jest.fn()}
/> />
); );
@ -644,6 +647,7 @@ describe('rule_form', () => {
operation="create" operation="create"
actionTypeRegistry={actionTypeRegistry} actionTypeRegistry={actionTypeRegistry}
ruleTypeRegistry={ruleTypeRegistry} ruleTypeRegistry={ruleTypeRegistry}
onChangeMetaData={jest.fn()}
/> />
); );

View file

@ -99,6 +99,7 @@ interface RuleFormProps<MetaData = Record<string, any>> {
metadata?: MetaData; metadata?: MetaData;
filteredRuleTypes?: string[]; filteredRuleTypes?: string[];
connectorFeatureId?: string; connectorFeatureId?: string;
onChangeMetaData: (metadata: MetaData) => void;
} }
export const RuleForm = ({ export const RuleForm = ({
@ -115,6 +116,7 @@ export const RuleForm = ({
metadata, metadata,
filteredRuleTypes: ruleTypeToFilter, filteredRuleTypes: ruleTypeToFilter,
connectorFeatureId = AlertingConnectorFeatureId, connectorFeatureId = AlertingConnectorFeatureId,
onChangeMetaData,
}: RuleFormProps) => { }: RuleFormProps) => {
const { const {
notifications: { toasts }, notifications: { toasts },
@ -522,6 +524,7 @@ export const RuleForm = ({
data={data} data={data}
dataViews={dataViews} dataViews={dataViews}
unifiedSearch={unifiedSearch} unifiedSearch={unifiedSearch}
onChangeMetaData={onChangeMetaData}
/> />
</Suspense> </Suspense>
</EuiErrorBoundary> </EuiErrorBoundary>

View file

@ -322,6 +322,7 @@ export interface RuleTypeParamsExpressionProps<
key: Prop, key: Prop,
value: SanitizedRule<Params>[Prop] | null value: SanitizedRule<Params>[Prop] | null
) => void; ) => void;
onChangeMetaData: (metadata: MetaData) => void;
errors: IErrorObject; errors: IErrorObject;
defaultActionGroupId: string; defaultActionGroupId: string;
actionGroups: Array<ActionGroup<ActionGroupIds>>; actionGroups: Array<ActionGroup<ActionGroupIds>>;
@ -359,10 +360,10 @@ export interface RuleEditProps<MetaData = Record<string, any>> {
initialRule: Rule; initialRule: Rule;
ruleTypeRegistry: RuleTypeRegistryContract; ruleTypeRegistry: RuleTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract;
onClose: (reason: RuleFlyoutCloseReason) => void; onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void;
/** @deprecated use `onSave` as a callback after an alert is saved*/ /** @deprecated use `onSave` as a callback after an alert is saved*/
reloadRules?: () => Promise<void>; reloadRules?: () => Promise<void>;
onSave?: () => Promise<void>; onSave?: (metadata?: MetaData) => Promise<void>;
metadata?: MetaData; metadata?: MetaData;
ruleType?: RuleType<string, string>; ruleType?: RuleType<string, string>;
} }
@ -371,13 +372,13 @@ export interface RuleAddProps<MetaData = Record<string, any>> {
consumer: string; consumer: string;
ruleTypeRegistry: RuleTypeRegistryContract; ruleTypeRegistry: RuleTypeRegistryContract;
actionTypeRegistry: ActionTypeRegistryContract; actionTypeRegistry: ActionTypeRegistryContract;
onClose: (reason: RuleFlyoutCloseReason) => void; onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void;
ruleTypeId?: string; ruleTypeId?: string;
canChangeTrigger?: boolean; canChangeTrigger?: boolean;
initialValues?: Partial<Rule>; initialValues?: Partial<Rule>;
/** @deprecated use `onSave` as a callback after an alert is saved*/ /** @deprecated use `onSave` as a callback after an alert is saved*/
reloadRules?: () => Promise<void>; reloadRules?: () => Promise<void>;
onSave?: () => Promise<void>; onSave?: (metadata?: MetaData) => Promise<void>;
metadata?: MetaData; metadata?: MetaData;
ruleTypeIndex?: RuleTypeIndex; ruleTypeIndex?: RuleTypeIndex;
filteredRuleTypes?: string[]; filteredRuleTypes?: string[];

View file

@ -7,7 +7,6 @@
import expect from '@kbn/expect'; import expect from '@kbn/expect';
import { asyncForEach } from '@kbn/std'; import { asyncForEach } from '@kbn/std';
import { last } from 'lodash';
import { FtrProviderContext } from '../../ftr_provider_context'; import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) { export default function ({ getService, getPageObjects }: FtrProviderContext) {
@ -32,12 +31,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const security = getService('security'); const security = getService('security');
const filterBar = getService('filterBar'); const filterBar = getService('filterBar');
const find = getService('find'); const find = getService('find');
const toasts = getService('toasts');
const SOURCE_DATA_INDEX = 'search-source-alert'; const SOURCE_DATA_INDEX = 'search-source-alert';
const OUTPUT_DATA_INDEX = 'search-source-alert-output'; const OUTPUT_DATA_INDEX = 'search-source-alert-output';
const ACTION_TYPE_ID = '.index'; const ACTION_TYPE_ID = '.index';
const RULE_NAME = 'test-search-source-alert'; const RULE_NAME = 'test-search-source-alert';
let sourceDataViewId: string; let sourceDataViewId: string;
let sourceAdHocDataViewId: string;
let outputDataViewId: string; let outputDataViewId: string;
let connectorId: string; let connectorId: string;
@ -115,6 +116,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
.expect(200); .expect(200);
}; };
const deleteDataView = async (dataViewId: string) => {
return await supertest
.delete(`/api/data_views/data_view/${dataViewId}`)
.set('kbn-xsrf', 'foo')
.expect(200);
};
const deleteIndexes = (indexes: string[]) => {
indexes.forEach((current) => {
es.transport.request({
path: `/${current}`,
method: 'DELETE',
});
});
};
const createConnector = async (): Promise<string> => { const createConnector = async (): Promise<string> => {
const { body: createdAction } = await supertest const { body: createdAction } = await supertest
.post(`/api/actions/connector`) .post(`/api/actions/connector`)
@ -133,16 +150,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const deleteConnector = (id: string) => const deleteConnector = (id: string) =>
supertest.delete(`/api/actions/connector/${id}`).set('kbn-xsrf', 'foo').expect(204, ''); supertest.delete(`/api/actions/connector/${id}`).set('kbn-xsrf', 'foo').expect(204, '');
const deleteDataViews = (dataViews: string[]) =>
asyncForEach(
dataViews,
async (dataView: string) =>
await supertest
.delete(`/api/data_views/data_view/${dataView}`)
.set('kbn-xsrf', 'foo')
.expect(200)
);
const defineSearchSourceAlert = async (alertName: string) => { const defineSearchSourceAlert = async (alertName: string) => {
await testSubjects.click('discoverAlertsButton'); await testSubjects.click('discoverAlertsButton');
await testSubjects.click('discoverCreateAlertButton'); await testSubjects.click('discoverCreateAlertButton');
@ -168,41 +175,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('saveRuleButton'); await testSubjects.click('saveRuleButton');
}; };
const getLastToast = async () => {
const toastList = await testSubjects.find('globalToastList');
const titles = await toastList.findAllByTestSubject('euiToastHeader');
const lastTitleElement = last(titles)!;
const title = await lastTitleElement.getVisibleText();
const messages = await toastList.findAllByTestSubject('euiToastBody');
const lastMessageElement = last(messages)!;
const message = await lastMessageElement.getVisibleText();
return { message, title };
};
const getErrorToastTitle = async () => {
const toastList = await testSubjects.find('globalToastList');
const title = await (
await toastList.findByCssSelector(
'[class*="euiToast-danger"] > [data-test-subj="euiToastHeader"]'
)
).getVisibleText();
return title;
};
const openOutputIndex = async () => {
await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX);
const [{ id: alertId }] = await getAlertsByName(RULE_NAME);
await queryBar.setQuery(`alert_id:${alertId}`);
await retry.waitFor('document explorer contains alert', async () => {
await queryBar.submitQuery();
await PageObjects.discover.waitUntilSearchingHasFinished();
return (await dataGrid.getDocCount()) > 0;
});
};
const getResultsLink = async () => { const getResultsLink = async () => {
// getting the link // getting the link
await dataGrid.clickRowToggle(); await dataGrid.clickRowToggle();
@ -214,24 +186,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
return link; return link;
}; };
const navigateToDiscover = async (link: string) => { const openAlertResults = async (ruleName: string, dataViewId?: string) => {
// following ling provided by alert to see documents triggered the alert await PageObjects.common.navigateToApp('discover');
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.discover.clickNewSearchButton(); // reset params
await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX);
const [{ id: alertId }] = await getAlertsByName(ruleName);
await filterBar.addFilter('alert_id', 'is', alertId);
await PageObjects.discover.waitUntilSearchingHasFinished();
const link = await getResultsLink();
await filterBar.removeFilter('alert_id'); // clear filter bar
// follow url provided by alert to see documents triggered the alert
const baseUrl = deployment.getHostPort(); const baseUrl = deployment.getHostPort();
await browser.navigateTo(baseUrl + link); await browser.navigateTo(baseUrl + link);
await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.waitUntilSearchingHasFinished();
await retry.waitFor('navigate to discover', async () => { await retry.waitFor('navigate to discover', async () => {
const currentUrl = await browser.getCurrentUrl(); const currentDataViewId = await PageObjects.discover.getCurrentDataViewId();
return currentUrl.includes(sourceDataViewId); return dataViewId ? currentDataViewId === dataViewId : true;
}); });
}; };
const navigateToResults = async () => { const openAlertRuleInManagement = async (ruleName: string) => {
const link = await getResultsLink();
await navigateToDiscover(link);
};
const openAlertRuleInManagement = async () => {
await PageObjects.common.navigateToApp('management'); await PageObjects.common.navigateToApp('management');
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
@ -239,7 +219,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
const rulesList = await testSubjects.find('rulesList'); const rulesList = await testSubjects.find('rulesList');
const alertRule = await rulesList.findByCssSelector('[title="test-search-source-alert"]'); const alertRule = await rulesList.findByCssSelector(`[title="${ruleName}"]`);
await alertRule.click(); await alertRule.click();
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
}; };
@ -269,18 +249,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}); });
after(async () => { after(async () => {
es.transport.request({ deleteIndexes([OUTPUT_DATA_INDEX, SOURCE_DATA_INDEX]);
path: `/${OUTPUT_DATA_INDEX}`, await deleteDataView(outputDataViewId);
method: 'DELETE',
});
await deleteDataViews([sourceDataViewId, outputDataViewId]);
await deleteConnector(connectorId); await deleteConnector(connectorId);
const alertsToDelete = await getAlertsByName(RULE_NAME); const alertsToDelete = await getAlertsByName('test');
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id)); await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
await security.testUser.restoreDefaults(); await security.testUser.restoreDefaults();
}); });
it('should navigate to discover via view in app link', async () => { it('should navigate to alert results via view in app link', async () => {
await PageObjects.common.navigateToApp('discover'); await PageObjects.common.navigateToApp('discover');
await PageObjects.discover.waitUntilSearchingHasFinished(); await PageObjects.discover.waitUntilSearchingHasFinished();
await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX); await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX);
@ -290,33 +267,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await defineSearchSourceAlert(RULE_NAME); await defineSearchSourceAlert(RULE_NAME);
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
await openAlertRuleInManagement(); await openAlertRuleInManagement(RULE_NAME);
await testSubjects.click('ruleDetails-viewInApp'); await testSubjects.click('ruleDetails-viewInApp');
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('navigate to discover', async () => { await retry.waitFor('navigate to discover', async () => {
const currentUrl = await browser.getCurrentUrl(); const currentDataViewId = await PageObjects.discover.getCurrentDataViewId();
return currentUrl.includes(sourceDataViewId); return sourceDataViewId ? currentDataViewId === sourceDataViewId : true;
}); });
expect(await dataGrid.getDocCount()).to.be(5); expect(await dataGrid.getDocCount()).to.be(5);
}); });
it('should open documents triggered the alert', async () => { it('should navigate to alert results via link provided in notification', async () => {
await openOutputIndex(); await openAlertResults(RULE_NAME, sourceDataViewId);
await navigateToResults();
const { message, title } = await getLastToast(); expect(await toasts.getToastCount()).to.be.equal(1);
expect(await dataGrid.getDocCount()).to.be(5); const content = await toasts.getToastContent(1);
expect(title).to.be.equal('Displayed documents may vary'); expect(content).to.equal(
expect(message).to.be.equal( `Displayed documents may vary\nThe displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.`
'The displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.'
); );
const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
expect(selectedDataView).to.be.equal('search-source-alert');
expect(await dataGrid.getDocCount()).to.be(5);
}); });
it('should display warning about updated alert rule', async () => { it('should display warning about updated alert rule', async () => {
await openAlertRuleInManagement(); await openAlertRuleInManagement(RULE_NAME);
// change rule configuration // change rule configuration
await testSubjects.click('openEditRuleFlyoutButton'); await testSubjects.click('openEditRuleFlyoutButton');
@ -328,21 +307,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('saveEditedRuleButton'); await testSubjects.click('saveEditedRuleButton');
await PageObjects.header.waitUntilLoadingHasFinished(); await PageObjects.header.waitUntilLoadingHasFinished();
await openOutputIndex(); await openAlertResults(RULE_NAME, sourceDataViewId);
await navigateToResults();
const { message, title } = await getLastToast();
const queryString = await queryBar.getQueryString(); const queryString = await queryBar.getQueryString();
const hasFilter = await filterBar.hasFilter('message.keyword', 'msg-1'); const hasFilter = await filterBar.hasFilter('message.keyword', 'msg-1');
expect(queryString).to.be.equal('message:msg-1'); expect(queryString).to.be.equal('message:msg-1');
expect(hasFilter).to.be.equal(true); expect(hasFilter).to.be.equal(true);
expect(await dataGrid.getDocCount()).to.be(1); expect(await toasts.getToastCount()).to.be.equal(1);
expect(title).to.be.equal('Alert rule has changed'); const content = await toasts.getToastContent(1);
expect(message).to.be.equal( expect(content).to.equal(
'The displayed documents might not match the documents that triggered the alert because the rule configuration changed.' `Alert rule has changed\nThe displayed documents might not match the documents that triggered the alert because the rule configuration changed.`
); );
expect(await dataGrid.getDocCount()).to.be(1);
}); });
it('should display warning about recently updated data view', async () => { it('should display warning about recently updated data view', async () => {
@ -356,45 +334,82 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await testSubjects.click('tab-sourceFilters'); await testSubjects.click('tab-sourceFilters');
await testSubjects.click('fieldFilterInput'); await testSubjects.click('fieldFilterInput');
await PageObjects.common.sleep(15000);
const input = await find.activeElement(); const input = await find.activeElement();
await input.type('message'); await input.type('message');
await testSubjects.click('addFieldFilterButton'); await testSubjects.click('addFieldFilterButton');
await openOutputIndex(); await openAlertResults(RULE_NAME, sourceDataViewId);
await navigateToResults();
await openOutputIndex(); expect(await toasts.getToastCount()).to.be(2);
await navigateToResults(); const firstContent = await toasts.getToastContent(1);
expect(firstContent).to.equal(
const { message, title } = await getLastToast(); `Data View has changed\nData view has been updated after the last update of the alert rule.`
);
const secondContent = await toasts.getToastContent(2);
expect(secondContent).to.equal(
`Alert rule has changed\nThe displayed documents might not match the documents that triggered the alert because the rule configuration changed.`
);
expect(await dataGrid.getDocCount()).to.be(1); expect(await dataGrid.getDocCount()).to.be(1);
expect(title).to.be.equal('Data View has changed');
expect(message).to.be.equal(
'Data view has been updated after the last update of the alert rule.'
);
}); });
it('should display not found index error', async () => { it('should display not found index error', async () => {
await openOutputIndex(); await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX);
const link = await getResultsLink();
await navigateToDiscover(link);
await es.transport.request({ await deleteDataView(sourceDataViewId);
path: `/${SOURCE_DATA_INDEX}`,
method: 'DELETE',
});
await browser.refresh();
await navigateToDiscover(link); // rty to open alert results after index deletion
await openAlertResults(RULE_NAME);
const title = await getErrorToastTitle(); expect(await toasts.getToastCount()).to.be(1);
expect(title).to.be.equal( const firstContent = await toasts.getToastContent(1);
'No matching indices found: No indices match "search-source-alert"' expect(firstContent).to.equal(
`Error fetching search source\nCould not locate that data view (id: ${sourceDataViewId}), click here to re-create it`
); );
}); });
it('should navigate to alert results via view in app link using adhoc data view', async () => {
await PageObjects.discover.createAdHocDataView('search-source-', true);
await PageObjects.header.waitUntilLoadingHasFinished();
await PageObjects.timePicker.setCommonlyUsedTime('Last_15 minutes');
await PageObjects.discover.addRuntimeField('runtime-message-field', `emit('mock-message')`);
// create an alert
await defineSearchSourceAlert('test-adhoc-alert');
await PageObjects.header.waitUntilLoadingHasFinished();
sourceAdHocDataViewId = await PageObjects.discover.getCurrentDataViewId();
// navigate to discover using view in app link
await openAlertRuleInManagement('test-adhoc-alert');
await testSubjects.click('ruleDetails-viewInApp');
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.waitFor('navigate to discover', async () => {
const currentDataViewId = await PageObjects.discover.getCurrentDataViewId();
return currentDataViewId === sourceAdHocDataViewId;
});
const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
expect(selectedDataView).to.be.equal('search-source-*');
const documentCell = await dataGrid.getCellElement(0, 3);
const firstRowContent = await documentCell.getVisibleText();
expect(firstRowContent.includes('runtime-message-fieldmock-message_id')).to.be.equal(true);
});
it('should navigate to alert results via link provided in notification using adhoc data view', async () => {
await openAlertResults('test-adhoc-alert', sourceAdHocDataViewId);
expect(await toasts.getToastCount()).to.be.equal(1);
const content = await toasts.getToastContent(1);
expect(content).to.equal(
`Displayed documents may vary\nThe displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.`
);
expect(await dataGrid.getDocCount()).to.be(5);
const selectedDataView = await PageObjects.discover.getCurrentlySelectedDataView();
expect(selectedDataView).to.be.equal('search-source-*');
});
}); });
} }