Add speed graphs

This commit is contained in:
John Furrow
2015-08-26 22:32:07 -07:00
parent 4ae56f1ea6
commit f8f9552bdf
9 changed files with 286 additions and 190 deletions
+56 -60
View File
@@ -1,90 +1,86 @@
var gulp = require('gulp'),
sass = require('gulp-sass'),
autoprefixer = require('gulp-autoprefixer'),
watch = require('gulp-watch'),
notify = require('gulp-notify'),
browserSync = require('browser-sync'),
browserify = require('browserify'),
watchify = require('watchify'),
reactify = require('reactify'),
source = require('vinyl-source-stream'),
svgmin = require('gulp-svgmin');
sass = require('gulp-sass'),
autoprefixer = require('gulp-autoprefixer'),
watch = require('gulp-watch'),
notify = require('gulp-notify'),
browserSync = require('browser-sync'),
browserify = require('browserify'),
watchify = require('watchify'),
reactify = require('reactify'),
sourcemaps = require('gulp-sourcemaps'),
source = require('vinyl-source-stream'),
svgmin = require('gulp-svgmin');
var supportedBrowsers = ['last 2 versions', '> 1%', 'ie >= 8', 'Firefox ESR', 'Opera >= 12'],
jsFiles = [];
jsFiles = [];
var sourceDir = './source/',
destDir = './dist/public/';
destDir = './dist/public/';
var reload = browserSync.reload;
function handleErrors() {
var args = Array.prototype.slice.call(arguments);
notify.onError({
title: "Compile Error",
message: "<%= error.message %>"
}).apply(this, args);
this.emit('end');
var args = Array.prototype.slice.call(arguments);
notify.onError({
title: "Compile Error",
message: "<%= error.message %>"
}).apply(this, args);
this.emit('end');
}
gulp.task('browser-sync', function() {
return browserSync.init({
port: 3001
});
return browserSync.init({
port: 3001
});
});
gulp.task('styles', function() {
return gulp.src(sourceDir + 'sass/style.scss')
.pipe(sass({
errLogToConsole: true
}))
.pipe(autoprefixer({
browsers: supportedBrowsers,
map: true
}))
.pipe(browserSync.reload({
stream: true
}))
.pipe(gulp.dest(destDir + 'stylesheets'));
return gulp.src(sourceDir + 'sass/style.scss')
.pipe(sass({
errLogToConsole: true
}))
.pipe(autoprefixer({
browsers: supportedBrowsers,
map: true
}))
.pipe(browserSync.reload({
stream: true
}))
.pipe(gulp.dest(destDir + 'stylesheets'));
});
gulp.task('scripts', function() {
var bundler = browserify({
entries: [sourceDir + '/scripts/app.js'],
cache: {},
packageCache: {},
fullPaths: true
});
var bundler = browserify({
entries: [sourceDir + '/scripts/app.js'],
cache: {},
packageCache: {},
fullPaths: true
});
bundler.transform(reactify);
function rebundle() {
var stream = bundler.bundle();
return stream.on('error', handleErrors)
.pipe(source('app.js'))
.pipe(gulp.dest(destDir + 'scripts/'));
}
bundler.transform(reactify);
bundler.on('update', function() {
rebundle();
});
function rebundle() {
var stream = bundler.bundle();
return stream.on('error', handleErrors)
.pipe(source('app.js'))
.pipe(gulp.dest(destDir + 'scripts/'));
}
bundler.on('update', function() {
rebundle();
});
return rebundle();
return rebundle();
});
gulp.task('svg', function() {
return gulp.src(sourceDir + '/images/*.svg')
.pipe(svgmin())
.pipe(gulp.dest(sourceDir + '/images'));
return gulp.src(sourceDir + '/images/*.svg')
.pipe(svgmin())
.pipe(gulp.dest(sourceDir + '/images'));
});
gulp.task('watch', function () {
gulp.watch(sourceDir + 'sass/**/*.scss', ['styles']);
gulp.watch(sourceDir + 'scripts/**/*.js', ['scripts', reload]);
gulp.watch(sourceDir + 'sass/**/*.scss', ['styles']);
gulp.watch(sourceDir + 'scripts/**/*.js', ['scripts', reload]);
});
gulp.task('default', ['scripts', 'styles', 'watch', 'browser-sync']);
+2
View File
@@ -8,6 +8,7 @@
"dependencies": {
"body-parser": "~1.12.0",
"cookie-parser": "~1.3.4",
"d3": "^3.5.6",
"debug": "~2.1.1",
"es6-promise": "^2.0.1",
"express": "~4.12.2",
@@ -30,6 +31,7 @@
"gulp-autoprefixer": "^2.1.0",
"gulp-notify": "^2.2.0",
"gulp-sass": "^1.3.3",
"gulp-sourcemaps": "^1.5.2",
"gulp-svgmin": "^1.1.1",
"gulp-watch": "^4.2.1",
"jquery": "^2.1.3",
-11
View File
@@ -5,13 +5,11 @@ var TorrentStore = require('../stores/TorrentStore');
var UIStore = require('../stores/UIStore');
var TorrentList = require('./torrent-list/TorrentList');
var TorrentListHeader = require('./torrent-list/TorrentListHeader');
var Modals = require('./modals/Modals');
var FloodApp = React.createClass({
getInitialState: function() {
return {
modal: null,
sortCriteria: {
direction: 'asc',
property: 'name'
@@ -21,19 +19,16 @@ var FloodApp = React.createClass({
componentDidMount: function() {
TorrentStore.addSortChangeListener(this._onSortChange);
UIStore.addModalChangeListener(this._onModalChange);
},
componentWillUnmount: function() {
TorrentStore.removeSortChangeListener(this._onSortChange);
UIStore.removeModalChangeListener(this._onModalChange);
},
render: function() {
return (
<div className="flood">
<Modals type={this.state.modal} />
<FilterBar />
<main className="main">
<ActionBar />
@@ -47,12 +42,6 @@ var FloodApp = React.createClass({
this.setState({
sortCriteria: TorrentStore.getSortCriteria()
});
},
_onModalChange: function() {
this.setState({
modal: UIStore.getActiveModal()
});
}
});
@@ -50,7 +50,9 @@ var FilterBar = React.createClass({
},
_start: function() {
TorrentActions.start(this.state.selectedTorrents);
TorrentActions.start({
hash: this.state.selectedTorrents
});
},
_stop: function() {
@@ -26,13 +26,19 @@ var SortDropdown = React.createClass({
<div className="dropdown__content__header">Add Torrent</div>
<div className="dropdown__content__container">
<div className="form__row">
<label className="form__label">
Torrents
</label>
<input className="textbox"
onChange={this._handleUrlChange}
placeholder="Torrent URL"
placeholder="Torrent URLs"
value={this.state.url}
type="text" />
</div>
<div className="form__row">
<label className="form__label">
Destination
</label>
<input className="textbox"
onChange={this._handleDestinationChange}
placeholder="Destination"
@@ -40,7 +46,7 @@ var SortDropdown = React.createClass({
type="text" />
</div>
<div className="form__row">
<button className="button" onClick={this._handleAddTorrent}>Add Torrent</button>
<button className="button button--primary" onClick={this._handleAddTorrent}>Add Torrent</button>
</div>
</div>
</div>
@@ -2,6 +2,7 @@ var React = require('react');
var ClientStore = require('../../stores/ClientStore');
var Icon = require('../icons/Icon');
var format = require('../../helpers/formatData');
var LineChart = require('./LineChart');
var getClientStats = function() {
return {
@@ -14,20 +15,28 @@ var ClientStats = React.createClass({
getInitialState: function() {
return {
clientStats: {
speed: {
currentSpeed: {
upload: 0,
download: 0
},
historicalSpeed: {
download: [],
upload: []
},
transferred: {
upload: 0,
download: 0
}
}
},
sidebarWidth: 0
};
},
componentDidMount: function() {
ClientStore.addChangeListener(this._onChange);
this.setState({
sidebarWidth: React.findDOMNode(this).offsetWidth
});
},
componentWillUnmount: function() {
@@ -35,10 +44,9 @@ var ClientStats = React.createClass({
},
render: function() {
var uploadSpeed = format.data(this.state.clientStats.speed.upload, '/s');
var uploadSpeed = format.data(this.state.clientStats.currentSpeed.upload, '/s');
var uploadTotal = format.data(this.state.clientStats.transferred.upload);
var downloadSpeed = format.data(this.state.clientStats.speed.download, '/s');
var downloadSpeed = format.data(this.state.clientStats.currentSpeed.download, '/s');
var downloadTotal = format.data(this.state.clientStats.transferred.download);
return (
@@ -60,6 +68,12 @@ var ClientStats = React.createClass({
<em className="unit">{downloadTotal.unit}</em> Downloaded
</div>
</div>
<LineChart
data={this.state.clientStats.historicalSpeed.download}
height={100}
id="graph--download"
slug="graph--download"
width={this.state.sidebarWidth} />
</div>
<div className="client-stat client-stat--upload">
<span className="client-stat__icon">
@@ -78,6 +92,12 @@ var ClientStats = React.createClass({
<em className="unit">{uploadTotal.unit}</em> Uploaded
</div>
</div>
<LineChart
data={this.state.clientStats.historicalSpeed.upload}
height={100}
id="graph--upload"
slug="graph--upload"
width={this.state.sidebarWidth} />
</div>
<button className="client-stats client-stat--limits">
<Icon icon="limits" /> Limits
@@ -0,0 +1,108 @@
var React = require('react');
var d3 = require('d3');
var LineChart = React.createClass({
componentWillUpdate: function() {
var graph = d3.select('#' + this.props.id);
var lineData = this.props.data;
var margin = {
bottom: 10,
top: 10
};
var width = this.props.width;
var height = this.props.height;
var xRange = d3
.scale
.linear()
.range([0, width])
.domain([
d3.min(lineData, function(d) {
return d.x;
}),
d3.max(lineData, function(d) {
return d.x;
})
]);
var yRange = d3
.scale
.linear()
.range([height - margin.bottom - margin.top, 0])
.domain([
d3.min(lineData, function(d) {
return d.y;
}),
d3.max(lineData, function(d) {
return d.y;
})
]);
// debugger;
var lineFunc = d3
.svg
.line()
.x(function(d) {
return xRange(d.x);
})
.y(function(d) {
return yRange(d.y);
})
.interpolate('basis');
var areaFunc = d3
.svg
.area()
.x(function(d) {
return xRange(d.x);
})
.y0(height)
.y1(function(d) {
return yRange(d.y);
})
.interpolate('basis');
var points = lineFunc(lineData);
var area = areaFunc(lineData);
graph
.select('g')
.remove();
graph
.append('g')
.append('svg:path')
.attr('class', 'graph--area')
.attr('d', area)
.attr('transform', 'translate(0,' + margin.top + ')');;
graph
.select('g')
.append('svg:path')
.attr('class', 'graph--line')
.attr('d', points)
.attr('transform', 'translate(0,' + margin.top + ')');
},
render: function() {
return (
<svg className="graph" id={this.props.id}>
<defs>
<linearGradient
id={this.props.slug + '--gradient'}
x1="0%"
y1="0%"
x2="0%"
y2="100%">
<stop className={this.props.slug + '--gradient--top'} offset="0%"/>
<stop className={this.props.slug + '--gradient--bottom'} offset="100%"/>
</linearGradient>
</defs>
</svg>
);
}
});
module.exports = LineChart;
@@ -1,65 +0,0 @@
var React = require('react');
var Icon = require('../icons/Icon');
var TorrentActions = require('../../actions/TorrentActions');
var UIActions = require('../../actions/UIActions');
var Modal = React.createClass({
getInitialState: function() {
return {
url: '',
destination: ''
}
},
render: function() {
return (
<aside className="modal__window" onClick={this.props.clickHandler}>
<header className="modal__header modal__header--toggle">
<h1>Add Torrent</h1>
</header>
<div className="modal__content">
<div className="form__row">
<input className="textbox"
onChange={this._onUrlChange}
placeholder="Torrent URL"
value={this.state.url}
type="text" />
</div>
<div className="form__row">
<input className="textbox"
onChange={this._onDestinationChange}
placeholder="Destination"
value={this.state.destination}
type="text" />
</div>
<div className="form__row">
<button className="button" onClick={this._onAddTorrent}>Add Torrent</button>
</div>
</div>
</aside>
);
},
_onDestinationChange: function(event) {
this.setState({
destination: event.target.value
})
},
_onUrlChange: function(event) {
this.setState({
url: event.target.value
})
},
_onAdd: function() {
TorrentActions.add({
method: 'url',
url: this.state.url,
destination: this.state.destination
});
}
});
module.exports = Modal;
+84 -46
View File
@@ -4,72 +4,110 @@ var ClientConstants = require('../constants/ClientConstants');
var $ = require('jquery');
var assign = require('object-assign');
var _historyLength = 20;
var _stats = {};
var _uploadSpeedHistory = [];
var _downloadSpeedHistory = [];
var ClientStore = assign({}, EventEmitter.prototype, {
getStats: function() {
return _stats;
},
getStats: function() {
return _stats;
},
emitChange: function() {
this.emit(ClientConstants.CLIENT_STATS_CHANGE);
},
emitChange: function() {
this.emit(ClientConstants.CLIENT_STATS_CHANGE);
},
addChangeListener: function(callback) {
this.on(ClientConstants.CLIENT_STATS_CHANGE, callback);
},
removeChangeListener: function(callback) {
this.removeListener(ClientConstants.CLIENT_STATS_CHANGE, callback);
}
addChangeListener: function(callback) {
this.on(ClientConstants.CLIENT_STATS_CHANGE, callback);
},
removeChangeListener: function(callback) {
this.removeListener(ClientConstants.CLIENT_STATS_CHANGE, callback);
}
});
var dispatcherIndex = AppDispatcher.register(function(action) {
var text;
var text;
switch(action.actionType) {
switch(action.actionType) {
case ClientConstants.ADD_TORRENT:
getClientStats();
break;
case ClientConstants.ADD_TORRENT:
getClientStats();
break;
case ClientConstants.REMOVE_TORRENT:
getClientStats();
break;
}
case ClientConstants.REMOVE_TORRENT:
getClientStats();
break;
}
});
var addHistory = function(uploadSpeed, downloadSpeed) {
var index = 0;
console.log(uploadSpeed, downloadSpeed);
while (index < _historyLength) {
if (index < _historyLength - 1) {
if (_uploadSpeedHistory[index] != null && _uploadSpeedHistory[index].x != null) {
_uploadSpeedHistory[index].y = _uploadSpeedHistory[index + 1].y;
_downloadSpeedHistory[index].y = _downloadSpeedHistory[index + 1].y;
} else {
_uploadSpeedHistory[index] = {
x: index,
y: 0
}
_downloadSpeedHistory[index] = {
x: index,
y: 0
}
}
} else {
_uploadSpeedHistory[index] = {
x: index,
y: uploadSpeed
}
_downloadSpeedHistory[index] = {
x: index,
y: downloadSpeed
}
}
index++;
}
}
var getClientStats = function(callback) {
$.ajax({
url: '/client/stats',
dataType: 'json',
success: function(data) {
addHistory(data.uploadRate, data.downloadRate);
$.ajax({
url: '/client/stats',
dataType: 'json',
_stats = {
currentSpeed: {
upload: data.uploadRate,
download: data.downloadRate
},
historicalSpeed: {
upload: _uploadSpeedHistory,
download: _downloadSpeedHistory
},
transferred: {
upload: data.uploadTotal,
download: data.downloadTotal
}
};
success: function(data) {
ClientStore.emitChange();
_stats = {
speed: {
upload: data.uploadRate,
download: data.downloadRate
},
transferred: {
upload: data.uploadTotal,
download: data.downloadTotal
}
};
}.bind(this),
ClientStore.emitChange();
}.bind(this),
error: function(xhr, status, err) {
console.error('/client/stats', status, err.toString());
}.bind(this)
});
error: function(xhr, status, err) {
console.error('/client/stats', status, err.toString());
}.bind(this)
});
};