diff --git a/.jshintrc b/.jshintrc
index 5688a3d2a1..6cd19abfe9 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -30,7 +30,7 @@
"nonbsp" : true,
"nonew" : true,
"plusplus" : false,
- "quotmark" : true,
+ "quotmark" : false,
"undef" : true,
"unused" : true,
"strict" : false,
diff --git a/unleash-server/package.json b/unleash-server/package.json
index 8f5bed7838..bd87f88f83 100644
--- a/unleash-server/package.json
+++ b/unleash-server/package.json
@@ -20,7 +20,7 @@
"start": "NODE_ENV=production node server.js",
"build": "./node_modules/.bin/webpack",
"dev": "NODE_ENV=development supervisor --ignore ./node_modules/,./public/js server.js",
- "test": "jshint server.js lib test && jsxhint public/js/*.jsx && mocha test test/* && npm run coverage",
+ "test": "jshint server.js lib test && jsxhint public/js/**/*.jsx && mocha test test/* && npm run coverage",
"tdd": "mocha --watch test test/*",
"test-bamboo-ci": "mocha test test/*",
"coverage": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec",
diff --git a/unleash-server/public/js/app.jsx b/unleash-server/public/js/app.jsx
new file mode 100644
index 0000000000..cd8a0dae76
--- /dev/null
+++ b/unleash-server/public/js/app.jsx
@@ -0,0 +1,7 @@
+var React = require('react');
+var Unleash = require('./components/Unleash');
+
+React.render(
+ ,
+ document.getElementById('content')
+);
\ No newline at end of file
diff --git a/unleash-server/public/js/components/ErrorMessages.jsx b/unleash-server/public/js/components/ErrorMessages.jsx
new file mode 100644
index 0000000000..153348ef0b
--- /dev/null
+++ b/unleash-server/public/js/components/ErrorMessages.jsx
@@ -0,0 +1,21 @@
+var React = require('react');
+
+var ErrorMessages = React.createClass({
+ render: function() {
+ if (!this.props.errors.length) {
+ return
;
+ }
+
+ var errorNodes = this.props.errors.map(function(e) {
+ return ({e});
+ });
+
+ return (
+
+ );
+ }
+});
+
+module.exports = ErrorMessages;
\ No newline at end of file
diff --git a/unleash-server/public/js/components/FeatureList.jsx b/unleash-server/public/js/components/FeatureList.jsx
new file mode 100644
index 0000000000..542f2d1c5c
--- /dev/null
+++ b/unleash-server/public/js/components/FeatureList.jsx
@@ -0,0 +1,52 @@
+var React = require('react');
+var SavedFeature = require('./SavedFeature');
+var UnsavedFeature = require('./UnsavedFeature');
+
+var FeatureList = React.createClass({
+ render: function() {
+ var featureNodes = [];
+
+ this.props.unsavedFeatures.forEach(function(feature, idx) {
+ var key = 'new-' + idx;
+ featureNodes.push(
+
+ );
+ }.bind(this));
+
+ this.props.savedFeatures.forEach(function(feature) {
+ featureNodes.push(
+
+ );
+ }.bind(this));
+
+ return (
+
+
+
+
Features
+
+
+
+
+
+
+ {featureNodes}
+
+
+
+ );
+ }
+});
+
+module.exports = FeatureList;
\ No newline at end of file
diff --git a/unleash-server/public/js/components/Menu.jsx b/unleash-server/public/js/components/Menu.jsx
new file mode 100644
index 0000000000..9516df1d19
--- /dev/null
+++ b/unleash-server/public/js/components/Menu.jsx
@@ -0,0 +1,14 @@
+var React = require('react');
+
+var Menu = React.createClass({
+ render: function() { return (
+
+ );
+ }
+});
+
+module.exports = Menu;
\ No newline at end of file
diff --git a/unleash-server/public/js/components/SavedFeature.jsx b/unleash-server/public/js/components/SavedFeature.jsx
new file mode 100644
index 0000000000..aef57f663a
--- /dev/null
+++ b/unleash-server/public/js/components/SavedFeature.jsx
@@ -0,0 +1,32 @@
+var React = require('react');
+
+var SavedFeature = React.createClass({
+ onChange: function(event) {
+ this.props.onChange({
+ name: this.props.feature.name,
+ field: 'enabled',
+ value: event.target.checked
+ });
+ },
+
+ render: function() {
+ return (
+
+
+
+
+
{this.props.feature.name}
+
+ {this.props.feature.strategy}
+
+
+ );
+ }
+});
+
+module.exports = SavedFeature;
\ No newline at end of file
diff --git a/unleash-server/public/js/components/Unleash.jsx b/unleash-server/public/js/components/Unleash.jsx
new file mode 100644
index 0000000000..7502377326
--- /dev/null
+++ b/unleash-server/public/js/components/Unleash.jsx
@@ -0,0 +1,131 @@
+var React = require('react');
+var reqwest = require('reqwest');
+var Timer = require('../utils/Timer');
+var Menu = require('./Menu');
+var ErrorMessages = require('./ErrorMessages');
+var FeatureList = require('./FeatureList');
+
+var Unleash = React.createClass({
+ getInitialState: function() {
+ return {
+ savedFeatures: [],
+ unsavedFeatures: [],
+ errors: [],
+ timer: null
+ };
+ },
+
+ componentDidMount: function () {
+ this.loadFeaturesFromServer();
+ this.state.timer = new Timer(this.loadFeaturesFromServer, this.props.pollInterval);
+ this.state.timer.start();
+ },
+
+ componentWillUnmount: function () {
+ if (this.state.timer != null) {
+ this.state.timer.stop();
+ }
+ },
+
+ loadFeaturesFromServer: function () {
+ reqwest('features').then(this.setFeatures, this.handleError);
+ },
+
+ setFeatures: function (data) {
+ this.setState({savedFeatures: data.features});
+ },
+
+ handleError: function (error) {
+ this.state.errors.push(error);
+ },
+
+ updateFeature: function (changeRequest) {
+ var newFeatures = this.state.savedFeatures;
+ newFeatures.forEach(function(f){
+ if(f.name === changeRequest.name) {
+ f[changeRequest.field] = changeRequest.value;
+ }
+ });
+
+ this.setState({features: newFeatures});
+ this.state.timer.stop();
+
+ reqwest({
+ url: 'features/' + changeRequest.name,
+ method: 'patch',
+ type: 'json',
+ contentType: 'application/json',
+ data: JSON.stringify(changeRequest)
+ }).then(function() {
+ // all good
+ this.state.timer.start();
+ }.bind(this), this.handleError);
+ },
+
+ createFeature: function (feature) {
+ var unsaved = [], state = this.state;
+
+ this.state.unsavedFeatures.forEach(function(f) {
+ // TODO: make sure we don't overwrite an existing feature
+ if (f.name === feature.name) {
+ state.savedFeatures.unshift(f);
+ } else {
+ unsaved.push(f);
+ }
+ });
+
+ this.setState({unsavedFeatures: unsaved});
+
+ reqwest({
+ url: 'features',
+ method: 'post',
+ type: 'json',
+ contentType: 'application/json',
+ data: JSON.stringify(feature)
+ }).then(function(r) {
+ console.log(r.statusText);
+ }.bind(this), this.handleError);
+ },
+
+ newFeature: function() {
+ var blankFeature = {
+ name: '',
+ enabled: false,
+ strategy: 'default',
+ parameters: {}
+ };
+
+ this.state.unsavedFeatures.push(blankFeature);
+ this.forceUpdate();
+ },
+
+ cancelNewFeature: function (feature) {
+ var unsaved = [];
+
+ this.state.unsavedFeatures.forEach(function (f) {
+ if (f.name !== feature.name) {
+ unsaved.push(f);
+ }
+ });
+
+ this.setState({unsavedFeatures: unsaved});
+ },
+
+ render: function() {
+ return (
+
+
+
+
+
+ );
+ }
+});
+
+module.exports = Unleash;
\ No newline at end of file
diff --git a/unleash-server/public/js/components/UnsavedFeature.jsx b/unleash-server/public/js/components/UnsavedFeature.jsx
new file mode 100644
index 0000000000..7ac66da90d
--- /dev/null
+++ b/unleash-server/public/js/components/UnsavedFeature.jsx
@@ -0,0 +1,68 @@
+var React = require('react');
+
+var UnsavedFeature = React.createClass({
+ render: function() {
+ return (
+
+ );
+ },
+
+ saveFeature: function(e) {
+ e.preventDefault();
+
+ this.props.feature.name = this.refs.name.getDOMNode().value;
+ this.props.feature.description = this.refs.description.getDOMNode().value;
+ this.props.feature.strategy = this.refs.strategy.getDOMNode().value;
+ this.props.feature.enabled = this.refs.enabled.getDOMNode().checked;
+
+ this.props.onSubmit(this.props.feature);
+ },
+
+ cancelFeature: function(e) {
+ e.preventDefault();
+ this.props.onCancel(this.props.feature);
+ }
+});
+
+module.exports = UnsavedFeature;
\ No newline at end of file
diff --git a/unleash-server/public/js/unleash.jsx b/unleash-server/public/js/unleash.jsx
deleted file mode 100644
index 468b79e86d..0000000000
--- a/unleash-server/public/js/unleash.jsx
+++ /dev/null
@@ -1,336 +0,0 @@
-/* jshint quotmark:false */
-
-var React = require('react');
-var reqwest = require('reqwest');
-
-// Unleash
-// - Menu
-// - FeatureList
-// - UnsavedFeature
-// - SavedFeature
-//
-
-var Timer = function(cb, interval) {
- this.cb = cb;
- this.interval = interval;
- this.timerId = null;
-};
-
-Timer.prototype.start = function() {
- if (this.timerId != null) {
- console.warn("timer already started");
- }
-
- console.log('starting timer');
- this.timerId = setInterval(this.cb, this.interval);
-};
-
-Timer.prototype.stop = function() {
- if (this.timerId == null) {
- console.warn('no timer running');
- } else {
- console.log('stopping timer');
- clearInterval(this.timerId);
- this.timerId = null;
- }
-};
-
-var Menu = React.createClass({
- render: function() { return (
-
- );
- }
-});
-
-var UnsavedFeature = React.createClass({
- render: function() {
- return (
-
- );
- },
-
- saveFeature: function(e) {
- e.preventDefault();
-
- this.props.feature.name = this.refs.name.getDOMNode().value;
- this.props.feature.description = this.refs.description.getDOMNode().value;
- this.props.feature.strategy = this.refs.strategy.getDOMNode().value;
- this.props.feature.enabled = this.refs.enabled.getDOMNode().checked;
-
- this.props.onSubmit(this.props.feature);
- },
-
- cancelFeature: function(e) {
- e.preventDefault();
- this.props.onCancel(this.props.feature);
- }
-});
-
-var SavedFeature = React.createClass({
- onChange: function(event) {
- this.props.onChange({
- name: this.props.feature.name,
- field: 'enabled',
- value: event.target.checked
- });
- },
-
- render: function() {
- return (
-
-
-
-
-
{this.props.feature.name}
-
- {this.props.feature.strategy}
-
-
- );
- }
-});
-
-var FeatureList = React.createClass({
- render: function() {
- var featureNodes = [];
-
- this.props.unsavedFeatures.forEach(function(feature, idx) {
- var key = 'new-' + idx;
- featureNodes.push(
-
- );
- }.bind(this));
-
- this.props.savedFeatures.forEach(function(feature) {
- featureNodes.push(
-
- );
- }.bind(this));
-
- return (
-
-
-
-
Features
-
-
-
-
-
-
- {featureNodes}
-
-
-
- );
- }
-});
-
-var ErrorMessages = React.createClass({
- render: function() {
- if (!this.props.errors.length) {
- return ;
- }
-
- var errorNodes = this.props.errors.map(function(e) {
- return ({e});
- });
-
- return (
-
- );
- }
-});
-
-var Unleash = React.createClass({
- getInitialState: function() {
- return {
- savedFeatures: [],
- unsavedFeatures: [],
- errors: [],
- timer: null
- };
- },
-
- componentDidMount: function () {
- this.loadFeaturesFromServer();
- this.state.timer = new Timer(this.loadFeaturesFromServer, this.props.pollInterval);
- this.state.timer.start();
- },
-
- componentWillUnmount: function () {
- if (this.state.timer != null) {
- this.state.timer.stop();
- }
- },
-
- loadFeaturesFromServer: function () {
- reqwest('features').then(this.setFeatures, this.handleError);
- },
-
- setFeatures: function (data) {
- this.setState({savedFeatures: data.features});
- },
-
- handleError: function (error) {
- this.state.errors.push(error);
- },
-
- updateFeature: function (changeRequest) {
- var newFeatures = this.state.savedFeatures;
- newFeatures.forEach(function(f){
- if(f.name === changeRequest.name) {
- f[changeRequest.field] = changeRequest.value;
- }
- });
-
- this.setState({features: newFeatures});
- this.state.timer.stop();
-
- reqwest({
- url: 'features/' + changeRequest.name,
- method: 'patch',
- type: 'json',
- contentType: 'application/json',
- data: JSON.stringify(changeRequest)
- }).then(function() {
- // all good
- this.state.timer.start();
- }.bind(this), this.handleError);
- },
-
- createFeature: function (feature) {
- var unsaved = [], state = this.state;
-
- this.state.unsavedFeatures.forEach(function(f) {
- // TODO: make sure we don't overwrite an existing feature
- if (f.name === feature.name) {
- state.savedFeatures.unshift(f);
- } else {
- unsaved.push(f);
- }
- });
-
- this.setState({unsavedFeatures: unsaved});
-
- reqwest({
- url: 'features',
- method: 'post',
- type: 'json',
- contentType: 'application/json',
- data: JSON.stringify(feature)
- }).then(function(r) {
- console.log(r.statusText);
- }.bind(this), this.handleError);
- },
-
- newFeature: function() {
- var blankFeature = {
- name: '',
- enabled: false,
- strategy: 'default',
- parameters: {}
- };
-
- this.state.unsavedFeatures.push(blankFeature);
- this.forceUpdate();
- },
-
- cancelNewFeature: function (feature) {
- var unsaved = [];
-
- this.state.unsavedFeatures.forEach(function (f) {
- if (f.name !== feature.name) {
- unsaved.push(f);
- }
- });
-
- this.setState({unsavedFeatures: unsaved});
- },
-
- render: function() {
- return (
-
-
-
-
-
- );
- }
-});
-
-
-React.render(
- ,
- document.getElementById('content')
-);
\ No newline at end of file
diff --git a/unleash-server/public/js/utils/Timer.js b/unleash-server/public/js/utils/Timer.js
new file mode 100644
index 0000000000..8e366fbbb1
--- /dev/null
+++ b/unleash-server/public/js/utils/Timer.js
@@ -0,0 +1,26 @@
+var Timer = function(cb, interval) {
+ this.cb = cb;
+ this.interval = interval;
+ this.timerId = null;
+};
+
+Timer.prototype.start = function() {
+ if (this.timerId != null) {
+ console.warn("timer already started");
+ }
+
+ console.log('starting timer');
+ this.timerId = setInterval(this.cb, this.interval);
+};
+
+Timer.prototype.stop = function() {
+ if (this.timerId == null) {
+ console.warn('no timer running');
+ } else {
+ console.log('stopping timer');
+ clearInterval(this.timerId);
+ this.timerId = null;
+ }
+};
+
+module.exports = Timer;
\ No newline at end of file
diff --git a/unleash-server/webpack.config.js b/unleash-server/webpack.config.js
index 4fd587cfca..67c64c6e61 100644
--- a/unleash-server/webpack.config.js
+++ b/unleash-server/webpack.config.js
@@ -3,7 +3,7 @@
module.exports = {
context: __dirname + '/public',
- entry: './js/unleash',
+ entry: './js/app',
output: {
path: __dirname + '/public/js',