[8.8] [RAM] Apply maintenance windows privilege to UI (#156191) (#156387)

# Backport

This will backport the following commits from `main` to `8.8`:
- [[RAM] Apply maintenance windows privilege to UI
(#156191)](https://github.com/elastic/kibana/pull/156191)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Xavier
Mouligneau","email":"xavier.mouligneau@elastic.co"},"sourceCommit":{"committedDate":"2023-05-02T13:47:29Z","message":"[RAM]
Apply maintenance windows privilege to UI (#156191)\n\n##
Summary\r\n\r\nWe will have three scenarios with kibana
privileges\r\n\r\n### NONE\r\nKibana privileges form maintenance
window:\r\n<img width=\"680\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188523-acaff7de-54d4-4991-a014-05c0f449738c.png\">\r\n\r\n`The
expected result is to not see maintenance window at all`\r\n\r\n<img
width=\"1481\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188658-6a53b463-4856-42c7-916e-aa8e6d7e326b.png\">\r\n\r\n###
READ\r\nKibana privileges form maintenance window:\r\n<img width=\"677\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188908-623d32ac-39a7-484e-bd5c-f858e04d16b2.png\">\r\n\r\n`The
expected result is to only see the table with window maintenance\r\nand
you can not edit them`\r\n\r\n<img width=\"1487\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189169-f71422bf-6394-4574-87fb-14c653ca1e79.png\">\r\n<img
width=\"1484\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235192048-149519ba-0505-46e3-b737-2703560eb3d6.png\">\r\n\r\n\r\n###
ALL\r\nKibana privileges form maintenance window:\r\n<img width=\"668\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189384-e71d9138-221c-4024-91bb-2ae32da1bd3b.png\">\r\n\r\n`The
expected result is to be able to create/edit/etc on any
maintenance\r\nwindows`\r\n\r\n<img width=\"1484\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189974-e36c1e65-0586-4840-ace5-32caf06455c6.png\">\r\n<img
width=\"1481\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235192269-0f8d1922-d48f-494c-9979-2288bf142286.png\">\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3c9da2cd296f9e23b5052e8bf624ddd062bcbcd0","branchLabelMapping":{"^v8.9.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","impact:high","Team:ResponseOps","v8.8.0","v8.9.0"],"number":156191,"url":"https://github.com/elastic/kibana/pull/156191","mergeCommit":{"message":"[RAM]
Apply maintenance windows privilege to UI (#156191)\n\n##
Summary\r\n\r\nWe will have three scenarios with kibana
privileges\r\n\r\n### NONE\r\nKibana privileges form maintenance
window:\r\n<img width=\"680\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188523-acaff7de-54d4-4991-a014-05c0f449738c.png\">\r\n\r\n`The
expected result is to not see maintenance window at all`\r\n\r\n<img
width=\"1481\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188658-6a53b463-4856-42c7-916e-aa8e6d7e326b.png\">\r\n\r\n###
READ\r\nKibana privileges form maintenance window:\r\n<img width=\"677\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188908-623d32ac-39a7-484e-bd5c-f858e04d16b2.png\">\r\n\r\n`The
expected result is to only see the table with window maintenance\r\nand
you can not edit them`\r\n\r\n<img width=\"1487\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189169-f71422bf-6394-4574-87fb-14c653ca1e79.png\">\r\n<img
width=\"1484\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235192048-149519ba-0505-46e3-b737-2703560eb3d6.png\">\r\n\r\n\r\n###
ALL\r\nKibana privileges form maintenance window:\r\n<img width=\"668\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189384-e71d9138-221c-4024-91bb-2ae32da1bd3b.png\">\r\n\r\n`The
expected result is to be able to create/edit/etc on any
maintenance\r\nwindows`\r\n\r\n<img width=\"1484\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189974-e36c1e65-0586-4840-ace5-32caf06455c6.png\">\r\n<img
width=\"1481\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235192269-0f8d1922-d48f-494c-9979-2288bf142286.png\">\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3c9da2cd296f9e23b5052e8bf624ddd062bcbcd0"}},"sourceBranch":"main","suggestedTargetBranches":["8.8"],"targetPullRequestStates":[{"branch":"8.8","label":"v8.8.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.9.0","labelRegex":"^v8.9.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/156191","number":156191,"mergeCommit":{"message":"[RAM]
Apply maintenance windows privilege to UI (#156191)\n\n##
Summary\r\n\r\nWe will have three scenarios with kibana
privileges\r\n\r\n### NONE\r\nKibana privileges form maintenance
window:\r\n<img width=\"680\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188523-acaff7de-54d4-4991-a014-05c0f449738c.png\">\r\n\r\n`The
expected result is to not see maintenance window at all`\r\n\r\n<img
width=\"1481\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188658-6a53b463-4856-42c7-916e-aa8e6d7e326b.png\">\r\n\r\n###
READ\r\nKibana privileges form maintenance window:\r\n<img width=\"677\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235188908-623d32ac-39a7-484e-bd5c-f858e04d16b2.png\">\r\n\r\n`The
expected result is to only see the table with window maintenance\r\nand
you can not edit them`\r\n\r\n<img width=\"1487\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189169-f71422bf-6394-4574-87fb-14c653ca1e79.png\">\r\n<img
width=\"1484\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235192048-149519ba-0505-46e3-b737-2703560eb3d6.png\">\r\n\r\n\r\n###
ALL\r\nKibana privileges form maintenance window:\r\n<img width=\"668\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189384-e71d9138-221c-4024-91bb-2ae32da1bd3b.png\">\r\n\r\n`The
expected result is to be able to create/edit/etc on any
maintenance\r\nwindows`\r\n\r\n<img width=\"1484\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235189974-e36c1e65-0586-4840-ace5-32caf06455c6.png\">\r\n<img
width=\"1481\"
alt=\"image\"\r\nsrc=\"https://user-images.githubusercontent.com/189600/235192269-0f8d1922-d48f-494c-9979-2288bf142286.png\">\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"3c9da2cd296f9e23b5052e8bf624ddd062bcbcd0"}}]}]
BACKPORT-->

Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
Kibana Machine 2023-05-02 11:26:07 -04:00 committed by GitHub
parent 7f7062543f
commit ad65768a1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 327 additions and 42 deletions

View file

@ -11,7 +11,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { I18nProvider } from '@kbn/i18n-react';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { render as reactRender, RenderOptions, RenderResult } from '@testing-library/react';
import { CoreStart } from '@kbn/core/public';
import { Capabilities, CoreStart } from '@kbn/core/public';
import { coreMock } from '@kbn/core/public/mocks';
import { euiDarkVars } from '@kbn/ui-theme';
import type { ILicense } from '@kbn/licensing-plugin/public';
@ -22,6 +22,7 @@ import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult;
interface AppMockRendererArgs {
capabilities?: Capabilities;
license?: ILicense | null;
}
@ -30,9 +31,15 @@ export interface AppMockRenderer {
coreStart: CoreStart;
queryClient: QueryClient;
AppWrapper: React.FC<{ children: React.ReactElement }>;
mocked: {
setBadge: jest.Mock;
};
}
export const createAppMockRenderer = ({ license }: AppMockRendererArgs = {}): AppMockRenderer => {
export const createAppMockRenderer = ({
capabilities,
license,
}: AppMockRendererArgs = {}): AppMockRenderer => {
const theme$ = of({ eui: euiDarkVars, darkMode: true });
const licensingPluginMock = licensingMock.createStart();
@ -53,13 +60,26 @@ export const createAppMockRenderer = ({ license }: AppMockRendererArgs = {}): Ap
error: () => {},
},
});
const mockedSetBadge = jest.fn();
const core = coreMock.createStart();
const services = {
...core,
application: {
...core.application,
capabilities: {
...core.application.capabilities,
...capabilities,
},
},
licensing:
license != null
? { ...licensingPluginMock, license$: new BehaviorSubject(license) }
: licensingPluginMock,
chrome: {
...core.chrome,
setBadge: mockedSetBadge,
},
};
const AppWrapper: React.FC<{ children: React.ReactElement }> = React.memo(({ children }) => (
<I18nProvider>
@ -85,5 +105,8 @@ export const createAppMockRenderer = ({ license }: AppMockRendererArgs = {}): Ap
render,
queryClient,
AppWrapper,
mocked: {
setBadge: mockedSetBadge,
},
};
};

View file

@ -41,7 +41,12 @@ export const EmptyPrompt = React.memo<EmptyPromptProps>(
}, [showCreateButton, onClickCreate, docLinks]);
return (
<EuiPageTemplate.EmptyPrompt title={emptyTitle} body={emptyBody} actions={renderActions} />
<EuiPageTemplate.EmptyPrompt
data-test-subj="mw-empty-prompt"
title={emptyTitle}
body={emptyBody}
actions={renderActions}
/>
);
}
);

View file

@ -24,6 +24,7 @@ export const LicensePrompt = React.memo(() => {
return (
<EuiPageTemplate.EmptyPrompt
data-test-subj="mw-license-prompt"
title={title}
body={
<EuiFlexGroup direction="column">

View file

@ -94,7 +94,12 @@ describe('MaintenanceWindowsList', () => {
test('it renders', () => {
const result = appMockRenderer.render(
<MaintenanceWindowsList refreshData={() => {}} loading={false} items={items} />
<MaintenanceWindowsList
refreshData={() => {}}
loading={false}
items={items}
readOnly={false}
/>
);
expect(result.getAllByTestId('list-item')).toHaveLength(items.length);
@ -115,5 +120,24 @@ describe('MaintenanceWindowsList', () => {
// check the endDate formatting
expect(result.getAllByText('05/05/23 00:00 AM')).toHaveLength(4);
// check if action menu is there
expect(result.getAllByTestId('table-actions-icon-button')).toHaveLength(items.length);
});
test('it does NOT renders action column in readonly', () => {
const result = appMockRenderer.render(
<MaintenanceWindowsList
refreshData={() => {}}
loading={false}
items={items}
readOnly={true}
/>
);
expect(result.getAllByTestId('list-item')).toHaveLength(items.length);
// check if action menu is there
expect(result.queryByTestId('table-actions-icon-button')).not.toBeInTheDocument();
});
});

View file

@ -32,10 +32,11 @@ import { useFinishAndArchiveMaintenanceWindow } from '../../../hooks/use_finish_
interface MaintenanceWindowsListProps {
loading: boolean;
items: MaintenanceWindowFindResponse[];
readOnly: boolean;
refreshData: () => void;
}
const columns: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = [
const COLUMNS: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = [
{
field: 'title',
name: i18n.NAME,
@ -99,7 +100,7 @@ const search: { filters: SearchFilterConfig[] } = {
};
export const MaintenanceWindowsList = React.memo<MaintenanceWindowsListProps>(
({ loading, items, refreshData }) => {
({ loading, items, readOnly, refreshData }) => {
const { euiTheme } = useEuiTheme();
const { navigateToEditMaintenanceWindows } = useEditMaintenanceWindowsNavigation();
const onEdit = useCallback(
@ -139,32 +140,41 @@ export const MaintenanceWindowsList = React.memo<MaintenanceWindowsListProps>(
`;
}, [euiTheme.colors.highlight]);
const actions: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = [
{
name: '',
render: ({ status, id }: { status: MaintenanceWindowStatus; id: string }) => {
return (
<TableActionsPopover
id={id}
status={status}
onEdit={onEdit}
onCancel={onCancel}
onArchive={onArchive}
onCancelAndArchive={onCancelAndArchive}
/>
);
const actions: Array<EuiBasicTableColumn<MaintenanceWindowFindResponse>> = useMemo(
() => [
{
name: '',
render: ({ status, id }: { status: MaintenanceWindowStatus; id: string }) => {
return (
<TableActionsPopover
id={id}
status={status}
onEdit={onEdit}
onCancel={onCancel}
onArchive={onArchive}
onCancelAndArchive={onCancelAndArchive}
/>
);
},
},
},
];
],
[onArchive, onCancel, onCancelAndArchive, onEdit]
);
const columns = useMemo(
() => (readOnly ? COLUMNS : COLUMNS.concat(actions)),
[actions, readOnly]
);
return (
<EuiInMemoryTable
data-test-subj="mw-table"
css={tableCss}
itemId="id"
loading={loading || isLoadingFinish || isLoadingArchive || isLoadingFinishAndArchive}
tableCaption="Maintenance Windows List"
items={items}
columns={columns.concat(actions)}
columns={columns}
pagination={true}
sorting={sorting}
rowProps={rowProps}

View file

@ -0,0 +1,84 @@
/*
* 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 { licensingMock } from '@kbn/licensing-plugin/public/mocks';
import type { Capabilities } from '@kbn/core-capabilities-common';
import { AppMockRenderer, createAppMockRenderer } from '../../lib/test_utils';
import { useFindMaintenanceWindows } from '../../hooks/use_find_maintenance_windows';
import { MaintenanceWindowsPage } from '.';
import { MAINTENANCE_WINDOW_FEATURE_ID } from '../../../common';
jest.mock('../../hooks/use_find_maintenance_windows', () => ({
useFindMaintenanceWindows: jest.fn(),
}));
describe('Maintenance windows page', () => {
let appMockRenderer: AppMockRenderer;
let license = licensingMock.createLicense({
license: { type: 'platinum' },
});
let capabilities: Capabilities = {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
show: true,
save: true,
},
navLinks: {},
management: {},
catalogue: {},
};
beforeEach(() => {
jest.clearAllMocks();
(useFindMaintenanceWindows as jest.Mock).mockReturnValue({
isLoading: false,
maintenanceWindows: [],
refetch: jest.fn(),
});
license = licensingMock.createLicense({
license: { type: 'platinum' },
});
capabilities = {
maintenanceWindow: {
show: true,
save: true,
},
navLinks: {},
management: {},
catalogue: {},
};
appMockRenderer = createAppMockRenderer({ capabilities, license });
});
test('show license prompt', () => {
license = licensingMock.createLicense({
license: { type: 'gold' },
});
appMockRenderer = createAppMockRenderer({ capabilities, license });
const result = appMockRenderer.render(<MaintenanceWindowsPage />);
expect(result.queryByTestId('mw-license-prompt')).toBeInTheDocument();
});
test('show empty prompt', () => {
const result = appMockRenderer.render(<MaintenanceWindowsPage />);
expect(result.queryByTestId('mw-empty-prompt')).toBeInTheDocument();
expect(appMockRenderer.mocked.setBadge).not.toBeCalled();
});
test('show table in read only', () => {
capabilities = {
...capabilities,
[MAINTENANCE_WINDOW_FEATURE_ID]: {
show: true,
save: false,
},
};
appMockRenderer = createAppMockRenderer({ capabilities, license });
const result = appMockRenderer.render(<MaintenanceWindowsPage />);
expect(result.queryByTestId('mw-table')).toBeInTheDocument();
expect(appMockRenderer.mocked.setBadge).toBeCalledTimes(1);
});
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import {
EuiButton,
EuiFlexGroup,
@ -28,9 +28,14 @@ import { CenterJustifiedSpinner } from './components/center_justified_spinner';
import { ExperimentalBadge } from './components/page_header';
import { useLicense } from '../../hooks/use_license';
import { LicensePrompt } from './components/license_prompt';
import { MAINTENANCE_WINDOW_FEATURE_ID } from '../../../common';
export const MaintenanceWindowsPage = React.memo(() => {
const { docLinks } = useKibana().services;
const {
application: { capabilities },
chrome,
docLinks,
} = useKibana().services;
const { isAtLeastPlatinum } = useLicense();
const { navigateToCreateMaintenanceWindow } = useCreateMaintenanceWindowNavigation();
@ -44,10 +49,37 @@ export const MaintenanceWindowsPage = React.memo(() => {
}, [navigateToCreateMaintenanceWindow]);
const refreshData = useCallback(() => refetch(), [refetch]);
const showEmptyPrompt = !isLoading && maintenanceWindows.length === 0;
const showWindowMaintenance = capabilities[MAINTENANCE_WINDOW_FEATURE_ID].show;
const writeWindowMaintenance = capabilities[MAINTENANCE_WINDOW_FEATURE_ID].save;
const showEmptyPrompt =
!isLoading &&
maintenanceWindows.length === 0 &&
showWindowMaintenance &&
writeWindowMaintenance;
const hasLicense = isAtLeastPlatinum();
const readOnly = showWindowMaintenance && !writeWindowMaintenance;
// if the user is read only then display the glasses badge in the global navigation header
const setBadge = useCallback(() => {
if (readOnly) {
chrome.setBadge({
text: i18n.READ_ONLY_BADGE_TEXT,
tooltip: i18n.READ_ONLY_BADGE_TOOLTIP,
iconType: 'glasses',
});
}
}, [chrome, readOnly]);
useEffect(() => {
setBadge();
// remove the icon after the component unmounts
return () => {
chrome.setBadge();
};
}, [setBadge, chrome]);
if (isLoading) {
return <CenterJustifiedSpinner />;
}
@ -71,9 +103,14 @@ export const MaintenanceWindowsPage = React.memo(() => {
<p>{i18n.MAINTENANCE_WINDOWS_DESCRIPTION}</p>
</EuiText>
</EuiPageHeaderSection>
{!showEmptyPrompt && hasLicense ? (
{!showEmptyPrompt && hasLicense && writeWindowMaintenance ? (
<EuiPageHeaderSection>
<EuiButton onClick={handleClickCreate} iconType="plusInCircle" fill>
<EuiButton
data-test-subj="mw-create-button"
onClick={handleClickCreate}
iconType="plusInCircle"
fill
>
{i18n.CREATE_NEW_BUTTON}
</EuiButton>
</EuiPageHeaderSection>
@ -87,6 +124,7 @@ export const MaintenanceWindowsPage = React.memo(() => {
<>
<EuiSpacer size="xl" />
<MaintenanceWindowsList
readOnly={readOnly}
refreshData={refreshData}
loading={isLoading}
items={maintenanceWindows}

View file

@ -20,6 +20,20 @@ export const MAINTENANCE_WINDOWS_DESCRIPTION = i18n.translate(
}
);
export const READ_ONLY_BADGE_TEXT = i18n.translate(
'xpack.alerting.maintenanceWindows.badge.readOnly.text',
{
defaultMessage: 'Read only',
}
);
export const READ_ONLY_BADGE_TOOLTIP = i18n.translate(
'xpack.alerting.maintenanceWindows.badge.readOnly.tooltip',
{
defaultMessage: 'Unable to create or edit maintenance Windows',
}
);
export const CREATE_NEW_BUTTON = i18n.translate(
'xpack.alerting.maintenanceWindows.createNewButton',
{

View file

@ -22,7 +22,7 @@ export const maintenanceWindowFeature: KibanaFeatureConfig = {
category: DEFAULT_APP_CATEGORIES.management,
app: [],
management: {
insightsAndAlerting: ['triggersActions'],
insightsAndAlerting: ['maintenanceWindows'],
},
privileges: {
all: {
@ -32,7 +32,7 @@ export const maintenanceWindowFeature: KibanaFeatureConfig = {
MAINTENANCE_WINDOW_API_PRIVILEGES.WRITE_MAINTENANCE_WINDOW,
],
management: {
insightsAndAlerting: ['triggersActions'],
insightsAndAlerting: ['maintenanceWindows'],
},
savedObject: {
all: [MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE],
@ -44,7 +44,7 @@ export const maintenanceWindowFeature: KibanaFeatureConfig = {
app: [],
api: [MAINTENANCE_WINDOW_API_PRIVILEGES.READ_MAINTENANCE_WINDOW],
management: {
insightsAndAlerting: ['triggersActions'],
insightsAndAlerting: ['maintenanceWindows'],
},
savedObject: {
all: [],

View file

@ -52,6 +52,7 @@
"@kbn/doc-links",
"@kbn/core-saved-objects-utils-server",
"@kbn/core-ui-settings-common",
"@kbn/core-capabilities-common",
],
"exclude": [
"target/**/*",

View file

@ -8,8 +8,12 @@
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 {
MaintenanceWindowStatus,
MAINTENANCE_WINDOW_FEATURE_ID,
} from '@kbn/alerting-plugin/common';
import type { MaintenanceWindow } from '@kbn/alerting-plugin/common';
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';
@ -22,6 +26,8 @@ jest.mock('./api', () => ({
fetchActiveMaintenanceWindows: jest.fn(() => Promise.resolve([])),
}));
jest.mock('../../../../common/lib/kibana');
const RUNNING_MAINTENANCE_WINDOW_1: Partial<MaintenanceWindow> = {
title: 'Maintenance window 1',
id: '63057284-ac31-42ba-fe22-adfe9732e5ae',
@ -46,6 +52,9 @@ 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>>;
@ -54,6 +63,18 @@ describe('MaintenanceWindowCallout', () => {
appToastsMock = useAppToastsMock.create();
(useAppToasts as jest.Mock).mockReturnValue(appToastsMock);
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: true,
show: true,
},
},
},
},
});
});
afterEach(() => {
@ -62,7 +83,7 @@ describe('MaintenanceWindowCallout', () => {
});
it('should be visible if currently there is at least one "running" maintenance window', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
const { findByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
@ -70,7 +91,7 @@ describe('MaintenanceWindowCallout', () => {
});
it('should be visible if currently there are multiple "running" maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
RUNNING_MAINTENANCE_WINDOW_1,
RUNNING_MAINTENANCE_WINDOW_2,
]);
@ -81,7 +102,7 @@ describe('MaintenanceWindowCallout', () => {
});
it('should NOT be visible if currently there are no active (running or upcoming) maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([]);
fetchActiveMaintenanceWindowsMock.mockResolvedValue([]);
const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
@ -89,7 +110,7 @@ describe('MaintenanceWindowCallout', () => {
});
it('should NOT be visible if currently there are no "running" maintenance windows', async () => {
(fetchActiveMaintenanceWindows as jest.Mock).mockResolvedValue([UPCOMING_MAINTENANCE_WINDOW]);
fetchActiveMaintenanceWindowsMock.mockResolvedValue([UPCOMING_MAINTENANCE_WINDOW]);
const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
@ -121,7 +142,7 @@ describe('MaintenanceWindowCallout', () => {
};
const mockError = new Error('Network error');
(fetchActiveMaintenanceWindows as jest.Mock).mockRejectedValue(mockError);
fetchActiveMaintenanceWindowsMock.mockRejectedValue(mockError);
render(<MaintenanceWindowCallout />, { wrapper: createReactQueryWrapper() });
@ -133,4 +154,44 @@ describe('MaintenanceWindowCallout', () => {
});
});
});
it('should return null if window maintenance privilege is NONE', async () => {
useKibanaMock.mockReturnValue({
services: {
application: {
capabilities: {
[MAINTENANCE_WINDOW_FEATURE_ID]: {
save: false,
show: false,
},
},
},
},
});
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
const { container } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
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,
},
},
},
},
});
fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]);
const { findByText } = render(<MaintenanceWindowCallout />, { wrapper: TestProviders });
expect(await findByText('A maintenance window is currently running')).toBeInTheDocument();
});
});

