mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
Feature Toggle View: reafactored tabs to be url based.
This commit is contained in:
parent
d63e05abc0
commit
61dd7680a4
@ -42,7 +42,7 @@ const Feature = ({
|
||||
<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}>
|
||||
<Link to={`/features/view/${name}`} className={style.link}>
|
||||
{name} <small>{shorten(description, 30) || ''}</small>
|
||||
</Link>
|
||||
</span>
|
||||
@ -54,7 +54,7 @@ const Feature = ({
|
||||
<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}>
|
||||
<Link to={`features/history/${name}`} title={`History htmlFor ${name}`} className={style.iconListItem}>
|
||||
<IconButton name="history" />
|
||||
</Link>
|
||||
<IconButton name="delete" onClick={() => onFeatureRemove(name)} className={style.iconListItem} />
|
||||
|
@ -27,14 +27,14 @@ const prepare = (methods, dispatch) => {
|
||||
// TODO: should add error handling
|
||||
requestUpdateFeatureToggle(input)(dispatch)
|
||||
.then(() => methods.clear())
|
||||
.then(() => window.history.back());
|
||||
.then(() => hashHistory.push(`/features/view/${input.name}`));
|
||||
}
|
||||
);
|
||||
|
||||
methods.onCancel = (evt) => {
|
||||
evt.preventDefault();
|
||||
methods.clear();
|
||||
hashHistory.push('/features');
|
||||
window.history.back();
|
||||
};
|
||||
|
||||
methods.addStrategy = (v) => {
|
||||
|
@ -3,64 +3,80 @@ import { Grid, Cell, Icon } from 'react-mdl';
|
||||
import Progress from './progress';
|
||||
import { AppsLinkList, SwitchWithLabel, calc } from '../common';
|
||||
|
||||
const MetricComponent = ({ metrics, featureToggle, toggleFeature }) => {
|
||||
const {
|
||||
|
||||
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);
|
||||
const lastHourPercent = 1 * calc(lastHour.yes, lastHour.yes + lastHour.no, 0);
|
||||
const lastMinutePercent = 1 * calc(lastMinute.yes, lastMinute.yes + lastMinute.no, 0);
|
||||
|
||||
return (<div>
|
||||
<SwitchWithLabel
|
||||
checked={featureToggle.enabled}
|
||||
onChange={() => toggleFeature(featureToggle)}>Toggle {featureToggle.name}</SwitchWithLabel>
|
||||
<hr />
|
||||
<Grid style={{ textAlign: 'center' }}>
|
||||
<Cell tablet={4} col={3} phone={12}>
|
||||
{
|
||||
lastMinute.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress animatePercentageText strokeWidth={10} percentage={lastMinutePercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last minute</strong><br /> Yes {lastMinute.yes}, No: {lastMinute.no}</p>
|
||||
</Cell>
|
||||
<Cell col={3} tablet={4} phone={12}>
|
||||
{
|
||||
lastHour.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={10} percentage={lastHourPercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last hour</strong><br /> Yes {lastHour.yes}, No: {lastHour.no}</p>
|
||||
</Cell>
|
||||
<Cell col={6} tablet={12}>
|
||||
{seenApps.length > 0 ?
|
||||
(<div><strong>Seen in applications:</strong></div>) :
|
||||
<div>
|
||||
return (<div>
|
||||
<SwitchWithLabel checked={featureToggle.enabled} onChange={() => toggleFeature(featureToggle)}>Toggle {featureToggle.name}</SwitchWithLabel>
|
||||
<hr />
|
||||
<Grid style={{ textAlign: 'center' }}>
|
||||
<Cell tablet={4} col={3} phone={12}>
|
||||
{
|
||||
lastMinute.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="Not used in a app in the last hour" />
|
||||
<div><small><strong>Not used in a app in the last hour.</strong>
|
||||
This might be due to your client implementation is not reporting usage.</small></div>
|
||||
</div>
|
||||
}
|
||||
<AppsLinkList apps={seenApps} />
|
||||
</Cell>
|
||||
</Grid>
|
||||
</div>);
|
||||
};
|
||||
|
||||
MetricComponent.propTypes = {
|
||||
metrics: PropTypes.object,
|
||||
featureToggle: PropTypes.object,
|
||||
toggleFeature: PropTypes.object,
|
||||
};
|
||||
|
||||
export default MetricComponent;
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress animatePercentageText strokeWidth={10} percentage={lastMinutePercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last minute</strong><br /> Yes {lastMinute.yes}, No: {lastMinute.no}</p>
|
||||
</Cell>
|
||||
<Cell col={3} tablet={4} phone={12}>
|
||||
{
|
||||
lastHour.isFallback ?
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="No metrics avaiable" /> :
|
||||
<div>
|
||||
<Progress strokeWidth={10} percentage={lastHourPercent} width="50" />
|
||||
</div>
|
||||
}
|
||||
<p><strong>Last hour</strong><br /> Yes {lastHour.yes}, No: {lastHour.no}</p>
|
||||
</Cell>
|
||||
<Cell col={6} tablet={12}>
|
||||
{seenApps.length > 0 ?
|
||||
(<div><strong>Seen in applications:</strong></div>) :
|
||||
<div>
|
||||
<Icon style={{ width: '100px', height: '100px', fontSize: '100px', color: '#ccc' }}
|
||||
name="report problem" title="Not used in a app in the last hour" />
|
||||
<div><small><strong>Not used in a app in the last hour.</strong>
|
||||
This might be due to your client implementation is not reporting usage.</small></div>
|
||||
</div>
|
||||
}
|
||||
<AppsLinkList apps={seenApps} />
|
||||
</Cell>
|
||||
</Grid>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
32
frontend/src/component/feature/metric-container.jsx
Normal file
32
frontend/src/component/feature/metric-container.jsx
Normal file
@ -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);
|
@ -1,25 +1,30 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import { Tabs, Tab, ProgressBar, List, ListItem, ListItemContent } from 'react-mdl';
|
||||
import { Link } from 'react-router';
|
||||
import { Tabs, Tab, ProgressBar } from 'react-mdl';
|
||||
import { hashHistory } from 'react-router';
|
||||
|
||||
import HistoryComponent from '../history/history-list-toggle-container';
|
||||
import MetricComponent from './metric-component';
|
||||
import MetricComponent from './metric-container';
|
||||
import EditFeatureToggle from './form-edit-container.jsx';
|
||||
import { getIcon } from '../common';
|
||||
|
||||
const TABS = {
|
||||
view: 0,
|
||||
edit: 1,
|
||||
history: 2,
|
||||
};
|
||||
|
||||
export default class ViewFeatureToggleComponent extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = { activeTab: 0 };
|
||||
}
|
||||
|
||||
static propTypes () {
|
||||
return {
|
||||
activeTab: PropTypes.string.isRequired,
|
||||
featureToggleName: PropTypes.string.isRequired,
|
||||
features: PropTypes.array.isRequired,
|
||||
fetchFeatureToggles: PropTypes.array.isRequired,
|
||||
featureToggle: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
@ -27,42 +32,44 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
if (this.props.features.length === 0) {
|
||||
this.props.fetchFeatureToggles();
|
||||
}
|
||||
this.props.fetchSeenApps();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
this.props.fetchFeatureMetrics();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.timer);
|
||||
getTabContent (activeTab) {
|
||||
const {
|
||||
featureToggle,
|
||||
featureToggleName,
|
||||
} = this.props;
|
||||
|
||||
if (TABS[activeTab] === TABS.history) {
|
||||
return <HistoryComponent toggleName={featureToggleName} />;
|
||||
} else if (TABS[activeTab] === TABS.edit) {
|
||||
return <EditFeatureToggle featureToggle={featureToggle} />;
|
||||
} else {
|
||||
return <MetricComponent featureToggle={featureToggle} />;
|
||||
}
|
||||
}
|
||||
|
||||
goToTab (tabName, featureToggleName) {
|
||||
hashHistory.push(`/features/${tabName}/${featureToggleName}`);
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
toggleFeature,
|
||||
featureToggle,
|
||||
features,
|
||||
activeTab,
|
||||
featureToggleName,
|
||||
metrics = {},
|
||||
} = this.props;
|
||||
|
||||
const featureToggle = features.find(toggle => toggle.name === featureToggleName);
|
||||
|
||||
if (!featureToggle) {
|
||||
if (features.length === 0 ) {
|
||||
return <ProgressBar indeterminate />;
|
||||
}
|
||||
return <span>Could not find the toggle "{this.props.featureToggleName}"</span>;
|
||||
return <span>Could not find the toggle "{featureToggleName}"</span>;
|
||||
}
|
||||
|
||||
let tabContent;
|
||||
if (this.state.activeTab === 0) {
|
||||
tabContent = <MetricComponent metrics={metrics} toggleFeature={toggleFeature} featureToggle={featureToggle} />;
|
||||
} else if (this.state.activeTab === 1) {
|
||||
tabContent = <EditFeatureToggle featureToggle={featureToggle} />;
|
||||
} else {
|
||||
tabContent = <HistoryComponent toggleName={featureToggleName} />;
|
||||
}
|
||||
const activeTabId = TABS[this.props.activeTab] ? TABS[this.props.activeTab] : TABS.view;
|
||||
const tabContent = this.getTabContent(activeTab);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -72,13 +79,10 @@ export default class ViewFeatureToggleComponent extends React.Component {
|
||||
</small>
|
||||
</h4>
|
||||
<div>{featureToggle.description}</div>
|
||||
<Tabs activeTab={this.state.activeTab}
|
||||
onChange={(tabId) => this.setState({ activeTab: tabId })}
|
||||
ripple
|
||||
style={{ marginBottom: '10px' }}>
|
||||
<Tab>Metrics</Tab>
|
||||
<Tab>Edit</Tab>
|
||||
<Tab>History</Tab>
|
||||
<Tabs activeTab={activeTabId} ripple style={{ marginBottom: '10px' }}>
|
||||
<Tab onClick={() => this.goToTab('view', featureToggleName)}>Metrics</Tab>
|
||||
<Tab onClick={() => this.goToTab('edit', featureToggleName)}>Edit</Tab>
|
||||
<Tab onClick={() => this.goToTab('history', featureToggleName)}>History</Tab>
|
||||
</Tabs>
|
||||
|
||||
{tabContent}
|
||||
|
14
frontend/src/component/feature/view-container.jsx
Normal file
14
frontend/src/component/feature/view-container.jsx
Normal file
@ -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);
|
@ -1,57 +0,0 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchFeatureToggles, toggleFeature } from '../../store/feature-actions';
|
||||
import { fetchFeatureMetrics, fetchSeenApps } from '../../store/feature-metrics-actions';
|
||||
import { fetchHistoryForToggle } from '../../store/history-actions';
|
||||
|
||||
import ViewToggleComponent from './view-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;
|
||||
}
|
||||
|
||||
function getHistoryFromToggle (state, toggleName) {
|
||||
if (!toggleName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (state.history.hasIn(['toggles', toggleName])) {
|
||||
return state.history
|
||||
.getIn(['toggles', toggleName])
|
||||
.slice(0, 10)
|
||||
.toJS()
|
||||
.map(({ createdAt, createdBy, type }) => ({
|
||||
createdAt: new Date(createdAt).toLocaleString('nb-NO'),
|
||||
createdBy,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
export default connect((state, props) => ({
|
||||
features: state.features.toJS(),
|
||||
metrics: getMetricsForToggle(state, props.featureToggleName),
|
||||
history: getHistoryFromToggle(state, props.featureToggleName),
|
||||
}), {
|
||||
fetchFeatureMetrics,
|
||||
fetchFeatureToggles,
|
||||
toggleFeature,
|
||||
fetchSeenApps,
|
||||
fetchHistoryForToggle,
|
||||
})(ViewToggleComponent);
|
@ -11,7 +11,7 @@ import App from './component/app';
|
||||
|
||||
import Features from './page/features';
|
||||
import CreateFeatureToggle from './page/features/create';
|
||||
import EditFeatureToggle from './page/features/edit';
|
||||
import ViewFeatureToggle from './page/features/show';
|
||||
import Strategies from './page/strategies';
|
||||
import StrategyView from './page/strategies/show';
|
||||
import CreateStrategies from './page/strategies/create';
|
||||
@ -39,7 +39,7 @@ ReactDOM.render(
|
||||
<Route pageTitle="Features" link="/features">
|
||||
<Route pageTitle="Features" path="/features" component={Features} />
|
||||
<Route pageTitle="New" path="/features/create" component={CreateFeatureToggle} />
|
||||
<Route pageTitle=":name" path="/features/edit/:name" component={EditFeatureToggle} />
|
||||
<Route pageTitle=":name" path="/features/:activeTab/:name" component={ViewFeatureToggle} />
|
||||
</Route>
|
||||
|
||||
<Route pageTitle="Strategies" link="/strategies">
|
||||
|
@ -1,16 +0,0 @@
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import EditFeatureToggleForm from '../../component/feature/view-edit-container';
|
||||
|
||||
export default class Features extends Component {
|
||||
static propTypes () {
|
||||
return {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<EditFeatureToggleForm featureToggleName={this.props.params.name} />
|
||||
);
|
||||
}
|
||||
};
|
17
frontend/src/page/features/show.js
Normal file
17
frontend/src/page/features/show.js
Normal file
@ -0,0 +1,17 @@
|
||||
import React, { PureComponent, PropTypes } from 'react';
|
||||
import ViewFeatureToggle from '../../component/feature/view-container';
|
||||
|
||||
export default class Features extends PureComponent {
|
||||
static propTypes () {
|
||||
return {
|
||||
params: PropTypes.object.isRequired,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
const { params } = this.props;
|
||||
return (
|
||||
<ViewFeatureToggle featureToggleName={params.name} activeTab={params.activeTab} />
|
||||
);
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user