mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
# 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:
parent
7f7062543f
commit
ad65768a1b
14 changed files with 327 additions and 42 deletions
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -24,6 +24,7 @@ export const LicensePrompt = React.memo(() => {
|
|||
|
||||
return (
|
||||
<EuiPageTemplate.EmptyPrompt
|
||||
data-test-subj="mw-license-prompt"
|
||||
title={title}
|
||||
body={
|
||||
<EuiFlexGroup direction="column">
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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}
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"@kbn/doc-links",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/core-ui-settings-common",
|
||||
"@kbn/core-capabilities-common",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue