show edit modal on dashboards list view (#9211)

* show edit modal on dashboards list view

* lint

* fix test

* simplify PropertiesModal interface

* lint

* comply with method ordering

* fix type issue
This commit is contained in:
David Aaron Suddjian 2020-03-19 08:05:35 -07:00 committed by GitHub
parent d7ea41a529
commit 46598830e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 158 additions and 67 deletions

View File

@ -24,6 +24,7 @@ import fetchMock from 'fetch-mock';
import DashboardList from 'src/views/dashboardList/DashboardList'; import DashboardList from 'src/views/dashboardList/DashboardList';
import ListView from 'src/components/ListView/ListView'; import ListView from 'src/components/ListView/ListView';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
// store needed for withToasts(DashboardTable) // store needed for withToasts(DashboardTable)
const mockStore = configureStore([thunk]); const mockStore = configureStore([thunk]);
@ -93,4 +94,13 @@ describe('DashboardList', () => {
`"/http//localhost/api/v1/dashboard/?q={%22order_column%22:%22changed_on%22,%22order_direction%22:%22desc%22,%22page%22:0,%22page_size%22:25}"`, `"/http//localhost/api/v1/dashboard/?q={%22order_column%22:%22changed_on%22,%22order_direction%22:%22desc%22,%22page%22:0,%22page_size%22:25}"`,
); );
}); });
it('edits', async () => {
expect(wrapper.find(PropertiesModal)).toHaveLength(0);
wrapper
.find('.fa-pencil')
.first()
.simulate('click');
expect(wrapper.find(PropertiesModal)).toHaveLength(1);
});
}); });

View File

@ -449,8 +449,7 @@ class Header extends React.PureComponent {
{this.state.showingPropertiesModal && ( {this.state.showingPropertiesModal && (
<PropertiesModal <PropertiesModal
dashboardTitle={dashboardTitle} dahboardId={dashboardInfo.id}
dashboardInfo={dashboardInfo}
show={this.state.showingPropertiesModal} show={this.state.showingPropertiesModal}
onHide={this.hidePropertiesModal} onHide={this.hidePropertiesModal}
onDashboardSave={updates => { onDashboardSave={updates => {

View File

@ -24,14 +24,13 @@ import Select from 'react-select';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
import { t } from '@superset-ui/translation'; import { t } from '@superset-ui/translation';
import { SupersetClient } from '@superset-ui/connection'; import { SupersetClient } from '@superset-ui/connection';
import '../stylesheets/buttons.less';
import getClientErrorObject from '../../utils/getClientErrorObject'; import getClientErrorObject from '../../utils/getClientErrorObject';
import withToasts from '../../messageToasts/enhancers/withToasts'; import withToasts from '../../messageToasts/enhancers/withToasts';
const propTypes = { const propTypes = {
dashboardTitle: PropTypes.string, dashboardId: PropTypes.number.isRequired,
dashboardInfo: PropTypes.object,
owners: PropTypes.arrayOf(PropTypes.object),
show: PropTypes.bool.isRequired, show: PropTypes.bool.isRequired,
onHide: PropTypes.func, onHide: PropTypes.func,
onDashboardSave: PropTypes.func, onDashboardSave: PropTypes.func,
@ -39,9 +38,6 @@ const propTypes = {
}; };
const defaultProps = { const defaultProps = {
dashboardInfo: {},
dashboardTitle: '[dashboard name]',
owners: [],
onHide: () => {}, onHide: () => {},
onDashboardSave: () => {}, onDashboardSave: () => {},
show: false, show: false,
@ -50,21 +46,16 @@ const defaultProps = {
class PropertiesModal extends React.PureComponent { class PropertiesModal extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.defaultMetadataValue = JSON.stringify(
props.dashboardInfo.metadata,
null,
2,
);
this.state = { this.state = {
errors: [], errors: [],
values: { values: {
dashboard_title: props.dashboardTitle, dashboard_title: '',
slug: props.dashboardInfo.slug, slug: '',
owners: props.owners || [], owners: [],
json_metadata: this.defaultMetadataValue, json_metadata: '',
}, },
isOwnersLoaded: false, isDashboardLoaded: false,
userOptions: null, ownerOptions: null,
isAdvancedOpen: false, isAdvancedOpen: false,
}; };
this.onChange = this.onChange.bind(this); this.onChange = this.onChange.bind(this);
@ -75,27 +66,8 @@ class PropertiesModal extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
SupersetClient.get({ this.fetchOwnerOptions();
endpoint: `/api/v1/dashboard/related/owners`, this.fetchDashboardDetails();
}).then(response => {
const options = response.json.result.map(item => ({
value: item.value,
label: item.text,
}));
this.setState({
userOptions: options,
});
});
SupersetClient.get({
endpoint: `/api/v1/dashboard/${this.props.dashboardInfo.id}`,
}).then(response => {
this.setState({ isOwnersLoaded: true });
const initialSelectedValues = response.json.result.owners.map(owner => ({
value: owner.id,
label: owner.username,
}));
this.onOwnersChange(initialSelectedValues);
});
} }
onOwnersChange(value) { onOwnersChange(value) {
@ -111,6 +83,50 @@ class PropertiesModal extends React.PureComponent {
this.updateFormState(name, value); this.updateFormState(name, value);
} }
fetchDashboardDetails() {
// We fetch the dashboard details because not all code
// that renders this component have all the values we need.
// At some point when we have a more consistent frontend
// datamodel, the dashboard could probably just be passed as a prop.
SupersetClient.get({
endpoint: `/api/v1/dashboard/${this.props.dashboardId}`,
})
.then(response => {
const dashboard = response.json.result;
this.setState(state => ({
isDashboardLoaded: true,
values: {
...state.values,
dashboard_title: dashboard.dashboard_title || '',
slug: dashboard.slug || '',
json_metadata: dashboard.json_metadata || '',
},
}));
const initialSelectedValues = dashboard.owners.map(owner => ({
value: owner.id,
label: owner.username,
}));
this.onOwnersChange(initialSelectedValues);
})
.catch(err => console.error(err));
}
fetchOwnerOptions() {
SupersetClient.get({
endpoint: `/api/v1/dashboard/related/owners`,
})
.then(response => {
const options = response.json.result.map(item => ({
value: item.value,
label: item.text,
}));
this.setState({
ownerOptions: options,
});
})
.catch(err => console.error(err));
}
updateFormState(name, value) { updateFormState(name, value) {
this.setState(state => ({ this.setState(state => ({
values: { values: {
@ -129,18 +145,23 @@ class PropertiesModal extends React.PureComponent {
save(e) { save(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const owners = this.state.values.owners.map(o => o.value); const { values } = this.state;
const owners = values.owners.map(o => o.value);
SupersetClient.put({ SupersetClient.put({
endpoint: `/api/v1/dashboard/${this.props.dashboardInfo.id}`, endpoint: `/api/v1/dashboard/${this.props.dashboardId}`,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
...this.state.values, dashboard_title: values.dashboard_title,
slug: values.slug || null,
json_metadata: values.json_metadata || null,
owners, owners,
}), }),
}) })
.then(({ json }) => { .then(({ json }) => {
this.props.addSuccessToast(t('The dashboard has been saved')); this.props.addSuccessToast(t('The dashboard has been saved'));
this.props.onDashboardSave({ this.props.onDashboardSave({
id: this.props.dashboardId,
title: json.result.dashboard_title, title: json.result.dashboard_title,
slug: json.result.slug, slug: json.result.slug,
jsonMetadata: json.result.json_metadata, jsonMetadata: json.result.json_metadata,
@ -162,7 +183,12 @@ class PropertiesModal extends React.PureComponent {
} }
render() { render() {
const { userOptions, values, isOwnersLoaded, isAdvancedOpen } = this.state; const {
ownerOptions,
values,
isDashboardLoaded,
isAdvancedOpen,
} = this.state;
return ( return (
<Modal show={this.props.show} onHide={this.props.onHide} bsSize="lg"> <Modal show={this.props.show} onHide={this.props.onHide} bsSize="lg">
<form onSubmit={this.save}> <form onSubmit={this.save}>
@ -190,6 +216,7 @@ class PropertiesModal extends React.PureComponent {
bsSize="sm" bsSize="sm"
value={values.dashboard_title} value={values.dashboard_title}
onChange={this.onChange} onChange={this.onChange}
disabled={!isDashboardLoaded}
/> />
</Col> </Col>
<Col md={6}> <Col md={6}>
@ -202,6 +229,7 @@ class PropertiesModal extends React.PureComponent {
bsSize="sm" bsSize="sm"
value={values.slug || ''} value={values.slug || ''}
onChange={this.onChange} onChange={this.onChange}
disabled={!isDashboardLoaded}
/> />
<p className="help-block"> <p className="help-block">
{t('A readable URL for your dashboard')} {t('A readable URL for your dashboard')}
@ -217,11 +245,11 @@ class PropertiesModal extends React.PureComponent {
<Select <Select
name="owners" name="owners"
multi multi
isLoading={!userOptions} isLoading={!ownerOptions}
value={values.owners} value={values.owners}
options={userOptions || []} options={ownerOptions || []}
onChange={this.onOwnersChange} onChange={this.onOwnersChange}
disabled={!userOptions || !isOwnersLoaded} disabled={!ownerOptions || !isDashboardLoaded}
/> />
<p className="help-block"> <p className="help-block">
{t('Owners is a list of users who can alter the dashboard.')} {t('Owners is a list of users who can alter the dashboard.')}

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { ComponentType } from 'react';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
@ -27,7 +29,7 @@ import {
} from '../actions'; } from '../actions';
// To work properly the redux state must have a `messageToasts` subtree // To work properly the redux state must have a `messageToasts` subtree
export default function withToasts(BaseComponent) { export default function withToasts(BaseComponent: ComponentType) {
return connect(null, dispatch => return connect(null, dispatch =>
bindActionCreators( bindActionCreators(
{ {
@ -38,5 +40,7 @@ export default function withToasts(BaseComponent) {
}, },
dispatch, dispatch,
), ),
)(BaseComponent); )(BaseComponent) as any;
// Rsedux has some confusing typings that cause problems for consumers of this function.
// If someone can fix the types, great, but for now it's just any.
} }

View File

@ -31,6 +31,7 @@ import {
Filters, Filters,
} from 'src/components/ListView/types'; } from 'src/components/ListView/types';
import withToasts from 'src/messageToasts/enhancers/withToasts'; import withToasts from 'src/messageToasts/enhancers/withToasts';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
const PAGE_SIZE = 25; const PAGE_SIZE = 25;
@ -48,6 +49,7 @@ interface State {
owners: Array<{ text: string; value: number }>; owners: Array<{ text: string; value: number }>;
permissions: string[]; permissions: string[];
lastFetchDataConfig: FetchDataConfig | null; lastFetchDataConfig: FetchDataConfig | null;
dashboardToEdit: Dashboard | null;
} }
interface Dashboard { interface Dashboard {
@ -75,6 +77,7 @@ class DashboardList extends React.PureComponent<Props, State> {
loading: false, loading: false,
owners: [], owners: [],
permissions: [], permissions: [],
dashboardToEdit: null,
}; };
componentDidMount() { componentDidMount() {
@ -183,7 +186,7 @@ class DashboardList extends React.PureComponent<Props, State> {
{ {
Cell: ({ row: { state, original } }: any) => { Cell: ({ row: { state, original } }: any) => {
const handleDelete = () => this.handleDashboardDelete(original); const handleDelete = () => this.handleDashboardDelete(original);
const handleEdit = () => this.handleDashboardEdit(original); const handleEdit = () => this.openDashboardEditModal(original);
const handleExport = () => this.handleBulkDashboardExport([original]); const handleExport = () => this.handleBulkDashboardExport([original]);
if (!this.canEdit && !this.canDelete && !this.canExport) { if (!this.canEdit && !this.canDelete && !this.canExport) {
return null; return null;
@ -251,8 +254,33 @@ class DashboardList extends React.PureComponent<Props, State> {
return Boolean(this.state.permissions.find(p => p === perm)); return Boolean(this.state.permissions.find(p => p === perm));
}; };
handleDashboardEdit = ({ id }: { id: number }) => { openDashboardEditModal = (dashboard: Dashboard) => {
window.location.assign(`/dashboard/edit/${id}`); this.setState({
dashboardToEdit: dashboard,
});
};
handleDashboardEdit = (edits: any) => {
this.setState({ loading: true });
return SupersetClient.get({
endpoint: `/api/v1/dashboard/${edits.id}`,
})
.then(({ json = {} }) => {
this.setState({
dashboards: this.state.dashboards.map(dashboard => {
if (dashboard.id === json.id) {
return json.result;
}
return dashboard;
}),
loading: false,
});
})
.catch(() => {
this.props.addDangerToast(
t('An error occurred while fetching Dashboards'),
);
});
}; };
handleDashboardDelete = ({ handleDashboardDelete = ({
@ -388,7 +416,13 @@ class DashboardList extends React.PureComponent<Props, State> {
}; };
render() { render() {
const { dashboards, dashboardCount, loading, filters } = this.state; const {
dashboards,
dashboardCount,
loading,
filters,
dashboardToEdit,
} = this.state;
return ( return (
<div className="container welcome"> <div className="container welcome">
@ -425,19 +459,29 @@ class DashboardList extends React.PureComponent<Props, State> {
}); });
} }
return ( return (
<ListView <>
className="dashboard-list-view" {dashboardToEdit && (
title={'Dashboards'} <PropertiesModal
columns={this.columns} show
data={dashboards} dashboardId={dashboardToEdit.id}
count={dashboardCount} onHide={() => this.setState({ dashboardToEdit: null })}
pageSize={PAGE_SIZE} onDashboardSave={this.handleDashboardEdit}
fetchData={this.fetchData} />
loading={loading} )}
initialSort={this.initialSort} <ListView
filters={filters} className="dashboard-list-view"
bulkActions={bulkActions} title={'Dashboards'}
/> columns={this.columns}
data={dashboards}
count={dashboardCount}
pageSize={PAGE_SIZE}
fetchData={this.fetchData}
loading={loading}
initialSort={this.initialSort}
filters={filters}
bulkActions={bulkActions}
/>
</>
); );
}} }}
</ConfirmStatusChange> </ConfirmStatusChange>

View File

@ -116,7 +116,7 @@ class DashboardPutSchema(BaseDashboardSchema):
owners = fields.List(fields.Integer(validate=validate_owner)) owners = fields.List(fields.Integer(validate=validate_owner))
position_json = fields.String(validate=validate_json) position_json = fields.String(validate=validate_json)
css = fields.String() css = fields.String()
json_metadata = fields.String(validate=validate_json_metadata) json_metadata = fields.String(allow_none=True, validate=validate_json_metadata)
published = fields.Boolean() published = fields.Boolean()
@post_load @post_load
@ -142,14 +142,20 @@ class DashboardRestApi(DashboardMixin, BaseOwnedModelRestApi):
class_permission_name = "DashboardModelView" class_permission_name = "DashboardModelView"
show_columns = [ show_columns = [
"id",
"charts", "charts",
"css", "css",
"dashboard_title", "dashboard_title",
"json_metadata", "json_metadata",
"owners.id", "owners.id",
"owners.username", "owners.username",
"changed_by_name",
"changed_by_url",
"changed_by.username",
"changed_on",
"position_json", "position_json",
"published", "published",
"url",
"slug", "slug",
"table_names", "table_names",
] ]