);
diff --git a/frontend/src/component/feature/form/strategies-section-container.jsx b/frontend/src/component/feature/form/strategies-section-container.jsx
index 7f12216935..263f5cbee1 100644
--- a/frontend/src/component/feature/form/strategies-section-container.jsx
+++ b/frontend/src/component/feature/form/strategies-section-container.jsx
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import StrategiesSection from './strategies-section';
-import { fetchStrategies } from '../../../store/strategy-actions';
+import { fetchStrategies } from '../../../store/strategy/actions';
export default connect((state) => ({
diff --git a/frontend/src/component/feature/form/strategies-section.jsx b/frontend/src/component/feature/form/strategies-section.jsx
index 83ec3416da..ea9097f67f 100644
--- a/frontend/src/component/feature/form/strategies-section.jsx
+++ b/frontend/src/component/feature/form/strategies-section.jsx
@@ -1,10 +1,8 @@
import React, { PropTypes } from 'react';
+import { ProgressBar } from 'react-mdl';
import StrategiesList from './strategies-list';
import AddStrategy from './strategies-add';
-
-const headerStyle = {
- marginBottom: '10px',
-};
+import { HeaderTitle } from '../../common';
class StrategiesSection extends React.Component {
@@ -24,12 +22,12 @@ class StrategiesSection extends React.Component {
render () {
if (!this.props.strategies || this.props.strategies.length === 0) {
- return
);
diff --git a/frontend/src/component/feature/form/strategy-configure.jsx b/frontend/src/component/feature/form/strategy-configure.jsx
index 6ed77fb337..f8475e016e 100644
--- a/frontend/src/component/feature/form/strategy-configure.jsx
+++ b/frontend/src/component/feature/form/strategy-configure.jsx
@@ -1,6 +1,20 @@
import React, { PropTypes } from 'react';
-import { Textfield, Button } from 'react-mdl';
+import {
+ Textfield, Button,
+ Card, CardTitle, CardText, CardActions, CardMenu,
+ IconButton, Icon,
+} from 'react-mdl';
+import { Link } from 'react-router';
+import StrategyInputPersentage from './strategy-input-persentage';
+import StrategyInputList from './strategy-input-list';
+const style = {
+ flex: '1',
+ minWidth: '300px',
+ maxWidth: '100%',
+ margin: '5px 20px 15px 0px',
+ background: '#f2f9fc',
+};
class StrategyConfigure extends React.Component {
static propTypes () {
@@ -12,57 +26,144 @@ class StrategyConfigure extends React.Component {
};
}
+ // shouldComponentUpdate (props, nextProps) {
+ // console.log({ props, nextProps });
+ // }
+
handleConfigChange = (key, e) => {
+ this.setConfig(key, e.target.value);
+ };
+
+ setConfig = (key, value) => {
const parameters = this.props.strategy.parameters || {};
- parameters[key] = e.target.value;
+ parameters[key] = value;
const updatedStrategy = Object.assign({}, this.props.strategy, { parameters });
this.props.updateStrategy(updatedStrategy);
- };
+ }
handleRemove = (evt) => {
evt.preventDefault();
this.props.removeStrategy();
}
- renderInputFields (strategyDefinition) {
- if (strategyDefinition.parametersTemplate) {
- return Object.keys(strategyDefinition.parametersTemplate).map(field => (
-
- ));
+ renderInputFields ({ parameters }) {
+ if (parameters && parameters.length > 0) {
+ return parameters.map(({ name, type, description, required }) => {
+ let value = this.props.strategy.parameters[name];
+ if (type === 'percentage') {
+ if (value == null || (typeof value === 'string' && value === '')) {
+ value = 50; // default value
+ }
+ return (
+
+ );
+ } else if (type === 'list') {
+ let list = [];
+ if (typeof value === 'string') {
+ list = value
+ .trim()
+ .split(',')
+ .filter(Boolean);
+ }
+ return (
+
+ );
+ } else if (type === 'number') {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
+ });
}
+ return null;
}
render () {
if (!this.props.strategyDefinition) {
+ const { name } = this.props.strategy;
return (
-
);
}
- const inputFields = this.renderInputFields(this.props.strategyDefinition) || [];
+ const inputFields = this.renderInputFields(this.props.strategyDefinition);
+
+ const { name } = this.props.strategy;
return (
-
);
}
}
diff --git a/frontend/src/component/feature/form/strategy-input-list.jsx b/frontend/src/component/feature/form/strategy-input-list.jsx
new file mode 100644
index 0000000000..71f611b61c
--- /dev/null
+++ b/frontend/src/component/feature/form/strategy-input-list.jsx
@@ -0,0 +1,80 @@
+import React, { Component, PropTypes } from 'react';
+import {
+ Textfield,
+ IconButton,
+ Chip,
+} from 'react-mdl';
+
+export default class InputList extends Component {
+
+ static propTypes = {
+ name: PropTypes.string.isRequired,
+ list: PropTypes.array.isRequired,
+ setConfig: PropTypes.func.isRequired,
+ }
+
+ onBlur = (e) => {
+ this.setValue(e);
+ window.removeEventListener('keydown', this.onKeyHandler, false);
+ }
+
+ onFocus = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ window.addEventListener('keydown', this.onKeyHandler, false);
+ }
+
+ onKeyHandler = (e) => {
+ if (e.key === 'Enter') {
+ this.setValue();
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ setValue = (e) => {
+ if (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ const { name, list, setConfig } = this.props;
+ const inputValue = document.querySelector(`[name="${name}_input"]`);
+ if (inputValue && inputValue.value) {
+ list.push(inputValue.value);
+ inputValue.value = '';
+ setConfig(name, list.join(','));
+ }
+ }
+
+ onClose (index) {
+ const { name, list, setConfig } = this.props;
+ list[index] = null;
+ setConfig(name, list.length === 1 ? '' : list.filter(Boolean).join(','));
+ }
+
+ render () {
+ const { name, list } = this.props;
+ return (
);
+ }
+}
diff --git a/frontend/src/component/feature/form/strategy-input-persentage.jsx b/frontend/src/component/feature/form/strategy-input-persentage.jsx
new file mode 100644
index 0000000000..563d949de1
--- /dev/null
+++ b/frontend/src/component/feature/form/strategy-input-persentage.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Slider } from 'react-mdl';
+
+const labelStyle = {
+ margin: '20px 0',
+ textAlign: 'center',
+ color: '#3f51b5',
+ fontSize: '12px',
+};
+
+export default ({ name, value, onChange }) => (
+
+);
diff --git a/frontend/src/component/feature/list-component.jsx b/frontend/src/component/feature/list-component.jsx
index 08852e3938..bed90978d2 100644
--- a/frontend/src/component/feature/list-component.jsx
+++ b/frontend/src/component/feature/list-component.jsx
@@ -52,7 +52,6 @@ export default class FeatureListComponent extends React.PureComponent {
return (
-
this.toggleMetrics()} className={styles.topListItem0}>
{ settings.showLastHour &&
@@ -68,8 +67,7 @@ export default class FeatureListComponent extends React.PureComponent {
}
{ '1 minute' }
-
-
+
-
+
-
-
-
+
-
- {features.map((feature, i) =>
-
- )}
+ {features.map((feature, i) =>
+
+ )}
diff --git a/frontend/src/component/feature/metric-component.jsx b/frontend/src/component/feature/metric-component.jsx
new file mode 100644
index 0000000000..a47e889e1e
--- /dev/null
+++ b/frontend/src/component/feature/metric-component.jsx
@@ -0,0 +1,84 @@
+import React, { PropTypes } from 'react';
+import { Grid, Cell, Icon } from 'react-mdl';
+import Progress from './progress';
+import { AppsLinkList, SwitchWithLabel, calc } from '../common';
+
+
+export default class MetricComponent extends React.Component {
+ static propTypes () {
+ return {
+ metrics: PropTypes.object.isRequired,
+ featureToggle: PropTypes.object.isRequired,
+ toggleFeature: PropTypes.func.isRequired,
+ fetchSeenApps: PropTypes.func.isRequired,
+ fetchFeatureMetrics: PropTypes.func.isRequired,
+ };
+ }
+
+ componentWillMount () {
+ this.props.fetchSeenApps();
+ this.props.fetchFeatureMetrics();
+ this.timer = setInterval(() => {
+ this.props.fetchFeatureMetrics();
+ }, 5000);
+ }
+
+ componentWillUnmount () {
+ clearInterval(this.timer);
+ }
+
+ render () {
+ const { metrics = {}, featureToggle, toggleFeature } = this.props;
+ const {
+ lastHour = { yes: 0, no: 0, isFallback: true },
+ lastMinute = { yes: 0, no: 0, isFallback: true },
+ seenApps = [],
+ } = metrics;
+
+ const lastHourPercent = 1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
+ const lastMinutePercent = 1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
+
+ return (
+
toggleFeature(featureToggle)}>Toggle {featureToggle.name}
+
+
+
+ {
+ lastMinute.isFallback ?
+ :
+
+ }
+ Last minute Yes {lastMinute.yes}, No: {lastMinute.no}
+ |
+
+ {
+ lastHour.isFallback ?
+ :
+
+ }
+ Last hour Yes {lastHour.yes}, No: {lastHour.no}
+ |
+
+ {seenApps.length > 0 ?
+ ( Seen in applications: ) :
+
+
+ Not used in a app in the last hour.
+ This might be due to your client implementation is not reporting usage.
+
+ }
+
+ |
+
+
);
+ }
+}
diff --git a/frontend/src/component/feature/metric-container.jsx b/frontend/src/component/feature/metric-container.jsx
new file mode 100644
index 0000000000..89bfff15bc
--- /dev/null
+++ b/frontend/src/component/feature/metric-container.jsx
@@ -0,0 +1,32 @@
+
+import { connect } from 'react-redux';
+
+import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions';
+import { toggleFeature } from '../../store/feature-actions';
+
+import MatricComponent from './metric-component';
+
+function getMetricsForToggle (state, toggleName) {
+ if (!toggleName) {
+ return;
+ }
+ const result = {};
+
+ if (state.featureMetrics.hasIn(['seenApps', toggleName])) {
+ result.seenApps = state.featureMetrics.getIn(['seenApps', toggleName]);
+ }
+ if (state.featureMetrics.hasIn(['lastHour', toggleName])) {
+ result.lastHour = state.featureMetrics.getIn(['lastHour', toggleName]);
+ result.lastMinute = state.featureMetrics.getIn(['lastMinute', toggleName]);
+ }
+ return result;
+}
+
+
+export default connect((state, props) => ({
+ metrics: getMetricsForToggle(state, props.featureToggleName),
+}), {
+ fetchFeatureMetrics,
+ toggleFeature,
+ fetchSeenApps,
+})(MatricComponent);
diff --git a/frontend/src/component/feature/progress-styles.scss b/frontend/src/component/feature/progress-styles.scss
index c036577277..b7a00a2a1b 100644
--- a/frontend/src/component/feature/progress-styles.scss
+++ b/frontend/src/component/feature/progress-styles.scss
@@ -14,4 +14,4 @@
line-height: 25px;
dominant-baseline: middle;
text-anchor: middle;
-}
\ No newline at end of file
+}
diff --git a/frontend/src/component/feature/progress.jsx b/frontend/src/component/feature/progress.jsx
index 59386daeb3..c17d970b79 100644
--- a/frontend/src/component/feature/progress.jsx
+++ b/frontend/src/component/feature/progress.jsx
@@ -7,13 +7,14 @@ class Progress extends Component {
this.state = {
percentage: props.initialAnimation ? 0 : props.percentage,
+ percentageText: props.initialAnimation ? 0 : props.percentage,
};
}
componentDidMount () {
if (this.props.initialAnimation) {
this.initialTimeout = setTimeout(() => {
- this.requestAnimationFrame = window.requestAnimationFrame(() => {
+ this.rafTimerInit = window.requestAnimationFrame(() => {
this.setState({
percentage: this.props.percentage,
});
@@ -23,16 +24,65 @@ class Progress extends Component {
}
componentWillReceiveProps ({ percentage }) {
- this.setState({ percentage });
+ if (this.state.percentage !== percentage) {
+ const nextState = { percentage };
+ if (this.props.animatePercentageText) {
+ this.animateTo(percentage, this.getTarget(percentage));
+ } else {
+ nextState.percentageText = percentage;
+ }
+ this.setState(nextState);
+ }
}
+ getTarget (target) {
+ const start = this.state.percentageText;
+ const TOTAL_ANIMATION_TIME = 5000;
+ const diff = start > target ? -(start - target) : target - start;
+ const perCycle = TOTAL_ANIMATION_TIME / diff;
+ const cyclesCounter = Math.round(Math.abs(TOTAL_ANIMATION_TIME / perCycle));
+ const perCycleTime = Math.round(Math.abs(perCycle));
+
+ return {
+ start,
+ target,
+ cyclesCounter,
+ perCycleTime,
+ increment: diff / cyclesCounter,
+ };
+ }
+
+ animateTo (percentage, targetState) {
+ cancelAnimationFrame(this.rafCounterTimer);
+ clearTimeout(this.nextTimer);
+
+ const current = this.state.percentageText;
+
+ targetState.cyclesCounter --;
+ if (targetState.cyclesCounter <= 0) {
+ this.setState({ percentageText: targetState.target });
+ return;
+ }
+
+ const next = Math.round(current + targetState.increment);
+ this.rafCounterTimer = requestAnimationFrame(() => {
+ this.setState({ percentageText: next });
+ this.nextTimer = setTimeout(() => {
+ this.animateTo(next, targetState);
+ }, targetState.perCycleTime);
+ });
+ }
+
+
componentWillUnmount () {
clearTimeout(this.initialTimeout);
- window.cancelAnimationFrame(this.requestAnimationFrame);
+ clearTimeout(this.nextTimer);
+ window.cancelAnimationFrame(this.rafTimerInit);
+ window.cancelAnimationFrame(this.rafCounterTimer);
}
render () {
- const { strokeWidth, percentage } = this.props;
+ const { strokeWidth } = this.props;
const radius = (50 - strokeWidth / 2);
const pathDescription = `
M 50,50 m 0,-${radius}
@@ -66,7 +116,7 @@ class Progress extends Component {
className={styles.text}
x={50}
y={50}
- >{percentage}%
+ >{this.state.percentageText}%
);
}
}
@@ -75,11 +125,13 @@ Progress.propTypes = {
percentage: PropTypes.number.isRequired,
strokeWidth: PropTypes.number,
initialAnimation: PropTypes.bool,
+ animatePercentageText: PropTypes.bool,
textForPercentage: PropTypes.func,
};
Progress.defaultProps = {
strokeWidth: 8,
+ animatePercentageText: false,
initialAnimation: false,
};
diff --git a/frontend/src/component/feature/view-component.jsx b/frontend/src/component/feature/view-component.jsx
new file mode 100644
index 0000000000..0ed75f4cd1
--- /dev/null
+++ b/frontend/src/component/feature/view-component.jsx
@@ -0,0 +1,97 @@
+import React, { PropTypes } from 'react';
+import { Tabs, Tab, ProgressBar } from 'react-mdl';
+import { hashHistory, Link } from 'react-router';
+
+import HistoryComponent from '../history/history-list-toggle-container';
+import MetricComponent from './metric-container';
+import EditFeatureToggle from './form-edit-container.jsx';
+
+const TABS = {
+ view: 0,
+ edit: 1,
+ history: 2,
+};
+
+export default class ViewFeatureToggleComponent extends React.Component {
+
+ constructor (props) {
+ super(props);
+ }
+
+ static propTypes () {
+ return {
+ activeTab: PropTypes.string.isRequired,
+ featureToggleName: PropTypes.string.isRequired,
+ features: PropTypes.array.isRequired,
+ fetchFeatureToggles: PropTypes.array.isRequired,
+ featureToggle: PropTypes.object.isRequired,
+ };
+ }
+
+ componentWillMount () {
+ if (this.props.features.length === 0) {
+ this.props.fetchFeatureToggles();
+ }
+ }
+
+ getTabContent (activeTab) {
+ const {
+ featureToggle,
+ featureToggleName,
+ } = this.props;
+
+ if (TABS[activeTab] === TABS.history) {
+ return
;
+ } else if (TABS[activeTab] === TABS.edit) {
+ return
;
+ } else {
+ return
;
+ }
+ }
+
+ goToTab (tabName, featureToggleName) {
+ hashHistory.push(`/features/${tabName}/${featureToggleName}`);
+ }
+
+ render () {
+ const {
+ featureToggle,
+ features,
+ activeTab,
+ featureToggleName,
+ } = this.props;
+
+ if (!featureToggle) {
+ if (features.length === 0 ) {
+ return
;
+ }
+ return (
+
+ Could not find the toggle
+ {featureToggleName}
+
+ );
+ }
+
+ const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.view;
+ const tabContent = this.getTabContent(activeTab);
+
+ return (
+
+
{featureToggle.name} {featureToggle.enabled ? 'is enabled' : 'is disabled'}
+
+ Created {(new Date(featureToggle.createdAt)).toLocaleString('nb-NO')}
+
+
+
{featureToggle.description}
+
+ this.goToTab('view', featureToggleName)}>Metrics
+ this.goToTab('edit', featureToggleName)}>Edit
+ this.goToTab('history', featureToggleName)}>History
+
+
+ {tabContent}
+
+ );
+ }
+}
diff --git a/frontend/src/component/feature/view-container.jsx b/frontend/src/component/feature/view-container.jsx
new file mode 100644
index 0000000000..47bedf154a
--- /dev/null
+++ b/frontend/src/component/feature/view-container.jsx
@@ -0,0 +1,14 @@
+
+import { connect } from 'react-redux';
+
+import { fetchFeatureToggles } from '../../store/feature-actions';
+
+import ViewToggleComponent from './view-component';
+
+export default connect((state, props) => ({
+ features: state.features.toJS(),
+ featureToggle: state.features.toJS().find(toggle => toggle.name === props.featureToggleName),
+ activeTab: props.activeTab,
+}), {
+ fetchFeatureToggles,
+})(ViewToggleComponent);
diff --git a/frontend/src/component/feature/view-edit-container.jsx b/frontend/src/component/feature/view-edit-container.jsx
deleted file mode 100644
index f08f9af9db..0000000000
--- a/frontend/src/component/feature/view-edit-container.jsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import React, { PropTypes } from 'react';
-import { Grid, Cell, Icon, Switch } from 'react-mdl';
-import { Link } from 'react-router';
-
-import percentLib from 'percent';
-import Progress from './progress';
-
-import { connect } from 'react-redux';
-import EditFeatureToggle from './form-edit-container.jsx';
-import { fetchFeatureToggles, toggleFeature } from '../../store/feature-actions';
-import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions';
-
-class EditFeatureToggleWrapper extends React.Component {
-
- static propTypes () {
- return {
- featureToggleName: PropTypes.string.isRequired,
- features: PropTypes.array.isRequired,
- fetchFeatureToggles: PropTypes.array.isRequired,
- };
- }
-
- componentWillMount () {
- if (this.props.features.length === 0) {
- this.props.fetchFeatureToggles();
- }
- this.props.fetchSeenApps();
- this.props.fetchFeatureMetrics();
- this.timer = setInterval(() => {
- this.props.fetchSeenApps();
- this.props.fetchFeatureMetrics();
- }, 5000);
- }
-
- componentWillUnmount () {
- clearInterval(this.timer);
- }
-
- render () {
- const {
- toggleFeature,
- features,
- featureToggleName,
- metrics = {},
- } = this.props;
-
- const {
- lastHour = { yes: 0, no: 0, isFallback: true },
- lastMinute = { yes: 0, no: 0, isFallback: true },
- seenApps = [],
- } = metrics;
-
- const lastHourPercent = 1 * percentLib.calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
- const lastMinutePercent = 1 * percentLib.calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
-
- const featureToggle = features.find(toggle => toggle.name === featureToggleName);
-
- if (!featureToggle) {
- if (features.length === 0 ) {
- return
Loading ;
- }
- return
Could not find {this.props.featureToggleName} ;
- }
-
- return (
-
-
{featureToggle.name} {featureToggle.enabled ? 'is enabled' : 'is disabled'}
-
-
- toggleFeature(featureToggle)} checked={featureToggle.enabled}>
- Toggle {featureToggle.name}
-
-
-
-
-
- {
- lastMinute.isFallback ?
- :
-
- }
- Last minute Yes {lastMinute.yes}, No: {lastMinute.no}
- |
-
- {
- lastHour.isFallback ?
- :
-
- }
- Last hour Yes {lastHour.yes}, No: {lastHour.no}
- |
-
- {seenApps.length > 0 ?
- ( Seen in applications: ) :
-
-
- Not used in a app in the last hour. This might be due to your client implementation is not reporting usage.
-
- }
- {seenApps.length > 0 && seenApps.map((appName) => (
-
- {appName}
-
- ))}
- add instances count?
- |
-
- add history
- |
-
-
-
Edit
-
-
- );
- }
-}
-
-function getMetricsForToggle (state, toggleName) {
- if (!toggleName) {
- return;
- }
- const result = {};
-
- if (state.featureMetrics.hasIn(['seenApps', toggleName])) {
- result.seenApps = state.featureMetrics.getIn(['seenApps', toggleName]);
- }
- if (state.featureMetrics.hasIn(['lastHour', toggleName])) {
- result.lastHour = state.featureMetrics.getIn(['lastHour', toggleName]);
- result.lastMinute = state.featureMetrics.getIn(['lastMinute', toggleName]);
- }
- return result;
-}
-
-
-export default connect((state, props) => ({
- features: state.features.toJS(),
- metrics: getMetricsForToggle(state, props.featureToggleName),
-}), {
- fetchFeatureMetrics,
- fetchFeatureToggles,
- toggleFeature,
- fetchSeenApps,
-})(EditFeatureToggleWrapper);
diff --git a/frontend/src/component/history/history-component.jsx b/frontend/src/component/history/history-component.jsx
index 153fa140e2..485654e1bc 100644
--- a/frontend/src/component/history/history-component.jsx
+++ b/frontend/src/component/history/history-component.jsx
@@ -18,10 +18,7 @@ class History extends PureComponent {
}
return (
-
-
Last 100 changes
-
-
+
);
}
}
diff --git a/frontend/src/component/history/history-item-diff.jsx b/frontend/src/component/history/history-item-diff.jsx
index e98bb530f8..78284cb624 100644
--- a/frontend/src/component/history/history-item-diff.jsx
+++ b/frontend/src/component/history/history-item-diff.jsx
@@ -1,5 +1,4 @@
import React, { PropTypes, PureComponent } from 'react';
-import { Icon } from 'react-mdl';
import style from './history.scss';
@@ -10,35 +9,25 @@ const DIFF_PREFIXES = {
N: '+',
};
-const SPADEN_CLASS = {
+const KLASSES = {
A: style.blue, // array edited
E: style.blue, // edited
D: style.negative, // deleted
N: style.positive, // added
};
-function getIcon (type) {
- switch (type) {
- case 'feature-updated': return 'autorenew';
- case 'feature-created': return 'add';
- case 'feature-deleted': return 'remove';
- case 'feature-archived': return 'archived';
- default: return 'star';
- }
-}
-
function buildItemDiff (diff, key) {
let change;
if (diff.lhs !== undefined) {
change = (
-
- {key}: {JSON.stringify(diff.lhs)}
+
- {key}: {JSON.stringify(diff.lhs)}
);
} else if (diff.rhs !== undefined) {
change = (
-
+ {key}: {JSON.stringify(diff.rhs)}
+
+ {key}: {JSON.stringify(diff.rhs)}
);
}
@@ -55,12 +44,12 @@ function buildDiff (diff, idx) {
} else if (diff.lhs !== undefined && diff.rhs !== undefined) {
change = (
-
- {key}: {JSON.stringify(diff.lhs)}
-
+ {key}: {JSON.stringify(diff.rhs)}
+
- {key}: {JSON.stringify(diff.lhs)}
+
+ {key}: {JSON.stringify(diff.rhs)}
);
} else {
- const spadenClass = SPADEN_CLASS[diff.kind];
+ const spadenClass = KLASSES[diff.kind];
const prefix = DIFF_PREFIXES[diff.kind];
change = (
{prefix} {key}: {JSON.stringify(diff.rhs || diff.item)}
);
@@ -77,50 +66,20 @@ class HistoryItem extends PureComponent {
};
}
- renderEventDiff (logEntry) {
+ render () {
+ const entry = this.props.entry;
let changes;
- if (logEntry.diffs) {
- changes = logEntry.diffs.map(buildDiff);
+ if (entry.diffs) {
+ changes = entry.diffs.map(buildDiff);
} else {
// Just show the data if there is no diff yet.
- changes =
{JSON.stringify(logEntry.data, null, 2)}
;
+ changes =
{JSON.stringify(entry.data, null, 2)}
;
}
- return
{changes.length === 0 ? '(no changes)' : changes}
;
- }
-
- render () {
- const {
- createdBy,
- id,
- type,
- } = this.props.entry;
-
- const createdAt = (new Date(this.props.entry.createdAt)).toLocaleString('nb-NO');
- const icon = getIcon(type);
-
- const data = this.renderEventDiff(this.props.entry);
-
- return (
-
-
- Id:
- {id}
- Type:
-
-
- {type}
-
- Timestamp:
- {createdAt}
- Username:
- {createdBy}
- Diff
- {data}
-
-
- );
+ return (
+ {changes.length === 0 ? '(no changes)' : changes}
+ );
}
}
diff --git a/frontend/src/component/history/history-list-component.jsx b/frontend/src/component/history/history-list-component.jsx
index d13ecfc3ea..a937c8ffa8 100644
--- a/frontend/src/component/history/history-list-component.jsx
+++ b/frontend/src/component/history/history-list-component.jsx
@@ -1,7 +1,8 @@
import React, { Component } from 'react';
import HistoryItemDiff from './history-item-diff';
import HistoryItemJson from './history-item-json';
-import { Switch } from 'react-mdl';
+import { Table, TableHeader } from 'react-mdl';
+import { HeaderTitle, SwitchWithLabel } from '../common';
import style from './history.scss';
@@ -23,13 +24,28 @@ class HistoryList extends Component {
if (showData) {
entries = history.map((entry) =>
);
} else {
- entries = history.map((entry) =>
);
+ entries = (
Object.assign({
+ diff: ( ),
+ }, entry))
+ }
+ style={{ width: '100%' }}
+ >
+ Type
+ User
+ Diff
+ (new Date(v)).toLocaleString('nb-NO')}>Time
+
);
}
return (
- Show full events
- {entries}
+ Show full events
+ }/>
+ {entries}
);
}
diff --git a/frontend/src/component/history/history-list-container.jsx b/frontend/src/component/history/history-list-container.jsx
index df8a725a80..b3055e52e3 100644
--- a/frontend/src/component/history/history-list-container.jsx
+++ b/frontend/src/component/history/history-list-container.jsx
@@ -1,5 +1,5 @@
import { connect } from 'react-redux';
-import HistoryListComponent from './history-list-component';
+import HistoryListToggleComponent from './history-list-component';
import { updateSettingForGroup } from '../../store/settings/actions';
const mapStateToProps = (state) => {
@@ -12,6 +12,6 @@ const mapStateToProps = (state) => {
const HistoryListContainer = connect(mapStateToProps, {
updateSetting: updateSettingForGroup('history'),
-})(HistoryListComponent);
+})(HistoryListToggleComponent);
export default HistoryListContainer;
diff --git a/frontend/src/component/history/history-list-toggle-component.jsx b/frontend/src/component/history/history-list-toggle-component.jsx
index aba456efb2..46580fd22b 100644
--- a/frontend/src/component/history/history-list-toggle-component.jsx
+++ b/frontend/src/component/history/history-list-toggle-component.jsx
@@ -1,17 +1,9 @@
import React, { Component, PropTypes } from 'react';
import ListComponent from './history-list-container';
-import { fetchHistoryForToggle } from '../../data/history-api';
+import { Link } from 'react-router';
class HistoryListToggle extends Component {
- constructor (props) {
- super(props);
- this.state = {
- fetching: true,
- history: undefined,
- };
- }
-
static propTypes () {
return {
toggleName: PropTypes.string.isRequired,
@@ -19,21 +11,24 @@ class HistoryListToggle extends Component {
}
componentDidMount () {
- fetchHistoryForToggle(this.props.toggleName)
- .then((res) => this.setState({ history: res, fetching: false }));
+ this.props.fetchHistoryForToggle(this.props.toggleName);
}
render () {
- if (this.state.fetching) {
+ if (!this.props.history || this.props.history.length === 0) {
return
fetching.. ;
}
-
+ const { history, toggleName } = this.props;
return (
-
-
Showing history for toggle: {this.props.toggleName}
-
-
+
Showing history for toggle:
+ {toggleName}
+
+ }/>
);
}
}
+
export default HistoryListToggle;
diff --git a/frontend/src/component/history/history-list-toggle-container.jsx b/frontend/src/component/history/history-list-toggle-container.jsx
new file mode 100644
index 0000000000..e44c4d4cba
--- /dev/null
+++ b/frontend/src/component/history/history-list-toggle-container.jsx
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import HistoryListToggleComponent from './history-list-toggle-component';
+import { fetchHistoryForToggle } from '../../store/history-actions';
+
+function getHistoryFromToggle (state, toggleName) {
+ if (!toggleName) {
+ return [];
+ }
+
+ if (state.history.hasIn(['toggles', toggleName])) {
+ return state.history.getIn(['toggles', toggleName]).toArray();
+ }
+
+ return [];
+}
+
+const mapStateToProps = (state, props) => ({
+ history: getHistoryFromToggle(state, props.toggleName),
+});
+
+const HistoryListToggleContainer = connect(mapStateToProps, {
+ fetchHistoryForToggle,
+})(HistoryListToggleComponent);
+
+export default HistoryListToggleContainer;
diff --git a/frontend/src/component/input-helpers.js b/frontend/src/component/input-helpers.js
index 8afefcec4e..c0b24790bc 100644
--- a/frontend/src/component/input-helpers.js
+++ b/frontend/src/component/input-helpers.js
@@ -57,8 +57,8 @@ export function createActions ({ id, prepare = (v) => v }) {
dispatch(createPop({ id: getId(id, ownProps), key, index }));
},
- updateInList (key, index, newValue) {
- dispatch(createUp({ id: getId(id, ownProps), key, index, newValue }));
+ updateInList (key, index, newValue, merge = false) {
+ dispatch(createUp({ id: getId(id, ownProps), key, index, newValue, merge }));
},
incValue (key) {
diff --git a/frontend/src/component/metrics/metrics-component.js b/frontend/src/component/metrics/metrics-component.js
deleted file mode 100644
index e2071220a5..0000000000
--- a/frontend/src/component/metrics/metrics-component.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import React, { Component } from 'react';
-import { DataTable, TableHeader } from 'react-mdl';
-
-class Metrics extends Component {
-
- componentDidMount () {
- this.props.fetchMetrics();
- }
-
- render () {
- const { globalCount, clientList } = this.props;
-
- return (
-
-
{`Total of ${globalCount} toggles`}
-
- Instance
- Application name
- (v.toString())
- }>Last seen
- Counted
-
-
-
- );
- }
-}
-
-
-export default Metrics;
diff --git a/frontend/src/component/metrics/metrics-container.js b/frontend/src/component/metrics/metrics-container.js
deleted file mode 100644
index e4ab36af79..0000000000
--- a/frontend/src/component/metrics/metrics-container.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { connect } from 'react-redux';
-import Metrics from './metrics-component';
-import { fetchMetrics } from '../../store/metrics-actions';
-
-const mapStateToProps = (state) => {
- const globalCount = state.metrics.get('globalCount');
- const apps = state.metrics.get('apps').toArray();
- const clients = state.metrics.get('clients').toJS();
-
- const clientList = Object
- .keys(clients)
- .map((k) => {
- const client = clients[k];
- return {
- name: k,
- appName: client.appName,
- count: client.count,
- ping: new Date(client.ping),
- };
- })
- .sort((a, b) => (a.ping > b.ping ? -1 : 1));
-
-
- /*
- Possible stuff to ask/answer:
- * toggles in use but not in unleash-server
- * nr of toggles using fallbackValue
- * strategies implemented but not used
- */
- return {
- globalCount,
- apps,
- clientList,
- };
-};
-
-const MetricsContainer = connect(mapStateToProps, { fetchMetrics })(Metrics);
-
-export default MetricsContainer;
diff --git a/frontend/src/component/strategies/add-container.js b/frontend/src/component/strategies/add-container.js
index 1ff835c1b8..9b82ee8e7f 100644
--- a/frontend/src/component/strategies/add-container.js
+++ b/frontend/src/component/strategies/add-container.js
@@ -1,9 +1,9 @@
import { connect } from 'react-redux';
import { createMapper, createActions } from '../input-helpers';
-import { createStrategy } from '../../store/strategy-actions';
+import { createStrategy } from '../../store/strategy/actions';
-import AddStrategy, { PARAM_PREFIX } from './add-strategy';
+import AddStrategy from './add-strategy';
const ID = 'add-strategy';
@@ -11,16 +11,26 @@ const prepare = (methods, dispatch) => {
methods.onSubmit = (input) => (
(e) => {
e.preventDefault();
+ // clean
+ const parameters = (input.parameters || [])
+ .filter((name) => !!name)
+ .map(({
+ name,
+ type = 'string',
+ description = '',
+ required = false,
+ }) => ({
+ name,
+ type,
+ description,
+ required,
+ }));
- const parametersTemplate = {};
- Object.keys(input).forEach(key => {
- if (key.startsWith(PARAM_PREFIX)) {
- parametersTemplate[input[key]] = 'string';
- }
- });
- input.parametersTemplate = parametersTemplate;
-
- createStrategy(input)(dispatch)
+ createStrategy({
+ name: input.name,
+ description: input.description,
+ parameters,
+ })(dispatch)
.then(() => methods.clear())
// somewhat quickfix / hacky to go back..
.then(() => window.history.back());
@@ -43,4 +53,13 @@ const actions = createActions({
prepare,
});
-export default connect(createMapper({ id: ID }), actions)(AddStrategy);
+export default connect(createMapper({
+ id: ID,
+ getDefault () {
+ let name;
+ try {
+ [, name] = document.location.hash.match(/name=([a-z0-9-_]+)/i);
+ } catch (e) {}
+ return { name };
+ },
+}), actions)(AddStrategy);
diff --git a/frontend/src/component/strategies/add-strategy.jsx b/frontend/src/component/strategies/add-strategy.jsx
index f81d4695bf..a3ec16f941 100644
--- a/frontend/src/component/strategies/add-strategy.jsx
+++ b/frontend/src/component/strategies/add-strategy.jsx
@@ -1,6 +1,8 @@
-import React, { PropTypes } from 'react';
+import React, { PropTypes, Component } from 'react';
+
+import { Textfield, IconButton, Menu, MenuItem, Checkbox } from 'react-mdl';
+import { FormButtons } from '../common';
-import { Textfield, Button, IconButton } from 'react-mdl';
const trim = (value) => {
if (value && value.trim) {
@@ -13,71 +15,161 @@ const trim = (value) => {
function gerArrayWithEntries (num) {
return Array.from(Array(num));
}
-export const PARAM_PREFIX = 'param_';
-const genParams = (input, num = 0, setValue) => ({gerArrayWithEntries(num).map((v, i) => {
- const key = `${PARAM_PREFIX}${i + 1}`;
- return (
+const Parameter = ({ set, input = {}, index }) => (
+
setValue(key, target.value)}
- value={input[key]} />
- );
-})}
);
-
-const AddStrategy = ({
- input,
- setValue,
- incValue,
- // clear,
- onCancel,
- onSubmit,
-}) => (
-
+ style={{ width: '50%' }}
+ floatingLabel
+ label={`Parameter name ${index + 1}`}
+ onChange={({ target }) => set({ name: target.value }, true)}
+ value={input.name} />
+
+
+
+ set({ type: 'string' })}>string
+ set({ type: 'percentage' })}>percentage
+ set({ type: 'list' })}>list
+ set({ type: 'number' })}>number
+
+
+
set({ description: target.value })}
+ value={input.description}
+ />
+ set({ required: !input.required })}
+ ripple
+ defaultChecked
+ />
+
);
-AddStrategy.propTypes = {
- input: PropTypes.object,
- setValue: PropTypes.func,
- incValue: PropTypes.func,
- clear: PropTypes.func,
- onCancel: PropTypes.func,
- onSubmit: PropTypes.func,
-};
+const EditHeader = () => (
+
+
Edit strategy
+
+ Be carefull! Changing a strategy definition might also require changes to the
+ implementation in the clients.
+
+
+);
+
+const CreateHeader = () => (
+
+
Create a new Strategy definition
+
+);
+
+
+const Parameters = ({ input = [], count = 0, updateInList }) => (
+{
+ gerArrayWithEntries(count)
+ .map((v, i) =>
updateInList('parameters', i, v, true)}
+ index={i}
+ input={input[i]}
+ />)
+} );
+
+class AddStrategy extends Component {
+
+ static propTypes () {
+ return {
+ input: PropTypes.object,
+ setValue: PropTypes.func,
+ updateInList: PropTypes.func,
+ incValue: PropTypes.func,
+ clear: PropTypes.func,
+ onCancel: PropTypes.func,
+ onSubmit: PropTypes.func,
+ editmode: PropTypes.bool,
+ initCallRequired: PropTypes.bool,
+ init: PropTypes.func,
+ };
+ }
+
+ componentWillMount () {
+ // TODO unwind this stuff
+ if (this.props.initCallRequired === true) {
+ this.props.init(this.props.input);
+ if (this.props.input.parameters) {
+ this.props.setValue('_params', this.props.input.parameters.length);
+ }
+ }
+ }
+
+
+ render () {
+ const {
+ input,
+ setValue,
+ updateInList,
+ incValue,
+ onCancel,
+ editmode = false,
+ onSubmit,
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
export default AddStrategy;
diff --git a/frontend/src/component/strategies/edit-container.js b/frontend/src/component/strategies/edit-container.js
new file mode 100644
index 0000000000..ccc852e1db
--- /dev/null
+++ b/frontend/src/component/strategies/edit-container.js
@@ -0,0 +1,70 @@
+import { connect } from 'react-redux';
+import { hashHistory } from 'react-router';
+import { createMapper, createActions } from '../input-helpers';
+import { updateStrategy } from '../../store/strategy/actions';
+
+import AddStrategy from './add-strategy';
+
+const ID = 'edit-strategy';
+
+function getId (props) {
+ return [ID, props.strategy.name];
+}
+
+// TODO: need to scope to the active strategy
+// best is to emulate the "input-storage"?
+const mapStateToProps = createMapper({
+ id: getId,
+ getDefault: (state, ownProps) => ownProps.strategy,
+ prepare: (props) => {
+ props.editmode = true;
+ return props;
+ },
+});
+
+const prepare = (methods, dispatch) => {
+ methods.onSubmit = (input) => (
+ (e) => {
+ e.preventDefault();
+ // clean
+ const parameters = (input.parameters || [])
+ .filter((name) => !!name)
+ .map(({
+ name,
+ type = 'string',
+ description = '',
+ required = false,
+ }) => ({
+ name,
+ type,
+ description,
+ required,
+ }));
+
+ updateStrategy({
+ name: input.name,
+ description: input.description,
+ parameters,
+ })(dispatch)
+ .then(() => methods.clear())
+ .then(() => hashHistory.push(`/strategies/view/${input.name}`));
+ }
+ );
+
+ methods.onCancel = (e) => {
+ e.preventDefault();
+ methods.clear();
+ // somewhat quickfix / hacky to go back..
+ window.history.back();
+ };
+
+
+ return methods;
+};
+
+const actions = createActions({
+ id: getId,
+ prepare,
+});
+
+export default connect(mapStateToProps, actions)(AddStrategy);
diff --git a/frontend/src/component/strategies/list-component.jsx b/frontend/src/component/strategies/list-component.jsx
index a0c45c06e4..7805ba44eb 100644
--- a/frontend/src/component/strategies/list-component.jsx
+++ b/frontend/src/component/strategies/list-component.jsx
@@ -1,8 +1,8 @@
import React, { Component } from 'react';
+import { Link } from 'react-router';
-import { List, ListItem, ListItemContent, Icon, IconButton, Chip } from 'react-mdl';
-
-import style from './strategies.scss';
+import { List, ListItem, ListItemContent, IconButton } from 'react-mdl';
+import { HeaderTitle } from '../common';
class StrategiesListComponent extends Component {
@@ -14,33 +14,29 @@ class StrategiesListComponent extends Component {
this.props.fetchStrategies();
}
- getParameterMap ({ parametersTemplate }) {
- return Object.keys(parametersTemplate || {}).map(k => (
- {k}
- ));
- }
-
render () {
const { strategies, removeStrategy } = this.props;
return (
-
Strategies
- this.context.router.push('/strategies/create')} title="Add new strategy"/>
-
-
-
- {strategies.length > 0 ? strategies.map((strategy, i) => {
- return (
-
- {strategy.name} {strategy.description}
- removeStrategy(strategy)} />
-
- );
- }) : No entries }
-
-
-
+ this.context.router.push('/strategies/create')}
+ title="Add new strategy" />} />
+
+ {strategies.length > 0 ? strategies.map((strategy, i) => (
+
+
+
+ {strategy.name}
+
+
+ removeStrategy(strategy)} />
+
+ )) : No entries }
+
);
}
diff --git a/frontend/src/component/strategies/list-container.jsx b/frontend/src/component/strategies/list-container.jsx
index 1b1d1eb87e..50be30b13b 100644
--- a/frontend/src/component/strategies/list-container.jsx
+++ b/frontend/src/component/strategies/list-container.jsx
@@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import StrategiesListComponent from './list-component.jsx';
-import { fetchStrategies, removeStrategy } from '../../store/strategy-actions';
+import { fetchStrategies, removeStrategy } from '../../store/strategy/actions';
const mapStateToProps = (state) => {
const list = state.strategies.get('list').toArray();
diff --git a/frontend/src/component/strategies/show-strategy-component.js b/frontend/src/component/strategies/show-strategy-component.js
new file mode 100644
index 0000000000..9930252eb8
--- /dev/null
+++ b/frontend/src/component/strategies/show-strategy-component.js
@@ -0,0 +1,69 @@
+import React, { PropTypes, PureComponent } from 'react';
+import { Grid, Cell, List, ListItem, ListItemContent } from 'react-mdl';
+import { AppsLinkList, TogglesLinkList } from '../common';
+
+class ShowStrategyComponent extends PureComponent {
+ static propTypes () {
+ return {
+ toggles: PropTypes.array,
+ applications: PropTypes.array,
+ strategy: PropTypes.object.isRequired,
+ };
+ }
+
+ renderParameters (params) {
+ if (params) {
+ return params.map(({ name, type, description, required }, i) => (
+
+
+ {name} ({type})
+
+
+ ));
+ } else {
+ return (no params) ;
+ }
+ }
+
+ render () {
+ const {
+ strategy,
+ applications,
+ toggles,
+ } = this.props;
+
+ const {
+ parameters = [],
+ } = strategy;
+
+ return (
+
+
+
+
+ Parameters
+
+
+ {this.renderParameters(parameters)}
+
+ |
+
+
+ Applications using this strategy
+
+
+ |
+
+
+ Toggles using this strategy
+
+
+ |
+
+
+ );
+ }
+}
+
+
+export default ShowStrategyComponent;
diff --git a/frontend/src/component/strategies/strategy-details-component.jsx b/frontend/src/component/strategies/strategy-details-component.jsx
new file mode 100644
index 0000000000..dc5dba66a6
--- /dev/null
+++ b/frontend/src/component/strategies/strategy-details-component.jsx
@@ -0,0 +1,78 @@
+import React, { PropTypes, Component } from 'react';
+import { hashHistory } from 'react-router';
+import { Tabs, Tab, ProgressBar } from 'react-mdl';
+import ShowStrategy from './show-strategy-component';
+import EditStrategy from './edit-container';
+import { HeaderTitle } from '../common';
+
+const TABS = {
+ view: 0,
+ edit: 1,
+};
+
+export default class StrategyDetails extends Component {
+ static propTypes () {
+ return {
+ strategyName: PropTypes.string.isRequired,
+ toggles: PropTypes.array,
+ applications: PropTypes.array,
+ activeTab: PropTypes.string.isRequired,
+ strategy: PropTypes.object.isRequired,
+ fetchStrategies: PropTypes.func.isRequired,
+ fetchApplications: PropTypes.func.isRequired,
+ fetchFeatureToggles: PropTypes.func.isRequired,
+ };
+ }
+
+ componentDidMount () {
+ if (!this.props.strategy) {
+ this.props.fetchStrategies();
+ };
+ if (!this.props.applications || this.props.applications.length === 0) {
+ this.props.fetchApplications();
+ }
+ if (!this.props.toggles || this.props.toggles.length === 0) {
+ this.props.fetchFeatureToggles();
+ }
+ }
+
+ getTabContent (activeTabId) {
+ if (activeTabId === TABS.edit) {
+ return ;
+ } else {
+ return ( );
+ }
+ }
+
+ goToTab (tabName) {
+ hashHistory.push(`/strategies/${tabName}/${this.props.strategyName}`);
+ }
+
+ render () {
+ const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.view;
+ const strategy = this.props.strategy;
+ if (!strategy) {
+ return ;
+ }
+
+ const tabContent = this.getTabContent(activeTabId);
+
+ return (
+
+
+
+ this.goToTab('view')}>Details
+ this.goToTab('edit')}>Edit
+
+
+
+ );
+ }
+}
diff --git a/frontend/src/component/strategies/strategy-details-container.js b/frontend/src/component/strategies/strategy-details-container.js
new file mode 100644
index 0000000000..66e82d91d5
--- /dev/null
+++ b/frontend/src/component/strategies/strategy-details-container.js
@@ -0,0 +1,33 @@
+import { connect } from 'react-redux';
+import ShowStrategy from './strategy-details-component';
+import { fetchStrategies } from '../../store/strategy/actions';
+import { fetchAll } from '../../store/application/actions';
+import { fetchFeatureToggles } from '../../store/feature-actions';
+
+const mapStateToProps = (state, props) => {
+ let strategy = state.strategies
+ .get('list')
+ .find(n => n.name === props.strategyName);
+ const applications = state.applications
+ .get('list')
+ .filter(app => app.strategies.includes(props.strategyName));
+ const toggles = state.features
+ .filter(toggle =>
+ toggle.get('strategies').findIndex(s => s.name === props.strategyName) > -1);
+
+ return {
+ strategy,
+ strategyName: props.strategyName,
+ applications: applications && applications.toJS(),
+ toggles: toggles && toggles.toJS(),
+ activeTab: props.activeTab,
+ };
+};
+
+const Constainer = connect(mapStateToProps, {
+ fetchStrategies,
+ fetchApplications: fetchAll,
+ fetchFeatureToggles,
+})(ShowStrategy);
+
+export default Constainer;
diff --git a/frontend/src/component/user/user-component.jsx b/frontend/src/component/user/user-component.jsx
index 6d4d717df1..15692b13e8 100644
--- a/frontend/src/component/user/user-component.jsx
+++ b/frontend/src/component/user/user-component.jsx
@@ -6,6 +6,7 @@ class EditUserComponent extends React.Component {
return {
user: PropTypes.object.isRequired,
updateUserName: PropTypes.func.isRequired,
+ save: PropTypes.func.isRequired,
};
}
@@ -21,7 +22,7 @@ class EditUserComponent extends React.Component {
Action required
- You are logged in as:You hav to specify a username to use Unleash. This will allow us to track changes.
+ You hav to specify a username to use Unleash. This will allow us to track changes.