diff --git a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx index dd53e74f4..8adfac7c3 100644 --- a/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/Header_spec.jsx @@ -63,6 +63,8 @@ describe('Header', () => { redoLength: 0, setMaxUndoHistoryExceeded: () => {}, maxUndoHistoryToast: () => {}, + dashboardInfoChanged: () => {}, + dashboardTitleChanged: () => {}, }; function setup(overrideProps) { diff --git a/superset/assets/src/dashboard/actions/dashboardInfo.js b/superset/assets/src/dashboard/actions/dashboardInfo.js new file mode 100644 index 000000000..10b6f21d6 --- /dev/null +++ b/superset/assets/src/dashboard/actions/dashboardInfo.js @@ -0,0 +1,25 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DASHBOARD_INFO_UPDATED = 'DASHBOARD_INFO_UPDATED'; + +// updates partially changed dashboard info +export function dashboardInfoChanged(newInfo) { + return { type: DASHBOARD_INFO_UPDATED, newInfo }; +} diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js index 7389399af..7ef8edb73 100644 --- a/superset/assets/src/dashboard/actions/dashboardLayout.js +++ b/superset/assets/src/dashboard/actions/dashboardLayout.js @@ -88,6 +88,16 @@ export function updateDashboardTitle(text) { }; } +export const DASHBOARD_TITLE_CHANGED = 'DASHBOARD_TITLE_CHANGED'; + +// call this one when it's not an undo-able action +export function dashboardTitleChanged(text) { + return { + type: DASHBOARD_TITLE_CHANGED, + text, + }; +} + export const DELETE_COMPONENT = 'DELETE_COMPONENT'; export const deleteComponent = setUnsavedChangesAfterAction((id, parentId) => ({ type: DELETE_COMPONENT, diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index 843eeb5c8..489d16cf6 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -44,6 +44,7 @@ import { LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, LOG_ACTIONS_TOGGLE_EDIT_DASHBOARD, } from '../../logger/LogUtils'; +import PropertiesModal from './PropertiesModal'; const propTypes = { addSuccessToast: PropTypes.func.isRequired, @@ -86,6 +87,8 @@ const propTypes = { maxUndoHistoryToast: PropTypes.func.isRequired, refreshFrequency: PropTypes.number.isRequired, setRefreshFrequency: PropTypes.func.isRequired, + dashboardInfoChanged: PropTypes.func.isRequired, + dashboardTitleChanged: PropTypes.func.isRequired, }; const defaultProps = { @@ -103,6 +106,7 @@ class Header extends React.PureComponent { this.state = { didNotifyMaxUndoHistoryToast: false, emphasizeUndo: false, + showingPropertiesModal: false, }; this.handleChangeText = this.handleChangeText.bind(this); @@ -116,6 +120,8 @@ class Header extends React.PureComponent { this.forceRefresh = this.forceRefresh.bind(this); this.startPeriodicRender = this.startPeriodicRender.bind(this); this.overwriteDashboard = this.overwriteDashboard.bind(this); + this.showPropertiesModal = this.showPropertiesModal.bind(this); + this.hidePropertiesModal = this.hidePropertiesModal.bind(this); } componentDidMount() { @@ -256,6 +262,14 @@ class Header extends React.PureComponent { } } + showPropertiesModal() { + this.setState({ showingPropertiesModal: true }); + } + + hidePropertiesModal() { + this.setState({ showingPropertiesModal: false }); + } + render() { const { dashboardTitle, @@ -405,6 +419,27 @@ class Header extends React.PureComponent { )} + {this.state.showingPropertiesModal && ( + { + this.props.dashboardInfoChanged({ + slug: updates.slug, + metadata: JSON.parse(updates.jsonMetadata), + }); + this.props.dashboardTitleChanged(updates.title); + history.pushState( + { event: 'dashboard_properties_changed' }, + '', + `/superset/dashboard/${updates.slug}/`, + ); + }} + /> + )} + diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx index cf89e5eca..c53c19679 100644 --- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx @@ -53,6 +53,7 @@ const propTypes = { layout: PropTypes.object.isRequired, expandedSlices: PropTypes.object.isRequired, onSave: PropTypes.func.isRequired, + showPropertiesModal: PropTypes.func.isRequired, }; const defaultProps = { @@ -190,8 +191,8 @@ class HeaderActionsDropdown extends React.PureComponent { /> {editMode && ( - - {t('Edit dashboard metadata')} + + {t('Edit dashboard properties')} )} diff --git a/superset/assets/src/dashboard/components/PropertiesModal.jsx b/superset/assets/src/dashboard/components/PropertiesModal.jsx new file mode 100644 index 000000000..916bdeafb --- /dev/null +++ b/superset/assets/src/dashboard/components/PropertiesModal.jsx @@ -0,0 +1,297 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Row, Col, Button, Modal, FormControl } from 'react-bootstrap'; +import Dialog from 'react-bootstrap-dialog'; +import Select from 'react-select'; +import { t } from '@superset-ui/translation'; +import { SupersetClient } from '@superset-ui/connection'; + +import getClientErrorObject from '../../utils/getClientErrorObject'; +import withToasts from '../../messageToasts/enhancers/withToasts'; + +const propTypes = { + dashboardTitle: PropTypes.string, + dashboardInfo: PropTypes.object, + owners: PropTypes.arrayOf(PropTypes.object), + show: PropTypes.bool.isRequired, + onHide: PropTypes.func, + onDashboardSave: PropTypes.func, + addSuccessToast: PropTypes.func.isRequired, +}; + +const defaultProps = { + dashboardInfo: {}, + dashboardTitle: '[dashboard name]', + owners: [], + onHide: () => {}, + onDashboardSave: () => {}, + show: false, +}; + +class PropertiesModal extends React.PureComponent { + constructor(props) { + super(props); + this.state = { + errors: [], + values: { + dashboard_title: this.props.dashboardTitle, + slug: this.props.dashboardInfo.slug, + owners: this.props.owners || [], + json_metadata: JSON.stringify(this.props.dashboardInfo.metadata), + }, + isOwnersLoaded: false, + userOptions: null, + isAdvancedOpen: false, + }; + this.onChange = this.onChange.bind(this); + this.onOwnersChange = this.onOwnersChange.bind(this); + this.save = this.save.bind(this); + this.toggleAdvanced = this.toggleAdvanced.bind(this); + } + + componentDidMount() { + SupersetClient.get({ endpoint: `/users/api/read` }).then(response => { + const options = response.json.result.map((user, i) => ({ + // ids are in a separate `pks` array in the results... need api v2 + value: response.json.pks[i], + label: `${user.first_name} ${user.last_name}`, + })); + 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) { + this.setState(state => ({ + values: { + ...state.values, + owners: value, + }, + })); + } + + onChange(e) { + const { name, value } = e.target; + this.setState(state => ({ + values: { + ...state.values, + [name]: value, + }, + })); + } + + toggleAdvanced() { + this.setState(state => ({ + isAdvancedOpen: !state.isAdvancedOpen, + })); + } + + save(e) { + e.preventDefault(); + e.stopPropagation(); + const owners = this.state.values.owners.map(o => o.value); + SupersetClient.put({ + endpoint: `/api/v1/dashboard/${this.props.dashboardInfo.id}`, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...this.state.values, + owners, + }), + }) + .then(({ json }) => { + this.props.addSuccessToast(t('The dashboard has been saved')); + this.props.onDashboardSave({ + title: json.result.dashboard_title, + slug: json.result.slug, + jsonMetadata: json.result.json_metadata, + ownerIds: json.result.owners, + }); + this.props.onHide(); + }) + .catch(response => + getClientErrorObject(response).then(({ error, statusText }) => { + this.dialog.show({ + title: 'Error', + bsSize: 'medium', + bsStyle: 'danger', + actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-danger')], + body: error || statusText || t('An error has occurred'), + }); + }), + ); + } + + render() { + const { userOptions, values, isOwnersLoaded, isAdvancedOpen } = this.state; + return ( + +
+ + +
+ {t('Dashboard Properties')} +
+
+
+ + + +

{t('Basic Information')}

+ +
+ + + + + + + + +

+ {t('A readable URL for your dashboard')} +

+ +
+ + +

{t('Access')}

+ + {userOptions && ( + <> +