diff --git a/superset/assets/src/explore/actions/exploreActions.js b/superset/assets/src/explore/actions/exploreActions.js index 31eb507e3..3c019de9f 100644 --- a/superset/assets/src/explore/actions/exploreActions.js +++ b/superset/assets/src/explore/actions/exploreActions.js @@ -145,3 +145,8 @@ export function createNewSlice( form_data, }; } + +export const SLICE_UPDATED = 'SLICE_UPDATED'; +export function sliceUpdated(slice) { + return { type: SLICE_UPDATED, slice }; +} diff --git a/superset/assets/src/explore/components/DisplayQueryButton.jsx b/superset/assets/src/explore/components/DisplayQueryButton.jsx index 2ac322113..0c087721c 100644 --- a/superset/assets/src/explore/components/DisplayQueryButton.jsx +++ b/superset/assets/src/explore/components/DisplayQueryButton.jsx @@ -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 ; @@ -222,6 +233,19 @@ export default class DisplayQueryButton extends React.PureComponent { pullRight id="query" > + {this.props.slice && ( + <> + + {t('Edit properties')} + + + + )} this.beforeOpen('query')} modalBody={this.renderQueryModalBody()} - eventKey="1" /> this.beforeOpen('results')} modalBody={this.renderResultsModalBody()} - eventKey="2" /> this.beforeOpen('samples')} modalBody={this.renderSamplesModalBody()} - eventKey="2" /> {this.state.sqlSupported && ( diff --git a/superset/assets/src/explore/components/ExploreActionButtons.jsx b/superset/assets/src/explore/components/ExploreActionButtons.jsx index c7a7f20c3..c606108c3 100644 --- a/superset/assets/src/explore/components/ExploreActionButtons.jsx +++ b/superset/assets/src/explore/components/ExploreActionButtons.jsx @@ -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} /> ); diff --git a/superset/assets/src/explore/components/PropertiesModal.jsx b/superset/assets/src/explore/components/PropertiesModal.jsx new file mode 100644 index 000000000..9424fd6da --- /dev/null +++ b/superset/assets/src/explore/components/PropertiesModal.jsx @@ -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 ( + + + + ); +} + +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 ( +
+ + Edit Chart Properties + + + + +

{t('Basic Information')}

+ + + setName(event.target.value)} + /> + + + + setDescription(event.target.value)} + style={{ maxWidth: '100%' }} + /> +

+ {t( + 'The description can be displayed as widget headers in the dashboard view. Supports markdown.', + )} +

+
+ + +

{t('Configuration')}

+ + + + setCacheTimeout(event.target.value.replace(/[^0-9]/, '')) + } + /> +

+ {t( + 'Duration (in seconds) of the caching timeout for this chart. Note this defaults to the datasource/table timeout if undefined.', + )} +

+
+

{t('Access')}

+ + +