mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
Use prettier (#87)
* Use prettier * Upgrade to 1.6 beta * Update lint deps * Upgrade to full 1.6
This commit is contained in:
parent
fc8d9a12b3
commit
683ae7e6d8
@ -1,7 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"finn",
|
||||
"finn/node"
|
||||
"finn/node",
|
||||
"finn-prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-shadow": 0
|
||||
|
@ -67,14 +67,16 @@
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-preset-stage-2": "^6.13.0",
|
||||
"css-loader": "^0.28.4",
|
||||
"eslint": "^4.1.1",
|
||||
"eslint": "^4.5.0",
|
||||
"eslint-config-finn": "^2.0.0",
|
||||
"eslint-config-finn-prettier": "^3.0.0",
|
||||
"eslint-config-finn-react": "^2.0.0",
|
||||
"eslint-plugin-react": "^7.1.0",
|
||||
"eslint-plugin-react": "^7.3.0",
|
||||
"extract-text-webpack-plugin": "^3.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^20.0.4",
|
||||
"node-sass": "^4.5.3",
|
||||
"prettier": "^1.6.0",
|
||||
"react-test-renderer": "^15.4.2",
|
||||
"redux-devtools": "^3.3.1",
|
||||
"sass-loader": "^6.0.6",
|
||||
|
@ -3,7 +3,9 @@
|
||||
"extends": [
|
||||
"finn",
|
||||
"finn-react",
|
||||
"finn/es-modules"
|
||||
"finn/es-modules",
|
||||
"finn-prettier",
|
||||
"finn-prettier/react"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
@ -16,10 +18,7 @@
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 7,
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"classes":true,
|
||||
"spread":true,
|
||||
"restParams": true
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
|
@ -1,7 +1,17 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { Layout, Drawer, Header, Navigation, Content,
|
||||
Footer, FooterSection, FooterDropDownSection, FooterLinkList,
|
||||
Grid, Cell, Icon,
|
||||
import {
|
||||
Layout,
|
||||
Drawer,
|
||||
Header,
|
||||
Navigation,
|
||||
Content,
|
||||
Footer,
|
||||
FooterSection,
|
||||
FooterDropDownSection,
|
||||
FooterLinkList,
|
||||
Grid,
|
||||
Cell,
|
||||
Icon,
|
||||
} from 'react-mdl';
|
||||
import { Link } from 'react-router';
|
||||
import styles from './styles.scss';
|
||||
@ -11,7 +21,7 @@ import UserContainer from './user/user-container';
|
||||
import ShowUserContainer from './user/show-user-container';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
|
||||
function replace (input, params) {
|
||||
function replace(input, params) {
|
||||
if (!params) {
|
||||
return input;
|
||||
}
|
||||
@ -26,13 +36,13 @@ export default class App extends Component {
|
||||
location: PropTypes.object.isRequired,
|
||||
params: PropTypes.object.isRequired,
|
||||
routes: PropTypes.array.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
};
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.location.pathname !== nextProps.location.pathname) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = setTimeout(() => {
|
||||
@ -46,11 +56,12 @@ export default class App extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getSections () {
|
||||
getSections() {
|
||||
const { routes, params } = this.props;
|
||||
const unique = {};
|
||||
const result = routes.splice(1)
|
||||
.map((routeEntry) => ({
|
||||
const result = routes
|
||||
.splice(1)
|
||||
.map(routeEntry => ({
|
||||
name: replace(routeEntry.pageTitle, params),
|
||||
link: replace(routeEntry.link || routeEntry.path, params),
|
||||
}))
|
||||
@ -72,14 +83,25 @@ export default class App extends Component {
|
||||
return result;
|
||||
}
|
||||
|
||||
getTitleWithLinks () {
|
||||
getTitleWithLinks() {
|
||||
const result = this.getSections();
|
||||
return (
|
||||
<span>
|
||||
{result.map((entry, index) => (
|
||||
<span key={entry.link + index} className={index > 0 ? 'mdl-layout--large-screen-only' : ''}>
|
||||
<span
|
||||
key={entry.link + index}
|
||||
className={
|
||||
index > 0 ? 'mdl-layout--large-screen-only' : ''
|
||||
}
|
||||
>
|
||||
{index > 0 ? ' › ' : null}
|
||||
<Link className={[styles.headerTitleLink, 'mdl-color-text--primary-contrast'].join(' ')} to={entry.link}>
|
||||
<Link
|
||||
className={[
|
||||
styles.headerTitleLink,
|
||||
'mdl-color-text--primary-contrast',
|
||||
].join(' ')}
|
||||
to={entry.link}
|
||||
>
|
||||
{entry.name}
|
||||
</Link>
|
||||
</span>
|
||||
@ -88,24 +110,49 @@ export default class App extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
if (prevRouterProps && location.pathname !== prevRouterProps.location.pathname) {
|
||||
if (
|
||||
prevRouterProps &&
|
||||
location.pathname !== prevRouterProps.location.pathname
|
||||
) {
|
||||
return location.action === 'POP';
|
||||
} else {
|
||||
return [0, 0];
|
||||
}
|
||||
};
|
||||
const createListItem = (path, caption, icon, isDrawerNavigation = false) => {
|
||||
const linkColor = isDrawerNavigation &&
|
||||
this.context.router.isActive(path) ? 'mdl-color-text--black' : 'mdl-color-text--grey-900';
|
||||
const iconColor = isDrawerNavigation &&
|
||||
this.context.router.isActive(path) ? 'mdl-color-text--black' : 'mdl-color-text--grey-600';
|
||||
const createListItem = (
|
||||
path,
|
||||
caption,
|
||||
icon,
|
||||
isDrawerNavigation = false
|
||||
) => {
|
||||
const linkColor =
|
||||
isDrawerNavigation && this.context.router.isActive(path)
|
||||
? 'mdl-color-text--black'
|
||||
: 'mdl-color-text--grey-900';
|
||||
const iconColor =
|
||||
isDrawerNavigation && this.context.router.isActive(path)
|
||||
? 'mdl-color-text--black'
|
||||
: 'mdl-color-text--grey-600';
|
||||
return (
|
||||
<Link
|
||||
to={path}
|
||||
className={isDrawerNavigation && [styles.navigationLink, linkColor].join(' ')}>
|
||||
{icon && <Icon name={icon} className={isDrawerNavigation && [styles.navigationIcon, iconColor].join(' ')}/>}{caption}
|
||||
className={
|
||||
isDrawerNavigation &&
|
||||
[styles.navigationLink, linkColor].join(' ')
|
||||
}
|
||||
>
|
||||
{icon && (
|
||||
<Icon
|
||||
name={icon}
|
||||
className={
|
||||
isDrawerNavigation &&
|
||||
[styles.navigationIcon, iconColor].join(' ')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{caption}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@ -120,30 +167,79 @@ export default class App extends Component {
|
||||
</Navigation>
|
||||
</Header>
|
||||
<Drawer className="mdl-color--white">
|
||||
<span className={[styles.drawerTitle, 'mdl-layout-title'].join(' ')}>
|
||||
<img src="public/logo.png" width="32" height="32" className={styles.drawerTitleLogo}/>
|
||||
<span className={styles.drawerTitleText}>Unleash</span>
|
||||
<span
|
||||
className={[
|
||||
styles.drawerTitle,
|
||||
'mdl-layout-title',
|
||||
].join(' ')}
|
||||
>
|
||||
<img
|
||||
src="public/logo.png"
|
||||
width="32"
|
||||
height="32"
|
||||
className={styles.drawerTitleLogo}
|
||||
/>
|
||||
<span className={styles.drawerTitleText}>
|
||||
Unleash
|
||||
</span>
|
||||
</span>
|
||||
<hr/>
|
||||
<hr />
|
||||
<Navigation className={styles.navigation}>
|
||||
{createListItem('/features', 'Feature Toggles', 'list', true)}
|
||||
{createListItem('/strategies', 'Strategies', 'extension', true)}
|
||||
{createListItem('/history', 'Event History', 'history', true)}
|
||||
{createListItem('/archive', 'Archived Toggles', 'archive', true)}
|
||||
{createListItem('/applications', 'Applications', 'apps', true)}
|
||||
{createListItem(
|
||||
'/features',
|
||||
'Feature Toggles',
|
||||
'list',
|
||||
true
|
||||
)}
|
||||
{createListItem(
|
||||
'/strategies',
|
||||
'Strategies',
|
||||
'extension',
|
||||
true
|
||||
)}
|
||||
{createListItem(
|
||||
'/history',
|
||||
'Event History',
|
||||
'history',
|
||||
true
|
||||
)}
|
||||
{createListItem(
|
||||
'/archive',
|
||||
'Archived Toggles',
|
||||
'archive',
|
||||
true
|
||||
)}
|
||||
{createListItem(
|
||||
'/applications',
|
||||
'Applications',
|
||||
'apps',
|
||||
true
|
||||
)}
|
||||
</Navigation>
|
||||
<hr/>
|
||||
<hr />
|
||||
<Navigation className={styles.navigation}>
|
||||
<a href="https://github.com/Unleash" target="_blank" className={[styles.navigationLink, 'mdl-color-text--grey-900'].join(' ')}>
|
||||
<i className={[
|
||||
'material-icons',
|
||||
styles.navigationIcon,
|
||||
styles.iconGitHub,
|
||||
].join(' ')}/>GitHub
|
||||
<a
|
||||
href="https://github.com/Unleash"
|
||||
target="_blank"
|
||||
className={[
|
||||
styles.navigationLink,
|
||||
'mdl-color-text--grey-900',
|
||||
].join(' ')}
|
||||
>
|
||||
<i
|
||||
className={[
|
||||
'material-icons',
|
||||
styles.navigationIcon,
|
||||
styles.iconGitHub,
|
||||
].join(' ')}
|
||||
/>GitHub
|
||||
</a>
|
||||
</Navigation>
|
||||
</Drawer>
|
||||
<ScrollContainer scrollKey="container" shouldUpdateScroll={shouldUpdateScroll}>
|
||||
<ScrollContainer
|
||||
scrollKey="container"
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
>
|
||||
<Content className="mdl-color--grey-50">
|
||||
<Grid noSpacing className={styles.content}>
|
||||
<Cell col={12}>
|
||||
@ -155,27 +251,56 @@ export default class App extends Component {
|
||||
<FooterSection type="middle">
|
||||
<FooterDropDownSection title="Menu">
|
||||
<FooterLinkList>
|
||||
{createListItem('/features', 'Feature Toggles')}
|
||||
{createListItem('/strategies', 'Strategies')}
|
||||
{createListItem('/history', 'Event History')}
|
||||
{createListItem('/archive', 'Archived Toggles')}
|
||||
{createListItem('/applications', 'Applications')}
|
||||
{createListItem(
|
||||
'/features',
|
||||
'Feature Toggles'
|
||||
)}
|
||||
{createListItem(
|
||||
'/strategies',
|
||||
'Strategies'
|
||||
)}
|
||||
{createListItem(
|
||||
'/history',
|
||||
'Event History'
|
||||
)}
|
||||
{createListItem(
|
||||
'/archive',
|
||||
'Archived Toggles'
|
||||
)}
|
||||
{createListItem(
|
||||
'/applications',
|
||||
'Applications'
|
||||
)}
|
||||
</FooterLinkList>
|
||||
</FooterDropDownSection>
|
||||
<FooterDropDownSection title="Clients">
|
||||
<FooterLinkList>
|
||||
<a href="https://github.com/Unleash/unleash-client-node/">Node.js</a>
|
||||
<a href="https://github.com/Unleash/unleash-client-java/">Java</a>
|
||||
<a href="https://github.com/Unleash/unleash-client-go/">Go</a>
|
||||
<a href="https://github.com/Unleash/unleash-client-node/">
|
||||
Node.js
|
||||
</a>
|
||||
<a href="https://github.com/Unleash/unleash-client-java/">
|
||||
Java
|
||||
</a>
|
||||
<a href="https://github.com/Unleash/unleash-client-go/">
|
||||
Go
|
||||
</a>
|
||||
</FooterLinkList>
|
||||
</FooterDropDownSection>
|
||||
</FooterSection>
|
||||
<FooterSection type="bottom" logo="Unleash">
|
||||
<FooterLinkList>
|
||||
<a href="https://github.com/Unleash/unleash/" target="_blank">
|
||||
<a
|
||||
href="https://github.com/Unleash/unleash/"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a href="https://finn.no" target="_blank"><small>A product by</small> FINN.no</a>
|
||||
<a
|
||||
href="https://finn.no"
|
||||
target="_blank"
|
||||
>
|
||||
<small>A product by</small> FINN.no
|
||||
</a>
|
||||
</FooterLinkList>
|
||||
</FooterSection>
|
||||
</Footer>
|
||||
@ -185,4 +310,4 @@ export default class App extends Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -6,9 +6,9 @@ import renderer from 'react-test-renderer';
|
||||
jest.mock('react-mdl');
|
||||
|
||||
test('renders correctly if no application', () => {
|
||||
const tree = renderer.create(
|
||||
<ClientApplications fetchApplication={jest.fn()} />
|
||||
).toJSON();
|
||||
const tree = renderer
|
||||
.create(<ClientApplications fetchApplication={jest.fn()} />)
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
@ -3,55 +3,64 @@ import React, { Component, PureComponent } from 'react';
|
||||
|
||||
import { Link } from 'react-router';
|
||||
import {
|
||||
Grid, Cell, Card, CardTitle, CardText, CardMenu,
|
||||
List, ListItem, ListItemContent,
|
||||
Textfield, Icon, ProgressBar,
|
||||
Tabs, Tab,
|
||||
Grid,
|
||||
Cell,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardText,
|
||||
CardMenu,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
Textfield,
|
||||
Icon,
|
||||
ProgressBar,
|
||||
Tabs,
|
||||
Tab,
|
||||
Switch,
|
||||
} from 'react-mdl';
|
||||
import { IconLink, shorten, styles as commonStyles } from '../common';
|
||||
import { formatFullDateTime } from '../common/util';
|
||||
|
||||
class StatefulTextfield extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { value: props.value };
|
||||
this.setValue = function setValue (e) {
|
||||
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} />
|
||||
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) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { activeTab: 0 };
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchApplication(this.props.appName);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (!this.props.application) {
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
const {
|
||||
application,
|
||||
storeApplicationMetaData,
|
||||
} = this.props;
|
||||
const { application, storeApplicationMetaData } = this.props;
|
||||
const {
|
||||
appName,
|
||||
instances,
|
||||
@ -63,108 +72,210 @@ class ClientApplications extends PureComponent {
|
||||
color,
|
||||
} = application;
|
||||
|
||||
const content = this.state.activeTab === 0 ? (
|
||||
<Grid style={{ margin: 0 }}>
|
||||
<Cell col={6} 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={shorten(description, 60)}>
|
||||
<Link to={`/features/view/${name}`}>
|
||||
{shorten(name, 50)}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>)
|
||||
)}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={6} 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={shorten(description, 60)}>
|
||||
<Link to={`/strategies/view/${name}`}>
|
||||
{shorten(name, 50)}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={12} tablet={12}>
|
||||
<h6>{instances.length} Instances registered</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{instances.map(({ instanceId, clientIp, lastSeen, sdkVersion }, i) => (
|
||||
<ListItem key={i} twoLine>
|
||||
<ListItemContent
|
||||
icon="timeline"
|
||||
subtitle={
|
||||
<span>{clientIp} last seen at <small>{formatFullDateTime(lastSeen)}</small></span>
|
||||
}>
|
||||
{instanceId} {sdkVersion ? `(${sdkVersion})` : ''}
|
||||
</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>);
|
||||
|
||||
const content =
|
||||
this.state.activeTab === 0 ? (
|
||||
<Grid style={{ margin: 0 }}>
|
||||
<Cell col={6} 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={shorten(
|
||||
description,
|
||||
60
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
to={`/features/view/${name}`}
|
||||
>
|
||||
{shorten(name, 50)}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={6} 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={shorten(
|
||||
description,
|
||||
60
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
to={`/strategies/view/${name}`}
|
||||
>
|
||||
{shorten(name, 50)}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
)
|
||||
)}
|
||||
</List>
|
||||
</Cell>
|
||||
<Cell col={12} tablet={12}>
|
||||
<h6>{instances.length} Instances registered</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{instances.map(
|
||||
(
|
||||
{
|
||||
instanceId,
|
||||
clientIp,
|
||||
lastSeen,
|
||||
sdkVersion,
|
||||
},
|
||||
i
|
||||
) => (
|
||||
<ListItem key={i} twoLine>
|
||||
<ListItemContent
|
||||
icon="timeline"
|
||||
subtitle={
|
||||
<span>
|
||||
{clientIp} last seen at{' '}
|
||||
<small>{formatFullDateTime(lastSeen)}</small>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{instanceId}{' '}
|
||||
{sdkVersion ? `(${sdkVersion})` : ''}
|
||||
</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 (
|
||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
||||
<CardTitle style={{ paddingTop: '24px', paddingRight: '64px', wordBreak: 'break-all' }}>
|
||||
<CardTitle
|
||||
style={{
|
||||
paddingTop: '24px',
|
||||
paddingRight: '64px',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} /> {appName}
|
||||
</CardTitle>
|
||||
{description &&
|
||||
<CardText>{description}</CardText>
|
||||
}
|
||||
{url &&
|
||||
<CardMenu><IconLink url={url} icon="link"/></CardMenu>
|
||||
}
|
||||
<hr/>
|
||||
<Tabs activeTab={this.state.activeTab} onChange={(tabId) => this.setState({ activeTab: tabId })} ripple
|
||||
tabBarProps={{ style: { width: '100%' } }} className="mdl-color--grey-100">
|
||||
{description && <CardText>{description}</CardText>}
|
||||
{url && (
|
||||
<CardMenu>
|
||||
<IconLink url={url} icon="link" />
|
||||
</CardMenu>
|
||||
)}
|
||||
<hr />
|
||||
<Tabs
|
||||
activeTab={this.state.activeTab}
|
||||
onChange={tabId => this.setState({ activeTab: tabId })}
|
||||
ripple
|
||||
tabBarProps={{ style: { width: '100%' } }}
|
||||
className="mdl-color--grey-100"
|
||||
>
|
||||
<Tab>Details</Tab>
|
||||
<Tab>Edit</Tab>
|
||||
</Tabs>
|
||||
@ -175,5 +286,4 @@ class ClientApplications extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientApplications;
|
||||
|
@ -1,6 +1,9 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ApplicationEdit from './application-edit-component';
|
||||
import { fetchApplication, storeApplicationMetaData } from '../../store/application/actions';
|
||||
import {
|
||||
fetchApplication,
|
||||
storeApplicationMetaData,
|
||||
} from '../../store/application/actions';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
let application = state.applications.getIn(['apps', props.appName]);
|
||||
|
@ -3,14 +3,12 @@ import { ProgressBar, Card } from 'react-mdl';
|
||||
import { AppsLinkList, styles as commonStyles } from '../common';
|
||||
|
||||
class ClientStrategies extends Component {
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchAll();
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
applications,
|
||||
} = this.props;
|
||||
render() {
|
||||
const { applications } = this.props;
|
||||
|
||||
if (!applications) {
|
||||
return <ProgressBar indeterminate />;
|
||||
@ -23,5 +21,4 @@ class ClientStrategies extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientStrategies;
|
||||
|
@ -2,7 +2,9 @@ import { connect } from 'react-redux';
|
||||
import ApplicationList from './application-list-component';
|
||||
import { fetchAll } from '../../store/application/actions';
|
||||
|
||||
const mapStateToProps = (state) => ({ applications: state.applications.get('list').toJS() });
|
||||
const mapStateToProps = state => ({
|
||||
applications: state.applications.get('list').toJS(),
|
||||
});
|
||||
|
||||
const Container = connect(mapStateToProps, { fetchAll })(ApplicationList);
|
||||
|
||||
|
@ -102,7 +102,8 @@ exports[`renders correctly with no archived toggles 1`] = `
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
No archived feature toggles, go see
|
||||
No archived feature toggles, go see
|
||||
|
||||
<a
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
|
@ -13,9 +13,11 @@ const archive = [
|
||||
strategies: [{ name: 'default', parameters: {} }],
|
||||
createdAt: '2016-10-25T15:38:28.573Z',
|
||||
reviveName: 'adin-pay-confirm-disabled',
|
||||
}, {
|
||||
},
|
||||
{
|
||||
name: 'adin-pay-platform-sch-payment',
|
||||
description: 'Enables use of schibsted payment from order-payment-management',
|
||||
description:
|
||||
'Enables use of schibsted payment from order-payment-management',
|
||||
enabled: true,
|
||||
strategies: [{ name: 'default', parameters: {} }],
|
||||
createdAt: '2016-08-03T12:41:35.631Z',
|
||||
@ -24,17 +26,17 @@ const archive = [
|
||||
];
|
||||
|
||||
test('renders correctly with no archived toggles', () => {
|
||||
const tree = renderer.create(
|
||||
<ArchiveList fetchArchive={jest.fn()} archive={[]} />
|
||||
).toJSON();
|
||||
const tree = renderer
|
||||
.create(<ArchiveList fetchArchive={jest.fn()} archive={[]} />)
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders correctly with archived toggles', () => {
|
||||
const tree = renderer.create(
|
||||
<ArchiveList fetchArchive={jest.fn()} archive={archive} />
|
||||
).toJSON();
|
||||
const tree = renderer
|
||||
.create(<ArchiveList fetchArchive={jest.fn()} archive={archive} />)
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||
import ListComponent from './archive-list-component';
|
||||
import { fetchArchive, revive } from '../../store/archive-actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = state => {
|
||||
const archive = state.archive.get('list').toArray();
|
||||
|
||||
return {
|
||||
@ -10,6 +10,8 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
const ArchiveListContainer = connect(mapStateToProps, { fetchArchive, revive })(ListComponent);
|
||||
const ArchiveListContainer = connect(mapStateToProps, { fetchArchive, revive })(
|
||||
ListComponent
|
||||
);
|
||||
|
||||
export default ArchiveListContainer;
|
||||
|
@ -4,42 +4,65 @@ import { DataTable, TableHeader, IconButton, Icon, Card } from 'react-mdl';
|
||||
import { styles as commonStyles } from '../common';
|
||||
|
||||
class ArchiveList extends Component {
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchArchive();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { archive, revive } = this.props;
|
||||
archive.forEach(e => {
|
||||
e.reviveName = e.name;
|
||||
});
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth}>
|
||||
{
|
||||
archive.length > 0 ?
|
||||
<div className={commonStyles.horisontalScroll}>
|
||||
<DataTable
|
||||
rows={archive}
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ border: 0 }}>
|
||||
<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> :
|
||||
<div className={commonStyles.emptyState}>
|
||||
<Icon name="archive" className="mdl-color-text--grey-300" style={{ fontSize: '56px' }}/><br />
|
||||
No archived feature toggles, go see <Link to="/features">active toggles here</Link>
|
||||
</div>
|
||||
}
|
||||
{archive.length > 0 ? (
|
||||
<div className={commonStyles.horisontalScroll}>
|
||||
<DataTable
|
||||
rows={archive}
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ border: 0 }}
|
||||
>
|
||||
<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>
|
||||
) : (
|
||||
<div className={commonStyles.emptyState}>
|
||||
<Icon
|
||||
name="archive"
|
||||
className="mdl-color-text--grey-300"
|
||||
style={{ fontSize: '56px' }}
|
||||
/>
|
||||
<br />
|
||||
No archived feature toggles, go see{' '}
|
||||
<Link to="/features">active toggles here</Link>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ArchiveList;
|
||||
|
@ -6,9 +6,14 @@ import renderer from 'react-test-renderer';
|
||||
jest.mock('react-mdl');
|
||||
|
||||
test('renders correctly with no clientInstances', () => {
|
||||
const tree = renderer.create(
|
||||
<ClientStrategies fetchClientInstances={jest.fn()} clientInstances={[]} />
|
||||
).toJSON();
|
||||
const tree = renderer
|
||||
.create(
|
||||
<ClientStrategies
|
||||
fetchClientInstances={jest.fn()}
|
||||
clientInstances={[]}
|
||||
/>
|
||||
)
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
@ -5,13 +5,13 @@ class ClientStrategies extends Component {
|
||||
static propTypes = {
|
||||
fetchClientInstances: PropTypes.func.isRequired,
|
||||
clientInstances: PropTypes.array.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchClientInstances();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const source = this.props.clientInstances;
|
||||
|
||||
return (
|
||||
@ -20,18 +20,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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ClientStrategies;
|
||||
|
@ -2,8 +2,12 @@ import { connect } from 'react-redux';
|
||||
import ClientInstances from './client-instance-component';
|
||||
import { fetchClientInstances } from '../../store/client-instance-actions';
|
||||
|
||||
const mapStateToProps = (state) => ({ clientInstances: state.clientInstances.toJS() });
|
||||
const mapStateToProps = state => ({
|
||||
clientInstances: state.clientInstances.toJS(),
|
||||
});
|
||||
|
||||
const StrategiesContainer = connect(mapStateToProps, { fetchClientInstances })(ClientInstances);
|
||||
const StrategiesContainer = connect(mapStateToProps, { fetchClientInstances })(
|
||||
ClientInstances
|
||||
);
|
||||
|
||||
export default StrategiesContainer;
|
||||
|
@ -1,42 +1,71 @@
|
||||
const React = require('react');
|
||||
import styles from './common.scss';
|
||||
|
||||
|
||||
const {
|
||||
List, ListItem, ListItemContent,
|
||||
Button, Icon,
|
||||
Switch, MenuItem,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
Button,
|
||||
Icon,
|
||||
Switch,
|
||||
MenuItem,
|
||||
} = require('react-mdl');
|
||||
const { Link } = require('react-router');
|
||||
|
||||
export { styles };
|
||||
|
||||
export const shorten = (str, len = 50) => (str && str.length > len ? `${str.substring(0, len)}...` : str);
|
||||
export const shorten = (str, len = 50) =>
|
||||
str && str.length > len ? `${str.substring(0, len)}...` : str;
|
||||
|
||||
export const AppsLinkList = ({ apps }) => (
|
||||
<List>
|
||||
{apps.length > 0 && apps.map(({ appName, description = '-', icon = 'apps' }) => (
|
||||
<ListItem twoLine key={appName}>
|
||||
<span className="mdl-list__item-primary-content" style={{ minWidth: 0 }}>
|
||||
<Icon name={icon} className="mdl-list__item-avatar"/>
|
||||
<Link to={`/applications/${appName}`} className={[styles.listLink, styles.truncate].join(' ')}>
|
||||
{appName}
|
||||
<span className={['mdl-list__item-sub-title', styles.truncate].join(' ')}>{description}</span>
|
||||
</Link>
|
||||
</span>
|
||||
</ListItem>
|
||||
))}
|
||||
{apps.length > 0 &&
|
||||
apps.map(({ appName, description = '-', icon = 'apps' }) => (
|
||||
<ListItem twoLine key={appName}>
|
||||
<span
|
||||
className="mdl-list__item-primary-content"
|
||||
style={{ minWidth: 0 }}
|
||||
>
|
||||
<Icon name={icon} className="mdl-list__item-avatar" />
|
||||
<Link
|
||||
to={`/applications/${appName}`}
|
||||
className={[styles.listLink, styles.truncate].join(
|
||||
' '
|
||||
)}
|
||||
>
|
||||
{appName}
|
||||
<span
|
||||
className={[
|
||||
'mdl-list__item-sub-title',
|
||||
styles.truncate,
|
||||
].join(' ')}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</Link>
|
||||
</span>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
export const HeaderTitle = ({ title, actions, subtitle }) => (
|
||||
<div style={{ display: 'flex', borderBottom: '1px solid #f1f1f1', marginBottom: '10px', padding: '16px 20px ' }}>
|
||||
<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>}
|
||||
{actions && (
|
||||
<div style={{ flex: '1', textAlign: 'right' }}>{actions}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -45,11 +74,7 @@ export const DataTableHeader = ({ title, actions }) => (
|
||||
<div className={styles.title}>
|
||||
<h2 className={styles.titleText}>{title}</h2>
|
||||
</div>
|
||||
{actions &&
|
||||
<div className={styles.actions}>
|
||||
{actions}
|
||||
</div>
|
||||
}
|
||||
{actions && <div className={styles.actions}>{actions}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -57,17 +82,27 @@ export const FormButtons = ({ submitText = 'Create', onCancel }) => (
|
||||
<div>
|
||||
<Button type="submit" ripple raised primary icon="add">
|
||||
<Icon name="add" />
|
||||
{ submitText }
|
||||
{submitText}
|
||||
</Button>
|
||||
|
||||
<Button type="cancel" ripple raised onClick={onCancel} style={{ float: 'right' }}>
|
||||
<Icon name="cancel" />
|
||||
Cancel
|
||||
<Button
|
||||
type="cancel"
|
||||
ripple
|
||||
raised
|
||||
onClick={onCancel}
|
||||
style={{ float: 'right' }}
|
||||
>
|
||||
<Icon name="cancel" /> Cancel
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SwitchWithLabel = ({ onChange, checked, children, ...switchProps }) => (
|
||||
export const SwitchWithLabel = ({
|
||||
onChange,
|
||||
checked,
|
||||
children,
|
||||
...switchProps
|
||||
}) => (
|
||||
<span className={styles.switchWithLabel}>
|
||||
<span className={styles.label}>{children}</span>
|
||||
<span className={styles.switch}>
|
||||
@ -78,54 +113,75 @@ export const SwitchWithLabel = ({ onChange, checked, children, ...switchProps })
|
||||
|
||||
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/view/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
{toggles.length > 0 &&
|
||||
toggles.map(({ name, description = '-', icon = 'toggle' }) => (
|
||||
<ListItem twoLine key={name}>
|
||||
<ListItemContent avatar={icon} subtitle={description}>
|
||||
<Link key={name} to={`/features/view/${name}`}>
|
||||
{name}
|
||||
</Link>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
|
||||
export function getIcon (type) {
|
||||
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';
|
||||
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 = ({ url, icon }) => (
|
||||
<a href={url} target="_blank" rel="noopener" className="mdl-color-text--grey-600">
|
||||
<Icon name={icon}/>
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="mdl-color-text--grey-600"
|
||||
>
|
||||
<Icon name={icon} />
|
||||
</a>
|
||||
);
|
||||
|
||||
export const DropdownButton = ({ label, id }) => (
|
||||
<Button id={id} className={styles.dropdownButton}>
|
||||
{label}
|
||||
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600"/>
|
||||
<Icon name="arrow_drop_down" className="mdl-color-text--grey-600" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const MenuItemWithIcon = ({ icon, label, disabled, ...menuItemProps }) => (
|
||||
<MenuItem disabled={disabled} style={{ display: 'flex', alignItems: 'center' }} {...menuItemProps}>
|
||||
<Icon name={icon} style={{ paddingRight: '16px' }}/>
|
||||
export const MenuItemWithIcon = ({
|
||||
icon,
|
||||
label,
|
||||
disabled,
|
||||
...menuItemProps
|
||||
}) => (
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
{...menuItemProps}
|
||||
>
|
||||
<Icon name={icon} style={{ paddingRight: '16px' }} />
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const badNumbers = [NaN, Infinity, -Infinity];
|
||||
export function calc (value, total, decimal) {
|
||||
if (typeof value !== 'number' ||
|
||||
export function calc(value, total, decimal) {
|
||||
if (
|
||||
typeof value !== 'number' ||
|
||||
typeof total !== 'number' ||
|
||||
typeof decimal !== 'number') {
|
||||
typeof decimal !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -133,7 +189,7 @@ export function calc (value, total, decimal) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
badNumbers.forEach((number) => {
|
||||
badNumbers.forEach(number => {
|
||||
if ([value, total, decimal].indexOf(number) > -1) {
|
||||
return number;
|
||||
}
|
||||
|
@ -1,3 +1,11 @@
|
||||
const dateTimeOptions = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' };
|
||||
const dateTimeOptions = {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
};
|
||||
|
||||
export const formatFullDateTime = v => new Date(v).toLocaleString('nb-NO', dateTimeOptions);
|
||||
export const formatFullDateTime = v =>
|
||||
new Date(v).toLocaleString('nb-NO', dateTimeOptions);
|
||||
|
@ -6,9 +6,9 @@ class ErrorComponent extends React.Component {
|
||||
static propTypes = {
|
||||
errors: PropTypes.array.isRequired,
|
||||
muteError: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const showError = this.props.errors.length > 0;
|
||||
const error = showError ? this.props.errors[0] : undefined;
|
||||
const muteError = () => this.props.muteError(error);
|
||||
|
@ -2,12 +2,11 @@ import { connect } from 'react-redux';
|
||||
import ErrorComponent from './error-component';
|
||||
import { muteError } from '../../store/error-actions';
|
||||
|
||||
|
||||
const mapDispatchToProps = {
|
||||
muteError,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = state => ({
|
||||
errors: state.error.get('list').toArray(),
|
||||
});
|
||||
|
||||
|
@ -16,36 +16,85 @@ const Feature = ({
|
||||
const { name, description, enabled, strategies } = feature;
|
||||
|
||||
const { showLastHour = false } = settings;
|
||||
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
||||
const isStale = showLastHour
|
||||
? metricsLastHour.isFallback
|
||||
: metricsLastMinute.isFallback;
|
||||
|
||||
const percent = 1 * (showLastHour ?
|
||||
calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) :
|
||||
calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)
|
||||
);
|
||||
const percent =
|
||||
1 *
|
||||
(showLastHour
|
||||
? calc(
|
||||
metricsLastHour.yes,
|
||||
metricsLastHour.yes + metricsLastHour.no,
|
||||
0
|
||||
)
|
||||
: calc(
|
||||
metricsLastMinute.yes,
|
||||
metricsLastMinute.yes + metricsLastMinute.no,
|
||||
0
|
||||
));
|
||||
|
||||
const strategiesToShow = Math.min(strategies.length, 3);
|
||||
const remainingStrategies = strategies.length - strategiesToShow;
|
||||
|
||||
const strategyChips = strategies && strategies.slice(0, strategiesToShow).map((s, i) =>
|
||||
<Chip className={styles.strategyChip} key={i}>{s.name}</Chip>);
|
||||
const summaryChip = remainingStrategies > 0 &&
|
||||
<Chip className={styles.strategyChip}>+{remainingStrategies}</Chip>;
|
||||
const strategyChips =
|
||||
strategies &&
|
||||
strategies.slice(0, strategiesToShow).map((s, i) => (
|
||||
<Chip className={styles.strategyChip} key={i}>
|
||||
{s.name}
|
||||
</Chip>
|
||||
));
|
||||
const summaryChip = remainingStrategies > 0 && (
|
||||
<Chip className={styles.strategyChip}>+{remainingStrategies}</Chip>
|
||||
);
|
||||
|
||||
return (
|
||||
<ListItem twoLine>
|
||||
<span className={styles.listItemMetric}>
|
||||
<Progress strokeWidth={15} percentage={percent} isFallback={isStale}/>
|
||||
<Progress
|
||||
strokeWidth={15}
|
||||
percentage={percent}
|
||||
isFallback={isStale}
|
||||
/>
|
||||
</span>
|
||||
<span className={styles.listItemToggle}>
|
||||
<Switch title={`Toggle ${name}`} key="left-actions" onChange={() => toggleFeature(name)} checked={enabled} />
|
||||
<Switch
|
||||
title={`Toggle ${name}`}
|
||||
key="left-actions"
|
||||
onChange={() => toggleFeature(name)}
|
||||
checked={enabled}
|
||||
/>
|
||||
</span>
|
||||
<span className={['mdl-list__item-primary-content', styles.listItemLink].join(' ')}>
|
||||
<Link to={`/features/view/${name}`} className={[commonStyles.listLink, commonStyles.truncate].join(' ')}>
|
||||
<span
|
||||
className={[
|
||||
'mdl-list__item-primary-content',
|
||||
styles.listItemLink,
|
||||
].join(' ')}
|
||||
>
|
||||
<Link
|
||||
to={`/features/view/${name}`}
|
||||
className={[
|
||||
commonStyles.listLink,
|
||||
commonStyles.truncate,
|
||||
].join(' ')}
|
||||
>
|
||||
{name}
|
||||
<span className={['mdl-list__item-sub-title', commonStyles.truncate].join(' ')}>{description}</span>
|
||||
<span
|
||||
className={[
|
||||
'mdl-list__item-sub-title',
|
||||
commonStyles.truncate,
|
||||
].join(' ')}
|
||||
>
|
||||
{description}
|
||||
</span>
|
||||
</Link>
|
||||
</span>
|
||||
<span className={[styles.listItemStrategies, commonStyles.hideLt920].join(' ')}>
|
||||
<span
|
||||
className={[
|
||||
styles.listItemStrategies,
|
||||
commonStyles.hideLt920,
|
||||
].join(' ')}
|
||||
>
|
||||
{strategyChips}
|
||||
{summaryChip}
|
||||
</span>
|
||||
|
@ -7,7 +7,7 @@ import { styles as commonStyles } from '../common';
|
||||
const FormAddComponent = ({ title, ...formProps }) => (
|
||||
<Card className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ paddingTop: '24px' }}>{title}</CardTitle>
|
||||
<FormComponent {...formProps}/>
|
||||
<FormComponent {...formProps} />
|
||||
</Card>
|
||||
);
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { hashHistory } from 'react-router';
|
||||
import { createFeatureToggles, validateName } from '../../store/feature-actions';
|
||||
import {
|
||||
createFeatureToggles,
|
||||
validateName,
|
||||
} from '../../store/feature-actions';
|
||||
import { createMapper, createActions } from '../input-helpers';
|
||||
import FormAddComponent from './form-add-component';
|
||||
|
||||
const ID = 'add-feature-toggle';
|
||||
const mapStateToProps = createMapper({
|
||||
id: ID,
|
||||
getDefault () {
|
||||
getDefault() {
|
||||
let name;
|
||||
try {
|
||||
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||
@ -16,30 +19,27 @@ const mapStateToProps = createMapper({
|
||||
},
|
||||
});
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
methods.onSubmit = input => e => {
|
||||
e.preventDefault();
|
||||
|
||||
if (Array.isArray(input.strategies)) {
|
||||
input.strategies.forEach((s) => {
|
||||
delete s.id;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
createFeatureToggles(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/features/edit/${input.name}`));
|
||||
if (Array.isArray(input.strategies)) {
|
||||
input.strategies.forEach(s => {
|
||||
delete s.id;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (evt) => {
|
||||
createFeatureToggles(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/features/edit/${input.name}`));
|
||||
};
|
||||
|
||||
methods.onCancel = evt => {
|
||||
evt.preventDefault();
|
||||
methods.clear();
|
||||
hashHistory.push('/features');
|
||||
};
|
||||
|
||||
methods.addStrategy = (v) => {
|
||||
methods.addStrategy = v => {
|
||||
v.id = Math.round(Math.random() * 10000000);
|
||||
methods.pushToList('strategies', v);
|
||||
};
|
||||
@ -52,23 +52,20 @@ const prepare = (methods, dispatch) => {
|
||||
methods.moveItem('strategies', index, toIndex);
|
||||
};
|
||||
|
||||
methods.removeStrategy = (index) => {
|
||||
methods.removeStrategy = index => {
|
||||
methods.removeFromList('strategies', index);
|
||||
};
|
||||
|
||||
methods.validateName = (featureToggleName) => {
|
||||
methods.validateName = featureToggleName => {
|
||||
validateName(featureToggleName)
|
||||
.then(() => methods.setValue('nameError', undefined))
|
||||
.catch((err) => methods.setValue('nameError', err.message));
|
||||
.catch(err => methods.setValue('nameError', err.message));
|
||||
};
|
||||
|
||||
return methods;
|
||||
};
|
||||
const actions = createActions({ id: ID, prepare });
|
||||
|
||||
const FormAddContainer = connect(
|
||||
mapStateToProps,
|
||||
actions
|
||||
)(FormAddComponent);
|
||||
const FormAddContainer = connect(mapStateToProps, actions)(FormAddComponent);
|
||||
|
||||
export default FormAddContainer;
|
||||
|
@ -6,7 +6,7 @@ import { createMapper, createActions } from '../input-helpers';
|
||||
import FormComponent from './form';
|
||||
|
||||
const ID = 'edit-feature-toggle';
|
||||
function getId (props) {
|
||||
function getId(props) {
|
||||
return [ID, props.featureToggle.name];
|
||||
}
|
||||
// TODO: need to scope to the active featureToggle
|
||||
@ -19,41 +19,39 @@ const mapStateToProps = createMapper({
|
||||
});
|
||||
return ownProps.featureToggle;
|
||||
},
|
||||
prepare: (props) => {
|
||||
prepare: props => {
|
||||
props.editmode = true;
|
||||
return props;
|
||||
},
|
||||
});
|
||||
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
methods.onSubmit = input => e => {
|
||||
e.preventDefault();
|
||||
|
||||
if (Array.isArray(input.strategies)) {
|
||||
input.strategies.forEach((s) => {
|
||||
delete s.id;
|
||||
});
|
||||
}
|
||||
// TODO: should add error handling
|
||||
requestUpdateFeatureToggle(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/features/view/${input.name}`));
|
||||
if (Array.isArray(input.strategies)) {
|
||||
input.strategies.forEach(s => {
|
||||
delete s.id;
|
||||
});
|
||||
}
|
||||
);
|
||||
// TODO: should add error handling
|
||||
requestUpdateFeatureToggle(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/features/view/${input.name}`));
|
||||
};
|
||||
|
||||
methods.onCancel = (evt) => {
|
||||
methods.onCancel = evt => {
|
||||
evt.preventDefault();
|
||||
methods.clear();
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
methods.addStrategy = (v) => {
|
||||
methods.addStrategy = v => {
|
||||
v.id = Math.round(Math.random() * 10000000);
|
||||
methods.pushToList('strategies', v);
|
||||
};
|
||||
|
||||
methods.removeStrategy = (index) => {
|
||||
methods.removeStrategy = index => {
|
||||
methods.removeFromList('strategies', index);
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import StrategiesSection from './strategies-section-container';
|
||||
|
||||
import { FormButtons } from '../../common';
|
||||
|
||||
const trim = (value) => {
|
||||
const trim = value => {
|
||||
if (value && value.trim) {
|
||||
return value.trim();
|
||||
} else {
|
||||
@ -13,14 +13,14 @@ const trim = (value) => {
|
||||
};
|
||||
|
||||
class AddFeatureToggleComponent extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// TODO unwind this stuff
|
||||
if (this.props.initCallRequired === true) {
|
||||
this.props.init(this.props.input);
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
input,
|
||||
setValue,
|
||||
@ -53,8 +53,9 @@ class AddFeatureToggleComponent extends Component {
|
||||
required
|
||||
value={name}
|
||||
error={nameError}
|
||||
onBlur={(v) => validateName(v.target.value)}
|
||||
onChange={(v) => setValue('name', trim(v.target.value))} />
|
||||
onBlur={v => validateName(v.target.value)}
|
||||
onChange={v => setValue('name', trim(v.target.value))}
|
||||
/>
|
||||
<br />
|
||||
<Textfield
|
||||
floatingLabel
|
||||
@ -63,24 +64,31 @@ class AddFeatureToggleComponent extends Component {
|
||||
label="Description"
|
||||
required
|
||||
value={description}
|
||||
onChange={(v) => setValue('description', v.target.value)} />
|
||||
onChange={v => setValue('description', v.target.value)}
|
||||
/>
|
||||
|
||||
{!editmode && <div>
|
||||
<br />
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
setValue('enabled', !enabled);
|
||||
}}>Enabled</Switch>
|
||||
<hr />
|
||||
</div>}
|
||||
{!editmode && (
|
||||
<div>
|
||||
<br />
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
setValue('enabled', !enabled);
|
||||
}}
|
||||
>
|
||||
Enabled
|
||||
</Switch>
|
||||
<hr />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<StrategiesSection
|
||||
configuredStrategies={configuredStrategies}
|
||||
addStrategy={addStrategy}
|
||||
updateStrategy={updateStrategy}
|
||||
moveStrategy={moveStrategy}
|
||||
removeStrategy={removeStrategy} />
|
||||
removeStrategy={removeStrategy}
|
||||
/>
|
||||
|
||||
<br />
|
||||
<FormButtons
|
||||
@ -91,7 +99,7 @@ class AddFeatureToggleComponent extends Component {
|
||||
</form>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
AddFeatureToggleComponent.propTypes = {
|
||||
input: PropTypes.object,
|
||||
|
@ -6,13 +6,17 @@ class AddStrategy extends React.Component {
|
||||
strategies: PropTypes.array.isRequired,
|
||||
addStrategy: PropTypes.func.isRequired,
|
||||
fetchStrategies: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
addStrategy = (strategyName) => {
|
||||
const selectedStrategy = this.props.strategies.find(s => s.name === strategyName);
|
||||
addStrategy = strategyName => {
|
||||
const selectedStrategy = this.props.strategies.find(
|
||||
s => s.name === strategyName
|
||||
);
|
||||
const parameters = {};
|
||||
|
||||
selectedStrategy.parameters.forEach(({ name }) => { parameters[name] = ''; });
|
||||
selectedStrategy.parameters.forEach(({ name }) => {
|
||||
parameters[name] = '';
|
||||
});
|
||||
|
||||
this.props.addStrategy({
|
||||
name: selectedStrategy.name,
|
||||
@ -20,30 +24,55 @@ class AddStrategy extends React.Component {
|
||||
});
|
||||
};
|
||||
|
||||
stopPropagation (e) {
|
||||
stopPropagation(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
render () {
|
||||
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" raised accent title="Add Strategy" onClick={this.stopPropagation}/>
|
||||
<Menu target="strategies-add" valign="bottom" align="right" ripple style={menuStyle}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '25px',
|
||||
height: '25px',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
>
|
||||
<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} title={s.description} 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default AddStrategy;
|
||||
|
@ -11,9 +11,9 @@ class StrategiesList extends React.Component {
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
moveStrategy: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
strategies,
|
||||
configuredStrategies,
|
||||
@ -34,12 +34,13 @@ class StrategiesList extends React.Component {
|
||||
moveStrategy={moveStrategy}
|
||||
removeStrategy={removeStrategy.bind(null, i)}
|
||||
updateStrategy={updateStrategy.bind(null, i)}
|
||||
strategyDefinition={strategies.find(s => s.name === strategy.name)} />
|
||||
strategyDefinition={strategies.find(
|
||||
s => s.name === strategy.name
|
||||
)}
|
||||
/>
|
||||
));
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||
{blocks}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>{blocks}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,9 @@ import { connect } from 'react-redux';
|
||||
import StrategiesSection from './strategies-section';
|
||||
import { fetchStrategies } from '../../../store/strategy/actions';
|
||||
|
||||
|
||||
export default connect((state) => ({
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
}), { fetchStrategies })(StrategiesSection);
|
||||
export default connect(
|
||||
state => ({
|
||||
strategies: state.strategies.get('list').toArray(),
|
||||
}),
|
||||
{ fetchStrategies }
|
||||
)(StrategiesSection);
|
||||
|
@ -11,20 +11,23 @@ class StrategiesSection extends React.Component {
|
||||
removeStrategy: PropTypes.func.isRequired,
|
||||
updateStrategy: PropTypes.func.isRequired,
|
||||
fetchStrategies: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
this.props.fetchStrategies();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (!this.props.strategies || this.props.strategies.length === 0) {
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderTitle title="Activation strategies" actions={<AddStrategy {...this.props} />} />
|
||||
<HeaderTitle
|
||||
title="Activation strategies"
|
||||
actions={<AddStrategy {...this.props} />}
|
||||
/>
|
||||
<StrategiesList {...this.props} />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,8 +1,14 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import {
|
||||
Textfield, Button,
|
||||
Card, CardTitle, CardText, CardActions, CardMenu,
|
||||
IconButton, Icon,
|
||||
Textfield,
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardText,
|
||||
CardActions,
|
||||
CardMenu,
|
||||
IconButton,
|
||||
Icon,
|
||||
} from 'react-mdl';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import { Link } from 'react-router';
|
||||
@ -11,13 +17,13 @@ import StrategyInputList from './strategy-input-list';
|
||||
import styles from './strategy.scss';
|
||||
|
||||
const dragSource = {
|
||||
beginDrag (props) {
|
||||
beginDrag(props) {
|
||||
return {
|
||||
id: props.id,
|
||||
index: props.index,
|
||||
};
|
||||
},
|
||||
endDrag (props, monitor) {
|
||||
endDrag(props, monitor) {
|
||||
if (!monitor.didDrop()) {
|
||||
return;
|
||||
}
|
||||
@ -29,22 +35,24 @@ const dragSource = {
|
||||
};
|
||||
|
||||
const dragTarget = {
|
||||
drop (props) {
|
||||
drop(props) {
|
||||
return {
|
||||
index: props.index,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@DropTarget('strategy', dragTarget, connect => ({ // eslint-disable-line new-cap
|
||||
/* eslint-disable new-cap */
|
||||
@DropTarget('strategy', dragTarget, connect => ({
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
}))
|
||||
@DragSource('strategy', dragSource, (connect, monitor) => ({ // eslint-disable-line new-cap
|
||||
@DragSource('strategy', dragSource, (connect, monitor) => ({
|
||||
connectDragSource: connect.dragSource(),
|
||||
connectDragPreview: connect.dragPreview(),
|
||||
isDragging: monitor.isDragging(),
|
||||
}))
|
||||
class StrategyConfigure extends React.Component {
|
||||
/* eslint-enable */
|
||||
static propTypes = {
|
||||
strategy: PropTypes.object.isRequired,
|
||||
strategyDefinition: PropTypes.object.isRequired,
|
||||
@ -55,7 +63,7 @@ class StrategyConfigure extends React.Component {
|
||||
connectDragPreview: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
handleConfigChange = (key, e) => {
|
||||
this.setConfig(key, e.target.value);
|
||||
@ -65,31 +73,42 @@ class StrategyConfigure extends React.Component {
|
||||
const parameters = this.props.strategy.parameters || {};
|
||||
parameters[key] = value;
|
||||
|
||||
const updatedStrategy = Object.assign({}, this.props.strategy, { parameters });
|
||||
const updatedStrategy = Object.assign({}, this.props.strategy, {
|
||||
parameters,
|
||||
});
|
||||
|
||||
this.props.updateStrategy(updatedStrategy);
|
||||
}
|
||||
};
|
||||
|
||||
handleRemove = (evt) => {
|
||||
handleRemove = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.removeStrategy();
|
||||
}
|
||||
};
|
||||
|
||||
renderInputFields ({ parameters }) {
|
||||
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 === '')) {
|
||||
if (
|
||||
value == null ||
|
||||
(typeof value === 'string' && value === '')
|
||||
) {
|
||||
this.setConfig(name, 50);
|
||||
}
|
||||
return (
|
||||
<div key={name}>
|
||||
<StrategyInputPercentage
|
||||
name={name}
|
||||
onChange={this.handleConfigChange.bind(this, name)}
|
||||
value={1 * value} />
|
||||
{description && <p className={styles.helpText}>{description}</p>}
|
||||
onChange={this.handleConfigChange.bind(
|
||||
this,
|
||||
name
|
||||
)}
|
||||
value={1 * value}
|
||||
/>
|
||||
{description && (
|
||||
<p className={styles.helpText}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'list') {
|
||||
@ -102,8 +121,14 @@ class StrategyConfigure extends React.Component {
|
||||
}
|
||||
return (
|
||||
<div key={name}>
|
||||
<StrategyInputList name={name} list={list} setConfig={this.setConfig} />
|
||||
{description && <p className={styles.helpText}>{description}</p>}
|
||||
<StrategyInputList
|
||||
name={name}
|
||||
list={list}
|
||||
setConfig={this.setConfig}
|
||||
/>
|
||||
{description && (
|
||||
<p className={styles.helpText}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'number') {
|
||||
@ -117,10 +142,15 @@ class StrategyConfigure extends React.Component {
|
||||
style={{ width: '100%' }}
|
||||
name={name}
|
||||
label={name}
|
||||
onChange={this.handleConfigChange.bind(this, name)}
|
||||
onChange={this.handleConfigChange.bind(
|
||||
this,
|
||||
name
|
||||
)}
|
||||
value={value}
|
||||
/>
|
||||
{description && <p className={styles.helpText}>{description}</p>}
|
||||
{description && (
|
||||
<p className={styles.helpText}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@ -133,10 +163,15 @@ class StrategyConfigure extends React.Component {
|
||||
required={required}
|
||||
name={name}
|
||||
label={name}
|
||||
onChange={this.handleConfigChange.bind(this, name)}
|
||||
onChange={this.handleConfigChange.bind(
|
||||
this,
|
||||
name
|
||||
)}
|
||||
value={value}
|
||||
/>
|
||||
{description && <p className={styles.helpText}>{description}</p>}
|
||||
{description && (
|
||||
<p className={styles.helpText}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -145,37 +180,56 @@ class StrategyConfigure extends React.Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isDragging, connectDragPreview, connectDragSource, connectDropTarget } = this.props;
|
||||
render() {
|
||||
const {
|
||||
isDragging,
|
||||
connectDragPreview,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
} = this.props;
|
||||
|
||||
let item;
|
||||
if (this.props.strategyDefinition) {
|
||||
const inputFields = this.renderInputFields(this.props.strategyDefinition);
|
||||
const inputFields = this.renderInputFields(
|
||||
this.props.strategyDefinition
|
||||
);
|
||||
const { name } = this.props.strategy;
|
||||
item = (
|
||||
<Card shadow={0} className={styles.card} style={{ opacity: isDragging ? '0.1' : '1' }}>
|
||||
<Card
|
||||
shadow={0}
|
||||
className={styles.card}
|
||||
style={{ opacity: isDragging ? '0.1' : '1' }}
|
||||
>
|
||||
<CardTitle className={styles.cardTitle}>
|
||||
<Icon name="extension" /> {name}
|
||||
</CardTitle>
|
||||
<CardText>
|
||||
{this.props.strategyDefinition.description}
|
||||
</CardText>
|
||||
{
|
||||
inputFields && <CardActions border style={{ padding: '20px' }}>
|
||||
{inputFields && (
|
||||
<CardActions border style={{ padding: '20px' }}>
|
||||
{inputFields}
|
||||
</CardActions>
|
||||
}
|
||||
)}
|
||||
|
||||
<CardMenu className="mdl-color-text--white">
|
||||
<Link
|
||||
title="View strategy"
|
||||
to={`/strategies/view/${name}`}
|
||||
className={styles.editLink}>
|
||||
className={styles.editLink}
|
||||
>
|
||||
<Icon name="link" />
|
||||
</Link>
|
||||
<IconButton title="Remove strategy from toggle" name="delete" onClick={this.handleRemove} />
|
||||
<IconButton
|
||||
title="Remove strategy from toggle"
|
||||
name="delete"
|
||||
onClick={this.handleRemove}
|
||||
/>
|
||||
{connectDragSource(
|
||||
<span className={styles.reorderIcon}><Icon name="reorder" /></span>)}
|
||||
<span className={styles.reorderIcon}>
|
||||
<Icon name="reorder" />
|
||||
</span>
|
||||
)}
|
||||
</CardMenu>
|
||||
</Card>
|
||||
);
|
||||
@ -186,19 +240,27 @@ class StrategyConfigure extends React.Component {
|
||||
<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>
|
||||
<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>
|
||||
<Button
|
||||
onClick={this.handleRemove}
|
||||
label="remove strategy"
|
||||
accent
|
||||
raised
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</CardActions>
|
||||
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (connectDropTarget(connectDragPreview(
|
||||
<div className={styles.item}>{item}</div>
|
||||
)));
|
||||
return connectDropTarget(
|
||||
connectDragPreview(<div className={styles.item}>{item}</div>)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,37 +1,33 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import {
|
||||
Textfield,
|
||||
IconButton,
|
||||
Chip,
|
||||
} from 'react-mdl';
|
||||
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) => {
|
||||
onBlur = e => {
|
||||
this.setValue(e);
|
||||
window.removeEventListener('keydown', this.onKeyHandler, false);
|
||||
}
|
||||
};
|
||||
|
||||
onFocus = (e) => {
|
||||
onFocus = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.addEventListener('keydown', this.onKeyHandler, false);
|
||||
}
|
||||
};
|
||||
|
||||
onKeyHandler = (e) => {
|
||||
onKeyHandler = e => {
|
||||
if (e.key === 'Enter') {
|
||||
this.setValue();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setValue = (e) => {
|
||||
setValue = e => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@ -44,36 +40,53 @@ export default class InputList extends Component {
|
||||
inputValue.value = '';
|
||||
setConfig(name, list.join(','));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClose (index) {
|
||||
onClose(index) {
|
||||
const { name, list, setConfig } = this.props;
|
||||
list[index] = null;
|
||||
setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','));
|
||||
setConfig(
|
||||
name,
|
||||
list.length === 1 ? '' : list.filter(Boolean).join(',')
|
||||
);
|
||||
}
|
||||
|
||||
render () {
|
||||
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>
|
||||
))}
|
||||
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 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>
|
||||
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,16 @@ const labelStyle = {
|
||||
|
||||
export default ({ name, value, onChange }) => (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<div style={labelStyle}>{name}: {value}%</div>
|
||||
<Slider min={0} max={100} defaultValue={value} value={value} onChange={onChange} label={name} />
|
||||
<div style={labelStyle}>
|
||||
{name}: {value}%
|
||||
</div>
|
||||
<Slider
|
||||
min={0}
|
||||
max={100}
|
||||
defaultValue={value}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
label={name}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,9 +1,22 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import Feature from './feature-list-item-component';
|
||||
import { Link } from 'react-router';
|
||||
import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
|
||||
import {
|
||||
Icon,
|
||||
FABButton,
|
||||
Textfield,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Card,
|
||||
CardActions,
|
||||
List,
|
||||
} from 'react-mdl';
|
||||
|
||||
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
|
||||
import {
|
||||
MenuItemWithIcon,
|
||||
DropdownButton,
|
||||
styles as commonStyles,
|
||||
} from '../common';
|
||||
import styles from './feature.scss';
|
||||
|
||||
export default class FeatureListComponent extends React.PureComponent {
|
||||
@ -14,13 +27,13 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
fetchFeatureMetrics: PropTypes.func.isRequired,
|
||||
updateSetting: PropTypes.func.isRequired,
|
||||
settings: React.PropTypes.object,
|
||||
}
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchFeatureToggles();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
@ -28,73 +41,147 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
toggleMetrics () {
|
||||
this.props.updateSetting('showLastHour', !this.props.settings.showLastHour);
|
||||
toggleMetrics() {
|
||||
this.props.updateSetting(
|
||||
'showLastHour',
|
||||
!this.props.settings.showLastHour
|
||||
);
|
||||
}
|
||||
|
||||
setFilter (v) {
|
||||
setFilter(v) {
|
||||
this.props.updateSetting('filter', typeof v === 'string' ? v : '');
|
||||
}
|
||||
|
||||
setSort (v) {
|
||||
setSort(v) {
|
||||
this.props.updateSetting('sort', typeof v === 'string' ? v.trim() : '');
|
||||
}
|
||||
|
||||
render () {
|
||||
const { features, toggleFeature, featureMetrics, settings } = this.props;
|
||||
render() {
|
||||
const {
|
||||
features,
|
||||
toggleFeature,
|
||||
featureMetrics,
|
||||
settings,
|
||||
} = this.props;
|
||||
|
||||
return (<div>
|
||||
<div className={styles.toolbar}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
value={settings.filter}
|
||||
onChange={(e) => { this.setFilter(e.target.value); }}
|
||||
label="Search"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Link to="/features/create" className={styles.toolbarButton}>
|
||||
<FABButton accent title="Create feature toggle">
|
||||
<Icon name="add"/>
|
||||
</FABButton>
|
||||
</Link>
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.toolbar}>
|
||||
<Textfield
|
||||
floatingLabel
|
||||
value={settings.filter}
|
||||
onChange={e => {
|
||||
this.setFilter(e.target.value);
|
||||
}}
|
||||
label="Search"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Link
|
||||
to="/features/create"
|
||||
className={styles.toolbarButton}
|
||||
>
|
||||
<FABButton accent title="Create feature toggle">
|
||||
<Icon name="add" />
|
||||
</FABButton>
|
||||
</Link>
|
||||
</div>
|
||||
<Card
|
||||
shadow={0}
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
<CardActions>
|
||||
<DropdownButton
|
||||
id="metric"
|
||||
label={`Last ${settings.showLastHour
|
||||
? 'hour'
|
||||
: 'minute'}`}
|
||||
/>
|
||||
<Menu
|
||||
target="metric"
|
||||
onClick={() => this.toggleMetrics()}
|
||||
style={{ width: '168px' }}
|
||||
>
|
||||
<MenuItemWithIcon
|
||||
icon="hourglass_empty"
|
||||
disabled={!settings.showLastHour}
|
||||
data-target="minute"
|
||||
label="Last minute"
|
||||
/>
|
||||
<MenuItemWithIcon
|
||||
icon="hourglass_full"
|
||||
disabled={settings.showLastHour}
|
||||
data-target="hour"
|
||||
label="Last hour"
|
||||
/>
|
||||
</Menu>
|
||||
<DropdownButton
|
||||
id="sorting"
|
||||
label={`By ${settings.sort}`}
|
||||
/>
|
||||
<Menu
|
||||
target="sorting"
|
||||
onClick={e =>
|
||||
this.setSort(
|
||||
e.target.getAttribute('data-target')
|
||||
)}
|
||||
style={{ width: '168px' }}
|
||||
>
|
||||
<MenuItem
|
||||
disabled={settings.sort === 'name'}
|
||||
data-target="name"
|
||||
>
|
||||
Name
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={settings.sort === 'enabled'}
|
||||
data-target="enabled"
|
||||
>
|
||||
Enabled
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={settings.sort === 'created'}
|
||||
data-target="created"
|
||||
>
|
||||
Created
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={settings.sort === 'strategies'}
|
||||
data-target="strategies"
|
||||
>
|
||||
Strategies
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={settings.sort === 'metrics'}
|
||||
data-target="metrics"
|
||||
>
|
||||
Metrics
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</CardActions>
|
||||
<hr />
|
||||
<List>
|
||||
{features.map((feature, i) => (
|
||||
<Feature
|
||||
key={i}
|
||||
settings={settings}
|
||||
metricsLastHour={
|
||||
featureMetrics.lastHour[feature.name]
|
||||
}
|
||||
metricsLastMinute={
|
||||
featureMetrics.lastMinute[feature.name]
|
||||
}
|
||||
feature={feature}
|
||||
toggleFeature={toggleFeature}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</div>
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardActions>
|
||||
<DropdownButton id="metric" label={`Last ${settings.showLastHour ? 'hour' : 'minute'}`}/>
|
||||
<Menu target="metric" onClick={() => this.toggleMetrics()}
|
||||
style={{ width: '168px' }}>
|
||||
<MenuItemWithIcon icon="hourglass_empty" disabled={!settings.showLastHour} data-target="minute"
|
||||
label="Last minute"/>
|
||||
<MenuItemWithIcon icon="hourglass_full" disabled={settings.showLastHour} data-target="hour"
|
||||
label="Last hour"/>
|
||||
</Menu>
|
||||
<DropdownButton id="sorting" label={`By ${settings.sort}`}/>
|
||||
<Menu target="sorting" onClick={(e) => this.setSort(e.target.getAttribute('data-target'))}
|
||||
style={{ width: '168px' }}>
|
||||
<MenuItem disabled={settings.sort === 'name'} data-target="name">Name</MenuItem>
|
||||
<MenuItem disabled={settings.sort === 'enabled'} data-target="enabled">Enabled</MenuItem>
|
||||
<MenuItem disabled={settings.sort === 'created'} data-target="created">Created</MenuItem>
|
||||
<MenuItem disabled={settings.sort === 'strategies'} data-target="strategies">Strategies</MenuItem>
|
||||
<MenuItem disabled={settings.sort === 'metrics'} data-target="metrics">Metrics</MenuItem>
|
||||
</Menu>
|
||||
</CardActions>
|
||||
<hr/>
|
||||
<List>
|
||||
{features.map((feature, i) =>
|
||||
(<Feature key={i}
|
||||
settings={settings}
|
||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||
feature={feature}
|
||||
toggleFeature={toggleFeature}/>)
|
||||
)}
|
||||
</List>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { toggleFeature, fetchFeatureToggles } from '../../store/feature-actions';
|
||||
import {
|
||||
toggleFeature,
|
||||
fetchFeatureToggles,
|
||||
} from '../../store/feature-actions';
|
||||
import { fetchFeatureMetrics } from '../../store/feature-metrics-actions';
|
||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
||||
|
||||
|
||||
import FeatureListComponent from './list-component';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = state => {
|
||||
const featureMetrics = state.featureMetrics.toJS();
|
||||
const settings = state.settings.toJS().feature || {};
|
||||
let features = state.features.toJS();
|
||||
if (settings.filter) {
|
||||
const regex = new RegExp(settings.filter, 'i');
|
||||
features = features.filter(feature =>
|
||||
(
|
||||
regex.test(feature.name) ||
|
||||
features = features.filter(
|
||||
feature =>
|
||||
regex.test(feature.name) ||
|
||||
regex.test(feature.description) ||
|
||||
feature.strategies.some(s => s && s.name && regex.test(s.name))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -26,30 +27,41 @@ const mapStateToProps = (state) => {
|
||||
}
|
||||
|
||||
if (settings.sort === 'enabled') {
|
||||
features = features.sort((a, b) => (
|
||||
// eslint-disable-next-line
|
||||
features = features.sort(
|
||||
(a, b) =>
|
||||
// eslint-disable-next-line
|
||||
a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1
|
||||
));
|
||||
);
|
||||
} else if (settings.sort === 'created') {
|
||||
features = features.sort((a, b) => (
|
||||
new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1
|
||||
));
|
||||
features = features.sort(
|
||||
(a, b) => (new Date(a.createdAt) > new Date(b.createdAt) ? -1 : 1)
|
||||
);
|
||||
} else if (settings.sort === 'name') {
|
||||
features = features.sort((a, b) => {
|
||||
if (a.name < b.name) { return -1; }
|
||||
if (a.name > b.name) { return 1; }
|
||||
if (a.name < b.name) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
} else if (settings.sort === 'strategies') {
|
||||
features = features.sort((a, b) => (
|
||||
a.strategies.length > b.strategies.length ? -1 : 1
|
||||
));
|
||||
features = features.sort(
|
||||
(a, b) => (a.strategies.length > b.strategies.length ? -1 : 1)
|
||||
);
|
||||
} else if (settings.sort === 'metrics') {
|
||||
const target = settings.showLastHour ? featureMetrics.lastHour : featureMetrics.lastMinute;
|
||||
const target = settings.showLastHour
|
||||
? featureMetrics.lastHour
|
||||
: featureMetrics.lastMinute;
|
||||
|
||||
features = features.sort((a, b) => {
|
||||
if (!target[a.name]) { return 1; }
|
||||
if (!target[b.name]) { return -1; }
|
||||
if (!target[a.name]) {
|
||||
return 1;
|
||||
}
|
||||
if (!target[b.name]) {
|
||||
return -1;
|
||||
}
|
||||
if (target[a.name].yes > target[b.name].yes) {
|
||||
return -1;
|
||||
}
|
||||
@ -71,9 +83,8 @@ const mapDispatchToProps = {
|
||||
updateSetting: updateSettingForGroup('feature'),
|
||||
};
|
||||
|
||||
const FeatureListContainer = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FeatureListComponent);
|
||||
const FeatureListContainer = connect(mapStateToProps, mapDispatchToProps)(
|
||||
FeatureListComponent
|
||||
);
|
||||
|
||||
export default FeatureListContainer;
|
||||
|
@ -11,15 +11,23 @@ const StrategyChipItem = ({ strategy }) => (
|
||||
<ChipContact className="mdl-color--blue-grey mdl-color-text--white">
|
||||
<Icon style={{ marginTop: '3px' }} name="link" />
|
||||
</ChipContact>
|
||||
<Link to={`/strategies/view/${strategy.name}`} className="mdl-color-text--blue-grey">{strategy.name}</Link>
|
||||
<Link
|
||||
to={`/strategies/view/${strategy.name}`}
|
||||
className="mdl-color-text--blue-grey"
|
||||
>
|
||||
{strategy.name}
|
||||
</Link>
|
||||
</Chip>
|
||||
);
|
||||
|
||||
// TODO what about "missing" strategies here?
|
||||
const StrategiesList = ({ strategies }) => (
|
||||
<div style={{ verticalAlign: 'middle', paddingTop: '14px' }}>With {strategies.length > 1 ? 'strategies' : 'strategy'} {
|
||||
strategies.map((strategy, i) => <StrategyChipItem key={i} strategy={strategy} />)
|
||||
}</div>
|
||||
<div style={{ verticalAlign: 'middle', paddingTop: '14px' }}>
|
||||
With {strategies.length > 1 ? 'strategies' : 'strategy'}{' '}
|
||||
{strategies.map((strategy, i) => (
|
||||
<StrategyChipItem key={i} strategy={strategy} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default class MetricComponent extends React.Component {
|
||||
@ -28,9 +36,9 @@ export default class MetricComponent extends React.Component {
|
||||
featureToggle: PropTypes.object.isRequired,
|
||||
fetchSeenApps: PropTypes.func.isRequired,
|
||||
fetchFeatureMetrics: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
this.props.fetchSeenApps();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
@ -38,11 +46,11 @@ export default class MetricComponent extends React.Component {
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { metrics = {}, featureToggle } = this.props;
|
||||
const {
|
||||
lastHour = { yes: 0, no: 0, isFallback: true },
|
||||
@ -50,41 +58,81 @@ export default class MetricComponent extends React.Component {
|
||||
seenApps = [],
|
||||
} = metrics;
|
||||
|
||||
const lastHourPercent = 1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent = 1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
const lastHourPercent =
|
||||
1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent =
|
||||
1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
|
||||
return (<div style={{ padding: '16px' }}>
|
||||
<Grid style={{ textAlign: 'center' }}>
|
||||
<Cell col={4} tablet={4} phone={12}>
|
||||
<Progress percentage={lastMinutePercent} isFallback={lastMinute.isFallback}
|
||||
colorClassName="mdl-color-text--accent" animatePercentageText/>
|
||||
{lastMinute.isFallback ?
|
||||
<p className="mdl-color-text--grey-500">No metrics available</p> :
|
||||
<p><strong>Last minute</strong><br /> Yes {lastMinute.yes}, No: {lastMinute.no}</p>
|
||||
}
|
||||
</Cell>
|
||||
<Cell col={4} tablet={4} phone={12}>
|
||||
<Progress percentage={lastHourPercent} isFallback={lastHour.isFallback}/>
|
||||
{lastHour.isFallback ?
|
||||
<p className="mdl-color-text--grey-500">No metrics available</p> :
|
||||
<p><strong>Last hour</strong><br /> Yes {lastHour.yes}, No: {lastHour.no}</p>
|
||||
}
|
||||
</Cell>
|
||||
<Cell col={4} tablet={12}>
|
||||
{seenApps.length > 0 ?
|
||||
(<div><strong>Seen in applications:</strong></div>) :
|
||||
<div>
|
||||
<Icon className={styles.problemIcon} name="report problem" title="Not used in an app in the last hour" />
|
||||
<div><small><strong>Not used in an app in the last hour.</strong>
|
||||
This might be due to your client implementation is not reporting usage.</small></div>
|
||||
</div>
|
||||
}
|
||||
<AppsLinkList apps={seenApps} />
|
||||
<span>Created {formatFullDateTime(featureToggle.createdAt)}</span>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<hr/>
|
||||
<StrategiesList strategies={featureToggle.strategies}/>
|
||||
</div>);
|
||||
return (
|
||||
<div style={{ padding: '16px' }}>
|
||||
<Grid style={{ textAlign: 'center' }}>
|
||||
<Cell col={4} tablet={4} phone={12}>
|
||||
<Progress
|
||||
percentage={lastMinutePercent}
|
||||
isFallback={lastMinute.isFallback}
|
||||
colorClassName="mdl-color-text--accent"
|
||||
animatePercentageText
|
||||
/>
|
||||
{lastMinute.isFallback ? (
|
||||
<p className="mdl-color-text--grey-500">
|
||||
No metrics available
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<strong>Last minute</strong>
|
||||
<br /> Yes {lastMinute.yes}, No: {lastMinute.no}
|
||||
</p>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell col={4} tablet={4} phone={12}>
|
||||
<Progress
|
||||
percentage={lastHourPercent}
|
||||
isFallback={lastHour.isFallback}
|
||||
/>
|
||||
{lastHour.isFallback ? (
|
||||
<p className="mdl-color-text--grey-500">
|
||||
No metrics available
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<strong>Last hour</strong>
|
||||
<br /> Yes {lastHour.yes}, No: {lastHour.no}
|
||||
</p>
|
||||
)}
|
||||
</Cell>
|
||||
<Cell col={4} tablet={12}>
|
||||
{seenApps.length > 0 ? (
|
||||
<div>
|
||||
<strong>Seen in applications:</strong>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.problemIcon}
|
||||
name="report problem"
|
||||
title="Not used in an app in the last hour"
|
||||
/>
|
||||
<div>
|
||||
<small>
|
||||
<strong>
|
||||
Not used in an app in the last hour.
|
||||
</strong>
|
||||
This might be due to your client
|
||||
implementation is not reporting usage.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<AppsLinkList apps={seenApps} />
|
||||
<span>
|
||||
Created{' '}
|
||||
{formatFullDateTime(featureToggle.createdAt)}
|
||||
</span>
|
||||
</Cell>
|
||||
</Grid>
|
||||
<hr />
|
||||
<StrategiesList strategies={featureToggle.strategies} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions';
|
||||
import {
|
||||
fetchFeatureMetrics,
|
||||
fetchSeenApps,
|
||||
} from '../../store/feature-metrics-actions';
|
||||
|
||||
import MatricComponent from './metric-component';
|
||||
|
||||
function getMetricsForToggle (state, toggleName) {
|
||||
function getMetricsForToggle(state, toggleName) {
|
||||
if (!toggleName) {
|
||||
return;
|
||||
}
|
||||
@ -16,14 +18,20 @@ function getMetricsForToggle (state, toggleName) {
|
||||
}
|
||||
if (state.featureMetrics.hasIn(['lastHour', toggleName])) {
|
||||
result.lastHour = state.featureMetrics.getIn(['lastHour', toggleName]);
|
||||
result.lastMinute = state.featureMetrics.getIn(['lastMinute', toggleName]);
|
||||
result.lastMinute = state.featureMetrics.getIn([
|
||||
'lastMinute',
|
||||
toggleName,
|
||||
]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default connect((state, props) => ({
|
||||
metrics: getMetricsForToggle(state, props.featureToggle.name),
|
||||
}), {
|
||||
fetchFeatureMetrics,
|
||||
fetchSeenApps,
|
||||
})(MatricComponent);
|
||||
export default connect(
|
||||
(state, props) => ({
|
||||
metrics: getMetricsForToggle(state, props.featureToggle.name),
|
||||
}),
|
||||
{
|
||||
fetchFeatureMetrics,
|
||||
fetchSeenApps,
|
||||
}
|
||||
)(MatricComponent);
|
||||
|
@ -2,7 +2,7 @@ import React, { PropTypes, Component } from 'react';
|
||||
import styles from './progress-styles.scss';
|
||||
|
||||
class Progress extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
@ -11,7 +11,7 @@ class Progress extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
if (this.props.initialAnimation) {
|
||||
this.initialTimeout = setTimeout(() => {
|
||||
this.rafTimerInit = window.requestAnimationFrame(() => {
|
||||
@ -23,7 +23,7 @@ class Progress extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps ({ percentage }) {
|
||||
componentWillReceiveProps({ percentage }) {
|
||||
if (this.state.percentage !== percentage) {
|
||||
const nextState = { percentage };
|
||||
if (this.props.animatePercentageText) {
|
||||
@ -35,12 +35,14 @@ class Progress extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getTarget (target) {
|
||||
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 cyclesCounter = Math.round(
|
||||
Math.abs(TOTAL_ANIMATION_TIME / perCycle)
|
||||
);
|
||||
const perCycleTime = Math.round(Math.abs(perCycle));
|
||||
|
||||
return {
|
||||
@ -52,7 +54,7 @@ class Progress extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
animateTo (percentage, targetState) {
|
||||
animateTo(percentage, targetState) {
|
||||
cancelAnimationFrame(this.rafCounterTimer);
|
||||
clearTimeout(this.nextTimer);
|
||||
|
||||
@ -73,17 +75,16 @@ class Progress extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
clearTimeout(this.initialTimeout);
|
||||
clearTimeout(this.nextTimer);
|
||||
window.cancelAnimationFrame(this.rafTimerInit);
|
||||
window.cancelAnimationFrame(this.rafCounterTimer);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { strokeWidth, colorClassName, isFallback } = this.props;
|
||||
const radius = (50 - strokeWidth / 2);
|
||||
const radius = 50 - strokeWidth / 2;
|
||||
const pathDescription = `
|
||||
M 50,50 m 0,-${radius}
|
||||
a ${radius},${radius} 0 1 1 0,${2 * radius}
|
||||
@ -93,17 +94,27 @@ class Progress extends Component {
|
||||
const diameter = Math.PI * 2 * radius;
|
||||
const progressStyle = {
|
||||
strokeDasharray: `${diameter}px ${diameter}px`,
|
||||
strokeDashoffset: `${((100 - this.state.percentage) / 100 * diameter)}px`,
|
||||
strokeDashoffset: `${(100 - this.state.percentage) /
|
||||
100 *
|
||||
diameter}px`,
|
||||
};
|
||||
|
||||
return (isFallback ?
|
||||
<svg viewBox="0 0 24 24" className="mdl-color-text--grey-300">{
|
||||
// eslint-disable-next-line max-len
|
||||
}<path fill="currentColor" d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z" />
|
||||
</svg> :
|
||||
return isFallback ? (
|
||||
<svg viewBox="0 0 24 24" className="mdl-color-text--grey-300">
|
||||
{
|
||||
// eslint-disable-next-line max-len
|
||||
}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.3,18C19,16.5 20,14.4 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12C4,14.4 5,16.5 6.7,18C8.2,16.7 10,16 12,16C14,16 15.9,16.7 17.3,18M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M7,9A1,1 0 0,1 8,10A1,1 0 0,1 7,11A1,1 0 0,1 6,10A1,1 0 0,1 7,9M10,6A1,1 0 0,1 11,7A1,1 0 0,1 10,8A1,1 0 0,1 9,7A1,1 0 0,1 10,6M17,9A1,1 0 0,1 18,10A1,1 0 0,1 17,11A1,1 0 0,1 16,10A1,1 0 0,1 17,9M14.4,6.1C14.9,6.3 15.1,6.9 15,7.4L13.6,10.8C13.8,11.1 14,11.5 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12C10,11 10.7,10.1 11.7,10L13.1,6.7C13.3,6.1 13.9,5.9 14.4,6.1Z"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 100 100">
|
||||
<path
|
||||
className={[styles.trail, 'mdl-color-text--grey-300'].join(' ')}
|
||||
className={[styles.trail, 'mdl-color-text--grey-300'].join(
|
||||
' '
|
||||
)}
|
||||
d={pathDescription}
|
||||
strokeWidth={strokeWidth}
|
||||
fillOpacity={0}
|
||||
@ -117,11 +128,9 @@ class Progress extends Component {
|
||||
style={progressStyle}
|
||||
/>
|
||||
|
||||
<text
|
||||
className={styles.text}
|
||||
x={50}
|
||||
y={50}
|
||||
>{this.state.percentageText}%</text>
|
||||
<text className={styles.text} x={50} y={50}>
|
||||
{this.state.percentageText}%
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,15 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Tabs, Tab, ProgressBar, Button, Card, CardTitle, CardText, CardActions, Switch } from 'react-mdl';
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
ProgressBar,
|
||||
Button,
|
||||
Card,
|
||||
CardTitle,
|
||||
CardText,
|
||||
CardActions,
|
||||
Switch,
|
||||
} from 'react-mdl';
|
||||
import { hashHistory, Link } from 'react-router';
|
||||
|
||||
import HistoryComponent from '../history/history-list-toggle-container';
|
||||
@ -14,7 +24,7 @@ const TABS = {
|
||||
};
|
||||
|
||||
export default class ViewFeatureToggleComponent extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
@ -26,19 +36,16 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
removeFeatureToggle: PropTypes.func.isRequired,
|
||||
fetchFeatureToggles: PropTypes.array.isRequired,
|
||||
featureToggle: PropTypes.object.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
if (this.props.features.length === 0) {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
}
|
||||
|
||||
getTabContent (activeTab) {
|
||||
const {
|
||||
featureToggle,
|
||||
featureToggleName,
|
||||
} = this.props;
|
||||
getTabContent(activeTab) {
|
||||
const { featureToggle, featureToggleName } = this.props;
|
||||
|
||||
if (TABS[activeTab] === TABS.history) {
|
||||
return <HistoryComponent toggleName={featureToggleName} />;
|
||||
@ -49,11 +56,11 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
goToTab (tabName, featureToggleName) {
|
||||
goToTab(tabName, featureToggleName) {
|
||||
hashHistory.push(`/features/${tabName}/${featureToggleName}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
featureToggle,
|
||||
features,
|
||||
@ -64,44 +71,95 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
} = this.props;
|
||||
|
||||
if (!featureToggle) {
|
||||
if (features.length === 0 ) {
|
||||
if (features.length === 0) {
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
Could not find the toggle <Link to={{ pathname: '/features/create', query: { name: featureToggleName } }}>
|
||||
{featureToggleName}</Link>
|
||||
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 activeTabId = TABS[this.props.activeTab]
|
||||
? TABS[this.props.activeTab]
|
||||
: TABS.view;
|
||||
const tabContent = this.getTabContent(activeTab);
|
||||
|
||||
const removeToggle = () => {
|
||||
if (window.confirm('Are you sure you want to remove this toggle?')) { // eslint-disable-line no-alert
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm('Are you sure you want to remove this toggle?')
|
||||
) {
|
||||
removeFeatureToggle(featureToggle.name);
|
||||
hashHistory.push('/features');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>{featureToggle.name}</CardTitle>
|
||||
<Card
|
||||
shadow={0}
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ overflow: 'visible' }}
|
||||
>
|
||||
<CardTitle
|
||||
style={{ paddingTop: '24px', wordBreak: 'break-all' }}
|
||||
>
|
||||
{featureToggle.name}
|
||||
</CardTitle>
|
||||
<CardText>{featureToggle.description}</CardText>
|
||||
<CardActions border style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<CardActions
|
||||
border
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<span style={{ paddingRight: '24px' }}>
|
||||
<Switch ripple checked={featureToggle.enabled} onChange={() => toggleFeature(featureToggle.name)}>
|
||||
<Switch
|
||||
ripple
|
||||
checked={featureToggle.enabled}
|
||||
onChange={() => toggleFeature(featureToggle.name)}
|
||||
>
|
||||
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
|
||||
</Switch>
|
||||
</span>
|
||||
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>Archive</Button>
|
||||
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>
|
||||
Archive
|
||||
</Button>
|
||||
</CardActions>
|
||||
<hr/>
|
||||
<Tabs activeTab={activeTabId} ripple tabBarProps={{ style: { width: '100%' } }} className="mdl-color--grey-100">
|
||||
<Tab onClick={() => this.goToTab('view', featureToggleName)}>Metrics</Tab>
|
||||
<Tab onClick={() => this.goToTab('edit', featureToggleName)}>Edit</Tab>
|
||||
<Tab onClick={() => this.goToTab('history', featureToggleName)}>History</Tab>
|
||||
<hr />
|
||||
<Tabs
|
||||
activeTab={activeTabId}
|
||||
ripple
|
||||
tabBarProps={{ style: { width: '100%' } }}
|
||||
className="mdl-color--grey-100"
|
||||
>
|
||||
<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}
|
||||
</Card>
|
||||
|
@ -1,16 +1,24 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFeatureToggles, toggleFeature, removeFeatureToggle } 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,
|
||||
}), {
|
||||
import {
|
||||
fetchFeatureToggles,
|
||||
toggleFeature,
|
||||
removeFeatureToggle,
|
||||
})(ViewToggleComponent);
|
||||
} 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,
|
||||
toggleFeature,
|
||||
removeFeatureToggle,
|
||||
}
|
||||
)(ViewToggleComponent);
|
||||
|
@ -4,15 +4,15 @@ import HistoryList from './history-list-container';
|
||||
import { styles as commonStyles } from '../common';
|
||||
|
||||
class History extends PureComponent {
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
|
||||
toggleShowDiff () {
|
||||
toggleShowDiff() {
|
||||
this.setState({ showData: !this.state.showData });
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { history } = this.props;
|
||||
if (history.length < 0) {
|
||||
return;
|
||||
|
@ -2,13 +2,15 @@ import { connect } from 'react-redux';
|
||||
import HistoryComponent from './history-component';
|
||||
import { fetchHistory } from '../../store/history-actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = state => {
|
||||
const history = state.history.get('list').toArray();
|
||||
return {
|
||||
history,
|
||||
};
|
||||
};
|
||||
|
||||
const HistoryListContainer = connect(mapStateToProps, { fetchHistory })(HistoryComponent);
|
||||
const HistoryListContainer = connect(mapStateToProps, { fetchHistory })(
|
||||
HistoryComponent
|
||||
);
|
||||
|
||||
export default HistoryListContainer;
|
||||
|
@ -16,18 +16,22 @@ const KLASSES = {
|
||||
N: style.positive, // added
|
||||
};
|
||||
|
||||
function buildItemDiff (diff, key) {
|
||||
function buildItemDiff(diff, key) {
|
||||
let change;
|
||||
if (diff.lhs !== undefined) {
|
||||
change = (
|
||||
<div>
|
||||
<div className={KLASSES.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={KLASSES.N}>+ {key}: {JSON.stringify(diff.rhs)}</div>
|
||||
<div className={KLASSES.N}>
|
||||
+ {key}: {JSON.stringify(diff.rhs)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -35,7 +39,7 @@ function buildItemDiff (diff, key) {
|
||||
return change;
|
||||
}
|
||||
|
||||
function buildDiff (diff, idx) {
|
||||
function buildDiff(diff, idx) {
|
||||
let change;
|
||||
const key = diff.path.join('.');
|
||||
|
||||
@ -44,26 +48,34 @@ function buildDiff (diff, idx) {
|
||||
} else if (diff.lhs !== undefined && diff.rhs !== undefined) {
|
||||
change = (
|
||||
<div>
|
||||
<div className={KLASSES.D}>- {key}: {JSON.stringify(diff.lhs)}</div>
|
||||
<div className={KLASSES.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 = KLASSES[diff.kind];
|
||||
const prefix = DIFF_PREFIXES[diff.kind];
|
||||
|
||||
change = (<div className={spadenClass}>{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}</div>);
|
||||
change = (
|
||||
<div className={spadenClass}>
|
||||
{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (<div key={idx}>{change}</div>);
|
||||
return <div key={idx}>{change}</div>;
|
||||
}
|
||||
|
||||
class HistoryItem extends PureComponent {
|
||||
static propTypes = {
|
||||
entry: PropTypes.object,
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const entry = this.props.entry;
|
||||
let changes;
|
||||
|
||||
@ -71,12 +83,20 @@ class HistoryItem extends PureComponent {
|
||||
changes = entry.diffs.map(buildDiff);
|
||||
} else {
|
||||
// Just show the data if there is no diff yet.
|
||||
changes = <div className={KLASSES.N}>{JSON.stringify(entry.data, null, 2)}</div>;
|
||||
changes = (
|
||||
<div className={KLASSES.N}>
|
||||
{JSON.stringify(entry.data, null, 2)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (<pre style={{ overflowX: 'auto', overflowY: 'hidden' }}>
|
||||
<code className="smalltext man">{changes.length === 0 ? '(no changes)' : changes}</code>
|
||||
</pre>);
|
||||
return (
|
||||
<pre style={{ overflowX: 'auto', overflowY: 'hidden' }}>
|
||||
<code className="smalltext man">
|
||||
{changes.length === 0 ? '(no changes)' : changes}
|
||||
</code>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@ import style from './history.scss';
|
||||
class HistoryItem extends PureComponent {
|
||||
static propTypes = {
|
||||
entry: PropTypes.object,
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const localEventData = JSON.parse(JSON.stringify(this.props.entry));
|
||||
delete localEventData.description;
|
||||
delete localEventData.name;
|
||||
|
@ -2,17 +2,21 @@ import React, { Component } from 'react';
|
||||
import HistoryItemDiff from './history-item-diff';
|
||||
import HistoryItemJson from './history-item-json';
|
||||
import { Table, TableHeader } from 'react-mdl';
|
||||
import { DataTableHeader, SwitchWithLabel, styles as commonStyles } from '../common';
|
||||
import {
|
||||
DataTableHeader,
|
||||
SwitchWithLabel,
|
||||
styles as commonStyles,
|
||||
} from '../common';
|
||||
import { formatFullDateTime } from '../common/util';
|
||||
|
||||
import styles from './history.scss';
|
||||
|
||||
class HistoryList extends Component {
|
||||
toggleShowDiff () {
|
||||
toggleShowDiff() {
|
||||
this.props.updateSetting('showData', !this.props.settings.showData);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const showData = this.props.settings.showData;
|
||||
const { history } = this.props;
|
||||
if (!history || history.length < 0) {
|
||||
@ -20,41 +24,84 @@ class HistoryList extends Component {
|
||||
}
|
||||
|
||||
const truncateTableCell = v => (
|
||||
<span className={commonStyles.truncate} style={{ display: 'inline-block', verticalAlign: 'middle', width: '100%' }}>{v}</span>
|
||||
<span
|
||||
className={commonStyles.truncate}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'middle',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{v}
|
||||
</span>
|
||||
);
|
||||
|
||||
let entries;
|
||||
|
||||
if (showData) {
|
||||
entries = history.map((entry) => <HistoryItemJson key={`log${entry.id}`} entry={entry} />);
|
||||
entries = history.map(entry => (
|
||||
<HistoryItemJson key={`log${entry.id}`} entry={entry} />
|
||||
));
|
||||
} else {
|
||||
entries = (<Table
|
||||
sortable
|
||||
rows={
|
||||
history.map((entry) => Object.assign({
|
||||
diff: (<HistoryItemDiff entry={entry} />),
|
||||
}, entry))
|
||||
}
|
||||
className={commonStyles.fullwidth}
|
||||
style={{ border: 0, tableLayout: 'fixed', minWidth: '840px' }}
|
||||
>
|
||||
<TableHeader name="type" cellFormatter={truncateTableCell} style={{ width: '136px' }}>Type</TableHeader>
|
||||
<TableHeader name="createdBy" cellFormatter={truncateTableCell} style={{ width: '115px' }}>User</TableHeader>
|
||||
<TableHeader name="diff">Diff</TableHeader>
|
||||
<TableHeader numeric name="createdAt" cellFormatter={formatFullDateTime} style={{ width: '165px' }}>Time</TableHeader>
|
||||
</Table>);
|
||||
entries = (
|
||||
<Table
|
||||
sortable
|
||||
rows={history.map(entry =>
|
||||
Object.assign(
|
||||
{
|
||||
diff: <HistoryItemDiff entry={entry} />,
|
||||
},
|
||||
entry
|
||||
)
|
||||
)}
|
||||
className={commonStyles.fullwidth}
|
||||
style={{
|
||||
border: 0,
|
||||
tableLayout: 'fixed',
|
||||
minWidth: '840px',
|
||||
}}
|
||||
>
|
||||
<TableHeader
|
||||
name="type"
|
||||
cellFormatter={truncateTableCell}
|
||||
style={{ width: '136px' }}
|
||||
>
|
||||
Type
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
name="createdBy"
|
||||
cellFormatter={truncateTableCell}
|
||||
style={{ width: '115px' }}
|
||||
>
|
||||
User
|
||||
</TableHeader>
|
||||
<TableHeader name="diff">Diff</TableHeader>
|
||||
<TableHeader
|
||||
numeric
|
||||
name="createdAt"
|
||||
cellFormatter={formatFullDateTime}
|
||||
style={{ width: '165px' }}
|
||||
>
|
||||
Time
|
||||
</TableHeader>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.history}>
|
||||
<DataTableHeader title={this.props.title} actions={
|
||||
<SwitchWithLabel checked={showData} onChange={this.toggleShowDiff.bind(this)}>
|
||||
Full events
|
||||
</SwitchWithLabel>
|
||||
}/>
|
||||
<div className={commonStyles.horisontalScroll}>
|
||||
{entries}
|
||||
</div>
|
||||
<DataTableHeader
|
||||
title={this.props.title}
|
||||
actions={
|
||||
<SwitchWithLabel
|
||||
checked={showData}
|
||||
onChange={this.toggleShowDiff.bind(this)}
|
||||
>
|
||||
Full events
|
||||
</SwitchWithLabel>
|
||||
}
|
||||
/>
|
||||
<div className={commonStyles.horisontalScroll}>{entries}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||
import HistoryListToggleComponent from './history-list-component';
|
||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = state => {
|
||||
const settings = state.settings.toJS().history || {};
|
||||
|
||||
return {
|
||||
|
@ -4,22 +4,18 @@ import HistoryList from './history-list-container';
|
||||
class HistoryListToggle extends Component {
|
||||
static propTypes = {
|
||||
toggleName: PropTypes.string.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchHistoryForToggle(this.props.toggleName);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (!this.props.history || this.props.history.length === 0) {
|
||||
return <span>fetching..</span>;
|
||||
}
|
||||
const { history } = this.props;
|
||||
return (
|
||||
<HistoryList
|
||||
history={history}
|
||||
title="Change log"/>
|
||||
);
|
||||
return <HistoryList history={history} title="Change log" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||
import HistoryListToggleComponent from './history-list-toggle-component';
|
||||
import { fetchHistoryForToggle } from '../../store/history-actions';
|
||||
|
||||
function getHistoryFromToggle (state, toggleName) {
|
||||
function getHistoryFromToggle(state, toggleName) {
|
||||
if (!toggleName) {
|
||||
return [];
|
||||
}
|
||||
|
@ -9,14 +9,14 @@ import {
|
||||
createMove,
|
||||
} from '../store/input-actions';
|
||||
|
||||
function getId (id, ownProps) {
|
||||
function getId(id, ownProps) {
|
||||
if (typeof id === 'function') {
|
||||
return id(ownProps); // should return array...
|
||||
}
|
||||
return [id];
|
||||
}
|
||||
|
||||
export function createMapper ({ id, getDefault, prepare = (v) => v }) {
|
||||
export function createMapper({ id, getDefault, prepare = v => v }) {
|
||||
return (state, ownProps) => {
|
||||
let input;
|
||||
let initCallRequired = false;
|
||||
@ -28,46 +28,75 @@ export function createMapper ({ id, getDefault, prepare = (v) => v }) {
|
||||
input = getDefault ? getDefault(state, ownProps) : {};
|
||||
}
|
||||
|
||||
return prepare({
|
||||
initCallRequired,
|
||||
input,
|
||||
}, state, ownProps);
|
||||
return prepare(
|
||||
{
|
||||
initCallRequired,
|
||||
input,
|
||||
},
|
||||
state,
|
||||
ownProps
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function createActions ({ id, prepare = (v) => v }) {
|
||||
return (dispatch, ownProps) => (prepare({
|
||||
export function createActions({ id, prepare = v => v }) {
|
||||
return (dispatch, ownProps) =>
|
||||
prepare(
|
||||
{
|
||||
clear() {
|
||||
dispatch(createClear({ id: getId(id, ownProps) }));
|
||||
},
|
||||
|
||||
clear () {
|
||||
dispatch(createClear({ id: getId(id, ownProps) }));
|
||||
},
|
||||
init(value) {
|
||||
dispatch(createInit({ id: getId(id, ownProps), value }));
|
||||
},
|
||||
|
||||
init (value) {
|
||||
dispatch(createInit({ id: getId(id, ownProps), value }));
|
||||
},
|
||||
setValue(key, value) {
|
||||
dispatch(
|
||||
createSet({ id: getId(id, ownProps), key, value })
|
||||
);
|
||||
},
|
||||
|
||||
setValue (key, value) {
|
||||
dispatch(createSet({ id: getId(id, ownProps), key, value }));
|
||||
},
|
||||
pushToList(key, value) {
|
||||
dispatch(
|
||||
createPush({ id: getId(id, ownProps), key, value })
|
||||
);
|
||||
},
|
||||
|
||||
pushToList (key, value) {
|
||||
dispatch(createPush({ id: getId(id, ownProps), key, value }));
|
||||
},
|
||||
removeFromList(key, index) {
|
||||
dispatch(
|
||||
createPop({ id: getId(id, ownProps), key, index })
|
||||
);
|
||||
},
|
||||
|
||||
removeFromList (key, index) {
|
||||
dispatch(createPop({ id: getId(id, ownProps), key, index }));
|
||||
},
|
||||
moveItem(key, index, toIndex) {
|
||||
dispatch(
|
||||
createMove({
|
||||
id: getId(id, ownProps),
|
||||
key,
|
||||
index,
|
||||
toIndex,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
moveItem (key, index, toIndex) {
|
||||
dispatch(createMove({ id: getId(id, ownProps), key, index, toIndex }));
|
||||
},
|
||||
updateInList(key, index, newValue, merge = false) {
|
||||
dispatch(
|
||||
createUp({
|
||||
id: getId(id, ownProps),
|
||||
key,
|
||||
index,
|
||||
newValue,
|
||||
merge,
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
updateInList (key, index, newValue, merge = false) {
|
||||
dispatch(createUp({ id: getId(id, ownProps), key, index, newValue, merge }));
|
||||
},
|
||||
|
||||
incValue (key) {
|
||||
dispatch(createInc({ id: getId(id, ownProps), key }));
|
||||
},
|
||||
}, dispatch, ownProps));
|
||||
incValue(key) {
|
||||
dispatch(createInc({ id: getId(id, ownProps), key }));
|
||||
},
|
||||
},
|
||||
dispatch,
|
||||
ownProps
|
||||
);
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ import AddStrategy from './add-strategy';
|
||||
const ID = 'add-strategy';
|
||||
|
||||
const prepare = (methods, dispatch) => {
|
||||
methods.onSubmit = (input) => (
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
// clean
|
||||
const parameters = (input.parameters || [])
|
||||
.filter((name) => !!name)
|
||||
.map(({
|
||||
methods.onSubmit = input => e => {
|
||||
e.preventDefault();
|
||||
// clean
|
||||
const parameters = (input.parameters || [])
|
||||
.filter(name => !!name)
|
||||
.map(
|
||||
({
|
||||
name,
|
||||
type = 'string',
|
||||
description = '',
|
||||
@ -24,27 +24,26 @@ const prepare = (methods, dispatch) => {
|
||||
type,
|
||||
description,
|
||||
required,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
createStrategy({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
parameters,
|
||||
})(dispatch)
|
||||
.then(() => methods.clear())
|
||||
// somewhat quickfix / hacky to go back..
|
||||
.then(() => window.history.back());
|
||||
}
|
||||
);
|
||||
createStrategy({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
parameters,
|
||||
})(dispatch)
|
||||
.then(() => methods.clear())
|
||||
// somewhat quickfix / hacky to go back..
|
||||
.then(() => window.history.back());
|
||||
};
|
||||
|
||||
methods.onCancel = (e) => {
|
||||
methods.onCancel = e => {
|
||||
e.preventDefault();
|
||||
methods.clear();
|
||||
// somewhat quickfix / hacky to go back..
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
|
||||
return methods;
|
||||
};
|
||||
|
||||
@ -53,13 +52,16 @@ const actions = createActions({
|
||||
prepare,
|
||||
});
|
||||
|
||||
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);
|
||||
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,10 +1,17 @@
|
||||
import React, { PropTypes, Component } from 'react';
|
||||
|
||||
import { Textfield, IconButton, Menu, MenuItem, Checkbox, Grid, Cell } from 'react-mdl';
|
||||
import {
|
||||
Textfield,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Checkbox,
|
||||
Grid,
|
||||
Cell,
|
||||
} from 'react-mdl';
|
||||
import { FormButtons } from '../common';
|
||||
|
||||
|
||||
const trim = (value) => {
|
||||
const trim = value => {
|
||||
if (value && value.trim) {
|
||||
return value.trim();
|
||||
} else {
|
||||
@ -12,36 +19,57 @@ const trim = (value) => {
|
||||
}
|
||||
};
|
||||
|
||||
function gerArrayWithEntries (num) {
|
||||
function gerArrayWithEntries(num) {
|
||||
return Array.from(Array(num));
|
||||
}
|
||||
|
||||
const Parameter = ({ set, input = {}, index }) => (
|
||||
<div style={{ background: '#f1f1f1', padding: '16px 20px', marginBottom: '20px' }}>
|
||||
<div
|
||||
style={{
|
||||
background: '#f1f1f1',
|
||||
padding: '16px 20px',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
<Textfield
|
||||
style={{ width: '50%' }}
|
||||
floatingLabel
|
||||
label={`Parameter name ${index + 1}`}
|
||||
onChange={({ target }) => set({ name: target.value }, true)}
|
||||
value={input.name} />
|
||||
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',
|
||||
}}>
|
||||
<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()} />
|
||||
<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: 'string' })}>
|
||||
string
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'percentage' })}>
|
||||
percentage
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'list' })}>list</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'number' })}>number</MenuItem>
|
||||
<MenuItem onClick={() => set({ type: 'number' })}>
|
||||
number
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
<Textfield
|
||||
@ -66,8 +94,8 @@ const EditHeader = () => (
|
||||
<div>
|
||||
<h4 style={{ marginTop: '16px' }}>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.
|
||||
Be carefull! Changing a strategy definition might also require
|
||||
changes to the implementation in the clients.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@ -78,17 +106,18 @@ const CreateHeader = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
const Parameters = ({ input = [], count = 0, updateInList }) => (
|
||||
<div>{
|
||||
gerArrayWithEntries(count)
|
||||
.map((v, i) => (<Parameter
|
||||
<div>
|
||||
{gerArrayWithEntries(count).map((v, i) => (
|
||||
<Parameter
|
||||
key={i}
|
||||
set={(v) => updateInList('parameters', i, v, true)}
|
||||
set={v => updateInList('parameters', i, v, true)}
|
||||
index={i}
|
||||
input={input[i]}
|
||||
/>))
|
||||
}</div>);
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
class AddStrategy extends Component {
|
||||
static propTypes = {
|
||||
@ -102,20 +131,22 @@ class AddStrategy extends Component {
|
||||
editmode: PropTypes.bool,
|
||||
initCallRequired: PropTypes.bool,
|
||||
init: PropTypes.func,
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
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);
|
||||
this.props.setValue(
|
||||
'_params',
|
||||
this.props.input.parameters.length
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {
|
||||
input,
|
||||
setValue,
|
||||
@ -131,13 +162,15 @@ class AddStrategy extends Component {
|
||||
<Cell col={12}>
|
||||
<form onSubmit={onSubmit(input)}>
|
||||
{editmode ? <EditHeader /> : <CreateHeader />}
|
||||
<Textfield label="Strategy name"
|
||||
<Textfield
|
||||
label="Strategy name"
|
||||
floatingLabel
|
||||
name="name"
|
||||
required
|
||||
disabled={editmode}
|
||||
pattern="^[0-9a-zA-Z\.\-]+$"
|
||||
onChange={({ target }) => setValue('name', trim(target.value))}
|
||||
onChange={({ target }) =>
|
||||
setValue('name', trim(target.value))}
|
||||
value={input.name}
|
||||
/>
|
||||
<br />
|
||||
@ -147,21 +180,27 @@ class AddStrategy extends Component {
|
||||
rows={1}
|
||||
label="Description"
|
||||
name="description"
|
||||
onChange={({ target }) => setValue('description', target.value)}
|
||||
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
|
||||
|
||||
|
||||
<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}
|
||||
|
@ -7,7 +7,7 @@ import AddStrategy from './add-strategy';
|
||||
|
||||
const ID = 'edit-strategy';
|
||||
|
||||
function getId (props) {
|
||||
function getId(props) {
|
||||
return [ID, props.strategy.name];
|
||||
}
|
||||
|
||||
@ -16,20 +16,20 @@ function getId (props) {
|
||||
const mapStateToProps = createMapper({
|
||||
id: getId,
|
||||
getDefault: (state, ownProps) => ownProps.strategy,
|
||||
prepare: (props) => {
|
||||
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(({
|
||||
methods.onSubmit = input => e => {
|
||||
e.preventDefault();
|
||||
// clean
|
||||
const parameters = (input.parameters || [])
|
||||
.filter(name => !!name)
|
||||
.map(
|
||||
({
|
||||
name,
|
||||
type = 'string',
|
||||
description = '',
|
||||
@ -39,26 +39,25 @@ const prepare = (methods, dispatch) => {
|
||||
type,
|
||||
description,
|
||||
required,
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
updateStrategy({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
parameters,
|
||||
})(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/strategies/view/${input.name}`));
|
||||
}
|
||||
);
|
||||
updateStrategy({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
parameters,
|
||||
})(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => hashHistory.push(`/strategies/view/${input.name}`));
|
||||
};
|
||||
|
||||
methods.onCancel = (e) => {
|
||||
methods.onCancel = e => {
|
||||
e.preventDefault();
|
||||
methods.clear();
|
||||
// somewhat quickfix / hacky to go back..
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
|
||||
return methods;
|
||||
};
|
||||
|
||||
|
@ -1,45 +1,73 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { List, ListItem, ListItemContent, IconButton, Grid, Cell } from 'react-mdl';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemContent,
|
||||
IconButton,
|
||||
Grid,
|
||||
Cell,
|
||||
} from 'react-mdl';
|
||||
import { HeaderTitle } from '../common';
|
||||
|
||||
class StrategiesListComponent extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object,
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.props.fetchStrategies();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { strategies, removeStrategy } = this.props;
|
||||
|
||||
return (
|
||||
<Grid className="mdl-color--white">
|
||||
<Cell col={12}>
|
||||
<HeaderTitle title="Strategies"
|
||||
<HeaderTitle
|
||||
title="Strategies"
|
||||
actions={
|
||||
<IconButton raised
|
||||
<IconButton
|
||||
raised
|
||||
name="add"
|
||||
onClick={() => this.context.router.push('/strategies/create')}
|
||||
title="Add new strategy" />} />
|
||||
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>
|
||||
{
|
||||
strategy.editable === false ?
|
||||
'' :
|
||||
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
|
||||
}
|
||||
</ListItem>
|
||||
)) : <ListItem>No entries</ListItem>}
|
||||
{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>
|
||||
{strategy.editable === false ? (
|
||||
''
|
||||
) : (
|
||||
<IconButton
|
||||
name="delete"
|
||||
onClick={() =>
|
||||
removeStrategy(strategy)}
|
||||
/>
|
||||
)}
|
||||
</ListItem>
|
||||
))
|
||||
) : (
|
||||
<ListItem>No entries</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</Cell>
|
||||
</Grid>
|
||||
@ -47,5 +75,4 @@ class StrategiesListComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StrategiesListComponent;
|
||||
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||
import StrategiesListComponent from './list-component.jsx';
|
||||
import { fetchStrategies, removeStrategy } from '../../store/strategy/actions';
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const mapStateToProps = state => {
|
||||
const list = state.strategies.get('list').toArray();
|
||||
|
||||
return {
|
||||
@ -10,15 +10,18 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
removeStrategy: (strategy) => {
|
||||
if (window.confirm('Are you sure you want to remove this strategy?')) { // eslint-disable-line no-alert
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
removeStrategy: strategy => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (window.confirm('Are you sure you want to remove this strategy?')) {
|
||||
removeStrategy(strategy)(dispatch);
|
||||
}
|
||||
},
|
||||
fetchStrategies: () => fetchStrategies()(dispatch),
|
||||
});
|
||||
|
||||
const StrategiesListContainer = connect(mapStateToProps, mapDispatchToProps)(StrategiesListComponent);
|
||||
const StrategiesListContainer = connect(mapStateToProps, mapDispatchToProps)(
|
||||
StrategiesListComponent
|
||||
);
|
||||
|
||||
export default StrategiesListContainer;
|
||||
|
@ -7,13 +7,20 @@ class ShowStrategyComponent extends PureComponent {
|
||||
toggles: PropTypes.array,
|
||||
applications: PropTypes.array,
|
||||
strategy: PropTypes.object.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
renderParameters (params) {
|
||||
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}>
|
||||
<ListItem
|
||||
twoLine
|
||||
key={`${name}-${i}`}
|
||||
title={required ? 'Required' : ''}
|
||||
>
|
||||
<ListItemContent
|
||||
avatar={required ? 'add' : ' '}
|
||||
subtitle={description}
|
||||
>
|
||||
{name} <small>({type})</small>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
@ -23,27 +30,18 @@ class ShowStrategyComponent extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
strategy,
|
||||
applications,
|
||||
toggles,
|
||||
} = this.props;
|
||||
render() {
|
||||
const { strategy, applications, toggles } = this.props;
|
||||
|
||||
const {
|
||||
parameters = [],
|
||||
} = strategy;
|
||||
const { parameters = [] } = strategy;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
<Grid>
|
||||
<Cell col={12} >
|
||||
<Cell col={12}>
|
||||
<h6>Parameters</h6>
|
||||
<hr />
|
||||
<List>
|
||||
{this.renderParameters(parameters)}
|
||||
</List>
|
||||
<List>{this.renderParameters(parameters)}</List>
|
||||
</Cell>
|
||||
|
||||
<Cell col={6} tablet={12}>
|
||||
@ -63,5 +61,4 @@ class ShowStrategyComponent extends PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default ShowStrategyComponent;
|
||||
|
@ -20,12 +20,12 @@ export default class StrategyDetails extends Component {
|
||||
fetchStrategies: PropTypes.func.isRequired,
|
||||
fetchApplications: PropTypes.func.isRequired,
|
||||
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
if (!this.props.strategy) {
|
||||
this.props.fetchStrategies();
|
||||
};
|
||||
}
|
||||
if (!this.props.applications || this.props.applications.length === 0) {
|
||||
this.props.fetchApplications();
|
||||
}
|
||||
@ -34,23 +34,28 @@ export default class StrategyDetails extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getTabContent (activeTabId) {
|
||||
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} />);
|
||||
return (
|
||||
<ShowStrategy
|
||||
strategy={this.props.strategy}
|
||||
toggles={this.props.toggles}
|
||||
applications={this.props.applications}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
goToTab (tabName) {
|
||||
goToTab(tabName) {
|
||||
hashHistory.push(`/strategies/${tabName}/${this.props.strategyName}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.view;
|
||||
render() {
|
||||
const activeTabId = TABS[this.props.activeTab]
|
||||
? TABS[this.props.activeTab]
|
||||
: TABS.view;
|
||||
const strategy = this.props.strategy;
|
||||
if (!strategy) {
|
||||
return <ProgressBar indeterminate />;
|
||||
@ -58,24 +63,29 @@ export default class StrategyDetails extends Component {
|
||||
|
||||
const tabContent = this.getTabContent(activeTabId);
|
||||
|
||||
return (<Grid className="mdl-color--white">
|
||||
<Cell col={12}>
|
||||
<HeaderTitle title={strategy.name} subtitle={strategy.description} />
|
||||
{strategy.editable === false ? '' : <Tabs activeTab={activeTabId} ripple>
|
||||
<Tab onClick={() => this.goToTab('view')}>
|
||||
Details
|
||||
</Tab>
|
||||
<Tab onClick={() => this.goToTab('edit')}>
|
||||
Edit
|
||||
</Tab>
|
||||
</Tabs>}
|
||||
return (
|
||||
<Grid className="mdl-color--white">
|
||||
<Cell col={12}>
|
||||
<HeaderTitle
|
||||
title={strategy.name}
|
||||
subtitle={strategy.description}
|
||||
/>
|
||||
{strategy.editable === false ? (
|
||||
''
|
||||
) : (
|
||||
<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>
|
||||
</Cell>
|
||||
</Grid>);
|
||||
<section>
|
||||
<div className="content">{tabContent}</div>
|
||||
</section>
|
||||
</Cell>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,9 +11,12 @@ const mapStateToProps = (state, props) => {
|
||||
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);
|
||||
const toggles = state.features.filter(
|
||||
toggle =>
|
||||
toggle
|
||||
.get('strategies')
|
||||
.findIndex(s => s.name === props.strategyName) > -1
|
||||
);
|
||||
|
||||
return {
|
||||
strategy,
|
||||
|
@ -7,16 +7,20 @@ export default class ShowUserComponent extends React.Component {
|
||||
openEdit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
openEdit = (evt) => {
|
||||
openEdit = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.openEdit();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<a className="mdl-navigation__link" href="#edit-user" onClick={this.openEdit}>
|
||||
<a
|
||||
className="mdl-navigation__link"
|
||||
href="#edit-user"
|
||||
onClick={this.openEdit}
|
||||
>
|
||||
<Tooltip label={this.props.user.userName || 'Unknown'} large>
|
||||
<Icon name="account_circle"/>
|
||||
<Icon name="account_circle" />
|
||||
</Tooltip>
|
||||
</a>
|
||||
);
|
||||
|
@ -2,12 +2,11 @@ import { connect } from 'react-redux';
|
||||
import ShowUserComponent from './show-user-component';
|
||||
import { openEdit } from '../../store/user/actions';
|
||||
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openEdit,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = state => ({
|
||||
user: state.user.toJS(),
|
||||
});
|
||||
|
||||
|
@ -28,24 +28,26 @@ class EditUserComponent extends React.Component {
|
||||
user: PropTypes.object.isRequired,
|
||||
updateUserName: PropTypes.func.isRequired,
|
||||
save: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmit = (evt) => {
|
||||
handleSubmit = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.save();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
isOpen={this.props.user.showDialog}
|
||||
contentLabel="test"
|
||||
style={customStyles} >
|
||||
style={customStyles}
|
||||
>
|
||||
<h2>Action required</h2>
|
||||
<div>
|
||||
<p>
|
||||
You have to specify a username to use Unleash. This will allow us to track your changes.
|
||||
You have to specify a username to use Unleash. This
|
||||
will allow us to track your changes.
|
||||
</p>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Textfield
|
||||
@ -53,10 +55,13 @@ class EditUserComponent extends React.Component {
|
||||
name="username"
|
||||
required
|
||||
value={this.props.user.userName}
|
||||
onChange={(e) => this.props.updateUserName(e.target.value)}
|
||||
onChange={e =>
|
||||
this.props.updateUserName(e.target.value)}
|
||||
/>
|
||||
<br />
|
||||
<Button raised accent>Save</Button>
|
||||
<Button raised accent>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -2,13 +2,12 @@ import { connect } from 'react-redux';
|
||||
import UserComponent from './user-component';
|
||||
import { updateUserName, save } from '../../store/user/actions';
|
||||
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateUserName,
|
||||
save,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
const mapStateToProps = state => ({
|
||||
user: state.user.toJS(),
|
||||
});
|
||||
|
||||
|
@ -2,19 +2,19 @@ import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = 'api/admin/metrics/applications';
|
||||
|
||||
function fetchAll () {
|
||||
function fetchAll() {
|
||||
return fetch(URI, { headers, credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function fetchApplication (appName) {
|
||||
function fetchApplication(appName) {
|
||||
return fetch(`${URI}/${appName}`, { headers, credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function fetchApplicationsWithStrategyName (strategyName) {
|
||||
function fetchApplicationsWithStrategyName(strategyName) {
|
||||
return fetch(`${URI}?strategyName=${strategyName}`, {
|
||||
headers,
|
||||
credentials: 'include',
|
||||
@ -23,7 +23,7 @@ function fetchApplicationsWithStrategyName (strategyName) {
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function storeApplicationMetaData (appName, key, value) {
|
||||
function storeApplicationMetaData(appName, key, value) {
|
||||
const data = {};
|
||||
data[key] = value;
|
||||
return fetch(`${URI}/${appName}`, {
|
||||
|
@ -2,13 +2,13 @@ import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = 'api/admin/archive';
|
||||
|
||||
function fetchAll () {
|
||||
function fetchAll() {
|
||||
return fetch(`${URI}/features`, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function revive (featureName) {
|
||||
function revive(featureName) {
|
||||
return fetch(`${URI}/revive/${featureName}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
@ -16,9 +16,7 @@ function revive (featureName) {
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
fetchAll,
|
||||
revive,
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = 'api/admin/metrics/instances';
|
||||
|
||||
function fetchAll () {
|
||||
function fetchAll() {
|
||||
return fetch(URI, { headers, credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
|
@ -2,9 +2,12 @@ import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = 'api/admin/features';
|
||||
|
||||
function validateToggle (featureToggle) {
|
||||
function validateToggle(featureToggle) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!featureToggle.strategies || featureToggle.strategies.length === 0) {
|
||||
if (
|
||||
!featureToggle.strategies ||
|
||||
featureToggle.strategies.length === 0
|
||||
) {
|
||||
reject(new Error('You must add at least one activation strategy'));
|
||||
} else {
|
||||
resolve(featureToggle);
|
||||
@ -12,24 +15,26 @@ function validateToggle (featureToggle) {
|
||||
});
|
||||
}
|
||||
|
||||
function fetchAll () {
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function create (featureToggle) {
|
||||
function create(featureToggle) {
|
||||
return validateToggle(featureToggle)
|
||||
.then(() => fetch(URI, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(featureToggle),
|
||||
}))
|
||||
.then(() =>
|
||||
fetch(URI, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(featureToggle),
|
||||
})
|
||||
)
|
||||
.then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function validate (featureToggle) {
|
||||
function validate(featureToggle) {
|
||||
return fetch(`${URI}/validate`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
@ -38,27 +43,28 @@ function validate (featureToggle) {
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function update (featureToggle) {
|
||||
function update(featureToggle) {
|
||||
return validateToggle(featureToggle)
|
||||
.then(() => fetch(`${URI}/${featureToggle.name}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(featureToggle),
|
||||
}))
|
||||
.then(() =>
|
||||
fetch(`${URI}/${featureToggle.name}`, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(featureToggle),
|
||||
})
|
||||
)
|
||||
.then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function toggle (name) {
|
||||
function toggle(name) {
|
||||
return fetch(`${URI}/${name}/toggle`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(throwIfNotSuccess);
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function remove (featureToggleName) {
|
||||
function remove(featureToggleName) {
|
||||
return fetch(`${URI}/${featureToggleName}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
|
@ -2,7 +2,7 @@ const { throwIfNotSuccess } = require('./helper');
|
||||
|
||||
const URI = 'api/admin/metrics/feature-toggles';
|
||||
|
||||
function fetchFeatureMetrics () {
|
||||
function fetchFeatureMetrics() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
@ -10,7 +10,7 @@ function fetchFeatureMetrics () {
|
||||
|
||||
const seenURI = 'api/admin/metrics/seen-apps';
|
||||
|
||||
function fetchSeenApps () {
|
||||
function fetchSeenApps() {
|
||||
return fetch(seenURI, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
|
@ -1,18 +1,23 @@
|
||||
const defaultErrorMessage = 'Unexptected exception when talking to unleash-api';
|
||||
|
||||
function extractJoiMsg (body) {
|
||||
return body.details.length > 0 ? body.details[0].message : defaultErrorMessage;
|
||||
function extractJoiMsg(body) {
|
||||
return body.details.length > 0
|
||||
? body.details[0].message
|
||||
: defaultErrorMessage;
|
||||
}
|
||||
function extractLegacyMsg (body) {
|
||||
function extractLegacyMsg(body) {
|
||||
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
|
||||
}
|
||||
|
||||
export function throwIfNotSuccess (response) {
|
||||
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.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
||||
const errorMsg =
|
||||
body && body.isJoi
|
||||
? extractJoiMsg(body)
|
||||
: extractLegacyMsg(body);
|
||||
let error = new Error(errorMsg);
|
||||
error.statusCode = response.status;
|
||||
reject(error);
|
||||
@ -23,10 +28,9 @@ export function throwIfNotSuccess (response) {
|
||||
}
|
||||
}
|
||||
return Promise.resolve(response);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export const headers = {
|
||||
'Accept': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
@ -2,13 +2,13 @@ import { throwIfNotSuccess } from './helper';
|
||||
|
||||
const URI = 'api/admin/events';
|
||||
|
||||
function fetchAll () {
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function fetchHistoryForToggle (toggleName) {
|
||||
function fetchHistoryForToggle(toggleName) {
|
||||
return fetch(`${URI}/${toggleName}`, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
|
@ -2,13 +2,13 @@ import { throwIfNotSuccess, headers } from './helper';
|
||||
|
||||
const URI = 'api/admin/strategies';
|
||||
|
||||
function fetchAll () {
|
||||
function fetchAll() {
|
||||
return fetch(URI, { credentials: 'include' })
|
||||
.then(throwIfNotSuccess)
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
function create (strategy) {
|
||||
function create(strategy) {
|
||||
return fetch(URI, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
@ -17,7 +17,7 @@ function create (strategy) {
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function update (strategy) {
|
||||
function update(strategy) {
|
||||
return fetch(`${URI}/${strategy.name}`, {
|
||||
method: 'put',
|
||||
headers,
|
||||
@ -26,7 +26,7 @@ function update (strategy) {
|
||||
}).then(throwIfNotSuccess);
|
||||
}
|
||||
|
||||
function remove (strategy) {
|
||||
function remove(strategy) {
|
||||
return fetch(`${URI}/${strategy.name}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
|
@ -4,7 +4,13 @@ import 'react-mdl/extra/material.js';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { applyRouterMiddleware, Router, Route, IndexRedirect, hashHistory } from 'react-router';
|
||||
import {
|
||||
applyRouterMiddleware,
|
||||
Router,
|
||||
Route,
|
||||
IndexRedirect,
|
||||
hashHistory,
|
||||
} from 'react-router';
|
||||
import { useScroll } from 'react-router-scroll';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunkMiddleware from 'redux-thunk';
|
||||
@ -27,7 +33,10 @@ import ApplicationView from './page/applications/view';
|
||||
|
||||
let composeEnhancers;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
) {
|
||||
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
|
||||
} else {
|
||||
composeEnhancers = compose;
|
||||
@ -35,42 +44,88 @@ if (process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION_C
|
||||
|
||||
const unleashStore = createStore(
|
||||
store,
|
||||
composeEnhancers(
|
||||
applyMiddleware(thunkMiddleware)
|
||||
)
|
||||
composeEnhancers(applyMiddleware(thunkMiddleware))
|
||||
);
|
||||
|
||||
// "pageTitle" and "link" attributes are for internal usage only
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={unleashStore}>
|
||||
<Router history={hashHistory} render={applyRouterMiddleware(useScroll())}>
|
||||
<Router
|
||||
history={hashHistory}
|
||||
render={applyRouterMiddleware(useScroll())}
|
||||
>
|
||||
<Route path="/" component={App}>
|
||||
<IndexRedirect to="/features" />
|
||||
|
||||
<Route pageTitle="Feature Toggles" link="/features">
|
||||
<Route pageTitle="Feature toggles" path="/features" component={Features} />
|
||||
<Route pageTitle="New" path="/features/create" component={CreateFeatureToggle} />
|
||||
<Route pageTitle=":name" path="/features/:activeTab/:name" component={ViewFeatureToggle} />
|
||||
<Route
|
||||
pageTitle="Feature toggles"
|
||||
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
|
||||
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="Event History" link="/history">
|
||||
<Route pageTitle="Event history" path="/history" component={HistoryPage} />
|
||||
<Route pageTitle=":toggleName" path="/history/:toggleName" component={HistoryTogglePage} />
|
||||
<Route
|
||||
pageTitle="Event history"
|
||||
path="/history"
|
||||
component={HistoryPage}
|
||||
/>
|
||||
<Route
|
||||
pageTitle=":toggleName"
|
||||
path="/history/:toggleName"
|
||||
component={HistoryTogglePage}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route pageTitle="Archived Toggles" path="/archive" component={Archive} />
|
||||
<Route
|
||||
pageTitle="Archived Toggles"
|
||||
path="/archive"
|
||||
component={Archive}
|
||||
/>
|
||||
<Route pageTitle="Applications" link="/applications">
|
||||
<Route pageTitle="Applications" path="/applications" component={Applications} />
|
||||
<Route pageTitle=":name" path="/applications/:name" component={ApplicationView} />
|
||||
<Route
|
||||
pageTitle="Applications"
|
||||
path="/applications"
|
||||
component={Applications}
|
||||
/>
|
||||
<Route
|
||||
pageTitle=":name"
|
||||
path="/applications/:name"
|
||||
component={ApplicationView}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
</Router>
|
||||
</Provider>, document.getElementById('app'));
|
||||
</Provider>,
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ApplicationEditComponent from '../../component/application/application-edit-container';
|
||||
|
||||
const render = ({ params }) => <ApplicationEditComponent appName={params.name} />;
|
||||
const render = ({ params }) => (
|
||||
<ApplicationEditComponent appName={params.name} />
|
||||
);
|
||||
|
||||
render.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import AddFeatureToggleForm from '../../component/feature/form-add-container';
|
||||
|
||||
|
||||
const render = () => (<AddFeatureToggleForm title="Create feature toggle" />);
|
||||
const render = () => <AddFeatureToggleForm title="Create feature toggle" />;
|
||||
|
||||
export default render;
|
||||
|
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import FeatureListContainer from '../../component/feature/list-container';
|
||||
|
||||
const render = () => (<FeatureListContainer />);
|
||||
const render = () => <FeatureListContainer />;
|
||||
|
||||
export default render;
|
||||
|
||||
|
||||
|
@ -4,12 +4,15 @@ import ViewFeatureToggle from '../../component/feature/view-container';
|
||||
export default class Features extends PureComponent {
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { params } = this.props;
|
||||
return (
|
||||
<ViewFeatureToggle featureToggleName={params.name} activeTab={params.activeTab} />
|
||||
<ViewFeatureToggle
|
||||
featureToggleName={params.name}
|
||||
activeTab={params.activeTab}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import HistoryListToggle from '../../component/history/history-list-toggle-container';
|
||||
|
||||
const render = ({ params }) => <HistoryListToggle toggleName={params.toggleName} />;
|
||||
const render = ({ params }) => (
|
||||
<HistoryListToggle toggleName={params.toggleName} />
|
||||
);
|
||||
|
||||
render.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import AddStrategies from '../../component/strategies/add-container';
|
||||
|
||||
export default () => (<AddStrategies />);
|
||||
export default () => <AddStrategies />;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import Strategies from '../../component/strategies/list-container';
|
||||
|
||||
export default () => (<Strategies />);
|
||||
export default () => <Strategies />;
|
||||
|
@ -1,7 +1,12 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ShowStrategy from '../../component/strategies/strategy-details-container';
|
||||
|
||||
const render = ({ params }) => <ShowStrategy strategyName={params.strategyName} activeTab={params.activeTab} />;
|
||||
const render = ({ params }) => (
|
||||
<ShowStrategy
|
||||
strategyName={params.strategyName}
|
||||
activeTab={params.activeTab}
|
||||
/>
|
||||
);
|
||||
|
||||
render.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
|
@ -2,39 +2,55 @@ 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 ERROR_UPDATING_APPLICATION_DATA =
|
||||
'ERROR_UPDATING_APPLICATION_DATA';
|
||||
|
||||
export const RECEIVE_APPLICATION = 'RECEIVE_APPLICATION';
|
||||
|
||||
const recieveAllApplications = (json) => ({
|
||||
const recieveAllApplications = json => ({
|
||||
type: RECEIVE_ALL_APPLICATIONS,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const recieveApplication = (json) => ({
|
||||
const recieveApplication = json => ({
|
||||
type: RECEIVE_APPLICATION,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveApplications = (statusCode, type = ERROR_RECEIVE_ALL_APPLICATIONS) => ({
|
||||
const errorReceiveApplications = (
|
||||
statusCode,
|
||||
type = ERROR_RECEIVE_ALL_APPLICATIONS
|
||||
) => ({
|
||||
type,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchAll () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(recieveAllApplications(json)))
|
||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
||||
export function fetchAll() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(recieveAllApplications(json)))
|
||||
.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 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)))
|
||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
||||
export function fetchApplication(appName) {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchApplication(appName)
|
||||
.then(json => dispatch(recieveApplication(json)))
|
||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { fromJS, List, Map } from 'immutable';
|
||||
import { RECEIVE_ALL_APPLICATIONS, RECEIVE_APPLICATION } from './actions';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
return fromJS({ list: [], apps: {} });
|
||||
}
|
||||
|
||||
const store = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_APPLICATION:
|
||||
return state.setIn(['apps', action.value.appName], new Map(action.value));
|
||||
return state.setIn(
|
||||
['apps', action.value.appName],
|
||||
new Map(action.value)
|
||||
);
|
||||
case RECEIVE_ALL_APPLICATIONS:
|
||||
return state.set('list', new List(action.value.applications));
|
||||
default:
|
||||
|
@ -4,30 +4,33 @@ export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
|
||||
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
|
||||
export const ERROR_RECEIVE_ARCHIVE = 'ERROR_RECEIVE_ARCHIVE';
|
||||
|
||||
const receiveArchive = (json) => ({
|
||||
const receiveArchive = json => ({
|
||||
type: RECEIVE_ARCHIVE,
|
||||
value: json.features,
|
||||
});
|
||||
|
||||
const reviveToggle = (archiveFeatureToggle) => ({
|
||||
const reviveToggle = archiveFeatureToggle => ({
|
||||
type: REVIVE_TOGGLE,
|
||||
value: archiveFeatureToggle,
|
||||
});
|
||||
|
||||
const errorReceiveArchive = (statusCode) => ({
|
||||
const errorReceiveArchive = statusCode => ({
|
||||
type: ERROR_RECEIVE_ARCHIVE,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function revive (featureToggle) {
|
||||
return dispatch => api.revive(featureToggle)
|
||||
.then(() => dispatch(reviveToggle(featureToggle)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
export function revive(featureToggle) {
|
||||
return dispatch =>
|
||||
api
|
||||
.revive(featureToggle)
|
||||
.then(() => dispatch(reviveToggle(featureToggle)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
}
|
||||
|
||||
|
||||
export function fetchArchive () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveArchive(json)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
export function fetchArchive() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveArchive(json)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
}
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_ARCHIVE, REVIVE_TOGGLE } from './archive-actions';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
return new $Map({ list: new List() });
|
||||
}
|
||||
|
||||
const archiveStore = (state = getInitState(), action) => {
|
||||
switch (action.type) {
|
||||
case REVIVE_TOGGLE:
|
||||
return state.update('list', (list) => list.filter(item => item.name !== action.value));
|
||||
return state.update('list', list =>
|
||||
list.filter(item => item.name !== action.value)
|
||||
);
|
||||
case RECEIVE_ARCHIVE:
|
||||
return state.set('list', new List(action.value));
|
||||
default:
|
||||
|
@ -3,18 +3,20 @@ import api from '../data/client-instance-api';
|
||||
export const RECEIVE_CLIENT_INSTANCES = 'RECEIVE_CLIENT_INSTANCES';
|
||||
export const ERROR_RECEIVE_CLIENT_INSTANCES = 'ERROR_RECEIVE_CLIENT_INSTANCES';
|
||||
|
||||
const receiveClientInstances = (json) => ({
|
||||
const receiveClientInstances = json => ({
|
||||
type: RECEIVE_CLIENT_INSTANCES,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveClientInstances = (statusCode) => ({
|
||||
const errorReceiveClientInstances = statusCode => ({
|
||||
type: RECEIVE_CLIENT_INSTANCES,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchClientInstances () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveClientInstances(json)))
|
||||
.catch(error => dispatch(errorReceiveClientInstances(error)));
|
||||
export function fetchClientInstances() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveClientInstances(json)))
|
||||
.catch(error => dispatch(errorReceiveClientInstances(error)));
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { fromJS } from 'immutable';
|
||||
import { RECEIVE_CLIENT_INSTANCES } from './client-instance-actions';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
return fromJS([]);
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,4 @@ export const MUTE_ERROR = 'MUTE_ERROR';
|
||||
|
||||
export const muteErrors = () => ({ type: MUTE_ERRORS });
|
||||
|
||||
export const muteError = (error) => ({ type: MUTE_ERROR, error });
|
||||
|
||||
|
||||
export const muteError = error => ({ type: MUTE_ERROR, error });
|
||||
|
@ -11,21 +11,20 @@ import {
|
||||
ERROR_UPDATING_STRATEGY,
|
||||
ERROR_CREATING_STRATEGY,
|
||||
ERROR_RECEIVE_STRATEGIES,
|
||||
|
||||
} from './strategy/actions';
|
||||
|
||||
const debug = require('debug')('unleash:error-store');
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
return new $Map({
|
||||
list: new List(),
|
||||
});
|
||||
}
|
||||
|
||||
function addErrorIfNotAlreadyInList (state, error) {
|
||||
function addErrorIfNotAlreadyInList(state, error) {
|
||||
debug('Got error', error);
|
||||
if (state.get('list').indexOf(error) < 0) {
|
||||
return state.update('list', (list) => list.push(error));
|
||||
return state.update('list', list => list.push(error));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -41,7 +40,9 @@ const strategies = (state = getInitState(), action) => {
|
||||
case ERROR_RECEIVE_STRATEGIES:
|
||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||
case MUTE_ERROR:
|
||||
return state.update('list', (list) => list.remove(list.indexOf(action.error)));
|
||||
return state.update('list', list =>
|
||||
list.remove(list.indexOf(action.error))
|
||||
);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -15,22 +15,21 @@ export const ERROR_CREATING_FEATURE_TOGGLE = 'ERROR_CREATING_FEATURE_TOGGLE';
|
||||
export const ERROR_UPDATE_FEATURE_TOGGLE = 'ERROR_UPDATE_FEATURE_TOGGLE';
|
||||
export const ERROR_REMOVE_FEATURE_TOGGLE = 'ERROR_REMOVE_FEATURE_TOGGLE';
|
||||
|
||||
export function toggleFeature (name) {
|
||||
export function toggleFeature(name) {
|
||||
debug('Toggle feature toggle ', name);
|
||||
return dispatch => {
|
||||
dispatch(requestToggleFeatureToggle(name));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function editFeatureToggle (featureToggle) {
|
||||
export function editFeatureToggle(featureToggle) {
|
||||
debug('Update feature toggle ', featureToggle);
|
||||
return dispatch => {
|
||||
dispatch(requestUpdateFeatureToggle(featureToggle));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function receiveFeatureToggles (json) {
|
||||
function receiveFeatureToggles(json) {
|
||||
debug('reviced feature toggles', json);
|
||||
return {
|
||||
type: RECEIVE_FEATURE_TOGGLES,
|
||||
@ -39,64 +38,73 @@ function receiveFeatureToggles (json) {
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchAndThrow (dispatch, type) {
|
||||
return (error) => {
|
||||
function dispatchAndThrow(dispatch, type) {
|
||||
return error => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchFeatureToggles () {
|
||||
export function fetchFeatureToggles() {
|
||||
debug('Start fetching feature toggles');
|
||||
return dispatch => {
|
||||
dispatch({ type: START_FETCH_FEATURE_TOGGLES });
|
||||
|
||||
return api.fetchAll()
|
||||
return api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveFeatureToggles(json)))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLES));
|
||||
};
|
||||
}
|
||||
|
||||
export function createFeatureToggles (featureToggle) {
|
||||
export function createFeatureToggles(featureToggle) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_CREATE_FEATURE_TOGGLE });
|
||||
|
||||
return api.create(featureToggle)
|
||||
return api
|
||||
.create(featureToggle)
|
||||
.then(() => dispatch({ type: ADD_FEATURE_TOGGLE, featureToggle }))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_CREATING_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function requestToggleFeatureToggle (name) {
|
||||
export function requestToggleFeatureToggle(name) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
|
||||
|
||||
return api.toggle(name)
|
||||
return api
|
||||
.toggle(name)
|
||||
.then(() => dispatch({ type: TOGGLE_FEATURE_TOGGLE, name }))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function requestUpdateFeatureToggle (featureToggle) {
|
||||
export function requestUpdateFeatureToggle(featureToggle) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_UPDATE_FEATURE_TOGGLE });
|
||||
|
||||
return api.update(featureToggle)
|
||||
.then(() => dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle }))
|
||||
return api
|
||||
.update(featureToggle)
|
||||
.then(() =>
|
||||
dispatch({ type: UPDATE_FEATURE_TOGGLE, featureToggle })
|
||||
)
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_UPDATE_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function removeFeatureToggle (featureToggleName) {
|
||||
export function removeFeatureToggle(featureToggleName) {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_REMOVE_FEATURE_TOGGLE });
|
||||
|
||||
return api.remove(featureToggleName)
|
||||
.then(() => dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName }))
|
||||
return api
|
||||
.remove(featureToggleName)
|
||||
.then(() =>
|
||||
dispatch({ type: REMOVE_FEATURE_TOGGLE, featureToggleName })
|
||||
)
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_REMOVE_FEATURE_TOGGLE));
|
||||
};
|
||||
}
|
||||
|
||||
export function validateName (featureToggleName) {
|
||||
export function validateName(featureToggleName) {
|
||||
return api.validate({ name: featureToggleName });
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ export const START_FETCH_SEEN_APP = 'START_FETCH_SEEN_APP';
|
||||
export const RECEIVE_SEEN_APPS = 'RECEIVE_SEEN_APPS';
|
||||
export const ERROR_FETCH_SEEN_APP = 'ERROR_FETCH_SEEN_APP';
|
||||
|
||||
function receiveFeatureMetrics (json) {
|
||||
function receiveFeatureMetrics(json) {
|
||||
return {
|
||||
type: RECEIVE_FEATURE_METRICS,
|
||||
value: json,
|
||||
@ -16,7 +16,7 @@ function receiveFeatureMetrics (json) {
|
||||
};
|
||||
}
|
||||
|
||||
function receiveSeenApps (json) {
|
||||
function receiveSeenApps(json) {
|
||||
return {
|
||||
type: RECEIVE_SEEN_APPS,
|
||||
value: json,
|
||||
@ -24,30 +24,31 @@ function receiveSeenApps (json) {
|
||||
};
|
||||
}
|
||||
|
||||
function dispatchAndThrow (dispatch, type) {
|
||||
return (error) => {
|
||||
function dispatchAndThrow(dispatch, type) {
|
||||
return error => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchFeatureMetrics () {
|
||||
export function fetchFeatureMetrics() {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_FETCH_SEEN_APP });
|
||||
|
||||
return api.fetchFeatureMetrics()
|
||||
return api
|
||||
.fetchFeatureMetrics()
|
||||
.then(json => dispatch(receiveFeatureMetrics(json)))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_SEEN_APP));
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchSeenApps () {
|
||||
export function fetchSeenApps() {
|
||||
return dispatch => {
|
||||
dispatch({ type: START_FETCH_FEATURE_METRICS });
|
||||
|
||||
return api.fetchSeenApps()
|
||||
return api
|
||||
.fetchSeenApps()
|
||||
.then(json => dispatch(receiveSeenApps(json)))
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_FEATURE_TOGGLES));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -5,13 +5,15 @@ import {
|
||||
RECEIVE_SEEN_APPS,
|
||||
} from './feature-metrics-actions';
|
||||
|
||||
|
||||
const metrics = (state = fromJS({ lastHour: {}, lastMinute: {}, seenApps: {} }), action) => {
|
||||
const metrics = (
|
||||
state = fromJS({ lastHour: {}, lastMinute: {}, seenApps: {} }),
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case RECEIVE_SEEN_APPS:
|
||||
return state.set('seenApps', new $Map(action.value));
|
||||
case RECEIVE_FEATURE_METRICS:
|
||||
return state.withMutations((ctx) => {
|
||||
return state.withMutations(ctx => {
|
||||
ctx.set('lastHour', new $Map(action.value.lastHour));
|
||||
ctx.set('lastMinute', new $Map(action.value.lastMinute));
|
||||
return ctx;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
const debug = require('debug')('unleash:feature-store');
|
||||
|
||||
|
||||
import {
|
||||
ADD_FEATURE_TOGGLE,
|
||||
RECEIVE_FEATURE_TOGGLES,
|
||||
@ -10,7 +9,6 @@ import {
|
||||
TOGGLE_FEATURE_TOGGLE,
|
||||
} from './feature-actions';
|
||||
|
||||
|
||||
const features = (state = new List([]), action) => {
|
||||
switch (action.type) {
|
||||
case ADD_FEATURE_TOGGLE:
|
||||
@ -18,7 +16,9 @@ const features = (state = new List([]), action) => {
|
||||
return state.push(new $Map(action.featureToggle));
|
||||
case REMOVE_FEATURE_TOGGLE:
|
||||
debug(REMOVE_FEATURE_TOGGLE, action);
|
||||
return state.filter(toggle => toggle.get('name') !== action.featureToggleName);
|
||||
return state.filter(
|
||||
toggle => toggle.get('name') !== action.featureToggleName
|
||||
);
|
||||
case TOGGLE_FEATURE_TOGGLE:
|
||||
debug(TOGGLE_FEATURE_TOGGLE, action);
|
||||
return state.map(toggle => {
|
||||
|
@ -5,30 +5,33 @@ export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
|
||||
|
||||
export const RECEIVE_HISTORY_FOR_TOGGLE = 'RECEIVE_HISTORY_FOR_TOGGLE';
|
||||
|
||||
const receiveHistory = (json) => ({
|
||||
const receiveHistory = json => ({
|
||||
type: RECEIVE_HISTORY,
|
||||
value: json.events,
|
||||
});
|
||||
|
||||
const receiveHistoryforToggle = (json) => ({
|
||||
const receiveHistoryforToggle = json => ({
|
||||
type: RECEIVE_HISTORY_FOR_TOGGLE,
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveHistory = (statusCode) => ({
|
||||
const errorReceiveHistory = statusCode => ({
|
||||
type: ERROR_RECEIVE_HISTORY,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchHistory () {
|
||||
return dispatch => api.fetchAll()
|
||||
.then(json => dispatch(receiveHistory(json)))
|
||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
||||
export function fetchHistory() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.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)));
|
||||
export function fetchHistoryForToggle(toggleName) {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchHistoryForToggle(toggleName)
|
||||
.then(json => dispatch(receiveHistoryforToggle(json)))
|
||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_HISTORY, RECEIVE_HISTORY_FOR_TOGGLE } from './history-actions';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
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));
|
||||
return state.setIn(
|
||||
['toggles', action.value.toggleName],
|
||||
new List(action.value.events)
|
||||
);
|
||||
case RECEIVE_HISTORY:
|
||||
return state.set('list', new List(action.value));
|
||||
default:
|
||||
|
@ -9,13 +9,49 @@ export const actions = {
|
||||
MOVE: 'MOVE',
|
||||
};
|
||||
|
||||
export const createInit = ({ id, value }) => ({ type: actions.INIT, id, value });
|
||||
export const createInc = ({ id, key }) => ({ type: actions.INCREMENT_VALUE, id, key });
|
||||
export const createSet = ({ id, key, value }) => ({ type: actions.SET_VALUE, id, key, value });
|
||||
export const createPush = ({ id, key, value }) => ({ type: actions.LIST_PUSH, id, key, value });
|
||||
export const createPop = ({ id, key, index }) => ({ type: actions.LIST_POP, id, key, index });
|
||||
export const createMove = ({ id, key, index, toIndex }) => ({ type: actions.MOVE, id, key, index, toIndex });
|
||||
export const createUp = ({ id, key, index, newValue, merge }) => ({ type: actions.LIST_UP, id, key, index, newValue, merge });
|
||||
export const createInit = ({ id, value }) => ({
|
||||
type: actions.INIT,
|
||||
id,
|
||||
value,
|
||||
});
|
||||
export const createInc = ({ id, key }) => ({
|
||||
type: actions.INCREMENT_VALUE,
|
||||
id,
|
||||
key,
|
||||
});
|
||||
export const createSet = ({ id, key, value }) => ({
|
||||
type: actions.SET_VALUE,
|
||||
id,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
export const createPush = ({ id, key, value }) => ({
|
||||
type: actions.LIST_PUSH,
|
||||
id,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
export const createPop = ({ id, key, index }) => ({
|
||||
type: actions.LIST_POP,
|
||||
id,
|
||||
key,
|
||||
index,
|
||||
});
|
||||
export const createMove = ({ id, key, index, toIndex }) => ({
|
||||
type: actions.MOVE,
|
||||
id,
|
||||
key,
|
||||
index,
|
||||
toIndex,
|
||||
});
|
||||
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;
|
||||
|
@ -1,75 +1,75 @@
|
||||
import { Map as $Map, List, fromJS } from 'immutable';
|
||||
import actions from './input-actions';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
return new $Map();
|
||||
}
|
||||
|
||||
function init (state, { id, value }) {
|
||||
function init(state, { id, value }) {
|
||||
state = assertId(state, id);
|
||||
return state.setIn(id, fromJS(value));
|
||||
}
|
||||
|
||||
function assertId (state, id) {
|
||||
function assertId(state, id) {
|
||||
if (!state.hasIn(id)) {
|
||||
return state.setIn(id, new $Map({ inputId: id }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function assertList (state, id, key) {
|
||||
function assertList(state, id, key) {
|
||||
if (!state.getIn(id).has(key)) {
|
||||
return state.setIn(id.concat([key]), new List());
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function setKeyValue (state, { id, key, value }) {
|
||||
function setKeyValue(state, { id, key, value }) {
|
||||
state = assertId(state, id);
|
||||
return state.setIn(id.concat([key]), value);
|
||||
}
|
||||
|
||||
function increment (state, { id, key }) {
|
||||
function increment(state, { id, key }) {
|
||||
state = assertId(state, id);
|
||||
return state.updateIn(id.concat([key]), (value = 0) => value + 1);
|
||||
}
|
||||
|
||||
function clear (state, { id }) {
|
||||
function clear(state, { id }) {
|
||||
if (state.hasIn(id)) {
|
||||
return state.removeIn(id);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function addToList (state, { id, key, value }) {
|
||||
function addToList(state, { id, key, value }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => list.push(value));
|
||||
return state.updateIn(id.concat([key]), list => list.push(value));
|
||||
}
|
||||
|
||||
function updateInList (state, { id, key, index, newValue, merge }) {
|
||||
function updateInList(state, { id, key, index, newValue, merge }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => {
|
||||
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' ) {
|
||||
} else if (typeof newValue !== 'string') {
|
||||
newValue = fromJS(newValue);
|
||||
}
|
||||
return list.set(index, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
function removeFromList (state, { id, key, index }) {
|
||||
function removeFromList(state, { id, key, index }) {
|
||||
state = assertId(state, id);
|
||||
state = assertList(state, id, key);
|
||||
|
||||
return state.updateIn(id.concat([key]), (list) => list.remove(index));
|
||||
return state.updateIn(id.concat([key]), list => list.remove(index));
|
||||
}
|
||||
|
||||
function move (state, { id, key, index, toIndex }) {
|
||||
function move(state, { id, key, index, toIndex }) {
|
||||
return state.updateIn(id.concat([key]), list => {
|
||||
const olditem = list.get(index);
|
||||
return list.delete(index).insert(toIndex, olditem);
|
||||
|
@ -7,4 +7,5 @@ export const updateSetting = (group, field, value) => ({
|
||||
value,
|
||||
});
|
||||
|
||||
export const updateSettingForGroup = (group) => (field, value) => updateSetting(group, field, value);
|
||||
export const updateSettingForGroup = group => (field, value) =>
|
||||
updateSetting(group, field, value);
|
||||
|
@ -5,7 +5,7 @@ import { UPDATE_SETTING } from './actions';
|
||||
const localStorage = window.localStorage || {};
|
||||
const SETTINGS = 'settings';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
try {
|
||||
const state = JSON.parse(localStorage.getItem(SETTINGS));
|
||||
return state ? fromJS(state) : new $Map();
|
||||
@ -14,8 +14,11 @@ function getInitState () {
|
||||
}
|
||||
}
|
||||
|
||||
function updateSetting (state, action) {
|
||||
const newState = state.updateIn([action.group, action.field], () => action.value);
|
||||
function updateSetting(state, action) {
|
||||
const newState = state.updateIn(
|
||||
[action.group, action.field],
|
||||
() => action.value
|
||||
);
|
||||
|
||||
localStorage.setItem(SETTINGS, JSON.stringify(newState.toJSON()));
|
||||
return newState;
|
||||
|
@ -12,77 +12,79 @@ 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 addStrategy = strategy => ({ type: ADD_STRATEGY, strategy });
|
||||
const createRemoveStrategy = strategy => ({ type: REMOVE_STRATEGY, strategy });
|
||||
const updatedStrategy = strategy => ({ type: UPDATE_STRATEGY, strategy });
|
||||
|
||||
const errorCreatingStrategy = (statusCode) => ({
|
||||
const errorCreatingStrategy = statusCode => ({
|
||||
type: ERROR_CREATING_STRATEGY,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
const startRequest = () => ({ type: REQUEST_STRATEGIES });
|
||||
|
||||
|
||||
const receiveStrategies = (json) => ({
|
||||
const receiveStrategies = json => ({
|
||||
type: RECEIVE_STRATEGIES,
|
||||
value: json.strategies,
|
||||
});
|
||||
|
||||
const startCreate = () => ({ type: START_CREATE_STRATEGY });
|
||||
|
||||
const errorReceiveStrategies = (statusCode) => ({
|
||||
const errorReceiveStrategies = statusCode => ({
|
||||
type: ERROR_RECEIVE_STRATEGIES,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
|
||||
|
||||
function dispatchAndThrow (dispatch, type) {
|
||||
return (error) => {
|
||||
function dispatchAndThrow(dispatch, type) {
|
||||
return error => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStrategies () {
|
||||
export function fetchStrategies() {
|
||||
return dispatch => {
|
||||
dispatch(startRequest());
|
||||
|
||||
return api.fetchAll()
|
||||
return api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveStrategies(json)))
|
||||
.catch(error => dispatch(errorReceiveStrategies(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function createStrategy (strategy) {
|
||||
export function createStrategy(strategy) {
|
||||
return dispatch => {
|
||||
dispatch(startCreate());
|
||||
|
||||
return api.create(strategy)
|
||||
return api
|
||||
.create(strategy)
|
||||
.then(() => dispatch(addStrategy(strategy)))
|
||||
.catch(error => dispatch(errorCreatingStrategy(error)));
|
||||
};
|
||||
}
|
||||
|
||||
export function updateStrategy (strategy) {
|
||||
export function updateStrategy(strategy) {
|
||||
return dispatch => {
|
||||
dispatch(startUpdate());
|
||||
|
||||
return api.update(strategy)
|
||||
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 removeStrategy(strategy) {
|
||||
return dispatch =>
|
||||
api
|
||||
.remove(strategy)
|
||||
.then(() => dispatch(createRemoveStrategy(strategy)))
|
||||
.catch(error => dispatch(errorCreatingStrategy(error)));
|
||||
}
|
||||
|
||||
export function getApplicationsWithStrategy (strategyName) {
|
||||
export function getApplicationsWithStrategy(strategyName) {
|
||||
return applicationApi.fetchApplicationsWithStrategyName(strategyName);
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,33 @@
|
||||
import { List, Map as $Map } from 'immutable';
|
||||
import { RECEIVE_STRATEGIES, REMOVE_STRATEGY, ADD_STRATEGY, UPDATE_STRATEGY } from './actions';
|
||||
import {
|
||||
RECEIVE_STRATEGIES,
|
||||
REMOVE_STRATEGY,
|
||||
ADD_STRATEGY,
|
||||
UPDATE_STRATEGY,
|
||||
} from './actions';
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
return new $Map({ list: new List() });
|
||||
}
|
||||
|
||||
function removeStrategy (state, action) {
|
||||
function removeStrategy(state, action) {
|
||||
const indexToRemove = state.get('list').indexOf(action.strategy);
|
||||
if (indexToRemove !== -1) {
|
||||
return state.update('list', (list) => list.remove(indexToRemove));
|
||||
return state.update('list', list => list.remove(indexToRemove));
|
||||
}
|
||||
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;
|
||||
}
|
||||
}));
|
||||
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) => {
|
||||
@ -30,7 +37,7 @@ const strategies = (state = getInitState(), action) => {
|
||||
case REMOVE_STRATEGY:
|
||||
return removeStrategy(state, action);
|
||||
case ADD_STRATEGY:
|
||||
return state.update('list', (list) => list.push(action.strategy));
|
||||
return state.update('list', list => list.push(action.strategy));
|
||||
case UPDATE_STRATEGY:
|
||||
return updateStrategy(state, action);
|
||||
default:
|
||||
|
@ -2,7 +2,7 @@ export const USER_UPDATE_USERNAME = 'USER_UPDATE_USERNAME';
|
||||
export const USER_SAVE = 'USER_SAVE';
|
||||
export const USER_EDIT = 'USER_EDIT';
|
||||
|
||||
export const updateUserName = (value) => ({
|
||||
export const updateUserName = value => ({
|
||||
type: USER_UPDATE_USERNAME,
|
||||
value,
|
||||
});
|
||||
|
@ -4,12 +4,13 @@ import { USER_UPDATE_USERNAME, USER_SAVE, USER_EDIT } from './actions';
|
||||
const COOKIE_NAME = 'username';
|
||||
|
||||
// Ref: http://stackoverflow.com/questions/10730362/get-cookie-by-name
|
||||
function readCookie () {
|
||||
function readCookie() {
|
||||
const nameEQ = `${COOKIE_NAME}=`;
|
||||
const ca = document.cookie.split(';');
|
||||
for (let i = 0;i < ca.length;i++) {
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) == ' ') { // eslint-disable-line eqeqeq
|
||||
// eslint-disable-next-line eqeqeq
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1, c.length);
|
||||
}
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
@ -18,22 +19,23 @@ function readCookie () {
|
||||
}
|
||||
}
|
||||
|
||||
function writeCookie (userName) {
|
||||
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(userName)}; expires=Thu, 18 Dec 2099 12:00:00 UTC`;
|
||||
function writeCookie(userName) {
|
||||
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(
|
||||
userName
|
||||
)}; expires=Thu, 18 Dec 2099 12:00:00 UTC`;
|
||||
}
|
||||
|
||||
|
||||
function getInitState () {
|
||||
function getInitState() {
|
||||
const userName = decodeURIComponent(readCookie(COOKIE_NAME));
|
||||
const showDialog = !userName;
|
||||
return new $Map({ userName, showDialog });
|
||||
}
|
||||
|
||||
function updateUserName (state, action) {
|
||||
function updateUserName(state, action) {
|
||||
return state.set('userName', action.value);
|
||||
}
|
||||
|
||||
function save (state) {
|
||||
function save(state) {
|
||||
const userName = state.get('userName');
|
||||
if (userName) {
|
||||
writeCookie(userName);
|
||||
|
@ -54,14 +54,17 @@ module.exports = {
|
||||
sourceMap: true,
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
localIdentName: '[name]__[local]___[hash:base64:5]',
|
||||
localIdentName:
|
||||
'[name]__[local]___[hash:base64:5]',
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
// data: '@import "theme/_config.scss";',
|
||||
includePaths: [path.resolve(__dirname, './src')],
|
||||
includePaths: [
|
||||
path.resolve(__dirname, './src'),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -69,7 +72,10 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' }),
|
||||
loader: ExtractTextPlugin.extract({
|
||||
fallback: 'style-loader',
|
||||
use: 'css-loader',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user