From c926b14c32307a12b31938641594356655faf360 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 11 Oct 2021 21:06:35 -0400 Subject: [PATCH] [Alerting] Showing last execution duration on Rule Management view (#113935) * Adding last duration to execution status and returning in alerting routes * Fixing types * Adding helper function to format duration * Returning rule timeout value in list rules API * Updating rules table to add duration column and tweaks to match mockup * Updating rules table to add duration column and tweaks to match mockup * i18n fix * Only showing duration warning if duration is long * Unit tests * i18n fix * Fixing functional test * Aligning warning icon * Reset last duration when rule is disabled then reenabled * Fixing functional test * Fixing functional test * Restoring muted badge. Fixing scss * Dont show muted badge if rule is disabled * Moving disabled icontip to right of rule name * Updating tooltips * Updating last run Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/common/alert.ts | 1 + x-pack/plugins/alerting/common/alert_type.ts | 1 + .../alerting/common/parse_duration.test.ts | 39 ++- .../plugins/alerting/common/parse_duration.ts | 16 + .../server/lib/alert_execution_status.test.ts | 49 +++ .../server/lib/alert_execution_status.ts | 20 +- .../alerting/server/routes/create_rule.ts | 3 +- .../alerting/server/routes/find_rules.ts | 3 +- .../alerting/server/routes/get_rule.ts | 3 +- .../alerting/server/routes/resolve_rule.ts | 3 +- .../alerting/server/routes/rule_types.test.ts | 3 + .../alerting/server/routes/rule_types.ts | 2 + .../alerting/server/routes/update_rule.ts | 1 + .../server/rule_type_registry.test.ts | 2 + .../alerting/server/rule_type_registry.ts | 3 + .../server/rules_client/rules_client.ts | 1 + .../server/rules_client/tests/enable.test.ts | 2 + .../server/saved_objects/mappings.json | 3 + .../server/task_runner/task_runner.test.ts | 2 + .../server/task_runner/task_runner.ts | 5 + x-pack/plugins/alerting/server/types.ts | 1 + .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 14 +- .../lib/alert_api/common_transformations.ts | 2 + .../application/lib/alert_api/rule_types.ts | 2 + .../components/alert_status_filter.tsx | 4 +- .../alerts_list/components/alerts_list.scss | 11 + .../components/alerts_list.test.tsx | 57 +++- .../alerts_list/components/alerts_list.tsx | 311 ++++++++++++------ .../collapsed_item_actions.test.tsx | 2 +- .../components/rule_enabled_switch.test.tsx | 4 +- .../triggers_actions_ui/public/types.ts | 3 +- .../tests/alerting/rule_types.ts | 2 + .../spaces_only/tests/alerting/rule_types.ts | 2 + .../apps/triggers_actions_ui/alerts_list.ts | 130 +++----- .../page_objects/triggers_actions_ui_page.ts | 12 +- 36 files changed, 512 insertions(+), 219 deletions(-) diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 1274e7b95b114..bf0c8e382c9d4 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -35,6 +35,7 @@ export enum AlertExecutionStatusErrorReasons { export interface AlertExecutionStatus { status: AlertExecutionStatuses; lastExecutionDate: Date; + lastDuration?: number; error?: { reason: AlertExecutionStatusErrorReasons; message: string; diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerting/common/alert_type.ts index d71540b4418e8..1b0ac28c9fa74 100644 --- a/x-pack/plugins/alerting/common/alert_type.ts +++ b/x-pack/plugins/alerting/common/alert_type.ts @@ -21,6 +21,7 @@ export interface AlertType< producer: string; minimumLicenseRequired: LicenseType; isExportable: boolean; + ruleTaskTimeout?: string; defaultScheduleInterval?: string; minimumScheduleInterval?: string; } diff --git a/x-pack/plugins/alerting/common/parse_duration.test.ts b/x-pack/plugins/alerting/common/parse_duration.test.ts index 9fbb662e21147..e68a3f479f228 100644 --- a/x-pack/plugins/alerting/common/parse_duration.test.ts +++ b/x-pack/plugins/alerting/common/parse_duration.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { parseDuration, getDurationNumberInItsUnit, getDurationUnitValue } from './parse_duration'; +import { + parseDuration, + formatDuration, + getDurationNumberInItsUnit, + getDurationUnitValue, +} from './parse_duration'; test('parses seconds', () => { const result = parseDuration('10s'); @@ -39,6 +44,38 @@ test('throws error when suffix is missing', () => { ); }); +test('formats seconds', () => { + const result = formatDuration('10s'); + expect(result).toEqual('10 sec'); +}); + +test('formats minutes', () => { + const result = formatDuration('10m'); + expect(result).toEqual('10 min'); +}); + +test('formats hours', () => { + const result = formatDuration('10h'); + expect(result).toEqual('10 hr'); +}); + +test('formats days', () => { + const result = formatDuration('10d'); + expect(result).toEqual('10 day'); +}); + +test('format throws error when the format is invalid', () => { + expect(() => formatDuration('10x')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"10x\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + +test('format throws error when suffix is missing', () => { + expect(() => formatDuration('1000')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"1000\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + test('throws error when 0 based', () => { expect(() => parseDuration('0s')).toThrowErrorMatchingInlineSnapshot( `"Invalid duration \\"0s\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` diff --git a/x-pack/plugins/alerting/common/parse_duration.ts b/x-pack/plugins/alerting/common/parse_duration.ts index 3494a48fc8ab9..af4f1d2c14099 100644 --- a/x-pack/plugins/alerting/common/parse_duration.ts +++ b/x-pack/plugins/alerting/common/parse_duration.ts @@ -27,6 +27,22 @@ export function parseDuration(duration: string): number { ); } +export function formatDuration(duration: string): string { + const parsed = parseInt(duration, 10); + if (isSeconds(duration)) { + return `${parsed} sec`; + } else if (isMinutes(duration)) { + return `${parsed} min`; + } else if (isHours(duration)) { + return `${parsed} hr`; + } else if (isDays(duration)) { + return `${parsed} day`; + } + throw new Error( + `Invalid duration "${duration}". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d"` + ); +} + export function getDurationNumberInItsUnit(duration: string): number { return parseInt(duration.replace(/[^0-9.]/g, ''), 10); } diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts b/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts index 0a8d5632f169f..93cf0c656c692 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts @@ -81,6 +81,7 @@ describe('AlertExecutionStatus', () => { expect(alertExecutionStatusToRaw({ lastExecutionDate: date, status })).toMatchInlineSnapshot(` Object { "error": null, + "lastDuration": 0, "lastExecutionDate": "2020-09-03T16:26:58.000Z", "status": "ok", } @@ -95,11 +96,24 @@ describe('AlertExecutionStatus', () => { "message": "wops", "reason": "decrypt", }, + "lastDuration": 0, "lastExecutionDate": "2020-09-03T16:26:58.000Z", "status": "ok", } `); }); + + test('status with a duration', () => { + expect(alertExecutionStatusToRaw({ lastExecutionDate: date, status, lastDuration: 1234 })) + .toMatchInlineSnapshot(` + Object { + "error": null, + "lastDuration": 1234, + "lastExecutionDate": "2020-09-03T16:26:58.000Z", + "status": "ok", + } + `); + }); }); describe('alertExecutionStatusFromRaw()', () => { @@ -177,6 +191,41 @@ describe('AlertExecutionStatus', () => { } `); }); + + test('valid status, date and duration', () => { + const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { + status, + lastExecutionDate: date, + lastDuration: 1234, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "lastDuration": 1234, + "lastExecutionDate": 2020-09-03T16:26:58.000Z, + "status": "active", + } + `); + }); + + test('valid status, date, error and duration', () => { + const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { + status, + lastExecutionDate: date, + error, + lastDuration: 1234, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "message": "wops", + "reason": "execute", + }, + "lastDuration": 1234, + "lastExecutionDate": 2020-09-03T16:26:58.000Z, + "status": "active", + } + `); + }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts index 47dfc659307a2..82d8514331704 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts +++ b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts @@ -32,11 +32,13 @@ export function executionStatusFromError(error: Error): AlertExecutionStatus { export function alertExecutionStatusToRaw({ lastExecutionDate, + lastDuration, status, error, }: AlertExecutionStatus): RawAlertExecutionStatus { return { lastExecutionDate: lastExecutionDate.toISOString(), + lastDuration: lastDuration ?? 0, status, // explicitly setting to null (in case undefined) due to partial update concerns error: error ?? null, @@ -50,7 +52,7 @@ export function alertExecutionStatusFromRaw( ): AlertExecutionStatus | undefined { if (!rawAlertExecutionStatus) return undefined; - const { lastExecutionDate, status = 'unknown', error } = rawAlertExecutionStatus; + const { lastExecutionDate, lastDuration, status = 'unknown', error } = rawAlertExecutionStatus; let parsedDateMillis = lastExecutionDate ? Date.parse(lastExecutionDate) : Date.now(); if (isNaN(parsedDateMillis)) { @@ -60,12 +62,20 @@ export function alertExecutionStatusFromRaw( parsedDateMillis = Date.now(); } - const parsedDate = new Date(parsedDateMillis); + const executionStatus: AlertExecutionStatus = { + status, + lastExecutionDate: new Date(parsedDateMillis), + }; + + if (null != lastDuration) { + executionStatus.lastDuration = lastDuration; + } + if (error) { - return { lastExecutionDate: parsedDate, status, error }; - } else { - return { lastExecutionDate: parsedDate, status }; + executionStatus.error = error; } + + return executionStatus; } export const getAlertExecutionStatusPending = (lastExecutionDate: string) => ({ diff --git a/x-pack/plugins/alerting/server/routes/create_rule.ts b/x-pack/plugins/alerting/server/routes/create_rule.ts index 55a98eb56bee4..ed124bfbd3a2d 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.ts @@ -67,7 +67,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ notifyWhen, muteAll, mutedInstanceIds, - executionStatus: { lastExecutionDate, ...executionStatus }, + executionStatus: { lastExecutionDate, lastDuration, ...executionStatus }, ...rest }) => ({ ...rest, @@ -84,6 +84,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ execution_status: { ...executionStatus, last_execution_date: lastExecutionDate, + last_duration: lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts index a4a066728555d..7826e924acdc5 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -93,8 +93,9 @@ const rewriteBodyRes: RewriteResponseCase> = ({ muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { - ...omit(executionStatus, 'lastExecutionDate'), + ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index c860ae725a253..4da9410517fe1 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -48,8 +48,9 @@ const rewriteBodyRes: RewriteResponseCase> = ({ muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { - ...omit(executionStatus, 'lastExecutionDate'), + ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts index 011d28780e718..a31cf5059b99f 100644 --- a/x-pack/plugins/alerting/server/routes/resolve_rule.ts +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts @@ -48,8 +48,9 @@ const rewriteBodyRes: RewriteResponseCase muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { - ...omit(executionStatus, 'lastExecutionDate'), + ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index e4247c9de6cad..7deb2704fb7ec 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -49,6 +49,7 @@ describe('ruleTypesRoute', () => { defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', isExportable: true, + ruleTaskTimeout: '10m', recoveryActionGroup: RecoveredActionGroup, authorizedConsumers: {}, actionVariables: { @@ -76,6 +77,7 @@ describe('ruleTypesRoute', () => { minimum_license_required: 'basic', minimum_schedule_interval: '1m', is_exportable: true, + rule_task_timeout: '10m', recovery_action_group: RecoveredActionGroup, authorized_consumers: {}, action_variables: { @@ -118,6 +120,7 @@ describe('ruleTypesRoute', () => { "id": "recovered", "name": "Recovered", }, + "rule_task_timeout": "10m", }, ], } diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts index 72502b25e9aff..d1f24538d76d8 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -20,6 +20,7 @@ const rewriteBodyRes: RewriteResponseCase = (result defaultActionGroupId, minimumLicenseRequired, isExportable, + ruleTaskTimeout, actionVariables, authorizedConsumers, minimumScheduleInterval, @@ -33,6 +34,7 @@ const rewriteBodyRes: RewriteResponseCase = (result default_action_group_id: defaultActionGroupId, minimum_license_required: minimumLicenseRequired, is_exportable: isExportable, + rule_task_timeout: ruleTaskTimeout, action_variables: actionVariables, authorized_consumers: authorizedConsumers, minimum_schedule_interval: minimumScheduleInterval, diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts index 6e8024a0ddbf5..007d24bb8251b 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -88,6 +88,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ execution_status: { status: executionStatus.status, last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, } : {}), diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index beb5f264eb725..895a5047339ef 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -494,6 +494,7 @@ describe('list()', () => { ], defaultActionGroupId: 'testActionGroup', isExportable: true, + ruleTaskTimeout: '20m', minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', @@ -530,6 +531,7 @@ describe('list()', () => { "id": "recovered", "name": "Recovered", }, + "ruleTaskTimeout": "20m", }, } `); diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index db02edf4d19dd..452729a9a01e9 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -48,6 +48,7 @@ export interface RegistryRuleType | 'producer' | 'minimumLicenseRequired' | 'isExportable' + | 'ruleTaskTimeout' | 'minimumScheduleInterval' | 'defaultScheduleInterval' > { @@ -327,6 +328,7 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + ruleTaskTimeout, minimumScheduleInterval, defaultScheduleInterval, }, @@ -340,6 +342,7 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + ruleTaskTimeout, minimumScheduleInterval, defaultScheduleInterval, enabledInLicense: !!this.licenseState.getLicenseCheckForAlertType( diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 2228b5d27910f..2492517f4bdc3 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1133,6 +1133,7 @@ export class RulesClient { updatedAt: new Date().toISOString(), executionStatus: { status: 'pending', + lastDuration: 0, lastExecutionDate: new Date().toISOString(), error: null, }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 7b8fbff4fca5a..5e3f148c2fc11 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -260,6 +260,7 @@ describe('enable()', () => { ], executionStatus: { status: 'pending', + lastDuration: 0, lastExecutionDate: '2019-02-12T21:01:22.479Z', error: null, }, @@ -369,6 +370,7 @@ describe('enable()', () => { ], executionStatus: { status: 'pending', + lastDuration: 0, lastExecutionDate: '2019-02-12T21:01:22.479Z', error: null, }, diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json index 21d7a05f2a76d..05e221a47499c 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.json @@ -101,6 +101,9 @@ "lastExecutionDate": { "type": "date" }, + "lastDuration": { + "type": "long" + }, "error": { "properties": { "reason": { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index bc477136ec111..c5ccc909eff46 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -338,6 +338,7 @@ describe('Task Runner', () => { { executionStatus: { error: null, + lastDuration: 0, lastExecutionDate: '1970-01-01T00:00:00.000Z', status: 'ok', }, @@ -4394,6 +4395,7 @@ describe('Task Runner', () => { { executionStatus: { error: null, + lastDuration: 0, lastExecutionDate: '1970-01-01T00:00:00.000Z', status: 'ok', }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 28cc0f2dba4d0..edf9bfe1b4846 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -585,6 +585,11 @@ export class TaskRunner< event.kibana.alerting = event.kibana.alerting || {}; event.kibana.alerting.status = executionStatus.status; + // Copy duration into execution status if available + if (null != event.event?.duration) { + executionStatus.lastDuration = Math.round(event.event?.duration / Millis2Nanos); + } + // if executionStatus indicates an error, fill in fields in // event from it if (executionStatus.error) { diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 1dc8291d28756..82bb94b121840 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -185,6 +185,7 @@ export interface AlertMeta extends SavedObjectAttributes { export interface RawAlertExecutionStatus extends SavedObjectAttributes { status: AlertExecutionStatuses; lastExecutionDate: string; + lastDuration?: number; error: null | { reason: AlertExecutionStatusErrorReasons; message: string; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f36f3abe66a4c..fbd9352fb427f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25336,7 +25336,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "ルールの実行中にエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "不明な理由でエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "アクション", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle": "アクション", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "型", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel": "削除", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip": "削除", @@ -25347,7 +25346,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "ステータス", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "アクティブ", "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "エラー", "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "ステータス", @@ -25383,11 +25381,11 @@ "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "ルール", "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "{pageSize}/{totalItemCount}件のルールを表示しています。", - "xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription": "有効:{totalStausesActive}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesErrorDescription": "エラー:{totalStausesError}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesOkDescription": "Ok:{totalStausesOk}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription": "保留中:{totalStausesPending}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesUnknownDescription": "不明:{totalStausesUnknown}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesActiveDescription": "有効:{totalStatusesActive}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesErrorDescription": "エラー:{totalStatusesError}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesOkDescription": "Ok:{totalStatusesOk}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesPendingDescription": "保留中:{totalStatusesPending}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesUnknownDescription": "不明:{totalStatusesUnknown}", "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage": "コネクタータイプを読み込めません", "xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage": "ルールを読み込めません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 11b951b97ae05..9408bb85db879 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25763,7 +25763,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "运行规则时发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "由于未知原因发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "操作", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle": "操作", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel": "删除", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip": "删除", @@ -25774,7 +25773,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "状态", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标签", "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "活动", "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "错误", "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "状态", @@ -25782,7 +25780,7 @@ "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "确定", "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "待处理", "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "未知", - "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStausesError, plural, other {# 个规则}}中有错误。", + "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStatusesError, plural, other {# 个规则}}中有错误。", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "管理规则", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "删除", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "禁用", @@ -25811,11 +25809,11 @@ "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "规则", "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "正在显示:{pageSize} 个规则(共 {totalItemCount} 个)。", - "xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription": "活动:{totalStausesActive}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesErrorDescription": "错误:{totalStausesError}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesOkDescription": "确定:{totalStausesOk}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription": "待处理:{totalStausesPending}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesUnknownDescription": "未知:{totalStausesUnknown}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesActiveDescription": "活动:{totalStatusesActive}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesErrorDescription": "错误:{totalStatusesError}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesOkDescription": "确定:{totalStatusesOk}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesPendingDescription": "待处理:{totalStatusesPending}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesUnknownDescription": "未知:{totalStatusesUnknown}", "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage": "无法加载连接器类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage": "无法加载规则", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts index 5049a37c317dd..6369937e59377 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts @@ -22,9 +22,11 @@ const transformAction: RewriteRequestCase = ({ const transformExecutionStatus: RewriteRequestCase = ({ last_execution_date: lastExecutionDate, + last_duration: lastDuration, ...rest }) => ({ lastExecutionDate, + lastDuration, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts index 54369d7959c93..67d317643ec06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts @@ -21,6 +21,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ minimum_license_required: minimumLicenseRequired, action_variables: actionVariables, authorized_consumers: authorizedConsumers, + rule_task_timeout: ruleTaskTimeout, ...rest }: AsApiContract) => ({ enabledInLicense, @@ -30,6 +31,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ minimumLicenseRequired, actionVariables, authorizedConsumers, + ruleTaskTimeout, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx index aca111df97e34..50295548f9aa4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx @@ -99,10 +99,10 @@ export function getHealthColor(status: AlertExecutionStatuses) { case 'error': return 'danger'; case 'ok': - return 'subdued'; + return 'primary'; case 'pending': return 'accent'; default: - return 'warning'; + return 'subdued'; } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss index c0e46b77b4156..138605421f202 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss @@ -31,3 +31,14 @@ opacity: 1; /* 2 */ } } + +.ruleDurationWarningIcon { + margin-bottom: $euiSizeXS; + margin-left: $euiSizeS; +} + +.ruleDisabledQuestionIcon { + bottom: $euiSizeXS; + margin-left: $euiSizeXS; + position: relative; +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 958511128de04..53f5e25530e98 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -17,6 +17,7 @@ import { AlertTypeModel, ValidationResult } from '../../../../types'; import { AlertExecutionStatusErrorReasons, ALERTS_FEATURE_ID, + parseDuration, } from '../../../../../../alerting/common'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); @@ -79,6 +80,7 @@ const alertTypeFromApi = { authorizedConsumers: { [ALERTS_FEATURE_ID]: { read: true, all: true }, }, + ruleTaskTimeout: '1m', }; ruleTypeRegistry.list.mockReturnValue([alertType]); actionTypeRegistry.list.mockReturnValue([]); @@ -170,6 +172,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'active', + lastDuration: 500, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: null, }, @@ -192,6 +195,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'ok', + lastDuration: 61000, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: null, }, @@ -214,6 +218,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'pending', + lastDuration: 30234, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: null, }, @@ -236,6 +241,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'error', + lastDuration: 122000, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: { reason: AlertExecutionStatusErrorReasons.Unknown, @@ -246,7 +252,7 @@ describe('alerts_list component with items', () => { { id: '5', name: 'test alert license error', - tags: ['tag1'], + tags: [], enabled: true, alertTypeId: 'test_alert_type', schedule: { interval: '5d' }, @@ -261,6 +267,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'error', + lastDuration: 500, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: { reason: AlertExecutionStatusErrorReasons.License, @@ -324,6 +331,53 @@ describe('alerts_list component with items', () => { await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(mockedAlertsData.length); + + // Enabled switch column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-enabled"]').length + ).toEqual(mockedAlertsData.length); + + // Name and rule type column + const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-name"]'); + expect(ruleNameColumns.length).toEqual(mockedAlertsData.length); + mockedAlertsData.forEach((rule, index) => { + expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${alertTypeFromApi.name}`); + }); + + // Tags column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-tagsPopover"]').length + ).toEqual(mockedAlertsData.length); + // only show tags popover if tags exist on rule + const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagsBadge"]'); + expect(tagsBadges.length).toEqual( + mockedAlertsData.filter((data) => data.tags.length > 0).length + ); + + // Last run column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-lastExecutionDate"]').length + ).toEqual(mockedAlertsData.length); + + // Schedule interval column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-interval"]').length + ).toEqual(mockedAlertsData.length); + + // Duration column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-duration"]').length + ).toEqual(mockedAlertsData.length); + // show warning if duration is long + const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); + expect(durationWarningIcon.length).toEqual( + mockedAlertsData.filter( + (data) => + data.executionStatus.lastDuration > parseDuration(alertTypeFromApi.ruleTaskTimeout) + ).length + ); + + // Status column expect(wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-status"]').length).toEqual( mockedAlertsData.length ); @@ -331,7 +385,6 @@ describe('alerts_list component with items', () => { expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-ok"]').length).toEqual(1); expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-pending"]').length).toEqual(1); expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-unknown"]').length).toEqual(0); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').length).toEqual(2); expect(wrapper.find('[data-test-subj="alertStatus-error-tooltip"]').length).toEqual(2); expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1daaf3b996126..91b1b14083ed2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -8,7 +8,8 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { i18n } from '@kbn/i18n'; -import { capitalize, sortBy } from 'lodash'; +import { capitalize, padStart, sortBy } from 'lodash'; +import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useEffect, useState } from 'react'; import { @@ -29,6 +30,10 @@ import { EuiToolTip, EuiTableSortingType, EuiButtonIcon, + EuiHorizontalRule, + EuiPopover, + EuiPopoverTitle, + EuiIcon, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -66,6 +71,8 @@ import { AlertExecutionStatusValues, ALERTS_FEATURE_ID, AlertExecutionStatusErrorReasons, + formatDuration, + parseDuration, } from '../../../../../../alerting/common'; import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; @@ -114,6 +121,7 @@ export const AlertsList: React.FunctionComponent = () => { const [dismissAlertErrors, setDismissAlertErrors] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); + const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); const [sort, setSort] = useState['sort']>({ field: 'name', @@ -334,7 +342,7 @@ export const AlertsList: React.FunctionComponent = () => { 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.enabledTitle', { defaultMessage: 'Enabled' } ), - width: '90px', + width: '50px', render(_enabled: boolean | undefined, item: AlertTableItem) { return ( { const checkEnabledResult = checkAlertTypeEnabled(ruleType); const link = ( <> - { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); - }} - > - {name} - + + + + + { + history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + }} + > + {name} + + + + {!checkEnabledResult.isEnabled && ( + + )} + + + + + + {alert.alertType} + + + ); - return checkEnabledResult.isEnabled ? ( - link - ) : ( + return ( <> {link} - + {alert.enabled && alert.muteAll && ( + + + + )} ); }, }, { - field: 'executionStatus.status', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', - { defaultMessage: 'Status' } - ), - sortable: true, - truncateText: false, - width: '120px', - 'data-test-subj': 'alertsTableCell-status', - render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { - return renderAlertExecutionStatus(item.executionStatus, item); - }, - }, - { - field: 'alertType', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle', - { defaultMessage: 'Type' } - ), + field: 'tags', + name: '', sortable: false, - truncateText: true, - render: (_count: number, item: AlertTableItem) => ( - {item.alertType} - ), - 'data-test-subj': 'alertsTableCell-alertType', + width: '50px', + 'data-test-subj': 'alertsTableCell-tagsPopover', + render: (tags: string[], item: AlertTableItem) => { + return tags.length > 0 ? ( + setTagPopoverOpenIndex(item.index)} + onClickAriaLabel="Tags" + iconOnClick={() => setTagPopoverOpenIndex(item.index)} + iconOnClickAriaLabel="Tags" + > + {tags.length} + + } + anchorPosition="upCenter" + isOpen={tagPopoverOpenIndex === item.index} + closePopover={() => setTagPopoverOpenIndex(-1)} + > + Tags +
+ {tags.map((tag: string, index: number) => ( + + {tag} + + ))} + + ) : null; + }, }, { - field: 'tagsText', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText', - { defaultMessage: 'Tags' } - ), - sortable: false, - 'data-test-subj': 'alertsTableCell-tagsText', - render: (_count: number, item: AlertTableItem) => ( -
- {item.tagsText} -
- ), + field: 'executionStatus.lastExecutionDate', + name: 'Last run', + sortable: true, + width: '15%', + 'data-test-subj': 'alertsTableCell-lastExecutionDate', + render: (date: Date) => { + if (date) { + return ( + <> + + + {moment(date).format('MMM D, YYYY HH:mm:ssa')} + + + + {moment(date).fromNow()} + + + + + ); + } + }, }, { field: 'schedule.interval', width: '6%', name: i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle', - { defaultMessage: 'Runs every' } + { defaultMessage: 'Interval' } ), sortable: false, truncateText: false, 'data-test-subj': 'alertsTableCell-interval', + render: (interval: string) => formatDuration(interval), }, { - width: '9%', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle', - { defaultMessage: 'Actions' } + field: 'executionStatus.lastDuration', + width: '12%', + name: ( + + + Duration{' '} + + + ), - render: (item: AlertTableItem) => { + sortable: true, + truncateText: false, + 'data-test-subj': 'alertsTableCell-duration', + render: (value: number, item: AlertTableItem) => { + const ruleTypeTimeout: string | undefined = alertTypesState.data.get( + item.alertTypeId + )?.ruleTaskTimeout; + const ruleTypeTimeoutMillis: number | undefined = ruleTypeTimeout + ? parseDuration(ruleTypeTimeout) + : undefined; + const showDurationWarning: boolean = ruleTypeTimeoutMillis + ? value > ruleTypeTimeoutMillis + : false; + const duration = moment.duration(value); + const durationString = [duration.hours(), duration.minutes(), duration.seconds()] + .map((v: number) => padStart(`${v}`, 2, '0')) + .join(':'); + + // add millis + const millisString = padStart(`${duration.milliseconds()}`, 3, '0'); return ( - - {item.actionsCount} - -
- {item.muteAll ? ( - - - - ) : null} -
-
-
+ <> + {`${durationString}.${millisString}`} + {showDurationWarning && ( + + )} + ); }, - 'data-test-subj': 'alertsTableCell-actions', + }, + { + field: 'executionStatus.status', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', + { defaultMessage: 'Status' } + ), + sortable: true, + truncateText: false, + width: '120px', + 'data-test-subj': 'alertsTableCell-status', + render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { + return renderAlertExecutionStatus(item.executionStatus, item); + }, }, { name: '', @@ -669,7 +777,7 @@ export const AlertsList: React.FunctionComponent = () => { - + {!dismissAlertErrors && alertsStatusesTotal.error > 0 ? ( @@ -679,9 +787,9 @@ export const AlertsList: React.FunctionComponent = () => { title={ } @@ -726,10 +834,10 @@ export const AlertsList: React.FunctionComponent = () => { @@ -737,46 +845,45 @@ export const AlertsList: React.FunctionComponent = () => { - + - + - {/* Large to remain consistent with ActionsList table spacing */} - + { setAlertsState({ ...alertsState, isLoading }); }} /> - + {loadedItems.length || isFilterApplied ? ( table ) : alertTypesState.isLoading || alertsState.isLoading ? ( @@ -958,10 +1065,10 @@ function convertAlertsToTableItems( ruleTypeIndex: RuleTypeIndex, canExecuteActions: boolean ) { - return alerts.map((alert) => ({ + return alerts.map((alert, index: number) => ({ ...alert, + index, actionsCount: alert.actions.length, - tagsText: alert.tags.join(', '), alertType: ruleTypeIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, isEditable: hasAllPrivilege(alert, ruleTypeIndex.get(alert.alertTypeId)) && diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx index 5a06b03311cbe..807fe65a561f8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx @@ -75,7 +75,7 @@ describe('CollapsedItemActions', () => { lastExecutionDate: new Date('2020-08-20T19:23:38Z'), }, actionsCount: 1, - tagsText: 'tag1', + index: 0, alertType: 'Test Alert Type', isEditable: true, enabledInLicense: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx index bfa760b65ed4e..da75faeda95e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx @@ -40,7 +40,7 @@ describe('RuleEnabledSwitch', () => { enabledInLicense: true, isEditable: false, notifyWhen: null, - tagsText: 'test', + index: 0, updatedAt: new Date('2020-08-20T19:23:38Z'), }, onAlertChanged: jest.fn(), @@ -84,7 +84,7 @@ describe('RuleEnabledSwitch', () => { enabledInLicense: true, isEditable: true, notifyWhen: null, - tagsText: 'test', + index: 0, updatedAt: new Date('2020-08-20T19:23:38Z'), }, }} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2ef20f36b7ca9..a78d1d52de0bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -197,6 +197,7 @@ export interface AlertType< | 'minimumLicenseRequired' | 'recoveryActionGroup' | 'defaultActionGroupId' + | 'ruleTaskTimeout' | 'defaultScheduleInterval' | 'minimumScheduleInterval' > { @@ -211,7 +212,7 @@ export type AlertUpdates = Omit; export interface AlertTableItem extends Alert { alertType: AlertType['name']; - tagsText: string; + index: number; actionsCount: number; isEditable: boolean; enabledInLicense: boolean; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts index f52f0977a630b..b070219410fd9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts @@ -36,6 +36,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { name: 'Recovered', }, enabled_in_license: true, + rule_task_timeout: '5m', }; const expectedRestrictedNoOpType = { @@ -59,6 +60,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { minimum_license_required: 'basic', is_exportable: true, enabled_in_license: true, + rule_task_timeout: '5m', }; describe('rule_types', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts index 86a0e269b26d6..77638ed90fbe4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts @@ -44,6 +44,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { minimum_license_required: 'basic', is_exportable: true, enabled_in_license: true, + rule_task_timeout: '5m', }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); @@ -129,6 +130,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { minimumLicenseRequired: 'basic', isExportable: true, enabledInLicense: true, + ruleTaskTimeout: '5m', }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 3f36032523e8b..dede481669664 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -88,9 +88,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); expect(searchResults).to.have.length(3); - expect(searchResults[0].name).to.eql('a'); - expect(searchResults[1].name).to.eql('b'); - expect(searchResults[2].name).to.eql('c'); + // rule list shows name and rule type id + expect(searchResults[0].name).to.eql('aTest: Noop'); + expect(searchResults[1].name).to.eql('bTest: Noop'); + expect(searchResults[2].name).to.eql('cTest: Noop'); }); it('should search for alert', async () => { @@ -99,67 +100,54 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResults.length).to.equal(1); + expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(searchResults[0].interval).to.equal('1 min'); + expect(searchResults[0].tags).to.equal('2'); + expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); it('should update alert list on the search clear button click', async () => { await createAlert({ name: 'b' }); - await createAlert({ name: 'c' }); + await createAlert({ name: 'c', tags: [] }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts('b'); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: 'b', - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResults.length).to.equal(1); + expect(searchResults[0].name).to.equal('bTest: Noop'); + expect(searchResults[0].interval).to.equal('1 min'); + expect(searchResults[0].tags).to.equal('2'); + expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); await searchClearButton.click(); await find.byCssSelector( '.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)' ); const searchResultsAfterClear = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterClear).to.eql([ - { - name: 'b', - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - { - name: 'c', - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResultsAfterClear.length).to.equal(2); + expect(searchResultsAfterClear[0].name).to.equal('bTest: Noop'); + expect(searchResultsAfterClear[0].interval).to.equal('1 min'); + expect(searchResultsAfterClear[0].tags).to.equal('2'); + expect(searchResultsAfterClear[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResultsAfterClear[1].name).to.equal('cTest: Noop'); + expect(searchResultsAfterClear[1].interval).to.equal('1 min'); + expect(searchResultsAfterClear[1].tags).to.equal(''); + expect(searchResultsAfterClear[1].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); it('should search for tags', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ tags: ['tag', 'tagtag', 'taggity tag'] }); await refreshAlertsList(); - await pageObjects.triggersActionsUI.searchAlerts(`${createdAlert.name} foo`); + await pageObjects.triggersActionsUI.searchAlerts(`${createdAlert.name} tag`); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResults.length).to.equal(1); + expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(searchResults[0].interval).to.equal('1 min'); + expect(searchResults[0].tags).to.equal('3'); + expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); it('should display an empty list when search did not return any alerts', async () => { @@ -385,15 +373,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const filterErrorOnlyResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); - expect(filterErrorOnlyResults).to.eql([ - { - name: failingAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Failing', - interval: '30s', - status: 'Error', - }, - ]); + expect(filterErrorOnlyResults.length).to.equal(1); + expect(filterErrorOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); + expect(filterErrorOnlyResults[0].interval).to.equal('30 sec'); + expect(filterErrorOnlyResults[0].status).to.equal('Error'); + expect(filterErrorOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); }); @@ -402,15 +386,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { await refreshAlertsList(); const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); - expect(refreshResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - status: 'Ok', - }, - ]); + expect(refreshResults.length).to.equal(1); + expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(refreshResults[0].interval).to.equal('1 min'); + expect(refreshResults[0].status).to.equal('Ok'); + expect(refreshResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); const alertsErrorBannerWhenNoErrors = await find.allByCssSelector( @@ -451,14 +431,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const filterFailingAlertOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(filterFailingAlertOnlyResults).to.eql([ - { - name: failingAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Failing', - interval: '30s', - }, - ]); + expect(filterFailingAlertOnlyResults.length).to.equal(1); + expect(filterFailingAlertOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); + expect(filterFailingAlertOnlyResults[0].interval).to.equal('30 sec'); + expect(filterFailingAlertOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); }); @@ -480,14 +456,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const filterWithSlackOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(filterWithSlackOnlyResults).to.eql([ - { - name: noopAlertWithAction.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(filterWithSlackOnlyResults.length).to.equal(1); + expect(filterWithSlackOnlyResults[0].name).to.equal( + `${noopAlertWithAction.name}Test: Noop` + ); + expect(filterWithSlackOnlyResults[0].interval).to.equal('1 min'); + expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); }); }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 890315698f74c..a49873c6d47b5 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -22,18 +22,18 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) function getRowItemData(row: CustomCheerio, $: CustomCheerioStatic) { return { name: $(row).findTestSubject('alertsTableCell-name').find('.euiTableCellContent').text(), - tagsText: $(row) - .findTestSubject('alertsTableCell-tagsText') - .find('.euiTableCellContent') - .text(), - alertType: $(row) - .findTestSubject('alertsTableCell-alertType') + duration: $(row) + .findTestSubject('alertsTableCell-duration') .find('.euiTableCellContent') .text(), interval: $(row) .findTestSubject('alertsTableCell-interval') .find('.euiTableCellContent') .text(), + tags: $(row) + .findTestSubject('alertsTableCell-tagsPopover') + .find('.euiTableCellContent') + .text(), }; }