Basic Report UI complete!

This commit is contained in:
Aaron Lindsay 2017-06-17 10:28:50 -04:00
parent 5dff27e7f7
commit 9844785b8d
10 changed files with 331 additions and 75 deletions

View File

@ -1,6 +1,6 @@
JS_SOURCES = $(wildcard js/*.js) $(wildcard js/*/*.js)
all: static/bundle.js static/react-widgets security_templates.go
all: static/bundle.js static/react-widgets static/codemirror/codemirror.css security_templates.go
node_modules:
npm install
@ -11,6 +11,10 @@ static/bundle.js: $(JS_SOURCES) node_modules
static/react-widgets: node_modules/react-widgets/dist node_modules
rsync -a node_modules/react-widgets/dist/ static/react-widgets/
static/codemirror/codemirror.css: node_modules/codemirror/lib/codemirror.js node_modules
mkdir -p static/codemirror
cp node_modules/codemirror/lib/codemirror.css static/codemirror/codemirror.css
security_templates.go: cusip_list.csv
./scripts/gen_security_list.py > security_templates.go

View File

@ -138,9 +138,9 @@ function create(report) {
if (e.isError()) {
dispatch(ErrorActions.serverError(e));
} else {
var a = new Report();
a.fromJSON(data);
dispatch(reportCreated(a));
var r = new Report();
r.fromJSON(data);
dispatch(reportCreated(r));
}
},
error: function(jqXHR, status, error) {
@ -165,9 +165,10 @@ function update(report) {
if (e.isError()) {
dispatch(ErrorActions.serverError(e));
} else {
var a = new Report();
a.fromJSON(data);
dispatch(reportUpdated(a));
var r = new Report();
r.fromJSON(data);
dispatch(reportUpdated(r));
dispatch(tabulate(r));
}
},
error: function(jqXHR, status, error) {

View File

@ -1,9 +1,25 @@
var React = require('react');
var ReactDOM = require('react-dom');
var ReactBootstrap = require('react-bootstrap');
var Col = ReactBootstrap.Col;
var Form = ReactBootstrap.Form;
var FormGroup = ReactBootstrap.FormGroup;
var FormControl = ReactBootstrap.FormControl;
var ControlLabel = ReactBootstrap.ControlLabel;
var Button = ReactBootstrap.Button;
var ButtonGroup = ReactBootstrap.ButtonGroup;
var ButtonToolbar = ReactBootstrap.ButtonToolbar;
var Glyphicon = ReactBootstrap.Glyphicon;
var Panel = ReactBootstrap.Panel;
var Modal = ReactBootstrap.Modal;
var ProgressBar = ReactBootstrap.ProgressBar;
var Combobox = require('react-widgets').Combobox;
var CodeMirror = require('react-codemirror');
require('codemirror/mode/lua/lua');
var StackedBarChart = require('../components/StackedBarChart');
var PieChart = require('../components/PieChart');
@ -12,13 +28,121 @@ var models = require('../models')
var Report = models.Report;
var Tabulation = models.Tabulation;
class AddEditReportModal extends React.Component {
getInitialState(props) {
var s = {
reportid: -1,
name: "",
lua: ""
};
if (props && props.editReport != null) {
s.reportid = props.editReport.ReportId;
s.name = props.editReport.Name;
s.lua = props.editReport.Lua;
}
return s;
}
constructor() {
super();
this.state = this.getInitialState();
this.onCancel = this.handleCancel.bind(this);
this.onNameChange = this.handleNameChange.bind(this);
this.onLuaChange = this.handleLuaChange.bind(this);
this.onSubmit = this.handleSubmit.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.show && !this.props.show) {
this.setState(this.getInitialState(nextProps));
}
}
handleCancel() {
if (this.props.onCancel != null)
this.props.onCancel();
}
handleNameChange() {
this.setState({
name: ReactDOM.findDOMNode(this.refs.name).value,
});
}
handleLuaChange(lua) {
this.setState({
lua: lua
});
}
handleSubmit() {
var r = new Report();
if (this.props.editReport != null)
r.ReportId = this.state.reportid;
r.Name = this.state.name;
r.Lua = this.state.lua;
if (this.props.onSubmit != null)
this.props.onSubmit(r);
}
render() {
var headerText = (this.props.editReport != null) ? "Edit" : "Create New";
var buttonText = (this.props.editReport != null) ? "Save Changes" : "Create Report";
var codeMirrorOptions = {
lineNumbers: true,
mode: 'lua',
};
return (
<Modal show={this.props.show} onHide={this.onCancel} bsSize="large">
<Modal.Header closeButton>
<Modal.Title>{headerText} Report</Modal.Title>
</Modal.Header>
<Modal.Body>
<Form horizontal onSubmit={this.onSubmit}>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Name</Col>
<Col xs={9}>
<FormControl type="text"
value={this.state.name}
onChange={this.onNameChange}
ref="name"/>
</Col>
</FormGroup>
<FormGroup>
<Col componentClass={ControlLabel} xs={3}>Lua Code</Col>
<Col xs={9}>
<CodeMirror
value={this.state.lua}
onChange={this.onLuaChange}
options={codeMirrorOptions} />
</Col>
</FormGroup>
</Form>
</Modal.Body>
<Modal.Footer>
<ButtonGroup className="pull-right">
<Button onClick={this.onCancel} bsStyle="warning">Cancel</Button>
<Button onClick={this.onSubmit} bsStyle="success">{buttonText}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>
);
}
}
class ReportsTab extends React.Component {
constructor() {
super();
this.state = {
initialized: false
initialized: false,
creatingNewReport: false,
editingReport: false
}
this.onSelectSeries = this.handleSelectSeries.bind(this);
this.onSelectReport = this.handleSelectReport.bind(this);
this.onNewReport = this.handleNewReport.bind(this);
this.onEditReport = this.handleEditReport.bind(this);
this.onDeleteReport = this.handleDeleteReport.bind(this);
this.onCreationCancel = this.handleCreationCancel.bind(this);
this.onCreationSubmit = this.handleCreationSubmit.bind(this);
this.onEditingCancel = this.handleEditingCancel.bind(this);
this.onEditingSubmit = this.handleEditingSubmit.bind(this);
}
componentWillMount() {
this.props.onFetchAllReports();
@ -27,80 +151,164 @@ class ReportsTab extends React.Component {
var selected = nextProps.reports.selected;
if (!this.state.initialized) {
if (selected == -1 &&
nextProps.reports.list.length > 0)
nextProps.reports.list.length > 0) {
nextProps.onSelectReport(nextProps.reports.map[nextProps.reports.list[0]]);
this.setState({initialized: true});
} else if (selected != -1 && !nextProps.reports.tabulations.hasOwnProperty(selected)) {
nextProps.onTabulateReport(nextProps.reports.map[nextProps.reports.list[0]]);
} else if (selected != -1 && nextProps.reports.selectedTabulation == null) {
nextProps.onSelectSeries(nextProps.reports.tabulations[nextProps.reports.list[0]]);
nextProps.onTabulateReport(nextProps.reports.map[nextProps.reports.list[0]]);
this.setState({initialized: true});
}
} else if (selected != -1 &&
nextProps.reports.tabulations.hasOwnProperty(selected) &&
nextProps.reports.selectedTabulation == null) {
nextProps.onSelectSeries(nextProps.reports.tabulations[selected]);
}
}
handleSelectSeries(series) {
if (series == Tabulation.topLevelSeriesName())
return;
var seriesTraversal = this.props.selectedTabulation.seriesTraversal.slice();
var seriesTraversal = this.props.reports.seriesTraversal.slice();
seriesTraversal.push(series);
var selectedTabulation = this.props.reports.tabulations[this.props.reports.selected];
this.props.onSelectSeries(selectedTabulation, seriesTraversal);
}
handleSelectReport(report) {
this.props.onSelectReport(report);
if (!this.props.reports.tabulations.hasOwnProperty(report.ReportId))
this.props.onTabulateReport(report);
}
handleNewReport() {
this.setState({creatingNewReport: true});
}
handleEditReport() {
this.setState({editingReport: true});
}
handleDeleteReport() {
this.props.onDeleteReport(this.props.reports.map[this.props.reports.selected]);
}
handleCreationCancel() {
this.setState({creatingNewReport: false});
}
handleCreationSubmit(report) {
this.setState({creatingNewReport: false});
this.props.onCreateReport(report);
}
handleEditingCancel() {
this.setState({editingReport: false});
}
handleEditingSubmit(report) {
this.setState({editingReport: false});
this.props.onUpdateReport(report);
}
render() {
var selectedTabulation = this.props.reports.selectedTabulation;
if (!selectedTabulation) {
return (
<div></div>
var reportPanel = [];
if (selectedTabulation) {
var titleTracks = [];
var seriesTraversal = [];
for (var i = 0; i < this.props.reports.seriesTraversal.length; i++) {
var name = this.props.reports.selectedTabulation.Title;
if (i > 0)
name = this.props.reports.seriesTraversal[i-1];
// Make a closure for going up the food chain
var self = this;
var navOnClick = function() {
var onSelectSeries = self.props.onSelectSeries;
var tabulation = self.props.reports.tabulations[self.props.reports.selected];
var mySeriesTraversal = seriesTraversal.slice();
return function() {
onSelectSeries(tabulation, mySeriesTraversal);
};
}();
titleTracks.push((
<Button key={i*2} bsStyle="link"
onClick={navOnClick}>
{name}
</Button>
));
titleTracks.push((<span key={i*2+1}>/</span>));
seriesTraversal.push(this.props.reports.seriesTraversal[i]);
}
if (titleTracks.length == 0) {
titleTracks.push((
<Button key={0} bsStyle="link">
{this.props.reports.selectedTabulation.Title}
</Button>
));
} else {
var i = this.props.reports.seriesTraversal.length-1;
titleTracks.push((
<Button key={i*2+2} bsStyle="link">
{this.props.reports.seriesTraversal[i]}
</Button>
));
}
if (this.props.reports.selectedTabulation.Labels.length > 1)
var report = (
<StackedBarChart
report={this.props.reports.selectedTabulation}
onSelectSeries={this.onSelectSeries}
seriesTraversal={this.props.reports.seriesTraversal} />
);
else
var report = (
<PieChart
report={this.props.reports.selectedTabulation}
onSelectSeries={this.onSelectSeries}
seriesTraversal={this.props.reports.seriesTraversal} />
);
reportPanel = (
<Panel header={titleTracks}>
{report}
</Panel>
);
} else if (this.props.reports.selected != -1) {
reportPanel = (
<Panel header={this.props.reports.map[this.props.reports.selected].Name}>
<ProgressBar active now={100} label={"Tabulating Report..."} />
</Panel>
);
}
var titleTracks = [];
var seriesTraversal = [];
for (var i = 0; i < this.props.selectedTabulation.seriesTraversal.length; i++) {
var name = this.props.selectedTabulation.tabulation.Title;
if (i > 0)
name = this.props.selectedTabulation.seriesTraversal[i-1];
// Make a closure for going up the food chain
var self = this;
var navOnClick = function() {
var onSelectTabulation = self.props.onSelectTabulation;
var report = self.props.reports[self.props.selectedTabulation.tabulation.ReportId];
var mySeriesTraversal = seriesTraversal.slice();
return function() {
onSelectTabulation(report, mySeriesTraversal);
};
}();
titleTracks.push((
<Button key={i*2} bsStyle="link"
onClick={navOnClick}>
{name}
</Button>
));
titleTracks.push((<span key={i*2+1}>/</span>));
seriesTraversal.push(this.props.selectedTabulation.seriesTraversal[i]);
}
if (titleTracks.length == 0) {
titleTracks.push((
<Button key={0} bsStyle="link">
{this.props.selectedTabulation.tabulation.Title}
</Button>
));
} else {
var i = this.props.selectedTabulation.seriesTraversal.length-1;
titleTracks.push((
<Button key={i*2+2} bsStyle="link">
{this.props.selectedTabulation.seriesTraversal[i]}
</Button>
));
}
var noReportSelected = this.props.reports.selected == -1;
var selectedReport = -1;
if (this.props.reports.map.hasOwnProperty(this.props.reports.selected))
selectedReport = this.props.reports.map[this.props.reports.selected];
return (
<Panel header={titleTracks}>
<PieChart
report={this.props.selectedTabulation.tabulation}
onSelectSeries={this.onSelectSeries}
seriesTraversal={this.props.selectedTabulation.seriesTraversal} />
</Panel>
<div>
<AddEditReportModal
show={this.state.creatingNewReport}
onCancel={this.onCreationCancel}
onSubmit={this.onCreationSubmit} />
<AddEditReportModal
show={this.state.editingReport}
editReport={selectedReport}
onCancel={this.onEditingCancel}
onSubmit={this.onEditingSubmit} />
<ButtonToolbar>
<ButtonGroup>
<Button onClick={this.onNewReport} bsStyle="success"><Glyphicon glyph='plus-sign'/> New Report</Button>
</ButtonGroup>
<ButtonGroup>
<Combobox
data={this.props.report_list}
valueField='ReportId'
textField={item => typeof item === 'string' ? item : item.Name}
value={selectedReport}
onChange={this.onSelectReport}
suggest
filter='contains'
ref="report" />
</ButtonGroup>
<ButtonGroup>
<Button onClick={this.onEditReport} bsStyle="primary" disabled={noReportSelected}><Glyphicon glyph='cog'/> Edit Report</Button>
<Button onClick={this.onDeleteReport} bsStyle="danger" disabled={noReportSelected}><Glyphicon glyph='trash'/> Delete Report</Button>
</ButtonGroup></ButtonToolbar>
{reportPanel}
</div>
);
}
}

View File

@ -135,12 +135,12 @@ class StackedBarChart extends React.Component {
if (value == 0)
continue;
if (value > 0) {
rectHeight = y(value) - y(0);
var rectHeight = y(value) - y(0);
positiveSum[j] += rectHeight;
rectY = height - y(0) - positiveSum[j];
var rectY = height - y(0) - positiveSum[j];
} else {
rectHeight = y(0) - y(value);
rectY = height - y(0) + negativeSum[j];
var rectHeight = y(0) - y(value);
var rectY = height - y(0) + negativeSum[j];
negativeSum[j] += rectHeight;
}

View File

@ -4,8 +4,14 @@ var ReportActions = require('../actions/ReportActions');
var ReportsTab = require('../components/ReportsTab');
function mapStateToProps(state) {
var report_list = [];
for (var reportId in state.reports.map) {
if (state.reports.map.hasOwnProperty(reportId))
report_list.push(state.reports.map[reportId]);
}
return {
reports: state.reports
reports: state.reports,
report_list: report_list
}
}

View File

@ -435,6 +435,35 @@ class Error {
}
}
class Report {
constructor() {
this.ReportId = -1;
this.UserId = -1;
this.Name = "";
this.Lua = "";
}
toJSON() {
var json_obj = {};
json_obj.ReportId = this.ReportId;
json_obj.UserId = this.UserId;
json_obj.Name = this.Name;
json_obj.Lua = this.Lua;
return JSON.stringify(json_obj);
}
fromJSON(json_input) {
var json_obj = getJSONObj(json_input)
if (json_obj.hasOwnProperty("ReportId"))
this.ReportId = json_obj.ReportId;
if (json_obj.hasOwnProperty("UserId"))
this.UserId = json_obj.UserId;
if (json_obj.hasOwnProperty("Name"))
this.Name = json_obj.Name;
if (json_obj.hasOwnProperty("Lua"))
this.Lua = json_obj.Lua;
}
}
class Series {
constructor() {
this.Values = [];
@ -496,7 +525,7 @@ class Series {
class Tabulation {
constructor() {
this.ReportId = "";
this.ReportId = -1;
this.Title = "";
this.Subtitle = "";
this.Units = "";
@ -578,6 +607,7 @@ module.exports = {
Account: Account,
Split: Split,
Transaction: Transaction,
Report: Report,
Tabulation: Tabulation,
OFXDownload: OFXDownload,
Error: Error,

View File

@ -63,16 +63,19 @@ module.exports = function(state = initialState, action) {
selectedTabulation: null,
seriesTraversal: []
});
case ReportConstants.TABULATION_FETCHED:
case ReportConstants.REPORT_TABULATED:
var tabulation = action.tabulation;
return assign({}, state, {
var tabulations = assign({}, state.tabulations, {
[tabulation.ReportId]: tabulation
});
return assign({}, state, {
tabulations: tabulations
});
case ReportConstants.SERIES_SELECTED:
return {
return assign({}, state, {
selectedTabulation: action.tabulation,
seriesTraversal: action.seriesTraversal
};
});
case UserConstants.USER_LOGGEDOUT:
return initialState;
default:

View File

@ -19,6 +19,7 @@
"react-dom": "^15.3.2",
"react-redux": "^5.0.5",
"react-widgets": "^3.4.4",
"react-codemirror": "^1.0.0",
"redux": "^3.6.0",
"redux-thunk": "^2.1.0"
},

View File

@ -203,6 +203,8 @@ func ReportTabulationHandler(w http.ResponseWriter, r *http.Request, user *User,
return
}
tabulation.ReportId = reportid
err = tabulation.Write(w)
if err != nil {
WriteError(w, 999 /*Internal Error*/)

View File

@ -5,6 +5,7 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="static/react-widgets/css/react-widgets.css">
<link rel="stylesheet" href="static/codemirror/codemirror.css">
<link rel="stylesheet" href="static/css/stylesheet.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>