Skip to content

Commit

Permalink
[RAM] add maintenance window banner (elastic#163516)
Browse files Browse the repository at this point in the history
## Summary

Solves: elastic#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="https://github.com/elastic/kibana/assets/26089545/de0708b1-db2a-4517-91aa-a3d6b3e62b44">

<img width="1350" alt="Screenshot 2023-08-09 at 13 05 10"
src="https://github.com/elastic/kibana/assets/26089545/9f7c488d-e992-4807-a60e-3c077b623b4e">

---------

Co-authored-by: Xavier Mouligneau <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
3 people authored Aug 17, 2023
1 parent 9079b1c commit b3122cb
Show file tree
Hide file tree
Showing 14 changed files with 306 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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([])),
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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' },
],
};
184 changes: 184 additions & 0 deletions x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
7 changes: 6 additions & 1 deletion x-pack/plugins/observability/public/pages/alerts/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand All @@ -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,
Expand Down Expand Up @@ -178,6 +180,9 @@ function InternalAlertsPage() {
>
<HeaderMenu />
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<MaintenanceWindowCallout kibanaServices={kibanaServices} />
</EuiFlexItem>
<EuiFlexItem>
<ObservabilityAlertSearchBar
{...alertSearchBarStateProps}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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%;
`;

Expand Down
Loading

0 comments on commit b3122cb

Please sign in to comment.