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 { config, locationService, reportInteraction } from '@grafana/runtime';
|
||||||
import { Alert, Button, Field, InputControl, Modal, RadioButtonGroup } from '@grafana/ui';
|
import { Alert, Button, Field, InputControl, Modal, RadioButtonGroup } from '@grafana/ui';
|
||||||
import { DashboardPicker } from 'app/core/components/Select/DashboardPicker';
|
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 { removeDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
|
||||||
import { ExploreId } from 'app/types';
|
import { ExploreId, AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
import { getExploreItemSelector } from '../state/selectors';
|
import { getExploreItemSelector } from '../state/selectors';
|
||||||
|
|
||||||
@ -19,17 +20,6 @@ enum SaveTarget {
|
|||||||
ExistingDashboard = 'existing-dashboard',
|
ExistingDashboard = 'existing-dashboard',
|
||||||
}
|
}
|
||||||
|
|
||||||
const SAVE_TARGETS: Array<SelectableValue<SaveTarget>> = [
|
|
||||||
{
|
|
||||||
label: 'New dashboard',
|
|
||||||
value: SaveTarget.NewDashboard,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Existing dashboard',
|
|
||||||
value: SaveTarget.ExistingDashboard,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface SaveTargetDTO {
|
interface SaveTargetDTO {
|
||||||
saveTarget: SaveTarget;
|
saveTarget: SaveTarget;
|
||||||
}
|
}
|
||||||
@ -82,7 +72,27 @@ export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
|
|||||||
} = useForm<FormDTO>({
|
} = useForm<FormDTO>({
|
||||||
defaultValues: { saveTarget: SaveTarget.NewDashboard },
|
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) => {
|
const onSubmit = async (openInNewTab: boolean, data: FormDTO) => {
|
||||||
setSubmissionError(undefined);
|
setSubmissionError(undefined);
|
||||||
@ -139,17 +149,19 @@ export const AddToDashboardModal = ({ onClose, exploreId }: Props) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title="Add panel to dashboard" onDismiss={onClose} isOpen>
|
<Modal title={modalTitle} onDismiss={onClose} isOpen>
|
||||||
<form>
|
<form>
|
||||||
<InputControl
|
{saveTargets.length > 1 && (
|
||||||
control={control}
|
<InputControl
|
||||||
render={({ field: { ref, ...field } }) => (
|
control={control}
|
||||||
<Field label="Target dashboard" description="Choose where to add the panel.">
|
render={({ field: { ref, ...field } }) => (
|
||||||
<RadioButtonGroup options={SAVE_TARGETS} {...field} id="e2d-save-target" />
|
<Field label="Target dashboard" description="Choose where to add the panel.">
|
||||||
</Field>
|
<RadioButtonGroup options={saveTargets} {...field} id="e2d-save-target" />
|
||||||
)}
|
</Field>
|
||||||
name="saveTarget"
|
)}
|
||||||
/>
|
name="saveTarget"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{saveTarget === SaveTarget.ExistingDashboard &&
|
{saveTarget === SaveTarget.ExistingDashboard &&
|
||||||
(() => {
|
(() => {
|
||||||
|
@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
|
|||||||
import { DataQuery } from '@grafana/data';
|
import { DataQuery } from '@grafana/data';
|
||||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
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 { Echo } from 'app/core/services/echo/Echo';
|
||||||
import * as initDashboard from 'app/features/dashboard/state/initDashboard';
|
import * as initDashboard from 'app/features/dashboard/state/initDashboard';
|
||||||
import { DashboardSearchItemType } from 'app/features/search/types';
|
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>);
|
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 }));
|
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', () => {
|
describe('AddToDashboardButton', () => {
|
||||||
@ -64,6 +71,7 @@ describe('AddToDashboardButton', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(api, 'setDashboardInLocalStorage').mockReturnValue(addToDashboardResponse);
|
jest.spyOn(api, 'setDashboardInLocalStorage').mockReturnValue(addToDashboardResponse);
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
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', () => {
|
describe('Error handling', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
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
|
// for the AutoSizer component to have a width
|
||||||
jest.mock('react-virtualized-auto-sizer', () => {
|
jest.mock('react-virtualized-auto-sizer', () => {
|
||||||
return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 });
|
return ({ children }: AutoSizerProps) => children({ height: 1, width: 1 });
|
||||||
|
@ -11,7 +11,9 @@ import {
|
|||||||
ToolbarButton,
|
ToolbarButton,
|
||||||
ToolbarButtonRow,
|
ToolbarButtonRow,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
import { ExploreId } from 'app/types/explore';
|
import { ExploreId } from 'app/types/explore';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
|
|
||||||
@ -120,6 +122,10 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
||||||
const showSmallTimePicker = splitted || containerWidth < 1210;
|
const showSmallTimePicker = splitted || containerWidth < 1210;
|
||||||
|
|
||||||
|
const showExploreToDashboard =
|
||||||
|
contextSrv.hasAccess(AccessControlAction.DashboardsCreate, contextSrv.isEditor) ||
|
||||||
|
contextSrv.hasAccess(AccessControlAction.DashboardsWrite, contextSrv.isEditor);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={topOfViewRef}>
|
<div ref={topOfViewRef}>
|
||||||
<PageToolbar
|
<PageToolbar
|
||||||
@ -158,7 +164,7 @@ class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{config.featureToggles.explore2Dashboard && (
|
{config.featureToggles.explore2Dashboard && showExploreToDashboard && (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<AddToDashboard exploreId={exploreId} />
|
<AddToDashboard exploreId={exploreId} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -50,6 +50,7 @@ jest.mock('app/core/core', () => {
|
|||||||
return {
|
return {
|
||||||
contextSrv: {
|
contextSrv: {
|
||||||
hasPermission: () => true,
|
hasPermission: () => true,
|
||||||
|
hasAccess: () => true,
|
||||||
},
|
},
|
||||||
appEvents: {
|
appEvents: {
|
||||||
subscribe: () => {},
|
subscribe: () => {},
|
||||||
|
@ -14,6 +14,12 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
getBackendSrv: () => ({ fetch }),
|
getBackendSrv: () => ({ fetch }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
hasAccess: () => true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('react-virtualized-auto-sizer', () => {
|
jest.mock('react-virtualized-auto-sizer', () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
|
@ -45,6 +45,12 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }),
|
getBackendSrv: () => ({ fetch: fetchMock, post: postMock, get: getMock }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('app/core/core', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
hasAccess: () => true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('app/core/services/PreferencesService', () => ({
|
jest.mock('app/core/services/PreferencesService', () => ({
|
||||||
PreferencesService: function () {
|
PreferencesService: function () {
|
||||||
return {
|
return {
|
||||||
|
Loading…
Reference in New Issue
Block a user