1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00
This commit is contained in:
sveisvei 2016-12-04 11:56:41 +01:00
parent 49ba034cfc
commit eeb40113c5
29 changed files with 617 additions and 306 deletions

View File

@ -2,12 +2,15 @@
<html>
<head>
<title>Unleash Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/node_modules/react-mdl/extra/material.min.css">
<link rel="stylesheet" href="/static/bundle.css" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
</head>
<body>
<div id='app'></div>
<script src="/node_modules/react-mdl/extra/material.min.js"></script>
<script src="/static/bundle.js"></script>
</body>
</html>

View File

@ -34,12 +34,13 @@
"immutability-helper": "^2.0.0",
"immutable": "^3.8.1",
"normalize.css": "^5.0.0",
"percent": "^2.0.0",
"react": "^15.3.1",
"react-addons-css-transition-group": "^15.3.1",
"react-dom": "^15.3.1",
"react-mdl": "^1.9.0",
"react-redux": "^4.4.5",
"react-router": "^3.0.0",
"react-toolbox": "^1.2.1",
"redux": "^3.6.0",
"redux-thunk": "^2.1.0",
"whatwg-fetch": "^2.0.0"

View File

@ -1,14 +1,18 @@
import React, { Component } from 'react';
import { Layout, Panel, NavDrawer, AppBar } from 'react-toolbox';
import { Layout, Drawer, Header, Navigation, Content,
Footer, FooterSection, FooterDropDownSection, FooterLinkList,
Grid, Cell
} from 'react-mdl';
import style from './styles.scss';
import ErrorContainer from './error/error-container';
import UserContainer from './user/user-container';
import ShowUserContainer from './user/show-user-container';
import Navigation from './nav';
export default class App extends Component {
constructor (props) {
super(props);
this.state = { drawerActive: false };
@ -17,14 +21,102 @@ export default class App extends Component {
this.setState({ drawerActive: !this.state.drawerActive });
};
}
static contextTypes = {
router: React.PropTypes.object,
}
onOverlayClick = () => this.setState({ drawerActive: false });
render () {
const createListItem = (path, caption) =>
<a
href={this.context.router.createHref(path)}
className={this.context.router.isActive(path) ? style.active : ''}>
{caption}
</a>;
return (
<div style={{}}>
<UserContainer />
<Layout fixedHeader>
<Header title={<span><span style={{ color: '#ddd' }}>Unleash Admin / </span><strong>The Title</strong></span>}>
<Navigation>
<a href="https://github.com/Unleash" target="_blank">Github</a>
<ShowUserContainer />
</Navigation>
</Header>
<Drawer title="Unleash Admin">
<Navigation>
{createListItem('/features', 'Feature toggles')}
{createListItem('/strategies', 'Strategies')}
{createListItem('/history', 'Event history')}
{createListItem('/archive', 'Archived toggles')}
<hr />
{createListItem('/metrics', 'Client metrics')}
{createListItem('/client-strategies', 'Client strategies')}
{createListItem('/client-instances', 'Client instances')}
</Navigation>
</Drawer>
<Content>
<Grid noSpacing>
<Cell col={12}>
{this.props.children}
<ErrorContainer />
</Cell>
</Grid>
<Footer size="mega">
<FooterSection type="middle">
<FooterDropDownSection title="Menu">
<FooterLinkList>
{createListItem('/features', 'Feature toggles')}
{createListItem('/strategies', 'Strategies')}
{createListItem('/history', 'Event history')}
{createListItem('/archive', 'Archived toggles')}
</FooterLinkList>
</FooterDropDownSection>
<FooterDropDownSection title="Metrics">
<FooterLinkList>
{createListItem('/metrics', 'Client metrics')}
{createListItem('/client-strategies', 'Client strategies')}
{createListItem('/client-instances', 'Client instances')}
</FooterLinkList>
</FooterDropDownSection>
<FooterDropDownSection title="FAQ">
<FooterLinkList>
<a href="#">Help</a>
<a href="#">Privacy & Terms</a>
<a href="#">Questions</a>
<a href="#">Answers</a>
<a href="#">Contact Us</a>
</FooterLinkList>
</FooterDropDownSection>
<FooterDropDownSection title="Clients">
<FooterLinkList>
<a href="https://github.com/Unleash/unleash-node-client/">Node.js</a>
<a href="https://github.com/Unleash/unleash-java-client/">Java</a>
</FooterLinkList>
</FooterDropDownSection>
</FooterSection>
<FooterSection type="bottom" logo="Unleash Admin">
<FooterLinkList>
<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>
</FooterLinkList>
</FooterSection>
</Footer>
</Content>
</Layout>
</div>
);
return (
<div className={style.container}>
<AppBar title="Unleash Admin" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive} className={style.appBar}>
<ShowUserContainer />
</AppBar>
<div className={style.container} style={{ top: '6.4rem' }}>
<Layout>
@ -33,11 +125,11 @@ export default class App extends Component {
</NavDrawer>
<Panel scrollY>
<div style={{ padding: '1.8rem' }}>
<UserContainer />
{this.props.children}
</div>
</Panel>
<ErrorContainer />
</Layout>
</div>
</div>

View File

@ -1,30 +1,5 @@
import React, { Component } from 'react';
import { List, ListItem, ListSubHeader } from 'react-toolbox/lib/list';
import FontIcon from 'react-toolbox/lib/font_icon';
import Chip from 'react-toolbox/lib/chip';
import Switch from 'react-toolbox/lib/switch';
const ArchivedFeature = ({ feature, revive }) => {
const { name, description, enabled, strategies } = feature;
const actions = [
<div>{strategies && strategies.map(s => <Chip><small>{s.name}</small></Chip>)}</div>,
<FontIcon style={{ cursor: 'pointer' }} value="undo" onClick={() => revive(feature)} />,
];
const leftActions = [
<Switch disabled checked={enabled} />,
];
return (
<ListItem
key={name}
leftActions={leftActions}
rightActions={actions}
caption={name}
legend={(description && description.substring(0, 100)) || '-'}
/>
);
};
import { DataTable, TableHeader, Chip, Switch, IconButton } from 'react-mdl';
class ArchiveList extends Component {
componentDidMount () {
@ -34,12 +9,20 @@ class ArchiveList extends Component {
render () {
const { archive, revive } = this.props;
return (
<List ripple >
<ListSubHeader caption="Archive" />
{archive.length > 0 ?
archive.map((feature, i) => <ArchivedFeature key={i} feature={feature} revive={revive} />) :
<ListItem caption="No archived feature toggles" />}
</List>
<div>
<h6>Toggle Archive</h6>
<DataTable
rows={archive}
style={{ width: '100%' }}>
<TableHeader style={{ width: '25px' }} name="strategies" cellFormatter={(name) => (
<IconButton colored name="undo" onClick={() => revive(name)} />
)}>Revive</TableHeader>
<TableHeader style={{ width: '25px' }} name="enabled" cellFormatter={(v) => (v ? 'Yes' : '-')}>Enabled</TableHeader>
<TableHeader name="name">Toggle name</TableHeader>
<TableHeader numeric name="createdAt">Created</TableHeader>
</DataTable>
</div>
);
}
}

View File

@ -1,13 +1,5 @@
import React, { Component, PropTypes } from 'react';
import Table from 'react-toolbox/lib/table';
const Model = {
appName: { type: String, title: 'Application Name' },
instanceId: { type: String },
clientIp: { type: String },
createdAt: { type: String },
lastSeen: { type: String },
};
import { DataTable, TableHeader } from 'react-mdl';
class ClientStrategies extends Component {
static propTypes () {
@ -25,11 +17,20 @@ class ClientStrategies extends Component {
const source = this.props.clientInstances;
return (
<Table
model={Model}
source={source}
<DataTable
style={{ width: '100%' }}
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>
);
}
}

View File

@ -1,10 +1,5 @@
import React, { Component } from 'react';
import Table from 'react-toolbox/lib/table';
const Model = {
appName: { type: String, title: 'Application Name' },
strategies: { type: String },
};
import { DataTable, TableHeader } from 'react-mdl';
class ClientStrategies extends Component {
@ -13,19 +8,25 @@ class ClientStrategies extends Component {
}
render () {
const source = this.props.clientStrategies.map(item => (
const source = this.props.clientStrategies
// temp hack for ignoring dumb data
.filter(item => item.strategies)
.map(item => (
{
appName: item.appName,
strategies: item.strategies.join(', '),
strategies: item.strategies && item.strategies.join(', '),
})
);
return (
<Table
model={Model}
source={source}
<DataTable
style={{ width: '100%' }}
rows={source}
selectable={false}
/>
>
<TableHeader name="appName">Application name</TableHeader>
<TableHeader name="strategies">Strategies</TableHeader>
</DataTable>
);
}
}

View File

@ -1,6 +1,7 @@
import Snackbar from 'react-toolbox/lib/snackbar';
import React, { PropTypes } from 'react';
import { Snackbar } from 'react-mdl';
class ErrorComponent extends React.Component {
static propTypes () {
return {

View File

@ -1,10 +1,10 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
import FontIcon from 'react-toolbox/lib/font_icon';
import Switch from 'react-toolbox/lib/switch';
import { ListItem } from 'react-toolbox/lib/list';
import Chip from 'react-toolbox/lib/chip';
import { Chip, Switch, Icon, Tooltip, IconButton, ChipContact } from 'react-mdl';
import percentLib from 'percent';
import Progress from './progress';
import style from './feature.scss';
@ -12,42 +12,55 @@ const Feature = ({
feature,
onFeatureClick,
onFeatureRemove,
metricsLastHour = { yes: 0, no: 0, hasData: false },
metricsLastMinute = { yes: 0, no: 0, hasData: false },
settings,
metricsLastHour = { yes: 0, no: 0, isFallback: true },
metricsLastMinute = { yes: 0, no: 0, isFallback: true },
}) => {
const { name, description, enabled, strategies, createdAt } = feature;
const created = new Date(createdAt);
const { name, description, enabled, strategies } = feature;
const actions = [
<div key="strategies">{strategies && strategies.map((s, i) => <Chip key={i}><small>{s.name}</small></Chip>)}</div>,
<div key="created"><small>({created.toLocaleDateString('nb-NO')})</small></div>,
<Link key="change" to={`/features/edit/${name}`} title={`Edit ${name}`}>
<FontIcon value="edit" className={style.action} />
</Link>,
<Link key="history" to={`/history/${name}`} title={`History for ${name}`}>
<FontIcon value="history" className={style.action} />
</Link>,
<FontIcon key="delete" className={style.action} value="delete" onClick={() => onFeatureRemove(name)} />,
];
const leftActions = [
<Chip key="m.hour">
<span className={style.yes}>{metricsLastHour.yes}</span> / <span className={style.no}>{metricsLastHour.no}</span>
</Chip>,
<Chip key="m.min">
<span className={style.yes}>{metricsLastMinute.yes}</span> / <span className={style.no}>{metricsLastMinute.no}</span>
</Chip>,
<Switch key="left-actions" onChange={() => onFeatureClick(feature)} checked={enabled} />,
];
const { showLastHour = false } = settings;
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
const percent = 1 * (showLastHour ?
percentLib.calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) :
percentLib.calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)
);
return (
<ListItem
key={name}
leftActions={leftActions}
rightActions={actions}
caption={name}
legend={(description && description.substring(0, 100)) || '-'}
/>
<li key={name} className="mdl-list__item">
<span className="mdl-list__item-primary-content">
<div style={{ width: '40px', textAlign: 'center' }}>
{
isStale ?
<Icon style={{ width: '25px', marginTop: '4px', fontSize: '25px', color: '#ccc' }} name="report problem" title="No metrics avaiable" /> :
<div>
<Progress strokeWidth={15} percentage={percent} width="50" />
</div>
}
</div>
&nbsp;
<span style={{ display: 'inline-block', width: '45px' }} title={`Toggle ${name}`}>
<Switch title="test" key="left-actions" onChange={() => onFeatureClick(feature)} checked={enabled} />
</span>
<Link to={`/features/edit/${name}`} className={style.link}>
{name} <small>{(description && description.substring(0, 100)) || ''}</small>
</Link>
</span>
<span className={style.iconList} >
{strategies && strategies.map((s, i) => <Chip className={style.iconListItemChip} key={i}>
<small>{s.name}</small>
</Chip>)}
<Link to={`/features/edit/${name}`} title={`Edit ${name}`} className={style.iconListItem}>
<IconButton name="edit" />
</Link>
<Link to={`/history/${name}`} title={`History htmlFor ${name}`} className={style.iconListItem}>
<IconButton name="history" />
</Link>
<IconButton name="delete" onClick={() => onFeatureRemove(name)} className={style.iconListItem} />
</span>
</li>
);
};

View File

@ -1,7 +1,3 @@
.link {
color: #212121;
}
.action {
color: #aaa !important;
cursor: pointer;
@ -14,3 +10,52 @@
.no {
color: red;
}
.link {
color: #212121;
text-decoration: none;
}
.link small {
color: #aaa;
font-weight: 100;
}
.link:hover {
color: #000;
}
.iconList {
display: flex;
}
.iconListItem {
flex: 1;
color: #bbb !important;
}
.iconListItem *:hover {
color: #333;
}
.iconListItemChip {
flex: 1;
margin-left: 5px !important;
}
.topList {
display: flex;
margin: 10px 10px 10px 10px;
}
.topListItem0 {
flex: 1;
flex-grow: 0;
}
.topListItem {
flex: 1;
}
.topListItem2 {
flex: 2;
}

View File

@ -3,7 +3,7 @@ import { hashHistory } from 'react-router';
import { requestUpdateFeatureToggle } from '../../store/feature-actions';
import { createMapper, createActions } from '../input-helpers';
import FormComponent from './form';
import EditAndView from './view-and-edit';
const ID = 'edit-feature-toggle';
function getId (props) {
@ -59,4 +59,4 @@ const actions = createActions({
prepare,
});
export default connect(mapStateToProps, actions)(FormComponent);
export default connect(mapStateToProps, actions)(EditAndView);

View File

@ -1,7 +1,5 @@
import React, { Component, PropTypes } from 'react';
import Input from 'react-toolbox/lib/input';
import Button from 'react-toolbox/lib/button';
import Switch from 'react-toolbox/lib/switch';
import { Textfield, Button, Switch } from 'react-mdl';
import StrategiesSection from './strategies-section-container';
const trim = (value) => {
@ -45,29 +43,31 @@ class AddFeatureToggleComponent extends Component {
return (
<form onSubmit={onSubmit(input)}>
<section>
<Input
type="text"
<Textfield
label="Name"
name="name"
disabled={editmode}
required
value={name}
error={nameError}
onBlur={(v) => validateName(v)}
onChange={(v) => setValue('name', trim(v))} />
<Input
type="text"
multiline label="Description"
onBlur={(v) => validateName(v.target.value)}
onChange={(v) => setValue('name', trim(v.target.value))} />
<br />
<Textfield
rows={2}
label="Description"
required
value={description}
onChange={(v) => setValue('description', v)} />
onChange={(v) => setValue('description', v.target.value)} />
<br />
<Switch
checked={enabled}
label="Enabled"
onChange={(v) => setValue('enabled', v)} />
onChange={(v) => {
// todo is wrong way to get value?
setValue('enabled', (console.log(v.target) && v.target.value === 'on'));
}}>Enabled</Switch>
<br />
</section>
@ -78,12 +78,9 @@ class AddFeatureToggleComponent extends Component {
removeStrategy={removeStrategy} />
<br />
<hr />
<Button type="submit" raised primary label={editmode ? 'Update' : 'Create'} />
<Button type="submit" raised primary>{editmode ? 'Update' : 'Create'}</Button>
&nbsp;
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
</form>
);
}

View File

@ -1,6 +1,7 @@
import React, { PropTypes } from 'react';
import Dropdown from 'react-toolbox/lib/dropdown';
import FontIcon from 'react-toolbox/lib/font_icon';
// import Dropdown from 'react-toolbox/lib/dropdown';
// TODO use menu
import { Icon } from 'react-mdl';
class AddStrategy extends React.Component {
@ -41,7 +42,7 @@ class AddStrategy extends React.Component {
return (
<div style={containerStyle}>
<FontIcon value="add" />
<Icon value="add" />
<div style={contentStyle}>
<strong>{item.name}</strong>
<small>{item.description}</small>
@ -56,9 +57,9 @@ class AddStrategy extends React.Component {
return s;
});
return (
<div style={{ maxWidth: '400px', marginTop: '20px' }}>
<Dropdown
/*
<Dropdown
allowBlank={false}
auto
source={strats}
@ -66,6 +67,12 @@ class AddStrategy extends React.Component {
label="Click to add activation strategy"
template={this.customItem}
/>
*/
return (
<div style={{ maxWidth: '400px', marginTop: '20px' }}>
</div>
);
}

View File

@ -1,6 +1,5 @@
import React, { PropTypes } from 'react';
import Input from 'react-toolbox/lib/input';
import Button from 'react-toolbox/lib/button';
import { Textfield, Button } from 'react-mdl';
class StrategyConfigure extends React.Component {
@ -30,7 +29,7 @@ class StrategyConfigure extends React.Component {
renderInputFields (strategyDefinition) {
if (strategyDefinition.parametersTemplate) {
return Object.keys(strategyDefinition.parametersTemplate).map(field => (
<Input
<Textfield
type="text"
key={field}
name={field}

View File

@ -1,7 +1,9 @@
import React, { PropTypes } from 'react';
import Feature from './feature-component';
import { Link } from 'react-router';
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
import { Icon, Chip, ChipContact, IconButton, Button, FABButton, Textfield, Menu, MenuItem } from 'react-mdl';
import styles from './feature.scss';
export default class FeatureListComponent extends React.PureComponent {
@ -32,25 +34,91 @@ export default class FeatureListComponent extends React.PureComponent {
clearInterval(this.timer);
}
toggleMetrics () {
this.props.updateSetting('showLastHour', !this.props.settings.showLastHour);
}
setFilter (v) {
this.props.updateSetting('filter', typeof v === 'string' ? v.trim() : '');
}
setSort (v) {
this.props.updateSetting('sort', typeof v === 'string' ? v.trim() : '');
}
render () {
const { features, onFeatureClick, onFeatureRemove, featureMetrics } = this.props;
const { features, onFeatureClick, onFeatureRemove, featureMetrics, settings } = this.props;
return (
<List>
<ListSubHeader caption="Feature toggles" />
<div>
<div className={styles.topList}>
<Chip onClick={() => this.toggleMetrics()} className={styles.topListItem0}>
{ settings.showLastHour &&
<ChipContact className="mdl-color--teal mdl-color-text--white">
<Icon name="hourglass_full" style={{ fontSize: '16px' }} />
</ChipContact> }
{ '1 hour' }
</Chip>
&nbsp;
<Chip onClick={() => this.toggleMetrics()} className={styles.topListItem0}>
{ !settings.showLastHour &&
<ChipContact className="mdl-color--teal mdl-color-text--white">
<Icon name="hourglass_empty" style={{ fontSize: '16px' }} />
</ChipContact> }
{ '1 minute' }
</Chip>
<div className={styles.topListItem2} style={{ margin: '-10px 10px 0 10px' }}>
<Textfield
floatingLabel
value={settings.filter}
onChange={(e) => { this.setFilter(e.target.value); }}
label="Filter toggles"
style={{ width: '100%' }}
/>
</div>
<div style={{ position: 'relative' }} className={styles.topListItem0}>
<IconButton name="sort" id="demo-menu-top-right" colored title="Sort" />
<Menu target="demo-menu-top-right" valign="bottom" align="right" ripple onClick={
(e) => this.setSort(e.target.getAttribute('data-target'))}>
<MenuItem disabled>Filter by:</MenuItem>
<MenuItem disabled={!settings.sort || settings.sort === 'nosort'} data-target="nosort">Default</MenuItem>
<MenuItem disabled={settings.sort === 'name'} data-target="name">Name</MenuItem>
<MenuItem disabled={settings.sort === 'enabled'} data-target="enabled">Enabled</MenuItem>
<MenuItem disabled={settings.sort === 'appName'} data-target="appName">Application name</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>
</div>
<Link to="/features/create" className={styles.topListItem0}>
<FABButton ripple component="span" mini>
<Icon name="add" />
</FABButton>
</Link>
</div>
<ul className="demo-list-item mdl-list">
{features.map((feature, i) =>
<Feature key={i}
settings={settings}
metricsLastHour={featureMetrics.lastHour[feature.name]}
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
feature={feature}
onFeatureClick={onFeatureClick}
onFeatureRemove={onFeatureRemove}/>
)}
<ListDivider />
</ul>
<hr />
<Link to="/features/create">
<ListItem caption="Create" legend="new feature toggle" leftIcon="add" />
<Icon name="add" />
<strong>Create </strong><small>new feature toggle</small>
</Link>
</List>
</div>
);
}
}

View File

@ -1,19 +1,79 @@
import { connect } from 'react-redux';
import { toggleFeature, fetchFeatureToggles, removeFeatureToggle } 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) => ({
features: state.features.toJS(),
featureMetrics: state.featureMetrics.toJS(),
});
const mapStateToProps = (state) => {
const featureMetrics = state.featureMetrics.toJS();
const settings = state.settings.toJS().feature || {};
let features = state.features.toJS();
if (settings.filter) {
features = features.filter(feature =>
(
feature.name.indexOf(settings.filter) > -1 ||
feature.description.indexOf(settings.filter) > -1 ||
feature.strategies.some(s => s && s.name && s.name.indexOf(settings.filter) > -1)
)
);
}
if (settings.sort) {
if (settings.sort === 'enabled') {
features = features.sort((a, b) => (
// eslint-disable-next-line
a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1
));
} else if (settings.sort === 'appName') {
// AppName
// features = features.sort((a, b) => {
// if (a.appName < b.appName) { return -1; }
// if (a.appName > b.appName) { return 1; }
// return 0;
// });
} else if (settings.sort === 'created') {
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; }
return 0;
});
} else if (settings.sort === 'strategies') {
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;
features = features.sort((a, b) => {
if (!target[a.name]) { return 1; }
if (!target[b.name]) { return -1; }
if (target[a.name].yes > target[b.name].yes) {
return -1;
}
return 1;
});
}
}
return {
features,
featureMetrics,
settings,
};
};
const mapDispatchToProps = {
onFeatureClick: toggleFeature,
onFeatureRemove: removeFeatureToggle,
fetchFeatureToggles,
fetchFeatureMetrics,
updateSetting: updateSettingForGroup('feature'),
};
const FeatureListContainer = connect(

View File

@ -0,0 +1,86 @@
import React, { PropTypes, Component } from 'react';
import styles from './progress.scss';
class Progress extends Component {
constructor (props) {
super(props);
this.state = {
percentage: props.initialAnimation ? 0 : props.percentage,
};
}
componentDidMount () {
if (this.props.initialAnimation) {
this.initialTimeout = setTimeout(() => {
this.requestAnimationFrame = window.requestAnimationFrame(() => {
this.setState({
percentage: this.props.percentage,
});
});
}, 0);
}
}
componentWillReceiveProps ({ percentage }) {
this.setState({ percentage });
}
componentWillUnmount () {
clearTimeout(this.initialTimeout);
window.cancelAnimationFrame(this.requestAnimationFrame);
}
render () {
const { strokeWidth, percentage } = this.props;
const radius = (50 - strokeWidth / 2);
const pathDescription = `
M 50,50 m 0,-${radius}
a ${radius},${radius} 0 1 1 0,${2 * radius}
a ${radius},${radius} 0 1 1 0,-${2 * radius}
`;
const diameter = Math.PI * 2 * radius;
const progressStyle = {
strokeDasharray: `${diameter}px ${diameter}px`,
strokeDashoffset: `${((100 - this.state.percentage) / 100 * diameter)}px`,
};
return (<svg viewBox="0 0 100 100">
<path
className={styles.trail}
d={pathDescription}
strokeWidth={strokeWidth}
fillOpacity={0}
/>
<path
className={styles.path}
d={pathDescription}
strokeWidth={strokeWidth}
fillOpacity={0}
style={progressStyle}
/>
<text
className={styles.text}
x={50}
y={50}
>{percentage}%</text>
</svg>);
}
}
Progress.propTypes = {
percentage: PropTypes.number.isRequired,
strokeWidth: PropTypes.number,
initialAnimation: PropTypes.bool,
textForPercentage: PropTypes.func,
};
Progress.defaultProps = {
strokeWidth: 8,
initialAnimation: false,
};
export default Progress;

View File

@ -0,0 +1,17 @@
.path {
stroke: #3f51b5;
stroke-linecap: round;
transition: stroke-dashoffset 5s ease 0s;
}
.trail {
stroke: #d6d6d6;
}
.text {
fill: rgba(0, 0, 0, 0.7);
font-size: 25px;
line-height: 25px;
dominant-baseline: middle;
text-anchor: middle;
}

View File

@ -0,0 +1,22 @@
import React, { PropTypes } from 'react';
import FormComponent from './form';
const Render = (props) => {
return (
<div>
<h1>{props.featureToggle.name}</h1>
<p>add metrics</p>
<p>add apps</p>
<p>add instances</p>
<hr />
<h5>Edit</h5>
<FormComponent {...props} />
</div>
);
};
export default Render;

View File

@ -1,6 +1,5 @@
import React, { PropTypes, PureComponent } from 'react';
import FontIcon from 'react-toolbox/lib/font_icon';
import { Icon } from 'react-mdl';
import style from './history.scss';
@ -110,7 +109,7 @@ class HistoryItem extends PureComponent {
<dd>{id}</dd>
<dt>Type:</dt>
<dd>
<FontIcon value={icon} title={type} style={{ fontSize: '1.6rem' }} />
<Icon name={icon} title={type} style={{ fontSize: '1.6rem' }} />
<span> {type}</span>
</dd>
<dt>Timestamp:</dt>

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import HistoryItemDiff from './history-item-diff';
import HistoryItemJson from './history-item-json';
import Switch from 'react-toolbox/lib/switch';
import { Switch } from 'react-mdl';
import style from './history.scss';
@ -28,11 +28,7 @@ class HistoryList extends Component {
return (
<div className={style.history}>
<Switch
checked={showData}
label="Show full events"
onChange={this.toggleShowDiff.bind(this)}
/>
<Switch checked={showData} onChange={this.toggleShowDiff.bind(this)}>Show full events</Switch>
{entries}
</div>
);

View File

@ -1,6 +1,5 @@
import React, { Component } from 'react';
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
import Chip from 'react-toolbox/lib/chip';
import { DataTable, TableHeader } from 'react-mdl';
class Metrics extends Component {
@ -12,17 +11,22 @@ class Metrics extends Component {
const { globalCount, clientList } = this.props;
return (
<List>
<ListSubHeader caption={`Total of ${globalCount} toggles`} />
<ListDivider />
{clientList.map(({ name, count, ping, appName }, i) =>
<ListItem
leftActions={[<Chip>{count}</Chip>]}
key={name + i}
caption={appName}
legend={`${name} pinged ${ping}`} />
)}
</List>
<div>
<h4>{`Total of ${globalCount} toggles`}</h4>
<DataTable
style={{ width: '100%' }}
rows={clientList}
selectable={false}
>
<TableHeader name="name">Instance</TableHeader>
<TableHeader name="appName">Application name</TableHeader>
<TableHeader numeric name="ping" cellFormatter={
(v) => (v.toString())
}>Last seen</TableHeader>
<TableHeader numeric name="count">Counted</TableHeader>
</DataTable>
</div>
);
}
}

View File

@ -1,44 +0,0 @@
import React, { Component } from 'react';
import { ListSubHeader, List, ListItem, ListDivider } from 'react-toolbox';
import style from './styles.scss';
export default class UnleashNav extends Component {
static contextTypes = {
router: React.PropTypes.object,
}
render () {
const createListItem = (path, caption) =>
<ListItem to={this.context.router.createHref(path)} caption={caption}
className={this.context.router.isActive(path) ? style.active : ''} />;
return (
<List selectable ripple className={style.navigation}>
{createListItem('/features', 'Feature toggles')}
{createListItem('/strategies', 'Strategies')}
{createListItem('/history', 'Event history')}
{createListItem('/archive', 'Archived toggles')}
<ListDivider />
<ListSubHeader caption="Clients" />
{createListItem('/applications', 'Client applications')}
{createListItem('/client-strategies', 'Client strategies')}
<ListDivider />
<ListSubHeader caption="Resources" />
{createListItem('/docs', 'Documentation')}
<a href="https://github.com/Unleash/unleash/" target="_blank">
<ListItem caption="GitHub" />
</a>
<ListDivider />
<ListItem selectable={false} ripple={false}>
<p>A product by <a href="https://finn.no" target="_blank">FINN.no</a></p>
</ListItem>
</List>
);
}
};

View File

@ -1,7 +1,6 @@
import React, { PropTypes } from 'react';
import Input from 'react-toolbox/lib/input';
import Button from 'react-toolbox/lib/button';
import { Textfield, Button, IconButton } from 'react-mdl';
const trim = (value) => {
if (value && value.trim) {
@ -19,11 +18,10 @@ export const PARAM_PREFIX = 'param_';
const genParams = (input, num = 0, setValue) => (<div>{gerArrayWithEntries(num).map((v, i) => {
const key = `${PARAM_PREFIX}${i + 1}`;
return (
<Input
type="text"
<Textfield
label={`Parameter name ${i + 1}`}
name={key} key={key}
onChange={(value) => setValue(key, value)}
onChange={({ target }) => setValue(key, target.value)}
value={input[key]} />
);
})}</div>);
@ -38,22 +36,25 @@ const AddStrategy = ({
}) => (
<form onSubmit={onSubmit(input)}>
<section>
<Input type="text" label="Strategy name"
<Textfield label="Strategy name"
name="name" required
pattern="^[0-9a-zA-Z\.\-]+$"
onChange={(value) => setValue('name', trim(value))}
onChange={({ target }) => setValue('name', trim(target.value))}
value={input.name}
/>
<Input type="text" multiline label="Description"
<br />
<Textfield
rows={2}
label="Description"
name="description"
onChange={(value) => setValue('description', value)}
onChange={({ target }) => setValue('description', target.value)}
value={input.description}
/>
</section>
<section>
{genParams(input, input._params, setValue)}
<Button icon="add" accent label="Add parameter" onClick={(e) => {
<IconButton name="add" title="Add parameter" onClick={(e) => {
e.preventDefault();
incValue('_params');
}}/>
@ -63,9 +64,9 @@ const AddStrategy = ({
<hr />
<section>
<Button type="submit" raised primary label="Create" />
<Button type="submit" raised primary >Create</Button>
&nbsp;
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
</section>
</form>
);

View File

@ -1,7 +1,6 @@
import React, { Component } from 'react';
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
import FontIcon from 'react-toolbox/lib/font_icon';
import Chip from 'react-toolbox/lib/chip';
import { List, ListItem, ListItemContent, Icon, IconButton, Chip } from 'react-mdl';
import style from './strategies.scss';
@ -25,27 +24,24 @@ class StrategiesListComponent extends Component {
const { strategies, removeStrategy } = this.props;
return (
<List ripple >
<ListSubHeader caption="Strategies" />
<div>
<h5>Strategies</h5>
<IconButton name="add" onClick={() => this.context.router.push('/strategies/create')} title="Add new strategy"/>
<hr />
<List>
{strategies.length > 0 ? strategies.map((strategy, i) => {
const actions = this.getParameterMap(strategy).concat([
<button className={style['non-style-button']} key="1" onClick={() => removeStrategy(strategy)}>
<FontIcon value="delete" />
</button>,
]);
return (
<ListItem key={i} rightActions={actions}
caption={strategy.name}
legend={strategy.description} />
<ListItem key={i}>
<ListItemContent><strong>{strategy.name}</strong> {strategy.description}</ListItemContent>
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
</ListItem>
);
}) : <ListItem caption="No entries" />}
<ListDivider />
<ListItem
onClick={() => this.context.router.push('/strategies/create')}
caption="Add" legend="new strategy" leftIcon="add" />
}) : <ListItem>No entries</ListItem>}
</List>
</div>
);
}
}

View File

@ -18,7 +18,7 @@ export default class ShowUserComponent extends React.Component {
<div style={{ textAlign: 'right' }}>
<p>
You are logged in as:
<strong> <a href="#edit-user" onClick={this.openEdit}>{this.props.user.userName}</a></strong>
<strong> <a href="#edit-user" onClick={this.openEdit}>{this.props.user.userName || 'unknown'}</a></strong>
</p>
</div>
);

View File

@ -1,6 +1,5 @@
import React, { PropTypes } from 'react';
import Dialog from 'react-toolbox/lib/dialog';
import Input from 'react-toolbox/lib/input';
import { Textfield, Dialog, DialogTitle, DialogContent, DialogActions, Button } from 'react-mdl';
class EditUserComponent extends React.Component {
static propTypes () {
@ -16,31 +15,29 @@ class EditUserComponent extends React.Component {
}
render () {
const actions = [
{ label: 'Save', onClick: this.props.save },
];
return (
<Dialog
active={this.props.user.showDialog}
title="Action required"
actions={actions}
>
<p>
You hav to specify a username to use Unleash. This will allow us to track changes.
</p>
<form onSubmit={this.handleSubmit}>
<Input
type="text"
label="USERNAME"
name="username"
required
value={this.props.user.userName}
onChange={(v) => this.props.updateUserName(v)}
/>
</form>
</Dialog>
<div>
<Dialog open={this.props.user.showDialog}>
<DialogTitle>Action required</DialogTitle>
<DialogContent>
<p>
You are logged in as:You hav to specify a username to use Unleash. This will allow us to track changes.
</p>
<form onSubmit={this.handleSubmit}>
<Textfield
label="USERNAME"
name="username"
required
value={this.props.user.userName}
onChange={(e) => this.props.updateUserName(e.target.value)}
/>
</form>
</DialogContent>
<DialogActions>
<Button onClick={this.props.save}>Save</Button>
</DialogActions>
</Dialog>
</div>
);
}
}

View File

@ -10,10 +10,7 @@ export default class Features extends Component {
render () {
return (
<div>
<h6>Edit feature toggle</h6>
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
</div>
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
);
}
};

View File

@ -1,31 +0,0 @@
@import "~react-toolbox/lib/colors";
@import "~react-toolbox/lib/globals";
@import "~react-toolbox/lib/mixins";
@import "~react-toolbox/lib/commons";
$color-primary:$palette-blue-400;
$color-primary-dark: $palette-blue-700;
$navigation-drawer-desktop-width: 4 * $standard-increment-desktop !default;
$navigation-drawer-max-desktop-width: 70 * $unit !default;
// Mobile:
// Width = Screen width 56 dp
// Maximum width: 320dp
$navigation-drawer-mobile-width: 5 * $standard-increment-mobile !default;
// sass doesn't like use of variable here: calc(100% - $standard-increment-mobile);
$navigation-drawer-max-mobile-width: calc(100% - 5.6rem) !default;
.appBar {
.leftIcon {
transition-timing-function: $animation-curve-default;
transition-duration: $animation-duration;
transition-property: width, min-width;
}
@media screen and (min-width: $layout-breakpoint-sm) {
.leftIcon {
display: none;
}
}
}

View File

@ -54,7 +54,7 @@ module.exports = {
plugins,
sassLoader: {
data: '@import "theme/_config.scss";',
// data: '@import "theme/_config.scss";',
includePaths: [path.resolve(__dirname, './src')],
},