AG-420 Improve React implementation

This commit is contained in:
Sean Landsman
2017-05-17 13:22:59 +01:00
parent 4069f6bc10
commit 8677118a27
16 changed files with 3807 additions and 18 deletions

View File

@@ -6,7 +6,9 @@
"scripts": {
"full-width": "webpack-dev-server --config webpack.config.full-width.js --progress --colors --hot --inline",
"standard": "webpack-dev-server --config webpack.config.standard.js --progress --colors --hot --inline",
"hoc": "webpack-dev-server --config webpack.config.hoc.js --progress --colors --hot --inline",
"grouped": "webpack-dev-server --config webpack.config.grouped.js --progress --colors --hot --inline",
"trader": "webpack-dev-server --content-base src-trader-dashboard/ --config webpack.config.trader.js --progress --colors --hot --inline",
"large": "webpack-dev-server --config webpack.config.large.js --progress --colors --hot --inline",
"clean": "rimraf dist",
"build-standard": "npm run clean && webpack --config webpack.config.standard.js --progress --profile --bail"
@@ -28,23 +30,25 @@
},
"homepage": "http://www.ag-grid.com/",
"devDependencies": {
"babel-core": "^6.0.0",
"babel-loader": "6.2.1",
"babel-preset-es2015": "6.3.13",
"babel-preset-react": "6.3.13",
"babel-core": "^6.0.0",
"babel-preset-stage-1": "^6.24.1",
"css-loader": "0.23.1",
"style-loader": "0.13.0",
"webpack": "1.12.11",
"webpack-dev-server": "^1.14.1"
},
"dependencies": {
"rimraf": "2.5.x",
"react": "0.14.6",
"react-dom": "0.14.6",
"ag-grid": "9.1.x",
"ag-grid-api": "file:../ag-grid-api",
"ag-grid-enterprise": "9.1.x",
"ag-grid-react": "9.1.x"
"ag-grid-react": "9.1.x",
"d3": "^4.9.1",
"lodash": "^4.17.4",
"react": "15.5.x",
"react-dom": "15.5.x",
"rimraf": "2.5.x"
}
}

View File

@@ -3,7 +3,7 @@
import ReactDOM from 'react-dom';
import React from 'react';
import MyApp from './myApp.jsx';
// is there a better way of doing this?
import 'ag-grid-root/dist/styles/ag-grid.css';
import 'ag-grid-root/dist/styles/theme-fresh.css';

View File

