[7.x] [Uptime] Filters in create alert flyout (#64753) (#65240)

This commit is contained in:
Shahzad 2020-05-05 13:49:57 +02:00 committed by GitHub
parent 742cca48a3
commit 6297981519
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1291 additions and 636 deletions

View file

@ -16007,7 +16007,6 @@
"xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteTitle": "クラスターがアップグレードされました",
"xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription": "1 つまたは複数の Elasticsearch ノードに、 Kibana よりも新しいバージョンの Elasticsearch があります。すべてのノードがアップグレードされた後で Kibana をアップグレードしてください。",
"xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle": "クラスターをアップグレード中です",
"xpack.uptime.alerts.locationSelectionItem.ariaLabel": "「{location}」の場所選択項目",
"xpack.uptime.alerts.message.emptyTitle": "停止状況監視 ID を受信していません。",
"xpack.uptime.alerts.message.fullListOverflow": "... とその他 {overflowCount} {pluralizedMonitor}",
"xpack.uptime.alerts.message.multipleTitle": "停止状況監視: ",
@ -16015,9 +16014,6 @@
"xpack.uptime.alerts.message.singularTitle": "停止状況監視: ",
"xpack.uptime.alerts.monitorStatus": "稼働状況監視ステータス",
"xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "監視状態アラートのフィルター基準を許可するインプット",
"xpack.uptime.alerts.monitorStatus.locationSelection": "場所 {location} を選択します",
"xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel": "アラートをトリガーする場所を選択します",
"xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel": "ポップオーバーを開いてアラートをトリガーする場所を選択する",
"xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel": "ダウンカウントインプットのポップオーバーを開く",
"xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel": "アラートのトリガーに必要な停止回数を入力します",
"xpack.uptime.alerts.monitorStatus.timerangeOption.days": "日",

View file

@ -16015,7 +16015,6 @@
"xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradeCompleteTitle": "您的集群已升级",
"xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingDescription": "一个或多个 Elasticsearch 节点的 Elasticsearch 版本比 Kibana 版本新。所有节点升级后,请升级 Kibana。",
"xpack.upgradeAssistant.tabs.upgradingInterstitial.upgradingTitle": "您的集群正在升级",
"xpack.uptime.alerts.locationSelectionItem.ariaLabel": "“{location}”的位置选择项",
"xpack.uptime.alerts.message.emptyTitle": "未接收到已关闭监测 ID",
"xpack.uptime.alerts.message.fullListOverflow": "...以及 {overflowCount} 个其他{pluralizedMonitor}",
"xpack.uptime.alerts.message.multipleTitle": "已关闭监测: ",
@ -16023,9 +16022,6 @@
"xpack.uptime.alerts.message.singularTitle": "已关闭监测: ",
"xpack.uptime.alerts.monitorStatus": "运行时间监测状态",
"xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel": "允许对监测状态告警使用筛选条件的输入",
"xpack.uptime.alerts.monitorStatus.locationSelection": "选择位置 {location}",
"xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel": "选择告警应触发的位置",
"xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel": "打开弹出框以选择告警应触发的位置",
"xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel": "打开弹出框以输入已关闭计数",
"xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel": "输入触发告警的已关闭计数",
"xpack.uptime.alerts.monitorStatus.timerangeOption.days": "天",

View file

@ -104,7 +104,7 @@ export interface AlertTableItem extends Alert {
export interface AlertTypeModel {
id: string;
name: string;
name: string | JSX.Element;
iconClass: string;
validate: (alertParams: any) => ValidationResult;
alertParamsExpression: React.FunctionComponent<any>;

View file

@ -5,12 +5,8 @@
*/
import React from 'react';
import {
selectedLocationsToString,
AlertFieldNumber,
handleAlertFieldNumberChange,
} from '../alert_monitor_status';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { AlertFieldNumber, handleAlertFieldNumberChange } from '../alert_field_number';
describe('alert monitor status component', () => {
describe('handleAlertFieldNumberChange', () => {
@ -146,34 +142,4 @@ describe('alert monitor status component', () => {
expect(mockValueHandler.mock.calls).toEqual([]);
});
});
describe('selectedLocationsToString', () => {
it('generates a formatted string for a valid list of options', () => {
const locations = [
{
checked: 'on',
label: 'fairbanks',
},
{
checked: 'on',
label: 'harrisburg',
},
{
checked: undefined,
label: 'orlando',
},
];
expect(selectedLocationsToString(locations)).toEqual('fairbanks, harrisburg');
});
it('generates a formatted string for a single item', () => {
expect(selectedLocationsToString([{ checked: 'on', label: 'fairbanks' }])).toEqual(
'fairbanks'
);
});
it('returns an empty string when no valid options are available', () => {
expect(selectedLocationsToString([{ checked: 'off', label: 'harrisburg' }])).toEqual('');
});
});
});

View file

@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiButtonEmpty, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { useFilterUpdate } from '../../../hooks/use_filter_update';
import * as labels from './translations';
interface Props {
newFilters: string[];
onNewFilter: (val: string) => void;
}
export const AddFilterButton: React.FC<Props> = ({ newFilters, onNewFilter }) => {
const [isPopoverOpen, setPopover] = useState(false);
const currentFilters = useFilterUpdate();
const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || [];
const onButtonClick = () => {
setPopover(!isPopoverOpen);
};
const closePopover = () => {
setPopover(false);
};
const items: JSX.Element[] = [];
const allFilters = [
{ id: 'observer.geo.name', label: labels.LOCATION },
{ id: 'tags', label: labels.TAG },
{ id: 'url.port', label: labels.PORT },
{ id: 'monitor.type', label: labels.TYPE },
];
allFilters.forEach(filter => {
if (getSelectedItems(filter.id)?.length === 0 && !newFilters.includes(filter.id)) {
items.push(
<EuiContextMenuItem
data-test-subj={'uptimeAlertAddFilter.' + filter.id}
key={filter.id}
onClick={() => {
closePopover();
onNewFilter(filter.id);
}}
>
{filter.label}
</EuiContextMenuItem>
);
}
});
const button = (
<EuiButtonEmpty
data-test-subj="uptimeCreateAlertAddFilter"
disabled={items.length === 0}
iconType="plusInCircleFilled"
onClick={onButtonClick}
>
{labels.ADD_FILTER}
</EuiButtonEmpty>
);
return (
<EuiPopover
id="singlePanel"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel items={items} />
</EuiPopover>
);
};

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiExpression, EuiPopover } from '@elastic/eui';
interface AlertExpressionPopoverProps {
'aria-label': string;
content: React.ReactElement;
description: string;
'data-test-subj': string;
id: string;
value: string;
}
export const AlertExpressionPopover: React.FC<AlertExpressionPopoverProps> = ({
'aria-label': ariaLabel,
content,
'data-test-subj': dataTestSubj,
description,
id,
value,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
return (
<EuiPopover
id={id}
anchorPosition="downLeft"
button={
<EuiExpression
aria-label={ariaLabel}
color={isOpen ? 'primary' : 'secondary'}
data-test-subj={dataTestSubj}
description={description}
isActive={isOpen}
onClick={() => setIsOpen(!isOpen)}
value={value}
/>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
>
{content}
</EuiPopover>
);
};

View file

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState } from 'react';
import { EuiFieldNumber } from '@elastic/eui';
interface AlertFieldNumberProps {
'aria-label': string;
'data-test-subj': string;
disabled: boolean;
fieldValue: number;
setFieldValue: React.Dispatch<React.SetStateAction<number>>;
}
export const handleAlertFieldNumberChange = (
e: React.ChangeEvent<HTMLInputElement>,
isInvalid: boolean,
setIsInvalid: React.Dispatch<React.SetStateAction<boolean>>,
setFieldValue: React.Dispatch<React.SetStateAction<number>>
) => {
const num = parseInt(e.target.value, 10);
if (isNaN(num) || num < 1) {
setIsInvalid(true);
} else {
if (isInvalid) setIsInvalid(false);
setFieldValue(num);
}
};
export const AlertFieldNumber = ({
'aria-label': ariaLabel,
'data-test-subj': dataTestSubj,
disabled,
fieldValue,
setFieldValue,
}: AlertFieldNumberProps) => {
const [isInvalid, setIsInvalid] = useState<boolean>(false);
return (
<EuiFieldNumber
aria-label={ariaLabel}
compressed
data-test-subj={dataTestSubj}
min={1}
onChange={e => handleAlertFieldNumberChange(e, isInvalid, setIsInvalid, setFieldValue)}
disabled={disabled}
value={fieldValue}
isInvalid={isInvalid}
/>
);
};

View file

@ -4,123 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, useEffect } from 'react';
import {
EuiExpression,
EuiFieldNumber,
EuiFlexGroup,
EuiFlexItem,
EuiPopover,
EuiSelectable,
EuiSpacer,
EuiSwitch,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useEffect, useState } from 'react';
import { EuiSpacer } from '@elastic/eui';
import { DataPublicPluginSetup } from 'src/plugins/data/public';
import * as labels from './translations';
import {
DownNoExpressionSelect,
TimeExpressionSelect,
FiltersExpressionsSelect,
} from './monitor_expressions';
import { AddFilterButton } from './add_filter_btn';
import { KueryBar } from '..';
interface AlertFieldNumberProps {
'aria-label': string;
'data-test-subj': string;
disabled: boolean;
fieldValue: number;
setFieldValue: React.Dispatch<React.SetStateAction<number>>;
}
export const handleAlertFieldNumberChange = (
e: React.ChangeEvent<HTMLInputElement>,
isInvalid: boolean,
setIsInvalid: React.Dispatch<React.SetStateAction<boolean>>,
setFieldValue: React.Dispatch<React.SetStateAction<number>>
) => {
const num = parseInt(e.target.value, 10);
if (isNaN(num) || num < 1) {
setIsInvalid(true);
} else {
if (isInvalid) setIsInvalid(false);
setFieldValue(num);
}
};
export const AlertFieldNumber = ({
'aria-label': ariaLabel,
'data-test-subj': dataTestSubj,
disabled,
fieldValue,
setFieldValue,
}: AlertFieldNumberProps) => {
const [isInvalid, setIsInvalid] = useState<boolean>(false);
return (
<EuiFieldNumber
aria-label={ariaLabel}
compressed
data-test-subj={dataTestSubj}
min={1}
onChange={e => handleAlertFieldNumberChange(e, isInvalid, setIsInvalid, setFieldValue)}
disabled={disabled}
value={fieldValue}
isInvalid={isInvalid}
/>
);
};
interface AlertExpressionPopoverProps {
'aria-label': string;
content: React.ReactElement;
description: string;
'data-test-subj': string;
id: string;
value: string;
}
const AlertExpressionPopover: React.FC<AlertExpressionPopoverProps> = ({
'aria-label': ariaLabel,
content,
'data-test-subj': dataTestSubj,
description,
id,
value,
}) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
return (
<EuiPopover
id={id}
anchorPosition="downLeft"
button={
<EuiExpression
aria-label={ariaLabel}
color={isOpen ? 'primary' : 'secondary'}
data-test-subj={dataTestSubj}
description={description}
isActive={isOpen}
onClick={() => setIsOpen(!isOpen)}
value={value}
/>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
>
{content}
</EuiPopover>
);
};
export const selectedLocationsToString = (selectedLocations: any[]) =>
// create a nicely-formatted description string for all `on` locations
selectedLocations
.filter(({ checked }) => checked === 'on')
.map(({ label }) => label)
.sort()
.reduce((acc, cur) => {
if (acc === '') {
return cur;
}
return acc + `, ${cur}`;
}, '');
interface AlertMonitorStatusProps {
autocomplete: DataPublicPluginSetup['autocomplete'];
enabled: boolean;
@ -135,101 +31,9 @@ interface AlertMonitorStatusProps {
}
export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = props => {
const { filters, locations } = props;
const [numTimes, setNumTimes] = useState<number>(5);
const [numMins, setNumMins] = useState<number>(15);
const [allLabels, setAllLabels] = useState<boolean>(true);
const { filters, setAlertParams } = props;
// locations is an array of `Option[]`, but that type doesn't seem to be exported by EUI
const [selectedLocations, setSelectedLocations] = useState<any[]>(
locations.map(location => ({
'aria-label': i18n.translate('xpack.uptime.alerts.locationSelectionItem.ariaLabel', {
defaultMessage: 'Location selection item for "{location}"',
values: {
location,
},
}),
disabled: allLabels,
label: location,
}))
);
const [timerangeUnitOptions, setTimerangeUnitOptions] = useState<any[]>([
{
'aria-label': i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.secondsOption.ariaLabel',
{
defaultMessage: '"Seconds" time range select item',
}
),
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption',
key: 's',
label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.seconds', {
defaultMessage: 'seconds',
}),
},
{
'aria-label': i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel',
{
defaultMessage: '"Minutes" time range select item',
}
),
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption',
checked: 'on',
key: 'm',
label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.minutes', {
defaultMessage: 'minutes',
}),
},
{
'aria-label': i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel',
{
defaultMessage: '"Hours" time range select item',
}
),
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption',
key: 'h',
label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.hours', {
defaultMessage: 'hours',
}),
},
{
'aria-label': i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel',
{
defaultMessage: '"Days" time range select item',
}
),
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption',
key: 'd',
label: i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.days', {
defaultMessage: 'days',
}),
},
]);
const { setAlertParams } = props;
useEffect(() => {
setAlertParams('numTimes', numTimes);
}, [numTimes, setAlertParams]);
useEffect(() => {
const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm';
setAlertParams('timerange', { from: `now-${numMins}${timerangeUnit}`, to: 'now' });
}, [numMins, timerangeUnitOptions, setAlertParams]);
useEffect(() => {
if (allLabels) {
setAlertParams('locations', []);
} else {
setAlertParams(
'locations',
selectedLocations.filter(l => l.checked === 'on').map(l => l.label)
);
}
}, [selectedLocations, setAlertParams, allLabels]);
const [newFilters, setNewFilters] = useState<string[]>([]);
useEffect(() => {
setAlertParams('filters', filters);
@ -239,207 +43,41 @@ export const AlertMonitorStatusComponent: React.FC<AlertMonitorStatusProps> = pr
<>
<EuiSpacer size="m" />
<KueryBar
aria-label={i18n.translate('xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel', {
defaultMessage: 'Input that allows filtering criteria for the monitor status alert',
})}
aria-label={labels.ALERT_KUERY_BAR_ARIA}
autocomplete={props.autocomplete}
data-test-subj="xpack.uptime.alerts.monitorStatus.filterBar"
/>
<EuiSpacer size="s" />
<AlertExpressionPopover
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel',
{
defaultMessage: 'Open the popover for down count input',
<DownNoExpressionSelect filters={filters} setAlertParams={setAlertParams} />
<EuiSpacer size="xs" />
<TimeExpressionSelect setAlertParams={setAlertParams} />
<EuiSpacer size="xs" />
<FiltersExpressionsSelect
setAlertParams={setAlertParams}
newFilters={newFilters}
onRemoveFilter={removeFiler => {
if (newFilters.includes(removeFiler)) {
setNewFilters(newFilters.filter(item => item !== removeFiler));
}
)}
content={
<AlertFieldNumber
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel',
{
defaultMessage: 'Enter number of down counts required to trigger the alert',
}
)}
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesField"
disabled={false}
fieldValue={numTimes}
setFieldValue={setNumTimes}
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression"
description={
filters
? i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description',
{
defaultMessage: 'matching monitors are down >',
}
)
: i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description',
{
defaultMessage: 'any monitor is down >',
}
)
}
id="ping-count"
value={`${numTimes} times`}
}}
/>
<EuiSpacer size="xs" />
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AlertExpressionPopover
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeValueExpression.ariaLabel',
{
defaultMessage: 'Open the popover for time range value field',
}
)}
content={
<AlertFieldNumber
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeValueField.ariaLabel',
{
defaultMessage: `Enter the number of time units for the alert's range`,
}
)}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueField"
disabled={false}
fieldValue={numMins}
setFieldValue={setNumMins}
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression"
description="within"
id="timerange"
value={`last ${numMins}`}
/>
</EuiFlexItem>
<EuiFlexItem>
<AlertExpressionPopover
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeUnitExpression.ariaLabel',
{
defaultMessage: 'Open the popover for time range unit select field',
}
)}
content={
<>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.timerangeSelectionHeader"
defaultMessage="Select time range unit"
/>
</h5>
</EuiTitle>
<EuiSelectable
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable',
{
defaultMessage: 'Selectable field for the time range units alerts should use',
}
)}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable"
options={timerangeUnitOptions}
onChange={newOptions => {
if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) {
setTimerangeUnitOptions(newOptions);
}
}}
singleSelection={true}
listProps={{
showIcons: true,
}}
>
{list => list}
</EuiSelectable>
</>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression"
description=""
id="timerange-unit"
value={
timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ??
''
}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
{selectedLocations.length === 0 && (
<EuiExpression
color="secondary"
data-test-subj="xpack.uptime.alerts.monitorStatus.locationsEmpty"
description="in"
isActive={false}
value="all locations"
/>
)}
{selectedLocations.length > 0 && (
<AlertExpressionPopover
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.locationsSelectionExpression.ariaLabel',
{
defaultMessage: 'Open the popover to select locations the alert should trigger',
}
)}
content={
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiSwitch
aria-label={i18n.translate(
'xpack.uptime.alerts.monitorStatus.locationSelectionSwitch.ariaLabel',
{
defaultMessage: 'Select the locations the alert should trigger',
}
)}
data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionSwitch"
label="Check all locations"
checked={allLabels}
onChange={() => {
setAllLabels(!allLabels);
setSelectedLocations(
selectedLocations.map((l: any) => ({
'aria-label': i18n.translate(
'xpack.uptime.alerts.monitorStatus.locationSelection',
{
defaultMessage: 'Select the location {location}',
values: {
location: l,
},
}
),
...l,
'data-test-subj': `xpack.uptime.alerts.monitorStatus.locationSelection.${l.label}LocationOption`,
disabled: !allLabels,
}))
);
}}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiSelectable
data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionSelectable"
options={selectedLocations}
onChange={e => setSelectedLocations(e)}
>
{location => location}
</EuiSelectable>
</EuiFlexItem>
</EuiFlexGroup>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.locationsSelectionExpression"
description="from"
id="locations"
value={
selectedLocations.length === 0 || allLabels
? 'any location'
: selectedLocationsToString(selectedLocations)
}
/>
)}
<AddFilterButton
newFilters={newFilters}
onNewFilter={newFilter => {
setNewFilters([...newFilters, newFilter]);
}}
/>
<EuiSpacer size="m" />
</>
);
};

