Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RAM] Apply maintenance windows privilege to UI #156191

Merged
merged 13 commits into from
May 2, 2023
Merged
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);
});
});
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(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't catch this earlier, and it's a bit unrelated to privileges, but I think we should rename this file to MaintenanceWindowsPage instead of index, since it's not really an index file, same for the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do that in follow up PR

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