[Alerting] Improve creation and editing of "Elasticsearch query" rule in Management (#134763)

* [Alerting][Discover] Add query form type selection for ES query

* [Alerting] Add labels

* [Alerting] Unify common rule expressiosn

* [Alerting] Fix code style

* [Alerting] More refactoring

* [Alerting] Show different UI based on user choice

* [Alerting] Improve validation and reset rule params when view changes

* Resolve conflicts

* [Alerting] Fix button color

* [Alerting] Revert tmp changes

* [Alerting] Fix query input

* [Alerting] Fix thresholdComparator and timeWindowUnit changes

* [Alerting] Unify UI for different query form types

* [Alerting] Clean up translations

* [Alerting] Fix for tests

* [Alerting] Update help tooltips

* [Alerting] Preselect a default data view

* [Alerting] Add validation tests

* [Alerting] Add more tests

* [Alerting] Add the smaller title

* [Alerting] Fix localization bug

* [Alerting] Fix rules editing view

* [Alerting] Fix layout for mobile

* [Alerting] Address PR comments

* [Discover] unify searchType and formType

* [Alerting] Allow to create new data views from Data View Expression

* [Alerting] Fix lint issue

* [Discover] fix management and discover views

* [Discover] remove redundant prop

* [Alerting] Add validation message when query type is not selected yet

* [Alerting] Update validations

* [Alerting] Update tests

* [Alerting] Update tests

* [Alerting] Update tests

* [Alerting] Prioritize index errors

* [Alerting] Fix size validation

* [Alerting] Fix timeWindowSize validation

* [Alerting] Address CI issues

* Address plugins concurrency effect on sample data server API

* [Alerting] Remove deprecated strings

* [Alerting] Update copy and spacing

* [Alerting] Cleanup translations

* [Alerting] Unify labels key

* [Alerting] Update copy

* [Alerting] Update copy key

* [Alerting] Bring back the original label

* [Alerting] Update after the merge

* [Alerting] Update validation message

* [Alerting] Update styles for Create a data view button

* [Alerting] Add message about missing privilieges

* [Alerting] Reduce padding in data view selector

* [Alerting] Update copy

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Dzmitry Tamashevich <diaamnj@mail.ru>
This commit is contained in:
Julia Rechkunova 2022-07-07 11:19:06 +02:00 committed by GitHub
parent b809237f84
commit b46763fc2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 991 additions and 373 deletions

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { sortBy } from 'lodash';
import type { IRouter, Logger, RequestHandlerContext } from '@kbn/core/server';
import type { AppLinkData, SampleDatasetSchema } from '../lib/sample_dataset_registry_types';
import { createIndexName } from '../lib/create_index_name';
@ -55,7 +56,7 @@ export const createListRoute = (
previewImagePath: sampleDataset.previewImagePath,
darkPreviewImagePath: sampleDataset.darkPreviewImagePath,
overviewDashboard: findObjectId('dashboard', sampleDataset.overviewDashboard),
appLinks,
appLinks: sortBy(appLinks, 'order'),
defaultIndex: findObjectId('index-pattern', sampleDataset.defaultIndex),
dataIndices: sampleDataset.dataIndices.map(({ id }) => ({ id })),
...sampleDataStatus,

View file

@ -21,6 +21,10 @@ export default function ({ getService }: FtrProviderContext) {
const FLIGHTS_CANVAS_APPLINK_PATH =
'/app/canvas#/workpad/workpad-a474e74b-aedc-47c3-894a-db77e62c41e0'; // includes default ID of the flights canvas applink path
const includesPathInAppLinks = (appLinks: Array<{ path: string }>, path: string): boolean => {
return appLinks.some((item) => item.path === path);
};
describe('sample data apis', () => {
before(async () => {
await esArchiver.emptyKibanaIndex();
@ -41,7 +45,9 @@ export default function ({ getService }: FtrProviderContext) {
// Check and make sure the sample dataset reflects the default object IDs, because no sample data objects exist.
// Instead of checking each object ID, we check the dashboard and canvas app link as representatives.
expect(flightsData.overviewDashboard).to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).to.be(FLIGHTS_CANVAS_APPLINK_PATH);
expect(includesPathInAppLinks(flightsData.appLinks, FLIGHTS_CANVAS_APPLINK_PATH)).to.be(
true
);
});
});
@ -109,11 +115,15 @@ export default function ({ getService }: FtrProviderContext) {
// Instead of checking each object ID, we check the dashboard and canvas app link as representatives.
if (space === 'default') {
expect(flightsData.overviewDashboard).to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).to.be(FLIGHTS_CANVAS_APPLINK_PATH);
expect(includesPathInAppLinks(flightsData.appLinks, FLIGHTS_CANVAS_APPLINK_PATH)).to.be(
true
);
} else {
// the sample data objects installed in the 'other' space had their IDs regenerated upon import
expect(flightsData.overviewDashboard).not.to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).not.to.be(FLIGHTS_CANVAS_APPLINK_PATH);
expect(includesPathInAppLinks(flightsData.appLinks, FLIGHTS_CANVAS_APPLINK_PATH)).to.be(
false
);
}
});
});
@ -145,7 +155,9 @@ export default function ({ getService }: FtrProviderContext) {
// Check and make sure the sample dataset reflects the default object IDs, because no sample data objects exist.
// Instead of checking each object ID, we check the dashboard and canvas app link as representatives.
expect(flightsData.overviewDashboard).to.be(FLIGHTS_OVERVIEW_DASHBOARD_ID);
expect(flightsData.appLinks[0].path).to.be(FLIGHTS_CANVAS_APPLINK_PATH);
expect(includesPathInAppLinks(flightsData.appLinks, FLIGHTS_CANVAS_APPLINK_PATH)).to.be(
true
);
});
});
}

View file

@ -7,15 +7,15 @@
import React from 'react';
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
import { DataViewSelectPopover } from './data_view_select_popover';
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 { act } from 'react-dom/test-utils';
const props = {
const props: DataViewSelectPopoverProps = {
onSelectDataView: () => {},
initialDataViewTitle: 'kibana_sample_data_logs',
initialDataViewId: 'mock-data-logs-id',
dataViewName: 'kibana_sample_data_logs',
dataViewId: 'mock-data-logs-id',
};
const dataViewOptions = [

View file

@ -5,49 +5,82 @@
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiButtonEmpty,
EuiButtonIcon,
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';
interface DataViewSelectPopoverProps {
export interface DataViewSelectPopoverProps {
onSelectDataView: (newDataViewId: string) => void;
initialDataViewTitle: string;
initialDataViewId?: string;
dataViewName?: string;
dataViewId?: string;
}
export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopoverProps> = ({
onSelectDataView,
initialDataViewTitle,
initialDataViewId,
dataViewName,
dataViewId,
}) => {
const { data } = useTriggersAndActionsUiDeps();
const { data, dataViewEditor } = useTriggersAndActionsUiDeps();
const [dataViewItems, setDataViewsItems] = useState<DataViewListItem[]>();
const [dataViewPopoverOpen, setDataViewPopoverOpen] = useState(false);
const [selectedDataViewId, setSelectedDataViewId] = useState(initialDataViewId);
const [selectedTitle, setSelectedTitle] = useState<string>(initialDataViewTitle);
const closeDataViewEditor = useRef<() => void | undefined>();
useEffect(() => {
const initDataViews = async () => {
const fetchedDataViewItems = await data.dataViews.getIdsWithTitle();
setDataViewsItems(fetchedDataViewItems);
};
initDataViews();
}, [data.dataViews]);
const loadDataViews = useCallback(async () => {
const fetchedDataViewItems = await data.dataViews.getIdsWithTitle();
setDataViewsItems(fetchedDataViewItems);
}, [setDataViewsItems, data.dataViews]);
const closeDataViewPopover = useCallback(() => setDataViewPopoverOpen(false), []);
const createDataView = useMemo(
() =>
dataViewEditor?.userPermissions.editDataView()
? () => {
closeDataViewEditor.current = dataViewEditor.openEditor({
onSave: async (createdDataView) => {
if (createdDataView.id) {
await onSelectDataView(createdDataView.id);
await loadDataViews();
}
},
});
}
: undefined,
[dataViewEditor, onSelectDataView, loadDataViews]
);
useEffect(() => {
return () => {
// Make sure to close the editor when unmounting
if (closeDataViewEditor.current) {
closeDataViewEditor.current();
}
};
}, []);
useEffect(() => {
loadDataViews();
}, [loadDataViews]);
const createDataViewButtonPadding = useEuiPaddingCSS('left');
if (!dataViewItems) {
return null;
}
@ -62,12 +95,17 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
description={i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewLabel', {
defaultMessage: 'data view',
})}
value={selectedTitle}
value={
dataViewName ??
i18n.translate('xpack.stackAlerts.components.ui.alertParams.dataViewPlaceholder', {
defaultMessage: 'Select a data view',
})
}
isActive={dataViewPopoverOpen}
onClick={() => {
setDataViewPopoverOpen(true);
}}
isInvalid={!selectedTitle}
isInvalid={!dataViewId}
/>
}
isOpen={dataViewPopoverOpen}
@ -98,22 +136,53 @@ export const DataViewSelectPopover: React.FunctionComponent<DataViewSelectPopove
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
<EuiFormRow id="indexSelectSearchBox" fullWidth>
<EuiFormRow
id="indexSelectSearchBox"
fullWidth
css={`
.euiPanel {
padding: 0;
}
`}
>
<DataViewsList
dataViewsList={dataViewItems}
onChangeDataView={(newId) => {
setSelectedDataViewId(newId);
const newTitle = dataViewItems?.find(({ id }) => id === newId)?.title;
if (newTitle) {
setSelectedTitle(newTitle);
}
onSelectDataView(newId);
closeDataViewPopover();
}}
currentDataViewId={selectedDataViewId}
currentDataViewId={dataViewId}
/>
</EuiFormRow>
{createDataView ? (
<EuiPopoverFooter paddingSize="none">
<EuiButtonEmpty
css={createDataViewButtonPadding.s}
iconType="plusInCircleFilled"
data-test-subj="chooseDataViewPopover.createDataViewButton"
onClick={() => {
closeDataViewPopover();
createDataView();
}}
>
{i18n.translate(
'xpack.stackAlerts.components.ui.alertParams.dataViewPopover.createDataViewButton',
{
defaultMessage: 'Create a data view',
}
)}
</EuiButtonEmpty>
</EuiPopoverFooter>
) : (
<EuiPopoverFooter>
<EuiText color="subdued" size="xs">
<FormattedMessage
id="xpack.stackAlerts.components.ui.alertParams.dataViewPopover.createDataViewButton.noPermissionDescription"
defaultMessage="You need additional privileges to create data views. Contact your administrator."
/>
</EuiText>
</EuiPopoverFooter>
)}
</div>
</EuiPopover>
);

