diff --git a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx
index 00e7276a5864c..6187f574867c4 100644
--- a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx
+++ b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx
@@ -27,61 +27,97 @@ import AddSliceContainer, {
import VizTypeGallery from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
import { styledMount as mount } from 'spec/helpers/theming';
import { act } from 'spec/helpers/testing-library';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
const datasource = {
value: '1',
label: 'table',
};
-describe('AddSliceContainer', () => {
- let wrapper: ReactWrapper<
+const mockUser: UserWithPermissionsAndRoles = {
+ createdOn: '2021-04-27T18:12:38.952304',
+ email: 'admin',
+ firstName: 'admin',
+ isActive: true,
+ lastName: 'admin',
+ permissions: {},
+ roles: { Admin: Array(173) },
+ userId: 1,
+ username: 'admin',
+ isAnonymous: false,
+};
+
+const mockUserWithDatasetWrite: UserWithPermissionsAndRoles = {
+ createdOn: '2021-04-27T18:12:38.952304',
+ email: 'admin',
+ firstName: 'admin',
+ isActive: true,
+ lastName: 'admin',
+ permissions: {},
+ roles: { Admin: [['can_write', 'Dataset']] },
+ userId: 1,
+ username: 'admin',
+ isAnonymous: false,
+};
+
+async function getWrapper(user = mockUser) {
+ const wrapper = mount() as ReactWrapper<
AddSliceContainerProps,
AddSliceContainerState,
AddSliceContainer
>;
+ await act(() => new Promise(resolve => setTimeout(resolve, 0)));
+ return wrapper;
+}
- beforeEach(async () => {
- wrapper = mount() as ReactWrapper<
- AddSliceContainerProps,
- AddSliceContainerState,
- AddSliceContainer
- >;
- // suppress a warning caused by some unusual async behavior in Icon
- await act(() => new Promise(resolve => setTimeout(resolve, 0)));
- });
+test('renders a select and a VizTypeControl', async () => {
+ const wrapper = await getWrapper();
+ expect(wrapper.find(Select)).toExist();
+ expect(wrapper.find(VizTypeGallery)).toExist();
+});
- it('renders a select and a VizTypeControl', () => {
- expect(wrapper.find(Select)).toExist();
- expect(wrapper.find(VizTypeGallery)).toExist();
- });
+test('renders dataset help text when user lacks dataset write permissions', async () => {
+ const wrapper = await getWrapper();
+ expect(wrapper.find('[data-test="dataset-write"]')).not.toExist();
+ expect(wrapper.find('[data-test="no-dataset-write"]')).toExist();
+});
- it('renders a button', () => {
- expect(wrapper.find(Button)).toExist();
- });
+test('renders dataset help text when user has dataset write permissions', async () => {
+ const wrapper = await getWrapper(mockUserWithDatasetWrite);
+ expect(wrapper.find('[data-test="dataset-write"]')).toExist();
+ expect(wrapper.find('[data-test="no-dataset-write"]')).not.toExist();
+});
- it('renders a disabled button if no datasource is selected', () => {
- expect(
- wrapper.find(Button).find({ disabled: true }).hostNodes(),
- ).toHaveLength(1);
- });
+test('renders a button', async () => {
+ const wrapper = await getWrapper();
+ expect(wrapper.find(Button)).toExist();
+});
- it('renders an enabled button if datasource and viz type is selected', () => {
- wrapper.setState({
- datasource,
- visType: 'table',
- });
- expect(
- wrapper.find(Button).find({ disabled: true }).hostNodes(),
- ).toHaveLength(0);
+test('renders a disabled button if no datasource is selected', async () => {
+ const wrapper = await getWrapper();
+ expect(
+ wrapper.find(Button).find({ disabled: true }).hostNodes(),
+ ).toHaveLength(1);
+});
+
+test('renders an enabled button if datasource and viz type are selected', async () => {
+ const wrapper = await getWrapper();
+ wrapper.setState({
+ datasource,
+ visType: 'table',
});
+ expect(
+ wrapper.find(Button).find({ disabled: true }).hostNodes(),
+ ).toHaveLength(0);
+});
- it('formats explore url', () => {
- wrapper.setState({
- datasource,
- visType: 'table',
- });
- const formattedUrl =
- '/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D';
- expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
+test('formats Explore url', async () => {
+ const wrapper = await getWrapper();
+ wrapper.setState({
+ datasource,
+ visType: 'table',
});
+ const formattedUrl =
+ '/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D';
+ expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
});
diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx b/superset-frontend/src/addSlice/AddSliceContainer.tsx
index fd22377314ac8..66183219adbd4 100644
--- a/superset-frontend/src/addSlice/AddSliceContainer.tsx
+++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx
@@ -24,12 +24,13 @@ import { URL_PARAMS } from 'src/constants';
import { isNullish } from 'src/utils/common';
import Button from 'src/components/Button';
import { Select, Steps } from 'src/components';
-import { FormLabel } from 'src/components/Form';
import { Tooltip } from 'src/components/Tooltip';
import VizTypeGallery, {
MAX_ADVISABLE_VIZ_GALLERY_WIDTH,
} from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
+import findPermission from 'src/dashboard/util/findPermission';
+import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
type Dataset = {
id: number;
@@ -38,11 +39,14 @@ type Dataset = {
datasource_type: string;
};
-export type AddSliceContainerProps = {};
+export type AddSliceContainerProps = {
+ user: UserWithPermissionsAndRoles;
+};
export type AddSliceContainerState = {
datasource?: { label: string; value: string };
visType: string | null;
+ canCreateDataset: boolean;
};
const ESTIMATED_NAV_HEIGHT = 56;
@@ -73,7 +77,6 @@ const StyledContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
- margin-bottom: ${theme.gridUnit * 2}px;
& > div {
min-width: 200px;
@@ -180,6 +183,24 @@ const StyledLabel = styled.span`
`}
`;
+const StyledStepTitle = styled.span`
+ ${({
+ theme: {
+ typography: { sizes, weights },
+ },
+ }) => `
+ font-size: ${sizes.m}px;
+ font-weight: ${weights.bold};
+ `}
+`;
+
+const StyledStepDescription = styled.div`
+ ${({ theme: { gridUnit } }) => `
+ margin-top: ${gridUnit * 4}px;
+ margin-bottom: ${gridUnit * 3}px;
+ `}
+`;
+
export default class AddSliceContainer extends React.PureComponent<
AddSliceContainerProps,
AddSliceContainerState
@@ -188,6 +209,11 @@ export default class AddSliceContainer extends React.PureComponent<
super(props);
this.state = {
visType: null,
+ canCreateDataset: findPermission(
+ 'can_write',
+ 'Dataset',
+ props.user.roles,
+ ),
};
this.changeDatasource = this.changeDatasource.bind(this);
@@ -276,15 +302,49 @@ export default class AddSliceContainer extends React.PureComponent<
render() {
const isButtonDisabled = this.isBtnDisabled();
+ const datasetHelpText = this.state.canCreateDataset ? (
+
+
+ {t('Add a dataset')}
+
+ {` ${t('or')} `}
+
+ {`${t('view instructions')} `}
+
+
+ .
+
+ ) : (
+
+
+ {`${t('View instructions')} `}
+
+
+ .
+
+ );
+
return (
{t('Create a new chart')}
{t('Choose a dataset')}}
+ title={{t('Choose a dataset')}}
status={this.state.datasource?.value ? 'finish' : 'process'}
description={
-
+
-
- {t(
- 'Instructions to add a dataset are available in the Superset tutorial.',
- )}{' '}
-
-
-
-
-
+ {datasetHelpText}
+
}
/>
{t('Choose chart type')}}
+ title={{t('Choose chart type')}}
status={this.state.visType ? 'finish' : 'process'}
description={
-
+
+
+
}
/>
diff --git a/superset-frontend/src/addSlice/App.tsx b/superset-frontend/src/addSlice/App.tsx
index dac830308364f..61c9078b2aa96 100644
--- a/superset-frontend/src/addSlice/App.tsx
+++ b/superset-frontend/src/addSlice/App.tsx
@@ -41,7 +41,7 @@ const App = () => (
-
+
);
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.test.jsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.test.jsx
index 6c416ae55e539..948c22fa4070a 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.test.jsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.test.jsx
@@ -183,6 +183,12 @@ describe('DatasetList', () => {
});
});
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useLocation: () => ({}),
+ useHistory: () => ({}),
+}));
+
describe('RTL', () => {
async function renderAndWait() {
const mounted = act(async () => {
@@ -191,7 +197,7 @@ describe('RTL', () => {
,
- { useRedux: true },
+ { useRedux: true, useRouter: true },
);
});
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index 80428a9a13c76..db66aa067448c 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -22,8 +22,10 @@ import React, {
useState,
useMemo,
useCallback,
+ useEffect,
} from 'react';
import rison from 'rison';
+import { useHistory, useLocation } from 'react-router-dom';
import {
createFetchRelated,
createFetchDistinct,
@@ -553,6 +555,26 @@ const DatasetList: FunctionComponent = ({
});
}
+ const CREATE_HASH = '#create';
+ const location = useLocation();
+ const history = useHistory();
+
+ // Sync Dataset Add modal with #create hash
+ useEffect(() => {
+ const modalOpen = location.hash === CREATE_HASH && canCreate;
+ setDatasetAddModalOpen(modalOpen);
+ }, [canCreate, location.hash]);
+
+ // Add #create hash
+ const openDatasetAddModal = useCallback(() => {
+ history.replace(`${location.pathname}${location.search}${CREATE_HASH}`);
+ }, [history, location.pathname, location.search]);
+
+ // Remove #create hash
+ const closeDatasetAddModal = useCallback(() => {
+ history.replace(`${location.pathname}${location.search}`);
+ }, [history, location.pathname, location.search]);
+
if (canCreate) {
buttonArr.push({
name: (
@@ -560,7 +582,7 @@ const DatasetList: FunctionComponent = ({
{t('Dataset')}{' '}
>
),
- onClick: () => setDatasetAddModalOpen(true),
+ onClick: openDatasetAddModal,
buttonStyle: 'primary',
});
@@ -631,7 +653,7 @@ const DatasetList: FunctionComponent = ({
setDatasetAddModalOpen(false)}
+ onHide={closeDatasetAddModal}
onDatasetAdd={refreshData}
/>
{datasetCurrentlyDeleting && (
diff --git a/superset/views/chart/views.py b/superset/views/chart/views.py
index 72058c32e7f33..60def486814f1 100644
--- a/superset/views/chart/views.py
+++ b/superset/views/chart/views.py
@@ -63,7 +63,7 @@ def pre_delete(self, item: "SliceModelView") -> None:
def add(self) -> FlaskResponse:
payload = {
"common": common_bootstrap_payload(),
- "user": bootstrap_user_data(g.user),
+ "user": bootstrap_user_data(g.user, include_perms=True),
}
return self.render_template(
"superset/add_slice.html", bootstrap_data=json.dumps(payload)