[Security Solution] Add active maintenance window callout to the Rules Management page (#155386)

**Addresses:** https://github.com/elastic/kibana/issues/155099
**Documentation issue:**
https://github.com/elastic/security-docs/issues/3181

## Summary

Adds a Maintenance Window callout to the Rules Management page. This
callout is only displayed when a maintenance window is running.

<img width="1260" alt="Screenshot 2023-04-21 at 13 24 11"
src="https://user-images.githubusercontent.com/15949146/233624339-9c9b6e3e-9e5e-424d-9d19-9cd7d4e92259.png">


### Checklist

- [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/packages/kbn-i18n/README.md)
- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
issue created: https://github.com/elastic/security-docs/issues/3181
- [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] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Georgii Gorbachev <banderror@gmail.com>
This commit is contained in:
Nikita Indik 2023-04-25 01:22:56 +02:00 committed by GitHub
parent a34dd8c260
commit 675ed0eee2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 359 additions and 23 deletions

View file

@ -58,6 +58,12 @@ export const LEGACY_BASE_ALERT_API_PATH = '/api/alerts';
export const BASE_ALERTING_API_PATH = '/api/alerting';
export const INTERNAL_BASE_ALERTING_API_PATH = '/internal/alerting';
export const INTERNAL_ALERTING_API_FIND_RULES_PATH = `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_find`;
export const INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH =
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window` as const;
export const INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH =
`${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/_active` as const;
export const ALERTS_FEATURE_ID = 'alerts';
export const MONITORING_HISTORY_LIMIT = 200;
export const ENABLE_MAINTENANCE_WINDOWS = false;

View file

@ -46,6 +46,11 @@ export type MaintenanceWindow = MaintenanceWindowSOAttributes & {
id: string;
};
export type MaintenanceWindowCreateBody = Omit<
MaintenanceWindowSOProperties,
'events' | 'expirationDate' | 'enabled' | 'archived'
>;
export interface MaintenanceWindowClientContext {
getModificationMetadata: () => Promise<MaintenanceWindowModificationMetadata>;
savedObjectsClient: SavedObjectsClientContract;

View file

@ -8,7 +8,10 @@
import { IRouter } from '@kbn/core/server';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH,
} from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
export const activeMaintenanceWindowsRoute = (
@ -17,7 +20,7 @@ export const activeMaintenanceWindowsRoute = (
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_active`,
path: INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH,
validate: {},
options: {
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`],

View file

@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext, rewritePartialMaintenanceBodyRes } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
const paramSchema = schema.object({
@ -26,7 +29,7 @@ export const archiveMaintenanceWindowRoute = (
) => {
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_archive`,
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}/_archive`,
validate: {
params: paramSchema,
body: bodySchema,

View file

@ -14,8 +14,11 @@ import {
RewriteRequestCase,
rewriteMaintenanceWindowRes,
} from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import { MaintenanceWindowSOProperties, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MaintenanceWindowCreateBody, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
const bodySchema = schema.object({
title: schema.string(),
@ -23,11 +26,6 @@ const bodySchema = schema.object({
r_rule: rRuleSchema,
});
type MaintenanceWindowCreateBody = Omit<
MaintenanceWindowSOProperties,
'events' | 'expirationDate' | 'enabled' | 'archived'
>;
export const rewriteQueryReq: RewriteRequestCase<MaintenanceWindowCreateBody> = ({
r_rule: rRule,
...rest
@ -42,7 +40,7 @@ export const createMaintenanceWindowRoute = (
) => {
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window`,
path: INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
validate: {
body: bodySchema,
},

View file

@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
const paramSchema = schema.object({
@ -22,7 +25,7 @@ export const deleteMaintenanceWindowRoute = (
) => {
router.delete(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`,
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}`,
validate: {
params: paramSchema,
},

View file

@ -8,7 +8,10 @@
import { IRouter } from '@kbn/core/server';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
export const findMaintenanceWindowsRoute = (
@ -17,7 +20,7 @@ export const findMaintenanceWindowsRoute = (
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/_find`,
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/_find`,
validate: {},
options: {
tags: [`access:${MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW}`],

View file

@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext, rewritePartialMaintenanceBodyRes } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
const paramSchema = schema.object({
@ -22,7 +25,7 @@ export const finishMaintenanceWindowRoute = (
) => {
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}/_finish`,
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}/_finish`,
validate: {
params: paramSchema,
},

View file

@ -9,7 +9,10 @@ import { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { ILicenseState } from '../../lib';
import { verifyAccessAndContext, rewriteMaintenanceWindowRes } from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
const paramSchema = schema.object({
@ -22,7 +25,7 @@ export const getMaintenanceWindowRoute = (
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`,
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}`,
validate: {
params: paramSchema,
},

View file

@ -14,7 +14,10 @@ import {
RewriteRequestCase,
rewritePartialMaintenanceBodyRes,
} from '../lib';
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../../types';
import {
AlertingRequestHandlerContext,
INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
} from '../../types';
import { MaintenanceWindowSOProperties, MAINTENANCE_WINDOW_API_PRIVILEGES } from '../../../common';
const paramSchema = schema.object({
@ -49,7 +52,7 @@ export const updateMaintenanceWindowRoute = (
) => {
router.post(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/{id}`,
path: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/{id}`,
validate: {
body: bodySchema,
params: paramSchema,

View file

@ -0,0 +1,58 @@
/*
* 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 { INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH } from '@kbn/alerting-plugin/common';
import type { MaintenanceWindowCreateBody } from '@kbn/alerting-plugin/common';
import type { AsApiContract } from '@kbn/alerting-plugin/server/routes/lib';
import { cleanKibana } from '../../tasks/common';
import { login, visit } from '../../tasks/login';
import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation';
describe('Maintenance window callout on Rule Management page', () => {
let maintenanceWindowId = '';
before(() => {
cleanKibana();
login();
const body: AsApiContract<MaintenanceWindowCreateBody> = {
title: 'My maintenance window',
duration: 60000, // 1 minute
r_rule: {
dtstart: new Date().toISOString(),
tzid: 'Europe/Amsterdam',
freq: 0,
count: 1,
},
};
// Create a test maintenance window
cy.request({
method: 'POST',
url: INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH,
headers: { 'kbn-xsrf': 'cypress-creds' },
body,
}).then((response) => {
maintenanceWindowId = response.body.id;
});
});
after(() => {
// Delete a test maintenance window
cy.request({
method: 'DELETE',
url: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/${maintenanceWindowId}`,
headers: { 'kbn-xsrf': 'cypress-creds' },
});
});
it('Displays the callout when there are running maintenance windows', () => {
visit(DETECTIONS_RULE_MANAGEMENT_URL);
cy.contains('A maintenance window is currently running');
});
});

View file

@ -28,6 +28,7 @@
"force": true
},
"@kbn/rison",
"@kbn/datemath"
"@kbn/datemath",
"@kbn/alerting-plugin"
]
}

View file

@ -0,0 +1,18 @@
/*
* 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 type { MaintenanceWindow } from '@kbn/alerting-plugin/common/maintenance_window';
import { INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH } from '@kbn/alerting-plugin/common';
import { KibanaServices } from '../../../../common/lib/kibana';
export const fetchActiveMaintenanceWindows = async (
signal?: AbortSignal
): Promise<MaintenanceWindow[]> =>
KibanaServices.get().http.fetch(INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, {
method: 'GET',
signal,
});

View file

@ -0,0 +1,136 @@
/*
* 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 { render, waitFor, cleanup } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common';
import type { MaintenanceWindow } from '@kbn/alerting-plugin/common';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock';
import { MaintenanceWindowCallout } from './maintenance_window_callout';
import { TestProviders } from '../../../../common/mock';
import { fetchActiveMaintenanceWindows } from './api';
jest.mock('../../../../common/hooks/use_app_toasts');
jest.mock('./api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
}));
const RUNNING_MAINTENANCE_WINDOW_1: Partial<MaintenanceWindow> = {
title: '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' }],
};
const RUNNING_MAINTENANCE_WINDOW_2: Partial<MaintenanceWindow> = {
title: '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' }],
};
const UPCOMING_MAINTENANCE_WINDOW: Partial<MaintenanceWindow> = {
title: 'Upcoming maintenance window',
id: '5eafe070-e030-11ed-ac81-bfcb4982b4fd',
status: MaintenanceWindowStatus.Upcoming,
events: [
{ gte: '2023-04-21T10:36:14.028Z', lte: '2023-04-21T10:37:00.000Z' },
{ gte: '2023-04-28T10:36:14.028Z', lte: '2023-04-28T10:37:00.000Z' },
],
};
describe('MaintenanceWindowCallout', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
beforeEach(() => {
jest.resetAllMocks();
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
});
afterEach(() => {
cleanup();
jest.restoreAllMocks();
});
it('should be visible if currently there is at least one "running" maintenance window', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
const { findByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
expect(await findByText('A maintenance window is currently running')).toBeInTheDocument();
});
it('should be visible if currently there are multiple "running" maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([
RUNNING_MAINTENANCE_WINDOW_1,
RUNNING_MAINTENANCE_WINDOW_2,
]);
const { findAllByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
expect(await findAllByText('A maintenance window is currently running')).toHaveLength(1);
});
it('should NOT be visible if currently there are no active (running or upcoming) maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([]);
const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
expect(container).toBeEmptyDOMElement();
});
it('should NOT be visible if currently there are no "running" maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([UPCOMING_MAINTENANCE_WINDOW]);
const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
expect(container).toBeEmptyDOMElement();
});
it('should see an error toast if there was an error while fetching maintenance windows', async () => {
const createReactQueryWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Turn retries off, otherwise we won't be able to test errors
retry: false,
},
},
logger: {
// Turn network error logging off, so we don't log the failed request to the console
error: () => {},
// eslint-disable-next-line no-console
log: console.log,
// eslint-disable-next-line no-console
warn: console.warn,
},
});
const wrapper: React.FC = ({ children }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
return wrapper;
};
const mockError = new Error('Network error');
(fetchActiveMaintenanceWindows as jest.Mock).mockRejectedValue(mockError);
render(<MaintenanceWindowCallout />, { wrapper: createReactQueryWrapper() });
await waitFor(() => {
expect(appToastsMock.addError).toHaveBeenCalledTimes(1);
expect(appToastsMock.addError).toHaveBeenCalledWith(mockError, {
title: 'Failed to check if any maintenance window is currently running',
toastMessage: "Notification actions won't run while a maintenance window is running.",
});
});
});
});

View file

@ -0,0 +1,27 @@
/*
* 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 { EuiCallOut } from '@elastic/eui';
import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common';
import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows';
import * as i18n from './translations';
export function MaintenanceWindowCallout(): JSX.Element | null {
const { data } = useFetchActiveMaintenanceWindows();
const activeMaintenanceWindows = data || [];
if (activeMaintenanceWindows.some(({ status }) => status === MaintenanceWindowStatus.Running)) {
return (
<EuiCallOut title={i18n.MAINTENANCE_WINDOW_RUNNING} color="warning" iconType="iInCircle">
{i18n.MAINTENANCE_WINDOW_RUNNING_DESCRIPTION}
</EuiCallOut>
);
}
return null;
}

View file

@ -0,0 +1,36 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const MAINTENANCE_WINDOW_RUNNING = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.maintenanceWindowActive',
{
defaultMessage: 'A maintenance window is currently running',
}
);
export const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.maintenanceWindowActiveDescription',
{
defaultMessage: "Notification actions won't run while a maintenance window is running.",
}
);
export const FETCH_ERROR = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.fetchError',
{
defaultMessage: 'Failed to check if any maintenance window is currently running',
}
);
export const FETCH_ERROR_DESCRIPTION = i18n.translate(
'xpack.securitySolution.detectionEngine.ruleManagementUi.maintenanceWindowCallout.fetchErrorDescription',
{
defaultMessage: "Notification actions won't run while a maintenance window is running.",
}
);

View file

@ -0,0 +1,27 @@
/*
* 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 { useQuery } from '@tanstack/react-query';
import { INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH } from '@kbn/alerting-plugin/common';
import { useAppToasts } from '../../../../common/hooks/use_app_toasts';
import * as i18n from './translations';
import { fetchActiveMaintenanceWindows } from './api';
export const useFetchActiveMaintenanceWindows = () => {
const { addError } = useAppToasts();
return useQuery(
['GET', INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH],
({ signal }) => fetchActiveMaintenanceWindows(signal),
{
refetchInterval: 60000,
onError: (error) => {
addError(error, { title: i18n.FETCH_ERROR, toastMessage: i18n.FETCH_ERROR_DESCRIPTION });
},
}
);
};

View file

@ -42,6 +42,8 @@ import { RulesTableContextProvider } from '../../components/rules_table/rules_ta
import * as i18n from '../../../../detections/pages/detection_engine/rules/translations';
import { useInvalidateFetchRuleManagementFiltersQuery } from '../../../rule_management/api/hooks/use_fetch_rule_management_filters_query';
import { MaintenanceWindowCallout } from '../../components/maintenance_window_callout/maintenance_window_callout';
const RulesPageComponent: React.FC = () => {
const [isImportModalVisible, showImportModal, hideImportModal] = useBoolState();
const [isValueListFlyoutVisible, showValueListFlyout, hideValueListFlyout] = useBoolState();
@ -158,6 +160,7 @@ const RulesPageComponent: React.FC = () => {
prePackagedTimelineStatus === 'timelineNeedUpdate') && (
<UpdatePrePackagedRulesCallOut data-test-subj="update-callout-button" />
)}
<MaintenanceWindowCallout />
<AllRules data-test-subj="all-rules" />
</SecuritySolutionPageWrapper>
</RulesTableContextProvider>