View file

@ -104,7 +104,13 @@ export const IndexSelectPopover: React.FunctionComponent<Props> = ({
description={i18n.translate('xpack.stackAlerts.components.ui.alertParams.indexLabel', {
defaultMessage: 'index',
})}
value={index && index.length > 0 ? renderIndices(index) : firstFieldOption.text}
value={
index && index.length > 0
? renderIndices(index)
: i18n.translate('xpack.stackAlerts.components.ui.alertParams.indexPlaceholder', {
defaultMessage: 'Select an index',
})
}
isActive={indexPopoverOpen}
onClick={() => {
setIndexPopoverOpen(true);

View file

@ -31,6 +31,7 @@ export const EXPRESSION_ERRORS = {
thresholdComparator: new Array<string>(),
timeWindowSize: new Array<string>(),
searchConfiguration: new Array<string>(),
searchType: new Array<string>(),
};
export const EXPRESSION_ERROR_KEYS = Object.keys(EXPRESSION_ERRORS) as ErrorKey[];

View file

@ -13,34 +13,20 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { XJsonMode } from '@kbn/ace';
import 'brace/theme/github';
import {
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiFormRow,
EuiTitle,
EuiLink,
EuiIconTip,
} from '@elastic/eui';
import { EuiFormRow, EuiLink, EuiSpacer, EuiTitle } from '@elastic/eui';
import { DocLinksStart, HttpSetup } from '@kbn/core/public';
import { XJson, EuiCodeEditor } from '@kbn/es-ui-shared-plugin/public';
import { EuiCodeEditor, XJson } from '@kbn/es-ui-shared-plugin/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
getFields,
ValueExpression,
RuleTypeParamsExpressionProps,
ForLastExpression,
ThresholdExpression,
} from '@kbn/triggers-actions-ui-plugin/public';
import { getFields, RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { parseDuration } from '@kbn/alerting-plugin/common';
import { validateExpression } from '../validation';
import { hasExpressionValidationErrors } from '../validation';
import { buildSortedEventsQuery } from '../../../../common/build_sorted_events_query';
import { EsQueryAlertParams, SearchType } from '../types';
import { IndexSelectPopover } from '../../components/index_select_popover';
import { DEFAULT_VALUES } from '../constants';
import { TestQueryRow } from './test_query_row';
import { totalHitsToNumber } from './use_test_query';
import { RuleCommonExpressions } from '../rule_common_expressions';
import { totalHitsToNumber } from '../test_query_row';
const { useXJsonMode } = XJson;
const xJsonMode = new XJsonMode();
@ -50,13 +36,9 @@ interface KibanaDeps {
docLinks: DocLinksStart;
}
export const EsQueryExpression = ({
ruleParams,
setRuleParams,
setRuleProperty,
errors,
data,
}: RuleTypeParamsExpressionProps<EsQueryAlertParams<SearchType.esQuery>>) => {
export const EsQueryExpression: React.FC<
RuleTypeParamsExpressionProps<EsQueryAlertParams<SearchType.esQuery>>
> = ({ ruleParams, setRuleParams, setRuleProperty, errors, data }) => {
const {
index,
timeField,
@ -68,7 +50,7 @@ export const EsQueryExpression = ({
timeWindowUnit,
} = ruleParams;
const [currentAlertParams, setCurrentAlertParams] = useState<
const [currentRuleParams, setCurrentRuleParams] = useState<
EsQueryAlertParams<SearchType.esQuery>
>({
...ruleParams,
@ -78,12 +60,12 @@ export const EsQueryExpression = ({
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
esQuery: esQuery ?? DEFAULT_VALUES.QUERY,
searchType: 'esQuery',
searchType: SearchType.esQuery,
});
const setParam = useCallback(
(paramField: string, paramValue: unknown) => {
setCurrentAlertParams((currentParams) => ({
setCurrentRuleParams((currentParams) => ({
...currentParams,
[paramField]: paramValue,
}));
@ -106,7 +88,7 @@ export const EsQueryExpression = ({
const { convertToJson, setXJson, xJson } = useXJsonMode(DEFAULT_VALUES.QUERY);
const setDefaultExpressionValues = async () => {
setRuleProperty('params', currentAlertParams);
setRuleProperty('params', currentRuleParams);
setXJson(esQuery ?? DEFAULT_VALUES.QUERY);
if (index && index.length > 0) {
@ -127,11 +109,8 @@ export const EsQueryExpression = ({
};
const hasValidationErrors = useCallback(() => {
const { errors: validationErrors } = validateExpression(currentAlertParams);
return Object.keys(validationErrors).some(
(key) => validationErrors[key] && validationErrors[key].length
);
}, [currentAlertParams]);
return hasExpressionValidationErrors(currentRuleParams);
}, [currentRuleParams]);
const onTestQuery = useCallback(async () => {
const window = `${timeWindowSize}${timeWindowUnit}`;
@ -165,12 +144,14 @@ export const EsQueryExpression = ({
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.selectIndex"
defaultMessage="Select an index and size"
id="xpack.stackAlerts.esQuery.ui.selectIndexPrompt"
defaultMessage="Select an index and time field"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<IndexSelectPopover
index={index}
data-test-subj="indexSelectPopover"
@ -199,25 +180,14 @@ export const EsQueryExpression = ({
}}
onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)}
/>
<ValueExpression
description={i18n.translate('xpack.stackAlerts.esQuery.ui.sizeExpression', {
defaultMessage: 'Size',
})}
data-test-subj="sizeValueExpression"
value={size}
errors={errors.size}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedValue={(updatedValue) => {
setParam('size', updatedValue);
}}
/>
<EuiSpacer />
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.queryPrompt"
defaultMessage="Define the Elasticsearch query"
id="xpack.stackAlerts.esQuery.ui.defineQueryPrompt"
defaultMessage="Define your query using Query DSL"
/>
</h5>
</EuiTitle>
@ -225,12 +195,6 @@ export const EsQueryExpression = ({
<EuiFormRow
id="queryEditor"
fullWidth
label={
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.queryPrompt.label"
defaultMessage="Elasticsearch query"
/>
}
isInvalid={errors.esQuery.length > 0}
error={errors.esQuery}
helpText={
@ -258,62 +222,33 @@ export const EsQueryExpression = ({
}}
/>
</EuiFormRow>
<TestQueryRow fetch={onTestQuery} hasValidationErrors={hasValidationErrors()} />
<EuiSpacer />
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="none">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.conditionPrompt"
defaultMessage="When number of matches"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
position="right"
color="subdued"
type="questionInCircle"
iconProps={{
className: 'eui-alignTop',
}}
content={i18n.translate('xpack.stackAlerts.esQuery.ui.conditionPrompt.toolTip', {
defaultMessage: 'The time window defined below applies only to the first rule check.',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<ThresholdExpression
data-test-subj="thresholdExpression"
thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR}
<RuleCommonExpressions
threshold={threshold ?? DEFAULT_VALUES.THRESHOLD}
errors={errors}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedThreshold={(selectedThresholds) =>
setParam('threshold', selectedThresholds)
}
onChangeSelectedThresholdComparator={(selectedThresholdComparator) =>
setParam('thresholdComparator', selectedThresholdComparator)
}
/>
<ForLastExpression
data-test-subj="forLastExpression"
popupPosition={'upLeft'}
thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR}
timeWindowSize={timeWindowSize}
timeWindowUnit={timeWindowUnit}
display="fullWidth"
errors={errors}
size={size}
onChangeThreshold={(selectedThresholds) => setParam('threshold', selectedThresholds)}
onChangeThresholdComparator={(selectedThresholdComparator) =>
setParam('thresholdComparator', selectedThresholdComparator)
}
onChangeWindowSize={(selectedWindowSize: number | undefined) =>
setParam('timeWindowSize', selectedWindowSize)
}
onChangeWindowUnit={(selectedWindowUnit: string) =>
setParam('timeWindowUnit', selectedWindowUnit)
}
onChangeSizeValue={(updatedValue) => {
setParam('size', updatedValue);
}}
errors={errors}
hasValidationErrors={hasValidationErrors()}
onTestFetch={onTestQuery}
/>
<EuiSpacer />
</Fragment>
);

View file

@ -0,0 +1,241 @@
/*
* 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 { mountWithIntl } from '@kbn/test-jest-helpers';
import 'brace';
import React, { useState } from 'react';
import { docLinksServiceMock } from '@kbn/core/public/mocks';
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 { CommonAlertParams, EsQueryAlertParams, SearchType } from '../types';
import { EsQueryAlertTypeExpression } from './expression';
import { chartPluginMock } from '@kbn/charts-plugin/public/mocks';
import { Subject } from 'rxjs';
import { ISearchSource } from '@kbn/data-plugin/common';
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 { ReactWrapper } from 'enzyme';
const defaultEsQueryRuleParams: EsQueryAlertParams<SearchType.esQuery> = {
size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 15,
timeWindowUnit: 's',
index: ['test-index'],
timeField: '@timestamp',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
searchType: SearchType.esQuery,
};
const defaultSearchSourceRuleParams: EsQueryAlertParams<SearchType.searchSource> = {
size: 100,
thresholdComparator: '>',
threshold: [0],
timeWindowSize: 15,
timeWindowUnit: 's',
index: ['test-index'],
timeField: '@timestamp',
searchType: SearchType.searchSource,
searchConfiguration: {},
};
const dataViewPluginMock = dataViewPluginMocks.createStartContract();
const chartsStartMock = chartPluginMock.createStartContract();
const unifiedSearchMock = unifiedSearchPluginMock.createStartContract();
const httpMock = httpServiceMock.createStartContract();
const docLinksMock = docLinksServiceMock.createStartContract();
export const uiSettingsMock = {
get: jest.fn(),
} as unknown as IUiSettingsClient;
const mockSearchResult = new Subject();
const searchSourceFieldsMock = {
query: {
query: '',
language: 'kuery',
},
filter: [],
index: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
title: 'kibana_sample_data_logs',
fields: [],
},
};
const searchSourceMock = {
id: 'data_source6',
fields: searchSourceFieldsMock,
getField: (name: string) => {
return (searchSourceFieldsMock as Record<string, object>)[name] || '';
},
setField: jest.fn(),
createCopy: jest.fn(() => {
return searchSourceMock;
}),
setParent: jest.fn(() => {
return searchSourceMock;
}),
fetch$: jest.fn(() => {
return mockSearchResult;
}),
} as unknown as ISearchSource;
const savedQueryMock = {
id: 'test-id',
attributes: {
title: 'test-filter-set',
description: '',
query: {
query: 'category.keyword : "Men\'s Shoes" ',
language: 'kuery',
},
filters: [],
},
};
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));
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
Promise.resolve(savedQueryMock)
);
dataMock.query.savedQueries.findSavedQueries = jest.fn(() =>
Promise.resolve({ total: 0, queries: [] })
);
(httpMock.post as jest.Mock).mockImplementation(() => Promise.resolve({ fields: [] }));
const Wrapper: React.FC<{
ruleParams: EsQueryAlertParams<SearchType.searchSource> | EsQueryAlertParams<SearchType.esQuery>;
}> = ({ ruleParams }) => {
const [currentRuleParams, setCurrentRuleParams] = useState<CommonAlertParams>(ruleParams);
const errors = {
index: [],
esQuery: [],
size: [],
timeField: [],
timeWindowSize: [],
searchConfiguration: [],
searchType: [],
};
return (
<EsQueryAlertTypeExpression
ruleInterval="1m"
ruleThrottle="1m"
alertNotifyWhen="onThrottleInterval"
ruleParams={currentRuleParams}
setRuleParams={(name, value) => {
setCurrentRuleParams((params) => ({ ...params, [name]: value }));
}}
setRuleProperty={(name, params) => {
if (name === 'params') {
setCurrentRuleParams(params as CommonAlertParams);
}
}}
errors={errors}
unifiedSearch={unifiedSearchMock}
data={dataMock}
dataViews={dataViewPluginMock}
defaultActionGroupId=""
actionGroups={[]}
charts={chartsStartMock}
/>
);
};
const setup = (
ruleParams: EsQueryAlertParams<SearchType.searchSource> | EsQueryAlertParams<SearchType.esQuery>
) => {
return mountWithIntl(
<KibanaContextProvider
services={{
data: dataMock,
uiSettings: uiSettingsMock,
docLinks: docLinksMock,
http: httpMock,
}}
>
<Wrapper ruleParams={ruleParams} />
</KibanaContextProvider>
);
};
describe('EsQueryAlertTypeExpression', () => {
test('should render options by default', async () => {
const wrapper = setup({} as EsQueryAlertParams<SearchType.esQuery>);
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy();
expect(findTestSubject(wrapper, 'queryFormType_searchSource').exists()).toBeTruthy();
expect(findTestSubject(wrapper, 'queryFormType_esQuery').exists()).toBeTruthy();
expect(findTestSubject(wrapper, 'queryFormTypeChooserCancel').exists()).toBeFalsy();
});
test('should switch to QueryDSL form type on selection and return back on cancel', async () => {
let wrapper = setup({} as EsQueryAlertParams<SearchType.esQuery>);
await act(async () => {
findTestSubject(wrapper, 'queryFormType_esQuery').simulate('click');
});
wrapper = await wrapper.update();
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeFalsy();
expect(findTestSubject(wrapper, 'queryJsonEditor').exists()).toBeTruthy();
expect(findTestSubject(wrapper, 'selectIndexExpression').exists()).toBeTruthy();
await act(async () => {
findTestSubject(wrapper, 'queryFormTypeChooserCancel').simulate('click');
});
wrapper = await wrapper.update();
expect(findTestSubject(wrapper, 'selectIndexExpression').exists()).toBeFalsy();
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy();
});
test('should switch to KQL or Lucene form type on selection and return back on cancel', async () => {
let wrapper = setup({} as EsQueryAlertParams<SearchType.searchSource>);
await act(async () => {
findTestSubject(wrapper, 'queryFormType_searchSource').simulate('click');
});
wrapper = await wrapper.update();
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeFalsy();
expect(findTestSubject(wrapper, 'selectDataViewExpression').exists()).toBeTruthy();
await act(async () => {
findTestSubject(wrapper, 'queryFormTypeChooserCancel').simulate('click');
});
wrapper = await wrapper.update();
expect(findTestSubject(wrapper, 'selectDataViewExpression').exists()).toBeFalsy();
expect(findTestSubject(wrapper, 'queryFormTypeChooserTitle').exists()).toBeTruthy();
});
test('should render QueryDSL view without the form type chooser if some rule params were passed', async () => {
let wrapper: ReactWrapper;
await act(async () => {
wrapper = setup(defaultEsQueryRuleParams);
wrapper = await wrapper.update();
});
expect(findTestSubject(wrapper!, 'queryFormTypeChooserTitle').exists()).toBeFalsy();
expect(findTestSubject(wrapper!, 'queryFormTypeChooserCancel').exists()).toBeFalsy();
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 () => {
let wrapper: ReactWrapper;
await act(async () => {
wrapper = setup(defaultSearchSourceRuleParams);
wrapper = await wrapper.update();
});
wrapper = await wrapper!.update();
expect(findTestSubject(wrapper!, 'queryFormTypeChooserTitle').exists()).toBeFalsy();
expect(findTestSubject(wrapper!, 'queryFormTypeChooserCancel').exists()).toBeFalsy();
expect(findTestSubject(wrapper!, 'selectDataViewExpression').exists()).toBeTruthy();
});
});

View file

@ -5,17 +5,16 @@
* 2.0.
*/
import React, { memo, PropsWithChildren } from 'react';
import { i18n } from '@kbn/i18n';
import React, { memo, PropsWithChildren, useCallback, useRef } from 'react';
import deepEqual from 'fast-deep-equal';
import 'brace/theme/github';
import { EuiSpacer, EuiCallOut } from '@elastic/eui';
import { EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { ErrorKey, EsQueryAlertParams } from '../types';
import { EsQueryAlertParams, SearchType } from '../types';
import { SearchSourceExpression, SearchSourceExpressionProps } from './search_source_expression';
import { EsQueryExpression } from './es_query_expression';
import { QueryFormTypeChooser } from './query_form_type_chooser';
import { isSearchSourceAlert } from '../util';
import { EXPRESSION_ERROR_KEYS } from '../constants';
@ -36,38 +35,69 @@ const SearchSourceExpressionMemoized = memo<SearchSourceExpressionProps>(
export const EsQueryAlertTypeExpression: React.FunctionComponent<
RuleTypeParamsExpressionProps<EsQueryAlertParams>
> = (props) => {
const { ruleParams, errors } = props;
const { ruleParams, errors, setRuleProperty, setRuleParams } = props;
const isSearchSource = isSearchSourceAlert(ruleParams);
const isManagementPage = useRef(!Object.keys(ruleParams).length).current;
const hasExpressionErrors = Object.keys(errors).some((errorKey) => {
return (
EXPRESSION_ERROR_KEYS.includes(errorKey as ErrorKey) &&
errors[errorKey].length >= 1 &&
ruleParams[errorKey] !== undefined
);
});
const formTypeSelected = useCallback(
(searchType: SearchType | null) => {
if (!searchType) {
// @ts-expect-error Reset rule params regardless of their type
setRuleProperty('params', {});
return;
}
setRuleParams('searchType', searchType);
},
[setRuleParams, setRuleProperty]
);
const expressionErrorMessage = i18n.translate(
const expressionGenericErrorMessage = i18n.translate(
'xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage',
{
defaultMessage: 'Expression contains errors.',
}
);
const errorParam = EXPRESSION_ERROR_KEYS.find((errorKey) => {
return errors[errorKey]?.length >= 1 && ruleParams[errorKey] !== undefined;
});
const expressionError = !!errorParam && (
<>
<EuiCallOut
color="danger"
size="s"
title={
['index', 'searchType'].includes(errorParam)
? errors[errorParam]
: expressionGenericErrorMessage
}
/>
<EuiSpacer />
</>
);
return (
<>
{hasExpressionErrors && (
<>
<EuiCallOut color="danger" size="s" title={expressionErrorMessage} />
<EuiSpacer />
</>
{expressionError}
{/* Showing the selected type */}
{isManagementPage && (
<QueryFormTypeChooser
searchType={ruleParams.searchType as SearchType}
onFormTypeSelect={formTypeSelected}
/>
)}
{isSearchSource ? (
{ruleParams.searchType && isSearchSource && (
<SearchSourceExpressionMemoized {...props} ruleParams={ruleParams} />
) : (
)}
{ruleParams.searchType && !isSearchSource && (
<EsQueryExpression {...props} ruleParams={ruleParams} />
)}
<EuiHorizontalRule />
</>
);
};

View file

@ -0,0 +1,131 @@
/*
* 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 React from 'react';
import {
EuiButtonIcon,
EuiFlexGroup,
EuiFlexItem,
EuiListGroup,
EuiListGroupItem,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { SearchType } from '../types';
const FORM_TYPE_ITEMS: Array<{ formType: SearchType; label: string; description: string }> = [
{
formType: SearchType.searchSource,
label: i18n.translate(
'xpack.stackAlerts.esQuery.ui.selectQueryFormType.kqlOrLuceneFormTypeLabel',
{
defaultMessage: 'KQL or Lucene',
}
),
description: i18n.translate(
'xpack.stackAlerts.esQuery.ui.selectQueryFormType.kqlOrLuceneFormTypeDescription',
{
defaultMessage: 'Use KQL or Lucene to define a text-based query.',
}
),
},
{
formType: SearchType.esQuery,
label: i18n.translate(
'xpack.stackAlerts.esQuery.ui.selectQueryFormType.queryDslFormTypeLabel',
{
defaultMessage: 'Query DSL',
}
),
description: i18n.translate(
'xpack.stackAlerts.esQuery.ui.selectQueryFormType.queryDslFormTypeDescription',
{
defaultMessage: 'Use the Elasticsearch Query DSL to define a query.',
}
),
},
];
export interface QueryFormTypeProps {
searchType: SearchType | null;
onFormTypeSelect: (formType: SearchType | null) => void;
}
export const QueryFormTypeChooser: React.FC<QueryFormTypeProps> = ({
searchType,
onFormTypeSelect,
}) => {
if (searchType) {
const activeFormTypeItem = FORM_TYPE_ITEMS.find((item) => item.formType === searchType);
return (
<>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem>
<EuiTitle size="xs" data-test-subj="selectedRuleFormTypeTitle">
<h5>{activeFormTypeItem?.label}</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
color="danger"
data-test-subj="queryFormTypeChooserCancel"
aria-label={i18n.translate(
'xpack.stackAlerts.esQuery.ui.selectQueryFormType.cancelSelectionAriaLabel',
{
defaultMessage: 'Cancel selection',
}
)}
onClick={() => onFormTypeSelect(null)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiText color="subdued" size="s" data-test-subj="selectedRuleFormTypeDescription">
{activeFormTypeItem?.description}
</EuiText>
<EuiSpacer />
</>
);
}
return (
<>
<EuiTitle size="xs">
<h5 data-test-subj="queryFormTypeChooserTitle">
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.selectQueryFormTypeLabel"
defaultMessage="Select a query type"
/>
</h5>
</EuiTitle>
<EuiListGroup flush gutterSize="m" size="l" maxWidth={false}>
{FORM_TYPE_ITEMS.map((item) => (
<EuiListGroupItem
wrapText
key={`form-type-${item.formType}`}
data-test-subj={`queryFormType_${item.formType}`}
color="primary"
label={
<span>
<strong>{item.label}</strong>
<EuiText color="subdued" size="s">
<p>{item.description}</p>
</EuiText>
</span>
}
onClick={() => onFormTypeSelect(item.formType)}
/>
))}
</EuiListGroup>
<EuiSpacer />
</>
);
};

View file

@ -47,7 +47,13 @@ const defaultSearchSourceExpressionParams: EsQueryAlertParams<SearchType.searchS
index: ['test-index'],
timeField: '@timestamp',
searchType: SearchType.searchSource,
searchConfiguration: {},
searchConfiguration: {
query: {
query: '',
language: 'lucene',
},
index: '90943e30-9a47-11e8-b64d-95841ca0b247',
},
};
const mockSearchResult = new Subject();
@ -64,24 +70,24 @@ const testResultPartial = {
running: true,
};
const searchSourceFieldsMock = {
query: {
query: '',
language: 'kuery',
},
filter: [],
index: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
title: 'kibana_sample_data_logs',
fields: [],
},
};
const searchSourceMock = {
id: 'data_source6',
fields: {
query: {
query: '',
language: 'kuery',
},
filter: [],
index: {
id: '90943e30-9a47-11e8-b64d-95841ca0b247',
title: 'kibana_sample_data_logs',
},
},
fields: searchSourceFieldsMock,
getField: (name: string) => {
if (name === 'filter') {
return [];
}
return '';
return (searchSourceFieldsMock as Record<string, object>)[name] || '';
},
setField: jest.fn(),
createCopy: jest.fn(() => {
@ -176,6 +182,7 @@ const dataMock = dataPluginMock.createStartContract();
Promise.resolve(searchSourceMock)
);
(dataMock.dataViews.getIdsWithTitle as jest.Mock).mockImplementation(() => Promise.resolve([]));
dataMock.dataViews.getDefaultDataView = jest.fn(() => Promise.resolve(null));
(dataMock.query.savedQueries.getSavedQuery as jest.Mock).mockImplementation(() =>
Promise.resolve(savedQueryMock)
);
@ -225,6 +232,17 @@ describe('SearchSourceAlertTypeExpression', () => {
expect(findTestSubject(wrapper, 'thresholdExpression')).toBeTruthy();
});
test('should disable Test Query button if data view is not selected yet', async () => {
let wrapper = setup({ ...defaultSearchSourceExpressionParams, searchConfiguration: undefined });
await act(async () => {
await nextTick();
});
wrapper = await wrapper.update();
const testButton = findTestSubject(wrapper, 'testQuery');
expect(testButton.prop('disabled')).toBeTruthy();
});
test('should show success message if Test Query is successful', async () => {
let wrapper = setup(defaultSearchSourceExpressionParams);
await act(async () => {
@ -232,7 +250,9 @@ describe('SearchSourceAlertTypeExpression', () => {
});
wrapper = await wrapper.update();
await act(async () => {
findTestSubject(wrapper, 'testQuery').simulate('click');
const testButton = findTestSubject(wrapper, 'testQuery');
expect(testButton.prop('disabled')).toBeFalsy();
testButton.simulate('click');
wrapper.update();
});
wrapper = await wrapper.update();

View file

@ -27,12 +27,13 @@ export const SearchSourceExpression = ({
setRuleProperty,
}: SearchSourceExpressionProps) => {
const {
searchConfiguration,
thresholdComparator,
threshold,
timeWindowSize,
timeWindowUnit,
size,
savedQueryId,
searchConfiguration,
} = ruleParams;
const { data } = useTriggersAndActionsUiDeps();
@ -46,31 +47,45 @@ export const SearchSourceExpression = ({
);
useEffect(() => {
setRuleProperty('params', {
searchConfiguration,
searchType: SearchType.searchSource,
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
});
const initSearchSource = async () => {
let initialSearchConfiguration = searchConfiguration;
// Init searchConfiguration when creating rule from Stack Management page
if (!searchConfiguration) {
const newSearchSource = data.search.searchSource.createEmpty();
newSearchSource.setField('query', data.query.queryString.getDefaultQuery());
const defaultDataView = await data.dataViews.getDefaultDataView();
if (defaultDataView) {
newSearchSource.setField('index', defaultDataView);
}
initialSearchConfiguration = newSearchSource.getSerializedFields();
}
setRuleProperty('params', {
searchConfiguration: initialSearchConfiguration,
searchType: SearchType.searchSource,
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
size: size ?? DEFAULT_VALUES.SIZE,
});
const initSearchSource = () =>
data.search.searchSource
.create(searchConfiguration)
.create(initialSearchConfiguration)
.then((fetchedSearchSource) => setSearchSource(fetchedSearchSource))
.catch(setParamsError);
};
initSearchSource();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data.search.searchSource, data.dataViews]);
useEffect(() => {
if (ruleParams.savedQueryId) {
data.query.savedQueries.getSavedQuery(ruleParams.savedQueryId).then(setSavedQuery);
if (savedQueryId) {
data.query.savedQueries.getSavedQuery(savedQueryId).then(setSavedQuery);
}
}, [data.query.savedQueries, ruleParams.savedQueryId]);
}, [data.query.savedQueries, savedQueryId]);
if (paramsError) {
return (
@ -89,9 +104,9 @@ export const SearchSourceExpression = ({
return (
<SearchSourceExpressionForm
ruleParams={ruleParams}
searchSource={searchSource}
errors={errors}
ruleParams={ruleParams}
initialSavedQuery={savedQuery}
setParam={setParam}
/>

View file

@ -11,36 +11,40 @@ import { lastValueFrom } from 'rxjs';
import { Filter } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { DataView, Query, ISearchSource, getTime } from '@kbn/data-plugin/common';
import {
ForLastExpression,
IErrorObject,
ThresholdExpression,
ValueExpression,
} from '@kbn/triggers-actions-ui-plugin/public';
import { SearchBar } from '@kbn/unified-search-plugin/public';
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 { EsQueryAlertParams, SearchType } from '../types';
import { CommonAlertParams, EsQueryAlertParams, SearchType } from '../types';
import { DEFAULT_VALUES } from '../constants';
import { DataViewSelectPopover } from '../../components/data_view_select_popover';
import { useTriggersAndActionsUiDeps } from '../util';
import { totalHitsToNumber } from './use_test_query';
import { TestQueryRow } from './test_query_row';
import { RuleCommonExpressions } from '../rule_common_expressions';
import { totalHitsToNumber } from '../test_query_row';
import { hasExpressionValidationErrors } from '../validation';
const HIDDEN_FILTER_PANEL_OPTIONS: SearchBarProps['hiddenFilterPanelOptions'] = [
'pinFilter',
'disableFilter',
];
interface LocalState {
index: DataView;
filter: Filter[];
query: Query;
threshold: number[];
timeWindowSize: number;
size: number;
thresholdComparator: CommonAlertParams['thresholdComparator'];
threshold: CommonAlertParams['threshold'];
timeWindowSize: CommonAlertParams['timeWindowSize'];
timeWindowUnit: CommonAlertParams['timeWindowUnit'];
size: CommonAlertParams['size'];
}
interface LocalStateAction {
type: SearchSourceParamsAction['type'] | ('threshold' | 'timeWindowSize' | 'size');
payload: SearchSourceParamsAction['payload'] | (number[] | number);
type:
| SearchSourceParamsAction['type']
| ('threshold' | 'thresholdComparator' | 'timeWindowSize' | 'timeWindowUnit' | 'size');
payload: SearchSourceParamsAction['payload'] | (number[] | number | string);
}
type LocalStateReducer = (prevState: LocalState, action: LocalStateAction) => LocalState;
@ -64,8 +68,7 @@ const isSearchSourceParam = (action: LocalStateAction): action is SearchSourcePa
export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProps) => {
const { data } = useTriggersAndActionsUiDeps();
const { searchSource, ruleParams, errors, initialSavedQuery, setParam } = props;
const { thresholdComparator, timeWindowUnit } = ruleParams;
const { searchSource, errors, initialSavedQuery, setParam, ruleParams } = props;
const [savedQuery, setSavedQuery] = useState<SavedQuery>();
const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []);
@ -86,20 +89,15 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
index: searchSource.getField('index')!,
query: searchSource.getField('query')!,
filter: mapAndFlattenFilters(searchSource.getField('filter') as Filter[]),
threshold: ruleParams.threshold,
timeWindowSize: ruleParams.timeWindowSize,
size: ruleParams.size,
threshold: ruleParams.threshold ?? DEFAULT_VALUES.THRESHOLD,
thresholdComparator: ruleParams.thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR,
timeWindowSize: ruleParams.timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
timeWindowUnit: ruleParams.timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
size: ruleParams.size ?? DEFAULT_VALUES.SIZE,
}
);
const {
index: dataView,
query,
filter: filters,
threshold,
timeWindowSize,
size,
} = ruleConfiguration;
const dataViews = useMemo(() => [dataView], [dataView]);
const { index: dataView, query, filter: filters } = ruleConfiguration;
const dataViews = useMemo(() => (dataView ? [dataView] : []), [dataView]);
const onSelectDataView = useCallback(
(newDataViewId) =>
@ -145,8 +143,9 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
// window size
const onChangeWindowUnit = useCallback(
(selectedWindowUnit: string) => setParam('timeWindowUnit', selectedWindowUnit),
[setParam]
(selectedWindowUnit: string) =>
dispatch({ type: 'timeWindowUnit', payload: selectedWindowUnit }),
[]
);
const onChangeWindowSize = useCallback(
@ -158,8 +157,9 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
// threshold
const onChangeSelectedThresholdComparator = useCallback(
(selectedThresholdComparator?: string) =>
setParam('thresholdComparator', selectedThresholdComparator),
[setParam]
selectedThresholdComparator &&
dispatch({ type: 'thresholdComparator', payload: selectedThresholdComparator }),
[]
);
const onChangeSelectedThreshold = useCallback(
@ -173,7 +173,7 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
[]
);
const timeWindow = `${timeWindowSize}${timeWindowUnit}`;
const timeWindow = `${ruleConfiguration.timeWindowSize}${ruleConfiguration.timeWindowUnit}`;
const createTestSearchSource = useCallback(() => {
const testSearchSource = searchSource.createCopy();
@ -204,8 +204,8 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchThreshold.ui.conditionPrompt"
defaultMessage="When the number of documents match"
id="xpack.stackAlerts.esQuery.ui.selectDataViewPrompt"
defaultMessage="Select a data view"
/>
</h5>
</EuiTitle>
@ -213,95 +213,70 @@ export const SearchSourceExpressionForm = (props: SearchSourceExpressionFormProp
<EuiSpacer size="s" />
<DataViewSelectPopover
initialDataViewTitle={dataView.title}
initialDataViewId={dataView.id}
dataViewName={dataView?.getName?.() ?? dataView?.title}
dataViewId={dataView?.id}
onSelectDataView={onSelectDataView}
/>
<EuiSpacer size="s" />
<SearchBar
onQuerySubmit={onQueryBarSubmit}
onQueryChange={onChangeQuery}
suggestionsSize="s"
displayStyle="inPage"
placeholder={i18n.translate('xpack.stackAlerts.searchSource.ui.searchQuery', {
defaultMessage: 'Search query',
})}
query={query}
indexPatterns={dataViews}
savedQuery={savedQuery}
filters={filters}
onFiltersUpdated={onUpdateFilters}
onClearSavedQuery={onClearSavedQuery}
onSavedQueryUpdated={onSavedQuery}
onSaved={onSavedQuery}
showSaveQuery={true}
showQueryBar={true}
showQueryInput={true}
showFilterBar={true}
showDatePicker={false}
showAutoRefreshOnly={false}
showSubmitButton={false}
dateRangeFrom={undefined}
dateRangeTo={undefined}
timeHistory={timeHistory}
hiddenFilterPanelOptions={['pinFilter', 'disableFilter']}
/>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchSource.ui.conditionPrompt"
defaultMessage="When the number of matches"
{Boolean(dataView?.id) && (
<>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.defineTextQueryPrompt"
defaultMessage="Define your query"
/>
</h5>
</EuiTitle>
<EuiSpacer size="xs" />
<SearchBar
onQuerySubmit={onQueryBarSubmit}
onQueryChange={onChangeQuery}
suggestionsSize="s"
displayStyle="inPage"
query={query}
indexPatterns={dataViews}
savedQuery={savedQuery}
filters={filters}
onFiltersUpdated={onUpdateFilters}
onClearSavedQuery={onClearSavedQuery}
onSavedQueryUpdated={onSavedQuery}
onSaved={onSavedQuery}
showSaveQuery
showQueryBar
showQueryInput
showFilterBar
showDatePicker={false}
showAutoRefreshOnly={false}
showSubmitButton={false}
dateRangeFrom={undefined}
dateRangeTo={undefined}
timeHistory={timeHistory}
hiddenFilterPanelOptions={HIDDEN_FILTER_PANEL_OPTIONS}
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ThresholdExpression
data-test-subj="thresholdExpression"
thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR}
threshold={threshold ?? DEFAULT_VALUES.THRESHOLD}
errors={errors}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedThreshold={onChangeSelectedThreshold}
onChangeSelectedThresholdComparator={onChangeSelectedThresholdComparator}
/>
<ForLastExpression
data-test-subj="forLastExpression"
popupPosition={'upLeft'}
timeWindowSize={timeWindowSize}
timeWindowUnit={timeWindowUnit}
display="fullWidth"
errors={errors}
</>
)}
<EuiSpacer size="m" />
<RuleCommonExpressions
threshold={ruleConfiguration.threshold}
thresholdComparator={ruleConfiguration.thresholdComparator}
timeWindowSize={ruleConfiguration.timeWindowSize}
timeWindowUnit={ruleConfiguration.timeWindowUnit}
size={ruleConfiguration.size}
onChangeThreshold={onChangeSelectedThreshold}
onChangeThresholdComparator={onChangeSelectedThresholdComparator}
onChangeWindowSize={onChangeWindowSize}
onChangeWindowUnit={onChangeWindowUnit}
onChangeSizeValue={onChangeSizeValue}
errors={errors}
hasValidationErrors={hasExpressionValidationErrors(ruleParams) || !dataView}
onTestFetch={onTestFetch}
onCopyQuery={onCopyQuery}
/>
<EuiSpacer size="s" />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.searchSource.ui.selectSizePrompt"
defaultMessage="Select a size"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ValueExpression
description={i18n.translate('xpack.stackAlerts.searchSource.ui.sizeExpression', {
defaultMessage: 'Size',
})}
data-test-subj="sizeValueExpression"
value={size}
errors={errors.size}
display="fullWidth"
popupPosition={'upLeft'}
onChangeSelectedValue={onChangeSizeValue}
/>
<EuiSpacer size="s" />
<TestQueryRow fetch={onTestFetch} copyQuery={onCopyQuery} hasValidationErrors={false} />
<EuiSpacer />
</Fragment>
);

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { RuleCommonExpressions } from './rule_common_expressions';

View file

@ -0,0 +1,132 @@
/*
* 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 React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import {
ForLastExpression,
IErrorObject,
ThresholdExpression,
ValueExpression,
} from '@kbn/triggers-actions-ui-plugin/public';
import { CommonAlertParams } from '../types';
import { DEFAULT_VALUES } from '../constants';
import { TestQueryRow, TestQueryRowProps } from '../test_query_row';
export interface RuleCommonExpressionsProps {
thresholdComparator?: CommonAlertParams['thresholdComparator'];
threshold?: CommonAlertParams['threshold'];
timeWindowSize: CommonAlertParams['timeWindowSize'];
timeWindowUnit: CommonAlertParams['timeWindowUnit'];
size: CommonAlertParams['size'];
errors: IErrorObject;
hasValidationErrors: boolean;
onChangeThreshold: Parameters<typeof ThresholdExpression>[0]['onChangeSelectedThreshold'];
onChangeThresholdComparator: Parameters<
typeof ThresholdExpression
>[0]['onChangeSelectedThresholdComparator'];
onChangeWindowSize: Parameters<typeof ForLastExpression>[0]['onChangeWindowSize'];
onChangeWindowUnit: Parameters<typeof ForLastExpression>[0]['onChangeWindowUnit'];
onChangeSizeValue: Parameters<typeof ValueExpression>[0]['onChangeSelectedValue'];
onTestFetch: TestQueryRowProps['fetch'];
onCopyQuery?: TestQueryRowProps['copyQuery'];
}
export const RuleCommonExpressions: React.FC<RuleCommonExpressionsProps> = ({
thresholdComparator,
threshold,
timeWindowSize,
timeWindowUnit,
size,
errors,
hasValidationErrors,
onChangeThreshold,
onChangeThresholdComparator,
onChangeWindowSize,
onChangeWindowUnit,
onChangeSizeValue,
onTestFetch,
onCopyQuery,
}) => {
return (
<>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.conditionsPrompt"
defaultMessage="Set the threshold and duration"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<ThresholdExpression
data-test-subj="thresholdExpression"
thresholdComparator={thresholdComparator ?? DEFAULT_VALUES.THRESHOLD_COMPARATOR}
threshold={threshold ?? DEFAULT_VALUES.THRESHOLD}
errors={errors}
display="fullWidth"
popupPosition="upLeft"
onChangeSelectedThreshold={onChangeThreshold}
onChangeSelectedThresholdComparator={onChangeThresholdComparator}
/>
<ForLastExpression
data-test-subj="forLastExpression"
popupPosition="upLeft"
timeWindowSize={timeWindowSize}
timeWindowUnit={timeWindowUnit}
display="fullWidth"
errors={errors}
onChangeWindowSize={onChangeWindowSize}
onChangeWindowUnit={onChangeWindowUnit}
/>
<EuiSpacer size="s" />
<EuiFlexGroup alignItems="center" responsive={false} gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.esQuery.ui.selectSizePrompt"
defaultMessage="Set the number of documents to send"
/>
</h5>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
position="right"
color="subdued"
type="questionInCircle"
content={i18n.translate('xpack.stackAlerts.esQuery.ui.selectSizePrompt.toolTip', {
defaultMessage:
'Specify the number of documents to pass to the configured actions when the threshold condition is met.',
})}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<ValueExpression
description={i18n.translate('xpack.stackAlerts.esQuery.ui.sizeExpression', {
defaultMessage: 'Size',
})}
data-test-subj="sizeValueExpression"
value={size}
errors={errors.size}
display="fullWidth"
popupPosition="upLeft"
onChangeSelectedValue={onChangeSizeValue}
/>
<EuiSpacer size="s" />
<TestQueryRow
fetch={onTestFetch}
copyQuery={onCopyQuery}
hasValidationErrors={hasValidationErrors}
/>
</>
);
};

View file

@ -0,0 +1,10 @@
/*
* 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.
*/
export { TestQueryRow } from './test_query_row';
export type { TestQueryRowProps } from './test_query_row';
export { useTestQuery, totalHitsToNumber } from './use_test_query';

View file

@ -18,15 +18,17 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { useTestQuery } from './use_test_query';
export function TestQueryRow({
fetch,
copyQuery,
hasValidationErrors,
}: {
export interface TestQueryRowProps {
fetch: () => Promise<{ nrOfDocs: number; timeWindow: string }>;
copyQuery?: () => string;
hasValidationErrors: boolean;
}) {
}
export const TestQueryRow: React.FC<TestQueryRowProps> = ({
fetch,
copyQuery,
hasValidationErrors,
}) => {
const { onTestQuery, testQueryResult, testQueryError, testQueryLoading } = useTestQuery(fetch);
const [copiedMessage, setCopiedMessage] = useState<ReactNode | null>(null);
@ -119,4 +121,4 @@ export function TestQueryRow({
)}
</>
);
}
};

View file

@ -9,6 +9,7 @@ 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 { EXPRESSION_ERRORS } from './constants';
export interface Comparator {
@ -40,8 +41,8 @@ export interface OnlyEsQueryAlertParams {
timeField: string;
}
export interface OnlySearchSourceAlertParams {
searchType: 'searchSource';
searchConfiguration: SerializedSearchSourceFields;
searchType?: 'searchSource';
searchConfiguration?: SerializedSearchSourceFields;
savedQueryId?: string;
}
@ -53,4 +54,5 @@ export type ErrorKey = keyof ExpressionErrors & unknown;
export interface TriggersAndActionsUiDeps {
data: DataPublicPluginStart;
dataViewEditor: DataViewEditorStart;
}

View file

@ -6,9 +6,16 @@
*/
import { EsQueryAlertParams, SearchType } from './types';
import { validateExpression } from './validation';
import { validateExpression, hasExpressionValidationErrors } from './validation';
describe('expression params validation', () => {
test('if params are not set should return a proper error message', () => {
const initialParams: EsQueryAlertParams<SearchType.esQuery> =
{} as EsQueryAlertParams<SearchType.esQuery>;
expect(validateExpression(initialParams).errors.searchType.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.searchType[0]).toBe('Query type is required.');
});
test('if index property is invalid should return proper error message', () => {
const initialParams: EsQueryAlertParams<SearchType.esQuery> = {
index: [],
@ -63,6 +70,7 @@ describe('expression params validation', () => {
};
expect(validateExpression(initialParams).errors.esQuery.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.esQuery[0]).toBe(`Query field is required.`);
expect(hasExpressionValidationErrors(initialParams)).toBe(true);
});
test('if searchConfiguration property is not set should return proper error message', () => {
@ -157,4 +165,18 @@ describe('expression params validation', () => {
'Size must be between 0 and 10,000.'
);
});
test('should not return error messages if all is correct', () => {
const initialParams: EsQueryAlertParams<SearchType.esQuery> = {
index: ['test'],
esQuery: '{"query":{"match_all":{}}}',
size: 250,
timeWindowSize: 100,
timeWindowUnit: 's',
threshold: [0],
timeField: '@timestamp',
};
expect(validateExpression(initialParams).errors.size.length).toBe(0);
expect(hasExpressionValidationErrors(initialParams)).toBe(false);
});
});

View file

@ -12,11 +12,22 @@ import { EsQueryAlertParams, ExpressionErrors } from './types';
import { isSearchSourceAlert } from './util';
import { EXPRESSION_ERRORS } from './constants';
export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => {
const { size, threshold, timeWindowSize, thresholdComparator } = alertParams;
export const validateExpression = (ruleParams: EsQueryAlertParams): ValidationResult => {
const { size, threshold, timeWindowSize, thresholdComparator } = ruleParams;
const validationResult = { errors: {} };
const errors: ExpressionErrors = defaultsDeep({}, EXPRESSION_ERRORS);
validationResult.errors = errors;
if (!('index' in ruleParams) && !ruleParams.searchType) {
errors.searchType.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSearchType', {
defaultMessage: 'Query type is required.',
})
);
return validationResult;
}
if (!threshold || threshold.length === 0 || threshold[0] === undefined) {
errors.threshold0.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredThreshold0Text', {
@ -72,9 +83,9 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
* Skip esQuery and index params check if it is search source alert,
* since it should contain searchConfiguration instead of esQuery and index.
*/
const isSearchSource = isSearchSourceAlert(alertParams);
const isSearchSource = isSearchSourceAlert(ruleParams);
if (isSearchSource) {
if (!alertParams.searchConfiguration) {
if (!ruleParams.searchConfiguration) {
errors.searchConfiguration.push(
i18n.translate(
'xpack.stackAlerts.esQuery.ui.validation.error.requiredSearchConfiguration',
@ -83,11 +94,17 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
}
)
);
} else if (!ruleParams.searchConfiguration.index) {
errors.index.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredDataViewText', {
defaultMessage: 'Data view is required.',
})
);
}
return validationResult;
}
if (!alertParams.index || alertParams.index.length === 0) {
if (!ruleParams.index || ruleParams.index.length === 0) {
errors.index.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredIndexText', {
defaultMessage: 'Index is required.',
@ -95,7 +112,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
);
}
if (!alertParams.timeField) {
if (!ruleParams.timeField) {
errors.timeField.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredTimeFieldText', {
defaultMessage: 'Time field is required.',
@ -103,7 +120,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
);
}
if (!alertParams.esQuery) {
if (!ruleParams.esQuery) {
errors.esQuery.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredQueryText', {
defaultMessage: 'Elasticsearch query is required.',
@ -111,7 +128,7 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
);
} else {
try {
const parsedQuery = JSON.parse(alertParams.esQuery);
const parsedQuery = JSON.parse(ruleParams.esQuery);
if (!parsedQuery.query) {
errors.esQuery.push(
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredEsQueryText', {
@ -130,3 +147,10 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
return validationResult;
};
export const hasExpressionValidationErrors = (ruleParams: EsQueryAlertParams) => {
const { errors: validationErrors } = validateExpression(ruleParams);
return Object.keys(validationErrors).some(
(key) => validationErrors[key] && validationErrors[key].length
);
};

View file

@ -28925,15 +28925,10 @@
"xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "L'expression contient des erreurs.",
"xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage": "L'alerte de recherche Elasticsearch \"\\{\\{alertName\\}\\}\" est active :\n\n- Valeur : \\{\\{context.value\\}\\}\n- Conditions remplies : \\{\\{context.conditions\\}\\} sur \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- Horodatage : \\{\\{context.date\\}\\}\n- Lien : \\{\\{context.link\\}\\}",
"xpack.stackAlerts.esQuery.ui.alertType.descriptionText": "Alerte lorsque des correspondances sont trouvées au cours de la dernière exécution de la requête.",
"xpack.stackAlerts.esQuery.ui.conditionPrompt": "Lorsque le nombre de correspondances",
"xpack.stackAlerts.esQuery.ui.conditionPrompt.toolTip": "La fenêtre temporelle définie ci-dessous s'applique uniquement à la première vérification de règle.",
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "La recherche correspond à {count} documents dans le/la/les dernier(s)/dernière(s) {window}.",
"xpack.stackAlerts.esQuery.ui.queryEditor": "Éditeur de recherche Elasticsearch",
"xpack.stackAlerts.esQuery.ui.queryError": "Erreur lors du test de la recherche : {message}",
"xpack.stackAlerts.esQuery.ui.queryPrompt": "Définir la recherche Elasticsearch",
"xpack.stackAlerts.esQuery.ui.queryPrompt.help": "Documentation DSL sur la recherche Elasticsearch",
"xpack.stackAlerts.esQuery.ui.queryPrompt.label": "Recherche Elasticsearch",
"xpack.stackAlerts.esQuery.ui.selectIndex": "Sélectionner un index et une taille",
"xpack.stackAlerts.esQuery.ui.sizeExpression": "Taille",
"xpack.stackAlerts.esQuery.ui.testQuery": "Tester la recherche",
"xpack.stackAlerts.esQuery.ui.testQueryIsExecuted": "La requête a été exécutée.",
@ -29008,11 +29003,6 @@
"xpack.stackAlerts.indexThreshold.alertTypeTitle": "Seuil de l'index",
"xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "thresholdComparator spécifié non valide : {comparator}",
"xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold] : requiert deux éléments pour le comparateur \"{thresholdComparator}\"",
"xpack.stackAlerts.searchSource.ui.conditionPrompt": "Lorsque le nombre de correspondances",
"xpack.stackAlerts.searchSource.ui.searchQuery": "Requête de recherche",
"xpack.stackAlerts.searchSource.ui.selectSizePrompt": "Sélectionner une taille",
"xpack.stackAlerts.searchSource.ui.sizeExpression": "Taille",
"xpack.stackAlerts.searchThreshold.ui.conditionPrompt": "Lorsque le nombre de documents correspond à",
"xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "L'expression contient des erreurs.",
"xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage": "l'alerte \"\\{\\{alertName\\}\\}\" est active pour le groupe \"\\{\\{context.group\\}\\}\" :\n\n- Valeur : \\{\\{context.value\\}\\}\n- Conditions remplies : \\{\\{context.conditions\\}\\} sur \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- Horodatage : \\{\\{context.date\\}\\}",
"xpack.stackAlerts.threshold.ui.alertType.descriptionText": "Alerte lorsqu'une recherche agrégée atteint le seuil.",

View file

@ -28908,15 +28908,10 @@
"xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
"xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage": "Elasticsearchクエリアラート'\\{\\{alertName\\}\\}'が有効です。\n\n- 値:\\{\\{context.value\\}\\}\n- 満たされた条件:\\{\\{context.conditions\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- タイムスタンプ:\\{\\{context.date\\}\\}\n- リンク:\\{\\{context.link\\}\\}",
"xpack.stackAlerts.esQuery.ui.alertType.descriptionText": "前回のクエリ実行中に一致が見つかったときにアラートを発行します。",
"xpack.stackAlerts.esQuery.ui.conditionPrompt": "一致数",
"xpack.stackAlerts.esQuery.ui.conditionPrompt.toolTip": "以下で定義された時間枠は最初のルールチェックにのみ適用されます。",
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "前回の{window}でクエリが{count}個のドキュメントと一致しました。",
"xpack.stackAlerts.esQuery.ui.queryEditor": "Elasticsearchクエリエディター",
"xpack.stackAlerts.esQuery.ui.queryError": "クエリのテストエラー:{message}",
"xpack.stackAlerts.esQuery.ui.queryPrompt": "Elasticsearchクエリを定義",
"xpack.stackAlerts.esQuery.ui.queryPrompt.help": "ElasticsearchクエリDSLドキュメント",
"xpack.stackAlerts.esQuery.ui.queryPrompt.label": "Elasticsearch クエリ",
"xpack.stackAlerts.esQuery.ui.selectIndex": "インデックスとサイズを選択",
"xpack.stackAlerts.esQuery.ui.sizeExpression": "サイズ",
"xpack.stackAlerts.esQuery.ui.testQuery": "クエリのテスト",
"xpack.stackAlerts.esQuery.ui.testQueryIsExecuted": "クエリが実行されます。",
@ -28991,11 +28986,6 @@
"xpack.stackAlerts.indexThreshold.alertTypeTitle": "インデックスしきい値",
"xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "無効な thresholdComparator が指定されました:{comparator}",
"xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:「{thresholdComparator}」比較子の場合には2つの要素が必要です",
"xpack.stackAlerts.searchSource.ui.conditionPrompt": "一致数",
"xpack.stackAlerts.searchSource.ui.searchQuery": "検索クエリ",
"xpack.stackAlerts.searchSource.ui.selectSizePrompt": "サイズを選択",
"xpack.stackAlerts.searchSource.ui.sizeExpression": "サイズ",
"xpack.stackAlerts.searchThreshold.ui.conditionPrompt": "ドキュメント数が一致するとき",
"xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "下の表現のエラーを修正してください。",
"xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage": "アラート '\\{\\{alertName\\}\\}' はグループ '\\{\\{context.group\\}\\}' でアクティブです:\n\n- 値:\\{\\{context.value\\}\\}\n- 満たされた条件:\\{\\{context.conditions\\}\\} over \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- タイムスタンプ:\\{\\{context.date\\}\\}",
"xpack.stackAlerts.threshold.ui.alertType.descriptionText": "アグリゲーションされたクエリがしきい値に達したときにアラートを発行します。",

View file

@ -28935,15 +28935,10 @@
"xpack.stackAlerts.esQuery.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
"xpack.stackAlerts.esQuery.ui.alertType.defaultActionMessage": "Elasticsearch 查询告警“\\{\\{alertName\\}\\}”处于活动状态:\n\n- 值:\\{\\{context.value\\}\\}\n- 满足的条件:\\{\\{context.conditions\\}\\} 超过 \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- 时间戳:\\{\\{context.date\\}\\}\n- 链接:\\{\\{context.link\\}\\}",
"xpack.stackAlerts.esQuery.ui.alertType.descriptionText": "在运行最新查询期间找到匹配项时告警。",
"xpack.stackAlerts.esQuery.ui.conditionPrompt": "当匹配数目",
"xpack.stackAlerts.esQuery.ui.conditionPrompt.toolTip": "下面定义的时间窗口仅适用于第一次规则检查。",
"xpack.stackAlerts.esQuery.ui.numQueryMatchesText": "查询在过去 {window} 匹配 {count} 个文档。",
"xpack.stackAlerts.esQuery.ui.queryEditor": "Elasticsearch 查询编辑器",
"xpack.stackAlerts.esQuery.ui.queryError": "测试查询时出错:{message}",
"xpack.stackAlerts.esQuery.ui.queryPrompt": "定义 Elasticsearch 查询",
"xpack.stackAlerts.esQuery.ui.queryPrompt.help": "Elasticsearch 查询 DSL 文档",
"xpack.stackAlerts.esQuery.ui.queryPrompt.label": "Elasticsearch 查询",
"xpack.stackAlerts.esQuery.ui.selectIndex": "选择索引和大小",
"xpack.stackAlerts.esQuery.ui.sizeExpression": "大小",
"xpack.stackAlerts.esQuery.ui.testQuery": "测试查询",
"xpack.stackAlerts.esQuery.ui.testQueryIsExecuted": "已执行查询。",
@ -29018,11 +29013,6 @@
"xpack.stackAlerts.indexThreshold.alertTypeTitle": "索引阈值",
"xpack.stackAlerts.indexThreshold.invalidComparatorErrorMessage": "指定的 thresholdComparator 无效:{comparator}",
"xpack.stackAlerts.indexThreshold.invalidThreshold2ErrorMessage": "[threshold]:对于“{thresholdComparator}”比较运算符,必须包含两个元素",
"xpack.stackAlerts.searchSource.ui.conditionPrompt": "当匹配数目",
"xpack.stackAlerts.searchSource.ui.searchQuery": "搜索查询",
"xpack.stackAlerts.searchSource.ui.selectSizePrompt": "选择大小",
"xpack.stackAlerts.searchSource.ui.sizeExpression": "大小",
"xpack.stackAlerts.searchThreshold.ui.conditionPrompt": "文档数量匹配时",
"xpack.stackAlerts.threshold.ui.alertParams.fixErrorInExpressionBelowValidationMessage": "表达式包含错误。",
"xpack.stackAlerts.threshold.ui.alertType.defaultActionMessage": "组“\\{\\{context.group\\}\\}”的告警“\\{\\{alertName\\}\\}”处于活动状态:\n\n- 值:\\{\\{context.value\\}\\}\n- 满足的条件:\\{\\{context.conditions\\}\\} 超过 \\{\\{params.timeWindowSize\\}\\}\\{\\{params.timeWindowUnit\\}\\}\n- 时间戳:\\{\\{context.date\\}\\}",
"xpack.stackAlerts.threshold.ui.alertType.descriptionText": "聚合查询达到阈值时告警。",

View file

@ -8,7 +8,7 @@
"server": true,
"ui": true,
"optionalPlugins": ["cloud", "features", "home", "spaces"],
"requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "unifiedSearch", "dataViews", "alerting", "actions"],
"requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "unifiedSearch", "dataViews", "dataViewEditor", "alerting", "actions"],
"configPath": ["xpack", "trigger_actions_ui"],
"extraPublicDirs": ["public/common", "public/common/constants"],
"requiredBundles": ["alerting", "esUiShared", "kibanaReact", "kibanaUtils", "actions"]

View file

@ -17,6 +17,7 @@ import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
@ -45,6 +46,7 @@ export interface TriggersAndActionsUiServices extends CoreStart {
actions: ActionsPublicPluginSetup;
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
charts: ChartsPluginStart;
alerting?: AlertingStart;
spaces?: SpacesPluginStart;

View file

@ -26,6 +26,7 @@ export const ConfirmRuleClose: React.FC<Props> = ({ onConfirm, onCancel }) => {
)}
onCancel={onCancel}
onConfirm={onConfirm}
buttonColor="danger"
confirmButtonText={i18n.translate(
'xpack.triggersActionsUI.sections.confirmRuleClose.confirmRuleCloseConfirmButtonText',
{

View file

@ -207,9 +207,12 @@ export const RuleEdit = ({
canChangeTrigger={false}
setHasActionsDisabled={setHasActionsDisabled}
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
operation="i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.operationName', {
defaultMessage: 'edit',
})"
operation={i18n.translate(
'xpack.triggersActionsUI.sections.ruleEdit.operationName',
{
defaultMessage: 'edit',
}
)}
metadata={metadata}
/>
</EuiFlyoutBody>

View file

@ -67,7 +67,7 @@ export const ForLastExpression = ({
}
)}
data-test-subj="forLastExpression"
value={`${timeWindowSize} ${getTimeUnitLabel(
value={`${timeWindowSize ?? '?'} ${getTimeUnitLabel(
timeWindowUnit as TIME_UNITS,
(timeWindowSize ?? '').toString()
)}`}
@ -97,13 +97,10 @@ export const ForLastExpression = ({
</ClosablePopoverTitle>
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiFormRow
isInvalid={errors.timeWindowSize.length > 0 && timeWindowSize !== undefined}
error={errors.timeWindowSize}
>
<EuiFormRow isInvalid={errors.timeWindowSize.length > 0} error={errors.timeWindowSize}>
<EuiFieldNumber
data-test-subj="timeWindowSizeNumber"
isInvalid={errors.timeWindowSize.length > 0 && timeWindowSize !== undefined}
isInvalid={errors.timeWindowSize.length > 0}
min={0}
value={timeWindowSize || ''}
onChange={(e) => {

View file

@ -59,6 +59,7 @@ export const ValueExpression = ({
onClick={() => {
setValuePopoverOpen(true);
}}
isInvalid={errors.length > 0}
/>
}
isOpen={valuePopoverOpen}

View file

@ -18,6 +18,7 @@ import {
ActionTypeRegistryContract,
AlertsTableConfigurationRegistryContract,
} from '../../../types';
import { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
export const createStartServicesMock = (): TriggersAndActionsUiServices => {
const core = coreMock.createStart();
@ -41,6 +42,9 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => {
setBreadcrumbs: jest.fn(),
data: dataPluginMock.createStartContract(),
dataViews: dataViewPluginMocks.createStartContract(),
dataViewEditor: {
openEditor: jest.fn(),
} as unknown as DataViewEditorStart,
unifiedSearch: unifiedSearchPluginMock.createStartContract(),
actionTypeRegistry: {
has: jest.fn(),

View file

@ -19,6 +19,7 @@ import { PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/publi
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import type { SpacesPluginStart } from '@kbn/spaces-plugin/public';
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
@ -125,6 +126,7 @@ interface PluginsSetup {
interface PluginsStart {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
dataViewEditor: DataViewEditorStart;
charts: ChartsPluginStart;
alerting?: AlertingStart;
spaces?: SpacesPluginStart;
@ -217,6 +219,7 @@ export class Plugin
actions: plugins.actions,
data: pluginsStart.data,
dataViews: pluginsStart.dataViews,
dataViewEditor: pluginsStart.dataViewEditor,
charts: pluginsStart.charts,
alerting: pluginsStart.alerting,
spaces: pluginsStart.spaces,

View file

@ -19,6 +19,7 @@
{ "path": "../features/tsconfig.json" },
{ "path": "../rule_registry/tsconfig.json" },
{ "path": "../../../src/plugins/data/tsconfig.json" },
{ "path": "../../../src/plugins/data_view_editor/tsconfig.json" },
{ "path": "../../../src/plugins/saved_objects/tsconfig.json" },
{ "path": "../../../src/plugins/home/tsconfig.json" },
{ "path": "../../../src/plugins/charts/tsconfig.json" },