mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
wip
This commit is contained in:
parent
49ba034cfc
commit
eeb40113c5
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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);
|
||||
|
@ -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>
|
||||
|
||||
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
|
||||
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
86
frontend/src/component/feature/progress.jsx
Normal file
86
frontend/src/component/feature/progress.jsx
Normal 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;
|
17
frontend/src/component/feature/progress.scss
Normal file
17
frontend/src/component/feature/progress.scss
Normal 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;
|
||||
}
|
22
frontend/src/component/feature/view-and-edit.jsx
Normal file
22
frontend/src/component/feature/view-and-edit.jsx
Normal 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;
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
};
|
@ -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>
|
||||
|
||||
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
|
||||
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
|
||||
</section>
|
||||
</form>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -54,7 +54,7 @@ module.exports = {
|
||||
plugins,
|
||||
|
||||
sassLoader: {
|
||||
data: '@import "theme/_config.scss";',
|
||||
// data: '@import "theme/_config.scss";',
|
||||
includePaths: [path.resolve(__dirname, './src')],
|
||||
},
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user