1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00

Merge pull request #92 from finn-no/improvement/reflux

Improvement/reflux
This commit is contained in:
Ivar Conradi Østhus 2015-03-23 17:43:50 +01:00
commit 2425a91be7
30 changed files with 877 additions and 456 deletions

View File

@ -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
};

View File

@ -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": "<rootDir>/jest-preprocessor.js",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/react"
"<rootDir>/node_modules/react",
"<rootDir>/node_modules/reflux"
],
"moduleFileExtensions": [
"jsx",

View File

@ -11,7 +11,7 @@
<link rel="stylesheet" href="css/unleash.css">
</head>
<body>
<div id="content">Loading...</div>
<div id="content"></div>
<script src="js/bundle.js"></script>
</body>
</html>

57
public/js/UnleashApp.jsx Normal file
View File

@ -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 (
<div>
<Menu />
<div className="container">
<div className="page">
<ErrorMessages />
<TabView tabPanes={tabPanes} />
</div>
</div>
</div>
);
}
});
module.exports = UnleashApp;

View File

@ -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(<FeatureArchive />);
FeatureToggleStore.initStore(archivedToggles);
Component = TestUtils.renderIntoDocument(<FeatureArchive />);
});
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();
});
});
});

View File

@ -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(<FeatureForm />);
Component = TestUtils .renderIntoDocument(<FeatureForm strategies={strategies} />);
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(<FeatureForm feature={feature} />);
Component = TestUtils .renderIntoDocument(<FeatureForm feature={feature} strategies={strategies} />);
var strategySelect = Component.getDOMNode().querySelector("select");
expect(strategySelect.value).toEqual("unknown (deleted)");
});
});
});
});

View File

@ -13,7 +13,10 @@ describe("FeatureList", function () {
{ name: "featureX", strategy: "other" },
{ name: "group.featureY", strategy: "default" }
];
Component = TestUtils .renderIntoDocument(<FeatureList features={features} />);
var strategies=[
{ name: "default"}
];
Component = TestUtils .renderIntoDocument(<FeatureList features={features} strategies={strategies} />);
});
afterEach(function() {
@ -52,4 +55,4 @@ describe("FeatureList", function () {
expect(features[0].textContent).toMatch(searchString);
});
});
});

View File

@ -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);
});
});

View File

@ -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(
<div>
<Menu />
<div className="container">
<div className="page">
<TabView tabPanes={tabPanes} />
</div>
</div>
</div>
,
document.getElementById('content')
);
var React = require('react');
var UnleashApp = require('./UnleashApp');
React.render(<UnleashApp />, document.getElementById('content'));

View File

@ -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 <div/>;
}
var errorNodes = this.props.errors.map(function(e, i) {
return (<li key={e + i} className="largetext">{e}</li>);
});
return (
<div className="container">
<div className="mod shadow mtm mrn">
<div className="inner bg-red-lt">
<div className="bd">
<div className="media centerify">
<div className="imgExt">
<a onClick={this.props.onClearErrors}
className="icon-kryss1 linkblock sharp">
</a>
</div>
<div className="bd">
<ul>{errorNodes}</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<Ui errors={this.state.errors} onClearErrors={this.onClearErrors}></Ui>
);
}
});
module.exports = ErrorMessages;

View File

@ -0,0 +1,36 @@
var React = require('react');
var ErrorMessages = React.createClass({
render: function() {
if (!this.props.errors.length) {
return <div/>;
}
var errorNodes = this.props.errors.map(function(e, i) {
return (<li key={e + i} className="largetext">{e}</li>);
});
return (
<div className="container">
<div className="mod shadow mtm mrn">
<div className="inner bg-red-lt">
<div className="bd">
<div className="media centerify">
<div className="imgExt">
<a onClick={this.props.onClearErrors}
className="icon-kryss1 linkblock sharp">
</a>
</div>
<div className="bd">
<ul>{errorNodes}</ul>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
});
module.exports = ErrorMessages;

View File

@ -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({
</tbody>
</table>
</div>
);
);
},
renderArchivedItem: function(f) {
@ -49,16 +51,15 @@ var ArchiveFeatureComponent = React.createClass({
<tr key={f.name}>
<td>
{f.name}<br />
<span className="opaque smalltext word-break">{f.description}</span>
</td>
<td className="rightify" width="150">
<button onClick={this.onRevive.bind(this, f)} title="Revive feature toggle">
<span className="icon-svar"></span>
</button>
</td>
</tr>);
<span className="opaque smalltext word-break">{f.description}</span>
</td>
<td className="rightify" width="150">
<button onClick={this.onRevive.bind(this, f)} title="Revive feature toggle">
<span className="icon-svar"></span>
</button>
</td>
</tr>);
}
});
module.exports = ArchiveFeatureComponent;
module.exports = ArchiveFeatureComponent;