@@ -5,9 +5,8 @@ import ColDefFactory from "./ColDefFactory.jsx";
import MyReactDateComponent from "./MyReactDateComponent.jsx";
import MyReactHeaderComponent from "./MyReactHeaderComponent.jsx";
import "./myApp.css";
import "ag-grid-enterprise";
// take this line out if you do not want to use ag-Grid-Enterprise
import "ag-grid-enterprise";
export default class MyApp extends React.Component {
@@ -40,20 +39,24 @@ export default class MyApp extends React.Component {
// what you want!
this.gridOptions = {
//We register the react date component that ag-grid will use to render
dateComponentFramework:MyReactDateComponent,
dateComponentFramework: MyReactDateComponent,
// this is how you listen for events using gridOptions
onModelUpdated: function () {
console.log('event onModelUpdated received');
},
defaultColDef : {
headerComponentFramework : MyReactHeaderComponent,
headerComponentParams : {
defaultColDef: {
headerComponentFramework: MyReactHeaderComponent,
headerComponentParams: {
menuIcon: 'fa-bars'
}
},
// this is a simple property
rowBuffer: 10 // no need to set this, the default is fine for almost all scenarios
};
this.onGridReady = this.onGridReady.bind(this);
this.onRowSelected = this.onRowSelected.bind(this);
this.onCellClicked = this.onCellClicked.bind(this);
}
onShowGrid(show) {
@@ -108,7 +111,7 @@ export default class MyApp extends React.Component {
componentInstance.helloFromSkillsFilter();
}
dobFilter () {
dobFilter() {
let dateFilterComponent = this.gridOptions.api.getFilterInstance('dob');
dateFilterComponent.setFilterType('equals');
dateFilterComponent.setDateFrom('2000-01-01');
@@ -181,9 +184,9 @@ export default class MyApp extends React.Component {
gridOptions={this.gridOptions}
// listening for events
onGridReady={this.onGridReady.bind(this)}
onRowSelected={this.onRowSelected.bind(this)}
onCellClicked={this.onCellClicked.bind(this)}
onGridReady={this.onGridReady}
onRowSelected={this.onRowSelected}
onCellClicked={this.onCellClicked}
// binding to simple properties
showToolPanel={this.state.showToolPanel}

View File

@@ -0,0 +1,28 @@
import React, { Component } from 'react';
export default class extends Component {
constructor(props) {
super(props);
this.onExchangeChanged = this.onExchangeChanged.bind(this);
}
onExchangeChanged(event) {
this.props.onExchangeChanged(event.target.value);
}
render() {
return (
<div>
<span style={{marginRight: 15}}>Control Panel</span>
<select value={this.props.selectedExchange.symbol} onChange={this.onExchangeChanged}>
{
this.props.exchanges.map((exchange) => {
return <option key={exchange.symbol} value={exchange.symbol}>{exchange.name}</option>
})
}
</select>
</div>
);
}
};

View File

@@ -0,0 +1,159 @@
import React, {Component} from "react";
import {AgGridReact} from "ag-grid-react";
import map from "lodash/map";
import difference from "lodash/difference";
import forEach from "lodash/forEach";
import includes from "lodash/includes";
import assign from "lodash/assign";
import ExchangeService from "../services/ExchangeService.jsx";
export default class extends Component {
constructor(props) {
super(props);
this.state = {
columnDefs: [
{
field: 'symbol',
headerName: 'Symbol',
sort: 'asc'
},
{
field: 'price',
headerName: 'Price',
cellFormatter: this.numberFormatter,
cellRenderer: 'animateShowChange',
cellStyle: {'text-align': 'right'}
},
{
field: 'bid',
headerName: 'Bid',
cellFormatter: this.numberFormatter,
cellRenderer: 'animateShowChange',
cellStyle: {'text-align': 'right'}
},
{
field: 'ask',
headerName: 'Ask',
cellFormatter: this.numberFormatter,
cellRenderer: 'animateShowChange',
cellStyle: {'text-align': 'right'}
}
]
};
this.exchangeService = new ExchangeService();
// grid events
this.onGridReady = this.onGridReady.bind(this);
// component events
this.updateQuote = this.updateQuote.bind(this);
}
numberFormatter(params) {
if (typeof params.value === 'number') {
return params.value.toFixed(2);
} else {
return params.value;
}
}
onGridReady(params) {
this.gridApi = params.api;
this.columnApi = params.columnApi;
this.gridApi.addItems(map(this.props.selectedExchange.supportedStocks, symbol => this.exchangeService.getTicker(symbol)));
this.gridApi.sizeColumnsToFit();
}
componentWillMount() {
this.props.selectedExchange.supportedStocks.forEach(symbol => {
this.exchangeService.addSubscriber(this.updateQuote, symbol);
});
}
componentWillUnmount() {
this.exchangeService.removeSubscribers();
}
componentWillReceiveProps(nextProps) {
if (nextProps.selectedExchange.supportedStocks !== this.props.selectedExchange.supportedStocks) {
if (!this.gridApi) {
return;
}
const currentSymbols = this.props.selectedExchange.supportedStocks;
const nextSymbols = nextProps.selectedExchange.supportedStocks;
// Unsubscribe to current ones that will be removed
const symbolsRemoved = difference(currentSymbols, nextSymbols);
forEach(symbolsRemoved, symbol => {
this.exchangeService.removeSubscriber(this.updateQuote, symbol);
});
// Subscribe to new ones that need to be added
const symbolsAdded = difference(nextSymbols, currentSymbols);
forEach(symbolsAdded, symbol => {
this.exchangeService.addSubscriber(this.updateQuote, symbol);
});
// Remove ag-grid nodes as necessary
const nodesToRemove = [];
this.gridApi.forEachNode(node => {
const {data} = node;
if (includes(symbolsRemoved, data.symbol)) {
nodesToRemove.push(node);
}
});
this.gridApi.removeItems(nodesToRemove);
// Insert new ag-grid nodes as necessary
this.gridApi.addItems(map(symbolsAdded, symbol => this.exchangeService.getTicker(symbol)));
}
}
updateQuote(quote) {
if (!this.gridApi) {
// the grid isn't ready yet
return;
}
const updatedNodes = [];
const updatedCols = [];
this.gridApi.forEachNode(node => {
const {data} = node;
if (data.symbol === quote.symbol) {
for (const def of this.state.columnDefs) {
if (data[def.field] !== quote[def.field]) {
updatedCols.push(def.field);
}
}
assign(data, quote);
updatedNodes.push(node);
}
});
this.gridApi.refreshCells(updatedNodes, updatedCols);
};
render() {
return (
<div style={{height: 400, width: 800}}
className="ag-fresh">
<AgGridReact
// properties
columnDefs={this.state.columnDefs}
enableSorting="true"
// events
onGridReady={this.onGridReady}>
</AgGridReact>
</div>
);
}
}

View File

@@ -0,0 +1,23 @@
import React, {Component} from "react";
import StockPriceDeltaPanel from "./StockPriceDeltaPanel.jsx";
import StockTimestampPanel from "./StockTimestampPanel.jsx";
import StockSummaryPanel from "./StockSummaryPanel.jsx";
import StockHistoricalChartPanel from "./StockHistoricalChartPanel.jsx";
export default class extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<StockPriceDeltaPanel />
<StockTimestampPanel />
<StockSummaryPanel />
<StockHistoricalChartPanel />
</div>
);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
import React, {Component} from "react";
import LiveUpdatesGrid from "./LiveUpdatesGrid.jsx";
import StockDetailPanel from "./StockDetailPanel.jsx";
export default class extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div style={{float: "left", marginRight: 25}}>
<LiveUpdatesGrid selectedExchange={this.props.selectedExchange}></LiveUpdatesGrid>
</div>
<div style={{float: "left"}}>
<StockDetailPanel></StockDetailPanel>
</div>
</div>
);
}
};

View File

@@ -0,0 +1,49 @@
import React, {Component} from "react";
export default class extends Component {
constructor(props) {
super(props);
}
render() {
let containerStyle = {
display: "inline-block"
};
let priceStyle = {
fontSize: "2.6em",
fontWeight: "bold",
marginRight: 10
};
let deltaStyle = {
fontWeight: "normal",
fontSize: "1.8em",
verticalAlign: "bottom"
};
let negativeSwingStyle = {
color: "#d14836",
marginRight: 5
};
let positiveSwingStyle = {
color: "#093",
marginRight: 5
};
return (
<div>
<span style={priceStyle}>
155.47
</span>
<div style={containerStyle}>
<span style={deltaStyle}>
<span style={negativeSwingStyle}>-0.23</span>
<span style={negativeSwingStyle}>(-0.15%)</span>
</span>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,94 @@
import React, {Component} from "react";
export default class extends Component {
constructor(props) {
super(props);
}
render() {
let containerStyle = {
fontSize: 13
};
let tableStyle = {
display: "inline-block",
verticalAlign: "top",
borderCollapse:"collapse"
};
let keyStyle = {
color: "#666"
};
let valueStyle = {
textAlign: "right"
};
return (
<div style={containerStyle}>
<table style={tableStyle}>
<tbody>
<tr>
<td style={keyStyle}>Range
</td>
<td style={valueStyle}>154.72 - 156.06
</td>
</tr>
<tr>
<td style={keyStyle}>52 week
</td>
<td style={valueStyle}>91.50 - 156.65
</td>
</tr>
<tr>
<td style={keyStyle}>Open
</td>
<td style={valueStyle}>155.94
</td>
</tr>
<tr>
<td style={keyStyle}>
Vol / Avg.
</td>
<td style={valueStyle}>15,931.00/24.94M
</td>
</tr>
</tbody>
</table>
<table style={tableStyle}>
<tbody>
<tr>
<td style={keyStyle}>
Div/yield
</td>
<td style={valueStyle}>0.63/1.62
</td>
</tr>
<tr>
<td style={keyStyle}>
EPS
</td>
<td style={valueStyle}>8.55
</td>
</tr>
<tr>
<td style={keyStyle}>
Shares
</td>
<td style={valueStyle}>5,213.84M
</td>
</tr>
<tr>
<td style={keyStyle}>
Mkt cap
</td>
<td style={valueStyle}>808,518.60M
</td>
</tr>
</tbody>
</table>
</div>
);
}
}

View File

@@ -0,0 +1,46 @@
import React, {Component} from "react";
export default class extends Component {
constructor(props) {
super(props);
}
render() {
let containerStyle = {
display: "inline-block"
};
let priceStyle = {
fontSize: "2.6em",
fontWeight: "bold",
marginRight: 10
};
let minorStyle = {
fontSize: 11,
color: "#6F6F6F"
};
let negativeSwingStyle = {
color: "#d14836",
marginRight: 5
};
let positiveSwingStyle = {
color: "#093",
marginRight: 5
};
return (
<div>
<div id="ref_304466804484872_elt">
May 17, 6:17am GMT-4
<div style={minorStyle}>
<span>NASDAQ</span>
<div>Currency in USD</div>
</div>
</div>
</div>
);
}
}

View File

@@ -0,0 +1,43 @@
import React, {Component} from "react";
import ExchangeService from "../services/ExchangeService.jsx";
import ControlPanel from "./ControlPanel.jsx";
import StockPanel from "./StockPanel.jsx";
export default class TraderDashboard extends Component {
constructor(props) {
super(props);
this.exchangeService = new ExchangeService();
this.state = {
selectedExchange: this.exchangeService.getExchangeInformation("NASDAQ"),
exchanges: this.exchangeService.getExchanges()
};
this.onExchangeChanged = this.onExchangeChanged.bind(this);
}
onExchangeChanged(selectedExchange) {
this.setState({
selectedExchange: this.exchangeService.getExchangeInformation(selectedExchange)
});
}
render() {
return (
<div>
<div style={{marginTop: 25, marginBottom: 25}}>
<ControlPanel
exchanges={this.state.exchanges}
selectedExchange={this.state.selectedExchange}
onExchangeChanged={this.onExchangeChanged}>
</ControlPanel>
</div>
<StockPanel selectedExchange={this.state.selectedExchange}></StockPanel>
</div>
);
}
};

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript" src="dist/bundle.js" charset="utf-8"></script>
<style>
html, body {
height: 100%;
}
</style>
</head>
<body>
<div id="traderDashboard"></div>
</body>
</html>

View File

@@ -0,0 +1,18 @@
'use strict';
import ReactDOM from "react-dom";
import React from "react";
import TraderDashboard from "./components/TraderDashboard.jsx";
import "ag-grid-root/dist/styles/ag-grid.css";
import "ag-grid-root/dist/styles/theme-fresh.css";
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(
React.createElement(TraderDashboard),
document.querySelector('#traderDashboard')
);
});

View File

@@ -0,0 +1,234 @@
import concat from "lodash/concat";
import remove from "lodash/remove";
import uniq from "lodash/uniq";
import find from "lodash/find";
import sampleSize from "lodash/sampleSize";
import keys from "lodash/keys";
export default class {
constructor() {
this.subscribers = {};
this.exchanges = [
{name: 'Nasdaq Stock Market', symbol: 'NASDAQ', supportedStocks: NASDAQ_SYMBOLS},
{name: 'London Stock Exchange', symbol: 'LSE', supportedStocks: LSE_SYMBOLS},
{name: 'Japan Exchange Group', symbol: 'JSE', supportedStocks: JSE_SYMBOLS},
{name: 'Deutsche Börse', symbol: 'DE', supportedStocks: DE_SYMBOLS}
];
this.initialiseTickerData();
}
initialiseTickerData() {
this.tickerData = {};
const allSymbols = uniq(concat(NASDAQ_SYMBOLS, LSE_SYMBOLS, JSE_SYMBOLS, DE_SYMBOLS));
allSymbols.forEach((symbol) => {
this.tickerData[symbol] = this.generateTickerRow(symbol);
});
}
generateTickerRow(symbol) {
let price = this.random(10, 600);
return {
symbol,
price,
bid: price - this.random(1, 3),
ask: price + this.random(1, 3)
}
}
random(min, max) {
return parseFloat((Math.random() * (max - min + 1) + min))
}
addSubscriber(subscriber, symbol) {
if (!this.subscribers[symbol]) {
this.subscribers[symbol] = [];
}
this.subscribers[symbol].push(subscriber);
if (!this.updateInterval) {
this.updateInterval = setInterval(this.applyDeltasToTickerData.bind(this), 500);
}
}
applyDeltasToTickerData() {
let symbols = keys(this.subscribers);
let properties = ['price', 'bid', 'ask'];
let symbolsToAlter = sampleSize(symbols, symbols.length / 4);
let propertyToAlter = sampleSize(properties, 1);
symbolsToAlter.forEach((symbol) => {
this.tickerData[symbol] = {
symbol,
price: this.tickerData[symbol].price,
bid: this.tickerData[symbol].bid,
ask: this.tickerData[symbol].ask,
};
this.tickerData[symbol][propertyToAlter] = +this.tickerData[symbol][propertyToAlter] + this.random(-2, 2);
});
symbols.forEach((symbol) => {
this.subscribers[symbol].forEach((subscriber) => {
subscriber(this.tickerData[symbol]);
});
});
}
removeSubscriber(subscriber, symbol) {
remove(this.subscribers[symbol], subscriber);
}
getTicker(symbol) {
return this.tickerData[symbol];
}
getExchanges() {
return this.exchanges;
}
getExchangeInformation(exchangeName) {
return find(this.exchanges, (exchange) => {
return exchange.symbol === exchangeName;
})
}
}
const NASDAQ_SYMBOLS = [
"SNCL.L",
"RNK.L",
"SWJ.L",
"JDT.L",
"UANC.L",
"SDP.L",
"HSBA.L",
"XPL.L",
"KLR.L",
"SSE.L",
"JSI.L",
"UBMN.L",
"WPC.L",
"VTC.L",
"UTG.L",
"DOR.L",
"44RS.L",
"GPOR.L",
"ASL.L",
"40JP.L",
"133716",
"PJF.L",
"MLC.L",
"137817",
"GHE.L",
"PML.L",
"SBRY.L",
"LEN.L",
"STS.L",
"138654",
"PTEC.L"
];
const LSE_SYMBOLS = [
"PVG.L",
"SN.L,",
"SWJ.L",
"JDT.L",
"UANC.L",
"SDP.L",
"HSBA.L",
"XPL.L",
"KLR.L",
"SSE.L",
"JSI.L",
"UBMN.L",
"DLN.L",
"SIR.L",
"SEC.L",
"DOR.L",
"44RS.L",
"GPOR.L",
"ASL.L",
"40JP.L",
"133716",
"PJF.L",
"MLC.L",
"137817",
"GHE.L",
"PML.L",
"SBRY.L",
"LEN.L",
"MAV4.L",
"GLEN.L",
"EDGD.L",
];
const JSE_SYMBOLS = [
"ECV.L",
"MHN.L",
"SWJ.L",
"JDT.L",
"UANC.L",
"PLAZ.L",
"CLDN.L",
"XPL.L",
"KLR.L",
"SSE.L",
"JSI.L",
"UBMN.L",
"WPC.L",
"VTC.L",
"UTG.L",
"DOR.L",
"44RS.L",
"GPOR.L",
"ASL.L",
"40JP.L",
"133716",
"CRW.L",
"JPR.L",
"UTLC.L",
"GHS.L",
"PML.L",
"SBRY.L",
"LEN.L",
"STS.L",
"138654",
"RWS.L"
];
const DE_SYMBOLS = [
"ECV.L",
"MHN.L",
"SWJ.L",
"JDT.L",
"UANC.L",
"SDP.L",
"KBC.L",
"VM.L,",
"KLR.L",
"SSE.L",
"JSI.L",
"UBMN.L",
"WPC.L",
"VTC.L",
"UTG.L",
"DOR.L",
"44RS.L",
"GPOR.L",
"ASL.L",
"40JP.L",
"133716",
"PJF.L",
"MLC.L",
"DPV6.L",
"LMIN.L",
"PML.L",
"SBRY.L",
"LEN.L",
"STS.L",
"BKIR.L",
"AFMF.L",
];

33
webpack.config.trader.js Normal file
View File

@@ -0,0 +1,33 @@
const webpack = require('webpack');
const path = require('path');
const SRC_DIR = path.resolve(__dirname, 'src-trader-dashboard');
module.exports = {
entry: SRC_DIR + "/index.js",
output: {
path: __dirname,
filename: "dist/bundle.js"
},
module: {
loaders: [
{
test: /\.css$/,
loader: "style!css"
},
{
test: /\.js$|\.jsx$/,
include: SRC_DIR,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
}
]
},
resolve: {
alias: {
"ag-grid-root" : __dirname + "/node_modules/ag-grid"
}
}
};