1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat/update project access (#571)

* feat: add user guidance in project access tab

* feat: add role description to the menu list

* feat: add tooltip to delete button

* feat: add role description to add user menu

* feat: auto select user when there is only one option

* fix: refactor role select

* fix: remove minwidth from form control

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2022-01-17 11:41:44 +01:00 committed by GitHub
parent 21ba2d8b7c
commit 3a41de2246
5 changed files with 246 additions and 171 deletions

View File

@ -0,0 +1,33 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(theme => ({
pageContent: {
minHeight: '200px',
},
divider: {
height: '1px',
width: '106.65%',
marginLeft: '-2rem',
backgroundColor: '#efefef',
marginTop: '2rem',
},
actionList: {
display: 'flex',
alignItems: 'center',
},
inputLabel: { backgroundColor: '#fff' },
roleName: {
fontWeight: 'bold',
padding: '5px 0px',
},
iconButton: {
marginLeft: '0.5rem',
},
menuItem: {
width: '340px',
whiteSpace: 'normal',
},
projectRoleSelect: {
minWidth: '150px',
},
}));

View File

@ -1,6 +1,5 @@
/* eslint-disable react/jsx-no-target-blank */ /* eslint-disable react/jsx-no-target-blank */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { import {
Avatar, Avatar,
Button, Button,
@ -9,28 +8,30 @@ import {
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
InputLabel,
IconButton,
List, List,
ListItem, ListItem,
ListItemAvatar, ListItemAvatar,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
MenuItem, MenuItem,
Select,
FormControl,
} from '@material-ui/core'; } from '@material-ui/core';
import { Delete } from '@material-ui/icons'; import { Delete } from '@material-ui/icons';
import { Alert } from '@material-ui/lab'; import { Alert } from '@material-ui/lab';
import AddUserComponent from './access-add-user'; import AddUserComponent from '../access-add-user';
import projectApi from '../../store/project/api'; import projectApi from '../../../store/project/api';
import PageContent from '../common/PageContent'; import PageContent from '../../common/PageContent';
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import { useStyles } from './ProjectAccess.styles';
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
import { useParams } from 'react-router-dom';
import { IFeatureViewParams } from '../../../interfaces/params';
import ProjectRoleSelect from './ProjectRoleSelect/ProjectRoleSelect';
const ProjectAccess = () => {
function AccessComponent({ projectId, project }) { const { id } = useParams<IFeatureViewParams>();
const styles = useStyles();
const [roles, setRoles] = useState([]); const [roles, setRoles] = useState([]);
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [error, setError] = useState(); const [error, setError] = useState();
@ -39,13 +40,11 @@ function AccessComponent({ projectId, project }) {
useEffect(() => { useEffect(() => {
fetchAccess(); fetchAccess();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [projectId]); }, [id]);
const fetchAccess = async () => { const fetchAccess = async () => {
try { try {
const access = await projectApi.fetchAccess(projectId); const access = await projectApi.fetchAccess(id);
setRoles(access.roles); setRoles(access.roles);
setUsers( setUsers(
access.users.map(u => ({ ...u, name: u.name || '(No name)' })) access.users.map(u => ({ ...u, name: u.name || '(No name)' }))
@ -57,19 +56,24 @@ function AccessComponent({ projectId, project }) {
if (isOss()) { if (isOss()) {
return ( return (
<PageContent> <PageContent>
<Alert severity="error"> <Alert severity="error">
Controlling access to projects requires a paid version of Unleash. Controlling access to projects requires a paid version of
Check out <a href="https://www.getunleash.io" target="_blank">getunleash.io</a> to find out more. Unleash. Check out{' '}
</Alert> <a href="https://www.getunleash.io" target="_blank">
</PageContent>); getunleash.io
</a>{' '}
to find out more.
</Alert>
</PageContent>
);
} }
const handleRoleChange = (userId, currRoleId) => async evt => { const handleRoleChange = (userId, currRoleId) => async evt => {
const roleId = evt.target.value; const roleId = evt.target.value;
try { try {
await projectApi.removeUserFromRole(projectId, currRoleId, userId); await projectApi.removeUserFromRole(id, currRoleId, userId);
await projectApi.addUserToRole(projectId, roleId, userId); await projectApi.addUserToRole(id, roleId, userId);
const newUsers = users.map(u => { const newUsers = users.map(u => {
if (u.id === userId) { if (u.id === userId) {
return { ...u, roleId }; return { ...u, roleId };
@ -83,7 +87,7 @@ function AccessComponent({ projectId, project }) {
const addUser = async (userId, roleId) => { const addUser = async (userId, roleId) => {
try { try {
await projectApi.addUserToRole(projectId, roleId, userId); await projectApi.addUserToRole(id, roleId, userId);
await fetchAccess(); await fetchAccess();
} catch (err) { } catch (err) {
setError(err.message || 'Server problems when adding users.'); setError(err.message || 'Server problems when adding users.');
@ -92,7 +96,7 @@ function AccessComponent({ projectId, project }) {
const removeAccess = (userId, roleId) => async () => { const removeAccess = (userId, roleId) => async () => {
try { try {
await projectApi.removeUserFromRole(projectId, roleId, userId); await projectApi.removeUserFromRole(id, roleId, userId);
const newUsers = users.filter(u => u.id !== userId); const newUsers = users.filter(u => u.id !== userId);
setUsers(newUsers); setUsers(newUsers);
} catch (err) { } catch (err) {
@ -105,9 +109,7 @@ function AccessComponent({ projectId, project }) {
}; };
return ( return (
<PageContent <PageContent className={styles.pageContent}>
style={{ minHeight: '400px' }}
>
<AddUserComponent roles={roles} addUserToRole={addUser} /> <AddUserComponent roles={roles} addUserToRole={addUser} />
<Dialog <Dialog
open={!!error} open={!!error}
@ -131,15 +133,7 @@ function AccessComponent({ projectId, project }) {
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<div <div className={styles.divider}></div>
style={{
height: '1px',
width: '106.65%',
marginLeft: '-2rem',
backgroundColor: '#efefef',
marginTop: '2rem',
}}
></div>
<List> <List>
{users.map(user => { {users.map(user => {
const labelId = `checkbox-list-secondary-label-${user.id}`; const labelId = `checkbox-list-secondary-label-${user.id}`;
@ -154,51 +148,40 @@ function AccessComponent({ projectId, project }) {
secondary={user.email || user.username} secondary={user.email || user.username}
/> />
<ListItemSecondaryAction <ListItemSecondaryAction
style={{ className={styles.actionList}
display: 'flex',
alignItems: 'center',
}}
> >
<FormControl variant="outlined" size="small"> <ProjectRoleSelect
<InputLabel labelId={`role-${user.id}-select-label`}
style={{ backgroundColor: '#fff' }} id={`role-${user.id}-select`}
for="add-user-select-role-label" key={user.id}
> placeholder="Choose role"
Role onChange={handleRoleChange(
</InputLabel> user.id,
<Select user.roleId
labelId={`role-${user.id}-select-label`} )}
id={`role-${user.id}-select`} roles={roles}
key={user.id} value={user.roleId || ''}
placeholder="Choose role" >
value={user.roleId || ''} <MenuItem value="" disabled>
onChange={handleRoleChange( Choose role
user.id, </MenuItem>
user.roleId </ProjectRoleSelect>
)}
> <PermissionIconButton
<MenuItem value="" disabled> className={styles.iconButton}
Choose role
</MenuItem>
{roles.map(role => (
<MenuItem
key={`${user.id}:${role.id}`}
value={role.id}
>
{role.name}
</MenuItem>
))}
</Select>
</FormControl>
<IconButton
style={{ marginLeft: '0.5rem' }}
edge="end" edge="end"
aria-label="delete" aria-label="delete"
title="Remove access" title="Remove access"
onClick={removeAccess(user.id, user.roleId)} onClick={removeAccess(user.id, user.roleId)}
disabled={users.length === 1}
tooltip={
users.length === 1
? 'A project must have at least one owner'
: 'Remove acccess'
}
> >
<Delete /> <Delete />
</IconButton> </PermissionIconButton>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
); );
@ -206,11 +189,6 @@ function AccessComponent({ projectId, project }) {
</List> </List>
</PageContent> </PageContent>
); );
}
AccessComponent.propTypes = {
projectId: PropTypes.string.isRequired,
project: PropTypes.object,
}; };
export default AccessComponent; export default ProjectAccess;

View File

@ -0,0 +1,70 @@
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
import React from 'react';
import IRole from '../../../../interfaces/role';
import { useStyles } from '../ProjectAccess.styles';
interface IProjectRoleSelect {
roles: IRole[];
labelId: string;
id: string;
placeholder?: string;
onChange: () => void;
value: any;
}
const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
roles,
onChange,
labelId,
id,
value,
placeholder,
children,
}) => {
const styles = useStyles();
return (
<FormControl variant="outlined" size="small">
<InputLabel
style={{ backgroundColor: '#fff' }}
id="add-user-select-role-label"
>
Role
</InputLabel>
<Select
labelId={labelId}
id={id}
classes={{ root: styles.projectRoleSelect }}
placeholder={placeholder}
value={value || ''}
onChange={onChange}
renderValue={roleId => {
return roles?.find(role => {
return role.id === roleId;
}).name;
}}
>
{children}
{roles?.map(role => (
<MenuItem
key={role.id}
value={role.id}
classes={{
root: styles.menuItem,
}}
>
<div>
<span className={styles.roleName}>{role.name}</span>
<p>
{role.description ||
'No role description available.'}
</p>
</div>
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default ProjectRoleSelect;

View File

@ -2,24 +2,23 @@ import React, { useEffect, useState } from 'react';
import projectApi from '../../store/project/api'; import projectApi from '../../store/project/api';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Select,
MenuItem,
TextField, TextField,
CircularProgress, CircularProgress,
InputLabel,
FormControl,
Grid, Grid,
Button, Button,
InputAdornment, InputAdornment,
} from '@material-ui/core'; } from '@material-ui/core';
import { Search } from '@material-ui/icons'; import { Search } from '@material-ui/icons';
import Autocomplete from '@material-ui/lab/Autocomplete'; import Autocomplete from '@material-ui/lab/Autocomplete';
import { Alert } from '@material-ui/lab';
import ProjectRoleSelect from './ProjectAccess/ProjectRoleSelect/ProjectRoleSelect';
function AddUserComponent({ roles, addUserToRole }) { function AddUserComponent({ roles, addUserToRole }) {
const [user, setUser] = useState(); const [user, setUser] = useState();
const [role, setRole] = useState({}); const [role, setRole] = useState({});
const [options, setOptions] = useState([]); const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [select, setSelect] = useState(false);
useEffect(() => { useEffect(() => {
if (roles.length > 0) { if (roles.length > 0) {
@ -39,13 +38,17 @@ function AddUserComponent({ roles, addUserToRole }) {
} else { } else {
setOptions([]); setOptions([]);
} }
setLoading(false); setLoading(false);
}; };
const handleQueryUpdate = evt => { const handleQueryUpdate = evt => {
const q = evt.target.value; const q = evt.target.value;
search(q); search(q);
if (options.length === 1) {
setSelect(true);
return;
}
setSelect(false);
}; };
const handleSelectUser = (evt, value) => { const handleSelectUser = (evt, value) => {
@ -67,96 +70,85 @@ function AddUserComponent({ roles, addUserToRole }) {
}; };
return ( return (
<Grid container spacing={3} alignItems="flex-end"> <>
<Grid item> <Alert severity="info" style={{ marginBottom: '20px' }}>
<Autocomplete The user must have an Unleash root role before added to the
id="add-user-component" project.
style={{ width: 300 }} </Alert>
noOptionsText="No users found." <Grid container spacing={3} alignItems="flex-end">
onChange={handleSelectUser} <Grid item>
autoSelect={false} <Autocomplete
value={user || ''} id="add-user-component"
freeSolo style={{ width: 300 }}
getOptionSelected={() => true} noOptionsText="No users found."
filterOptions={o => o} onChange={handleSelectUser}
getOptionLabel={option => { autoSelect={select}
if (option) { value={user || ''}
return `${option.name || '(Empty name)'} <${ freeSolo
option.email || option.username getOptionSelected={() => true}
}>`; filterOptions={o => o}
} else return ''; getOptionLabel={option => {
}} if (option) {
options={options} return `${option.name || '(Empty name)'} <${
loading={loading} option.email || option.username
renderInput={params => ( }>`;
<TextField } else return '';
{...params} }}
label="User" options={options}
variant="outlined" loading={loading}
size="small" renderInput={params => (
name="search" <TextField
onChange={handleQueryUpdate} {...params}
InputProps={{ label="User"
...params.InputProps, variant="outlined"
startAdornment: ( size="small"
<InputAdornment position="start"> name="search"
<Search /> onChange={handleQueryUpdate}
</InputAdornment> InputProps={{
), ...params.InputProps,
endAdornment: ( startAdornment: (
<React.Fragment> <InputAdornment position="start">
{loading ? ( <Search />
<CircularProgress </InputAdornment>
color="inherit" ),
size={20} endAdornment: (
/> <React.Fragment>
) : null} {loading ? (
{params.InputProps.endAdornment} <CircularProgress
</React.Fragment> color="inherit"
), size={20}
}} />
/> ) : null}
)} {params.InputProps.endAdornment}
/> </React.Fragment>
</Grid> ),
<Grid item> }}
<FormControl />
variant="outlined" )}
size="small" />
style={{ minWidth: '125px' }} </Grid>
> <Grid item>
<InputLabel <ProjectRoleSelect
style={{ backgroundColor: '#fff' }}
id="add-user-select-role-label"
>
Role
</InputLabel>
<Select
labelId="add-user-select-role-label" labelId="add-user-select-role-label"
id="add-user-select-role" id="add-user-select-role"
placeholder="Project role" placeholder="Project role"
value={role.id || ''} value={role.id || ''}
onChange={handleRoleChange} onChange={handleRoleChange}
roles={roles}
/>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
disabled={!user}
onClick={handleSubmit}
> >
{roles.map(role => ( Add user
<MenuItem key={role.id} value={role.id}> </Button>
{role.name} </Grid>
</MenuItem>
))}
</Select>
</FormControl>
</Grid> </Grid>
<Grid item> </>
<Button
variant="contained"
color="primary"
disabled={!user}
onClick={handleSubmit}
>
Add user
</Button>
</Grid>
</Grid>
); );
} }

View File

@ -1,9 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Component from './access-component'; import Component from './ProjectAccess/ProjectAccess';
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const projectBase = { id: '', name: '', description: '' }; const projectBase = { id: '', name: '', description: '' };
const realProject = state.projects.toJS().find(n => n.id === props.projectId); const realProject = state.projects
.toJS()
.find(n => n.id === props.projectId);
const project = Object.assign(projectBase, realProject); const project = Object.assign(projectBase, realProject);
return { return {