1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-04 00:18:40 +01:00

feature: Add support for permission system in unleash frontend

This commit is contained in:
Benjamin Ludewig 2018-12-19 14:54:52 +01:00
parent 6de8c297fd
commit 1eb8fc0464
11 changed files with 244 additions and 61 deletions

View File

@ -22,6 +22,8 @@ import {
} from 'react-mdl';
import { IconLink, shorten, styles as commonStyles } from '../common';
import { formatFullDateTimeWithLocale } from '../common/util';
import { CREATE_FEATURE, CREATE_STRATEGY, UPDATE_APPLICATION } from '../../permissions';
import PermissionComponent from '../common/permission-container';
class StatefulTextfield extends Component {
static propTypes = {
@ -91,11 +93,26 @@ class ClientApplications extends PureComponent {
{seenToggles.map(
({ name, description, enabled, notFound }, i) =>
notFound ? (
<ListItem twoLine key={i}>
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
</ListItem>
<PermissionComponent
permission={CREATE_FEATURE}
component={
<ListItem twoLine key={i}>
<ListItemContent
icon={'report'}
subtitle={'Missing, want to create?'}
>
<Link to={`/features/create?name=${name}`}>{name}</Link>
</ListItemContent>
</ListItem>
}
otherwise={
<ListItem twoLine key={i}>
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
</ListItem>
}
/>
) : (
<ListItem twoLine key={i}>
<ListItemContent
@ -120,11 +137,26 @@ class ClientApplications extends PureComponent {
{strategies.map(
({ name, description, notFound }, i) =>
notFound ? (
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'report'} subtitle={'Missing, want to create?'}>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
</ListItem>
<PermissionComponent
permission={CREATE_STRATEGY}
component={
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent
icon={'report'}
subtitle={'Missing, want to create?'}
>
<Link to={`/strategies/create?name=${name}`}>{name}</Link>
</ListItemContent>
</ListItem>
}
otherwise={
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'report'} subtitle={'Missing'}>
{name}
</ListItemContent>
</ListItem>
}
/>
) : (
<ListItem twoLine key={`${name}-${i}`}>
<ListItemContent icon={'extension'} subtitle={shorten(description, 60)}>
@ -203,16 +235,21 @@ class ClientApplications extends PureComponent {
</CardMenu>
)}
<hr />
<Tabs
activeTab={this.state.activeTab}
onChange={tabId => this.setState({ activeTab: tabId })}
ripple
tabBarProps={{ style: { width: '100%' } }}
className="mdl-color--grey-100"
>
<Tab>Details</Tab>
<Tab>Edit</Tab>
</Tabs>
<PermissionComponent
permission={UPDATE_APPLICATION}
component={
<Tabs
activeTab={this.state.activeTab}
onChange={tabId => this.setState({ activeTab: tabId })}
ripple
tabBarProps={{ style: { width: '100%' } }}
className="mdl-color--grey-100"
>
<Tab>Details</Tab>
<Tab>Edit</Tab>
</Tabs>
}
/>
{content}
</Card>

View File

@ -0,0 +1,47 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ADMIN } from '../../permissions';
class PermissionComponent extends Component {
static propTypes = {
user: PropTypes.object,
component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
others: PropTypes.object,
denied: PropTypes.object,
granted: PropTypes.object,
otherwise: PropTypes.node,
permission: PropTypes.string,
children: PropTypes.node,
};
render() {
const { user, otherwise, component: Component, permission, granted, denied, children, ...others } = this.props;
let grantedComponent = Component;
let deniedCompoinent = otherwise || '';
if (granted || denied) {
grantedComponent = (
<Component {...others} {...granted || {}}>
{children}
</Component>
);
deniedCompoinent = (
<Component {...others} {...denied || {}}>
{children}
</Component>
);
}
if (!user) return deniedCompoinent;
if (
!user.permissions ||
user.permissions.indexOf(ADMIN) !== -1 ||
user.permissions.indexOf(permission) !== -1
) {
return grantedComponent;
}
return deniedCompoinent;
}
}
export default PermissionComponent;

View File

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import PermissionComponent from './permission-component';
const mapStateToProps = state => ({ user: state.user.get('profile') });
const Container = connect(mapStateToProps)(PermissionComponent);
export default Container;

View File

@ -22,7 +22,7 @@ exports[`renders correctly with one feature 1`] = `
>
<react-mdl-Switch
checked={false}
disabled={false}
disabled={true}
onChange={[Function]}
title="Toggle Another"
/>

View File

@ -14,6 +14,7 @@ const Feature = ({
metricsLastHour = { yes: 0, no: 0, isFallback: true },
metricsLastMinute = { yes: 0, no: 0, isFallback: true },
revive,
updateable,
}) => {
const { name, description, enabled, strategies } = feature;
const { showLastHour = false } = settings;
@ -42,7 +43,7 @@ const Feature = ({
</span>
<span className={styles.listItemToggle}>
<Switch
disabled={toggleFeature === undefined}
disabled={!updateable || toggleFeature === undefined}
title={`Toggle ${name}`}
key="left-actions"
onChange={() => toggleFeature(name)}
@ -59,7 +60,7 @@ const Feature = ({
{strategyChips}
{summaryChip}
</span>
{revive ? (
{updateable && revive ? (
<ListItemAction onClick={() => revive(feature.name)}>
<Icon name="undo" />
</ListItemAction>
@ -77,6 +78,7 @@ Feature.propTypes = {
metricsLastHour: PropTypes.object,
metricsLastMinute: PropTypes.object,
revive: PropTypes.func,
updateable: PropTypes.bool,
};
export default Feature;

View File

@ -5,6 +5,8 @@ import { Link } from 'react-router-dom';
import { Icon, FABButton, Textfield, Menu, MenuItem, Card, CardActions, List } from 'react-mdl';
import { MenuItemWithIcon, DropdownButton, styles as commonStyles } from '../common';
import styles from './feature.scss';
import { CREATE_FEATURE } from '../../permissions';
import PermissionComponent from '../common/permission-container';
export default class FeatureListComponent extends React.Component {
static propTypes = {
@ -62,11 +64,16 @@ export default class FeatureListComponent extends React.Component {
label="Search"
style={{ width: '100%' }}
/>
<Link to="/features/create" className={styles.toolbarButton}>
<FABButton accent title="Create feature toggle">
<Icon name="add" />
</FABButton>
</Link>
<PermissionComponent
permission={CREATE_FEATURE}
component={
<Link to="/features/create" className={styles.toolbarButton}>
<FABButton accent title="Create feature toggle">
<Icon name="add" />
</FABButton>
</Link>
}
/>
</div>
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
<CardActions>

View File

@ -8,6 +8,8 @@ import MetricComponent from './metric-container';
import EditFeatureToggle from './form/form-update-feature-container';
import ViewFeatureToggle from './form/form-view-feature-container';
import { styles as commonStyles } from '../common';
import { CREATE_FEATURE, DELETE_FEATURE, UPDATE_FEATURE } from '../../permissions';
import PermissionComponent from '../common/permission-container';
const TABS = {
strategies: 0,
@ -54,7 +56,17 @@ export default class ViewFeatureToggleComponent extends React.Component {
} else if (TABS[activeTab] === TABS.strategies) {
if (this.isFeatureView) {
return (
<EditFeatureToggle featureToggle={featureToggle} features={features} history={this.props.history} />
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<EditFeatureToggle
featureToggle={featureToggle}
features={features}
history={this.props.history}
/>
}
otherwise={<ViewFeatureToggle featureToggle={featureToggle} />}
/>
);
}
return <ViewFeatureToggle featureToggle={featureToggle} />;
@ -87,14 +99,20 @@ export default class ViewFeatureToggleComponent extends React.Component {
return (
<span>
Could not find the toggle{' '}
<Link
to={{
pathname: '/features/create',
query: { name: featureToggleName },
}}
>
{featureToggleName}
</Link>
<PermissionComponent
permission={CREATE_FEATURE}
component={
<Link
to={{
pathname: '/features/create',
query: { name: featureToggleName },
}}
>
{featureToggleName}
</Link>
}
otherwise={featureToggleName}
/>
</span>
);
}
@ -115,8 +133,8 @@ export default class ViewFeatureToggleComponent extends React.Component {
revive(featureToggle.name);
this.props.history.push('/features');
};
const updateFeatureToggle = () => {
let feature = { ...featureToggle };
const updateFeatureToggle = e => {
let feature = { ...featureToggle, description: e.target.value };
if (Array.isArray(feature.strategies)) {
feature.strategies.forEach(s => {
delete s.id;
@ -135,15 +153,22 @@ export default class ViewFeatureToggleComponent extends React.Component {
<CardTitle style={{ paddingTop: '24px', wordBreak: 'break-all' }}>{featureToggle.name}</CardTitle>
<CardText>
{this.isFeatureView ? (
<Textfield
<PermissionComponent
permission={UPDATE_FEATURE}
component={Textfield}
granted={{
onChange: v => setValue('description', v),
onBlur: updateFeatureToggle,
}}
denied={{
disabled: true,
}}
floatingLabel
style={{ width: '100%' }}
rows={1}
label="Description"
required
value={featureToggle.description}
onChange={v => setValue('description', v)}
onBlur={updateFeatureToggle}
/>
) : (
<Textfield
@ -167,24 +192,41 @@ export default class ViewFeatureToggleComponent extends React.Component {
}}
>
<span style={{ paddingRight: '24px' }}>
<Switch
disabled={!this.isFeatureView}
<PermissionComponent
permission={UPDATE_FEATURE}
component={Switch}
granted={{
disabled: !this.isFeatureView,
}}
denied={{
disabled: true,
}}
ripple
checked={featureToggle.enabled}
onChange={() => toggleFeature(featureToggle.name)}
>
{featureToggle.enabled ? 'Enabled' : 'Disabled'}
</Switch>
</PermissionComponent>
</span>
{this.isFeatureView ? (
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>
Archive
</Button>
<PermissionComponent
permission={DELETE_FEATURE}
component={
<Button onClick={removeToggle} style={{ flexShrink: 0 }}>
Archive
</Button>
}
/>
) : (
<Button onClick={reviveToggle} style={{ flexShrink: 0 }}>
Revive
</Button>
<PermissionComponent
permission={UPDATE_FEATURE}
component={
<Button onClick={reviveToggle} style={{ flexShrink: 0 }}>
Revive
</Button>
}
/>
)}
</CardActions>
<hr />

View File

@ -4,6 +4,8 @@ import { Link } from 'react-router-dom';
import { List, ListItem, ListItemContent, IconButton, Grid, Cell } from 'react-mdl';
import { HeaderTitle } from '../common';
import { CREATE_STRATEGY, DELETE_STRATEGY } from '../../permissions';
import PermissionComponent from '../common/permission-container';
class StrategiesListComponent extends Component {
static propTypes = {
@ -26,11 +28,16 @@ class StrategiesListComponent extends Component {
<HeaderTitle
title="Strategies"
actions={
<IconButton
raised
name="add"
onClick={() => this.props.history.push('/strategies/create')}
title="Add new strategy"
<PermissionComponent
permission={CREATE_STRATEGY}
component={
<IconButton
raised
name="add"
onClick={() => this.props.history.push('/strategies/create')}
title="Add new strategy"
/>
}
/>
}
/>
@ -46,7 +53,12 @@ class StrategiesListComponent extends Component {
{strategy.editable === false ? (
''
) : (
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
<PermissionComponent
permission={DELETE_STRATEGY}
component={
<IconButton name="delete" onClick={() => removeStrategy(strategy)} />
}
/>
)}
</ListItem>
))

View File

@ -4,6 +4,8 @@ import { Tabs, Tab, ProgressBar, Grid, Cell } from 'react-mdl';
import ShowStrategy from './show-strategy-component';
import EditStrategy from './edit-container';
import { HeaderTitle } from '../common';
import { UPDATE_STRATEGY } from '../../permissions';
import PermissionComponent from '../common/permission-container';
const TABS = {
view: 0,
@ -69,10 +71,15 @@ export default class StrategyDetails extends Component {
{strategy.editable === false ? (
''
) : (
<Tabs activeTab={activeTabId} ripple>
<Tab onClick={() => this.goToTab('view')}>Details</Tab>
<Tab onClick={() => this.goToTab('edit')}>Edit</Tab>
</Tabs>
<PermissionComponent
permission={UPDATE_STRATEGY}
component={
<Tabs activeTab={activeTabId} ripple>
<Tab onClick={() => this.goToTab('view')}>Details</Tab>
<Tab onClick={() => this.goToTab('edit')}>Edit</Tab>
</Tabs>
}
/>
)}
<section>

View File

@ -23,12 +23,25 @@ export class AuthenticationError extends Error {
}
}
export class ForbiddenError extends Error {
constructor(statusCode, body) {
super('You cannot perform this action');
this.name = 'ForbiddenError';
this.statusCode = statusCode;
this.body = body;
}
}
export function throwIfNotSuccess(response) {
if (!response.ok) {
if (response.status === 401) {
return new Promise((resolve, reject) => {
response.json().then(body => reject(new AuthenticationError(response.status, body)));
});
} else if (response.status === 403) {
return new Promise((resolve, reject) => {
response.json().then(body => reject(new ForbiddenError(response.status, body)));
});
} else if (response.status > 399 && response.status < 404) {
return new Promise((resolve, reject) => {
response.json().then(body => {

View File

@ -0,0 +1,8 @@
export const ADMIN = 'ADMIN';
export const CREATE_FEATURE = 'CREATE_FEATURE';
export const UPDATE_FEATURE = 'UPDATE_FEATURE';
export const DELETE_FEATURE = 'DELETE_FEATURE';
export const CREATE_STRATEGY = 'CREATE_STRATEGY';
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
export const DELETE_STRATEGY = 'DELETE_STRATEGY';
export const UPDATE_APPLICATION = 'UPDATE_APPLICATION';