View file

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DownNoExpressionSelect component should renders against props 1`] = `
<div
class="euiPopover euiPopover--anchorDownLeft"
id="ping-count"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="Open the popover for down count input"
class="euiExpression euiExpression-isClickable euiExpression-isUppercase euiExpression--secondary"
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression"
>
<span
class="euiExpression__description"
>
matching monitors are down &gt;
</span>
<span
class="euiExpression__value"
>
5 times
</span>
</button>
</div>
</div>
`;
exports[`DownNoExpressionSelect component should shallow renders against props 1`] = `
<AlertExpressionPopover
aria-label="Open the popover for down count input"
content={
<AlertFieldNumber
aria-label="Enter number of down counts required to trigger the alert"
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesField"
disabled={false}
fieldValue={5}
setFieldValue={[Function]}
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression"
description="matching monitors are down >"
id="ping-count"
value="5 times"
/>
`;

View file

@ -0,0 +1,160 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TimeExpressionSelect component should renders against props 1`] = `
<div
class="euiFlexGroup euiFlexGroup--gutterSmall euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiPopover euiPopover--anchorDownLeft"
id="timerange"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="Open the popover for time range value field"
class="euiExpression euiExpression-isClickable euiExpression-isUppercase euiExpression--secondary"
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression"
>
<span
class="euiExpression__description"
>
within
</span>
<span
class="euiExpression__value"
>
last 15
</span>
</button>
</div>
</div>
</div>
<div
class="euiFlexItem"
>
<div
class="euiPopover euiPopover--anchorDownLeft"
id="timerange-unit"
>
<div
class="euiPopover__anchor"
>
<button
aria-label="Open the popover for time range unit select field"
class="euiExpression euiExpression-isClickable euiExpression-isUppercase euiExpression--secondary"
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression"
>
<span
class="euiExpression__description"
/>
<span
class="euiExpression__value"
>
minutes
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`TimeExpressionSelect component should shallow renders against props 1`] = `
<EuiFlexGroup
gutterSize="s"
>
<EuiFlexItem
grow={false}
>
<AlertExpressionPopover
aria-label="Open the popover for time range value field"
content={
<AlertFieldNumber
aria-label="Enter the number of time units for the alert's range"
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueField"
disabled={false}
fieldValue={15}
setFieldValue={[Function]}
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression"
description="within"
id="timerange"
value="last 15"
/>
</EuiFlexItem>
<EuiFlexItem>
<AlertExpressionPopover
aria-label="Open the popover for time range unit select field"
content={
<React.Fragment>
<EuiTitle
size="xxs"
>
<h5>
<FormattedMessage
defaultMessage="Select time range unit"
id="xpack.uptime.alerts.monitorStatus.timerangeSelectionHeader"
values={Object {}}
/>
</h5>
</EuiTitle>
<EuiSelectable
aria-label="Selectable field for the time range units alerts should use"
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable"
listProps={
Object {
"showIcons": true,
}
}
onChange={[Function]}
options={
Array [
Object {
"aria-label": "\\"Seconds\\" time range select item",
"data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption",
"key": "s",
"label": "seconds",
},
Object {
"aria-label": "\\"Minutes\\" time range select item",
"checked": "on",
"data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption",
"key": "m",
"label": "minutes",
},
Object {
"aria-label": "\\"Hours\\" time range select item",
"data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption",
"key": "h",
"label": "hours",
},
Object {
"aria-label": "\\"Days\\" time range select item",
"data-test-subj": "xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption",
"key": "d",
"label": "days",
},
]
}
searchable={false}
singleSelection={true}
>
[Function]
</EuiSelectable>
</React.Fragment>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression"
description=""
id="timerange-unit"
value="minutes"
/>
</EuiFlexItem>
</EuiFlexGroup>
`;

View file

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { DownNoExpressionSelect } from '../down_number_select';
describe('DownNoExpressionSelect component', () => {
const filters =
'"{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"US-West"}}],"minimum_should_match":1}},' +
'{"bool":{"should":[{"match":{"url.port":443}}],"minimum_should_match":1}}]}}"';
it('should shallow renders against props', function() {
const component = shallowWithIntl(
<DownNoExpressionSelect filters={filters} setAlertParams={jest.fn()} />
);
expect(component).toMatchSnapshot();
});
it('should renders against props', function() {
const component = renderWithIntl(
<DownNoExpressionSelect filters={filters} setAlertParams={jest.fn()} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { renderWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { TimeExpressionSelect } from '../time_expression_select';
describe('TimeExpressionSelect component', () => {
it('should shallow renders against props', function() {
const component = shallowWithIntl(<TimeExpressionSelect setAlertParams={jest.fn()} />);
expect(component).toMatchSnapshot();
});
it('should renders against props', function() {
const component = renderWithIntl(<TimeExpressionSelect setAlertParams={jest.fn()} />);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState } from 'react';
import { AlertExpressionPopover } from '../alert_expression_popover';
import * as labels from '../translations';
import { AlertFieldNumber } from '../alert_field_number';
interface Props {
setAlertParams: (key: string, value: any) => void;
filters: string;
}
export const DownNoExpressionSelect: React.FC<Props> = ({ filters, setAlertParams }) => {
const [numTimes, setNumTimes] = useState<number>(5);
useEffect(() => {
setAlertParams('numTimes', numTimes);
}, [numTimes, setAlertParams]);
return (
<AlertExpressionPopover
aria-label={labels.OPEN_THE_POPOVER_DOWN_COUNT}
content={
<AlertFieldNumber
aria-label={labels.ENTER_NUMBER_OF_DOWN_COUNTS}
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesField"
disabled={false}
fieldValue={numTimes}
setFieldValue={setNumTimes}
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.numTimesExpression"
description={filters ? labels.MATCHING_MONITORS_DOWN : labels.ANY_MONITOR_DOWN}
id="ping-count"
value={`${numTimes} times`}
/>
);
};

View file

@ -0,0 +1,163 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { EuiButtonIcon, EuiExpression, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
import { FilterPopover } from '../../filter_group/filter_popover';
import { overviewFiltersSelector } from '../../../../state/selectors';
import { useFilterUpdate } from '../../../../hooks/use_filter_update';
import { filterLabels } from '../../filter_group/translations';
import { alertFilterLabels } from './translations';
interface Props {
newFilters: string[];
onRemoveFilter: (val: string) => void;
setAlertParams: (key: string, value: any) => void;
}
export const FiltersExpressionsSelect: React.FC<Props> = ({
setAlertParams,
newFilters,
onRemoveFilter,
}) => {
const { tags, ports, schemes, locations } = useSelector(overviewFiltersSelector);
const [updatedFieldValues, setUpdatedFieldValues] = useState<{
fieldName: string;
values: string[];
}>({ fieldName: '', values: [] });
const currentFilters = useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values);
useEffect(() => {
if (updatedFieldValues.fieldName === 'observer.geo.name') {
setAlertParams('locations', updatedFieldValues.values);
}
}, [setAlertParams, updatedFieldValues]);
useEffect(() => {
setAlertParams('locations', []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const selectedTags = currentFilters.get('tags');
const selectedPorts = currentFilters.get('url.port');
const selectedScheme = currentFilters.get('monitor.type');
const selectedLocation = currentFilters.get('observer.geo.name');
const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || [];
const onFilterFieldChange = (fieldName: string, values: string[]) => {
setUpdatedFieldValues({ fieldName, values });
};
const monitorFilters = [
{
onFilterFieldChange,
loading: false,
fieldName: 'url.port',
id: 'filter_port',
disabled: ports?.length === 0,
items: ports?.map((p: number) => p.toString()) ?? [],
selectedItems: getSelectedItems('url.port'),
title: filterLabels.PORT,
description: selectedPorts ? alertFilterLabels.USING_PORT : alertFilterLabels.USING,
value: selectedPorts?.join(',') ?? alertFilterLabels.ANY_PORT,
},
{
onFilterFieldChange,
loading: false,
fieldName: 'tags',
id: 'filter_tags',
disabled: tags?.length === 0,
items: tags ?? [],
selectedItems: getSelectedItems('tags'),
title: filterLabels.TAGS,
description: selectedTags ? alertFilterLabels.WITH_TAG : alertFilterLabels.WITH,
value: selectedTags?.join(',') ?? alertFilterLabels.ANY_TAG,
},
{
onFilterFieldChange,
loading: false,
fieldName: 'monitor.type',
id: 'filter_scheme',
disabled: schemes?.length === 0,
items: schemes ?? [],
selectedItems: getSelectedItems('monitor.type'),
title: filterLabels.SCHEME,
description: selectedScheme ? alertFilterLabels.OF_TYPE : alertFilterLabels.OF,
value: selectedScheme?.join(',') ?? alertFilterLabels.ANY_TYPE,
},
{
onFilterFieldChange,
loading: false,
fieldName: 'observer.geo.name',
id: 'filter_location',
disabled: locations?.length === 0,
items: locations ?? [],
selectedItems: getSelectedItems('observer.geo.name'),
title: filterLabels.SCHEME,
description: selectedLocation ? alertFilterLabels.FROM_LOCATION : alertFilterLabels.FROM,
value: selectedLocation?.join(',') ?? alertFilterLabels.ANY_LOCATION,
},
];
const [isOpen, setIsOpen] = useState<any>({
filter_port: false,
filter_tags: false,
filter_scheme: false,
filter_location: false,
});
const filtersToDisplay = monitorFilters.filter(
curr => curr.selectedItems.length > 0 || newFilters?.includes(curr.fieldName)
);
return (
<>
{filtersToDisplay.map(({ description, value, ...item }) => (
<EuiFlexGroup key={item.id}>
<EuiFlexItem>
<FilterPopover
{...item}
btnContent={
<EuiExpression
aria-label={'ariaLabel'}
color={'secondary'}
data-test-subj={'uptimeCreateStatusAlert.' + item.id}
description={description}
value={value}
onClick={() => setIsOpen({ ...isOpen, [item.id]: !isOpen[item.id] })}
/>
}
forceOpen={isOpen[item.id]}
setForceOpen={() => {
setIsOpen({ ...isOpen, [item.id]: !isOpen[item.id] });
}}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
aria-label="Remove filter"
iconType="trash"
color="danger"
onClick={() => {
onRemoveFilter(item.fieldName);
onFilterFieldChange(item.fieldName, []);
}}
/>
</EuiFlexItem>
<EuiSpacer size="xs" />
</EuiFlexGroup>
))}
<EuiSpacer size="xs" />
</>
);
};

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { DownNoExpressionSelect } from './down_number_select';
export { FiltersExpressionsSelect } from './filters_expression_select';
export { TimeExpressionSelect } from './time_expression_select';

View file

@ -0,0 +1,118 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiSelectable, EuiTitle } from '@elastic/eui';
import { AlertExpressionPopover } from '../alert_expression_popover';
import * as labels from '../translations';
import { AlertFieldNumber } from '../alert_field_number';
import { timeExpLabels } from './translations';
interface Props {
setAlertParams: (key: string, value: any) => void;
}
const TimeRangeOptions = [
{
'aria-label': labels.SECONDS_TIME_RANGE,
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.secondsOption',
key: 's',
label: labels.SECONDS,
},
{
'aria-label': labels.MINUTES_TIME_RANGE,
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.minutesOption',
checked: 'on',
key: 'm',
label: labels.MINUTES,
},
{
'aria-label': labels.HOURS_TIME_RANGE,
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption',
key: 'h',
label: labels.HOURS,
},
{
'aria-label': labels.DAYS_TIME_RANGE,
'data-test-subj': 'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.daysOption',
key: 'd',
label: labels.DAYS,
},
];
export const TimeExpressionSelect: React.FC<Props> = ({ setAlertParams }) => {
const [numUnits, setNumUnits] = useState<number>(15);
const [timerangeUnitOptions, setTimerangeUnitOptions] = useState<any[]>(TimeRangeOptions);
useEffect(() => {
const timerangeUnit = timerangeUnitOptions.find(({ checked }) => checked === 'on')?.key ?? 'm';
setAlertParams('timerange', { from: `now-${numUnits}${timerangeUnit}`, to: 'now' });
}, [numUnits, timerangeUnitOptions, setAlertParams]);
return (
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<AlertExpressionPopover
aria-label={labels.OPEN_THE_POPOVER_TIME_RANGE_VALUE}
content={
<AlertFieldNumber
aria-label={labels.ENTER_NUMBER_OF_TIME_UNITS}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueField"
disabled={false}
fieldValue={numUnits}
setFieldValue={setNumUnits}
/>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeValueExpression"
description="within"
id="timerange"
value={`last ${numUnits}`}
/>
</EuiFlexItem>
<EuiFlexItem>
<AlertExpressionPopover
aria-label={timeExpLabels.OPEN_TIME_POPOVER}
content={
<>
<EuiTitle size="xxs">
<h5>
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.timerangeSelectionHeader"
defaultMessage="Select time range unit"
/>
</h5>
</EuiTitle>
<EuiSelectable
aria-label={timeExpLabels.SELECT_TIME_RANGE_ARIA}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable"
options={timerangeUnitOptions}
onChange={newOptions => {
if (newOptions.reduce((acc, { checked }) => acc || checked === 'on', false)) {
setTimerangeUnitOptions(newOptions);
}
}}
singleSelection={true}
listProps={{
showIcons: true,
}}
>
{list => list}
</EuiSelectable>
</>
}
data-test-subj="xpack.uptime.alerts.monitorStatus.timerangeUnitExpression"
description=""
id="timerange-unit"
value={
timerangeUnitOptions.find(({ checked }) => checked === 'on')?.label.toLowerCase() ?? ''
}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const alertFilterLabels = {
USING: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.using', {
defaultMessage: 'Using',
}),
USING_PORT: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.usingPort', {
defaultMessage: 'Using port',
}),
ANY_PORT: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyPort', {
defaultMessage: 'any port',
}),
WITH: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.with', {
defaultMessage: 'Using',
}),
WITH_TAG: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.withTag', {
defaultMessage: 'With tag',
}),
ANY_TAG: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyTag', {
defaultMessage: 'any tag',
}),
OF: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.of', {
defaultMessage: 'Of',
}),
OF_TYPE: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.ofType', {
defaultMessage: 'Of type',
}),
ANY_TYPE: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyType', {
defaultMessage: 'any type',
}),
FROM: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.from', {
defaultMessage: 'From',
}),
FROM_LOCATION: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.fromLocation', {
defaultMessage: 'From location',
}),
ANY_LOCATION: i18n.translate('xpack.uptime.alerts.monitorStatus.filters.anyLocation', {
defaultMessage: 'any location',
}),
};
export const timeExpLabels = {
OPEN_TIME_POPOVER: i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeUnitExpression.ariaLabel',
{
defaultMessage: 'Open the popover for time range unit select field',
}
),
SELECT_TIME_RANGE_ARIA: i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable',
{
defaultMessage: 'Selectable field for the time range units alerts should use',
}
),
};

View file

@ -6,6 +6,119 @@
import { i18n } from '@kbn/i18n';
export const SECONDS_TIME_RANGE = i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.secondsOption.ariaLabel',
{
defaultMessage: '"Seconds" time range select item',
}
);
export const SECONDS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.seconds', {
defaultMessage: 'seconds',
});
export const MINUTES_TIME_RANGE = i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.minutesOption.ariaLabel',
{
defaultMessage: '"Minutes" time range select item',
}
);
export const MINUTES = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.minutes', {
defaultMessage: 'minutes',
});
export const HOURS_TIME_RANGE = i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.hoursOption.ariaLabel',
{
defaultMessage: '"Hours" time range select item',
}
);
export const HOURS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.hours', {
defaultMessage: 'hours',
});
export const DAYS_TIME_RANGE = i18n.translate(
'xpack.uptime.alerts.timerangeUnitSelectable.daysOption.ariaLabel',
{
defaultMessage: '"Days" time range select item',
}
);
export const DAYS = i18n.translate('xpack.uptime.alerts.monitorStatus.timerangeOption.days', {
defaultMessage: 'days',
});
export const ALERT_KUERY_BAR_ARIA = i18n.translate(
'xpack.uptime.alerts.monitorStatus.filterBar.ariaLabel',
{
defaultMessage: 'Input that allows filtering criteria for the monitor status alert',
}
);
export const OPEN_THE_POPOVER_DOWN_COUNT = i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesExpression.ariaLabel',
{
defaultMessage: 'Open the popover for down count input',
}
);
export const ENTER_NUMBER_OF_DOWN_COUNTS = i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesField.ariaLabel',
{
defaultMessage: 'Enter number of down counts required to trigger the alert',
}
);
export const MATCHING_MONITORS_DOWN = i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesExpression.matchingMonitors.description',
{
defaultMessage: 'matching monitors are down >',
}
);
export const ANY_MONITOR_DOWN = i18n.translate(
'xpack.uptime.alerts.monitorStatus.numTimesExpression.anyMonitors.description',
{
defaultMessage: 'any monitor is down >',
}
);
export const OPEN_THE_POPOVER_TIME_RANGE_VALUE = i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeValueExpression.ariaLabel',
{
defaultMessage: 'Open the popover for time range value field',
}
);
export const ENTER_NUMBER_OF_TIME_UNITS = i18n.translate(
'xpack.uptime.alerts.monitorStatus.timerangeValueField.ariaLabel',
{
defaultMessage: `Enter the number of time units for the alert's range`,
}
);
export const ADD_FILTER = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter', {
defaultMessage: `Add filter`,
});
export const LOCATION = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.location', {
defaultMessage: `Location`,
});
export const TAG = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.tag', {
defaultMessage: `Tag`,
});
export const PORT = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.port', {
defaultMessage: `Port`,
});
export const TYPE = i18n.translate('xpack.uptime.alerts.monitorStatus.addFilter.type', {
defaultMessage: `Type`,
});
export const TlsTranslations = {
criteriaAriaLabel: i18n.translate('xpack.uptime.alerts.tls.criteriaExpression.ariaLabel', {
defaultMessage:

View file

@ -21,7 +21,7 @@ exports[`FilterPopover component does not show item list when loading 1`] = `
ownFocus={true}
panelPaddingSize="m"
withTitle={true}
zIndex={1000}
zIndex={10000}
>
<EuiPopoverTitle>
<EuiFieldSearch
@ -59,7 +59,7 @@ exports[`FilterPopover component renders without errors for valid props 1`] = `
ownFocus={true}
panelPaddingSize="m"
withTitle={true}
zIndex={1000}
zIndex={10000}
>
<EuiPopoverTitle>
<EuiFieldSearch

View file

@ -4,57 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import React, { useState } from 'react';
import { EuiFilterGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FilterPopoverProps, FilterPopover } from './filter_popover';
import { FilterStatusButton } from './filter_status_button';
import { OverviewFilters } from '../../../../common/runtime_types/overview_filters';
import { filterLabels } from './translations';
import { useFilterUpdate } from '../../../hooks/use_filter_update';
interface PresentationalComponentProps {
loading: boolean;
overviewFilters: OverviewFilters;
currentFilter: string;
onFilterUpdate: (filtersKuery: string) => void;
}
export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
currentFilter,
overviewFilters,
loading,
onFilterUpdate,
}) => {
const { locations, ports, schemes, tags } = overviewFilters;
let filterKueries: Map<string, string[]>;
try {
filterKueries = new Map<string, string[]>(JSON.parse(currentFilter));
} catch {
filterKueries = new Map<string, string[]>();
}
const [updatedFieldValues, setUpdatedFieldValues] = useState<{
fieldName: string;
values: string[];
}>({ fieldName: '', values: [] });
const currentFilters = useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values);
/**
* Handle an added or removed value to filter against for an uptime field.
* @param fieldName the name of the field to filter against
* @param values the list of values to use when filter a field
*/
const onFilterFieldChange = (fieldName: string, values: string[]) => {
// add new term to filter map, toggle it off if already present
const updatedFilterMap = new Map<string, string[]>(filterKueries);
updatedFilterMap.set(fieldName, values);
Array.from(updatedFilterMap.keys()).forEach(key => {
const value = updatedFilterMap.get(key);
if (value && value.length === 0) {
updatedFilterMap.delete(key);
}
});
// store the new set of filters
const persistedFilters = Array.from(updatedFilterMap);
onFilterUpdate(persistedFilters.length === 0 ? '' : JSON.stringify(persistedFilters));
setUpdatedFieldValues({ fieldName, values });
};
const getSelectedItems = (fieldName: string) => filterKueries.get(fieldName) || [];
const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || [];
const filterPopoverProps: FilterPopoverProps[] = [
{
@ -64,9 +45,7 @@ export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
id: 'location',
items: locations,
selectedItems: getSelectedItems('observer.geo.name'),
title: i18n.translate('xpack.uptime.filterBar.options.location.name', {
defaultMessage: 'Location',
}),
title: filterLabels.LOCATION,
},
{
loading,
@ -76,7 +55,7 @@ export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
disabled: ports.length === 0,
items: ports.map((p: number) => p.toString()),
selectedItems: getSelectedItems('url.port'),
title: i18n.translate('xpack.uptime.filterBar.options.portLabel', { defaultMessage: 'Port' }),
title: filterLabels.PORT,
},
{
loading,
@ -86,9 +65,7 @@ export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
disabled: schemes.length === 0,
items: schemes,
selectedItems: getSelectedItems('monitor.type'),
title: i18n.translate('xpack.uptime.filterBar.options.schemeLabel', {
defaultMessage: 'Scheme',
}),
title: filterLabels.SCHEME,
},
{
loading,
@ -98,9 +75,7 @@ export const FilterGroupComponent: React.FC<PresentationalComponentProps> = ({
disabled: tags.length === 0,
items: tags,
selectedItems: getSelectedItems('tags'),
title: i18n.translate('xpack.uptime.filterBar.options.tagsLabel', {
defaultMessage: 'Tags',
}),
title: filterLabels.TAGS,
},
];

View file

@ -5,56 +5,41 @@
*/
import React, { useContext, useEffect } from 'react';
import { connect } from 'react-redux';
import { useUrlParams } from '../../../hooks';
import { useDispatch, useSelector } from 'react-redux';
import { useGetUrlParams } from '../../../hooks';
import { parseFiltersMap } from './parse_filter_map';
import { AppState } from '../../../state';
import { fetchOverviewFilters, GetOverviewFiltersPayload } from '../../../state/actions';
import { fetchOverviewFilters } from '../../../state/actions';
import { FilterGroupComponent } from './index';
import { OverviewFilters } from '../../../../common/runtime_types/overview_filters';
import { UptimeRefreshContext } from '../../../contexts';
import { filterGroupDataSelector } from '../../../state/selectors';
interface OwnProps {
interface Props {
esFilters?: string;
}
interface StoreProps {
esKuery: string;
lastRefresh: number;
loading: boolean;
overviewFilters: OverviewFilters;
}
interface DispatchProps {
loadFilterGroup: typeof fetchOverviewFilters;
}
type Props = OwnProps & StoreProps & DispatchProps;
export const Container: React.FC<Props> = ({
esKuery,
esFilters,
loading,
loadFilterGroup,
overviewFilters,
}: Props) => {
export const FilterGroup: React.FC<Props> = ({ esFilters }: Props) => {
const { lastRefresh } = useContext(UptimeRefreshContext);
const [getUrlParams, updateUrl] = useUrlParams();
const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = getUrlParams();
const { esKuery, filters: overviewFilters, loading } = useSelector(filterGroupDataSelector);
const { dateRangeStart, dateRangeEnd, statusFilter, filters: urlFilters } = useGetUrlParams();
const dispatch = useDispatch();
useEffect(() => {
const filterSelections = parseFiltersMap(urlFilters);
loadFilterGroup({
dateRangeStart,
dateRangeEnd,
locations: filterSelections.locations ?? [],
ports: filterSelections.ports ?? [],
schemes: filterSelections.schemes ?? [],
search: esKuery,
statusFilter,
tags: filterSelections.tags ?? [],
});
dispatch(
fetchOverviewFilters({
dateRangeStart,
dateRangeEnd,
locations: filterSelections.locations ?? [],
ports: filterSelections.ports ?? [],
schemes: filterSelections.schemes ?? [],
search: esKuery,
statusFilter,
tags: filterSelections.tags ?? [],
})
);
}, [
lastRefresh,
dateRangeStart,
@ -63,42 +48,8 @@ export const Container: React.FC<Props> = ({
esFilters,
statusFilter,
urlFilters,
loadFilterGroup,
dispatch,
]);
// update filters in the URL from filter group
const onFilterUpdate = (filtersKuery: string) => {
if (urlFilters !== filtersKuery) {
updateUrl({ filters: filtersKuery, pagination: '' });
}
};
return (
<FilterGroupComponent
currentFilter={urlFilters}
overviewFilters={overviewFilters}
loading={loading}
onFilterUpdate={onFilterUpdate}
/>
);
return <FilterGroupComponent overviewFilters={overviewFilters} loading={loading} />;
};
const mapStateToProps = ({
overviewFilters: { loading, filters },
ui: { esKuery, lastRefresh },
}: AppState): StoreProps => ({
esKuery,
overviewFilters: filters,
lastRefresh,
loading,
});
const mapDispatchToProps = (dispatch: any): DispatchProps => ({
loadFilterGroup: (payload: GetOverviewFiltersPayload) => dispatch(fetchOverviewFilters(payload)),
});
export const FilterGroup = connect<StoreProps, DispatchProps, OwnProps>(
// @ts-ignore connect is expecting null | undefined for some reason
mapStateToProps,
mapDispatchToProps
)(Container);

View file

@ -20,6 +20,9 @@ export interface FilterPopoverProps {
onFilterFieldChange: (fieldName: string, values: string[]) => void;
selectedItems: string[];
title: string;
btnContent?: JSX.Element;
forceOpen?: boolean;
setForceOpen?: (val: boolean) => void;
}
const isItemSelected = (selectedItems: string[], item: string): 'on' | undefined =>
@ -34,6 +37,9 @@ export const FilterPopover = ({
onFilterFieldChange,
selectedItems,
title,
btnContent,
forceOpen,
setForceOpen,
}: FilterPopoverProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [itemsToDisplay, setItemsToDisplay] = useState<string[]>([]);
@ -52,28 +58,33 @@ export const FilterPopover = ({
return (
<EuiPopover
button={
<UptimeFilterButton
isDisabled={disabled}
isSelected={tempSelectedItems.length > 0}
numFilters={items.length}
numActiveFilters={tempSelectedItems.length}
onClick={() => {
setIsOpen(!isOpen);
onFilterFieldChange(fieldName, tempSelectedItems);
}}
title={title}
/>
btnContent ?? (
<UptimeFilterButton
isDisabled={disabled}
isSelected={tempSelectedItems.length > 0}
numFilters={items.length}
numActiveFilters={tempSelectedItems.length}
onClick={() => {
setIsOpen(!isOpen);
onFilterFieldChange(fieldName, tempSelectedItems);
}}
title={title}
/>
)
}
closePopover={() => {
setIsOpen(false);
onFilterFieldChange(fieldName, tempSelectedItems);
if (setForceOpen) {
setForceOpen(false);
}
}}
data-test-subj={`filter-popover_${id}`}
id={id}
isOpen={isOpen}
isOpen={isOpen || forceOpen}
ownFocus={true}
withTitle
zIndex={1000}
zIndex={10000}
>
<EuiPopoverTitle>
<EuiFieldSearch

View file

@ -0,0 +1,23 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
export const filterLabels = {
LOCATION: i18n.translate('xpack.uptime.filterBar.options.location.name', {
defaultMessage: 'Location',
}),
PORT: i18n.translate('xpack.uptime.filterBar.options.portLabel', { defaultMessage: 'Port' }),
SCHEME: i18n.translate('xpack.uptime.filterBar.options.schemeLabel', {
defaultMessage: 'Scheme',
}),
TAGS: i18n.translate('xpack.uptime.filterBar.options.tagsLabel', {
defaultMessage: 'Tags',
}),
};

View file

@ -0,0 +1,56 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { useEffect } from 'react';
import { useUrlParams } from './use_url_params';
/**
* Handle an added or removed value to filter against for an uptime field.
* @param fieldName the name of the field to filter against
* @param values the list of values to use when filter a field
*/
export const useFilterUpdate = (fieldName?: string, values?: string[]) => {
const [getUrlParams, updateUrl] = useUrlParams();
const { filters: currentFilters } = getUrlParams();
// update filters in the URL from filter group
const onFilterUpdate = (filtersKuery: string) => {
if (currentFilters !== filtersKuery) {
updateUrl({ filters: filtersKuery, pagination: '' });
}
};
let filterKueries: Map<string, string[]>;
try {
filterKueries = new Map<string, string[]>(JSON.parse(currentFilters));
} catch {
filterKueries = new Map<string, string[]>();
}
useEffect(() => {
if (fieldName) {
// add new term to filter map, toggle it off if already present
const updatedFilterMap = new Map<string, string[] | undefined>(filterKueries);
updatedFilterMap.set(fieldName, values);
Array.from(updatedFilterMap.keys()).forEach(key => {
const value = updatedFilterMap.get(key);
if (value && value.length === 0) {
updatedFilterMap.delete(key);
}
});
// store the new set of filters
const persistedFilters = Array.from(updatedFilterMap);
onFilterUpdate(persistedFilters.length === 0 ? '' : JSON.stringify(persistedFilters));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fieldName, values]);
return filterKueries;
};

View file

@ -174,7 +174,7 @@ describe('monitor status alert type', () => {
{{context.downMonitorsWithGeo}}",
"iconClass": "uptimeApp",
"id": "xpack.uptime.alerts.monitorStatus",
"name": "Uptime monitor status",
"name": <MonitorStatusTitle />,
"validate": [Function],
}
`);

View file

@ -12,6 +12,7 @@ import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
import { AlertTypeInitializer } from '.';
import { StatusCheckExecutorParamsType } from '../../../common/runtime_types';
import { AlertMonitorStatus } from '../../components/overview/alerts/alerts_containers';
import { MonitorStatusTitle } from './monitor_status_title';
import { CLIENT_ALERT_TYPES } from '../../../common/constants';
import { MonitorStatusTranslations } from './translations';
@ -54,13 +55,13 @@ export const validate = (alertParams: any) => {
return { errors };
};
const { name, defaultActionMessage } = MonitorStatusTranslations;
const { defaultActionMessage } = MonitorStatusTranslations;
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
autocomplete,
}): AlertTypeModel => ({
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
name,
name: <MonitorStatusTitle />,
iconClass: 'uptimeApp',
alertParamsExpression: params => <AlertMonitorStatus {...params} autocomplete={autocomplete} />,
validate,

View file

@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui';
import { snapshotDataSelector } from '../../state/selectors';
export const MonitorStatusTitle = () => {
const { count, loading } = useSelector(snapshotDataSelector);
return (
<EuiFlexGroup>
<EuiFlexItem>
<FormattedMessage
id="xpack.uptime.alerts.monitorStatus.title.label"
defaultMessage="Uptime monitor status"
/>{' '}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
{!loading ? (
<EuiText size="s" color="subdued">
{count.total} monitors
</EuiText>
) : (
<EuiLoadingSpinner size="m" />
)}
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -107,3 +107,16 @@ export const monitorListSelector = ({ monitorList, ui: { lastRefresh } }: AppSta
monitorList,
lastRefresh,
});
export const overviewFiltersSelector = ({ overviewFilters }: AppState) => {
return overviewFilters.filters;
};
export const filterGroupDataSelector = ({
overviewFilters: { loading, filters },
ui: { esKuery },
}: AppState) => ({
esKuery,
filters,
loading,
});

View file

@ -9,7 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context';
export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) {
const pageObjects = getPageObjects(['common', 'timePicker']);
const { alerts, common: commonService, monitor, navigation } = getService('uptime');
const { common: commonService, monitor, navigation } = getService('uptime');
const retry = getService('retry');
return new (class UptimePage {
@ -97,42 +97,9 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo
return await commonService.getSnapshotCount();
}
public async openAlertFlyoutAndCreateMonitorStatusAlert({
alertInterval,
alertName,
alertNumTimes,
alertTags,
alertThrottleInterval,
alertTimerangeSelection,
alertType,
filters,
}: {
alertName: string;
alertTags: string[];
alertInterval: string;
alertThrottleInterval: string;
alertNumTimes: string;
alertTimerangeSelection: string;
alertType?: string;
filters?: string;
}) {
public async setAlertKueryBarText(filters: string) {
const { setKueryBarText } = commonService;
await alerts.openFlyout();
if (alertType) {
await alerts.openMonitorStatusAlertType(alertType);
}
await alerts.setAlertName(alertName);
await alerts.setAlertTags(alertTags);
await alerts.setAlertInterval(alertInterval);
await alerts.setAlertThrottleInterval(alertThrottleInterval);
if (filters) {
await setKueryBarText('xpack.uptime.alerts.monitorStatus.filterBar', filters);
}
await alerts.setAlertStatusNumTimes(alertNumTimes);
await alerts.setAlertTimerangeSelection(alertTimerangeSelection);
await alerts.setMonitorStatusSelectableToHours();
await alerts.setLocationsSelectable();
await alerts.clickSaveAlertButtion();
await setKueryBarText('xpack.uptime.alerts.monitorStatus.filterBar', filters);
}
public async setMonitorListPageSize(size: number): Promise<void> {

View file

@ -77,19 +77,37 @@ export function UptimeAlertsProvider({ getService }: FtrProviderContext) {
['xpack.uptime.alerts.monitorStatus.timerangeUnitSelectable.hoursOption']
);
},
async setLocationsSelectable() {
await testSubjects.click(
'xpack.uptime.alerts.monitorStatus.locationsSelectionExpression',
5000
);
await testSubjects.click('xpack.uptime.alerts.monitorStatus.locationsSelectionSwitch', 5000);
await testSubjects.click(
'xpack.uptime.alerts.monitorStatus.locationsSelectionSelectable',
5000
);
async clickAddFilter() {
await testSubjects.click('uptimeCreateAlertAddFilter');
},
async clickAddFilterLocation() {
await this.clickAddFilter();
await testSubjects.click('uptimeAlertAddFilter.observer.geo.name');
},
async clickAddFilterPort() {
await this.clickAddFilter();
await testSubjects.click('uptimeAlertAddFilter.url.port');
},
async clickAddFilterType() {
await this.clickAddFilter();
await testSubjects.click('uptimeAlertAddFilter.monitor.type');
},
async clickLocationExpression(filter: string) {
await testSubjects.click('uptimeCreateStatusAlert.filter_location');
await testSubjects.click(`filter-popover-item_${filter}`);
return browser.pressKeys(browser.keys.ESCAPE);
},
async clickSaveAlertButtion() {
async clickPortExpression(filter: string) {
await testSubjects.click('uptimeCreateStatusAlert.filter_port');
await testSubjects.click(`filter-popover-item_${filter}`);
return browser.pressKeys(browser.keys.ESCAPE);
},
async clickTypeExpression(filter: string) {
await testSubjects.click('uptimeCreateStatusAlert.filter_scheme');
await testSubjects.click(`filter-popover-item_${filter}`);
return browser.pressKeys(browser.keys.ESCAPE);
},
async clickSaveAlertButton() {
return testSubjects.click('saveAlertButton');
},
};

View file

@ -14,20 +14,67 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
const pageObjects = getPageObjects(['common', 'uptime']);
const supertest = getService('supertest');
const retry = getService('retry');
let alerts: any;
it('posts an alert, verfies its presence, and deletes the alert', async () => {
before(async () => {
alerts = getService('uptime').alerts;
});
it('can open alert flyout', async () => {
await pageObjects.uptime.goToUptimeOverviewAndLoadData(DEFAULT_DATE_START, DEFAULT_DATE_END);
await alerts.openFlyout();
});
await pageObjects.uptime.openAlertFlyoutAndCreateMonitorStatusAlert({
alertInterval: '11',
alertName: 'uptime-test',
alertNumTimes: '3',
alertTags: ['uptime', 'another'],
alertThrottleInterval: '30',
alertTimerangeSelection: '1',
filters: 'monitor.id: "0001-up"',
});
it('can set alert name', async () => {
await alerts.setAlertName('uptime-test');
});
it('can set alert tags', async () => {
await alerts.setAlertTags(['uptime', 'another']);
});
it('can set alert interval', async () => {
await alerts.setAlertInterval('11');
});
it('can set alert throttle interval', async () => {
await alerts.setAlertThrottleInterval('30');
});
it('can set alert status number of time', async () => {
await alerts.setAlertStatusNumTimes('3');
});
it('can set alert time range', async () => {
await alerts.setAlertTimerangeSelection('1');
});
it('can set monitor hours', async () => {
await alerts.setMonitorStatusSelectableToHours();
});
it('can set kuery bar filters', async () => {
await pageObjects.uptime.setAlertKueryBarText('monitor.id: "0001-up"');
});
it('can select location filter', async () => {
await alerts.clickAddFilterLocation();
await alerts.clickLocationExpression('mpls');
});
it('can select port filter', async () => {
await alerts.clickAddFilterPort();
await alerts.clickPortExpression('5678');
});
it('can select type/scheme filter', async () => {
await alerts.clickAddFilterType();
await alerts.clickTypeExpression('http');
});
it('can save alert', async () => {
await alerts.clickSaveAlertButton();
});
it('posts an alert, verifies its presence, and deletes the alert', async () => {
// The creation of the alert could take some time, so the first few times we query after
// the previous line resolves, the API may not be done creating the alert yet, so we
// put the fetch code in a retry block with a timeout.
@ -67,7 +114,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
expect(timerange.to).to.be('now');
expect(locations).to.eql(['mpls']);
expect(filters).to.eql(
'{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],"minimum_should_match":1}}'
'{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"monitor.id":"0001-up"}}],' +
'"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"observer.geo.name":"mpls"}}],' +
'"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match":{"url.port":5678}}],' +
'"minimum_should_match":1}},{"bool":{"should":[{"match":{"monitor.type":"http"}}],"minimum_should_match":1}}]}}]}}]}}'
);
} finally {
await supertest