mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
init
This commit is contained in:
parent
54ec9de3cf
commit
d21bf84a5b
8
frontend/.babelrc
Normal file
8
frontend/.babelrc
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"presets": ["react", "es2015", "stage-2"],
|
||||
"env": {
|
||||
"development": {
|
||||
"presets": ["react-hmre"]
|
||||
}
|
||||
}
|
||||
}
|
2
frontend/.eslintignore
Normal file
2
frontend/.eslintignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
bundle.js
|
9
frontend/.eslintrc
Normal file
9
frontend/.eslintrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"finn",
|
||||
"finn/node"
|
||||
],
|
||||
"rules": {
|
||||
"no-shadow": 0
|
||||
}
|
||||
}
|
3
frontend/.gitignore
vendored
3
frontend/.gitignore
vendored
@ -35,3 +35,6 @@ jspm_packages
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# bundled assets
|
||||
dist
|
||||
|
14
frontend/README.md
Normal file
14
frontend/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
## Start developing:
|
||||
|
||||
1. start mock-api:
|
||||
|
||||
```bash
|
||||
npm run start:api
|
||||
```
|
||||
|
||||
2. start webpack-dev-server with hot-reload:
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
Happy coding!
|
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Unleash Admin</title>
|
||||
<link rel="stylesheet" href="/static/bundle.css" />
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id='app'></div>
|
||||
<script src="/static/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
7
frontend/index.js
Normal file
7
frontend/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
publicFolder: path.join(__dirname, 'dist'),
|
||||
};
|
9
frontend/jest-preprocessor.js
Normal file
9
frontend/jest-preprocessor.js
Normal file
@ -0,0 +1,9 @@
|
||||
// preprocessor.js
|
||||
'use strict';
|
||||
|
||||
const ReactTools = require('react-tools');
|
||||
module.exports = {
|
||||
process (src) {
|
||||
return ReactTools.transform(src);
|
||||
},
|
||||
};
|
74
frontend/mock-api.json
Normal file
74
frontend/mock-api.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"features": {
|
||||
"version": 1,
|
||||
"features": [
|
||||
{
|
||||
"name": "Feature.A",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Feature.B",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": true,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "ActiveForUserWithId",
|
||||
"parameters": {
|
||||
"userIdList": "123,221,998"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GradualRolloutRandom",
|
||||
"parameters": {
|
||||
"percentage": "10"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Feature.C",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": false,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Feature.D",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": true,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Feature.E",
|
||||
"description": "lorem ipsum",
|
||||
"enabled": true,
|
||||
"strategies": [
|
||||
{
|
||||
"name": "default",
|
||||
"parameters": {}
|
||||
},
|
||||
{
|
||||
"name": "FancyStrat",
|
||||
"parameters": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
89
frontend/package.json
Normal file
89
frontend/package.json
Normal file
@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "unleash-frontend-next",
|
||||
"description": "unleash your features",
|
||||
"version": "1.0.0",
|
||||
"keywords": [
|
||||
"unleash",
|
||||
"feature toggle",
|
||||
"feature",
|
||||
"toggle"
|
||||
],
|
||||
"files": [
|
||||
"public"
|
||||
],
|
||||
"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": "webpack-dev-server --config webpack.config.js --hot --progress --colors --port 3000",
|
||||
"start:api": "json-server --watch mock-api.json -p 3001",
|
||||
"lint": "eslint . --ext=js,jsx",
|
||||
"test": "echo 'no test'",
|
||||
"test:ci": "npm run test",
|
||||
"prepublish": "npm run build"
|
||||
},
|
||||
"main": "./index.js",
|
||||
"dependencies": {
|
||||
"debug": "^2.2.0",
|
||||
"immutability-helper": "^2.0.0",
|
||||
"immutable": "^3.8.1",
|
||||
"normalize.css": "^4.2.0",
|
||||
"react": "^15.3.1",
|
||||
"react-addons-css-transition-group": "^15.3.1",
|
||||
"react-dom": "^15.3.1",
|
||||
"react-redux": "^4.4.5",
|
||||
"react-router": "^2.8.0",
|
||||
"react-toolbox": "^1.2.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-thunk": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.14.0",
|
||||
"babel-loader": "^6.2.5",
|
||||
"babel-preset-es2015": "^6.14.0",
|
||||
"babel-preset-react": "^6.11.1",
|
||||
"babel-preset-react-hmre": "^1.1.1",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-preset-stage-2": "^6.13.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"eslint": "^3.4.0",
|
||||
"eslint-config-finn": "1.0.0-alpha.11",
|
||||
"eslint-config-finn-react": "^1.0.0-alpha.2",
|
||||
"eslint-plugin-react": "^6.2.0",
|
||||
"extract-text-webpack-plugin": "^1.0.1",
|
||||
"json-server": "^0.8.21",
|
||||
"node-sass": "~3.7.0",
|
||||
"postcss-loader": "^0.13.0",
|
||||
"redux-devtools": "^3.3.1",
|
||||
"sass-loader": "^4.0.2",
|
||||
"style-loader": "^0.13.1",
|
||||
"toolbox-loader": "0.0.3",
|
||||
"webpack": "^1.13.2",
|
||||
"webpack-dev-server": "^1.15.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"
|
||||
]
|
||||
}
|
25
frontend/src/.eslintrc
Normal file
25
frontend/src/.eslintrc
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"extends": [
|
||||
"finn",
|
||||
"finn-react",
|
||||
"finn/es-modules"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es6": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 7,
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"classes":true,
|
||||
"spread":true,
|
||||
"restParams": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-shadow": 0
|
||||
}
|
||||
}
|
40
frontend/src/component/app.jsx
Normal file
40
frontend/src/component/app.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Layout, Panel, NavDrawer, AppBar } from 'react-toolbox';
|
||||
import style from './styles.scss';
|
||||
import ErrorContainer from './error/error-container';
|
||||
|
||||
import Navigation from './nav';
|
||||
|
||||
export default class App extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = { drawerActive: false };
|
||||
|
||||
this.toggleDrawerActive = () => {
|
||||
this.setState({ drawerActive: !this.state.drawerActive });
|
||||
};
|
||||
}
|
||||
|
||||
onOverlayClick = () => this.setState({ drawerActive: false });
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<AppBar title="Unleash Admin" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive} className={style.appBar} />
|
||||
<div className={style.container} style={{ top: '6.4rem' }}>
|
||||
<Layout>
|
||||
<NavDrawer active={this.state.drawerActive} permanentAt="sm" onOverlayClick={this.onOverlayClick} >
|
||||
<Navigation />
|
||||
</NavDrawer>
|
||||
<Panel scrollY>
|
||||
<div style={{ padding: '1.8rem' }}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Panel>
|
||||
<ErrorContainer />
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
15
frontend/src/component/archive/archive-container.js
Normal file
15
frontend/src/component/archive/archive-container.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ListComponent from './archive-list-component';
|
||||
import { fetchArchive, revive } from '../../store/archive-actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const archive = state.archive.get('list').toArray();
|
||||
|
||||
return {
|
||||
archive,
|
||||
};
|
||||
};
|
||||
|
||||
const ArchiveListContainer = connect(mapStateToProps, { fetchArchive, revive })(ListComponent);
|
||||
|
||||
export default ArchiveListContainer;
|
48
frontend/src/component/archive/archive-list-component.jsx
Normal file
48
frontend/src/component/archive/archive-list-component.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React, { Component } from 'react';
|
||||
import { List, ListItem, ListSubHeader } from 'react-toolbox/lib/list';
|
||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
||||
import Chip from 'react-toolbox/lib/chip';
|
||||
import Switch from 'react-toolbox/lib/switch';
|
||||
|
||||
const ArchivedFeature = ({ feature, revive }) => {
|
||||
const { name, description, enabled, strategies } = feature;
|
||||
const actions = [
|
||||
<div>{strategies && strategies.map(s => <Chip><small>{s.name}</small></Chip>)}</div>,
|
||||
<FontIcon style={{ cursor: 'pointer' }} value="restore" onClick={() => revive(feature)} />,
|
||||
];
|
||||
|
||||
const leftActions = [
|
||||
<Switch disabled checked={enabled} />,
|
||||
];
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={name}
|
||||
leftActions={leftActions}
|
||||
rightActions={actions}
|
||||
caption={name}
|
||||
legend={(description && description.substring(0, 100)) || '-'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
class ArchiveList extends Component {
|
||||
componentDidMount () {
|
||||
this.props.fetchArchive();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { archive, revive } = this.props;
|
||||
return (
|
||||
<List ripple >
|
||||
<ListSubHeader caption="Archive" />
|
||||
{archive.length > 0 ?
|
||||
archive.map((feature, i) => <ArchivedFeature key={i} feature={feature} revive={revive} />) :
|
||||
<ListItem caption="No archived feature toggles" />}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ArchiveList;
|
@ -0,0 +1,38 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import Table from 'react-toolbox/lib/table';
|
||||
|
||||
const Model = {
|
||||
appName: { type: String, title: 'Application Name' },
|
||||
instanceId: { type: String },
|
||||
clientIp: { type: String },
|
||||
createdAt: { type: String },
|
||||
lastSeen: { type: String },
|
||||
};
|
||||
|
||||
class ClientStrategies extends Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
fetchClientInstances: PropTypes.func.isRequired,
|
||||
clientInstances: PropTypes.array.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchClientInstances();
|
||||
}
|
||||
|
||||
render () {
|
||||
const source = this.props.clientInstances;
|
||||
|
||||
return (
|
||||
<Table
|
||||
model={Model}
|
||||
source={source}
|
||||
selectable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientStrategies;
|
@ -0,0 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ClientInstances from './client-instance-component';
|
||||
import { fetchClientInstances } from '../../store/client-instance-actions';
|
||||
|
||||
const mapStateToProps = (state) => ({ clientInstances: state.clientInstances.toJS() });
|
||||
|
||||
const StrategiesContainer = connect(mapStateToProps, { fetchClientInstances })(ClientInstances);
|
||||
|
||||
export default StrategiesContainer;
|
34
frontend/src/component/client-strategy/strategy-component.js
Normal file
34
frontend/src/component/client-strategy/strategy-component.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
import Table from 'react-toolbox/lib/table';
|
||||
|
||||
const Model = {
|
||||
appName: { type: String, title: 'Application Name' },
|
||||
strategies: { type: String },
|
||||
};
|
||||
|
||||
class ClientStrategies extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchClientStrategies();
|
||||
}
|
||||
|
||||
render () {
|
||||
const source = this.props.clientStrategies.map(item => (
|
||||
{
|
||||
appName: item.appName,
|
||||
strategies: item.strategies.join(', '),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
model={Model}
|
||||
source={source}
|
||||
selectable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientStrategies;
|
@ -0,0 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ClientStrategies from './strategy-component';
|
||||
import { fetchClientStrategies } from '../../store/client-strategy-actions';
|
||||
|
||||
const mapStateToProps = (state) => ({ clientStrategies: state.clientStrategies.toJS() });
|
||||
|
||||
const StrategiesContainer = connect(mapStateToProps, { fetchClientStrategies })(ClientStrategies);
|
||||
|
||||
export default StrategiesContainer;
|
28
frontend/src/component/error/error-component.jsx
Normal file
28
frontend/src/component/error/error-component.jsx
Normal file
@ -0,0 +1,28 @@
|
||||
import Snackbar from 'react-toolbox/lib/snackbar';
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
class ErrorComponent extends React.Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
errors: PropTypes.array.isRequired,
|
||||
muteError: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const showError = this.props.errors.length > 0;
|
||||
const error = showError ? this.props.errors[0] : undefined;
|
||||
return (
|
||||
<Snackbar
|
||||
action="Dismiss"
|
||||
active={showError}
|
||||
icon="question_answer"
|
||||
label={error}
|
||||
onClick={() => this.props.muteError(error)}
|
||||
type="warning"
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorComponent;
|
14
frontend/src/component/error/error-container.jsx
Normal file
14
frontend/src/component/error/error-container.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ErrorComponent from './error-component';
|
||||
import { muteError } from '../../store/error-actions';
|
||||
|
||||
|
||||
const mapDispatchToProps = {
|
||||
muteError,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
errors: state.error.get('list').toArray(),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ErrorComponent);
|
53
frontend/src/component/feature/feature-component.jsx
Normal file
53
frontend/src/component/feature/feature-component.jsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
||||
import Switch from 'react-toolbox/lib/switch';
|
||||
import { ListItem } from 'react-toolbox/lib/list';
|
||||
import Chip from 'react-toolbox/lib/chip';
|
||||
|
||||
import style from './feature.scss';
|
||||
|
||||
const Feature = ({
|
||||
feature,
|
||||
onFeatureClick,
|
||||
onFeatureRemove,
|
||||
metricsLastHour = { yes: 0, no: 0, hasData: false },
|
||||
metricsLastMinute = { yes: 0, no: 0, hasData: false },
|
||||
}) => {
|
||||
const { name, description, enabled, strategies, createdAt } = feature;
|
||||
const created = new Date(createdAt);
|
||||
|
||||
const actions = [
|
||||
<div key="strategies">{strategies && strategies.map((s, i) => <Chip key={i}><small>{s.name}</small></Chip>)}</div>,
|
||||
<div key="created"><small>({created.toLocaleDateString('nb-NO')})</small></div>,
|
||||
<Link key="change" to={`/features/edit/${name}`} title={`Edit ${name}`}>
|
||||
<FontIcon value="edit" className={style.action} />
|
||||
</Link>,
|
||||
<FontIcon key="delete" className={style.action} value="delete" onClick={() => onFeatureRemove(name)} />,
|
||||
];
|
||||
|
||||
const leftActions = [
|
||||
<Chip><span className={style.yes}>{metricsLastHour.yes}</span> / <span className={style.no}>{metricsLastHour.no}</span></Chip>,
|
||||
<Chip><span className={style.yes}>{metricsLastMinute.yes}</span> / <span className={style.no}>{metricsLastMinute.no}</span></Chip>,
|
||||
<Switch key="left-actions" onChange={() => onFeatureClick(feature)} checked={enabled} />,
|
||||
];
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={name}
|
||||
leftActions={leftActions}
|
||||
rightActions={actions}
|
||||
caption={name}
|
||||
legend={(description && description.substring(0, 100)) || '-'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Feature.propTypes = {
|
||||
feature: PropTypes.object,
|
||||
onFeatureClick: PropTypes.func,
|
||||
onFeatureRemove: PropTypes.func,
|
||||
};
|
||||
|
||||
export default Feature;
|
16
frontend/src/component/feature/feature.scss
Normal file
16
frontend/src/component/feature/feature.scss
Normal file
@ -0,0 +1,16 @@
|
||||
.link {
|
||||
color: #212121;
|
||||
}
|
||||
|
||||
.action {
|
||||
color: #aaa !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.yes {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.no {
|
||||
color: red;
|
||||
}
|
47
frontend/src/component/feature/form-add-container.jsx
Normal file
47
frontend/src/component/feature/form-add-container.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { hashHistory } from 'react-router';
|
||||
import { createFeatureToggles, validateName } from '../../store/feature-actions';
|
||||
import { createMapper, createActions } from '../input-helpers';
|
||||
import FormComponent from './form';
|
||||
|
||||
const ID = 'add-feature-toggle';
|
||||
const mapStateToProps = createMapper({ id: ID });
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
createFeatureToggles(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push('/features'));
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (evt) => {
|
||||
evt.preventDefault();
|
||||
hashHistory.push('/features');
|
||||
};
|
||||
|
||||
methods.addStrategy = (v) => {
|
||||
methods.pushToList('strategies', v);
|
||||
};
|
||||
|
||||
methods.updateStrategy = (v, n) => {
|
||||
methods.updateInList('strategies', v, n);
|
||||
};
|
||||
|
||||
methods.removeStrategy = (v) => {
|
||||
methods.removeFromList('strategies', v);
|
||||
};
|
||||
|
||||
methods.validateName = (v) => {
|
||||
const featureToggleName = v.target.value;
|
||||
validateName(featureToggleName)
|
||||
.then(() => methods.setValue('nameError', undefined))
|
||||
.catch((err) => methods.setValue('nameError', err.message));
|
||||
};
|
||||
|
||||
return methods;
|
||||
};
|
||||
const actions = createActions({ id: ID, prepare });
|
||||
|
||||
export default connect(mapStateToProps, actions)(FormComponent);
|
70
frontend/src/component/feature/form-edit-container.jsx
Normal file
70
frontend/src/component/feature/form-edit-container.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { hashHistory } from 'react-router';
|
||||
|
||||
import { requestUpdateFeatureToggle } from '../../store/feature-actions';
|
||||
import { createMapper, createActions } from '../input-helpers';
|
||||
import FormComponent from './form';
|
||||
|
||||
const ID = 'edit-feature-toggle';
|
||||
function getId (props) {
|
||||
return [ID, props.featureToggleName];
|
||||
}
|
||||
// TODO: need to scope to the active featureToggle
|
||||
// best is to emulate the "input-storage"?
|
||||
const mapStateToProps = createMapper({
|
||||
id: getId,
|
||||
getDefault: (state, ownProps) => {
|
||||
if (ownProps.featureToggleName) {
|
||||
const match = state.features.findEntry((entry) => entry.get('name') === ownProps.featureToggleName);
|
||||
|
||||
if (match && match[1]) {
|
||||
return match[1].toJS();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
prepare: (props) => {
|
||||
props.editmode = true;
|
||||
return props;
|
||||
},
|
||||
});
|
||||
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
// TODO: should add error handling
|
||||
requestUpdateFeatureToggle(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => window.history.back());
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (evt) => {
|
||||
evt.preventDefault();
|
||||
hashHistory.push('/features');
|
||||
};
|
||||
|
||||
methods.addStrategy = (v) => {
|
||||
methods.pushToList('strategies', v);
|
||||
};
|
||||
|
||||
methods.removeStrategy = (v) => {
|
||||
methods.removeFromList('strategies', v);
|
||||
};
|
||||
|
||||
methods.updateStrategy = (v, n) => {
|
||||
methods.updateInList('strategies', v, n);
|
||||
};
|
||||
|
||||
methods.validateName = () => {};
|
||||
|
||||
return methods;
|
||||
};
|
||||
|
||||
const actions = createActions({
|
||||
id: getId,
|
||||
prepare,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, actions)(FormComponent);
|
95
frontend/src/component/feature/form/index.jsx
Normal file
95
frontend/src/component/feature/form/index.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import Input from 'react-toolbox/lib/input';
|
||||
import Button from 'react-toolbox/lib/button';
|
||||
import Switch from 'react-toolbox/lib/switch';
|
||||
import StrategiesSection from './strategies-section-container';
|
||||
|
||||
class AddFeatureToggleComponent extends Component {
|
||||
|
||||
componentWillMount () {
|
||||
// TODO unwind this stuff
|
||||
if (this.props.initCallRequired === true) {
|
||||
this.props.init(this.props.input);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
input,
|
||||
setValue,
|
||||
validateName,
|
||||
addStrategy,
|
||||
removeStrategy,
|
||||
updateStrategy,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
editmode = false,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
name, // eslint-disable-line
|
||||
nameError,
|
||||
description,
|
||||
enabled,
|
||||
} = input;
|
||||
const configuredStrategies = input.strategies || [];
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
<section>
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
name="name"
|
||||
disabled={editmode}
|
||||
required
|
||||
value={name}
|
||||
error={nameError}
|
||||
onBlur={(v) => validateName(v)}
|
||||
onChange={(v) => setValue('name', v)} />
|
||||
<Input
|
||||
type="text"
|
||||
multiline label="Description"
|
||||
required
|
||||
value={description}
|
||||
onChange={(v) => setValue('description', v)} />
|
||||
|
||||
<br />
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
label="Enabled"
|
||||
onChange={(v) => setValue('enabled', v)} />
|
||||
<br />
|
||||
</section>
|
||||
|
||||
<StrategiesSection
|
||||
configuredStrategies={configuredStrategies}
|
||||
addStrategy={addStrategy}
|
||||
updateStrategy={updateStrategy}
|
||||
removeStrategy={removeStrategy} />
|
||||
|
||||
<br />
|
||||
|
||||
<Button type="submit" raised primary label={editmode ? 'Update' : 'Create'} />
|
||||
|
||||
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
AddFeatureToggleComponent.propTypes = {
|
||||
input: PropTypes.object,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
addStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
validateName: PropTypes.func.isRequired,
|
||||
editmode: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default AddFeatureToggleComponent;
|
72
frontend/src/component/feature/form/strategies-add.jsx
Normal file
72
frontend/src/component/feature/form/strategies-add.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import Dropdown from 'react-toolbox/lib/dropdown';
|
||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
||||
|
||||
class AddStrategy extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
addStrategy: PropTypes.func.isRequired,
|
||||
fetchStrategies: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
addStrategy = (strategyName) => {
|
||||
const selectedStrategy = this.props.strategies.find(s => s.name === strategyName);
|
||||
const parameters = {};
|
||||
const keys = Object.keys(selectedStrategy.parametersTemplate || {});
|
||||
keys.forEach(prop => { parameters[prop] = ''; });
|
||||
|
||||
|
||||
this.props.addStrategy({
|
||||
name: selectedStrategy.name,
|
||||
parameters,
|
||||
});
|
||||
};
|
||||
|
||||
customItem (item) {
|
||||
const containerStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
};
|
||||
|
||||
|
||||
const contentStyle = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 2,
|
||||
marginLeft: '10px',
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<FontIcon value="add" />
|
||||
<div style={contentStyle}>
|
||||
<strong>{item.name}</strong>
|
||||
<small>{item.description}</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const strats = this.props.strategies.map(s => {
|
||||
s.value = s.name;
|
||||
return s;
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
allowBlank={false}
|
||||
source={strats}
|
||||
onChange={this.addStrategy}
|
||||
label="Add an activation strategy"
|
||||
template={this.customItem}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default AddStrategy;
|
43
frontend/src/component/feature/form/strategies-list.jsx
Normal file
43
frontend/src/component/feature/form/strategies-list.jsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ConfigureStrategy from './strategy-configure';
|
||||
import { List } from 'react-toolbox/lib/list';
|
||||
|
||||
|
||||
class StrategiesList extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
configuredStrategies: PropTypes.array.isRequired,
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
strategies,
|
||||
configuredStrategies,
|
||||
} = this.props;
|
||||
|
||||
if (!configuredStrategies || configuredStrategies.length === 0) {
|
||||
return <i>No strategies added</i>;
|
||||
}
|
||||
|
||||
const blocks = configuredStrategies.map((strat, i) => (
|
||||
<ConfigureStrategy
|
||||
key={`${strat.name}-${i}`}
|
||||
strategy={strat}
|
||||
removeStrategy={this.props.removeStrategy}
|
||||
updateStrategy={this.props.updateStrategy}
|
||||
strategyDefinition={strategies.find(s => s.name === strat.name)} />
|
||||
));
|
||||
return (
|
||||
<List>
|
||||
{blocks}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StrategiesList;
|
@ -0,0 +1,8 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StrategiesSection from './strategies-section';
|
||||
import { fetchStrategies } from '../../../store/strategy-actions';
|
||||
|
||||
|
||||
export default connect((state) => ({
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
}), { fetchStrategies })(StrategiesSection);
|
44
frontend/src/component/feature/form/strategies-section.jsx
Normal file
44
frontend/src/component/feature/form/strategies-section.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import StrategiesList from './strategies-list';
|
||||
import AddStrategy from './strategies-add';
|
||||
|
||||
const headerStyle = {
|
||||
borderBottom: '1px solid rgba(0, 0, 0, 0.12)',
|
||||
paddingBottom: '5px',
|
||||
marginBottom: '10px',
|
||||
};
|
||||
|
||||
class StrategiesSection extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
addStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
fetchStrategies: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.fetchStrategies();
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.strategies || this.props.strategies.length === 0) {
|
||||
return <i>Loding available strategies</i>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h5 style={headerStyle}>Strategies:</h5>
|
||||
<StrategiesList {...this.props} />
|
||||
<AddStrategy {...this.props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StrategiesSection;
|
80
frontend/src/component/feature/form/strategy-configure.jsx
Normal file
80
frontend/src/component/feature/form/strategy-configure.jsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import Input from 'react-toolbox/lib/input';
|
||||
import Button from 'react-toolbox/lib/button';
|
||||
import { ListItem } from 'react-toolbox/lib/list';
|
||||
|
||||
class StrategyConfigure extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
strategy: PropTypes.object.isRequired,
|
||||
strategyDefinition: PropTypes.object.isRequired,
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
updateStrategy = (evt) => {
|
||||
evt.preventDefault();
|
||||
this.props.updateStrategy({
|
||||
name: this.state.selectedStrategy.name,
|
||||
parameters: this.state.parameters,
|
||||
});
|
||||
};
|
||||
|
||||
handleConfigChange = (key, value) => {
|
||||
const parameters = {};
|
||||
parameters[key] = value;
|
||||
|
||||
const updatedStrategy = Object.assign({}, this.props.strategy, { parameters });
|
||||
|
||||
this.props.updateStrategy(this.props.strategy, updatedStrategy);
|
||||
};
|
||||
|
||||
handleRemove = (evt) => {
|
||||
evt.preventDefault();
|
||||
this.props.removeStrategy(this.props.strategy);
|
||||
}
|
||||
|
||||
renderInputFields (strategyDefinition) {
|
||||
if (strategyDefinition.parametersTemplate) {
|
||||
return Object.keys(strategyDefinition.parametersTemplate).map(field => (
|
||||
<Input
|
||||
type="text"
|
||||
key={field}
|
||||
name={field}
|
||||
label={field}
|
||||
onChange={this.handleConfigChange.bind(this, field)}
|
||||
value={this.props.strategy.parameters[field]}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const leftActions = [
|
||||
<Button key="remove" onClick={this.handleRemove} icon="remove" floating accent mini />,
|
||||
];
|
||||
|
||||
if (!this.props.strategyDefinition) {
|
||||
return (
|
||||
<ListItem
|
||||
leftActions={leftActions}
|
||||
caption={<span style={{ color: 'red' }}>Strategy "{this.props.strategy.name}" deleted</span>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputFields = this.renderInputFields(this.props.strategyDefinition) || [];
|
||||
|
||||
return (
|
||||
<ListItem leftActions={leftActions}
|
||||
caption={this.props.strategy.name}
|
||||
legend={this.props.strategyDefinition.description}
|
||||
rightActions={inputFields}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StrategyConfigure;
|
55
frontend/src/component/feature/list-component.jsx
Normal file
55
frontend/src/component/feature/list-component.jsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import Feature from './feature-component';
|
||||
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
|
||||
|
||||
export default class FeatureListComponent extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
onFeatureClick: PropTypes.func.isRequired,
|
||||
onFeatureRemove: PropTypes.func.isRequired,
|
||||
features: PropTypes.array.isRequired,
|
||||
featureMetrics: PropTypes.object.isRequired,
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
fetchFeatureMetrics: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchFeatureToggles();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
this.props.fetchFeatureMetrics();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { features, onFeatureClick, onFeatureRemove, featureMetrics } = this.props;
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListSubHeader caption="Feature toggles" />
|
||||
{features.map((feature, i) =>
|
||||
<Feature key={i}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
onFeatureClick={onFeatureClick}
|
||||
onFeatureRemove={onFeatureRemove}/>
|
||||
)}
|
||||
<ListDivider />
|
||||
<ListItem
|
||||
onClick={() => this.context.router.push('/features/create')}
|
||||
caption="Add" legend="new feature toggle" leftIcon="add" />
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
24
frontend/src/component/feature/list-container.jsx
Normal file
24
frontend/src/component/feature/list-container.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { toggleFeature, fetchFeatureToggles, removeFeatureToggle } from '../../store/feature-actions';
|
||||
import { fetchFeatureMetrics } from '../../store/feature-metrics-actions';
|
||||
|
||||
import FeatureListComponent from './list-component';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
features: state.features.toJS(),
|
||||
featureMetrics: state.featureMetrics.toJS(),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onFeatureClick: toggleFeature,
|
||||
onFeatureRemove: removeFeatureToggle,
|
||||
fetchFeatureToggles,
|
||||
fetchFeatureMetrics,
|
||||
};
|
||||
|
||||
const FeatureListContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FeatureListComponent);
|
||||
|
||||
export default FeatureListContainer;
|
15
frontend/src/component/history/history-container.js
Normal file
15
frontend/src/component/history/history-container.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ListComponent from './history-list-component';
|
||||
import { fetchHistory } from '../../store/history-actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const history = state.history.get('list').toArray();
|
||||
|
||||
return {
|
||||
history,
|
||||
};
|
||||
};
|
||||
|
||||
const HistoryListContainer = connect(mapStateToProps, { fetchHistory })(ListComponent);
|
||||
|
||||
export default HistoryListContainer;
|
58
frontend/src/component/history/history-list-component.jsx
Normal file
58
frontend/src/component/history/history-list-component.jsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { Component } from 'react';
|
||||
import { List, ListItem, ListSubHeader } from 'react-toolbox/lib/list';
|
||||
|
||||
class HistoryList extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
|
||||
getIcon (type) {
|
||||
if (type.indexOf('created') > -1 ) {
|
||||
return 'add';
|
||||
}
|
||||
|
||||
if (type.indexOf('deleted') > -1 ) {
|
||||
return 'remove';
|
||||
}
|
||||
|
||||
if (type.indexOf('updated') > -1 ) {
|
||||
return 'update';
|
||||
}
|
||||
|
||||
if (type.indexOf('archived') > -1 ) {
|
||||
return 'archived';
|
||||
}
|
||||
return 'bookmark';
|
||||
}
|
||||
|
||||
render () {
|
||||
const { history } = this.props;
|
||||
|
||||
return (
|
||||
<List ripple >
|
||||
<ListSubHeader caption="History" />
|
||||
{history.length > 0 ? history.map((log, i) => {
|
||||
const actions = [];
|
||||
|
||||
|
||||
const icon = this.getIcon(log.type);
|
||||
|
||||
const caption = <div>{log.data.name} <small>{log.type}</small></div>;
|
||||
|
||||
return (
|
||||
<ListItem key={i}
|
||||
leftIcon={icon}
|
||||
rightActions={actions}
|
||||
caption={caption}
|
||||
legend={log.createdAt} />
|
||||
);
|
||||
}) : <ListItem caption="No log entries" />}
|
||||
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default HistoryList;
|
68
frontend/src/component/input-helpers.js
Normal file
68
frontend/src/component/input-helpers.js
Normal file
@ -0,0 +1,68 @@
|
||||
import {
|
||||
createInc,
|
||||
createClear,
|
||||
createSet,
|
||||
createPop,
|
||||
createPush,
|
||||
createUp,
|
||||
createInit,
|
||||
} from '../store/input-actions';
|
||||
|
||||
function getId (id, ownProps) {
|
||||
if (typeof id === 'function') {
|
||||
return id(ownProps); // should return array...
|
||||
}
|
||||
return [id];
|
||||
}
|
||||
|
||||
export function createMapper ({ id, getDefault, prepare = (v) => v }) {
|
||||
return (state, ownProps) => {
|
||||
let input;
|
||||
let initCallRequired = false;
|
||||
const scope = getId(id, ownProps);
|
||||
if (state.input.hasIn(scope)) {
|
||||
input = state.input.getIn(scope).toJS();
|
||||
} else {
|
||||
initCallRequired = true;
|
||||
input = getDefault ? getDefault(state, ownProps) : {};
|
||||
}
|
||||
|
||||
return prepare({
|
||||
initCallRequired,
|
||||
input,
|
||||
}, state, ownProps);
|
||||
};
|
||||
}
|
||||
|
||||
export function createActions ({ id, prepare = (v) => v }) {
|
||||
return (dispatch, ownProps) => (prepare({
|
||||
|
||||
clear () {
|
||||
dispatch(createClear({ id: getId(id, ownProps) }));
|
||||
},
|
||||
|
||||
init (value) {
|
||||
dispatch(createInit({ id: getId(id, ownProps), value }));
|
||||
},
|
||||
|
||||
setValue (key, value) {
|
||||
dispatch(createSet({ id: getId(id, ownProps), key, value }));
|
||||
},
|
||||
|
||||
pushToList (key, value) {
|
||||
dispatch(createPush({ id: getId(id, ownProps), key, value }));
|
||||
},
|
||||
|
||||
removeFromList (key, value) {
|
||||
dispatch(createPop({ id: getId(id, ownProps), key, value }));
|
||||
},
|
||||
|
||||
updateInList (key, value, newValue) {
|
||||
dispatch(createUp({ id: getId(id, ownProps), key, value, newValue }));
|
||||
},
|
||||
|
||||
incValue (key) {
|
||||
dispatch(createInc({ id: getId(id, ownProps), key }));
|
||||
},
|
||||
}, dispatch, ownProps));
|
||||
}
|
31
frontend/src/component/metrics/metrics-component.js
Normal file
31
frontend/src/component/metrics/metrics-component.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
|
||||
import Chip from 'react-toolbox/lib/chip';
|
||||
|
||||
class Metrics extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchMetrics();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { globalCount, clientList } = this.props;
|
||||
|
||||
return (
|
||||
<List>
|
||||
<ListSubHeader caption={<span>Total of {globalCount} toggles </span>} />
|
||||
<ListDivider />
|
||||
{clientList.map(({ name, count, ping, appName }, i) =>
|
||||
<ListItem
|
||||
leftActions={[<Chip>{count}</Chip>]}
|
||||
key={name + i}
|
||||
caption={appName}
|
||||
legend={`${name} pinged ${ping}`} />
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Metrics;
|
39
frontend/src/component/metrics/metrics-container.js
Normal file
39
frontend/src/component/metrics/metrics-container.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { connect } from 'react-redux';
|
||||
import Metrics from './metrics-component';
|
||||
import { fetchMetrics } from '../../store/metrics-actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const globalCount = state.metrics.get('globalCount');
|
||||
const apps = state.metrics.get('apps').toArray();
|
||||
const clients = state.metrics.get('clients').toJS();
|
||||
|
||||
const clientList = Object
|
||||
.keys(clients)
|
||||
.map((k) => {
|
||||
const client = clients[k];
|
||||
return {
|
||||
name: k,
|
||||
appName: client.appName,
|
||||
count: client.count,
|
||||
ping: new Date(client.ping),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.ping > b.ping ? -1 : 1));
|
||||
|
||||
|
||||
/*
|
||||
Possible stuff to ask/answer:
|
||||
* toggles in use but not in unleash-server
|
||||
* nr of toggles using fallbackValue
|
||||
* strategies implemented but not used
|
||||
*/
|
||||
return {
|
||||
globalCount,
|
||||
apps,
|
||||
clientList,
|
||||
};
|
||||
};
|
||||
|
||||
const MetricsContainer = connect(mapStateToProps, { fetchMetrics })(Metrics);
|
||||
|
||||
export default MetricsContainer;
|
45
frontend/src/component/nav.jsx
Normal file
45
frontend/src/component/nav.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import { ListSubHeader, List, ListItem, ListDivider } from 'react-toolbox';
|
||||
import style from './styles.scss';
|
||||
|
||||
export default class UnleashNav extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
|
||||
render () {
|
||||
const createListItem = (path, caption) =>
|
||||
<ListItem to={this.context.router.createHref(path)} caption={caption}
|
||||
className={this.context.router.isActive(path) ? style.active : ''} />;
|
||||
|
||||
return (
|
||||
<List selectable ripple className={style.navigation}>
|
||||
{createListItem('/features', 'Feature toggles')}
|
||||
{createListItem('/strategies', 'Strategies')}
|
||||
{createListItem('/history', 'Event history')}
|
||||
{createListItem('/archive', 'Archived toggles')}
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<ListSubHeader caption="Clients" />
|
||||
{createListItem('/metrics', 'Client metrics')}
|
||||
{createListItem('/client-strategies', 'Client strategies')}
|
||||
{createListItem('/client-instances', 'Client instances')}
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<ListSubHeader caption="Resources" />
|
||||
{createListItem('/docs', 'Documentation')}
|
||||
<a href="https://github.com/finn-no/unleash/" target="_blank">
|
||||
<ListItem caption="GitHub" />
|
||||
</a>
|
||||
|
||||
<ListDivider />
|
||||
<ListItem selectable={false} ripple="false">
|
||||
<p>A product by <a href="https://finn.no" target="_blank">FINN.no</a></p>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
);
|
||||
}
|
||||
};
|
45
frontend/src/component/strategies/add-container.js
Normal file
45
frontend/src/component/strategies/add-container.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createMapper, createActions } from '../input-helpers';
|
||||
import { createStrategy } from '../../store/strategy-actions';
|
||||
|
||||
import AddStrategy, { PARAM_PREFIX } from './add-strategy';
|
||||
|
||||
const ID = 'add-strategy';
|
||||
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const parametersTemplate = {};
|
||||
Object.keys(input).forEach(key => {
|
||||
if (key.startsWith(PARAM_PREFIX)) {
|
||||
parametersTemplate[input[key]] = 'string';
|
||||
}
|
||||
});
|
||||
input.parametersTemplate = parametersTemplate;
|
||||
|
||||
createStrategy(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
// somewhat quickfix / hacky to go back..
|
||||
.then(() => window.history.back());
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (e) => {
|
||||
e.preventDefault();
|
||||
// somewhat quickfix / hacky to go back..
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
|
||||
return methods;
|
||||
};
|
||||
|
||||
const actions = createActions({
|
||||
id: ID,
|
||||
prepare,
|
||||
});
|
||||
|
||||
export default connect(createMapper({ id: ID }), actions)(AddStrategy);
|
74
frontend/src/component/strategies/add-strategy.jsx
Normal file
74
frontend/src/component/strategies/add-strategy.jsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import Input from 'react-toolbox/lib/input';
|
||||
import Button from 'react-toolbox/lib/button';
|
||||
|
||||
function gerArrayWithEntries (num) {
|
||||
return Array.from(Array(num));
|
||||
}
|
||||
export const PARAM_PREFIX = 'param_';
|
||||
|
||||
const genParams = (input, num = 0, setValue) => (<div>{gerArrayWithEntries(num).map((v, i) => {
|
||||
const key = `${PARAM_PREFIX}${i + 1}`;
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
label={`Parameter name ${i + 1}`}
|
||||
name={key} key={key}
|
||||
onChange={(value) => setValue(key, value)}
|
||||
value={input[key]} />
|
||||
);
|
||||
})}</div>);
|
||||
|
||||
const AddStrategy = ({
|
||||
input,
|
||||
setValue,
|
||||
incValue,
|
||||
// clear,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}) => (
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
<section>
|
||||
<Input type="text" label="Strategy name"
|
||||
name="name" required
|
||||
pattern="^[0-9a-zA-Z\.\-]+$"
|
||||
onChange={(value) => setValue('name', value)}
|
||||
value={input.name}
|
||||
/>
|
||||
<Input type="text" multiline label="Description"
|
||||
name="description"
|
||||
onChange={(value) => setValue('description', value)}
|
||||
value={input.description}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
{genParams(input, input._params, setValue)}
|
||||
<Button icon="add" accent label="Add parameter" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
incValue('_params');
|
||||
}}/>
|
||||
</section>
|
||||
|
||||
<br />
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<Button type="submit" raised primary label="Create" />
|
||||
|
||||
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
|
||||
</section>
|
||||
</form>
|
||||
);
|
||||
|
||||
AddStrategy.propTypes = {
|
||||
input: PropTypes.object,
|
||||
setValue: PropTypes.func,
|
||||
incValue: PropTypes.func,
|
||||
clear: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
||||
|
||||
export default AddStrategy;
|
54
frontend/src/component/strategies/list-component.jsx
Normal file
54
frontend/src/component/strategies/list-component.jsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
|
||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
||||
import Chip from 'react-toolbox/lib/chip';
|
||||
|
||||
import style from './strategies.scss';
|
||||
|
||||
class StrategiesListComponent extends Component {
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchStrategies();
|
||||
}
|
||||
|
||||
getParameterMap ({ parametersTemplate }) {
|
||||
return Object.keys(parametersTemplate || {}).map(k => (
|
||||
<Chip key={k}><small>{k}</small></Chip>
|
||||
));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { strategies, removeStrategy } = this.props;
|
||||
|
||||
return (
|
||||
<List ripple >
|
||||
<ListSubHeader caption="Strategies" />
|
||||
{strategies.length > 0 ? strategies.map((strategy, i) => {
|
||||
const actions = this.getParameterMap(strategy).concat([
|
||||
<button className={style['non-style-button']} key="1" onClick={() => removeStrategy(strategy)}>
|
||||
<FontIcon value="delete" />
|
||||
</button>,
|
||||
]);
|
||||
|
||||
|
||||
return (
|
||||
<ListItem key={i} rightActions={actions}
|
||||
caption={strategy.name}
|
||||
legend={strategy.description} />
|
||||
);
|
||||
}) : <ListItem caption="No entries" />}
|
||||
<ListDivider />
|
||||
<ListItem
|
||||
onClick={() => this.context.router.push('/strategies/create')}
|
||||
caption="Add" legend="new strategy" leftIcon="add" />
|
||||
</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StrategiesListComponent;
|
24
frontend/src/component/strategies/list-container.jsx
Normal file
24
frontend/src/component/strategies/list-container.jsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StrategiesListComponent from './list-component.jsx';
|
||||
import { fetchStrategies, removeStrategy } from '../../store/strategy-actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const list = state.strategies.get('list').toArray();
|
||||
|
||||
return {
|
||||
strategies: list,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
removeStrategy: (strategy) => {
|
||||
if (window.confirm('Are you sure you want to remove this strategy?')) { // eslint-disable-line no-alert
|
||||
removeStrategy(strategy)(dispatch);
|
||||
}
|
||||
},
|
||||
fetchStrategies: () => fetchStrategies()(dispatch),
|
||||
});
|
||||
|
||||
const StrategiesListContainer = connect(mapStateToProps, mapDispatchToProps)(StrategiesListComponent);
|
||||
|
||||
export default StrategiesListContainer;
|
8
frontend/src/component/strategies/strategies.scss
Normal file
8
frontend/src/component/strategies/strategies.scss
Normal file
@ -0,0 +1,8 @@
|
||||
.non-style-button {
|
||||
cursor: pointer;
|
||||
color: #757575;
|
||||
background: none;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
17
frontend/src/component/styles.scss
Normal file
17
frontend/src/component/styles.scss
Normal file
@ -0,0 +1,17 @@
|
||||
.container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
.active {
|
||||
background-color: #EEE;
|
||||
}
|
||||
}
|
||||
|
24
frontend/src/data/archive-api.js
Normal file
24
frontend/src/data/archive-api.js
Normal file
@ -0,0 +1,24 @@
|
||||
import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = '/archive';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(`${URI}/features`)
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function revive (feature) {
|
||||
return fetch(`${URI}/revive`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(feature),
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
revive,
|
||||
};
|
||||
|
13
frontend/src/data/client-instance-api.js
Normal file
13
frontend/src/data/client-instance-api.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = '/client/instances';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI, { headers })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
};
|
13
frontend/src/data/client-strategy-api.js
Normal file
13
frontend/src/data/client-strategy-api.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = '/client/strategies';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI, { headers })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
};
|
48
frontend/src/data/feature-api.js
Normal file
48
frontend/src/data/feature-api.js
Normal file
@ -0,0 +1,48 @@
|
||||
import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = '/features';
|
||||
const URI_VALIDATE = '/features-validate';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI)
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function create (featureToggle) {
|
||||
return fetch(URI, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(featureToggle),
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function validate (featureToggle) {
|
||||
return fetch(URI_VALIDATE, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(featureToggle),
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function update (featureToggle) {
|
||||
return fetch(`${URI}/${featureToggle.name}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(featureToggle),
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function remove (featureToggleName) {
|
||||
return fetch(`${URI}/${featureToggleName}`, {
|
||||
method: 'DELETE',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
create,
|
||||
validate,
|
||||
update,
|
||||
remove,
|
||||
};
|
25
frontend/src/data/helper.js
Normal file
25
frontend/src/data/helper.js
Normal file
@ -0,0 +1,25 @@
|
||||
const defaultErrorMessage = 'Unexptected exception when talking to unleash-api';
|
||||
|
||||
export function throwIfNotSuccess (response) {
|
||||
if (!response.ok) {
|
||||
if (response.status > 399 && response.status < 404) {
|
||||
return new Promise((resolve, reject) => {
|
||||
response.json().then(body => {
|
||||
const errorMsg = body && body.length > 0 ? body[0].msg : defaultErrorMessage;
|
||||
let error = new Error(errorMsg);
|
||||
error.statusCode = response.status;
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error(defaultErrorMessage));
|
||||
}
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
};
|
||||
|
||||
|
||||
export const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
13
frontend/src/data/history-api.js
Normal file
13
frontend/src/data/history-api.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { throwIfNotSuccess } from './helper';
|
||||
|
||||
const URI = '/events';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI)
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
};
|
13
frontend/src/data/metrics-api.js
Normal file
13
frontend/src/data/metrics-api.js
Normal file
@ -0,0 +1,13 @@
|
||||
import { throwIfNotSuccess } from './helper';
|
||||
|
||||
const URI = '/metrics';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI)
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
};
|
30
frontend/src/data/strategy-api.js
Normal file
30
frontend/src/data/strategy-api.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = '/strategies';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI)
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function create (strategy) {
|
||||
return fetch(URI, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(strategy),
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function remove (strategy) {
|
||||
return fetch(`${URI}/${strategy.name}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
create,
|
||||
remove,
|
||||
};
|
46
frontend/src/index.jsx
Normal file
46
frontend/src/index.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRedirect, hashHistory } from 'react-router';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
import store from './store';
|
||||
import App from './component/app';
|
||||
|
||||
import Features from './page/features';
|
||||
import CreateFeatureToggle from './page/features/create';
|
||||
import EditFeatureToggle from './page/features/edit';
|
||||
import Strategies from './page/strategies';
|
||||
import CreateStrategies from './page/strategies/create';
|
||||
import HistoryPage from './page/history';
|
||||
import Archive from './page/archive';
|
||||
import Metrics from './page/metrics';
|
||||
import ClientStrategies from './page/client-strategies';
|
||||
import ClientInstances from './page/client-instances';
|
||||
|
||||
const unleashStore = createStore(
|
||||
store,
|
||||
applyMiddleware(
|
||||
thunkMiddleware
|
||||
)
|
||||
);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={unleashStore}>
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={App}>
|
||||
<IndexRedirect to="/features" />
|
||||
<Route path="/features" component={Features} />
|
||||
<Route path="/features/create" component={CreateFeatureToggle} />
|
||||
<Route path="/features/edit/:name" component={EditFeatureToggle} />
|
||||
<Route path="/strategies" component={Strategies} />
|
||||
<Route path="/strategies/create" component={CreateStrategies} />
|
||||
<Route path="/history" component={HistoryPage} />
|
||||
<Route path="/archive" component={Archive} />
|
||||
<Route path="/metrics" component={Metrics} />
|
||||
<Route path="/client-strategies" component={ClientStrategies} />
|
||||
<Route path="/client-instances" component={ClientInstances} />
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>, document.getElementById('app'));
|
6
frontend/src/page/archive/index.js
Normal file
6
frontend/src/page/archive/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import Archive from '../../component/archive/archive-container';
|
||||
|
||||
const render = () => <Archive />;
|
||||
|
||||
export default render;
|
6
frontend/src/page/client-instances/index.js
Normal file
6
frontend/src/page/client-instances/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import ClientInstance from '../../component/client-instance/client-instance-container';
|
||||
|
||||
const render = () => <ClientInstance />;
|
||||
|
||||
export default render;
|
11
frontend/src/page/client-strategies/index.js
Normal file
11
frontend/src/page/client-strategies/index.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import ClientStrategy from '../../component/client-strategy/strategy-container';
|
||||
|
||||
const render = () => (
|
||||
<div>
|
||||
<h5>Client Strategies</h5>
|
||||
<ClientStrategy />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default render;
|
11
frontend/src/page/features/create.js
Normal file
11
frontend/src/page/features/create.js
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import AddFeatureToggleForm from '../../component/feature/form-add-container';
|
||||
|
||||
const render = () => (
|
||||
<div>
|
||||
<h3>Create feature toggle</h3>
|
||||
<AddFeatureToggleForm />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default render;
|
19
frontend/src/page/features/edit.js
Normal file
19
frontend/src/page/features/edit.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import EditFeatureToggleForm from '../../component/feature/form-edit-container';
|
||||
|
||||
export default class Features extends Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<h3>Edit feature toggle</h3>
|
||||
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
8
frontend/src/page/features/index.js
Normal file
8
frontend/src/page/features/index.js
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import FeatureListContainer from '../../component/feature/list-container';
|
||||
|
||||
const render = () => (<FeatureListContainer />);
|
||||
|
||||
export default render;
|
||||
|
||||
|
6
frontend/src/page/history/index.js
Normal file
6
frontend/src/page/history/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import HistoryComponent from '../../component/history/history-container';
|
||||
|
||||
const render = () => <HistoryComponent />;
|
||||
|
||||
export default render;
|
6
frontend/src/page/metrics/index.js
Normal file
6
frontend/src/page/metrics/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import Metrics from '../../component/metrics/metrics-container';
|
||||
|
||||
const render = () => <Metrics />;
|
||||
|
||||
export default render;
|
4
frontend/src/page/strategies/create.js
Normal file
4
frontend/src/page/strategies/create.js
Normal file
@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import AddStrategies from '../../component/strategies/add-container';
|
||||
|
||||
export default () => (<AddStrategies />);
|
4
frontend/src/page/strategies/index.js
Normal file
4
frontend/src/page/strategies/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
import React from 'react';
|
||||
import Strategies from '../../component/strategies/list-container';
|
||||
|
||||
export default () => (<Strategies />);
|
33
frontend/src/store/archive-actions.js
Normal file
33
frontend/src/store/archive-actions.js
Normal file
@ -0,0 +1,33 @@
|
||||
import api from '../data/archive-api';
|
||||
|
||||
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
|
||||
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
|
||||
export const ERROR_RECEIVE_ARCHIVE = 'ERROR_RECEIVE_ARCHIVE';
|
||||
|
||||
const receiveArchive = (json) => ({
|
||||
type: RECEIVE_ARCHIVE,
|
||||
value: json.features,
|
||||
});
|
||||
|
||||
const reviveToggle = (archiveFeatureToggle) => ({
|
||||
type: REVIVE_TOGGLE,
|
||||
value: archiveFeatureToggle,
|
||||
});
|
||||
|
||||
const errorReceiveArchive = (statusCode) => ({
|
||||
type: ERROR_RECEIVE_ARCHIVE,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function revive (featureToggle) {
|
||||
return dispatch => api.revive(featureToggle)
|
||||
.then(() => dispatch(reviveToggle(featureToggle)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
}
|
||||
|
||||
|
||||
export function fetchArchive () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveArchive(json)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
}
|
19
frontend/src/store/archive-store.js
Normal file
19
frontend/src/store/archive-store.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_ARCHIVE, REVIVE_TOGGLE } from './archive-actions';
|
||||
|
||||
function getInitState () {
|
||||
return new $Map({ list: new List() });
|
||||
}
|
||||
|
||||
const archiveStore = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case REVIVE_TOGGLE:
|
||||
return state.update('list', (list) => list.remove(list.indexOf(action.value)));
|
||||
case RECEIVE_ARCHIVE:
|
||||
return state.set('list', new List(action.value));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default archiveStore;
|
20
frontend/src/store/client-instance-actions.js
Normal file
20
frontend/src/store/client-instance-actions.js
Normal file
@ -0,0 +1,20 @@
|
||||
import api from '../data/client-instance-api';
|
||||
|
||||
export const RECEIVE_CLIENT_INSTANCES = 'RECEIVE_CLIENT_INSTANCES';
|
||||
export const ERROR_RECEIVE_CLIENT_INSTANCES = 'ERROR_RECEIVE_CLIENT_INSTANCES';
|
||||
|
||||
const receiveClientInstances = (json) => ({
|
||||
type: RECEIVE_CLIENT_INSTANCES,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveClientInstances = (statusCode) => ({
|
||||
type: RECEIVE_CLIENT_INSTANCES,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchClientInstances () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveClientInstances(json)))
|
||||
.catch(error => dispatch(errorReceiveClientInstances(error)));
|
||||
}
|
17
frontend/src/store/client-instance-store.js
Normal file
17
frontend/src/store/client-instance-store.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { RECEIVE_CLIENT_INSTANCES } from './client-instance-actions';
|
||||
|
||||
function getInitState () {
|
||||
return fromJS([]);
|
||||
}
|
||||
|
||||
const store = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_CLIENT_INSTANCES:
|
||||
return fromJS(action.value);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default store;
|
20
frontend/src/store/client-strategy-actions.js
Normal file
20
frontend/src/store/client-strategy-actions.js
Normal file
@ -0,0 +1,20 @@
|
||||
import api from '../data/client-strategy-api';
|
||||
|
||||
export const RECEIVE_CLIENT_STRATEGIES = 'RECEIVE_CLIENT_STRATEGIES';
|
||||
export const ERROR_RECEIVE_CLIENT_STRATEGIES = 'ERROR_RECEIVE_CLIENT_STRATEGIES';
|
||||
|
||||
const receiveMetrics = (json) => ({
|
||||
type: RECEIVE_CLIENT_STRATEGIES,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveMetrics = (statusCode) => ({
|
||||
type: RECEIVE_CLIENT_STRATEGIES,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchClientStrategies () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveMetrics(json)))
|
||||
.catch(error => dispatch(errorReceiveMetrics(error)));
|
||||
}
|
17
frontend/src/store/client-strategy-store.js
Normal file
17
frontend/src/store/client-strategy-store.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { RECEIVE_CLIENT_STRATEGIES } from './client-strategy-actions';
|
||||
|
||||
function getInitState () {
|
||||
return fromJS([]);
|
||||
}
|
||||
|
||||
const store = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_CLIENT_STRATEGIES:
|
||||
return fromJS(action.value);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default store;
|
8
frontend/src/store/error-actions.js
Normal file
8
frontend/src/store/error-actions.js
Normal file
@ -0,0 +1,8 @@
|
||||
export const MUTE_ERRORS = 'MUTE_ERRORS';
|
||||
export const MUTE_ERROR = 'MUTE_ERROR';
|
||||
|
||||
export const muteErrors = () => ({ type: MUTE_ERRORS });
|
||||
|
||||
export const muteError = (error) => ({ type: MUTE_ERROR, error });
|
||||
|
||||
|
40
frontend/src/store/error-store.js
Normal file
40
frontend/src/store/error-store.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { MUTE_ERROR } from './error-actions';
|
||||
import {
|
||||
ERROR_FETCH_FEATURE_TOGGLES,
|
||||
ERROR_CREATING_FEATURE_TOGGLE,
|
||||
ERROR_REMOVE_FEATURE_TOGGLE,
|
||||
ERROR_UPDATE_FEATURE_TOGGLE,
|
||||
} from './feature-actions';
|
||||
|
||||
const debug = require('debug')('unleash:error-store');
|
||||
|
||||
function getInitState () {
|
||||
return new $Map({
|
||||
list: new List(),
|
||||
});
|
||||
}
|
||||
|
||||
function addErrorIfNotAlreadyInList (state, error) {
|
||||
debug('Got error', error);
|
||||
if (state.get('list').indexOf(error) < 0) {
|
||||
return state.update('list', (list) => list.push(error));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const strategies = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case ERROR_CREATING_FEATURE_TOGGLE:
|
||||
case ERROR_REMOVE_FEATURE_TOGGLE:
|
||||
case ERROR_FETCH_FEATURE_TOGGLES:
|
||||
case ERROR_UPDATE_FEATURE_TOGGLE:
|
||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||
case MUTE_ERROR:
|
||||
return state.update('list', (list) => list.remove(list.indexOf(action.error)));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default strategies;
|
93
frontend/src/store/feature-actions.js
Normal file
93
frontend/src/store/feature-actions.js
Normal file
@ -0,0 +1,93 @@
|
||||
import api from '../data/feature-api';
|
||||
const debug = require('debug')('unleash:feature-actions');
|
||||
|
||||
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
|
||||
export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE';
|
||||
export const UPDATE_FEATURE_TOGGLE = 'UPDATE_FEATURE_TOGGLE';
|
||||
export const TOGGLE_FEATURE_TOGGLE = 'TOGGLE_FEATURE_TOGGLE';
|
||||
export const START_FETCH_FEATURE_TOGGLES = 'START_FETCH_FEATURE_TOGGLES';
|
||||
export const START_UPDATE_FEATURE_TOGGLE = 'START_UPDATE_FEATURE_TOGGLE';
|
||||
export const START_CREATE_FEATURE_TOGGLE = 'START_CREATE_FEATURE_TOGGLE';
|
||||
export const START_REMOVE_FEATURE_TOGGLE = 'START_REMOVE_FEATURE_TOGGLE';
|
||||
export const RECEIVE_FEATURE_TOGGLES = 'RECEIVE_FEATURE_TOGGLES';
|
||||
export const ERROR_FETCH_FEATURE_TOGGLES = 'ERROR_FETCH_FEATURE_TOGGLES';
|
||||
export const ERROR_CREATING_FEATURE_TOGGLE = 'ERROR_CREATING_FEATURE_TOGGLE';
|
||||
export const ERROR_UPDATE_FEATURE_TOGGLE = 'ERROR_UPDATE_FEATURE_TOGGLE';
|
||||
export const ERROR_REMOVE_FEATURE_TOGGLE = 'ERROR_REMOVE_FEATURE_TOGGLE';
|
||||
|
||||
export function toggleFeature (featureToggle) {
|
||||
debug('Toggle feature toggle ', featureToggle);
|
||||
return dispatch => {
|
||||
const newValue = Object.assign({}, featureToggle, { enabled: !featureToggle.enabled });
|
||||
dispatch(requestUpdateFeatureToggle(newValue));
|
||||
};
|
||||
};
|
||||
|
||||
export function editFeatureToggle (featureToggle) {
|
||||
debug('Update feature toggle ', featureToggle);
|
||||
return dispatch => {
|
||||
dispatch(requestUpdateFeatureToggle(featureToggle));
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function receiveFeatureToggles (json) {
|
||||
debug('reviced feature toggles', json);
|
||||
return {
|
||||
type: RECEIVE_FEATURE_TOGGLES,
|
||||
featureToggles: json.features.map(features => features),
|
||||
receivedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchAndThrow (dispatch, type) {
|
||||
return (error) => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchFeatureToggles () {
|
||||
debug('Start fetching feature toggles');
|
||||
return dispatch => {
|
||||
dispatch({ type: START_FETCH_FEATURE_TOGGLES });
|
||||
|
||||
return api.fetchAll()
|
||||
.then(json => dispatch(receiveFeatureToggles(json)))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLES));
|
||||
};
|
||||
}
|
||||
|
||||
export function createFeatureToggles (featureToggle) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_CREATE_FEATURE_TOGGLE });
|
||||
|
||||
return api.create(featureToggle)
|
||||
.then(() => dispatch({ type: ADD_FEATURE_TOGGLE, featureToggle }))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_CREATING_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function requestUpdateFeatureToggle (featureToggle) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
|
||||
|
||||
return api.update(featureToggle)
|
||||
.then(() => dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle }))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function removeFeatureToggle (featureToggleName) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_REMOVE_FEATURE_TOGGLE });
|
||||
|
||||
return api.remove(featureToggleName)
|
||||
.then(() => dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName }))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_REMOVE_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function validateName (featureToggleName) {
|
||||
return api.validate({ name: featureToggleName });
|
||||
}
|
30
frontend/src/store/feature-metrics-actions.js
Normal file
30
frontend/src/store/feature-metrics-actions.js
Normal file
@ -0,0 +1,30 @@
|
||||
import api from './feature-metrics-api';
|
||||
|
||||
export const START_FETCH_FEATURE_METRICS = 'START_FETCH_FEATURE_METRICS';
|
||||
export const RECEIVE_FEATURE_METRICS = 'RECEIVE_FEATURE_METRICS';
|
||||
export const ERROR_FETCH_FEATURE_TOGGLES = 'ERROR_FETCH_FEATURE_TOGGLES';
|
||||
|
||||
function receiveFeatureMetrics (json) {
|
||||
return {
|
||||
type: RECEIVE_FEATURE_METRICS,
|
||||
metrics: json,
|
||||
receivedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchAndThrow (dispatch, type) {
|
||||
return (error) => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchFeatureMetrics () {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_FETCH_FEATURE_METRICS });
|
||||
|
||||
return api.fetchFeatureMetrics()
|
||||
.then(json => dispatch(receiveFeatureMetrics(json)))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLES));
|
||||
};
|
||||
}
|
29
frontend/src/store/feature-metrics-api.js
Normal file
29
frontend/src/store/feature-metrics-api.js
Normal file
@ -0,0 +1,29 @@
|
||||
const defaultErrorMessage = 'Unexptected exception when talking to unleash-api';
|
||||
|
||||
function throwIfNotSuccess (response) {
|
||||
if (!response.ok) {
|
||||
if (response.status > 400 && response.status < 404) {
|
||||
return new Promise((resolve, reject) => {
|
||||
response.json().then(body => {
|
||||
const errorMsg = body && body.length > 0 ? body[0].msg : defaultErrorMessage;
|
||||
let error = new Error(errorMsg);
|
||||
error.statusCode = response.status;
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error(defaultErrorMessage));
|
||||
}
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
|
||||
function fetchFeatureMetrics () {
|
||||
return fetch('/metrics/features')
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchFeatureMetrics,
|
||||
};
|
21
frontend/src/store/feature-metrics-store.js
Normal file
21
frontend/src/store/feature-metrics-store.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { Map as $Map, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
RECEIVE_FEATURE_METRICS,
|
||||
} from './feature-metrics-actions';
|
||||
|
||||
|
||||
const metrics = (state = fromJS({ lastHour: {}, lastMinute: {} }), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_FEATURE_METRICS:
|
||||
return state.withMutations((ctx) => {
|
||||
ctx.set('lastHour', new $Map(action.metrics.lastHour));
|
||||
ctx.set('lastMinute', new $Map(action.metrics.lastMinute));
|
||||
return ctx;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default metrics;
|
38
frontend/src/store/feature-store.js
Normal file
38
frontend/src/store/feature-store.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
const debug = require('debug')('unleash:feature-store');
|
||||
|
||||
|
||||
import {
|
||||
ADD_FEATURE_TOGGLE,
|
||||
RECEIVE_FEATURE_TOGGLES,
|
||||
UPDATE_FEATURE_TOGGLE,
|
||||
REMOVE_FEATURE_TOGGLE,
|
||||
} from './feature-actions';
|
||||
|
||||
|
||||
const features = (state = new List([]), action) => {
|
||||
switch (action.type) {
|
||||
case ADD_FEATURE_TOGGLE:
|
||||
debug(ADD_FEATURE_TOGGLE, action);
|
||||
return state.push(new $Map(action.featureToggle));
|
||||
case REMOVE_FEATURE_TOGGLE:
|
||||
debug(REMOVE_FEATURE_TOGGLE, action);
|
||||
return state.filter(toggle => toggle.get('name') !== action.featureToggleName);
|
||||
case UPDATE_FEATURE_TOGGLE:
|
||||
debug(UPDATE_FEATURE_TOGGLE, action);
|
||||
return state.map(toggle => {
|
||||
if (toggle.get('name') === action.featureToggle.name) {
|
||||
return new $Map(action.featureToggle);
|
||||
} else {
|
||||
return toggle;
|
||||
}
|
||||
});
|
||||
case RECEIVE_FEATURE_TOGGLES:
|
||||
debug(RECEIVE_FEATURE_TOGGLES, action);
|
||||
return new List(action.featureToggles.map($Map));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default features;
|
20
frontend/src/store/history-actions.js
Normal file
20
frontend/src/store/history-actions.js
Normal file
@ -0,0 +1,20 @@
|
||||
import api from '../data/history-api';
|
||||
|
||||
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
|
||||
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
|
||||
|
||||
const receiveHistory = (json) => ({
|
||||
type: RECEIVE_HISTORY,
|
||||
value: json.events,
|
||||
});
|
||||
|
||||
const errorReceiveHistory = (statusCode) => ({
|
||||
type: ERROR_RECEIVE_HISTORY,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchHistory () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveHistory(json)))
|
||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
||||
}
|
17
frontend/src/store/history-store.js
Normal file
17
frontend/src/store/history-store.js
Normal file
@ -0,0 +1,17 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_HISTORY } from './history-actions';
|
||||
|
||||
function getInitState () {
|
||||
return new $Map({ list: new List() });
|
||||
}
|
||||
|
||||
const historyStore = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_HISTORY:
|
||||
return state.set('list', new List(action.value));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default historyStore;
|
26
frontend/src/store/index.js
Normal file
26
frontend/src/store/index.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import features from './feature-store';
|
||||
import featureMetrics from './feature-metrics-store';
|
||||
import strategies from './strategy-store';
|
||||
import input from './input-store';
|
||||
import history from './history-store'; // eslint-disable-line
|
||||
import archive from './archive-store';
|
||||
import error from './error-store';
|
||||
import metrics from './metrics-store';
|
||||
import clientStrategies from './client-strategy-store';
|
||||
import clientInstances from './client-instance-store';
|
||||
|
||||
const unleashStore = combineReducers({
|
||||
features,
|
||||
featureMetrics,
|
||||
strategies,
|
||||
input,
|
||||
history,
|
||||
archive,
|
||||
error,
|
||||
metrics,
|
||||
clientStrategies,
|
||||
clientInstances,
|
||||
});
|
||||
|
||||
export default unleashStore;
|
19
frontend/src/store/input-actions.js
Normal file
19
frontend/src/store/input-actions.js
Normal file
@ -0,0 +1,19 @@
|
||||
export const actions = {
|
||||
SET_VALUE: 'SET_VALUE',
|
||||
INCREMENT_VALUE: 'INCREMENT_VALUE',
|
||||
LIST_PUSH: 'LIST_PUSH',
|
||||
LIST_POP: 'LIST_POP',
|
||||
LIST_UP: 'LIST_UP',
|
||||
CLEAR: 'CLEAR',
|
||||
INIT: 'INIT',
|
||||
};
|
||||
|
||||
export const createInit = ({ id, value }) => ({ type: actions.INIT, id, value });
|
||||
export const createInc = ({ id, key }) => ({ type: actions.INCREMENT_VALUE, id, key });
|
||||
export const createSet = ({ id, key, value }) => ({ type: actions.SET_VALUE, id, key, value });
|
||||
export const createPush = ({ id, key, value }) => ({ type: actions.LIST_PUSH, id, key, value });
|
||||
export const createPop = ({ id, key, value }) => ({ type: actions.LIST_POP, id, key, value });
|
||||
export const createUp = ({ id, key, value, newValue }) => ({ type: actions.LIST_UP, id, key, value, newValue });
|
||||
export const createClear = ({ id }) => ({ type: actions.CLEAR, id });
|
||||
|
||||
export default actions;
|
94
frontend/src/store/input-store.js
Normal file
94
frontend/src/store/input-store.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { Map as $Map, List, fromJS } from 'immutable';
|
||||
import actions from './input-actions';
|
||||
|
||||
function getInitState () {
|
||||
return new $Map();
|
||||
}
|
||||
|
||||
function init (state, { id, value }) {
|
||||
state = assertId(state, id);
|
||||
return state.setIn(id, fromJS(value));
|
||||
}
|
||||
|
||||
function assertId (state, id) {
|
||||
if (!state.hasIn(id)) {
|
||||
return state.setIn(id, new $Map({ inputId: id }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function assertList (state, id, key) {
|
||||
if (!state.getIn(id).has(key)) {
|
||||
return state.setIn(id.concat([key]), new List());
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function setKeyValue (state, { id, key, value }) {
|
||||
state = assertId(state, id);
|
||||
return state.setIn(id.concat([key]), value);
|
||||
}
|
||||
|
||||
function increment (state, { id, key }) {
|
||||
state = assertId(state, id);
|
||||
return state.updateIn(id.concat([key]), (value = 0) => value + 1);
|
||||
}
|
||||
|
||||
function clear (state, { id }) {
|
||||
if (state.hasIn(id)) {
|
||||
return state.removeIn(id);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function addToList (state, { id, key, value }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => list.push(value));
|
||||
}
|
||||
|
||||
function updateInList (state, { id, key, value, newValue }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => list.set(list.indexOf(value), newValue));
|
||||
}
|
||||
|
||||
function removeFromList (state, { id, key, value }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => list.remove(list.indexOf(value)));
|
||||
}
|
||||
|
||||
const inputState = (state = getInitState(), action) => {
|
||||
if (!action.id) {
|
||||
return state;
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case actions.INIT:
|
||||
return init(state, action);
|
||||
case actions.SET_VALUE:
|
||||
if (actions.key != null && actions.value != null) {
|
||||
throw new Error('Missing required key / value');
|
||||
}
|
||||
return setKeyValue(state, action);
|
||||
case actions.INCREMENT_VALUE:
|
||||
return increment(state, action);
|
||||
case actions.LIST_PUSH:
|
||||
return addToList(state, action);
|
||||
case actions.LIST_POP:
|
||||
return removeFromList(state, action);
|
||||
case actions.LIST_UP:
|
||||
return updateInList(state, action);
|
||||
case actions.CLEAR:
|
||||
return clear(state, action);
|
||||
default:
|
||||
// console.log('TYPE', action.type, action);
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default inputState;
|
20
frontend/src/store/metrics-actions.js
Normal file
20
frontend/src/store/metrics-actions.js
Normal file
@ -0,0 +1,20 @@
|
||||
import api from '../data/metrics-api';
|
||||
|
||||
export const RECEIVE_METRICS = 'RECEIVE_METRICS';
|
||||
export const ERROR_RECEIVE_METRICS = 'ERROR_RECEIVE_METRICS';
|
||||
|
||||
const receiveMetrics = (json) => ({
|
||||
type: RECEIVE_METRICS,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveMetrics = (statusCode) => ({
|
||||
type: ERROR_RECEIVE_METRICS,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchMetrics () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveMetrics(json)))
|
||||
.catch(error => dispatch(errorReceiveMetrics(error)));
|
||||
}
|
21
frontend/src/store/metrics-store.js
Normal file
21
frontend/src/store/metrics-store.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { RECEIVE_METRICS } from './metrics-actions';
|
||||
|
||||
function getInitState () {
|
||||
return fromJS({
|
||||
totalCount: 0,
|
||||
apps: [],
|
||||
clients: {},
|
||||
});
|
||||
}
|
||||
|
||||
const historyStore = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_METRICS:
|
||||
return fromJS(action.value);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default historyStore;
|
61
frontend/src/store/strategy-actions.js
Normal file
61
frontend/src/store/strategy-actions.js
Normal file
@ -0,0 +1,61 @@
|
||||
import api from '../data/strategy-api';
|
||||
|
||||
export const ADD_STRATEGY = 'ADD_STRATEGY';
|
||||
export const REMOVE_STRATEGY = 'REMOVE_STRATEGY';
|
||||
export const REQUEST_STRATEGIES = 'REQUEST_STRATEGIES';
|
||||
export const START_CREATE_STRATEGY = 'START_CREATE_STRATEGY';
|
||||
export const RECEIVE_STRATEGIES = 'RECEIVE_STRATEGIES';
|
||||
export const ERROR_RECEIVE_STRATEGIES = 'ERROR_RECEIVE_STRATEGIES';
|
||||
export const ERROR_CREATING_STRATEGY = 'ERROR_CREATING_STRATEGY';
|
||||
|
||||
const addStrategy = (strategy) => ({ type: ADD_STRATEGY, strategy });
|
||||
const createRemoveStrategy = (strategy) => ({ type: REMOVE_STRATEGY, strategy });
|
||||
|
||||
const errorCreatingStrategy = (statusCode) => ({
|
||||
type: ERROR_CREATING_STRATEGY,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
const startRequest = () => ({ type: REQUEST_STRATEGIES });
|
||||
|
||||
|
||||
const receiveStrategies = (json) => ({
|
||||
type: RECEIVE_STRATEGIES,
|
||||
value: json.strategies,
|
||||
});
|
||||
|
||||
const startCreate = () => ({ type: START_CREATE_STRATEGY });
|
||||
|
||||
const errorReceiveStrategies = (statusCode) => ({
|
||||
type: ERROR_RECEIVE_STRATEGIES,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchStrategies () {
|
||||
return dispatch => {
|
||||
dispatch(startRequest());
|
||||
|
||||
return api.fetchAll()
|
||||
.then(json => dispatch(receiveStrategies(json)))
|
||||
.catch(error => dispatch(errorReceiveStrategies(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function createStrategy (strategy) {
|
||||
return dispatch => {
|
||||
dispatch(startCreate());
|
||||
|
||||
return api.create(strategy)
|
||||
.then(() => dispatch(addStrategy(strategy)))
|
||||
.catch(error => dispatch(errorCreatingStrategy(error)));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function removeStrategy (strategy) {
|
||||
return dispatch => api.remove(strategy)
|
||||
.then(() => dispatch(createRemoveStrategy(strategy)))
|
||||
.catch(error => dispatch(errorCreatingStrategy(error)));
|
||||
}
|
||||
|
||||
|
29
frontend/src/store/strategy-store.js
Normal file
29
frontend/src/store/strategy-store.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_STRATEGIES, REMOVE_STRATEGY, ADD_STRATEGY } from './strategy-actions';
|
||||
|
||||
function getInitState () {
|
||||
return new $Map({ list: new List() });
|
||||
}
|
||||
|
||||
function removeStrategy (state, action) {
|
||||
const indexToRemove = state.get('list').indexOf(action.strategy);
|
||||
if (indexToRemove !== -1) {
|
||||
return state.update('list', (list) => list.remove(indexToRemove));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const strategies = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_STRATEGIES:
|
||||
return state.set('list', new List(action.value));
|
||||
case REMOVE_STRATEGY:
|
||||
return removeStrategy(state, action);
|
||||
case ADD_STRATEGY:
|
||||
return state.update('list', (list) => list.push(action.strategy));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default strategies;
|
31
frontend/src/theme/_config.scss
Normal file
31
frontend/src/theme/_config.scss
Normal file
@ -0,0 +1,31 @@
|
||||
@import "~react-toolbox/lib/colors";
|
||||
@import "~react-toolbox/lib/globals";
|
||||
@import "~react-toolbox/lib/mixins";
|
||||
@import "~react-toolbox/lib/commons";
|
||||
|
||||
$color-primary:$palette-blue-400;
|
||||
$color-primary-dark: $palette-blue-700;
|
||||
|
||||
$navigation-drawer-desktop-width: 4 * $standard-increment-desktop !default;
|
||||
$navigation-drawer-max-desktop-width: 70 * $unit !default;
|
||||
|
||||
// Mobile:
|
||||
// Width = Screen width − 56 dp
|
||||
// Maximum width: 320dp
|
||||
$navigation-drawer-mobile-width: 5 * $standard-increment-mobile !default;
|
||||
|
||||
// sass doesn't like use of variable here: calc(100% - $standard-increment-mobile);
|
||||
$navigation-drawer-max-mobile-width: calc(100% - 5.6rem) !default;
|
||||
|
||||
.appBar {
|
||||
.leftIcon {
|
||||
transition-timing-function: $animation-curve-default;
|
||||
transition-duration: $animation-duration;
|
||||
transition-property: width, min-width;
|
||||
}
|
||||
@media screen and (min-width: $layout-breakpoint-sm) {
|
||||
.leftIcon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
86
frontend/webpack.config.js
Normal file
86
frontend/webpack.config.js
Normal file
@ -0,0 +1,86 @@
|
||||
// docs: http://webpack.github.io/docs/configuration.html
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: [
|
||||
'webpack-dev-server/client?http://localhost:3000',
|
||||
'webpack/hot/only-dev-server',
|
||||
'./src/index',
|
||||
],
|
||||
|
||||
resolve: {
|
||||
root: [path.join(__dirname, 'src')],
|
||||
extensions: ['', '.scss', '.css', '.js', '.jsx', '.json'],
|
||||
},
|
||||
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
publicPath: '/static/',
|
||||
},
|
||||
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
exclude: /node_modules/,
|
||||
loaders: ['babel'],
|
||||
include: path.join(__dirname, 'src'),
|
||||
},
|
||||
{
|
||||
test: /(\.scss|\.css)$/,
|
||||
loader: ExtractTextPlugin.extract('style',
|
||||
'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
new ExtractTextPlugin('bundle.css', { allChunks: true }),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
|
||||
sassLoader: {
|
||||
data: '@import "theme/_config.scss";',
|
||||
includePaths: [path.resolve(__dirname, './src')],
|
||||
},
|
||||
|
||||
devtool: 'source-map',
|
||||
|
||||
externals: {
|
||||
// stuff not in node_modules can be resolved here.
|
||||
},
|
||||
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/features': {
|
||||
target: 'http://localhost:4242',
|
||||
secure: false,
|
||||
},
|
||||
'/strategies': {
|
||||
target: 'http://localhost:4242',
|
||||
secure: false,
|
||||
},
|
||||
'/archive': {
|
||||
target: 'http://localhost:4242',
|
||||
secure: false,
|
||||
},
|
||||
'/events': {
|
||||
target: 'http://localhost:4242',
|
||||
secure: false,
|
||||
},
|
||||
'/metrics': {
|
||||
target: 'http://localhost:4242',
|
||||
secure: false,
|
||||
},
|
||||
'/client': {
|
||||
target: 'http://localhost:4242',
|
||||
secure: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user