mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-23 01:16:27 +02:00
wip
This commit is contained in:
parent
49ba034cfc
commit
eeb40113c5
@ -2,12 +2,15 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Unleash Admin</title>
|
<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 rel="stylesheet" href="/static/bundle.css" />
|
||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<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">
|
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id='app'></div>
|
<div id='app'></div>
|
||||||
|
<script src="/node_modules/react-mdl/extra/material.min.js"></script>
|
||||||
<script src="/static/bundle.js"></script>
|
<script src="/static/bundle.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -34,12 +34,13 @@
|
|||||||
"immutability-helper": "^2.0.0",
|
"immutability-helper": "^2.0.0",
|
||||||
"immutable": "^3.8.1",
|
"immutable": "^3.8.1",
|
||||||
"normalize.css": "^5.0.0",
|
"normalize.css": "^5.0.0",
|
||||||
|
"percent": "^2.0.0",
|
||||||
"react": "^15.3.1",
|
"react": "^15.3.1",
|
||||||
"react-addons-css-transition-group": "^15.3.1",
|
"react-addons-css-transition-group": "^15.3.1",
|
||||||
"react-dom": "^15.3.1",
|
"react-dom": "^15.3.1",
|
||||||
|
"react-mdl": "^1.9.0",
|
||||||
"react-redux": "^4.4.5",
|
"react-redux": "^4.4.5",
|
||||||
"react-router": "^3.0.0",
|
"react-router": "^3.0.0",
|
||||||
"react-toolbox": "^1.2.1",
|
|
||||||
"redux": "^3.6.0",
|
"redux": "^3.6.0",
|
||||||
"redux-thunk": "^2.1.0",
|
"redux-thunk": "^2.1.0",
|
||||||
"whatwg-fetch": "^2.0.0"
|
"whatwg-fetch": "^2.0.0"
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import React, { Component } from 'react';
|
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 style from './styles.scss';
|
||||||
import ErrorContainer from './error/error-container';
|
import ErrorContainer from './error/error-container';
|
||||||
|
|
||||||
import UserContainer from './user/user-container';
|
import UserContainer from './user/user-container';
|
||||||
import ShowUserContainer from './user/show-user-container';
|
import ShowUserContainer from './user/show-user-container';
|
||||||
|
|
||||||
import Navigation from './nav';
|
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
|
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { drawerActive: false };
|
this.state = { drawerActive: false };
|
||||||
@ -17,14 +21,102 @@ export default class App extends Component {
|
|||||||
this.setState({ drawerActive: !this.state.drawerActive });
|
this.setState({ drawerActive: !this.state.drawerActive });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
static contextTypes = {
|
||||||
|
router: React.PropTypes.object,
|
||||||
|
}
|
||||||
|
|
||||||
onOverlayClick = () => this.setState({ drawerActive: false });
|
onOverlayClick = () => this.setState({ drawerActive: false });
|
||||||
|
|
||||||
render () {
|
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 (
|
return (
|
||||||
<div className={style.container}>
|
<div className={style.container}>
|
||||||
<AppBar title="Unleash Admin" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive} className={style.appBar}>
|
<AppBar title="Unleash Admin" leftIcon="menu" onLeftIconClick={this.toggleDrawerActive} className={style.appBar}>
|
||||||
<ShowUserContainer />
|
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<div className={style.container} style={{ top: '6.4rem' }}>
|
<div className={style.container} style={{ top: '6.4rem' }}>
|
||||||
<Layout>
|
<Layout>
|
||||||
@ -33,11 +125,11 @@ export default class App extends Component {
|
|||||||
</NavDrawer>
|
</NavDrawer>
|
||||||
<Panel scrollY>
|
<Panel scrollY>
|
||||||
<div style={{ padding: '1.8rem' }}>
|
<div style={{ padding: '1.8rem' }}>
|
||||||
<UserContainer />
|
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
<ErrorContainer />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,30 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { List, ListItem, ListSubHeader } from 'react-toolbox/lib/list';
|
import { DataTable, TableHeader, Chip, Switch, IconButton } from 'react-mdl';
|
||||||
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)) || '-'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
class ArchiveList extends Component {
|
class ArchiveList extends Component {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
@ -34,12 +9,20 @@ class ArchiveList extends Component {
|
|||||||
render () {
|
render () {
|
||||||
const { archive, revive } = this.props;
|
const { archive, revive } = this.props;
|
||||||
return (
|
return (
|
||||||
<List ripple >
|
<div>
|
||||||
<ListSubHeader caption="Archive" />
|
<h6>Toggle Archive</h6>
|
||||||
{archive.length > 0 ?
|
|
||||||
archive.map((feature, i) => <ArchivedFeature key={i} feature={feature} revive={revive} />) :
|
<DataTable
|
||||||
<ListItem caption="No archived feature toggles" />}
|
rows={archive}
|
||||||
</List>
|
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 React, { Component, PropTypes } from 'react';
|
||||||
import Table from 'react-toolbox/lib/table';
|
import { DataTable, TableHeader } from 'react-mdl';
|
||||||
|
|
||||||
const Model = {
|
|
||||||
appName: { type: String, title: 'Application Name' },
|
|
||||||
instanceId: { type: String },
|
|
||||||
clientIp: { type: String },
|
|
||||||
createdAt: { type: String },
|
|
||||||
lastSeen: { type: String },
|
|
||||||
};
|
|
||||||
|
|
||||||
class ClientStrategies extends Component {
|
class ClientStrategies extends Component {
|
||||||
static propTypes () {
|
static propTypes () {
|
||||||
@ -25,11 +17,20 @@ class ClientStrategies extends Component {
|
|||||||
const source = this.props.clientInstances;
|
const source = this.props.clientInstances;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<DataTable
|
||||||
model={Model}
|
style={{ width: '100%' }}
|
||||||
source={source}
|
rows={source}
|
||||||
selectable={false}
|
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 React, { Component } from 'react';
|
||||||
import Table from 'react-toolbox/lib/table';
|
import { DataTable, TableHeader } from 'react-mdl';
|
||||||
|
|
||||||
const Model = {
|
|
||||||
appName: { type: String, title: 'Application Name' },
|
|
||||||
strategies: { type: String },
|
|
||||||
};
|
|
||||||
|
|
||||||
class ClientStrategies extends Component {
|
class ClientStrategies extends Component {
|
||||||
|
|
||||||
@ -13,19 +8,25 @@ class ClientStrategies extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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,
|
appName: item.appName,
|
||||||
strategies: item.strategies.join(', '),
|
strategies: item.strategies && item.strategies.join(', '),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<DataTable
|
||||||
model={Model}
|
style={{ width: '100%' }}
|
||||||
source={source}
|
rows={source}
|
||||||
selectable={false}
|
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 React, { PropTypes } from 'react';
|
||||||
|
|
||||||
|
import { Snackbar } from 'react-mdl';
|
||||||
|
|
||||||
class ErrorComponent extends React.Component {
|
class ErrorComponent extends React.Component {
|
||||||
static propTypes () {
|
static propTypes () {
|
||||||
return {
|
return {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
import { Chip, Switch, Icon, Tooltip, IconButton, ChipContact } from 'react-mdl';
|
||||||
import Switch from 'react-toolbox/lib/switch';
|
import percentLib from 'percent';
|
||||||
import { ListItem } from 'react-toolbox/lib/list';
|
import Progress from './progress';
|
||||||
import Chip from 'react-toolbox/lib/chip';
|
|
||||||
|
|
||||||
|
|
||||||
import style from './feature.scss';
|
import style from './feature.scss';
|
||||||
|
|
||||||
@ -12,42 +12,55 @@ const Feature = ({
|
|||||||
feature,
|
feature,
|
||||||
onFeatureClick,
|
onFeatureClick,
|
||||||
onFeatureRemove,
|
onFeatureRemove,
|
||||||
metricsLastHour = { yes: 0, no: 0, hasData: false },
|
settings,
|
||||||
metricsLastMinute = { yes: 0, no: 0, hasData: false },
|
metricsLastHour = { yes: 0, no: 0, isFallback: true },
|
||||||
|
metricsLastMinute = { yes: 0, no: 0, isFallback: true },
|
||||||
}) => {
|
}) => {
|
||||||
const { name, description, enabled, strategies, createdAt } = feature;
|
const { name, description, enabled, strategies } = feature;
|
||||||
const created = new Date(createdAt);
|
|
||||||
|
|
||||||
const actions = [
|
const { showLastHour = false } = settings;
|
||||||
<div key="strategies">{strategies && strategies.map((s, i) => <Chip key={i}><small>{s.name}</small></Chip>)}</div>,
|
const isStale = showLastHour ? metricsLastHour.isFallback : metricsLastMinute.isFallback;
|
||||||
<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 percent = 1 * (showLastHour ?
|
||||||
|
percentLib.calc(metricsLastHour.yes, metricsLastHour.yes + metricsLastHour.no, 0) :
|
||||||
|
percentLib.calc(metricsLastMinute.yes, metricsLastMinute.yes + metricsLastMinute.no, 0)
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<li key={name} className="mdl-list__item">
|
||||||
key={name}
|
<span className="mdl-list__item-primary-content">
|
||||||
leftActions={leftActions}
|
<div style={{ width: '40px', textAlign: 'center' }}>
|
||||||
rightActions={actions}
|
{
|
||||||
caption={name}
|
isStale ?
|
||||||
legend={(description && description.substring(0, 100)) || '-'}
|
<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 {
|
.action {
|
||||||
color: #aaa !important;
|
color: #aaa !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -14,3 +10,52 @@
|
|||||||
.no {
|
.no {
|
||||||
color: red;
|
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 { requestUpdateFeatureToggle } from '../../store/feature-actions';
|
||||||
import { createMapper, createActions } from '../input-helpers';
|
import { createMapper, createActions } from '../input-helpers';
|
||||||
import FormComponent from './form';
|
import EditAndView from './view-and-edit';
|
||||||
|
|
||||||
const ID = 'edit-feature-toggle';
|
const ID = 'edit-feature-toggle';
|
||||||
function getId (props) {
|
function getId (props) {
|
||||||
@ -59,4 +59,4 @@ const actions = createActions({
|
|||||||
prepare,
|
prepare,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, actions)(FormComponent);
|
export default connect(mapStateToProps, actions)(EditAndView);
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import Input from 'react-toolbox/lib/input';
|
import { Textfield, Button, Switch } from 'react-mdl';
|
||||||
import Button from 'react-toolbox/lib/button';
|
|
||||||
import Switch from 'react-toolbox/lib/switch';
|
|
||||||
import StrategiesSection from './strategies-section-container';
|
import StrategiesSection from './strategies-section-container';
|
||||||
|
|
||||||
const trim = (value) => {
|
const trim = (value) => {
|
||||||
@ -45,29 +43,31 @@ class AddFeatureToggleComponent extends Component {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit(input)}>
|
<form onSubmit={onSubmit(input)}>
|
||||||
<section>
|
<section>
|
||||||
<Input
|
<Textfield
|
||||||
type="text"
|
|
||||||
label="Name"
|
label="Name"
|
||||||
name="name"
|
name="name"
|
||||||
disabled={editmode}
|
disabled={editmode}
|
||||||
required
|
required
|
||||||
value={name}
|
value={name}
|
||||||
error={nameError}
|
error={nameError}
|
||||||
onBlur={(v) => validateName(v)}
|
onBlur={(v) => validateName(v.target.value)}
|
||||||
onChange={(v) => setValue('name', trim(v))} />
|
onChange={(v) => setValue('name', trim(v.target.value))} />
|
||||||
<Input
|
<br />
|
||||||
type="text"
|
<Textfield
|
||||||
multiline label="Description"
|
rows={2}
|
||||||
|
label="Description"
|
||||||
required
|
required
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(v) => setValue('description', v)} />
|
onChange={(v) => setValue('description', v.target.value)} />
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
checked={enabled}
|
checked={enabled}
|
||||||
label="Enabled"
|
onChange={(v) => {
|
||||||
onChange={(v) => setValue('enabled', v)} />
|
// todo is wrong way to get value?
|
||||||
|
setValue('enabled', (console.log(v.target) && v.target.value === 'on'));
|
||||||
|
}}>Enabled</Switch>
|
||||||
<br />
|
<br />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -78,12 +78,9 @@ class AddFeatureToggleComponent extends Component {
|
|||||||
removeStrategy={removeStrategy} />
|
removeStrategy={removeStrategy} />
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
<Button type="submit" raised primary>{editmode ? 'Update' : 'Create'}</Button>
|
||||||
<hr />
|
|
||||||
|
|
||||||
<Button type="submit" raised primary label={editmode ? 'Update' : 'Create'} />
|
|
||||||
|
|
||||||
<Button type="cancel" raised label="Cancel" onClick={onCancel} />
|
<Button type="cancel" raised onClick={onCancel}>Cancel</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import Dropdown from 'react-toolbox/lib/dropdown';
|
// import Dropdown from 'react-toolbox/lib/dropdown';
|
||||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
// TODO use menu
|
||||||
|
import { Icon } from 'react-mdl';
|
||||||
|
|
||||||
class AddStrategy extends React.Component {
|
class AddStrategy extends React.Component {
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ class AddStrategy extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
<FontIcon value="add" />
|
<Icon value="add" />
|
||||||
<div style={contentStyle}>
|
<div style={contentStyle}>
|
||||||
<strong>{item.name}</strong>
|
<strong>{item.name}</strong>
|
||||||
<small>{item.description}</small>
|
<small>{item.description}</small>
|
||||||
@ -56,8 +57,8 @@ class AddStrategy extends React.Component {
|
|||||||
return s;
|
return s;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
/*
|
||||||
<div style={{ maxWidth: '400px', marginTop: '20px' }}>
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
allowBlank={false}
|
allowBlank={false}
|
||||||
auto
|
auto
|
||||||
@ -66,6 +67,12 @@ class AddStrategy extends React.Component {
|
|||||||
label="Click to add activation strategy"
|
label="Click to add activation strategy"
|
||||||
template={this.customItem}
|
template={this.customItem}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '400px', marginTop: '20px' }}>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import Input from 'react-toolbox/lib/input';
|
import { Textfield, Button } from 'react-mdl';
|
||||||
import Button from 'react-toolbox/lib/button';
|
|
||||||
|
|
||||||
class StrategyConfigure extends React.Component {
|
class StrategyConfigure extends React.Component {
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ class StrategyConfigure extends React.Component {
|
|||||||
renderInputFields (strategyDefinition) {
|
renderInputFields (strategyDefinition) {
|
||||||
if (strategyDefinition.parametersTemplate) {
|
if (strategyDefinition.parametersTemplate) {
|
||||||
return Object.keys(strategyDefinition.parametersTemplate).map(field => (
|
return Object.keys(strategyDefinition.parametersTemplate).map(field => (
|
||||||
<Input
|
<Textfield
|
||||||
type="text"
|
type="text"
|
||||||
key={field}
|
key={field}
|
||||||
name={field}
|
name={field}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import Feature from './feature-component';
|
import Feature from './feature-component';
|
||||||
import { Link } from 'react-router';
|
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 {
|
export default class FeatureListComponent extends React.PureComponent {
|
||||||
|
|
||||||
@ -32,25 +34,91 @@ export default class FeatureListComponent extends React.PureComponent {
|
|||||||
clearInterval(this.timer);
|
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 () {
|
render () {
|
||||||
const { features, onFeatureClick, onFeatureRemove, featureMetrics } = this.props;
|
const { features, onFeatureClick, onFeatureRemove, featureMetrics, settings } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<div>
|
||||||
<ListSubHeader caption="Feature toggles" />
|
<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) =>
|
{features.map((feature, i) =>
|
||||||
<Feature key={i}
|
<Feature key={i}
|
||||||
|
settings={settings}
|
||||||
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
metricsLastHour={featureMetrics.lastHour[feature.name]}
|
||||||
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
metricsLastMinute={featureMetrics.lastMinute[feature.name]}
|
||||||
feature={feature}
|
feature={feature}
|
||||||
onFeatureClick={onFeatureClick}
|
onFeatureClick={onFeatureClick}
|
||||||
onFeatureRemove={onFeatureRemove}/>
|
onFeatureRemove={onFeatureRemove}/>
|
||||||
)}
|
)}
|
||||||
<ListDivider />
|
</ul>
|
||||||
|
<hr />
|
||||||
<Link to="/features/create">
|
<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>
|
</Link>
|
||||||
</List>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,79 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { toggleFeature, fetchFeatureToggles, removeFeatureToggle } from '../../store/feature-actions';
|
import { toggleFeature, fetchFeatureToggles, removeFeatureToggle } from '../../store/feature-actions';
|
||||||
import { fetchFeatureMetrics } from '../../store/feature-metrics-actions';
|
import { fetchFeatureMetrics } from '../../store/feature-metrics-actions';
|
||||||
|
import { updateSettingForGroup } from '../../store/settings/actions';
|
||||||
|
|
||||||
|
|
||||||
import FeatureListComponent from './list-component';
|
import FeatureListComponent from './list-component';
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
const mapStateToProps = (state) => {
|
||||||
features: state.features.toJS(),
|
const featureMetrics = state.featureMetrics.toJS();
|
||||||
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 = {
|
const mapDispatchToProps = {
|
||||||
onFeatureClick: toggleFeature,
|
onFeatureClick: toggleFeature,
|
||||||
onFeatureRemove: removeFeatureToggle,
|
onFeatureRemove: removeFeatureToggle,
|
||||||
fetchFeatureToggles,
|
fetchFeatureToggles,
|
||||||
fetchFeatureMetrics,
|
fetchFeatureMetrics,
|
||||||
|
updateSetting: updateSettingForGroup('feature'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const FeatureListContainer = connect(
|
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 React, { PropTypes, PureComponent } from 'react';
|
||||||
|
import { Icon } from 'react-mdl';
|
||||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
|
||||||
|
|
||||||
import style from './history.scss';
|
import style from './history.scss';
|
||||||
|
|
||||||
@ -110,7 +109,7 @@ class HistoryItem extends PureComponent {
|
|||||||
<dd>{id}</dd>
|
<dd>{id}</dd>
|
||||||
<dt>Type:</dt>
|
<dt>Type:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<FontIcon value={icon} title={type} style={{ fontSize: '1.6rem' }} />
|
<Icon name={icon} title={type} style={{ fontSize: '1.6rem' }} />
|
||||||
<span> {type}</span>
|
<span> {type}</span>
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Timestamp:</dt>
|
<dt>Timestamp:</dt>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import HistoryItemDiff from './history-item-diff';
|
import HistoryItemDiff from './history-item-diff';
|
||||||
import HistoryItemJson from './history-item-json';
|
import HistoryItemJson from './history-item-json';
|
||||||
import Switch from 'react-toolbox/lib/switch';
|
import { Switch } from 'react-mdl';
|
||||||
|
|
||||||
import style from './history.scss';
|
import style from './history.scss';
|
||||||
|
|
||||||
@ -28,11 +28,7 @@ class HistoryList extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.history}>
|
<div className={style.history}>
|
||||||
<Switch
|
<Switch checked={showData} onChange={this.toggleShowDiff.bind(this)}>Show full events</Switch>
|
||||||
checked={showData}
|
|
||||||
label="Show full events"
|
|
||||||
onChange={this.toggleShowDiff.bind(this)}
|
|
||||||
/>
|
|
||||||
{entries}
|
{entries}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
|
import { DataTable, TableHeader } from 'react-mdl';
|
||||||
import Chip from 'react-toolbox/lib/chip';
|
|
||||||
|
|
||||||
class Metrics extends Component {
|
class Metrics extends Component {
|
||||||
|
|
||||||
@ -12,17 +11,22 @@ class Metrics extends Component {
|
|||||||
const { globalCount, clientList } = this.props;
|
const { globalCount, clientList } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<div>
|
||||||
<ListSubHeader caption={`Total of ${globalCount} toggles`} />
|
<h4>{`Total of ${globalCount} toggles`}</h4>
|
||||||
<ListDivider />
|
<DataTable
|
||||||
{clientList.map(({ name, count, ping, appName }, i) =>
|
style={{ width: '100%' }}
|
||||||
<ListItem
|
rows={clientList}
|
||||||
leftActions={[<Chip>{count}</Chip>]}
|
selectable={false}
|
||||||
key={name + i}
|
>
|
||||||
caption={appName}
|
<TableHeader name="name">Instance</TableHeader>
|
||||||
legend={`${name} pinged ${ping}`} />
|
<TableHeader name="appName">Application name</TableHeader>
|
||||||
)}
|
<TableHeader numeric name="ping" cellFormatter={
|
||||||
</List>
|
(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 React, { PropTypes } from 'react';
|
||||||
|
|
||||||
import Input from 'react-toolbox/lib/input';
|
import { Textfield, Button, IconButton } from 'react-mdl';
|
||||||
import Button from 'react-toolbox/lib/button';
|
|
||||||
|
|
||||||
const trim = (value) => {
|
const trim = (value) => {
|
||||||
if (value && value.trim) {
|
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 genParams = (input, num = 0, setValue) => (<div>{gerArrayWithEntries(num).map((v, i) => {
|
||||||
const key = `${PARAM_PREFIX}${i + 1}`;
|
const key = `${PARAM_PREFIX}${i + 1}`;
|
||||||
return (
|
return (
|
||||||
<Input
|
<Textfield
|
||||||
type="text"
|
|
||||||
label={`Parameter name ${i + 1}`}
|
label={`Parameter name ${i + 1}`}
|
||||||
name={key} key={key}
|
name={key} key={key}
|
||||||
onChange={(value) => setValue(key, value)}
|
onChange={({ target }) => setValue(key, target.value)}
|
||||||
value={input[key]} />
|
value={input[key]} />
|
||||||
);
|
);
|
||||||
})}</div>);
|
})}</div>);
|
||||||
@ -38,22 +36,25 @@ const AddStrategy = ({
|
|||||||
}) => (
|
}) => (
|
||||||
<form onSubmit={onSubmit(input)}>
|
<form onSubmit={onSubmit(input)}>
|
||||||
<section>
|
<section>
|
||||||
<Input type="text" label="Strategy name"
|
<Textfield label="Strategy name"
|
||||||
name="name" required
|
name="name" required
|
||||||
pattern="^[0-9a-zA-Z\.\-]+$"
|
pattern="^[0-9a-zA-Z\.\-]+$"
|
||||||
onChange={(value) => setValue('name', trim(value))}
|
onChange={({ target }) => setValue('name', trim(target.value))}
|
||||||
value={input.name}
|
value={input.name}
|
||||||
/>
|
/>
|
||||||
<Input type="text" multiline label="Description"
|
<br />
|
||||||
|
<Textfield
|
||||||
|
rows={2}
|
||||||
|
label="Description"
|
||||||
name="description"
|
name="description"
|
||||||
onChange={(value) => setValue('description', value)}
|
onChange={({ target }) => setValue('description', target.value)}
|
||||||
value={input.description}
|
value={input.description}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
{genParams(input, input._params, setValue)}
|
{genParams(input, input._params, setValue)}
|
||||||
<Button icon="add" accent label="Add parameter" onClick={(e) => {
|
<IconButton name="add" title="Add parameter" onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
incValue('_params');
|
incValue('_params');
|
||||||
}}/>
|
}}/>
|
||||||
@ -63,9 +64,9 @@ const AddStrategy = ({
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<section>
|
<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>
|
</section>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { List, ListItem, ListSubHeader, ListDivider } from 'react-toolbox/lib/list';
|
|
||||||
import FontIcon from 'react-toolbox/lib/font_icon';
|
import { List, ListItem, ListItemContent, Icon, IconButton, Chip } from 'react-mdl';
|
||||||
import Chip from 'react-toolbox/lib/chip';
|
|
||||||
|
|
||||||
import style from './strategies.scss';
|
import style from './strategies.scss';
|
||||||
|
|
||||||
@ -25,27 +24,24 @@ class StrategiesListComponent extends Component {
|
|||||||
const { strategies, removeStrategy } = this.props;
|
const { strategies, removeStrategy } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List ripple >
|
<div>
|
||||||
<ListSubHeader caption="Strategies" />
|
<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) => {
|
{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 (
|
return (
|
||||||
<ListItem key={i} rightActions={actions}
|
<ListItem key={i}>
|
||||||
caption={strategy.name}
|
<ListItemContent><strong>{strategy.name}</strong> {strategy.description}</ListItemContent>
|
||||||
legend={strategy.description} />
|
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
|
||||||
|
</ListItem>
|
||||||
);
|
);
|
||||||
}) : <ListItem caption="No entries" />}
|
}) : <ListItem>No entries</ListItem>}
|
||||||
<ListDivider />
|
|
||||||
<ListItem
|
|
||||||
onClick={() => this.context.router.push('/strategies/create')}
|
|
||||||
caption="Add" legend="new strategy" leftIcon="add" />
|
|
||||||
</List>
|
</List>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export default class ShowUserComponent extends React.Component {
|
|||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
<p>
|
<p>
|
||||||
You are logged in as:
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import Dialog from 'react-toolbox/lib/dialog';
|
import { Textfield, Dialog, DialogTitle, DialogContent, DialogActions, Button } from 'react-mdl';
|
||||||
import Input from 'react-toolbox/lib/input';
|
|
||||||
|
|
||||||
class EditUserComponent extends React.Component {
|
class EditUserComponent extends React.Component {
|
||||||
static propTypes () {
|
static propTypes () {
|
||||||
@ -16,31 +15,29 @@ class EditUserComponent extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const actions = [
|
|
||||||
{ label: 'Save', onClick: this.props.save },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<div>
|
||||||
active={this.props.user.showDialog}
|
<Dialog open={this.props.user.showDialog}>
|
||||||
title="Action required"
|
<DialogTitle>Action required</DialogTitle>
|
||||||
actions={actions}
|
<DialogContent>
|
||||||
>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You hav to specify a username to use Unleash. This will allow us to track changes.
|
You are logged in as:You hav to specify a username to use Unleash. This will allow us to track changes.
|
||||||
</p>
|
</p>
|
||||||
<form onSubmit={this.handleSubmit}>
|
<form onSubmit={this.handleSubmit}>
|
||||||
<Input
|
<Textfield
|
||||||
type="text"
|
|
||||||
label="USERNAME"
|
label="USERNAME"
|
||||||
name="username"
|
name="username"
|
||||||
required
|
required
|
||||||
value={this.props.user.userName}
|
value={this.props.user.userName}
|
||||||
onChange={(v) => this.props.updateUserName(v)}
|
onChange={(e) => this.props.updateUserName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={this.props.save}>Save</Button>
|
||||||
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,7 @@ export default class Features extends Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<h6>Edit feature toggle</h6>
|
|
||||||
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
|
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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,
|
plugins,
|
||||||
|
|
||||||
sassLoader: {
|
sassLoader: {
|
||||||
data: '@import "theme/_config.scss";',
|
// data: '@import "theme/_config.scss";',
|
||||||
includePaths: [path.resolve(__dirname, './src')],
|
includePaths: [path.resolve(__dirname, './src')],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user