mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[RAM][Maintenance Window] Maintenance window scoped query frontend changes (#171949)
## Summary Partially resolves: https://github.com/elastic/kibana/issues/164255, this is 2/3 of the scoped query changes. Maintenance window scoped query frontend changes. Adds the ability to add and edit scoped query for maintenance windows. Due to limitations with the alerts search bar and each solution fetches AAD fields, we only allow users to associate scoped query with 1 category (manangement, o11y, or security solution). The intended usage in this case is for the user to create multiple maintenance windows if they wish to apply scoped queries to multiple solutions. ### To test: go to `x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts` and set `IS_SCOPED_QUERY_ENABLED` to `true` ### Scoped query off, multiple category allowed:  ### Scoped query on, multiple category disallowed:  ### 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: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
bf66b25564
commit
e4805fc9e0
94 changed files with 2274 additions and 560 deletions
3
.github/CODEOWNERS
vendored
3
.github/CODEOWNERS
vendored
|
@ -9,6 +9,7 @@ x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops
|
|||
packages/kbn-ace @elastic/platform-deployment-management
|
||||
x-pack/plugins/actions @elastic/response-ops
|
||||
x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops
|
||||
packages/kbn-actions-types @elastic/response-ops
|
||||
src/plugins/advanced_settings @elastic/appex-sharedux @elastic/platform-deployment-management
|
||||
x-pack/packages/ml/aiops_components @elastic/ml-ui
|
||||
x-pack/plugins/aiops @elastic/ml-ui
|
||||
|
@ -19,6 +20,7 @@ x-pack/examples/alerting_example @elastic/response-ops
|
|||
x-pack/test/functional_with_es_ssl/plugins/alerts @elastic/response-ops
|
||||
x-pack/plugins/alerting @elastic/response-ops
|
||||
x-pack/packages/kbn-alerting-state-types @elastic/response-ops
|
||||
packages/kbn-alerting-types @elastic/response-ops
|
||||
packages/kbn-alerts-as-data-utils @elastic/response-ops
|
||||
x-pack/test/alerting_api_integration/common/plugins/alerts_restricted @elastic/response-ops
|
||||
packages/kbn-alerts-ui-shared @elastic/response-ops
|
||||
|
@ -791,6 +793,7 @@ x-pack/plugins/transform @elastic/ml-ui
|
|||
x-pack/plugins/translations @elastic/kibana-localization
|
||||
x-pack/examples/triggers_actions_ui_example @elastic/response-ops
|
||||
x-pack/plugins/triggers_actions_ui @elastic/response-ops
|
||||
packages/kbn-triggers-actions-ui-types @elastic/response-ops
|
||||
packages/kbn-ts-projects @elastic/kibana-operations
|
||||
packages/kbn-ts-type-check-cli @elastic/kibana-operations
|
||||
packages/kbn-typed-react-router-config @elastic/obs-knowledge-team @elastic/obs-ux-management-team
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"advancedSettings": "src/plugins/advanced_settings",
|
||||
"alerts": "packages/kbn-alerts/src",
|
||||
"alertsUIShared": "packages/kbn-alerts-ui-shared/src",
|
||||
"alertingTypes": "packages/kbn-alerting-types",
|
||||
"apmOss": "src/plugins/apm_oss",
|
||||
"autocomplete": "packages/kbn-securitysolution-autocomplete/src",
|
||||
"bfetch": "src/plugins/bfetch",
|
||||
|
|
|
@ -133,6 +133,7 @@
|
|||
"@kbn/ace": "link:packages/kbn-ace",
|
||||
"@kbn/actions-plugin": "link:x-pack/plugins/actions",
|
||||
"@kbn/actions-simulators-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/actions_simulators",
|
||||
"@kbn/actions-types": "link:packages/kbn-actions-types",
|
||||
"@kbn/advanced-settings-plugin": "link:src/plugins/advanced_settings",
|
||||
"@kbn/aiops-components": "link:x-pack/packages/ml/aiops_components",
|
||||
"@kbn/aiops-plugin": "link:x-pack/plugins/aiops",
|
||||
|
@ -142,6 +143,7 @@
|
|||
"@kbn/alerting-fixture-plugin": "link:x-pack/test/functional_with_es_ssl/plugins/alerts",
|
||||
"@kbn/alerting-plugin": "link:x-pack/plugins/alerting",
|
||||
"@kbn/alerting-state-types": "link:x-pack/packages/kbn-alerting-state-types",
|
||||
"@kbn/alerting-types": "link:packages/kbn-alerting-types",
|
||||
"@kbn/alerts-as-data-utils": "link:packages/kbn-alerts-as-data-utils",
|
||||
"@kbn/alerts-restricted-fixtures-plugin": "link:x-pack/test/alerting_api_integration/common/plugins/alerts_restricted",
|
||||
"@kbn/alerts-ui-shared": "link:packages/kbn-alerts-ui-shared",
|
||||
|
@ -782,6 +784,7 @@
|
|||
"@kbn/translations-plugin": "link:x-pack/plugins/translations",
|
||||
"@kbn/triggers-actions-ui-example-plugin": "link:x-pack/examples/triggers_actions_ui_example",
|
||||
"@kbn/triggers-actions-ui-plugin": "link:x-pack/plugins/triggers_actions_ui",
|
||||
"@kbn/triggers-actions-ui-types": "link:packages/kbn-triggers-actions-ui-types",
|
||||
"@kbn/typed-react-router-config": "link:packages/kbn-typed-react-router-config",
|
||||
"@kbn/ui-actions-browser": "link:packages/kbn-ui-actions-browser",
|
||||
"@kbn/ui-actions-enhanced-examples-plugin": "link:x-pack/examples/ui_actions_enhanced_examples",
|
||||
|
|
3
packages/kbn-actions-types/README.md
Normal file
3
packages/kbn-actions-types/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/actions-types
|
||||
|
||||
Empty package generated by @kbn/generate
|
9
packages/kbn-actions-types/index.ts
Normal file
9
packages/kbn-actions-types/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 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 './rewrite_request_case_types';
|
13
packages/kbn-actions-types/jest.config.js
Normal file
13
packages/kbn-actions-types/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-actions-types'],
|
||||
};
|
5
packages/kbn-actions-types/kibana.jsonc
Normal file
5
packages/kbn-actions-types/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/actions-types",
|
||||
"owner": "@elastic/response-ops"
|
||||
}
|
6
packages/kbn-actions-types/package.json
Normal file
6
packages/kbn-actions-types/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/actions-types",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
51
packages/kbn-actions-types/rewrite_request_case_types.ts
Normal file
51
packages/kbn-actions-types/rewrite_request_case_types.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
type RenameActionToConnector<K extends string> = K extends `actionTypeId`
|
||||
? `connectorTypeId`
|
||||
: K extends `actionId`
|
||||
? `connectorId`
|
||||
: K;
|
||||
|
||||
export type AsApiContract<T> = {
|
||||
[K in keyof T as CamelToSnake<RenameActionToConnector<Extract<K, string>>>]: K extends 'frequency'
|
||||
? AsApiContract<T[K]>
|
||||
: T[K];
|
||||
};
|
||||
|
||||
export type RewriteRequestCase<T> = (requested: AsApiContract<T>) => T;
|
||||
export type RewriteResponseCase<T> = (
|
||||
responded: T
|
||||
) => T extends Array<infer Item> ? Array<AsApiContract<Item>> : AsApiContract<T>;
|
||||
|
||||
/**
|
||||
* This type maps Camel Case strings into their Snake Case version.
|
||||
* This is achieved by checking each character and, if it is an uppercase character, it is mapped to an
|
||||
* underscore followed by a lowercase one.
|
||||
*
|
||||
* The reason there are two ternaries is that, for perfformance reasons, TS limits its
|
||||
* character parsing to ~15 characters.
|
||||
* To get around this we use the second turnery to parse 2 characters at a time, which allows us to support
|
||||
* strings that are 30 characters long.
|
||||
*
|
||||
* If you get the TS #2589 error ("Type instantiation is excessively deep and possibly infinite") then most
|
||||
* likely you have a string that's longer than 30 characters.
|
||||
* Address this by reducing the length if possible, otherwise, you'll need to add a 3rd ternary which
|
||||
* parses 3 chars at a time :grimace:
|
||||
*
|
||||
* For more details see this PR comment: https://github.com/microsoft/TypeScript/pull/40336#issuecomment-686723087
|
||||
*/
|
||||
type CamelToSnake<T extends string> = string extends T
|
||||
? string
|
||||
: T extends `${infer C0}${infer C1}${infer R}`
|
||||
? `${C0 extends Uppercase<C0> ? '_' : ''}${Lowercase<C0>}${C1 extends Uppercase<C1>
|
||||
? '_'
|
||||
: ''}${Lowercase<C1>}${CamelToSnake<R>}`
|
||||
: T extends `${infer C0}${infer R}`
|
||||
? `${C0 extends Uppercase<C0> ? '_' : ''}${Lowercase<C0>}${CamelToSnake<R>}`
|
||||
: '';
|
19
packages/kbn-actions-types/tsconfig.json
Normal file
19
packages/kbn-actions-types/tsconfig.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": []
|
||||
}
|
3
packages/kbn-alerting-types/README.md
Normal file
3
packages/kbn-alerting-types/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/alerting-types
|
||||
|
||||
Empty package generated by @kbn/generate
|
12
packages/kbn-alerting-types/action_group_types.ts
Normal file
12
packages/kbn-alerting-types/action_group_types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 interface ActionGroup<ActionGroupIds extends string> {
|
||||
id: ActionGroupIds;
|
||||
name: string;
|
||||
}
|
21
packages/kbn-alerting-types/builtin_action_groups_types.ts
Normal file
21
packages/kbn-alerting-types/builtin_action_groups_types.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.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionGroup } from './action_group_types';
|
||||
|
||||
export const RecoveredActionGroup: Readonly<ActionGroup<'recovered'>> = Object.freeze({
|
||||
id: 'recovered',
|
||||
name: i18n.translate('alertingTypes.builtinActionGroups.recovered', {
|
||||
defaultMessage: 'Recovered',
|
||||
}),
|
||||
});
|
||||
|
||||
export type DefaultActionGroupId = 'default';
|
||||
|
||||
export type RecoveredActionGroupId = typeof RecoveredActionGroup['id'];
|
11
packages/kbn-alerting-types/index.ts
Normal file
11
packages/kbn-alerting-types/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 './builtin_action_groups_types';
|
||||
export * from './rule_type';
|
||||
export * from './action_group_types';
|
13
packages/kbn-alerting-types/jest.config.js
Normal file
13
packages/kbn-alerting-types/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-alerting-types'],
|
||||
};
|
5
packages/kbn-alerting-types/kibana.jsonc
Normal file
5
packages/kbn-alerting-types/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/alerting-types",
|
||||
"owner": "@elastic/response-ops"
|
||||
}
|
6
packages/kbn-alerting-types/package.json
Normal file
6
packages/kbn-alerting-types/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/alerting-types",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
55
packages/kbn-alerting-types/rule_type.ts
Normal file
55
packages/kbn-alerting-types/rule_type.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import type { RecoveredActionGroupId, DefaultActionGroupId } from './builtin_action_groups_types';
|
||||
import { ActionGroup } from './action_group_types';
|
||||
|
||||
interface ConsumerPrivileges {
|
||||
read: boolean;
|
||||
all: boolean;
|
||||
}
|
||||
|
||||
export interface ActionVariable {
|
||||
name: string;
|
||||
description: string;
|
||||
deprecated?: boolean;
|
||||
useWithTripleBracesInTemplates?: boolean;
|
||||
usesPublicBaseUrl?: boolean;
|
||||
}
|
||||
|
||||
export interface RuleType<
|
||||
ActionGroupIds extends Exclude<string, RecoveredActionGroupId> = DefaultActionGroupId,
|
||||
RecoveryActionGroupId extends string = RecoveredActionGroupId
|
||||
> {
|
||||
id: string;
|
||||
name: string;
|
||||
actionGroups: Array<ActionGroup<ActionGroupIds>>;
|
||||
recoveryActionGroup: ActionGroup<RecoveryActionGroupId>;
|
||||
actionVariables: {
|
||||
context: ActionVariable[];
|
||||
state: ActionVariable[];
|
||||
params: ActionVariable[];
|
||||
};
|
||||
defaultActionGroupId: ActionGroupIds;
|
||||
category: string;
|
||||
producer: string;
|
||||
minimumLicenseRequired: LicenseType;
|
||||
isExportable: boolean;
|
||||
ruleTaskTimeout?: string;
|
||||
defaultScheduleInterval?: string;
|
||||
doesSetRecoveryContext?: boolean;
|
||||
enabledInLicense: boolean;
|
||||
authorizedConsumers: Record<string, ConsumerPrivileges>;
|
||||
}
|
||||
|
||||
export type ActionGroupIdsOf<T> = T extends ActionGroup<infer groups>
|
||||
? groups
|
||||
: T extends Readonly<ActionGroup<infer groups>>
|
||||
? groups
|
||||
: never;
|
22
packages/kbn-alerting-types/tsconfig.json
Normal file
22
packages/kbn-alerting-types/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
"@kbn/licensing-plugin",
|
||||
]
|
||||
}
|
|
@ -10,3 +10,8 @@ export { AlertLifecycleStatusBadge } from './src/alert_lifecycle_status_badge';
|
|||
export type { AlertLifecycleStatusBadgeProps } from './src/alert_lifecycle_status_badge';
|
||||
export { MaintenanceWindowCallout } from './src/maintenance_window_callout';
|
||||
export { AddMessageVariables } from './src/add_message_variables';
|
||||
|
||||
export * from './src/alerts_search_bar/hooks';
|
||||
export * from './src/alerts_search_bar/apis';
|
||||
export { AlertsSearchBar } from './src/alerts_search_bar';
|
||||
export type { AlertsSearchBarProps } from './src/alerts_search_bar/types';
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
EuiToolTip,
|
||||
EuiSelectableOption,
|
||||
} from '@elastic/eui';
|
||||
import { ActionVariable } from '@kbn/alerting-plugin/common';
|
||||
import type { ActionVariable } from '@kbn/alerting-types';
|
||||
import './add_message_variables.scss';
|
||||
import { TruncatedText } from './truncated_text';
|
||||
import * as i18n from './translations';
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { HttpStart } from '@kbn/core/public';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { BASE_RAC_ALERTS_API_PATH, EMPTY_AAD_FIELDS } from '../constants';
|
||||
|
||||
export async function fetchAadFields({
|
||||
http,
|
||||
ruleTypeId,
|
||||
}: {
|
||||
http: HttpStart;
|
||||
ruleTypeId?: string;
|
||||
}): Promise<DataViewField[]> {
|
||||
if (!ruleTypeId) return EMPTY_AAD_FIELDS;
|
||||
const fields = await http.get<DataViewField[]>(`${BASE_RAC_ALERTS_API_PATH}/aad_fields`, {
|
||||
query: { ruleTypeId },
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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 { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../constants';
|
||||
|
||||
export async function fetchAlertFields({
|
||||
http,
|
||||
featureIds,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
featureIds: ValidFeatureId[];
|
||||
}): Promise<FieldSpec[]> {
|
||||
const { fields: alertFields = [] } = await http.get<{ fields: FieldSpec[] }>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
|
||||
{
|
||||
query: { featureIds },
|
||||
}
|
||||
);
|
||||
return alertFields;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../constants';
|
||||
|
||||
export async function fetchAlertIndexNames({
|
||||
http,
|
||||
features,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
features: string;
|
||||
}): Promise<string[]> {
|
||||
const { index_name: indexNamesStr = [] } = await http.get<{ index_name: string[] }>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/index`,
|
||||
{
|
||||
query: { features },
|
||||
}
|
||||
);
|
||||
return indexNamesStr;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { HttpSetup } from '@kbn/core/public';
|
||||
import type { AsApiContract, RewriteRequestCase } from '@kbn/actions-types';
|
||||
import type { RuleType } from '@kbn/triggers-actions-ui-types';
|
||||
import { BASE_ALERTING_API_PATH } from '../constants';
|
||||
|
||||
const rewriteResponseRes = (results: Array<AsApiContract<RuleType>>): RuleType[] => {
|
||||
return results.map((item) => rewriteBodyReq(item));
|
||||
};
|
||||
|
||||
const rewriteBodyReq: RewriteRequestCase<RuleType> = ({
|
||||
enabled_in_license: enabledInLicense,
|
||||
recovery_action_group: recoveryActionGroup,
|
||||
action_groups: actionGroups,
|
||||
default_action_group_id: defaultActionGroupId,
|
||||
minimum_license_required: minimumLicenseRequired,
|
||||
action_variables: actionVariables,
|
||||
authorized_consumers: authorizedConsumers,
|
||||
rule_task_timeout: ruleTaskTimeout,
|
||||
does_set_recovery_context: doesSetRecoveryContext,
|
||||
default_schedule_interval: defaultScheduleInterval,
|
||||
has_alerts_mappings: hasAlertsMappings,
|
||||
has_fields_for_a_a_d: hasFieldsForAAD,
|
||||
...rest
|
||||
}: AsApiContract<RuleType>) => ({
|
||||
enabledInLicense,
|
||||
recoveryActionGroup,
|
||||
actionGroups,
|
||||
defaultActionGroupId,
|
||||
minimumLicenseRequired,
|
||||
actionVariables,
|
||||
authorizedConsumers,
|
||||
ruleTaskTimeout,
|
||||
doesSetRecoveryContext,
|
||||
defaultScheduleInterval,
|
||||
hasAlertsMappings,
|
||||
hasFieldsForAAD,
|
||||
...rest,
|
||||
});
|
||||
|
||||
export async function fetchRuleTypes({ http }: { http: HttpSetup }): Promise<RuleType[]> {
|
||||
const res = await http.get<Array<AsApiContract<RuleType<string, string>>>>(
|
||||
`${BASE_ALERTING_API_PATH}/rule_types`
|
||||
);
|
||||
return rewriteResponseRes(res);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 './fetch_aad_fields';
|
||||
export * from './fetch_alert_fields';
|
||||
export * from './fetch_alert_index_names';
|
||||
export * from './fetch_rule_types';
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* 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 { DataView, DataViewField } from '@kbn/data-views-plugin/common';
|
||||
|
||||
export const NO_INDEX_PATTERNS: DataView[] = [];
|
||||
export const EMPTY_AAD_FIELDS: DataViewField[] = [];
|
||||
export const ALERTS_FEATURE_ID = 'alerts';
|
||||
export const BASE_ALERTING_API_PATH = '/api/alerting';
|
||||
export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts';
|
|
@ -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 './use_alert_data_view';
|
||||
export * from './use_load_rule_types_query';
|
||||
export * from './use_rule_aad_fields';
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { useEffect, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { DataView, DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchAlertIndexNames } from '../apis/fetch_alert_index_names';
|
||||
import { fetchAlertFields } from '../apis/fetch_alert_fields';
|
||||
|
||||
export interface UseAlertDataViewResult {
|
||||
dataViews?: DataView[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface UseAlertDataViewProps {
|
||||
featureIds: ValidFeatureId[];
|
||||
http: HttpStart;
|
||||
dataViewsService: DataViewsContract;
|
||||
toasts: ToastsStart;
|
||||
}
|
||||
|
||||
export function useAlertDataView(props: UseAlertDataViewProps): UseAlertDataViewResult {
|
||||
const { http, dataViewsService, toasts, featureIds } = props;
|
||||
|
||||
const [dataViews, setDataViews] = useState<DataView[]>([]);
|
||||
const features = featureIds.sort().join(',');
|
||||
const isOnlySecurity = featureIds.length === 1 && featureIds.includes(AlertConsumers.SIEM);
|
||||
|
||||
const hasSecurityAndO11yFeatureIds =
|
||||
featureIds.length > 1 && featureIds.includes(AlertConsumers.SIEM);
|
||||
|
||||
const hasNoSecuritySolution =
|
||||
featureIds.length > 0 && !isOnlySecurity && !hasSecurityAndO11yFeatureIds;
|
||||
|
||||
const queryIndexNameFn = () => {
|
||||
return fetchAlertIndexNames({ http, features });
|
||||
};
|
||||
|
||||
const queryAlertFieldsFn = () => {
|
||||
return fetchAlertFields({ http, featureIds });
|
||||
};
|
||||
|
||||
const onErrorFn = () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('alertsUIShared.hooks.useAlertDataView.useAlertDataMessage', {
|
||||
defaultMessage: 'Unable to load alert data view',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
data: indexNames,
|
||||
isSuccess: isIndexNameSuccess,
|
||||
isInitialLoading: isIndexNameInitialLoading,
|
||||
isLoading: isIndexNameLoading,
|
||||
} = useQuery({
|
||||
queryKey: ['loadAlertIndexNames', features],
|
||||
queryFn: queryIndexNameFn,
|
||||
onError: onErrorFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: featureIds.length > 0 && !hasSecurityAndO11yFeatureIds,
|
||||
});
|
||||
|
||||
const {
|
||||
data: alertFields,
|
||||
isSuccess: isAlertFieldsSuccess,
|
||||
isInitialLoading: isAlertFieldsInitialLoading,
|
||||
isLoading: isAlertFieldsLoading,
|
||||
} = useQuery({
|
||||
queryKey: ['loadAlertFields', features],
|
||||
queryFn: queryAlertFieldsFn,
|
||||
onError: onErrorFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: hasNoSecuritySolution,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dataViews.map((dv) => {
|
||||
dataViewsService.clearInstanceCache(dv.id);
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataViews]);
|
||||
|
||||
// FUTURE ENGINEER this useEffect is for security solution user since
|
||||
// we are using the user privilege to access the security alert index
|
||||
useEffect(() => {
|
||||
async function createDataView() {
|
||||
const localDataview = await dataViewsService.create({
|
||||
title: (indexNames ?? []).join(','),
|
||||
allowNoIndex: true,
|
||||
});
|
||||
setDataViews([localDataview]);
|
||||
}
|
||||
|
||||
if (isOnlySecurity && isIndexNameSuccess) {
|
||||
createDataView();
|
||||
}
|
||||
}, [dataViewsService, indexNames, isIndexNameSuccess, isOnlySecurity]);
|
||||
|
||||
// FUTURE ENGINEER this useEffect is for o11y and stack solution user since
|
||||
// we are using the kibana user privilege to access the alert index
|
||||
useEffect(() => {
|
||||
if (
|
||||
indexNames &&
|
||||
alertFields &&
|
||||
!isOnlySecurity &&
|
||||
isAlertFieldsSuccess &&
|
||||
isIndexNameSuccess
|
||||
) {
|
||||
setDataViews([
|
||||
{
|
||||
title: (indexNames ?? []).join(','),
|
||||
fieldFormatMap: {},
|
||||
fields: (alertFields ?? [])?.map((field) => {
|
||||
return {
|
||||
...field,
|
||||
...(field.esTypes && field.esTypes.includes('flattened') ? { type: 'string' } : {}),
|
||||
};
|
||||
}),
|
||||
},
|
||||
] as unknown as DataView[]);
|
||||
}
|
||||
}, [
|
||||
alertFields,
|
||||
dataViewsService,
|
||||
indexNames,
|
||||
isIndexNameSuccess,
|
||||
isOnlySecurity,
|
||||
isAlertFieldsSuccess,
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
dataViews,
|
||||
loading:
|
||||
featureIds.length === 0 || hasSecurityAndO11yFeatureIds
|
||||
? false
|
||||
: isOnlySecurity
|
||||
? isIndexNameInitialLoading || isIndexNameLoading
|
||||
: isIndexNameInitialLoading ||
|
||||
isIndexNameLoading ||
|
||||
isAlertFieldsInitialLoading ||
|
||||
isAlertFieldsLoading,
|
||||
}),
|
||||
[
|
||||
dataViews,
|
||||
featureIds.length,
|
||||
hasSecurityAndO11yFeatureIds,
|
||||
isOnlySecurity,
|
||||
isIndexNameInitialLoading,
|
||||
isIndexNameLoading,
|
||||
isAlertFieldsInitialLoading,
|
||||
isAlertFieldsLoading,
|
||||
]
|
||||
);
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { useQuery } from '@tanstack/react-query';
|
||||
import type { RuleType, RuleTypeIndex } from '@kbn/triggers-actions-ui-types';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
import { ALERTS_FEATURE_ID } from '../constants';
|
||||
import { fetchRuleTypes } from '../apis/fetch_rule_types';
|
||||
|
||||
export interface UseLoadRuleTypesQueryProps {
|
||||
filteredRuleTypes: string[];
|
||||
enabled?: boolean;
|
||||
http: HttpStart;
|
||||
toasts: ToastsStart;
|
||||
}
|
||||
|
||||
const getFilteredIndex = (data: Array<RuleType<string, string>>, filteredRuleTypes: string[]) => {
|
||||
const index: RuleTypeIndex = new Map();
|
||||
for (const ruleType of data) {
|
||||
index.set(ruleType.id, ruleType);
|
||||
}
|
||||
let filteredIndex = index;
|
||||
if (filteredRuleTypes?.length) {
|
||||
filteredIndex = new Map(
|
||||
[...index].filter(([k, v]) => {
|
||||
return filteredRuleTypes.includes(v.id);
|
||||
})
|
||||
);
|
||||
}
|
||||
return filteredIndex;
|
||||
};
|
||||
|
||||
export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => {
|
||||
const { filteredRuleTypes, enabled = true, http, toasts } = props;
|
||||
|
||||
const queryFn = () => {
|
||||
return fetchRuleTypes({ http });
|
||||
};
|
||||
|
||||
const onErrorFn = () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('alertsUIShared.hooks.useLoadRuleTypesQuery.unableToLoadRuleTypesMessage', {
|
||||
defaultMessage: 'Unable to load rule types',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const { data, isSuccess, isFetching, isInitialLoading, isLoading } = useQuery({
|
||||
queryKey: ['loadRuleTypes'],
|
||||
queryFn,
|
||||
onError: onErrorFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled,
|
||||
});
|
||||
|
||||
const filteredIndex = data ? getFilteredIndex(data, filteredRuleTypes) : new Map();
|
||||
|
||||
const hasAnyAuthorizedRuleType = filteredIndex.size > 0;
|
||||
const authorizedRuleTypes = [...filteredIndex.values()];
|
||||
const authorizedToCreateAnyRules = authorizedRuleTypes.some(
|
||||
(ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all
|
||||
);
|
||||
const authorizedToReadAnyRules =
|
||||
authorizedToCreateAnyRules ||
|
||||
authorizedRuleTypes.some((ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.read);
|
||||
|
||||
return {
|
||||
ruleTypesState: {
|
||||
initialLoad: isLoading || isInitialLoading,
|
||||
isLoading: isLoading || isFetching,
|
||||
data: filteredIndex,
|
||||
},
|
||||
hasAnyAuthorizedRuleType,
|
||||
authorizedRuleTypes,
|
||||
authorizedToReadAnyRules,
|
||||
authorizedToCreateAnyRules,
|
||||
isSuccess,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { EMPTY_AAD_FIELDS } from '../constants';
|
||||
import { fetchAadFields } from '../apis/fetch_aad_fields';
|
||||
|
||||
export interface UseRuleAADFieldsProps {
|
||||
ruleTypeId?: string;
|
||||
http: HttpStart;
|
||||
toasts: ToastsStart;
|
||||
}
|
||||
|
||||
export interface UseRuleAADFieldsResult {
|
||||
aadFields: DataViewField[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function useRuleAADFields(props: UseRuleAADFieldsProps): UseRuleAADFieldsResult {
|
||||
const { ruleTypeId, http, toasts } = props;
|
||||
|
||||
const queryAadFieldsFn = () => {
|
||||
return fetchAadFields({ http, ruleTypeId });
|
||||
};
|
||||
|
||||
const onErrorFn = () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('alertsUIShared.hooks.useRuleAADFields.errorMessage', {
|
||||
defaultMessage: 'Unable to load alert fields per rule type',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
data: aadFields = EMPTY_AAD_FIELDS,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
} = useQuery({
|
||||
queryKey: ['loadAlertAadFieldsPerRuleType', ruleTypeId],
|
||||
queryFn: queryAadFieldsFn,
|
||||
onError: onErrorFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: ruleTypeId !== undefined,
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
aadFields,
|
||||
loading: ruleTypeId === undefined ? false : isInitialLoading || isLoading,
|
||||
}),
|
||||
[aadFields, isInitialLoading, isLoading, ruleTypeId]
|
||||
);
|
||||
}
|
126
packages/kbn-alerts-ui-shared/src/alerts_search_bar/index.tsx
Normal file
126
packages/kbn-alerts-ui-shared/src/alerts_search_bar/index.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
* 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 { useCallback, useState } from 'react';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
import type { SuggestionsAbstraction } from '@kbn/unified-search-plugin/public/typeahead/suggestions_component';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { NO_INDEX_PATTERNS } from './constants';
|
||||
import { SEARCH_BAR_PLACEHOLDER } from './translations';
|
||||
import type { AlertsSearchBarProps, QueryLanguageType } from './types';
|
||||
import { useAlertDataView } from './hooks/use_alert_data_view';
|
||||
import { useRuleAADFields } from './hooks/use_rule_aad_fields';
|
||||
import { useLoadRuleTypesQuery } from './hooks/use_load_rule_types_query';
|
||||
|
||||
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
|
||||
|
||||
export const AlertsSearchBar = ({
|
||||
appName,
|
||||
disableQueryLanguageSwitcher = false,
|
||||
featureIds,
|
||||
ruleTypeId,
|
||||
query,
|
||||
filters,
|
||||
onQueryChange,
|
||||
onQuerySubmit,
|
||||
onFiltersUpdated,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
showFilterBar = false,
|
||||
showDatePicker = true,
|
||||
showSubmitButton = true,
|
||||
placeholder = SEARCH_BAR_PLACEHOLDER,
|
||||
submitOnBlur = false,
|
||||
http,
|
||||
toasts,
|
||||
unifiedSearchBar,
|
||||
dataViewsService,
|
||||
}: AlertsSearchBarProps) => {
|
||||
const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
|
||||
const { dataViews, loading } = useAlertDataView({
|
||||
featureIds: featureIds ?? [],
|
||||
http,
|
||||
toasts,
|
||||
dataViewsService,
|
||||
});
|
||||
const { aadFields, loading: fieldsLoading } = useRuleAADFields({
|
||||
ruleTypeId,
|
||||
http,
|
||||
toasts,
|
||||
});
|
||||
|
||||
const indexPatterns =
|
||||
ruleTypeId && aadFields?.length ? [{ title: ruleTypeId, fields: aadFields }] : dataViews;
|
||||
|
||||
const ruleType = useLoadRuleTypesQuery({
|
||||
filteredRuleTypes: ruleTypeId !== undefined ? [ruleTypeId] : [],
|
||||
enabled: ruleTypeId !== undefined,
|
||||
http,
|
||||
toasts,
|
||||
});
|
||||
|
||||
const isSecurity =
|
||||
(featureIds && featureIds.length === 1 && featureIds.includes(AlertConsumers.SIEM)) ||
|
||||
(ruleType &&
|
||||
ruleTypeId &&
|
||||
ruleType.ruleTypesState.data.get(ruleTypeId)?.producer === AlertConsumers.SIEM);
|
||||
|
||||
const onSearchQuerySubmit = useCallback(
|
||||
({ dateRange, query: nextQuery }: { dateRange: TimeRange; query?: Query }) => {
|
||||
onQuerySubmit({
|
||||
dateRange,
|
||||
query: typeof nextQuery?.query === 'string' ? nextQuery.query : undefined,
|
||||
});
|
||||
setQueryLanguage((nextQuery?.language ?? 'kuery') as QueryLanguageType);
|
||||
},
|
||||
[onQuerySubmit, setQueryLanguage]
|
||||
);
|
||||
|
||||
const onSearchQueryChange = useCallback(
|
||||
({ dateRange, query: nextQuery }: { dateRange: TimeRange; query?: Query }) => {
|
||||
onQueryChange?.({
|
||||
dateRange,
|
||||
query: typeof nextQuery?.query === 'string' ? nextQuery.query : undefined,
|
||||
});
|
||||
setQueryLanguage((nextQuery?.language ?? 'kuery') as QueryLanguageType);
|
||||
},
|
||||
[onQueryChange, setQueryLanguage]
|
||||
);
|
||||
const onRefresh = ({ dateRange }: { dateRange: TimeRange }) => {
|
||||
onQuerySubmit({
|
||||
dateRange,
|
||||
});
|
||||
};
|
||||
|
||||
return unifiedSearchBar({
|
||||
appName,
|
||||
disableQueryLanguageSwitcher,
|
||||
// @ts-expect-error - DataView fields prop and SearchBar indexPatterns props are overly broad
|
||||
indexPatterns: loading || fieldsLoading ? NO_INDEX_PATTERNS : indexPatterns,
|
||||
placeholder,
|
||||
query: { query: query ?? '', language: queryLanguage },
|
||||
filters,
|
||||
dateRangeFrom: rangeFrom,
|
||||
dateRangeTo: rangeTo,
|
||||
displayStyle: 'inPage',
|
||||
showFilterBar,
|
||||
onQuerySubmit: onSearchQuerySubmit,
|
||||
onFiltersUpdated,
|
||||
onRefresh,
|
||||
showDatePicker,
|
||||
showQueryInput: true,
|
||||
saveQueryMenuVisibility: 'allowed_by_app_privilege',
|
||||
showSubmitButton,
|
||||
submitOnBlur,
|
||||
onQueryChange: onSearchQueryChange,
|
||||
suggestionsAbstraction: isSecurity ? undefined : SA_ALERTS,
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export { AlertsSearchBar as default };
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 const SEARCH_BAR_PLACEHOLDER = i18n.translate(
|
||||
'alertsUIShared.component.alertsSearchBar.placeholder',
|
||||
{
|
||||
defaultMessage: 'Search alerts (e.g. kibana.alert.evaluation.threshold > 75)',
|
||||
}
|
||||
);
|
44
packages/kbn-alerts-ui-shared/src/alerts_search_bar/types.ts
Normal file
44
packages/kbn-alerts-ui-shared/src/alerts_search_bar/types.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { Filter } from '@kbn/es-query';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { DataViewsContract } from '@kbn/data-views-plugin/common';
|
||||
|
||||
export type QueryLanguageType = 'lucene' | 'kuery';
|
||||
|
||||
export interface AlertsSearchBarProps {
|
||||
appName: string;
|
||||
disableQueryLanguageSwitcher?: boolean;
|
||||
featureIds: ValidFeatureId[];
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
query?: string;
|
||||
filters?: Filter[];
|
||||
showFilterBar?: boolean;
|
||||
showDatePicker?: boolean;
|
||||
showSubmitButton?: boolean;
|
||||
placeholder?: string;
|
||||
submitOnBlur?: boolean;
|
||||
ruleTypeId?: string;
|
||||
onQueryChange?: (query: {
|
||||
dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' };
|
||||
query?: string;
|
||||
}) => void;
|
||||
onQuerySubmit: (query: {
|
||||
dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' };
|
||||
query?: string;
|
||||
}) => void;
|
||||
onFiltersUpdated?: (filters: Filter[]) => void;
|
||||
http: HttpStart;
|
||||
toasts: ToastsStart;
|
||||
unifiedSearchBar: UnifiedSearchPublicPluginStart['ui']['SearchBar'];
|
||||
dataViewsService: DataViewsContract;
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { AsApiContract } from '@kbn/actions-plugin/common';
|
||||
import { AsApiContract } from '@kbn/actions-types';
|
||||
import type { KibanaServices, MaintenanceWindow } from './types';
|
||||
|
||||
const rewriteMaintenanceWindowRes = ({
|
||||
|
|
|
@ -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 const MAINTENANCE_WINDOW_FEATURE_ID = 'maintenanceWindow';
|
|
@ -10,7 +10,7 @@ import React from 'react';
|
|||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, waitFor, cleanup, screen } from '@testing-library/react';
|
||||
import { MAINTENANCE_WINDOW_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { MAINTENANCE_WINDOW_FEATURE_ID } from './constants';
|
||||
import { MaintenanceWindowCallout } from '.';
|
||||
import { fetchActiveMaintenanceWindows } from './api';
|
||||
import {
|
||||
|
|
|
@ -12,20 +12,11 @@ import { EuiCallOut } from '@elastic/eui';
|
|||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import { MaintenanceWindowStatus, KibanaServices } from './types';
|
||||
import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows';
|
||||
|
||||
const MAINTENANCE_WINDOW_FEATURE_ID = 'maintenanceWindow';
|
||||
const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate(
|
||||
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveDescription',
|
||||
{
|
||||
defaultMessage: 'Rule notifications are stopped while maintenance windows are running.',
|
||||
}
|
||||
);
|
||||
const MAINTENANCE_WINDOW_NO_CATEGORY_TITLE = i18n.translate(
|
||||
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveNoCategories',
|
||||
{
|
||||
defaultMessage: 'One or more maintenance windows are running',
|
||||
}
|
||||
);
|
||||
import { MAINTENANCE_WINDOW_FEATURE_ID } from './constants';
|
||||
import {
|
||||
MAINTENANCE_WINDOW_NO_CATEGORY_TITLE,
|
||||
MAINTENANCE_WINDOW_RUNNING_DESCRIPTION,
|
||||
} from './translations';
|
||||
|
||||
const maintenanceWindowTwoCategoryNames = (names: string[]) =>
|
||||
i18n.translate('alertsUIShared.maintenanceWindowCallout.maintenanceWindowTwoCategoryNames', {
|
||||
|
|
|
@ -6,24 +6,23 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AsApiContract } from '@kbn/alerting-plugin/server/routes/lib';
|
||||
import { MaintenanceWindow, MaintenanceWindowStatus } from './types';
|
||||
import { MaintenanceWindowStatus } from './types';
|
||||
|
||||
export const RUNNING_MAINTENANCE_WINDOW_1: Partial<MaintenanceWindow> = {
|
||||
export const RUNNING_MAINTENANCE_WINDOW_1 = {
|
||||
title: 'Running maintenance window 1',
|
||||
id: '63057284-ac31-42ba-fe22-adfe9732e5ae',
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
events: [{ gte: '2023-04-20T16:27:30.753Z', lte: '2023-04-20T16:57:30.753Z' }],
|
||||
};
|
||||
|
||||
export const RUNNING_MAINTENANCE_WINDOW_2: Partial<MaintenanceWindow> = {
|
||||
export const RUNNING_MAINTENANCE_WINDOW_2 = {
|
||||
title: 'Running maintenance window 2',
|
||||
id: '45894340-df98-11ed-ac81-bfcb4982b4fd',
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
events: [{ gte: '2023-04-20T16:47:42.871Z', lte: '2023-04-20T17:11:32.192Z' }],
|
||||
};
|
||||
|
||||
export const RECURRING_RUNNING_MAINTENANCE_WINDOW: Partial<AsApiContract<MaintenanceWindow>> = {
|
||||
export const RECURRING_RUNNING_MAINTENANCE_WINDOW = {
|
||||
title: 'Recurring running maintenance window',
|
||||
id: 'e2228300-e9ad-11ed-ba37-db17c6e6182b',
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
|
@ -42,7 +41,7 @@ export const RECURRING_RUNNING_MAINTENANCE_WINDOW: Partial<AsApiContract<Mainten
|
|||
},
|
||||
};
|
||||
|
||||
export const UPCOMING_MAINTENANCE_WINDOW: Partial<MaintenanceWindow> = {
|
||||
export const UPCOMING_MAINTENANCE_WINDOW = {
|
||||
title: 'Upcoming maintenance window',
|
||||
id: '5eafe070-e030-11ed-ac81-bfcb4982b4fd',
|
||||
status: MaintenanceWindowStatus.Upcoming,
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate(
|
||||
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveDescription',
|
||||
{
|
||||
defaultMessage: 'Rule notifications are stopped while maintenance windows are running.',
|
||||
}
|
||||
);
|
||||
export const MAINTENANCE_WINDOW_NO_CATEGORY_TITLE = i18n.translate(
|
||||
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveNoCategories',
|
||||
{
|
||||
defaultMessage: 'One or more maintenance windows are running',
|
||||
}
|
||||
);
|
|
@ -20,9 +20,13 @@
|
|||
"@kbn/rule-data-utils",
|
||||
"@kbn/core",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/alerting-plugin",
|
||||
"@kbn/rrule",
|
||||
"@kbn/actions-plugin",
|
||||
"@kbn/core-application-common"
|
||||
"@kbn/core-application-common",
|
||||
"@kbn/triggers-actions-ui-types",
|
||||
"@kbn/alerting-types",
|
||||
"@kbn/actions-types",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/es-query",
|
||||
]
|
||||
}
|
||||
|
|
3
packages/kbn-triggers-actions-ui-types/README.md
Normal file
3
packages/kbn-triggers-actions-ui-types/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/triggers-actions-ui-types
|
||||
|
||||
Empty package generated by @kbn/generate
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { ActionVariable } from '@kbn/alerting-types';
|
||||
|
||||
export const REQUIRED_ACTION_VARIABLES = ['params'] as const;
|
||||
export const CONTEXT_ACTION_VARIABLES = ['context'] as const;
|
||||
export const OPTIONAL_ACTION_VARIABLES = [...CONTEXT_ACTION_VARIABLES, 'state'] as const;
|
||||
|
||||
type AsActionVariables<Keys extends string> = {
|
||||
[Req in Keys]: ActionVariable[];
|
||||
};
|
||||
|
||||
export type ActionVariables = AsActionVariables<typeof REQUIRED_ACTION_VARIABLES[number]> &
|
||||
Partial<AsActionVariables<typeof OPTIONAL_ACTION_VARIABLES[number]>>;
|
10
packages/kbn-triggers-actions-ui-types/index.ts
Normal file
10
packages/kbn-triggers-actions-ui-types/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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_types';
|
||||
export * from './action_variable_types';
|
13
packages/kbn-triggers-actions-ui-types/jest.config.js
Normal file
13
packages/kbn-triggers-actions-ui-types/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-triggers-actions-ui-types'],
|
||||
};
|
5
packages/kbn-triggers-actions-ui-types/kibana.jsonc
Normal file
5
packages/kbn-triggers-actions-ui-types/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/triggers-actions-ui-types",
|
||||
"owner": "@elastic/response-ops"
|
||||
}
|
6
packages/kbn-triggers-actions-ui-types/package.json
Normal file
6
packages/kbn-triggers-actions-ui-types/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/triggers-actions-ui-types",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
35
packages/kbn-triggers-actions-ui-types/rule_types.ts
Normal file
35
packages/kbn-triggers-actions-ui-types/rule_types.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { RuleType as CommonRuleType } from '@kbn/alerting-types';
|
||||
import type { ActionVariables } from './action_variable_types';
|
||||
|
||||
export interface RuleType<
|
||||
ActionGroupIds extends string = string,
|
||||
RecoveryActionGroupId extends string = string
|
||||
> extends Pick<
|
||||
CommonRuleType<ActionGroupIds, RecoveryActionGroupId>,
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'actionGroups'
|
||||
| 'producer'
|
||||
| 'minimumLicenseRequired'
|
||||
| 'recoveryActionGroup'
|
||||
| 'defaultActionGroupId'
|
||||
| 'ruleTaskTimeout'
|
||||
| 'defaultScheduleInterval'
|
||||
| 'doesSetRecoveryContext'
|
||||
> {
|
||||
actionVariables: ActionVariables;
|
||||
authorizedConsumers: Record<string, { read: boolean; all: boolean }>;
|
||||
enabledInLicense: boolean;
|
||||
hasFieldsForAAD?: boolean;
|
||||
hasAlertsMappings?: boolean;
|
||||
}
|
||||
|
||||
export type RuleTypeIndex = Map<string, RuleType>;
|
21
packages/kbn-triggers-actions-ui-types/tsconfig.json
Normal file
21
packages/kbn-triggers-actions-ui-types/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/alerting-types"
|
||||
]
|
||||
}
|
|
@ -12,6 +12,8 @@
|
|||
"@kbn/actions-plugin/*": ["x-pack/plugins/actions/*"],
|
||||
"@kbn/actions-simulators-plugin": ["x-pack/test/alerting_api_integration/common/plugins/actions_simulators"],
|
||||
"@kbn/actions-simulators-plugin/*": ["x-pack/test/alerting_api_integration/common/plugins/actions_simulators/*"],
|
||||
"@kbn/actions-types": ["packages/kbn-actions-types"],
|
||||
"@kbn/actions-types/*": ["packages/kbn-actions-types/*"],
|
||||
"@kbn/advanced-settings-plugin": ["src/plugins/advanced_settings"],
|
||||
"@kbn/advanced-settings-plugin/*": ["src/plugins/advanced_settings/*"],
|
||||
"@kbn/aiops-components": ["x-pack/packages/ml/aiops_components"],
|
||||
|
@ -32,6 +34,8 @@
|
|||
"@kbn/alerting-plugin/*": ["x-pack/plugins/alerting/*"],
|
||||
"@kbn/alerting-state-types": ["x-pack/packages/kbn-alerting-state-types"],
|
||||
"@kbn/alerting-state-types/*": ["x-pack/packages/kbn-alerting-state-types/*"],
|
||||
"@kbn/alerting-types": ["packages/kbn-alerting-types"],
|
||||
"@kbn/alerting-types/*": ["packages/kbn-alerting-types/*"],
|
||||
"@kbn/alerts-as-data-utils": ["packages/kbn-alerts-as-data-utils"],
|
||||
"@kbn/alerts-as-data-utils/*": ["packages/kbn-alerts-as-data-utils/*"],
|
||||
"@kbn/alerts-restricted-fixtures-plugin": ["x-pack/test/alerting_api_integration/common/plugins/alerts_restricted"],
|
||||
|
@ -1576,6 +1580,8 @@
|
|||
"@kbn/triggers-actions-ui-example-plugin/*": ["x-pack/examples/triggers_actions_ui_example/*"],
|
||||
"@kbn/triggers-actions-ui-plugin": ["x-pack/plugins/triggers_actions_ui"],
|
||||
"@kbn/triggers-actions-ui-plugin/*": ["x-pack/plugins/triggers_actions_ui/*"],
|
||||
"@kbn/triggers-actions-ui-types": ["packages/kbn-triggers-actions-ui-types"],
|
||||
"@kbn/triggers-actions-ui-types/*": ["packages/kbn-triggers-actions-ui-types/*"],
|
||||
"@kbn/ts-projects": ["packages/kbn-ts-projects"],
|
||||
"@kbn/ts-projects/*": ["packages/kbn-ts-projects/*"],
|
||||
"@kbn/ts-type-check-cli": ["packages/kbn-ts-type-check-cli"],
|
||||
|
|
|
@ -5,46 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
type RenameActionToConnector<K extends string> = K extends `actionTypeId`
|
||||
? `connectorTypeId`
|
||||
: K extends `actionId`
|
||||
? `connectorId`
|
||||
: K;
|
||||
|
||||
export type AsApiContract<T> = {
|
||||
[K in keyof T as CamelToSnake<RenameActionToConnector<Extract<K, string>>>]: K extends 'frequency'
|
||||
? AsApiContract<T[K]>
|
||||
: T[K];
|
||||
};
|
||||
|
||||
export type RewriteRequestCase<T> = (requested: AsApiContract<T>) => T;
|
||||
export type RewriteResponseCase<T> = (
|
||||
responded: T
|
||||
) => T extends Array<infer Item> ? Array<AsApiContract<Item>> : AsApiContract<T>;
|
||||
|
||||
/**
|
||||
* This type maps Camel Case strings into their Snake Case version.
|
||||
* This is achieved by checking each character and, if it is an uppercase character, it is mapped to an
|
||||
* underscore followed by a lowercase one.
|
||||
*
|
||||
* The reason there are two ternaries is that, for perfformance reasons, TS limits its
|
||||
* character parsing to ~15 characters.
|
||||
* To get around this we use the second turnery to parse 2 characters at a time, which allows us to support
|
||||
* strings that are 30 characters long.
|
||||
*
|
||||
* If you get the TS #2589 error ("Type instantiation is excessively deep and possibly infinite") then most
|
||||
* likely you have a string that's longer than 30 characters.
|
||||
* Address this by reducing the length if possible, otherwise, you'll need to add a 3rd ternary which
|
||||
* parses 3 chars at a time :grimace:
|
||||
*
|
||||
* For more details see this PR comment: https://github.com/microsoft/TypeScript/pull/40336#issuecomment-686723087
|
||||
*/
|
||||
type CamelToSnake<T extends string> = string extends T
|
||||
? string
|
||||
: T extends `${infer C0}${infer C1}${infer R}`
|
||||
? `${C0 extends Uppercase<C0> ? '_' : ''}${Lowercase<C0>}${C1 extends Uppercase<C1>
|
||||
? '_'
|
||||
: ''}${Lowercase<C1>}${CamelToSnake<R>}`
|
||||
: T extends `${infer C0}${infer R}`
|
||||
? `${C0 extends Uppercase<C0> ? '_' : ''}${Lowercase<C0>}${CamelToSnake<R>}`
|
||||
: '';
|
||||
export type { AsApiContract, RewriteRequestCase, RewriteResponseCase } from '@kbn/actions-types';
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"@kbn/core-elasticsearch-server-mocks",
|
||||
"@kbn/core-logging-server-mocks",
|
||||
"@kbn/serverless",
|
||||
"@kbn/actions-types"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -5,18 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ActionGroup } from './rule_type';
|
||||
|
||||
export type DefaultActionGroupId = 'default';
|
||||
|
||||
export type RecoveredActionGroupId = typeof RecoveredActionGroup['id'];
|
||||
export const RecoveredActionGroup: Readonly<ActionGroup<'recovered'>> = Object.freeze({
|
||||
id: 'recovered',
|
||||
name: i18n.translate('xpack.alerting.builtinActionGroups.recovered', {
|
||||
defaultMessage: 'Recovered',
|
||||
}),
|
||||
});
|
||||
import type { RecoveredActionGroupId, ActionGroup } from '@kbn/alerting-types';
|
||||
import { RecoveredActionGroup } from '@kbn/alerting-types';
|
||||
|
||||
export type ReservedActionGroups<RecoveryActionGroupId extends string> =
|
||||
| RecoveryActionGroupId
|
||||
|
@ -32,3 +22,7 @@ export function getBuiltinActionGroups<RecoveryActionGroupId extends string>(
|
|||
): [ActionGroup<ReservedActionGroups<RecoveryActionGroupId>>] {
|
||||
return [customRecoveryGroup ?? RecoveredActionGroup];
|
||||
}
|
||||
|
||||
export type { RecoveredActionGroupId, DefaultActionGroupId } from '@kbn/alerting-types';
|
||||
|
||||
export { RecoveredActionGroup } from '@kbn/alerting-types';
|
||||
|
|
|
@ -38,6 +38,7 @@ export * from './rule_tags_aggregation';
|
|||
export * from './iso_weekdays';
|
||||
export * from './saved_objects/rules/mappings';
|
||||
export * from './rule_circuit_breaker_error_message';
|
||||
export * from './maintenance_window_scoped_query_error_message';
|
||||
|
||||
export type {
|
||||
MaintenanceWindowModificationMetadata,
|
||||
|
@ -48,6 +49,7 @@ export type {
|
|||
MaintenanceWindowCreateBody,
|
||||
MaintenanceWindowClientContext,
|
||||
MaintenanceWindowDeepLinkIds,
|
||||
ScopedQueryAttributes,
|
||||
} from './maintenance_window';
|
||||
|
||||
export {
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const errorMessageIdentifier = 'invalid scoped query';
|
||||
|
||||
export const getScopedQueryErrorMessage = (errorMessage: string) => {
|
||||
return `${errorMessageIdentifier} - ${errorMessage}`;
|
||||
};
|
||||
|
||||
export const isScopedQueryError = (errorMessage: string) => {
|
||||
return errorMessage.includes(errorMessageIdentifier);
|
||||
};
|
|
@ -15,6 +15,8 @@ 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';
|
||||
|
||||
export type RuleTypeState = Record<string, unknown>;
|
||||
export type RuleTypeParams = Record<string, unknown>;
|
||||
|
||||
|
@ -242,14 +244,6 @@ export interface AlertsHealth {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ActionVariable {
|
||||
name: string;
|
||||
description: string;
|
||||
deprecated?: boolean;
|
||||
useWithTripleBracesInTemplates?: boolean;
|
||||
usesPublicBaseUrl?: boolean;
|
||||
}
|
||||
|
||||
export interface RuleMonitoringHistory extends SavedObjectAttributes {
|
||||
success: boolean;
|
||||
timestamp: number;
|
||||
|
|
|
@ -5,50 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LicenseType } from '@kbn/licensing-plugin/common/types';
|
||||
import { RecoveredActionGroupId, DefaultActionGroupId } from './builtin_action_groups';
|
||||
|
||||
interface ConsumerPrivileges {
|
||||
read: boolean;
|
||||
all: boolean;
|
||||
}
|
||||
|
||||
interface ActionVariable {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
export interface RuleType<
|
||||
ActionGroupIds extends Exclude<string, RecoveredActionGroupId> = DefaultActionGroupId,
|
||||
RecoveryActionGroupId extends string = RecoveredActionGroupId
|
||||
> {
|
||||
id: string;
|
||||
name: string;
|
||||
actionGroups: Array<ActionGroup<ActionGroupIds>>;
|
||||
recoveryActionGroup: ActionGroup<RecoveryActionGroupId>;
|
||||
actionVariables: {
|
||||
context: ActionVariable[];
|
||||
state: ActionVariable[];
|
||||
params: ActionVariable[];
|
||||
};
|
||||
defaultActionGroupId: ActionGroupIds;
|
||||
category: string;
|
||||
producer: string;
|
||||
minimumLicenseRequired: LicenseType;
|
||||
isExportable: boolean;
|
||||
ruleTaskTimeout?: string;
|
||||
defaultScheduleInterval?: string;
|
||||
doesSetRecoveryContext?: boolean;
|
||||
enabledInLicense: boolean;
|
||||
authorizedConsumers: Record<string, ConsumerPrivileges>;
|
||||
}
|
||||
|
||||
export interface ActionGroup<ActionGroupIds extends string> {
|
||||
id: ActionGroupIds;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ActionGroupIdsOf<T> = T extends ActionGroup<infer groups>
|
||||
? groups
|
||||
: T extends Readonly<ActionGroup<infer groups>>
|
||||
? groups
|
||||
: never;
|
||||
export type { RuleType, ActionGroup, ActionGroupIdsOf } from '@kbn/alerting-types';
|
||||
|
|
|
@ -7,12 +7,20 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { MaintenanceWindow } from '../pages/maintenance_windows/types';
|
||||
import { createMaintenanceWindow } from '../services/maintenance_windows_api/create';
|
||||
|
||||
export function useCreateMaintenanceWindow() {
|
||||
interface UseCreateMaintenanceWindowProps {
|
||||
onError?: (error: IHttpFetchError<KibanaServerError>) => void;
|
||||
}
|
||||
|
||||
export function useCreateMaintenanceWindow(props?: UseCreateMaintenanceWindowProps) {
|
||||
const { onError } = props || {};
|
||||
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
|
@ -33,12 +41,13 @@ export function useCreateMaintenanceWindow() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error: IHttpFetchError<KibanaServerError>) => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsCreateFailure', {
|
||||
defaultMessage: 'Failed to create maintenance window.',
|
||||
})
|
||||
);
|
||||
onError?.(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ export const useGetRuleTypes = () => {
|
|||
queryKey: ['useGetRuleTypes'],
|
||||
queryFn,
|
||||
onError,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -7,12 +7,20 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { useKibana } from '../utils/kibana_react';
|
||||
import { MaintenanceWindow } from '../pages/maintenance_windows/types';
|
||||
import { updateMaintenanceWindow } from '../services/maintenance_windows_api/update';
|
||||
|
||||
export function useUpdateMaintenanceWindow() {
|
||||
interface UseUpdateMaintenanceWindowProps {
|
||||
onError?: (error: IHttpFetchError<KibanaServerError>) => void;
|
||||
}
|
||||
|
||||
export function useUpdateMaintenanceWindow(props?: UseUpdateMaintenanceWindowProps) {
|
||||
const { onError } = props || {};
|
||||
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
|
@ -39,12 +47,13 @@ export function useUpdateMaintenanceWindow() {
|
|||
})
|
||||
);
|
||||
},
|
||||
onError: () => {
|
||||
onError: (error: IHttpFetchError<KibanaServerError>) => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.alerting.maintenanceWindowsUpdateFailure', {
|
||||
defaultMessage: 'Failed to update maintenance window.',
|
||||
})
|
||||
);
|
||||
onError?.(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -45,6 +45,11 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
addDanger: jest.fn(),
|
||||
},
|
||||
},
|
||||
unifiedSearch: {
|
||||
ui: {
|
||||
SearchBar: <div />,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -136,13 +141,13 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-observability');
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-securitySolution');
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-management');
|
||||
).getByTestId('option-management');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
|
@ -176,13 +181,13 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-observability');
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-securitySolution');
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-management');
|
||||
).getByTestId('option-management');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
|
@ -213,13 +218,13 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-observability');
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-securitySolution');
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-management');
|
||||
).getByTestId('option-management');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
|
@ -237,13 +242,13 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-observability');
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-securitySolution');
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('checkbox-management');
|
||||
).getByTestId('option-management');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
|
|
|
@ -30,12 +30,16 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { TIMEZONE_OPTIONS as UI_TIMEZONE_OPTIONS } from '@kbn/core-ui-settings-common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public';
|
||||
import { FormProps, schema } from './schema';
|
||||
import * as i18n from '../translations';
|
||||
import { RecurringSchedule } from './recurring_schedule_form/recurring_schedule';
|
||||
import { SubmitButton } from './submit_button';
|
||||
import { convertToRRule } from '../helpers/convert_to_rrule';
|
||||
import { isScopedQueryError } from '../../../../common';
|
||||
import { useCreateMaintenanceWindow } from '../../../hooks/use_create_maintenance_window';
|
||||
import { useUpdateMaintenanceWindow } from '../../../hooks/use_update_maintenance_window';
|
||||
import { useGetRuleTypes } from '../../../hooks/use_get_rule_types';
|
||||
|
@ -43,14 +47,23 @@ import { useUiSetting } from '../../../utils/kibana_react';
|
|||
import { DatePickerRangeField } from './fields/date_picker_range_field';
|
||||
import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window';
|
||||
import { MaintenanceWindowCategorySelection } from './maintenance_window_category_selection';
|
||||
import { MaintenanceWindowScopedQuerySwitch } from './maintenance_window_scoped_query_switch';
|
||||
import { MaintenanceWindowScopedQuery } from './maintenance_window_scoped_query';
|
||||
|
||||
const UseField = getUseField({ component: Field });
|
||||
|
||||
const VALID_CATEGORIES = [
|
||||
DEFAULT_APP_CATEGORIES.observability.id,
|
||||
DEFAULT_APP_CATEGORIES.security.id,
|
||||
DEFAULT_APP_CATEGORIES.management.id,
|
||||
];
|
||||
|
||||
export interface CreateMaintenanceWindowFormProps {
|
||||
onCancel: () => void;
|
||||
onSuccess: () => void;
|
||||
initialValue?: FormProps;
|
||||
maintenanceWindowId?: string;
|
||||
scopedQueryFeatureFlag?: boolean;
|
||||
}
|
||||
|
||||
const useDefaultTimezone = () => {
|
||||
|
@ -62,317 +75,433 @@ const useDefaultTimezone = () => {
|
|||
};
|
||||
const TIMEZONE_OPTIONS = UI_TIMEZONE_OPTIONS.map((n) => ({ label: n })) ?? [{ label: 'UTC' }];
|
||||
|
||||
export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFormProps>(
|
||||
({ onCancel, onSuccess, initialValue, maintenanceWindowId }) => {
|
||||
const [defaultStartDateValue] = useState<string>(moment().toISOString());
|
||||
const [defaultEndDateValue] = useState<string>(moment().add(30, 'minutes').toISOString());
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { defaultTimezone, isBrowser } = useDefaultTimezone();
|
||||
export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFormProps>((props) => {
|
||||
const {
|
||||
onCancel,
|
||||
onSuccess,
|
||||
initialValue,
|
||||
maintenanceWindowId,
|
||||
scopedQueryFeatureFlag = true,
|
||||
} = props;
|
||||
|
||||
const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined;
|
||||
const [defaultStartDateValue] = useState<string>(moment().toISOString());
|
||||
const [defaultEndDateValue] = useState<string>(moment().add(30, 'minutes').toISOString());
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const { defaultTimezone, isBrowser } = useDefaultTimezone();
|
||||
|
||||
const hasSetInitialCategories = useRef<boolean>(false);
|
||||
const [isScopedQueryEnabled, setIsScopedQueryEnabled] = useState(!!initialValue?.scopedQuery);
|
||||
const [query, setQuery] = useState<string>(initialValue?.scopedQuery?.kql || '');
|
||||
const [filters, setFilters] = useState<Filter[]>(
|
||||
(initialValue?.scopedQuery?.filters as Filter[]) || []
|
||||
);
|
||||
const [scopedQueryErrors, setScopedQueryErrors] = useState<string[]>([]);
|
||||
const hasSetInitialCategories = useRef<boolean>(false);
|
||||
const categoryIdsHistory = useRef<string[]>([]);
|
||||
|
||||
const { mutate: createMaintenanceWindow, isLoading: isCreateLoading } =
|
||||
useCreateMaintenanceWindow();
|
||||
const { mutate: updateMaintenanceWindow, isLoading: isUpdateLoading } =
|
||||
useUpdateMaintenanceWindow();
|
||||
const { mutate: archiveMaintenanceWindow } = useArchiveMaintenanceWindow();
|
||||
const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined;
|
||||
|
||||
const { data: ruleTypes, isLoading: isLoadingRuleTypes } = useGetRuleTypes();
|
||||
const onCreateOrUpdateError = useCallback((error: IHttpFetchError<KibanaServerError>) => {
|
||||
if (!error.body?.message) {
|
||||
return;
|
||||
}
|
||||
if (isScopedQueryError(error.body.message)) {
|
||||
setScopedQueryErrors([i18n.CREATE_FORM_SCOPED_QUERY_INVALID_ERROR_MESSAGE]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const submitMaintenanceWindow = useCallback(
|
||||
async (formData, isValid) => {
|
||||
if (isValid) {
|
||||
const startDate = moment(formData.startDate);
|
||||
const endDate = moment(formData.endDate);
|
||||
const maintenanceWindow = {
|
||||
title: formData.title,
|
||||
duration: endDate.diff(startDate),
|
||||
rRule: convertToRRule(
|
||||
startDate,
|
||||
formData.timezone ? formData.timezone[0] : defaultTimezone,
|
||||
formData.recurringSchedule
|
||||
),
|
||||
categoryIds: formData.categoryIds,
|
||||
};
|
||||
if (isEditMode) {
|
||||
updateMaintenanceWindow({ maintenanceWindowId, maintenanceWindow }, { onSuccess });
|
||||
} else {
|
||||
createMaintenanceWindow(maintenanceWindow, { onSuccess });
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
isEditMode,
|
||||
maintenanceWindowId,
|
||||
updateMaintenanceWindow,
|
||||
createMaintenanceWindow,
|
||||
onSuccess,
|
||||
defaultTimezone,
|
||||
]
|
||||
);
|
||||
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
onSubmit: submitMaintenanceWindow,
|
||||
const { mutate: createMaintenanceWindow, isLoading: isCreateLoading } =
|
||||
useCreateMaintenanceWindow({
|
||||
onError: onCreateOrUpdateError,
|
||||
});
|
||||
|
||||
const [{ recurring, timezone, categoryIds }, _, mounted] = useFormData<FormProps>({
|
||||
form,
|
||||
watch: ['recurring', 'timezone', 'categoryIds'],
|
||||
const { mutate: updateMaintenanceWindow, isLoading: isUpdateLoading } =
|
||||
useUpdateMaintenanceWindow({
|
||||
onError: onCreateOrUpdateError,
|
||||
});
|
||||
const isRecurring = recurring || false;
|
||||
const showTimezone = isBrowser || initialValue?.timezone !== undefined;
|
||||
|
||||
const closeModal = useCallback(() => setIsModalVisible(false), []);
|
||||
const showModal = useCallback(() => setIsModalVisible(true), []);
|
||||
const { mutate: archiveMaintenanceWindow } = useArchiveMaintenanceWindow();
|
||||
|
||||
const { setFieldValue } = form;
|
||||
const { data: ruleTypes, isLoading: isLoadingRuleTypes } = useGetRuleTypes();
|
||||
|
||||
const onCategoryIdsChange = useCallback(
|
||||
(id: string) => {
|
||||
if (!categoryIds) {
|
||||
return;
|
||||
}
|
||||
if (categoryIds.includes(id)) {
|
||||
setFieldValue(
|
||||
'categoryIds',
|
||||
categoryIds.filter((category) => category !== id)
|
||||
);
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', [...categoryIds, id]);
|
||||
},
|
||||
[categoryIds, setFieldValue]
|
||||
);
|
||||
const scopedQueryPayload = useMemo(() => {
|
||||
if (!isScopedQueryEnabled || !scopedQueryFeatureFlag) {
|
||||
return null;
|
||||
}
|
||||
if (!query && !filters.length) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kql: query,
|
||||
filters,
|
||||
};
|
||||
}, [isScopedQueryEnabled, scopedQueryFeatureFlag, query, filters]);
|
||||
|
||||
const modal = useMemo(() => {
|
||||
let m;
|
||||
if (isModalVisible) {
|
||||
m = (
|
||||
<EuiConfirmModal
|
||||
title={i18n.ARCHIVE_TITLE}
|
||||
onCancel={closeModal}
|
||||
onConfirm={() => {
|
||||
closeModal();
|
||||
archiveMaintenanceWindow(
|
||||
{ maintenanceWindowId: maintenanceWindowId!, archive: true },
|
||||
{ onSuccess }
|
||||
);
|
||||
}}
|
||||
cancelButtonText={i18n.CANCEL}
|
||||
confirmButtonText={i18n.ARCHIVE_TITLE}
|
||||
defaultFocusedButton="confirm"
|
||||
buttonColor="danger"
|
||||
>
|
||||
<p>{i18n.ARCHIVE_CALLOUT_SUBTITLE}</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
const submitMaintenanceWindow = useCallback(
|
||||
async (formData, isValid) => {
|
||||
if (!isValid || scopedQueryErrors.length !== 0) {
|
||||
return;
|
||||
}
|
||||
return m;
|
||||
}, [closeModal, archiveMaintenanceWindow, isModalVisible, maintenanceWindowId, onSuccess]);
|
||||
|
||||
const availableCategories = useMemo(() => {
|
||||
if (!ruleTypes) {
|
||||
return [];
|
||||
if (isScopedQueryEnabled && !scopedQueryPayload) {
|
||||
setScopedQueryErrors([i18n.CREATE_FORM_SCOPED_QUERY_EMPTY_ERROR_MESSAGE]);
|
||||
return;
|
||||
}
|
||||
return [...new Set(ruleTypes.map((ruleType) => ruleType.category))];
|
||||
}, [ruleTypes]);
|
||||
|
||||
// For create mode, we want to initialize options to the rule type category the
|
||||
// user has access
|
||||
useEffect(() => {
|
||||
const startDate = moment(formData.startDate);
|
||||
const endDate = moment(formData.endDate);
|
||||
const maintenanceWindow = {
|
||||
title: formData.title,
|
||||
duration: endDate.diff(startDate),
|
||||
rRule: convertToRRule(
|
||||
startDate,
|
||||
formData.timezone ? formData.timezone[0] : defaultTimezone,
|
||||
formData.recurringSchedule
|
||||
),
|
||||
categoryIds: formData.categoryIds,
|
||||
scopedQuery: scopedQueryPayload,
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
return;
|
||||
updateMaintenanceWindow({ maintenanceWindowId, maintenanceWindow }, { onSuccess });
|
||||
} else {
|
||||
createMaintenanceWindow(maintenanceWindow, { onSuccess });
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (hasSetInitialCategories.current) {
|
||||
return;
|
||||
}
|
||||
if (!ruleTypes) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', [...new Set(ruleTypes.map((ruleType) => ruleType.category))]);
|
||||
hasSetInitialCategories.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, ruleTypes, mounted]);
|
||||
},
|
||||
[
|
||||
isEditMode,
|
||||
isScopedQueryEnabled,
|
||||
scopedQueryErrors,
|
||||
maintenanceWindowId,
|
||||
updateMaintenanceWindow,
|
||||
createMaintenanceWindow,
|
||||
onSuccess,
|
||||
defaultTimezone,
|
||||
scopedQueryPayload,
|
||||
]
|
||||
);
|
||||
|
||||
// For edit mode, if a maintenance window => category_ids is not an array, this means
|
||||
// the maintenance window was created before the introduction of category filters.
|
||||
// For backwards compat we will initialize all options for these.
|
||||
useEffect(() => {
|
||||
if (!isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (hasSetInitialCategories.current) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(categoryIds)) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', [
|
||||
DEFAULT_APP_CATEGORIES.observability.id,
|
||||
DEFAULT_APP_CATEGORIES.security.id,
|
||||
DEFAULT_APP_CATEGORIES.management.id,
|
||||
]);
|
||||
hasSetInitialCategories.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, categoryIds, mounted]);
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialValue,
|
||||
options: { stripEmptyFields: false },
|
||||
schema,
|
||||
onSubmit: submitMaintenanceWindow,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup direction="column" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="title"
|
||||
componentProps={{
|
||||
'data-test-subj': 'title-field',
|
||||
euiFieldProps: {
|
||||
autoFocus: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_TIMEFRAME_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.CREATE_FORM_TIMEFRAME_DESCRIPTION}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexEnd" responsive={false}>
|
||||
<EuiFlexItem grow={3}>
|
||||
<UseMultiFields
|
||||
fields={{
|
||||
startDate: {
|
||||
path: 'startDate',
|
||||
config: {
|
||||
label: i18n.CREATE_FORM_SCHEDULE,
|
||||
defaultValue: defaultStartDateValue,
|
||||
validations: [],
|
||||
},
|
||||
const [{ recurring, timezone, categoryIds }, _, mounted] = useFormData<FormProps>({
|
||||
form,
|
||||
watch: ['recurring', 'timezone', 'categoryIds', 'scopedQuery'],
|
||||
});
|
||||
const isRecurring = recurring || false;
|
||||
const showTimezone = isBrowser || initialValue?.timezone !== undefined;
|
||||
|
||||
const closeModal = useCallback(() => setIsModalVisible(false), []);
|
||||
const showModal = useCallback(() => setIsModalVisible(true), []);
|
||||
|
||||
const { setFieldValue } = form;
|
||||
|
||||
const validRuleTypes = useMemo(() => {
|
||||
if (!ruleTypes) {
|
||||
return [];
|
||||
}
|
||||
return ruleTypes.filter((ruleType) => VALID_CATEGORIES.includes(ruleType.category));
|
||||
}, [ruleTypes]);
|
||||
|
||||
const availableCategories = useMemo(() => {
|
||||
return [...new Set(validRuleTypes.map((ruleType) => ruleType.category))];
|
||||
}, [validRuleTypes]);
|
||||
|
||||
const featureIds = useMemo(() => {
|
||||
if (!Array.isArray(validRuleTypes) || !Array.isArray(categoryIds) || !mounted) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const featureIdsSet = new Set<ValidFeatureId>();
|
||||
|
||||
validRuleTypes.forEach((ruleType) => {
|
||||
if (categoryIds.includes(ruleType.category)) {
|
||||
featureIdsSet.add(ruleType.producer as ValidFeatureId);
|
||||
}
|
||||
});
|
||||
|
||||
return [...featureIdsSet];
|
||||
}, [validRuleTypes, categoryIds, mounted]);
|
||||
|
||||
const onCategoryIdsChange = useCallback(
|
||||
(ids: string[]) => {
|
||||
if (!categoryIds) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', ids);
|
||||
},
|
||||
[categoryIds, setFieldValue]
|
||||
);
|
||||
|
||||
const onScopeQueryToggle = useCallback(
|
||||
(isEnabled: boolean) => {
|
||||
if (isEnabled) {
|
||||
setFieldValue('categoryIds', [categoryIds?.sort()[0] || availableCategories.sort()[0]]);
|
||||
} else {
|
||||
setFieldValue('categoryIds', categoryIdsHistory.current);
|
||||
}
|
||||
setIsScopedQueryEnabled(isEnabled);
|
||||
},
|
||||
[categoryIds, availableCategories, setFieldValue]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
(newQuery: string) => {
|
||||
if (scopedQueryErrors.length) {
|
||||
setScopedQueryErrors([]);
|
||||
}
|
||||
setQuery(newQuery);
|
||||
},
|
||||
[scopedQueryErrors]
|
||||
);
|
||||
|
||||
const modal = useMemo(() => {
|
||||
let m;
|
||||
if (isModalVisible) {
|
||||
m = (
|
||||
<EuiConfirmModal
|
||||
title={i18n.ARCHIVE_TITLE}
|
||||
onCancel={closeModal}
|
||||
onConfirm={() => {
|
||||
closeModal();
|
||||
archiveMaintenanceWindow(
|
||||
{ maintenanceWindowId: maintenanceWindowId!, archive: true },
|
||||
{ onSuccess }
|
||||
);
|
||||
}}
|
||||
cancelButtonText={i18n.CANCEL}
|
||||
confirmButtonText={i18n.ARCHIVE_TITLE}
|
||||
defaultFocusedButton="confirm"
|
||||
buttonColor="danger"
|
||||
>
|
||||
<p>{i18n.ARCHIVE_CALLOUT_SUBTITLE}</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
}
|
||||
return m;
|
||||
}, [closeModal, archiveMaintenanceWindow, isModalVisible, maintenanceWindowId, onSuccess]);
|
||||
|
||||
// For create mode, we want to initialize options to the rule type category the
|
||||
// user has access
|
||||
useEffect(() => {
|
||||
if (isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (hasSetInitialCategories.current) {
|
||||
return;
|
||||
}
|
||||
if (!validRuleTypes.length) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', [...new Set(validRuleTypes.map((ruleType) => ruleType.category))]);
|
||||
hasSetInitialCategories.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, validRuleTypes, mounted]);
|
||||
|
||||
// For edit mode, if a maintenance window => category_ids is not an array, this means
|
||||
// the maintenance window was created before the introduction of category filters.
|
||||
// For backwards compat we will initialize all options for these.
|
||||
useEffect(() => {
|
||||
if (!isEditMode) {
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (hasSetInitialCategories.current) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(categoryIds)) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', VALID_CATEGORIES);
|
||||
hasSetInitialCategories.current = true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isEditMode, categoryIds, mounted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isScopedQueryEnabled && Array.isArray(categoryIds)) {
|
||||
categoryIdsHistory.current = categoryIds;
|
||||
}
|
||||
}, [categoryIds, isScopedQueryEnabled]);
|
||||
|
||||
return (
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup direction="column" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="title"
|
||||
componentProps={{
|
||||
'data-test-subj': 'title-field',
|
||||
euiFieldProps: {
|
||||
autoFocus: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_TIMEFRAME_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">{i18n.CREATE_FORM_TIMEFRAME_DESCRIPTION}</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexEnd" responsive={false}>
|
||||
<EuiFlexItem grow={3}>
|
||||
<UseMultiFields
|
||||
fields={{
|
||||
startDate: {
|
||||
path: 'startDate',
|
||||
config: {
|
||||
label: i18n.CREATE_FORM_SCHEDULE,
|
||||
defaultValue: defaultStartDateValue,
|
||||
validations: [],
|
||||
},
|
||||
endDate: {
|
||||
path: 'endDate',
|
||||
config: {
|
||||
label: '',
|
||||
defaultValue: defaultEndDateValue,
|
||||
validations: [],
|
||||
},
|
||||
},
|
||||
endDate: {
|
||||
path: 'endDate',
|
||||
config: {
|
||||
label: '',
|
||||
defaultValue: defaultEndDateValue,
|
||||
validations: [],
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(fields) => (
|
||||
<DatePickerRangeField
|
||||
fields={fields}
|
||||
timezone={timezone ?? [defaultTimezone]}
|
||||
data-test-subj="date-field"
|
||||
/>
|
||||
)}
|
||||
</UseMultiFields>
|
||||
</EuiFlexItem>
|
||||
{showTimezone ? (
|
||||
<EuiFlexItem grow={1}>
|
||||
<UseField
|
||||
path="timezone"
|
||||
config={{
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
validations: [],
|
||||
defaultValue: [defaultTimezone],
|
||||
}}
|
||||
componentProps={{
|
||||
'data-test-subj': 'timezone-field',
|
||||
id: 'timezone',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
options: TIMEZONE_OPTIONS,
|
||||
singleSelection: { asPlainText: true },
|
||||
isClearable: false,
|
||||
noSuggestions: false,
|
||||
placeholder: '',
|
||||
prepend: (
|
||||
<EuiFormLabel htmlFor={'timezone'}>
|
||||
{i18n.CREATE_FORM_TIMEZONE}
|
||||
</EuiFormLabel>
|
||||
),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{(fields) => (
|
||||
<DatePickerRangeField
|
||||
fields={fields}
|
||||
timezone={timezone ?? [defaultTimezone]}
|
||||
data-test-subj="date-field"
|
||||
/>
|
||||
)}
|
||||
</UseMultiFields>
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{showTimezone ? (
|
||||
<EuiFlexItem grow={1}>
|
||||
<UseField
|
||||
path="timezone"
|
||||
config={{
|
||||
type: FIELD_TYPES.COMBO_BOX,
|
||||
validations: [],
|
||||
defaultValue: [defaultTimezone],
|
||||
}}
|
||||
componentProps={{
|
||||
'data-test-subj': 'timezone-field',
|
||||
id: 'timezone',
|
||||
euiFieldProps: {
|
||||
fullWidth: true,
|
||||
options: TIMEZONE_OPTIONS,
|
||||
singleSelection: { asPlainText: true },
|
||||
isClearable: false,
|
||||
noSuggestions: false,
|
||||
placeholder: '',
|
||||
prepend: (
|
||||
<EuiFormLabel htmlFor={'timezone'}>
|
||||
{i18n.CREATE_FORM_TIMEZONE}
|
||||
</EuiFormLabel>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="recurring"
|
||||
componentProps={{
|
||||
'data-test-subj': 'recurring-field',
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isRecurring && (
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
path="recurring"
|
||||
componentProps={{
|
||||
'data-test-subj': 'recurring-field',
|
||||
}}
|
||||
/>
|
||||
<RecurringSchedule data-test-subj="recurring-form" />
|
||||
</EuiFlexItem>
|
||||
{isRecurring && (
|
||||
<EuiFlexItem>
|
||||
<RecurringSchedule data-test-subj="recurring-form" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
)}
|
||||
{scopedQueryFeatureFlag && (
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
<UseField path="categoryIds">
|
||||
{(field) => (
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={categoryIds || []}
|
||||
availableCategories={availableCategories}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
errors={field.errors.map((error) => error.message)}
|
||||
onChange={onCategoryIdsChange}
|
||||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuerySwitch
|
||||
checked={isScopedQueryEnabled}
|
||||
onEnabledChange={onScopeQueryToggle}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<EuiCallOut title={i18n.ARCHIVE_TITLE} color="danger" iconType="trash">
|
||||
<p>{i18n.ARCHIVE_SUBTITLE}</p>
|
||||
<EuiButton fill color="danger" onClick={showModal}>
|
||||
{i18n.ARCHIVE}
|
||||
</EuiButton>
|
||||
{modal}
|
||||
</EuiCallOut>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="l"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCancel} size="s" data-test-subj="cancelMaintenanceWindow">
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
<UseField path="categoryIds">
|
||||
{(field) => (
|
||||
<MaintenanceWindowCategorySelection
|
||||
isScopedQueryEnabled={isScopedQueryEnabled}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
selectedCategories={categoryIds || []}
|
||||
availableCategories={availableCategories}
|
||||
errors={field.errors.map((error) => error.message)}
|
||||
onChange={onCategoryIdsChange}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
{scopedQueryFeatureFlag && (
|
||||
<EuiFlexItem>
|
||||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={featureIds}
|
||||
query={query}
|
||||
filters={filters}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
isEnabled={isScopedQueryEnabled}
|
||||
errors={scopedQueryErrors}
|
||||
onQueryChange={onQueryChange}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SubmitButton isLoading={isCreateLoading || isUpdateLoading} editMode={isEditMode} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
);
|
||||
)}
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</EuiFlexGroup>
|
||||
{isEditMode && (
|
||||
<>
|
||||
<EuiCallOut title={i18n.ARCHIVE_TITLE} color="danger" iconType="trash">
|
||||
<p>{i18n.ARCHIVE_SUBTITLE}</p>
|
||||
<EuiButton fill color="danger" onClick={showModal}>
|
||||
{i18n.ARCHIVE}
|
||||
</EuiButton>
|
||||
{modal}
|
||||
</EuiCallOut>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" gutterSize="l" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={onCancel} size="s" data-test-subj="cancelMaintenanceWindow">
|
||||
{i18n.CANCEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SubmitButton isLoading={isCreateLoading || isUpdateLoading} editMode={isEditMode} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
);
|
||||
});
|
||||
|
||||
CreateMaintenanceWindowForm.displayName = 'CreateMaintenanceWindowForm';
|
||||
|
|
|
@ -29,9 +29,9 @@ describe('maintenanceWindowCategorySelection', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkbox-observability')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('checkbox-securitySolution')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('checkbox-management')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('option-observability')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('option-securitySolution')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('option-management')).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should disable options if option is not in the available categories array', () => {
|
||||
|
@ -43,9 +43,9 @@ describe('maintenanceWindowCategorySelection', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkbox-observability')).toBeDisabled();
|
||||
expect(screen.getByTestId('checkbox-securitySolution')).toBeDisabled();
|
||||
expect(screen.getByTestId('checkbox-management')).toBeDisabled();
|
||||
expect(screen.getByTestId('option-observability')).toBeDisabled();
|
||||
expect(screen.getByTestId('option-securitySolution')).toBeDisabled();
|
||||
expect(screen.getByTestId('option-management')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('can initialize checkboxes with initial values from props', async () => {
|
||||
|
@ -57,9 +57,9 @@ describe('maintenanceWindowCategorySelection', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('checkbox-observability')).not.toBeChecked();
|
||||
expect(screen.getByTestId('checkbox-securitySolution')).toBeChecked();
|
||||
expect(screen.getByTestId('checkbox-management')).toBeChecked();
|
||||
expect(screen.getByTestId('option-observability')).not.toBeChecked();
|
||||
expect(screen.getByTestId('option-securitySolution')).toBeChecked();
|
||||
expect(screen.getByTestId('option-management')).toBeChecked();
|
||||
});
|
||||
|
||||
it('can check checkboxes', async () => {
|
||||
|
@ -71,16 +71,16 @@ describe('maintenanceWindowCategorySelection', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const managementCheckbox = screen.getByTestId('checkbox-management');
|
||||
const securityCheckbox = screen.getByTestId('checkbox-securitySolution');
|
||||
const managementCheckbox = screen.getByTestId('option-management');
|
||||
const securityCheckbox = screen.getByTestId('option-securitySolution');
|
||||
|
||||
fireEvent.click(managementCheckbox);
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith('management', expect.anything());
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(['observability', 'management']);
|
||||
|
||||
fireEvent.click(securityCheckbox);
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith('securitySolution', expect.anything());
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(['observability', 'securitySolution']);
|
||||
});
|
||||
|
||||
it('should display loading spinner if isLoading is true', () => {
|
||||
|
@ -106,4 +106,62 @@ describe('maintenanceWindowCategorySelection', () => {
|
|||
);
|
||||
expect(screen.getByText('test error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display radio group if scoped query is enabled', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
isScopedQueryEnabled={false}
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('maintenanceWindowCategorySelectionCheckboxGroup')
|
||||
).toBeInTheDocument();
|
||||
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
isScopedQueryEnabled={true}
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('maintenanceWindowCategorySelectionRadioGroup')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should set only 1 category at a time if scoped query is enabled', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
isScopedQueryEnabled={true}
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
let managementCheckbox = screen.getByLabelText('Stack rules');
|
||||
|
||||
fireEvent.click(managementCheckbox);
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(['management']);
|
||||
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
isScopedQueryEnabled={true}
|
||||
selectedCategories={['observability']}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
managementCheckbox = screen.getByLabelText('Stack rules');
|
||||
|
||||
fireEvent.click(managementCheckbox);
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(['management']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
|
@ -14,36 +14,37 @@ import {
|
|||
EuiFormRow,
|
||||
EuiTextColor,
|
||||
EuiCheckboxGroup,
|
||||
EuiCheckboxGroupOption,
|
||||
EuiRadioGroup,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const CHECKBOX_OPTIONS: EuiCheckboxGroupOption[] = [
|
||||
const CHECKBOX_OPTIONS = [
|
||||
{
|
||||
id: DEFAULT_APP_CATEGORIES.observability.id,
|
||||
label: i18n.CREATE_FORM_CATEGORY_OBSERVABILITY_RULES,
|
||||
['data-test-subj']: `checkbox-${DEFAULT_APP_CATEGORIES.observability.id}`,
|
||||
['data-test-subj']: `option-${DEFAULT_APP_CATEGORIES.observability.id}`,
|
||||
},
|
||||
{
|
||||
id: DEFAULT_APP_CATEGORIES.security.id,
|
||||
label: i18n.CREATE_FORM_CATEGORY_SECURITY_RULES,
|
||||
['data-test-subj']: `checkbox-${DEFAULT_APP_CATEGORIES.security.id}`,
|
||||
['data-test-subj']: `option-${DEFAULT_APP_CATEGORIES.security.id}`,
|
||||
},
|
||||
{
|
||||
id: DEFAULT_APP_CATEGORIES.management.id,
|
||||
label: i18n.CREATE_FORM_CATEGORY_STACK_RULES,
|
||||
['data-test-subj']: `checkbox-${DEFAULT_APP_CATEGORIES.management.id}`,
|
||||
['data-test-subj']: `option-${DEFAULT_APP_CATEGORIES.management.id}`,
|
||||
},
|
||||
];
|
||||
].sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
export interface MaintenanceWindowCategorySelectionProps {
|
||||
selectedCategories: string[];
|
||||
availableCategories: string[];
|
||||
errors?: string[];
|
||||
isLoading?: boolean;
|
||||
onChange: (category: string) => void;
|
||||
isScopedQueryEnabled?: boolean;
|
||||
onChange: (categories: string[]) => void;
|
||||
}
|
||||
|
||||
export const MaintenanceWindowCategorySelection = (
|
||||
|
@ -54,6 +55,7 @@ export const MaintenanceWindowCategorySelection = (
|
|||
availableCategories,
|
||||
errors = [],
|
||||
isLoading = false,
|
||||
isScopedQueryEnabled = false,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
|
@ -64,13 +66,59 @@ export const MaintenanceWindowCategorySelection = (
|
|||
}, {});
|
||||
}, [selectedCategories]);
|
||||
|
||||
const options: EuiCheckboxGroupOption[] = useMemo(() => {
|
||||
const options = useMemo(() => {
|
||||
return CHECKBOX_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
disabled: !availableCategories.includes(option.id),
|
||||
}));
|
||||
})).sort((a, b) => a.id.localeCompare(b.id));
|
||||
}, [availableCategories]);
|
||||
|
||||
const onCheckboxChange = useCallback(
|
||||
(id: string) => {
|
||||
if (selectedCategories.includes(id)) {
|
||||
onChange(selectedCategories.filter((category) => category !== id));
|
||||
} else {
|
||||
onChange([...selectedCategories, id]);
|
||||
}
|
||||
},
|
||||
[selectedCategories, onChange]
|
||||
);
|
||||
|
||||
const onRadioChange = useCallback(
|
||||
(id: string) => {
|
||||
onChange([id]);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const categorySelection = useMemo(() => {
|
||||
if (isScopedQueryEnabled) {
|
||||
return (
|
||||
<EuiRadioGroup
|
||||
data-test-subj="maintenanceWindowCategorySelectionRadioGroup"
|
||||
options={options}
|
||||
idSelected={selectedCategories[0]}
|
||||
onChange={onRadioChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiCheckboxGroup
|
||||
data-test-subj="maintenanceWindowCategorySelectionCheckboxGroup"
|
||||
options={options}
|
||||
idToSelectedMap={selectedMap}
|
||||
onChange={onCheckboxChange}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
isScopedQueryEnabled,
|
||||
options,
|
||||
selectedCategories,
|
||||
selectedMap,
|
||||
onCheckboxChange,
|
||||
onRadioChange,
|
||||
]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
|
@ -102,7 +150,7 @@ export const MaintenanceWindowCategorySelection = (
|
|||
isInvalid={!!errors.length}
|
||||
error={errors[0]}
|
||||
>
|
||||
<EuiCheckboxGroup options={options} idToSelectedMap={selectedMap} onChange={onChange} />
|
||||
{categorySelection}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import type { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
|
||||
import { MaintenanceWindowScopedQuery } from './maintenance_window_scoped_query';
|
||||
|
||||
jest.mock('../../../utils/kibana_react');
|
||||
jest.mock('@kbn/alerts-ui-shared', () => ({
|
||||
AlertsSearchBar: () => <div />,
|
||||
}));
|
||||
|
||||
const { useKibana } = jest.requireMock('../../../utils/kibana_react');
|
||||
|
||||
describe('MaintenanceWindowScopedQuery', () => {
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
useKibana.mockReturnValue({
|
||||
services: {
|
||||
notifications: {
|
||||
toasts: {
|
||||
addSuccess: jest.fn(),
|
||||
addDanger: jest.fn(),
|
||||
},
|
||||
},
|
||||
data: {
|
||||
dataViews: {},
|
||||
},
|
||||
unifiedSearch: {
|
||||
ui: {
|
||||
SearchBar: <div />,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={['observability', 'management', 'securitySolution'] as AlertConsumers[]}
|
||||
query={''}
|
||||
filters={[]}
|
||||
onQueryChange={jest.fn()}
|
||||
onFiltersChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('maintenanceWindowScopeQuery')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should hide the search bar if isEnabled is false', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={['observability', 'management', 'securitySolution'] as AlertConsumers[]}
|
||||
isEnabled={false}
|
||||
query={''}
|
||||
filters={[]}
|
||||
onQueryChange={jest.fn()}
|
||||
onFiltersChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId('maintenanceWindowScopeQuery')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading if isLoading is true', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={['observability', 'management', 'securitySolution'] as AlertConsumers[]}
|
||||
isLoading={true}
|
||||
query={''}
|
||||
filters={[]}
|
||||
onQueryChange={jest.fn()}
|
||||
onFiltersChange={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('maintenanceWindowScopedQueryLoading')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { AlertsSearchBar } from '@kbn/alerts-ui-shared';
|
||||
import { PLUGIN } from '../../../../common/constants/plugin';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export interface MaintenanceWindowScopedQueryProps {
|
||||
featureIds: AlertConsumers[];
|
||||
query: string;
|
||||
filters: Filter[];
|
||||
errors?: string[];
|
||||
isLoading?: boolean;
|
||||
isEnabled?: boolean;
|
||||
onQueryChange: (query: string) => void;
|
||||
onFiltersChange: (filters: Filter[]) => void;
|
||||
}
|
||||
|
||||
export const MaintenanceWindowScopedQuery = React.memo(
|
||||
(props: MaintenanceWindowScopedQueryProps) => {
|
||||
const {
|
||||
featureIds,
|
||||
query,
|
||||
filters,
|
||||
errors = [],
|
||||
isLoading,
|
||||
isEnabled = true,
|
||||
onQueryChange,
|
||||
onFiltersChange,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
http,
|
||||
data,
|
||||
notifications: { toasts },
|
||||
unifiedSearch: {
|
||||
ui: { SearchBar },
|
||||
},
|
||||
} = useKibana().services;
|
||||
|
||||
const onQueryChangeInternal = useCallback(
|
||||
({ query: newQuery }: { query?: string }) => {
|
||||
onQueryChange(newQuery || '');
|
||||
},
|
||||
[onQueryChange]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
data-test-subj="maintenanceWindowScopedQueryLoading"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="maintenanceWindowScopeQuery" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow fullWidth isInvalid={errors.length !== 0} error={errors[0]}>
|
||||
<AlertsSearchBar
|
||||
appName={PLUGIN.getI18nName(i18n)}
|
||||
featureIds={featureIds}
|
||||
disableQueryLanguageSwitcher={true}
|
||||
query={query}
|
||||
filters={filters}
|
||||
onQueryChange={onQueryChangeInternal}
|
||||
onQuerySubmit={onQueryChangeInternal}
|
||||
onFiltersUpdated={onFiltersChange}
|
||||
showFilterBar
|
||||
submitOnBlur
|
||||
showDatePicker={false}
|
||||
showSubmitButton={false}
|
||||
http={http}
|
||||
toasts={toasts}
|
||||
unifiedSearchBar={SearchBar}
|
||||
dataViewsService={data.dataViews}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, fireEvent, render } from '@testing-library/react';
|
||||
import { MaintenanceWindowScopedQuerySwitch } from './maintenance_window_scoped_query_switch';
|
||||
|
||||
const mockOnEnabledChange = jest.fn();
|
||||
|
||||
describe('MaintenanceWindowScopedQuerySwitch', () => {
|
||||
it('renders correctly', () => {
|
||||
render(
|
||||
<MaintenanceWindowScopedQuerySwitch checked={true} onEnabledChange={mockOnEnabledChange} />
|
||||
);
|
||||
expect(screen.getByTestId('maintenanceWindowScopedQuerySwitch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onChange when switch is clicked', () => {
|
||||
render(
|
||||
<MaintenanceWindowScopedQuerySwitch checked={true} onEnabledChange={mockOnEnabledChange} />
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('switch'));
|
||||
expect(mockOnEnabledChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiText,
|
||||
EuiFlexItem,
|
||||
EuiTextColor,
|
||||
EuiSwitch,
|
||||
EuiSwitchEvent,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
interface MaintenanceWindowScopedQuerySwitchProps {
|
||||
checked: boolean;
|
||||
onEnabledChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export const MaintenanceWindowScopedQuerySwitch = (
|
||||
props: MaintenanceWindowScopedQuerySwitchProps
|
||||
) => {
|
||||
const { checked, onEnabledChange } = props;
|
||||
|
||||
const onEnabledChangeInternal = useCallback(
|
||||
(event: EuiSwitchEvent) => {
|
||||
onEnabledChange(event.target.checked);
|
||||
},
|
||||
[onEnabledChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="maintenanceWindowScopedQuerySwitch" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_SCOPED_QUERY_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">{i18n.CREATE_FORM_SCOPED_QUERY_DESCRIPTION}</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiSwitch
|
||||
label={i18n.CREATE_FORM_SCOPED_QUERY_TOGGLE_TITLE}
|
||||
checked={checked}
|
||||
onChange={onEnabledChangeInternal}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -8,10 +8,10 @@
|
|||
import type { FormSchema } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { FIELD_TYPES } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import { fieldValidators } from '@kbn/es-ui-shared-plugin/static/forms/helpers';
|
||||
|
||||
import { Frequency } from '@kbn/rrule';
|
||||
import * as i18n from '../translations';
|
||||
import { EndsOptions, MaintenanceWindowFrequency } from '../constants';
|
||||
import { ScopedQueryAttributes } from '../../../../common';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
|
@ -23,6 +23,7 @@ export interface FormProps {
|
|||
recurring: boolean;
|
||||
recurringSchedule?: RecurringScheduleFormProps;
|
||||
categoryIds?: string[];
|
||||
scopedQuery?: ScopedQueryAttributes | null;
|
||||
}
|
||||
|
||||
export interface RecurringScheduleFormProps {
|
||||
|
@ -53,6 +54,12 @@ export const schema: FormSchema<FormProps> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
scopedQuery: {
|
||||
defaultValue: {
|
||||
kql: '',
|
||||
filters: [],
|
||||
},
|
||||
},
|
||||
startDate: {},
|
||||
endDate: {},
|
||||
timezone: {},
|
||||
|
|
|
@ -123,3 +123,5 @@ export const STATUS_OPTIONS = [
|
|||
{ value: MaintenanceWindowStatus.Finished, name: i18n.TABLE_STATUS_FINISHED },
|
||||
{ value: MaintenanceWindowStatus.Archived, name: i18n.TABLE_STATUS_ARCHIVED },
|
||||
];
|
||||
|
||||
export const IS_SCOPED_QUERY_ENABLED = true;
|
||||
|
|
|
@ -28,6 +28,7 @@ export const convertFromMaintenanceWindowToForm = (
|
|||
timezone: [maintenanceWindow.rRule.tzid],
|
||||
recurring,
|
||||
categoryIds: maintenanceWindow.categoryIds || [],
|
||||
scopedQuery: maintenanceWindow.scopedQuery,
|
||||
};
|
||||
if (!recurring) return form;
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as i18n from './translations';
|
|||
import { PageHeader } from './components/page_header';
|
||||
import { CreateMaintenanceWindowForm } from './components/create_maintenance_windows_form';
|
||||
import { MAINTENANCE_WINDOW_DEEP_LINK_IDS } from '../../../common';
|
||||
import { IS_SCOPED_QUERY_ENABLED } from './constants';
|
||||
|
||||
export const MaintenanceWindowsCreatePage = React.memo(() => {
|
||||
useBreadcrumbs(MAINTENANCE_WINDOW_DEEP_LINK_IDS.maintenanceWindowsCreate);
|
||||
|
@ -30,6 +31,7 @@ export const MaintenanceWindowsCreatePage = React.memo(() => {
|
|||
<CreateMaintenanceWindowForm
|
||||
onCancel={navigateToMaintenanceWindows}
|
||||
onSuccess={navigateToMaintenanceWindows}
|
||||
scopedQueryFeatureFlag={IS_SCOPED_QUERY_ENABLED}
|
||||
/>
|
||||
</EuiPageSection>
|
||||
);
|
||||
|
|
|
@ -17,6 +17,7 @@ import { CreateMaintenanceWindowForm } from './components/create_maintenance_win
|
|||
import { MAINTENANCE_WINDOW_DEEP_LINK_IDS } from '../../../common';
|
||||
import { useGetMaintenanceWindow } from '../../hooks/use_get_maintenance_window';
|
||||
import { CenterJustifiedSpinner } from './components/center_justified_spinner';
|
||||
import { IS_SCOPED_QUERY_ENABLED } from './constants';
|
||||
|
||||
export const MaintenanceWindowsEditPage = React.memo(() => {
|
||||
const { navigateToMaintenanceWindows } = useMaintenanceWindowsNavigation();
|
||||
|
@ -43,6 +44,7 @@ export const MaintenanceWindowsEditPage = React.memo(() => {
|
|||
maintenanceWindowId={maintenanceWindowId}
|
||||
onCancel={navigateToMaintenanceWindows}
|
||||
onSuccess={navigateToMaintenanceWindows}
|
||||
scopedQueryFeatureFlag={IS_SCOPED_QUERY_ENABLED}
|
||||
/>
|
||||
</EuiPageSection>
|
||||
);
|
||||
|
|
|
@ -208,6 +208,42 @@ export const CREATE_FORM_CATEGORY_STACK_RULES = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_SCOPED_QUERY_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.scopedQuery.title',
|
||||
{
|
||||
defaultMessage: 'Define scope of rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_SCOPED_QUERY_DESCRIPTION = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.scopedQuery.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Use KQL to further narrow down the alerts affected by this maintenance window.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_SCOPED_QUERY_TOGGLE_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.scopedQuery.toggleTitle',
|
||||
{
|
||||
defaultMessage: 'Filter alerts',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_SCOPED_QUERY_INVALID_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.scopedQuery.invalidErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Invalid scoped query.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_SCOPED_QUERY_EMPTY_ERROR_MESSAGE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.scopedQuery.emptyErrorMessage',
|
||||
{
|
||||
defaultMessage: 'Scoped query is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_FREQUENCY_WEEKLY_ON = (dayOfWeek: string) =>
|
||||
i18n.translate('xpack.alerting.maintenanceWindows.createForm.frequency.weeklyOnWeekday', {
|
||||
defaultMessage: 'Weekly on {dayOfWeek}',
|
||||
|
|
|
@ -20,7 +20,7 @@ export const RRuleFrequencyMap = {
|
|||
|
||||
export type MaintenanceWindow = Pick<
|
||||
MaintenanceWindowServerSide,
|
||||
'title' | 'duration' | 'rRule' | 'categoryIds'
|
||||
'title' | 'duration' | 'rRule' | 'categoryIds' | 'scopedQuery'
|
||||
>;
|
||||
|
||||
export type MaintenanceWindowFindResponse = MaintenanceWindowServerSide &
|
||||
|
|
|
@ -10,6 +10,8 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/cor
|
|||
import { ManagementAppMountParams, ManagementSetup } from '@kbn/management-plugin/public';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/public';
|
||||
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
|
||||
import { AlertNavigationRegistry, AlertNavigationHandler } from './alert_navigation_registry';
|
||||
import { loadRule, loadRuleType } from './services/alert_api';
|
||||
|
@ -63,6 +65,8 @@ export interface AlertingPluginSetup {
|
|||
export interface AlertingPluginStart {
|
||||
licensing: LicensingPluginStart;
|
||||
spaces: SpacesPluginStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
export class AlertingPublicPlugin
|
||||
|
|
|
@ -13,21 +13,25 @@ import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common';
|
|||
const rewriteBodyRequest: RewriteResponseCase<MaintenanceWindow> = ({
|
||||
rRule,
|
||||
categoryIds,
|
||||
scopedQuery,
|
||||
...res
|
||||
}) => ({
|
||||
...res,
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
scoped_query: scopedQuery,
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
scoped_query: scopedQuery,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
categoryIds,
|
||||
scopedQuery,
|
||||
});
|
||||
|
||||
export async function createMaintenanceWindow({
|
||||
|
|
|
@ -13,9 +13,11 @@ import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common';
|
|||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
scoped_query: scopedQuery,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
scopedQuery,
|
||||
categoryIds,
|
||||
rRule,
|
||||
});
|
||||
|
|
|
@ -13,21 +13,25 @@ import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common';
|
|||
const rewriteBodyRequest: RewriteResponseCase<MaintenanceWindow> = ({
|
||||
rRule,
|
||||
categoryIds,
|
||||
scopedQuery,
|
||||
...res
|
||||
}) => ({
|
||||
...res,
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
scoped_query: scopedQuery,
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
scoped_query: scopedQuery,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
categoryIds,
|
||||
scopedQuery,
|
||||
});
|
||||
|
||||
export async function updateMaintenanceWindow({
|
||||
|
|
|
@ -151,7 +151,7 @@ describe('MaintenanceWindowClient - create', () => {
|
|||
title: mockMaintenanceWindow.title,
|
||||
duration: mockMaintenanceWindow.duration,
|
||||
rRule: mockMaintenanceWindow.rRule as CreateMaintenanceWindowParams['data']['rRule'],
|
||||
categoryIds: ['observability', 'securitySolution'],
|
||||
categoryIds: ['securitySolution'],
|
||||
scopedQuery: {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
|
@ -189,7 +189,7 @@ describe('MaintenanceWindowClient - create', () => {
|
|||
rRule: mockMaintenanceWindow.rRule,
|
||||
enabled: true,
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
categoryIds: ['observability', 'securitySolution'],
|
||||
categoryIds: ['securitySolution'],
|
||||
...updatedMetadata,
|
||||
}),
|
||||
{
|
||||
|
@ -245,12 +245,59 @@ describe('MaintenanceWindowClient - create', () => {
|
|||
},
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Error validating create maintenance scoped query - Expected \\"(\\", \\"{\\", value, whitespace but end of input found.
|
||||
"Error validating create maintenance window data - invalid scoped query - Expected \\"(\\", \\"{\\", value, whitespace but end of input found.
|
||||
invalid:
|
||||
---------^"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw if trying to create a MW with a scoped query with other than 1 category ID', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
await expect(async () => {
|
||||
await createMaintenanceWindow(mockContext, {
|
||||
data: {
|
||||
title: mockMaintenanceWindow.title,
|
||||
duration: mockMaintenanceWindow.duration,
|
||||
rRule: mockMaintenanceWindow.rRule as CreateMaintenanceWindowParams['data']['rRule'],
|
||||
categoryIds: ['observability', 'securitySolution'],
|
||||
scopedQuery: {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'kibana.alert.action_group',
|
||||
field: 'kibana.alert.action_group',
|
||||
params: {
|
||||
query: 'test',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error validating create maintenance window data - scoped query must be accompanied by 1 category ID"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if trying to create a maintenance window with invalid category ids', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z'));
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { SavedObjectsUtils } from '@kbn/core/server';
|
|||
import { buildEsQuery, Filter } from '@kbn/es-query';
|
||||
import { generateMaintenanceWindowEvents } from '../../lib/generate_maintenance_window_events';
|
||||
import type { MaintenanceWindowClientContext } from '../../../../../common';
|
||||
import { getScopedQueryErrorMessage } from '../../../../../common';
|
||||
import type { MaintenanceWindow } from '../../types';
|
||||
import type { CreateMaintenanceWindowParams } from './types';
|
||||
import {
|
||||
|
@ -50,7 +51,19 @@ export async function createMaintenanceWindow(
|
|||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error validating create maintenance scoped query - ${error.message}`);
|
||||
throw Boom.badRequest(
|
||||
`Error validating create maintenance window data - ${getScopedQueryErrorMessage(
|
||||
error.message
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (scopedQueryWithGeneratedValue) {
|
||||
if (data.categoryIds?.length !== 1) {
|
||||
throw Boom.badRequest(
|
||||
`Error validating create maintenance window data - scoped query must be accompanied by 1 category ID`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const id = SavedObjectsUtils.generateId();
|
||||
|
|
|
@ -223,6 +223,7 @@ describe('MaintenanceWindowClient - update', () => {
|
|||
} as MaintenanceWindow['rRule'],
|
||||
events: modifiedEvents,
|
||||
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').add(2, 'week').toISOString(),
|
||||
categoryIds: ['observability'],
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValue({
|
||||
|
@ -367,12 +368,62 @@ describe('MaintenanceWindowClient - update', () => {
|
|||
},
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Error validating update maintenance scoped query - Expected \\"(\\", \\"{\\", value, whitespace but end of input found.
|
||||
"Error validating update maintenance window data - invalid scoped query - Expected \\"(\\", \\"{\\", value, whitespace but end of input found.
|
||||
invalid:
|
||||
---------^"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw if trying to update a MW with a scoped query with other than 1 category ID', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
expirationDate: moment(new Date(firstTimestamp)).tz('UTC').subtract(1, 'year').toISOString(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
attributes: mockMaintenanceWindow,
|
||||
version: '123',
|
||||
id: 'test-id',
|
||||
categoryIds: ['observability', 'securitySolution'],
|
||||
} as unknown as SavedObject);
|
||||
|
||||
await expect(async () => {
|
||||
await updateMaintenanceWindow(mockContext, {
|
||||
id: 'test-id',
|
||||
data: {
|
||||
scopedQuery: {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'kibana.alert.action_group',
|
||||
field: 'kibana.alert.action_group',
|
||||
params: {
|
||||
query: 'test',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Failed to update maintenance window by id: test-id, Error: Error: Cannot edit archived maintenance windows: Cannot edit archived maintenance windows"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if updating a maintenance window that has expired', async () => {
|
||||
jest.useFakeTimers().setSystemTime(new Date(firstTimestamp));
|
||||
const mockMaintenanceWindow = getMockMaintenanceWindow({
|
||||
|
|
|
@ -9,6 +9,7 @@ import moment from 'moment';
|
|||
import Boom from '@hapi/boom';
|
||||
import { buildEsQuery, Filter } from '@kbn/es-query';
|
||||
import type { MaintenanceWindowClientContext } from '../../../../../common';
|
||||
import { getScopedQueryErrorMessage } from '../../../../../common';
|
||||
import type { MaintenanceWindow } from '../../types';
|
||||
import {
|
||||
generateMaintenanceWindowEvents,
|
||||
|
@ -70,7 +71,11 @@ async function updateWithOCC(
|
|||
};
|
||||
}
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error validating update maintenance scoped query - ${error.message}`);
|
||||
throw Boom.badRequest(
|
||||
`Error validating update maintenance window data - ${getScopedQueryErrorMessage(
|
||||
error.message
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -119,6 +124,14 @@ async function updateWithOCC(
|
|||
updatedAt: modificationMetadata.updatedAt,
|
||||
});
|
||||
|
||||
if (updateMaintenanceWindowAttributes.scopedQuery) {
|
||||
if (updateMaintenanceWindowAttributes.categoryIds?.length !== 1) {
|
||||
throw Boom.badRequest(
|
||||
`Error validating update maintenance window data - scoped query must be accompanied by 1 category ID`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We are deleting and then creating rather than updating because SO.update
|
||||
// performs a partial update on the rRule, we would need to null out all of the fields
|
||||
// that are removed from a new rRule if that were the case.
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
"@kbn/share-plugin",
|
||||
"@kbn/safer-lodash-set",
|
||||
"@kbn/alerting-state-types",
|
||||
"@kbn/alerting-types",
|
||||
"@kbn/alerts-as-data-utils",
|
||||
"@kbn/core-elasticsearch-client-server-mocks",
|
||||
"@kbn/rrule",
|
||||
|
@ -60,7 +61,9 @@
|
|||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/core-application-common",
|
||||
"@kbn/core-saved-objects-api-server"
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/alerts-ui-shared",
|
||||
"@kbn/core-http-browser"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -8027,7 +8027,6 @@
|
|||
"xpack.alerting.breadcrumbs.editMaintenanceWindowsLinkText": "Modifier",
|
||||
"xpack.alerting.breadcrumbs.maintenanceWindowsLinkText": "Fenêtres de maintenance",
|
||||
"xpack.alerting.breadcrumbs.stackManagementLinkText": "Gestion de la Suite",
|
||||
"xpack.alerting.builtinActionGroups.recovered": "Récupéré",
|
||||
"xpack.alerting.feature.flappingSettingsSubFeatureName": "Détection de bagotement",
|
||||
"xpack.alerting.feature.maintenanceWindowFeatureName": "Fenêtres de maintenance",
|
||||
"xpack.alerting.feature.rulesSettingsFeatureName": "Paramètres des règles",
|
||||
|
@ -41499,7 +41498,6 @@
|
|||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "Impossible de charger les règles",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "Impossible de charger les infos de statut de règles",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags": "Impossible de charger les balises de règle",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "Impossible de charger les types de règles",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToRunRuleSoon": "Impossible de programmer l'exécution de votre règle",
|
||||
"xpack.triggersActionsUI.sections.rulesList.weeksLabel": "semaines",
|
||||
"xpack.triggersActionsUI.sections.ruleTagFilter.loading": "Chargement des balises",
|
||||
|
|
|
@ -8042,7 +8042,6 @@
|
|||
"xpack.alerting.breadcrumbs.editMaintenanceWindowsLinkText": "編集",
|
||||
"xpack.alerting.breadcrumbs.maintenanceWindowsLinkText": "保守時間枠",
|
||||
"xpack.alerting.breadcrumbs.stackManagementLinkText": "スタック管理",
|
||||
"xpack.alerting.builtinActionGroups.recovered": "回復済み",
|
||||
"xpack.alerting.feature.flappingSettingsSubFeatureName": "フラップ検出",
|
||||
"xpack.alerting.feature.maintenanceWindowFeatureName": "保守時間枠",
|
||||
"xpack.alerting.feature.rulesSettingsFeatureName": "ルール設定",
|
||||
|
@ -41490,7 +41489,6 @@
|
|||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "ルールを読み込めません",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "ルールステータス情報を読み込めません",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags": "ルールタグを読み込めません",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToRunRuleSoon": "ルールの実行をスケジュールできません",
|
||||
"xpack.triggersActionsUI.sections.rulesList.weeksLabel": "週",
|
||||
"xpack.triggersActionsUI.sections.ruleTagFilter.loading": "タグを読み込み中",
|
||||
|
|
|
@ -8041,7 +8041,6 @@
|
|||
"xpack.alerting.breadcrumbs.editMaintenanceWindowsLinkText": "编辑",
|
||||
"xpack.alerting.breadcrumbs.maintenanceWindowsLinkText": "维护窗口",
|
||||
"xpack.alerting.breadcrumbs.stackManagementLinkText": "Stack Management",
|
||||
"xpack.alerting.builtinActionGroups.recovered": "已恢复",
|
||||
"xpack.alerting.feature.flappingSettingsSubFeatureName": "摆动检测",
|
||||
"xpack.alerting.feature.maintenanceWindowFeatureName": "维护窗口",
|
||||
"xpack.alerting.feature.rulesSettingsFeatureName": "规则设置",
|
||||
|
@ -41483,7 +41482,6 @@
|
|||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRulesMessage": "无法加载规则",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleStatusInfoMessage": "无法加载规则状态信息",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags": "无法加载规则标签",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTypesMessage": "无法加载规则类型",
|
||||
"xpack.triggersActionsUI.sections.rulesList.unableToRunRuleSoon": "无法计划您的要运行的规则",
|
||||
"xpack.triggersActionsUI.sections.rulesList.weeksLabel": "周",
|
||||
"xpack.triggersActionsUI.sections.ruleTagFilter.loading": "正在加载标签",
|
||||
|
|
|
@ -21,6 +21,7 @@ import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query';
|
|||
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
|
||||
|
||||
// TODO Share buildEsQuery to be used between AlertsSearchBar and AlertsStateTable component https://github.com/elastic/kibana/issues/144615
|
||||
// Also TODO: Replace all references to this component with the one from alerts-ui-shared
|
||||
export function AlertsSearchBar({
|
||||
appName,
|
||||
disableQueryLanguageSwitcher = false,
|
||||
|
|
|
@ -14,6 +14,7 @@ export type {
|
|||
RuleAction,
|
||||
Rule,
|
||||
RuleType,
|
||||
RuleTypeIndex,
|
||||
RuleTypeModel,
|
||||
RuleStatusFilterProps,
|
||||
RuleStatus,
|
||||
|
|
|
@ -52,7 +52,6 @@ import {
|
|||
RuleNotifyWhenType,
|
||||
RuleTypeParams,
|
||||
ActionVariable,
|
||||
RuleType as CommonRuleType,
|
||||
RuleLastRun,
|
||||
MaintenanceWindow,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
|
@ -65,6 +64,7 @@ import {
|
|||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import React from 'react';
|
||||
import { ActionsPublicPluginSetup } from '@kbn/actions-plugin/public';
|
||||
import type { RuleType, RuleTypeIndex } from '@kbn/triggers-actions-ui-types';
|
||||
import { TypeRegistry } from './application/type_registry';
|
||||
import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown';
|
||||
import type { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter';
|
||||
|
@ -95,6 +95,14 @@ import type { RulesListNotifyBadgePropsWithApi } from './application/sections/ru
|
|||
import { Case } from './application/sections/alerts_table/hooks/apis/bulk_get_cases';
|
||||
import { AlertTableConfigRegistry } from './application/alert_table_config_registry';
|
||||
|
||||
export type { ActionVariables, RuleType, RuleTypeIndex } from '@kbn/triggers-actions-ui-types';
|
||||
|
||||
export {
|
||||
REQUIRED_ACTION_VARIABLES,
|
||||
CONTEXT_ACTION_VARIABLES,
|
||||
OPTIONAL_ACTION_VARIABLES,
|
||||
} from '@kbn/triggers-actions-ui-types';
|
||||
|
||||
// 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<
|
||||
|
@ -151,7 +159,6 @@ export {
|
|||
};
|
||||
|
||||
export type ActionTypeIndex = Record<string, ActionType>;
|
||||
export type RuleTypeIndex = Map<string, RuleType>;
|
||||
export type ActionTypeRegistryContract<
|
||||
ActionConnector = unknown,
|
||||
ActionParams = unknown
|
||||
|
@ -329,38 +336,6 @@ export type ActionConnectorTableItem = ActionConnector & {
|
|||
compatibility: string[];
|
||||
};
|
||||
|
||||
type AsActionVariables<Keys extends string> = {
|
||||
[Req in Keys]: ActionVariable[];
|
||||
};
|
||||
export const REQUIRED_ACTION_VARIABLES = ['params'] as const;
|
||||
export const CONTEXT_ACTION_VARIABLES = ['context'] as const;
|
||||
export const OPTIONAL_ACTION_VARIABLES = [...CONTEXT_ACTION_VARIABLES, 'state'] as const;
|
||||
export type ActionVariables = AsActionVariables<typeof REQUIRED_ACTION_VARIABLES[number]> &
|
||||
Partial<AsActionVariables<typeof OPTIONAL_ACTION_VARIABLES[number]>>;
|
||||
|
||||
export interface RuleType<
|
||||
ActionGroupIds extends string = string,
|
||||
RecoveryActionGroupId extends string = string
|
||||
> extends Pick<
|
||||
CommonRuleType<ActionGroupIds, RecoveryActionGroupId>,
|
||||
| 'id'
|
||||
| 'name'
|
||||
| 'actionGroups'
|
||||
| 'producer'
|
||||
| 'minimumLicenseRequired'
|
||||
| 'recoveryActionGroup'
|
||||
| 'defaultActionGroupId'
|
||||
| 'ruleTaskTimeout'
|
||||
| 'defaultScheduleInterval'
|
||||
| 'doesSetRecoveryContext'
|
||||
> {
|
||||
actionVariables: ActionVariables;
|
||||
authorizedConsumers: Record<string, { read: boolean; all: boolean }>;
|
||||
enabledInLicense: boolean;
|
||||
hasFieldsForAAD?: boolean;
|
||||
hasAlertsMappings?: boolean;
|
||||
}
|
||||
|
||||
export type SanitizedRuleType = Omit<RuleType, 'apiKey'>;
|
||||
|
||||
export type RuleUpdates = Omit<Rule, 'id' | 'executionStatus' | 'lastRun' | 'nextRun'>;
|
||||
|
|
|
@ -56,7 +56,8 @@
|
|||
"@kbn/licensing-plugin",
|
||||
"@kbn/expressions-plugin",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/serverless"
|
||||
"@kbn/serverless",
|
||||
"@kbn/triggers-actions-ui-types"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -10,6 +10,33 @@ import { UserAtSpaceScenarios } from '../../../scenarios';
|
|||
import { getUrlPrefix, ObjectRemover } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
const scopedQuery = {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'kibana.alert.action_group',
|
||||
field: 'kibana.alert.action_group',
|
||||
params: {
|
||||
query: 'test',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -25,32 +52,7 @@ export default function createMaintenanceWindowTests({ getService }: FtrProvider
|
|||
tzid: 'UTC',
|
||||
freq: 2, // weekly
|
||||
},
|
||||
scoped_query: {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'kibana.alert.action_group',
|
||||
field: 'kibana.alert.action_group',
|
||||
params: {
|
||||
query: 'test',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
category_ids: ['management'],
|
||||
};
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
|
@ -62,7 +64,10 @@ export default function createMaintenanceWindowTests({ getService }: FtrProvider
|
|||
.post(`${getUrlPrefix(space.id)}/internal/alerting/rules/maintenance_window`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send(createParams);
|
||||
.send({
|
||||
...createParams,
|
||||
scoped_query: scopedQuery,
|
||||
});
|
||||
|
||||
if (response.body.id) {
|
||||
objectRemover.add(
|
||||
|
|
|
@ -11,38 +11,38 @@ import { UserAtSpaceScenarios } from '../../../scenarios';
|
|||
import { getUrlPrefix, ObjectRemover } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
const scopedQuery = {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'kibana.alert.action_group',
|
||||
field: 'kibana.alert.action_group',
|
||||
params: {
|
||||
query: 'test',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function updateMaintenanceWindowTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
const scopedQuery = {
|
||||
kql: "_id: '1234'",
|
||||
filters: [
|
||||
{
|
||||
meta: {
|
||||
disabled: false,
|
||||
negate: false,
|
||||
alias: null,
|
||||
key: 'kibana.alert.action_group',
|
||||
field: 'kibana.alert.action_group',
|
||||
params: {
|
||||
query: 'test',
|
||||
},
|
||||
type: 'phrase',
|
||||
},
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'kibana.alert.action_group': 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('updateMaintenanceWindow', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
const createParams = {
|
||||
|
@ -53,6 +53,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
|
|||
tzid: 'UTC',
|
||||
freq: 2, // weekly
|
||||
},
|
||||
category_ids: ['management'],
|
||||
scoped_query: scopedQuery,
|
||||
};
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
@ -276,6 +277,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider
|
|||
freq: 2, // weekly
|
||||
count: 1,
|
||||
},
|
||||
category_ids: ['management'],
|
||||
scoped_query: scopedQuery,
|
||||
})
|
||||
.expect(200);
|
||||
|
|
12
yarn.lock
12
yarn.lock
|
@ -2959,6 +2959,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/actions-types@link:packages/kbn-actions-types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/advanced-settings-plugin@link:src/plugins/advanced_settings":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -2999,6 +3003,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/alerting-types@link:packages/kbn-alerting-types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/alerts-as-data-utils@link:packages/kbn-alerts-as-data-utils":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
@ -6087,6 +6095,10 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/triggers-actions-ui-types@link:packages/kbn-triggers-actions-ui-types":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/ts-projects@link:packages/kbn-ts-projects":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue