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:
parent
d7ea41a529
commit
46598830e9
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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.')}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue