From dc3f76b55788dc2e3b3acd01971283371f08b07f Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Thu, 30 May 2024 19:11:33 -0700 Subject: [PATCH] [Response Ops][Rule Form V2] Rule Form V2: Rule Definition (#183325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Issue: https://github.com/elastic/kibana/issues/179105 Related PR: https://github.com/elastic/kibana/pull/180539 Part 1 of 3 PRs of new rule form. This PR extracts the first section of the rule form, the rule definition, from the original PR. The purpose is to fix a few bugs (Such as improving the alert delay and the rule schedule input validation), and also try to make the PR much smaller for review. The design philosophy in the PR is to create components that are devoid of any fetching or form logic. These are simply dumb components. I have also created a example plugin to demonstrate this PR. To access: 1. Run the branch with `yarn start --run-examples` 2. Navigate to `http://localhost:5601/app/triggersActionsUiExample/rule_definition` And you should be able to play around with the components in this PR: Screenshot 2024-05-13 at 10 10 51 AM ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Zacqary Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-alerting-types/index.ts | 5 +- packages/kbn-alerting-types/r_rule_types.ts | 19 ++ .../rule_notify_when_type.ts | 21 ++ .../{rule_type.ts => rule_type_types.ts} | 0 packages/kbn-alerting-types/rule_types.ts | 241 +++++++++++++++ packages/kbn-alerting-types/tsconfig.json | 5 +- packages/kbn-alerts-ui-shared/index.ts | 2 + .../common/hooks/use_load_rule_types_query.ts | 18 +- .../src/rule_form/index.ts | 11 + .../src/rule_form/rule_definition/index.ts | 9 + .../rule_definition/rule_alert_delay.test.tsx | 98 ++++++ .../rule_definition/rule_alert_delay.tsx | 68 ++++ .../rule_consumer_selection.test.tsx | 91 ++++++ .../rule_consumer_selection.tsx | 111 +++++++ .../rule_definition/rule_definition.test.tsx | 219 +++++++++++++ .../rule_definition/rule_definition.tsx | 292 ++++++++++++++++++ .../rule_definition/rule_schedule.test.tsx | 86 ++++++ .../rule_definition/rule_schedule.tsx | 130 ++++++++ .../src/rule_form/translations.ts | 196 ++++++++++++ .../src/rule_form/types/index.ts | 97 ++++++ .../src/rule_form/utils/get_errors.ts | 107 +++++++ .../rule_form/utils/get_time_options.test.ts | 43 +++ .../src/rule_form/utils/get_time_options.ts | 69 +++++ .../src/rule_form/utils/index.ts | 11 + .../rule_form/utils/parse_duration.test.ts | 221 +++++++++++++ .../src/rule_form/utils/parse_duration.ts | 82 +++++ packages/kbn-alerts-ui-shared/tsconfig.json | 3 + .../triggers_actions_ui_example/kibana.jsonc | 6 +- .../public/application.tsx | 52 +++- .../rule_form/rule_definition_sandbox.tsx | 171 ++++++++++ .../public/components/sidebar.tsx | 11 + .../public/plugin.tsx | 10 +- .../triggers_actions_ui_example/tsconfig.json | 7 + .../plugins/alerting/common/iso_weekdays.ts | 4 +- x-pack/plugins/alerting/common/rrule_type.ts | 12 +- x-pack/plugins/alerting/common/rule.ts | 246 +++------------ .../alerting/common/rule_notify_when_type.ts | 17 +- .../alerting/common/rule_snooze_type.ts | 19 +- 38 files changed, 2545 insertions(+), 265 deletions(-) create mode 100644 packages/kbn-alerting-types/r_rule_types.ts create mode 100644 packages/kbn-alerting-types/rule_notify_when_type.ts rename packages/kbn-alerting-types/{rule_type.ts => rule_type_types.ts} (100%) create mode 100644 packages/kbn-alerting-types/rule_types.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.test.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/translations.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/types/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/utils/get_errors.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.test.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.test.ts create mode 100644 packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.ts create mode 100644 x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_definition_sandbox.tsx diff --git a/packages/kbn-alerting-types/index.ts b/packages/kbn-alerting-types/index.ts index 6d63366f4343d..fcfccebbae425 100644 --- a/packages/kbn-alerting-types/index.ts +++ b/packages/kbn-alerting-types/index.ts @@ -7,6 +7,9 @@ */ export * from './builtin_action_groups_types'; -export * from './rule_type'; +export * from './rule_type_types'; export * from './action_group_types'; export * from './alert_type'; +export * from './rule_notify_when_type'; +export * from './r_rule_types'; +export * from './rule_types'; diff --git a/packages/kbn-alerting-types/r_rule_types.ts b/packages/kbn-alerting-types/r_rule_types.ts new file mode 100644 index 0000000000000..104d3784f085e --- /dev/null +++ b/packages/kbn-alerting-types/r_rule_types.ts @@ -0,0 +1,19 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { WeekdayStr, Options } from '@kbn/rrule'; + +export type RRuleParams = Partial & Pick; + +// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec +export type RRuleRecord = Omit & { + dtstart: string; + byweekday?: Array; + wkst?: WeekdayStr; + until?: string; +}; diff --git a/packages/kbn-alerting-types/rule_notify_when_type.ts b/packages/kbn-alerting-types/rule_notify_when_type.ts new file mode 100644 index 0000000000000..f078a9d0d0b28 --- /dev/null +++ b/packages/kbn-alerting-types/rule_notify_when_type.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const RuleNotifyWhenTypeValues = [ + 'onActionGroupChange', + 'onActiveAlert', + 'onThrottleInterval', +] as const; + +export type RuleNotifyWhenType = typeof RuleNotifyWhenTypeValues[number]; + +export enum RuleNotifyWhen { + CHANGE = 'onActionGroupChange', + ACTIVE = 'onActiveAlert', + THROTTLE = 'onThrottleInterval', +} diff --git a/packages/kbn-alerting-types/rule_type.ts b/packages/kbn-alerting-types/rule_type_types.ts similarity index 100% rename from packages/kbn-alerting-types/rule_type.ts rename to packages/kbn-alerting-types/rule_type_types.ts diff --git a/packages/kbn-alerting-types/rule_types.ts b/packages/kbn-alerting-types/rule_types.ts new file mode 100644 index 0000000000000..c6fc66788cb0d --- /dev/null +++ b/packages/kbn-alerting-types/rule_types.ts @@ -0,0 +1,241 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { SavedObjectAttributes } from '@kbn/core/server'; +import type { Filter } from '@kbn/es-query'; +import type { RuleNotifyWhenType, RRuleParams } from '.'; + +export type RuleTypeParams = Record; +export type RuleActionParams = SavedObjectAttributes; + +export const ISO_WEEKDAYS = [1, 2, 3, 4, 5, 6, 7] as const; +export type IsoWeekday = typeof ISO_WEEKDAYS[number]; + +export interface IntervalSchedule extends SavedObjectAttributes { + interval: string; +} + +export interface RuleActionFrequency extends SavedObjectAttributes { + summary: boolean; + notifyWhen: RuleNotifyWhenType; + throttle: string | null; +} + +export interface AlertsFilterTimeframe extends SavedObjectAttributes { + days: IsoWeekday[]; + timezone: string; + hours: { + start: string; + end: string; + }; +} + +export interface AlertsFilter extends SavedObjectAttributes { + query?: { + kql: string; + filters: Filter[]; + dsl?: string; // This fields is generated in the code by using "kql", therefore it's not optional but defined as optional to avoid modifying a lot of files in different plugins + }; + timeframe?: AlertsFilterTimeframe; +} + +export interface RuleAction { + uuid?: string; + group: string; + id: string; + actionTypeId: string; + params: RuleActionParams; + frequency?: RuleActionFrequency; + alertsFilter?: AlertsFilter; + useAlertDataForTemplate?: boolean; +} + +export interface RuleSystemAction { + uuid?: string; + id: string; + actionTypeId: string; + params: RuleActionParams; +} + +export interface MappedParamsProperties { + risk_score?: number; + severity?: string; +} + +export type MappedParams = SavedObjectAttributes & MappedParamsProperties; + +// for the `typeof ThingValues[number]` types below, become string types that +// only accept the values in the associated string arrays +export const RuleExecutionStatusValues = [ + 'ok', + 'active', + 'error', + 'pending', + 'unknown', + 'warning', +] as const; + +export const RuleLastRunOutcomeValues = ['succeeded', 'warning', 'failed'] as const; + +export enum RuleExecutionStatusErrorReasons { + Read = 'read', + Decrypt = 'decrypt', + Execute = 'execute', + Unknown = 'unknown', + License = 'license', + Timeout = 'timeout', + Disabled = 'disabled', + Validate = 'validate', +} + +export enum RuleExecutionStatusWarningReasons { + MAX_EXECUTABLE_ACTIONS = 'maxExecutableActions', + MAX_ALERTS = 'maxAlerts', + MAX_QUEUED_ACTIONS = 'maxQueuedActions', +} + +export type RuleExecutionStatuses = typeof RuleExecutionStatusValues[number]; +export type RuleLastRunOutcomes = typeof RuleLastRunOutcomeValues[number]; + +export interface RuleExecutionStatus { + status: RuleExecutionStatuses; + lastExecutionDate: Date; + lastDuration?: number; + error?: { + reason: RuleExecutionStatusErrorReasons; + message: string; + }; + warning?: { + reason: RuleExecutionStatusWarningReasons; + message: string; + }; +} + +export interface RuleMonitoringHistory extends SavedObjectAttributes { + success: boolean; + timestamp: number; + duration?: number; + outcome?: RuleLastRunOutcomes; +} + +export interface RuleMonitoringCalculatedMetrics extends SavedObjectAttributes { + p50?: number; + p95?: number; + p99?: number; + success_ratio: number; +} + +export interface RuleMonitoringLastRunMetrics extends SavedObjectAttributes { + duration?: number; + total_search_duration_ms?: number | null; + total_indexing_duration_ms?: number | null; + total_alerts_detected?: number | null; + total_alerts_created?: number | null; + gap_duration_s?: number | null; +} + +export interface RuleMonitoringLastRun extends SavedObjectAttributes { + timestamp: string; + metrics: RuleMonitoringLastRunMetrics; +} + +export interface RuleMonitoring { + run: { + history: RuleMonitoringHistory[]; + calculated_metrics: RuleMonitoringCalculatedMetrics; + last_run: RuleMonitoringLastRun; + }; +} + +export interface RuleSnoozeSchedule { + duration: number; + rRule: RRuleParams; + // For scheduled/recurring snoozes, `id` uniquely identifies them so that they can be displayed, modified, and deleted individually + id?: string; + skipRecurrences?: string[]; +} + +// Type signature of has to be repeated here to avoid issues with SavedObject compatibility +// RuleSnooze = RuleSnoozeSchedule[] throws typescript errors across the whole lib +export type RuleSnooze = Array<{ + duration: number; + rRule: RRuleParams; + id?: string; + skipRecurrences?: string[]; +}>; + +export interface RuleLastRun { + outcome: RuleLastRunOutcomes; + outcomeOrder?: number; + warning?: RuleExecutionStatusErrorReasons | RuleExecutionStatusWarningReasons | null; + outcomeMsg?: string[] | null; + alertsCount: { + active?: number | null; + new?: number | null; + recovered?: number | null; + ignored?: number | null; + }; +} + +export interface AlertDelay extends SavedObjectAttributes { + active: number; +} + +export interface SanitizedAlertsFilter extends AlertsFilter { + query?: { + kql: string; + filters: Filter[]; + }; + timeframe?: AlertsFilterTimeframe; +} + +export type SanitizedRuleAction = Omit & { + alertsFilter?: SanitizedAlertsFilter; +}; + +export interface Rule { + id: string; + enabled: boolean; + name: string; + tags: string[]; + alertTypeId: string; // this is persisted in the Rule saved object so we would need a migration to change this to ruleTypeId + consumer: string; + schedule: IntervalSchedule; + actions: RuleAction[]; + systemActions?: RuleSystemAction[]; + params: Params; + mapped_params?: MappedParams; + scheduledTaskId?: string | null; + createdBy: string | null; + updatedBy: string | null; + createdAt: Date; + updatedAt: Date; + apiKey: string | null; + apiKeyOwner: string | null; + apiKeyCreatedByUser?: boolean | null; + throttle?: string | null; + muteAll: boolean; + notifyWhen?: RuleNotifyWhenType | null; + mutedInstanceIds: string[]; + executionStatus: RuleExecutionStatus; + monitoring?: RuleMonitoring; + snoozeSchedule?: RuleSnooze; // Remove ? when this parameter is made available in the public API + activeSnoozes?: string[]; + isSnoozedUntil?: Date | null; + lastRun?: RuleLastRun | null; + nextRun?: Date | null; + revision: number; + running?: boolean | null; + viewInAppRelativeUrl?: string; + alertDelay?: AlertDelay; +} + +export type SanitizedRule = Omit< + Rule, + 'apiKey' | 'actions' +> & { actions: SanitizedRuleAction[] }; diff --git a/packages/kbn-alerting-types/tsconfig.json b/packages/kbn-alerting-types/tsconfig.json index 911e35551bbbd..195502cd5a729 100644 --- a/packages/kbn-alerting-types/tsconfig.json +++ b/packages/kbn-alerting-types/tsconfig.json @@ -18,6 +18,9 @@ "kbn_references": [ "@kbn/i18n", "@kbn/licensing-plugin", - "@kbn/rule-data-utils" + "@kbn/rule-data-utils", + "@kbn/rrule", + "@kbn/core", + "@kbn/es-query" ] } diff --git a/packages/kbn-alerts-ui-shared/index.ts b/packages/kbn-alerts-ui-shared/index.ts index 2e766387acf64..45724a0a4f87d 100644 --- a/packages/kbn-alerts-ui-shared/index.ts +++ b/packages/kbn-alerts-ui-shared/index.ts @@ -20,3 +20,5 @@ export * from './src/alert_fields_table'; export * from './src/rule_type_modal'; export * from './src/alert_filter_controls/types'; +export * from './src/rule_form'; +export * from './src/common/hooks'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts index d5c66e5c502fc..c89837cee6f68 100644 --- a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts @@ -20,16 +20,20 @@ import { RuleTypeIndexWithDescriptions, RuleTypeWithDescription } from '../types export interface UseRuleTypesProps { http: HttpStart; toasts: ToastsStart; - filteredRuleTypes: string[]; + filteredRuleTypes?: string[]; registeredRuleTypes?: Array<{ id: string; description: string }>; enabled?: boolean; } -const getFilteredIndex = ( - data: Array>, - filteredRuleTypes: string[], - registeredRuleTypes: UseRuleTypesProps['registeredRuleTypes'] -) => { +const getFilteredIndex = ({ + data, + filteredRuleTypes, + registeredRuleTypes, +}: { + data: Array>; + filteredRuleTypes?: string[]; + registeredRuleTypes: UseRuleTypesProps['registeredRuleTypes']; +}) => { const index: RuleTypeIndexWithDescriptions = new Map(); const registeredRuleTypesDictionary = registeredRuleTypes ? keyBy(registeredRuleTypes, 'id') : {}; for (const ruleType of data) { @@ -88,7 +92,7 @@ export const useLoadRuleTypesQuery = ({ const filteredIndex = useMemo( () => data - ? getFilteredIndex(data, filteredRuleTypes, registeredRuleTypes) + ? getFilteredIndex({ data, filteredRuleTypes, registeredRuleTypes }) : new Map(), [data, filteredRuleTypes, registeredRuleTypes] ); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/index.ts new file mode 100644 index 0000000000000..dbdcd4efa464f --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/index.ts @@ -0,0 +1,11 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './rule_definition'; +export * from './utils'; +export * from './types'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/index.ts new file mode 100644 index 0000000000000..6e81a156ec42b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './rule_definition'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx new file mode 100644 index 0000000000000..0613fa616de2c --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.test.tsx @@ -0,0 +1,98 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { RuleAlertDelay } from './rule_alert_delay'; + +const mockOnChange = jest.fn(); + +describe('RuleAlertDelay', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + test('Renders correctly', () => { + render( + + ); + + expect(screen.getByTestId('alertDelay')).toBeInTheDocument(); + }); + + test('Should handle input change', () => { + render( + + ); + + fireEvent.change(screen.getByTestId('alertDelayInput'), { + target: { + value: '3', + }, + }); + + expect(mockOnChange).toHaveBeenCalledWith('alertDelay', { active: 3 }); + }); + + test('Should only allow integers as inputs', async () => { + render(); + + ['-', '+', 'e', 'E', '.', 'a', '01'].forEach((char) => { + fireEvent.change(screen.getByTestId('alertDelayInput'), { + target: { + value: char, + }, + }); + }); + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + test('Should call onChange with null if empty string is typed', () => { + render( + + ); + + fireEvent.change(screen.getByTestId('alertDelayInput'), { + target: { + value: '', + }, + }); + expect(mockOnChange).toHaveBeenCalledWith('alertDelay', null); + }); + + test('Should display error when input is invalid', () => { + render( + + ); + + expect(screen.getByText('Alert delay must be greater than 1.')).toBeInTheDocument(); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx new file mode 100644 index 0000000000000..7418215c71755 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_alert_delay.tsx @@ -0,0 +1,68 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import type { SanitizedRule, RuleTypeParams } from '@kbn/alerting-types'; +import { ALERT_DELAY_TITLE_PREFIX, ALERT_DELAY_TITLE_SUFFIX } from '../translations'; +import { RuleFormErrors } from '../types'; + +const INTEGER_REGEX = /^[1-9][0-9]*$/; +const INVALID_KEYS = ['-', '+', '.', 'e', 'E']; + +export interface RuleAlertDelayProps { + alertDelay?: SanitizedRule['alertDelay'] | null; + errors?: RuleFormErrors; + onChange: (property: string, value: unknown) => void; +} + +export const RuleAlertDelay = (props: RuleAlertDelayProps) => { + const { alertDelay, errors = {}, onChange } = props; + + const onAlertDelayChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value.trim(); + if (value === '') { + onChange('alertDelay', null); + } else if (INTEGER_REGEX.test(value)) { + const parsedValue = parseInt(value, 10); + onChange('alertDelay', { active: parsedValue }); + } + }, + [onChange] + ); + + const onKeyDown = useCallback((e: React.KeyboardEvent) => { + if (INVALID_KEYS.includes(e.key)) { + e.preventDefault(); + } + }, []); + + return ( + 0} + error={errors.alertDelay} + data-test-subj="alertDelay" + display="rowCompressed" + > + 0} + append={ALERT_DELAY_TITLE_SUFFIX} + onChange={onAlertDelayChange} + onKeyDown={onKeyDown} + /> + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.test.tsx new file mode 100644 index 0000000000000..1a1a577d5d684 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { RuleConsumerSelection } from './rule_consumer_selection'; + +const mockOnChange = jest.fn(); +const mockConsumers: RuleCreationValidConsumer[] = ['logs', 'infrastructure', 'stackAlerts']; + +describe('RuleConsumerSelection', () => { + test('Renders correctly', () => { + render( + + ); + + expect(screen.getByTestId('ruleConsumerSelection')).toBeInTheDocument(); + }); + + test('Should default to the selected consumer', () => { + render( + + ); + + expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('Stack Rules'); + }); + + it('Should not display the initial selected consumer if it is not a selectable option', () => { + render( + + ); + expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue(''); + }); + + it('should display nothing if there is only 1 consumer to select', () => { + render( + + ); + + expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument(); + }); + + it('should be able to select logs and call onChange', () => { + render( + + ); + + fireEvent.click(screen.getByTestId('comboBoxToggleListButton')); + fireEvent.click(screen.getByTestId('ruleConsumerSelectionOption-logs')); + expect(mockOnChange).toHaveBeenLastCalledWith('consumer', 'logs'); + }); + + it('should be able to show errors when there is one', () => { + render( + + ); + expect(screen.queryAllByText('Scope is required')).toHaveLength(1); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx new file mode 100644 index 0000000000000..957d5c0152220 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_consumer_selection.tsx @@ -0,0 +1,111 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useCallback } from 'react'; +import { EuiComboBox, EuiFormRow, EuiComboBoxOptionOption } from '@elastic/eui'; +import { AlertConsumers, RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { FEATURE_NAME_MAP, CONSUMER_SELECT_COMBO_BOX_TITLE } from '../translations'; +import { RuleFormErrors } from '../types'; + +export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ + AlertConsumers.LOGS, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.STACK_ALERTS, + 'alerts', +]; + +export interface RuleConsumerSelectionProps { + consumers: RuleCreationValidConsumer[]; + selectedConsumer?: RuleCreationValidConsumer | null; + errors?: RuleFormErrors; + onChange: (property: string, value: unknown) => void; +} + +const SINGLE_SELECTION = { asPlainText: true }; + +type ComboBoxOption = EuiComboBoxOptionOption; + +export const RuleConsumerSelection = (props: RuleConsumerSelectionProps) => { + const { consumers, selectedConsumer, errors = {}, onChange } = props; + + const isInvalid = (errors.consumer?.length || 0) > 0; + + const validatedSelectedConsumer = useMemo(() => { + if ( + selectedConsumer && + consumers.includes(selectedConsumer) && + FEATURE_NAME_MAP[selectedConsumer] + ) { + return selectedConsumer; + } + return null; + }, [selectedConsumer, consumers]); + + const selectedOptions = useMemo(() => { + if (validatedSelectedConsumer) { + return [ + { + value: validatedSelectedConsumer, + label: FEATURE_NAME_MAP[validatedSelectedConsumer], + }, + ]; + } + return []; + }, [validatedSelectedConsumer]); + + const formattedSelectOptions = useMemo(() => { + return consumers + .reduce((result, consumer) => { + if (FEATURE_NAME_MAP[consumer]) { + result.push({ + value: consumer, + 'data-test-subj': `ruleConsumerSelectionOption-${consumer}`, + label: FEATURE_NAME_MAP[consumer], + }); + } + return result; + }, []) + .sort((a, b) => a.value!.localeCompare(b.value!)); + }, [consumers]); + + const onConsumerChange = useCallback( + (selected: ComboBoxOption[]) => { + if (selected.length > 0) { + const newSelectedConsumer = selected[0]; + onChange('consumer', newSelectedConsumer.value); + } else { + onChange('consumer', null); + } + }, + [onChange] + ); + + if (consumers.length <= 1 || consumers.includes(AlertConsumers.OBSERVABILITY)) { + return null; + } + + return ( + + + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx new file mode 100644 index 0000000000000..9ff8a704a728a --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.test.tsx @@ -0,0 +1,219 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; + +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DocLinksStart } from '@kbn/core-doc-links-browser'; + +import { RuleDefinition } from './rule_definition'; +import { RuleTypeModel } from '../types'; +import { RuleType } from '@kbn/alerting-types'; +import { ALERT_DELAY_TITLE } from '../translations'; + +const ruleType = { + id: '.es-query', + name: 'Test', + actionGroups: [ + { + id: 'testActionGroup', + name: 'Test Action Group', + }, + { + id: 'recovered', + name: 'Recovered', + }, + ], + defaultActionGroupId: 'testActionGroup', + minimumLicenseRequired: 'basic', + recoveryActionGroup: 'recovered', + producer: 'logs', + authorizedConsumers: { + alerting: { read: true, all: true }, + test: { read: true, all: true }, + }, + actionVariables: { + params: [], + state: [], + }, + enabledInLicense: true, +} as unknown as RuleType; + +const ruleModel: RuleTypeModel = { + id: '.es-query', + description: 'Sample rule type model', + iconClass: 'sampleIconClass', + documentationUrl: 'testurl', + validate: (params, isServerless) => ({ errors: {} }), + ruleParamsExpression: () =>
Expression
, + defaultActionMessage: 'Sample default action message', + defaultRecoveryMessage: 'Sample default recovery message', + requiresAppContext: false, +}; + +const requiredPlugins = { + charts: {} as ChartsPluginSetup, + data: {} as DataPublicPluginStart, + dataViews: {} as DataViewsPublicPluginStart, + unifiedSearch: {} as UnifiedSearchPublicPluginStart, + docLinks: {} as DocLinksStart, +}; + +const mockOnChange = jest.fn(); + +describe('Rule Definition', () => { + test('Renders correctly', () => { + render( + + ); + expect(screen.getByTestId('ruleDefinition')).toBeInTheDocument(); + expect(screen.getByTestId('ruleSchedule')).toBeInTheDocument(); + expect(screen.getByTestId('ruleConsumerSelection')).toBeInTheDocument(); + expect(screen.getByTestId('ruleDefinitionHeaderDocsLink')).toBeInTheDocument(); + + expect(screen.getByText(ALERT_DELAY_TITLE)).not.toBeVisible(); + expect(screen.getByText('Expression')).toBeInTheDocument(); + }); + + test('Hides doc link if not provided', () => { + render( + + ); + + expect(screen.queryByTestId('ruleDefinitionHeaderDocsLink')).not.toBeInTheDocument(); + }); + + test('Hides consumer selection if canShowConsumerSelection is false', () => { + render( + + ); + + expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument(); + }); + + test('Can toggle advanced options', async () => { + render( + + ); + + fireEvent.click(screen.getByTestId('advancedOptionsAccordionButton')); + expect(screen.getByText(ALERT_DELAY_TITLE)).toBeVisible(); + }); + + test('Calls onChange when inputs are modified', () => { + render( + + ); + + fireEvent.change(screen.getByTestId('ruleScheduleNumberInput'), { + target: { + value: '10', + }, + }); + expect(mockOnChange).toHaveBeenCalledWith('interval', '10m'); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx new file mode 100644 index 0000000000000..adec6e0966cbd --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_definition.tsx @@ -0,0 +1,292 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { Suspense, useMemo, useState, useCallback } from 'react'; +import { + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiSplitPanel, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiLink, + EuiDescribedFormGroup, + EuiAccordion, + EuiPanel, + EuiSpacer, + EuiErrorBoundary, + EuiIconTip, +} from '@elastic/eui'; +import { + RuleCreationValidConsumer, + ES_QUERY_ID, + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, +} from '@kbn/rule-data-utils'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { SanitizedRule, RuleTypeParams } from '@kbn/alerting-types'; +import type { RuleType } from '@kbn/triggers-actions-ui-types'; +import type { RuleTypeModel, RuleFormErrors, MinimumScheduleInterval } from '../types'; +import { + DOC_LINK_TITLE, + LOADING_RULE_TYPE_PARAMS_TITLE, + SCHEDULE_TITLE, + SCHEDULE_DESCRIPTION_TEXT, + ALERT_DELAY_TITLE, + SCOPE_TITLE, + SCOPE_DESCRIPTION_TEXT, + ADVANCED_OPTIONS_TITLE, + ALERT_DELAY_DESCRIPTION_TEXT, + SCHEDULE_TOOLTIP_TEXT, + ALERT_DELAY_HELP_TEXT, +} from '../translations'; +import { RuleAlertDelay } from './rule_alert_delay'; +import { RuleConsumerSelection } from './rule_consumer_selection'; +import { RuleSchedule } from './rule_schedule'; + +const MULTI_CONSUMER_RULE_TYPE_IDS = [ + OBSERVABILITY_THRESHOLD_RULE_TYPE_ID, + ES_QUERY_ID, + ML_ANOMALY_DETECTION_RULE_TYPE_ID, +]; + +interface RuleDefinitionProps { + requiredPlugins: { + charts: ChartsPluginSetup; + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; + docLinks: DocLinksStart; + }; + formValues: { + id?: SanitizedRule['id']; + params: SanitizedRule['params']; + schedule: SanitizedRule['schedule']; + alertDelay?: SanitizedRule['alertDelay']; + notifyWhen?: SanitizedRule['notifyWhen']; + consumer?: SanitizedRule['consumer']; + }; + minimumScheduleInterval?: MinimumScheduleInterval; + errors?: RuleFormErrors; + canShowConsumerSelection?: boolean; + authorizedConsumers?: RuleCreationValidConsumer[]; + selectedRuleTypeModel: RuleTypeModel; + selectedRuleType: RuleType; + validConsumers?: RuleCreationValidConsumer[]; + onChange: (property: string, value: unknown) => void; +} + +export const RuleDefinition = (props: RuleDefinitionProps) => { + const { + requiredPlugins, + formValues, + errors = {}, + canShowConsumerSelection = false, + authorizedConsumers = [], + selectedRuleTypeModel, + selectedRuleType, + minimumScheduleInterval, + onChange, + } = props; + + const { charts, data, dataViews, unifiedSearch, docLinks } = requiredPlugins; + + const { id, params, schedule, alertDelay, notifyWhen, consumer = 'alerts' } = formValues; + + const [metadata, setMetadata] = useState>(); + const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState(false); + + const shouldShowConsumerSelect = useMemo(() => { + if (!canShowConsumerSelection) { + return false; + } + if (!authorizedConsumers.length) { + return false; + } + return ( + selectedRuleTypeModel.id && MULTI_CONSUMER_RULE_TYPE_IDS.includes(selectedRuleTypeModel.id) + ); + }, [authorizedConsumers, selectedRuleTypeModel, canShowConsumerSelection]); + + const RuleParamsExpressionComponent = selectedRuleTypeModel.ruleParamsExpression ?? null; + + const docsUrl = useMemo(() => { + const { documentationUrl } = selectedRuleTypeModel; + if (typeof documentationUrl === 'function') { + return documentationUrl(docLinks); + } + return documentationUrl; + }, [selectedRuleTypeModel, docLinks]); + + const onSetRuleParams = useCallback( + (property: string, value: unknown) => { + onChange('params', { + ...params, + [property]: value, + }); + }, + [onChange, params] + ); + + const onSetRule = useCallback( + (property: string, value: unknown) => { + onChange(property, value); + }, + [onChange] + ); + + return ( + + + + + + {selectedRuleType.name} + + + + +

{selectedRuleTypeModel.description}

+
+
+ {docsUrl && ( + + + + {DOC_LINK_TITLE} + + + + )} +
+
+ + {RuleParamsExpressionComponent && ( + } + body={LOADING_RULE_TYPE_PARAMS_TITLE} + /> + } + > + + + + + + + + + )} + + + {SCHEDULE_TITLE}} + description={ + +

+ {SCHEDULE_DESCRIPTION_TEXT}  + +

+
+ } + > + +
+ {shouldShowConsumerSelect && ( + {SCOPE_TITLE}} + description={

{SCOPE_DESCRIPTION_TEXT}

} + > + +
+ )} + + +

{ADVANCED_OPTIONS_TITLE}

+ + } + > + + + {ALERT_DELAY_TITLE}} + description={ + +

+ {ALERT_DELAY_DESCRIPTION_TEXT}  + +

+
+ } + > + +
+
+
+
+
+
+ ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.test.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.test.tsx new file mode 100644 index 0000000000000..e0f2f3c9ab5a1 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { RuleSchedule } from './rule_schedule'; + +const mockOnChange = jest.fn(); + +describe('RuleSchedule', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + test('Renders correctly', () => { + render(); + + expect(screen.getByTestId('ruleSchedule')).toBeInTheDocument(); + }); + + test('Should allow interval number to be changed', () => { + render(); + + fireEvent.change(screen.getByTestId('ruleScheduleNumberInput'), { + target: { + value: '10', + }, + }); + expect(mockOnChange).toHaveBeenCalledWith('interval', '10m'); + }); + + test('Should allow interval unit to be changed', () => { + render(); + + userEvent.selectOptions(screen.getByTestId('ruleScheduleUnitInput'), 'hours'); + expect(mockOnChange).toHaveBeenCalledWith('interval', '5h'); + }); + + test('Should only allow integers as inputs', async () => { + render(); + + ['-', '+', 'e', 'E', '.', 'a', '01'].forEach((char) => { + fireEvent.change(screen.getByTestId('ruleScheduleNumberInput'), { + target: { + value: char, + }, + }); + }); + expect(mockOnChange).not.toHaveBeenCalled(); + }); + + test('Should display error properly', () => { + render( + + ); + + expect(screen.getByText('something went wrong!')).toBeInTheDocument(); + }); + + test('Should enforce minimum schedule interval', () => { + render( + + ); + + expect(screen.getByText('Interval must be at least 1 minute.')).toBeInTheDocument(); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx new file mode 100644 index 0000000000000..5af00de31b695 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/rule_definition/rule_schedule.tsx @@ -0,0 +1,130 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { EuiFlexItem, EuiFormRow, EuiFlexGroup, EuiSelect, EuiFieldNumber } from '@elastic/eui'; +import { + parseDuration, + formatDuration, + getDurationUnitValue, + getDurationNumberInItsUnit, +} from '../utils/parse_duration'; +import { getTimeOptions } from '../utils/get_time_options'; +import { MinimumScheduleInterval, RuleFormErrors } from '../types'; +import { + SCHEDULE_TITLE_PREFIX, + INTERVAL_MINIMUM_TEXT, + INTERVAL_WARNING_TEXT, +} from '../translations'; + +const INTEGER_REGEX = /^[1-9][0-9]*$/; +const INVALID_KEYS = ['-', '+', '.', 'e', 'E']; + +const getHelpTextForInterval = ( + currentInterval: string, + minimumScheduleInterval: MinimumScheduleInterval +) => { + if (!minimumScheduleInterval) { + return ''; + } + + if (minimumScheduleInterval.enforce) { + // Always show help text if minimum is enforced + return INTERVAL_MINIMUM_TEXT(formatDuration(minimumScheduleInterval.value, true)); + } else if ( + currentInterval && + parseDuration(currentInterval) < parseDuration(minimumScheduleInterval.value) + ) { + // Only show help text if current interval is less than suggested + return INTERVAL_WARNING_TEXT(formatDuration(minimumScheduleInterval.value, true)); + } else { + return ''; + } +}; + +export interface RuleScheduleProps { + interval: string; + minimumScheduleInterval?: MinimumScheduleInterval; + errors?: RuleFormErrors; + onChange: (property: string, value: unknown) => void; +} + +export const RuleSchedule = (props: RuleScheduleProps) => { + const { interval, minimumScheduleInterval, errors = {}, onChange } = props; + + const hasIntervalError = errors.interval?.length > 0; + + const intervalNumber = getDurationNumberInItsUnit(interval); + + const intervalUnit = getDurationUnitValue(interval); + + // No help text if there is an error + const helpText = + minimumScheduleInterval && !hasIntervalError + ? getHelpTextForInterval(interval, minimumScheduleInterval) + : ''; + + const onIntervalNumberChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value.trim(); + if (INTEGER_REGEX.test(value)) { + const parsedValue = parseInt(value, 10); + onChange('interval', `${parsedValue}${intervalUnit}`); + } + }, + [intervalUnit, onChange] + ); + + const onIntervalUnitChange = useCallback( + (e: React.ChangeEvent) => { + onChange('interval', `${intervalNumber}${e.target.value}`); + }, + [intervalNumber, onChange] + ); + + const onKeyDown = useCallback((e: React.KeyboardEvent) => { + if (INVALID_KEYS.includes(e.key)) { + e.preventDefault(); + } + }, []); + + return ( + 0} + error={errors.interval} + > + + + 0} + value={intervalNumber} + name="interval" + data-test-subj="ruleScheduleNumberInput" + onChange={onIntervalNumberChange} + onKeyDown={onKeyDown} + /> + + + + + + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts new file mode 100644 index 0000000000000..4ae1cbfd206c4 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/translations.ts @@ -0,0 +1,196 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { AlertConsumers } from '@kbn/rule-data-utils'; + +export const DOC_LINK_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.docLinkTitle', + { + defaultMessage: 'View documentation', + } +); + +export const LOADING_RULE_TYPE_PARAMS_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.loadingRuleTypeParamsTitle', + { + defaultMessage: 'Loading rule type params', + } +); + +export const SCHEDULE_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.scheduleTitle', + { + defaultMessage: 'Rule schedule', + } +); + +export const SCHEDULE_DESCRIPTION_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.scheduleDescriptionText', + { + defaultMessage: 'Set the frequency to check the alert conditions', + } +); + +export const SCHEDULE_TOOLTIP_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.scheduleTooltipText', + { + defaultMessage: 'Checks are queued; they run as close to the defined value as capacity allows.', + } +); + +export const ALERT_DELAY_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.alertDelayTitle', + { + defaultMessage: 'Alert delay', + } +); + +export const SCOPE_TITLE = i18n.translate('alertsUIShared.ruleForm.ruleDefinition.scopeTitle', { + defaultMessage: 'Rule scope', +}); + +export const SCOPE_DESCRIPTION_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.scopeDescriptionText', + { + defaultMessage: 'Select the applications to associate the corresponding role privilege', + } +); + +export const ADVANCED_OPTIONS_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.advancedOptionsTitle', + { + defaultMessage: 'Advanced options', + } +); + +export const ALERT_DELAY_DESCRIPTION_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.ruleDefinition.alertDelayDescription', + { + defaultMessage: + 'Set the number of consecutive runs for which this rule must meet the alert conditions before an alert occurs', + } +); + +export const ALERT_DELAY_TITLE_PREFIX = i18n.translate( + 'alertsUIShared.ruleForm.ruleAlertDelay.alertDelayTitlePrefix', + { + defaultMessage: 'Alert after', + } +); + +export const SCHEDULE_TITLE_PREFIX = i18n.translate( + 'alertsUIShared.ruleForm.ruleSchedule.scheduleTitlePrefix', + { + defaultMessage: 'Every', + } +); + +export const ALERT_DELAY_TITLE_SUFFIX = i18n.translate( + 'alertsUIShared.ruleForm.ruleAlertDelay.alertDelayTitleSuffix', + { + defaultMessage: 'consecutive matches', + } +); + +export const ALERT_DELAY_HELP_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.ruleAlertDelay.alertDelayHelpText', + { + defaultMessage: + 'An alert occurs only when the specified number of consecutive runs meet the rule conditions.', + } +); + +export const CONSUMER_SELECT_TITLE: string = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormConsumerSelection.consumerSelectTitle', + { + defaultMessage: 'Role visibility', + } +); + +export const FEATURE_NAME_MAP: Record = { + [AlertConsumers.LOGS]: i18n.translate('alertsUIShared.ruleForm.ruleFormConsumerSelection.logs', { + defaultMessage: 'Logs', + }), + [AlertConsumers.INFRASTRUCTURE]: i18n.translate( + 'alertsUIShared.ruleForm.ruleFormConsumerSelection.infrastructure', + { + defaultMessage: 'Metrics', + } + ), + [AlertConsumers.APM]: i18n.translate('alertsUIShared.ruleForm.ruleFormConsumerSelection.apm', { + defaultMessage: 'APM and User Experience', + }), + [AlertConsumers.UPTIME]: i18n.translate( + 'alertsUIShared.ruleForm.ruleFormConsumerSelection.uptime', + { + defaultMessage: 'Synthetics and Uptime', + } + ), + [AlertConsumers.SLO]: i18n.translate('alertsUIShared.ruleForm.ruleFormConsumerSelection.slo', { + defaultMessage: 'SLOs', + }), + [AlertConsumers.STACK_ALERTS]: i18n.translate( + 'alertsUIShared.ruleForm.ruleFormConsumerSelection.stackAlerts', + { + defaultMessage: 'Stack Rules', + } + ), +}; + +export const CONSUMER_SELECT_COMBO_BOX_TITLE = i18n.translate( + 'alertsUIShared.ruleForm.ruleFormConsumerSelection.consumerSelectComboBoxTitle', + { + defaultMessage: 'Select a scope', + } +); + +export const NAME_REQUIRED_TEXT = i18n.translate('alertsUIShared.ruleForm.error.requiredNameText', { + defaultMessage: 'Name is required.', +}); + +export const CONSUMER_REQUIRED_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.error.requiredConsumerText', + { + defaultMessage: 'Scope is required.', + } +); + +export const INTERVAL_REQUIRED_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.error.requiredIntervalText', + { + defaultMessage: 'Check interval is required.', + } +); + +export const RULE_TYPE_REQUIRED_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.error.requiredRuleTypeIdText', + { + defaultMessage: 'Rule type is required.', + } +); + +export const RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT = i18n.translate( + 'alertsUIShared.ruleForm.error.belowMinimumAlertDelayText', + { + defaultMessage: 'Alert delay must be greater than 1.', + } +); + +export const INTERVAL_MINIMUM_TEXT = (minimum: string) => + i18n.translate('alertsUIShared.ruleForm.error.belowMinimumText', { + defaultMessage: 'Interval must be at least {minimum}.', + values: { minimum }, + }); + +export const INTERVAL_WARNING_TEXT = (minimum: string) => + i18n.translate('alertsUIShared.ruleForm.intervalWarningText', { + defaultMessage: + 'Intervals less than {minimum} are not recommended due to performance considerations.', + values: { minimum }, + }); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/types/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/types/index.ts new file mode 100644 index 0000000000000..ed3241974fa79 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/types/index.ts @@ -0,0 +1,97 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { DocLinksStart } from '@kbn/core-doc-links-browser'; +import type { ComponentType } from 'react'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { + RuleNotifyWhenType, + ActionGroup, + SanitizedRule as AlertingSanitizedRule, + RuleAction, + RuleSystemAction, +} from '@kbn/alerting-types'; + +export type RuleTypeParams = Record; + +export interface RuleFormErrors { + [key: string]: string | string[] | RuleFormErrors; +} + +export interface MinimumScheduleInterval { + value: string; + enforce: boolean; +} + +export interface ValidationResult { + errors: Record; +} + +type RuleUiAction = RuleAction | RuleSystemAction; + +// In Triggers and Actions we treat all `Alert`s as `SanitizedRule` +// so the `Params` is a black-box of Record +type SanitizedRule = Omit< + AlertingSanitizedRule, + 'alertTypeId' | 'actions' | 'systemActions' +> & { + ruleTypeId: AlertingSanitizedRule['alertTypeId']; + actions: RuleUiAction[]; +}; + +type Rule = SanitizedRule; + +export type InitialRule = Partial & + Pick; + +export interface RuleTypeParamsExpressionProps< + Params extends RuleTypeParams = RuleTypeParams, + MetaData = Record, + ActionGroupIds extends string = string +> { + id?: string; + ruleParams: Params; + ruleInterval: string; + ruleThrottle: string; + alertNotifyWhen: RuleNotifyWhenType; + setRuleParams: (property: Key, value: Params[Key] | undefined) => void; + setRuleProperty: ( + key: Prop, + value: SanitizedRule[Prop] | null + ) => void; + onChangeMetaData: (metadata: MetaData) => void; + errors: RuleFormErrors; + defaultActionGroupId: string; + actionGroups: Array>; + metadata?: MetaData; + charts: ChartsPluginSetup; + data: DataPublicPluginStart; + dataViews: DataViewsPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; +} + +export interface RuleTypeModel { + id: string; + description: string; + iconClass: string; + documentationUrl: string | ((docLinks: DocLinksStart) => string) | null; + validate: (ruleParams: Params, isServerless?: boolean) => ValidationResult; + ruleParamsExpression: + | React.FunctionComponent + | React.LazyExoticComponent>>; + requiresAppContext: boolean; + defaultActionMessage?: string; + defaultRecoveryMessage?: string; + defaultSummaryMessage?: string; + alertDetailsAppSection?: + | React.FunctionComponent + | React.LazyExoticComponent>; +} diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_errors.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_errors.ts new file mode 100644 index 0000000000000..445d900d3f56c --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_errors.ts @@ -0,0 +1,107 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + InitialRule, + RuleTypeModel, + RuleFormErrors, + ValidationResult, + MinimumScheduleInterval, +} from '../types'; +import { parseDuration, formatDuration } from './parse_duration'; +import { + NAME_REQUIRED_TEXT, + CONSUMER_REQUIRED_TEXT, + RULE_TYPE_REQUIRED_TEXT, + INTERVAL_REQUIRED_TEXT, + INTERVAL_MINIMUM_TEXT, + RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT, +} from '../translations'; + +export function validateBaseProperties({ + rule, + minimumScheduleInterval, +}: { + rule: InitialRule; + minimumScheduleInterval?: MinimumScheduleInterval; +}): ValidationResult { + const validationResult = { errors: {} }; + + const errors = { + name: new Array(), + interval: new Array(), + consumer: new Array(), + ruleTypeId: new Array(), + actionConnectors: new Array(), + alertDelay: new Array(), + }; + + validationResult.errors = errors; + + if (!rule.name) { + errors.name.push(NAME_REQUIRED_TEXT); + } + + if (rule.consumer === null) { + errors.consumer.push(CONSUMER_REQUIRED_TEXT); + } + + if (rule.schedule.interval.length < 2) { + errors.interval.push(INTERVAL_REQUIRED_TEXT); + } else if (minimumScheduleInterval && minimumScheduleInterval.enforce) { + const duration = parseDuration(rule.schedule.interval); + const minimumDuration = parseDuration(minimumScheduleInterval.value); + if (duration < minimumDuration) { + errors.interval.push( + INTERVAL_MINIMUM_TEXT(formatDuration(minimumScheduleInterval.value, true)) + ); + } + } + + if (!rule.ruleTypeId) { + errors.ruleTypeId.push(RULE_TYPE_REQUIRED_TEXT); + } + + if (rule.alertDelay?.active && rule.alertDelay?.active < 1) { + errors.alertDelay.push(RULE_ALERT_DELAY_BELOW_MINIMUM_TEXT); + } + + return validationResult; +} + +export function getRuleErrors({ + rule, + ruleTypeModel, + minimumScheduleInterval, + isServerless, +}: { + rule: InitialRule; + ruleTypeModel: RuleTypeModel | null; + minimumScheduleInterval?: MinimumScheduleInterval; + isServerless?: boolean; +}) { + const ruleParamsErrors: RuleFormErrors = ruleTypeModel + ? ruleTypeModel.validate(rule.params, isServerless).errors + : {}; + + const ruleBaseErrors = validateBaseProperties({ + rule, + minimumScheduleInterval, + }).errors as RuleFormErrors; + + const ruleErrors = { + ...ruleParamsErrors, + ...ruleBaseErrors, + } as RuleFormErrors; + + return { + ruleParamsErrors, + ruleBaseErrors, + ruleErrors, + }; +} diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.test.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.test.ts new file mode 100644 index 0000000000000..07c06020fea1e --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.test.ts @@ -0,0 +1,43 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getTimeOptions, getTimeFieldOptions } from './get_time_options'; + +describe('get_time_options', () => { + test('if getTimeOptions return single unit time options', () => { + const timeUnitValue = getTimeOptions(1); + expect(timeUnitValue).toMatchObject([ + { text: 'second', value: 's' }, + { text: 'minute', value: 'm' }, + { text: 'hour', value: 'h' }, + { text: 'day', value: 'd' }, + ]); + }); + + test('if getTimeOptions return multiple unit time options', () => { + const timeUnitValue = getTimeOptions(10); + expect(timeUnitValue).toMatchObject([ + { text: 'seconds', value: 's' }, + { text: 'minutes', value: 'm' }, + { text: 'hours', value: 'h' }, + { text: 'days', value: 'd' }, + ]); + }); + + test('if getTimeFieldOptions return only date type fields', () => { + const timeOnlyTypeFields = getTimeFieldOptions([ + { type: 'date', name: 'order_date' }, + { type: 'date_nanos', name: 'order_date_nanos' }, + { type: 'number', name: 'sum' }, + ]); + expect(timeOnlyTypeFields).toMatchObject([ + { text: 'order_date', value: 'order_date' }, + { text: 'order_date_nanos', value: 'order_date_nanos' }, + ]); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.ts new file mode 100644 index 0000000000000..5fcaa295e89dc --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/get_time_options.ts @@ -0,0 +1,69 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export enum TIME_UNITS { + SECOND = 's', + MINUTE = 'm', + HOUR = 'h', + DAY = 'd', +} + +export const getTimeUnitLabel = (timeUnit = TIME_UNITS.SECOND, timeValue = '0') => { + switch (timeUnit) { + case TIME_UNITS.SECOND: + return i18n.translate('alertsUIShared.timeUnits.secondLabel', { + defaultMessage: '{timeValue, plural, one {second} other {seconds}}', + values: { timeValue }, + }); + case TIME_UNITS.MINUTE: + return i18n.translate('alertsUIShared.timeUnits.minuteLabel', { + defaultMessage: '{timeValue, plural, one {minute} other {minutes}}', + values: { timeValue }, + }); + case TIME_UNITS.HOUR: + return i18n.translate('alertsUIShared.timeUnits.hourLabel', { + defaultMessage: '{timeValue, plural, one {hour} other {hours}}', + values: { timeValue }, + }); + case TIME_UNITS.DAY: + return i18n.translate('alertsUIShared.timeUnits.dayLabel', { + defaultMessage: '{timeValue, plural, one {day} other {days}}', + values: { timeValue }, + }); + } +}; + +export const getTimeOptions = (unitSize: number) => { + return Object.entries(TIME_UNITS).map(([_, value]) => { + return { + text: getTimeUnitLabel(value, unitSize.toString()), + value, + }; + }); +}; + +interface TimeFieldOptions { + text: string; + value: string; +} + +export const getTimeFieldOptions = ( + fields: Array<{ type: string; name: string }> +): TimeFieldOptions[] => { + return fields.reduce((result, field: { type: string; name: string }) => { + if (field.type === 'date' || field.type === 'date_nanos') { + result.push({ + text: field.name, + value: field.name, + }); + } + return result; + }, []); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts new file mode 100644 index 0000000000000..b25e2f561a86a --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts @@ -0,0 +1,11 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './get_errors'; +export * from './get_time_options'; +export * from './parse_duration'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.test.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.test.ts new file mode 100644 index 0000000000000..296c8e2bc4900 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.test.ts @@ -0,0 +1,221 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + parseDuration, + formatDuration, + getDurationNumberInItsUnit, + getDurationUnitValue, +} from './parse_duration'; + +const MS_PER_MINUTE = 60 * 1000; + +export function convertDurationToFrequency( + duration: string, + denomination: number = MS_PER_MINUTE +): number { + const durationInMs = parseDuration(duration); + if (denomination === 0) { + throw new Error(`Invalid denomination value: value cannot be 0`); + } + + const intervalInDenominationUnits = durationInMs / denomination; + return 1 / intervalInDenominationUnits; +} + +test('parses seconds', () => { + const result = parseDuration('10s'); + expect(result).toEqual(10000); +}); + +test('parses minutes', () => { + const result = parseDuration('10m'); + expect(result).toEqual(600000); +}); + +test('parses hours', () => { + const result = parseDuration('10h'); + expect(result).toEqual(36000000); +}); + +test('parses days', () => { + const result = parseDuration('10d'); + expect(result).toEqual(864000000); +}); + +test('throws error when the format is invalid', () => { + expect(() => parseDuration('10x')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"10x\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + +test('throws error when suffix is missing', () => { + expect(() => parseDuration('1000')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"1000\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + +test('formats single second', () => { + const result = formatDuration('1s'); + expect(result).toEqual('1 sec'); +}); + +test('formats single second with full unit', () => { + const result = formatDuration('1s', true); + expect(result).toEqual('1 second'); +}); + +test('formats seconds', () => { + const result = formatDuration('10s'); + expect(result).toEqual('10 sec'); +}); + +test('formats seconds with full unit', () => { + const result = formatDuration('10s', true); + expect(result).toEqual('10 seconds'); +}); + +test('formats single minute', () => { + const result = formatDuration('1m'); + expect(result).toEqual('1 min'); +}); + +test('formats single minute with full unit', () => { + const result = formatDuration('1m', true); + expect(result).toEqual('1 minute'); +}); + +test('formats minutes', () => { + const result = formatDuration('10m'); + expect(result).toEqual('10 min'); +}); + +test('formats minutes with full unit', () => { + const result = formatDuration('10m', true); + expect(result).toEqual('10 minutes'); +}); + +test('formats single hour', () => { + const result = formatDuration('1h'); + expect(result).toEqual('1 hr'); +}); + +test('formats single hour with full unit', () => { + const result = formatDuration('1h', true); + expect(result).toEqual('1 hour'); +}); + +test('formats hours', () => { + const result = formatDuration('10h'); + expect(result).toEqual('10 hr'); +}); + +test('formats hours with full unit', () => { + const result = formatDuration('10h', true); + expect(result).toEqual('10 hours'); +}); + +test('formats single day', () => { + const result = formatDuration('1d'); + expect(result).toEqual('1 day'); +}); + +test('formats single day with full unit', () => { + const result = formatDuration('1d', true); + expect(result).toEqual('1 day'); +}); + +test('formats days', () => { + const result = formatDuration('10d'); + expect(result).toEqual('10 days'); +}); + +test('formats days with full unit', () => { + const result = formatDuration('10d', true); + expect(result).toEqual('10 days'); +}); + +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\\""` + ); + expect(() => parseDuration('0m')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"0m\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); + expect(() => parseDuration('0h')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"0h\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); + expect(() => parseDuration('0d')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"0d\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + +test('getDurationNumberInItsUnit days', () => { + const result = getDurationNumberInItsUnit('10d'); + expect(result).toEqual(10); +}); + +test('getDurationNumberInItsUnit minutes', () => { + const result = getDurationNumberInItsUnit('1m'); + expect(result).toEqual(1); +}); + +test('getDurationNumberInItsUnit seconds', () => { + const result = getDurationNumberInItsUnit('123s'); + expect(result).toEqual(123); +}); + +test('getDurationUnitValue minutes', () => { + const result = getDurationUnitValue('1m'); + expect(result).toEqual('m'); +}); + +test('getDurationUnitValue days', () => { + const result = getDurationUnitValue('23d'); + expect(result).toEqual('d'); +}); + +test('getDurationUnitValue hours', () => { + const result = getDurationUnitValue('100h'); + expect(result).toEqual('h'); +}); + +test('convertDurationToFrequency converts duration', () => { + let result = convertDurationToFrequency('1m'); + expect(result).toEqual(1); + result = convertDurationToFrequency('5m'); + expect(result).toEqual(0.2); + result = convertDurationToFrequency('10s'); + expect(result).toEqual(6); + result = convertDurationToFrequency('1s'); + expect(result).toEqual(60); +}); + +test('convertDurationToFrequency throws when duration is invalid', () => { + expect(() => convertDurationToFrequency('0d')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"0d\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + +test('convertDurationToFrequency throws when denomination is 0', () => { + expect(() => convertDurationToFrequency('1s', 0)).toThrowErrorMatchingInlineSnapshot( + `"Invalid denomination value: value cannot be 0"` + ); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.ts b/packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.ts new file mode 100644 index 0000000000000..81578eb5ae71b --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_form/utils/parse_duration.ts @@ -0,0 +1,82 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const DEFAULT_RULE_INTERVAL = '1m'; + +const SECONDS_REGEX = /^[1-9][0-9]*s$/; +const MINUTES_REGEX = /^[1-9][0-9]*m$/; +const HOURS_REGEX = /^[1-9][0-9]*h$/; +const DAYS_REGEX = /^[1-9][0-9]*d$/; + +const isSeconds = (duration: string) => { + return SECONDS_REGEX.test(duration); +}; + +const isMinutes = (duration: string) => { + return MINUTES_REGEX.test(duration); +}; + +const isHours = (duration: string) => { + return HOURS_REGEX.test(duration); +}; + +const isDays = (duration: string) => { + return DAYS_REGEX.test(duration); +}; + +// parse an interval string '{digit*}{s|m|h|d}' into milliseconds +export const parseDuration = (duration: string): number => { + const parsed = parseInt(duration, 10); + if (isSeconds(duration)) { + return parsed * 1000; + } else if (isMinutes(duration)) { + return parsed * 60 * 1000; + } else if (isHours(duration)) { + return parsed * 60 * 60 * 1000; + } else if (isDays(duration)) { + return parsed * 24 * 60 * 60 * 1000; + } + throw new Error( + `Invalid duration "${duration}". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d"` + ); +}; + +export const formatDuration = (duration: string, fullUnit?: boolean): string => { + const parsed = parseInt(duration, 10); + if (isSeconds(duration)) { + return `${parsed} ${fullUnit ? (parsed > 1 ? 'seconds' : 'second') : 'sec'}`; + } else if (isMinutes(duration)) { + return `${parsed} ${fullUnit ? (parsed > 1 ? 'minutes' : 'minute') : 'min'}`; + } else if (isHours(duration)) { + return `${parsed} ${fullUnit ? (parsed > 1 ? 'hours' : 'hour') : 'hr'}`; + } else if (isDays(duration)) { + return `${parsed} ${parsed > 1 ? 'days' : 'day'}`; + } + throw new Error( + `Invalid duration "${duration}". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d"` + ); +}; + +export const getDurationNumberInItsUnit = (duration: string): number => { + return parseInt(duration.replace(/[^0-9.]/g, ''), 10); +}; + +export const getDurationUnitValue = (duration: string): string => { + const durationNumber = getDurationNumberInItsUnit(duration); + return duration.replace(durationNumber.toString(), ''); +}; + +export const getInitialInterval = (minimumScheduleInterval?: string): string => { + if (minimumScheduleInterval) { + // return minimum schedule interval if it is larger than the default + if (parseDuration(minimumScheduleInterval) > parseDuration(DEFAULT_RULE_INTERVAL)) { + return minimumScheduleInterval; + } + } + return DEFAULT_RULE_INTERVAL; +}; diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index 0b3a0b3361326..dc2e78cbd49bb 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -34,5 +34,8 @@ "@kbn/core-http-browser", "@kbn/core-notifications-browser", "@kbn/kibana-utils-plugin", + "@kbn/core-doc-links-browser", + "@kbn/charts-plugin", + "@kbn/data-plugin", ] } diff --git a/x-pack/examples/triggers_actions_ui_example/kibana.jsonc b/x-pack/examples/triggers_actions_ui_example/kibana.jsonc index e3e6c8adc4a97..e3d9a82a2c368 100644 --- a/x-pack/examples/triggers_actions_ui_example/kibana.jsonc +++ b/x-pack/examples/triggers_actions_ui_example/kibana.jsonc @@ -13,7 +13,11 @@ "developerExamples", "kibanaReact", "cases", - "actions" + "actions", + "charts", + "dataViews", + "dataViewEditor", + "unifiedSearch" ], "optionalPlugins": ["spaces"] } diff --git a/x-pack/examples/triggers_actions_ui_example/public/application.tsx b/x-pack/examples/triggers_actions_ui_example/public/application.tsx index d36d4a91ddd87..6b1dfe98c22b2 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/application.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/application.tsx @@ -8,13 +8,19 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter as Router } from 'react-router-dom'; +import { QueryClient } from '@tanstack/react-query'; import { Route } from '@kbn/shared-ux-router'; import { EuiPage, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; import { AppMountParameters, CoreStart } from '@kbn/core/public'; import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { QueryClientProvider } from '@tanstack/react-query'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import { TriggersActionsUiExamplePublicStartDeps } from './plugin'; @@ -32,16 +38,26 @@ import { RuleStatusFilterSandbox } from './components/rule_status_filter_sandbox import { AlertsTableSandbox } from './components/alerts_table_sandbox'; import { RulesSettingsLinkSandbox } from './components/rules_settings_link_sandbox'; +import { RuleDefinitionSandbox } from './components/rule_form/rule_definition_sandbox'; + export interface TriggersActionsUiExampleComponentParams { http: CoreStart['http']; basename: string; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; + charts: ChartsPluginSetup; + dataViews: DataViewsPublicPluginStart; + dataViewsEditor: DataViewEditorStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } const TriggersActionsUiExampleApp = ({ basename, triggersActionsUi, + data, + charts, + dataViews, + unifiedSearch, }: TriggersActionsUiExampleComponentParams) => { return ( @@ -144,11 +160,27 @@ const TriggersActionsUiExampleApp = ({ )} /> + ( + + + + )} + /> ); }; +export const queryClient = new QueryClient(); + export const renderApp = ( core: CoreStart, deps: TriggersActionsUiExamplePublicStartDeps, @@ -168,12 +200,18 @@ export const renderApp = ( }} > - + + + , diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_definition_sandbox.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_definition_sandbox.tsx new file mode 100644 index 0000000000000..55dbfab6ddc4c --- /dev/null +++ b/x-pack/examples/triggers_actions_ui_example/public/components/rule_form/rule_definition_sandbox.tsx @@ -0,0 +1,171 @@ +/* + * 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, { useMemo, useState, useCallback } from 'react'; +import { EuiLoadingSpinner, EuiCodeBlock, EuiTitle, EuiButton } from '@elastic/eui'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import type { DocLinksStart } from '@kbn/core/public'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { ToastsStart } from '@kbn/core-notifications-browser'; +import type { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import { AlertConsumers, RuleCreationValidConsumer } from '@kbn/rule-data-utils'; +import { + RuleDefinition, + useLoadRuleTypesQuery, + getRuleErrors, + InitialRule, +} from '@kbn/alerts-ui-shared'; + +interface RuleDefinitionSandboxProps { + data: DataPublicPluginStart; + charts: ChartsPluginSetup; + dataViews: DataViewsPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; +} + +export const VALID_CONSUMERS: RuleCreationValidConsumer[] = [ + AlertConsumers.LOGS, + AlertConsumers.INFRASTRUCTURE, + AlertConsumers.STACK_ALERTS, +]; + +const DEFAULT_FORM_VALUES = (ruleTypeId: string) => ({ + id: 'test-id', + name: 'test', + params: {}, + schedule: { + interval: '1m', + }, + alertDelay: { + active: 5, + }, + notifyWhen: null, + consumer: 'stackAlerts', + enabled: true, + tags: [], + actions: [], + ruleTypeId, +}); + +export const RuleDefinitionSandbox = (props: RuleDefinitionSandboxProps) => { + const { data, charts, dataViews, unifiedSearch, triggersActionsUi } = props; + + const [ruleTypeId, setRuleTypeId] = useState('.es-query'); + + const [formValue, setFormValue] = useState(DEFAULT_FORM_VALUES(ruleTypeId)); + + const onChange = useCallback( + (property: string, value: unknown) => { + if (property === 'interval') { + setFormValue({ + ...formValue, + schedule: { + interval: value as string, + }, + }); + return; + } + if (property === 'params') { + setFormValue({ + ...formValue, + params: value as Record, + }); + return; + } + setFormValue({ + ...formValue, + [property]: value, + }); + }, + [formValue] + ); + + const onRuleTypeChange = useCallback((newRuleTypeId: string) => { + setRuleTypeId(newRuleTypeId); + setFormValue(DEFAULT_FORM_VALUES(newRuleTypeId)); + }, []); + + const { docLinks, http, toasts } = useKibana<{ + docLinks: DocLinksStart; + http: HttpStart; + toasts: ToastsStart; + }>().services; + + const { + ruleTypesState: { data: ruleTypeIndex, isLoading }, + } = useLoadRuleTypesQuery({ + http, + toasts, + }); + + const ruleTypes = useMemo(() => [...ruleTypeIndex.values()], [ruleTypeIndex]); + const selectedRuleType = ruleTypes.find((ruleType) => ruleType.id === ruleTypeId); + const selectedRuleTypeModel = triggersActionsUi.ruleTypeRegistry.get(ruleTypeId); + + const errors = useMemo(() => { + if (!selectedRuleType || !selectedRuleTypeModel) { + return {}; + } + + return getRuleErrors({ + rule: formValue, + minimumScheduleInterval: { + value: '1m', + enforce: true, + }, + ruleTypeModel: selectedRuleTypeModel, + }).ruleErrors; + }, [formValue, selectedRuleType, selectedRuleTypeModel]); + + if (isLoading || !selectedRuleType) { + return ; + } + + return ( + <> +
+ +

Form State

+
+ {JSON.stringify(formValue, null, 2)} +
+
+ +

Switch Rule Types:

+
+ onRuleTypeChange('.es-query')}>Es Query + onRuleTypeChange('metrics.alert.threshold')}> + Metric Threshold + + onRuleTypeChange('observability.rules.custom_threshold')}> + Custom Threshold + +
+ + + ); +}; diff --git a/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx b/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx index 90a44353c646e..caaad858b4cc4 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/components/sidebar.tsx @@ -76,6 +76,17 @@ export const Sidebar = () => { }, ], }, + { + name: 'Rule Form Components', + id: 'rule-form-components', + items: [ + { + id: 'rule-definition', + name: 'Rule Definition', + onClick: () => history.push('/rule_definition'), + }, + ], + }, ]} /> diff --git a/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx b/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx index 5077795b620f7..ca932cb81bd6c 100644 --- a/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx +++ b/x-pack/examples/triggers_actions_ui_example/public/plugin.tsx @@ -8,7 +8,11 @@ import React from 'react'; import { Plugin, CoreSetup, AppMountParameters, CoreStart } from '@kbn/core/public'; import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/public'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { ChartsPluginSetup } from '@kbn/charts-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public'; import { get } from 'lodash'; import { @@ -35,6 +39,10 @@ export interface TriggersActionsUiExamplePublicStartDeps { alerting: AlertingSetup; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; + charts: ChartsPluginSetup; + dataViews: DataViewsPublicPluginStart; + dataViewsEditor: DataViewEditorStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } export class TriggersActionsUiExamplePlugin diff --git a/x-pack/examples/triggers_actions_ui_example/tsconfig.json b/x-pack/examples/triggers_actions_ui_example/tsconfig.json index 193feffd2d5ee..0bb226e46c8a9 100644 --- a/x-pack/examples/triggers_actions_ui_example/tsconfig.json +++ b/x-pack/examples/triggers_actions_ui_example/tsconfig.json @@ -26,6 +26,13 @@ "@kbn/i18n", "@kbn/actions-plugin", "@kbn/config-schema", + "@kbn/charts-plugin", + "@kbn/data-views-plugin", + "@kbn/unified-search-plugin", + "@kbn/alerts-ui-shared", + "@kbn/data-view-editor-plugin", + "@kbn/core-http-browser", + "@kbn/core-notifications-browser", "@kbn/react-kibana-context-render", ] } diff --git a/x-pack/plugins/alerting/common/iso_weekdays.ts b/x-pack/plugins/alerting/common/iso_weekdays.ts index 1277370b0e6fc..15b2b5cb3d492 100644 --- a/x-pack/plugins/alerting/common/iso_weekdays.ts +++ b/x-pack/plugins/alerting/common/iso_weekdays.ts @@ -5,5 +5,5 @@ * 2.0. */ -export type IsoWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7; -export const ISO_WEEKDAYS: IsoWeekday[] = [1, 2, 3, 4, 5, 6, 7]; +export type { IsoWeekday } from '@kbn/alerting-types'; +export { ISO_WEEKDAYS } from '@kbn/alerting-types'; diff --git a/x-pack/plugins/alerting/common/rrule_type.ts b/x-pack/plugins/alerting/common/rrule_type.ts index 7d250a0302317..8b65ec192d316 100644 --- a/x-pack/plugins/alerting/common/rrule_type.ts +++ b/x-pack/plugins/alerting/common/rrule_type.ts @@ -5,14 +5,4 @@ * 2.0. */ -import type { WeekdayStr, Options } from '@kbn/rrule'; - -export type RRuleParams = Partial & Pick; - -// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec -export type RRuleRecord = Omit & { - dtstart: string; - byweekday?: Array; - wkst?: WeekdayStr; - until?: string; -}; +export type { RRuleParams, RRuleRecord } from '@kbn/alerting-types'; diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index bc6c60fd75a53..7d28c5fce8b54 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -10,206 +10,71 @@ import type { SavedObjectAttributes, SavedObjectsResolveResponse, } from '@kbn/core/server'; -import type { Filter } from '@kbn/es-query'; -import { IsoWeekday } from './iso_weekdays'; -import { RuleNotifyWhenType } from './rule_notify_when_type'; -import { RuleSnooze } from './rule_snooze_type'; -export type { ActionVariable } from '@kbn/alerting-types'; +import type { + SanitizedRule, + RuleLastRunOutcomes, + AlertsFilterTimeframe, + RuleAction, + RuleSystemAction, + RuleTypeParams, +} from '@kbn/alerting-types'; + +export type { + ActionVariable, + Rule, + SanitizedRule, + RuleTypeParams, + RuleActionParams, + IntervalSchedule, + RuleActionFrequency, + AlertsFilterTimeframe, + AlertsFilter, + RuleAction, + RuleSystemAction, + MappedParamsProperties, + MappedParams, + RuleExecutionStatuses, + RuleLastRunOutcomes, + RuleExecutionStatus, + RuleMonitoringHistory, + RuleMonitoringCalculatedMetrics, + RuleMonitoringLastRun, + RuleMonitoring, + RuleLastRun, + AlertDelay, + SanitizedAlertsFilter, + SanitizedRuleAction, +} from '@kbn/alerting-types'; + +export { + RuleExecutionStatusValues, + RuleLastRunOutcomeValues, + RuleExecutionStatusErrorReasons, + RuleExecutionStatusWarningReasons, +} from '@kbn/alerting-types'; export type RuleTypeState = Record; -export type RuleTypeParams = Record; export type RuleTypeMetaData = Record; // rule type defined alert fields to persist in alerts index export type RuleAlertData = Record; -export interface IntervalSchedule extends SavedObjectAttributes { - interval: string; -} - -// for the `typeof ThingValues[number]` types below, become string types that -// only accept the values in the associated string arrays -export const RuleExecutionStatusValues = [ - 'ok', - 'active', - 'error', - 'pending', - 'unknown', - 'warning', -] as const; -export type RuleExecutionStatuses = typeof RuleExecutionStatusValues[number]; - -export const RuleLastRunOutcomeValues = ['succeeded', 'warning', 'failed'] as const; -export type RuleLastRunOutcomes = typeof RuleLastRunOutcomeValues[number]; - export const RuleLastRunOutcomeOrderMap: Record = { succeeded: 0, warning: 10, failed: 20, }; -export enum RuleExecutionStatusErrorReasons { - Read = 'read', - Decrypt = 'decrypt', - Execute = 'execute', - Unknown = 'unknown', - License = 'license', - Timeout = 'timeout', - Disabled = 'disabled', - Validate = 'validate', -} - -export enum RuleExecutionStatusWarningReasons { - MAX_EXECUTABLE_ACTIONS = 'maxExecutableActions', - MAX_ALERTS = 'maxAlerts', - MAX_QUEUED_ACTIONS = 'maxQueuedActions', -} - export type RuleAlertingOutcome = 'failure' | 'success' | 'unknown' | 'warning'; -export interface RuleExecutionStatus { - status: RuleExecutionStatuses; - lastExecutionDate: Date; - lastDuration?: number; - error?: { - reason: RuleExecutionStatusErrorReasons; - message: string; - }; - warning?: { - reason: RuleExecutionStatusWarningReasons; - message: string; - }; -} - -export type RuleActionParams = SavedObjectAttributes; export type RuleActionParam = SavedObjectAttribute; -export interface RuleActionFrequency extends SavedObjectAttributes { - summary: boolean; - notifyWhen: RuleNotifyWhenType; - throttle: string | null; -} - -export interface AlertsFilterTimeframe extends SavedObjectAttributes { - days: IsoWeekday[]; - timezone: string; - hours: { - start: string; - end: string; - }; -} - -export interface AlertsFilter extends SavedObjectAttributes { - query?: { - kql: string; - filters: Filter[]; - dsl?: string; // This fields is generated in the code by using "kql", therefore it's not optional but defined as optional to avoid modifying a lot of files in different plugins - }; - timeframe?: AlertsFilterTimeframe; -} - export type RuleActionAlertsFilterProperty = AlertsFilterTimeframe | RuleActionParam; -export interface RuleAction { - uuid?: string; - group: string; - id: string; - actionTypeId: string; - params: RuleActionParams; - frequency?: RuleActionFrequency; - alertsFilter?: AlertsFilter; - useAlertDataForTemplate?: boolean; -} - -export interface RuleSystemAction { - uuid?: string; - id: string; - actionTypeId: string; - params: RuleActionParams; -} - export type RuleActionKey = keyof RuleAction; export type RuleSystemActionKey = keyof RuleSystemAction; -export interface RuleLastRun { - outcome: RuleLastRunOutcomes; - outcomeOrder?: number; - warning?: RuleExecutionStatusErrorReasons | RuleExecutionStatusWarningReasons | null; - outcomeMsg?: string[] | null; - alertsCount: { - active?: number | null; - new?: number | null; - recovered?: number | null; - ignored?: number | null; - }; -} - -export interface MappedParamsProperties { - risk_score?: number; - severity?: string; -} - -export type MappedParams = SavedObjectAttributes & MappedParamsProperties; - -export interface AlertDelay extends SavedObjectAttributes { - active: number; -} - -export interface Rule { - id: string; - enabled: boolean; - name: string; - tags: string[]; - alertTypeId: string; // this is persisted in the Rule saved object so we would need a migration to change this to ruleTypeId - consumer: string; - schedule: IntervalSchedule; - actions: RuleAction[]; - systemActions?: RuleSystemAction[]; - params: Params; - mapped_params?: MappedParams; - scheduledTaskId?: string | null; - createdBy: string | null; - updatedBy: string | null; - createdAt: Date; - updatedAt: Date; - apiKey: string | null; - apiKeyOwner: string | null; - apiKeyCreatedByUser?: boolean | null; - throttle?: string | null; - muteAll: boolean; - notifyWhen?: RuleNotifyWhenType | null; - mutedInstanceIds: string[]; - executionStatus: RuleExecutionStatus; - monitoring?: RuleMonitoring; - snoozeSchedule?: RuleSnooze; // Remove ? when this parameter is made available in the public API - activeSnoozes?: string[]; - isSnoozedUntil?: Date | null; - lastRun?: RuleLastRun | null; - nextRun?: Date | null; - revision: number; - running?: boolean | null; - viewInAppRelativeUrl?: string; - alertDelay?: AlertDelay; -} - -export interface SanitizedAlertsFilter extends AlertsFilter { - query?: { - kql: string; - filters: Filter[]; - }; - timeframe?: AlertsFilterTimeframe; -} - -export type SanitizedRuleAction = Omit & { - alertsFilter?: SanitizedAlertsFilter; -}; - -export type SanitizedRule = Omit< - Rule, - 'apiKey' | 'actions' -> & { actions: SanitizedRuleAction[] }; - export type ResolvedSanitizedRule = SanitizedRule & Omit & { outcome: string; @@ -262,20 +127,6 @@ export interface AlertsHealth { }; } -export interface RuleMonitoringHistory extends SavedObjectAttributes { - success: boolean; - timestamp: number; - duration?: number; - outcome?: RuleLastRunOutcomes; -} - -export interface RuleMonitoringCalculatedMetrics extends SavedObjectAttributes { - p50?: number; - p95?: number; - p99?: number; - success_ratio: number; -} - export interface RuleMonitoringLastRunMetrics extends SavedObjectAttributes { duration?: number; total_search_duration_ms?: number | null; @@ -284,16 +135,3 @@ export interface RuleMonitoringLastRunMetrics extends SavedObjectAttributes { total_alerts_created?: number | null; gap_duration_s?: number | null; } - -export interface RuleMonitoringLastRun extends SavedObjectAttributes { - timestamp: string; - metrics: RuleMonitoringLastRunMetrics; -} - -export interface RuleMonitoring { - run: { - history: RuleMonitoringHistory[]; - calculated_metrics: RuleMonitoringCalculatedMetrics; - last_run: RuleMonitoringLastRun; - }; -} diff --git a/x-pack/plugins/alerting/common/rule_notify_when_type.ts b/x-pack/plugins/alerting/common/rule_notify_when_type.ts index 76182636e9f71..272e458ade9d9 100644 --- a/x-pack/plugins/alerting/common/rule_notify_when_type.ts +++ b/x-pack/plugins/alerting/common/rule_notify_when_type.ts @@ -5,18 +5,8 @@ * 2.0. */ -export const RuleNotifyWhenTypeValues = [ - 'onActionGroupChange', - 'onActiveAlert', - 'onThrottleInterval', -] as const; -export type RuleNotifyWhenType = typeof RuleNotifyWhenTypeValues[number]; - -export enum RuleNotifyWhen { - CHANGE = 'onActionGroupChange', - ACTIVE = 'onActiveAlert', - THROTTLE = 'onThrottleInterval', -} +import type { RuleNotifyWhenType } from '@kbn/alerting-types'; +import { RuleNotifyWhenTypeValues } from '@kbn/alerting-types'; export function validateNotifyWhenType(notifyWhen: string) { if (RuleNotifyWhenTypeValues.includes(notifyWhen as RuleNotifyWhenType)) { @@ -24,3 +14,6 @@ export function validateNotifyWhenType(notifyWhen: string) { } return `string is not a valid RuleNotifyWhenType: ${notifyWhen}`; } + +export type { RuleNotifyWhenType } from '@kbn/alerting-types'; +export { RuleNotifyWhenTypeValues, RuleNotifyWhen } from '@kbn/alerting-types'; diff --git a/x-pack/plugins/alerting/common/rule_snooze_type.ts b/x-pack/plugins/alerting/common/rule_snooze_type.ts index 17e4d0eabb02a..b3b8b8b030a61 100644 --- a/x-pack/plugins/alerting/common/rule_snooze_type.ts +++ b/x-pack/plugins/alerting/common/rule_snooze_type.ts @@ -5,21 +5,4 @@ * 2.0. */ -import { RRuleParams } from './rrule_type'; - -export interface RuleSnoozeSchedule { - duration: number; - rRule: RRuleParams; - // For scheduled/recurring snoozes, `id` uniquely identifies them so that they can be displayed, modified, and deleted individually - id?: string; - skipRecurrences?: string[]; -} - -// Type signature of has to be repeated here to avoid issues with SavedObject compatibility -// RuleSnooze = RuleSnoozeSchedule[] throws typescript errors across the whole lib -export type RuleSnooze = Array<{ - duration: number; - rRule: RRuleParams; - id?: string; - skipRecurrences?: string[]; -}>; +export type { RuleSnoozeSchedule, RuleSnooze } from '@kbn/alerting-types';