mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-23 13:46:45 +02:00
Merge pull request #14 from Unleash/react-mdl-wip
Convert to React-MDL and feature creep
This commit is contained in:
commit
85640f4d04
16
frontend/.editorconfig
Normal file
16
frontend/.editorconfig
Normal file
@ -0,0 +1,16 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@ -36,3 +36,4 @@ jspm_packages
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
typings*
|
||||
|
@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<title>Unleash Admin</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/node_modules/react-mdl/extra/material.min.css">
|
||||
<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">
|
||||
|
@ -22,7 +22,7 @@
|
||||
"build": "npm run build:assets && npm run build:html",
|
||||
"build:assets": "NODE_ENV=production webpack -p",
|
||||
"build:html": "cp public/*.* dist/.",
|
||||
"start": "NODE_ENV=development webpack-dev-server --config webpack.config.js --hot --progress --colors --port 3000",
|
||||
"start": "NODE_ENV=development webpack-dev-server --config webpack.config.js --progress --colors --port 3000",
|
||||
"lint": "eslint . --ext=js,jsx",
|
||||
"test": "echo 'no test'",
|
||||
"test:ci": "npm run test",
|
||||
@ -34,7 +34,6 @@
|
||||
"immutability-helper": "^2.0.0",
|
||||
"immutable": "^3.8.1",
|
||||
"normalize.css": "^5.0.0",
|
||||
"percent": "^2.0.0",
|
||||
"react": "^15.3.1",
|
||||
"react-addons-css-transition-group": "^15.3.1",
|
||||
"react-dom": "^15.3.1",
|
||||
|
@ -20,6 +20,7 @@
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-shadow": 0
|
||||
"no-shadow": 0,
|
||||
"react/sort-comp": 0
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,114 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Layout, Drawer, Header, Navigation, Content,
|
||||
Footer, FooterSection, FooterDropDownSection, FooterLinkList,
|
||||
Grid, Cell,
|
||||
Grid, Cell, Icon,
|
||||
} from 'react-mdl';
|
||||
import { Link } from 'react-router';
|
||||
import style from './styles.scss';
|
||||
import ErrorContainer from './error/error-container';
|
||||
|
||||
import UserContainer from './user/user-container';
|
||||
import ShowUserContainer from './user/show-user-container';
|
||||
|
||||
export default class App extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = { drawerActive: false };
|
||||
const base = {
|
||||
name: 'Unleash',
|
||||
link: '/',
|
||||
};
|
||||
|
||||
this.toggleDrawerActive = () => {
|
||||
this.setState({ drawerActive: !this.state.drawerActive });
|
||||
function replace (input, params) {
|
||||
if (!params) {
|
||||
return input;
|
||||
}
|
||||
Object.keys(params).forEach(key => {
|
||||
input = input.replace(`:${key}`, params[key]);
|
||||
});
|
||||
return input;
|
||||
}
|
||||
|
||||
export default class App extends Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
location: PropTypes.object.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
routes: PropTypes.array.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.title = `${this.getCurrentSection()} - Unleash Admin`;
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.location.pathname !== nextProps.location.pathname) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
document.querySelector('.mdl-layout__content').scrollTop = 0;
|
||||
});
|
||||
|
||||
const layout = document.querySelector('.mdl-js-layout');
|
||||
const drawer = document.querySelector('.mdl-layout__drawer');
|
||||
// hack, might get a built in alternative later
|
||||
if (drawer.classList.contains('is-visible')) {
|
||||
layout.MaterialLayout.toggleDrawer();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentSection () {
|
||||
const { routes } = this.props;
|
||||
const lastRoute = routes[routes.length - 1];
|
||||
return lastRoute ? lastRoute.pageTitle : '';
|
||||
getSections () {
|
||||
const { routes, params } = this.props;
|
||||
const unique = {};
|
||||
let result = [base].concat(routes.splice(1).map((routeEntry) => ({
|
||||
name: replace(routeEntry.pageTitle, params),
|
||||
link: replace(routeEntry.link || routeEntry.path, params),
|
||||
}))).filter(entry => {
|
||||
if (!unique[entry.link]) {
|
||||
unique[entry.link] = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// mutate document.title:
|
||||
document.title = result
|
||||
.map(e => e.name)
|
||||
.reverse()
|
||||
.join(' - ');
|
||||
|
||||
if (result.length > 2) {
|
||||
result = result.splice(1);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
onOverlayClick = () => this.setState({ drawerActive: false });
|
||||
getTitleWithLinks () {
|
||||
const result = this.getSections();
|
||||
return (
|
||||
<span>
|
||||
{result.map((entry, index) => (
|
||||
<span key={entry.link + index}><Link style={{ color: '#f1f1f1', textDecoration: 'none' }} to={entry.link}>
|
||||
{entry.name}
|
||||
</Link> {(index + 1) < result.length ? ' / ' : null}</span>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
const createListItem = (path, caption) =>
|
||||
const createListItem = (path, caption, icon) =>
|
||||
<a
|
||||
href={this.context.router.createHref(path)}
|
||||
className={this.context.router.isActive(path) ? style.active : ''}>
|
||||
{caption}
|
||||
{icon && <Icon name={icon} />} {caption}
|
||||
</a>;
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
<UserContainer />
|
||||
<Layout fixedHeader>
|
||||
<Header title={<span><span style={{ color: '#ddd' }}>Unleash Admin / </span><strong>{this.getCurrentSection()}</strong></span>}>
|
||||
<Header title={this.getTitleWithLinks()}>
|
||||
<Navigation>
|
||||
<a href="https://github.com/Unleash" target="_blank">Github</a>
|
||||
<ShowUserContainer />
|
||||
@ -54,19 +116,15 @@ export default class App extends Component {
|
||||
</Header>
|
||||
<Drawer title="Unleash Admin">
|
||||
<Navigation>
|
||||
{createListItem('/features', 'Feature toggles')}
|
||||
{createListItem('/strategies', 'Strategies')}
|
||||
{createListItem('/history', 'Event history')}
|
||||
{createListItem('/archive', 'Archived toggles')}
|
||||
<hr />
|
||||
{createListItem('/applications', 'Applications')}
|
||||
{createListItem('/metrics', 'Client metrics')}
|
||||
{createListItem('/client-strategies', 'Client strategies')}
|
||||
{createListItem('/client-instances', 'Client instances')}
|
||||
{createListItem('/features', 'Feature toggles', 'list')}
|
||||
{createListItem('/strategies', 'Strategies', 'extension')}
|
||||
{createListItem('/history', 'Event history', 'history')}
|
||||
{createListItem('/archive', 'Archived toggles', 'archive')}
|
||||
{createListItem('/applications', 'Applications', 'apps')}
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
<Content>
|
||||
<Grid>
|
||||
<Grid shadow={1} style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<Cell col={12}>
|
||||
{this.props.children}
|
||||
<ErrorContainer />
|
||||
@ -85,18 +143,6 @@ export default class App extends Component {
|
||||
<FooterDropDownSection title="Metrics">
|
||||
<FooterLinkList>
|
||||
{createListItem('/applications', 'Applications')}
|
||||
{createListItem('/metrics', 'Client metrics')}
|
||||
{createListItem('/client-strategies', 'Client strategies')}
|
||||
{createListItem('/client-instances', 'Client instances')}
|
||||
</FooterLinkList>
|
||||
</FooterDropDownSection>
|
||||
<FooterDropDownSection title="FAQ">
|
||||
<FooterLinkList>
|
||||
<a href="#">Help</a>
|
||||
<a href="#">Privacy & Terms</a>
|
||||
<a href="#">Questions</a>
|
||||
<a href="#">Answers</a>
|
||||
<a href="#">Contact Us</a>
|
||||
</FooterLinkList>
|
||||
</FooterDropDownSection>
|
||||
<FooterDropDownSection title="Clients">
|
||||
@ -119,29 +165,5 @@ export default class App extends Component {
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={style.container}>
|
||||
<AppBar title="Unleash Admin" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive} className={style.appBar}>
|
||||
|
||||
</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>
|
||||
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,57 +1,170 @@
|
||||
import React, { Component } from 'react';
|
||||
/* eslint react/no-multi-comp:off */
|
||||
import React, { Component, PureComponent } from 'react';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import { Grid, Cell } from 'react-mdl';
|
||||
import {
|
||||
Grid, Cell,
|
||||
List, ListItem, ListItemContent,
|
||||
Textfield, Icon, ProgressBar,
|
||||
Tabs, Tab,
|
||||
Switch,
|
||||
} from 'react-mdl';
|
||||
import { HeaderTitle, ExternalIconLink } from '../common';
|
||||
|
||||
class ClientStrategies extends Component {
|
||||
class StatefulTextfield extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = { value: props.value };
|
||||
this.setValue = function setValue (e) {
|
||||
this.setState({ value: e.target.value });
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (<Textfield
|
||||
style={{ width: '100%' }}
|
||||
label={this.props.label}
|
||||
floatingLabel
|
||||
rows={this.props.rows}
|
||||
value={this.state.value}
|
||||
onChange={this.setValue}
|
||||
onBlur={this.props.onBlur} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClientApplications extends PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = { activeTab: 0 };
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchApplication(this.props.appName);
|
||||
}
|
||||
|
||||
render () {
|
||||
render () {
|
||||
if (!this.props.application) {
|
||||
return <div>Loading application info...</div>;
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
const {
|
||||
application,
|
||||
storeApplicationMetaData,
|
||||
} = this.props;
|
||||
const {
|
||||
appName,
|
||||
instances,
|
||||
strategies,
|
||||
seenToggles,
|
||||
} = this.props.application;
|
||||
|
||||
url,
|
||||
description,
|
||||
icon = 'apps',
|
||||
color,
|
||||
} = application;
|
||||
|
||||
const content = this.state.activeTab === 0 ? (
|
||||
<Grid>
|
||||
<Cell col={3} tablet={4} phone={12}>
|
||||
<h6> Toggles</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{seenToggles.map(({ name, description, enabled, notFound }, i) =>
|
||||
(notFound ?
|
||||
<ListItem twoLine key={i}>
|
||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
||||
<Link to={`/features/create?name=${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem> :
|
||||
<ListItem twoLine key={i}>
|
||||
<ListItemContent icon={<span><Switch disabled checked={!!enabled} /></span>} subtitle={description}>
|
||||
|
||||
<Link to={`/features/edit/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>)
|
||||
)}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={3} tablet={4} phone={12}>
|
||||
<h6>Implemented strategies</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{strategies.map(({ name, description, notFound }, i) => (
|
||||
notFound ?
|
||||
<ListItem twoLine key={`${name}-${i}`}>
|
||||
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
|
||||
<Link to={`/strategies/create?name=${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem> :
|
||||
<ListItem twoLine key={`${name}-${i}`}>
|
||||
<ListItemContent icon={'extension'} subtitle={description}>
|
||||
<Link to={`/strategies/view/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={6} tablet={12}>
|
||||
<h6>{instances.length} Instances connected</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{instances.map(({ instanceId, clientIp, lastSeen }, i) => (
|
||||
<ListItem key={i} twoLine>
|
||||
<ListItemContent
|
||||
icon="timeline"
|
||||
subtitle={
|
||||
<span>{clientIp} last seen at <small>{new Date(lastSeen).toLocaleString('nb-NO')}</small></span>
|
||||
}>
|
||||
{instanceId}
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Cell>
|
||||
</Grid>) : (
|
||||
<Grid>
|
||||
<Cell col={12}>
|
||||
<h5>Edit app meta data</h5>
|
||||
</Cell>
|
||||
<Cell col={6} tablet={12}>
|
||||
<StatefulTextfield
|
||||
value={url} label="URL" onBlur={(e) => storeApplicationMetaData(appName, 'url', e.target.value)} /><br />
|
||||
<StatefulTextfield
|
||||
value={description}
|
||||
label="Description" rows={5} onBlur={(e) => storeApplicationMetaData(appName, 'description', e.target.value)} />
|
||||
</Cell>
|
||||
<Cell col={6} tablet={12}>
|
||||
<StatefulTextfield
|
||||
value={icon} label="Select icon" onBlur={(e) => storeApplicationMetaData(appName, 'icon', e.target.value)} />
|
||||
<StatefulTextfield
|
||||
value={color} label="Select color" onBlur={(e) => storeApplicationMetaData(appName, 'color', e.target.value)} />
|
||||
</Cell>
|
||||
</Grid>);
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>{appName}</h5>
|
||||
|
||||
<Grid>
|
||||
<Cell col={4}>
|
||||
<h6>Instances</h6>
|
||||
<ol className="demo-list-item mdl-list">
|
||||
{instances.map(({ instanceId }, i) => <li className="mdl-list__item" key={i}>{instanceId}</li>)}
|
||||
</ol>
|
||||
</Cell>
|
||||
<Cell col={4}>
|
||||
<h6>Strategies</h6>
|
||||
<ol className="demo-list-item mdl-list">
|
||||
{/*strategies.map((name, i) => <li className="mdl-list__item" key={i}>{name}</li>)*/}
|
||||
</ol>
|
||||
</Cell>
|
||||
<Cell col={4}>
|
||||
<h6>Toggles</h6>
|
||||
<ol className="demo-list-item mdl-list">
|
||||
{seenToggles.map((name, i) => <li className="mdl-list__item" key={i}>
|
||||
<Link to={`/features/edit/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</li>)}
|
||||
</ol>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<HeaderTitle title={<span><Icon name={icon} /> {appName}</span>} subtitle={description}
|
||||
actions={url && <ExternalIconLink url={url}>Visit site</ExternalIconLink>}
|
||||
/>
|
||||
|
||||
<Tabs activeTab={this.state.activeTab} onChange={(tabId) => this.setState({ activeTab: tabId })} ripple>
|
||||
<Tab>Metrics</Tab>
|
||||
<Tab>Edit</Tab>
|
||||
</Tabs>
|
||||
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientStrategies;
|
||||
export default ClientApplications;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ApplicationEdit from './application-edit-component';
|
||||
import { fetchApplication } from '../../store/application/actions';
|
||||
import { fetchApplication, storeApplicationMetaData } from '../../store/application/actions';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
let application = state.applications.getIn(['apps', props.appName]);
|
||||
@ -12,6 +12,9 @@ const mapStateToProps = (state, props) => {
|
||||
};
|
||||
};
|
||||
|
||||
const Constainer = connect(mapStateToProps, { fetchApplication })(ApplicationEdit);
|
||||
const Constainer = connect(mapStateToProps, {
|
||||
fetchApplication,
|
||||
storeApplicationMetaData,
|
||||
})(ApplicationEdit);
|
||||
|
||||
export default Constainer;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { ProgressBar } from 'react-mdl';
|
||||
import { AppsLinkList, HeaderTitle } from '../common';
|
||||
|
||||
class ClientStrategies extends Component {
|
||||
|
||||
@ -13,15 +14,12 @@ class ClientStrategies extends Component {
|
||||
} = this.props;
|
||||
|
||||
if (!applications) {
|
||||
return <div>loading...</div>;
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{applications.map(item => (
|
||||
<Link key={item.appName} to={`/applications/${item.appName}`}>
|
||||
Link: {item.appName}
|
||||
</Link>
|
||||
))}
|
||||
<HeaderTitle title="Applications" />
|
||||
<AppsLinkList apps={applications} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DataTable, TableHeader, Chip, Switch, IconButton } from 'react-mdl';
|
||||
import { Link } from 'react-router';
|
||||
import { DataTable, TableHeader, IconButton, Icon } from 'react-mdl';
|
||||
import { HeaderTitle } from '../common';
|
||||
|
||||
class ArchiveList extends Component {
|
||||
componentDidMount () {
|
||||
@ -8,20 +10,29 @@ class ArchiveList extends Component {
|
||||
|
||||
render () {
|
||||
const { archive, revive } = this.props;
|
||||
archive.forEach(e => {
|
||||
e.reviveName = e.name;
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<h6>Toggle Archive</h6>
|
||||
|
||||
<DataTable
|
||||
rows={archive}
|
||||
style={{ width: '100%' }}>
|
||||
<TableHeader style={{ width: '25px' }} name="strategies" cellFormatter={(name) => (
|
||||
<IconButton colored name="undo" onClick={() => revive(name)} />
|
||||
)}>Revive</TableHeader>
|
||||
<TableHeader style={{ width: '25px' }} name="enabled" cellFormatter={(v) => (v ? 'Yes' : '-')}>Enabled</TableHeader>
|
||||
<TableHeader name="name">Toggle name</TableHeader>
|
||||
<TableHeader numeric name="createdAt">Created</TableHeader>
|
||||
</DataTable>
|
||||
<HeaderTitle title="Toggle Archive" />
|
||||
{
|
||||
archive.length > 0 ?
|
||||
<DataTable
|
||||
rows={archive}
|
||||
style={{ width: '100%' }}>
|
||||
<TableHeader style={{ width: '25px' }} name="reviveName" cellFormatter={(reviveName) => (
|
||||
<IconButton colored name="undo" onClick={() => revive(reviveName)} />
|
||||
)}>Revive</TableHeader>
|
||||
<TableHeader style={{ width: '25px' }} name="enabled" cellFormatter={(v) => (v ? 'Yes' : '-')}>Enabled</TableHeader>
|
||||
<TableHeader name="name">Toggle name</TableHeader>
|
||||
<TableHeader numeric name="createdAt">Created</TableHeader>
|
||||
</DataTable> :
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Icon name="report" style={{ color: '#aaa', fontSize: '40px' }}/><br />
|
||||
No archived feature toggles, go see <Link to="/features">active toggles here</Link>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -22,14 +22,14 @@ class ClientStrategies extends Component {
|
||||
rows={source}
|
||||
selectable={false}
|
||||
>
|
||||
|
||||
|
||||
|
||||
|
||||
<TableHeader name="instanceId">Instance ID</TableHeader>
|
||||
<TableHeader name="appName">Application name</TableHeader>
|
||||
<TableHeader name="clientIp">IP</TableHeader>
|
||||
<TableHeader name="createdAt">Created</TableHeader>
|
||||
<TableHeader name="lastSeen">Last Seen</TableHeader>
|
||||
|
||||
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DataTable, TableHeader } from 'react-mdl';
|
||||
|
||||
class ClientStrategies extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchClientStrategies();
|
||||
}
|
||||
|
||||
render () {
|
||||
const source = this.props.clientStrategies
|
||||
// temp hack for ignoring dumb data
|
||||
.filter(item => item.strategies)
|
||||
.map(item => (
|
||||
{
|
||||
appName: item.appName,
|
||||
strategies: item.strategies && item.strategies.join(', '),
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
style={{ width: '100%' }}
|
||||
rows={source}
|
||||
selectable={false}
|
||||
>
|
||||
<TableHeader name="appName">Application name</TableHeader>
|
||||
<TableHeader name="strategies">Strategies</TableHeader>
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientStrategies;
|
@ -1,9 +0,0 @@
|
||||
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;
|
5
frontend/src/component/common/common.scss
Normal file
5
frontend/src/component/common/common.scss
Normal file
@ -0,0 +1,5 @@
|
||||
.truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
119
frontend/src/component/common/index.js
Normal file
119
frontend/src/component/common/index.js
Normal file
@ -0,0 +1,119 @@
|
||||
const React = require('react');
|
||||
import styles from './common.scss';
|
||||
|
||||
|
||||
const {
|
||||
List, ListItem, ListItemContent,
|
||||
Button, Icon,
|
||||
Switch,
|
||||
} = require('react-mdl');
|
||||
const { Link } = require('react-router');
|
||||
|
||||
export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str);
|
||||
|
||||
export const AppsLinkList = ({ apps }) => (
|
||||
<List style={{ textAlign: 'left' }}>
|
||||
{apps.length > 0 && apps.map(({ appName, description = '-', icon = 'apps' }) => (
|
||||
<ListItem twoLine key={appName}>
|
||||
<ListItemContent avatar={icon} subtitle={shorten(description)}>
|
||||
<Link key={appName} to={`/applications/${appName}`}>
|
||||
{appName}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
export const HeaderTitle = ({ title, actions, subtitle }) => (
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #f1f1f1', marginBottom: '10px', padding: '16px 20px ' }}>
|
||||
<div style={{ flex: '2' }}>
|
||||
<h6 style={{ margin: 0 }}>{title}</h6>
|
||||
{subtitle && <small>{subtitle}</small>}
|
||||
</div>
|
||||
|
||||
{actions && <div style={{ flex: '1', textAlign: 'right' }}>{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const FormButtons = ({ submitText = 'Create', onCancel }) => (
|
||||
<div>
|
||||
<Button type="submit" ripple raised primary icon="add">
|
||||
<Icon name="add" />
|
||||
{ submitText }
|
||||
</Button>
|
||||
|
||||
<Button type="cancel" ripple raised onClick={onCancel} style={{ float: 'right' }}>
|
||||
<Icon name="cancel" />
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SwitchWithLabel = ({ onChange, children, checked }) => (
|
||||
<span>
|
||||
<span style={{ cursor: 'pointer', display: 'inline-block', width: '45px' }}>
|
||||
<Switch onChange={onChange} checked={checked} />
|
||||
</span>
|
||||
<span>{children}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
export const TogglesLinkList = ({ toggles }) => (
|
||||
<List style={{ textAlign: 'left' }} className={styles.truncate}>
|
||||
{toggles.length > 0 && toggles.map(({ name, description = '-', icon = 'toggle' }) => (
|
||||
<ListItem twoLine key={name}>
|
||||
<ListItemContent avatar={icon} subtitle={description}>
|
||||
<Link key={name} to={`/features/edit/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
export function getIcon (type) {
|
||||
switch (type) {
|
||||
case 'feature-updated': return 'autorenew';
|
||||
case 'feature-created': return 'add';
|
||||
case 'feature-deleted': return 'remove';
|
||||
case 'feature-archived': return 'archived';
|
||||
default: return 'star';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const IconLink = ({ icon, children, ...props }) => (
|
||||
<a {...props} style={{ textDecoration: 'none' }}>
|
||||
<Icon name={icon} style={{ marginRight: '5px', verticalAlign: 'middle' }}/>
|
||||
<span style={{ textDecoration: 'none', verticalAlign: 'middle' }}>{children}</span>
|
||||
</a>
|
||||
);
|
||||
|
||||
export const ExternalIconLink = ({ url, children }) => (
|
||||
<IconLink icon="queue" href={url} target="_blank" rel="noopener">
|
||||
{children}
|
||||
</IconLink>
|
||||
);
|
||||
|
||||
const badNumbers = [NaN, Infinity, -Infinity];
|
||||
export function calc (value, total, decimal) {
|
||||
if (typeof value !== 'number' ||
|
||||
typeof total !== 'number' ||
|
||||
typeof decimal !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
badNumbers.forEach((number) => {
|
||||
if ([value, total, decimal].indexOf(number) > -1) {
|
||||
return number;
|
||||
}
|
||||
});
|
||||
|
||||
return (value / total * 100).toFixed(decimal);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
import { Snackbar } from 'react-mdl';
|
||||
import { Snackbar, Icon } from 'react-mdl';
|
||||
|
||||
class ErrorComponent extends React.Component {
|
||||
static propTypes () {
|
||||
@ -18,13 +18,12 @@ class ErrorComponent extends React.Component {
|
||||
<Snackbar
|
||||
action="Dismiss"
|
||||
active={showError}
|
||||
icon="question_answer"
|
||||
timeout={10000}
|
||||
label={error}
|
||||
onClick={muteError}
|
||||
onActionClick={muteError}
|
||||
onTimeout={muteError}
|
||||
type="warning"
|
||||
/>
|
||||
timeout={10000}
|
||||
>
|
||||
<Icon name="question_answer" /> {error}
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import { Chip, Switch, Icon, IconButton } from 'react-mdl';
|
||||
import percentLib from 'percent';
|
||||
import Progress from './progress';
|
||||
import { shorten, calc } from '../common';
|
||||
|
||||
import style from './feature.scss';
|
||||
|
||||
@ -20,8 +20,8 @@ const Feature = ({
|
||||
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
||||
|
||||
const percent = 1 * (showLastHour ?
|
||||
percentLib.calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) :
|
||||
percentLib.calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)
|
||||
calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) :
|
||||
calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)
|
||||
);
|
||||
return (
|
||||
<li key={name} className="mdl-list__item">
|
||||
@ -29,19 +29,21 @@ const Feature = ({
|
||||
<div style={{ width: '40px', textAlign: 'center' }}>
|
||||
{
|
||||
isStale ?
|
||||
<Icon style={{ width: '25px', marginTop: '4px', fontSize: '25px', color: '#ccc' }} name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={15} percentage={percent} width="50" />
|
||||
</div>
|
||||
<Icon
|
||||
style={{ width: '25px', marginTop: '4px', fontSize: '25px', color: '#ccc' }}
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={15} percentage={percent} width="50" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<span style={{ display: 'inline-block', width: '45px' }} title={`Toggle ${name}`}>
|
||||
<span style={{ display: 'inline-block', width: '45px' }} title={`Toggle ${name}`}>
|
||||
<Switch title="test" key="left-actions" onChange={() => onFeatureClick(feature)} checked={enabled} />
|
||||
</span>
|
||||
<Link to={`/features/edit/${name}`} className={style.link}>
|
||||
{name} <small>{(description && description.substring(0, 100)) || ''}</small>
|
||||
<Link to={`/features/view/${name}`} className={style.link}>
|
||||
{name} <small>{shorten(description, 30) || ''}</small>
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
@ -52,7 +54,7 @@ const Feature = ({
|
||||
<Link to={`/features/edit/${name}`} title={`Edit ${name}`} className={style.iconListItem}>
|
||||
<IconButton name="edit" />
|
||||
</Link>
|
||||
<Link to={`/history/${name}`} title={`History htmlFor ${name}`} className={style.iconListItem}>
|
||||
<Link to={`features/history/${name}`} title={`History htmlFor ${name}`} className={style.iconListItem}>
|
||||
<IconButton name="history" />
|
||||
</Link>
|
||||
<IconButton name="delete" onClick={() => onFeatureRemove(name)} className={style.iconListItem} />
|
||||
|
@ -58,4 +58,10 @@
|
||||
|
||||
.topListItem2 {
|
||||
flex: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.iconListItemChip {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,23 @@ import { createMapper, createActions } from '../input-helpers';
|
||||
import FormComponent from './form';
|
||||
|
||||
const ID = 'add-feature-toggle';
|
||||
const mapStateToProps = createMapper({ id: ID });
|
||||
const mapStateToProps = createMapper({
|
||||
id: ID,
|
||||
getDefault () {
|
||||
let name;
|
||||
try {
|
||||
[, name] = document.location.hash.match(/name=([a-z0-9-_]+)/i);
|
||||
} catch (e) {}
|
||||
return { name };
|
||||
},
|
||||
});
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
createFeatureToggles(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push('/features'));
|
||||
.then(() => hashHistory.push(`/features/edit/${input.name}`));
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -27,14 +27,14 @@ const prepare = (methods, dispatch) => {
|
||||
// TODO: should add error handling
|
||||
requestUpdateFeatureToggle(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => window.history.back());
|
||||
.then(() => hashHistory.push(`/features/view/${input.name}`));
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (evt) => {
|
||||
evt.preventDefault();
|
||||
methods.clear();
|
||||
hashHistory.push('/features');
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
methods.addStrategy = (v) => {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Textfield, Button, Switch } from 'react-mdl';
|
||||
import { Textfield, Switch } from 'react-mdl';
|
||||
import StrategiesSection from './strategies-section-container';
|
||||
|
||||
import { FormButtons, HeaderTitle } from '../../common';
|
||||
|
||||
const trim = (value) => {
|
||||
if (value && value.trim) {
|
||||
return value.trim();
|
||||
@ -30,6 +32,7 @@ class AddFeatureToggleComponent extends Component {
|
||||
onSubmit,
|
||||
onCancel,
|
||||
editmode = false,
|
||||
title,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -42,8 +45,10 @@ class AddFeatureToggleComponent extends Component {
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
{title && <HeaderTitle title={title} />}
|
||||
<section>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
label="Name"
|
||||
name="name"
|
||||
disabled={editmode}
|
||||
@ -54,7 +59,9 @@ class AddFeatureToggleComponent extends Component {
|
||||
onChange={(v) => setValue('name', trim(v.target.value))} />
|
||||
<br />
|
||||
<Textfield
|
||||
rows={2}
|
||||
floatingLabel
|
||||
style={{ width: '100%' }}
|
||||
rows={5}
|
||||
label="Description"
|
||||
required
|
||||
value={description}
|
||||
@ -64,11 +71,10 @@ class AddFeatureToggleComponent extends Component {
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={(v) => {
|
||||
// todo is wrong way to get value?
|
||||
setValue('enabled', (console.log(v.target) && v.target.value === 'on'));
|
||||
onChange={() => {
|
||||
setValue('enabled', !enabled);
|
||||
}}>Enabled</Switch>
|
||||
<br />
|
||||
<hr />
|
||||
</section>
|
||||
|
||||
<StrategiesSection
|
||||
@ -78,9 +84,10 @@ class AddFeatureToggleComponent extends Component {
|
||||
removeStrategy={removeStrategy} />
|
||||
|
||||
<br />
|
||||
<Button type="submit" raised primary>{editmode ? 'Update' : 'Create'}</Button>
|
||||
|
||||
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
|
||||
<FormButtons
|
||||
submitText={editmode ? 'Update' : 'Create'}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -14,9 +14,8 @@ class AddStrategy extends React.Component {
|
||||
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] = ''; });
|
||||
|
||||
selectedStrategy.parameters.forEach(({ name }) => { parameters[name] = ''; });
|
||||
|
||||
this.props.addStrategy({
|
||||
name: selectedStrategy.name,
|
||||
@ -30,13 +29,19 @@ class AddStrategy extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const menuStyle = {
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'rgb(247, 248, 255)',
|
||||
};
|
||||
return (
|
||||
<div style={{ position: 'relative', width: '25px', height: '25px', display: 'inline-block' }} >
|
||||
<IconButton name="add" id="strategies-add" colored title="Sort" onClick={this.stopPropagation}/>
|
||||
<Menu target="strategies-add" valign="bottom" align="left" ripple onClick={
|
||||
(e) => this.setSort(e.target.getAttribute('data-target'))}>
|
||||
<IconButton name="add" id="strategies-add" raised accent title="Add Strategy" onClick={this.stopPropagation}/>
|
||||
<Menu target="strategies-add" valign="bottom" align="right" ripple style={menuStyle}>
|
||||
<MenuItem disabled>Add Strategy:</MenuItem>
|
||||
{this.props.strategies.map((s) => <MenuItem key={s.name} onClick={() => this.addStrategy(s.name)}>{s.name}</MenuItem>)}
|
||||
{this.props.strategies.map((s) =>
|
||||
<MenuItem key={s.name} title={s.description} onClick={() => this.addStrategy(s.name)}>{s.name}</MenuItem>)
|
||||
}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
@ -22,16 +22,16 @@ class StrategiesList extends React.Component {
|
||||
return <i style={{ color: 'red' }}>No strategies added</i>;
|
||||
}
|
||||
|
||||
const blocks = configuredStrategies.map((strat, i) => (
|
||||
const blocks = configuredStrategies.map((strategy, i) => (
|
||||
<ConfigureStrategy
|
||||
key={`${strat.name}-${i}`}
|
||||
strategy={strat}
|
||||
key={`${strategy.name}-${i}`}
|
||||
strategy={strategy}
|
||||
removeStrategy={this.props.removeStrategy.bind(null, i)}
|
||||
updateStrategy={this.props.updateStrategy.bind(null, i)}
|
||||
strategyDefinition={strategies.find(s => s.name === strat.name)} />
|
||||
strategyDefinition={strategies.find(s => s.name === strategy.name)} />
|
||||
));
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||
{blocks}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StrategiesSection from './strategies-section';
|
||||
import { fetchStrategies } from '../../../store/strategy-actions';
|
||||
import { fetchStrategies } from '../../../store/strategy/actions';
|
||||
|
||||
|
||||
export default connect((state) => ({
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { ProgressBar } from 'react-mdl';
|
||||
import StrategiesList from './strategies-list';
|
||||
import AddStrategy from './strategies-add';
|
||||
|
||||
const headerStyle = {
|
||||
marginBottom: '10px',
|
||||
};
|
||||
import { HeaderTitle } from '../../common';
|
||||
|
||||
class StrategiesSection extends React.Component {
|
||||
|
||||
@ -24,12 +22,12 @@ class StrategiesSection extends React.Component {
|
||||
|
||||
render () {
|
||||
if (!this.props.strategies || this.props.strategies.length === 0) {
|
||||
return <i>Loding available strategies</i>;
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5 style={headerStyle}>Activation strategies <AddStrategy {...this.props} /> </h5>
|
||||
<HeaderTitle title="Activation strategies" actions={<AddStrategy {...this.props} />} />
|
||||
<StrategiesList {...this.props} />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,6 +1,20 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Textfield, Button } from 'react-mdl';
|
||||
import {
|
||||
Textfield, Button,
|
||||
Card, CardTitle, CardText, CardActions, CardMenu,
|
||||
IconButton, Icon,
|
||||
} from 'react-mdl';
|
||||
import { Link } from 'react-router';
|
||||
import StrategyInputPersentage from './strategy-input-persentage';
|
||||
import StrategyInputList from './strategy-input-list';
|
||||
|
||||
const style = {
|
||||
flex: '1',
|
||||
minWidth: '300px',
|
||||
maxWidth: '100%',
|
||||
margin: '5px 20px 15px 0px',
|
||||
background: '#f2f9fc',
|
||||
};
|
||||
class StrategyConfigure extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
@ -12,57 +26,144 @@ class StrategyConfigure extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
// shouldComponentUpdate (props, nextProps) {
|
||||
// console.log({ props, nextProps });
|
||||
// }
|
||||
|
||||
handleConfigChange = (key, e) => {
|
||||
this.setConfig(key, e.target.value);
|
||||
};
|
||||
|
||||
setConfig = (key, value) => {
|
||||
const parameters = this.props.strategy.parameters || {};
|
||||
parameters[key] = e.target.value;
|
||||
parameters[key] = value;
|
||||
|
||||
const updatedStrategy = Object.assign({}, this.props.strategy, { parameters });
|
||||
|
||||
this.props.updateStrategy(updatedStrategy);
|
||||
};
|
||||
}
|
||||
|
||||
handleRemove = (evt) => {
|
||||
evt.preventDefault();
|
||||
this.props.removeStrategy();
|
||||
}
|
||||
|
||||
renderInputFields (strategyDefinition) {
|
||||
if (strategyDefinition.parametersTemplate) {
|
||||
return Object.keys(strategyDefinition.parametersTemplate).map(field => (
|
||||
<Textfield
|
||||
key={field}
|
||||
name={field}
|
||||
label={field}
|
||||
onChange={this.handleConfigChange.bind(this, field)}
|
||||
value={this.props.strategy.parameters[field]}
|
||||
/>
|
||||
));
|
||||
renderInputFields ({ parameters }) {
|
||||
if (parameters && parameters.length > 0) {
|
||||
return parameters.map(({ name, type, description, required }) => {
|
||||
let value = this.props.strategy.parameters[name];
|
||||
if (type === 'percentage') {
|
||||
if (value == null || (typeof value === 'string' && value === '')) {
|
||||
value = 50; // default value
|
||||
}
|
||||
return (
|
||||
<div key={name}>
|
||||
<StrategyInputPersentage
|
||||
name={name}
|
||||
onChange={this.handleConfigChange.bind(this, name)}
|
||||
value={1 * value} />
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'list') {
|
||||
let list = [];
|
||||
if (typeof value === 'string') {
|
||||
list = value
|
||||
.trim()
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
}
|
||||
return (
|
||||
<div key={name}>
|
||||
<StrategyInputList name={name} list={list} setConfig={this.setConfig} />
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'number') {
|
||||
return (
|
||||
<div key={name}>
|
||||
<Textfield
|
||||
pattern="-?[0-9]*(\.[0-9]+)?"
|
||||
error={`${name} is not a number!`}
|
||||
floatingLabel
|
||||
required={required}
|
||||
style={{ width: '100%' }}
|
||||
name={name}
|
||||
label={name}
|
||||
onChange={this.handleConfigChange.bind(this, name)}
|
||||
value={value}
|
||||
/>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={name}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
rows={2}
|
||||
style={{ width: '100%' }}
|
||||
required={required}
|
||||
name={name}
|
||||
label={name}
|
||||
onChange={this.handleConfigChange.bind(this, name)}
|
||||
value={value}
|
||||
/>
|
||||
{description && <p>{description}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.strategyDefinition) {
|
||||
const { name } = this.props.strategy;
|
||||
return (
|
||||
<div>
|
||||
<h6><span style={{ color: 'red' }}>Strategy "{this.props.strategy.name}" deleted</span></h6>
|
||||
<Button onClick={this.handleRemove} icon="remove" label="remove strategy" flat/>
|
||||
</div>
|
||||
<Card shadow={0} style={style}>
|
||||
<CardTitle>"{name}" deleted?</CardTitle>
|
||||
<CardText>
|
||||
The strategy "{name}" does not exist on this server.
|
||||
<Link to={`/strategies/create?name=${name}`}>Want to create it now?</Link>
|
||||
</CardText>
|
||||
<CardActions>
|
||||
<Button onClick={this.handleRemove} label="remove strategy" accent raised>Remove</Button>
|
||||
</CardActions>
|
||||
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const inputFields = this.renderInputFields(this.props.strategyDefinition) || [];
|
||||
const inputFields = this.renderInputFields(this.props.strategyDefinition);
|
||||
|
||||
const { name } = this.props.strategy;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '5px 15px', backgroundColor: '#f7f8ff', marginBottom: '10px' }}>
|
||||
<h6>
|
||||
<strong>{this.props.strategy.name} </strong>
|
||||
(<a style={{ color: '#ff4081' }} onClick={this.handleRemove} href="#remove-strat">remove</a>)
|
||||
</h6>
|
||||
<small>{this.props.strategyDefinition.description}</small>
|
||||
<div>
|
||||
{inputFields}
|
||||
</div>
|
||||
</div>
|
||||
<Card shadow={0} style={style}>
|
||||
<CardTitle style={{ color: '#fff', height: '65px', background: '#607d8b' }}>
|
||||
<Icon name="extension" /> { name }
|
||||
</CardTitle>
|
||||
<CardText>
|
||||
{this.props.strategyDefinition.description}
|
||||
</CardText>
|
||||
{
|
||||
inputFields && <CardActions border style={{ padding: '20px' }}>
|
||||
{inputFields}
|
||||
</CardActions>
|
||||
}
|
||||
|
||||
<CardMenu style={{ color: '#fff' }}>
|
||||
<Link
|
||||
title="View strategy"
|
||||
to={`/strategies/view/${name}`}
|
||||
style={{ color: '#fff', display: 'inline-block', verticalAlign: 'bottom', marginRight: '5px' }}>
|
||||
<Icon name="link" />
|
||||
</Link>
|
||||
<IconButton title="Remove strategy from toggle" name="delete" onClick={this.handleRemove} />
|
||||
</CardMenu>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
80
frontend/src/component/feature/form/strategy-input-list.jsx
Normal file
80
frontend/src/component/feature/form/strategy-input-list.jsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import {
|
||||
Textfield,
|
||||
IconButton,
|
||||
Chip,
|
||||
} from 'react-mdl';
|
||||
|
||||
export default class InputList extends Component {
|
||||
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
list: PropTypes.array.isRequired,
|
||||
setConfig: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
onBlur = (e) => {
|
||||
this.setValue(e);
|
||||
window.removeEventListener('keydown', this.onKeyHandler, false);
|
||||
}
|
||||
|
||||
onFocus = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.addEventListener('keydown', this.onKeyHandler, false);
|
||||
}
|
||||
|
||||
onKeyHandler = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.setValue();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
setValue = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
const { name, list, setConfig } = this.props;
|
||||
const inputValue = document.querySelector(`[name="${name}_input"]`);
|
||||
if (inputValue && inputValue.value) {
|
||||
list.push(inputValue.value);
|
||||
inputValue.value = '';
|
||||
setConfig(name, list.join(','));
|
||||
}
|
||||
}
|
||||
|
||||
onClose (index) {
|
||||
const { name, list, setConfig } = this.props;
|
||||
list[index] = null;
|
||||
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','));
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, list } = this.props;
|
||||
return (<div>
|
||||
<p>{name}</p>
|
||||
{list.map((entryValue, index) => (
|
||||
<Chip
|
||||
key={index + entryValue}
|
||||
style={{ marginRight: '3px' }}
|
||||
onClose={() => this.onClose(index)}>{entryValue}</Chip>
|
||||
))}
|
||||
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Textfield
|
||||
name={`${name}_input`}
|
||||
style={{ width: '100%', flex: 1 }}
|
||||
floatingLabel
|
||||
label="Add list entry"
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur} />
|
||||
<IconButton name="add" raised style={{ flex: 1, flexGrow: 0, margin: '20px 0 0 10px' }} onClick={this.setValue} />
|
||||
</div>
|
||||
|
||||
</div>);
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Slider } from 'react-mdl';
|
||||
|
||||
const labelStyle = {
|
||||
margin: '20px 0',
|
||||
textAlign: 'center',
|
||||
color: '#3f51b5',
|
||||
fontSize: '12px',
|
||||
};
|
||||
|
||||
export default ({ name, value, onChange }) => (
|
||||
<div>
|
||||
<div style={labelStyle}>{name}: {value}%</div>
|
||||
<Slider min={0} max={100} defaultValue={value} value={value} onChange={onChange} label={name} />
|
||||
</div>
|
||||
);
|
@ -52,7 +52,6 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.topList}>
|
||||
|
||||
<Chip onClick={() => this.toggleMetrics()} className={styles.topListItem0}>
|
||||
{ settings.showLastHour &&
|
||||
<ChipContact className="mdl-color--teal mdl-color-text--white">
|
||||
@ -68,8 +67,7 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
</ChipContact> }
|
||||
{ '1 minute' }
|
||||
</Chip>
|
||||
|
||||
|
||||
|
||||
<div className={styles.topListItem2} style={{ margin: '-10px 10px 0 10px' }}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
@ -79,7 +77,7 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<div style={{ position: 'relative' }} className={styles.topListItem0}>
|
||||
<IconButton name="sort" id="demo-menu-top-right" colored title="Sort" />
|
||||
<Menu target="demo-menu-top-right" valign="bottom" align="right" ripple onClick={
|
||||
@ -95,23 +93,20 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
</Menu>
|
||||
</div>
|
||||
<Link to="/features/create" className={styles.topListItem0}>
|
||||
<FABButton ripple component="span" mini>
|
||||
<Icon name="add" />
|
||||
</FABButton>
|
||||
<IconButton ripple raised name="add" component="span" style={{ color: 'black' }}/>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
<ul className="demo-list-item mdl-list">
|
||||
{features.map((feature, i) =>
|
||||
<Feature key={i}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
onFeatureClick={onFeatureClick}
|
||||
onFeatureRemove={onFeatureRemove}/>
|
||||
)}
|
||||
{features.map((feature, i) =>
|
||||
<Feature key={i}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
onFeatureClick={onFeatureClick}
|
||||
onFeatureRemove={onFeatureRemove}/>
|
||||
)}
|
||||
</ul>
|
||||
<hr />
|
||||
<Link to="/features/create" className={styles.topListItem0}>
|
||||
|
84
frontend/src/component/feature/metric-component.jsx
Normal file
84
frontend/src/component/feature/metric-component.jsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Grid, Cell, Icon } from 'react-mdl';
|
||||
import Progress from './progress';
|
||||
import { AppsLinkList, SwitchWithLabel, calc } from '../common';
|
||||
|
||||
|
||||
export default class MetricComponent extends React.Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
metrics: PropTypes.object.isRequired,
|
||||
featureToggle: PropTypes.object.isRequired,
|
||||
toggleFeature: PropTypes.func.isRequired,
|
||||
fetchSeenApps: PropTypes.func.isRequired,
|
||||
fetchFeatureMetrics: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.props.fetchSeenApps();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
this.props.fetchFeatureMetrics();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { metrics = {}, featureToggle, toggleFeature } = this.props;
|
||||
const {
|
||||
lastHour = { yes: 0, no: 0, isFallback: true },
|
||||
lastMinute = { yes: 0, no: 0, isFallback: true },
|
||||
seenApps = [],
|
||||
} = metrics;
|
||||
|
||||
const lastHourPercent = 1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent = 1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
|
||||
return (<div>
|
||||
<SwitchWithLabel
|
||||
checked={featureToggle.enabled}
|
||||
onChange={() => toggleFeature(featureToggle)}>Toggle {featureToggle.name}</SwitchWithLabel>
|
||||
<hr />
|
||||
<Grid style={{ textAlign: 'center' }}>
|
||||
<Cell tablet={4} col={3} phone={12}>
|
||||
{
|
||||
lastMinute.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress animatePercentageText strokeWidth={10} percentage={lastMinutePercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last minute</strong><br /> Yes {lastMinute.yes}, No: {lastMinute.no}</p>
|
||||
</Cell>
|
||||
<Cell col={3} tablet={4} phone={12}>
|
||||
{
|
||||
lastHour.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={10} percentage={lastHourPercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last hour</strong><br /> Yes {lastHour.yes}, No: {lastHour.no}</p>
|
||||
</Cell>
|
||||
<Cell col={6} tablet={12}>
|
||||
{seenApps.length > 0 ?
|
||||
(<div><strong>Seen in applications:</strong></div>) :
|
||||
<div>
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="Not used in a app in the last hour" />
|
||||
<div><small><strong>Not used in a app in the last hour.</strong>
|
||||
This might be due to your client implementation is not reporting usage.</small></div>
|
||||
</div>
|
||||
}
|
||||
<AppsLinkList apps={seenApps} />
|
||||
</Cell>
|
||||
</Grid>
|
||||
</div>);
|
||||
}
|
||||
}
|
32
frontend/src/component/feature/metric-container.jsx
Normal file
32
frontend/src/component/feature/metric-container.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions';
|
||||
import { toggleFeature } from '../../store/feature-actions';
|
||||
|
||||
import MatricComponent from './metric-component';
|
||||
|
||||
function getMetricsForToggle (state, toggleName) {
|
||||
if (!toggleName) {
|
||||
return;
|
||||
}
|
||||
const result = {};
|
||||
|
||||
if (state.featureMetrics.hasIn(['seenApps', toggleName])) {
|
||||
result.seenApps = state.featureMetrics.getIn(['seenApps', toggleName]);
|
||||
}
|
||||
if (state.featureMetrics.hasIn(['lastHour', toggleName])) {
|
||||
result.lastHour = state.featureMetrics.getIn(['lastHour', toggleName]);
|
||||
result.lastMinute = state.featureMetrics.getIn(['lastMinute', toggleName]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export default connect((state, props) => ({
|
||||
metrics: getMetricsForToggle(state, props.featureToggleName),
|
||||
}), {
|
||||
fetchFeatureMetrics,
|
||||
toggleFeature,
|
||||
fetchSeenApps,
|
||||
})(MatricComponent);
|
@ -14,4 +14,4 @@
|
||||
line-height: 25px;
|
||||
dominant-baseline: middle;
|
||||
text-anchor: middle;
|
||||
}
|
||||
}
|
||||
|
@ -7,13 +7,14 @@ class Progress extends Component {
|
||||
|
||||
this.state = {
|
||||
percentage: props.initialAnimation ? 0 : props.percentage,
|
||||
percentageText: props.initialAnimation ? 0 : props.percentage,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.initialAnimation) {
|
||||
this.initialTimeout = setTimeout(() => {
|
||||
this.requestAnimationFrame = window.requestAnimationFrame(() => {
|
||||
this.rafTimerInit = window.requestAnimationFrame(() => {
|
||||
this.setState({
|
||||
percentage: this.props.percentage,
|
||||
});
|
||||
@ -23,16 +24,65 @@ class Progress extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps ({ percentage }) {
|
||||
this.setState({ percentage });
|
||||
if (this.state.percentage !== percentage) {
|
||||
const nextState = { percentage };
|
||||
if (this.props.animatePercentageText) {
|
||||
this.animateTo(percentage, this.getTarget(percentage));
|
||||
} else {
|
||||
nextState.percentageText = percentage;
|
||||
}
|
||||
this.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
getTarget (target) {
|
||||
const start = this.state.percentageText;
|
||||
const TOTAL_ANIMATION_TIME = 5000;
|
||||
const diff = start > target ? -(start - target) : target - start;
|
||||
const perCycle = TOTAL_ANIMATION_TIME / diff;
|
||||
const cyclesCounter = Math.round(Math.abs(TOTAL_ANIMATION_TIME / perCycle));
|
||||
const perCycleTime = Math.round(Math.abs(perCycle));
|
||||
|
||||
return {
|
||||
start,
|
||||
target,
|
||||
cyclesCounter,
|
||||
perCycleTime,
|
||||
increment: diff / cyclesCounter,
|
||||
};
|
||||
}
|
||||
|
||||
animateTo (percentage, targetState) {
|
||||
cancelAnimationFrame(this.rafCounterTimer);
|
||||
clearTimeout(this.nextTimer);
|
||||
|
||||
const current = this.state.percentageText;
|
||||
|
||||
targetState.cyclesCounter --;
|
||||
if (targetState.cyclesCounter <= 0) {
|
||||
this.setState({ percentageText: targetState.target });
|
||||
return;
|
||||
}
|
||||
|
||||
const next = Math.round(current + targetState.increment);
|
||||
this.rafCounterTimer = requestAnimationFrame(() => {
|
||||
this.setState({ percentageText: next });
|
||||
this.nextTimer = setTimeout(() => {
|
||||
this.animateTo(next, targetState);
|
||||
}, targetState.perCycleTime);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this.initialTimeout);
|
||||
window.cancelAnimationFrame(this.requestAnimationFrame);
|
||||
clearTimeout(this.nextTimer);
|
||||
window.cancelAnimationFrame(this.rafTimerInit);
|
||||
window.cancelAnimationFrame(this.rafCounterTimer);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { strokeWidth, percentage } = this.props;
|
||||
const { strokeWidth } = this.props;
|
||||
const radius = (50 - strokeWidth / 2);
|
||||
const pathDescription = `
|
||||
M 50,50 m 0,-${radius}
|
||||
@ -66,7 +116,7 @@ class Progress extends Component {
|
||||
className={styles.text}
|
||||
x={50}
|
||||
y={50}
|
||||
>{percentage}%</text>
|
||||
>{this.state.percentageText}%</text>
|
||||
</svg>);
|
||||
}
|
||||
}
|
||||
@ -75,11 +125,13 @@ Progress.propTypes = {
|
||||
percentage: PropTypes.number.isRequired,
|
||||
strokeWidth: PropTypes.number,
|
||||
initialAnimation: PropTypes.bool,
|
||||
animatePercentageText: PropTypes.bool,
|
||||
textForPercentage: PropTypes.func,
|
||||
};
|
||||
|
||||
Progress.defaultProps = {
|
||||
strokeWidth: 8,
|
||||
animatePercentageText: false,
|
||||
initialAnimation: false,
|
||||
};
|
||||
|
||||
|
97
frontend/src/component/feature/view-component.jsx
Normal file
97
frontend/src/component/feature/view-component.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Tabs, Tab, ProgressBar } from 'react-mdl';
|
||||
import { hashHistory, Link } from 'react-router';
|
||||
|
||||
import HistoryComponent from '../history/history-list-toggle-container';
|
||||
import MetricComponent from './metric-container';
|
||||
import EditFeatureToggle from './form-edit-container.jsx';
|
||||
|
||||
const TABS = {
|
||||
view: 0,
|
||||
edit: 1,
|
||||
history: 2,
|
||||
};
|
||||
|
||||
export default class ViewFeatureToggleComponent extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
features: PropTypes.array.isRequired,
|
||||
fetchFeatureToggles: PropTypes.array.isRequired,
|
||||
featureToggle: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
if (this.props.features.length === 0) {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
}
|
||||
|
||||
getTabContent (activeTab) {
|
||||
const {
|
||||
featureToggle,
|
||||
featureToggleName,
|
||||
} = this.props;
|
||||
|
||||
if (TABS[activeTab] === TABS.history) {
|
||||
return <HistoryComponent toggleName={featureToggleName} />;
|
||||
} else if (TABS[activeTab] === TABS.edit) {
|
||||
return <EditFeatureToggle featureToggle={featureToggle} />;
|
||||
} else {
|
||||
return <MetricComponent featureToggle={featureToggle} />;
|
||||
}
|
||||
}
|
||||
|
||||
goToTab (tabName, featureToggleName) {
|
||||
hashHistory.push(`/features/${tabName}/${featureToggleName}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
featureToggle,
|
||||
features,
|
||||
activeTab,
|
||||
featureToggleName,
|
||||
} = this.props;
|
||||
|
||||
if (!featureToggle) {
|
||||
if (features.length === 0 ) {
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
Could not find the toggle <Link to={{ pathname: '/features/create', query: { name: featureToggleName } }}>
|
||||
{featureToggleName}</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.view;
|
||||
const tabContent = this.getTabContent(activeTab);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{featureToggle.name} <small>{featureToggle.enabled ? 'is enabled' : 'is disabled'}</small>
|
||||
<small style={{ float: 'right', lineHeight: '38px' }}>
|
||||
Created {(new Date(featureToggle.createdAt)).toLocaleString('nb-NO')}
|
||||
</small>
|
||||
</h4>
|
||||
<div>{featureToggle.description}</div>
|
||||
<Tabs activeTab={activeTabId} ripple style={{ marginBottom: '10px' }}>
|
||||
<Tab onClick={() => this.goToTab('view', featureToggleName)}>Metrics</Tab>
|
||||
<Tab onClick={() => this.goToTab('edit', featureToggleName)}>Edit</Tab>
|
||||
<Tab onClick={() => this.goToTab('history', featureToggleName)}>History</Tab>
|
||||
</Tabs>
|
||||
|
||||
{tabContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
14
frontend/src/component/feature/view-container.jsx
Normal file
14
frontend/src/component/feature/view-container.jsx
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFeatureToggles } from '../../store/feature-actions';
|
||||
|
||||
import ViewToggleComponent from './view-component';
|
||||
|
||||
export default connect((state, props) => ({
|
||||
features: state.features.toJS(),
|
||||
featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
|
||||
activeTab: props.activeTab,
|
||||
}), {
|
||||
fetchFeatureToggles,
|
||||
})(ViewToggleComponent);
|
@ -1,148 +0,0 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Grid, Cell, Icon, Switch } from 'react-mdl';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import percentLib from 'percent';
|
||||
import Progress from './progress';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import EditFeatureToggle from './form-edit-container.jsx';
|
||||
import { fetchFeatureToggles, toggleFeature } from '../../store/feature-actions';
|
||||
import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions';
|
||||
|
||||
class EditFeatureToggleWrapper extends React.Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
features: PropTypes.array.isRequired,
|
||||
fetchFeatureToggles: PropTypes.array.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
if (this.props.features.length === 0) {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
this.props.fetchSeenApps();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
this.props.fetchSeenApps();
|
||||
this.props.fetchFeatureMetrics();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
toggleFeature,
|
||||
features,
|
||||
featureToggleName,
|
||||
metrics = {},
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
lastHour = { yes: 0, no: 0, isFallback: true },
|
||||
lastMinute = { yes: 0, no: 0, isFallback: true },
|
||||
seenApps = [],
|
||||
} = metrics;
|
||||
|
||||
const lastHourPercent = 1 * percentLib.calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent = 1 * percentLib.calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
|
||||
const featureToggle = features.find(toggle => toggle.name === featureToggleName);
|
||||
|
||||
if (!featureToggle) {
|
||||
if (features.length === 0 ) {
|
||||
return <span>Loading</span>;
|
||||
}
|
||||
return <span>Could not find {this.props.featureToggleName}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{featureToggle.name} <small>{featureToggle.enabled ? 'is enabled' : 'is disabled'}</small></h4>
|
||||
<hr />
|
||||
<div style={{ maxWidth: '200px' }} >
|
||||
<Switch style={{ cursor: 'pointer' }} onChange={() => toggleFeature(featureToggle)} checked={featureToggle.enabled}>
|
||||
Toggle {featureToggle.name}
|
||||
</Switch>
|
||||
</div>
|
||||
<hr />
|
||||
<Grid style={{ textAlign: 'center' }}>
|
||||
<Cell col={3}>
|
||||
{
|
||||
lastMinute.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }} name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={10} percentage={lastMinutePercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last minute</strong><br /> Yes {lastMinute.yes}, No: {lastMinute.no}</p>
|
||||
</Cell>
|
||||
<Cell col={3}>
|
||||
{
|
||||
lastHour.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }} name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={10} percentage={lastHourPercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last hour</strong><br /> Yes {lastHour.yes}, No: {lastHour.no}</p>
|
||||
</Cell>
|
||||
<Cell col={3}>
|
||||
{seenApps.length > 0 ?
|
||||
(<div><strong>Seen in applications:</strong></div>) :
|
||||
<div>
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }} name="report problem" title="Not used in a app in the last hour" />
|
||||
<div><small><strong>Not used in a app in the last hour.</strong> This might be due to your client implementation is not reporting usage.</small></div>
|
||||
</div>
|
||||
}
|
||||
{seenApps.length > 0 && seenApps.map((appName) => (
|
||||
<Link key={appName} to={`/applications/${appName}`}>
|
||||
{appName}
|
||||
</Link>
|
||||
))}
|
||||
<p>add instances count?</p>
|
||||
</Cell>
|
||||
<Cell col={3}>
|
||||
<p>add history</p>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<hr />
|
||||
<h4>Edit</h4>
|
||||
<EditFeatureToggle featureToggle={featureToggle} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getMetricsForToggle (state, toggleName) {
|
||||
if (!toggleName) {
|
||||
return;
|
||||
}
|
||||
const result = {};
|
||||
|
||||
if (state.featureMetrics.hasIn(['seenApps', toggleName])) {
|
||||
result.seenApps = state.featureMetrics.getIn(['seenApps', toggleName]);
|
||||
}
|
||||
if (state.featureMetrics.hasIn(['lastHour', toggleName])) {
|
||||
result.lastHour = state.featureMetrics.getIn(['lastHour', toggleName]);
|
||||
result.lastMinute = state.featureMetrics.getIn(['lastMinute', toggleName]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export default connect((state, props) => ({
|
||||
features: state.features.toJS(),
|
||||
metrics: getMetricsForToggle(state, props.featureToggleName),
|
||||
}), {
|
||||
fetchFeatureMetrics,
|
||||
fetchFeatureToggles,
|
||||
toggleFeature,
|
||||
fetchSeenApps,
|
||||
})(EditFeatureToggleWrapper);
|
@ -18,10 +18,7 @@ class History extends PureComponent {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h5>Last 100 changes</h5>
|
||||
<HistoryList history={history} />
|
||||
</div>
|
||||
<HistoryList history={history} title="Last 100 changes" />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import { Icon } from 'react-mdl';
|
||||
|
||||
import style from './history.scss';
|
||||
|
||||
@ -10,35 +9,25 @@ const DIFF_PREFIXES = {
|
||||
N: '+',
|
||||
};
|
||||
|
||||
const SPADEN_CLASS = {
|
||||
const KLASSES = {
|
||||
A: style.blue, // array edited
|
||||
E: style.blue, // edited
|
||||
D: style.negative, // deleted
|
||||
N: style.positive, // added
|
||||
};
|
||||
|
||||
function getIcon (type) {
|
||||
switch (type) {
|
||||
case 'feature-updated': return 'autorenew';
|
||||
case 'feature-created': return 'add';
|
||||
case 'feature-deleted': return 'remove';
|
||||
case 'feature-archived': return 'archived';
|
||||
default: return 'star';
|
||||
}
|
||||
}
|
||||
|
||||
function buildItemDiff (diff, key) {
|
||||
let change;
|
||||
if (diff.lhs !== undefined) {
|
||||
change = (
|
||||
<div>
|
||||
<div className={SPADEN_CLASS.D}>- {key}: {JSON.stringify(diff.lhs)}</div>
|
||||
<div className={KLASSES.D}>- {key}: {JSON.stringify(diff.lhs)}</div>
|
||||
</div>
|
||||
);
|
||||
} else if (diff.rhs !== undefined) {
|
||||
change = (
|
||||
<div>
|
||||
<div className={SPADEN_CLASS.N}>+ {key}: {JSON.stringify(diff.rhs)}</div>
|
||||
<div className={KLASSES.N}>+ {key}: {JSON.stringify(diff.rhs)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -55,12 +44,12 @@ function buildDiff (diff, idx) {
|
||||
} else 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 className={KLASSES.D}>- {key}: {JSON.stringify(diff.lhs)}</div>
|
||||
<div className={KLASSES.N}>+ {key}: {JSON.stringify(diff.rhs)}</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
const spadenClass = SPADEN_CLASS[diff.kind];
|
||||
const spadenClass = KLASSES[diff.kind];
|
||||
const prefix = DIFF_PREFIXES[diff.kind];
|
||||
|
||||
change = (<div className={spadenClass}>{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}</div>);
|
||||
@ -77,50 +66,20 @@ class HistoryItem extends PureComponent {
|
||||
};
|
||||
}
|
||||
|
||||
renderEventDiff (logEntry) {
|
||||
render () {
|
||||
const entry = this.props.entry;
|
||||
let changes;
|
||||
|
||||
if (logEntry.diffs) {
|
||||
changes = logEntry.diffs.map(buildDiff);
|
||||
if (entry.diffs) {
|
||||
changes = entry.diffs.map(buildDiff);
|
||||
} else {
|
||||
// Just show the data if there is no diff yet.
|
||||
changes = <div className={SPADEN_CLASS.N}>{JSON.stringify(logEntry.data, null, 2)}</div>;
|
||||
changes = <div className={KLASSES.N}>{JSON.stringify(entry.data, null, 2)}</div>;
|
||||
}
|
||||
|
||||
return <code className="smalltext man">{changes.length === 0 ? '(no changes)' : changes}</code>;
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
createdBy,
|
||||
id,
|
||||
type,
|
||||
} = this.props.entry;
|
||||
|
||||
const createdAt = (new Date(this.props.entry.createdAt)).toLocaleString('nb-NO');
|
||||
const icon = getIcon(type);
|
||||
|
||||
const data = this.renderEventDiff(this.props.entry);
|
||||
|
||||
return (
|
||||
<div className={style['history-item']}>
|
||||
<dl>
|
||||
<dt>Id:</dt>
|
||||
<dd>{id}</dd>
|
||||
<dt>Type:</dt>
|
||||
<dd>
|
||||
<Icon name={icon} title={type} style={{ fontSize: '1.6rem' }} />
|
||||
<span> {type}</span>
|
||||
</dd>
|
||||
<dt>Timestamp:</dt>
|
||||
<dd>{createdAt}</dd>
|
||||
<dt>Username:</dt>
|
||||
<dd>{createdBy}</dd>
|
||||
<dt>Diff</dt>
|
||||
<dd>{data}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
return (<pre style={{ maxWidth: '500px', overflowX: 'auto', overflowY: 'hidden', width: 'auto' }}>
|
||||
<code className="smalltext man">{changes.length === 0 ? '(no changes)' : changes}</code>
|
||||
</pre>);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import HistoryItemDiff from './history-item-diff';
|
||||
import HistoryItemJson from './history-item-json';
|
||||
import { Switch } from 'react-mdl';
|
||||
import { Table, TableHeader } from 'react-mdl';
|
||||
import { HeaderTitle, SwitchWithLabel } from '../common';
|
||||
|
||||
import style from './history.scss';
|
||||
|
||||
@ -23,13 +24,28 @@ class HistoryList extends Component {
|
||||
if (showData) {
|
||||
entries = history.map((entry) => <HistoryItemJson key={`log${entry.id}`} entry={entry} />);
|
||||
} else {
|
||||
entries = history.map((entry) => <HistoryItemDiff key={`log${entry.id}`} entry={entry} />);
|
||||
entries = (<Table
|
||||
sortable
|
||||
rows={
|
||||
history.map((entry) => Object.assign({
|
||||
diff: (<HistoryItemDiff entry={entry} />),
|
||||
}, entry))
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<TableHeader name="type">Type</TableHeader>
|
||||
<TableHeader name="createdBy">User</TableHeader>
|
||||
<TableHeader name="diff">Diff</TableHeader>
|
||||
<TableHeader numeric name="createdAt" cellFormatter={(v) => (new Date(v)).toLocaleString('nb-NO')}>Time</TableHeader>
|
||||
</Table>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={style.history}>
|
||||
<Switch checked={showData} onChange={this.toggleShowDiff.bind(this)}>Show full events</Switch>
|
||||
{entries}
|
||||
<HeaderTitle title={this.props.title} actions={
|
||||
<SwitchWithLabel checked={showData} onChange={this.toggleShowDiff.bind(this)}>Show full events</SwitchWithLabel>
|
||||
}/>
|
||||
{entries}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { connect } from 'react-redux';
|
||||
import HistoryListComponent from './history-list-component';
|
||||
import HistoryListToggleComponent from './history-list-component';
|
||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
@ -12,6 +12,6 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const HistoryListContainer = connect(mapStateToProps, {
|
||||
updateSetting: updateSettingForGroup('history'),
|
||||
})(HistoryListComponent);
|
||||
})(HistoryListToggleComponent);
|
||||
|
||||
export default HistoryListContainer;
|
||||
|
@ -1,17 +1,9 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ListComponent from './history-list-container';
|
||||
import { fetchHistoryForToggle } from '../../data/history-api';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
class HistoryListToggle extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
fetching: true,
|
||||
history: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
toggleName: PropTypes.string.isRequired,
|
||||
@ -19,21 +11,24 @@ class HistoryListToggle extends Component {
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
fetchHistoryForToggle(this.props.toggleName)
|
||||
.then((res) => this.setState({ history: res, fetching: false }));
|
||||
this.props.fetchHistoryForToggle(this.props.toggleName);
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.state.fetching) {
|
||||
if (!this.props.history || this.props.history.length === 0) {
|
||||
return <span>fetching..</span>;
|
||||
}
|
||||
|
||||
const { history, toggleName } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h5>Showing history for toggle: <strong>{this.props.toggleName}</strong></h5>
|
||||
<ListComponent history={this.state.history} />
|
||||
</div>
|
||||
<ListComponent
|
||||
history={history}
|
||||
title={
|
||||
<span>Showing history for toggle: <Link to={`/features/edit/${toggleName}`}>
|
||||
<strong>{toggleName}</strong>
|
||||
</Link>
|
||||
</span>}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default HistoryListToggle;
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { connect } from 'react-redux';
|
||||
import HistoryListToggleComponent from './history-list-toggle-component';
|
||||
import { fetchHistoryForToggle } from '../../store/history-actions';
|
||||
|
||||
function getHistoryFromToggle (state, toggleName) {
|
||||
if (!toggleName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (state.history.hasIn(['toggles', toggleName])) {
|
||||
return state.history.getIn(['toggles', toggleName]).toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
history: getHistoryFromToggle(state, props.toggleName),
|
||||
});
|
||||
|
||||
const HistoryListToggleContainer = connect(mapStateToProps, {
|
||||
fetchHistoryForToggle,
|
||||
})(HistoryListToggleComponent);
|
||||
|
||||
export default HistoryListToggleContainer;
|
@ -57,8 +57,8 @@ export function createActions ({ id, prepare = (v) => v }) {
|
||||
dispatch(createPop({ id: getId(id, ownProps), key, index }));
|
||||
},
|
||||
|
||||
updateInList (key, index, newValue) {
|
||||
dispatch(createUp({ id: getId(id, ownProps), key, index, newValue }));
|
||||
updateInList (key, index, newValue, merge = false) {
|
||||
dispatch(createUp({ id: getId(id, ownProps), key, index, newValue, merge }));
|
||||
},
|
||||
|
||||
incValue (key) {
|
||||
|
@ -1,35 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DataTable, TableHeader } from 'react-mdl';
|
||||
|
||||
class Metrics extends Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.props.fetchMetrics();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { globalCount, clientList } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{`Total of ${globalCount} toggles`}</h4>
|
||||
<DataTable
|
||||
style={{ width: '100%' }}
|
||||
rows={clientList}
|
||||
selectable={false}
|
||||
>
|
||||
<TableHeader name="name">Instance</TableHeader>
|
||||
<TableHeader name="appName">Application name</TableHeader>
|
||||
<TableHeader numeric name="ping" cellFormatter={
|
||||
(v) => (v.toString())
|
||||
}>Last seen</TableHeader>
|
||||
<TableHeader numeric name="count">Counted</TableHeader>
|
||||
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default Metrics;
|
@ -1,39 +0,0 @@
|
||||
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;
|
@ -1,9 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { createMapper, createActions } from '../input-helpers';
|
||||
import { createStrategy } from '../../store/strategy-actions';
|
||||
import { createStrategy } from '../../store/strategy/actions';
|
||||
|
||||
import AddStrategy, { PARAM_PREFIX } from './add-strategy';
|
||||
import AddStrategy from './add-strategy';
|
||||
|
||||
const ID = 'add-strategy';
|
||||
|
||||
@ -11,16 +11,26 @@ const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
// clean
|
||||
const parameters = (input.parameters || [])
|
||||
.filter((name) => !!name)
|
||||
.map(({
|
||||
name,
|
||||
type = 'string',
|
||||
description = '',
|
||||
required = false,
|
||||
}) => ({
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
required,
|
||||
}));
|
||||
|
||||
const parametersTemplate = {};
|
||||
Object.keys(input).forEach(key => {
|
||||
if (key.startsWith(PARAM_PREFIX)) {
|
||||
parametersTemplate[input[key]] = 'string';
|
||||
}
|
||||
});
|
||||
input.parametersTemplate = parametersTemplate;
|
||||
|
||||
createStrategy(input)(dispatch)
|
||||
createStrategy({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
parameters,
|
||||
})(dispatch)
|
||||
.then(() => methods.clear())
|
||||
// somewhat quickfix / hacky to go back..
|
||||
.then(() => window.history.back());
|
||||
@ -43,4 +53,13 @@ const actions = createActions({
|
||||
prepare,
|
||||
});
|
||||
|
||||
export default connect(createMapper({ id: ID }), actions)(AddStrategy);
|
||||
export default connect(createMapper({
|
||||
id: ID,
|
||||
getDefault () {
|
||||
let name;
|
||||
try {
|
||||
[, name] = document.location.hash.match(/name=([a-z0-9-_]+)/i);
|
||||
} catch (e) {}
|
||||
return { name };
|
||||
},
|
||||
}), actions)(AddStrategy);
|
||||
|
@ -1,6 +1,8 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
|
||||
import { Textfield, IconButton, Menu, MenuItem, Checkbox } from 'react-mdl';
|
||||
import { FormButtons } from '../common';
|
||||
|
||||
import { Textfield, Button, IconButton } from 'react-mdl';
|
||||
|
||||
const trim = (value) => {
|
||||
if (value && value.trim) {
|
||||
@ -13,71 +15,161 @@ const trim = (value) => {
|
||||
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 (
|
||||
const Parameter = ({ set, input = {}, index }) => (
|
||||
<div style={{ background: '#f1f1f1', padding: '16px 20px', marginBottom: '20px' }}>
|
||||
<Textfield
|
||||
label={`Parameter name ${i + 1}`}
|
||||
name={key} key={key}
|
||||
onChange={({ target }) => setValue(key, target.value)}
|
||||
value={input[key]} />
|
||||
);
|
||||
})}</div>);
|
||||
|
||||
const AddStrategy = ({
|
||||
input,
|
||||
setValue,
|
||||
incValue,
|
||||
// clear,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}) => (
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
<section>
|
||||
<Textfield label="Strategy name"
|
||||
name="name" required
|
||||
pattern="^[0-9a-zA-Z\.\-]+$"
|
||||
onChange={({ target }) => setValue('name', trim(target.value))}
|
||||
value={input.name}
|
||||
/>
|
||||
<br />
|
||||
<Textfield
|
||||
rows={2}
|
||||
label="Description"
|
||||
name="description"
|
||||
onChange={({ target }) => setValue('description', target.value)}
|
||||
value={input.description}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
{genParams(input, input._params, setValue)}
|
||||
<IconButton name="add" title="Add parameter" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
incValue('_params');
|
||||
}}/>
|
||||
</section>
|
||||
|
||||
<br />
|
||||
<hr />
|
||||
|
||||
<section>
|
||||
<Button type="submit" raised primary >Create</Button>
|
||||
|
||||
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
|
||||
</section>
|
||||
</form>
|
||||
style={{ width: '50%' }}
|
||||
floatingLabel
|
||||
label={`Parameter name ${index + 1}`}
|
||||
onChange={({ target }) => set({ name: target.value }, true)}
|
||||
value={input.name} />
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<span className="mdl-outline" id={`${index}-type-menu`} style={{
|
||||
borderRadius: '2px',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 2px 0 rgba(0,0,0,.04),0 3px 1px -2px rgba(0,0,0,.1),0 1px 5px 0 rgba(0,0,0,.12)',
|
||||
marginLeft: '10px',
|
||||
border: '1px solid #f1f1f1',
|
||||
backgroundColor: 'white',
|
||||
padding: '10px 2px 10px 20px',
|
||||
}}>
|
||||
{input.type || 'string'}
|
||||
<IconButton name="arrow_drop_down" onClick={(evt) => evt.preventDefault()} />
|
||||
</span>
|
||||
<Menu target={`${index}-type-menu`} align="right">
|
||||
<MenuItem onClick={() => set({ type: 'string' })}>string</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'percentage' })}>percentage</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'list' })}>list</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'number' })}>number</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
style={{ width: '100%' }}
|
||||
rows={2}
|
||||
label={`Parameter name ${index + 1} description`}
|
||||
onChange={({ target }) => set({ description: target.value })}
|
||||
value={input.description}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Required"
|
||||
checked={!!input.required}
|
||||
onChange={() => set({ required: !input.required })}
|
||||
ripple
|
||||
defaultChecked
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
AddStrategy.propTypes = {
|
||||
input: PropTypes.object,
|
||||
setValue: PropTypes.func,
|
||||
incValue: PropTypes.func,
|
||||
clear: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
};
|
||||
const EditHeader = () => (
|
||||
<div>
|
||||
<h4>Edit strategy</h4>
|
||||
<p style={{ background: '#ffb7b7', padding: '16px 20px' }}>
|
||||
Be carefull! Changing a strategy definition might also require changes to the
|
||||
implementation in the clients.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const CreateHeader = () => (
|
||||
<div>
|
||||
<h4>Create a new Strategy definition</h4>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const Parameters = ({ input = [], count = 0, updateInList }) => (
|
||||
<div>{
|
||||
gerArrayWithEntries(count)
|
||||
.map((v, i) => <Parameter
|
||||
key={i}
|
||||
set={(v) => updateInList('parameters', i, v, true)}
|
||||
index={i}
|
||||
input={input[i]}
|
||||
/>)
|
||||
}</div>);
|
||||
|
||||
class AddStrategy extends Component {
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
input: PropTypes.object,
|
||||
setValue: PropTypes.func,
|
||||
updateInList: PropTypes.func,
|
||||
incValue: PropTypes.func,
|
||||
clear: PropTypes.func,
|
||||
onCancel: PropTypes.func,
|
||||
onSubmit: PropTypes.func,
|
||||
editmode: PropTypes.bool,
|
||||
initCallRequired: PropTypes.bool,
|
||||
init: PropTypes.func,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// TODO unwind this stuff
|
||||
if (this.props.initCallRequired === true) {
|
||||
this.props.init(this.props.input);
|
||||
if (this.props.input.parameters) {
|
||||
this.props.setValue('_params', this.props.input.parameters.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
const {
|
||||
input,
|
||||
setValue,
|
||||
updateInList,
|
||||
incValue,
|
||||
onCancel,
|
||||
editmode = false,
|
||||
onSubmit,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
{editmode ? <EditHeader /> : <CreateHeader />}
|
||||
<Textfield label="Strategy name"
|
||||
floatingLabel
|
||||
name="name"
|
||||
required
|
||||
disabled={editmode}
|
||||
pattern="^[0-9a-zA-Z\.\-]+$"
|
||||
onChange={({ target }) => setValue('name', trim(target.value))}
|
||||
value={input.name}
|
||||
/>
|
||||
<br />
|
||||
<Textfield
|
||||
floatingLabel
|
||||
style={{ width: '100%' }}
|
||||
rows={2}
|
||||
label="Description"
|
||||
name="description"
|
||||
onChange={({ target }) => setValue('description', target.value)}
|
||||
value={input.description}
|
||||
/>
|
||||
|
||||
|
||||
<Parameters input={input.parameters} count={input._params} updateInList={updateInList} />
|
||||
<IconButton raised name="add" title="Add parameter" onClick={(e) => {
|
||||
e.preventDefault();
|
||||
incValue('_params');
|
||||
}}/> Add parameter
|
||||
|
||||
|
||||
<br />
|
||||
<hr />
|
||||
|
||||
<FormButtons
|
||||
submitText={editmode ? 'Update' : 'Create'}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddStrategy;
|
||||
|
70
frontend/src/component/strategies/edit-container.js
Normal file
70
frontend/src/component/strategies/edit-container.js
Normal file
@ -0,0 +1,70 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { hashHistory } from 'react-router';
|
||||
import { createMapper, createActions } from '../input-helpers';
|
||||
import { updateStrategy } from '../../store/strategy/actions';
|
||||
|
||||
import AddStrategy from './add-strategy';
|
||||
|
||||
const ID = 'edit-strategy';
|
||||
|
||||
function getId (props) {
|
||||
return [ID, props.strategy.name];
|
||||
}
|
||||
|
||||
// TODO: need to scope to the active strategy
|
||||
// best is to emulate the "input-storage"?
|
||||
const mapStateToProps = createMapper({
|
||||
id: getId,
|
||||
getDefault: (state, ownProps) => ownProps.strategy,
|
||||
prepare: (props) => {
|
||||
props.editmode = true;
|
||||
return props;
|
||||
},
|
||||
});
|
||||
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
// clean
|
||||
const parameters = (input.parameters || [])
|
||||
.filter((name) => !!name)
|
||||
.map(({
|
||||
name,
|
||||
type = 'string',
|
||||
description = '',
|
||||
required = false,
|
||||
}) => ({
|
||||
name,
|
||||
type,
|
||||
description,
|
||||
required,
|
||||
}));
|
||||
|
||||
updateStrategy({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
parameters,
|
||||
})(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/strategies/view/${input.name}`));
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (e) => {
|
||||
e.preventDefault();
|
||||
methods.clear();
|
||||
// somewhat quickfix / hacky to go back..
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
|
||||
return methods;
|
||||
};
|
||||
|
||||
const actions = createActions({
|
||||
id: getId,
|
||||
prepare,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, actions)(AddStrategy);
|
@ -1,8 +1,8 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { List, ListItem, ListItemContent, Icon, IconButton, Chip } from 'react-mdl';
|
||||
|
||||
import style from './strategies.scss';
|
||||
import { List, ListItem, ListItemContent, IconButton } from 'react-mdl';
|
||||
import { HeaderTitle } from '../common';
|
||||
|
||||
class StrategiesListComponent extends Component {
|
||||
|
||||
@ -14,33 +14,29 @@ class StrategiesListComponent extends Component {
|
||||
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 (
|
||||
<div>
|
||||
<h5>Strategies</h5>
|
||||
<IconButton name="add" onClick={() => this.context.router.push('/strategies/create')} title="Add new strategy"/>
|
||||
|
||||
<hr />
|
||||
<List>
|
||||
{strategies.length > 0 ? strategies.map((strategy, i) => {
|
||||
return (
|
||||
<ListItem key={i}>
|
||||
<ListItemContent><strong>{strategy.name}</strong> {strategy.description}</ListItemContent>
|
||||
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
|
||||
</ListItem>
|
||||
);
|
||||
}) : <ListItem>No entries</ListItem>}
|
||||
|
||||
|
||||
</List>
|
||||
<HeaderTitle title="Strategies"
|
||||
actions={
|
||||
<IconButton raised
|
||||
name="add"
|
||||
onClick={() => this.context.router.push('/strategies/create')}
|
||||
title="Add new strategy" />} />
|
||||
<List>
|
||||
{strategies.length > 0 ? strategies.map((strategy, i) => (
|
||||
<ListItem key={i} twoLine>
|
||||
<ListItemContent icon="extension" subtitle={strategy.description}>
|
||||
<Link to={`/strategies/view/${strategy.name}`}>
|
||||
<strong>{strategy.name}</strong>
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
|
||||
</ListItem>
|
||||
)) : <ListItem>No entries</ListItem>}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import StrategiesListComponent from './list-component.jsx';
|
||||
import { fetchStrategies, removeStrategy } from '../../store/strategy-actions';
|
||||
import { fetchStrategies, removeStrategy } from '../../store/strategy/actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const list = state.strategies.get('list').toArray();
|
||||
|
69
frontend/src/component/strategies/show-strategy-component.js
Normal file
69
frontend/src/component/strategies/show-strategy-component.js
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { PropTypes, PureComponent } from 'react';
|
||||
import { Grid, Cell, List, ListItem, ListItemContent } from 'react-mdl';
|
||||
import { AppsLinkList, TogglesLinkList } from '../common';
|
||||
|
||||
class ShowStrategyComponent extends PureComponent {
|
||||
static propTypes () {
|
||||
return {
|
||||
toggles: PropTypes.array,
|
||||
applications: PropTypes.array,
|
||||
strategy: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
renderParameters (params) {
|
||||
if (params) {
|
||||
return params.map(({ name, type, description, required }, i) => (
|
||||
<ListItem twoLine key={`${name}-${i}`} title={required ? 'Required' : ''}>
|
||||
<ListItemContent avatar={required ? 'add' : ' '} subtitle={description}>
|
||||
{name} <small>({type})</small>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
));
|
||||
} else {
|
||||
return <ListItem>(no params)</ListItem>;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
strategy,
|
||||
applications,
|
||||
toggles,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
parameters = [],
|
||||
} = strategy;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Grid>
|
||||
<Cell col={12} >
|
||||
<h6>Parameters</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{this.renderParameters(parameters)}
|
||||
</List>
|
||||
</Cell>
|
||||
|
||||
<Cell col={6} tablet={12}>
|
||||
<h6>Applications using this strategy</h6>
|
||||
<hr />
|
||||
<AppsLinkList apps={applications} />
|
||||
</Cell>
|
||||
|
||||
<Cell col={6} tablet={12}>
|
||||
<h6>Toggles using this strategy</h6>
|
||||
<hr />
|
||||
<TogglesLinkList toggles={toggles} />
|
||||
</Cell>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ShowStrategyComponent;
|
@ -0,0 +1,78 @@
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
import { hashHistory } from 'react-router';
|
||||
import { Tabs, Tab, ProgressBar } from 'react-mdl';
|
||||
import ShowStrategy from './show-strategy-component';
|
||||
import EditStrategy from './edit-container';
|
||||
import { HeaderTitle } from '../common';
|
||||
|
||||
const TABS = {
|
||||
view: 0,
|
||||
edit: 1,
|
||||
};
|
||||
|
||||
export default class StrategyDetails extends Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
strategyName: PropTypes.string.isRequired,
|
||||
toggles: PropTypes.array,
|
||||
applications: PropTypes.array,
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
strategy: PropTypes.object.isRequired,
|
||||
fetchStrategies: PropTypes.func.isRequired,
|
||||
fetchApplications: PropTypes.func.isRequired,
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (!this.props.strategy) {
|
||||
this.props.fetchStrategies();
|
||||
};
|
||||
if (!this.props.applications || this.props.applications.length === 0) {
|
||||
this.props.fetchApplications();
|
||||
}
|
||||
if (!this.props.toggles || this.props.toggles.length === 0) {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
}
|
||||
|
||||
getTabContent (activeTabId) {
|
||||
if (activeTabId === TABS.edit) {
|
||||
return <EditStrategy strategy={this.props.strategy} />;
|
||||
} else {
|
||||
return (<ShowStrategy
|
||||
strategy={this.props.strategy}
|
||||
toggles={this.props.toggles}
|
||||
applications={this.props.applications} />);
|
||||
}
|
||||
}
|
||||
|
||||
goToTab (tabName) {
|
||||
hashHistory.push(`/strategies/${tabName}/${this.props.strategyName}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.view;
|
||||
const strategy = this.props.strategy;
|
||||
if (!strategy) {
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
|
||||
const tabContent = this.getTabContent(activeTabId);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderTitle title={strategy.name} subtitle={strategy.description} />
|
||||
<Tabs activeTab={activeTabId} ripple>
|
||||
<Tab onClick={() => this.goToTab('view')}>Details</Tab>
|
||||
<Tab onClick={() => this.goToTab('edit')}>Edit</Tab>
|
||||
</Tabs>
|
||||
<section>
|
||||
<div className="content">
|
||||
{tabContent}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ShowStrategy from './strategy-details-component';
|
||||
import { fetchStrategies } from '../../store/strategy/actions';
|
||||
import { fetchAll } from '../../store/application/actions';
|
||||
import { fetchFeatureToggles } from '../../store/feature-actions';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
let strategy = state.strategies
|
||||
.get('list')
|
||||
.find(n => n.name === props.strategyName);
|
||||
const applications = state.applications
|
||||
.get('list')
|
||||
.filter(app => app.strategies.includes(props.strategyName));
|
||||
const toggles = state.features
|
||||
.filter(toggle =>
|
||||
toggle.get('strategies').findIndex(s => s.name === props.strategyName) > -1);
|
||||
|
||||
return {
|
||||
strategy,
|
||||
strategyName: props.strategyName,
|
||||
applications: applications && applications.toJS(),
|
||||
toggles: toggles && toggles.toJS(),
|
||||
activeTab: props.activeTab,
|
||||
};
|
||||
};
|
||||
|
||||
const Constainer = connect(mapStateToProps, {
|
||||
fetchStrategies,
|
||||
fetchApplications: fetchAll,
|
||||
fetchFeatureToggles,
|
||||
})(ShowStrategy);
|
||||
|
||||
export default Constainer;
|
@ -6,6 +6,7 @@ class EditUserComponent extends React.Component {
|
||||
return {
|
||||
user: PropTypes.object.isRequired,
|
||||
updateUserName: PropTypes.func.isRequired,
|
||||
save: PropTypes.func.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
@ -21,7 +22,7 @@ class EditUserComponent extends React.Component {
|
||||
<DialogTitle>Action required</DialogTitle>
|
||||
<DialogContent>
|
||||
<p>
|
||||
You are logged in as:You hav to specify a username to use Unleash. This will allow us to track changes.
|
||||
You hav to specify a username to use Unleash. This will allow us to track changes.
|
||||
</p>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Textfield
|
||||
|
@ -14,7 +14,26 @@ function fetchApplication (appName) {
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function fetchApplicationsWithStrategyName (strategyName) {
|
||||
return fetch(`${URI}?strategyName=${strategyName}`, { headers })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function storeApplicationMetaData (appName, key, value) {
|
||||
const data = {};
|
||||
data[key] = value;
|
||||
return fetch(`${URI}/${appName}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchApplication,
|
||||
fetchAll,
|
||||
fetchApplicationsWithStrategyName,
|
||||
storeApplicationMetaData,
|
||||
};
|
||||
|
@ -8,11 +8,10 @@ function fetchAll () {
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function revive (feature) {
|
||||
return fetch(`${URI}/revive`, {
|
||||
function revive (featureName) {
|
||||
return fetch(`${URI}/revive/${featureName}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(feature),
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = '/api/client/strategies';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI, { headers })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
};
|
@ -1,11 +1,18 @@
|
||||
const defaultErrorMessage = 'Unexptected exception when talking to unleash-api';
|
||||
|
||||
function extractJoiMsg (body) {
|
||||
return body.details.length > 0 ? body.details[0].message : defaultErrorMessage;
|
||||
}
|
||||
function extractLegacyMsg (body) {
|
||||
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
|
||||
}
|
||||
|
||||
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;
|
||||
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
||||
let error = new Error(errorMsg);
|
||||
error.statusCode = response.status;
|
||||
reject(error);
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { throwIfNotSuccess } from './helper';
|
||||
|
||||
const URI = '/api/metrics';
|
||||
|
||||
function fetchAll () {
|
||||
return fetch(URI)
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
};
|
@ -17,6 +17,15 @@ function create (strategy) {
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function update (strategy) {
|
||||
return fetch(`${URI}/${strategy.name}`, {
|
||||
method: 'put',
|
||||
headers,
|
||||
body: JSON.stringify(strategy),
|
||||
credentials: 'include',
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function remove (strategy) {
|
||||
return fetch(`${URI}/${strategy.name}`, {
|
||||
method: 'DELETE',
|
||||
@ -28,5 +37,6 @@ function remove (strategy) {
|
||||
module.exports = {
|
||||
fetchAll,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
};
|
||||
|
@ -1,4 +1,7 @@
|
||||
import 'react-mdl/extra/material.css';
|
||||
import 'react-mdl/extra/material.js';
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Router, Route, IndexRedirect, hashHistory } from 'react-router';
|
||||
@ -11,15 +14,15 @@ import App from './component/app';
|
||||
|
||||
import Features from './page/features';
|
||||
import CreateFeatureToggle from './page/features/create';
|
||||
import EditFeatureToggle from './page/features/edit';
|
||||
import ViewFeatureToggle from './page/features/show';
|
||||
import Strategies from './page/strategies';
|
||||
import StrategyView from './page/strategies/show';
|
||||
import CreateStrategies from './page/strategies/create';
|
||||
import HistoryPage from './page/history';
|
||||
import HistoryTogglePage from './page/history/toggle';
|
||||
import Archive from './page/archive';
|
||||
import Applications from './page/applications';
|
||||
import ApplicationView from './page/applications/view';
|
||||
import ClientStrategies from './page/client-strategies';
|
||||
|
||||
const unleashStore = createStore(
|
||||
store,
|
||||
@ -28,22 +31,37 @@ const unleashStore = createStore(
|
||||
)
|
||||
);
|
||||
|
||||
// "pageTitle" and "link" attributes are for internal usage only
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={unleashStore}>
|
||||
<Router history={hashHistory}>
|
||||
<Route path="/" component={App}>
|
||||
<IndexRedirect to="/features" />
|
||||
<Route pageTitle="Features" path="/features" component={Features} />
|
||||
<Route pageTitle="Features" path="/features/create" component={CreateFeatureToggle} />
|
||||
<Route pageTitle="Features" path="/features/edit/:name" component={EditFeatureToggle} />
|
||||
<Route pageTitle="Strategies" path="/strategies" component={Strategies} />
|
||||
<Route pageTitle="Strategies" path="/strategies/create" component={CreateStrategies} />
|
||||
<Route pageTitle="History" path="/history" component={HistoryPage} />
|
||||
<Route pageTitle="History" path="/history/:toggleName" component={HistoryTogglePage} />
|
||||
|
||||
<Route pageTitle="Features" link="/features">
|
||||
<Route pageTitle="Features" path="/features" component={Features} />
|
||||
<Route pageTitle="New" path="/features/create" component={CreateFeatureToggle} />
|
||||
<Route pageTitle=":name" path="/features/:activeTab/:name" component={ViewFeatureToggle} />
|
||||
</Route>
|
||||
|
||||
<Route pageTitle="Strategies" link="/strategies">
|
||||
<Route pageTitle="Strategies" path="/strategies" component={Strategies} />
|
||||
<Route pageTitle="New" path="/strategies/create" component={CreateStrategies} />
|
||||
<Route pageTitle=":strategyName" path="/strategies/:activeTab/:strategyName" component={StrategyView} />
|
||||
</Route>
|
||||
|
||||
<Route pageTitle="History" link="/history">
|
||||
<Route pageTitle="History" path="/history" component={HistoryPage} />
|
||||
<Route pageTitle=":toggleName" path="/history/:toggleName" component={HistoryTogglePage} />
|
||||
</Route>
|
||||
|
||||
<Route pageTitle="Archive" path="/archive" component={Archive} />
|
||||
<Route pageTitle="Applications" path="/applications" component={Applications} />
|
||||
<Route pageTitle="Applications" path="/applications/:name" component={ApplicationView} />
|
||||
<Route pageTitle="Client strategies" ppath="/client-strategies" component={ClientStrategies} />
|
||||
<Route pageTitle="Applications" link="/applications">
|
||||
<Route pageTitle="Applications" path="/applications" component={Applications} />
|
||||
<Route pageTitle=":name" path="/applications/:name" component={ApplicationView} />
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>, document.getElementById('app'));
|
||||
|
@ -1,6 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { PropTypes } from 'react';
|
||||
import ApplicationEditComponent from '../../component/application/application-edit-container';
|
||||
|
||||
const render = ({ params }) => <ApplicationEditComponent appName={params.name} />;
|
||||
|
||||
render.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default render;
|
||||
|
@ -1,11 +0,0 @@
|
||||
import React from 'react';
|
||||
import ClientStrategy from '../../component/client-strategy/strategy-container';
|
||||
|
||||
const render = () => (
|
||||
<div>
|
||||
<h5>Client Strategies</h5>
|
||||
<ClientStrategy />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default render;
|
@ -1,11 +1,7 @@
|
||||
import React from 'react';
|
||||
import AddFeatureToggleForm from '../../component/feature/form-add-container';
|
||||
|
||||
const render = () => (
|
||||
<div>
|
||||
<h6>Create feature toggle</h6>
|
||||
<AddFeatureToggleForm />
|
||||
</div>
|
||||
);
|
||||
|
||||
const render = () => (<AddFeatureToggleForm title="Create feature toggle" />);
|
||||
|
||||
export default render;
|
||||
|
@ -1,16 +0,0 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import EditFeatureToggleForm from '../../component/feature/view-edit-container';
|
||||
|
||||
export default class Features extends Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
|
||||
);
|
||||
}
|
||||
};
|
17
frontend/src/page/features/show.js
Normal file
17
frontend/src/page/features/show.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import ViewFeatureToggle from '../../component/feature/view-container';
|
||||
|
||||
export default class Features extends PureComponent {
|
||||
static propTypes () {
|
||||
return {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { params } = this.props;
|
||||
return (
|
||||
<ViewFeatureToggle featureToggleName={params.name} activeTab={params.activeTab} />
|
||||
);
|
||||
}
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import HistoryListToggle from '../../component/history/history-list-toggle-component';
|
||||
import HistoryListToggle from '../../component/history/history-list-toggle-container';
|
||||
|
||||
const render = ({ params }) => <HistoryListToggle toggleName={params.toggleName} />;
|
||||
|
||||
|
10
frontend/src/page/strategies/show.js
Normal file
10
frontend/src/page/strategies/show.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ShowStrategy from '../../component/strategies/strategy-details-container';
|
||||
|
||||
const render = ({ params }) => <ShowStrategy strategyName={params.strategyName} activeTab={params.activeTab} />;
|
||||
|
||||
render.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default render;
|
@ -2,6 +2,7 @@ import api from '../../data/applications-api';
|
||||
|
||||
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
|
||||
export const ERROR_RECEIVE_ALL_APPLICATIONS = 'ERROR_RECEIVE_ALL_APPLICATIONS';
|
||||
export const ERROR_UPDATING_APPLICATION_DATA = 'ERROR_UPDATING_APPLICATION_DATA';
|
||||
|
||||
export const RECEIVE_APPLICATION = 'RECEIVE_APPLICATION';
|
||||
|
||||
@ -15,8 +16,8 @@ const recieveApplication = (json) => ({
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveApplications = (statusCode) => ({
|
||||
type: ERROR_RECEIVE_ALL_APPLICATIONS,
|
||||
const errorReceiveApplications = (statusCode, type = ERROR_RECEIVE_ALL_APPLICATIONS) => ({
|
||||
type,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
@ -26,6 +27,11 @@ export function fetchAll () {
|
||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
||||
}
|
||||
|
||||
export function storeApplicationMetaData (appName, key, value) {
|
||||
return dispatch => api.storeApplicationMetaData(appName, key, value)
|
||||
.catch(error => dispatch(errorReceiveApplications(error, ERROR_UPDATING_APPLICATION_DATA)));
|
||||
}
|
||||
|
||||
export function fetchApplication (appName) {
|
||||
return dispatch => api.fetchApplication(appName)
|
||||
.then(json => dispatch(recieveApplication(json)))
|
||||
|
@ -1,20 +0,0 @@
|
||||
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)));
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
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;
|
@ -7,6 +7,13 @@ import {
|
||||
ERROR_UPDATE_FEATURE_TOGGLE,
|
||||
} from './feature-actions';
|
||||
|
||||
import {
|
||||
ERROR_UPDATING_STRATEGY,
|
||||
ERROR_CREATING_STRATEGY,
|
||||
ERROR_RECEIVE_STRATEGIES,
|
||||
|
||||
} from './strategy/actions';
|
||||
|
||||
const debug = require('debug')('unleash:error-store');
|
||||
|
||||
function getInitState () {
|
||||
@ -29,6 +36,9 @@ const strategies = (state = getInitState(), action) => {
|
||||
case ERROR_REMOVE_FEATURE_TOGGLE:
|
||||
case ERROR_FETCH_FEATURE_TOGGLES:
|
||||
case ERROR_UPDATE_FEATURE_TOGGLE:
|
||||
case ERROR_UPDATING_STRATEGY:
|
||||
case ERROR_CREATING_STRATEGY:
|
||||
case ERROR_RECEIVE_STRATEGIES:
|
||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||
case MUTE_ERROR:
|
||||
return state.update('list', (list) => list.remove(list.indexOf(action.error)));
|
||||
|
@ -3,11 +3,18 @@ import api from '../data/history-api';
|
||||
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
|
||||
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
|
||||
|
||||
export const RECEIVE_HISTORY_FOR_TOGGLE = 'RECEIVE_HISTORY_FOR_TOGGLE';
|
||||
|
||||
const receiveHistory = (json) => ({
|
||||
type: RECEIVE_HISTORY,
|
||||
value: json.events,
|
||||
});
|
||||
|
||||
const receiveHistoryforToggle = (json) => ({
|
||||
type: RECEIVE_HISTORY_FOR_TOGGLE,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveHistory = (statusCode) => ({
|
||||
type: ERROR_RECEIVE_HISTORY,
|
||||
statusCode,
|
||||
@ -18,3 +25,10 @@ export function fetchHistory () {
|
||||
.then(json => dispatch(receiveHistory(json)))
|
||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
||||
}
|
||||
|
||||
|
||||
export function fetchHistoryForToggle (toggleName) {
|
||||
return dispatch => api.fetchHistoryForToggle(toggleName)
|
||||
.then(json => dispatch(receiveHistoryforToggle(json)))
|
||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_HISTORY } from './history-actions';
|
||||
import { RECEIVE_HISTORY, RECEIVE_HISTORY_FOR_TOGGLE } from './history-actions';
|
||||
|
||||
function getInitState () {
|
||||
return new $Map({ list: new List() });
|
||||
return new $Map({ list: new List(), toggles: new $Map() });
|
||||
}
|
||||
|
||||
const historyStore = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_HISTORY_FOR_TOGGLE:
|
||||
return state.setIn(['toggles', action.value.toggleName], new List(action.value.events));
|
||||
case RECEIVE_HISTORY:
|
||||
return state.set('list', new List(action.value));
|
||||
default:
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import features from './feature-store';
|
||||
import featureMetrics from './feature-metrics-store';
|
||||
import strategies from './strategy-store';
|
||||
import strategies from './strategy';
|
||||
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';
|
||||
import settings from './settings';
|
||||
import user from './user';
|
||||
@ -21,8 +19,6 @@ const unleashStore = combineReducers({
|
||||
history,
|
||||
archive,
|
||||
error,
|
||||
metrics,
|
||||
clientStrategies,
|
||||
clientInstances,
|
||||
settings,
|
||||
user,
|
||||
|
@ -13,7 +13,7 @@ export const createInc = ({ id, key }) => ({ type: actions.INCREMENT_VALUE, id,
|
||||
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, index }) => ({ type: actions.LIST_POP, id, key, index });
|
||||
export const createUp = ({ id, key, index, newValue }) => ({ type: actions.LIST_UP, id, key, index, newValue });
|
||||
export const createUp = ({ id, key, index, newValue, merge }) => ({ type: actions.LIST_UP, id, key, index, newValue, merge });
|
||||
export const createClear = ({ id }) => ({ type: actions.CLEAR, id });
|
||||
|
||||
export default actions;
|
||||
|
@ -48,11 +48,18 @@ function addToList (state, { id, key, value }) {
|
||||
return state.updateIn(id.concat([key]), (list) => list.push(value));
|
||||
}
|
||||
|
||||
function updateInList (state, { id, key, index, newValue }) {
|
||||
function updateInList (state, { id, key, index, newValue, merge }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => list.set(index, newValue));
|
||||
return state.updateIn(id.concat([key]), (list) => {
|
||||
if (merge && list.has(index)) {
|
||||
newValue = list.get(index).merge(new $Map(newValue));
|
||||
} else if (typeof newValue !== 'string' ) {
|
||||
newValue = fromJS(newValue);
|
||||
}
|
||||
return list.set(index, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromList (state, { id, key, index }) {
|
||||
|
@ -1,20 +0,0 @@
|
||||
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)));
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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;
|
@ -1,61 +0,0 @@
|
||||
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)));
|
||||
}
|
||||
|
||||
|
88
frontend/src/store/strategy/actions.js
Normal file
88
frontend/src/store/strategy/actions.js
Normal file
@ -0,0 +1,88 @@
|
||||
import api from '../../data/strategy-api';
|
||||
import { fetchApplicationsWithStrategyName } from '../../data/applications-api';
|
||||
|
||||
export const ADD_STRATEGY = 'ADD_STRATEGY';
|
||||
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
|
||||
export const REMOVE_STRATEGY = 'REMOVE_STRATEGY';
|
||||
export const REQUEST_STRATEGIES = 'REQUEST_STRATEGIES';
|
||||
export const START_CREATE_STRATEGY = 'START_CREATE_STRATEGY';
|
||||
export const START_UPDATE_STRATEGY = 'START_UPDATE_STRATEGY';
|
||||
export const RECEIVE_STRATEGIES = 'RECEIVE_STRATEGIES';
|
||||
export const ERROR_RECEIVE_STRATEGIES = 'ERROR_RECEIVE_STRATEGIES';
|
||||
export const ERROR_CREATING_STRATEGY = 'ERROR_CREATING_STRATEGY';
|
||||
export const ERROR_UPDATING_STRATEGY = 'ERROR_UPDATING_STRATEGY';
|
||||
|
||||
const addStrategy = (strategy) => ({ type: ADD_STRATEGY, strategy });
|
||||
const createRemoveStrategy = (strategy) => ({ type: REMOVE_STRATEGY, strategy });
|
||||
const updatedStrategy = (strategy) => ({ type: UPDATE_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,
|
||||
});
|
||||
|
||||
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
|
||||
|
||||
function dispatchAndThrow (dispatch, type) {
|
||||
return (error) => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
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 updateStrategy (strategy) {
|
||||
return dispatch => {
|
||||
dispatch(startUpdate());
|
||||
|
||||
return api.update(strategy)
|
||||
.then(() => dispatch(updatedStrategy(strategy)))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_UPDATING_STRATEGY));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function removeStrategy (strategy) {
|
||||
return dispatch => api.remove(strategy)
|
||||
.then(() => dispatch(createRemoveStrategy(strategy)))
|
||||
.catch(error => dispatch(errorCreatingStrategy(error)));
|
||||
}
|
||||
|
||||
export function getApplicationsWithStrategy (strategyName) {
|
||||
return fetchApplicationsWithStrategyName(strategyName);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_STRATEGIES, REMOVE_STRATEGY, ADD_STRATEGY } from './strategy-actions';
|
||||
import { RECEIVE_STRATEGIES, REMOVE_STRATEGY, ADD_STRATEGY, UPDATE_STRATEGY } from './actions';
|
||||
|
||||
function getInitState () {
|
||||
return new $Map({ list: new List() });
|
||||
@ -13,6 +13,16 @@ function removeStrategy (state, action) {
|
||||
return state;
|
||||
}
|
||||
|
||||
function updateStrategy (state, action) {
|
||||
return state.update('list', (list) => list.map(strategy => {
|
||||
if (strategy.name === action.strategy.name) {
|
||||
return action.strategy;
|
||||
} else {
|
||||
return strategy;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const strategies = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_STRATEGIES:
|
||||
@ -21,6 +31,8 @@ const strategies = (state = getInitState(), action) => {
|
||||
return removeStrategy(state, action);
|
||||
case ADD_STRATEGY:
|
||||
return state.update('list', (list) => list.push(action.strategy));
|
||||
case UPDATE_STRATEGY:
|
||||
return updateStrategy(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
@ -38,16 +38,20 @@ module.exports = {
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?$/,
|
||||
test: /\.(jsx|js)$/,
|
||||
exclude: /node_modules/,
|
||||
loaders: ['babel'],
|
||||
include: path.join(__dirname, 'src'),
|
||||
},
|
||||
{
|
||||
test: /(\.scss|\.css)$/,
|
||||
test: /(\.scss)$/,
|
||||
loader: ExtractTextPlugin.extract('style',
|
||||
'css?sourceMap&modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss!sass'),
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: 'style-loader!css-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user