[dashboard] Modal for editing dashboard properties & metadata (#8876)
* wip * wip * wip * modal to update dashboard properties * cleanup * translations and flavor text * linted * more explanatory text in the modal
This commit is contained in:
parent
50f21cb7db
commit
614f13377b
|
|
@ -63,6 +63,8 @@ describe('Header', () => {
|
|||
redoLength: 0,
|
||||
setMaxUndoHistoryExceeded: () => {},
|
||||
maxUndoHistoryToast: () => {},
|
||||
dashboardInfoChanged: () => {},
|
||||
dashboardTitleChanged: () => {},
|
||||
};
|
||||
|
||||
function setup(overrideProps) {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
</Button>
|
||||
)}
|
||||
|
||||
{this.state.showingPropertiesModal && (
|
||||
<PropertiesModal
|
||||
dashboardTitle={dashboardTitle}
|
||||
dashboardInfo={dashboardInfo}
|
||||
show={this.state.showingPropertiesModal}
|
||||
onHide={this.hidePropertiesModal}
|
||||
onDashboardSave={updates => {
|
||||
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}/`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HeaderActionsDropdown
|
||||
addSuccessToast={this.props.addSuccessToast}
|
||||
addDangerToast={this.props.addDangerToast}
|
||||
|
|
@ -427,6 +462,7 @@ class Header extends React.PureComponent {
|
|||
userCanEdit={userCanEdit}
|
||||
userCanSave={userCanSaveAs}
|
||||
isLoading={isLoading}
|
||||
showPropertiesModal={this.showPropertiesModal}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<MenuItem target="_blank" href={`/dashboard/edit/${dashboardId}`}>
|
||||
{t('Edit dashboard metadata')}
|
||||
<MenuItem onClick={this.props.showPropertiesModal}>
|
||||
{t('Edit dashboard properties')}
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Modal show={this.props.show} onHide={this.props.onHide} bsSize="lg">
|
||||
<form onSubmit={this.save}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
<div>
|
||||
<span className="float-left">{t('Dashboard Properties')}</span>
|
||||
</div>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h3>{t('Basic Information')}</h3>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<label className="control-label" htmlFor="embed-height">
|
||||
{t('Title')}
|
||||
</label>
|
||||
<FormControl
|
||||
name="dashboard_title"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
value={values.dashboard_title}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<label className="control-label" htmlFor="embed-height">
|
||||
{t('URL Slug')}
|
||||
</label>
|
||||
<FormControl
|
||||
name="slug"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
value={values.slug}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t('A readable URL for your dashboard')}
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
<label className="control-label" htmlFor="owners">
|
||||
{t('Owners')}
|
||||
</label>
|
||||
{userOptions && (
|
||||
<>
|
||||
<Select
|
||||
name="owners"
|
||||
multi
|
||||
isLoading={!userOptions}
|
||||
value={values.owners}
|
||||
options={userOptions || []}
|
||||
onChange={this.onOwnersChange}
|
||||
disabled={!isOwnersLoaded}
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'Owners is a list of users who can alter the dashboard.',
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col md={12}>
|
||||
<h3 style={{ marginTop: '1em' }}>
|
||||
<button
|
||||
type="button"
|
||||
className="text-button"
|
||||
onClick={this.toggleAdvanced}
|
||||
>
|
||||
<i
|
||||
className={`fa fa-angle-${
|
||||
isAdvancedOpen ? 'down' : 'right'
|
||||
}`}
|
||||
style={{ minWidth: '1em' }}
|
||||
/>
|
||||
{t('Advanced')}
|
||||
</button>
|
||||
</h3>
|
||||
{isAdvancedOpen && (
|
||||
<>
|
||||
<label className="control-label" htmlFor="json_metadata">
|
||||
{t('JSON Metadata')}
|
||||
</label>
|
||||
<FormControl
|
||||
componentClass="textarea"
|
||||
style={{ maxWidth: '100%' }}
|
||||
name="json_metadata"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
value={values.json_metadata}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.',
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<span className="float-right">
|
||||
<Button
|
||||
type="submit"
|
||||
bsSize="sm"
|
||||
bsStyle="primary"
|
||||
className="m-r-5"
|
||||
disabled={this.state.errors.length > 0}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
<Button type="button" bsSize="sm" onClick={this.props.onHide}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Dialog
|
||||
ref={ref => {
|
||||
this.dialog = ref;
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PropertiesModal.propTypes = propTypes;
|
||||
PropertiesModal.defaultProps = defaultProps;
|
||||
|
||||
export default withToasts(PropertiesModal);
|
||||
|
|
@ -22,6 +22,8 @@ import { connect } from 'react-redux';
|
|||
import DashboardHeader from '../components/Header';
|
||||
import isDashboardLoading from '../util/isDashboardLoading';
|
||||
|
||||
import { dashboardInfoChanged } from '../actions/dashboardInfo';
|
||||
|
||||
import {
|
||||
setEditMode,
|
||||
showBuilderPane,
|
||||
|
|
@ -42,6 +44,7 @@ import {
|
|||
undoLayoutAction,
|
||||
redoLayoutAction,
|
||||
updateDashboardTitle,
|
||||
dashboardTitleChanged,
|
||||
} from '../actions/dashboardLayout';
|
||||
|
||||
import {
|
||||
|
|
@ -107,6 +110,8 @@ function mapDispatchToProps(dispatch) {
|
|||
maxUndoHistoryToast,
|
||||
logEvent,
|
||||
setRefreshFrequency,
|
||||
dashboardInfoChanged,
|
||||
dashboardTitleChanged,
|
||||
},
|
||||
dispatch,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* 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 { DASHBOARD_INFO_UPDATED } from '../actions/dashboardInfo';
|
||||
|
||||
export default function dashboardStateReducer(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case DASHBOARD_INFO_UPDATED:
|
||||
return {
|
||||
...state,
|
||||
...action.newInfo,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
DASHBOARD_ROOT_ID,
|
||||
DASHBOARD_GRID_ID,
|
||||
NEW_COMPONENTS_SOURCE_ID,
|
||||
DASHBOARD_HEADER_ID,
|
||||
} from '../util/constants';
|
||||
import componentIsResizable from '../util/componentIsResizable';
|
||||
import findParentId from '../util/findParentId';
|
||||
|
|
@ -39,6 +40,7 @@ import {
|
|||
MOVE_COMPONENT,
|
||||
CREATE_TOP_LEVEL_TABS,
|
||||
DELETE_TOP_LEVEL_TABS,
|
||||
DASHBOARD_TITLE_CHANGED,
|
||||
} from '../actions/dashboardLayout';
|
||||
|
||||
const actionHandlers = {
|
||||
|
|
@ -273,6 +275,19 @@ const actionHandlers = {
|
|||
...nextState,
|
||||
};
|
||||
},
|
||||
|
||||
[DASHBOARD_TITLE_CHANGED](state, action) {
|
||||
return {
|
||||
...state,
|
||||
[DASHBOARD_HEADER_ID]: {
|
||||
...state[DASHBOARD_HEADER_ID],
|
||||
meta: {
|
||||
...state[DASHBOARD_HEADER_ID].meta,
|
||||
text: action.text,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default function layoutReducer(state = {}, action) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
import { combineReducers } from 'redux';
|
||||
|
||||
import charts from '../../chart/chartReducer';
|
||||
import dashboardInfo from './dashboardInfo';
|
||||
import dashboardState from './dashboardState';
|
||||
import dashboardFilters from './dashboardFilters';
|
||||
import datasources from './datasources';
|
||||
|
|
@ -26,7 +27,6 @@ import sliceEntities from './sliceEntities';
|
|||
import dashboardLayout from '../reducers/undoableDashboardLayout';
|
||||
import messageToasts from '../../messageToasts/reducers';
|
||||
|
||||
const dashboardInfo = (state = {}) => state;
|
||||
const impressionId = (state = '') => state;
|
||||
|
||||
export default combineReducers({
|
||||
|
|
|
|||
|
|
@ -41,3 +41,14 @@
|
|||
padding-left: 8px;
|
||||
font-size: @font-size-m;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
outline: none;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue