[RAM] add maintenance window banner (#163516)

## Summary

Solves: https://github.com/elastic/kibana/issues/163465

Add maintenance window banner to Rules list and Alerts list in O11y and
Management.

<img width="1334" alt="Screenshot 2023-08-09 at 13 03 50"
src="de0708b1-db2a-4517-91aa-a3d6b3e62b44">

<img width="1350" alt="Screenshot 2023-08-09 at 13 05 10"
src="9f7c488d-e992-4807-a60e-3c077b623b4e">

---------

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia 2023-08-17 13:04:56 +02:00 committed by GitHub
parent 9079b1c60b
commit b3122cb656
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 306 additions and 59 deletions

View file

@ -10,14 +10,15 @@ import React from 'react';
import { I18nProvider } from '@kbn/i18n-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, waitFor, cleanup } from '@testing-library/react';
import {
MaintenanceWindowStatus,
MAINTENANCE_WINDOW_FEATURE_ID,
} from '@kbn/alerting-plugin/common';
import type { MaintenanceWindow } from '@kbn/alerting-plugin/common';
import type { AsApiContract } from '@kbn/alerting-plugin/server/routes/lib';
import { MAINTENANCE_WINDOW_FEATURE_ID } from '@kbn/alerting-plugin/common';
import { MaintenanceWindowCallout } from '.';
import { fetchActiveMaintenanceWindows } from './api';
import {
RECURRING_RUNNING_MAINTENANCE_WINDOW,
RUNNING_MAINTENANCE_WINDOW_1,
RUNNING_MAINTENANCE_WINDOW_2,
UPCOMING_MAINTENANCE_WINDOW,
} from './mock';
jest.mock('./api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
@ -32,49 +33,6 @@ const TestProviders: React.FC<{}> = ({ children }) => {
);
};
const RUNNING_MAINTENANCE_WINDOW_1: Partial<MaintenanceWindow> = {
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' }],
};
const RUNNING_MAINTENANCE_WINDOW_2: Partial<MaintenanceWindow> = {
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' }],
};
const RECURRING_RUNNING_MAINTENANCE_WINDOW: Partial<AsApiContract<MaintenanceWindow>> = {
title: 'Recurring running maintenance window',
id: 'e2228300-e9ad-11ed-ba37-db17c6e6182b',
status: MaintenanceWindowStatus.Running,
events: [
{ gte: '2023-05-03T12:27:18.569Z', lte: '2023-05-03T12:57:18.569Z' },
{ gte: '2023-05-10T12:27:18.569Z', lte: '2023-05-10T12:57:18.569Z' },
],
expiration_date: '2024-05-03T12:27:35.088Z',
r_rule: {
dtstart: '2023-05-03T12:27:18.569Z',
tzid: 'Europe/Amsterdam',
freq: 3,
interval: 1,
count: 2,
byweekday: ['WE'],
},
};
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' },
],
};
const fetchActiveMaintenanceWindowsMock = fetchActiveMaintenanceWindows as jest.Mock;
const kibanaServicesMock = {

View file

@ -50,7 +50,12 @@ export function MaintenanceWindowCallout({
if (activeMaintenanceWindows.some(({ status }) => status === MaintenanceWindowStatus.Running)) {
return (
<EuiCallOut title={MAINTENANCE_WINDOW_RUNNING} color="warning" iconType="iInCircle">
<EuiCallOut
title={MAINTENANCE_WINDOW_RUNNING}
color="warning"
iconType="iInCircle"
data-test-subj="maintenanceWindowCallout"
>
{MAINTENANCE_WINDOW_RUNNING_DESCRIPTION}
</EuiCallOut>
);

View file

@ -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 type { AsApiContract } from '@kbn/alerting-plugin/server/routes/lib';
import { MaintenanceWindow, MaintenanceWindowStatus } from './types';
export const RUNNING_MAINTENANCE_WINDOW_1: Partial<MaintenanceWindow> = {
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> = {
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>> = {
title: 'Recurring running maintenance window',
id: 'e2228300-e9ad-11ed-ba37-db17c6e6182b',
status: MaintenanceWindowStatus.Running,
events: [
{ gte: '2023-05-03T12:27:18.569Z', lte: '2023-05-03T12:57:18.569Z' },
{ gte: '2023-05-10T12:27:18.569Z', lte: '2023-05-10T12:57:18.569Z' },
],
expiration_date: '2024-05-03T12:27:35.088Z',
r_rule: {
dtstart: '2023-05-03T12:27:18.569Z',
tzid: 'Europe/Amsterdam',
freq: 3,
interval: 1,
count: 2,
byweekday: ['WE'],
},
};
export 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' },
],
};

View file

@ -0,0 +1,184 @@
/*
* 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 } from '@testing-library/react';
import { CoreStart } from '@kbn/core/public';
import { AppMountParameters } from '@kbn/core/public';
import { TimeBuckets } from '@kbn/data-plugin/common';
import { fetchActiveMaintenanceWindows } from '@kbn/alerts-ui-shared/src/maintenance_window_callout/api';
import { RUNNING_MAINTENANCE_WINDOW_1 } from '@kbn/alerts-ui-shared/src/maintenance_window_callout/mock';
import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { MAINTENANCE_WINDOW_FEATURE_ID } from '@kbn/alerting-plugin/common/maintenance_window';
import { ObservabilityPublicPluginsStart } from '../../plugin';
import { AlertsPage } from './alerts';
import { kibanaStartMock } from '../../utils/kibana_react.mock';
import * as pluginContext from '../../hooks/use_plugin_context';
import * as dataContext from '../../hooks/use_has_data';
import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock';
import { ThemeProvider } from '@emotion/react';
import { euiDarkVars } from '@kbn/ui-theme';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const mockUseKibanaReturnValue = kibanaStartMock.startContract();
mockUseKibanaReturnValue.services.application.capabilities = {
...mockUseKibanaReturnValue.services.application.capabilities,
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: true,
show: true,
},
};
jest.mock('../../utils/kibana_react', () => ({
__esModule: true,
useKibana: jest.fn(() => mockUseKibanaReturnValue),
}));
jest.mock('@kbn/kibana-react-plugin/public', () => ({
__esModule: true,
useKibana: jest.fn(() => mockUseKibanaReturnValue),
}));
jest.mock('@kbn/observability-shared-plugin/public');
jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({
appMountParameters: {
setHeaderActionMenu: () => {},
} as unknown as AppMountParameters,
config: {
unsafe: {
slo: { enabled: false },
alertDetails: {
apm: { enabled: false },
logs: { enabled: false },
metrics: { enabled: false },
uptime: { enabled: false },
observability: { enabled: false },
},
thresholdRule: { enabled: false },
},
compositeSlo: {
enabled: false,
},
aiAssistant: {
enabled: false,
feedback: {
enabled: false,
},
},
},
observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(),
ObservabilityPageTemplate: KibanaPageTemplate,
kibanaFeatures: [],
core: {} as CoreStart,
plugins: {} as ObservabilityPublicPluginsStart,
hasAnyData: true,
isAllRequestsComplete: true,
}));
jest.spyOn(dataContext, 'useHasData').mockImplementation(() => ({
hasDataMap: {},
hasAnyData: true,
isAllRequestsComplete: true,
onRefreshTimeRange: jest.fn(),
forceUpdate: 'false',
}));
jest.mock('@kbn/alerts-ui-shared/src/maintenance_window_callout/api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
}));
const fetchActiveMaintenanceWindowsMock = fetchActiveMaintenanceWindows as jest.Mock;
jest.mock('../../hooks/use_time_buckets', () => ({
useTimeBuckets: jest.fn(),
}));
jest.mock('../../hooks/use_has_data', () => ({
useHasData: jest.fn(),
}));
const { useTimeBuckets } = jest.requireMock('../../hooks/use_time_buckets');
const { useHasData } = jest.requireMock('../../hooks/use_has_data');
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
cacheTime: 0,
},
},
});
function AllTheProviders({ children }: { children: any }) {
return (
<ThemeProvider
theme={() => ({ eui: { ...euiDarkVars, euiColorLightShade: '#ece' }, darkMode: true })}
>
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</IntlProvider>
</ThemeProvider>
);
}
describe('AlertsPage with all capabilities', () => {
const timeBuckets = new TimeBuckets({
'histogram:maxBars': 12,
'histogram:barTarget': 10,
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
'dateFormat:scaled': [
['', 'HH:mm:ss.SSS'],
['PT1S', 'HH:mm:ss'],
['PT1M', 'HH:mm'],
['PT1H', 'YYYY-MM-DD HH:mm'],
['P1DT', 'YYYY-MM-DD'],
['P1YT', 'YYYY'],
],
});
async function setup() {
return render(<AlertsPage />, { wrapper: AllTheProviders });
}
beforeAll(() => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([]);
useHasData.mockReturnValue({
hasDataMap: {
apm: { hasData: true, status: 'success' },
synthetics: { hasData: true, status: 'success' },
infra_logs: { hasData: undefined, status: 'success' },
infra_metrics: { hasData: true, status: 'success' },
ux: { hasData: undefined, status: 'success' },
alert: { hasData: false, status: 'success' },
},
hasAnyData: true,
isAllRequestsComplete: true,
onRefreshTimeRange: () => {},
forceUpdate: '',
});
});
beforeEach(() => {
fetchActiveMaintenanceWindowsMock.mockClear();
useTimeBuckets.mockReturnValue(timeBuckets);
});
it('should render an alerts page template', async () => {
const wrapper = await setup();
await waitFor(() => {
expect(wrapper.getByText('Alerts')).toBeInTheDocument();
});
});
it('renders MaintenanceWindowCallout if one exists', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
const wrapper = await setup();
await waitFor(() => {
expect(wrapper.getByTestId('maintenanceWindowCallout')).toBeInTheDocument();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
});
});

View file

@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n';
import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public';
import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared';
import { useKibana } from '../../utils/kibana_react';
import { useHasData } from '../../hooks/use_has_data';
@ -41,6 +42,7 @@ const DEFAULT_INTERVAL = '60s';
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD HH:mm';
function InternalAlertsPage() {
const kibanaServices = useKibana().services;
const {
charts,
data: {
@ -56,7 +58,7 @@ function InternalAlertsPage() {
getAlertsStateTable: AlertsStateTable,
getAlertSummaryWidget: AlertSummaryWidget,
},
} = useKibana().services;
} = kibanaServices;
const { ObservabilityPageTemplate, observabilityRuleTypeRegistry } = usePluginContext();
const alertSearchBarStateProps = useAlertSearchBarStateContainer(ALERTS_URL_STORAGE_KEY, {
replace: false,
@ -178,6 +180,9 @@ function InternalAlertsPage() {
>
<HeaderMenu />
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<MaintenanceWindowCallout kibanaServices={kibanaServices} />
</EuiFlexItem>
<EuiFlexItem>
<ObservabilityAlertSearchBar
{...alertSearchBarStateProps}

View file

@ -8,6 +8,7 @@
import React from 'react';
import { EuiButtonEmpty, EuiStat } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { euiThemeVars } from '@kbn/ui-theme';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
export interface RuleStatsState {
@ -20,7 +21,7 @@ export interface RuleStatsState {
type StatType = 'disabled' | 'snoozed' | 'error';
const Divider = euiStyled.div`
border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
border-right: 1px solid ${euiThemeVars.euiColorLightShade};
height: 100%;
`;

View file

@ -6,10 +6,14 @@
*/
import React from 'react';
import { mockCasesContract } from '@kbn/cases-plugin/public/mocks';
import { timefilterServiceMock } from '@kbn/data-plugin/public/query/timefilter/timefilter_service.mock';
const triggersActionsUiStartMock = {
createStart() {
return {
getAlertSummaryWidget: jest.fn(() => (
<div data-test-subj="alerts-summary-widget">mocked component</div>
)),
getAlertsSearchBar: jest.fn(() => (
<div data-test-subj="alerts-search-bar">mocked component</div>
)),
@ -67,9 +71,7 @@ const data = {
create: jest.fn(),
},
query: {
timefilter: {
timefilter: jest.fn(),
},
timefilter: timefilterServiceMock.createSetupContract(),
},
search: {
searchSource: {

View file

@ -19,6 +19,7 @@ export const kibanaStartMock = {
...observabilityPublicPluginsStartMock.createStart(),
storage: coreMock.createStorage(),
cases: { ...casesPluginMock.createStartContract() },
charts: { theme: { useChartsTheme: () => {}, useChartsBaseTheme: () => {} } },
},
};
},

View file

@ -14,6 +14,9 @@ import {
waitForElementToBeRemoved,
cleanup,
} from '@testing-library/react';
import { fetchActiveMaintenanceWindows } from '@kbn/alerts-ui-shared/src/maintenance_window_callout/api';
import { RUNNING_MAINTENANCE_WINDOW_1 } from '@kbn/alerts-ui-shared/src/maintenance_window_callout/mock';
import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
import { percentileFields, RulesList } from './rules_list';
@ -38,7 +41,7 @@ import {
getDisabledByLicenseRuleTypeFromApi,
} from './test_helpers';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { parseDuration } from '@kbn/alerting-plugin/common';
import { MAINTENANCE_WINDOW_FEATURE_ID, parseDuration } from '@kbn/alerting-plugin/common';
import { getFormattedDuration } from '../../../lib/monitoring_utils';
jest.mock('../../../../common/lib/kibana');
@ -104,6 +107,12 @@ jest.mock('react-router-dom', () => ({
pathname: '/triggersActions/rules/',
}),
}));
jest.mock('@kbn/alerts-ui-shared/src/maintenance_window_callout/api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
}));
const fetchActiveMaintenanceWindowsMock = fetchActiveMaintenanceWindows as jest.Mock;
jest.mock('../../../lib/capabilities', () => ({
hasAllPrivilege: jest.fn(() => true),
hasSaveRulesCapability: jest.fn(() => true),
@ -152,6 +161,7 @@ describe('Update Api Key', () => {
const addDanger = jest.fn();
beforeAll(() => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([]);
loadRulesWithKueryFilter.mockResolvedValue({
page: 1,
perPage: 10000,
@ -161,6 +171,13 @@ describe('Update Api Key', () => {
loadActionTypes.mockResolvedValue([]);
loadRuleTypes.mockResolvedValue([ruleTypeFromApi]);
loadAllActions.mockResolvedValue([]);
useKibanaMock().services.application.capabilities = {
...useKibanaMock().services.application.capabilities,
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: true,
show: true,
},
};
useKibanaMock().services.notifications.toasts = {
addSuccess,
addError,
@ -197,6 +214,7 @@ describe('Update Api Key', () => {
describe('rules_list component empty', () => {
beforeEach(() => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([]);
loadRulesWithKueryFilter.mockResolvedValue({
page: 1,
perPage: 10000,
@ -228,7 +246,13 @@ describe('rules_list component empty', () => {
ruleTypeRegistry.list.mockReturnValue([ruleType]);
actionTypeRegistry.list.mockReturnValue([]);
useKibanaMock().services.application.capabilities = {
...useKibanaMock().services.application.capabilities,
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: true,
show: true,
},
};
useKibanaMock().services.ruleTypeRegistry = ruleTypeRegistry;
useKibanaMock().services.actionTypeRegistry = actionTypeRegistry;
});
@ -244,6 +268,13 @@ describe('rules_list component empty', () => {
expect(await screen.findByTestId('createFirstRuleEmptyPrompt')).toBeInTheDocument();
});
it('renders MaintenanceWindowCallout if one exists', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
renderWithProviders(<RulesList />);
expect(await screen.findByText('Maintenance window is running')).toBeInTheDocument();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
it('renders Create rule button', async () => {
renderWithProviders(<RulesList showCreateRuleButtonInPrompt />);

View file

@ -39,6 +39,7 @@ import {
RuleLastRunOutcomeValues,
} from '@kbn/alerting-plugin/common';
import { ruleDetailsRoute as commonRuleDetailsRoute } from '@kbn/rule-data-utils';
import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared';
import {
Rule,
RuleTableItem,
@ -169,6 +170,7 @@ export const RulesList = ({
setHeaderActions,
}: RulesListProps) => {
const history = useHistory();
const kibanaServices = useKibana().services;
const {
actionTypeRegistry,
application: { capabilities },
@ -176,7 +178,7 @@ export const RulesList = ({
kibanaFeatures,
notifications: { toasts },
ruleTypeRegistry,
} = useKibana().services;
} = kibanaServices;
const canExecuteActions = hasExecuteActionsCapability(capabilities);
const [isPerformingAction, setIsPerformingAction] = useState<boolean>(false);
const [page, setPage] = useState<Pagination>({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE });
@ -722,7 +724,7 @@ export const RulesList = ({
{showSearchBar && !isEmpty(filters.ruleParams) ? (
<RulesListClearRuleFilterBanner onClickClearFilter={handleClearRuleParamFilter} />
) : null}
<MaintenanceWindowCallout kibanaServices={kibanaServices} />
<RulesListPrompts
showNoAuthPrompt={showNoAuthPrompt}
showCreateFirstRulePrompt={showCreateFirstRulePrompt}

View file

@ -92,6 +92,8 @@ jest.mock('../../../lib/capabilities', () => ({
jest.mock('../../../../common/get_experimental_features', () => ({
getIsExperimentalFeatureEnabled: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
'../../../lib/rule_api/aggregate_kuery_filter'
);

View file

@ -91,6 +91,7 @@ jest.mock('../../../../common/get_experimental_features', () => ({
jest.mock('../../../lib/rule_api/aggregate_kuery_filter', () => ({
loadRuleAggregationsWithKueryFilter: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
'../../../lib/rule_api/aggregate_kuery_filter'

View file

@ -90,6 +90,7 @@ jest.mock('../../../../common/get_experimental_features', () => ({
jest.mock('../../../lib/rule_api/aggregate_kuery_filter', () => ({
loadRuleAggregationsWithKueryFilter: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
'../../../lib/rule_api/aggregate_kuery_filter'

View file

@ -91,6 +91,7 @@ jest.mock('../../../../common/get_experimental_features', () => ({
jest.mock('../../../lib/rule_api/aggregate_kuery_filter', () => ({
loadRuleAggregationsWithKueryFilter: jest.fn(),
}));
jest.mock('@kbn/alerts-ui-shared', () => ({ MaintenanceWindowCallout: jest.fn(() => <></>) }));
const { loadRuleAggregationsWithKueryFilter } = jest.requireMock(
'../../../lib/rule_api/aggregate_kuery_filter'