View file

@ -7,12 +7,28 @@
import React from 'react';
import { EuiCallOut } from '@elastic/eui';
import { MaintenanceWindowStatus } from '@kbn/alerting-plugin/common';
import {
MaintenanceWindowStatus,
MAINTENANCE_WINDOW_FEATURE_ID,
} from '@kbn/alerting-plugin/common';
import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows';
import * as i18n from './translations';
import { useKibana } from '../../../../common/lib/kibana';
export function MaintenanceWindowCallout(): JSX.Element | null {
const { data } = useFetchActiveMaintenanceWindows();
const {
application: { capabilities },
} = useKibana().services;
const isMaintenanceWindowDisabled =
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID].show &&
!capabilities[MAINTENANCE_WINDOW_FEATURE_ID].save;
const { data } = useFetchActiveMaintenanceWindows({ enabled: !isMaintenanceWindowDisabled });
if (isMaintenanceWindowDisabled) {
return null;
}
const activeMaintenanceWindows = data || [];
if (activeMaintenanceWindows.some(({ status }) => status === MaintenanceWindowStatus.Running)) {

View file

@ -5,19 +5,21 @@
* 2.0.
*/
import type { UseQueryOptions } from '@tanstack/react-query';
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 = () => {
export const useFetchActiveMaintenanceWindows = ({ enabled }: Pick<UseQueryOptions, 'enabled'>) => {
const { addError } = useAppToasts();
return useQuery(
['GET', INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH],
({ signal }) => fetchActiveMaintenanceWindows(signal),
{
enabled,
refetchInterval: 60000,
onError: (error) => {
addError(error, { title: i18n.FETCH_ERROR, toastMessage: i18n.FETCH_ERROR_DESCRIPTION });

View file

@ -64,7 +64,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
expect(sections).to.have.length(2);
expect(sections[0]).to.eql({
sectionId: 'insightsAndAlerting',
sectionLinks: ['triggersActions', 'cases', 'triggersActionsConnectors', 'jobsListLink'],
sectionLinks: [
'triggersActions',
'cases',
'triggersActionsConnectors',
'jobsListLink',
'maintenanceWindows',
],
});
expect(sections[1]).to.eql({
sectionId: 'kibana',