mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Response Ops][Rule Form V2] Rule Form V2: Rule Definition (#183325)
## 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:
<img width="1257" alt="Screenshot 2024-05-13 at 10 10 51 AM"
src="a1ab6d96
-946d-4bf6-94e2-6aa903d0b8f5">
### 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 <zacqary.xeper@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
37215fbb00
commit
dc3f76b557
38 changed files with 2545 additions and 265 deletions
|
@ -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';
|
||||
|
|
19
packages/kbn-alerting-types/r_rule_types.ts
Normal file
19
packages/kbn-alerting-types/r_rule_types.ts
Normal file
|
@ -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<RRuleRecord> & Pick<RRuleRecord, 'dtstart' | 'tzid'>;
|
||||
|
||||
// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec
|
||||
export type RRuleRecord = Omit<Options, 'dtstart' | 'byweekday' | 'wkst' | 'until'> & {
|
||||
dtstart: string;
|
||||
byweekday?: Array<WeekdayStr | string | number>;
|
||||
wkst?: WeekdayStr;
|
||||
until?: string;
|
||||
};
|
21
packages/kbn-alerting-types/rule_notify_when_type.ts
Normal file
21
packages/kbn-alerting-types/rule_notify_when_type.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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',
|
||||
}
|
241
packages/kbn-alerting-types/rule_types.ts
Normal file
241
packages/kbn-alerting-types/rule_types.ts
Normal file
|
@ -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<string, unknown>;
|
||||
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<RuleAction, 'alertsFilter'> & {
|
||||
alertsFilter?: SanitizedAlertsFilter;
|
||||
};
|
||||
|
||||
export interface Rule<Params extends RuleTypeParams = never> {
|
||||
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<Params extends RuleTypeParams = never> = Omit<
|
||||
Rule<Params>,
|
||||
'apiKey' | 'actions'
|
||||
> & { actions: SanitizedRuleAction[] };
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<RuleType<string, string>>,
|
||||
filteredRuleTypes: string[],
|
||||
registeredRuleTypes: UseRuleTypesProps['registeredRuleTypes']
|
||||
) => {
|
||||
const getFilteredIndex = ({
|
||||
data,
|
||||
filteredRuleTypes,
|
||||
registeredRuleTypes,
|
||||
}: {
|
||||
data: Array<RuleType<string, string>>;
|
||||
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<string, RuleTypeWithDescription>(),
|
||||
[data, filteredRuleTypes, registeredRuleTypes]
|
||||
);
|
||||
|
|
11
packages/kbn-alerts-ui-shared/src/rule_form/index.ts
Normal file
11
packages/kbn-alerts-ui-shared/src/rule_form/index.ts
Normal file
|
@ -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';
|
|
@ -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';
|
|
@ -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(
|
||||
<RuleAlertDelay
|
||||
alertDelay={{
|
||||
active: 5,
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('alertDelay')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should handle input change', () => {
|
||||
render(
|
||||
<RuleAlertDelay
|
||||
alertDelay={{
|
||||
active: 5,
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('alertDelayInput'), {
|
||||
target: {
|
||||
value: '3',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith('alertDelay', { active: 3 });
|
||||
});
|
||||
|
||||
test('Should only allow integers as inputs', async () => {
|
||||
render(<RuleAlertDelay onChange={mockOnChange} />);
|
||||
|
||||
['-', '+', '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(
|
||||
<RuleAlertDelay
|
||||
alertDelay={{
|
||||
active: 5,
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('alertDelayInput'), {
|
||||
target: {
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith('alertDelay', null);
|
||||
});
|
||||
|
||||
test('Should display error when input is invalid', () => {
|
||||
render(
|
||||
<RuleAlertDelay
|
||||
alertDelay={{
|
||||
active: -5,
|
||||
}}
|
||||
errors={{
|
||||
alertDelay: 'Alert delay must be greater than 1.',
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Alert delay must be greater than 1.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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<RuleTypeParams>['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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
if (INVALID_KEYS.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={errors.alertDelay?.length > 0}
|
||||
error={errors.alertDelay}
|
||||
data-test-subj="alertDelay"
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={1}
|
||||
value={alertDelay?.active ?? ''}
|
||||
name="alertDelay"
|
||||
data-test-subj="alertDelayInput"
|
||||
prepend={[ALERT_DELAY_TITLE_PREFIX]}
|
||||
isInvalid={errors.alertDelay?.length > 0}
|
||||
append={ALERT_DELAY_TITLE_SUFFIX}
|
||||
onChange={onAlertDelayChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -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(
|
||||
<RuleConsumerSelection
|
||||
consumers={mockConsumers}
|
||||
selectedConsumer={'stackAlerts'}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('ruleConsumerSelection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should default to the selected consumer', () => {
|
||||
render(
|
||||
<RuleConsumerSelection
|
||||
consumers={mockConsumers}
|
||||
selectedConsumer={'stackAlerts'}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('Stack Rules');
|
||||
});
|
||||
|
||||
it('Should not display the initial selected consumer if it is not a selectable option', () => {
|
||||
render(
|
||||
<RuleConsumerSelection
|
||||
consumers={['stackAlerts', 'infrastructure']}
|
||||
selectedConsumer={'logs'}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('comboBoxSearchInput')).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should display nothing if there is only 1 consumer to select', () => {
|
||||
render(
|
||||
<RuleConsumerSelection
|
||||
selectedConsumer={null}
|
||||
consumers={['stackAlerts']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to select logs and call onChange', () => {
|
||||
render(
|
||||
<RuleConsumerSelection
|
||||
selectedConsumer={null}
|
||||
consumers={mockConsumers}
|
||||
onChange={mockOnChange}
|
||||
errors={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<RuleConsumerSelection
|
||||
selectedConsumer={null}
|
||||
consumers={mockConsumers}
|
||||
onChange={mockOnChange}
|
||||
errors={{ consumer: ['Scope is required'] }}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryAllByText('Scope is required')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -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<RuleCreationValidConsumer>;
|
||||
|
||||
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<ComboBoxOption[]>((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 (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
isInvalid={isInvalid}
|
||||
error={errors?.consumer ?? ''}
|
||||
data-test-subj="ruleConsumerSelection"
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
data-test-subj="ruleConsumerSelectionInput"
|
||||
aria-label={CONSUMER_SELECT_COMBO_BOX_TITLE}
|
||||
placeholder={CONSUMER_SELECT_COMBO_BOX_TITLE}
|
||||
singleSelection={SINGLE_SELECTION}
|
||||
options={formattedSelectOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onConsumerChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -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: () => <div>Expression</div>,
|
||||
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(
|
||||
<RuleDefinition
|
||||
requiredPlugins={requiredPlugins}
|
||||
formValues={{
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
}}
|
||||
selectedRuleType={ruleType as RuleType}
|
||||
selectedRuleTypeModel={ruleModel as RuleTypeModel}
|
||||
canShowConsumerSelection
|
||||
authorizedConsumers={['logs', 'stackAlerts']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
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(
|
||||
<RuleDefinition
|
||||
requiredPlugins={requiredPlugins}
|
||||
formValues={{
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
}}
|
||||
selectedRuleType={ruleType}
|
||||
selectedRuleTypeModel={{
|
||||
...ruleModel,
|
||||
documentationUrl: null,
|
||||
}}
|
||||
canShowConsumerSelection
|
||||
authorizedConsumers={['logs', 'stackAlerts']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('ruleDefinitionHeaderDocsLink')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Hides consumer selection if canShowConsumerSelection is false', () => {
|
||||
render(
|
||||
<RuleDefinition
|
||||
requiredPlugins={requiredPlugins}
|
||||
formValues={{
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
}}
|
||||
selectedRuleType={ruleType}
|
||||
selectedRuleTypeModel={ruleModel}
|
||||
authorizedConsumers={['logs', 'stackAlerts']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('ruleConsumerSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Can toggle advanced options', async () => {
|
||||
render(
|
||||
<RuleDefinition
|
||||
requiredPlugins={requiredPlugins}
|
||||
formValues={{
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
}}
|
||||
selectedRuleType={ruleType}
|
||||
selectedRuleTypeModel={ruleModel}
|
||||
authorizedConsumers={['logs', 'stackAlerts']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('advancedOptionsAccordionButton'));
|
||||
expect(screen.getByText(ALERT_DELAY_TITLE)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Calls onChange when inputs are modified', () => {
|
||||
render(
|
||||
<RuleDefinition
|
||||
requiredPlugins={requiredPlugins}
|
||||
formValues={{
|
||||
id: 'test-id',
|
||||
params: {},
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
alertDelay: {
|
||||
active: 5,
|
||||
},
|
||||
notifyWhen: null,
|
||||
consumer: 'stackAlerts',
|
||||
}}
|
||||
selectedRuleType={ruleType}
|
||||
selectedRuleTypeModel={ruleModel}
|
||||
authorizedConsumers={['logs', 'stackAlerts']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByTestId('ruleScheduleNumberInput'), {
|
||||
target: {
|
||||
value: '10',
|
||||
},
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith('interval', '10m');
|
||||
});
|
||||
});
|
|
@ -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<RuleTypeParams>['id'];
|
||||
params: SanitizedRule<RuleTypeParams>['params'];
|
||||
schedule: SanitizedRule<RuleTypeParams>['schedule'];
|
||||
alertDelay?: SanitizedRule<RuleTypeParams>['alertDelay'];
|
||||
notifyWhen?: SanitizedRule<RuleTypeParams>['notifyWhen'];
|
||||
consumer?: SanitizedRule<RuleTypeParams>['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<Record<string, unknown>>();
|
||||
const [isAdvancedOptionsVisible, setIsAdvancedOptionsVisible] = useState<boolean>(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 (
|
||||
<EuiSplitPanel.Outer hasBorder hasShadow={false} data-test-subj="ruleDefinition">
|
||||
<EuiSplitPanel.Inner color="subdued">
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false} data-test-subj="ruleDefinitionHeaderRuleTypeName">
|
||||
<EuiText size="xs">
|
||||
<strong>{selectedRuleType.name}</strong>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} data-test-subj="ruleDefinitionHeaderRuleTypeDescription">
|
||||
<EuiText size="xs">
|
||||
<p>{selectedRuleTypeModel.description}</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
{docsUrl && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
<EuiLink
|
||||
href={docsUrl}
|
||||
target="_blank"
|
||||
data-test-subj="ruleDefinitionHeaderDocsLink"
|
||||
>
|
||||
{DOC_LINK_TITLE}
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner>
|
||||
{RuleParamsExpressionComponent && (
|
||||
<Suspense
|
||||
fallback={
|
||||
<EuiEmptyPrompt
|
||||
title={<EuiLoadingSpinner size="xl" />}
|
||||
body={LOADING_RULE_TYPE_PARAMS_TITLE}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiErrorBoundary>
|
||||
<RuleParamsExpressionComponent
|
||||
id={id}
|
||||
ruleParams={params}
|
||||
ruleInterval={schedule.interval}
|
||||
ruleThrottle={''}
|
||||
alertNotifyWhen={notifyWhen || 'onActionGroupChange'}
|
||||
errors={errors}
|
||||
setRuleParams={onSetRuleParams}
|
||||
setRuleProperty={onSetRule}
|
||||
defaultActionGroupId={selectedRuleType.defaultActionGroupId}
|
||||
actionGroups={selectedRuleType.actionGroups}
|
||||
metadata={metadata}
|
||||
charts={charts}
|
||||
data={data}
|
||||
dataViews={dataViews}
|
||||
unifiedSearch={unifiedSearch}
|
||||
onChangeMetaData={setMetadata}
|
||||
/>
|
||||
</EuiErrorBoundary>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Suspense>
|
||||
)}
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner>
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={<h3>{SCHEDULE_TITLE}</h3>}
|
||||
description={
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{SCHEDULE_DESCRIPTION_TEXT}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={SCHEDULE_TOOLTIP_TEXT}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<RuleSchedule
|
||||
interval={schedule.interval}
|
||||
minimumScheduleInterval={minimumScheduleInterval}
|
||||
errors={errors}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiDescribedFormGroup>
|
||||
{shouldShowConsumerSelect && (
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={<h3>{SCOPE_TITLE}</h3>}
|
||||
description={<p>{SCOPE_DESCRIPTION_TEXT}</p>}
|
||||
>
|
||||
<RuleConsumerSelection
|
||||
consumers={authorizedConsumers}
|
||||
selectedConsumer={consumer as RuleCreationValidConsumer}
|
||||
errors={errors}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</EuiDescribedFormGroup>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiAccordion
|
||||
id="advancedOptionsAccordion"
|
||||
data-test-subj="advancedOptionsAccordion"
|
||||
onToggle={setIsAdvancedOptionsVisible}
|
||||
initialIsOpen={isAdvancedOptionsVisible}
|
||||
buttonProps={{
|
||||
'data-test-subj': 'advancedOptionsAccordionButton',
|
||||
}}
|
||||
buttonContent={
|
||||
<EuiText size="s">
|
||||
<p>{ADVANCED_OPTIONS_TITLE}</p>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiPanel hasShadow={false} hasBorder color="subdued">
|
||||
<EuiDescribedFormGroup
|
||||
fullWidth
|
||||
title={<h4>{ALERT_DELAY_TITLE}</h4>}
|
||||
description={
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
{ALERT_DELAY_DESCRIPTION_TEXT}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={ALERT_DELAY_HELP_TEXT}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<RuleAlertDelay alertDelay={alertDelay} errors={errors} onChange={onChange} />
|
||||
</EuiDescribedFormGroup>
|
||||
</EuiPanel>
|
||||
</EuiAccordion>
|
||||
</EuiFlexItem>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
);
|
||||
};
|
|
@ -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(<RuleSchedule interval={'5m'} onChange={mockOnChange} />);
|
||||
|
||||
expect(screen.getByTestId('ruleSchedule')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should allow interval number to be changed', () => {
|
||||
render(<RuleSchedule interval={'5m'} onChange={mockOnChange} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('ruleScheduleNumberInput'), {
|
||||
target: {
|
||||
value: '10',
|
||||
},
|
||||
});
|
||||
expect(mockOnChange).toHaveBeenCalledWith('interval', '10m');
|
||||
});
|
||||
|
||||
test('Should allow interval unit to be changed', () => {
|
||||
render(<RuleSchedule interval={'5m'} onChange={mockOnChange} />);
|
||||
|
||||
userEvent.selectOptions(screen.getByTestId('ruleScheduleUnitInput'), 'hours');
|
||||
expect(mockOnChange).toHaveBeenCalledWith('interval', '5h');
|
||||
});
|
||||
|
||||
test('Should only allow integers as inputs', async () => {
|
||||
render(<RuleSchedule interval={'5m'} onChange={mockOnChange} />);
|
||||
|
||||
['-', '+', 'e', 'E', '.', 'a', '01'].forEach((char) => {
|
||||
fireEvent.change(screen.getByTestId('ruleScheduleNumberInput'), {
|
||||
target: {
|
||||
value: char,
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(mockOnChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Should display error properly', () => {
|
||||
render(
|
||||
<RuleSchedule
|
||||
interval={'5m'}
|
||||
errors={{
|
||||
interval: 'something went wrong!',
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('something went wrong!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Should enforce minimum schedule interval', () => {
|
||||
render(
|
||||
<RuleSchedule
|
||||
interval={'30s'}
|
||||
minimumScheduleInterval={{
|
||||
enforce: true,
|
||||
value: '1m',
|
||||
}}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Interval must be at least 1 minute.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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<HTMLInputElement>) => {
|
||||
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<HTMLSelectElement>) => {
|
||||
onChange('interval', `${intervalNumber}${e.target.value}`);
|
||||
},
|
||||
[intervalNumber, onChange]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (INVALID_KEYS.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
data-test-subj="ruleSchedule"
|
||||
display="rowCompressed"
|
||||
helpText={helpText}
|
||||
isInvalid={errors.interval?.length > 0}
|
||||
error={errors.interval}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
prepend={[SCHEDULE_TITLE_PREFIX]}
|
||||
isInvalid={errors.interval?.length > 0}
|
||||
value={intervalNumber}
|
||||
name="interval"
|
||||
data-test-subj="ruleScheduleNumberInput"
|
||||
onChange={onIntervalNumberChange}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={3}>
|
||||
<EuiSelect
|
||||
fullWidth
|
||||
value={intervalUnit}
|
||||
options={getTimeOptions(intervalNumber ?? 1)}
|
||||
onChange={onIntervalUnitChange}
|
||||
data-test-subj="ruleScheduleUnitInput"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
196
packages/kbn-alerts-ui-shared/src/rule_form/translations.ts
Normal file
196
packages/kbn-alerts-ui-shared/src/rule_form/translations.ts
Normal file
|
@ -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<string, string> = {
|
||||
[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 },
|
||||
});
|
97
packages/kbn-alerts-ui-shared/src/rule_form/types/index.ts
Normal file
97
packages/kbn-alerts-ui-shared/src/rule_form/types/index.ts
Normal file
|
@ -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<string, unknown>;
|
||||
|
||||
export interface RuleFormErrors {
|
||||
[key: string]: string | string[] | RuleFormErrors;
|
||||
}
|
||||
|
||||
export interface MinimumScheduleInterval {
|
||||
value: string;
|
||||
enforce: boolean;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
errors: Record<string, any>;
|
||||
}
|
||||
|
||||
type RuleUiAction = RuleAction | RuleSystemAction;
|
||||
|
||||
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule<RuleTypeParams>`
|
||||
// so the `Params` is a black-box of Record<string, unknown>
|
||||
type SanitizedRule<Params extends RuleTypeParams = never> = Omit<
|
||||
AlertingSanitizedRule<Params>,
|
||||
'alertTypeId' | 'actions' | 'systemActions'
|
||||
> & {
|
||||
ruleTypeId: AlertingSanitizedRule['alertTypeId'];
|
||||
actions: RuleUiAction[];
|
||||
};
|
||||
|
||||
type Rule<Params extends RuleTypeParams = RuleTypeParams> = SanitizedRule<Params>;
|
||||
|
||||
export type InitialRule = Partial<Rule> &
|
||||
Pick<Rule, 'params' | 'consumer' | 'schedule' | 'actions' | 'tags'>;
|
||||
|
||||
export interface RuleTypeParamsExpressionProps<
|
||||
Params extends RuleTypeParams = RuleTypeParams,
|
||||
MetaData = Record<string, unknown>,
|
||||
ActionGroupIds extends string = string
|
||||
> {
|
||||
id?: string;
|
||||
ruleParams: Params;
|
||||
ruleInterval: string;
|
||||
ruleThrottle: string;
|
||||
alertNotifyWhen: RuleNotifyWhenType;
|
||||
setRuleParams: <Key extends keyof Params>(property: Key, value: Params[Key] | undefined) => void;
|
||||
setRuleProperty: <Prop extends keyof Rule>(
|
||||
key: Prop,
|
||||
value: SanitizedRule<Params>[Prop] | null
|
||||
) => void;
|
||||
onChangeMetaData: (metadata: MetaData) => void;
|
||||
errors: RuleFormErrors;
|
||||
defaultActionGroupId: string;
|
||||
actionGroups: Array<ActionGroup<ActionGroupIds>>;
|
||||
metadata?: MetaData;
|
||||
charts: ChartsPluginSetup;
|
||||
data: DataPublicPluginStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
}
|
||||
|
||||
export interface RuleTypeModel<Params extends RuleTypeParams = RuleTypeParams> {
|
||||
id: string;
|
||||
description: string;
|
||||
iconClass: string;
|
||||
documentationUrl: string | ((docLinks: DocLinksStart) => string) | null;
|
||||
validate: (ruleParams: Params, isServerless?: boolean) => ValidationResult;
|
||||
ruleParamsExpression:
|
||||
| React.FunctionComponent<any>
|
||||
| React.LazyExoticComponent<ComponentType<RuleTypeParamsExpressionProps<Params>>>;
|
||||
requiresAppContext: boolean;
|
||||
defaultActionMessage?: string;
|
||||
defaultRecoveryMessage?: string;
|
||||
defaultSummaryMessage?: string;
|
||||
alertDetailsAppSection?:
|
||||
| React.FunctionComponent<any>
|
||||
| React.LazyExoticComponent<ComponentType<any>>;
|
||||
}
|
107
packages/kbn-alerts-ui-shared/src/rule_form/utils/get_errors.ts
Normal file
107
packages/kbn-alerts-ui-shared/src/rule_form/utils/get_errors.ts
Normal file
|
@ -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<string>(),
|
||||
interval: new Array<string>(),
|
||||
consumer: new Array<string>(),
|
||||
ruleTypeId: new Array<string>(),
|
||||
actionConnectors: new Array<string>(),
|
||||
alertDelay: new Array<string>(),
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
|
@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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<TimeFieldOptions[]>((result, field: { type: string; name: string }) => {
|
||||
if (field.type === 'date' || field.type === 'date_nanos') {
|
||||
result.push({
|
||||
text: field.name,
|
||||
value: field.name,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
};
|
11
packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts
Normal file
11
packages/kbn-alerts-ui-shared/src/rule_form/utils/index.ts
Normal file
|
@ -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';
|
|
@ -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"`
|
||||
);
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,7 +13,11 @@
|
|||
"developerExamples",
|
||||
"kibanaReact",
|
||||
"cases",
|
||||
"actions"
|
||||
"actions",
|
||||
"charts",
|
||||
"dataViews",
|
||||
"dataViewEditor",
|
||||
"unifiedSearch"
|
||||
],
|
||||
"optionalPlugins": ["spaces"]
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<Router basename={basename}>
|
||||
|
@ -144,11 +160,27 @@ const TriggersActionsUiExampleApp = ({
|
|||
</Page>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
path="/rule_definition"
|
||||
render={() => (
|
||||
<Page title="Rule Definition">
|
||||
<RuleDefinitionSandbox
|
||||
triggersActionsUi={triggersActionsUi}
|
||||
data={data}
|
||||
charts={charts}
|
||||
dataViews={dataViews}
|
||||
unifiedSearch={unifiedSearch}
|
||||
/>
|
||||
</Page>
|
||||
)}
|
||||
/>
|
||||
</EuiPage>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
export const queryClient = new QueryClient();
|
||||
|
||||
export const renderApp = (
|
||||
core: CoreStart,
|
||||
deps: TriggersActionsUiExamplePublicStartDeps,
|
||||
|
@ -168,12 +200,18 @@ export const renderApp = (
|
|||
}}
|
||||
>
|
||||
<IntlProvider locale="en">
|
||||
<TriggersActionsUiExampleApp
|
||||
basename={appBasePath}
|
||||
http={http}
|
||||
triggersActionsUi={deps.triggersActionsUi}
|
||||
data={deps.data}
|
||||
/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TriggersActionsUiExampleApp
|
||||
basename={appBasePath}
|
||||
http={http}
|
||||
triggersActionsUi={deps.triggersActionsUi}
|
||||
data={deps.data}
|
||||
charts={deps.charts}
|
||||
dataViews={deps.dataViews}
|
||||
dataViewsEditor={deps.dataViewsEditor}
|
||||
unifiedSearch={deps.unifiedSearch}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
</KibanaContextProvider>
|
||||
</KibanaRenderContextProvider>,
|
||||
|
|
|
@ -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<string>('.es-query');
|
||||
|
||||
const [formValue, setFormValue] = useState<InitialRule>(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<string, unknown>,
|
||||
});
|
||||
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 <EuiLoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<EuiTitle>
|
||||
<h1>Form State</h1>
|
||||
</EuiTitle>
|
||||
<EuiCodeBlock>{JSON.stringify(formValue, null, 2)}</EuiCodeBlock>
|
||||
</div>
|
||||
<div>
|
||||
<EuiTitle>
|
||||
<h1>Switch Rule Types:</h1>
|
||||
</EuiTitle>
|
||||
<EuiButton onClick={() => onRuleTypeChange('.es-query')}>Es Query</EuiButton>
|
||||
<EuiButton onClick={() => onRuleTypeChange('metrics.alert.threshold')}>
|
||||
Metric Threshold
|
||||
</EuiButton>
|
||||
<EuiButton onClick={() => onRuleTypeChange('observability.rules.custom_threshold')}>
|
||||
Custom Threshold
|
||||
</EuiButton>
|
||||
</div>
|
||||
<RuleDefinition
|
||||
requiredPlugins={{
|
||||
data,
|
||||
charts,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
docLinks,
|
||||
}}
|
||||
formValues={formValue}
|
||||
canShowConsumerSelection
|
||||
authorizedConsumers={VALID_CONSUMERS}
|
||||
errors={errors}
|
||||
selectedRuleType={selectedRuleType}
|
||||
selectedRuleTypeModel={selectedRuleTypeModel}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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'),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</EuiPageSidebar>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -5,14 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { WeekdayStr, Options } from '@kbn/rrule';
|
||||
|
||||
export type RRuleParams = Partial<RRuleRecord> & Pick<RRuleRecord, 'dtstart' | 'tzid'>;
|
||||
|
||||
// An iCal RRULE to define a recurrence schedule, see https://github.com/jakubroztocil/rrule for the spec
|
||||
export type RRuleRecord = Omit<Options, 'dtstart' | 'byweekday' | 'wkst' | 'until'> & {
|
||||
dtstart: string;
|
||||
byweekday?: Array<WeekdayStr | string | number>;
|
||||
wkst?: WeekdayStr;
|
||||
until?: string;
|
||||
};
|
||||
export type { RRuleParams, RRuleRecord } from '@kbn/alerting-types';
|
||||
|
|
|
@ -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<string, unknown>;
|
||||
export type RuleTypeParams = Record<string, unknown>;
|
||||
export type RuleTypeMetaData = Record<string, unknown>;
|
||||
|
||||
// rule type defined alert fields to persist in alerts index
|
||||
export type RuleAlertData = Record<string, unknown>;
|
||||
|
||||
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<RuleLastRunOutcomes, number> = {
|
||||
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<Params extends RuleTypeParams = never> {
|
||||
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<RuleAction, 'alertsFilter'> & {
|
||||
alertsFilter?: SanitizedAlertsFilter;
|
||||
};
|
||||
|
||||
export type SanitizedRule<Params extends RuleTypeParams = never> = Omit<
|
||||
Rule<Params>,
|
||||
'apiKey' | 'actions'
|
||||
> & { actions: SanitizedRuleAction[] };
|
||||
|
||||
export type ResolvedSanitizedRule<Params extends RuleTypeParams = never> = SanitizedRule<Params> &
|
||||
Omit<SavedObjectsResolveResponse, 'saved_object'> & {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue