Skip to content

Commit

Permalink
[RAM] Apply maintenance windows privilege to UI (#156191)
Browse files Browse the repository at this point in the history
## Summary

We will have three scenarios with kibana privileges

### NONE
Kibana privileges form maintenance window:
<img width="680" alt="image"
src="https://user-images.githubusercontent.com/189600/235188523-acaff7de-54d4-4991-a014-05c0f449738c.png">

`The expected result is to not see maintenance window at all`

<img width="1481" alt="image"
src="https://user-images.githubusercontent.com/189600/235188658-6a53b463-4856-42c7-916e-aa8e6d7e326b.png">

### READ
Kibana privileges form maintenance window:
<img width="677" alt="image"
src="https://user-images.githubusercontent.com/189600/235188908-623d32ac-39a7-484e-bd5c-f858e04d16b2.png">

`The expected result is to only see the table with window maintenance
and you can not edit them`

<img width="1487" alt="image"
src="https://user-images.githubusercontent.com/189600/235189169-f71422bf-6394-4574-87fb-14c653ca1e79.png">
<img width="1484" alt="image"
src="https://user-images.githubusercontent.com/189600/235192048-149519ba-0505-46e3-b737-2703560eb3d6.png">


### ALL
Kibana privileges form maintenance window:
<img width="668" alt="image"
src="https://user-images.githubusercontent.com/189600/235189384-e71d9138-221c-4024-91bb-2ae32da1bd3b.png">

`The expected result is to be able to create/edit/etc on any maintenance
windows`

<img width="1484" alt="image"
src="https://user-images.githubusercontent.com/189600/235189974-e36c1e65-0586-4840-ace5-32caf06455c6.png">
<img width="1481" alt="image"
src="https://user-images.githubusercontent.com/189600/235192269-0f8d1922-d48f-494c-9979-2288bf142286.png">



### Checklist

Delete any items that are not applicable to this PR.

- [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 <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
XavierM and kibanamachine committed May 2, 2023
1 parent aeded80 commit 3c9da2c
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 42 deletions.
27 changes: 25 additions & 2 deletions x-pack/plugins/alerting/public/lib/test_utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}

Expand All @@ -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();
Expand All @@ -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>
Expand All @@ -85,5 +105,8 @@ export const createAppMockRenderer = ({ license }: AppMockRendererArgs = {}): Ap
render,
queryClient,
AppWrapper,
mocked: {
setBadge: mockedSetBadge,
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -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}
/>
);
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const LicensePrompt = React.memo(() => {

return (
<EuiPageTemplate.EmptyPrompt
data-test-subj="mw-license-prompt"
title={title}
body={
<EuiFlexGroup direction="column">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
50 changes: 44 additions & 6 deletions x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import {
EuiButton,
EuiFlexGroup,
Expand All @@ -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();
Expand All @@ -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 />;
}
Expand All @@ -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>
Expand All @@ -87,6 +124,7 @@ export const MaintenanceWindowsPage = React.memo(() => {
<>
<EuiSpacer size="xl" />
<MaintenanceWindowsList
readOnly={readOnly}
refreshData={refreshData}
loading={isLoading}
items={maintenanceWindows}
Expand Down
Loading

0 comments on commit 3c9da2c

Please sign in to comment.