mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
b809237f84
commit
b46763fc2a
35 changed files with 991 additions and 373 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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';
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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": "アグリゲーションされたクエリがしきい値に達したときにアラートを発行します。",
|
||||
|
|
|
@ -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": "聚合查询达到阈值时告警。",
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -59,6 +59,7 @@ export const ValueExpression = ({
|
|||
onClick={() => {
|
||||
setValuePopoverOpen(true);
|
||||
}}
|
||||
isInvalid={errors.length > 0}
|
||||
/>
|
||||
}
|
||||
isOpen={valuePopoverOpen}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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" },
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue