Explore: Use Dashboard permissions in Explore To Dashboard (#49539)

* Look for dashboard permissions in e2d button

* Get tests to pass

* Add permissions tests
This commit is contained in:
Kristina 2022-05-26 06:29:19 -05:00 committed by GitHub
parent ab85029969
commit a95ba8eb3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 107 additions and 26 deletions

View File

@ -7,8 +7,9 @@ import { locationUtil, SelectableValue } from '@grafana/data';
import { config, locationService, reportInteraction } from '@grafana/runtime';
import { Alert, Button, Field, InputControl, Modal, RadioButtonGroup } from '@grafana/ui';
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
import { contextSrv } from 'app/core/services/context_srv';
import { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
import { ExploreId } from 'app/types';
import { ExploreId, AccessControlAction } from 'app/types';
import { getExploreItemSelector } from '../state/selectors';
@ -19,17 +20,6 @@ enum SaveTarget {
ExistingDashboard = 'existing-dashboard',
}
const SAVE_TARGETS: Array<SelectableValue<SaveTarget>> = [
{
label: 'New dashboard',
value: SaveTarget.NewDashboard,
},
{
label: 'Existing dashboard',
value: SaveTarget.ExistingDashboard,
},
];
interface SaveTargetDTO {
saveTarget: SaveTarget;
}
@ -82,7 +72,27 @@ export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
} = useForm<FormDTO>({
defaultValues: { saveTarget: SaveTarget.NewDashboard },
});
const saveTarget = watch('saveTarget');
const canCreateDashboard = contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor);
const canWriteDashboard = contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor);
const saveTargets: Array<SelectableValue<SaveTarget>> = [];
if (canCreateDashboard) {
saveTargets.push({
label: 'New dashboard',
value: SaveTarget.NewDashboard,
});
}
if (canWriteDashboard) {
saveTargets.push({
label: 'Existing dashboard',
value: SaveTarget.ExistingDashboard,
});
}
const saveTarget = saveTargets.length > 1 ? watch('saveTarget') : saveTargets[0].value;
const modalTitle = `Add panel to ${saveTargets.length > 1 ? 'dashboard' : saveTargets[0].label!.toLowerCase()}`;
const onSubmit = async (openInNewTab: boolean, data: FormDTO) => {
setSubmissionError(undefined);
@ -139,17 +149,19 @@ export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
}, []);
return (
<Modal title="Add panel to dashboard" onDismiss={onClose} isOpen>
<Modal title={modalTitle} onDismiss={onClose} isOpen>
<form>
<InputControl
control={control}
render={({ field: { ref, ...field } }) => (
<Field label="Target dashboard" description="Choose where to add the panel.">
<RadioButtonGroup options={SAVE_TARGETS} {...field} id="e2d-save-target" />
</Field>
)}
name="saveTarget"
/>
{saveTargets.length > 1 && (
<InputControl
control={control}
render={({ field: { ref, ...field } }) => (
<Field label="Target dashboard" description="Choose where to add the panel.">
<RadioButtonGroup options={saveTargets} {...field} id="e2d-save-target" />
</Field>
)}
name="saveTarget"
/>
)}
{saveTarget === SaveTarget.ExistingDashboard &&
(() => {

View File

@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
import { DataQuery } from '@grafana/data';
import { locationService, setEchoSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { Echo } from 'app/core/services/echo/Echo';
import * as initDashboard from 'app/features/dashboard/state/initDashboard';
import { DashboardSearchItemType } from 'app/features/search/types';
@ -31,10 +32,16 @@ const setup = (children: ReactNode, queries: DataQuery[] = [{ refId: 'A' }]) =>
return render(<Provider store={store}>{children}</Provider>);
};
const openModal = async () => {
jest.mock('app/core/services/context_srv');
const mocks = {
contextSrv: jest.mocked(contextSrv),
};
const openModal = async (nameOverride?: string) => {
await userEvent.click(screen.getByRole('button', { name: /add to dashboard/i }));
expect(await screen.findByRole('dialog', { name: 'Add panel to dashboard' })).toBeInTheDocument();
expect(await screen.findByRole('dialog', { name: nameOverride || 'Add panel to dashboard' })).toBeInTheDocument();
};
describe('AddToDashboardButton', () => {
@ -64,6 +71,7 @@ describe('AddToDashboardButton', () => {
beforeEach(() => {
jest.spyOn(api, 'setDashboardInLocalStorage').mockReturnValue(addToDashboardResponse);
mocks.contextSrv.hasAccess.mockImplementation(() => true);
});
afterEach(() => {
@ -271,7 +279,43 @@ describe('AddToDashboardButton', () => {
});
});
describe('Permissions', () => {
afterEach(() => {
jest.restoreAllMocks();
});
it('Should only show existing dashboard option with no access to create', async () => {
mocks.contextSrv.hasAccess.mockImplementation((action) => {
if (action === 'dashboards:create') {
return false;
} else {
return true;
}
});
setup(<AddToDashboard exploreId={ExploreId.left} />);
await openModal('Add panel to existing dashboard');
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
});
it('Should only show new dashboard option with no access to write', async () => {
mocks.contextSrv.hasAccess.mockImplementation((action) => {
if (action === 'dashboards:write') {
return false;
} else {
return true;
}
});
setup(<AddToDashboard exploreId={ExploreId.left} />);
await openModal('Add panel to new dashboard');
expect(screen.queryByRole('radio')).not.toBeInTheDocument();
});
});
describe('Error handling', () => {
beforeEach(() => {
mocks.contextSrv.hasAccess.mockImplementation(() => true);
});
afterEach(() => {
jest.restoreAllMocks();
});

View File

@ -97,6 +97,12 @@ jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {
};
});
jest.mock('app/core/core', () => ({
contextSrv: {
hasAccess: () => true,
},
}));
// for the AutoSizer component to have a width
jest.mock('react-virtualized-auto-sizer', () => {
return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 });

View File

@ -11,7 +11,9 @@ import {
ToolbarButton,
ToolbarButtonRow,
} from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { AccessControlAction } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { StoreState } from 'app/types/store';
@ -120,6 +122,10 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
const showSmallTimePicker = splitted || containerWidth < 1210;
const showExploreToDashboard =
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor) ||
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor);
return (
<div ref={topOfViewRef}>
<PageToolbar
@ -158,7 +164,7 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
</ToolbarButton>
)}
{config.featureToggles.explore2Dashboard && (
{config.featureToggles.explore2Dashboard && showExploreToDashboard && (
<Suspense fallback={null}>
<AddToDashboard exploreId={exploreId} />
</Suspense>

View File

@ -50,6 +50,7 @@ jest.mock('app/core/core', () => {
return {
contextSrv: {
hasPermission: () => true,
hasAccess: () => true,
},
appEvents: {
subscribe: () => {},

View File

@ -14,6 +14,12 @@ jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({ fetch }),
}));
jest.mock('app/core/core', () => ({
contextSrv: {
hasAccess: () => true,
},
}));
jest.mock('react-virtualized-auto-sizer', () => {
return {
__esModule: true,

View File

@ -45,6 +45,12 @@ jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }),
}));
jest.mock('app/core/core', () => ({
contextSrv: {
hasAccess: () => true,
},
}));
jest.mock('app/core/services/PreferencesService', () => ({
PreferencesService: function () {
return {