[explore] Modal to edit chart properties (#9051)
* chart edit modal * remove commented code * remove unused prop * address PR feedback * small tweak
This commit is contained in:
parent
1a2ba56f46
commit
d4d7134bf8
|
|
@ -145,3 +145,8 @@ export function createNewSlice(
|
|||
form_data,
|
||||
};
|
||||
}
|
||||
|
||||
export const SLICE_UPDATED = 'SLICE_UPDATED';
|
||||
export function sliceUpdated(slice) {
|
||||
return { type: SLICE_UPDATED, slice };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import ModalTrigger from './../../components/ModalTrigger';
|
|||
import Button from '../../components/Button';
|
||||
import RowCountLabel from './RowCountLabel';
|
||||
import { prepareCopyToClipboardTabularData } from '../../utils/common';
|
||||
import PropertiesModal from './PropertiesModal';
|
||||
|
||||
registerLanguage('markdown', markdownSyntax);
|
||||
registerLanguage('html', htmlSyntax);
|
||||
|
|
@ -58,6 +59,7 @@ const propTypes = {
|
|||
queryResponse: PropTypes.object,
|
||||
chartStatus: PropTypes.string,
|
||||
latestQueryFormData: PropTypes.object.isRequired,
|
||||
slice: PropTypes.object,
|
||||
};
|
||||
const defaultProps = {
|
||||
animation: true,
|
||||
|
|
@ -75,9 +77,12 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
error: null,
|
||||
filterText: '',
|
||||
sqlSupported: datasource && datasource.split('__')[1] === 'table',
|
||||
isPropertiesModalOpen: false,
|
||||
};
|
||||
this.beforeOpen = this.beforeOpen.bind(this);
|
||||
this.changeFilterText = this.changeFilterText.bind(this);
|
||||
this.openPropertiesModal = this.openPropertiesModal.bind(this);
|
||||
this.closePropertiesModal = this.closePropertiesModal.bind(this);
|
||||
}
|
||||
beforeOpen(endpointType) {
|
||||
this.setState({ isLoading: true });
|
||||
|
|
@ -113,6 +118,12 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
redirectSQLLab() {
|
||||
this.props.onOpenInEditor(this.props.latestQueryFormData);
|
||||
}
|
||||
openPropertiesModal() {
|
||||
this.setState({ isPropertiesModalOpen: true });
|
||||
}
|
||||
closePropertiesModal() {
|
||||
this.setState({ isPropertiesModalOpen: false });
|
||||
}
|
||||
renderQueryModalBody() {
|
||||
if (this.state.isLoading) {
|
||||
return <Loading />;
|
||||
|
|
@ -222,6 +233,19 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
pullRight
|
||||
id="query"
|
||||
>
|
||||
{this.props.slice && (
|
||||
<>
|
||||
<MenuItem onClick={this.openPropertiesModal}>
|
||||
{t('Edit properties')}
|
||||
</MenuItem>
|
||||
<PropertiesModal
|
||||
slice={this.props.slice}
|
||||
show={this.state.isPropertiesModalOpen}
|
||||
onHide={this.closePropertiesModal}
|
||||
animation={this.props.animation}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ModalTrigger
|
||||
isMenuItem
|
||||
animation={this.props.animation}
|
||||
|
|
@ -230,7 +254,6 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
bsSize="large"
|
||||
beforeOpen={() => this.beforeOpen('query')}
|
||||
modalBody={this.renderQueryModalBody()}
|
||||
eventKey="1"
|
||||
/>
|
||||
<ModalTrigger
|
||||
isMenuItem
|
||||
|
|
@ -240,7 +263,6 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
bsSize="large"
|
||||
beforeOpen={() => this.beforeOpen('results')}
|
||||
modalBody={this.renderResultsModalBody()}
|
||||
eventKey="2"
|
||||
/>
|
||||
<ModalTrigger
|
||||
isMenuItem
|
||||
|
|
@ -250,7 +272,6 @@ export default class DisplayQueryButton extends React.PureComponent {
|
|||
bsSize="large"
|
||||
beforeOpen={() => this.beforeOpen('samples')}
|
||||
modalBody={this.renderSamplesModalBody()}
|
||||
eventKey="2"
|
||||
/>
|
||||
{this.state.sqlSupported && (
|
||||
<MenuItem eventKey="3" onClick={this.redirectSQLLab.bind(this)}>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ const propTypes = {
|
|||
chartStatus: PropTypes.string,
|
||||
latestQueryFormData: PropTypes.object,
|
||||
queryResponse: PropTypes.object,
|
||||
slice: PropTypes.object,
|
||||
};
|
||||
|
||||
export default function ExploreActionButtons({
|
||||
|
|
@ -41,6 +42,7 @@ export default function ExploreActionButtons({
|
|||
chartStatus,
|
||||
latestQueryFormData,
|
||||
queryResponse,
|
||||
slice,
|
||||
}) {
|
||||
const exportToCSVClasses = cx('btn btn-default btn-sm', {
|
||||
'disabled disabledButton': !canDownload,
|
||||
|
|
@ -89,6 +91,7 @@ export default function ExploreActionButtons({
|
|||
latestQueryFormData={latestQueryFormData}
|
||||
chartStatus={chartStatus}
|
||||
onOpenInEditor={actions.redirectSQLLab}
|
||||
slice={slice}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* 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, { useState, useEffect, useRef } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Row,
|
||||
Col,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
} 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 { sliceUpdated } from '../actions/exploreActions';
|
||||
import getClientErrorObject from '../../utils/getClientErrorObject';
|
||||
|
||||
function PropertiesModalWrapper({ show, onHide, animation, slice, onSave }) {
|
||||
// The wrapper is a separate component so that hooks only run when the modal opens
|
||||
return (
|
||||
<Modal show={show} onHide={onHide} animation={animation} bsSize="large">
|
||||
<PropertiesModal slice={slice} onHide={onHide} onSave={onSave} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function PropertiesModal({ slice, onHide, onSave }) {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const errorDialog = useRef();
|
||||
const [ownerOptions, setOwnerOptions] = useState(null);
|
||||
|
||||
// values of form inputs
|
||||
const [name, setName] = useState(slice.slice_name || '');
|
||||
const [description, setDescription] = useState(slice.description || '');
|
||||
const [cacheTimeout, setCacheTimeout] = useState(
|
||||
slice.cache_timeout != null ? slice.cache_timeout : '',
|
||||
);
|
||||
const [owners, setOwners] = useState(null);
|
||||
|
||||
function showError({ error, statusText }) {
|
||||
errorDialog.current.show({
|
||||
title: 'Error',
|
||||
bsSize: 'medium',
|
||||
bsStyle: 'danger',
|
||||
actions: [Dialog.DefaultAction('Ok', () => {}, 'btn-danger')],
|
||||
body: error || statusText || t('An error has occurred'),
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchOwners() {
|
||||
try {
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/${slice.slice_id}`,
|
||||
});
|
||||
setOwners(
|
||||
response.json.result.owners.map(owner => ({
|
||||
value: owner.id,
|
||||
label: owner.username,
|
||||
})),
|
||||
);
|
||||
} catch (response) {
|
||||
const clientError = await getClientErrorObject(response);
|
||||
showError(clientError);
|
||||
}
|
||||
}
|
||||
|
||||
// get the owners of this slice
|
||||
useEffect(() => {
|
||||
fetchOwners();
|
||||
}, []);
|
||||
|
||||
// get the list of users who can own a chart
|
||||
useEffect(() => {
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/related/owners`,
|
||||
}).then(res => {
|
||||
setOwnerOptions(
|
||||
res.json.result.map(item => ({
|
||||
value: item.value,
|
||||
label: item.text,
|
||||
})),
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onSubmit = async event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
slice_name: name || null,
|
||||
description: description || null,
|
||||
cache_timeout: cacheTimeout || null,
|
||||
owners: owners.map(o => o.value),
|
||||
};
|
||||
try {
|
||||
const res = await SupersetClient.put({
|
||||
endpoint: `/api/v1/chart/${slice.slice_id}`,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
// update the redux state
|
||||
onSave(res.json.result);
|
||||
onHide();
|
||||
} catch (res) {
|
||||
const clientError = await getClientErrorObject(res);
|
||||
showError(clientError);
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Edit Chart Properties</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Row>
|
||||
<Col md={6}>
|
||||
<h3>{t('Basic Information')}</h3>
|
||||
<FormGroup>
|
||||
<label className="control-label" htmlFor="name">
|
||||
{t('Name')}
|
||||
</label>
|
||||
<FormControl
|
||||
name="name"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
value={name}
|
||||
onChange={event => setName(event.target.value)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<label className="control-label" htmlFor="description">
|
||||
{t('Description')}
|
||||
</label>
|
||||
<FormControl
|
||||
name="description"
|
||||
type="text"
|
||||
componentClass="textarea"
|
||||
bsSize="sm"
|
||||
value={description}
|
||||
onChange={event => setDescription(event.target.value)}
|
||||
style={{ maxWidth: '100%' }}
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'The description can be displayed as widget headers in the dashboard view. Supports markdown.',
|
||||
)}
|
||||
</p>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col md={6}>
|
||||
<h3>{t('Configuration')}</h3>
|
||||
<FormGroup>
|
||||
<label className="control-label" htmlFor="cacheTimeout">
|
||||
{t('Cache Timeout')}
|
||||
</label>
|
||||
<FormControl
|
||||
name="cacheTimeout"
|
||||
type="text"
|
||||
bsSize="sm"
|
||||
value={cacheTimeout}
|
||||
onChange={event =>
|
||||
setCacheTimeout(event.target.value.replace(/[^0-9]/, ''))
|
||||
}
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t(
|
||||
'Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.',
|
||||
)}
|
||||
</p>
|
||||
</FormGroup>
|
||||
<h3 style={{ marginTop: '1em' }}>{t('Access')}</h3>
|
||||
<FormGroup>
|
||||
<label className="control-label" htmlFor="owners">
|
||||
{t('Owners')}
|
||||
</label>
|
||||
<Select
|
||||
name="owners"
|
||||
multi
|
||||
isLoading={!ownerOptions}
|
||||
value={owners}
|
||||
options={ownerOptions || []}
|
||||
onChange={setOwners}
|
||||
disabled={!owners || !ownerOptions}
|
||||
/>
|
||||
<p className="help-block">
|
||||
{t('A list of users who can alter the chart')}
|
||||
</p>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
type="submit"
|
||||
bsSize="sm"
|
||||
bsStyle="primary"
|
||||
className="m-r-5"
|
||||
disabled={submitting}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
<Button type="button" bsSize="sm" onClick={onHide}>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Dialog ref={errorDialog} />
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators({ onSave: sliceUpdated }, dispatch);
|
||||
}
|
||||
|
||||
export { PropertiesModalWrapper };
|
||||
export default connect(null, mapDispatchToProps)(PropertiesModalWrapper);
|
||||
|
|
@ -158,6 +158,15 @@ export default function exploreReducer(state = {}, action) {
|
|||
can_overwrite: action.can_overwrite,
|
||||
};
|
||||
},
|
||||
[actions.SLICE_UPDATED]() {
|
||||
return {
|
||||
...state,
|
||||
slice: {
|
||||
...state.slice,
|
||||
...action.slice,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
if (action.type in actionHandlers) {
|
||||
return actionHandlers[action.type]();
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ class Slice(
|
|||
logging.exception(e)
|
||||
d["error"] = str(e)
|
||||
return {
|
||||
"cache_timeout": self.cache_timeout,
|
||||
"datasource": self.datasource_name,
|
||||
"description": self.description,
|
||||
"description_markeddown": self.description_markeddown,
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class ChartPostSchema(BaseOwnedSchema):
|
|||
viz_type = fields.String(allow_none=True, validate=Length(0, 250))
|
||||
owners = fields.List(fields.Integer(validate=validate_owner))
|
||||
params = fields.String(allow_none=True, validate=validate_json)
|
||||
cache_timeout = fields.Integer()
|
||||
cache_timeout = fields.Integer(allow_none=True)
|
||||
datasource_id = fields.Integer(required=True)
|
||||
datasource_type = fields.String(required=True)
|
||||
datasource_name = fields.String(allow_none=True)
|
||||
|
|
@ -110,7 +110,7 @@ class ChartPutSchema(BaseOwnedSchema):
|
|||
viz_type = fields.String(allow_none=True, validate=Length(0, 250))
|
||||
owners = fields.List(fields.Integer(validate=validate_owner))
|
||||
params = fields.String(allow_none=True)
|
||||
cache_timeout = fields.Integer()
|
||||
cache_timeout = fields.Integer(allow_none=True)
|
||||
datasource_id = fields.Integer(allow_none=True)
|
||||
datasource_type = fields.String(allow_none=True)
|
||||
dashboards = fields.List(fields.Integer(validate=validate_dashboard))
|
||||
|
|
|
|||
Loading…
Reference in New Issue