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:
parent
6de8c297fd
commit
1eb8fc0464
@ -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>
|
||||
|
47
frontend/src/component/common/permission-component.jsx
Normal file
47
frontend/src/component/common/permission-component.jsx
Normal 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;
|
8
frontend/src/component/common/permission-container.js
Normal file
8
frontend/src/component/common/permission-container.js
Normal 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;
|
@ -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"
|
||||
/>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
@ -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>
|
||||
))
|
||||
|
@ -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>
|
||||
|
@ -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 => {
|
||||
|
8
frontend/src/permissions.js
Normal file
8
frontend/src/permissions.js
Normal 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';
|
Loading…
Reference in New Issue
Block a user