[AO] Update the design of Threshold rule creation form (#163313)

## Summary

Fixes #162768 
Fixes #162544

### After update 
<img width="466" alt="Screenshot 2023-08-09 at 17 53 44"
src="926f0c9e-ca55-4711-be3a-2da39726caa8">
This commit is contained in:
Faisal Kanout 2023-08-11 19:28:04 +02:00 committed by GitHub
parent 1e7efae56a
commit 2093a1fee3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 405 additions and 466 deletions

View file

@ -1,23 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ExpressionRow should render a helpText for the of expression 1`] = `
<FormattedMessage
defaultMessage="Can't find a metric? {documentationLink}."
id="xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail"
values={
Object {
"documentationLink": <EuiLink
data-test-subj="thresholdRuleExpressionRowLearnHowToAddMoreDataLink"
href="https://www.elastic.co/guide/en/observability/current/configure-settings.html"
target="BLANK"
>
<FormattedMessage
defaultMessage="Learn how to add more data"
id="xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel"
values={Object {}}
/>
</EuiLink>,
}
}
/>
`;

View file

@ -0,0 +1,31 @@
/*
* 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 * as React from 'react';
import { mount } from 'enzyme';
import { ClosablePopoverTitle } from './closable_popover_title';
describe('closable popover title', () => {
it('renders with defined options', () => {
const onClose = jest.fn();
const children = <div className="foo" />;
const wrapper = mount(
<ClosablePopoverTitle onClose={onClose}>{children}</ClosablePopoverTitle>
);
expect(wrapper.contains(<div className="foo" />)).toBeTruthy();
});
it('onClose function gets called', () => {
const onClose = jest.fn();
const children = <div className="foo" />;
const wrapper = mount(
<ClosablePopoverTitle onClose={onClose}>{children}</ClosablePopoverTitle>
);
wrapper.find('EuiButtonIcon').simulate('click');
expect(onClose).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,38 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { EuiPopoverTitle, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui';
interface ClosablePopoverTitleProps {
children: JSX.Element;
onClose: () => void;
}
export function ClosablePopoverTitle({ children, onClose }: ClosablePopoverTitleProps) {
return (
<EuiPopoverTitle>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem>{children}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType="cross"
color="danger"
aria-label={i18n.translate(
'xpack.observability.thresholdRule.closablePopoverTitle.closeLabel',
{
defaultMessage: 'Close',
}
)}
onClick={() => onClose()}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPopoverTitle>
);
}

View file

@ -11,12 +11,15 @@ import {
EuiFlexGroup,
EuiButtonEmpty,
EuiSpacer,
EuiExpression,
EuiPopover,
} from '@elastic/eui';
import React, { useState, useCallback, useMemo } from 'react';
import { omit, range, first, xor, debounce } from 'lodash';
import { IErrorObject } from '@kbn/triggers-actions-ui-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { DataViewBase } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS } from '../../../../../common/threshold_rule/metrics_explorer';
import {
Aggregators,
@ -27,13 +30,8 @@ import {
import { MetricExpression } from '../../types';
import { CustomMetrics, AggregationTypes, NormalizedFields } from './types';
import { MetricRowWithAgg } from './metric_row_with_agg';
import { MetricRowWithCount } from './metric_row_with_count';
import {
CUSTOM_EQUATION,
EQUATION_HELP_MESSAGE,
LABEL_HELP_MESSAGE,
LABEL_LABEL,
} from '../../i18n_strings';
import { ClosablePopoverTitle } from '../closable_popover_title';
import { EQUATION_HELP_MESSAGE } from '../../i18n_strings';
export interface CustomEquationEditorProps {
onChange: (expression: MetricExpression) => void;
@ -61,7 +59,7 @@ export function CustomEquationEditor({
const [customMetrics, setCustomMetrics] = useState<CustomMetrics>(
expression?.customMetrics ?? [NEW_METRIC]
);
const [label, setLabel] = useState<string | undefined>(expression?.label || undefined);
const [customEqPopoverOpen, setCustomEqPopoverOpen] = useState(false);
const [equation, setEquation] = useState<string | undefined>(expression?.equation || undefined);
const debouncedOnChange = useMemo(() => debounce(onChange, 500), [onChange]);
@ -70,48 +68,40 @@ export function CustomEquationEditor({
const currentVars = previous?.map((m) => m.name) ?? [];
const name = first(xor(VAR_NAMES, currentVars))!;
const nextMetrics = [...(previous || []), { ...NEW_METRIC, name }];
debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation, label });
debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation });
return nextMetrics;
});
}, [debouncedOnChange, equation, expression, label]);
}, [debouncedOnChange, equation, expression]);
const handleDelete = useCallback(
(name: string) => {
setCustomMetrics((previous) => {
const nextMetrics = previous?.filter((row) => row.name !== name) ?? [NEW_METRIC];
const finalMetrics = (nextMetrics.length && nextMetrics) || [NEW_METRIC];
debouncedOnChange({ ...expression, customMetrics: finalMetrics, equation, label });
debouncedOnChange({ ...expression, customMetrics: finalMetrics, equation });
return finalMetrics;
});
},
[equation, expression, debouncedOnChange, label]
[equation, expression, debouncedOnChange]
);
const handleChange = useCallback(
(metric: MetricExpressionCustomMetric) => {
setCustomMetrics((previous) => {
const nextMetrics = previous?.map((m) => (m.name === metric.name ? metric : m));
debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation, label });
debouncedOnChange({ ...expression, customMetrics: nextMetrics, equation });
return nextMetrics;
});
},
[equation, expression, debouncedOnChange, label]
[equation, expression, debouncedOnChange]
);
const handleEquationChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setEquation(e.target.value);
debouncedOnChange({ ...expression, customMetrics, equation: e.target.value, label });
debouncedOnChange({ ...expression, customMetrics, equation: e.target.value });
},
[debouncedOnChange, expression, customMetrics, label]
);
const handleLabelChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
debouncedOnChange({ ...expression, customMetrics, equation, label: e.target.value });
},
[debouncedOnChange, expression, customMetrics, equation]
[debouncedOnChange, expression, customMetrics]
);
const disableAdd = customMetrics?.length === MAX_VARIABLES;
@ -119,42 +109,24 @@ export function CustomEquationEditor({
const filteredAggregationTypes = omit(aggregationTypes, OMITTED_AGGREGATIONS_FOR_CUSTOM_METRICS);
const metricRows = customMetrics?.map((row) => {
if (row.aggType === Aggregators.COUNT) {
return (
<MetricRowWithCount
key={row.name}
name={row.name}
agg={row.aggType}
filter={row.filter}
onAdd={handleAddNewRow}
onDelete={handleDelete}
disableAdd={disableAdd}
aggregationTypes={filteredAggregationTypes}
disableDelete={disableDelete}
onChange={handleChange}
errors={errors}
dataView={dataView}
/>
);
}
return (
<MetricRowWithAgg
key={row.name}
name={row.name}
aggType={row.aggType}
aggregationTypes={filteredAggregationTypes}
field={row.field}
fields={fields}
onAdd={handleAddNewRow}
onDelete={handleDelete}
disableAdd={disableAdd}
disableDelete={disableDelete}
onChange={handleChange}
errors={errors}
/>
);
});
const metricRows = customMetrics?.map((row) => (
<MetricRowWithAgg
key={row.name}
name={row.name}
aggType={row.aggType}
aggregationTypes={filteredAggregationTypes}
field={row.field}
filter={row.filter}
fields={fields}
onAdd={handleAddNewRow}
onDelete={handleDelete}
disableAdd={disableAdd}
disableDelete={disableDelete}
onChange={handleChange}
errors={errors}
dataView={dataView}
/>
));
const placeholder = useMemo(() => {
return customMetrics?.map((row) => row.name).join(' + ');
@ -181,42 +153,69 @@ export function CustomEquationEditor({
</EuiButtonEmpty>
</EuiFlexGroup>
<EuiSpacer size={'m'} />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
label="Equation"
fullWidth
helpText={EQUATION_HELP_MESSAGE}
isInvalid={errors.equation != null}
error={[errors.equation]}
>
<EuiFieldText
data-test-subj="thresholdRuleCustomEquationEditorFieldText"
<EuiFlexItem>
<EuiPopover
button={
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.equationAndThreshold',
{ defaultMessage: 'Equation and threshold' }
)}
error={[errors.equation]}
isInvalid={errors.equation != null}
compressed
>
<>
<EuiSpacer size="xs" />
<EuiExpression
data-test-subj="customEquation"
description={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.equationLabel',
{ defaultMessage: 'Equation' }
)}
value={equation ?? placeholder}
display={'columns'}
onClick={() => {
setCustomEqPopoverOpen(true);
}}
/>
</>
</EuiFormRow>
}
isOpen={customEqPopoverOpen}
closePopover={() => {
setCustomEqPopoverOpen(false);
}}
display="block"
ownFocus
anchorPosition={'downLeft'}
repositionOnScroll
>
<div>
<ClosablePopoverTitle onClose={() => setCustomEqPopoverOpen(false)}>
<FormattedMessage
id="xpack.observability.threshold.rule.alertFlyout.customEquationLabel"
defaultMessage="Custom equation"
/>
</ClosablePopoverTitle>
<EuiFormRow
fullWidth
placeholder={placeholder}
onChange={handleEquationChange}
value={equation ?? ''}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size={'s'} />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label={LABEL_LABEL} fullWidth helpText={LABEL_HELP_MESSAGE}>
<EuiFieldText
data-test-subj="thresholdRuleCustomEquationEditorFieldText"
compressed
fullWidth
value={label}
placeholder={CUSTOM_EQUATION}
onChange={handleLabelChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
helpText={EQUATION_HELP_MESSAGE}
isInvalid={errors.equation != null}
>
<EuiFieldText
data-test-subj="thresholdRuleCustomEquationEditorFieldText"
isInvalid={errors.equation != null}
compressed
fullWidth
placeholder={placeholder}
onChange={handleEquationChange}
value={equation ?? ''}
/>
</EuiFormRow>
</div>
</EuiPopover>
</EuiFlexItem>
</div>
);
}

View file

@ -21,7 +21,7 @@ export function MetricRowControls({ onDelete, disableDelete }: MetricRowControlP
aria-label={DELETE_LABEL}
iconType="trash"
color="danger"
style={{ marginBottom: '0.2em' }}
style={{ marginBottom: '0.6em' }}
onClick={onDelete}
disabled={disableDelete}
title={DELETE_LABEL}

View file

@ -7,24 +7,31 @@
import {
EuiFormRow,
EuiHorizontalRule,
EuiFlexItem,
EuiFlexGroup,
EuiSelect,
EuiComboBox,
EuiComboBoxOptionOption,
EuiPopover,
EuiExpression,
} from '@elastic/eui';
import React, { useMemo, useCallback } from 'react';
import React, { useMemo, useCallback, useState } from 'react';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ValidNormalizedTypes } from '@kbn/triggers-actions-ui-plugin/public';
import { DataViewBase } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { Aggregators, CustomMetricAggTypes } from '../../../../../common/threshold_rule/types';
import { MetricRowControls } from './metric_row_controls';
import { NormalizedFields, MetricRowBaseProps } from './types';
import { ClosablePopoverTitle } from '../closable_popover_title';
import { MetricsExplorerKueryBar } from '../kuery_bar';
interface MetricRowWithAggProps extends MetricRowBaseProps {
aggType?: CustomMetricAggTypes;
field?: string;
dataView: DataViewBase;
filter?: string;
fields: NormalizedFields;
}
@ -33,6 +40,8 @@ export function MetricRowWithAgg({
aggType = Aggregators.AVERAGE,
field,
onDelete,
dataView,
filter,
disableDelete,
fields,
aggregationTypes,
@ -43,6 +52,8 @@ export function MetricRowWithAgg({
onDelete(name);
}, [name, onDelete]);
const [aggTypePopoverOpen, setAggTypePopoverOpen] = useState(false);
const fieldOptions = useMemo(
() =>
fields.reduce((acc, fieldValue) => {
@ -59,15 +70,6 @@ export function MetricRowWithAgg({
[fields, aggregationTypes, aggType]
);
const aggOptions = useMemo(
() =>
Object.values(aggregationTypes).map((a) => ({
text: a.text,
value: a.value,
})),
[aggregationTypes]
);
const handleFieldChange = useCallback(
(selectedOptions: EuiComboBoxOptionOption[]) => {
onChange({
@ -80,62 +82,141 @@ export function MetricRowWithAgg({
);
const handleAggChange = useCallback(
(el: React.ChangeEvent<HTMLSelectElement>) => {
(customAggType: string) => {
onChange({
name,
field,
aggType: el.target.value as CustomMetricAggTypes,
aggType: customAggType as CustomMetricAggTypes,
});
},
[name, field, onChange]
);
const handleFilterChange = useCallback(
(filterString: string) => {
onChange({
name,
filter: filterString,
aggType,
});
},
[name, aggType, onChange]
);
const isAggInvalid = get(errors, ['customMetrics', name, 'aggType']) != null;
const isFieldInvalid = get(errors, ['customMetrics', name, 'field']) != null || !field;
return (
<>
<EuiFlexGroup gutterSize="xs" alignItems="flexEnd">
<EuiFlexItem style={{ maxWidth: 145 }}>
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel',
{ defaultMessage: 'Aggregation {name}', values: { name } }
)}
isInvalid={isAggInvalid}
<EuiFlexItem grow>
<EuiPopover
button={
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel',
{ defaultMessage: 'Aggregation {name}', values: { name } }
)}
isInvalid={aggType !== Aggregators.COUNT && !field}
>
<EuiExpression
data-test-subj="aggregationName"
description={aggregationTypes[aggType].text}
value={aggType === Aggregators.COUNT ? filter : field}
isActive={aggTypePopoverOpen}
display={'columns'}
onClick={() => {
setAggTypePopoverOpen(true);
}}
/>
</EuiFormRow>
}
isOpen={aggTypePopoverOpen}
closePopover={() => {
setAggTypePopoverOpen(false);
}}
display="block"
ownFocus
anchorPosition={'downLeft'}
repositionOnScroll
>
<EuiSelect
data-test-subj="thresholdRuleMetricRowWithAggSelect"
compressed
options={aggOptions}
value={aggType}
isInvalid={isAggInvalid}
onChange={handleAggChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel',
{ defaultMessage: 'Field {name}', values: { name } }
)}
isInvalid={isFieldInvalid}
>
<EuiComboBox
fullWidth
compressed
isInvalid={isFieldInvalid}
singleSelection={{ asPlainText: true }}
options={fieldOptions}
selectedOptions={field ? [{ label: field }] : []}
onChange={handleFieldChange}
/>
</EuiFormRow>
<div>
<ClosablePopoverTitle onClose={() => setAggTypePopoverOpen(false)}>
<FormattedMessage
id="xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel"
defaultMessage="Aggregation {name}"
values={{ name }}
/>
</ClosablePopoverTitle>
<EuiFlexGroup gutterSize="l" alignItems="flexEnd">
<EuiFlexItem grow>
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationType',
{ defaultMessage: 'Aggregation type' }
)}
isInvalid={isAggInvalid}
>
<EuiSelect
data-test-subj="aggregationTypeSelect"
id="aggTypeField"
value={aggType}
fullWidth
onChange={(e) => {
handleAggChange(e.target.value);
}}
options={Object.values(aggregationTypes).map(({ text, value }) => {
return {
text,
value,
};
})}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: 300 }}>
{aggType === Aggregators.COUNT ? (
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel',
{ defaultMessage: 'KQL Filter {name}', values: { name } }
)}
>
<MetricsExplorerKueryBar
placeholder={' '}
derivedIndexPattern={dataView}
onChange={handleFilterChange}
onSubmit={handleFilterChange}
value={filter}
/>
</EuiFormRow>
) : (
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel',
{ defaultMessage: 'Field name' }
)}
isInvalid={isFieldInvalid}
>
<EuiComboBox
fullWidth
isInvalid={isFieldInvalid}
singleSelection={{ asPlainText: true }}
options={fieldOptions}
selectedOptions={field ? [{ label: field }] : []}
onChange={handleFieldChange}
/>
</EuiFormRow>
)}
</EuiFlexItem>
</EuiFlexGroup>
</div>
</EuiPopover>
</EuiFlexItem>
<MetricRowControls onDelete={handleDelete} disableDelete={disableDelete} />
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
</>
);
}

View file

@ -1,110 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFormRow, EuiHorizontalRule, EuiFlexItem, EuiFlexGroup, EuiSelect } from '@elastic/eui';
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { DataViewBase } from '@kbn/es-query';
import { Aggregators, CustomMetricAggTypes } from '../../../../../common/threshold_rule/types';
import { MetricRowControls } from './metric_row_controls';
import { MetricRowBaseProps } from './types';
import { MetricsExplorerKueryBar } from '../kuery_bar';
interface MetricRowWithCountProps extends MetricRowBaseProps {
agg?: Aggregators;
filter?: string;
dataView: DataViewBase;
}
export function MetricRowWithCount({
name,
agg,
filter,
onDelete,
disableDelete,
onChange,
aggregationTypes,
dataView,
}: MetricRowWithCountProps) {
const aggOptions = useMemo(
() =>
Object.values(aggregationTypes)
.filter((aggType) => aggType.value !== Aggregators.CUSTOM)
.map((aggType) => ({
text: aggType.text,
value: aggType.value,
})),
[aggregationTypes]
);
const handleDelete = useCallback(() => {
onDelete(name);
}, [name, onDelete]);
const handleAggChange = useCallback(
(el: React.ChangeEvent<HTMLSelectElement>) => {
onChange({
name,
filter,
aggType: el.target.value as CustomMetricAggTypes,
});
},
[name, filter, onChange]
);
const handleFilterChange = useCallback(
(filterString: string) => {
onChange({
name,
filter: filterString,
aggType: agg as CustomMetricAggTypes,
});
},
[name, agg, onChange]
);
return (
<>
<EuiFlexGroup gutterSize="xs" alignItems="flexEnd">
<EuiFlexItem style={{ maxWidth: 145 }}>
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel',
{ defaultMessage: 'Aggregation {name}', values: { name } }
)}
>
<EuiSelect
data-test-subj="thresholdRuleMetricRowWithCountSelect"
compressed
options={aggOptions}
value={agg}
onChange={handleAggChange}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem>
<EuiFormRow
label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel',
{ defaultMessage: 'KQL Filter {name}', values: { name } }
)}
>
<MetricsExplorerKueryBar
placeholder={' '}
compressed
derivedIndexPattern={dataView}
onChange={handleFilterChange}
onSubmit={handleFilterChange}
value={filter}
/>
</EuiFormRow>
</EuiFlexItem>
<MetricRowControls onDelete={handleDelete} disableDelete={disableDelete} />
</EuiFlexGroup>
<EuiHorizontalRule margin="xs" />
</>
);
}

View file

@ -67,15 +67,16 @@ describe('ExpressionRow', () => {
threshold: [0.5],
timeSize: 1,
timeUnit: 'm',
aggType: 'avg',
aggType: 'custom',
};
const { wrapper, update } = await setup(expression as MetricExpression);
await update();
const [valueMatch] =
wrapper
.html()
.match('<span class="euiExpression__value css-1lfq7nz-euiExpression__value">50</span>') ??
[];
.match(
'<span class="euiExpression__value css-uocz3u-euiExpression__value-columns">50</span>'
) ?? [];
expect(valueMatch).toBeTruthy();
});
@ -86,34 +87,15 @@ describe('ExpressionRow', () => {
threshold: [0.5],
timeSize: 1,
timeUnit: 'm',
aggType: 'avg',
aggType: 'custom',
};
const { wrapper } = await setup(expression as MetricExpression);
const [valueMatch] =
wrapper
.html()
.match('<span class="euiExpression__value css-1lfq7nz-euiExpression__value">0.5</span>') ??
[];
.match(
'<span class="euiExpression__value css-uocz3u-euiExpression__value-columns">0.5</span>'
) ?? [];
expect(valueMatch).toBeTruthy();
});
it('should render a helpText for the of expression', async () => {
const expression = {
metric: 'system.load.1',
comparator: Comparator.GT,
threshold: [0.5],
timeSize: 1,
timeUnit: 'm',
aggType: 'avg',
} as MetricExpression;
const { wrapper } = await setup(expression as MetricExpression);
const helpText = wrapper
.find('[data-test-subj="thresholdRuleOfExpression"]')
.at(0)
.prop('helpText');
expect(helpText).toMatchSnapshot();
});
});

View file

@ -6,30 +6,28 @@
*/
import {
EuiButtonIcon,
EuiExpression,
EuiFieldText,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiFormRow,
EuiSpacer,
EuiText,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import {
AggregationType,
builtInComparators,
IErrorObject,
OfExpression,
ThresholdExpression,
} from '@kbn/triggers-actions-ui-plugin/public';
import { DataViewBase } from '@kbn/es-query';
import useToggle from 'react-use/lib/useToggle';
import { Aggregators, Comparator } from '../../../../common/threshold_rule/types';
import { debounce } from 'lodash';
import { Comparator } from '../../../../common/threshold_rule/types';
import { AGGREGATION_TYPES, DerivedIndexPattern, MetricExpression } from '../types';
import { CustomEquationEditor } from './custom_equation';
import { CUSTOM_EQUATION } from '../i18n_strings';
import { CUSTOM_EQUATION, LABEL_HELP_MESSAGE, LABEL_LABEL } from '../i18n_strings';
import { decimalToPct, pctToDecimal } from '../helpers/corrected_percent_convert';
const customComparators = {
@ -62,14 +60,8 @@ const StyledExpressionRow = euiStyled(EuiFlexGroup)`
margin: 0 -4px;
`;
const StyledExpression = euiStyled.div`
padding: 0 4px;
`;
// eslint-disable-next-line react/function-component-definition
export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
const [isExpanded, toggle] = useToggle(true);
const {
dataView,
children,
@ -82,21 +74,10 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
canDelete,
} = props;
const {
aggType = AGGREGATION_TYPES.MAX,
metric,
comparator = Comparator.GT,
threshold = [],
} = expression;
const { metric, comparator = Comparator.GT, threshold = [] } = expression;
const isMetricPct = useMemo(() => Boolean(metric && metric.endsWith('.pct')), [metric]);
const updateMetric = useCallback(
(m?: MetricExpression['metric']) => {
setRuleParams(expressionId, { ...expression, metric: m });
},
[expressionId, expression, setRuleParams]
);
const [label, setLabel] = useState<string | undefined>(expression?.label || undefined);
const updateComparator = useCallback(
(c?: string) => {
@ -127,6 +108,10 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
},
[expressionId, setRuleParams]
);
const debouncedLabelChange = useMemo(
() => debounce(handleCustomMetricChange, 300),
[handleCustomMetricChange]
);
const criticalThresholdExpression = (
<ThresholdElement
@ -144,89 +129,46 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
name: f.name,
}));
const handleLabelChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
debouncedLabelChange({ ...expression, label: e.target.value });
},
[debouncedLabelChange, expression]
);
return (
<>
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiButtonIcon
iconType={isExpanded ? 'arrowDown' : 'arrowRight'}
onClick={toggle}
data-test-subj="thresholdRuleExpandRow"
aria-label={i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.expandRowLabel',
{
defaultMessage: 'Expand row.',
}
)}
/>
</EuiFlexItem>
<EuiFlexItem grow>
<StyledExpressionRow style={{ gap: aggType !== 'custom' ? 24 : 12 }}>
<StyledExpression>
<EuiExpression
data-test-subj="thresholdRuleCustomEquationWhen"
description={i18n.translate(
'xpack.observability.thresholdRule.expressionItems.descriptionLabel',
{
defaultMessage: 'when',
}
)}
value={aggregationType.custom.text}
display={'inline'}
/>
</StyledExpression>
{!['count', 'custom'].includes(aggType) && (
<StyledExpression>
<OfExpression
customAggTypesOptions={aggregationType}
aggField={metric}
fields={normalizedFields}
aggType={aggType}
errors={errors}
onChangeSelectedAggField={updateMetric}
helpText={
<FormattedMessage
id="xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail"
defaultMessage="Can't find a metric? {documentationLink}."
values={{
documentationLink: (
<EuiLink
data-test-subj="thresholdRuleExpressionRowLearnHowToAddMoreDataLink"
href="https://www.elastic.co/guide/en/observability/current/configure-settings.html"
target="BLANK"
>
<FormattedMessage
id="xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel"
defaultMessage="Learn how to add more data"
/>
</EuiLink>
),
}}
/>
}
data-test-subj="thresholdRuleOfExpression"
/>
</StyledExpression>
)}
<StyledExpressionRow style={{ gap: 24 }} />
<>
<EuiSpacer size={'xs'} />
<CustomEquationEditor
expression={expression}
fields={normalizedFields}
aggregationTypes={aggregationType}
onChange={handleCustomMetricChange}
errors={errors}
dataView={dataView}
/>
{criticalThresholdExpression}
</StyledExpressionRow>
{aggType === Aggregators.CUSTOM && (
<>
<EuiSpacer size={'m'} />
<StyledExpressionRow>
<CustomEquationEditor
expression={expression}
fields={normalizedFields}
aggregationTypes={aggregationType}
onChange={handleCustomMetricChange}
errors={errors}
dataView={dataView}
/>
</StyledExpressionRow>
<EuiSpacer size={'s'} />
</>
)}
<EuiSpacer size={'s'} />
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label={LABEL_LABEL} fullWidth helpText={LABEL_HELP_MESSAGE}>
<EuiFieldText
data-test-subj="thresholdRuleCustomEquationEditorFieldText"
compressed
fullWidth
value={label}
placeholder={CUSTOM_EQUATION}
onChange={handleLabelChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
</>
</EuiFlexItem>
{canDelete && (
<EuiFlexItem grow={false}>
@ -244,7 +186,7 @@ export const ExpressionRow: React.FC<ExpressionRowProps> = (props) => {
</EuiFlexItem>
)}
</EuiFlexGroup>
{isExpanded ? <div style={{ padding: '0 0 0 28px' }}>{children}</div> : null}
{children}
<EuiSpacer size={'s'} />
</>
);
@ -266,16 +208,16 @@ const ThresholdElement: React.FC<{
return (
<>
<StyledExpression>
<ThresholdExpression
thresholdComparator={comparator || Comparator.GT}
threshold={displayedThreshold}
customComparators={customComparators}
onChangeSelectedThresholdComparator={updateComparator}
onChangeSelectedThreshold={updateThreshold}
errors={errors}
/>
</StyledExpression>
<ThresholdExpression
thresholdComparator={comparator || Comparator.GT}
threshold={displayedThreshold}
customComparators={customComparators}
onChangeSelectedThresholdComparator={updateComparator}
onChangeSelectedThreshold={updateThreshold}
errors={errors}
display="fullWidth"
/>
{isMetricPct && (
<div
style={{

View file

@ -20,7 +20,7 @@ export const LABEL_LABEL = i18n.translate(
export const LABEL_HELP_MESSAGE = i18n.translate(
'xpack.observability.threshold.rule.alertFlyout.customEquationEditor.labelHelpMessage',
{
defaultMessage: 'Custom label will show on the alert chart and in reason/alert title',
defaultMessage: 'Custom label will show on the alert chart and in reason',
}
);

View file

@ -381,42 +381,56 @@ export default function Expressions(props: Props) {
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
{ruleParams.criteria &&
ruleParams.criteria.map((e, idx) => {
return (
<ExpressionRow
canDelete={(ruleParams.criteria && ruleParams.criteria.length > 1) || false}
fields={derivedIndexPattern.fields as any}
remove={removeExpression}
addExpression={addExpression}
key={idx} // idx's don't usually make good key's but here the index has semantic meaning
expressionId={idx}
setRuleParams={updateParams}
errors={(errors[idx] as IErrorObject) || emptyError}
expression={e || {}}
dataView={derivedIndexPattern}
>
{/* Preview */}
<ExpressionChart
expression={e}
derivedIndexPattern={derivedIndexPattern}
filterQuery={ruleParams.filterQuery}
groupBy={ruleParams.groupBy}
timeFieldName={dataView?.timeFieldName}
/>
</ExpressionRow>
<div key={idx}>
{/* index has semantic meaning, we show the condition title starting from the 2nd one */}
{idx >= 1 && (
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.observability.threshold.rule.alertFlyout.condition"
defaultMessage="Condition {conditionNumber}"
values={{ conditionNumber: idx + 1 }}
/>
</h5>
</EuiTitle>
)}
<ExpressionRow
canDelete={(ruleParams.criteria && ruleParams.criteria.length > 1) || false}
fields={derivedIndexPattern.fields as any}
remove={removeExpression}
addExpression={addExpression}
key={idx} // idx's don't usually make good key's but here the index has semantic meaning
expressionId={idx}
setRuleParams={updateParams}
errors={(errors[idx] as IErrorObject) || emptyError}
expression={e || {}}
dataView={derivedIndexPattern}
>
{/* Preview */}
<ExpressionChart
expression={e}
derivedIndexPattern={derivedIndexPattern}
filterQuery={ruleParams.filterQuery}
groupBy={ruleParams.groupBy}
timeFieldName={dataView?.timeFieldName}
/>
</ExpressionRow>
</div>
);
})}
<div style={{ marginLeft: 28 }}>
<ForLastExpression
timeWindowSize={timeSize}
timeWindowUnit={timeUnit}
errors={emptyError}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
/>
</div>
<ForLastExpression
timeWindowSize={timeSize}
timeWindowUnit={timeUnit}
errors={emptyError}
onChangeWindowSize={updateTimeSize}
onChangeWindowUnit={updateTimeUnit}
display="fullWidth"
/>
<EuiSpacer size="m" />
<div>
<EuiButtonEmpty

View file

@ -27294,9 +27294,7 @@
"xpack.observability.threshold.rule.alertDetailsAppSection.criterion.subtitle": "Dernière {lookback} {timeLabel}",
"xpack.observability.threshold.rule.alertFlyout.alertPerRedundantFilterError": "Il est possible que cette règle signale {matchedGroups} moins que prévu, car la requête de filtre comporte une correspondance pour {groupCount, plural, one {ce champ} many {ces champs} other {ces champs}}. Pour en savoir plus, consultez notre {filteringAndGroupingLink}.",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel": "Agrégation {name}",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel": "Champ {name}",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel": "Filtre KQL {name}",
"xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail": "Vous ne trouvez pas un indicateur ? {documentationLink}.",
"xpack.observability.threshold.rule.alerts.dataTimeRangeLabel": "Dernière {lookback} {timeLabel}",
"xpack.observability.threshold.rule.alerts.dataTimeRangeLabelWithGrouping": "Dernières {lookback} {timeLabel} de données pour {id}",
"xpack.observability.threshold.rule.threshold.errorAlertReason": "Elasticsearch a échoué lors de l'interrogation des données pour {metric}",
@ -27809,9 +27807,7 @@
"xpack.observability.threshold.rule.alertFlyout.error.thresholdRequired": "Le seuil est requis.",
"xpack.observability.threshold.rule.alertFlyout.error.thresholdTypeRequired": "Les seuils doivent contenir un nombre valide.",
"xpack.observability.threshold.rule.alertFlyout.error.timeRequred": "La taille de temps est requise.",
"xpack.observability.threshold.rule.alertFlyout.expandRowLabel": "Développer la ligne.",
"xpack.observability.threshold.rule.alertFlyout.groupDisappearHelpText": "Activez cette option pour déclencher laction si un groupe précédemment détecté cesse de signaler des résultats. Ce nest pas recommandé pour les infrastructures à montée en charge dynamique qui peuvent rapidement lancer ou stopper des nœuds automatiquement.",
"xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel": "Apprenez comment ajouter davantage de données",
"xpack.observability.threshold.rule.alertFlyout.outsideRangeLabel": "N'est pas entre",
"xpack.observability.threshold.rule.alertFlyout.removeCondition": "Retirer la condition",
"xpack.observability.threshold.rule.alerting.noDataFormattedValue": "[AUCUNE DONNÉE]",
@ -27851,7 +27847,6 @@
"xpack.observability.threshold.ruleExplorer.groupByAriaLabel": "Graphique par",
"xpack.observability.threshold.ruleExplorer.groupByLabel": "Tout",
"xpack.observability.threshold.ruleName": "Seuil (Version d'évaluation technique)",
"xpack.observability.thresholdRule.expressionItems.descriptionLabel": "quand",
"xpack.observability.uiSettings.betaLabel": "bêta",
"xpack.observability.uiSettings.technicalPreviewLabel": "version d'évaluation technique",
"xpack.observability.uiSettings.throttlingDocsLinkText": "lisez la notification ici.",

View file

@ -27294,9 +27294,7 @@
"xpack.observability.threshold.rule.alertDetailsAppSection.criterion.subtitle": "最後の{lookback} {timeLabel}",
"xpack.observability.threshold.rule.alertFlyout.alertPerRedundantFilterError": "フィルタークエリには{groupCount, plural, other {これらのフィールド}}に対する一致が含まれているため、このルールによって、想定を下回る{matchedGroups}に関するアラートが発行される場合があります。詳細については、{filteringAndGroupingLink}を参照してください。",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel": "アグリゲーション{name}",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel": "フィールド{name}",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel": "KQLフィルター{name}",
"xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail": "メトリックが見つからない場合は、{documentationLink}。",
"xpack.observability.threshold.rule.alerts.dataTimeRangeLabel": "最後の{lookback} {timeLabel}",
"xpack.observability.threshold.rule.alerts.dataTimeRangeLabelWithGrouping": "{id}のデータの最後の{lookback} {timeLabel}",
"xpack.observability.threshold.rule.threshold.errorAlertReason": "{metric}のデータのクエリを試行しているときに、Elasticsearchが失敗しました",
@ -27809,9 +27807,7 @@
"xpack.observability.threshold.rule.alertFlyout.error.thresholdRequired": "しきい値が必要です。",
"xpack.observability.threshold.rule.alertFlyout.error.thresholdTypeRequired": "しきい値には有効な数値を含める必要があります。",
"xpack.observability.threshold.rule.alertFlyout.error.timeRequred": "ページサイズが必要です。",
"xpack.observability.threshold.rule.alertFlyout.expandRowLabel": "行を展開します。",
"xpack.observability.threshold.rule.alertFlyout.groupDisappearHelpText": "以前に検出されたグループが結果を報告しなくなった場合は、これを有効にすると、アクションがトリガーされます。自動的に急速にノードを開始および停止することがある動的に拡張するインフラストラクチャーでは、これは推奨されません。",
"xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel": "データの追加方法",
"xpack.observability.threshold.rule.alertFlyout.outsideRangeLabel": "is not between",
"xpack.observability.threshold.rule.alertFlyout.removeCondition": "条件を削除",
"xpack.observability.threshold.rule.alerting.noDataFormattedValue": "[データなし]",
@ -27851,7 +27847,6 @@
"xpack.observability.threshold.ruleExplorer.groupByAriaLabel": "graph/",
"xpack.observability.threshold.ruleExplorer.groupByLabel": "すべて",
"xpack.observability.threshold.ruleName": "しきい値(テクニカルプレビュー)",
"xpack.observability.thresholdRule.expressionItems.descriptionLabel": "タイミング",
"xpack.observability.uiSettings.betaLabel": "ベータ",
"xpack.observability.uiSettings.technicalPreviewLabel": "テクニカルプレビュー",
"xpack.observability.uiSettings.throttlingDocsLinkText": "こちらで通知をお読みください。",

View file

@ -27292,9 +27292,7 @@
"xpack.observability.threshold.rule.alertDetailsAppSection.criterion.subtitle": "过去 {lookback} {timeLabel}",
"xpack.observability.threshold.rule.alertFlyout.alertPerRedundantFilterError": "此规则可能针对低于预期的 {matchedGroups} 告警,因为筛选查询包含{groupCount, plural, other {这些字段}}的匹配项。有关更多信息,请参阅 {filteringAndGroupingLink}。",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.aggregationLabel": "聚合 {name}",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.fieldLabel": "字段 {name}",
"xpack.observability.threshold.rule.alertFlyout.customEquationEditor.filterLabel": "KQL 筛选 {name}",
"xpack.observability.threshold.rule.alertFlyout.ofExpression.helpTextDetail": "找不到指标?{documentationLink}。",
"xpack.observability.threshold.rule.alerts.dataTimeRangeLabel": "过去 {lookback} {timeLabel}",
"xpack.observability.threshold.rule.alerts.dataTimeRangeLabelWithGrouping": "{id} 过去 {lookback} {timeLabel}的数据",
"xpack.observability.threshold.rule.threshold.errorAlertReason": "Elasticsearch 尝试查询 {metric} 的数据时出现故障",
@ -27807,9 +27805,7 @@
"xpack.observability.threshold.rule.alertFlyout.error.thresholdRequired": "“阈值”必填。",
"xpack.observability.threshold.rule.alertFlyout.error.thresholdTypeRequired": "阈值必须包含有效数字。",
"xpack.observability.threshold.rule.alertFlyout.error.timeRequred": "“时间大小”必填。",
"xpack.observability.threshold.rule.alertFlyout.expandRowLabel": "展开行。",
"xpack.observability.threshold.rule.alertFlyout.groupDisappearHelpText": "启用此选项可在之前检测的组开始不报告任何数据时触发操作。不建议将此选项用于可能会快速自动启动和停止节点的动态扩展基础架构。",
"xpack.observability.threshold.rule.alertFlyout.ofExpression.popoverLinkLabel": "了解如何添加更多数据",
"xpack.observability.threshold.rule.alertFlyout.outsideRangeLabel": "不介于",
"xpack.observability.threshold.rule.alertFlyout.removeCondition": "删除条件",
"xpack.observability.threshold.rule.alerting.noDataFormattedValue": "[无数据]",
@ -27849,7 +27845,6 @@
"xpack.observability.threshold.ruleExplorer.groupByAriaLabel": "图表绘制依据",
"xpack.observability.threshold.ruleExplorer.groupByLabel": "所有内容",
"xpack.observability.threshold.ruleName": "阈值(技术预览)",
"xpack.observability.thresholdRule.expressionItems.descriptionLabel": "当",
"xpack.observability.uiSettings.betaLabel": "公测版",
"xpack.observability.uiSettings.technicalPreviewLabel": "技术预览",
"xpack.observability.uiSettings.throttlingDocsLinkText": "在此处阅读通知。",