View File

@ -44,7 +44,11 @@ var Feature = React.createClass({
return (
<tr>
<td colSpan="4" className="pan man no-border">
<FeatureForm feature={this.props.feature} onSubmit={this.saveFeature} onCancel={this.toggleEditMode} />
<FeatureForm
feature={this.props.feature}
onSubmit={this.saveFeature}
onCancel={this.toggleEditMode}
strategies={this.props.strategies} />
</td>
</tr>
);
@ -110,4 +114,4 @@ var Feature = React.createClass({
});
module.exports = Feature;
module.exports = Feature;

View File

@ -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 (
<option key={strategy.name} value={strategy.name}>
{strategy.name}
</option>
<option key={strategy.name} value={strategy.name}>
{strategy.name}
</option>
);
}.bind(this));
},
@ -178,4 +170,4 @@ var FeatureForm = React.createClass({
}
});
module.exports = FeatureForm;
module.exports = FeatureForm;

View File

@ -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;
module.exports = FeatureList;

View File

@ -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 (
<div>
<ErrorMessages
errors={this.state.errors}
onClearErrors={this.clearErrors} />
{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()} />
</div>
);
},
renderCreateView: function() {
return <FeatureForm onCancel={this.cancelNewFeature} onSubmit={this.createFeature} />
return <FeatureForm
onCancel={this.cancelNewFeature}
onSubmit={this.createFeature}
strategies={StrategyStore.getStrategies()} />;
},
renderCreateButton: function() {
return <button className="mal" onClick={this.newFeature}>Create feature toggle</button>
return <button className="mal" onClick={this.newFeature}>Create feature toggle</button>;
}
});
module.exports = FeatureTogglesComponent;
module.exports = FeatureTogglesComponent;

View File

@ -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 (
<div>
<ErrorMessages errors={this.state.errors} onClearErrors={this.clearErrors} />
<hr />
<LogEntryList events={this.state.events} />
</div>
);
},
});
module.exports = LogEntriesComponent;
module.exports = LogEntriesComponent;

View File

@ -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 (
<div>
<label className="prs fright-ht768 smalltext">
Show full events
<input type="checkbox" className="mlm" value={this.state.fullEvents} onChange={this.toggleFullEvents}></input>
<label className="prs fright-ht768 smalltext">
Show full events
<input
type="checkbox"
className="mlm"
value={this.state.fullEvents}
onChange={this.toggleFullEvents}>
</input>
</label>
<table className='outerborder zebra-striped'>
@ -30,7 +35,7 @@ var LogEntryList = React.createClass({
<th>When</th>
<th>Action</th>
<th>
Data
Data
</th>
<th>Author</th>
</tr>
@ -40,13 +45,13 @@ var LogEntryList = React.createClass({
</tbody>
</table>
</div>
);
);
},
toggleFullEvents: function(e) {
toggleFullEvents: function() {
this.setState({showFullEvents: !this.state.showFullEvents});
}
});
module.exports = LogEntryList;
module.exports = LogEntryList;

View File

@ -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 (
<div>
<ErrorMessages errors={this.state.errors} onClearErrors={this.clearErrors} />
{this.state.createView ? this.renderCreateView() : this.renderCreateButton()}
{this.state.createView ?
this.renderCreateView() : this.renderCreateButton()}
<hr />
<StrategyList strategies={this.state.strategies} onRemove={this.onRemove} />
<StrategyList
strategies={this.state.strategies}
onRemove={this.onRemove} />
</div>
);
);
},
renderCreateView: function() {
return (<StrategyForm onCancelNewStrategy={this.onCancelNewStrategy} onSave={this.onSave} />)
},
renderCreateButton: function() {
return (
<button className="mal" onClick={this.onNewStrategy}>Create strategy</button>
);
}
});
<StrategyForm
onCancelNewStrategy={this.onCancelNewStrategy}
onSave={this.onSave}
/>);
},
module.exports = StrategiesComponent;
renderCreateButton: function() {
return (
<button className="mal" onClick={this.onNewStrategy}>
Create strategy
</button>
);
}
});
module.exports = StrategiesComponent;

View File

@ -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;

View File

@ -0,0 +1,8 @@
var Reflux = require('reflux');
var ErrorActions = Reflux.createActions([
"clear",
"error"
]);
module.exports = ErrorActions;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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();
};