Fix Watcher stuck firing state (#138563)

* Update WATCH_STATES by replacing DISABLED with INACTIVE and OK/FIRING with ACTIVE. Update ACTION_STATES by replacing FIRING with OK.
- Refactor isAckable logic to directly translate value provided by ES.
- Replace WatchStatus component with ActionStateBadge and WatchStateBadge components.

* Change WatchListPage 'Last fired' column to 'Condition last met' and 'Last triggered' column to 'Last checked'.
- Add tooltips to both.
- Add tooltips to State and Comment headers.

* Rename Watch Status Model's lastFired -> lastExecution for consistency. Remove unused properties from client WatchStatus model.

* Add Condition Met column and tooltips to State and Comment headers on Execution History Panel.

* Add tooltip to State header in Execution History flyout. Add Last executed header to Action Statuses Panel and tooltips. Update tests.

* Use EUI types instead of hand-rolling them.

* Remove unused ACTION_STATES.FIRING and references to WATCH_STATES.FIRING.

* Use dots to denote states, similar to Index Management.

* Refactor deriveComment to use early exits.
This commit is contained in:
CJ Cenizal 2022-08-15 18:25:19 -07:00 committed by GitHub
parent 1adabbcb23
commit f77afae85e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 496 additions and 390 deletions

View file

@ -31249,7 +31249,6 @@
"xpack.watcher.constants.actionStates.acknowledgedStateText": "Reconnu",
"xpack.watcher.constants.actionStates.configErrorStateText": "Erreur de config",
"xpack.watcher.constants.actionStates.errorStateText": "Erreur",
"xpack.watcher.constants.actionStates.firingStateText": "Déclenchement",
"xpack.watcher.constants.actionStates.okStateText": "OK",
"xpack.watcher.constants.actionStates.throttledStateText": "Contraint",
"xpack.watcher.constants.actionStates.unknownStateText": "Inconnu",
@ -31259,10 +31258,7 @@
"xpack.watcher.constants.watchStateComments.partiallyThrottledStateCommentText": "Partiellement contraint",
"xpack.watcher.constants.watchStateComments.throttledStateCommentText": "Contraint",
"xpack.watcher.constants.watchStates.configErrorStateText": "Erreur de config",
"xpack.watcher.constants.watchStates.disabledStateText": "Désactivé",
"xpack.watcher.constants.watchStates.errorStateText": "Erreur",
"xpack.watcher.constants.watchStates.firingStateText": "Déclenchement",
"xpack.watcher.constants.watchStates.okStateText": "OK",
"xpack.watcher.data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "Intervalle de calendrier non valide : {interval}, la valeur doit être 1",
"xpack.watcher.data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "Format d'intervalle non valide : {interval}",
"xpack.watcher.deleteSelectedWatchesConfirmModal.cancelButtonLabel": "Annuler",

View file

@ -31325,7 +31325,6 @@
"xpack.watcher.constants.actionStates.acknowledgedStateText": "承認済み",
"xpack.watcher.constants.actionStates.configErrorStateText": "構成エラー",
"xpack.watcher.constants.actionStates.errorStateText": "エラー",
"xpack.watcher.constants.actionStates.firingStateText": "実行中",
"xpack.watcher.constants.actionStates.okStateText": "OK",
"xpack.watcher.constants.actionStates.throttledStateText": "スロットル",
"xpack.watcher.constants.actionStates.unknownStateText": "不明",
@ -31335,10 +31334,7 @@
"xpack.watcher.constants.watchStateComments.partiallyThrottledStateCommentText": "部分スロットル",
"xpack.watcher.constants.watchStateComments.throttledStateCommentText": "スロットル",
"xpack.watcher.constants.watchStates.configErrorStateText": "構成エラー",
"xpack.watcher.constants.watchStates.disabledStateText": "無効",
"xpack.watcher.constants.watchStates.errorStateText": "エラー",
"xpack.watcher.constants.watchStates.firingStateText": "実行中",
"xpack.watcher.constants.watchStates.okStateText": "OK",
"xpack.watcher.data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "無効なカレンダー間隔:{interval}、1よりも大きな値が必要です",
"xpack.watcher.data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "無効な間隔形式:{interval}",
"xpack.watcher.deleteSelectedWatchesConfirmModal.cancelButtonLabel": "キャンセル",

View file

@ -31354,7 +31354,6 @@
"xpack.watcher.constants.actionStates.acknowledgedStateText": "已确认",
"xpack.watcher.constants.actionStates.configErrorStateText": "配置错误",
"xpack.watcher.constants.actionStates.errorStateText": "错误",
"xpack.watcher.constants.actionStates.firingStateText": "正在发送",
"xpack.watcher.constants.actionStates.okStateText": "确定",
"xpack.watcher.constants.actionStates.throttledStateText": "已限制",
"xpack.watcher.constants.actionStates.unknownStateText": "未知",
@ -31364,10 +31363,7 @@
"xpack.watcher.constants.watchStateComments.partiallyThrottledStateCommentText": "已部分限制",
"xpack.watcher.constants.watchStateComments.throttledStateCommentText": "已限制",
"xpack.watcher.constants.watchStates.configErrorStateText": "配置错误",
"xpack.watcher.constants.watchStates.disabledStateText": "已禁用",
"xpack.watcher.constants.watchStates.errorStateText": "错误",
"xpack.watcher.constants.watchStates.firingStateText": "正在发送",
"xpack.watcher.constants.watchStates.okStateText": "确定",
"xpack.watcher.data.parseEsInterval.invalidEsCalendarIntervalErrorMessage": "无效的日历时间间隔:{interval},值必须为 1",
"xpack.watcher.data.parseEsInterval.invalidEsIntervalFormatErrorMessage": "时间间隔格式无效:{interval}",
"xpack.watcher.deleteSelectedWatchesConfirmModal.cancelButtonLabel": "取消",

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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import moment, { Moment } from 'moment';
import { ACTION_STATES, WATCH_STATES } from '../common/constants';
import { ClientWatchStatusModel, ClientActionStatusModel } from '../common/types';
interface WatchHistory {
id: string;
watchId: string;
startTime: Moment;
watchStatus: {
state: ClientWatchStatusModel['state'];
comment?: string;
lastExecution: Moment;
actionStatuses?: Array<{
id: string;
state: ClientActionStatusModel['state'];
}>;
};
details?: object;
}
export const getWatchHistory = ({
id,
startTime,
}: {
id: string;
startTime: string;
}): WatchHistory => ({
id,
startTime: moment(startTime),
watchId: id,
watchStatus: {
state: WATCH_STATES.OK,
lastExecution: moment('2019-06-03T19:44:11.088Z'),
actionStatuses: [
{
id: 'a',
state: ACTION_STATES.OK,
},
],
},
details: {},
});

View file

@ -6,5 +6,5 @@
*/
export * from './watch';
export * from './watch_history';
export * from './get_watch_history';
export * from './execute_details';

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { Moment } from 'moment';
import { getRandomString } from '@kbn/test-jest-helpers';
import { ClientWatchStatusModel } from '../common/types';
import { WATCH_STATES, WATCH_STATE_COMMENTS } from '../common/constants';
interface Watch {
id: string;
@ -22,14 +23,7 @@ interface Watch {
timeWindowUnit?: string;
threshold?: number[];
isSystemWatch: boolean;
watchStatus: {
state: 'OK' | 'Firing' | 'Error' | 'Config error' | 'Disabled';
comment?: string;
lastMetCondition?: Moment;
lastChecked?: Moment;
isActive: boolean;
actionStatuses?: any[];
};
watchStatus: ClientWatchStatusModel;
}
export const getWatch = ({
@ -47,8 +41,13 @@ export const getWatch = ({
threshold,
isSystemWatch = false,
watchStatus = {
state: 'OK',
id: 'a',
state: WATCH_STATES.OK,
isActive: true,
lastChecked: null,
lastMetCondition: null,
comment: WATCH_STATE_COMMENTS.OK,
actionStatuses: [],
},
}: Partial<Watch> = {}): Watch => ({
id,

View file

@ -1,39 +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 { getRandomString } from '@kbn/test-jest-helpers';
interface WatchHistory {
startTime: string;
id: string;
watchId: string;
watchStatus: {
state: 'OK' | 'Firing' | 'Error' | 'Config error' | 'Disabled';
comment?: string;
actionStatuses?: Array<{
id: string;
state: 'OK' | 'Firing' | 'Error' | 'Acked' | 'Throttled' | 'Config error';
}>;
};
details?: object;
}
export const getWatchHistory = ({
startTime = '2019-06-03T19:44:11.088Z',
id = getRandomString(),
watchId = getRandomString(),
watchStatus = {
state: 'OK',
},
details = {},
}: Partial<WatchHistory> = {}): WatchHistory => ({
startTime,
id,
watchId,
watchStatus,
details,
});

View file

@ -86,17 +86,17 @@ describe('<WatchListPage />', () => {
// Expect "watch1" is only visible in the table
expect(tableCellsValues.length).toEqual(1);
const row = tableCellsValues[0];
const { name, id, watchStatus } = watch1;
const { name, id } = watch1;
const expectedRow = [
'', // checkbox
id,
name,
watchStatus.state,
'', // comment
'', // state
'', // lastMetCondition
'', // lastChecked
'', // actions
'', // comment
'', // row actions
];
expect(row).toEqual(expectedRow);
@ -128,7 +128,7 @@ describe('<WatchListPage />', () => {
const { table } = testBed;
const { tableCellsValues } = table.getMetaData('watchesTable');
const getExpectedValue = (value: any) => (typeof value === 'undefined' ? '' : value);
const getExpectedValue = (value: any) => value ?? '';
tableCellsValues.forEach((row, i) => {
const watch = watches[i];
@ -138,11 +138,11 @@ describe('<WatchListPage />', () => {
'',
id, // required value
getExpectedValue(name),
watchStatus.state, // required value
getExpectedValue(watchStatus.comment),
'', // state
getExpectedValue(watchStatus.lastMetCondition),
getExpectedValue(watchStatus.lastChecked),
'',
getExpectedValue(watchStatus.comment),
'', // row actions
]);
});
});

View file

@ -16,8 +16,8 @@ import { API_BASE_PATH } from '../../common/constants';
const { setup } = pageHelpers.watchStatusPage;
const watchHistory1 = getWatchHistory({ startTime: '2019-06-04T01:11:11.294' });
const watchHistory2 = getWatchHistory({ startTime: '2019-06-04T01:10:10.987Z' });
const watchHistory1 = getWatchHistory({ id: 'a', startTime: '2019-06-04T01:11:11.294' });
const watchHistory2 = getWatchHistory({ id: 'b', startTime: '2019-06-04T01:10:10.987Z' });
const watchHistoryItems = { watchHistoryItems: [watchHistory1, watchHistory2] };
@ -26,13 +26,15 @@ const ACTION_ID = 'my_logging_action_1';
const watch = {
...WATCH.watch,
watchStatus: {
state: WATCH_STATES.FIRING,
state: WATCH_STATES.ACTIVE,
isActive: true,
lastExecution: moment('2019-06-03T19:44:11.088Z'),
actionStatuses: [
{
id: ACTION_ID,
state: ACTION_STATES.FIRING,
state: ACTION_STATES.OK,
isAckable: true,
lastExecution: moment('2019-06-03T19:44:11.088Z'),
},
],
},
@ -100,7 +102,8 @@ describe('<WatchStatusPage />', () => {
expect(row).toEqual([
getExpectedValue(moment(startTime).format()),
getExpectedValue(watchStatus.state),
getExpectedValue(watchStatus.comment),
'',
'',
]);
});
});
@ -112,11 +115,11 @@ describe('<WatchStatusPage />', () => {
...watchHistory1,
watchId: watch.id,
watchStatus: {
state: WATCH_STATES.FIRING,
state: WATCH_STATES.ACTIVE,
actionStatuses: [
{
id: 'my_logging_action_1',
state: ACTION_STATES.FIRING,
state: ACTION_STATES.OK,
isAckable: true,
},
],
@ -204,7 +207,7 @@ describe('<WatchStatusPage />', () => {
httpRequestsMockHelpers.setActivateWatchResponse(WATCH_ID, {
watchStatus: {
state: WATCH_STATES.FIRING,
state: WATCH_STATES.ACTIVE,
isActive: true,
},
});
@ -231,18 +234,22 @@ describe('<WatchStatusPage />', () => {
tableCellsValues.forEach((row, i) => {
const action = watch.watchStatus.actionStatuses[i];
const { id, state, isAckable } = action;
const { id, state, lastExecution, isAckable } = action;
expect(row).toEqual([id, state, isAckable ? 'Acknowledge' : '']);
expect(row).toEqual([
id, // Name
state, // State
lastExecution.format(), // Last executed
isAckable ? 'Acknowledge' : '', // Row actions
]);
});
});
test('should allow an action to be acknowledged', async () => {
const { actions, table } = testBed;
httpRequestsMockHelpers.setAcknowledgeWatchResponse(WATCH_ID, ACTION_ID, {
const watchHistoryItem = {
watchStatus: {
state: WATCH_STATES.FIRING,
state: WATCH_STATES.ACTIVE,
isActive: true,
comment: 'Acked',
actionStatuses: [
@ -250,10 +257,13 @@ describe('<WatchStatusPage />', () => {
id: ACTION_ID,
state: ACTION_STATES.ACKNOWLEDGED,
isAckable: false,
lastExecution: moment('2019-06-03T19:44:11.088Z'),
},
],
},
});
};
httpRequestsMockHelpers.setAcknowledgeWatchResponse(WATCH_ID, ACTION_ID, watchHistoryItem);
await actions.clickAcknowledgeButton(0);
@ -268,7 +278,12 @@ describe('<WatchStatusPage />', () => {
const { tableCellsValues } = table.getMetaData('watchActionStatusTable');
tableCellsValues.forEach((row) => {
expect(row).toEqual([ACTION_ID, ACTION_STATES.ACKNOWLEDGED, '']);
expect(row).toEqual([
ACTION_ID, // Name
ACTION_STATES.ACKNOWLEDGED, // State
watchHistoryItem.watchStatus.actionStatuses[0].lastExecution.format(), // Last executed
'', // Row actions
]);
});
});
});

View file

@ -15,7 +15,7 @@ export const ACTION_STATES: { [key: string]: string } = {
// Action has been acknowledged by user
ACKNOWLEDGED: i18n.translate('xpack.watcher.constants.actionStates.acknowledgedStateText', {
defaultMessage: 'Acked',
defaultMessage: 'Acknowledged',
}),
// Action has been throttled (time-based) by the system
@ -23,11 +23,6 @@ export const ACTION_STATES: { [key: string]: string } = {
defaultMessage: 'Throttled',
}),
// Action has been completed
FIRING: i18n.translate('xpack.watcher.constants.actionStates.firingStateText', {
defaultMessage: 'Firing',
}),
// Action has failed
ERROR: i18n.translate('xpack.watcher.constants.actionStates.errorStateText', {
defaultMessage: 'Error',

View file

@ -27,14 +27,14 @@ export const WATCH_STATE_COMMENTS: { [key: string]: string } = {
PARTIALLY_ACKNOWLEDGED: i18n.translate(
'xpack.watcher.constants.watchStateComments.partiallyAcknowledgedStateCommentText',
{
defaultMessage: 'Partially acked',
defaultMessage: 'Partially acknowledged',
}
),
ACKNOWLEDGED: i18n.translate(
'xpack.watcher.constants.watchStateComments.acknowledgedStateCommentText',
{
defaultMessage: 'Acked',
defaultMessage: 'Acknowledged',
}
),

View file

@ -8,16 +8,12 @@
import { i18n } from '@kbn/i18n';
export const WATCH_STATES: { [key: string]: string } = {
DISABLED: i18n.translate('xpack.watcher.constants.watchStates.disabledStateText', {
defaultMessage: 'Disabled',
INACTIVE: i18n.translate('xpack.watcher.constants.watchStates.inactiveStateText', {
defaultMessage: 'Inactive',
}),
OK: i18n.translate('xpack.watcher.constants.watchStates.okStateText', {
defaultMessage: 'OK',
}),
FIRING: i18n.translate('xpack.watcher.constants.watchStates.firingStateText', {
defaultMessage: 'Firing',
ACTIVE: i18n.translate('xpack.watcher.constants.watchStates.activeStateText', {
defaultMessage: 'Active',
}),
ERROR: i18n.translate('xpack.watcher.constants.watchStates.errorStateText', {

View file

@ -79,6 +79,6 @@ export interface ClientWatchStatusModel {
lastMetCondition: Moment | null;
state: keyof typeof WATCH_STATES;
comment: keyof typeof WATCH_STATE_COMMENTS;
lastFired?: Moment | null;
lastExecution?: Moment | null;
actionStatuses: ClientActionStatusModel[];
}

View file

@ -0,0 +1,39 @@
/*
* 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 { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import type { EuiTextProps } from '@elastic/eui';
import { ACTION_STATES } from '../../../common/constants';
interface Props {
state: string;
size?: EuiTextProps['size'];
}
const stateToColorMap = {
[ACTION_STATES.OK]: 'success',
[ACTION_STATES.ACKNOWLEDGED]: 'success',
[ACTION_STATES.THROTTLED]: 'warning',
[ACTION_STATES.UNKNOWN]: 'subdued',
[ACTION_STATES.CONFIG_ERROR]: 'danger',
[ACTION_STATES.ERROR]: 'danger',
};
export const ActionStateBadge = ({ state, size = 's' }: Props) => {
return (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="dot" color={stateToColorMap[state]} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size={size}>{state}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -9,7 +9,8 @@ export { getPageErrorCode, PageError } from './page_error';
export { ConfirmWatchesModal } from './confirm_watches_modal';
export { DeleteWatchesModal } from './delete_watches_modal';
export { ErrableFormRow } from './form_errors';
export { WatchStatus } from './watch_status';
export { WatchStateBadge } from './watch_state_badge';
export { ActionStateBadge } from './action_state_badge';
export { SectionLoading } from './section_loading';
export type { Error } from './section_error';
export { SectionError } from './section_error';

View file

@ -0,0 +1,37 @@
/*
* 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 { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import type { EuiTextProps } from '@elastic/eui';
import { WATCH_STATES } from '../../../common/constants';
interface Props {
state: string;
size?: EuiTextProps['size'];
}
const stateToColorMap = {
[WATCH_STATES.ACTIVE]: 'success',
[WATCH_STATES.INACTIVE]: 'subdued',
[WATCH_STATES.CONFIG_ERROR]: 'subdued',
[WATCH_STATES.ERROR]: 'subdued',
};
export const WatchStateBadge = ({ state, size = 's' }: Props) => {
return (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon type="dot" color={stateToColorMap[state]} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size={size}>{state}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -1,47 +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 React from 'react';
import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { ACTION_STATES, WATCH_STATES } from '../../../common/constants';
function StatusIcon({ status }: { status: string }) {
switch (status) {
case WATCH_STATES.FIRING:
case ACTION_STATES.FIRING:
return <EuiIcon type="play" color="primary" />;
case WATCH_STATES.OK:
case ACTION_STATES.OK:
case ACTION_STATES.ACKNOWLEDGED:
return <EuiIcon type="check" color="success" />;
case ACTION_STATES.THROTTLED:
return <EuiIcon type="clock" color="warning" />;
case WATCH_STATES.DISABLED:
return <EuiIcon type="minusInCircleFilled" color="subdued" />;
case WATCH_STATES.CONFIG_ERROR:
case WATCH_STATES.ERROR:
case ACTION_STATES.UNKNOWN:
return <EuiIcon type="cross" color="subdued" />;
case ACTION_STATES.CONFIG_ERROR:
case ACTION_STATES.ERROR:
return <EuiIcon type="crossInACircleFilled" color="danger" />;
}
return null;
}
export function WatchStatus({ status, size = 's' }: { status: string; size?: 'xs' | 's' | 'm' }) {
return (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<StatusIcon status={status} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText size={size}>{status}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -15,18 +15,10 @@ export class WatchStatus {
this.state = get(props, 'state');
this.comment = get(props, 'comment');
this.isActive = get(props, 'isActive');
this.lastFired = getMoment(get(props, 'lastFired'));
this.lastExecution = getMoment(get(props, 'lastExecution'));
this.lastChecked = getMoment(get(props, 'lastChecked'));
this.lastMetCondition = getMoment(get(props, 'lastMetCondition'));
if (this.lastFired) {
this.lastFiredHumanized = this.lastFired.fromNow();
}
if (this.lastChecked) {
this.lastCheckedHumanized = this.lastChecked.fromNow();
}
const actionStatuses = get(props, 'actionStatuses', []);
this.actionStatuses = actionStatuses.map((actionStatus) =>
ActionStatus.fromUpstreamJson(actionStatus)

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { Fragment, useContext, useState } from 'react';
import React, { Fragment, useContext, useEffect, useState } from 'react';
import {
EuiButton,
@ -67,9 +67,11 @@ export const JsonWatchEditForm = () => {
json: hasActionErrors ? [...errors.json, invalidActionMessage] : [...errors.json],
};
if (errors.json.length === 0) {
setWatchProperty('watch', JSON.parse(watch.watchString));
}
useEffect(() => {
if (errors.json.length === 0) {
setWatchProperty('watch', JSON.parse(watch.watchString));
}
}, [setWatchProperty, errors, watch]);
return (
<Fragment>

View file

@ -6,7 +6,8 @@
*/
import React, { Fragment, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiBasicTable,
EuiCodeBlock,
@ -17,15 +18,14 @@ import {
EuiText,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import {
ExecutedWatchDetails,
ExecutedWatchResults,
} from '../../../../../../common/types/watch_types';
import { ActionStateBadge, WatchStateBadge, SectionError } from '../../../../components';
import { getTypeFromAction } from '../../watch_edit_actions';
import { WatchContext } from '../../watch_context';
import { WatchStatus, SectionError } from '../../../../components';
export const SimulateWatchResultsFlyout = ({
executeResults,
@ -104,7 +104,7 @@ export const SimulateWatchResultsFlyout = ({
),
dataType: 'string' as const,
render: (actionState: string, _item: typeof actionsTableData[number]) => (
<WatchStatus status={actionState} />
<ActionStateBadge state={actionState} />
),
},
{
@ -171,9 +171,10 @@ export const SimulateWatchResultsFlyout = ({
>
<EuiFlyoutHeader hasBorder>
{flyoutTitle}
<EuiSpacer size="xs" />
<WatchStatus status={state} size="m" />
<EuiSpacer size="s" />
<WatchStateBadge state={state} size="m" />
</EuiFlyoutHeader>
<EuiFlyoutBody>
{actionsTableData && actionsTableData.length > 0 && (
<Fragment>

View file

@ -12,6 +12,7 @@ import {
EuiButton,
EuiButtonEmpty,
EuiInMemoryTable,
EuiIcon,
EuiLink,
EuiPageContent,
EuiSpacer,
@ -36,7 +37,7 @@ import {
getPageErrorCode,
PageError,
DeleteWatchesModal,
WatchStatus,
WatchStateBadge,
SectionLoading,
Error,
} from '../../components';
@ -275,42 +276,99 @@ export const WatchListPage = () => {
},
{
field: 'watchStatus.state',
name: i18n.translate('xpack.watcher.sections.watchList.watchTable.stateHeader', {
defaultMessage: 'State',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchList.watchTable.stateHeader.tooltipText',
{
defaultMessage: 'Active, inactive, or error.',
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchList.watchTable.stateHeader', {
defaultMessage: 'State',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
width: '130px',
render: (state: string) => <WatchStatus status={state} />,
render: (state: string) => <WatchStateBadge state={state} />,
},
{
field: 'watchStatus.lastMetCondition',
name: i18n.translate('xpack.watcher.sections.watchList.watchTable.lastFiredHeader', {
defaultMessage: 'Last fired',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchList.watchTable.lastFiredHeader.tooltipText',
{
defaultMessage: `The last time the condition was met and action taken.`,
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchList.watchTable.lastFiredHeader', {
defaultMessage: 'Condition last met',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
width: '130px',
width: '160px',
render: (lastMetCondition: Moment) => {
return lastMetCondition ? lastMetCondition.fromNow() : lastMetCondition;
},
},
{
field: 'watchStatus.lastChecked',
name: i18n.translate('xpack.watcher.sections.watchList.watchTable.lastTriggeredHeader', {
defaultMessage: 'Last triggered',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchList.watchTable.lastTriggeredHeader.tooltipText',
{
defaultMessage: `The last time the condition was checked.`,
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchList.watchTable.lastTriggeredHeader', {
defaultMessage: 'Last checked',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
width: '130px',
width: '160px',
render: (lastChecked: Moment) => {
return lastChecked ? lastChecked.fromNow() : lastChecked;
},
},
{
field: 'watchStatus.comment',
name: i18n.translate('xpack.watcher.sections.watchList.watchTable.commentHeader', {
defaultMessage: 'Comment',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchList.watchTable.commentHeader.tooltipText',
{
defaultMessage:
'Whether any actions have been acknowledged, throttled, or failed to execute.',
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchList.watchTable.commentHeader', {
defaultMessage: 'Comment',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
},

View file

@ -8,6 +8,7 @@
import React, { Fragment, useState, useEffect, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { Moment } from 'moment';
import {
EuiInMemoryTable,
@ -18,12 +19,14 @@ import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiIcon,
} from '@elastic/eui';
import { ackWatchAction } from '../../../lib/api';
import { WatchStatus } from '../../../components';
import { PAGINATION } from '../../../../../common/constants';
import { WatchDetailsContext } from '../watch_details_context';
import { ackWatchAction } from '../../../lib/api';
import { ActionStateBadge } from '../../../components';
import { useAppContext } from '../../../app_context';
import { WatchDetailsContext } from '../watch_details_context';
interface ActionError {
code: string;
@ -71,16 +74,56 @@ export const ActionStatusesPanel = () => {
defaultMessage: 'Name',
}),
sortable: true,
truncateText: true,
truncateText: false,
},
{
field: 'state',
name: i18n.translate('xpack.watcher.sections.watchDetail.watchTable.stateHeader', {
defaultMessage: 'State',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchDetail.watchTable.stateHeader.tooltipText',
{
defaultMessage: 'OK, acknowledged, throttled, or error.',
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchDetail.watchTable.stateHeader', {
defaultMessage: 'State',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
render: (state: string) => <WatchStatus status={state} />,
render: (state: string) => <ActionStateBadge state={state} />,
},
{
field: 'lastExecution',
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchHistory.watchActionStatusTable.lastExecuted.tooltipText',
{
defaultMessage: `The last time this action was executed.`,
}
)}
>
<span>
{i18n.translate(
'xpack.watcher.sections.watchHistory.watchActionStatusTable.lastExecuted',
{
defaultMessage: 'Last executed',
}
)}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: false,
render: (lastExecution?: Moment) => lastExecution?.format() ?? '',
},
];

View file

@ -13,6 +13,8 @@ import { Moment } from 'moment';
import {
EuiCodeBlock,
EuiFlexGroup,
EuiToolTip,
EuiIcon,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
@ -25,7 +27,7 @@ import {
} from '@elastic/eui';
import { PAGINATION } from '../../../../../common/constants';
import { WatchStatus, SectionError, Error } from '../../../components';
import { ActionStateBadge, WatchStateBadge, SectionError, Error } from '../../../components';
import { useLoadWatchHistory, useLoadWatchHistoryDetail } from '../../../lib/api';
import { WatchDetailsContext } from '../watch_details_context';
@ -120,7 +122,8 @@ export const ExecutionHistoryPanel = () => {
defaultMessage: 'Trigger time',
}),
sortable: true,
truncateText: true,
truncateText: false,
// TODO: Once we convert the client-side models to TS, this should be a WatchHistoryItemModel.
render: (startTime: Moment, item: any) => {
const formattedDate = startTime.format();
return (
@ -135,18 +138,79 @@ export const ExecutionHistoryPanel = () => {
},
{
field: 'watchStatus.state',
name: i18n.translate('xpack.watcher.sections.watchHistory.watchTable.stateHeader', {
defaultMessage: 'State',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchHistory.watchTable.stateHeader.tooltipText',
{
defaultMessage: 'Active or error state.',
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchHistory.watchTable.stateHeader', {
defaultMessage: 'State',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
render: (state: string) => <WatchStatus status={state} />,
render: (state: string) => <WatchStateBadge state={state} />,
},
{
field: 'startTime',
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchHistory.watchTable.metConditionHeader.tooltipText',
{
defaultMessage: 'Whether the condition was met and action taken.',
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchHistory.watchTable.metConditionHeader', {
defaultMessage: 'Condition met',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
// TODO: Once we convert the client-side models to TS, this should be a WatchHistoryItemModel.
render: (startTime: Moment, item: any) => {
const {
watchStatus: { lastExecution },
} = item;
if (startTime.isSame(lastExecution)) {
return <EuiIcon color="green" type="check" />;
}
},
},
{
field: 'watchStatus.comment',
name: i18n.translate('xpack.watcher.sections.watchHistory.watchTable.commentHeader', {
defaultMessage: 'Comment',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchHistory.watchTable.commentHeader.tooltipText',
{
defaultMessage:
'Whether the action was throttled, acknowledged, or failed to execute.',
}
)}
>
<span>
{i18n.translate('xpack.watcher.sections.watchHistory.watchTable.commentHeader', {
defaultMessage: 'Comment',
})}{' '}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
},
@ -202,16 +266,38 @@ export const ExecutionHistoryPanel = () => {
defaultMessage: 'Name',
}),
sortable: true,
truncateText: true,
truncateText: false,
},
{
field: 'state',
name: i18n.translate('xpack.watcher.sections.watchHistory.watchActionStatusTable.state', {
defaultMessage: 'State',
}),
name: (
<EuiToolTip
content={i18n.translate(
'xpack.watcher.sections.watchHistory.watchActionStatusTable.state.tooltipText',
{
defaultMessage: 'OK, acknowledged, throttled, or error.',
}
)}
>
<span>
{i18n.translate(
'xpack.watcher.sections.watchHistory.watchActionStatusTable.state',
{
defaultMessage: 'State',
}
)}{' '}
<EuiIcon
size="s"
color="subdued"
type="questionInCircle"
className="eui-alignTop"
/>
</span>
</EuiToolTip>
),
sortable: true,
truncateText: true,
render: (state: string) => <WatchStatus status={state} />,
render: (state: string) => <ActionStateBadge state={state} />,
},
];

View file

@ -117,25 +117,6 @@ describe('ActionStatusModel states', () => {
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.ERROR);
});
it(`isn't ackable`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'ackable',
timestamp: '2017-03-01T00:00:00.000Z',
},
last_successful_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-02T00:00:00.000Z',
},
last_throttle: {},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.ERROR);
expect(clientActionStatusModel.isAckable).toBe(false);
});
});
describe(`ACTION_STATES.OK`, () => {
@ -148,13 +129,36 @@ describe('ActionStatusModel states', () => {
expect(clientActionStatusModel.state).toBe(ACTION_STATES.OK);
});
it(`isn't ackable`, () => {
it(`is set when lastSuccessfulExecution is equal to lastExecution`, () => {
const clientActionStatusModel = createModelWithActions({
ack: { state: 'awaits_successful_execution' },
ack: {
state: 'ackable',
},
last_successful_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_throttle: {},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.OK);
});
it(`is set when lastSuccessfulExecution is greater than lastExecution`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'ackable',
},
last_successful_execution: {
timestamp: '2017-03-02T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_throttle: {},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.OK);
expect(clientActionStatusModel.isAckable).toBe(false);
});
});
@ -184,21 +188,6 @@ describe('ActionStatusModel states', () => {
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.ACKNOWLEDGED);
});
it(`isn't ackable`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'acked',
timestamp: '2017-03-01T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.ACKNOWLEDGED);
expect(clientActionStatusModel.isAckable).toBe(false);
});
});
describe(`ACTION_STATES.THROTTLED`, () => {
@ -231,75 +220,6 @@ describe('ActionStatusModel states', () => {
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.THROTTLED);
});
it(`is ackable`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'ackable',
},
last_throttle: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.THROTTLED);
expect(clientActionStatusModel.isAckable).toBe(true);
});
});
describe(`ACTION_STATES.FIRING`, () => {
it(`is set when lastSuccessfulExecution is equal to lastExecution`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'ackable',
},
last_successful_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_throttle: {},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.FIRING);
});
it(`is set when lastSuccessfulExecution is greater than lastExecution`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'ackable',
},
last_successful_execution: {
timestamp: '2017-03-02T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_throttle: {},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.FIRING);
});
it(`is ackable`, () => {
const clientActionStatusModel = createModelWithActions({
ack: {
state: 'ackable',
},
last_successful_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_execution: {
timestamp: '2017-03-01T00:00:00.000Z',
},
last_throttle: {},
});
expect(clientActionStatusModel.state).toBe(ACTION_STATES.FIRING);
expect(clientActionStatusModel.isAckable).toBe(true);
});
});
describe(`ACTION_STATES.UNKNOWN`, () => {

View file

@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n';
import { ActionStatusModelEs, ServerActionStatusModel } from '../../../common/types';
import { getMoment } from '../../../common/lib/get_moment';
import { deriveState, deriveIsAckable } from './action_status_model_utils';
import { deriveState } from './action_status_model_utils';
export const buildServerActionStatusModel = (
actionStatusModelEs: ActionStatusModelEs
@ -62,7 +62,7 @@ export const buildClientActionStatusModel = (serverActionStatusModel: ServerActi
lastSuccessfulExecution,
} = serverActionStatusModel;
const state = deriveState(serverActionStatusModel);
const isAckable = deriveIsAckable(state);
const isAckable = serverActionStatusModel.actionStatusJson.ack.state === 'ackable';
return {
id,

View file

@ -58,7 +58,7 @@ export const deriveState = (serverActionStatusModel: ServerActionStatusModel) =>
if (lastSuccessfulExecution) {
// Might be null
if (ackState === 'ackable' && lastSuccessfulExecution >= lastExecution) {
return ACTION_STATES.FIRING;
return ACTION_STATES.OK;
}
if (ackState === 'ackable' && lastSuccessfulExecution < lastExecution) {
@ -72,11 +72,3 @@ export const deriveState = (serverActionStatusModel: ServerActionStatusModel) =>
// missing an action status and the logic to determine it.
return ACTION_STATES.UNKNOWN;
};
export const deriveIsAckable = (state: keyof typeof ACTION_STATES) => {
if (state === ACTION_STATES.THROTTLED || state === ACTION_STATES.FIRING) {
return true;
}
return false;
};

View file

@ -98,7 +98,7 @@ describe('watch_history_item', () => {
lastChecked: null,
lastMetCondition: null,
lastFired: undefined,
state: 'OK',
state: 'Active',
},
};
expect(watchHistoryItem.downstreamJson).toEqual(expected);

View file

@ -135,7 +135,7 @@ describe('WatchStatusModel', () => {
expect(serverWatchStatusModel.isActive).toBe(clientWatchStatusModel.isActive);
expect(serverWatchStatusModel.lastChecked).toBe(clientWatchStatusModel.lastChecked);
expect(serverWatchStatusModel.lastMetCondition).toBe(clientWatchStatusModel.lastMetCondition);
expect(clientWatchStatusModel.state).toBe(WATCH_STATES.OK);
expect(clientWatchStatusModel.state).toBe(WATCH_STATES.ACTIVE);
expect(clientWatchStatusModel.comment).toBe(WATCH_STATE_COMMENTS.OK);
expect(
clientWatchStatusModel.actionStatuses && clientWatchStatusModel.actionStatuses.length

View file

@ -15,7 +15,7 @@ import {
} from '../../../common/types';
import { getMoment } from '../../../common/lib/get_moment';
import { buildServerActionStatusModel, buildClientActionStatusModel } from '../action_status_model';
import { deriveState, deriveComment, deriveLastFired } from './watch_status_model_utils';
import { deriveState, deriveComment, deriveLastExecution } from './watch_status_model_utils';
export const buildServerWatchStatusModel = (
watchStatusModelEs: WatchStatusModelEs
@ -80,7 +80,7 @@ export const buildClientWatchStatusModel = (
lastMetCondition,
state: deriveState(isActive, watchState, clientActionStatuses),
comment: deriveComment(isActive, clientActionStatuses),
lastFired: deriveLastFired(clientActionStatuses),
lastExecution: deriveLastExecution(clientActionStatuses),
actionStatuses: clientActionStatuses,
};
};

View file

@ -9,10 +9,10 @@ import moment from 'moment';
import { ACTION_STATES, WATCH_STATES, WATCH_STATE_COMMENTS } from '../../../common/constants';
import { ClientActionStatusModel } from '../../../common/types';
import { deriveState, deriveComment, deriveLastFired } from './watch_status_model_utils';
import { deriveState, deriveComment, deriveLastExecution } from './watch_status_model_utils';
const mockActionStatus = (opts: Partial<ClientActionStatusModel>): ClientActionStatusModel => ({
state: ACTION_STATES.OK,
state: ACTION_STATES.ACTIVE,
id: 'no-id',
isAckable: false,
lastAcknowledged: null,
@ -25,13 +25,13 @@ const mockActionStatus = (opts: Partial<ClientActionStatusModel>): ClientActionS
});
describe('WatchStatusModel utils', () => {
describe('deriveLastFired', () => {
describe('deriveLastExecution', () => {
it(`is the latest lastExecution from the client action statuses`, () => {
const actionStatuses = [
mockActionStatus({ lastExecution: moment('2017-07-05T00:00:00.000Z') }),
mockActionStatus({ lastExecution: moment('2015-05-26T18:21:08.630Z') }),
];
expect(deriveLastFired(actionStatuses)).toEqual(moment('2017-07-05T00:00:00.000Z'));
expect(deriveLastExecution(actionStatuses)).toEqual(moment('2017-07-05T00:00:00.000Z'));
});
});
@ -45,8 +45,7 @@ describe('WatchStatusModel utils', () => {
const isActive = true;
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
];
expect(deriveComment(isActive, actionStatuses)).toBe(
WATCH_STATE_COMMENTS.PARTIALLY_THROTTLED
@ -67,9 +66,8 @@ describe('WatchStatusModel utils', () => {
const isActive = true;
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.ACKNOWLEDGED }),
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
];
expect(deriveComment(isActive, actionStatuses)).toBe(
WATCH_STATE_COMMENTS.PARTIALLY_ACKNOWLEDGED
@ -89,10 +87,9 @@ describe('WatchStatusModel utils', () => {
it(`is FAILING when one action state is failing`, () => {
const isActive = true;
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.ACKNOWLEDGED }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.ERROR }),
];
expect(deriveComment(isActive, actionStatuses)).toBe(WATCH_STATE_COMMENTS.FAILING);
@ -101,10 +98,9 @@ describe('WatchStatusModel utils', () => {
it(`is OK when watch is inactive`, () => {
const isActive = false;
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.ACKNOWLEDGED }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.ERROR }),
];
expect(deriveComment(isActive, actionStatuses)).toBe(WATCH_STATE_COMMENTS.OK);
@ -112,43 +108,37 @@ describe('WatchStatusModel utils', () => {
});
describe('deriveState', () => {
it(`is OK there are no actions`, () => {
it(`is ACTIVE there are no actions`, () => {
const isActive = true;
const watchState = 'awaits_execution';
expect(deriveState(isActive, watchState, [])).toBe(WATCH_STATES.OK);
expect(deriveState(isActive, watchState, [])).toBe(WATCH_STATES.ACTIVE);
});
it(`is FIRING when at least one action state is firing`, () => {
it(`is ACTIVE when at least one action state is active`, () => {
const isActive = true;
const watchState = 'awaits_execution';
let actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
];
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.FIRING);
let actionStatuses = [mockActionStatus({ state: ACTION_STATES.ACTIVE })];
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.ACTIVE);
actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
];
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.FIRING);
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.ACTIVE);
actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.ACKNOWLEDGED }),
];
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.FIRING);
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.ACTIVE);
});
it(`is ERROR when at least one action state is error`, () => {
const isActive = true;
const watchState = 'awaits_execution';
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.ACKNOWLEDGED }),
mockActionStatus({ state: ACTION_STATES.ERROR }),
@ -161,23 +151,22 @@ describe('WatchStatusModel utils', () => {
const isActive = true;
const watchState = 'awaits_execution';
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.CONFIG_ERROR }),
];
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.CONFIG_ERROR);
});
it(`is DISABLED when watch is inactive`, () => {
it(`is INACTIVE when watch is inactive`, () => {
const isActive = false;
const watchState = 'awaits_execution';
const actionStatuses = [
mockActionStatus({ state: ACTION_STATES.OK }),
mockActionStatus({ state: ACTION_STATES.FIRING }),
mockActionStatus({ state: ACTION_STATES.ACTIVE }),
mockActionStatus({ state: ACTION_STATES.THROTTLED }),
mockActionStatus({ state: ACTION_STATES.ACKNOWLEDGED }),
mockActionStatus({ state: ACTION_STATES.ERROR }),
];
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.DISABLED);
expect(deriveState(isActive, watchState, actionStatuses)).toBe(WATCH_STATES.INACTIVE);
});
});
});

View file

@ -28,7 +28,7 @@ export const deriveActionStatusTotals = (
return result;
};
export const deriveLastFired = (actionStatuses: ClientWatchStatusModel['actionStatuses']) => {
export const deriveLastExecution = (actionStatuses: ClientWatchStatusModel['actionStatuses']) => {
const actionStatus = maxBy(actionStatuses, 'lastExecution');
if (actionStatus) {
return actionStatus.lastExecution;
@ -41,7 +41,7 @@ export const deriveState = (
actionStatuses: ClientWatchStatusModel['actionStatuses']
) => {
if (!isActive) {
return WATCH_STATES.DISABLED;
return WATCH_STATES.INACTIVE;
}
if (watchState === 'failed') {
@ -58,16 +58,7 @@ export const deriveState = (
return WATCH_STATES.CONFIG_ERROR;
}
const firingTotal =
totals[ACTION_STATES.FIRING] +
totals[ACTION_STATES.ACKNOWLEDGED] +
totals[ACTION_STATES.THROTTLED];
if (firingTotal > 0) {
return WATCH_STATES.FIRING;
}
return WATCH_STATES.OK;
return WATCH_STATES.ACTIVE;
};
export const deriveComment = (
@ -76,34 +67,33 @@ export const deriveComment = (
) => {
const totals = deriveActionStatusTotals(actionStatuses);
const totalActions = actionStatuses ? actionStatuses.length : 0;
let result = WATCH_STATE_COMMENTS.OK;
if (totals[ACTION_STATES.THROTTLED] > 0 && totals[ACTION_STATES.THROTTLED] < totalActions) {
result = WATCH_STATE_COMMENTS.PARTIALLY_THROTTLED;
if (!isActive) {
return WATCH_STATE_COMMENTS.OK;
}
if (totals[ACTION_STATES.THROTTLED] > 0 && totals[ACTION_STATES.THROTTLED] === totalActions) {
result = WATCH_STATE_COMMENTS.THROTTLED;
}
if (totals[ACTION_STATES.ACKNOWLEDGED] > 0 && totals[ACTION_STATES.ACKNOWLEDGED] < totalActions) {
result = WATCH_STATE_COMMENTS.PARTIALLY_ACKNOWLEDGED;
if (totals[ACTION_STATES.ERROR] > 0) {
return WATCH_STATE_COMMENTS.FAILING;
}
if (
totals[ACTION_STATES.ACKNOWLEDGED] > 0 &&
totals[ACTION_STATES.ACKNOWLEDGED] === totalActions
) {
result = WATCH_STATE_COMMENTS.ACKNOWLEDGED;
return WATCH_STATE_COMMENTS.ACKNOWLEDGED;
}
if (totals[ACTION_STATES.ERROR] > 0) {
result = WATCH_STATE_COMMENTS.FAILING;
if (totals[ACTION_STATES.ACKNOWLEDGED] > 0 && totals[ACTION_STATES.ACKNOWLEDGED] < totalActions) {
return WATCH_STATE_COMMENTS.PARTIALLY_ACKNOWLEDGED;
}
if (!isActive) {
result = WATCH_STATE_COMMENTS.OK;
if (totals[ACTION_STATES.THROTTLED] > 0 && totals[ACTION_STATES.THROTTLED] === totalActions) {
return WATCH_STATE_COMMENTS.THROTTLED;
}
return result;
if (totals[ACTION_STATES.THROTTLED] > 0 && totals[ACTION_STATES.THROTTLED] < totalActions) {
return WATCH_STATE_COMMENTS.PARTIALLY_THROTTLED;
}
return WATCH_STATE_COMMENTS.OK;
};