"Add Slices" modal on dashboard page (#678)

* Add slice modal

* use datatables, filter by slice creator

* tests & landscaping

* code review + react-bootstrap-table + modularity
This commit is contained in:
George Ke
2016-07-07 21:40:33 -07:00
committed by GitHub
parent afff78868f
commit 04f3e3bc8f
10 changed files with 602 additions and 254 deletions

View File

@@ -0,0 +1,186 @@
import React, { PropTypes } from 'react';
import { Responsive, WidthProvider } from 'react-grid-layout';
const ResponsiveReactGridLayout = WidthProvider(Responsive);
require('../../../node_modules/react-grid-layout/css/styles.css');
require('../../../node_modules/react-resizable/css/styles.css');
const sliceCellPropTypes = {
slice: PropTypes.object.isRequired,
removeSlice: PropTypes.func.isRequired,
expandedSlices: PropTypes.object
};
const gridLayoutPropTypes = {
dashboard: PropTypes.object.isRequired,
slices: PropTypes.arrayOf(PropTypes.object).isRequired,
posDict: PropTypes.object.isRequired
};
class SliceCell extends React.Component {
render() {
const slice = this.props.slice,
createMarkup = function () {
return { __html: slice.description_markeddown };
};
return (
<div>
<div className="chart-header">
<div className="row">
<div className="col-md-12 text-center header">
{slice.slice_name}
</div>
<div className="col-md-12 chart-controls">
<div className="pull-left">
<a title="Move chart" data-toggle="tooltip">
<i className="fa fa-arrows drag"/>
</a>
<a className="refresh" title="Force refresh data" data-toggle="tooltip">
<i className="fa fa-repeat"/>
</a>
</div>
<div className="pull-right">
{slice.description ?
<a title="Toggle chart description">
<i className="fa fa-info-circle slice_info" title={slice.description} data-toggle="tooltip"/>
</a>
: ""}
<a href={slice.edit_url} title="Edit chart" data-toggle="tooltip">
<i className="fa fa-pencil"/>
</a>
<a href={slice.slice_url} title="Explore chart" data-toggle="tooltip">
<i className="fa fa-share"/>
</a>
<a className="remove-chart" title="Remove chart from dashboard" data-toggle="tooltip">
<i className="fa fa-close" onClick={this.props.removeSlice.bind(null, slice.slice_id)}/>
</a>
</div>
</div>
</div>
</div>
<div
className="slice_description bs-callout bs-callout-default"
style={this.props.expandedSlices && this.props.expandedSlices[String(slice.slice_id)] ? {} : { display: "none" }}
dangerouslySetInnerHTML={createMarkup()}>
</div>
<div className="row chart-container">
<input type="hidden" value="false"/>
<div id={slice.token} className="token col-md-12">
<img src="/static/assets/images/loading.gif" className="loading" alt="loading"/>
<div className="slice_container" id={slice.token + "_con"}></div>
</div>
</div>
</div>
);
}
}
class GridLayout extends React.Component {
removeSlice(sliceId) {
$('[data-toggle="tooltip"]').tooltip("hide");
this.setState({
layout: this.state.layout.filter(function (reactPos) {
return reactPos.i !== String(sliceId);
}),
slices: this.state.slices.filter(function (slice) {
return slice.slice_id !== sliceId;
})
});
}
onResizeStop(layout, oldItem, newItem) {
if (oldItem.w !== newItem.w || oldItem.h !== newItem.h) {
this.setState({
layout: layout
}, function () {
this.props.dashboard.getSlice(newItem.i).resize();
});
}
}
onDragStop(layout) {
this.setState({
layout: layout
});
}
serialize() {
return this.state.layout.map(function (reactPos) {
return {
slice_id: reactPos.i,
col: reactPos.x + 1,
row: reactPos.y,
size_x: reactPos.w,
size_y: reactPos.h
};
});
}
componentWillMount() {
var layout = [];
this.props.slices.forEach(function (slice, index) {
var pos = this.props.posDict[slice.slice_id];
if (!pos) {
pos = {
col: (index * 4 + 1) % 12,
row: Math.floor((index) / 3) * 4,
size_x: 4,
size_y: 4
};
}
layout.push({
i: String(slice.slice_id),
x: pos.col - 1,
y: pos.row,
w: pos.size_x,
minW: 2,
h: pos.size_y
});
}, this);
this.setState({
layout: layout,
slices: this.props.slices
});
}
render() {
return (
<ResponsiveReactGridLayout
className="layout"
layouts={{ lg: this.state.layout }}
onResizeStop={this.onResizeStop.bind(this)}
onDragStop={this.onDragStop.bind(this)}
cols={{ lg: 12, md: 12, sm: 10, xs: 8, xxs: 6 }}
rowHeight={100}
autoSize={true}
margin={[20, 20]}
useCSSTransforms={false}
draggableHandle=".drag">
{this.state.slices.map((slice) => {
return (
<div
id="slice_${slice.slice_id}"
key={slice.slice_id}
data-slice-id={slice.slice_id}
className={"widget " + slice.viz_name}>
<SliceCell
slice={slice}
removeSlice={this.removeSlice.bind(this)}
expandedSlices={this.props.dashboard.metadata.expanded_slices}/>
</div>
);
})}
</ResponsiveReactGridLayout>
);
}
}
SliceCell.propTypes = sliceCellPropTypes;
GridLayout.propTypes = gridLayoutPropTypes;
export default GridLayout;

View File

@@ -0,0 +1,42 @@
import React, { PropTypes } from 'react';
const propTypes = {
modalId: PropTypes.string.isRequired,
title: PropTypes.string,
modalContent: PropTypes.node,
customButtons: PropTypes.node
};
class Modal extends React.Component {
render() {
return (
<div className="modal fade" id={this.props.modalId} role="dialog">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 className="modal-title">{this.props.title}</h4>
</div>
<div className="modal-body">
{this.props.modalContent}
</div>
<div className="modal-footer">
<button type="button"
className="btn btn-default"
data-dismiss="modal">
Cancel
</button>
{this.props.customButtons}
</div>
</div>
</div>
</div>
);
}
}
Modal.propTypes = propTypes;
export default Modal;

View File

@@ -0,0 +1,190 @@
import React, { PropTypes } from 'react';
import update from 'immutability-helper';
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
import Modal from './Modal.jsx';
require('../../../node_modules/react-bootstrap-table/css/react-bootstrap-table.css');
const propTypes = {
dashboard: PropTypes.object.isRequired,
caravel: PropTypes.object.isRequired
};
class SliceAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
slices: []
};
this.addSlices = this.addSlices.bind(this);
this.toggleSlice = this.toggleSlice.bind(this);
this.toggleAllSlices = this.toggleAllSlices.bind(this);
this.slicesLoaded = false;
this.selectRowProp = {
mode: "checkbox",
clickToSelect: true,
onSelect: this.toggleSlice,
onSelectAll: this.toggleAllSlices
};
this.options = {
defaultSortOrder: "desc",
defaultSortName: "modified",
sizePerPage: 10
};
}
componentDidMount() {
var uri = "/sliceaddview/api/read?_flt_0_created_by=" + this.props.dashboard.curUserId;
this.slicesRequest = $.ajax({
url: uri,
type: 'GET',
success: function (response) {
this.slicesLoaded = true;
// Prepare slice data for table
let slices = response.result;
slices.forEach(function (slice) {
slice.id = slice.data.slice_id;
slice.sliceName = slice.data.slice_name;
slice.vizType = slice.viz_type;
slice.modified = slice.modified;
});
this.setState({
slices: slices,
selectionMap: {}
});
}.bind(this),
error: function (error) {
this.errored = true;
this.setState({
errorMsg: this.props.dashboard.getAjaxErrorMsg(error)
});
}.bind(this)
});
}
componentWillUnmount() {
this.slicesRequest.abort();
}
addSlices() {
var slices = this.state.slices.filter(function (slice) {
return this.state.selectionMap[slice.id];
}, this);
slices.forEach(function (slice) {
var sliceObj = this.props.caravel.Slice(slice.data, this.props.dashboard);
$("#slice_" + slice.data.slice_id).find('a.refresh').click(function () {
sliceObj.render(true);
});
this.props.dashboard.slices.push(sliceObj);
}, this);
this.props.dashboard.addSlicesToDashboard(Object.keys(this.state.selectionMap));
}
toggleSlice(slice) {
this.setState({
selectionMap: update(this.state.selectionMap, {
[slice.id]: {
$set: !this.state.selectionMap[slice.id]
}
})
});
}
toggleAllSlices(value) {
let updatePayload = {};
this.state.slices.forEach(function (slice) {
updatePayload[slice.id] = {
$set: value
};
}, this);
this.setState({
selectionMap: update(this.state.selectionMap, updatePayload)
});
}
modifiedDateComparator(a, b, order) {
if (order === 'desc') {
if (a.changed_on > b.changed_on) {
return -1;
} else if (a.changed_on < b.changed_on) {
return 1;
}
return 0;
}
if (a.changed_on < b.changed_on) {
return -1;
} else if (a.changed_on > b.changed_on) {
return 1;
}
return 0;
}
render() {
const hideLoad = this.slicesLoaded || this.errored;
const enableAddSlice = this.state.selectionMap && Object.keys(this.state.selectionMap).some(function (key) {
return this.state.selectionMap[key];
}, this);
const modalContent = (
<div>
<img src="/static/assets/images/loading.gif"
className={"loading " + (hideLoad ? "hidden" : "")}
alt={hideLoad ? "" : "loading"}/>
<div className={this.errored ? "" : "hidden"}>
{this.state.errorMsg}
</div>
<div className={this.slicesLoaded ? "" : "hidden"}>
<BootstrapTable
ref="table"
data={this.state.slices}
selectRow={this.selectRowProp}
options={this.options}
hover
search
pagination
height="auto">
<TableHeaderColumn dataField="sliceName" isKey={true} dataSort={true}>Name</TableHeaderColumn>
<TableHeaderColumn dataField="vizType" dataSort={true}>Viz</TableHeaderColumn>
<TableHeaderColumn
dataField="modified"
dataSort={true}
sortFunc={this.modifiedDateComparator}
// Will cause react-bootstrap-table to interpret the HTML returned
dataFormat={modified => modified}>
Modified
</TableHeaderColumn>
</BootstrapTable>
</div>
</div>
);
const customButtons = [
<button key={0}
type="button"
className="btn btn-default"
data-dismiss="modal"
onClick={this.addSlices}
disabled={!enableAddSlice}>
Add Slices
</button>
];
return (
<Modal modalId='add_slice_modal'
modalContent={modalContent}
title='Add New Slices'
customButtons={customButtons}
/>
);
}
}
SliceAdder.propTypes = propTypes;
export default SliceAdder;