mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[ES|QL] Enable ESQL alerts from the Discover app (#165973)
## Summary
Enables the Alerts menu in Discover nav for the ES|QL mode and defaults
to ESQL alerts by carrying the query that the user has typed.
<img width="1621" alt="image"
src="5ffef9d1
-179a-464a-8941-b6bf18b4f30f">
### Checklist
- [ ] [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
This commit is contained in:
parent
dcce011f91
commit
87dc64e0bf
10 changed files with 170 additions and 24 deletions
|
@ -92,6 +92,10 @@ export const buildDataViewMock = ({
|
|||
return dataViewFields.find((field) => field.name === fieldName);
|
||||
};
|
||||
|
||||
dataViewFields.getByType = (type: string) => {
|
||||
return dataViewFields.filter((field) => field.type === type);
|
||||
};
|
||||
|
||||
dataViewFields.getAll = () => {
|
||||
return dataViewFields;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { buildDataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: '_index',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
displayName: 'message',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: false,
|
||||
},
|
||||
{
|
||||
name: 'extension',
|
||||
displayName: 'extension',
|
||||
type: 'string',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'bytes',
|
||||
displayName: 'bytes',
|
||||
type: 'number',
|
||||
scripted: false,
|
||||
filterable: true,
|
||||
aggregatable: true,
|
||||
},
|
||||
{
|
||||
name: 'scripted',
|
||||
displayName: 'scripted',
|
||||
type: 'number',
|
||||
scripted: true,
|
||||
filterable: false,
|
||||
},
|
||||
] as DataView['fields'];
|
||||
|
||||
export const dataViewWithNoTimefieldMock = buildDataViewMock({
|
||||
name: 'index-pattern-with-timefield',
|
||||
fields,
|
||||
});
|
|
@ -53,6 +53,7 @@ export const getTopNavLinks = ({
|
|||
services,
|
||||
stateContainer: state,
|
||||
adHocDataViews,
|
||||
isPlainRecord,
|
||||
});
|
||||
},
|
||||
testId: 'discoverAlertsButton',
|
||||
|
@ -232,7 +233,6 @@ export const getTopNavLinks = ({
|
|||
if (
|
||||
services.triggersActionsUi &&
|
||||
services.capabilities.management?.insightsAndAlerting?.triggersActions &&
|
||||
!isPlainRecord &&
|
||||
!defaultMenu?.alertsItem?.disabled
|
||||
) {
|
||||
entries.push({ data: alerts, order: defaultMenu?.alertsItem?.order ?? 400 });
|
||||
|
|
|
@ -13,10 +13,11 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
|||
import { AlertsPopover } from './open_alerts_popover';
|
||||
import { discoverServiceMock } from '../../../../__mocks__/services';
|
||||
import { dataViewWithTimefieldMock } from '../../../../__mocks__/data_view_with_timefield';
|
||||
import { dataViewWithNoTimefieldMock } from '../../../../__mocks__/data_view_no_timefield';
|
||||
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
|
||||
import { getDiscoverStateMock } from '../../../../__mocks__/discover_state.mock';
|
||||
|
||||
const mount = (dataView = dataViewMock) => {
|
||||
const mount = (dataView = dataViewMock, isPlainRecord = false) => {
|
||||
const stateContainer = getDiscoverStateMock({ isTimeBased: true });
|
||||
stateContainer.actions.setDataView(dataView);
|
||||
return mountWithIntl(
|
||||
|
@ -25,6 +26,7 @@ const mount = (dataView = dataViewMock) => {
|
|||
stateContainer={stateContainer}
|
||||
anchorElement={document.createElement('div')}
|
||||
adHocDataViews={[]}
|
||||
isPlainRecord={isPlainRecord}
|
||||
services={discoverServiceMock}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
|
@ -33,18 +35,42 @@ const mount = (dataView = dataViewMock) => {
|
|||
};
|
||||
|
||||
describe('OpenAlertsPopover', () => {
|
||||
it('should render with the create search threshold rule button disabled if the data view has no time field', () => {
|
||||
const component = mount();
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeTruthy();
|
||||
describe('Dataview mode', () => {
|
||||
it('should render with the create search threshold rule button disabled if the data view has no time field', () => {
|
||||
const component = mount();
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with the create search threshold rule button enabled if the data view has a time field', () => {
|
||||
const component = mount(dataViewWithTimefieldMock);
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render the manage rules and connectors link', () => {
|
||||
const component = mount();
|
||||
expect(findTestSubject(component, 'discoverManageAlertsButton').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render with the create search threshold rule button enabled if the data view has a time field', () => {
|
||||
const component = mount(dataViewWithTimefieldMock);
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeFalsy();
|
||||
});
|
||||
describe('ES|QL mode', () => {
|
||||
it('should render with the create search threshold rule button enabled if the data view has no timeFieldName but at least one time field', () => {
|
||||
const component = mount(dataViewMock, true);
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render the manage rules and connectors link', () => {
|
||||
const component = mount();
|
||||
expect(findTestSubject(component, 'discoverManageAlertsButton').exists()).toBeTruthy();
|
||||
it('should render with the create search threshold rule button enabled if the data view has a time field', () => {
|
||||
const component = mount(dataViewWithTimefieldMock, true);
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render with the create search threshold rule button disabled if the data view has no time fields at all', () => {
|
||||
const component = mount(dataViewWithNoTimefieldMock, true);
|
||||
expect(findTestSubject(component, 'discoverCreateAlertButton').prop('disabled')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the manage rules and connectors link', () => {
|
||||
const component = mount();
|
||||
expect(findTestSubject(component, 'discoverManageAlertsButton').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,6 +28,7 @@ interface AlertsPopoverProps {
|
|||
savedQueryId?: string;
|
||||
adHocDataViews: DataView[];
|
||||
services: DiscoverServices;
|
||||
isPlainRecord?: boolean;
|
||||
}
|
||||
|
||||
interface EsQueryAlertMetaData {
|
||||
|
@ -41,8 +42,13 @@ export function AlertsPopover({
|
|||
services,
|
||||
stateContainer,
|
||||
onClose: originalOnClose,
|
||||
isPlainRecord,
|
||||
}: AlertsPopoverProps) {
|
||||
const dataView = stateContainer.internalState.getState().dataView;
|
||||
const query = stateContainer.appState.getState().query;
|
||||
const dateFields = dataView?.fields.getByType('date');
|
||||
const timeField = dataView?.timeFieldName || dateFields?.[0]?.name;
|
||||
|
||||
const { triggersActionsUi } = services;
|
||||
const [alertFlyoutVisible, setAlertFlyoutVisibility] = useState(false);
|
||||
const onClose = useCallback(() => {
|
||||
|
@ -54,6 +60,13 @@ export function AlertsPopover({
|
|||
* Provides the default parameters used to initialize the new rule
|
||||
*/
|
||||
const getParams = useCallback(() => {
|
||||
if (isPlainRecord) {
|
||||
return {
|
||||
searchType: 'esqlQuery',
|
||||
esqlQuery: query,
|
||||
timeField,
|
||||
};
|
||||
}
|
||||
const savedQueryId = stateContainer.appState.getState().savedQuery;
|
||||
return {
|
||||
searchType: 'searchSource',
|
||||
|
@ -62,7 +75,7 @@ export function AlertsPopover({
|
|||
.searchSource.getSerializedFields(),
|
||||
savedQueryId,
|
||||
};
|
||||
}, [stateContainer]);
|
||||
}, [isPlainRecord, stateContainer.appState, stateContainer.savedSearchState, query, timeField]);
|
||||
|
||||
const discoverMetadata: EsQueryAlertMetaData = useMemo(
|
||||
() => ({
|
||||
|
@ -98,7 +111,14 @@ export function AlertsPopover({
|
|||
});
|
||||
}, [alertFlyoutVisible, triggersActionsUi, discoverMetadata, getParams, onClose, stateContainer]);
|
||||
|
||||
const hasTimeFieldName = Boolean(dataView?.timeFieldName);
|
||||
const hasTimeFieldName: boolean = useMemo(() => {
|
||||
if (!isPlainRecord) {
|
||||
return Boolean(dataView?.timeFieldName);
|
||||
} else {
|
||||
return Boolean(timeField);
|
||||
}
|
||||
}, [dataView?.timeFieldName, isPlainRecord, timeField]);
|
||||
|
||||
const panels = [
|
||||
{
|
||||
id: 'mainPanel',
|
||||
|
@ -165,11 +185,13 @@ export function openAlertsPopover({
|
|||
stateContainer,
|
||||
services,
|
||||
adHocDataViews,
|
||||
isPlainRecord,
|
||||
}: {
|
||||
anchorElement: HTMLElement;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
services: DiscoverServices;
|
||||
adHocDataViews: DataView[];
|
||||
isPlainRecord?: boolean;
|
||||
}) {
|
||||
if (isOpen) {
|
||||
closeAlertsPopover();
|
||||
|
@ -188,6 +210,7 @@ export function openAlertsPopover({
|
|||
stateContainer={stateContainer}
|
||||
adHocDataViews={adHocDataViews}
|
||||
services={services}
|
||||
isPlainRecord={isPlainRecord}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>
|
||||
|
|
|
@ -20,7 +20,7 @@ const isActualAlert = (queryParams: QueryParams): queryParams is NonNullableEntr
|
|||
};
|
||||
|
||||
export function ViewAlertRoute() {
|
||||
const { core, data, locator, toastNotifications } = useDiscoverServices();
|
||||
const { core, data, locator, toastNotifications, dataViews } = useDiscoverServices();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const history = useHistory();
|
||||
const { search } = useLocation();
|
||||
|
@ -46,7 +46,8 @@ export function ViewAlertRoute() {
|
|||
queryParams,
|
||||
toastNotifications,
|
||||
core,
|
||||
data
|
||||
data,
|
||||
dataViews
|
||||
);
|
||||
|
||||
const navigateWithDiscoverState = (state: DiscoverAppLocatorParams) => {
|
||||
|
@ -63,7 +64,17 @@ export function ViewAlertRoute() {
|
|||
.then(buildLocatorParams)
|
||||
.then(navigateWithDiscoverState)
|
||||
.catch(navigateToDiscoverRoot);
|
||||
}, [core, data, history, id, locator, openActualAlert, queryParams, toastNotifications]);
|
||||
}, [
|
||||
core,
|
||||
data,
|
||||
dataViews,
|
||||
history,
|
||||
id,
|
||||
locator,
|
||||
openActualAlert,
|
||||
queryParams,
|
||||
toastNotifications,
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { getIndexPatternFromESQLQuery, type AggregateQuery } from '@kbn/es-query';
|
||||
import { CoreStart, ToastsStart } from '@kbn/core/public';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import type { DataView, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { Rule } from '@kbn/alerting-plugin/common';
|
||||
import type { RuleTypeParams } from '@kbn/alerting-plugin/common';
|
||||
import { ISearchSource, SerializedSearchSourceFields, getTime } from '@kbn/data-plugin/common';
|
||||
|
@ -21,6 +22,8 @@ import { DiscoverAppLocatorParams } from '../../../common/locator';
|
|||
|
||||
export interface SearchThresholdAlertParams extends RuleTypeParams {
|
||||
searchConfiguration: SerializedSearchSourceFields;
|
||||
esqlQuery?: AggregateQuery;
|
||||
timeField?: string;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
|
@ -50,7 +53,8 @@ export const getAlertUtils = (
|
|||
queryParams: QueryParams,
|
||||
toastNotifications: ToastsStart,
|
||||
core: CoreStart,
|
||||
data: DataPublicPluginStart
|
||||
data: DataPublicPluginStart,
|
||||
dataViews: DataViewsPublicPluginStart
|
||||
) => {
|
||||
const showDataViewFetchError = (alertId: string) => {
|
||||
const errorTitle = i18n.translate('discover.viewAlert.dataViewErrorTitle', {
|
||||
|
@ -111,14 +115,31 @@ export const getAlertUtils = (
|
|||
}
|
||||
};
|
||||
|
||||
const buildLocatorParams = ({
|
||||
const buildLocatorParams = async ({
|
||||
alert,
|
||||
searchSource,
|
||||
}: {
|
||||
alert: Rule<SearchThresholdAlertParams>;
|
||||
searchSource: ISearchSource;
|
||||
}): DiscoverAppLocatorParams => {
|
||||
const dataView = searchSource.getField('index');
|
||||
}): Promise<DiscoverAppLocatorParams> => {
|
||||
let dataView = searchSource.getField('index');
|
||||
let query = searchSource.getField('query') || data.query.queryString.getDefaultQuery();
|
||||
|
||||
// Dataview and query for ES|QL alerts
|
||||
if (
|
||||
alert.params &&
|
||||
'esqlQuery' in alert.params &&
|
||||
alert.params.esqlQuery &&
|
||||
'esql' in alert.params.esqlQuery
|
||||
) {
|
||||
query = alert.params.esqlQuery;
|
||||
const indexPattern: string = getIndexPatternFromESQLQuery(alert.params.esqlQuery.esql);
|
||||
dataView = await dataViews.create({
|
||||
title: indexPattern,
|
||||
timeFieldName: alert.params.timeField,
|
||||
});
|
||||
}
|
||||
|
||||
const timeFieldName = dataView?.timeFieldName;
|
||||
// data view fetch error
|
||||
if (!dataView || !timeFieldName) {
|
||||
|
@ -131,7 +152,7 @@ export const getAlertUtils = (
|
|||
: buildTimeRangeFilter(dataView, alert, timeFieldName);
|
||||
|
||||
return {
|
||||
query: searchSource.getField('query') || data.query.queryString.getDefaultQuery(),
|
||||
query,
|
||||
dataViewSpec: dataView.toSpec(false),
|
||||
timeRange,
|
||||
filters: searchSource.getField('filter') as Filter[],
|
||||
|
|
|
@ -75,7 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
// when Lens suggests a table, we render an ESQL based histogram
|
||||
expect(await testSubjects.exists('unifiedHistogramChart')).to.be(true);
|
||||
expect(await testSubjects.exists('unifiedHistogramQueryHits')).to.be(true);
|
||||
expect(await testSubjects.exists('discoverAlertsButton')).to.be(false);
|
||||
expect(await testSubjects.exists('discoverAlertsButton')).to.be(true);
|
||||
expect(await testSubjects.exists('shareTopNavButton')).to.be(true);
|
||||
expect(await testSubjects.exists('dataGridColumnSortingButton')).to.be(false);
|
||||
expect(await testSubjects.exists('docTableExpandToggleColumn')).to.be(true);
|
||||
|
|
|
@ -25,6 +25,7 @@ jest.mock('@kbn/text-based-editor', () => ({
|
|||
fetchFieldsFromESQL: jest.fn(),
|
||||
}));
|
||||
const { fetchFieldsFromESQL } = jest.requireMock('@kbn/text-based-editor');
|
||||
const { getFields } = jest.requireMock('@kbn/triggers-actions-ui-plugin/public');
|
||||
|
||||
const AppWrapper: React.FC<{ children: React.ReactElement }> = React.memo(({ children }) => (
|
||||
<I18nProvider>{children}</I18nProvider>
|
||||
|
@ -133,6 +134,7 @@ describe('EsqlQueryRuleTypeExpression', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
getFields.mockResolvedValue([]);
|
||||
const result = render(
|
||||
<EsqlQueryExpression
|
||||
unifiedSearch={unifiedSearchMock}
|
||||
|
|
|
@ -76,6 +76,11 @@ export const EsqlQueryExpression: React.FC<
|
|||
const setDefaultExpressionValues = async () => {
|
||||
setRuleProperty('params', currentRuleParams);
|
||||
setQuery(esqlQuery ?? { esql: '' });
|
||||
if (esqlQuery && 'esql' in esqlQuery) {
|
||||
if (esqlQuery.esql) {
|
||||
refreshTimeFields(esqlQuery);
|
||||
}
|
||||
}
|
||||
if (timeField) {
|
||||
setTimeFieldOptions([firstFieldOption, { text: timeField, value: timeField }]);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue