Skip to content

Commit

Permalink
Edit Dashboard title and Slice title in place (#2940)
Browse files Browse the repository at this point in the history
* Edit Dashboard title and Slice title in place

Add EditableTitle component into Dashboard and Explore view to support edit title inline.
  • Loading branch information
Grace Guo authored Jun 14, 2017
1 parent da0a87a commit 8329ea2
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 15 deletions.
84 changes: 84 additions & 0 deletions superset/assets/javascripts/components/EditableTitle.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React from 'react';
import PropTypes from 'prop-types';
import TooltipWrapper from './TooltipWrapper';

const propTypes = {
title: PropTypes.string,
canEdit: PropTypes.bool,
onSaveTitle: PropTypes.func.isRequired,
};
const defaultProps = {
title: 'Title',
canEdit: false,
};

class EditableTitle extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
isEditing: false,
title: this.props.title,
lastTitle: this.props.title,
};
this.handleClick = this.handleClick.bind(this);
this.handleBlur = this.handleBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
}
handleClick() {
if (!this.props.canEdit) {
return;
}

this.setState({
isEditing: true,
});
}
handleBlur() {
if (!this.props.canEdit) {
return;
}

this.setState({
isEditing: false,
});

if (this.state.lastTitle !== this.state.title) {
this.setState({
lastTitle: this.state.title,
});
this.props.onSaveTitle(this.state.title);
}
}
handleChange(ev) {
if (!this.props.canEdit) {
return;
}

this.setState({
title: ev.target.value,
});
}
render() {
return (
<span className="editable-title">
<TooltipWrapper
label="title"
tooltip={this.props.canEdit ? 'click to edit title' : 'You don\'t have the rights to alter this title.'}
>
<input
required
type={this.state.isEditing ? 'text' : 'button'}
value={this.state.title}
onChange={this.handleChange}
onBlur={this.handleBlur}
onClick={this.handleClick}
/>
</TooltipWrapper>
</span>
);
}
}
EditableTitle.propTypes = propTypes;
EditableTitle.defaultProps = defaultProps;

export default EditableTitle;
4 changes: 4 additions & 0 deletions superset/assets/javascripts/dashboard/Dashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,10 @@ export function dashboardContainer(dashboard, datasources, userid) {
},
});
},
updateDashboardTitle(title) {
this.dashboard_title = title;
this.onChange();
},
});
}

Expand Down
13 changes: 11 additions & 2 deletions superset/assets/javascripts/dashboard/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';

import Controls from './Controls';
import EditableTitle from '../../components/EditableTitle';

const propTypes = {
dashboard: PropTypes.object,
Expand All @@ -14,14 +15,22 @@ class Header extends React.PureComponent {
super(props);
this.state = {
};
this.handleSaveTitle = this.handleSaveTitle.bind(this);
}
handleSaveTitle(title) {
this.props.dashboard.updateDashboardTitle(title);
}
render() {
const dashboard = this.props.dashboard;
return (
<div className="title">
<div className="pull-left">
<h1>
{dashboard.dashboard_title} &nbsp;
<h1 className="outer-container">
<EditableTitle
title={dashboard.dashboard_title}
canEdit={dashboard.dash_save_perm}
onSaveTitle={this.handleSaveTitle}
/>
<span is class="favstar" class_name="Dashboard" obj_id={dashboard.id} />
</h1>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class SaveModal extends React.PureComponent {
positions,
css: this.state.css,
expanded_slices: expandedSlices,
dashboard_title: dashboard.dashboard_title,
};
let url = null;
if (saveType === 'overwrite') {
Expand Down
14 changes: 11 additions & 3 deletions superset/assets/javascripts/explore/actions/exploreActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,10 @@ export const SAVE_SLICE_FAILED = 'SAVE_SLICE_FAILED';
export function saveSliceFailed() {
return { type: SAVE_SLICE_FAILED };
}
export const SAVE_SLICE_SUCCESS = 'SAVE_SLICE_SUCCESS';
export function saveSliceSuccess(data) {
return { type: SAVE_SLICE_SUCCESS, data };
}

export const REMOVE_SAVE_MODAL_ALERT = 'REMOVE_SAVE_MODAL_ALERT';
export function removeSaveModalAlert() {
Expand All @@ -220,17 +224,21 @@ export function removeSaveModalAlert() {

export function saveSlice(url) {
return function (dispatch) {
$.get(url, (data, status) => {
return $.get(url, (data, status) => {
if (status === 'success') {
// Go to new slice url or dashboard url
window.location = data;
dispatch(saveSliceSuccess(data));
} else {
dispatch(saveSliceFailed());
}
});
};
}

export const UPDATE_CHART_TITLE = 'UPDATE_CHART_TITLE';
export function updateChartTitle(slice_name) {
return { type: UPDATE_CHART_TITLE, slice_name };
}

export const UPDATE_CHART_STATUS = 'UPDATE_CHART_STATUS';
export function updateChartStatus(status) {
return { type: UPDATE_CHART_STATUS, status };
Expand Down
26 changes: 24 additions & 2 deletions superset/assets/javascripts/explore/components/ChartContainer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Alert, Collapse, Panel } from 'react-bootstrap';
import visMap from '../../../visualizations/main';
import { d3format } from '../../modules/utils';
import ExploreActionButtons from './ExploreActionButtons';
import EditableTitle from '../../components/EditableTitle';
import FaveStar from '../../components/FaveStar';
import TooltipWrapper from '../../components/TooltipWrapper';
import Timer from '../../components/Timer';
Expand All @@ -23,6 +24,7 @@ const CHART_STATUS_MAP = {
const propTypes = {
actions: PropTypes.object.isRequired,
alert: PropTypes.string,
can_overwrite: PropTypes.bool.isRequired,
can_download: PropTypes.bool.isRequired,
chartStatus: PropTypes.string,
chartUpdateEndTime: PropTypes.number,
Expand All @@ -39,6 +41,8 @@ const propTypes = {
queryResponse: PropTypes.object,
triggerRender: PropTypes.bool,
standalone: PropTypes.bool,
datasourceType: PropTypes.string,
datasourceId: PropTypes.number,
};

class ChartContainer extends React.PureComponent {
Expand Down Expand Up @@ -145,6 +149,18 @@ class ChartContainer extends React.PureComponent {
this.props.actions.runQuery(this.props.formData, true);
}

updateChartTitle(newTitle) {
const params = {
slice_name: newTitle,
action: 'overwrite',
};
const saveUrl = getExploreUrl(this.props.formData, 'base', false, null, params);
this.props.actions.saveSlice(saveUrl)
.then(() => {
this.props.actions.updateChartTitle(newTitle);
});
}

renderChartTitle() {
let title;
if (this.props.slice) {
Expand Down Expand Up @@ -240,7 +256,11 @@ class ChartContainer extends React.PureComponent {
id="slice-header"
className="clearfix panel-title-large"
>
{this.renderChartTitle()}
<EditableTitle
title={this.renderChartTitle()}
canEdit={this.props.can_overwrite}
onSaveTitle={this.updateChartTitle.bind(this)}
/>

{this.props.slice &&
<span>
Expand Down Expand Up @@ -304,6 +324,7 @@ function mapStateToProps(state) {
const formData = getFormDataFromControls(state.controls);
return {
alert: state.chartAlert,
can_overwrite: state.can_overwrite,
can_download: state.can_download,
chartStatus: state.chartStatus,
chartUpdateEndTime: state.chartUpdateEndTime,
Expand All @@ -320,7 +341,8 @@ function mapStateToProps(state) {
table_name: formData.datasource_name,
viz_type: formData.viz_type,
triggerRender: state.triggerRender,
datasourceType: state.datasource ? state.datasource.type : null,
datasourceType: state.datasource_type,
datasourceId: state.datasource_id,
};
}

Expand Down
6 changes: 5 additions & 1 deletion superset/assets/javascripts/explore/components/SaveModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ class SaveModal extends React.Component {
const saveUrl = `${baseUrl}?form_data=` +
`${encodeURIComponent(JSON.stringify(this.props.form_data))}` +
`&${$.param(sliceParams, true)}`;
this.props.actions.saveSlice(saveUrl);
this.props.actions.saveSlice(saveUrl)
.then((data) => {
// Go to new slice url or dashboard url
window.location = data;
});
this.props.onHide();
}
removeAlert() {
Expand Down
11 changes: 10 additions & 1 deletion superset/assets/javascripts/explore/exploreUtils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* eslint camelcase: 0 */
import URI from 'urijs';

export function getExploreUrl(form_data, endpointType = 'base', force = false, curUrl = null) {
export function getExploreUrl(form_data, endpointType = 'base', force = false,
curUrl = null, requestParams = {}) {
if (!form_data.datasource) {
return null;
}
Expand Down Expand Up @@ -38,6 +39,14 @@ export function getExploreUrl(form_data, endpointType = 'base', force = false, c
if (endpointType === 'query') {
search.query = 'true';
}
const paramNames = Object.keys(requestParams);
if (paramNames.length) {
paramNames.forEach((name) => {
if (requestParams.hasOwnProperty(name)) {
search[name] = requestParams[name];
}
});
}
uri = uri.search(search).directory(directory);
return uri.toString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ export const exploreReducer = function (state, action) {
}
return newState;
},
[actions.UPDATE_CHART_TITLE]() {
const updatedSlice = Object.assign({}, state.slice, { slice_name: action.slice_name });
return Object.assign({}, state, { slice: updatedSlice });
},
[actions.REMOVE_CHART_ALERT]() {
if (state.chartAlert !== null) {
return Object.assign({}, state, { chartAlert: null });
Expand All @@ -144,6 +148,9 @@ export const exploreReducer = function (state, action) {
[actions.SAVE_SLICE_FAILED]() {
return Object.assign({}, state, { saveModalAlert: 'Failed to save slice' });
},
[actions.SAVE_SLICE_SUCCESS](data) {
return Object.assign({}, state, { data });
},
[actions.REMOVE_SAVE_MODAL_ALERT]() {
return Object.assign({}, state, { saveModalAlert: null });
},
Expand Down
85 changes: 85 additions & 0 deletions superset/assets/spec/javascripts/profile/EditableTitle_spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';
import { shallow } from 'enzyme';
import { describe, it } from 'mocha';
import sinon from 'sinon';
import { expect } from 'chai';

import EditableTable from '../../../javascripts/components/EditableTitle';

describe('EditableTitle', () => {
const callback = sinon.spy();
const mockProps = {
title: 'my title',
canEdit: true,
onSaveTitle: callback,
};
const mockEvent = {
target: {
value: 'new title',
},
};
const editableWrapper = shallow(<EditableTable {...mockProps} />);
const notEditableWrapper = shallow(<EditableTable title="my title" />);
it('is valid', () => {
expect(
React.isValidElement(<EditableTable {...mockProps} />),
).to.equal(true);
});
it('should render title', () => {
const titleElement = editableWrapper.find('input');
expect(titleElement.props().value).to.equal('my title');
expect(titleElement.props().type).to.equal('button');
});

describe('should handle click', () => {
it('should change title', () => {
editableWrapper.find('input').simulate('click');
expect(editableWrapper.find('input').props().type).to.equal('text');
});
it('should not change title', () => {
notEditableWrapper.find('input').simulate('click');
expect(notEditableWrapper.find('input').props().type).to.equal('button');
});
});

describe('should handle change', () => {
afterEach(() => {
editableWrapper.setState({ title: 'my title' });
editableWrapper.setState({ lastTitle: 'my title' });
});
it('should change title', () => {
editableWrapper.find('input').simulate('change', mockEvent);
expect(editableWrapper.find('input').props().value).to.equal('new title');
});
it('should not change title', () => {
notEditableWrapper.find('input').simulate('change', mockEvent);
expect(editableWrapper.find('input').props().value).to.equal('my title');
});
});

describe('should handle blur', () => {
beforeEach(() => {
editableWrapper.find('input').simulate('click');
expect(editableWrapper.find('input').props().type).to.equal('text');
});
afterEach(() => {
callback.reset();
editableWrapper.setState({ title: 'my title' });
editableWrapper.setState({ lastTitle: 'my title' });
});

it('should trigger callback', () => {
editableWrapper.setState({ title: 'new title' });
editableWrapper.find('input').simulate('blur');
expect(editableWrapper.find('input').props().type).to.equal('button');
expect(callback.callCount).to.equal(1);
expect(callback.getCall(0).args[0]).to.equal('new title');
});
it('should not trigger callback', () => {
editableWrapper.find('input').simulate('blur');
expect(editableWrapper.find('input').props().type).to.equal('button');
// no change
expect(callback.callCount).to.equal(0);
});
});
});
Loading

0 comments on commit 8329ea2

Please sign in to comment.