mirror of
https://github.com/grafana/grafana.git
synced 2025-01-18 12:33:37 -06:00
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:
parent
ab85029969
commit
a95ba8eb3f
@ -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 &&
|
||||
(() => {
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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 });
|
||||
|
@ -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>
|
||||
|
@ -50,6 +50,7 @@ jest.mock('app/core/core', () => {
|
||||
return {
|
||||
contextSrv: {
|
||||
hasPermission: () => true,
|
||||
hasAccess: () => true,
|
||||
},
|
||||
appEvents: {
|
||||
subscribe: () => {},
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user