mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-15 17:50:48 +02:00
feat/rbac: edit access for projects. (#251)
This commit is contained in:
parent
ae977b0bf1
commit
b9e6586c30
@ -42,6 +42,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@material-ui/core": "^4.11.3",
|
"@material-ui/core": "^4.11.3",
|
||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@material-ui/lab": "4.0.0-alpha.57",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"date-fns": "^2.17.0",
|
"date-fns": "^2.17.0",
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export const P = 'P';
|
export const P = 'P';
|
||||||
export const C = 'C';
|
export const C = 'C';
|
||||||
|
export const RBAC = 'RBAC';
|
||||||
|
@ -185,6 +185,12 @@ Array [
|
|||||||
"path": "/projects/edit/:id",
|
"path": "/projects/edit/:id",
|
||||||
"title": ":id",
|
"title": ":id",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"parent": "/projects",
|
||||||
|
"path": "/projects/:id/access",
|
||||||
|
"title": ":id",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"flag": "P",
|
"flag": "P",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { routes, baseRoutes, getRoute } from '../routes';
|
import { routes, baseRoutes, getRoute } from '../routes';
|
||||||
|
|
||||||
test('returns all defined routes', () => {
|
test('returns all defined routes', () => {
|
||||||
expect(routes.length).toEqual(33);
|
expect(routes.length).toEqual(34);
|
||||||
expect(routes).toMatchSnapshot();
|
expect(routes).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import LogoutFeatures from '../../page/user/logout';
|
|||||||
import ListProjects from '../../page/project';
|
import ListProjects from '../../page/project';
|
||||||
import CreateProject from '../../page/project/create';
|
import CreateProject from '../../page/project/create';
|
||||||
import EditProject from '../../page/project/edit';
|
import EditProject from '../../page/project/edit';
|
||||||
|
import EditProjectAccess from '../../page/project/access';
|
||||||
import ListTagTypes from '../../page/tag-types';
|
import ListTagTypes from '../../page/tag-types';
|
||||||
import CreateTagType from '../../page/tag-types/create';
|
import CreateTagType from '../../page/tag-types/create';
|
||||||
import EditTagType from '../../page/tag-types/edit';
|
import EditTagType from '../../page/tag-types/edit';
|
||||||
@ -156,6 +157,13 @@ export const routes = [
|
|||||||
title: ':id',
|
title: ':id',
|
||||||
component: EditProject,
|
component: EditProject,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/projects/:id/access',
|
||||||
|
parent: '/projects',
|
||||||
|
title: ':id',
|
||||||
|
component: EditProjectAccess,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/projects',
|
path: '/projects',
|
||||||
title: 'Projects',
|
title: 'Projects',
|
||||||
|
143
frontend/src/component/project/access-add-user.js
Normal file
143
frontend/src/component/project/access-add-user.js
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
CircularProgress,
|
||||||
|
InputLabel,
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
InputAdornment,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
|
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||||
|
|
||||||
|
function AddUserComponent({ roles, addUserToRole }) {
|
||||||
|
const [user, setUser] = useState();
|
||||||
|
const [role, setRole] = useState({});
|
||||||
|
const [options, setOptions] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (roles.length > 0) {
|
||||||
|
const regularRole = roles.find(r => r.name.toLowerCase() === 'regular');
|
||||||
|
setRole(regularRole || roles[0]);
|
||||||
|
}
|
||||||
|
}, [roles]);
|
||||||
|
|
||||||
|
const search = async q => {
|
||||||
|
if (q.length > 1) {
|
||||||
|
setLoading(true);
|
||||||
|
// TODO: Do not hard-code fetch here.
|
||||||
|
const response = await fetch(`api/admin/user-admin/search?q=${q}`);
|
||||||
|
const users = await response.json();
|
||||||
|
setOptions([...users]);
|
||||||
|
} else {
|
||||||
|
setOptions([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQueryUpdate = evt => {
|
||||||
|
const q = evt.target.value;
|
||||||
|
search(q);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectUser = (evt, value) => {
|
||||||
|
setOptions([]);
|
||||||
|
setUser(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleChange = evt => {
|
||||||
|
const roleId = +evt.target.value;
|
||||||
|
const role = roles.find(r => r.id === roleId);
|
||||||
|
setRole(role);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
await addUserToRole(user.id, role.id);
|
||||||
|
setUser(undefined);
|
||||||
|
setOptions([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container justify="center" spacing={3} alignItems="flex-end">
|
||||||
|
<Grid item>
|
||||||
|
<Autocomplete
|
||||||
|
id="add-user-component"
|
||||||
|
style={{ width: 300 }}
|
||||||
|
noOptionsText="No users found."
|
||||||
|
onChange={handleSelectUser}
|
||||||
|
autoSelect={false}
|
||||||
|
value={user || ''}
|
||||||
|
freeSolo
|
||||||
|
getOptionSelected={() => true}
|
||||||
|
filterOptions={o => o}
|
||||||
|
getOptionLabel={option => {
|
||||||
|
if (option) {
|
||||||
|
return `${option.name || '(Empty name)'} <${option.email || option.username}>`;
|
||||||
|
} else return '';
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
loading={loading}
|
||||||
|
renderInput={params => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
label="User"
|
||||||
|
name="search"
|
||||||
|
onChange={handleQueryUpdate}
|
||||||
|
InputProps={{
|
||||||
|
...params.InputProps,
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<Icon>search</Icon>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
endAdornment: (
|
||||||
|
<React.Fragment>
|
||||||
|
{loading ? <CircularProgress color="inherit" size={20} /> : null}
|
||||||
|
{params.InputProps.endAdornment}
|
||||||
|
</React.Fragment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<FormControl>
|
||||||
|
<InputLabel id="add-user-select-role-label">Role</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="add-user-select-role-label"
|
||||||
|
id="add-user-select-role"
|
||||||
|
placeholder="Project role"
|
||||||
|
value={role.id || ''}
|
||||||
|
onChange={handleRoleChange}
|
||||||
|
>
|
||||||
|
{roles.map(role => (
|
||||||
|
<MenuItem key={role.id} value={role.id}>
|
||||||
|
{role.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button variant="contained" color="primary" disabled={!user} onClick={handleSubmit}>
|
||||||
|
Add user
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AddUserComponent.propTypes = {
|
||||||
|
roles: PropTypes.array.isRequired,
|
||||||
|
addUserToRole: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUserComponent;
|
154
frontend/src/component/project/access-component.js
Normal file
154
frontend/src/component/project/access-component.js
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
Avatar,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContentText,
|
||||||
|
DialogContent,
|
||||||
|
Button,
|
||||||
|
} from '@material-ui/core';
|
||||||
|
|
||||||
|
import AddUserComponent from './access-add-user';
|
||||||
|
|
||||||
|
import projectApi from '../../store/project/api';
|
||||||
|
|
||||||
|
function AccessComponent({ projectId, project }) {
|
||||||
|
const [roles, setRoles] = useState([]);
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [error, setError] = useState();
|
||||||
|
|
||||||
|
const fetchAccess = async () => {
|
||||||
|
const access = await projectApi.fetchAccess(projectId);
|
||||||
|
setRoles(access.roles);
|
||||||
|
setUsers(access.users.map(u => ({ ...u, name: u.name || '(No name)' })));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAccess();
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return <p>....</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRoleChange = (userId, currRoleId) => async evt => {
|
||||||
|
const roleId = evt.target.value;
|
||||||
|
try {
|
||||||
|
await projectApi.removeUserFromRole(projectId, currRoleId, userId);
|
||||||
|
await projectApi.addUserToRole(projectId, roleId, userId);
|
||||||
|
const newUsers = users.map(u => {
|
||||||
|
if (u.id === userId) {
|
||||||
|
return { ...u, roleId };
|
||||||
|
} else return u;
|
||||||
|
});
|
||||||
|
setUsers(newUsers);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Server problems when adding users.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUser = async (userId, roleId) => {
|
||||||
|
try {
|
||||||
|
await projectApi.addUserToRole(projectId, roleId, userId);
|
||||||
|
await fetchAccess();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Server problems when adding users.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAccess = (userId, roleId) => async () => {
|
||||||
|
try {
|
||||||
|
await projectApi.removeUserFromRole(projectId, roleId, userId);
|
||||||
|
const newUsers = users.filter(u => u.id !== userId);
|
||||||
|
setUsers(newUsers);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || 'Server problems when adding users.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseError = () => {
|
||||||
|
setError(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={{ minHeight: '400px' }}>
|
||||||
|
<CardHeader title={`Managed Access for project "${project.name}"`} />
|
||||||
|
<AddUserComponent roles={roles} addUserToRole={addUser} />
|
||||||
|
<Dialog
|
||||||
|
open={!!error}
|
||||||
|
onClose={handleCloseError}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">{error}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleCloseError} color="secondary" autoFocus>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<List>
|
||||||
|
{users.map(user => {
|
||||||
|
const labelId = `checkbox-list-secondary-label-${user.id}`;
|
||||||
|
return (
|
||||||
|
<ListItem key={user.id} button>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar alt={user.name} src={user.imageUrl} />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText id={labelId} primary={user.name} secondary={user.email || user.username} />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Select
|
||||||
|
labelId={`role-${user.id}-select-label`}
|
||||||
|
id={`role-${user.id}-select`}
|
||||||
|
placeholder="Choose role"
|
||||||
|
value={user.roleId}
|
||||||
|
onChange={handleRoleChange(user.id, user.roleId)}
|
||||||
|
>
|
||||||
|
<MenuItem value="" disabled>
|
||||||
|
Choose role
|
||||||
|
</MenuItem>
|
||||||
|
{roles.map(role => (
|
||||||
|
<MenuItem key={`${user.id}:${role.id}`} value={role.id}>
|
||||||
|
{role.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<IconButton
|
||||||
|
edge="end"
|
||||||
|
aria-label="delete"
|
||||||
|
title="Remove access"
|
||||||
|
onClick={removeAccess(user.id, user.roleId)}
|
||||||
|
>
|
||||||
|
<Icon>delete</Icon>
|
||||||
|
</IconButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessComponent.propTypes = {
|
||||||
|
projectId: PropTypes.string.isRequired,
|
||||||
|
project: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessComponent;
|
18
frontend/src/component/project/access-container.js
Normal file
18
frontend/src/component/project/access-container.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Component from './access-component';
|
||||||
|
|
||||||
|
const mapStateToProps = (state, props) => {
|
||||||
|
const projectBase = { id: '', name: '', description: '' };
|
||||||
|
const realProject = state.projects.toJS().find(n => n.id === props.projectId);
|
||||||
|
const project = Object.assign(projectBase, realProject);
|
||||||
|
|
||||||
|
return {
|
||||||
|
project,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
const AccessContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||||
|
|
||||||
|
export default AccessContainer;
|
@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
|
|||||||
|
|
||||||
import { List, ListItem, ListItemAction, ListItemContent, IconButton, Card } from 'react-mdl';
|
import { List, ListItem, ListItemAction, ListItemContent, IconButton, Card } from 'react-mdl';
|
||||||
import { HeaderTitle, styles as commonStyles } from '../common';
|
import { HeaderTitle, styles as commonStyles } from '../common';
|
||||||
import { CREATE_PROJECT, DELETE_PROJECT } from '../../permissions';
|
import { CREATE_PROJECT, DELETE_PROJECT, UPDATE_PROJECT } from '../../permissions';
|
||||||
|
|
||||||
class ProjectListComponent extends Component {
|
class ProjectListComponent extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -13,6 +13,7 @@ class ProjectListComponent extends Component {
|
|||||||
removeProject: PropTypes.func.isRequired,
|
removeProject: PropTypes.func.isRequired,
|
||||||
history: PropTypes.object.isRequired,
|
history: PropTypes.object.isRequired,
|
||||||
hasPermission: PropTypes.func.isRequired,
|
hasPermission: PropTypes.func.isRequired,
|
||||||
|
rbacEnabled: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -25,7 +26,7 @@ class ProjectListComponent extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { projects, hasPermission } = this.props;
|
const { projects, hasPermission, rbacEnabled } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
<Card shadow={0} className={commonStyles.fullwidth} style={{ overflow: 'visible' }}>
|
||||||
@ -56,6 +57,13 @@ class ProjectListComponent extends Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</ListItemContent>
|
</ListItemContent>
|
||||||
<ListItemAction>
|
<ListItemAction>
|
||||||
|
{hasPermission(UPDATE_PROJECT) && rbacEnabled ? (
|
||||||
|
<Link to={`/projects/${project.id}/access`} style={{ color: 'black' }}>
|
||||||
|
<IconButton name="supervised_user_circle" title="Manage access" />
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
{hasPermission(DELETE_PROJECT) ? (
|
{hasPermission(DELETE_PROJECT) ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
name="delete"
|
name="delete"
|
||||||
|
@ -2,12 +2,15 @@ import { connect } from 'react-redux';
|
|||||||
import Component from './list-component.jsx';
|
import Component from './list-component.jsx';
|
||||||
import { fetchProjects, removeProject } from './../../store/project/actions';
|
import { fetchProjects, removeProject } from './../../store/project/actions';
|
||||||
import { hasPermission } from '../../permissions';
|
import { hasPermission } from '../../permissions';
|
||||||
|
import { RBAC } from '../common/flags';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const projects = state.projects.toJS();
|
const projects = state.projects.toJS();
|
||||||
|
const rbacEnabled = !!state.uiConfig.toJS().flags[RBAC];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
|
rbacEnabled,
|
||||||
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
hasPermission: hasPermission.bind(null, state.user.get('profile')),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
14
frontend/src/page/project/access.js
Normal file
14
frontend/src/page/project/access.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ProjectAccess from '../../component/project/access-container.js';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const render = ({ match: { params }, history }) => (
|
||||||
|
<ProjectAccess projectId={params.id} title="Edit project Access" history={history} />
|
||||||
|
);
|
||||||
|
|
||||||
|
render.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
history: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default render;
|
@ -55,7 +55,7 @@ export function throwIfNotSuccess(response) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
reject(new NotFoundError(response.status));
|
reject(new NotFoundError(response.status));
|
||||||
});
|
});
|
||||||
} else if (response.status > 399 && response.status < 499) {
|
} else if (response.status > 399 && response.status < 501) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
response.json().then(body => {
|
response.json().then(body => {
|
||||||
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
||||||
|
@ -8,6 +8,28 @@ function fetchAll() {
|
|||||||
.then(response => response.json());
|
.then(response => response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchAccess(projectId) {
|
||||||
|
return fetch(`${URI}/${projectId}/users`, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUserToRole(projectId, roleId, userId) {
|
||||||
|
return fetch(`${URI}/${projectId}/users/${userId}/roles/${roleId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeUserFromRole(projectId, roleId, userId) {
|
||||||
|
return fetch(`${URI}/${projectId}/users/${userId}/roles/${roleId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers,
|
||||||
|
credentials: 'include',
|
||||||
|
}).then(throwIfNotSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
function create(project) {
|
function create(project) {
|
||||||
return fetch(URI, {
|
return fetch(URI, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -49,4 +71,7 @@ export default {
|
|||||||
update,
|
update,
|
||||||
remove,
|
remove,
|
||||||
validate,
|
validate,
|
||||||
|
fetchAccess,
|
||||||
|
addUserToRole,
|
||||||
|
removeUserFromRole,
|
||||||
};
|
};
|
||||||
|
@ -1475,6 +1475,17 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.4.4"
|
"@babel/runtime" "^7.4.4"
|
||||||
|
|
||||||
|
"@material-ui/lab@4.0.0-alpha.57":
|
||||||
|
version "4.0.0-alpha.57"
|
||||||
|
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a"
|
||||||
|
integrity sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.4.4"
|
||||||
|
"@material-ui/utils" "^4.11.2"
|
||||||
|
clsx "^1.0.4"
|
||||||
|
prop-types "^15.7.2"
|
||||||
|
react-is "^16.8.0 || ^17.0.0"
|
||||||
|
|
||||||
"@material-ui/styles@^4.11.3":
|
"@material-ui/styles@^4.11.3":
|
||||||
version "4.11.3"
|
version "4.11.3"
|
||||||
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2"
|
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.3.tgz#1b8d97775a4a643b53478c895e3f2a464e8916f2"
|
||||||
|
Loading…
Reference in New Issue
Block a user