mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[ResponseOps][MW] Remove mw category selection from UI (#211793)
Fix: https://github.com/elastic/kibana/issues/197530 Fix: https://github.com/elastic/kibana/issues/212857 ## Summary I did from DOD: - Remove the category selection from the UI when creating a MW. - Show the section only if Filter alerts is ON. - The terminology should change from category to Solution. - Show a warning callout to users when editing a MW if they have configured the categories and inform them that if upon saving the category configuration will be removed What'll be covered in follow up PR: - Show only two solutions, O11y and Security. O11y will also include Stack. ### Checklist Check the PR satisfies following conditions. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [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 - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed
This commit is contained in:
parent
f5cebe2c23
commit
8aa7d8b0a1
20 changed files with 678 additions and 462 deletions
|
@ -10655,13 +10655,6 @@
|
|||
"xpack.alerting.maintenanceWindows.create.maintenanceWindow": "Créer la fenêtre de maintenance",
|
||||
"xpack.alerting.maintenanceWindows.createForm.byweekdayFieldRequiredError": "Un jour ouvré est nécessaire.",
|
||||
"xpack.alerting.maintenanceWindows.createForm.cancel": "Annuler",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoriesSelection.description": "Seules les règles associées avec les catégories sélectionnées sont concernées par la fenêtre de maintenance.",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoriesSelection.title": "Fenêtre de maintenance spécifique à la catégorie",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.observabilityRules": "Règles d'Observability",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.required": "Une catégorie est requise.",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.securityRules": "Règles de sécurité",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.stackRules": "Règles de la Suite Elastic",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categorySelection.checkboxGroupTitle": "Sélectionner les catégories qui devraient être concernées",
|
||||
"xpack.alerting.maintenanceWindows.createForm.count.after": "Après",
|
||||
"xpack.alerting.maintenanceWindows.createForm.count.occurrence": "occurrence",
|
||||
"xpack.alerting.maintenanceWindows.createForm.countFieldRequiredError": "Un nombre est requis.",
|
||||
|
|
|
@ -10646,13 +10646,6 @@
|
|||
"xpack.alerting.maintenanceWindows.create.maintenanceWindow": "保守時間枠を作成",
|
||||
"xpack.alerting.maintenanceWindows.createForm.byweekdayFieldRequiredError": "曜日は必須です。",
|
||||
"xpack.alerting.maintenanceWindows.createForm.cancel": "キャンセル",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoriesSelection.description": "選択したカテゴリーに関連するルールのみがメンテナンスウィンドウの影響を受けます。",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoriesSelection.title": "カテゴリー固有のメンテナンスウィンドウ",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.observabilityRules": "オブザーバビリティルール",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.required": "カテゴリーは必須です。",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.securityRules": "セキュリティルール",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.stackRules": "スタックルール",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categorySelection.checkboxGroupTitle": "これが影響するカテゴリーを選択",
|
||||
"xpack.alerting.maintenanceWindows.createForm.count.after": "後",
|
||||
"xpack.alerting.maintenanceWindows.createForm.count.occurrence": "発生",
|
||||
"xpack.alerting.maintenanceWindows.createForm.countFieldRequiredError": "カウントが必要です。",
|
||||
|
|
|
@ -10663,13 +10663,6 @@
|
|||
"xpack.alerting.maintenanceWindows.create.maintenanceWindow": "创建维护窗口",
|
||||
"xpack.alerting.maintenanceWindows.createForm.byweekdayFieldRequiredError": "“工作日”必填。",
|
||||
"xpack.alerting.maintenanceWindows.createForm.cancel": "取消",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoriesSelection.description": "仅与选定类别关联的规则受维护窗口影响。",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoriesSelection.title": "特定于类别的维护窗口",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.observabilityRules": "Observability 规则",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.required": "“类别”必填。",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.securityRules": "安全规则",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categoryIds.stackRules": "Stack 规则",
|
||||
"xpack.alerting.maintenanceWindows.createForm.categorySelection.checkboxGroupTitle": "选择此项应影响的类别",
|
||||
"xpack.alerting.maintenanceWindows.createForm.count.after": "之后",
|
||||
"xpack.alerting.maintenanceWindows.createForm.count.occurrence": "发生",
|
||||
"xpack.alerting.maintenanceWindows.createForm.countFieldRequiredError": "“计数”必填。",
|
||||
|
|
|
@ -31,6 +31,9 @@ jest.mock('../utils/kibana_react', () => {
|
|||
jest.mock('../services/maintenance_windows_api/get', () => ({
|
||||
getMaintenanceWindow: jest.fn(),
|
||||
}));
|
||||
jest.mock('../pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form', () => ({
|
||||
convertFromMaintenanceWindowToForm: jest.fn(),
|
||||
}));
|
||||
|
||||
const { getMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/get');
|
||||
|
||||
|
@ -52,4 +55,64 @@ describe('useGetMaintenanceWindow', () => {
|
|||
|
||||
await waitFor(() => expect(mockAddDanger).toBeCalledWith('Unable to get maintenance window.'));
|
||||
});
|
||||
|
||||
it('should return an object where showMultipleSolutionsWarning is false when disabled scoped query filter', async () => {
|
||||
getMaintenanceWindow.mockResolvedValue({
|
||||
categoryIds: ['observability', 'management', 'securitySolution'],
|
||||
scopedQuery: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetMaintenanceWindow('testId'), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toEqual({
|
||||
showMultipleSolutionsWarning: false,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
maintenanceWindow: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return object with a hasOldChosenSolutions is true when disabled scoped query filter', async () => {
|
||||
getMaintenanceWindow.mockResolvedValue({
|
||||
categoryIds: ['observability', 'management'],
|
||||
scopedQuery: undefined,
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetMaintenanceWindow('testId'), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toEqual({
|
||||
showMultipleSolutionsWarning: true,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
maintenanceWindow: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an object where a hasOldChosenSolutions is false if scopedQuery is defined', async () => {
|
||||
getMaintenanceWindow.mockResolvedValue({
|
||||
categoryIds: ['observability'],
|
||||
scopedQuery: { filter: 'filter' },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGetMaintenanceWindow('testId'), {
|
||||
wrapper: appMockRenderer.AppWrapper,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toEqual({
|
||||
showMultipleSolutionsWarning: false,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
maintenanceWindow: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,7 +19,17 @@ export const useGetMaintenanceWindow = (maintenanceWindowId: string) => {
|
|||
|
||||
const queryFn = async () => {
|
||||
const maintenanceWindow = await getMaintenanceWindow({ http, maintenanceWindowId });
|
||||
return convertFromMaintenanceWindowToForm(maintenanceWindow);
|
||||
|
||||
const hasScopedQuery = !!maintenanceWindow.scopedQuery;
|
||||
const hasOldCategorySettings = maintenanceWindow.categoryIds
|
||||
? maintenanceWindow.categoryIds.length > 0 && maintenanceWindow.categoryIds.length < 3
|
||||
: false;
|
||||
|
||||
const showMultipleSolutionsWarning = !hasScopedQuery && hasOldCategorySettings;
|
||||
return {
|
||||
maintenanceWindow: convertFromMaintenanceWindowToForm(maintenanceWindow),
|
||||
showMultipleSolutionsWarning,
|
||||
};
|
||||
};
|
||||
|
||||
const onErrorFn = () => {
|
||||
|
@ -40,7 +50,8 @@ export const useGetMaintenanceWindow = (maintenanceWindowId: string) => {
|
|||
});
|
||||
|
||||
return {
|
||||
maintenanceWindow: data,
|
||||
maintenanceWindow: data?.maintenanceWindow,
|
||||
showMultipleSolutionsWarning: data?.showMultipleSolutionsWarning,
|
||||
isLoading: isLoading || isInitialLoading,
|
||||
isError,
|
||||
};
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { within, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { within, waitFor, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { AppMockRenderer } from '../../../lib/test_utils';
|
||||
import { createAppMockRenderer } from '../../../lib/test_utils';
|
||||
import type { CreateMaintenanceWindowFormProps } from './create_maintenance_windows_form';
|
||||
|
@ -16,6 +17,10 @@ jest.mock('../../../utils/kibana_react');
|
|||
jest.mock('../../../services/rule_api', () => ({
|
||||
loadRuleTypes: jest.fn(),
|
||||
}));
|
||||
jest.mock('@kbn/alerts-ui-shared', () => ({
|
||||
...jest.requireActual('@kbn/alerts-ui-shared'),
|
||||
AlertsSearchBar: () => <div data-test-subj="mockAlertsSearchBar" />,
|
||||
}));
|
||||
|
||||
const { loadRuleTypes } = jest.requireMock('../../../services/rule_api');
|
||||
const { useKibana, useUiSetting } = jest.requireMock('../../../utils/kibana_react');
|
||||
|
@ -25,6 +30,24 @@ const formProps: CreateMaintenanceWindowFormProps = {
|
|||
onSuccess: jest.fn(),
|
||||
};
|
||||
|
||||
const formPropsForEditMode: CreateMaintenanceWindowFormProps = {
|
||||
onCancel: jest.fn(),
|
||||
onSuccess: jest.fn(),
|
||||
initialValue: {
|
||||
title: 'test',
|
||||
startDate: '2023-03-24',
|
||||
endDate: '2023-03-26',
|
||||
recurring: false,
|
||||
solutionId: 'observability',
|
||||
scopedQuery: {
|
||||
kql: 'kibana.alert.job_errors_results.job_id : * ',
|
||||
filters: [],
|
||||
dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"kibana.alert.job_errors_results.job_id"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}',
|
||||
},
|
||||
},
|
||||
maintenanceWindowId: 'fake_mw_id',
|
||||
};
|
||||
|
||||
describe('CreateMaintenanceWindowForm', () => {
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
|
@ -49,6 +72,13 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
SearchBar: <div />,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
dataViews: {
|
||||
get: jest.fn(),
|
||||
getIdsWithTitle: jest.fn().mockResolvedValue([]),
|
||||
getDefaultDataView: jest.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -68,7 +98,6 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
expect(result.getByTestId('title-field')).toBeInTheDocument();
|
||||
expect(result.getByTestId('date-field')).toBeInTheDocument();
|
||||
expect(result.getByTestId('recurring-field')).toBeInTheDocument();
|
||||
expect(result.getByTestId('maintenanceWindowCategorySelection')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('recurring-form')).not.toBeInTheDocument();
|
||||
expect(result.queryByTestId('timezone-field')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@ -87,7 +116,6 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
expect(result.getByTestId('title-field')).toBeInTheDocument();
|
||||
expect(result.getByTestId('date-field')).toBeInTheDocument();
|
||||
expect(result.getByTestId('recurring-field')).toBeInTheDocument();
|
||||
expect(result.getByTestId('maintenanceWindowCategorySelection')).toBeInTheDocument();
|
||||
expect(result.queryByTestId('recurring-form')).not.toBeInTheDocument();
|
||||
expect(result.getByTestId('timezone-field')).toBeInTheDocument();
|
||||
});
|
||||
|
@ -128,7 +156,7 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
endDate: '2023-03-26',
|
||||
timezone: ['America/Los_Angeles'],
|
||||
recurring: true,
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -147,25 +175,6 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
'comboBoxSearchInput'
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByTestId('maintenanceWindowCategorySelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-management');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
expect(titleInput).toHaveValue('test');
|
||||
expect(dateInputs[0]).toHaveValue('2023.03.23, 9:00:00');
|
||||
expect(dateInputs[1]).toHaveValue('2023.03.25, 9:00:00');
|
||||
|
@ -173,8 +182,8 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
expect(timezoneInput).toHaveValue('America/Los_Angeles');
|
||||
});
|
||||
|
||||
it('should initialize MWs without category ids properly', async () => {
|
||||
const result = appMockRenderer.render(
|
||||
it('should initialize MW with selected solution id properly', async () => {
|
||||
appMockRenderer.render(
|
||||
<CreateMaintenanceWindowForm
|
||||
{...formProps}
|
||||
initialValue={{
|
||||
|
@ -183,102 +192,98 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
endDate: '2023-03-26',
|
||||
timezone: ['America/Los_Angeles'],
|
||||
recurring: true,
|
||||
solutionId: 'observability',
|
||||
scopedQuery: {
|
||||
filters: [],
|
||||
kql: 'kibana.alert.job_errors_results.job_id : * ',
|
||||
dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"exists":{"field":"kibana.alert.job_errors_results.job_id"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByTestId('maintenanceWindowCategorySelectionLoading')
|
||||
screen.queryByTestId('maintenanceWindowSolutionSelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-management');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
});
|
||||
|
||||
it('should initialize MWs with selected category ids properly', async () => {
|
||||
const result = appMockRenderer.render(
|
||||
<CreateMaintenanceWindowForm
|
||||
{...formProps}
|
||||
initialValue={{
|
||||
title: 'test',
|
||||
startDate: '2023-03-24',
|
||||
endDate: '2023-03-26',
|
||||
timezone: ['America/Los_Angeles'],
|
||||
recurring: true,
|
||||
categoryIds: ['observability', 'management'],
|
||||
}}
|
||||
maintenanceWindowId="test"
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByTestId('maintenanceWindowCategorySelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('maintenanceWindowSolutionSelection')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-management');
|
||||
const observabilityInput = screen.getByLabelText('Observability rules');
|
||||
const securityInput = screen.getByLabelText('Security rules');
|
||||
const managementInput = screen.getByLabelText('Stack rules');
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
expect(managementInput).not.toBeChecked();
|
||||
expect(securityInput).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('can select category IDs', async () => {
|
||||
it('can select one in the time solution id', async () => {
|
||||
const user = userEvent.setup();
|
||||
const result = appMockRenderer.render(<CreateMaintenanceWindowForm {...formProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByTestId('maintenanceWindowCategorySelectionLoading')
|
||||
result.queryByTestId('maintenanceWindowSolutionSelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const observabilityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-observability');
|
||||
const securityInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-securitySolution');
|
||||
const managementInput = within(
|
||||
result.getByTestId('maintenanceWindowCategorySelection')
|
||||
).getByTestId('option-management');
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('maintenanceWindowSolutionSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
const switchContainer = screen.getByTestId('maintenanceWindowScopedQuerySwitch');
|
||||
const scopedQueryToggle = within(switchContainer).getByRole('switch');
|
||||
|
||||
fireEvent.click(observabilityInput);
|
||||
expect(scopedQueryToggle).not.toBeChecked();
|
||||
await user.click(scopedQueryToggle);
|
||||
expect(scopedQueryToggle).toBeChecked();
|
||||
|
||||
const observabilityInput = screen.getByLabelText('Observability rules');
|
||||
const securityInput = screen.getByLabelText('Security rules');
|
||||
const managementInput = screen.getByLabelText('Stack rules');
|
||||
|
||||
await user.click(securityInput);
|
||||
|
||||
expect(observabilityInput).not.toBeChecked();
|
||||
expect(managementInput).not.toBeChecked();
|
||||
expect(securityInput).toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
|
||||
fireEvent.click(securityInput);
|
||||
fireEvent.click(observabilityInput);
|
||||
await user.click(observabilityInput);
|
||||
|
||||
expect(observabilityInput).toBeChecked();
|
||||
expect(managementInput).not.toBeChecked();
|
||||
expect(securityInput).not.toBeChecked();
|
||||
expect(managementInput).toBeChecked();
|
||||
});
|
||||
|
||||
it('should hide "Filter alerts" toggle when do not have access to any solutions', async () => {
|
||||
loadRuleTypes.mockResolvedValue([]);
|
||||
const result = appMockRenderer.render(<CreateMaintenanceWindowForm {...formProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByTestId('maintenanceWindowSolutionSelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('maintenanceWindowScopedQuerySwitch')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show warning when edit if "Filter alerts" toggle on when do not have access to chosen solution', async () => {
|
||||
loadRuleTypes.mockResolvedValue([]);
|
||||
const result = appMockRenderer.render(
|
||||
<CreateMaintenanceWindowForm {...formPropsForEditMode} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
result.queryByTestId('maintenanceWindowSolutionSelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('maintenanceWindowNoAvailableSolutionsWarning')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useCallback, useMemo, useState, useRef, useEffect } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import type { FormSubmitHandler } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib';
|
||||
import {
|
||||
|
@ -30,7 +30,6 @@ import {
|
|||
EuiTextColor,
|
||||
} 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 { Filter } from '@kbn/es-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -47,18 +46,13 @@ import { useGetRuleTypes } from '../../../hooks/use_get_rule_types';
|
|||
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 { MaintenanceWindowSolutionSelection } from './maintenance_window_category_selection';
|
||||
import { MaintenanceWindowScopedQuerySwitch } from './maintenance_window_scoped_query_switch';
|
||||
import { MaintenanceWindowScopedQuery } from './maintenance_window_scoped_query';
|
||||
import { VALID_CATEGORIES } from '../constants';
|
||||
|
||||
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;
|
||||
|
@ -92,8 +86,6 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
(initialValue?.scopedQuery?.filters as Filter[]) || []
|
||||
);
|
||||
const [scopedQueryErrors, setScopedQueryErrors] = useState<string[]>([]);
|
||||
const hasSetInitialCategories = useRef<boolean>(false);
|
||||
const categoryIdsHistory = useRef<string[]>([]);
|
||||
|
||||
const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined;
|
||||
|
||||
|
@ -148,6 +140,17 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
};
|
||||
}, [isScopedQueryEnabled, query, filters]);
|
||||
|
||||
const validRuleTypes = useMemo(() => {
|
||||
if (!ruleTypes) {
|
||||
return [];
|
||||
}
|
||||
return ruleTypes.filter((ruleType) => VALID_CATEGORIES.includes(ruleType.category));
|
||||
}, [ruleTypes]);
|
||||
|
||||
const availableSolutions = useMemo(() => {
|
||||
return [...new Set(validRuleTypes.map((ruleType) => ruleType.category))];
|
||||
}, [validRuleTypes]);
|
||||
|
||||
const submitMaintenanceWindow = useCallback<FormSubmitHandler<FormProps>>(
|
||||
async (formData, isValid) => {
|
||||
if (!isValid || scopedQueryErrors.length !== 0) {
|
||||
|
@ -169,8 +172,8 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
formData.timezone ? formData.timezone[0] : defaultTimezone,
|
||||
formData.recurringSchedule
|
||||
),
|
||||
categoryIds: formData.categoryIds,
|
||||
scopedQuery: scopedQueryPayload,
|
||||
categoryIds: formData.solutionId ? [formData.solutionId] : null,
|
||||
scopedQuery: availableSolutions.length !== 0 ? scopedQueryPayload : null,
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
|
@ -183,28 +186,29 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
}
|
||||
},
|
||||
[
|
||||
isEditMode,
|
||||
scopedQueryErrors.length,
|
||||
isScopedQueryEnabled,
|
||||
scopedQueryErrors,
|
||||
maintenanceWindowId,
|
||||
updateMaintenanceWindow,
|
||||
createMaintenanceWindow,
|
||||
onSuccess,
|
||||
defaultTimezone,
|
||||
scopedQueryPayload,
|
||||
defaultTimezone,
|
||||
availableSolutions.length,
|
||||
isEditMode,
|
||||
updateMaintenanceWindow,
|
||||
maintenanceWindowId,
|
||||
onSuccess,
|
||||
createMaintenanceWindow,
|
||||
]
|
||||
);
|
||||
|
||||
const { form } = useForm<FormProps>({
|
||||
defaultValue: initialValue,
|
||||
options: { stripEmptyFields: false },
|
||||
options: { stripEmptyFields: true },
|
||||
schema,
|
||||
onSubmit: submitMaintenanceWindow,
|
||||
});
|
||||
|
||||
const [{ recurring, timezone, categoryIds }, _, mounted] = useFormData<FormProps>({
|
||||
const [{ recurring, timezone, solutionId }, _, mounted] = useFormData<FormProps>({
|
||||
form,
|
||||
watch: ['recurring', 'timezone', 'categoryIds', 'scopedQuery'],
|
||||
watch: ['recurring', 'timezone', 'solutionId', 'scopedQuery'],
|
||||
});
|
||||
|
||||
const isRecurring = recurring || false;
|
||||
|
@ -215,53 +219,42 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
|
||||
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 ruleTypeIds = useMemo(() => {
|
||||
if (!Array.isArray(validRuleTypes) || !Array.isArray(categoryIds) || !mounted) {
|
||||
if (!Array.isArray(validRuleTypes) || !mounted) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const uniqueRuleTypeIds = new Set<string>();
|
||||
|
||||
validRuleTypes.forEach((ruleType) => {
|
||||
if (categoryIds.includes(ruleType.category)) {
|
||||
if (solutionId === ruleType.category) {
|
||||
uniqueRuleTypeIds.add(ruleType.id);
|
||||
}
|
||||
});
|
||||
|
||||
return [...uniqueRuleTypeIds];
|
||||
}, [validRuleTypes, categoryIds, mounted]);
|
||||
}, [validRuleTypes, solutionId, mounted]);
|
||||
|
||||
const onCategoryIdsChange = useCallback(
|
||||
(ids: string[]) => {
|
||||
if (!categoryIds) {
|
||||
const onSolutionIdChange = useCallback(
|
||||
(id: string) => {
|
||||
if (!solutionId) {
|
||||
return;
|
||||
}
|
||||
setFieldValue('categoryIds', ids);
|
||||
setFieldValue('solutionId', id);
|
||||
},
|
||||
[categoryIds, setFieldValue]
|
||||
[solutionId, setFieldValue]
|
||||
);
|
||||
|
||||
const onScopeQueryToggle = useCallback(
|
||||
(isEnabled: boolean) => {
|
||||
if (isEnabled) {
|
||||
setFieldValue('categoryIds', [categoryIds?.sort()[0] || availableCategories.sort()[0]]);
|
||||
setFieldValue('solutionId', availableSolutions.sort()[0]);
|
||||
} else {
|
||||
setFieldValue('categoryIds', categoryIdsHistory.current);
|
||||
setFieldValue('solutionId', undefined);
|
||||
}
|
||||
setIsScopedQueryEnabled(isEnabled);
|
||||
},
|
||||
[categoryIds, availableCategories, setFieldValue]
|
||||
[setFieldValue, availableSolutions]
|
||||
);
|
||||
|
||||
const onQueryChange = useCallback(
|
||||
|
@ -300,55 +293,23 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
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]);
|
||||
const showNoAvailableSolutionsWarning =
|
||||
availableSolutions.length === 0 && isScopedQueryEnabled && isEditMode;
|
||||
|
||||
return (
|
||||
<Form form={form} data-test-subj="createMaintenanceWindowForm">
|
||||
{showNoAvailableSolutionsWarning && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
data-test-subj="maintenanceWindowNoAvailableSolutionsWarning"
|
||||
title={i18n.NO_AVAILABLE_SOLUTIONS_WARNING_TITLE}
|
||||
color="warning"
|
||||
>
|
||||
<p>{i18n.NO_AVAILABLE_SOLUTIONS_WARNING_SUBTITLE}</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup direction="column" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<UseField
|
||||
|
@ -450,48 +411,52 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
<RecurringSchedule data-test-subj="recurring-form" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuerySwitch
|
||||
checked={isScopedQueryEnabled}
|
||||
onEnabledChange={onScopeQueryToggle}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
<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>
|
||||
<EuiFlexItem>
|
||||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuery
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
query={query}
|
||||
filters={filters}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
isEnabled={isScopedQueryEnabled}
|
||||
errors={scopedQueryErrors}
|
||||
onQueryChange={onQueryChange}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
|
||||
{availableSolutions.length > 0 && (
|
||||
<>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuerySwitch
|
||||
checked={isScopedQueryEnabled}
|
||||
onEnabledChange={onScopeQueryToggle}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<UseField path="solutionId">
|
||||
{(field) => (
|
||||
<MaintenanceWindowSolutionSelection
|
||||
isScopedQueryEnabled={isScopedQueryEnabled}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
selectedSolution={solutionId}
|
||||
availableSolutions={availableSolutions}
|
||||
errors={field.errors.map((error) => error.message)}
|
||||
onChange={onSolutionIdChange}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuery
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
query={query}
|
||||
filters={filters}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
isEnabled={isScopedQueryEnabled}
|
||||
errors={scopedQueryErrors}
|
||||
onQueryChange={onQueryChange}
|
||||
onFiltersChange={setFilters}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</EuiFlexGroup>
|
||||
{isEditMode && (
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { MaintenanceWindowCategorySelection } from './maintenance_window_category_selection';
|
||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { MaintenanceWindowSolutionSelection } from './maintenance_window_category_selection';
|
||||
import type { AppMockRenderer } from '../../../lib/test_utils';
|
||||
import { createAppMockRenderer } from '../../../lib/test_utils';
|
||||
|
||||
|
@ -21,148 +21,121 @@ describe('maintenanceWindowCategorySelection', () => {
|
|||
appMockRenderer = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
it('should not display radio group if scoped query is disabled', async () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
<MaintenanceWindowSolutionSelection
|
||||
selectedSolution={''}
|
||||
availableSolutions={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('option-observability')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('option-securitySolution')).not.toBeDisabled();
|
||||
expect(screen.getByTestId('option-management')).not.toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId('maintenanceWindowSolutionSelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('maintenanceWindowSolutionSelection')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable options if option is not in the available categories array', () => {
|
||||
it('should display radio group if scoped query is enabled', async () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={[]}
|
||||
availableCategories={[]}
|
||||
<MaintenanceWindowSolutionSelection
|
||||
selectedSolution={''}
|
||||
availableSolutions={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('option-observability')).toBeDisabled();
|
||||
expect(screen.getByTestId('option-securitySolution')).toBeDisabled();
|
||||
expect(screen.getByTestId('option-management')).toBeDisabled();
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId('maintenanceWindowSolutionSelectionLoading')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(await screen.findByTestId('maintenanceWindowSolutionSelection')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('maintenanceWindowSolutionSelectionRadioGroup')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should disable options if option is not in the available solutions array', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowSolutionSelection
|
||||
selectedSolution={'management'}
|
||||
availableSolutions={[]}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const observabilityInput = screen.getByLabelText('Observability rules');
|
||||
const securityInput = screen.getByLabelText('Security rules');
|
||||
const managementInput = screen.getByLabelText('Stack rules');
|
||||
|
||||
expect(observabilityInput).toBeDisabled();
|
||||
expect(managementInput).toBeDisabled();
|
||||
expect(securityInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it('can initialize checkboxes with initial values from props', async () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={['securitySolution', 'management']}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
<MaintenanceWindowSolutionSelection
|
||||
selectedSolution={'securitySolution'}
|
||||
availableSolutions={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('option-observability')).not.toBeChecked();
|
||||
expect(screen.getByTestId('option-securitySolution')).toBeChecked();
|
||||
expect(screen.getByTestId('option-management')).toBeChecked();
|
||||
expect(screen.getByLabelText('Observability rules')).not.toBeChecked();
|
||||
expect(screen.getByLabelText('Security rules')).toBeChecked();
|
||||
expect(screen.getByLabelText('Stack rules')).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('can check checkboxes', async () => {
|
||||
it('can choose solution', async () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={['observability']}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
<MaintenanceWindowSolutionSelection
|
||||
selectedSolution={'observability'}
|
||||
availableSolutions={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={true}
|
||||
/>
|
||||
);
|
||||
|
||||
const managementCheckbox = screen.getByTestId('option-management');
|
||||
const securityCheckbox = screen.getByTestId('option-securitySolution');
|
||||
fireEvent.click(screen.getByLabelText('Stack rules'));
|
||||
|
||||
fireEvent.click(managementCheckbox);
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith('management');
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(['observability', 'management']);
|
||||
fireEvent.click(screen.getByLabelText('Security rules'));
|
||||
|
||||
fireEvent.click(securityCheckbox);
|
||||
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith(['observability', 'securitySolution']);
|
||||
expect(mockOnChange).toHaveBeenLastCalledWith('securitySolution');
|
||||
});
|
||||
|
||||
it('should display loading spinner if isLoading is true', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
<MaintenanceWindowSolutionSelection
|
||||
isLoading
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
selectedSolution={''}
|
||||
availableSolutions={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={true}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('maintenanceWindowCategorySelectionLoading')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('maintenanceWindowSolutionSelectionLoading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display error message if it exists', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
<MaintenanceWindowSolutionSelection
|
||||
selectedSolution={''}
|
||||
availableSolutions={['observability', 'management', 'securitySolution']}
|
||||
errors={['test error']}
|
||||
onChange={mockOnChange}
|
||||
isScopedQueryEnabled={true}
|
||||
/>
|
||||
);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,117 +13,81 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiTextColor,
|
||||
EuiCheckboxGroup,
|
||||
EuiRadioGroup,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import * as i18n from '../translations';
|
||||
|
||||
const CHECKBOX_OPTIONS = [
|
||||
const RADIO_OPTIONS = [
|
||||
{
|
||||
id: DEFAULT_APP_CATEGORIES.observability.id,
|
||||
label: i18n.CREATE_FORM_CATEGORY_OBSERVABILITY_RULES,
|
||||
label: i18n.CREATE_FORM_SOLUTION_OBSERVABILITY_RULES,
|
||||
['data-test-subj']: `option-${DEFAULT_APP_CATEGORIES.observability.id}`,
|
||||
},
|
||||
{
|
||||
id: DEFAULT_APP_CATEGORIES.security.id,
|
||||
label: i18n.CREATE_FORM_CATEGORY_SECURITY_RULES,
|
||||
label: i18n.CREATE_FORM_SOLUTION_SECURITY_RULES,
|
||||
['data-test-subj']: `option-${DEFAULT_APP_CATEGORIES.security.id}`,
|
||||
},
|
||||
{
|
||||
id: DEFAULT_APP_CATEGORIES.management.id,
|
||||
label: i18n.CREATE_FORM_CATEGORY_STACK_RULES,
|
||||
label: i18n.CREATE_FORM_SOLUTION_STACK_RULES,
|
||||
['data-test-subj']: `option-${DEFAULT_APP_CATEGORIES.management.id}`,
|
||||
},
|
||||
].sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
export interface MaintenanceWindowCategorySelectionProps {
|
||||
selectedCategories: string[];
|
||||
availableCategories: string[];
|
||||
export interface MaintenanceWindowSolutionSelectionProps {
|
||||
selectedSolution?: string;
|
||||
availableSolutions: string[];
|
||||
errors?: string[];
|
||||
isLoading?: boolean;
|
||||
isScopedQueryEnabled?: boolean;
|
||||
onChange: (categories: string[]) => void;
|
||||
onChange: (solution: string) => void;
|
||||
}
|
||||
|
||||
export const MaintenanceWindowCategorySelection = (
|
||||
props: MaintenanceWindowCategorySelectionProps
|
||||
export const MaintenanceWindowSolutionSelection = (
|
||||
props: MaintenanceWindowSolutionSelectionProps
|
||||
) => {
|
||||
const {
|
||||
selectedCategories,
|
||||
availableCategories,
|
||||
selectedSolution,
|
||||
availableSolutions,
|
||||
errors = [],
|
||||
isLoading = false,
|
||||
isScopedQueryEnabled = false,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const selectedMap = useMemo(() => {
|
||||
return selectedCategories.reduce<Record<string, boolean>>((result, category) => {
|
||||
result[category] = true;
|
||||
return result;
|
||||
}, {});
|
||||
}, [selectedCategories]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return CHECKBOX_OPTIONS.map((option) => ({
|
||||
return RADIO_OPTIONS.map((option) => ({
|
||||
...option,
|
||||
disabled: !availableCategories.includes(option.id),
|
||||
disabled: !availableSolutions.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]
|
||||
);
|
||||
}, [availableSolutions]);
|
||||
|
||||
const onRadioChange = useCallback(
|
||||
(id: string) => {
|
||||
onChange([id]);
|
||||
onChange(id);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const categorySelection = useMemo(() => {
|
||||
if (isScopedQueryEnabled) {
|
||||
return (
|
||||
<EuiRadioGroup
|
||||
data-test-subj="maintenanceWindowCategorySelectionRadioGroup"
|
||||
options={options}
|
||||
idSelected={selectedCategories[0]}
|
||||
onChange={onRadioChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const solutionSelection = useMemo(() => {
|
||||
return (
|
||||
<EuiCheckboxGroup
|
||||
data-test-subj="maintenanceWindowCategorySelectionCheckboxGroup"
|
||||
<EuiRadioGroup
|
||||
data-test-subj="maintenanceWindowSolutionSelectionRadioGroup"
|
||||
options={options}
|
||||
idToSelectedMap={selectedMap}
|
||||
onChange={onCheckboxChange}
|
||||
idSelected={selectedSolution}
|
||||
onChange={onRadioChange}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
isScopedQueryEnabled,
|
||||
options,
|
||||
selectedCategories,
|
||||
selectedMap,
|
||||
onCheckboxChange,
|
||||
onRadioChange,
|
||||
]);
|
||||
}, [options, selectedSolution, onRadioChange]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
data-test-subj="maintenanceWindowCategorySelectionLoading"
|
||||
data-test-subj="maintenanceWindowSolutionSelectionLoading"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
|
@ -133,26 +97,28 @@ export const MaintenanceWindowCategorySelection = (
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="maintenanceWindowCategorySelection">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_CATEGORY_SELECTION_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.CREATE_FORM_CATEGORY_SELECTION_DESCRIPTION}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.CREATE_FORM_CATEGORIES_SELECTION_CHECKBOX_GROUP_TITLE}
|
||||
isInvalid={!!errors.length}
|
||||
error={errors[0]}
|
||||
>
|
||||
{categorySelection}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
isScopedQueryEnabled && (
|
||||
<EuiFlexGroup data-test-subj="maintenanceWindowSolutionSelection">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_SOLUTION_SELECTION_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.CREATE_FORM_SOLUTION_SELECTION_DESCRIPTION}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.CREATE_FORM_SOLUTION_SELECTION_CHECKBOX_GROUP_TITLE}
|
||||
isInvalid={!!errors.length}
|
||||
error={errors[0]}
|
||||
>
|
||||
{solutionSelection}
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -26,7 +26,6 @@ const initialValue: FormProps = {
|
|||
frequency: 'CUSTOM',
|
||||
ends: EndsOptions.NEVER,
|
||||
},
|
||||
categoryIds: [],
|
||||
};
|
||||
|
||||
describe('CustomRecurringSchedule', () => {
|
||||
|
|
|
@ -22,7 +22,6 @@ const initialValue: FormProps = {
|
|||
startDate: '2023-03-24',
|
||||
endDate: '2023-03-26',
|
||||
recurring: true,
|
||||
categoryIds: [],
|
||||
};
|
||||
|
||||
describe('RecurringSchedule', () => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import * as i18n from '../translations';
|
|||
import type { MaintenanceWindowFrequency } from '../constants';
|
||||
import { EndsOptions } from '../constants';
|
||||
import type { ScopedQueryAttributes } from '../../../../common';
|
||||
import { VALID_CATEGORIES } from '../constants';
|
||||
|
||||
const { emptyField } = fieldValidators;
|
||||
|
||||
|
@ -23,7 +24,7 @@ export interface FormProps {
|
|||
timezone?: string[];
|
||||
recurring: boolean;
|
||||
recurringSchedule?: RecurringScheduleFormProps;
|
||||
categoryIds?: string[];
|
||||
solutionId?: string;
|
||||
scopedQuery?: ScopedQueryAttributes | null;
|
||||
}
|
||||
|
||||
|
@ -48,12 +49,24 @@ export const schema: FormSchema<FormProps> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
categoryIds: {
|
||||
solutionId: {
|
||||
type: FIELD_TYPES.SELECT,
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.CREATE_FORM_CATEGORY_IDS_REQUIRED),
|
||||
validator: ({ value }: { value: string }) => {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (!VALID_CATEGORIES.includes(value)) {
|
||||
return {
|
||||
message: `Value must be one of: ${VALID_CATEGORIES.join(', ')}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
// The empty string appears by default because of how form libraries typically handle form inputs
|
||||
deserializer: (value) => (value === '' ? undefined : value),
|
||||
},
|
||||
scopedQuery: {
|
||||
defaultValue: {
|
||||
|
|
|
@ -7,9 +7,16 @@
|
|||
import { invert, mapValues } from 'lodash';
|
||||
import { Frequency } from '@kbn/rrule';
|
||||
import moment from 'moment';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import * as i18n from './translations';
|
||||
import { ISO_WEEKDAYS, MaintenanceWindowStatus } from '../../../common';
|
||||
|
||||
export const VALID_CATEGORIES = [
|
||||
DEFAULT_APP_CATEGORIES.observability.id,
|
||||
DEFAULT_APP_CATEGORIES.security.id,
|
||||
DEFAULT_APP_CATEGORIES.management.id,
|
||||
];
|
||||
|
||||
export type MaintenanceWindowFrequency = Extract<
|
||||
Frequency,
|
||||
Frequency.YEARLY | Frequency.MONTHLY | Frequency.WEEKLY | Frequency.DAILY
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
endDate: endDate.toISOString(),
|
||||
timezone: ['UTC'],
|
||||
recurring: false,
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -65,7 +65,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: Frequency.DAILY,
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -98,7 +98,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: Frequency.DAILY,
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -129,7 +129,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: Frequency.DAILY,
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -158,7 +158,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
byweekday: { 1: false, 2: false, 3: true, 4: false, 5: false, 6: false, 7: false },
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -187,7 +187,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
bymonth: 'weekday',
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,7 +216,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: Frequency.YEARLY,
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -244,7 +244,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: 'CUSTOM',
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -274,7 +274,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: 'CUSTOM',
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -304,7 +304,7 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: 'CUSTOM',
|
||||
interval: 1,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -334,7 +334,104 @@ describe('convertFromMaintenanceWindowToForm', () => {
|
|||
frequency: 'CUSTOM',
|
||||
interval: 3,
|
||||
},
|
||||
categoryIds: [],
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('should convert categoryIds into solutionId when scopedQuery is on', () => {
|
||||
const maintenanceWindow = convertFromMaintenanceWindowToForm({
|
||||
title,
|
||||
duration,
|
||||
rRule: {
|
||||
dtstart: startDate.toISOString(),
|
||||
tzid: 'UTC',
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 3,
|
||||
bymonth: [3],
|
||||
bymonthday: [22],
|
||||
},
|
||||
categoryIds: ['observability'],
|
||||
scopedQuery: null,
|
||||
} as MaintenanceWindow);
|
||||
|
||||
expect(maintenanceWindow).toEqual({
|
||||
title,
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
timezone: ['UTC'],
|
||||
recurring: true,
|
||||
recurringSchedule: {
|
||||
customFrequency: Frequency.YEARLY,
|
||||
ends: 'never',
|
||||
frequency: 'CUSTOM',
|
||||
interval: 3,
|
||||
},
|
||||
solutionId: undefined,
|
||||
scopedQuery: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('should convert categoryIds into solutionId when scopedQuery is off', () => {
|
||||
const maintenanceWindow = convertFromMaintenanceWindowToForm({
|
||||
title,
|
||||
duration,
|
||||
rRule: {
|
||||
dtstart: startDate.toISOString(),
|
||||
tzid: 'UTC',
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 3,
|
||||
bymonth: [3],
|
||||
bymonthday: [22],
|
||||
},
|
||||
categoryIds: ['observability'],
|
||||
scopedQuery: { kql: '' },
|
||||
} as MaintenanceWindow);
|
||||
|
||||
expect(maintenanceWindow).toEqual({
|
||||
title,
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
timezone: ['UTC'],
|
||||
recurring: true,
|
||||
recurringSchedule: {
|
||||
customFrequency: Frequency.YEARLY,
|
||||
ends: 'never',
|
||||
frequency: 'CUSTOM',
|
||||
interval: 3,
|
||||
},
|
||||
solutionId: 'observability',
|
||||
scopedQuery: { kql: '' },
|
||||
});
|
||||
});
|
||||
|
||||
test('should convert old multivalued categoryIds into solutionId equal undefined', () => {
|
||||
const maintenanceWindow = convertFromMaintenanceWindowToForm({
|
||||
title,
|
||||
duration,
|
||||
rRule: {
|
||||
dtstart: startDate.toISOString(),
|
||||
tzid: 'UTC',
|
||||
freq: Frequency.YEARLY,
|
||||
interval: 3,
|
||||
bymonth: [3],
|
||||
bymonthday: [22],
|
||||
},
|
||||
categoryIds: ['observability', 'security'],
|
||||
} as MaintenanceWindow);
|
||||
|
||||
expect(maintenanceWindow).toEqual({
|
||||
title,
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
timezone: ['UTC'],
|
||||
recurring: true,
|
||||
recurringSchedule: {
|
||||
customFrequency: Frequency.YEARLY,
|
||||
ends: 'never',
|
||||
frequency: 'CUSTOM',
|
||||
interval: 3,
|
||||
},
|
||||
solutionId: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,13 +21,17 @@ export const convertFromMaintenanceWindowToForm = (
|
|||
const endDate = moment(startDate).add(maintenanceWindow.duration);
|
||||
// maintenance window is considered recurring if interval is defined
|
||||
const recurring = has(maintenanceWindow, 'rRule.interval');
|
||||
const hasScopedQuery = !!maintenanceWindow.scopedQuery;
|
||||
const form: FormProps = {
|
||||
title: maintenanceWindow.title,
|
||||
startDate,
|
||||
endDate: endDate.toISOString(),
|
||||
timezone: [maintenanceWindow.rRule.tzid],
|
||||
recurring,
|
||||
categoryIds: maintenanceWindow.categoryIds || [],
|
||||
solutionId:
|
||||
maintenanceWindow.categoryIds && maintenanceWindow.categoryIds.length === 1 && hasScopedQuery
|
||||
? maintenanceWindow.categoryIds[0]
|
||||
: undefined,
|
||||
scopedQuery: maintenanceWindow.scopedQuery,
|
||||
};
|
||||
if (!recurring) return form;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { EuiPageSection, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiPageSection, EuiSpacer, EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import { useBreadcrumbs } from '../../hooks/use_breadcrumbs';
|
||||
import { useMaintenanceWindowsNavigation } from '../../hooks/use_navigation';
|
||||
|
@ -24,7 +24,8 @@ export const MaintenanceWindowsEditPage = React.memo(() => {
|
|||
useBreadcrumbs(MAINTENANCE_WINDOW_DEEP_LINK_IDS.maintenanceWindowsEdit);
|
||||
|
||||
const { maintenanceWindowId } = useParams<{ maintenanceWindowId: string }>();
|
||||
const { maintenanceWindow, isLoading, isError } = useGetMaintenanceWindow(maintenanceWindowId);
|
||||
const { maintenanceWindow, showMultipleSolutionsWarning, isLoading, isError } =
|
||||
useGetMaintenanceWindow(maintenanceWindowId);
|
||||
|
||||
if (isError) {
|
||||
navigateToMaintenanceWindows();
|
||||
|
@ -38,6 +39,18 @@ export const MaintenanceWindowsEditPage = React.memo(() => {
|
|||
<EuiPageSection restrictWidth={true}>
|
||||
<PageHeader showBackButton={true} title={i18n.EDIT_MAINTENANCE_WINDOW} />
|
||||
<EuiSpacer size="xl" />
|
||||
{showMultipleSolutionsWarning && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
data-test-subj="maintenanceWindowMultipleSolutionsWarning"
|
||||
title={i18n.SOLUTION_CONFIG_REMOVAL_WARNING_TITLE}
|
||||
color="warning"
|
||||
>
|
||||
<p>{i18n.SOLUTION_CONFIG_REMOVAL_WARNING_SUBTITLE}</p>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="xl" />
|
||||
</>
|
||||
)}
|
||||
<CreateMaintenanceWindowForm
|
||||
initialValue={maintenanceWindow}
|
||||
maintenanceWindowId={maintenanceWindowId}
|
||||
|
|
|
@ -158,51 +158,51 @@ export const CREATE_FORM_TIMEFRAME_DESCRIPTION = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORY_IDS_REQUIRED = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categoryIds.required',
|
||||
export const CREATE_FORM_SOLUTION_IDS_REQUIRED = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionIds.required',
|
||||
{
|
||||
defaultMessage: 'A category is required.',
|
||||
defaultMessage: 'A solution is required.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORY_SELECTION_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categoriesSelection.title',
|
||||
export const CREATE_FORM_SOLUTION_SELECTION_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionSelection.title',
|
||||
{
|
||||
defaultMessage: 'Category specific maintenance window',
|
||||
defaultMessage: 'Solution specific maintenance window',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORY_SELECTION_DESCRIPTION = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categoriesSelection.description',
|
||||
export const CREATE_FORM_SOLUTION_SELECTION_DESCRIPTION = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionSelection.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Only rules associated with the selected categories are affected by the maintenance window.',
|
||||
'Only rules associated with the selected solution are affected by the maintenance window.',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORIES_SELECTION_CHECKBOX_GROUP_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categorySelection.checkboxGroupTitle',
|
||||
export const CREATE_FORM_SOLUTION_SELECTION_CHECKBOX_GROUP_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionSelection.checkboxGroupTitle',
|
||||
{
|
||||
defaultMessage: 'Select the categories this should affect',
|
||||
defaultMessage: 'Select the solution this should affect',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORY_OBSERVABILITY_RULES = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categoryIds.observabilityRules',
|
||||
export const CREATE_FORM_SOLUTION_OBSERVABILITY_RULES = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionIds.observabilityRules',
|
||||
{
|
||||
defaultMessage: 'Observability rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORY_SECURITY_RULES = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categoryIds.securityRules',
|
||||
export const CREATE_FORM_SOLUTION_SECURITY_RULES = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionIds.securityRules',
|
||||
{
|
||||
defaultMessage: 'Security rules',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_FORM_CATEGORY_STACK_RULES = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.categoryIds.stackRules',
|
||||
export const CREATE_FORM_SOLUTION_STACK_RULES = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.createForm.solutionIds.stackRules',
|
||||
{
|
||||
defaultMessage: 'Stack rules',
|
||||
}
|
||||
|
@ -219,7 +219,7 @@ export const CREATE_FORM_SCOPED_QUERY_DESCRIPTION = i18n.translate(
|
|||
'xpack.alerting.maintenanceWindows.createForm.scopedQuery.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'Add filters that refine the scope of the maintenance window. You can select only one category when filters are enabled.',
|
||||
'Add filters that refine the scope of the maintenance window. You can select only one solution when filters are enabled.',
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -667,6 +667,21 @@ export const ARCHIVE_SUBTITLE = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const SOLUTION_CONFIG_REMOVAL_WARNING_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.solutionConfigRemovalWarning.title',
|
||||
{
|
||||
defaultMessage: 'Support for multiple solution categories is removed.',
|
||||
}
|
||||
);
|
||||
|
||||
export const SOLUTION_CONFIG_REMOVAL_WARNING_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.solutionConfigRemovalWarning.subtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
'When you save the changes, the maintenance window will affect rules in all solutions.',
|
||||
}
|
||||
);
|
||||
|
||||
export const TABLE_ACTION_UNARCHIVE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.table.unarchive',
|
||||
{
|
||||
|
@ -758,3 +773,16 @@ export const SEARCH_PLACEHOLDER = i18n.translate(
|
|||
'xpack.alerting.maintenanceWindows.searchPlaceholder',
|
||||
{ defaultMessage: 'Search' }
|
||||
);
|
||||
|
||||
export const NO_AVAILABLE_SOLUTIONS_WARNING_TITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.noAvailableSolutionsWarning.title',
|
||||
{ defaultMessage: 'Limited permissions detected.' }
|
||||
);
|
||||
|
||||
export const NO_AVAILABLE_SOLUTIONS_WARNING_SUBTITLE = i18n.translate(
|
||||
'xpack.alerting.maintenanceWindows.noAvailableSolutionsWarning.subtitle',
|
||||
{
|
||||
defaultMessage:
|
||||
"You don't have access to all solution types required for alert filtering. If you save, your alert filter settings will be reset.",
|
||||
}
|
||||
);
|
||||
|
|
|
@ -149,6 +149,66 @@ describe('getMaintenanceWindows', () => {
|
|||
).toEqual([mockMaintenanceWindows[0], mockMaintenanceWindows[2]]);
|
||||
});
|
||||
|
||||
test('filters to rule type category and category IDs is null', async () => {
|
||||
const mockMaintenanceWindows = [
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id1',
|
||||
categoryIds: [maintenanceWindowCategoryIdTypes.OBSERVABILITY],
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id2',
|
||||
categoryIds: [maintenanceWindowCategoryIdTypes.SECURITY_SOLUTION],
|
||||
},
|
||||
{
|
||||
...getMockMaintenanceWindow(),
|
||||
eventStartTime: new Date().toISOString(),
|
||||
eventEndTime: new Date().toISOString(),
|
||||
status: MaintenanceWindowStatus.Running,
|
||||
id: 'test-id3',
|
||||
categoryIds: null,
|
||||
},
|
||||
];
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValue(mockMaintenanceWindows);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
fakeRequest,
|
||||
getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient),
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'management',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[2]]);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
fakeRequest,
|
||||
getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient),
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'observability',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[0], mockMaintenanceWindows[2]]);
|
||||
expect(
|
||||
await getMaintenanceWindows({
|
||||
fakeRequest,
|
||||
getMaintenanceWindowClientWithRequest: jest.fn().mockReturnValue(maintenanceWindowClient),
|
||||
logger,
|
||||
ruleTypeId,
|
||||
ruleTypeCategory: 'securitySolution',
|
||||
ruleId,
|
||||
})
|
||||
).toEqual([mockMaintenanceWindows[1], mockMaintenanceWindows[2]]);
|
||||
});
|
||||
|
||||
test('returns empty array if no active maintenance windows exist', async () => {
|
||||
maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([]);
|
||||
expect(
|
||||
|
|
|
@ -75,5 +75,39 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
|
|||
expect(toastTitle).to.eql(`Updated maintenance window 'Test Maintenance Window updated'`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should show callout when update a maintenance window with old chosen solutions', async () => {
|
||||
const createdMaintenanceWindow = await createMaintenanceWindow({
|
||||
name: 'Test Maintenance Window',
|
||||
getService,
|
||||
overwrite: {
|
||||
r_rule: {
|
||||
dtstart: new Date().toISOString(),
|
||||
tzid: 'UTC',
|
||||
freq: 3,
|
||||
interval: 12,
|
||||
count: 5,
|
||||
},
|
||||
category_ids: ['observability', 'securitySolution'],
|
||||
},
|
||||
});
|
||||
|
||||
objectRemover.add(createdMaintenanceWindow.id, 'rules/maintenance_window', 'alerting', true);
|
||||
|
||||
await browser.refresh();
|
||||
|
||||
await pageObjects.maintenanceWindows.searchMaintenanceWindows('Test Maintenance Window 2');
|
||||
|
||||
await testSubjects.click('table-actions-popover');
|
||||
await testSubjects.click('table-actions-edit');
|
||||
|
||||
await retry.try(async () => {
|
||||
await testSubjects.existOrFail('createMaintenanceWindowForm');
|
||||
});
|
||||
|
||||
expect(
|
||||
await testSubjects.getVisibleText('maintenanceWindowMultipleSolutionsWarning')
|
||||
).to.contain('Support for multiple solution categories is removed.');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -36,7 +36,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
'[data-test-subj="maintenanceWindowScopedQuerySwitch"] .euiSwitch__button'
|
||||
);
|
||||
await filterAlerts.click();
|
||||
const radioGroup = await testSubjects.find('maintenanceWindowCategorySelectionRadioGroup');
|
||||
const radioGroup = await testSubjects.find('maintenanceWindowSolutionSelectionRadioGroup');
|
||||
const label = await radioGroup.findByCssSelector(`label[for="observability"]`);
|
||||
await label.click();
|
||||
await testSubjects.setValue('queryInput', 'kibana.alert.rule.name: custom-threshold-rule-1');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue