mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 02:09:32 -04:00
[Discover] Enable esQuery
alert for adhoc data views (#140885)
## Summary Closes #142514 #142389 This PR does the following: - Enables to create `esQuery` (in KQL or Lucene mode) using adhoc data views from discover and management pages - Adds `explore matching indices` button to data view picker in alert flyout - Adding adhoc data views from alert flyout should propage them to a main discover picker ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [x] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [x] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
parent
3d7b01e28b
commit
a9162f7481
57 changed files with 812 additions and 401 deletions
|
@ -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', () => {
|
||||
searchSource.setField('index', indexPattern123);
|
||||
|
@ -991,6 +995,7 @@ describe('SearchSource', () => {
|
|||
return filter;
|
||||
});
|
||||
const serializedFields = searchSource.getSerializedFields();
|
||||
expect(indexPattern123.toSpec).toHaveBeenCalledTimes(0);
|
||||
expect(serializedFields).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
@ -1000,11 +1005,19 @@ describe('SearchSource', () => {
|
|||
const childSearchSource = searchSource.createChild();
|
||||
childSearchSource.setField('timeout', '100');
|
||||
const serializedFields = childSearchSource.getSerializedFields(true);
|
||||
expect(indexPattern123.toSpec).toHaveBeenCalledTimes(0);
|
||||
expect(serializedFields).toMatchObject({
|
||||
timeout: '100',
|
||||
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$', () => {
|
||||
|
|
|
@ -923,7 +923,7 @@ export class SearchSource {
|
|||
/**
|
||||
* 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 {
|
||||
filter: originalFilters,
|
||||
aggs: searchSourceAggs,
|
||||
|
@ -938,7 +938,9 @@ export class SearchSource {
|
|||
...searchSourceFields,
|
||||
};
|
||||
if (index) {
|
||||
serializedSearchSourceFields.index = index.isPersisted() ? index.id : index.toSpec();
|
||||
serializedSearchSourceFields.index = index.isPersisted()
|
||||
? index.id
|
||||
: index.toSpec(includeFields);
|
||||
}
|
||||
if (sort) {
|
||||
serializedSearchSourceFields.sort = !Array.isArray(sort) ? [sort] : sort;
|
||||
|
|
|
@ -188,6 +188,8 @@ async function mountComponent(
|
|||
persistDataView: jest.fn(),
|
||||
updateAdHocDataViewId: jest.fn(),
|
||||
adHocDataViewList: [],
|
||||
savedDataViewList: [],
|
||||
updateDataViewList: jest.fn(),
|
||||
};
|
||||
|
||||
const component = mountWithIntl(
|
||||
|
|
|
@ -72,6 +72,8 @@ export function DiscoverLayout({
|
|||
persistDataView,
|
||||
updateAdHocDataViewId,
|
||||
adHocDataViewList,
|
||||
savedDataViewList,
|
||||
updateDataViewList,
|
||||
}: DiscoverLayoutProps) {
|
||||
const {
|
||||
trackUiMetric,
|
||||
|
@ -233,6 +235,8 @@ export function DiscoverLayout({
|
|||
persistDataView={persistDataView}
|
||||
updateAdHocDataViewId={updateAdHocDataViewId}
|
||||
adHocDataViewList={adHocDataViewList}
|
||||
savedDataViewList={savedDataViewList}
|
||||
updateDataViewList={updateDataViewList}
|
||||
/>
|
||||
<EuiPageBody className="dscPageBody" aria-describedby="savedSearchTitle">
|
||||
<SavedSearchURLConflictCallout
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import type { Query, TimeRange, AggregateQuery } from '@kbn/es-query';
|
||||
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 { SavedSearch } from '@kbn/saved-search-plugin/public';
|
||||
import { DataTableRecord } from '../../../../types';
|
||||
|
@ -35,6 +35,8 @@ export interface DiscoverLayoutProps {
|
|||
state: AppState;
|
||||
stateContainer: GetStateReturn;
|
||||
persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
|
||||
updateDataViewList: (dataViews: DataView[]) => Promise<void>;
|
||||
updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>;
|
||||
adHocDataViewList: DataView[];
|
||||
savedDataViewList: DataViewListItem[];
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@ function getProps(savePermissions = true): DiscoverTopNavProps {
|
|||
persistDataView: jest.fn(),
|
||||
updateAdHocDataViewId: jest.fn(),
|
||||
adHocDataViewList: [],
|
||||
savedDataViewList: [],
|
||||
updateDataViewList: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
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 { ENABLE_SQL } from '../../../../../common';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
|
@ -38,6 +38,8 @@ export type DiscoverTopNavProps = Pick<
|
|||
persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
|
||||
updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>;
|
||||
adHocDataViewList: DataView[];
|
||||
savedDataViewList: DataViewListItem[];
|
||||
updateDataViewList: (DataViewEditorStart: DataView[]) => Promise<void>;
|
||||
};
|
||||
|
||||
export const DiscoverTopNav = ({
|
||||
|
@ -58,6 +60,8 @@ export const DiscoverTopNav = ({
|
|||
persistDataView,
|
||||
updateAdHocDataViewId,
|
||||
adHocDataViewList,
|
||||
savedDataViewList,
|
||||
updateDataViewList,
|
||||
}: DiscoverTopNavProps) => {
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -161,6 +165,8 @@ export const DiscoverTopNav = ({
|
|||
searchSource,
|
||||
onOpenSavedSearch,
|
||||
isPlainRecord,
|
||||
adHocDataViews: adHocDataViewList,
|
||||
updateDataViewList,
|
||||
persistDataView,
|
||||
updateAdHocDataViewId,
|
||||
}),
|
||||
|
@ -174,8 +180,10 @@ export const DiscoverTopNav = ({
|
|||
searchSource,
|
||||
onOpenSavedSearch,
|
||||
isPlainRecord,
|
||||
adHocDataViewList,
|
||||
persistDataView,
|
||||
updateAdHocDataViewId,
|
||||
updateDataViewList,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -213,6 +221,7 @@ export const DiscoverTopNav = ({
|
|||
onChangeDataView,
|
||||
textBasedLanguages: supportedTextBasedLanguages as DataViewPickerProps['textBasedLanguages'],
|
||||
adHocDataViews: adHocDataViewList,
|
||||
savedDataViewList,
|
||||
};
|
||||
|
||||
const onTextBasedSavedAndExit = useCallback(
|
||||
|
|
|
@ -38,6 +38,8 @@ test('getTopNavLinks result', () => {
|
|||
onOpenSavedSearch: () => {},
|
||||
isPlainRecord: false,
|
||||
persistDataView: jest.fn(),
|
||||
updateDataViewList: jest.fn(),
|
||||
adHocDataViews: [],
|
||||
updateAdHocDataViewId: jest.fn(),
|
||||
});
|
||||
expect(topNavLinks).toMatchInlineSnapshot(`
|
||||
|
@ -102,6 +104,8 @@ test('getTopNavLinks result for sql mode', () => {
|
|||
onOpenSavedSearch: () => {},
|
||||
isPlainRecord: true,
|
||||
persistDataView: jest.fn(),
|
||||
updateDataViewList: jest.fn(),
|
||||
adHocDataViews: [],
|
||||
updateAdHocDataViewId: jest.fn(),
|
||||
});
|
||||
expect(topNavLinks).toMatchInlineSnapshot(`
|
||||
|
|
|
@ -34,6 +34,8 @@ export const getTopNavLinks = ({
|
|||
onOpenSavedSearch,
|
||||
isPlainRecord,
|
||||
persistDataView,
|
||||
adHocDataViews,
|
||||
updateDataViewList,
|
||||
updateAdHocDataViewId,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
|
@ -45,6 +47,8 @@ export const getTopNavLinks = ({
|
|||
searchSource: ISearchSource;
|
||||
onOpenSavedSearch: (id: string) => void;
|
||||
isPlainRecord: boolean;
|
||||
adHocDataViews: DataView[];
|
||||
updateDataViewList: (dataView: DataView[]) => Promise<void>;
|
||||
persistDataView: (dataView: DataView) => Promise<DataView | undefined>;
|
||||
updateAdHocDataViewId: (dataView: DataView) => Promise<DataView>;
|
||||
}): TopNavMenuData[] => {
|
||||
|
@ -75,16 +79,15 @@ export const getTopNavLinks = ({
|
|||
defaultMessage: 'Alerts',
|
||||
}),
|
||||
run: async (anchorElement: HTMLElement) => {
|
||||
const updatedDataView = await persistDataView(dataView);
|
||||
if (updatedDataView) {
|
||||
openAlertsPopover({
|
||||
I18nContext: services.core.i18n.Context,
|
||||
anchorElement,
|
||||
searchSource: savedSearch.searchSource,
|
||||
services,
|
||||
adHocDataViews,
|
||||
updateDataViewList,
|
||||
savedQueryId: state.appStateContainer.getState().savedQuery,
|
||||
});
|
||||
}
|
||||
},
|
||||
testId: 'discoverAlertsButton',
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
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 { dataViewMock } from '../../../../__mocks__/data_view';
|
||||
|
||||
const Context = ({ children }: { children: ReactNode }) => <>{children}</>;
|
||||
|
||||
const mount = (dataView = dataViewMock) =>
|
||||
mountWithIntl(
|
||||
<KibanaContextProvider services={discoverServiceMock}>
|
||||
|
@ -23,7 +25,11 @@ const mount = (dataView = dataViewMock) =>
|
|||
searchSource={createSearchSourceMock({ index: dataView })}
|
||||
anchorElement={document.createElement('div')}
|
||||
savedQueryId={undefined}
|
||||
onClose={() => {}}
|
||||
adHocDataViews={[]}
|
||||
services={discoverServiceMock}
|
||||
updateDataViewList={jest.fn()}
|
||||
onClose={jest.fn()}
|
||||
I18nContext={Context}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
|
|
@ -11,11 +11,10 @@ import ReactDOM from 'react-dom';
|
|||
import { I18nStart } from '@kbn/core/public';
|
||||
import { EuiWrappingPopover, EuiContextMenu } from '@elastic/eui';
|
||||
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 { DiscoverServices } from '../../../../build_services';
|
||||
import { updateSearchSource } from '../../utils/update_search_source';
|
||||
import { useDiscoverServices } from '../../../../hooks/use_discover_services';
|
||||
|
||||
const container = document.createElement('div');
|
||||
let isOpen = false;
|
||||
|
@ -27,16 +26,27 @@ interface AlertsPopoverProps {
|
|||
anchorElement: HTMLElement;
|
||||
searchSource: ISearchSource;
|
||||
savedQueryId?: string;
|
||||
adHocDataViews: DataView[];
|
||||
I18nContext: I18nStart['Context'];
|
||||
services: DiscoverServices;
|
||||
updateDataViewList: (dataViews: DataView[]) => Promise<void>;
|
||||
}
|
||||
|
||||
interface EsQueryAlertMetaData {
|
||||
isManagementPage?: boolean;
|
||||
adHocDataViewList: DataView[];
|
||||
}
|
||||
|
||||
export function AlertsPopover({
|
||||
searchSource,
|
||||
anchorElement,
|
||||
savedQueryId,
|
||||
adHocDataViews,
|
||||
services,
|
||||
onClose: originalOnClose,
|
||||
updateDataViewList,
|
||||
}: AlertsPopoverProps) {
|
||||
const dataView = searchSource.getField('index')!;
|
||||
const services = useDiscoverServices();
|
||||
const { triggersActionsUi } = services;
|
||||
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false);
|
||||
const onClose = useCallback(() => {
|
||||
|
@ -63,20 +73,45 @@ export function AlertsPopover({
|
|||
};
|
||||
}, [savedQueryId, searchSource, services]);
|
||||
|
||||
const discoverMetadata: EsQueryAlertMetaData = useMemo(
|
||||
() => ({
|
||||
isManagementPage: false,
|
||||
adHocDataViewList: adHocDataViews,
|
||||
}),
|
||||
[adHocDataViews]
|
||||
);
|
||||
|
||||
const SearchThresholdAlertFlyout = useMemo(() => {
|
||||
if (!alertFlyoutVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onFinishFlyoutInteraction = (metadata: EsQueryAlertMetaData) => {
|
||||
updateDataViewList(metadata.adHocDataViewList);
|
||||
};
|
||||
|
||||
return triggersActionsUi?.getAddAlertFlyout({
|
||||
metadata: discoverMetadata,
|
||||
consumer: 'discover',
|
||||
onClose,
|
||||
onClose: (_, metadata) => {
|
||||
onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData);
|
||||
onClose();
|
||||
},
|
||||
onSave: async (metadata) => {
|
||||
onFinishFlyoutInteraction(metadata as EsQueryAlertMetaData);
|
||||
},
|
||||
canChangeTrigger: false,
|
||||
ruleTypeId: ALERT_TYPE_ID,
|
||||
initialValues: {
|
||||
params: getParams(),
|
||||
},
|
||||
initialValues: { params: getParams() },
|
||||
});
|
||||
}, [getParams, onClose, triggersActionsUi, alertFlyoutVisible]);
|
||||
}, [
|
||||
alertFlyoutVisible,
|
||||
triggersActionsUi,
|
||||
discoverMetadata,
|
||||
getParams,
|
||||
updateDataViewList,
|
||||
onClose,
|
||||
]);
|
||||
|
||||
const hasTimeFieldName = dataView.timeFieldName;
|
||||
const panels = [
|
||||
|
@ -145,13 +180,17 @@ export function openAlertsPopover({
|
|||
anchorElement,
|
||||
searchSource,
|
||||
services,
|
||||
adHocDataViews,
|
||||
savedQueryId,
|
||||
updateDataViewList,
|
||||
}: {
|
||||
I18nContext: I18nStart['Context'];
|
||||
anchorElement: HTMLElement;
|
||||
searchSource: ISearchSource;
|
||||
services: DiscoverServices;
|
||||
adHocDataViews: DataView[];
|
||||
savedQueryId?: string;
|
||||
updateDataViewList: (dataViews: DataView[]) => Promise<void>;
|
||||
}) {
|
||||
if (isOpen) {
|
||||
closeAlertsPopover();
|
||||
|
@ -169,6 +208,10 @@ export function openAlertsPopover({
|
|||
anchorElement={anchorElement}
|
||||
searchSource={searchSource}
|
||||
savedQueryId={savedQueryId}
|
||||
adHocDataViews={adHocDataViews}
|
||||
I18nContext={I18nContext}
|
||||
services={services}
|
||||
updateDataViewList={updateDataViewList}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</I18nContext>
|
||||
|
|
|
@ -56,12 +56,14 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
|||
onUpdateQuery,
|
||||
persistDataView,
|
||||
updateAdHocDataViewId,
|
||||
updateDataViewList,
|
||||
refetch$,
|
||||
resetSavedSearch,
|
||||
searchSource,
|
||||
state,
|
||||
stateContainer,
|
||||
adHocDataViewList,
|
||||
savedDataViewList,
|
||||
} = useDiscoverState({
|
||||
services,
|
||||
history: usedHistory,
|
||||
|
@ -120,7 +122,9 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
|||
stateContainer={stateContainer}
|
||||
persistDataView={persistDataView}
|
||||
updateAdHocDataViewId={updateAdHocDataViewId}
|
||||
updateDataViewList={updateDataViewList}
|
||||
adHocDataViewList={adHocDataViewList}
|
||||
savedDataViewList={savedDataViewList}
|
||||
/>
|
||||
</DiscoverAppStateProvider>
|
||||
);
|
||||
|
|
|
@ -14,20 +14,20 @@ import { discoverServiceMock } from '../../__mocks__/services';
|
|||
import { DiscoverMainRoute } from './discover_main_route';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
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 { DiscoverMainApp } from './discover_main_app';
|
||||
import { SearchSource } from '@kbn/data-plugin/common';
|
||||
import { searchSourceInstanceMock } from '@kbn/data-plugin/common/search/search_source/mocks';
|
||||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { scopedHistoryMock } from '@kbn/core/public/mocks';
|
||||
jest.mock('./discover_main_app', () => {
|
||||
return {
|
||||
DiscoverMainApp: jest.fn().mockReturnValue(<></>),
|
||||
};
|
||||
});
|
||||
|
||||
setScopedHistory({ location: {} } as ScopedHistory);
|
||||
|
||||
setScopedHistory(scopedHistoryMock.create());
|
||||
describe('DiscoverMainRoute', () => {
|
||||
test('renders the main app when hasESData=true & hasUserDataView=true ', async () => {
|
||||
const component = mountComponent(true, true);
|
||||
|
|
|
@ -69,11 +69,11 @@ export const useAdHocDataViews = ({
|
|||
prev.filter((d) => d.id && dataViewToUpdate.id && d.id !== dataViewToUpdate.id)
|
||||
);
|
||||
|
||||
// update filters references
|
||||
const uiActions = await getUiActions();
|
||||
const trigger = uiActions.getTrigger(UPDATE_FILTER_REFERENCES_TRIGGER);
|
||||
const action = uiActions.getAction(UPDATE_FILTER_REFERENCES_ACTION);
|
||||
|
||||
// execute shouldn't be awaited, this is important for pending history push cancellation
|
||||
action?.execute({
|
||||
trigger,
|
||||
fromDataView: dataViewToUpdate.id,
|
||||
|
@ -81,11 +81,12 @@ export const useAdHocDataViews = ({
|
|||
usedDataViews: [],
|
||||
} as ActionExecutionContext);
|
||||
|
||||
savedSearch.searchSource.setField('index', newDataView);
|
||||
stateContainer.replaceUrlAppState({ index: newDataView.id });
|
||||
setUrlTracking(newDataView);
|
||||
return newDataView;
|
||||
},
|
||||
[dataViews, setUrlTracking, stateContainer]
|
||||
[dataViews, setUrlTracking, stateContainer, savedSearch.searchSource]
|
||||
);
|
||||
|
||||
const { openConfirmSavePrompt, updateSavedSearch } =
|
||||
|
@ -105,5 +106,19 @@ export const useAdHocDataViews = ({
|
|||
return currentDataView;
|
||||
}, [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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { useMemo, useEffect, useState, useCallback } from 'react';
|
||||
import { isEqual } from 'lodash';
|
||||
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 type { SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import { useTextBasedQueryLanguage } from './use_text_based_query_language';
|
||||
|
@ -36,7 +36,7 @@ export function useDiscoverState({
|
|||
history,
|
||||
savedSearch,
|
||||
setExpandedDoc,
|
||||
dataViewList,
|
||||
dataViewList: initialDataViewList,
|
||||
}: {
|
||||
services: DiscoverServices;
|
||||
savedSearch: SavedSearch;
|
||||
|
@ -124,16 +124,30 @@ export function useDiscoverState({
|
|||
/**
|
||||
* Adhoc data views functionality
|
||||
*/
|
||||
const { adHocDataViewList, persistDataView, updateAdHocDataViewId } = useAdHocDataViews({
|
||||
const { adHocDataViewList, persistDataView, updateAdHocDataViewId, onAddAdHocDataViews } =
|
||||
useAdHocDataViews({
|
||||
dataView,
|
||||
dataViews,
|
||||
stateContainer,
|
||||
savedSearch,
|
||||
setUrlTracking,
|
||||
dataViews,
|
||||
toastNotifications,
|
||||
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
|
||||
*/
|
||||
|
@ -153,7 +167,7 @@ export function useDiscoverState({
|
|||
documents$: data$.documents$,
|
||||
dataViews,
|
||||
stateContainer,
|
||||
dataViewList,
|
||||
dataViewList: savedDataViewList,
|
||||
savedSearch,
|
||||
});
|
||||
|
||||
|
@ -306,7 +320,9 @@ export function useDiscoverState({
|
|||
state,
|
||||
stateContainer,
|
||||
adHocDataViewList,
|
||||
savedDataViewList,
|
||||
persistDataView,
|
||||
updateAdHocDataViewId,
|
||||
updateDataViewList,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -78,23 +78,11 @@ export function ViewAlertRoute() {
|
|||
history.push(DISCOVER_MAIN_ROUTE);
|
||||
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);
|
||||
if (!fetchedSearchSource) {
|
||||
history.push(DISCOVER_MAIN_ROUTE);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataView = fetchedSearchSource.getField('index');
|
||||
const timeFieldName = dataView?.timeFieldName;
|
||||
// data view fetch error
|
||||
|
@ -104,7 +92,12 @@ export function ViewAlertRoute() {
|
|||
return;
|
||||
}
|
||||
|
||||
const dataViewSavedObject = await core.savedObjects.client.get('index-pattern', dataView.id!);
|
||||
if (dataView.isPersisted()) {
|
||||
const dataViewSavedObject = await core.savedObjects.client.get(
|
||||
'index-pattern',
|
||||
dataView.id!
|
||||
);
|
||||
|
||||
const alertUpdatedAt = fetchedAlert.updatedAt;
|
||||
const dataViewUpdatedAt = dataViewSavedObject.updatedAt!;
|
||||
// data view updated after the last update of the alert rule
|
||||
|
@ -114,13 +107,23 @@ export function ViewAlertRoute() {
|
|||
) {
|
||||
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
|
||||
? { from: queryParams.from, to: queryParams.to }
|
||||
: buildTimeRangeFilter(dataView, fetchedAlert, timeFieldName);
|
||||
const state: DiscoverAppLocatorParams = {
|
||||
query: fetchedSearchSource.getField('query') || data.query.queryString.getDefaultQuery(),
|
||||
dataViewId: dataView.id,
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
timeRange,
|
||||
};
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@ import { DiscoverStartPlugins } from './plugin';
|
|||
import { DiscoverContextAppLocator } from './application/context/services/locator';
|
||||
import { DiscoverSingleDocLocator } from './application/doc/locator';
|
||||
|
||||
/**
|
||||
* Location state of internal Discover history instance
|
||||
*/
|
||||
export interface HistoryLocationState {
|
||||
referrer: string;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
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 {
|
||||
EuiPopover,
|
||||
|
@ -24,16 +24,16 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiToolTip,
|
||||
EuiSpacer,
|
||||
} from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { IUnifiedSearchPluginServices } from '../types';
|
||||
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 { TextBasedLanguagesTransitionModalProps } from './text_languages_transition_modal';
|
||||
import adhoc from './assets/adhoc.svg';
|
||||
import { changeDataViewStyles } from './change_dataview.styles';
|
||||
import { DataViewSelector } from './data_view_selector';
|
||||
|
||||
// local storage key for the text based languages transition modal
|
||||
const TEXT_LANG_TRANSITION_MODAL_KEY = 'data.textLangTransitionModal';
|
||||
|
@ -62,6 +62,7 @@ export function ChangeDataView({
|
|||
isMissingCurrent,
|
||||
currentDataViewId,
|
||||
adHocDataViews,
|
||||
savedDataViews,
|
||||
onChangeDataView,
|
||||
onAddField,
|
||||
onDataViewCreated,
|
||||
|
@ -77,9 +78,6 @@ export function ChangeDataView({
|
|||
}: DataViewPickerPropsExtended) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
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 [triggerLabel, setTriggerLabel] = useState('');
|
||||
const [isTextBasedLangSelected, setIsTextBasedLangSelected] = useState(
|
||||
|
@ -100,7 +98,9 @@ export function ChangeDataView({
|
|||
|
||||
useEffect(() => {
|
||||
const fetchDataViews = async () => {
|
||||
const dataViewsRefs: DataViewListItemEnhanced[] = await data.dataViews.getIdsWithTitle();
|
||||
const dataViewsRefs: DataViewListItemEnhanced[] = savedDataViews
|
||||
? savedDataViews
|
||||
: await data.dataViews.getIdsWithTitle();
|
||||
if (adHocDataViews?.length) {
|
||||
adHocDataViews.forEach((adHocDataView) => {
|
||||
if (adHocDataView.id) {
|
||||
|
@ -116,25 +116,7 @@ export function ChangeDataView({
|
|||
setDataViewsList(dataViewsRefs);
|
||||
};
|
||||
fetchDataViews();
|
||||
}, [data, currentDataViewId, adHocDataViews]);
|
||||
|
||||
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]);
|
||||
}, [data, currentDataViewId, adHocDataViews, savedDataViews]);
|
||||
|
||||
useEffect(() => {
|
||||
if (trigger.label) {
|
||||
|
@ -313,9 +295,13 @@ export function ChangeDataView({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
|
||||
<DataViewsList
|
||||
<DataViewSelector
|
||||
currentDataViewId={currentDataViewId}
|
||||
searchListInputId={searchListInputId}
|
||||
dataViewsList={dataViewsList}
|
||||
selectableProps={selectableProps}
|
||||
isTextBasedLangSelected={isTextBasedLangSelected}
|
||||
setPopoverIsOpen={setPopoverIsOpen}
|
||||
onChangeDataView={async (newId) => {
|
||||
const dataView = await data.dataViews.get(newId);
|
||||
await data.dataViews.refreshFields(dataView);
|
||||
|
@ -336,58 +322,8 @@ export function ChangeDataView({
|
|||
onChangeDataView(newId);
|
||||
}
|
||||
}}
|
||||
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}
|
||||
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import React from 'react';
|
||||
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 { ChangeDataView } from './change_dataview';
|
||||
|
||||
|
@ -54,6 +54,10 @@ export interface DataViewPickerProps {
|
|||
* The adHocDataviews.
|
||||
*/
|
||||
adHocDataViews?: DataView[];
|
||||
/**
|
||||
* Saved data views
|
||||
*/
|
||||
savedDataViews?: DataViewListItem[];
|
||||
/**
|
||||
* EuiSelectable properties.
|
||||
*/
|
||||
|
@ -102,6 +106,7 @@ export const DataViewPicker = ({
|
|||
isMissingCurrent,
|
||||
currentDataViewId,
|
||||
adHocDataViews,
|
||||
savedDataViews,
|
||||
onChangeDataView,
|
||||
onEditDataView,
|
||||
onAddField,
|
||||
|
@ -126,6 +131,7 @@ export const DataViewPicker = ({
|
|||
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
|
||||
trigger={trigger}
|
||||
adHocDataViews={adHocDataViews}
|
||||
savedDataViews={savedDataViews}
|
||||
selectableProps={selectableProps}
|
||||
textBasedLanguages={textBasedLanguages}
|
||||
onSaveTextLanguageQuery={onSaveTextLanguageQuery}
|
||||
|
|
|
@ -21,6 +21,7 @@ export { SearchBar } from './search_bar';
|
|||
export type { FilterItemsProps } from './filter_bar';
|
||||
export { FilterLabel, FilterItem, FilterItems } from './filter_bar';
|
||||
export { DataViewsList } from './dataview_picker/dataview_list';
|
||||
export { DataViewSelector } from './dataview_picker/data_view_selector';
|
||||
export { DataViewPicker } from './dataview_picker';
|
||||
|
||||
export type { DataViewPickerProps } from './dataview_picker';
|
||||
|
|
|
@ -38,12 +38,20 @@ export type StatefulSearchBarProps<QT extends Query | AggregateQuery = Query> =
|
|||
useDefaultBehaviors?: boolean;
|
||||
savedQueryId?: string;
|
||||
onSavedQueryIdChange?: (savedQueryId?: string) => void;
|
||||
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||
};
|
||||
|
||||
// Respond to user changing the filters
|
||||
const defaultFiltersUpdated = (queryService: QueryStart) => {
|
||||
const defaultFiltersUpdated = (
|
||||
queryService: QueryStart,
|
||||
onFiltersUpdated?: (filters: Filter[]) => void
|
||||
) => {
|
||||
return (filters: Filter[]) => {
|
||||
if (onFiltersUpdated) {
|
||||
onFiltersUpdated(filters);
|
||||
} else {
|
||||
queryService.filterManager.setFilters(filters);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -206,7 +214,7 @@ export function createSearchBar({
|
|||
isRefreshPaused={refreshInterval.pause}
|
||||
filters={filters}
|
||||
query={query}
|
||||
onFiltersUpdated={defaultFiltersUpdated(data.query)}
|
||||
onFiltersUpdated={defaultFiltersUpdated(data.query, props.onFiltersUpdated)}
|
||||
onRefreshChange={defaultOnRefreshChange(data.query)}
|
||||
savedQuery={savedQuery}
|
||||
onQuerySubmit={defaultOnQuerySubmit(props, data.query, query)}
|
||||
|
|
|
@ -14,8 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const toasts = getService('toasts');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const filterBar = getService('filterBar');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const fieldEditor = getService('fieldEditor');
|
||||
const dashboardAddPanel = getService('dashboardAddPanel');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const retry = getService('retry');
|
||||
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 () => {
|
||||
const prevDataViewId = await PageObjects.discover.getCurrentDataViewId();
|
||||
|
||||
await testSubjects.click('discoverAlertsButton');
|
||||
await testSubjects.click('shareTopNavButton');
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
|
|
|
@ -67,6 +67,11 @@ export class ToastsService extends FtrService {
|
|||
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() {
|
||||
const list = await this.getGlobalToastList();
|
||||
return await list.findAllByCssSelector(`.euiToast`);
|
||||
|
|
|
@ -117,7 +117,6 @@ export const SearchPanel: FC<Props> = ({
|
|||
displayStyle={'inPage'}
|
||||
isClearable={true}
|
||||
customSubmitButton={<div />}
|
||||
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
|
||||
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiThemeComputed, useEuiTheme } from '@elastic/eui';
|
|||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { DataView } from '@kbn/data-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { SecuritySolutionContext } from '../../../application/security_solution_context';
|
||||
import * as TEST_SUBJECTS from '../test_subjects';
|
||||
import type { FindingsBaseURLQuery } from '../types';
|
||||
|
@ -49,7 +50,6 @@ export const FindingsSearchBar = ({
|
|||
isLoading={loading}
|
||||
indexPatterns={[dataView]}
|
||||
onQuerySubmit={setQuery}
|
||||
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
|
||||
onFiltersUpdated={(value: Filter[]) => setQuery({ filters: value })}
|
||||
placeholder={i18n.translate('xpack.csp.findings.searchBar.searchPlaceholder', {
|
||||
defaultMessage: 'Search findings (eg. rule.section : "API Server" )',
|
||||
|
|
|
@ -136,7 +136,6 @@ export const SearchPanel: FC<Props> = ({
|
|||
onQuerySubmit={(params: { dateRange: TimeRange; query?: Query | undefined }) =>
|
||||
searchHandler({ query: params.query })
|
||||
}
|
||||
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
|
||||
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
|
||||
indexPatterns={[dataView]}
|
||||
placeholder={i18n.translate('xpack.dataVisualizer.searchPanel.queryBarPlaceholderText', {
|
||||
|
|
|
@ -86,7 +86,7 @@ type Props = Omit<
|
|||
},
|
||||
AlertContextMeta
|
||||
>,
|
||||
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch'
|
||||
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
|
||||
>;
|
||||
|
||||
export const defaultExpression = {
|
||||
|
|
|
@ -39,7 +39,7 @@ type AlertParams = RuleTypeParams &
|
|||
|
||||
type Props = Omit<
|
||||
RuleTypeParamsExpressionProps<AlertParams, AlertContextMeta>,
|
||||
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch'
|
||||
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
|
||||
>;
|
||||
|
||||
export const defaultExpression = {
|
||||
|
|
|
@ -42,7 +42,7 @@ const FILTER_TYPING_DEBOUNCE_MS = 500;
|
|||
|
||||
type Props = Omit<
|
||||
RuleTypeParamsExpressionProps<RuleTypeParams & AlertParams, AlertContextMeta>,
|
||||
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch'
|
||||
'defaultActionGroupId' | 'actionGroups' | 'charts' | 'data' | 'unifiedSearch' | 'onChangeMetaData'
|
||||
>;
|
||||
|
||||
const defaultExpression = {
|
||||
|
|
|
@ -70,7 +70,6 @@ export const UnifiedSearchBar = ({ dataView }: Props) => {
|
|||
onClearSavedQuery={onClearSavedQuery}
|
||||
showSaveQuery
|
||||
showQueryInput
|
||||
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
|
||||
onFiltersUpdated={onFilterChange}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -143,6 +143,7 @@ describe('alert_form', () => {
|
|||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={() => {}}
|
||||
/>
|
||||
</KibanaReactContext.Provider>
|
||||
</I18nProvider>
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const configSchema = schema.object({});
|
||||
|
||||
export type Config = TypeOf<typeof configSchema>;
|
|
@ -5,8 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO: https://github.com/elastic/kibana/issues/110895
|
||||
/* eslint-disable @kbn/eslint/no_export_all */
|
||||
|
||||
export * from './config';
|
||||
export { STACK_ALERTS_FEATURE_ID } from './constants';
|
||||
|
|
|
@ -19,6 +19,8 @@ export interface StackAlertsPublicSetupDeps {
|
|||
}
|
||||
|
||||
export class StackAlertsPublicPlugin implements Plugin<Setup, Start, StackAlertsPublicSetupDeps> {
|
||||
constructor() {}
|
||||
|
||||
public setup(core: CoreSetup, { triggersActionsUi, alerting }: StackAlertsPublicSetupDeps) {
|
||||
registerRuleTypes({
|
||||
ruleTypeRegistry: triggersActionsUi.ruleTypeRegistry,
|
||||
|
|
|
@ -10,46 +10,72 @@ import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
|||
import { DataViewSelectPopover, DataViewSelectPopoverProps } from './data_view_select_popover';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
const props: DataViewSelectPopoverProps = {
|
||||
onSelectDataView: () => {},
|
||||
dataViewName: 'kibana_sample_data_logs',
|
||||
dataViewId: 'mock-data-logs-id',
|
||||
};
|
||||
|
||||
const dataViewOptions = [
|
||||
{
|
||||
const selectedDataView = {
|
||||
id: 'mock-data-logs-id',
|
||||
namespaces: ['default'],
|
||||
title: 'kibana_sample_data_logs',
|
||||
},
|
||||
isTimeBased: jest.fn(),
|
||||
isPersisted: jest.fn(() => true),
|
||||
getName: () => 'kibana_sample_data_logs',
|
||||
} as unknown as DataView;
|
||||
|
||||
const props: DataViewSelectPopoverProps = {
|
||||
onSelectDataView: () => {},
|
||||
onChangeMetaData: () => {},
|
||||
dataView: selectedDataView,
|
||||
};
|
||||
|
||||
const dataViewIds = ['mock-data-logs-id', 'mock-ecommerce-id', 'mock-test-id'];
|
||||
|
||||
const dataViewOptions = [
|
||||
selectedDataView,
|
||||
{
|
||||
id: 'mock-flyghts-id',
|
||||
namespaces: ['default'],
|
||||
title: 'kibana_sample_data_flights',
|
||||
isTimeBased: jest.fn(),
|
||||
isPersisted: jest.fn(() => true),
|
||||
getName: () => 'kibana_sample_data_flights',
|
||||
},
|
||||
{
|
||||
id: 'mock-ecommerce-id',
|
||||
namespaces: ['default'],
|
||||
title: 'kibana_sample_data_ecommerce',
|
||||
typeMeta: {},
|
||||
isTimeBased: jest.fn(),
|
||||
isPersisted: jest.fn(() => true),
|
||||
getName: () => 'kibana_sample_data_ecommerce',
|
||||
},
|
||||
{
|
||||
id: 'mock-test-id',
|
||||
namespaces: ['default'],
|
||||
title: 'test',
|
||||
typeMeta: {},
|
||||
isTimeBased: jest.fn(),
|
||||
isPersisted: jest.fn(() => true),
|
||||
getName: () => 'test',
|
||||
},
|
||||
];
|
||||
|
||||
const mount = () => {
|
||||
const dataViewsMock = dataViewPluginMocks.createStartContract();
|
||||
dataViewsMock.getIdsWithTitle.mockImplementation(() => Promise.resolve(dataViewOptions));
|
||||
dataViewsMock.getIds = jest.fn().mockImplementation(() => Promise.resolve(dataViewIds));
|
||||
dataViewsMock.get = jest
|
||||
.fn()
|
||||
.mockImplementation((id: string) =>
|
||||
Promise.resolve(dataViewOptions.find((current) => current.id === id))
|
||||
);
|
||||
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
|
||||
|
||||
return {
|
||||
wrapper: mountWithIntl(
|
||||
<KibanaContextProvider services={{ data: { dataViews: dataViewsMock } }}>
|
||||
<KibanaContextProvider
|
||||
services={{ dataViews: dataViewsMock, dataViewEditor: dataViewEditorMock }}
|
||||
>
|
||||
<DataViewSelectPopover {...props} />
|
||||
</KibanaContextProvider>
|
||||
),
|
||||
|
@ -66,10 +92,10 @@ describe('DataViewSelectPopover', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(dataViewsMock.getIdsWithTitle).toHaveBeenCalled();
|
||||
expect(dataViewsMock.getIds).toHaveBeenCalled();
|
||||
expect(wrapper.find('[data-test-subj="selectDataViewExpression"]').exists()).toBeTruthy();
|
||||
|
||||
const getIdsWithTitleResult = await dataViewsMock.getIdsWithTitle.mock.results[0].value;
|
||||
expect(getIdsWithTitleResult).toBe(dataViewOptions);
|
||||
const getIdsResult = await dataViewsMock.getIds.mock.results[0].value;
|
||||
expect(getIdsResult).toBe(dataViewIds);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,56 +14,97 @@ import {
|
|||
EuiExpression,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiPopover,
|
||||
EuiPopoverFooter,
|
||||
EuiPopoverTitle,
|
||||
EuiText,
|
||||
useEuiPaddingCSS,
|
||||
} from '@elastic/eui';
|
||||
import { DataViewsList } from '@kbn/unified-search-plugin/public';
|
||||
import { DataViewListItem } from '@kbn/data-views-plugin/public';
|
||||
import { useTriggersAndActionsUiDeps } from '../es_query/util';
|
||||
import type { DataViewListItem, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { DataViewSelector } from '@kbn/unified-search-plugin/public';
|
||||
import { useTriggerUiActionServices } from '../es_query/util';
|
||||
import { EsQueryRuleMetaData } from '../es_query/types';
|
||||
|
||||
export interface DataViewSelectPopoverProps {
|
||||
onSelectDataView: (newDataViewId: string) => void;
|
||||
dataViewName?: string;
|
||||
dataViewId?: string;
|
||||
dataView: DataView;
|
||||
metadata?: EsQueryRuleMetaData;
|
||||
onSelectDataView: (selectedDataView: DataView) => void;
|
||||
onChangeMetaData: (metadata: EsQueryRuleMetaData) => void;
|
||||
}
|
||||
|
||||
const toDataViewListItem = (dataView: DataView): DataViewListItem => {
|
||||
return {
|
||||
id: dataView.id!,
|
||||
title: dataView.title,
|
||||
name: dataView.name,
|
||||
};
|
||||
};
|
||||
|
||||
export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopoverProps> = ({
|
||||
metadata = { adHocDataViewList: [], isManagementPage: true },
|
||||
dataView,
|
||||
onSelectDataView,
|
||||
dataViewName,
|
||||
dataViewId,
|
||||
onChangeMetaData,
|
||||
}) => {
|
||||
const { data, dataViewEditor } = useTriggersAndActionsUiDeps();
|
||||
const [dataViewItems, setDataViewsItems] = useState<DataViewListItem[]>();
|
||||
const { dataViews, dataViewEditor } = useTriggerUiActionServices();
|
||||
const [dataViewItems, setDataViewsItems] = useState<DataViewListItem[]>([]);
|
||||
const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false);
|
||||
|
||||
const closeDataViewEditor = useRef<() => void | undefined>();
|
||||
|
||||
const loadDataViews = useCallback(async () => {
|
||||
const fetchedDataViewItems = await data.dataViews.getIdsWithTitle();
|
||||
setDataViewsItems(fetchedDataViewItems);
|
||||
}, [setDataViewsItems, data.dataViews]);
|
||||
const allDataViewItems = useMemo(
|
||||
() => [...dataViewItems, ...metadata.adHocDataViewList.map(toDataViewListItem)],
|
||||
[dataViewItems, metadata.adHocDataViewList]
|
||||
);
|
||||
|
||||
const closeDataViewPopover = useCallback(() => setDataViewPopoverOpen(false), []);
|
||||
|
||||
const onChangeDataView = useCallback(
|
||||
async (selectedDataViewId: string) => {
|
||||
const selectedDataView = await dataViews.get(selectedDataViewId);
|
||||
onSelectDataView(selectedDataView);
|
||||
closeDataViewPopover();
|
||||
},
|
||||
[closeDataViewPopover, dataViews, onSelectDataView]
|
||||
);
|
||||
|
||||
const loadPersistedDataViews = useCallback(async () => {
|
||||
const ids = await dataViews.getIds();
|
||||
const dataViewsList = await Promise.all(ids.map((id) => dataViews.get(id)));
|
||||
|
||||
setDataViewsItems(dataViewsList.map(toDataViewListItem));
|
||||
}, [dataViews]);
|
||||
|
||||
const onAddAdHocDataView = useCallback(
|
||||
(adHocDataView: DataView) => {
|
||||
onChangeMetaData({
|
||||
...metadata,
|
||||
adHocDataViewList: [...metadata.adHocDataViewList, adHocDataView],
|
||||
});
|
||||
},
|
||||
[metadata, onChangeMetaData]
|
||||
);
|
||||
|
||||
const createDataView = useMemo(
|
||||
() =>
|
||||
dataViewEditor?.userPermissions.editDataView()
|
||||
dataViewEditor.userPermissions.editDataView()
|
||||
? () => {
|
||||
closeDataViewEditor.current = dataViewEditor.openEditor({
|
||||
onSave: async (createdDataView) => {
|
||||
if (createdDataView.id) {
|
||||
await onSelectDataView(createdDataView.id);
|
||||
await loadDataViews();
|
||||
if (!createdDataView.isPersisted()) {
|
||||
onAddAdHocDataView(createdDataView);
|
||||
}
|
||||
|
||||
await loadPersistedDataViews();
|
||||
await onChangeDataView(createdDataView.id);
|
||||
}
|
||||
},
|
||||
allowAdHocDataView: true,
|
||||
});
|
||||
}
|
||||
: undefined,
|
||||
[dataViewEditor, onSelectDataView, loadDataViews]
|
||||
[dataViewEditor, loadPersistedDataViews, onChangeDataView, onAddAdHocDataView]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -76,12 +117,25 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadDataViews();
|
||||
}, [loadDataViews]);
|
||||
loadPersistedDataViews();
|
||||
}, [loadPersistedDataViews]);
|
||||
|
||||
const createDataViewButtonPadding = useEuiPaddingCSS('left');
|
||||
|
||||
if (!dataViewItems) {
|
||||
const onCreateDefaultAdHocDataView = useCallback(
|
||||
async (pattern: string) => {
|
||||
const newDataView = await dataViews.create({ title: pattern });
|
||||
if (newDataView.fields.getByName('@timestamp')?.type === 'date') {
|
||||
newDataView.timeFieldName = '@timestamp';
|
||||
}
|
||||
|
||||
onAddAdHocDataView(newDataView);
|
||||
onChangeDataView(newDataView.id!);
|
||||
},
|
||||
[dataViews, onAddAdHocDataView, onChangeDataView]
|
||||
);
|
||||
|
||||
if (!allDataViewItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -96,7 +150,7 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
|
|||
defaultMessage: 'data view',
|
||||
})}
|
||||
value={
|
||||
dataViewName ??
|
||||
dataView.getName() ??
|
||||
i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewPlaceholder', {
|
||||
defaultMessage: 'Select a data view',
|
||||
})
|
||||
|
@ -105,7 +159,7 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
|
|||
onClick={() => {
|
||||
setDataViewPopoverOpen(true);
|
||||
}}
|
||||
isInvalid={!dataViewId}
|
||||
isInvalid={!dataView.id}
|
||||
/>
|
||||
}
|
||||
isOpen={dataViewPopoverOpen}
|
||||
|
@ -136,24 +190,14 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopoverTitle>
|
||||
<EuiFormRow
|
||||
id="indexSelectSearchBox"
|
||||
fullWidth
|
||||
css={`
|
||||
.euiPanel {
|
||||
padding: 0;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<DataViewsList
|
||||
dataViewsList={dataViewItems}
|
||||
onChangeDataView={(newId) => {
|
||||
onSelectDataView(newId);
|
||||
closeDataViewPopover();
|
||||
}}
|
||||
currentDataViewId={dataViewId}
|
||||
<DataViewSelector
|
||||
currentDataViewId={dataView.id}
|
||||
dataViewsList={allDataViewItems}
|
||||
setPopoverIsOpen={setDataViewPopoverOpen}
|
||||
onChangeDataView={onChangeDataView}
|
||||
onCreateDefaultAdHocDataView={onCreateDefaultAdHocDataView}
|
||||
isTextBasedLangSelected={false}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{createDataView ? (
|
||||
<EuiPopoverFooter paddingSize="none">
|
||||
<EuiButtonEmpty
|
||||
|
|
|
@ -159,6 +159,7 @@ describe('EsQueryRuleTypeExpression', () => {
|
|||
defaultActionGroupId=""
|
||||
actionGroups={[]}
|
||||
charts={chartsStartMock}
|
||||
onChangeMetaData={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ import { getFields, RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-
|
|||
import { parseDuration } from '@kbn/alerting-plugin/common';
|
||||
import { hasExpressionValidationErrors } from '../validation';
|
||||
import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
|
||||
import { EsQueryRuleParams, SearchType } from '../types';
|
||||
import { EsQueryRuleParams, EsQueryRuleMetaData, SearchType } from '../types';
|
||||
import { IndexSelectPopover } from '../../components/index_select_popover';
|
||||
import { DEFAULT_VALUES } from '../constants';
|
||||
import { RuleCommonExpressions } from '../rule_common_expressions';
|
||||
|
@ -33,7 +33,7 @@ interface KibanaDeps {
|
|||
}
|
||||
|
||||
export const EsQueryExpression: React.FC<
|
||||
RuleTypeParamsExpressionProps<EsQueryRuleParams<SearchType.esQuery>>
|
||||
RuleTypeParamsExpressionProps<EsQueryRuleParams<SearchType.esQuery>, EsQueryRuleMetaData>
|
||||
> = ({ ruleParams, setRuleParams, setRuleProperty, errors, data }) => {
|
||||
const {
|
||||
index,
|
||||
|
|
|
@ -13,7 +13,7 @@ import { httpServiceMock } from '@kbn/core/public/mocks';
|
|||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { CommonRuleParams, EsQueryRuleParams, SearchType } from '../types';
|
||||
import { CommonRuleParams, EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types';
|
||||
import { EsQueryRuleTypeExpression } from './expression';
|
||||
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
|
||||
import { Subject } from 'rxjs';
|
||||
|
@ -22,6 +22,7 @@ import { IUiSettingsClient } from '@kbn/core/public';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => {
|
||||
|
@ -87,6 +88,8 @@ const searchSourceFieldsMock = {
|
|||
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
title: 'kibana_sample_data_logs',
|
||||
fields: [],
|
||||
getName: () => 'kibana_sample_data_logs',
|
||||
isPersisted: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -122,11 +125,15 @@ const savedQueryMock = {
|
|||
};
|
||||
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
const dataViewsMock = dataViewPluginMocks.createStartContract();
|
||||
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
|
||||
|
||||
(dataMock.search.searchSource.create as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve(searchSourceMock)
|
||||
);
|
||||
(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([]));
|
||||
dataMock.dataViews.getDefaultDataView = jest.fn(() => Promise.resolve(null));
|
||||
(dataViewsMock.getIds as jest.Mock) = jest.fn().mockImplementation(() => Promise.resolve([]));
|
||||
dataViewsMock.getDefaultDataView = jest.fn(() => Promise.resolve(null));
|
||||
dataViewsMock.get = jest.fn();
|
||||
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve(savedQueryMock)
|
||||
);
|
||||
|
@ -137,7 +144,8 @@ dataMock.query.savedQueries.findSavedQueries = jest.fn(() =>
|
|||
|
||||
const Wrapper: React.FC<{
|
||||
ruleParams: EsQueryRuleParams<SearchType.searchSource> | EsQueryRuleParams<SearchType.esQuery>;
|
||||
}> = ({ ruleParams }) => {
|
||||
metadata?: EsQueryRuleMetaData;
|
||||
}> = ({ ruleParams, metadata }) => {
|
||||
const [currentRuleParams, setCurrentRuleParams] = useState<CommonRuleParams>(ruleParams);
|
||||
const errors = {
|
||||
index: [],
|
||||
|
@ -170,23 +178,29 @@ const Wrapper: React.FC<{
|
|||
defaultActionGroupId=""
|
||||
actionGroups={[]}
|
||||
charts={chartsStartMock}
|
||||
metadata={metadata}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const setup = (
|
||||
ruleParams: EsQueryRuleParams<SearchType.searchSource> | EsQueryRuleParams<SearchType.esQuery>
|
||||
ruleParams: EsQueryRuleParams<SearchType.searchSource> | EsQueryRuleParams<SearchType.esQuery>,
|
||||
metadata?: EsQueryRuleMetaData
|
||||
) => {
|
||||
return mountWithIntl(
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
data: dataMock,
|
||||
dataViews: dataViewsMock,
|
||||
uiSettings: uiSettingsMock,
|
||||
docLinks: docLinksMock,
|
||||
http: httpMock,
|
||||
unifiedSearch: unifiedSearchMock,
|
||||
dataViewEditor: dataViewEditorMock,
|
||||
}}
|
||||
>
|
||||
<Wrapper ruleParams={ruleParams} />
|
||||
<Wrapper ruleParams={ruleParams} metadata={metadata} />
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
};
|
||||
|
@ -236,10 +250,10 @@ describe('EsQueryRuleTypeExpression', () => {
|
|||
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should render QueryDSL view without the form type chooser if some rule params were passed', async () => {
|
||||
test('should render QueryDSL view without the form type chooser', async () => {
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = setup(defaultEsQueryRuleParams);
|
||||
wrapper = setup(defaultEsQueryRuleParams, { adHocDataViewList: [], isManagementPage: false });
|
||||
wrapper = await wrapper.update();
|
||||
});
|
||||
expect(findTestSubject(wrapper!, 'queryFormTypeChooserTitle').exists()).toBeFalsy();
|
||||
|
@ -247,10 +261,13 @@ describe('EsQueryRuleTypeExpression', () => {
|
|||
expect(findTestSubject(wrapper!, 'selectIndexExpression').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should render KQL and Lucene view without the form type chooser if some rule params were passed', async () => {
|
||||
test('should render KQL and Lucene view without the form type chooser', async () => {
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = setup(defaultSearchSourceRuleParams);
|
||||
wrapper = setup(defaultSearchSourceRuleParams, {
|
||||
adHocDataViewList: [],
|
||||
isManagementPage: false,
|
||||
});
|
||||
wrapper = await wrapper.update();
|
||||
});
|
||||
wrapper = await wrapper!.update();
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { memo, PropsWithChildren, useCallback, useRef } from 'react';
|
||||
import React, { memo, PropsWithChildren, useCallback } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import 'brace/theme/github';
|
||||
import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { EsQueryRuleParams, SearchType } from '../types';
|
||||
import { EsQueryRuleParams, EsQueryRuleMetaData, SearchType } from '../types';
|
||||
import { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression';
|
||||
import { EsQueryExpression } from './es_query_expression';
|
||||
import { QueryFormTypeChooser } from './query_form_type_chooser';
|
||||
|
@ -33,11 +33,12 @@ const SearchSourceExpressionMemoized = memo<SearchSourceExpressionProps>(
|
|||
);
|
||||
|
||||
export const EsQueryRuleTypeExpression: React.FunctionComponent<
|
||||
RuleTypeParamsExpressionProps<EsQueryRuleParams>
|
||||
RuleTypeParamsExpressionProps<EsQueryRuleParams, EsQueryRuleMetaData>
|
||||
> = (props) => {
|
||||
const { ruleParams, errors, setRuleProperty, setRuleParams } = props;
|
||||
const isSearchSource = isSearchSourceRule(ruleParams);
|
||||
const isManagementPage = useRef(!Object.keys(ruleParams).length).current;
|
||||
// metadata provided only when open alert from Discover page
|
||||
const isManagementPage = props.metadata?.isManagementPage ?? true;
|
||||
|
||||
const formTypeSelected = useCallback(
|
||||
(searchType: SearchType | null) => {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { IUiSettingsClient } from '@kbn/core/public';
|
|||
import { findTestSubject } from '@elastic/eui/lib/test';
|
||||
import { copyToClipboard, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { indexPatternEditorPluginMock as dataViewEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
|
||||
jest.mock('@elastic/eui', () => {
|
||||
|
@ -81,6 +82,9 @@ const searchSourceFieldsMock = {
|
|||
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
|
||||
title: 'kibana_sample_data_logs',
|
||||
fields: [],
|
||||
isPersisted: () => true,
|
||||
getName: () => 'kibana_sample_data_logs',
|
||||
isTimeBased: () => true,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -182,8 +186,9 @@ const dataMock = dataPluginMock.createStartContract();
|
|||
(dataMock.search.searchSource.create as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve(searchSourceMock)
|
||||
);
|
||||
(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([]));
|
||||
dataMock.dataViews.getDefaultDataView = jest.fn(() => Promise.resolve(null));
|
||||
(dataViewPluginMock.getIds as jest.Mock) = jest.fn().mockImplementation(() => Promise.resolve([]));
|
||||
dataViewPluginMock.getDefaultDataView = jest.fn(() => Promise.resolve(null));
|
||||
dataViewPluginMock.get = jest.fn();
|
||||
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
|
||||
Promise.resolve(savedQueryMock)
|
||||
);
|
||||
|
@ -198,9 +203,18 @@ const setup = (alertParams: EsQueryRuleParams<SearchType.searchSource>) => {
|
|||
timeWindowSize: [],
|
||||
searchConfiguration: [],
|
||||
};
|
||||
const dataViewEditorMock = dataViewEditorPluginMock.createStartContract();
|
||||
|
||||
return mountWithIntl(
|
||||
<KibanaContextProvider services={{ data: dataMock, uiSettings: uiSettingsMock }}>
|
||||
<KibanaContextProvider
|
||||
services={{
|
||||
dataViews: dataViewPluginMock,
|
||||
data: dataMock,
|
||||
uiSettings: uiSettingsMock,
|
||||
dataViewEditor: dataViewEditorMock,
|
||||
unifiedSearch: unifiedSearchMock,
|
||||
}}
|
||||
>
|
||||
<SearchSourceExpression
|
||||
ruleInterval="1m"
|
||||
ruleThrottle="1m"
|
||||
|
@ -215,6 +229,8 @@ const setup = (alertParams: EsQueryRuleParams<SearchType.searchSource>) => {
|
|||
defaultActionGroupId=""
|
||||
actionGroups={[]}
|
||||
charts={chartsStartMock}
|
||||
metadata={{ adHocDataViewList: [] }}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
|
|
|
@ -11,13 +11,14 @@ import { EuiSpacer, EuiLoadingSpinner, EuiEmptyPrompt, EuiCallOut } from '@elast
|
|||
import { ISearchSource } from '@kbn/data-plugin/common';
|
||||
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SavedQuery } from '@kbn/data-plugin/public';
|
||||
import { EsQueryRuleParams, SearchType } from '../types';
|
||||
import { useTriggersAndActionsUiDeps } from '../util';
|
||||
import { EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types';
|
||||
import { SearchSourceExpressionForm } from './search_source_expression_form';
|
||||
import { DEFAULT_VALUES } from '../constants';
|
||||
import { useTriggerUiActionServices } from '../util';
|
||||
|
||||
export type SearchSourceExpressionProps = RuleTypeParamsExpressionProps<
|
||||
EsQueryRuleParams<SearchType.searchSource>
|
||||
EsQueryRuleParams<SearchType.searchSource>,
|
||||
EsQueryRuleMetaData
|
||||
>;
|
||||
|
||||
export const SearchSourceExpression = ({
|
||||
|
@ -25,6 +26,8 @@ export const SearchSourceExpression = ({
|
|||
errors,
|
||||
setRuleParams,
|
||||
setRuleProperty,
|
||||
metadata,
|
||||
onChangeMetaData,
|
||||
}: SearchSourceExpressionProps) => {
|
||||
const {
|
||||
thresholdComparator,
|
||||
|
@ -36,7 +39,7 @@ export const SearchSourceExpression = ({
|
|||
searchConfiguration,
|
||||
excludeHitsFromPreviousRun,
|
||||
} = ruleParams;
|
||||
const { data } = useTriggersAndActionsUiDeps();
|
||||
const { data } = useTriggerUiActionServices();
|
||||
|
||||
const [searchSource, setSearchSource] = useState<ISearchSource>();
|
||||
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
|
||||
|
@ -112,6 +115,8 @@ export const SearchSourceExpression = ({
|
|||
errors={errors}
|
||||
initialSavedQuery={savedQuery}
|
||||
setParam={setParam}
|
||||
metadata={metadata}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,21 +8,26 @@
|
|||
import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { DataView, Query, ISearchSource, getTime } from '@kbn/data-plugin/common';
|
||||
import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { SearchBar, SearchBarProps } from '@kbn/unified-search-plugin/public';
|
||||
import { mapAndFlattenFilters, SavedQuery, TimeHistory } from '@kbn/data-plugin/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { CommonRuleParams, EsQueryRuleParams, SearchType } from '../types';
|
||||
import type { SearchBarProps } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import {
|
||||
mapAndFlattenFilters,
|
||||
getTime,
|
||||
type SavedQuery,
|
||||
type ISearchSource,
|
||||
} from '@kbn/data-plugin/public';
|
||||
import { STACK_ALERTS_FEATURE_ID } from '../../../../common';
|
||||
import { CommonRuleParams, EsQueryRuleMetaData, EsQueryRuleParams, SearchType } from '../types';
|
||||
import { DEFAULT_VALUES } from '../constants';
|
||||
import { DataViewSelectPopover } from '../../components/data_view_select_popover';
|
||||
import { useTriggersAndActionsUiDeps } from '../util';
|
||||
import { RuleCommonExpressions } from '../rule_common_expressions';
|
||||
import { totalHitsToNumber } from '../test_query_row';
|
||||
import { hasExpressionValidationErrors } from '../validation';
|
||||
import { useTriggerUiActionServices } from '../util';
|
||||
|
||||
const HIDDEN_FILTER_PANEL_OPTIONS: SearchBarProps['hiddenFilterPanelOptions'] = [
|
||||
'pinFilter',
|
||||
|
@ -66,8 +71,10 @@ interface SearchSourceExpressionFormProps {
|
|||
searchSource: ISearchSource;
|
||||
ruleParams: EsQueryRuleParams<SearchType.searchSource>;
|
||||
errors: IErrorObject;
|
||||
metadata?: EsQueryRuleMetaData;
|
||||
initialSavedQuery?: SavedQuery;
|
||||
setParam: (paramField: string, paramValue: unknown) => void;
|
||||
onChangeMetaData: (metadata: EsQueryRuleMetaData) => void;
|
||||
}
|
||||
|
||||
const isSearchSourceParam = (action: LocalStateAction): action is SearchSourceParamsAction => {
|
||||
|
@ -75,12 +82,11 @@ const isSearchSourceParam = (action: LocalStateAction): action is SearchSourcePa
|
|||
};
|
||||
|
||||
export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => {
|
||||
const { data } = useTriggersAndActionsUiDeps();
|
||||
const services = useTriggerUiActionServices();
|
||||
const unifiedSearch = services.unifiedSearch;
|
||||
const { searchSource, errors, initialSavedQuery, setParam, ruleParams } = props;
|
||||
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
|
||||
|
||||
const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []);
|
||||
|
||||
useEffect(() => setSavedQuery(initialSavedQuery), [initialSavedQuery]);
|
||||
|
||||
const [ruleConfiguration, dispatch] = useReducer<LocalStateReducer>(
|
||||
|
@ -110,11 +116,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
const dataViews = useMemo(() => (dataView ? [dataView] : []), [dataView]);
|
||||
|
||||
const onSelectDataView = useCallback(
|
||||
(newDataViewId) =>
|
||||
data.dataViews
|
||||
.get(newDataViewId)
|
||||
.then((newDataView) => dispatch({ type: 'index', payload: newDataView })),
|
||||
[data.dataViews]
|
||||
(newDataView: DataView) => dispatch({ type: 'index', payload: newDataView }),
|
||||
[]
|
||||
);
|
||||
|
||||
const onUpdateFilters = useCallback((newFilters) => {
|
||||
|
@ -228,9 +231,10 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
<EuiSpacer size="s" />
|
||||
|
||||
<DataViewSelectPopover
|
||||
dataViewName={dataView?.getName?.() ?? dataView?.title}
|
||||
dataViewId={dataView?.id}
|
||||
dataView={dataView}
|
||||
metadata={props.metadata}
|
||||
onSelectDataView={onSelectDataView}
|
||||
onChangeMetaData={props.onChangeMetaData}
|
||||
/>
|
||||
|
||||
{Boolean(dataView?.id) && (
|
||||
|
@ -245,7 +249,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="xs" />
|
||||
<SearchBar
|
||||
<unifiedSearch.ui.SearchBar
|
||||
appName={STACK_ALERTS_FEATURE_ID}
|
||||
onQuerySubmit={onQueryBarSubmit}
|
||||
onQueryChange={onChangeQuery}
|
||||
suggestionsSize="s"
|
||||
|
@ -266,7 +271,6 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
|
|||
showSubmitButton={false}
|
||||
dateRangeFrom={undefined}
|
||||
dateRangeTo={undefined}
|
||||
timeHistory={timeHistory}
|
||||
hiddenFilterPanelOptions={HIDDEN_FILTER_PANEL_OPTIONS}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
import { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
|
||||
import { EuiComboBoxOptionOption } from '@elastic/eui';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public';
|
||||
import { EXPRESSION_ERRORS } from './constants';
|
||||
|
||||
export interface Comparator {
|
||||
|
@ -32,6 +34,11 @@ export interface CommonRuleParams extends RuleTypeParams {
|
|||
excludeHitsFromPreviousRun: boolean;
|
||||
}
|
||||
|
||||
export interface EsQueryRuleMetaData {
|
||||
adHocDataViewList: DataView[];
|
||||
isManagementPage?: boolean;
|
||||
}
|
||||
|
||||
export type EsQueryRuleParams<T = SearchType> = T extends SearchType.searchSource
|
||||
? CommonRuleParams & OnlySearchSourceRuleParams
|
||||
: CommonRuleParams & OnlyEsQueryRuleParams;
|
||||
|
@ -55,6 +62,8 @@ export type ExpressionErrors = typeof EXPRESSION_ERRORS;
|
|||
export type ErrorKey = keyof ExpressionErrors & unknown;
|
||||
|
||||
export interface TriggersAndActionsUiDeps {
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
dataViewEditor: DataViewEditorStart;
|
||||
}
|
||||
|
|
|
@ -14,4 +14,4 @@ export const isSearchSourceRule = (
|
|||
return ruleParams.searchType === 'searchSource';
|
||||
};
|
||||
|
||||
export const useTriggersAndActionsUiDeps = () => useKibana<TriggersAndActionsUiDeps>().services;
|
||||
export const useTriggerUiActionServices = () => useKibana<TriggersAndActionsUiDeps>().services;
|
||||
|
|
|
@ -100,6 +100,18 @@ export const validateExpression = (ruleParams: EsQueryRuleParams): ValidationRes
|
|||
defaultMessage: 'Data view is required.',
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
typeof ruleParams.searchConfiguration.index === 'object' &&
|
||||
!Object.hasOwn(ruleParams.searchConfiguration.index, 'timeFieldName')
|
||||
) {
|
||||
errors.index.push(
|
||||
i18n.translate(
|
||||
'xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewTimeFieldText',
|
||||
{
|
||||
defaultMessage: 'Data view should have a time field.',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
}
|
||||
|
|
|
@ -101,6 +101,7 @@ describe('IndexThresholdRuleTypeExpression', () => {
|
|||
defaultActionGroupId=""
|
||||
actionGroups={[]}
|
||||
charts={chartsStartMock}
|
||||
onChangeMetaData={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -5,11 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { get } from 'lodash';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { AlertingBuiltinsPlugin } from './plugin';
|
||||
import { configSchema, Config } from '../common/config';
|
||||
export { ID as INDEX_THRESHOLD_ID } from './rule_types/index_threshold/rule_type';
|
||||
|
||||
export const configSchema = schema.object({});
|
||||
|
||||
export type Config = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<Config> = {
|
||||
exposeToBrowser: {},
|
||||
schema: configSchema,
|
||||
|
|
|
@ -207,7 +207,10 @@ describe('rule_add', () => {
|
|||
expect(wrapper.find('[data-test-subj="saveRuleButton"]').exists()).toBeTruthy();
|
||||
|
||||
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 () => {
|
||||
|
@ -301,7 +304,10 @@ describe('rule_add', () => {
|
|||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED);
|
||||
expect(onClose).toHaveBeenCalledWith(RuleFlyoutCloseReason.SAVED, {
|
||||
test: 'some value',
|
||||
fields: ['test'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should enforce any default interval', async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -47,11 +47,13 @@ const RuleAdd = ({
|
|||
initialValues,
|
||||
reloadRules,
|
||||
onSave,
|
||||
metadata,
|
||||
metadata: initialMetadata,
|
||||
filteredRuleTypes,
|
||||
...props
|
||||
}: RuleAddProps) => {
|
||||
const onSaveHandler = onSave ?? reloadRules;
|
||||
const [metadata, setMetadata] = useState(initialMetadata);
|
||||
const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []);
|
||||
|
||||
const initialRule: InitialRule = useMemo(() => {
|
||||
return {
|
||||
|
@ -177,7 +179,7 @@ const RuleAdd = ({
|
|||
) {
|
||||
setIsConfirmRuleCloseModalOpen(true);
|
||||
} else {
|
||||
onClose(RuleFlyoutCloseReason.CANCELED);
|
||||
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -185,9 +187,9 @@ const RuleAdd = ({
|
|||
const savedRule = await onSaveRule();
|
||||
setIsSaving(false);
|
||||
if (savedRule) {
|
||||
onClose(RuleFlyoutCloseReason.SAVED);
|
||||
onClose(RuleFlyoutCloseReason.SAVED, metadata);
|
||||
if (onSaveHandler) {
|
||||
onSaveHandler();
|
||||
onSaveHandler(metadata);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -263,6 +265,7 @@ const RuleAdd = ({
|
|||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
metadata={metadata}
|
||||
filteredRuleTypes={filteredRuleTypes}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<RuleAddFooter
|
||||
|
@ -308,7 +311,7 @@ const RuleAdd = ({
|
|||
<ConfirmRuleClose
|
||||
onConfirm={() => {
|
||||
setIsConfirmRuleCloseModalOpen(false);
|
||||
onClose(RuleFlyoutCloseReason.CANCELED);
|
||||
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsConfirmRuleCloseModalOpen(false);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useReducer, useState, useEffect } from 'react';
|
||||
import React, { useReducer, useState, useEffect, useCallback } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import {
|
||||
EuiTitle,
|
||||
|
@ -52,7 +52,7 @@ export const RuleEdit = ({
|
|||
onSave,
|
||||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
metadata,
|
||||
metadata: initialMetadata,
|
||||
...props
|
||||
}: RuleEditProps) => {
|
||||
const onSaveHandler = onSave ?? reloadRules;
|
||||
|
@ -71,6 +71,9 @@ export const RuleEdit = ({
|
|||
);
|
||||
const [config, setConfig] = useState<TriggersActionsUiConfig>({ isUsingSecurity: false });
|
||||
|
||||
const [metadata, setMetadata] = useState(initialMetadata);
|
||||
const onChangeMetaData = useCallback((newMetadata) => setMetadata(newMetadata), []);
|
||||
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
|
@ -119,7 +122,7 @@ export const RuleEdit = ({
|
|||
if (hasRuleChanged(rule, initialRule, true)) {
|
||||
setIsConfirmRuleCloseModalOpen(true);
|
||||
} else {
|
||||
onClose(RuleFlyoutCloseReason.CANCELED);
|
||||
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -140,9 +143,9 @@ export const RuleEdit = ({
|
|||
},
|
||||
})
|
||||
);
|
||||
onClose(RuleFlyoutCloseReason.SAVED);
|
||||
onClose(RuleFlyoutCloseReason.SAVED, metadata);
|
||||
if (onSaveHandler) {
|
||||
onSaveHandler();
|
||||
onSaveHandler(metadata);
|
||||
}
|
||||
} else {
|
||||
setRule(
|
||||
|
@ -214,6 +217,7 @@ export const RuleEdit = ({
|
|||
}
|
||||
)}
|
||||
metadata={metadata}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
|
@ -280,7 +284,7 @@ export const RuleEdit = ({
|
|||
<ConfirmRuleClose
|
||||
onConfirm={() => {
|
||||
setIsConfirmRuleCloseModalOpen(false);
|
||||
onClose(RuleFlyoutCloseReason.CANCELED);
|
||||
onClose(RuleFlyoutCloseReason.CANCELED, metadata);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsConfirmRuleCloseModalOpen(false);
|
||||
|
|
|
@ -211,6 +211,7 @@ describe('rule_form', () => {
|
|||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -334,6 +335,7 @@ describe('rule_form', () => {
|
|||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
connectorFeatureId={featureId}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -578,6 +580,7 @@ describe('rule_form', () => {
|
|||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -644,6 +647,7 @@ describe('rule_form', () => {
|
|||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -99,6 +99,7 @@ interface RuleFormProps<MetaData = Record<string, any>> {
|
|||
metadata?: MetaData;
|
||||
filteredRuleTypes?: string[];
|
||||
connectorFeatureId?: string;
|
||||
onChangeMetaData: (metadata: MetaData) => void;
|
||||
}
|
||||
|
||||
export const RuleForm = ({
|
||||
|
@ -115,6 +116,7 @@ export const RuleForm = ({
|
|||
metadata,
|
||||
filteredRuleTypes: ruleTypeToFilter,
|
||||
connectorFeatureId = AlertingConnectorFeatureId,
|
||||
onChangeMetaData,
|
||||
}: RuleFormProps) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
|
@ -522,6 +524,7 @@ export const RuleForm = ({
|
|||
data={data}
|
||||
dataViews={dataViews}
|
||||
unifiedSearch={unifiedSearch}
|
||||
onChangeMetaData={onChangeMetaData}
|
||||
/>
|
||||
</Suspense>
|
||||
</EuiErrorBoundary>
|
||||
|
|
|
@ -322,6 +322,7 @@ export interface RuleTypeParamsExpressionProps<
|
|||
key: Prop,
|
||||
value: SanitizedRule<Params>[Prop] | null
|
||||
) => void;
|
||||
onChangeMetaData: (metadata: MetaData) => void;
|
||||
errors: IErrorObject;
|
||||
defaultActionGroupId: string;
|
||||
actionGroups: Array<ActionGroup<ActionGroupIds>>;
|
||||
|
@ -359,10 +360,10 @@ export interface RuleEditProps<MetaData = Record<string, any>> {
|
|||
initialRule: Rule;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
onClose: (reason: RuleFlyoutCloseReason) => void;
|
||||
onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void;
|
||||
/** @deprecated use `onSave` as a callback after an alert is saved*/
|
||||
reloadRules?: () => Promise<void>;
|
||||
onSave?: () => Promise<void>;
|
||||
onSave?: (metadata?: MetaData) => Promise<void>;
|
||||
metadata?: MetaData;
|
||||
ruleType?: RuleType<string, string>;
|
||||
}
|
||||
|
@ -371,13 +372,13 @@ export interface RuleAddProps<MetaData = Record<string, any>> {
|
|||
consumer: string;
|
||||
ruleTypeRegistry: RuleTypeRegistryContract;
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
onClose: (reason: RuleFlyoutCloseReason) => void;
|
||||
onClose: (reason: RuleFlyoutCloseReason, metadata?: MetaData) => void;
|
||||
ruleTypeId?: string;
|
||||
canChangeTrigger?: boolean;
|
||||
initialValues?: Partial<Rule>;
|
||||
/** @deprecated use `onSave` as a callback after an alert is saved*/
|
||||
reloadRules?: () => Promise<void>;
|
||||
onSave?: () => Promise<void>;
|
||||
onSave?: (metadata?: MetaData) => Promise<void>;
|
||||
metadata?: MetaData;
|
||||
ruleTypeIndex?: RuleTypeIndex;
|
||||
filteredRuleTypes?: string[];
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
import { last } from 'lodash';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
||||
|
@ -32,12 +31,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const security = getService('security');
|
||||
const filterBar = getService('filterBar');
|
||||
const find = getService('find');
|
||||
const toasts = getService('toasts');
|
||||
|
||||
const SOURCE_DATA_INDEX = 'search-source-alert';
|
||||
const OUTPUT_DATA_INDEX = 'search-source-alert-output';
|
||||
const ACTION_TYPE_ID = '.index';
|
||||
const RULE_NAME = 'test-search-source-alert';
|
||||
let sourceDataViewId: string;
|
||||
let sourceAdHocDataViewId: string;
|
||||
let outputDataViewId: string;
|
||||
let connectorId: string;
|
||||
|
||||
|
@ -115,6 +116,22 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
.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 { body: createdAction } = await supertest
|
||||
.post(`/api/actions/connector`)
|
||||
|
@ -133,16 +150,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const deleteConnector = (id: string) =>
|
||||
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) => {
|
||||
await testSubjects.click('discoverAlertsButton');
|
||||
await testSubjects.click('discoverCreateAlertButton');
|
||||
|
@ -168,41 +175,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
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 () => {
|
||||
// getting the link
|
||||
await dataGrid.clickRowToggle();
|
||||
|
@ -214,24 +186,32 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
return link;
|
||||
};
|
||||
|
||||
const navigateToDiscover = async (link: string) => {
|
||||
// following ling provided by alert to see documents triggered the alert
|
||||
const openAlertResults = async (ruleName: string, dataViewId?: string) => {
|
||||
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();
|
||||
await browser.navigateTo(baseUrl + link);
|
||||
await PageObjects.discover.waitUntilSearchingHasFinished();
|
||||
|
||||
await retry.waitFor('navigate to discover', async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
return currentUrl.includes(sourceDataViewId);
|
||||
const currentDataViewId = await PageObjects.discover.getCurrentDataViewId();
|
||||
return dataViewId ? currentDataViewId === dataViewId : true;
|
||||
});
|
||||
};
|
||||
|
||||
const navigateToResults = async () => {
|
||||
const link = await getResultsLink();
|
||||
await navigateToDiscover(link);
|
||||
};
|
||||
|
||||
const openAlertRuleInManagement = async () => {
|
||||
const openAlertRuleInManagement = async (ruleName: string) => {
|
||||
await PageObjects.common.navigateToApp('management');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
|
@ -239,7 +219,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
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 PageObjects.header.waitUntilLoadingHasFinished();
|
||||
};
|
||||
|
@ -269,18 +249,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
es.transport.request({
|
||||
path: `/${OUTPUT_DATA_INDEX}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
await deleteDataViews([sourceDataViewId, outputDataViewId]);
|
||||
deleteIndexes([OUTPUT_DATA_INDEX, SOURCE_DATA_INDEX]);
|
||||
await deleteDataView(outputDataViewId);
|
||||
await deleteConnector(connectorId);
|
||||
const alertsToDelete = await getAlertsByName(RULE_NAME);
|
||||
const alertsToDelete = await getAlertsByName('test');
|
||||
await deleteAlerts(alertsToDelete.map((alertItem: { id: string }) => alertItem.id));
|
||||
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.discover.waitUntilSearchingHasFinished();
|
||||
await PageObjects.discover.selectIndexPattern(SOURCE_DATA_INDEX);
|
||||
|
@ -290,33 +267,35 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await defineSearchSourceAlert(RULE_NAME);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await openAlertRuleInManagement();
|
||||
|
||||
await openAlertRuleInManagement(RULE_NAME);
|
||||
await testSubjects.click('ruleDetails-viewInApp');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await retry.waitFor('navigate to discover', async () => {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
return currentUrl.includes(sourceDataViewId);
|
||||
const currentDataViewId = await PageObjects.discover.getCurrentDataViewId();
|
||||
return sourceDataViewId ? currentDataViewId === sourceDataViewId : true;
|
||||
});
|
||||
|
||||
expect(await dataGrid.getDocCount()).to.be(5);
|
||||
});
|
||||
|
||||
it('should open documents triggered the alert', async () => {
|
||||
await openOutputIndex();
|
||||
await navigateToResults();
|
||||
it('should navigate to alert results via link provided in notification', async () => {
|
||||
await openAlertResults(RULE_NAME, sourceDataViewId);
|
||||
|
||||
const { message, title } = await getLastToast();
|
||||
expect(await dataGrid.getDocCount()).to.be(5);
|
||||
expect(title).to.be.equal('Displayed documents may vary');
|
||||
expect(message).to.be.equal(
|
||||
'The displayed documents might differ from the documents that triggered the alert. Some documents might have been added or deleted.'
|
||||
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.`
|
||||
);
|
||||
|
||||
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 () => {
|
||||
await openAlertRuleInManagement();
|
||||
await openAlertRuleInManagement(RULE_NAME);
|
||||
|
||||
// change rule configuration
|
||||
await testSubjects.click('openEditRuleFlyoutButton');
|
||||
|
@ -328,21 +307,20 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.click('saveEditedRuleButton');
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
|
||||
await openOutputIndex();
|
||||
await navigateToResults();
|
||||
await openAlertResults(RULE_NAME, sourceDataViewId);
|
||||
|
||||
const { message, title } = await getLastToast();
|
||||
const queryString = await queryBar.getQueryString();
|
||||
const hasFilter = await filterBar.hasFilter('message.keyword', 'msg-1');
|
||||
|
||||
expect(queryString).to.be.equal('message:msg-1');
|
||||
expect(hasFilter).to.be.equal(true);
|
||||
|
||||
expect(await dataGrid.getDocCount()).to.be(1);
|
||||
expect(title).to.be.equal('Alert rule has changed');
|
||||
expect(message).to.be.equal(
|
||||
'The displayed documents might not match the documents that triggered the alert because the rule configuration changed.'
|
||||
expect(await toasts.getToastCount()).to.be.equal(1);
|
||||
const content = await toasts.getToastContent(1);
|
||||
expect(content).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);
|
||||
});
|
||||
|
||||
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('fieldFilterInput');
|
||||
|
||||
await PageObjects.common.sleep(15000);
|
||||
|
||||
const input = await find.activeElement();
|
||||
await input.type('message');
|
||||
|
||||
await testSubjects.click('addFieldFilterButton');
|
||||
|
||||
await openOutputIndex();
|
||||
await navigateToResults();
|
||||
await openAlertResults(RULE_NAME, sourceDataViewId);
|
||||
|
||||
await openOutputIndex();
|
||||
await navigateToResults();
|
||||
|
||||
const { message, title } = await getLastToast();
|
||||
expect(await toasts.getToastCount()).to.be(2);
|
||||
const firstContent = await toasts.getToastContent(1);
|
||||
expect(firstContent).to.equal(
|
||||
`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(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 () => {
|
||||
await openOutputIndex();
|
||||
const link = await getResultsLink();
|
||||
await navigateToDiscover(link);
|
||||
await PageObjects.discover.selectIndexPattern(OUTPUT_DATA_INDEX);
|
||||
|
||||
await es.transport.request({
|
||||
path: `/${SOURCE_DATA_INDEX}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
await browser.refresh();
|
||||
await deleteDataView(sourceDataViewId);
|
||||
|
||||
await navigateToDiscover(link);
|
||||
// rty to open alert results after index deletion
|
||||
await openAlertResults(RULE_NAME);
|
||||
|
||||
const title = await getErrorToastTitle();
|
||||
expect(title).to.be.equal(
|
||||
'No matching indices found: No indices match "search-source-alert"'
|
||||
expect(await toasts.getToastCount()).to.be(1);
|
||||
const firstContent = await toasts.getToastContent(1);
|
||||
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-*');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue