Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit Dashboard title and Slice title in place #2940

Merged
merged 16 commits into from
Jun 14, 2017
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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