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