Skip to content

Commit 96ee367

Browse files
geidoeschutho
authored andcommitted
chore: Add permission to view and drill on Dashboard context (apache#26798)
1 parent 188dbe0 commit 96ee367

File tree

10 files changed

+242
-36
lines changed

10 files changed

+242
-36
lines changed

superset-frontend/src/components/Chart/ChartContextMenu/ChartContextMenu.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ const ChartContextMenu = (
9090
const canExplore = useSelector((state: RootState) =>
9191
findPermission('can_explore', 'Superset', state.user?.roles),
9292
);
93+
const canViewDrill = useSelector((state: RootState) =>
94+
findPermission('can_view_and_drill', 'Dashboard', state.user?.roles),
95+
);
96+
const canDatasourceSamples = useSelector((state: RootState) =>
97+
findPermission('can_samples', 'Datasource', state.user?.roles),
98+
);
9399
const crossFiltersEnabled = useSelector<RootState, boolean>(
94100
({ dashboardInfo }) => dashboardInfo.crossFiltersEnabled,
95101
);
@@ -105,15 +111,17 @@ const ChartContextMenu = (
105111
}>({ clientX: 0, clientY: 0 });
106112

107113
const menuItems = [];
114+
const canExploreOrView = canExplore || canViewDrill;
108115

109116
const showDrillToDetail =
110117
isFeatureEnabled(FeatureFlag.DRILL_TO_DETAIL) &&
111-
canExplore &&
118+
canExploreOrView &&
119+
canDatasourceSamples &&
112120
isDisplayed(ContextMenuItem.DrillToDetail);
113121

114122
const showDrillBy =
115123
isFeatureEnabled(FeatureFlag.DRILL_BY) &&
116-
canExplore &&
124+
canExploreOrView &&
117125
isDisplayed(ContextMenuItem.DrillBy);
118126

119127
const showCrossFilters =

superset-frontend/src/components/Chart/ChartContextMenu/useContextMenu.test.tsx

+34-2
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,12 @@ const setup = ({
3838
onSelection = noOp,
3939
displayedItems = ContextMenuItem.All,
4040
additionalConfig = {},
41+
roles = undefined,
4142
}: {
4243
onSelection?: () => void;
4344
displayedItems?: ContextMenuItem | ContextMenuItem[];
4445
additionalConfig?: Record<string, any>;
46+
roles?: Record<string, string[][]>;
4547
} = {}) => {
4648
const { result } = renderHook(() =>
4749
useContextMenu(
@@ -58,7 +60,12 @@ const setup = ({
5860
...mockState,
5961
user: {
6062
...mockState.user,
61-
roles: { Admin: [['can_explore', 'Superset']] },
63+
roles: roles ?? {
64+
Admin: [
65+
['can_explore', 'Superset'],
66+
['can_samples', 'Datasource'],
67+
],
68+
},
6269
},
6370
},
6471
});
@@ -75,7 +82,7 @@ test('Context menu renders', () => {
7582
expect(screen.getByText('Drill by')).toBeInTheDocument();
7683
});
7784

78-
test('Context menu contains all items only', () => {
85+
test('Context menu contains all displayed items only', () => {
7986
const result = setup({
8087
displayedItems: [ContextMenuItem.DrillToDetail, ContextMenuItem.DrillBy],
8188
});
@@ -84,3 +91,28 @@ test('Context menu contains all items only', () => {
8491
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
8592
expect(screen.getByText('Drill by')).toBeInTheDocument();
8693
});
94+
95+
test('Context menu shows all items tied to can_view_and_drill permission', () => {
96+
const result = setup({
97+
roles: {
98+
Admin: [
99+
['can_view_and_drill', 'Dashboard'],
100+
['can_samples', 'Datasource'],
101+
],
102+
},
103+
});
104+
result.current.onContextMenu(0, 0, {});
105+
expect(screen.getByText('Drill to detail')).toBeInTheDocument();
106+
expect(screen.getByText('Drill by')).toBeInTheDocument();
107+
expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
108+
});
109+
110+
test('Context menu does not show "Drill to detail" without proper permissions', () => {
111+
const result = setup({
112+
roles: { Admin: [['can_view_and_drill', 'Dashboard']] },
113+
});
114+
result.current.onContextMenu(0, 0, {});
115+
expect(screen.queryByText('Drill to detail')).not.toBeInTheDocument();
116+
expect(screen.getByText('Drill by')).toBeInTheDocument();
117+
expect(screen.getByText('Add cross-filter')).toBeInTheDocument();
118+
});

superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx

+34-2
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ const dataset = {
6868
],
6969
};
7070

71-
const renderModal = async (modalProps: Partial<DrillByModalProps> = {}) => {
71+
const renderModal = async (
72+
modalProps: Partial<DrillByModalProps> = {},
73+
overrideState: Record<string, any> = {},
74+
) => {
7275
const DrillByModalWrapper = () => {
7376
const [showModal, setShowModal] = useState(false);
7477

@@ -93,7 +96,10 @@ const renderModal = async (modalProps: Partial<DrillByModalProps> = {}) => {
9396
useDnd: true,
9497
useRedux: true,
9598
useRouter: true,
96-
initialState: drillByModalState,
99+
initialState: {
100+
...drillByModalState,
101+
...overrideState,
102+
},
97103
});
98104

99105
userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
@@ -233,3 +239,29 @@ test('render breadcrumbs', async () => {
233239
expect(newBreadcrumbItems).toHaveLength(1);
234240
expect(within(breadcrumbItems[0]).getByText('gender')).toBeInTheDocument();
235241
});
242+
243+
test('should render "Edit chart" as disabled without can_explore permission', async () => {
244+
await renderModal(
245+
{},
246+
{
247+
user: {
248+
...drillByModalState.user,
249+
roles: { Admin: [['test_invalid_role', 'Superset']] },
250+
},
251+
},
252+
);
253+
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeDisabled();
254+
});
255+
256+
test('should render "Edit chart" enabled with can_explore permission', async () => {
257+
await renderModal(
258+
{},
259+
{
260+
user: {
261+
...drillByModalState.user,
262+
roles: { Admin: [['can_explore', 'Superset']] },
263+
},
264+
},
265+
);
266+
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeEnabled();
267+
});

superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx

+16-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
LOG_ACTIONS_DRILL_BY_MODAL_OPENED,
5555
LOG_ACTIONS_FURTHER_DRILL_BY,
5656
} from 'src/logger/LogUtils';
57+
import { findPermission } from 'src/utils/findPermission';
5758
import { Dataset, DrillByType } from '../types';
5859
import DrillByChart from './DrillByChart';
5960
import { ContextMenuItem } from '../ChartContextMenu/ChartContextMenu';
@@ -75,6 +76,7 @@ interface ModalFooterProps {
7576
const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
7677
const dispatch = useDispatch();
7778
const { addDangerToast } = useToasts();
79+
const theme = useTheme();
7880
const [url, setUrl] = useState('');
7981
const dashboardPageId = useContext(DashboardPageIdContext);
8082
const onEditChartClick = useCallback(() => {
@@ -84,6 +86,9 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
8486
}),
8587
);
8688
}, [dispatch, formData.slice_id]);
89+
const canExplore = useSelector((state: RootState) =>
90+
findPermission('can_explore', 'Superset', state.user?.roles),
91+
);
8792

8893
const [datasource_id, datasource_type] = formData.datasource.split('__');
8994
useEffect(() => {
@@ -103,13 +108,20 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
103108
datasource_type,
104109
formData,
105110
]);
111+
const isEditDisabled = !url || !canExplore;
112+
106113
return (
107114
<>
108115
<Button
109116
buttonStyle="secondary"
110117
buttonSize="small"
111118
onClick={onEditChartClick}
112-
disabled={!url}
119+
disabled={isEditDisabled}
120+
tooltip={
121+
isEditDisabled
122+
? t('You do not have sufficient permissions to edit the chart')
123+
: undefined
124+
}
113125
>
114126
<Link
115127
css={css`
@@ -128,6 +140,9 @@ const ModalFooter = ({ formData, closeModal }: ModalFooterProps) => {
128140
buttonSize="small"
129141
onClick={closeModal}
130142
data-test="close-drill-by-modal"
143+
css={css`
144+
margin-left: ${theme.gridUnit * 2}px;
145+
`}
131146
>
132147
{t('Close')}
133148
</Button>

superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.test.tsx

+23-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,16 @@ jest.mock('react-router-dom', () => ({
3535

3636
const { id: chartId, form_data: formData } = chartQueries[sliceId];
3737
const { slice_name: chartName } = formData;
38+
const store = getMockStoreWithNativeFilters();
39+
const drillToDetailModalState = {
40+
...store.getState(),
41+
user: {
42+
...store.getState().user,
43+
roles: { Admin: [['can_explore', 'Superset']] },
44+
},
45+
};
3846

39-
const renderModal = async () => {
40-
const store = getMockStoreWithNativeFilters();
47+
const renderModal = async (overrideState: Record<string, any> = {}) => {
4148
const DrillDetailModalWrapper = () => {
4249
const [showModal, setShowModal] = useState(false);
4350
return (
@@ -59,7 +66,10 @@ const renderModal = async () => {
5966
render(<DrillDetailModalWrapper />, {
6067
useRouter: true,
6168
useRedux: true,
62-
store,
69+
initialState: {
70+
...drillToDetailModalState,
71+
...overrideState,
72+
},
6373
});
6474

6575
userEvent.click(screen.getByRole('button', { name: 'Show modal' }));
@@ -93,3 +103,13 @@ test('should forward to Explore', async () => {
93103
`/explore/?dashboard_page_id=&slice_id=${sliceId}`,
94104
);
95105
});
106+
107+
test('should render "Edit chart" as disabled without can_explore permission', async () => {
108+
await renderModal({
109+
user: {
110+
...drillToDetailModalState.user,
111+
roles: { Admin: [['test_invalid_role', 'Superset']] },
112+
},
113+
});
114+
expect(screen.getByRole('button', { name: 'Edit chart' })).toBeDisabled();
115+
});

superset-frontend/src/components/Chart/DrillDetail/DrillDetailModal.tsx

+46-17
Original file line numberDiff line numberDiff line change
@@ -31,28 +31,52 @@ import Button from 'src/components/Button';
3131
import { useSelector } from 'react-redux';
3232
import { DashboardPageIdContext } from 'src/dashboard/containers/DashboardPage';
3333
import { Slice } from 'src/types/Chart';
34+
import { RootState } from 'src/dashboard/types';
35+
import { findPermission } from 'src/utils/findPermission';
3436
import DrillDetailPane from './DrillDetailPane';
3537

3638
interface ModalFooterProps {
37-
exploreChart: () => void;
39+
canExplore: boolean;
3840
closeModal?: () => void;
41+
exploreChart: () => void;
3942
}
4043

41-
const ModalFooter = ({ exploreChart, closeModal }: ModalFooterProps) => (
42-
<>
43-
<Button buttonStyle="secondary" buttonSize="small" onClick={exploreChart}>
44-
{t('Edit chart')}
45-
</Button>
46-
<Button
47-
buttonStyle="primary"
48-
buttonSize="small"
49-
onClick={closeModal}
50-
data-test="close-drilltodetail-modal"
51-
>
52-
{t('Close')}
53-
</Button>
54-
</>
55-
);
44+
const ModalFooter = ({
45+
canExplore,
46+
closeModal,
47+
exploreChart,
48+
}: ModalFooterProps) => {
49+
const theme = useTheme();
50+
51+
return (
52+
<>
53+
<Button
54+
buttonStyle="secondary"
55+
buttonSize="small"
56+
onClick={exploreChart}
57+
disabled={!canExplore}
58+
tooltip={
59+
!canExplore
60+
? t('You do not have sufficient permissions to edit the chart')
61+
: undefined
62+
}
63+
>
64+
{t('Edit chart')}
65+
</Button>
66+
<Button
67+
buttonStyle="primary"
68+
buttonSize="small"
69+
onClick={closeModal}
70+
data-test="close-drilltodetail-modal"
71+
css={css`
72+
margin-left: ${theme.gridUnit * 2}px;
73+
`}
74+
>
75+
{t('Close')}
76+
</Button>
77+
</>
78+
);
79+
};
5680

5781
interface DrillDetailModalProps {
5882
chartId: number;
@@ -76,6 +100,9 @@ export default function DrillDetailModal({
76100
(state: { sliceEntities: { slices: Record<number, Slice> } }) =>
77101
state.sliceEntities.slices[chartId],
78102
);
103+
const canExplore = useSelector((state: RootState) =>
104+
findPermission('can_explore', 'Superset', state.user?.roles),
105+
);
79106

80107
const exploreUrl = useMemo(
81108
() => `/explore/?dashboard_page_id=${dashboardPageId}&slice_id=${chartId}`,
@@ -97,7 +124,9 @@ export default function DrillDetailModal({
97124
}
98125
`}
99126
title={t('Drill to detail: %s', chartName)}
100-
footer={<ModalFooter exploreChart={exploreChart} />}
127+
footer={
128+
<ModalFooter exploreChart={exploreChart} canExplore={canExplore} />
129+
}
101130
responsive
102131
resizable
103132
resizableConfig={{

0 commit comments

Comments
 (0)