Skip to content

Commit

Permalink
[RAM] Move maintenance window callout to @kbn/alerts-ui-shared package (
Browse files Browse the repository at this point in the history
#160678)

## Summary

Moves the Security Solution's `MaintenanceWindowCallout` into a shared
KBN package to make it more accessible by other plugins.

### 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] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
Zacqary and kibanamachine authored Jul 6, 2023
1 parent bcb1649 commit 0dc7312
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 204 deletions.
1 change: 1 addition & 0 deletions packages/kbn-alerts-ui-shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export { AlertLifecycleStatusBadge } from './src/alert_lifecycle_status_badge';
export type { AlertLifecycleStatusBadgeProps } from './src/alert_lifecycle_status_badge';
export { MaintenanceWindowCallout } from './src/maintenance_window_callout';
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { KibanaServices } from './types';

export const fetchActiveMaintenanceWindows = async (
http: KibanaServices['http'],
signal?: AbortSignal
) =>
http.fetch(INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, {
method: 'GET',
signal,
});

const INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH = `/internal/alerting/rules/maintenance_window/_active`;
Original file line number Diff line number Diff line change
@@ -1,33 +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.
* 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 React from 'react';
import { render, waitFor, cleanup } from '@testing-library/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 { useKibana } from '../../../../common/lib/kibana';
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 { MaintenanceWindowCallout } from '.';
import { fetchActiveMaintenanceWindows } from './api';

jest.mock('../../../../common/hooks/use_app_toasts');

jest.mock('./api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
}));

jest.mock('../../../../common/lib/kibana');
const TestProviders: React.FC<{}> = ({ children }) => {
const queryClient = new QueryClient();
return (
<I18nProvider>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</I18nProvider>
);
};

const RUNNING_MAINTENANCE_WINDOW_1: Partial<MaintenanceWindow> = {
title: 'Running maintenance window 1',
Expand Down Expand Up @@ -72,29 +75,37 @@ const UPCOMING_MAINTENANCE_WINDOW: Partial<MaintenanceWindow> = {
],
};

const useKibanaMock = useKibana as jest.Mock;
const fetchActiveMaintenanceWindowsMock = fetchActiveMaintenanceWindows as jest.Mock;

describe('MaintenanceWindowCallout', () => {
let appToastsMock: jest.Mocked<ReturnType<typeof useAppToastsMock.create>>;
const kibanaServicesMock = {
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: true,
show: true,
},
},
},
notifications: {
toasts: {
addError: jest.fn(),
add: jest.fn(),
remove: jest.fn(),
get$: jest.fn(),
addInfo: jest.fn(),
addWarning: jest.fn(),
addDanger: jest.fn(),
addSuccess: jest.fn(),
},
},
http: {
fetch: jest.fn(),
},
};

describe('MaintenanceWindowCallout', () => {
beforeEach(() => {
jest.resetAllMocks();

appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: true,
show: true,
},
},
},
},
});
});

