diff --git a/lib/featureDb.js b/lib/featureDb.js index c64ae80c1a..100c13aeb0 100644 --- a/lib/featureDb.js +++ b/lib/featureDb.js @@ -40,10 +40,11 @@ function getFeature(name) { function getArchivedFeatures() { return knex - .select(['name', 'description']) + .select(FEATURE_COLUMNS) .from('features') .where({archived: 1}) - .orderBy('name', 'asc'); + .orderBy('name', 'asc') + .map(rowToFeature); } @@ -115,4 +116,3 @@ module.exports = { _createFeature: createFeature, // visible for testing _updateFeature: updateFeature // visible for testing }; - diff --git a/package.json b/package.json index 575dc19143..e3cfb50614 100644 --- a/package.json +++ b/package.json @@ -46,11 +46,13 @@ "jsx-loader": "0.12.2", "jsxhint": "0.4.15", "knex": "^0.7.3", + "lodash": "^3.5.0", "log4js": "0.6.21", "moment": "^2.9.0", "nconf": "0.6.9", "pg": "3.6.1", "react": "^0.12.0", + "reflux": "^0.2.5", "reqwest": "^1.1.4", "webpack": "1.4.15", "webpack-dev-middleware": "^1.0.11" @@ -73,7 +75,8 @@ "jest": { "scriptPreprocessor": "/jest-preprocessor.js", "unmockedModulePathPatterns": [ - "/node_modules/react" + "/node_modules/react", + "/node_modules/reflux" ], "moduleFileExtensions": [ "jsx", diff --git a/public/index.html b/public/index.html index 313610169b..f3e1670009 100644 --- a/public/index.html +++ b/public/index.html @@ -11,7 +11,7 @@ -
Loading...
+
diff --git a/public/js/UnleashApp.jsx b/public/js/UnleashApp.jsx new file mode 100644 index 0000000000..e15b0d1256 --- /dev/null +++ b/public/js/UnleashApp.jsx @@ -0,0 +1,57 @@ +var React = require('react'); +var TabView = require('./components/TabView'); +var Menu = require('./components/Menu'); +var UserStore = require('./stores/UserStore'); +var ErrorMessages = require('./components/ErrorMessages'); +var initalizer = require('./stores/initalizer'); +var LogEntriesComponent = React.createFactory(require('./components/log/LogEntriesComponent')); +var FeatureTogglesComponent = React.createFactory(require('./components/feature/FeatureTogglesComponent')); +var StrategiesComponent = React.createFactory(require('./components/strategy/StrategiesComponent')); +var ArchiveFeatureComponent = React.createFactory(require('./components/feature/ArchiveFeatureComponent')); + +UserStore.init(); + +var tabPanes = [ +{ + name: 'Feature Toggles', + slug: 'feature-toggles', + content: new FeatureTogglesComponent({}) +}, +{ + name: 'Strategies', + slug: 'strategies', + content: new StrategiesComponent({}) +}, +{ + name: "Log", + slug: 'log', + content: new LogEntriesComponent({}) +}, +{ + name: "Archive", + slug: 'archive', + content: new ArchiveFeatureComponent({}) +} +]; + + +var UnleashApp = React.createClass({ + componentWillMount: function() { + initalizer(30); + }, + render: function () { + return ( +
+ +
+
+ + +
+
+
+ ); + } +}); + +module.exports = UnleashApp; diff --git a/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js b/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js index 9391e19fae..a219abd55c 100644 --- a/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js +++ b/public/js/__tests__/components/feature/ArchiveFeatureComponent-test.js @@ -1,32 +1,28 @@ jest.dontMock("../../../components/feature/ArchiveFeatureComponent"); +jest.mock("../../../stores/FeatureToggleServerFacade"); +jest.autoMockOff(); var React = require("react/addons"); var TestUtils = React.addons.TestUtils; -var FeatureArchive = require("../../../components/feature/ArchiveFeatureComponent"); -var FeatureStore = require("../../../stores/FeatureStore"); +var FeatureArchive = require("../../../components/feature/ArchiveFeatureComponent"); +var Server = require("../../../stores/FeatureToggleServerFacade"); +var FeatureToggleStore = require("../../../stores/ArchivedToggleStore"); describe("FeatureForm", function () { var Component; beforeEach(function() { - FeatureStore.getArchivedFeatures.mockImplementation(function() { - return { - then: function (callback) { - return callback({ - features: [ - { name: "featureX" }, - { name: "featureY" } - ] - }); - } - }; - }); - FeatureStore.reviveFeature.mockImplementation(function() { - return { - then: function (callback) {return callback();} - }; + var archivedToggles = [ + { name: "featureX" }, + { name: "featureY" } + ]; + + Server.getArchivedFeatures.mockImplementation(function(cb) { + cb(null, archivedToggles); }); - Component = TestUtils .renderIntoDocument(); + FeatureToggleStore.initStore(archivedToggles); + + Component = TestUtils.renderIntoDocument(); }); afterEach(function() { @@ -35,17 +31,15 @@ describe("FeatureForm", function () { it("should render two archived features", function() { var rows = Component.getDOMNode().querySelectorAll("tbody tr"); + expect(rows.length).toEqual(2); }); it("should revive archived feature toggle", function() { - var button = Component.getDOMNode().querySelector("tbody button"); - TestUtils.Simulate.click(button); - var rows = Component.getDOMNode().querySelectorAll("tbody tr"); + var button = Component.getDOMNode().querySelector("tbody button"); + TestUtils.Simulate.click(button); - expect(rows.length).toEqual(1); - expect(FeatureStore.reviveFeature).toBeCalledWith({ - name: "featureX" - }); + jest.runAllTimers(); + expect(Server.reviveFeature).toBeCalled(); }); -}); \ No newline at end of file +}); diff --git a/public/js/__tests__/components/feature/FeatureForm-test.js b/public/js/__tests__/components/feature/FeatureForm-test.js index d42a0c9639..93fe28f78b 100644 --- a/public/js/__tests__/components/feature/FeatureForm-test.js +++ b/public/js/__tests__/components/feature/FeatureForm-test.js @@ -3,31 +3,19 @@ jest.dontMock("../../../components/feature/FeatureForm"); var React = require("react/addons"); var TestUtils = React.addons.TestUtils; var FeatureForm = require("../../../components/feature/FeatureForm"); -var strategyStore = require("../../../stores/StrategyStore"); describe("FeatureForm", function () { var Component; - beforeEach(function() { - strategyStore.getStrategies.mockImplementation(function() { - return { - then: function (callback) { - return callback({ - strategies: [ - { name: "default"} - ] - }); - } - }; - }); - }); - + var strategies = [ + { name: "default"} + ]; afterEach(function() { React.unmountComponentAtNode(document.body); }); describe("new", function () { it("should render empty form", function() { - Component = TestUtils .renderIntoDocument(); + Component = TestUtils .renderIntoDocument(); var name = Component.getDOMNode().querySelectorAll("input"); expect(name[0].value).toEqual(undefined); }); @@ -37,11 +25,11 @@ describe("FeatureForm", function () { var feature = {name: "Test", strategy: "unknown"}; it("should show unknown strategy as deleted", function () { - Component = TestUtils .renderIntoDocument(); + Component = TestUtils .renderIntoDocument(); var strategySelect = Component.getDOMNode().querySelector("select"); expect(strategySelect.value).toEqual("unknown (deleted)"); }); }); -}); \ No newline at end of file +}); diff --git a/public/js/__tests__/components/feature/FeatureList-test.js b/public/js/__tests__/components/feature/FeatureList-test.js index b86230a24b..7c970c66fd 100644 --- a/public/js/__tests__/components/feature/FeatureList-test.js +++ b/public/js/__tests__/components/feature/FeatureList-test.js @@ -13,7 +13,10 @@ describe("FeatureList", function () { { name: "featureX", strategy: "other" }, { name: "group.featureY", strategy: "default" } ]; - Component = TestUtils .renderIntoDocument(); + var strategies=[ + { name: "default"} + ]; + Component = TestUtils .renderIntoDocument(); }); afterEach(function() { @@ -52,4 +55,4 @@ describe("FeatureList", function () { expect(features[0].textContent).toMatch(searchString); }); -}); \ No newline at end of file +}); diff --git a/public/js/__tests__/stores/FeatureToggleStore-test.js b/public/js/__tests__/stores/FeatureToggleStore-test.js new file mode 100644 index 0000000000..ba2b62ff8d --- /dev/null +++ b/public/js/__tests__/stores/FeatureToggleStore-test.js @@ -0,0 +1,81 @@ +jest.autoMockOff() +jest.dontMock('../../stores/FeatureToggleActions'); +jest.dontMock('../../stores/FeatureToggleStore'); + +describe('FeatureToggleStore', function() { + + var Actions, Store, toggles; + + beforeEach(function() { + Actions = require('../../stores/FeatureToggleActions'); + Store = require('../../stores/FeatureToggleStore'); + toggles = [ + {name: "app.feature", enabled: true, strategy: "default"} + ]; + }); + + it('should be an empty store', function() { + expect(Store.getFeatureToggles().length).toBe(0); + }); + + it('should inititialize the store', function() { + Actions.init.completed(toggles); + + jest.runAllTimers(); + expect(Store.getFeatureToggles().length).toBe(1); + expect(Store.getFeatureToggles()[0].name).toEqual("app.feature"); + }); + + it('should add a another toggle', function() { + Actions.init.completed(toggles); + + var newToggle = {name: "app.featureB", enabled: true, strategy: "default"}; + + Actions.create.completed(newToggle); + + jest.runAllTimers(); + expect(Store.getFeatureToggles().length).toBe(2); + expect(Store.getFeatureToggles()[1].name).toEqual("app.featureB"); + }); + + it('should archive toggle', function() { + Actions.init.completed(toggles); + + Actions.archive.completed(toggles[0]); + + jest.runAllTimers(); + expect(Store.getFeatureToggles().length).toBe(0); + }); + + + it('should keep toggles in sorted order', function() { + Actions.init.completed([ + {name: "A"}, + {name: "B"}, + {name: "C"} + ]); + + Actions.create.completed({name: "AA"}); + + jest.runAllTimers(); + expect(Store.getFeatureToggles()[0].name).toEqual("A"); + expect(Store.getFeatureToggles()[1].name).toEqual("AA"); + expect(Store.getFeatureToggles()[3].name).toEqual("C"); + }); + + it('should update toggle', function() { + Actions.init.completed(toggles); + var toggle = toggles[0]; + + toggle.enabled = false; + Actions.update.completed(toggle); + + + jest.runAllTimers(); + expect(Store.getFeatureToggles()[0].enabled).toEqual(false); + }); + + + + +}); diff --git a/public/js/app.jsx b/public/js/app.jsx index 22a8ea2ee3..4cf42046b0 100644 --- a/public/js/app.jsx +++ b/public/js/app.jsx @@ -1,46 +1,3 @@ -var React = require('react'); -var TabView = require('./components/TabView'); -var Menu = require('./components/Menu'); -var UserStore = require('./stores/UserStore'); -var LogEntriesComponent = React.createFactory(require('./components/log/LogEntriesComponent')); -var FeatureTogglesComponent = React.createFactory(require('./components/feature/FeatureTogglesComponent')); -var StrategiesComponent = React.createFactory(require('./components/strategy/StrategiesComponent')); -var ArchiveFeatureComponent = React.createFactory(require('./components/feature/ArchiveFeatureComponent')); - -UserStore.init(); - -var tabPanes = [ - { - name: 'Feature Toggles', - slug: 'feature-toggles', - content: new FeatureTogglesComponent({pollInterval: 5000}) - }, - { - name: 'Strategies', - slug: 'strategies', - content: new StrategiesComponent({}) - }, - { - name: "Log", - slug: 'log', - content: new LogEntriesComponent({}) - }, - { - name: "Archive", - slug: 'archive', - content: new ArchiveFeatureComponent({}) - } -]; - -React.render( -
- -
-
- -
-
-
- , - document.getElementById('content') -); +var React = require('react'); +var UnleashApp = require('./UnleashApp'); +React.render(, document.getElementById('content')); diff --git a/public/js/components/ErrorMessages.jsx b/public/js/components/ErrorMessages.jsx index cf3b0ff58b..3594583876 100644 --- a/public/js/components/ErrorMessages.jsx +++ b/public/js/components/ErrorMessages.jsx @@ -1,37 +1,38 @@ -var React = require('react'); +var React = require('react'); +var Ui = require('./ErrorMessages.ui'); +var ErrorStore = require('../stores/ErrorStore'); +var ErrorActions = require('../stores/ErrorActions'); var ErrorMessages = React.createClass({ + getInitialState: function() { + return { + errors: ErrorStore.getErrors() + }; + }, + + onStoreChange: function() { + this.setState({ + errors: ErrorStore.getErrors() + }); + }, + + componentDidMount: function() { + this.unsubscribe = ErrorStore.listen(this.onStoreChange); + }, + + componentWillUnmount: function() { + this.unsubscribe(); + }, + + onClearErrors: function() { + ErrorActions.clear(); + }, + render: function() { - if (!this.props.errors.length) { - return
; - } - - var errorNodes = this.props.errors.map(function(e, i) { - return (
  • {e}
  • ); - }); - return ( -
    -
    -
    -
    -
    -
    - - -
    -
    -
      {errorNodes}
    -
    -
    -
    -
    -
    -
    + ); } }); module.exports = ErrorMessages; - diff --git a/public/js/components/ErrorMessages.ui.jsx b/public/js/components/ErrorMessages.ui.jsx new file mode 100644 index 0000000000..7a93bcb899 --- /dev/null +++ b/public/js/components/ErrorMessages.ui.jsx @@ -0,0 +1,36 @@ +var React = require('react'); + +var ErrorMessages = React.createClass({ + render: function() { + if (!this.props.errors.length) { + return
    ; + } + + var errorNodes = this.props.errors.map(function(e, i) { + return (
  • {e}
  • ); + }); + + return ( +
    +
    +
    +
    +
    +
    + + +
    +
    +
      {errorNodes}
    +
    +
    +
    +
    +
    +
    + ); + } +}); + +module.exports = ErrorMessages; diff --git a/public/js/components/feature/ArchiveFeatureComponent.jsx b/public/js/components/feature/ArchiveFeatureComponent.jsx index 7bd403eb75..3730d0d0cb 100644 --- a/public/js/components/feature/ArchiveFeatureComponent.jsx +++ b/public/js/components/feature/ArchiveFeatureComponent.jsx @@ -1,28 +1,30 @@ -var React = require("react"); -var FeatureStore = require('../../stores/FeatureStore'); +var React = require("react"); +var FeatureActions = require('../../stores/FeatureToggleActions'); +var FeatureToggleStore = require('../../stores/ArchivedToggleStore'); var ArchiveFeatureComponent = React.createClass({ getInitialState: function() { return { - archivedFeatures: [] + archivedFeatures: FeatureToggleStore.getArchivedToggles() }; }, - removeToggleFromState: function(item) { - var updatedArchive = this.state.archivedFeatures.filter(function(f) { - return f.name !== item.name; + onStoreChange: function() { + this.setState({ + archivedFeatures: FeatureToggleStore.getArchivedToggles() }); - this.setState({archivedFeatures: updatedArchive}); }, - onRevive: function( item) { - FeatureStore.reviveFeature(item).then(this.removeToggleFromState.bind(this, item)); + componentDidMount: function() { + this.unsubscribe = FeatureToggleStore.listen(this.onStoreChange); }, - componentDidMount: function () { - FeatureStore.getArchivedFeatures().then(function(data) { - this.setState({archivedFeatures: data.features}); - }.bind(this)) + componentWillUnmount: function() { + this.unsubscribe(); + }, + + onRevive: function(item) { + FeatureActions.revive.triggerPromise(item); }, render: function () { @@ -41,7 +43,7 @@ var ArchiveFeatureComponent = React.createClass({
    - ); + ); }, renderArchivedItem: function(f) { @@ -49,16 +51,15 @@ var ArchiveFeatureComponent = React.createClass({ {f.name}
    - {f.description} - - - - - - ); + {f.description} + + + + + ); } }); -module.exports = ArchiveFeatureComponent; \ No newline at end of file +module.exports = ArchiveFeatureComponent; diff --git a/public/js/components/feature/Feature.jsx b/public/js/components/feature/Feature.jsx index bad6ccd442..40b93747e3 100644 --- a/public/js/components/feature/Feature.jsx +++ b/public/js/components/feature/Feature.jsx @@ -44,7 +44,11 @@ var Feature = React.createClass({ return ( - + ); @@ -110,4 +114,4 @@ var Feature = React.createClass({ }); -module.exports = Feature; \ No newline at end of file +module.exports = Feature; diff --git a/public/js/components/feature/FeatureForm.jsx b/public/js/components/feature/FeatureForm.jsx index 26cb524186..87898da5dc 100644 --- a/public/js/components/feature/FeatureForm.jsx +++ b/public/js/components/feature/FeatureForm.jsx @@ -1,22 +1,16 @@ var React = require('react'); var TextInput = require('../form/TextInput'); -var strategyStore = require('../../stores/StrategyStore'); var FeatureForm = React.createClass({ getInitialState: function() { - return { - strategyOptions: [], - requiredParams: [], - currentStrategy: this.props.feature ? this.props.feature.strategy : "default" - }; + return { + strategyOptions: this.props.strategies, + requiredParams: [], + currentStrategy: this.props.feature ? this.props.feature.strategy : "default" + }; }, componentWillMount: function() { - strategyStore.getStrategies().then(this.handleStrategyResponse); - }, - - handleStrategyResponse: function(response) { - this.setState({strategyOptions: response.strategies}); if(this.props.feature) { this.setSelectedStrategy(this.props.feature.strategy); } @@ -41,9 +35,7 @@ var FeatureForm = React.createClass({ })[0]; if(selectedStrategy) { - if(selectedStrategy.parametersTemplate) { - this.setStrategyParams(selectedStrategy); - } + this.setStrategyParams(selectedStrategy); } else { var updatedStrategyName = name + " (deleted)"; this.setState({ @@ -64,9 +56,9 @@ var FeatureForm = React.createClass({ render: function() { var feature = this.props.feature || { - name: '', - strategy: 'default', - enabled: false + name: '', + strategy: 'default', + enabled: false }; var idPrefix = this.props.feature ? this.props.feature.name : 'new'; @@ -131,9 +123,9 @@ var FeatureForm = React.createClass({ renderStrategyOptions: function() { return this.state.strategyOptions.map(function(strategy) { return ( - + ); }.bind(this)); }, @@ -178,4 +170,4 @@ var FeatureForm = React.createClass({ } }); -module.exports = FeatureForm; \ No newline at end of file +module.exports = FeatureForm; diff --git a/public/js/components/feature/FeatureList.jsx b/public/js/components/feature/FeatureList.jsx index f62a9583bf..1e407aae83 100644 --- a/public/js/components/feature/FeatureList.jsx +++ b/public/js/components/feature/FeatureList.jsx @@ -5,7 +5,8 @@ var noop = function() {}; var FeatureList = React.createClass({ propTypes: { - features: React.PropTypes.array.isRequired + features: React.PropTypes.array.isRequired, + strategies: React.PropTypes.array.isRequired }, getDefaultProps: function() { @@ -47,7 +48,9 @@ var FeatureList = React.createClass({ key={feature.name} feature={feature} onChange={this.props.onFeatureChanged} - onArchive={this.props.onFeatureArchive}/> + onArchive={this.props.onFeatureArchive} + strategies={this.props.strategies} + /> ); }.bind(this)); @@ -83,4 +86,4 @@ var FeatureList = React.createClass({ } }); -module.exports = FeatureList; \ No newline at end of file +module.exports = FeatureList; diff --git a/public/js/components/feature/FeatureTogglesComponent.jsx b/public/js/components/feature/FeatureTogglesComponent.jsx index be50960d3d..683d362d3a 100644 --- a/public/js/components/feature/FeatureTogglesComponent.jsx +++ b/public/js/components/feature/FeatureTogglesComponent.jsx @@ -1,132 +1,56 @@ -var React = require('react'); -var Timer = require('../../utils/Timer'); -var ErrorMessages = require('../ErrorMessages'); -var FeatureList = require('./FeatureList'); -var FeatureForm = require('./FeatureForm'); -var FeatureStore = require('../../stores/FeatureStore'); +var React = require('react'); +var FeatureList = require('./FeatureList'); +var FeatureForm = require('./FeatureForm'); +var FeatureActions = require('../../stores/FeatureToggleActions'); +var ErrorActions = require('../../stores/ErrorActions'); +var FeatureToggleStore = require('../../stores/FeatureToggleStore'); +var StrategyStore = require('../../stores/StrategyStore'); var FeatureTogglesComponent = React.createClass({ getInitialState: function() { return { - features: [], - errors: [], - createView: false, - featurePoller: new Timer(this.loadFeaturesFromServer, this.props.pollInterval) + features: FeatureToggleStore.getFeatureToggles(), + createView: false }; }, - componentDidMount: function () { - this.loadFeaturesFromServer(); - this.startFeaturePoller(); + onFeatureToggleChange: function() { + this.setState({ + features: FeatureToggleStore.getFeatureToggles() + }); }, - - componentWillUnmount: function () { - this.stopFeaturePoller(); + componentDidMount: function() { + this.unsubscribe = FeatureToggleStore.listen(this.onFeatureToggleChange); }, - - loadFeaturesFromServer: function () { - FeatureStore.getFeatures().then(this.setFeatures).catch(this.handleError); - }, - - setFeatures: function (data) { - this.setState({features: data.features}); - }, - - handleError: function (error) { - if (this.isClientError(error)) { - var errors = JSON.parse(error.responseText) - errors.forEach(function(e) { this.addError(e.msg); }.bind(this)) - } else if (error.status === 0) { - this.addError("server unreachable"); - } else { - this.addError(error); - } - - this.forceUpdate(); + componentWillUnmount: function() { + this.unsubscribe(); }, updateFeature: function (feature) { - this.stopFeaturePoller(); - - FeatureStore - .updateFeature(feature) - .then(this.startFeaturePoller) - .catch(this.handleError); + FeatureActions.update.triggerPromise(feature); }, archiveFeature: function (feature) { - var updatedFeatures = this.state.features.filter(function(item) { - return item.name !== feature.name; - }); - - FeatureStore - .archiveFeature(feature) - .then(function() { - this.setState({features: updatedFeatures}) - }.bind(this)) - .catch(this.handleError); - }, - - startFeaturePoller: function () { - this.state.featurePoller.start(); - }, - - stopFeaturePoller: function () { - if (this.state.featurePoller != null) { - this.state.featurePoller.stop(); - } + FeatureActions.archive.triggerPromise(feature); }, createFeature: function (feature) { - this.stopFeaturePoller(); - - FeatureStore - .createFeature(feature) - .then(this.cancelNewFeature) - .then(this.startFeaturePoller) - .catch(this.handleError); + FeatureActions.create.triggerPromise(feature) + .then(this.cancelNewFeature); }, newFeature: function() { this.setState({createView: true}); }, - cancelNewFeature: function (feature) { + cancelNewFeature: function () { this.setState({createView: false}); - }, - - clearErrors: function() { - this.setState({errors: []}); - }, - - addError: function(msg) { - if (this.state.errors[this.state.errors.length - 1] !== msg) { - this.state.errors.push(msg); - } - }, - - isClientError: function(error) { - try { - return error.status >= 400 && - error.status < 500 && - JSON.parse(error.responseText); - } catch (e) { - if (e instanceof SyntaxError) { - // fall through; - } else { - throw e; - } - } - - return false; + ErrorActions.clear(); }, render: function() { return (
    - {this.state.createView ? this.renderCreateView() : this.renderCreateButton()} @@ -136,18 +60,22 @@ var FeatureTogglesComponent = React.createClass({ onFeatureArchive={this.archiveFeature} onFeatureSubmit={this.createFeature} onFeatureCancel={this.cancelNewFeature} - onNewFeature={this.newFeature} /> + onNewFeature={this.newFeature} + strategies={StrategyStore.getStrategies()} />
    ); }, renderCreateView: function() { - return + return ; }, renderCreateButton: function() { - return + return ; } }); -module.exports = FeatureTogglesComponent; \ No newline at end of file +module.exports = FeatureTogglesComponent; diff --git a/public/js/components/log/LogEntriesComponent.jsx b/public/js/components/log/LogEntriesComponent.jsx index 45a943d7f6..71d5992e53 100644 --- a/public/js/components/log/LogEntriesComponent.jsx +++ b/public/js/components/log/LogEntriesComponent.jsx @@ -1,14 +1,13 @@ -var React = require('react'), - LogEntryList = require('./LogEntryList'), - eventStore = require('../../stores/EventStore'), - ErrorMessages = require('../ErrorMessages'); +var React = require('react'); +var LogEntryList = require('./LogEntryList'); +var eventStore = require('../../stores/EventStore'); +var ErrorActions = require('../../stores/ErrorActions'); var LogEntriesComponent = React.createClass({ getInitialState: function() { return { createView: false, - events: [], - errors: [] + events: [] }; }, @@ -19,29 +18,17 @@ var LogEntriesComponent = React.createClass({ }, initError: function() { - this.onError("Could not load events from server"); - }, - - clearErrors: function() { - this.setState({errors: []}); - }, - - onError: function(error) { - var errors = this.state.errors.concat([error]); - this.setState({errors: errors}); + ErrorActions.error("Could not load events from server"); }, render: function() { return (
    - -
    -
    ); }, }); -module.exports = LogEntriesComponent; \ No newline at end of file +module.exports = LogEntriesComponent; diff --git a/public/js/components/log/LogEntryList.jsx b/public/js/components/log/LogEntryList.jsx index 697f71f7d9..372f8f9aef 100644 --- a/public/js/components/log/LogEntryList.jsx +++ b/public/js/components/log/LogEntryList.jsx @@ -1,5 +1,5 @@ var React = require('react'), - LogEntry = require('./LogEntry'); +LogEntry = require('./LogEntry'); var LogEntryList = React.createClass({ propTypes: { @@ -9,7 +9,7 @@ var LogEntryList = React.createClass({ getInitialState: function() { return { showFullEvents: false - } + }; }, render: function() { @@ -19,9 +19,14 @@ var LogEntryList = React.createClass({ return (
    -
    - ); + ); }, - toggleFullEvents: function(e) { + toggleFullEvents: function() { this.setState({showFullEvents: !this.state.showFullEvents}); } }); -module.exports = LogEntryList; \ No newline at end of file +module.exports = LogEntryList; diff --git a/public/js/components/strategy/StrategiesComponent.jsx b/public/js/components/strategy/StrategiesComponent.jsx index 0df00d17de..e913f6c03b 100644 --- a/public/js/components/strategy/StrategiesComponent.jsx +++ b/public/js/components/strategy/StrategiesComponent.jsx @@ -1,42 +1,27 @@ -var React = require('react'), - StrategyList = require('./StrategyList'), - StrategyForm = require('./StrategyForm'), - strategyStore = require('../../stores/StrategyStore'), - ErrorMessages = require('../ErrorMessages'); +var React = require('react'); +var StrategyList = require('./StrategyList'); +var StrategyForm = require('./StrategyForm'); +var StrategyStore = require('../../stores/StrategyStore'); +var StrategyActions = require('../../stores/StrategyActions'); var StrategiesComponent = React.createClass({ getInitialState: function() { return { createView: false, - strategies: [], - errors: [] + strategies: StrategyStore.getStrategies() }; }, - componentDidMount: function () { - this.fetchStrategies(); + onStoreChange: function() { + this.setState({ + strategies: StrategyStore.getStrategies() + }); }, - - fetchStrategies: function(res) { - strategyStore.getStrategies() - .then(function(res) { - this.setState({strategies: res.strategies}) - }.bind(this)) - .catch(this.initError); - + componentDidMount: function() { + this.unsubscribe = StrategyStore.listen(this.onStoreChange); }, - - initError: function() { - this.onError("Could not load inital strategies from server"); - }, - - clearErrors: function() { - this.setState({errors: []}); - }, - - onError: function(error) { - var errors = this.state.errors.concat([error]); - this.setState({errors: errors}); + componentWillUnmount: function() { + this.unsubscribe(); }, onNewStrategy: function() { @@ -48,51 +33,42 @@ var StrategiesComponent = React.createClass({ }, onSave: function(strategy) { - function handleSuccess() { - var strategies = this.state.strategies.concat([strategy]); - - this.setState({ - createView: false, - strategies: strategies - }); - - console.log("Saved strategy: ", strategy); - } - - strategyStore.createStrategy(strategy) - .then(handleSuccess.bind(this)) - .catch(this.onError); + StrategyActions.create.triggerPromise(strategy) + .then(this.onCancelNewStrategy); }, onRemove: function(strategy) { - strategyStore.removeStrategy(strategy) - .then(this.fetchStrategies) - .catch(this.onError); + StrategyActions.remove.triggerPromise(strategy); }, render: function() { return (
    - - - {this.state.createView ? this.renderCreateView() : this.renderCreateButton()} - + {this.state.createView ? + this.renderCreateView() : this.renderCreateButton()}
    - - +
    - ); + ); }, renderCreateView: function() { - return () - }, - - renderCreateButton: function() { return ( - - ); - } -}); + ); + }, -module.exports = StrategiesComponent; \ No newline at end of file + renderCreateButton: function() { + return ( + + ); + } + }); + + module.exports = StrategiesComponent; diff --git a/public/js/stores/ArchivedToggleStore.js b/public/js/stores/ArchivedToggleStore.js new file mode 100644 index 0000000000..607f24ca31 --- /dev/null +++ b/public/js/stores/ArchivedToggleStore.js @@ -0,0 +1,48 @@ +var Reflux = require('reflux'); +var FeatureActions = require('./FeatureToggleActions'); +var filter = require('lodash/collection/filter'); +var sortBy = require('lodash/collection/sortBy'); + +var _archivedToggles = []; + +// Creates a DataStore +var FeatureStore = Reflux.createStore({ + + // Initial setup + init: function() { + this.listenTo(FeatureActions.initArchive.completed, this.onInit); + this.listenTo(FeatureActions.archive.completed, this.onArchive); + this.listenTo(FeatureActions.revive.completed, this.onRevive); + + }, + + onInit: function(toggles) { + _archivedToggles = toggles; + this.trigger(); + }, + + onArchive: function(feature) { + var toggles = _archivedToggles.concat([feature]); + _archivedToggles = sortBy(toggles, 'name'); + this.trigger(); + }, + + onRevive: function(item) { + var newStore = filter(_archivedToggles, function(f) { + return f.name !== item.name; + }); + + _archivedToggles = sortBy(newStore, 'name'); + this.trigger(); + }, + + getArchivedToggles: function() { + return _archivedToggles; + }, + + initStore: function(archivedToggles) { + _archivedToggles = archivedToggles; + } +}); + +module.exports = FeatureStore; diff --git a/public/js/stores/ErrorActions.js b/public/js/stores/ErrorActions.js new file mode 100644 index 0000000000..f6e4aebc4b --- /dev/null +++ b/public/js/stores/ErrorActions.js @@ -0,0 +1,8 @@ +var Reflux = require('reflux'); + +var ErrorActions = Reflux.createActions([ + "clear", + "error" +]); + +module.exports = ErrorActions; diff --git a/public/js/stores/ErrorStore.js b/public/js/stores/ErrorStore.js new file mode 100644 index 0000000000..f3734663a8 --- /dev/null +++ b/public/js/stores/ErrorStore.js @@ -0,0 +1,66 @@ +var Reflux = require('reflux'); +var FeatureActions = require('./FeatureToggleActions'); +var ErrorActions = require('./ErrorActions'); + +// Creates a DataStore +var FeatureStore = Reflux.createStore({ + // Initial setup + init: function() { + this.listenTo(FeatureActions.create.failed, this.onError); + this.listenTo(FeatureActions.init.failed, this.onError); + this.listenTo(FeatureActions.update.failed, this.onError); + this.listenTo(FeatureActions.archive.failed, this.onError); + this.listenTo(FeatureActions.revive.failed, this.onError); + this.listenTo(ErrorActions.error, this.onError); + this.listenTo(ErrorActions.clear, this.onClear); + this.errors = []; + }, + + onError: function (error) { + if (this.isClientError(error)) { + var errors = JSON.parse(error.responseText); + errors.forEach(function(e) { this.addError(e.msg); }.bind(this)); + } else if (error.status === 0) { + this.addError("server unreachable"); + } else { + this.addError(error); + } + }, + + onClear: function() { + this.errors = []; + this.trigger([]); + }, + + addError: function(msg) { + var errors = this.errors; + if (errors[errors.length - 1] !== msg) { + errors.push(msg); + this.errors = errors; + this.trigger(errors); + } + }, + + isClientError: function(error) { + try { + return error.status >= 400 && + error.status < 500 && + JSON.parse(error.responseText); + } catch (e) { + if (e instanceof SyntaxError) { + // fall through; + console.log("Syntax error!"); + } else { + throw e; + } + } + + return false; + }, + + getErrors: function() { + return this.errors; + } +}); + +module.exports = FeatureStore; diff --git a/public/js/stores/FeatureStore.js b/public/js/stores/FeatureStore.js deleted file mode 100644 index 333ab6bdb6..0000000000 --- a/public/js/stores/FeatureStore.js +++ /dev/null @@ -1,62 +0,0 @@ -var reqwest = require('reqwest'); - -var TYPE = 'json'; -var CONTENT_TYPE = 'application/json'; - -FeatureStore = { - updateFeature: function (feature) { - return reqwest({ - url: 'features/' + feature.name, - method: 'put', - type: TYPE, - contentType: CONTENT_TYPE, - data: JSON.stringify(feature) - }); - }, - - createFeature: function (feature) { - return reqwest({ - url: 'features', - method: 'post', - type: TYPE, - contentType: CONTENT_TYPE, - data: JSON.stringify(feature) - }); - }, - - archiveFeature: function (feature) { - return reqwest({ - url: 'features/' + feature.name, - method: 'delete', - type: TYPE - }); - }, - - getFeatures: function () { - return reqwest({ - url: 'features', - method: 'get', - type: TYPE - }); - }, - - getArchivedFeatures: function () { - return reqwest({ - url: 'archive/features', - method: 'get', - type: TYPE - }); - }, - - reviveFeature: function (feature) { - return reqwest({ - url: 'archive/revive', - method: 'post', - type: TYPE, - contentType: CONTENT_TYPE, - data: JSON.stringify(feature) - }); - } -}; - -module.exports = FeatureStore; diff --git a/public/js/stores/FeatureToggleActions.js b/public/js/stores/FeatureToggleActions.js new file mode 100644 index 0000000000..f41f071057 --- /dev/null +++ b/public/js/stores/FeatureToggleActions.js @@ -0,0 +1,73 @@ +var Reflux = require("reflux"); +var Server = require('./FeatureToggleServerFacade'); + +var FeatureToggleActions = Reflux.createActions({ + 'init': { asyncResult: true }, + 'initArchive':{ asyncResult: true }, + 'create': { asyncResult: true }, + 'update': { asyncResult: true }, + 'archive': { asyncResult: true }, + 'revive': { asyncResult: true } +}); + +FeatureToggleActions.init.listen(function(){ + Server.getFeatures(function(error, features) { + if(error) { + this.failed(error); + } else { + this.completed(features); + } + }.bind(this)); +}); + +FeatureToggleActions.initArchive.listen(function(){ + Server.getArchivedFeatures(function(error, archivedToggles) { + if(error) { + this.failed(error); + } else { + this.completed(archivedToggles); + } + }.bind(this)); +}); + +FeatureToggleActions.create.listen(function(feature){ + Server.createFeature(feature, function(error) { + if(error) { + this.failed(error); + } else { + this.completed(feature); + } + }.bind(this)); +}); + +FeatureToggleActions.update.listen(function(feature){ + Server.updateFeature(feature, function(error) { + if(error) { + this.failed(error); + } else { + this.completed(feature); + } + }.bind(this)); +}); + +FeatureToggleActions.archive.listen(function(feature){ + Server.archiveFeature(feature, function(error) { + if(error) { + this.failed(error); + } else { + this.completed(feature); + } + }.bind(this)); +}); + +FeatureToggleActions.revive.listen(function(feature){ + Server.reviveFeature(feature, function(error) { + if(error) { + this.failed(error); + } else { + this.completed(feature); + } + }.bind(this)); +}); + +module.exports = FeatureToggleActions; diff --git a/public/js/stores/FeatureToggleServerFacade.js b/public/js/stores/FeatureToggleServerFacade.js new file mode 100644 index 0000000000..20b2452b20 --- /dev/null +++ b/public/js/stores/FeatureToggleServerFacade.js @@ -0,0 +1,98 @@ +var reqwest = require('reqwest'); + +var TYPE = 'json'; +var CONTENT_TYPE = 'application/json'; + +var FeatureToggleServerFacade = { + updateFeature: function (feature, cb) { + reqwest({ + url: 'features/' + feature.name, + method: 'put', + type: TYPE, + contentType: CONTENT_TYPE, + data: JSON.stringify(feature), + error: function(error) { + cb(error); + }, + success: function() { + cb(); + } + }); + }, + + createFeature: function (feature, cb) { + reqwest({ + url: 'features', + method: 'post', + type: TYPE, + contentType: CONTENT_TYPE, + data: JSON.stringify(feature), + error: function(error) { + cb(error); + }, + success: function() { + cb(); + } + }); + }, + + archiveFeature: function(feature, cb) { + reqwest({ + url: 'features/' + feature.name, + method: 'delete', + type: TYPE, + error: function(error) { + cb(error); + }, + success: function() { + cb(); + } + }); + }, + + getFeatures: function(cb) { + reqwest({ + url: 'features', + method: 'get', + type: TYPE, + error: function(error) { + cb(error); + }, + success: function(data) { + cb(null, data.features); + } + }); + }, + + getArchivedFeatures: function(cb) { + reqwest({ + url: 'archive/features', + method: 'get', + type: TYPE, + error: function(error) { + cb(error); + }, + success: function(data) { + cb(null, data.features); + } + }); + }, + + reviveFeature: function (feature, cb) { + reqwest({ + url: 'archive/revive', + method: 'post', + type: TYPE, + contentType: CONTENT_TYPE, + data: JSON.stringify(feature), + error: function(error) { + cb(error); + }, + success: function() { + cb(); + } + }); + } +}; + +module.exports = FeatureToggleServerFacade; diff --git a/public/js/stores/FeatureToggleStore.js b/public/js/stores/FeatureToggleStore.js new file mode 100644 index 0000000000..383e0a5274 --- /dev/null +++ b/public/js/stores/FeatureToggleStore.js @@ -0,0 +1,57 @@ +var Reflux = require('reflux'); +var FeatureActions = require('./FeatureToggleActions'); +var filter = require('lodash/collection/filter'); +var sortBy = require('lodash/collection/sortBy'); +var findIndex = require('lodash/array/findIndex'); + +var _featureToggles = []; + +var FeatureStore = Reflux.createStore({ + + // Initial setup + init: function() { + this.listenTo(FeatureActions.init.completed, this.setToggles); + this.listenTo(FeatureActions.create.completed, this.onCreate); + this.listenTo(FeatureActions.update.completed, this.onUpdate); + this.listenTo(FeatureActions.archive.completed, this.onArchive); + this.listenTo(FeatureActions.revive.completed, this.onRevive); + }, + + onCreate: function(feature) { + this.setToggles([feature].concat(_featureToggles)); + }, + + setToggles: function(toggles) { + _featureToggles = sortBy(toggles, 'name'); + this.trigger(); + }, + + onUpdate: function(feature) { + var idx = findIndex(_featureToggles, 'name', feature.name); + _featureToggles[idx] = feature; + this.trigger(); + }, + + onArchive: function(feature) { + var featureToggles = filter(_featureToggles, function(item) { + return item.name !== feature.name; + }); + this.setToggles(featureToggles); + this.trigger(); + }, + + onRevive: function(item) { + this.setToggles(_featureToggles.concat([item])); + this.trigger(); + }, + + getFeatureToggles: function() { + return _featureToggles; + }, + + initStore: function(toggles) { + _featureToggles = toggles; + } +}); + +module.exports = FeatureStore; diff --git a/public/js/stores/StrategyAPI.js b/public/js/stores/StrategyAPI.js new file mode 100644 index 0000000000..9ddbcb5ca8 --- /dev/null +++ b/public/js/stores/StrategyAPI.js @@ -0,0 +1,52 @@ +var reqwest = require('reqwest'); + +var TYPE = 'json'; +var CONTENT_TYPE = 'application/json'; + +var StrategyAPI = { + createStrategy: function (strategy, cb) { + reqwest({ + url: 'strategies', + method: 'post', + type: TYPE, + contentType: CONTENT_TYPE, + data: JSON.stringify(strategy), + error: function(error) { + cb(error); + }, + success: function() { + cb(null, strategy); + } + }); + }, + + removeStrategy: function (strategy, cb) { + reqwest({ + url: 'strategies/'+strategy.name, + method: 'delete', + type: TYPE, + error: function(error) { + cb(error); + }, + success: function() { + cb(null, strategy); + } + }); + }, + + getStrategies: function (cb) { + reqwest({ + url: 'strategies', + method: 'get', + type: TYPE, + error: function(error) { + cb(error); + }, + success: function(data) { + cb(null, data.strategies); + } + }); + } +}; + +module.exports = StrategyAPI; diff --git a/public/js/stores/StrategyActions.js b/public/js/stores/StrategyActions.js new file mode 100644 index 0000000000..aa135cf1f1 --- /dev/null +++ b/public/js/stores/StrategyActions.js @@ -0,0 +1,40 @@ +var Reflux = require("reflux"); +var StrategyAPI = require('./StrategyAPI'); + +var StrategyActions = Reflux.createActions({ + 'init': { asyncResult: true }, + 'create': { asyncResult: true }, + 'remove': { asyncResult: true }, +}); + +StrategyActions.init.listen(function(){ + StrategyAPI.getStrategies(function(err, strategies) { + if(err) { + this.failed(err); + } else { + this.completed(strategies); + } + }.bind(this)); +}); + +StrategyActions.create.listen(function(feature){ + StrategyAPI.createStrategy(feature, function(err) { + if(err) { + this.failed(err); + } else { + this.completed(feature); + } + }.bind(this)); +}); + +StrategyActions.remove.listen(function(feature){ + StrategyAPI.removeStrategy(feature, function(err) { + if(err) { + this.failed(err); + } else { + this.completed(feature); + } + }.bind(this)); +}); + +module.exports = StrategyActions; diff --git a/public/js/stores/StrategyStore.js b/public/js/stores/StrategyStore.js index 09388e6e34..da571022b2 100644 --- a/public/js/stores/StrategyStore.js +++ b/public/js/stores/StrategyStore.js @@ -1,34 +1,42 @@ -var reqwest = require('reqwest'); +var Reflux = require('reflux'); +var StrategyActions = require('./StrategyActions'); +var filter = require('lodash/collection/filter'); -var TYPE = 'json'; -var CONTENT_TYPE = 'application/json'; +var _strategies = []; -var StrategyStore = { - createStrategy: function (strategy) { - return reqwest({ - url: 'strategies', - method: 'post', - type: TYPE, - contentType: CONTENT_TYPE, - data: JSON.stringify(strategy) - }); +// Creates a DataStore +var StrategyStore = Reflux.createStore({ + + // Initial setup + init: function() { + this.listenTo(StrategyActions.init.completed, this.setStrategies); + this.listenTo(StrategyActions.create.completed, this.onCreate); + this.listenTo(StrategyActions.remove.completed, this.onRemove); }, - removeStrategy: function (strategy) { - return reqwest({ - url: 'strategies/'+strategy.name, - method: 'delete', - type: TYPE - }); + onCreate: function(strategy) { + this.setStrategies(_strategies.concat([strategy])); }, - getStrategies: function () { - return reqwest({ - url: 'strategies', - method: 'get', - type: TYPE + onRemove: function(strategy) { + var strategies = filter(_strategies, function(item) { + return item.name !== strategy.name; }); + this.setStrategies(strategies); + }, + + setStrategies: function(strategies) { + _strategies = strategies; + this.trigger(_strategies); + }, + + getStrategies: function() { + return _strategies; + }, + + initStore: function(strategies) { + _strategies = strategies; } -}; +}); module.exports = StrategyStore; diff --git a/public/js/stores/initalizer.js b/public/js/stores/initalizer.js new file mode 100644 index 0000000000..e949b19d63 --- /dev/null +++ b/public/js/stores/initalizer.js @@ -0,0 +1,17 @@ +var FeatureToggleActions = require('./FeatureToggleActions'); +var StrategyActions = require('./StrategyActions'); +var Timer = require('../utils/Timer'); + +var _timer; + +function load() { + FeatureToggleActions.init.triggerPromise(); + StrategyActions.init.triggerPromise(); + FeatureToggleActions.initArchive.triggerPromise(); +} + +module.exports = function(pollInterval) { + var intervall = pollInterval || 30; + _timer = new Timer(load, intervall*1000); + _timer.start(); +};