1
0
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:
Simen Bekkhus 2017-08-28 19:15:47 +02:00 committed by GitHub
parent fc8d9a12b3
commit 683ae7e6d8
101 changed files with 2443 additions and 1237 deletions

View File

@ -1,7 +1,8 @@
{
"extends": [
"finn",
"finn/node"
"finn/node",
"finn-prettier"
],
"rules": {
"no-shadow": 0

View File

@ -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",

View File

@ -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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />&nbsp;&nbsp;&nbsp;
{ submitText }
{submitText}
</Button>
&nbsp;
<Button type="cancel" ripple raised onClick={onCancel} style={{ float: 'right' }}>
<Icon name="cancel" />&nbsp;&nbsp;&nbsp;
Cancel
<Button
type="cancel"
ripple
raised
onClick={onCancel}
style={{ float: 'right' }}
>
<Icon name="cancel" />&nbsp;&nbsp;&nbsp; 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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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" />&nbsp;{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>)
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');
}}/> &nbsp;Add parameter
<Parameters
input={input.parameters}
count={input._params}
updateInList={updateInList}
/>
<IconButton
raised
name="add"
title="Add parameter"
onClick={e => {
e.preventDefault();
incValue('_params');
}}
/>{' '}
&nbsp;Add parameter
<br />
<hr />
<FormButtons
submitText={editmode ? 'Update' : 'Create'}
onCancel={onCancel}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}`, {

View File

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

View File

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

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,6 @@
import React from 'react';
import FeatureListContainer from '../../component/feature/list-container';
const render = () => (<FeatureListContainer />);
const render = () => <FeatureListContainer />;
export default render;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React from 'react';
import AddStrategies from '../../component/strategies/add-container';
export default () => (<AddStrategies />);
export default () => <AddStrategies />;

View File

@ -1,4 +1,4 @@
import React from 'react';
import Strategies from '../../component/strategies/list-container';
export default () => (<Strategies />);
export default () => <Strategies />;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { fromJS } from 'immutable';
import { RECEIVE_CLIENT_INSTANCES } from './client-instance-actions';
function getInitState () {
function getInitState() {
return fromJS([]);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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