afterEach(() => {
Expand All @@ -105,7 +116,10 @@ describe('MaintenanceWindowCallout', () => {
it('should be visible if currently there is at least one "running" maintenance window', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);

const { findAllByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
const { findAllByText } = render(
<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />,
{ wrapper: TestProviders }
);

expect(await findAllByText('Maintenance window is running')).toHaveLength(1);
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
Expand All @@ -117,7 +131,10 @@ describe('MaintenanceWindowCallout', () => {
RUNNING_MAINTENANCE_WINDOW_2,
]);

const { findAllByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
const { findAllByText } = render(
<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />,
{ wrapper: TestProviders }
);

expect(await findAllByText('Maintenance window is running')).toHaveLength(1);
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
Expand All @@ -126,8 +143,12 @@ describe('MaintenanceWindowCallout', () => {
it('should be visible if currently there is a recurring "running" maintenance window', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RECURRING_RUNNING_MAINTENANCE_WINDOW]);

const { findByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
const { findByText } = render(
<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />,
{ wrapper: TestProviders }
);

// @ts-expect-error Jest types are incomplete in packages
expect(await findByText('Maintenance window is running')).toBeInTheDocument();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
Expand All @@ -137,17 +158,21 @@ describe('MaintenanceWindowCallout', () => {
[] // API returns an empty array if there are no active maintenance windows
);

const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

const { container } = render(<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />, {
wrapper: TestProviders,
});
// @ts-expect-error Jest types are incomplete in packages
expect(container).toBeEmptyDOMElement();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});

it('should NOT be visible if currently there are only "upcoming" maintenance windows', async () => {
fetchActiveMaintenanceWindowsMock.mockResolvedValue([UPCOMING_MAINTENANCE_WINDOW]);

const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

const { container } = render(<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />, {
wrapper: TestProviders,
});
// @ts-expect-error Jest types are incomplete in packages
expect(container).toBeEmptyDOMElement();
expect(fetchActiveMaintenanceWindowsMock).toHaveBeenCalledTimes(1);
});
Expand Down Expand Up @@ -179,54 +204,58 @@ describe('MaintenanceWindowCallout', () => {
const mockError = new Error('Network error');
fetchActiveMaintenanceWindowsMock.mockRejectedValue(mockError);

render(<MaintenanceWindowCallout />, { wrapper: createReactQueryWrapper() });
render(<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />, {
wrapper: createReactQueryWrapper(),
});

await waitFor(() => {
expect(appToastsMock.addError).toHaveBeenCalledTimes(1);
expect(appToastsMock.addError).toHaveBeenCalledWith(mockError, {
expect(kibanaServicesMock.notifications.toasts.addError).toHaveBeenCalledTimes(1);
expect(kibanaServicesMock.notifications.toasts.addError).toHaveBeenCalledWith(mockError, {
title: 'Failed to check if maintenance windows are active',
toastMessage: 'Rule notifications are stopped while the maintenance window is running.',
});
});
});

it('should return null if window maintenance privilege is NONE', async () => {
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: false,
show: false,
},
const servicesMock = {
...kibanaServicesMock,
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: false,
show: false,
},
},
},
});
};
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);

const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

const { container } = render(<MaintenanceWindowCallout kibanaServices={servicesMock} />, {
wrapper: TestProviders,
});
// @ts-expect-error Jest types are incomplete in packages
expect(container).toBeEmptyDOMElement();
});

it('should work as expected if window maintenance privilege is READ ', async () => {
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: false,
show: true,
},
const servicesMock = {
...kibanaServicesMock,
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: false,
show: true,
},
},
},
});
};
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);

const { findByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });

const { findByText } = render(<MaintenanceWindowCallout kibanaServices={servicesMock} />, {
wrapper: TestProviders,
});
// @ts-expect-error Jest types are incomplete in packages
expect(await findByText('Maintenance window is running')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut } from '@elastic/eui';
import { MaintenanceWindow, MaintenanceWindowStatus, KibanaServices } from './types';
import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows';

const MAINTENANCE_WINDOW_FEATURE_ID = 'maintenanceWindow';
const MAINTENANCE_WINDOW_RUNNING = i18n.translate(
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActive',
{
defaultMessage: 'Maintenance window is running',
}
);
const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate(
'alertsUIShared.maintenanceWindowCallout.maintenanceWindowActiveDescription',
{
defaultMessage: 'Rule notifications are stopped while the maintenance window is running.',
}
);

export function MaintenanceWindowCallout({
kibanaServices,
}: {
kibanaServices: KibanaServices;
}): JSX.Element | null {
const {
application: { capabilities },
} = kibanaServices;

const isMaintenanceWindowDisabled =
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID].show &&
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID].save;
const { data } = useFetchActiveMaintenanceWindows(kibanaServices, {
enabled: !isMaintenanceWindowDisabled,
});

if (isMaintenanceWindowDisabled) {
return null;
}

const activeMaintenanceWindows = (data as MaintenanceWindow[]) || [];

if (activeMaintenanceWindows.some(({ status }) => status === MaintenanceWindowStatus.Running)) {
return (
<EuiCallOut title={MAINTENANCE_WINDOW_RUNNING} color="warning" iconType="iInCircle">
{MAINTENANCE_WINDOW_RUNNING_DESCRIPTION}
</EuiCallOut>
);
}

return null;
}
Loading

0 comments on commit 0dc7312

Please sign in to comment.