mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
chore(newchart): update chart creation dataset selection help text, styles (#20369)
* Update dataset selection help text. * Update 'Create a new chart' flow styles. * Add support for linking directly to Create Dataset modal via URL hash. * Add support for linking directly to Create Dataset modal via URL hash. * Update dataset help text to not include spaces in translated strings and only include an 'Add dataset' link when user has permission to add dataset. * Clean up test file Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
parent
f3b289d3c3
commit
44c5e2879b
@ -27,61 +27,97 @@ import AddSliceContainer, {
|
|||||||
import VizTypeGallery from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
|
import VizTypeGallery from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
|
||||||
import { styledMount as mount } from 'spec/helpers/theming';
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
import { act } from 'spec/helpers/testing-library';
|
import { act } from 'spec/helpers/testing-library';
|
||||||
|
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||||
|
|
||||||
const datasource = {
|
const datasource = {
|
||||||
value: '1',
|
value: '1',
|
||||||
label: 'table',
|
label: 'table',
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('AddSliceContainer', () => {
|
const mockUser: UserWithPermissionsAndRoles = {
|
||||||
let wrapper: ReactWrapper<
|
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(<AddSliceContainer user={user} />) as ReactWrapper<
|
||||||
AddSliceContainerProps,
|
AddSliceContainerProps,
|
||||||
AddSliceContainerState,
|
AddSliceContainerState,
|
||||||
AddSliceContainer
|
AddSliceContainer
|
||||||
>;
|
>;
|
||||||
|
await act(() => new Promise(resolve => setTimeout(resolve, 0)));
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
test('renders a select and a VizTypeControl', async () => {
|
||||||
wrapper = mount(<AddSliceContainer />) as ReactWrapper<
|
const wrapper = await getWrapper();
|
||||||
AddSliceContainerProps,
|
expect(wrapper.find(Select)).toExist();
|
||||||
AddSliceContainerState,
|
expect(wrapper.find(VizTypeGallery)).toExist();
|
||||||
AddSliceContainer
|
});
|
||||||
>;
|
|
||||||
// suppress a warning caused by some unusual async behavior in Icon
|
test('renders dataset help text when user lacks dataset write permissions', async () => {
|
||||||
await act(() => new Promise(resolve => setTimeout(resolve, 0)));
|
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 select and a VizTypeControl', () => {
|
});
|
||||||
expect(wrapper.find(Select)).toExist();
|
|
||||||
expect(wrapper.find(VizTypeGallery)).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();
|
||||||
it('renders a button', () => {
|
expect(wrapper.find('[data-test="no-dataset-write"]')).not.toExist();
|
||||||
expect(wrapper.find(Button)).toExist();
|
});
|
||||||
});
|
|
||||||
|
test('renders a button', async () => {
|
||||||
it('renders a disabled button if no datasource is selected', () => {
|
const wrapper = await getWrapper();
|
||||||
expect(
|
expect(wrapper.find(Button)).toExist();
|
||||||
wrapper.find(Button).find({ disabled: true }).hostNodes(),
|
});
|
||||||
).toHaveLength(1);
|
|
||||||
});
|
test('renders a disabled button if no datasource is selected', async () => {
|
||||||
|
const wrapper = await getWrapper();
|
||||||
it('renders an enabled button if datasource and viz type is selected', () => {
|
expect(
|
||||||
wrapper.setState({
|
wrapper.find(Button).find({ disabled: true }).hostNodes(),
|
||||||
datasource,
|
).toHaveLength(1);
|
||||||
visType: 'table',
|
});
|
||||||
});
|
|
||||||
expect(
|
test('renders an enabled button if datasource and viz type are selected', async () => {
|
||||||
wrapper.find(Button).find({ disabled: true }).hostNodes(),
|
const wrapper = await getWrapper();
|
||||||
).toHaveLength(0);
|
wrapper.setState({
|
||||||
});
|
datasource,
|
||||||
|
visType: 'table',
|
||||||
it('formats explore url', () => {
|
});
|
||||||
wrapper.setState({
|
expect(
|
||||||
datasource,
|
wrapper.find(Button).find({ disabled: true }).hostNodes(),
|
||||||
visType: 'table',
|
).toHaveLength(0);
|
||||||
});
|
});
|
||||||
const formattedUrl =
|
|
||||||
'/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D';
|
test('formats Explore url', async () => {
|
||||||
expect(wrapper.instance().exploreUrl()).toBe(formattedUrl);
|
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);
|
||||||
});
|
});
|
||||||
|
@ -24,12 +24,13 @@ import { URL_PARAMS } from 'src/constants';
|
|||||||
import { isNullish } from 'src/utils/common';
|
import { isNullish } from 'src/utils/common';
|
||||||
import Button from 'src/components/Button';
|
import Button from 'src/components/Button';
|
||||||
import { Select, Steps } from 'src/components';
|
import { Select, Steps } from 'src/components';
|
||||||
import { FormLabel } from 'src/components/Form';
|
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
|
||||||
import VizTypeGallery, {
|
import VizTypeGallery, {
|
||||||
MAX_ADVISABLE_VIZ_GALLERY_WIDTH,
|
MAX_ADVISABLE_VIZ_GALLERY_WIDTH,
|
||||||
} from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
|
} from 'src/explore/components/controls/VizTypeControl/VizTypeGallery';
|
||||||
|
import findPermission from 'src/dashboard/util/findPermission';
|
||||||
|
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||||
|
|
||||||
type Dataset = {
|
type Dataset = {
|
||||||
id: number;
|
id: number;
|
||||||
@ -38,11 +39,14 @@ type Dataset = {
|
|||||||
datasource_type: string;
|
datasource_type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddSliceContainerProps = {};
|
export type AddSliceContainerProps = {
|
||||||
|
user: UserWithPermissionsAndRoles;
|
||||||
|
};
|
||||||
|
|
||||||
export type AddSliceContainerState = {
|
export type AddSliceContainerState = {
|
||||||
datasource?: { label: string; value: string };
|
datasource?: { label: string; value: string };
|
||||||
visType: string | null;
|
visType: string | null;
|
||||||
|
canCreateDataset: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ESTIMATED_NAV_HEIGHT = 56;
|
const ESTIMATED_NAV_HEIGHT = 56;
|
||||||
@ -73,7 +77,6 @@ const StyledContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: ${theme.gridUnit * 2}px;
|
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
min-width: 200px;
|
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<
|
export default class AddSliceContainer extends React.PureComponent<
|
||||||
AddSliceContainerProps,
|
AddSliceContainerProps,
|
||||||
AddSliceContainerState
|
AddSliceContainerState
|
||||||
@ -188,6 +209,11 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visType: null,
|
visType: null,
|
||||||
|
canCreateDataset: findPermission(
|
||||||
|
'can_write',
|
||||||
|
'Dataset',
|
||||||
|
props.user.roles,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeDatasource = this.changeDatasource.bind(this);
|
this.changeDatasource = this.changeDatasource.bind(this);
|
||||||
@ -276,15 +302,49 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const isButtonDisabled = this.isBtnDisabled();
|
const isButtonDisabled = this.isBtnDisabled();
|
||||||
|
const datasetHelpText = this.state.canCreateDataset ? (
|
||||||
|
<span data-test="dataset-write">
|
||||||
|
<a
|
||||||
|
href="/tablemodelview/list/#create"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t('Add a dataset')}
|
||||||
|
</a>
|
||||||
|
{` ${t('or')} `}
|
||||||
|
<a
|
||||||
|
href="https://superset.apache.org/docs/creating-charts-dashboards/creating-your-first-dashboard/#registering-a-new-table"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{`${t('view instructions')} `}
|
||||||
|
<i className="fa fa-external-link" />
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span data-test="no-dataset-write">
|
||||||
|
<a
|
||||||
|
href="https://superset.apache.org/docs/creating-charts-dashboards/creating-your-first-dashboard/#registering-a-new-table"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{`${t('View instructions')} `}
|
||||||
|
<i className="fa fa-external-link" />
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<h3>{t('Create a new chart')}</h3>
|
<h3>{t('Create a new chart')}</h3>
|
||||||
<Steps direction="vertical" size="small">
|
<Steps direction="vertical" size="small">
|
||||||
<Steps.Step
|
<Steps.Step
|
||||||
title={<FormLabel>{t('Choose a dataset')}</FormLabel>}
|
title={<StyledStepTitle>{t('Choose a dataset')}</StyledStepTitle>}
|
||||||
status={this.state.datasource?.value ? 'finish' : 'process'}
|
status={this.state.datasource?.value ? 'finish' : 'process'}
|
||||||
description={
|
description={
|
||||||
<div className="dataset">
|
<StyledStepDescription className="dataset">
|
||||||
<Select
|
<Select
|
||||||
autoFocus
|
autoFocus
|
||||||
ariaLabel={t('Dataset')}
|
ariaLabel={t('Dataset')}
|
||||||
@ -296,30 +356,21 @@ export default class AddSliceContainer extends React.PureComponent<
|
|||||||
showSearch
|
showSearch
|
||||||
value={this.state.datasource}
|
value={this.state.datasource}
|
||||||
/>
|
/>
|
||||||
<span>
|
{datasetHelpText}
|
||||||
{t(
|
</StyledStepDescription>
|
||||||
'Instructions to add a dataset are available in the Superset tutorial.',
|
|
||||||
)}{' '}
|
|
||||||
<a
|
|
||||||
href="https://superset.apache.org/docs/creating-charts-dashboards/creating-your-first-dashboard/#registering-a-new-table"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<i className="fa fa-external-link" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Steps.Step
|
<Steps.Step
|
||||||
title={<FormLabel>{t('Choose chart type')}</FormLabel>}
|
title={<StyledStepTitle>{t('Choose chart type')}</StyledStepTitle>}
|
||||||
status={this.state.visType ? 'finish' : 'process'}
|
status={this.state.visType ? 'finish' : 'process'}
|
||||||
description={
|
description={
|
||||||
<VizTypeGallery
|
<StyledStepDescription>
|
||||||
className="viz-gallery"
|
<VizTypeGallery
|
||||||
onChange={this.changeVisType}
|
className="viz-gallery"
|
||||||
selectedViz={this.state.visType}
|
onChange={this.changeVisType}
|
||||||
/>
|
selectedViz={this.state.visType}
|
||||||
|
/>
|
||||||
|
</StyledStepDescription>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
@ -41,7 +41,7 @@ const App = () => (
|
|||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<GlobalStyles />
|
<GlobalStyles />
|
||||||
<DynamicPluginProvider>
|
<DynamicPluginProvider>
|
||||||
<AddSliceContainer />
|
<AddSliceContainer user={bootstrapData.user} />
|
||||||
</DynamicPluginProvider>
|
</DynamicPluginProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -183,6 +183,12 @@ describe('DatasetList', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useLocation: () => ({}),
|
||||||
|
useHistory: () => ({}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('RTL', () => {
|
describe('RTL', () => {
|
||||||
async function renderAndWait() {
|
async function renderAndWait() {
|
||||||
const mounted = act(async () => {
|
const mounted = act(async () => {
|
||||||
@ -191,7 +197,7 @@ describe('RTL', () => {
|
|||||||
<QueryParamProvider>
|
<QueryParamProvider>
|
||||||
<DatasetList {...mockedProps} user={mockUser} />
|
<DatasetList {...mockedProps} user={mockUser} />
|
||||||
</QueryParamProvider>,
|
</QueryParamProvider>,
|
||||||
{ useRedux: true },
|
{ useRedux: true, useRouter: true },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -22,8 +22,10 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
useMemo,
|
useMemo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import rison from 'rison';
|
import rison from 'rison';
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
createFetchRelated,
|
createFetchRelated,
|
||||||
createFetchDistinct,
|
createFetchDistinct,
|
||||||
@ -553,6 +555,26 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (canCreate) {
|
||||||
buttonArr.push({
|
buttonArr.push({
|
||||||
name: (
|
name: (
|
||||||
@ -560,7 +582,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
<i className="fa fa-plus" /> {t('Dataset')}{' '}
|
<i className="fa fa-plus" /> {t('Dataset')}{' '}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
onClick: () => setDatasetAddModalOpen(true),
|
onClick: openDatasetAddModal,
|
||||||
buttonStyle: 'primary',
|
buttonStyle: 'primary',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -631,7 +653,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||||||
<SubMenu {...menuData} />
|
<SubMenu {...menuData} />
|
||||||
<AddDatasetModal
|
<AddDatasetModal
|
||||||
show={datasetAddModalOpen}
|
show={datasetAddModalOpen}
|
||||||
onHide={() => setDatasetAddModalOpen(false)}
|
onHide={closeDatasetAddModal}
|
||||||
onDatasetAdd={refreshData}
|
onDatasetAdd={refreshData}
|
||||||
/>
|
/>
|
||||||
{datasetCurrentlyDeleting && (
|
{datasetCurrentlyDeleting && (
|
||||||
|
@ -63,7 +63,7 @@ class SliceModelView(
|
|||||||
def add(self) -> FlaskResponse:
|
def add(self) -> FlaskResponse:
|
||||||
payload = {
|
payload = {
|
||||||
"common": common_bootstrap_payload(),
|
"common": common_bootstrap_payload(),
|
||||||
"user": bootstrap_user_data(g.user),
|
"user": bootstrap_user_data(g.user, include_perms=True),
|
||||||
}
|
}
|
||||||
return self.render_template(
|
return self.render_template(
|
||||||
"superset/add_slice.html", bootstrap_data=json.dumps(payload)
|
"superset/add_slice.html", bootstrap_data=json.dumps(payload)
|
||||||
|
Loading…
Reference in New Issue
Block a user