mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	remove old unleash-frontend from master
This commit is contained in:
		
							parent
							
								
									8f6932fb78
								
							
						
					
					
						commit
						b8b6e1eab8
					
				| @ -1,3 +0,0 @@ | ||||
| { | ||||
|   "presets": ["es2015", "stage-2", "react"] | ||||
| } | ||||
| @ -1,6 +0,0 @@ | ||||
| { | ||||
|     "extends": [ | ||||
|         "finn", | ||||
|         "finn/node" | ||||
|     ] | ||||
| } | ||||
| @ -1,9 +0,0 @@ | ||||
| // preprocessor.js
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| const ReactTools = require('react-tools'); | ||||
| module.exports = { | ||||
|     process (src) { | ||||
|         return ReactTools.transform(src); | ||||
|     }, | ||||
| }; | ||||
| @ -1,7 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const path = require('path'); | ||||
| 
 | ||||
| module.exports = { | ||||
|     publicFolder: path.join(__dirname, '..', 'public'), | ||||
| }; | ||||
| @ -1,17 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const server = require('unleash-api'); | ||||
| 
 | ||||
| const unleash = server.start({}); | ||||
| const app = unleash.app; | ||||
| const config = unleash.config; | ||||
| 
 | ||||
| const webpack = require('webpack'); | ||||
| const webpackDevMiddleware = require('webpack-dev-middleware'); | ||||
| const webpackConfig = require('./webpack.config'); | ||||
| const compiler = webpack(webpackConfig); | ||||
| 
 | ||||
| app.use(config.baseUriPath, webpackDevMiddleware(compiler, { | ||||
|     publicPath: '/js', | ||||
|     noInfo: true, | ||||
| })); | ||||
| @ -1,79 +0,0 @@ | ||||
| { | ||||
|   "name": "unleash-frontend", | ||||
|   "description": "unleash your features", | ||||
|   "version": "1.0.0-alpha.2", | ||||
|   "keywords": [ | ||||
|     "unleash", | ||||
|     "feature toggle", | ||||
|     "feature", | ||||
|     "toggle" | ||||
|   ], | ||||
|   "files": [ | ||||
|     "public", | ||||
|     "README.md", | ||||
|     "LICENSE" | ||||
|   ], | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "ssh://git@github.com:finn-no/unleash.git" | ||||
|   }, | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/finn-no/unleash/issues" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": "6" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build": "webpack -p", | ||||
|     "start": "NODE_ENV=development supervisor --ignore ./node_modules/,./public/js lib/server-dev.js", | ||||
|     "test": "jest", | ||||
|     "test:ci": "npm run test", | ||||
|     "prepublish": "npm run build" | ||||
|   }, | ||||
|   "main": "./lib/index.js", | ||||
|   "dependencies": { | ||||
|     "lodash": "^3.5.0", | ||||
|     "moment": "^2.13.0", | ||||
|     "react": "^0.13.1", | ||||
|     "react-router": "^0.13.2", | ||||
|     "reflux": "^0.2.10", | ||||
|     "reqwest": "^2.0.5" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "babel-core": "^6.9.1", | ||||
|     "babel-loader": "^6.2.4", | ||||
|     "babel-preset-es2015": "^6.9.0", | ||||
|     "babel-preset-react": "^6.5.0", | ||||
|     "babel-preset-stage-2": "^6.5.0", | ||||
|     "chai": "3.5.0", | ||||
|     "coveralls": "^2.11.9", | ||||
|     "istanbul": "^0.4.3", | ||||
|     "jest-cli": "0.5.8", | ||||
|     "mocha": "^2.4.5", | ||||
|     "mocha-lcov-reporter": "1.2.0", | ||||
|     "nsp": "^2.3.2", | ||||
|     "react-tools": "^0.13.1", | ||||
|     "supertest": "^1.2.0", | ||||
|     "supervisor": "^0.10.0", | ||||
|     "unleash-api": "1.0.0-alpha.2", | ||||
|     "webpack": "^1.13.2", | ||||
|     "webpack-dev-middleware": "^1.6.1" | ||||
|   }, | ||||
|   "jest": { | ||||
|     "scriptPreprocessor": "<rootDir>/jest-preprocessor.js", | ||||
|     "modulePathIgnorePatterns": [ | ||||
|       "<rootDir>/node_modules/npm" | ||||
|     ], | ||||
|     "unmockedModulePathPatterns": [ | ||||
|       "<rootDir>/node_modules/react", | ||||
|       "<rootDir>/node_modules/reflux" | ||||
|     ], | ||||
|     "moduleFileExtensions": [ | ||||
|       "jsx", | ||||
|       "js" | ||||
|     ] | ||||
|   }, | ||||
|   "pre-commit": [ | ||||
|     "lint" | ||||
|   ] | ||||
| } | ||||
| @ -1,13 +0,0 @@ | ||||
| { | ||||
|     "extends": [ | ||||
|         "finn", | ||||
|         "finn-react" | ||||
|     ], | ||||
|     "env": { | ||||
|         "browser": true, | ||||
|         "commonjs": true | ||||
|     }, | ||||
|     "rules": { | ||||
|         "react/sort-comp": "off" | ||||
|     } | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 34 KiB | 
| @ -1,17 +0,0 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <title>unleash admin</title> | ||||
|     <meta name="description" content="unleash"> | ||||
| 
 | ||||
|     <link rel="stylesheet" href="//finncdn.no/bb/css/so/5.5.28/so.min.css"> | ||||
|     <link rel="stylesheet" href="unleash.css"> | ||||
| </head> | ||||
| <body> | ||||
|     <div id="content"></div> | ||||
|     <script src="bundle.js"></script> | ||||
| </body> | ||||
| </html> | ||||
| @ -1,99 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React                   = require('react'); | ||||
| const Router                  = require('react-router'); | ||||
| const Menu                    = require('./components/Menu'); | ||||
| const ErrorMessages           = require('./components/ErrorMessages'); | ||||
| const initalizer              = require('./stores/initalizer'); | ||||
| const FeatureToggleStore      = require('./stores/FeatureToggleStore'); | ||||
| const StrategyStore           = require('./stores/StrategyStore'); | ||||
| const ArchiveStore            = require('./stores/ArchivedToggleStore'); | ||||
| const Link = Router.Link; | ||||
| const RouteHandler = Router.RouteHandler; | ||||
| 
 | ||||
| const UnleashApp = React.createClass({ | ||||
|     contextTypes: { | ||||
|         router: React.PropTypes.func, | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             features: FeatureToggleStore.getFeatureToggles(), | ||||
|             strategies: StrategyStore.getStrategies(), | ||||
|             archivedFeatures: ArchiveStore.getArchivedToggles(), | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onFeatureToggleChange () { | ||||
|         this.setState({ | ||||
|             features: FeatureToggleStore.getFeatureToggles(), | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onStrategiesChange () { | ||||
|         this.setState({ | ||||
|             strategies: StrategyStore.getStrategies(), | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onArchiveChange () { | ||||
|         this.setState({ | ||||
|             archivedFeatures: ArchiveStore.getArchivedToggles(), | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     componentDidMount () { | ||||
|         this.unsubscribeFS = FeatureToggleStore.listen(this.onFeatureToggleChange); | ||||
|         this.unsubscribeSS = StrategyStore.listen(this.onStrategiesChange); | ||||
|         this.unsubscribeAS = ArchiveStore.listen(this.onArchiveChange); | ||||
|     }, | ||||
|     componentWillUnmount () { | ||||
|         this.unsubscribeFS(); | ||||
|         this.unsubscribeSS(); | ||||
|         this.unsubscribeAS(); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount () { | ||||
|         initalizer(30); | ||||
|     }, | ||||
| 
 | ||||
|     renderLink (id, label) { | ||||
|         return    ( | ||||
|             <Link to={id} className="nav-element centerify" activeClassName="nav-active"> | ||||
|                 <span className="topbar-nav-svg-caption caption showbydefault no-break">{label}</span> | ||||
|             </Link> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <Menu> | ||||
|                     {this.renderLink('features',    'Toggles')} | ||||
|                     {this.renderLink('strategies',  'Strategies')} | ||||
|                     {this.renderLink('log',         'Log')} | ||||
|                     {this.renderLink('archive',     'Archive')} | ||||
|                 </Menu> | ||||
|                 <div className="container"> | ||||
|                     <div className="page"> | ||||
|                         <ErrorMessages /> | ||||
|                         <div className="mod shadow mrn pan"> | ||||
|                             <div className="inner pan"> | ||||
|                                 <div className="bd"> | ||||
|                                     <RouteHandler | ||||
|                                             features={this.state.features} | ||||
|                                             strategies={this.state.strategies} | ||||
|                                             archivedFeatures={this.state.archivedFeatures} | ||||
|                                     /> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| module.exports = UnleashApp; | ||||
| @ -1,18 +0,0 @@ | ||||
| { | ||||
|     "extends": [ | ||||
|         "finn", | ||||
|         "finn/node" | ||||
|     ], | ||||
|     "env": { | ||||
|         "browser": true, | ||||
|         "mocha": true | ||||
|     }, | ||||
|     "globals": { | ||||
|         "it": false, | ||||
|         "jest": false, | ||||
|         "beforeEach": false, | ||||
|         "expect": false, | ||||
|         "describe": false, | ||||
|         "afterEach": false | ||||
|     } | ||||
| } | ||||
| @ -1,15 +0,0 @@ | ||||
| /** @jsx React.DOM */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| jest.dontMock('../../components/Menu'); | ||||
| 
 | ||||
| const Menu = require('../../components/Menu'); | ||||
| const React = require('react/addons'); | ||||
| const TestUtils = React.addons.TestUtils; | ||||
| 
 | ||||
| describe('Menu test', () => { | ||||
|     it('should include unleash in menu', () => { | ||||
|         const Compononent = TestUtils .renderIntoDocument(<Menu />); | ||||
|         expect(Compononent.getDOMNode().textContent).toMatch('unleash'); | ||||
|     }); | ||||
| }); | ||||
| @ -1,41 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| jest.dontMock('../../../components/feature/ArchiveFeatureComponent'); | ||||
| jest.mock('../../../stores/FeatureToggleActions'); | ||||
| jest.autoMockOff(); | ||||
| 
 | ||||
| const React = require('react/addons'); | ||||
| const TestUtils = React.addons.TestUtils; | ||||
| const FeatureArchive      = require('../../../components/feature/ArchiveFeatureComponent'); | ||||
| const FeatureActions  = require('../../../stores/FeatureToggleActions'); | ||||
| 
 | ||||
| describe('FeatureForm', () => { | ||||
|     let Component; | ||||
|     beforeEach(() => { | ||||
|         const archivedToggles = [ | ||||
|             { name: 'featureX' }, | ||||
|             { name: 'featureY' }, | ||||
|         ]; | ||||
| 
 | ||||
|         Component = TestUtils.renderIntoDocument( | ||||
|             <FeatureArchive archivedFeatures={archivedToggles} />); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|         React.unmountComponentAtNode(document.body); | ||||
|     }); | ||||
| 
 | ||||
|     it('should render two archived features', () => { | ||||
|         const rows = Component.getDOMNode().querySelectorAll('tbody tr'); | ||||
| 
 | ||||
|         expect(rows.length).toEqual(2); | ||||
|     }); | ||||
| 
 | ||||
|     it('should revive archived feature toggle', () => { | ||||
|         const button = Component.getDOMNode().querySelector('tbody button'); | ||||
|         TestUtils.Simulate.click(button); | ||||
| 
 | ||||
|         jest.runAllTimers(); | ||||
|         expect(FeatureActions.revive.triggerPromise).toBeCalled(); | ||||
|     }); | ||||
| }); | ||||
| @ -1,36 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| jest.dontMock('../../../components/feature/FeatureForm'); | ||||
| 
 | ||||
| const React = require('react/addons'); | ||||
| const TestUtils = React.addons.TestUtils; | ||||
| const FeatureForm = require('../../../components/feature/FeatureForm'); | ||||
| 
 | ||||
| describe('FeatureForm', () => { | ||||
|     let Component; | ||||
|     const strategies = [ | ||||
|         { name: 'default' }, | ||||
|     ]; | ||||
|     afterEach(() => { | ||||
|         React.unmountComponentAtNode(document.body); | ||||
|     }); | ||||
| 
 | ||||
|     describe('new', () => { | ||||
|         it('should render empty form', () => { | ||||
|             Component = TestUtils .renderIntoDocument(<FeatureForm strategies={strategies} />); | ||||
|             const value = Component.getDOMNode().querySelectorAll('input'); | ||||
|             expect(value[0].value).toEqual(''); | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     describe('edit', () => { | ||||
|         const feature = { name: 'Test', strategy: 'unknown' }; | ||||
| 
 | ||||
|         it('should show unknown strategy as default', () => { | ||||
|             Component = TestUtils .renderIntoDocument(<FeatureForm feature={feature} strategies={strategies} />); | ||||
| 
 | ||||
|             const strategySelect = Component.getDOMNode().querySelector('select'); | ||||
|             expect(strategySelect.value).toEqual('default'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @ -1,59 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| jest.dontMock('../../../components/feature/FeatureList'); | ||||
| jest.dontMock('../../../components/feature/Feature'); | ||||
| 
 | ||||
| const React           = require('react/addons'); | ||||
| const TestUtils       = React.addons.TestUtils; | ||||
| const FeatureList     = require('../../../components/feature/FeatureList'); | ||||
| 
 | ||||
| describe('FeatureList', () => { | ||||
|     let Component; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         const features = [ | ||||
|             { name: 'featureX', strategy: 'other' }, | ||||
|             { name: 'group.featureY', strategy: 'default' }, | ||||
|         ]; | ||||
|         const strategies = [ | ||||
|             { name: 'default' }, | ||||
|         ]; | ||||
|         Component = TestUtils .renderIntoDocument(<FeatureList features={features} strategies={strategies} />); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|         React.unmountComponentAtNode(document.body); | ||||
|     }); | ||||
| 
 | ||||
|     it('should render all features', () => { | ||||
|         const featuresElement = Component.getDOMNode().querySelectorAll('.feature'); | ||||
|         expect(featuresElement.length).toEqual(2); | ||||
|     }); | ||||
| 
 | ||||
|     it('should filter list of features', () => { | ||||
|         const filterNode = Component.refs.filter.getDOMNode(); | ||||
|         TestUtils.Simulate.change(filterNode, { target: { value: 'group' } }); | ||||
| 
 | ||||
|         const featuresElement = Component.getDOMNode().querySelectorAll('.feature'); | ||||
|         expect(featuresElement.length).toEqual(1); | ||||
|     }); | ||||
| 
 | ||||
|     it('should filter list of features ignoring case', () => { | ||||
|         const filterNode = Component.refs.filter.getDOMNode(); | ||||
|         TestUtils.Simulate.change(filterNode, { target: { value: 'GROUP' } }); | ||||
| 
 | ||||
|         const featuresElement = Component.getDOMNode().querySelectorAll('.feature'); | ||||
|         expect(featuresElement.length).toEqual(1); | ||||
|         expect(featuresElement[0].textContent).toMatch('group'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should filter list of features by strategy name', () => { | ||||
|         const searchString = 'other'; | ||||
|         const filterNode = Component.refs.filter.getDOMNode(); | ||||
|         TestUtils.Simulate.change(filterNode, { target: { value: searchString } }); | ||||
| 
 | ||||
|         const featuresElement = Component.getDOMNode().querySelectorAll('.feature'); | ||||
|         expect(featuresElement.length).toEqual(1); | ||||
|         expect(featuresElement[0].textContent).toMatch(searchString); | ||||
|     }); | ||||
| }); | ||||
| @ -1,79 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| jest.autoMockOff(); | ||||
| jest.dontMock('../../stores/FeatureToggleActions'); | ||||
| jest.dontMock('../../stores/FeatureToggleStore'); | ||||
| 
 | ||||
| describe('FeatureToggleStore', () => { | ||||
|     let Actions; | ||||
|     let Store; | ||||
|     let toggles; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         Actions = require('../../stores/FeatureToggleActions'); | ||||
|         Store = require('../../stores/FeatureToggleStore'); | ||||
|         toggles = [ | ||||
|             { name: 'app.feature', enabled: true, strategy: 'default' }, | ||||
|         ]; | ||||
|     }); | ||||
| 
 | ||||
|     it('should be an empty store', () => { | ||||
|         expect(Store.getFeatureToggles().length).toBe(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should inititialize the store', () => { | ||||
|         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', () => { | ||||
|         Actions.init.completed(toggles); | ||||
| 
 | ||||
|         const 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', () => { | ||||
|         Actions.init.completed(toggles); | ||||
| 
 | ||||
|         Actions.archive.completed(toggles[0]); | ||||
| 
 | ||||
|         jest.runAllTimers(); | ||||
|         expect(Store.getFeatureToggles().length).toBe(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should keep toggles in sorted order', () => { | ||||
|         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', () => { | ||||
|         Actions.init.completed(toggles); | ||||
|         const toggle = toggles[0]; | ||||
| 
 | ||||
|         toggle.enabled = false; | ||||
|         Actions.update.completed(toggle); | ||||
| 
 | ||||
| 
 | ||||
|         jest.runAllTimers(); | ||||
|         expect(Store.getFeatureToggles()[0].enabled).toEqual(false); | ||||
|     }); | ||||
| }); | ||||
| @ -1,12 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React                   = require('react'); | ||||
| const Router                  = require('react-router'); | ||||
| const UserStore               = require('./stores/UserStore'); | ||||
| const routes                  = require('./routes'); | ||||
| 
 | ||||
| UserStore.init(); | ||||
| 
 | ||||
| Router.run(routes, Handler => { | ||||
|     React.render(<Handler/>, document.getElementById('content')); | ||||
| }); | ||||
| @ -1,40 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React       = require('react'); | ||||
| const Ui          = require('./ErrorMessages.ui'); | ||||
| const ErrorStore  = require('../stores/ErrorStore'); | ||||
| const ErrorActions  = require('../stores/ErrorActions'); | ||||
| 
 | ||||
| const ErrorMessages = React.createClass({ | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             errors: ErrorStore.getErrors(), | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onStoreChange () { | ||||
|         this.setState({ | ||||
|             errors: ErrorStore.getErrors(), | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     componentDidMount () { | ||||
|         this.unsubscribe = ErrorStore.listen(this.onStoreChange); | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount () { | ||||
|         this.unsubscribe(); | ||||
|     }, | ||||
| 
 | ||||
|     onClearErrors () { | ||||
|         ErrorActions.clear(); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|         <Ui errors={this.state.errors} onClearErrors={this.onClearErrors} /> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = ErrorMessages; | ||||
| @ -1,35 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| 
 | ||||
| const ErrorMessages = React.createClass({ | ||||
|     render () { | ||||
|         if (!this.props.errors.length) { | ||||
|             return <div/>; | ||||
|         } | ||||
| 
 | ||||
|         const errorNodes = this.props.errors.map((e, i) => <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" /> | ||||
|                                 </div> | ||||
|                                 <div className="bd"> | ||||
|                                     <ul>{errorNodes}</ul> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = ErrorMessages; | ||||
| @ -1,65 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const User = require('./User'); | ||||
| 
 | ||||
| const Menu = React.createClass({ | ||||
|     render () { | ||||
|         return ( | ||||
|             <div className="topbar mbl"> | ||||
|             <div className="container"> | ||||
|             <div className="page"> | ||||
|                 <div className="fright-ht768"> | ||||
|                 <User /> | ||||
|                 </div> | ||||
|                 <div className="nav-level1 h4"> | ||||
|                 <a href="#" className="homelink pln"> | ||||
|                     <span className="topbar-nav-svg-home"> | ||||
|                     <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53M | ||||
|                                 y5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MjcuNDExIiBoZWlnaHQ9IjE2OS4z | ||||
|                                 OTgiIHZpZXdCb3g9IjAgMCA1MjcuNDExIDE2OS4zOTgiPjxwYXRoIGZpbGw | ||||
|                                 9IiNmZmYiIGQ9Ik00NjguNTA3IDBoLTI1Ni4xODdjLTIxLjcwNyAwLTQwLj | ||||
|                                 Y5NSAxMS44MTItNTAuOTEyIDI5LjMzNy0xMC4yMTYtMTcuNTI1LTI5LjIwN | ||||
|                                 C0yOS4zMzctNTAuOTExLTI5LjMzN2gtNTEuNTk1Yy0zMi40NzkgMC01OC45 | ||||
|                                 MDIgMjYuNDI1LTU4LjkwMiA1OC45MDV2NTEuNTg3YzAgMzIuNDgxIDI2LjQ | ||||
|                                 yMyA1OC45MDYgNTguOTAyIDU4LjkwNmg0MDkuNjA1YzMyLjQ3OSAwIDU4Lj | ||||
|                                 kwMy0yNi40MjUgNTguOTAzLTU4LjkwNnYtNTEuNTg3Yy4wMDEtMzIuNDgtM | ||||
|                                 jYuNDIzLTU4LjkwNS01OC45MDMtNTguOTA1eiIvPjxwYXRoIGZpbGw9IiMw | ||||
|                                 OWYiIGQ9Ik00NjguNTA3IDE1My4zODNjMjMuNjg3IDAgNDIuODg4LTE5LjE | ||||
|                                 5OSA0Mi44ODgtNDIuODl2LTUxLjU4OGMwLTIzLjY5MS0xOS4yMDEtNDIuOD | ||||
|                                 ktNDIuODg4LTQyLjg5aC0yNTYuMTg3Yy0yMy42ODYgMC00Mi44ODcgMTkuM | ||||
|                                 Tk4LTQyLjg4NyA0Mi44OXY5NC40NzhoMjk5LjA3NHoiLz48cGF0aCBmaWxs | ||||
|                                 PSIjMDA2IiBkPSJNMTUzLjM4NCAxNTMuMzgzdi05NC40NzhjMC0yMy42OTE | ||||
|                                 tMTkuMjAxLTQyLjg5LTQyLjg4Ny00Mi44OWgtNTEuNTk1Yy0yMy42ODYgMC | ||||
|                                 00Mi44ODcgMTkuMTk4LTQyLjg4NyA0Mi44OXY1MS41ODdjMCAyMy42OTEgM | ||||
|                                 TkuMjAxIDQyLjg5IDQyLjg4NyA0Mi44OWg5NC40ODJ6Ii8%2BPHJlY3QgeD | ||||
|                                 0iMzIwLjE1NiIgeT0iNzUuMjc1IiBmaWxsPSIjZmZmIiB3aWR0aD0iMTkuN | ||||
|                                 jIxIiBoZWlnaHQ9IjUzLjIxMSIvPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0y | ||||
|                                 NjIuOTEyIDg2LjI4MWMwLTUuNTI5IDMuODEzLTExLjAwNiAxMy4wNjktMTE | ||||
|                                 uMDA2aDI4LjQyMXYxNS42MTNoLTE4LjYxMmMtMi40OTggMC0zLjI1NS45OT | ||||
|                                 ItMy4yNTUgMi42NjR2Ny40NzJoMjEuODY3djE1LjYxaC0yMS44Njd2MTEuO | ||||
|                                 DUyaC0xOS42MjN2LTQyLjIwNXpNMzc1LjE2NSA5MS4wOTloMTAuMzk5YzIu | ||||
|                                 NDA5IDAgMy4yNDYuODMyIDMuMjQ2IDMuMjM1bC0uMDA4IDM0LjE1MmgxOS4 | ||||
|                                 2MzJ2LTQxLjk5NmMwLTUuNTI3LTMuODE1LTExLjAwNC0xMy4wNjktMTEuMD | ||||
|                                 A0aC0zOS44MjRsLS4wMSA1M2gxOS42MzR2LTM3LjM4N3pNNDQyLjcxOSA5M | ||||
|                                 S4wOTloMTAuNGMyLjQwOCAwIDMuMjQ1LjgzMiAzLjI0NSAzLjIzNWwtLjAw | ||||
|                                 OSAzNC4xNTJoMTkuNjM0di00MS45OTZjMC01LjUyNy0zLjgxNS0xMS4wMDQ | ||||
|                                 tMTMuMDctMTEuMDA0aC0zOS44MjNsLS4wMSA1M2gxOS42MzN2LTM3LjM4N3 | ||||
|                                 oiLz48L3N2Zz4%3D" width="106" height="34" /> | ||||
|                     </span> | ||||
| 
 | ||||
|                     <span | ||||
|                     className="topbar-nav-svg-caption caption showbydefault hide-lt900"> | ||||
|                     unleash admin | ||||
|                 </span> | ||||
|                 </a> | ||||
|                 {this.props.children} | ||||
|                 </div> | ||||
|             </div> | ||||
|             </div> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = Menu; | ||||
| @ -1,25 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const UserStore = require('../stores/UserStore'); | ||||
| 
 | ||||
| const User = React.createClass({ | ||||
| 
 | ||||
|     onSave () { | ||||
|         const value = this.refs.username.getDOMNode().value.trim(); | ||||
|         UserStore.set(value); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|       <div className="r-pvm"> | ||||
|         <input type="text" placeholder="username" | ||||
|           ref="username" | ||||
|           defaultValue={UserStore.get()} | ||||
|           onBlur={this.onSave} /> | ||||
|       </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = User; | ||||
| @ -1,48 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React               = require('react'); | ||||
| const FeatureActions      = require('../../stores/FeatureToggleActions'); | ||||
| 
 | ||||
| const ArchiveFeatureComponent = React.createClass({ | ||||
| 
 | ||||
|     onRevive (item) { | ||||
|         FeatureActions.revive.triggerPromise(item); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <h1>Archived Feature Toggles</h1> | ||||
|                 <hr /> | ||||
|                 <table className="outerborder man"> | ||||
|                     <thead> | ||||
|                         <tr> | ||||
|                             <th>Name</th> | ||||
|                             <th /> | ||||
|                         </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                         {this.props.archivedFeatures.map(this.renderArchivedItem)} | ||||
|                     </tbody> | ||||
|                 </table> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderArchivedItem (f) { | ||||
|         return ( | ||||
|             <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" /> | ||||
|                 </button> | ||||
|             </td> | ||||
|         </tr>); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = ArchiveFeatureComponent; | ||||
| @ -1,124 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const FeatureForm = require('./FeatureForm'); | ||||
| const LogEntryList = require('../log/LogEntryList'); | ||||
| const eventStore = require('../../stores/EventStore'); | ||||
| 
 | ||||
| const Feature = React.createClass({ | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             editMode: false, | ||||
|             showHistory: false, | ||||
|             events: [], | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     handleEventsResponse (response) { | ||||
|         this.setState({ events: response }); | ||||
|     }, | ||||
| 
 | ||||
|     toggleHistory () { | ||||
|         eventStore.getEventsByName(this.props.feature.name).then(this.handleEventsResponse); | ||||
|         this.setState({ showHistory: !this.state.showHistory }); | ||||
|     }, | ||||
| 
 | ||||
| 
 | ||||
|     toggleEditMode () { | ||||
|         this.setState({ editMode: !this.state.editMode }); | ||||
|     }, | ||||
| 
 | ||||
|     saveFeature (feature) { | ||||
|         this.props.onChange(feature); | ||||
|         this.toggleEditMode(); | ||||
|     }, | ||||
| 
 | ||||
|     archiveFeature () { | ||||
|         if (window.confirm(`Are you sure you want to delete ${this.props.feature.name}?`)) {  // eslint-disable-line no-alert | ||||
|             this.props.onArchive(this.props.feature); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
| 
 | ||||
|     renderEditMode () { | ||||
|         return ( | ||||
|             <tr> | ||||
|                 <td colSpan="4" className="pan man no-border"> | ||||
|                     <FeatureForm | ||||
|                         feature={this.props.feature} | ||||
|                         onSubmit={this.saveFeature} | ||||
|                         onCancel={this.toggleEditMode} | ||||
|                         strategies={this.props.strategies} /> | ||||
|                 </td> | ||||
|             </tr> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <tbody className="feature"> | ||||
|                 <tr className={this.state.editMode ? 'edit bg-lilac-xlt' : ''}> | ||||
|                     <td width="20"> | ||||
|                         <span className= | ||||
|                         {this.props.feature.enabled ? 'toggle-active' : 'toggle-inactive'} title="Status" /> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         {this.props.feature.name} <br /> | ||||
|                         <span className="opaque smalltext word-break"> | ||||
|                             {this.props.feature.description || '\u00a0'} | ||||
|                         </span> | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td  className="hide-lt480"> | ||||
|                         {this.props.feature.strategy} | ||||
|                     </td> | ||||
| 
 | ||||
|                     <td width="150"> | ||||
|                         <div className="line"> | ||||
|                             <div className="unit size1of3"> | ||||
|                                 <button | ||||
|                                     title="Archive" | ||||
|                                     onClick={this.archiveFeature}> | ||||
|                                     <span className="icon-kryss1" /> | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                             <div className="unit size1of3"> | ||||
|                                 <button | ||||
|                                     className={this.state.editMode ? 'primary' : ''} | ||||
|                                     title="Edit" | ||||
|                                     onClick={this.toggleEditMode}> | ||||
|                                     <span className="icon-redigere" /> | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                             <div className="unit size1of3"> | ||||
|                                 <button | ||||
|                                     className={this.state.showHistory ? 'primary' : ''} | ||||
|                                     title="History" | ||||
|                                     onClick={this.toggleHistory}> | ||||
|                                     <span className="icon-visning_liste" /> | ||||
|                                 </button> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 {this.state.editMode ? this.renderEditMode() : this.renderEmptyRow()} | ||||
|                 {this.state.showHistory ? this.renderHistory() : this.renderEmptyRow()} | ||||
|             </tbody> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderEmptyRow () { | ||||
|         return (<tr />); | ||||
|     }, | ||||
| 
 | ||||
|     renderHistory () { | ||||
|         return (<tr> | ||||
|                     <td colSpan="4" className="no-border"> | ||||
|                         <LogEntryList events={this.state.events} /> | ||||
|                     </td> | ||||
|                 </tr>); | ||||
|     }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| module.exports = Feature; | ||||
| @ -1,175 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const TextInput = require('../form/TextInput'); | ||||
| 
 | ||||
| const FeatureForm = React.createClass({ | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             strategyOptions: this.props.strategies, | ||||
|             requiredParams: [], | ||||
|             currentStrategy: this.props.feature ? this.props.feature.strategy : 'default', | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount () { | ||||
|         if (this.props.feature) { | ||||
|             this.setSelectedStrategy(this.props.feature.strategy); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     onStrategyChange (e) { | ||||
|         this.setSelectedStrategy(e.target.value); | ||||
|         this.setState({ currentStrategy: e.target.value }); | ||||
|     }, | ||||
| 
 | ||||
|     getParameterValue (feature) { | ||||
|         if (this.props.feature && this.props.feature.parameters) { | ||||
|             return this.props.feature.parameters[feature]; | ||||
|         } | ||||
|         return ''; | ||||
|     }, | ||||
| 
 | ||||
|     setSelectedStrategy (strategyName) { | ||||
|         const selectedStrategy = this.props.strategies.filter(strategy => strategy.name ===  strategyName)[0]; | ||||
| 
 | ||||
|         if (selectedStrategy) { | ||||
|             this.setStrategyParams(selectedStrategy); | ||||
|         } else { | ||||
|             this.setState({ | ||||
|                 currentStrategy: 'default', | ||||
|                 requiredParams: [], | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     setStrategyParams (strategy) { | ||||
|         const requiredParams = []; | ||||
|         let key; | ||||
|         for (key in strategy.parametersTemplate) { | ||||
|             if (Object.hasOwnProperty.call(strategy.parametersTemplate, key)) { | ||||
|                 requiredParams.push({ name: key, value: this.getParameterValue(key) }); | ||||
|             } | ||||
|         } | ||||
|         this.setState({ requiredParams }); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         const feature = this.props.feature || { | ||||
|             name: '', | ||||
|             strategy: 'default', | ||||
|             enabled: false, | ||||
|         }; | ||||
| 
 | ||||
|         const idPrefix = this.props.feature ? this.props.feature.name : 'new'; | ||||
| 
 | ||||
|         return ( | ||||
|             <div className="bg-lilac-xlt r-pam"> | ||||
|                 <form ref="form" className="r-size1of2"> | ||||
| 
 | ||||
|                     <fieldset> | ||||
|                         {this.props.feature ? '' : <legend>Create new toggle</legend>} | ||||
| 
 | ||||
|                         <TextInput | ||||
|                             id={`${idPrefix}-name`} | ||||
|                             name="name" | ||||
|                             label="Name" | ||||
|                             value={feature.name} | ||||
|                             disabled={feature.name.length} | ||||
|                             ref="name" | ||||
|                             placeholder="Toggle name" /> | ||||
| 
 | ||||
|                         <TextInput | ||||
|                             id={`${idPrefix}-description`} | ||||
|                             name="description" | ||||
|                             label="Description" | ||||
|                             value={feature.description} | ||||
|                             ref="description" | ||||
|                             placeholder="Enter description" /> | ||||
| 
 | ||||
|                         <div className="formelement"> | ||||
|                             <label htmlFor={`${idPrefix}-strategy`}>Strategy</label> | ||||
|                             <div className="input select"> | ||||
|                                 <select id={`${idPrefix}-strategy`} | ||||
|                                     ref="strategy" | ||||
|                                     value={this.state.currentStrategy} | ||||
|                                     onChange={this.onStrategyChange}> | ||||
|                                     {this.renderStrategyOptions()} | ||||
|                                 </select> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         <div className="formelement"> | ||||
|                             <div className="input"> | ||||
|                                 <ul className="inputs-list"> | ||||
|                                     <li> | ||||
|                                         <input id={`${idPrefix}-active`} | ||||
|                                         ref="enabled" | ||||
|                                         type="checkbox" | ||||
|                                         defaultChecked={feature.enabled} /> | ||||
|                                         <label htmlFor={`${idPrefix}-active`}> | ||||
|                                             Active | ||||
|                                         </label> | ||||
|                                     </li> | ||||
|                                 </ul> | ||||
|                             </div> | ||||
|                         </div> | ||||
| 
 | ||||
|                         {this.renderStrategyProperties()} | ||||
| 
 | ||||
|                     </fieldset> | ||||
| 
 | ||||
|                     <div className="actions"> | ||||
|                         <button className="primary mrs" onClick={this.saveFeature}>Save</button> | ||||
|                         <button className="" onClick={this.cancelFeature}>Cancel</button> | ||||
|                     </div> | ||||
|                 </form> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderStrategyOptions () { | ||||
|         return this.props.strategies.map(strategy => <option key={strategy.name} value={strategy.name}> | ||||
|             {strategy.name} | ||||
|         </option>); | ||||
|     }, | ||||
| 
 | ||||
|     renderStrategyProperties () { | ||||
|         return this.state.requiredParams.map(param => <TextInput | ||||
|             id={`param-${param.name}`} | ||||
|             key={`param-${param.name}`} | ||||
|             name={param.name} | ||||
|             label={param.name} | ||||
|             ref={param.name} | ||||
|             value={param.value} />); | ||||
|     }, | ||||
| 
 | ||||
|     saveFeature (e) { | ||||
|         e.preventDefault(); | ||||
| 
 | ||||
|         const feature = { | ||||
|             name: this.refs.name.getValue(), | ||||
|             description: this.refs.description.getValue(), | ||||
|             strategy: this.state.currentStrategy, | ||||
|             enabled: this.refs.enabled.getDOMNode().checked, | ||||
|             parameters: this.getParameters(), | ||||
|         }; | ||||
| 
 | ||||
|         this.props.onSubmit(feature); | ||||
|     }, | ||||
| 
 | ||||
|     cancelFeature (e) { | ||||
|         e.preventDefault(); | ||||
|         this.props.onCancel(); | ||||
|     }, | ||||
| 
 | ||||
|     getParameters () { | ||||
|         const parameters = {}; | ||||
|         this.state.requiredParams.forEach(param => { | ||||
|             parameters[param.name] = this.refs[param.name].getValue(); | ||||
|         }); | ||||
|         return parameters; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureForm; | ||||
| @ -1,82 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React   = require('react'); | ||||
| const Feature = require('./Feature'); | ||||
| 
 | ||||
| const noop = function () {}; | ||||
| 
 | ||||
| const FeatureList = React.createClass({ | ||||
|     propTypes: { | ||||
|         features: React.PropTypes.array.isRequired, | ||||
|         strategies: React.PropTypes.array.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps () { | ||||
|         return { | ||||
|             onFeatureChanged: noop, | ||||
|             onFeatureArchive: noop, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             filter: undefined, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onFilterChange (e) { | ||||
|         e.preventDefault(); | ||||
|         this.setState({ filter: e.target.value.trim() }); | ||||
|     }, | ||||
| 
 | ||||
|     filteredFeatures () { | ||||
|         if (this.state.filter) { | ||||
|             const regex = new RegExp(this.state.filter, 'i'); | ||||
| 
 | ||||
|             return this.props.features.filter(item => regex.test(item.name) || regex.test(item.strategy)); | ||||
|         } | ||||
|         return this.props.features; | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         const featureNodes = this.filteredFeatures().map(feature => <Feature | ||||
|           key={feature.name} | ||||
|           feature={feature} | ||||
|           onChange={this.props.onFeatureChanged} | ||||
|           onArchive={this.props.onFeatureArchive} | ||||
|           strategies={this.props.strategies} | ||||
|           />); | ||||
| 
 | ||||
|         return ( | ||||
|             <div className=""> | ||||
|                 <table className="outerborder man"> | ||||
|                     <thead> | ||||
|                         <tr> | ||||
|                             <th /> | ||||
|                             <th>Name</th> | ||||
|                             <th className="hide-lt480">Strategy</th> | ||||
|                             <th /> | ||||
|                         </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                         <tr> | ||||
|                             <td colSpan="4"> | ||||
|                                 <input | ||||
|                                     name="filter" | ||||
|                                     ref="filter" | ||||
|                                     type="text" | ||||
|                                     placeholder="Filter by name or strategy" | ||||
|                                     onChange={this.onFilterChange} | ||||
|                                     value={this.state.filter} | ||||
|                                 /> | ||||
|                             </td> | ||||
|                         </tr> | ||||
|                     </tbody> | ||||
|                     {featureNodes} | ||||
|                 </table> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureList; | ||||
| @ -1,70 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React               = require('react'); | ||||
| const FeatureList         = require('./FeatureList'); | ||||
| const FeatureForm         = require('./FeatureForm'); | ||||
| const FeatureActions      = require('../../stores/FeatureToggleActions'); | ||||
| const ErrorActions        = require('../../stores/ErrorActions'); | ||||
| 
 | ||||
| const FeatureTogglesComponent = React.createClass({ | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             createView: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     updateFeature (feature) { | ||||
|         FeatureActions.update.triggerPromise(feature); | ||||
|     }, | ||||
| 
 | ||||
|     archiveFeature (feature) { | ||||
|         FeatureActions.archive.triggerPromise(feature); | ||||
|     }, | ||||
| 
 | ||||
|     createFeature (feature) { | ||||
|         FeatureActions.create.triggerPromise(feature) | ||||
|           .then(this.cancelNewFeature); | ||||
|     }, | ||||
| 
 | ||||
|     newFeature () { | ||||
|         this.setState({ createView: true }); | ||||
|     }, | ||||
| 
 | ||||
|     cancelNewFeature () { | ||||
|         this.setState({ createView: false }); | ||||
|         ErrorActions.clear(); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div> | ||||
| 
 | ||||
|                 <h1>Feature Toggles</h1> | ||||
| 
 | ||||
|                 {this.state.createView ? this.renderCreateView() : this.renderCreateButton()} | ||||
| 
 | ||||
|                 <FeatureList | ||||
|                   features={this.props.features} | ||||
|                   onFeatureChanged={this.updateFeature} | ||||
|                   onFeatureArchive={this.archiveFeature} | ||||
|                   onFeatureSubmit={this.createFeature} | ||||
|                   onFeatureCancel={this.cancelNewFeature} | ||||
|                   onNewFeature={this.newFeature} | ||||
|                   strategies={this.props.strategies} /> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderCreateView () { | ||||
|         return (<FeatureForm | ||||
|             onCancel={this.cancelNewFeature} | ||||
|             onSubmit={this.createFeature} | ||||
|             strategies={this.props.strategies} />); | ||||
|     }, | ||||
| 
 | ||||
|     renderCreateButton () { | ||||
|         return <button className="mal" onClick={this.newFeature}>Create feature toggle</button>; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureTogglesComponent; | ||||
| @ -1,48 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React          = require('react'); | ||||
| 
 | ||||
| const TextInput = React.createClass({ | ||||
|     propTypes: { | ||||
|         name: React.PropTypes.string.isRequired, | ||||
|         label: React.PropTypes.string.isRequired, | ||||
|         id: React.PropTypes.string.isRequired, | ||||
|         placeholder: React.PropTypes.string, | ||||
|         value: React.PropTypes.string, | ||||
|         required: React.PropTypes.bool, | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps () { | ||||
|         return { | ||||
|             required: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState () { | ||||
|         return {}; | ||||
|     }, | ||||
| 
 | ||||
|     getValue () { | ||||
|         return this.refs.input.getDOMNode().value.trim(); | ||||
|     }, | ||||
| 
 | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div className="formelement required"> | ||||
|                 <label htmlFor={this.props.id} className="t4">{this.props.label}</label> | ||||
|                 <div className="input"> | ||||
|                     <input type="text" | ||||
|                         id={this.props.id} | ||||
|                         name={this.props.name} | ||||
|                         defaultValue={this.props.value} | ||||
|                         placeholder={this.props.placeholder} | ||||
|                         disabled={this.props.disabled} | ||||
|                         ref="input" /> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = TextInput; | ||||
| @ -1,37 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React          = require('react'); | ||||
| const LogEntryList   = require('./LogEntryList'); | ||||
| const eventStore     = require('../../stores/EventStore'); | ||||
| const ErrorActions   = require('../../stores/ErrorActions'); | ||||
| 
 | ||||
| const LogEntriesComponent = React.createClass({ | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             createView: false, | ||||
|             events: [], | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentDidMount () { | ||||
|         eventStore.getEvents().then(res => { | ||||
|             this.setState({ events: res.events }); | ||||
|         }, this.initError); | ||||
|     }, | ||||
| 
 | ||||
|     initError () { | ||||
|         ErrorActions.error('Could not load events from server'); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <h1>Log</h1> | ||||
|                 <hr /> | ||||
|                 <LogEntryList events={this.state.events} /> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = LogEntriesComponent; | ||||
| @ -1,88 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const moment = require('moment'); | ||||
| 
 | ||||
| const DIFF_PREFIXES = { | ||||
|     A: ' ', | ||||
|     E: ' ', | ||||
|     D: '-', | ||||
|     N: '+', | ||||
| }; | ||||
| 
 | ||||
| const SPADEN_CLASS = { | ||||
|     A: 'blue', // array edited | ||||
|     E: 'blue', // edited | ||||
|     D: 'negative', // deleted | ||||
|     N: 'positive', // added | ||||
| }; | ||||
| 
 | ||||
| const LogEntry = React.createClass({ | ||||
|     propTypes: { | ||||
|         event: React.PropTypes.object.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         const date = moment(this.props.event.createdAt); | ||||
| 
 | ||||
|         return ( | ||||
|             <tr> | ||||
|             <td> | ||||
|               {date.format('ll')}<br /> | ||||
|               {date.format('HH:mm')} | ||||
|             </td> | ||||
|             <td> | ||||
|             <strong>{this.props.event.data.name}</strong><em>[{this.props.event.type}]</em> | ||||
|             </td> | ||||
|             <td style={{ maxWidth: 300 }}> | ||||
|               {this.renderEventDiff()} | ||||
|             </td> | ||||
|             <td>{this.props.event.createdBy}</td> | ||||
|             </tr> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderFullEventData () { | ||||
|         const localEventData = JSON.parse(JSON.stringify(this.props.event.data)); | ||||
|         delete localEventData.description; | ||||
|         delete localEventData.name; | ||||
| 
 | ||||
|         const prettyPrinted = JSON.stringify(localEventData, null, 2); | ||||
| 
 | ||||
|         return (<code className="JSON smalltext man">{prettyPrinted}</code>); | ||||
|     }, | ||||
| 
 | ||||
|     renderEventDiff () { | ||||
|         if (!this.props.showFullEvents && this.props.event.diffs) { | ||||
|             const changes = this.props.event.diffs.map(this.buildDiff); | ||||
|             return ( | ||||
|                 <code className="smalltext man">{changes.length === 0 ? '(no changes)' : changes}</code> | ||||
|             ); | ||||
|         } | ||||
|         return this.renderFullEventData(); | ||||
|     }, | ||||
| 
 | ||||
|     buildDiff (diff, idx) { | ||||
|         let change; | ||||
|         const key = diff.path.join('.'); | ||||
| 
 | ||||
|         if (diff.lhs !== undefined && diff.rhs !== undefined) { | ||||
|             change = ( | ||||
|                 <div> | ||||
|                   <div className={SPADEN_CLASS.D}>- {key}: {JSON.stringify(diff.lhs)}</div> | ||||
|                   <div className={SPADEN_CLASS.N}>+ {key}: {JSON.stringify(diff.rhs)}</div> | ||||
|                 </div> | ||||
|             ); | ||||
|         } else { | ||||
|             const spadenClass = SPADEN_CLASS[diff.kind]; | ||||
|             const prefix      = DIFF_PREFIXES[diff.kind]; | ||||
| 
 | ||||
|             change = (<div className={spadenClass}>{prefix} {key}: {JSON.stringify(diff.rhs)}</div>); | ||||
|         } | ||||
| 
 | ||||
|         return (<div key={idx}>{change}</div>); | ||||
|     }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| module.exports = LogEntry; | ||||
| @ -1,57 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const LogEntry = require('./LogEntry'); | ||||
| 
 | ||||
| const LogEntryList = React.createClass({ | ||||
|     propTypes: { | ||||
|         events: React.PropTypes.array.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             showFullEvents: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         const logEntryNodes = this.props.events.map(evt => | ||||
|             <LogEntry event={evt} key={evt.id} showFullEvents={this.state.showFullEvents} />); | ||||
| 
 | ||||
|         return ( | ||||
|             <div> | ||||
|                 <label className="prs fright-ht768 smalltext"> | ||||
|                     Show full events | ||||
|                     <input | ||||
|                         type="checkbox" | ||||
|                         className="mlm" | ||||
|                         value={this.state.fullEvents} | ||||
|                         onChange={this.toggleFullEvents} /> | ||||
|                 </label> | ||||
| 
 | ||||
|                 <table className="outerborder zebra-striped"> | ||||
|                     <thead> | ||||
|                         <tr> | ||||
|                             <th>When</th> | ||||
|                             <th>Action</th> | ||||
|                             <th> | ||||
|                                 Data | ||||
|                             </th> | ||||
|                             <th>Author</th> | ||||
|                         </tr> | ||||
|                     </thead> | ||||
|                     <tbody> | ||||
|                         {logEntryNodes} | ||||
|                     </tbody> | ||||
|                 </table> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     toggleFullEvents () { | ||||
|         this.setState({ showFullEvents: !this.state.showFullEvents }); | ||||
|     }, | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| module.exports = LogEntryList; | ||||
| @ -1,63 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React             = require('react'); | ||||
| const StrategyList      = require('./StrategyList'); | ||||
| const StrategyForm      = require('./StrategyForm'); | ||||
| const StrategyActions   = require('../../stores/StrategyActions'); | ||||
| 
 | ||||
| const StrategiesComponent = React.createClass({ | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             createView: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onNewStrategy () { | ||||
|         this.setState({ createView: true }); | ||||
|     }, | ||||
| 
 | ||||
|     onCancelNewStrategy () { | ||||
|         this.setState({ createView: false }); | ||||
|     }, | ||||
| 
 | ||||
|     onSave (strategy) { | ||||
|         StrategyActions.create.triggerPromise(strategy) | ||||
|         .then(this.onCancelNewStrategy); | ||||
|     }, | ||||
| 
 | ||||
|     onRemove (strategy) { | ||||
|         StrategyActions.remove.triggerPromise(strategy); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div> | ||||
|                 <h1>Activation Strategies</h1> | ||||
|                 {this.state.createView ? | ||||
|                     this.renderCreateView() : this.renderCreateButton()} | ||||
|                 <hr /> | ||||
|                 <StrategyList | ||||
|                     strategies={this.props.strategies} | ||||
|                     onRemove={this.onRemove} /> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderCreateView () { | ||||
|         return ( | ||||
|             <StrategyForm | ||||
|                 onCancelNewStrategy={this.onCancelNewStrategy} | ||||
|                 onSave={this.onSave} | ||||
|                 />); | ||||
|     }, | ||||
| 
 | ||||
|     renderCreateButton () { | ||||
|         return ( | ||||
|                     <button className="mal" onClick={this.onNewStrategy}> | ||||
|                         Create strategy | ||||
|                     </button> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = StrategiesComponent; | ||||
| @ -1,32 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React          = require('react'); | ||||
| 
 | ||||
| const Strategy = React.createClass({ | ||||
|     propTypes: { | ||||
|         strategy: React.PropTypes.object.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     onRemove (evt) { | ||||
|         evt.preventDefault(); | ||||
|         if (window.confirm(`Are you sure you want to delete strategy '${this.props.strategy.name}'?`)) {  // eslint-disable-line no-alert | ||||
|             this.props.onRemove(this.props.strategy); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div className="line mal"> | ||||
|                 <div className="unit"> | ||||
|                     <strong>{this.props.strategy.name} </strong> | ||||
|                     <a href="" | ||||
|                         title="Delete strategy" | ||||
|                         onClick={this.onRemove}>(remove)</a><br /> | ||||
|                     <em>{this.props.strategy.description}</em><br /> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = Strategy; | ||||
| @ -1,140 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React          = require('react'); | ||||
| const TextInput      = require('../form/TextInput'); | ||||
| 
 | ||||
| const StrategyForm = React.createClass({ | ||||
| 
 | ||||
|     getDefaultProps () { | ||||
|         return { | ||||
|             maxParams: 4, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState () { | ||||
|         return { | ||||
|             parameters: [], | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     onSubmit (evt) { | ||||
|         evt.preventDefault(); | ||||
| 
 | ||||
|         const strategy = {}; | ||||
|         strategy.name = this.refs.name.getValue(); | ||||
|         strategy.description = this.refs.description.getValue(); | ||||
|         strategy.parametersTemplate = {}; | ||||
| 
 | ||||
|         this.state.parameters.forEach(parameter => { | ||||
|             const value = this.refs[parameter.name].getDOMNode().value.trim(); | ||||
|             if (value) { | ||||
|                 strategy.parametersTemplate[value] = 'string'; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         this.props.onSave(strategy); | ||||
|     }, | ||||
| 
 | ||||
|     onCancel (evt) { | ||||
|         evt.preventDefault(); | ||||
| 
 | ||||
|         this.props.onCancelNewStrategy(); | ||||
|     }, | ||||
| 
 | ||||
|     onAddParam (evt) { | ||||
|         evt.preventDefault(); | ||||
|         const id = this.state.parameters.length + 1; | ||||
|         const params = this.state.parameters.concat([{ id, name: `param_${id}`, label: `Parameter ${id}` }]); | ||||
|         this.setState({ parameters: params }); | ||||
|     }, | ||||
| 
 | ||||
|     onRemoveParam (evt) { | ||||
|         evt.preventDefault(); | ||||
|         const params = this.state.parameters.slice(0, -1); | ||||
| 
 | ||||
|         this.setState({ parameters: params }); | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         return ( | ||||
|             <div className="line r-pam bg-lilac-xlt"> | ||||
|                 <div className="unit r-size1of2"> | ||||
|                     <form onSubmit={this.onSubmit}> | ||||
|                         <fieldset> | ||||
|                             <legend>Create strategy</legend> | ||||
| 
 | ||||
|                             <TextInput | ||||
|                                 id="name" | ||||
|                                 name="name" | ||||
|                                 label="Name" | ||||
|                                 ref="name" | ||||
|                                 placeholder="Strategy name" /> | ||||
| 
 | ||||
|                             <TextInput | ||||
|                                 id="description" | ||||
|                                 name="description" | ||||
|                                 label="Description" | ||||
|                                 ref="description" | ||||
|                                 placeholder="Please write a short description" /> | ||||
| 
 | ||||
|                             {this.renderParameters()} | ||||
|                             {this.renderRemoveLink()} | ||||
| 
 | ||||
|                             <div className="actions"> | ||||
|                                 <input type="submit" value="Save" className="primary mrs" /> | ||||
|                                 <button onClick={this.onCancel} className="mrs">Cancel</button> | ||||
|                                 {this.renderAddLink()} | ||||
|                             </div> | ||||
|                         </fieldset> | ||||
|                     </form> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     renderParameters () { | ||||
|         return this.state.parameters.map(param => <div className="formelement" key={param.name}> | ||||
|             <label className="t4">{param.label}</label> | ||||
|             <div className="input"> | ||||
|                 <div className="line"> | ||||
| 
 | ||||
|                     <div className="unit size2of3"> | ||||
|                         <input | ||||
|                             type="text" | ||||
|                             name={param.name} | ||||
|                             ref={param.name} | ||||
|                             placeholder="Parameter name" | ||||
|                         /> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <div className="unit size1of3"> | ||||
|                         <select defaultValue="string"> | ||||
|                             <option value="string">string</option> | ||||
|                         </select> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div>); | ||||
|     }, | ||||
| 
 | ||||
|     renderAddLink () { | ||||
|         if (this.state.parameters.length < this.props.maxParams) { | ||||
|             return <a href="#add" onClick={this.onAddParam}>+ Add required parameter</a>; | ||||
|         } | ||||
|     }, | ||||
|     renderRemoveLink () { | ||||
|         if (this.state.parameters.length > 0) { | ||||
|             return ( | ||||
|                 <div className="formelement mtn"> | ||||
|                     <a href="#add" | ||||
|                         className="negative" | ||||
|                         onClick={this.onRemoveParam}> | ||||
|                         - Remove parameter | ||||
|                     </a> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = StrategyForm; | ||||
| @ -1,20 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React = require('react'); | ||||
| const Strategy = require('./Strategy'); | ||||
| 
 | ||||
| const StrategyList = React.createClass({ | ||||
|     propTypes: { | ||||
|         strategies: React.PropTypes.array.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     render () { | ||||
|         const strategyNodes = this.props.strategies.map(strategy => | ||||
|             <Strategy strategy={strategy} key={strategy.name} onRemove={this.props.onRemove} />); | ||||
|         return ( | ||||
|             <div>{strategyNodes}</div> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = StrategyList; | ||||
| @ -1,22 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const React                   = require('react'); | ||||
| const Router                  = require('react-router'); | ||||
| const UnleashApp              = require('./UnleashApp'); | ||||
| const LogEntriesComponent     = require('./components/log/LogEntriesComponent'); | ||||
| const FeatureTogglesComponent = require('./components/feature/FeatureTogglesComponent'); | ||||
| const StrategiesComponent     = require('./components/strategy/StrategiesComponent'); | ||||
| const ArchiveFeatureComponent = require('./components/feature/ArchiveFeatureComponent'); | ||||
| const DefaultRoute = Router.DefaultRoute; | ||||
| const Route = Router.Route; | ||||
| 
 | ||||
| const routes = ( | ||||
|     <Route name="app" path="/" handler={UnleashApp}> | ||||
|         <Route name="strategies" handler={StrategiesComponent}/> | ||||
|         <Route name="log" handler={LogEntriesComponent}/> | ||||
|         <Route name="archive" handler={ArchiveFeatureComponent}/> | ||||
|         <DefaultRoute name="features" handler={FeatureTogglesComponent}/> | ||||
|     </Route> | ||||
| ); | ||||
| 
 | ||||
| module.exports = routes; | ||||
| @ -1,47 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux          = require('reflux'); | ||||
| const FeatureActions  = require('./FeatureToggleActions'); | ||||
| const filter          = require('lodash/collection/filter'); | ||||
| const sortBy          = require('lodash/collection/sortBy'); | ||||
| 
 | ||||
| let _archivedToggles = []; | ||||
| 
 | ||||
| // Creates a DataStore
 | ||||
| const FeatureStore = Reflux.createStore({ | ||||
| 
 | ||||
|     // Initial setup
 | ||||
|     init () { | ||||
|         this.listenTo(FeatureActions.initArchive.completed, this.onInit); | ||||
|         this.listenTo(FeatureActions.archive.completed, this.onArchive); | ||||
|         this.listenTo(FeatureActions.revive.completed,  this.onRevive); | ||||
|     }, | ||||
| 
 | ||||
|     onInit (toggles) { | ||||
|         _archivedToggles = toggles; | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     onArchive (feature) { | ||||
|         const toggles = _archivedToggles.concat([feature]); | ||||
|         _archivedToggles = sortBy(toggles, 'name'); | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     onRevive (item) { | ||||
|         const newStore = filter(_archivedToggles, f => f.name !== item.name); | ||||
| 
 | ||||
|         _archivedToggles = sortBy(newStore, 'name'); | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     getArchivedToggles () { | ||||
|         return _archivedToggles; | ||||
|     }, | ||||
| 
 | ||||
|     initStore (archivedToggles) { | ||||
|         _archivedToggles = archivedToggles; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureStore; | ||||
| @ -1,10 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux = require('reflux'); | ||||
| 
 | ||||
| const ErrorActions = Reflux.createActions([ | ||||
|     'clear', | ||||
|     'error', | ||||
| ]); | ||||
| 
 | ||||
| module.exports = ErrorActions; | ||||
| @ -1,70 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux          = require('reflux'); | ||||
| const FeatureActions  = require('./FeatureToggleActions'); | ||||
| const ErrorActions    = require('./ErrorActions'); | ||||
| 
 | ||||
| // Creates a DataStore
 | ||||
| const FeatureStore = Reflux.createStore({ | ||||
|     // Initial setup
 | ||||
|     init () { | ||||
|         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 (error) { | ||||
|         if (this.isClientError(error)) { | ||||
|             const errors = JSON.parse(error.responseText); | ||||
|             errors.forEach(e => { | ||||
|                 this.addError(e.msg); | ||||
|             }); | ||||
|         } else if (error.status === 0) { | ||||
|             this.addError('server unreachable'); | ||||
|         } else { | ||||
|             this.addError(error); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     onClear () { | ||||
|         this.errors = []; | ||||
|         this.trigger([]); | ||||
|     }, | ||||
| 
 | ||||
|     addError (msg) { | ||||
|         const errors = this.errors; | ||||
|         if (errors[errors.length - 1] !== msg) { | ||||
|             errors.push(msg); | ||||
|             this.errors = errors; | ||||
|             this.trigger(errors); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     isClientError (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!'); // eslint-disable-line no-console
 | ||||
|             } else { | ||||
|                 throw e; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     }, | ||||
| 
 | ||||
|     getErrors () { | ||||
|         return this.errors; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureStore; | ||||
| @ -1,26 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const reqwest = require('reqwest'); | ||||
| 
 | ||||
| const TYPE         = 'json'; | ||||
| 
 | ||||
| const EventStore = { | ||||
|     getEvents () { | ||||
|         return reqwest({ | ||||
|             url: 'events', | ||||
|             method: 'get', | ||||
|             type: TYPE, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     getEventsByName (eventName) { | ||||
|         return reqwest({ | ||||
|             url: `events/${eventName}`, | ||||
|             method: 'get', | ||||
|             type: TYPE, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
| }; | ||||
| 
 | ||||
| module.exports = EventStore; | ||||
| @ -1,75 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux = require('reflux'); | ||||
| const Server = require('./FeatureToggleServerFacade'); | ||||
| 
 | ||||
| const 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((error, features) => { | ||||
|         if (error) { | ||||
|             this.failed(error); | ||||
|         } else { | ||||
|             this.completed(features); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| FeatureToggleActions.initArchive.listen(function () { | ||||
|     Server.getArchivedFeatures((error, archivedToggles) => { | ||||
|         if (error) { | ||||
|             this.failed(error); | ||||
|         } else { | ||||
|             this.completed(archivedToggles); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| FeatureToggleActions.create.listen(function (feature) { | ||||
|     Server.createFeature(feature, error => { | ||||
|         if (error) { | ||||
|             this.failed(error); | ||||
|         } else { | ||||
|             this.completed(feature); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| FeatureToggleActions.update.listen(function (feature) { | ||||
|     Server.updateFeature(feature, error => { | ||||
|         if (error) { | ||||
|             this.failed(error); | ||||
|         } else { | ||||
|             this.completed(feature); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| FeatureToggleActions.archive.listen(function (feature) { | ||||
|     Server.archiveFeature(feature, error => { | ||||
|         if (error) { | ||||
|             this.failed(error); | ||||
|         } else { | ||||
|             this.completed(feature); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| FeatureToggleActions.revive.listen(function (feature) { | ||||
|     Server.reviveFeature(feature, error => { | ||||
|         if (error) { | ||||
|             this.failed(error); | ||||
|         } else { | ||||
|             this.completed(feature); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureToggleActions; | ||||
| @ -1,100 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const reqwest = require('reqwest'); | ||||
| 
 | ||||
| const TYPE         = 'json'; | ||||
| const CONTENT_TYPE = 'application/json'; | ||||
| 
 | ||||
| const FeatureToggleServerFacade = { | ||||
|     updateFeature (feature, cb) { | ||||
|         reqwest({ | ||||
|             url: `features/${feature.name}`, | ||||
|             method: 'put', | ||||
|             type: TYPE, | ||||
|             contentType: CONTENT_TYPE, | ||||
|             data: JSON.stringify(feature), | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success () { | ||||
|                 cb(); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     createFeature (feature, cb) { | ||||
|         reqwest({ | ||||
|             url: 'features', | ||||
|             method: 'post', | ||||
|             type: TYPE, | ||||
|             contentType: CONTENT_TYPE, | ||||
|             data: JSON.stringify(feature), | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success () { | ||||
|                 cb(); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     archiveFeature (feature, cb) { | ||||
|         reqwest({ | ||||
|             url: `features/${feature.name}`, | ||||
|             method: 'delete', | ||||
|             type: TYPE, | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success () { | ||||
|                 cb(); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     getFeatures (cb) { | ||||
|         reqwest({ | ||||
|             url: 'features', | ||||
|             method: 'get', | ||||
|             type: TYPE, | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success (data) { | ||||
|                 cb(null, data.features); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     getArchivedFeatures (cb) { | ||||
|         reqwest({ | ||||
|             url: 'archive/features', | ||||
|             method: 'get', | ||||
|             type: TYPE, | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success (data) { | ||||
|                 cb(null, data.features); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     reviveFeature (feature, cb) { | ||||
|         reqwest({ | ||||
|             url: 'archive/revive', | ||||
|             method: 'post', | ||||
|             type: TYPE, | ||||
|             contentType: CONTENT_TYPE, | ||||
|             data: JSON.stringify(feature), | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success () { | ||||
|                 cb(); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| module.exports = FeatureToggleServerFacade; | ||||
| @ -1,57 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux          = require('reflux'); | ||||
| const FeatureActions  = require('./FeatureToggleActions'); | ||||
| const filter          = require('lodash/collection/filter'); | ||||
| const sortBy          = require('lodash/collection/sortBy'); | ||||
| const findIndex       = require('lodash/array/findIndex'); | ||||
| 
 | ||||
| let _featureToggles = []; | ||||
| 
 | ||||
| const FeatureStore = Reflux.createStore({ | ||||
| 
 | ||||
|   // Initial setup
 | ||||
|     init () { | ||||
|         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 (feature) { | ||||
|         this.setToggles([feature].concat(_featureToggles)); | ||||
|     }, | ||||
| 
 | ||||
|     setToggles (toggles) { | ||||
|         _featureToggles = sortBy(toggles, 'name'); | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     onUpdate (feature) { | ||||
|         const idx = findIndex(_featureToggles, 'name', feature.name); | ||||
|         _featureToggles[idx] = feature; | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     onArchive (feature) { | ||||
|         const featureToggles = filter(_featureToggles, item => item.name !== feature.name); | ||||
|         this.setToggles(featureToggles); | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     onRevive (item) { | ||||
|         this.setToggles(_featureToggles.concat([item])); | ||||
|         this.trigger(); | ||||
|     }, | ||||
| 
 | ||||
|     getFeatureToggles () { | ||||
|         return _featureToggles; | ||||
|     }, | ||||
| 
 | ||||
|     initStore (toggles) { | ||||
|         _featureToggles = toggles; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = FeatureStore; | ||||
| @ -1,54 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const reqwest = require('reqwest'); | ||||
| 
 | ||||
| const TYPE         = 'json'; | ||||
| const CONTENT_TYPE = 'application/json'; | ||||
| 
 | ||||
| const StrategyAPI = { | ||||
|     createStrategy (strategy, cb) { | ||||
|         reqwest({ | ||||
|             url: 'strategies', | ||||
|             method: 'post', | ||||
|             type: TYPE, | ||||
|             contentType: CONTENT_TYPE, | ||||
|             data: JSON.stringify(strategy), | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success () { | ||||
|                 cb(null, strategy); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     removeStrategy (strategy, cb) { | ||||
|         reqwest({ | ||||
|             url: `strategies/${strategy.name}`, | ||||
|             method: 'delete', | ||||
|             type: TYPE, | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success () { | ||||
|                 cb(null, strategy); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     getStrategies (cb) { | ||||
|         reqwest({ | ||||
|             url: 'strategies', | ||||
|             method: 'get', | ||||
|             type: TYPE, | ||||
|             error (error) { | ||||
|                 cb(error); | ||||
|             }, | ||||
|             success (data) { | ||||
|                 cb(null, data.strategies); | ||||
|             }, | ||||
|         }); | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| module.exports = StrategyAPI; | ||||
| @ -1,42 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux = require('reflux'); | ||||
| const StrategyAPI = require('./StrategyAPI'); | ||||
| 
 | ||||
| const StrategyActions = Reflux.createActions({ | ||||
|     init: { asyncResult: true }, | ||||
|     create: { asyncResult: true }, | ||||
|     remove: { asyncResult: true }, | ||||
| }); | ||||
| 
 | ||||
| StrategyActions.init.listen(function () { | ||||
|     StrategyAPI.getStrategies((err, strategies) => { | ||||
|         if (err) { | ||||
|             this.failed(err); | ||||
|         } else { | ||||
|             this.completed(strategies); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| StrategyActions.create.listen(function (feature) { | ||||
|     StrategyAPI.createStrategy(feature, err => { | ||||
|         if (err) { | ||||
|             this.failed(err); | ||||
|         } else { | ||||
|             this.completed(feature); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| StrategyActions.remove.listen(function (feature) { | ||||
|     StrategyAPI.removeStrategy(feature, err => { | ||||
|         if (err) { | ||||
|             this.failed(err); | ||||
|         } else { | ||||
|             this.completed(feature); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| module.exports = StrategyActions; | ||||
| @ -1,42 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const Reflux          = require('reflux'); | ||||
| const StrategyActions = require('./StrategyActions'); | ||||
| const filter          = require('lodash/collection/filter'); | ||||
| 
 | ||||
| let _strategies = []; | ||||
| 
 | ||||
| // Creates a DataStore
 | ||||
| const StrategyStore = Reflux.createStore({ | ||||
| 
 | ||||
|     // Initial setup
 | ||||
|     init () { | ||||
|         this.listenTo(StrategyActions.init.completed, this.setStrategies); | ||||
|         this.listenTo(StrategyActions.create.completed, this.onCreate); | ||||
|         this.listenTo(StrategyActions.remove.completed,  this.onRemove); | ||||
|     }, | ||||
| 
 | ||||
|     onCreate (strategy) { | ||||
|         this.setStrategies(_strategies.concat([strategy])); | ||||
|     }, | ||||
| 
 | ||||
|     onRemove (strategy) { | ||||
|         const strategies = filter(_strategies, item => item.name !== strategy.name); | ||||
|         this.setStrategies(strategies); | ||||
|     }, | ||||
| 
 | ||||
|     setStrategies (strategies) { | ||||
|         _strategies = strategies; | ||||
|         this.trigger(_strategies); | ||||
|     }, | ||||
| 
 | ||||
|     getStrategies () { | ||||
|         return _strategies; | ||||
|     }, | ||||
| 
 | ||||
|     initStore (strategies) { | ||||
|         _strategies = strategies; | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| module.exports = StrategyStore; | ||||
| @ -1,36 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| let _username; | ||||
| 
 | ||||
| // Ref: http://stackoverflow.com/questions/10730362/get-cookie-by-name
 | ||||
| function readCookie (cookieName) { | ||||
|     const nameEQ = `${cookieName}=`; | ||||
|     const ca = document.cookie.split(';'); | ||||
|     for (let i = 0;i < ca.length;i++) { | ||||
|         let c = ca[i]; | ||||
|         while (c.charAt(0) == ' ') { // eslint-disable-line eqeqeq
 | ||||
|             c = c.substring(1, c.length); | ||||
|         } | ||||
|         if (c.indexOf(nameEQ) === 0) { | ||||
|             return c.substring(nameEQ.length, c.length); | ||||
|         } | ||||
|     } | ||||
|     return null; | ||||
| } | ||||
| 
 | ||||
| const UserStore = { | ||||
|     init () { | ||||
|         _username = readCookie('username'); | ||||
|     }, | ||||
| 
 | ||||
|     set (username) { | ||||
|         _username = username; | ||||
|         document.cookie = `username=${_username}; expires=Thu, 18 Dec 2099 12:00:00 UTC`; | ||||
|     }, | ||||
| 
 | ||||
|     get () { | ||||
|         return _username; | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| module.exports = UserStore; | ||||
| @ -1,19 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const FeatureToggleActions = require('./FeatureToggleActions'); | ||||
| const StrategyActions = require('./StrategyActions'); | ||||
| const Timer = require('../utils/Timer'); | ||||
| 
 | ||||
| let _timer; | ||||
| 
 | ||||
| function load () { | ||||
|     FeatureToggleActions.init.triggerPromise(); | ||||
|     StrategyActions.init.triggerPromise(); | ||||
|     FeatureToggleActions.initArchive.triggerPromise(); | ||||
| } | ||||
| 
 | ||||
| module.exports = function (pollInterval) { | ||||
|     const intervall = pollInterval || 30; | ||||
|     _timer = new Timer(load, intervall * 1000); | ||||
|     _timer.start(); | ||||
| }; | ||||
| @ -1,29 +0,0 @@ | ||||
| 'use strict'; | ||||
| 
 | ||||
| const 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'); // eslint-disable-line no-console
 | ||||
|     } | ||||
| 
 | ||||
|     console.log('starting timer'); // eslint-disable-line no-console
 | ||||
|     this.timerId = setInterval(this.cb, this.interval); | ||||
|     this.cb(); | ||||
| }; | ||||
| 
 | ||||
| Timer.prototype.stop  = function () { | ||||
|     if (this.timerId == null) { | ||||
|         console.warn('no timer running'); // eslint-disable-line no-console
 | ||||
|     } else { | ||||
|         console.log('stopping timer'); // eslint-disable-line no-console
 | ||||
|         clearInterval(this.timerId); | ||||
|         this.timerId = null; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| module.exports = Timer; | ||||
| @ -1,37 +0,0 @@ | ||||
| .toggle-active { | ||||
|     border: 1px solid black; | ||||
|     display: inline-block; | ||||
|     background-color: lawngreen; | ||||
|     border-radius: 50%; | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
| } | ||||
| .toggle-inactive { | ||||
|     border: 1px solid black; | ||||
|     display: inline-block; | ||||
|     background-color: red; | ||||
|     border-radius: 50%; | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
| } | ||||
| 
 | ||||
| .no-border { | ||||
|     border: none; | ||||
| } | ||||
| 
 | ||||
| code { | ||||
|     word-wrap: break-word; | ||||
|     white-space: pre; | ||||
| } | ||||
| 
 | ||||
| code > .diff-N { | ||||
|     color: green; | ||||
| } | ||||
| 
 | ||||
| code > .diff-D { | ||||
|     color: red; | ||||
| } | ||||
| 
 | ||||
| code > .diff-A, .diff-E { | ||||
|     color: black; | ||||
| } | ||||
| @ -1,41 +0,0 @@ | ||||
| // docs: http://webpack.github.io/docs/configuration.html
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| const path = require('path'); | ||||
| const publicRoot = path.join(__dirname, 'public'); | ||||
| const jsroot = path.join(publicRoot, 'js'); | ||||
| 
 | ||||
| module.exports = { | ||||
| 
 | ||||
|     context: jsroot, | ||||
|     entry: './app.jsx', | ||||
| 
 | ||||
|     output: { | ||||
|         path: publicRoot, | ||||
|         filename: 'bundle.js', | ||||
|         publicPath: '/js/', | ||||
|     }, | ||||
| 
 | ||||
|     resolve: { | ||||
|         root: [jsroot], | ||||
|         extensions: ['', '.js', '.jsx'], | ||||
|         modulesDirectories: ['web_modules', 'node_modules'], | ||||
|     }, | ||||
| 
 | ||||
|     module: { | ||||
|         loaders: [ | ||||
|             { | ||||
|                 test: /\.jsx?$/, | ||||
|                 exclude: /node_modules/, | ||||
|                 loader: 'babel', | ||||
|             }, | ||||
|         ], | ||||
|     }, | ||||
| 
 | ||||
|     devtool: 'source-map', | ||||
| 
 | ||||
|     externals: { | ||||
|         // stuff not in node_modules can be resolved here.
 | ||||
|     }, | ||||
| 
 | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user