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

fix: project access (#621)

* feat: update useProjectApi hook

* fix: refactor to hooks

* fix: remove some ts errors

* fix: set message if error exists directly on response

* fix: remove console logs

* fix: typo

* delete: context2

* feat: filter added user from user add list

* fix: cleanup PR based on feedback

* fix: handle undefined roles in ProjectRoleSelect

* fix: use target value

* fix: type event

* fix: conflict

* fix: add appropriate types

* fix conflicts

* fix: explicit query

* fix: refactor list

* refactor: permission icon button

* fix: conflict

* fix: ts errors

* refactor: break list into its own component

* fix: use stringifed deps

* fix: explicit export

* fix: update pr according to comments

Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
Youssef Khedher 2022-02-09 12:25:02 +01:00 committed by GitHub
parent 9c2ac3e55b
commit 08c4b60cef
18 changed files with 618 additions and 382 deletions

View File

@ -15,6 +15,7 @@ interface IPaginateUIProps {
prevPage: () => void;
setPageIndex: (idx: number) => void;
nextPage: () => void;
style?: React.CSSProperties;
}
const PaginateUI = ({

View File

@ -3,13 +3,19 @@ import { useContext } from 'react';
import AccessContext from '../../../contexts/AccessContext';
interface IPermissionIconButtonProps
extends React.HTMLProps<HTMLButtonElement> {
extends React.DetailedHTMLProps<
React.HTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
permission: string;
Icon?: React.ElementType;
tooltip: string;
onClick?: (e: any) => void;
projectId?: string;
environmentId?: string;
edge?: string;
className?: string;
title?: string;
}
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({

View File

@ -12,7 +12,7 @@ import useQueryParams from '../../../hooks/useQueryParams';
import { useEffect } from 'react';
import useTabs from '../../../hooks/useTabs';
import TabPanel from '../../common/TabNav/TabPanel';
import ProjectAccess from '../access-container';
import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
import ProjectOverview from './ProjectOverview';
import ProjectHealth from './ProjectHealth/ProjectHealth';

View File

@ -11,18 +11,11 @@ export const useStyles = makeStyles(theme => ({
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',

View File

@ -1,71 +1,40 @@
/* eslint-disable react/jsx-no-target-blank */
import { useEffect, useState } from 'react';
import {
Avatar,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
List,
ListItem,
ListItemAvatar,
ListItemSecondaryAction,
ListItemText,
MenuItem,
} from '@material-ui/core';
import { Delete } from '@material-ui/icons';
import React, { useState } from 'react';
import { Alert } from '@material-ui/lab';
import AddUserComponent from '../access-add-user';
import { ProjectAccessAddUser } from './ProjectAccessAddUser/ProjectAccessAddUser';
import projectApi from '../../../store/project/api';
import PageContent from '../../common/PageContent';
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';
import { IProjectViewParams } from '../../../interfaces/params';
import usePagination from '../../../hooks/usePagination';
import PaginateUI from '../../common/PaginateUI/PaginateUI';
import useToast from '../../../hooks/useToast';
import ConfirmDialogue from '../../common/Dialogue';
import useProjectAccess, {
IProjectAccessUser,
} from '../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import useProjectApi from '../../../hooks/api/actions/useProjectApi/useProjectApi';
import HeaderTitle from '../../common/HeaderTitle';
import { ProjectAccessList } from './ProjectAccessList/ProjectAccessList';
const ProjectAccess = () => {
const { id } = useParams<IFeatureViewParams>();
export const ProjectAccess = () => {
const { id: projectId } = useParams<IProjectViewParams>();
const styles = useStyles();
const [roles, setRoles] = useState([]);
const [users, setUsers] = useState([]);
const [error, setError] = useState();
const { setToastData, setToastApiError } = useToast();
const { access, refetchProjectAccess } = useProjectAccess(projectId);
const { setToastData } = useToast();
const { isOss } = useUiConfig();
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
usePagination(users, 10);
usePagination(access.users, 10);
const { removeUserFromRole, addUserToRole } = useProjectApi();
const [showDelDialogue, setShowDelDialogue] = useState(false);
const [user, setUser] = useState({});
useEffect(() => {
fetchAccess();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
const fetchAccess = async () => {
try {
const access = await projectApi.fetchAccess(id);
setRoles(access.roles);
setUsers(
access.users.map(u => ({ ...u, name: u.name || '(No name)' }))
);
} catch (e) {
setToastApiError(e.toString());
}
};
const [user, setUser] = useState<IProjectAccessUser | undefined>();
if (isOss()) {
return (
<PageContent>
<PageContent headerContent={<HeaderTitle title="Project Access" />}>
<Alert severity="error">
Controlling access to projects requires a paid version of
Unleash. Check out{' '}
@ -78,58 +47,49 @@ const ProjectAccess = () => {
);
}
const handleRoleChange = (userId, currRoleId) => async evt => {
const roleId = evt.target.value;
try {
await projectApi.removeUserFromRole(id, currRoleId, userId);
await projectApi.addUserToRole(id, roleId, userId).then(() => {
const handleRoleChange =
(userId: number, currRoleId: number) =>
async (
evt: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => {
const roleId = Number(evt.target.value);
try {
await removeUserFromRole(projectId, currRoleId, userId);
await addUserToRole(projectId, roleId, userId);
refetchProjectAccess();
setToastData({
type: 'success',
title: 'User role changed successfully',
});
});
const newUsers = users.map(u => {
if (u.id === userId) {
return { ...u, roleId };
} else return u;
});
setUsers(newUsers);
} catch (err) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
});
}
} catch (err: any) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
});
}
};
const handleRemoveAccess = (user: IProjectAccessUser) => {
setUser(user);
setShowDelDialogue(true);
};
const addUser = async (userId, roleId) => {
try {
await projectApi.addUserToRole(id, roleId, userId);
await fetchAccess().then(() => {
setToastData({
type: 'success',
title: 'Successfully added user to the project',
});
});
} catch (err) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
});
}
};
const removeAccess = (user: IProjectAccessUser | undefined) => async () => {
if (!user) return;
const { id, roleId } = user;
const removeAccess = (userId: number, roleId: number) => async () => {
try {
await projectApi.removeUserFromRole(id, roleId, userId).then(() => {
setToastData({
type: 'success',
title: 'User have been removed from project',
});
await removeUserFromRole(projectId, roleId, id);
refetchProjectAccess();
setToastData({
type: 'success',
title: 'The user has been removed from project',
});
const newUsers = users.filter(u => u.id !== userId);
setUsers(newUsers);
} catch (err) {
} catch (err: any) {
setToastData({
type: 'error',
title: err.message || 'Server problems when adding users.',
@ -138,91 +98,20 @@ const ProjectAccess = () => {
setShowDelDialogue(false);
};
const handleCloseError = () => {
setError(undefined);
};
return (
<PageContent className={styles.pageContent}>
<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>
<div className={styles.divider}></div>
<List>
{page.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
className={styles.actionList}
>
<ProjectRoleSelect
labelId={`role-${user.id}-select-label`}
id={`role-${user.id}-select`}
key={user.id}
placeholder="Choose role"
onChange={handleRoleChange(
user.id,
user.roleId
)}
roles={roles}
value={user.roleId || ''}
>
<MenuItem value="" disabled>
Choose role
</MenuItem>
</ProjectRoleSelect>
<PageContent
headerContent={<HeaderTitle title="Project Roles"></HeaderTitle>}
className={styles.pageContent}
>
<ProjectAccessAddUser roles={access?.roles} />
<PermissionIconButton
className={styles.iconButton}
edge="end"
aria-label="delete"
title="Remove access"
onClick={() => {
setUser(user);
setShowDelDialogue(true);
}}
disabled={users.length === 1}
tooltip={
users.length === 1
? 'A project must have at least one owner'
: 'Remove access'
}
>
<Delete />
</PermissionIconButton>
</ListItemSecondaryAction>
</ListItem>
);
})}
<div className={styles.divider}></div>
<ProjectAccessList
handleRoleChange={handleRoleChange}
handleRemoveAccess={handleRemoveAccess}
page={page}
access={access}
>
<PaginateUI
pages={pages}
pageIndex={pageIndex}
@ -231,12 +120,13 @@ const ProjectAccess = () => {
prevPage={prevPage}
style={{ bottom: '-21px' }}
/>
</List>
</ProjectAccessList>
<ConfirmDialogue
open={showDelDialogue}
onClick={removeAccess(user.id, user.roleId)}
onClick={removeAccess(user)}
onClose={() => {
setUser({});
setUser(undefined);
setShowDelDialogue(false);
}}
title="Really remove user from this project"
@ -244,5 +134,3 @@ const ProjectAccess = () => {
</PageContent>
);
};
export default ProjectAccess;

View File

@ -0,0 +1,236 @@
import React, { ChangeEvent, useEffect, useState } from 'react';
import {
TextField,
CircularProgress,
Grid,
Button,
InputAdornment,
} from '@material-ui/core';
import { Search } from '@material-ui/icons';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { Alert } from '@material-ui/lab';
import { ProjectRoleSelect } from '../ProjectRoleSelect/ProjectRoleSelect';
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
import { useParams } from 'react-router-dom';
import useToast from '../../../../hooks/useToast';
import useProjectAccess, {
IProjectAccessUser,
} from '../../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import { IProjectRole } from '../../../../interfaces/role';
import ConditionallyRender from '../../../common/ConditionallyRender';
interface IProjectAccessAddUserProps {
roles: IProjectRole[];
}
export const ProjectAccessAddUser = ({ roles }: IProjectAccessAddUserProps) => {
const { id } = useParams<{ id: string }>();
const [user, setUser] = useState<IProjectAccessUser | undefined>();
const [role, setRole] = useState<IProjectRole | undefined>();
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const { setToastData } = useToast();
const { refetchProjectAccess, access } = useProjectAccess(id);
const { searchProjectUser, addUserToRole } = useProjectApi();
useEffect(() => {
if (roles.length > 0) {
const regularRole = roles.find(
r => r.name.toLowerCase() === 'regular'
);
setRole(regularRole || roles[0]);
}
}, [roles]);
const search = async (query: string) => {
if (query.length > 1) {
setLoading(true);
const result = await searchProjectUser(query);
const userSearchResults = await result.json();
const filteredUsers = userSearchResults.filter(
(selectedUser: IProjectAccessUser) => {
const selected = access.users.find(
(user: IProjectAccessUser) =>
user.id === selectedUser.id
);
return !selected;
}
);
setOptions(filteredUsers);
} else {
setOptions([]);
}
setLoading(false);
};
const handleQueryUpdate = (evt: { target: { value: string } }) => {
const q = evt.target.value;
search(q);
};
const handleBlur = () => {
if (options.length > 0) {
const user = options[0];
setUser(user);
}
};
const handleSelectUser = (
evt: ChangeEvent<{}>,
selectedUser: string | IProjectAccessUser | null
) => {
setOptions([]);
if (typeof selectedUser === 'string' || selectedUser === null) {
return;
}
if (selectedUser?.id) {
setUser(selectedUser);
}
};
const handleRoleChange = (
evt: React.ChangeEvent<{
name?: string | undefined;
value: unknown;
}>
) => {
const roleId = Number(evt.target.value);
const role = roles.find(role => role.id === roleId);
if (role) {
setRole(role);
}
};
const handleSubmit = async (evt: React.SyntheticEvent) => {
evt.preventDefault();
if (!role || !user) {
setToastData({
type: 'error',
title: 'Invalid selection',
text: `The selected user or role does not exist`,
});
return;
}
try {
await addUserToRole(id, role.id, user.id);
refetchProjectAccess();
setUser(undefined);
setOptions([]);
setToastData({
type: 'success',
title: 'Added user to project',
text: `User added to the project with the role of ${role.name}`,
});
} catch (e: any) {
let error;
if (
e
.toString()
.includes(`User already has access to project=${id}`)
) {
error = `User already has access to project ${id}`;
} else {
error = e.toString() || 'Server problems when adding users.';
}
setToastData({
type: 'error',
title: error,
});
}
};
const getOptionLabel = (option: IProjectAccessUser) => {
if (option) {
return `${option.name || '(Empty name)'} <${
option.email || option.username
}>`;
} else return '';
};
return (
<>
<Alert severity="info" style={{ marginBottom: '20px' }}>
The user must have an Unleash root role before added to the
project.
</Alert>
<Grid container spacing={3} alignItems="flex-end">
<Grid item>
<Autocomplete
id="add-user-component"
style={{ width: 300 }}
noOptionsText="No users found."
onChange={handleSelectUser}
onBlur={() => handleBlur()}
value={user || ''}
freeSolo
getOptionSelected={() => true}
filterOptions={o => o}
getOptionLabel={getOptionLabel}
options={options}
loading={loading}
renderInput={params => (
<TextField
{...params}
label="User"
variant="outlined"
size="small"
name="search"
onChange={handleQueryUpdate}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
endAdornment: (
<>
<ConditionallyRender
condition={loading}
show={
<CircularProgress
color="inherit"
size={20}
/>
}
/>
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
</Grid>
<Grid item>
<ProjectRoleSelect
labelId="add-user-select-role-label"
id="add-user-select-role"
placeholder="Project role"
value={role?.id || -1}
onChange={handleRoleChange}
roles={roles}
/>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
disabled={!user}
onClick={handleSubmit}
>
Add user
</Button>
</Grid>
</Grid>
</>
);
};

View File

@ -0,0 +1,64 @@
import { List } from '@material-ui/core';
import {
IProjectAccessOutput,
IProjectAccessUser,
} from '../../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import { ProjectAccessListItem } from './ProjectAccessListItem/ProjectAccessListItem';
interface IProjectAccesListProps {
page: IProjectAccessUser[];
handleRoleChange: (
userId: number,
currRoleId: number
) => (
evt: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => void;
handleRemoveAccess: (user: IProjectAccessUser) => void;
access: IProjectAccessOutput;
}
export const ProjectAccessList: React.FC<IProjectAccesListProps> = ({
page,
access,
handleRoleChange,
handleRemoveAccess,
children,
}) => {
const sortUsers = (users: IProjectAccessUser[]): IProjectAccessUser[] => {
/* This should be done on the API side in the future,
we should expect the list of users to come in the
same order each time and not jump around on the screen*/
return users.sort(
(userA: IProjectAccessUser, userB: IProjectAccessUser) => {
if (!userA.name) {
return -1;
} else if (!userB.name) {
return 1;
}
return userA.name.localeCompare(userB.name);
}
);
};
return (
<List>
{sortUsers(page).map(user => {
return (
<ProjectAccessListItem
key={user.id}
user={user}
access={access}
handleRoleChange={handleRoleChange}
handleRemoveAccess={handleRemoveAccess}
/>
);
})}
{children}
</List>
);
};

View File

@ -0,0 +1,11 @@
import { makeStyles } from '@material-ui/core/styles';
export const useStyles = makeStyles(() => ({
iconButton: {
marginLeft: '0.5rem',
},
actionList: {
display: 'flex',
alignItems: 'center',
},
}));

View File

@ -0,0 +1,93 @@
import {
ListItem,
ListItemAvatar,
Avatar,
ListItemText,
ListItemSecondaryAction,
MenuItem,
} from '@material-ui/core';
import { Delete } from '@material-ui/icons';
import { useParams } from 'react-router-dom';
import {
IProjectAccessUser,
IProjectAccessOutput,
} from '../../../../../hooks/api/getters/useProjectAccess/useProjectAccess';
import { IProjectViewParams } from '../../../../../interfaces/params';
import PermissionIconButton from '../../../../common/PermissionIconButton/PermissionIconButton';
import { UPDATE_PROJECT } from '../../../../providers/AccessProvider/permissions';
import { ProjectRoleSelect } from '../../ProjectRoleSelect/ProjectRoleSelect';
import { useStyles } from '../ProjectAccessListItem/ProjectAccessListItem.styles';
interface IProjectAccessListItemProps {
user: IProjectAccessUser;
handleRoleChange: (
userId: number,
currRoleId: number
) => (
evt: React.ChangeEvent<{
name?: string;
value: unknown;
}>
) => void;
handleRemoveAccess: (user: IProjectAccessUser) => void;
access: IProjectAccessOutput;
}
export const ProjectAccessListItem = ({
user,
access,
handleRoleChange,
handleRemoveAccess,
}: IProjectAccessListItemProps) => {
const { id: projectId } = useParams<IProjectViewParams>();
const styles = useStyles();
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 className={styles.actionList}>
<ProjectRoleSelect
labelId={`role-${user.id}-select-label`}
id={`role-${user.id}-select`}
key={user.id}
placeholder="Choose role"
onChange={handleRoleChange(user.id, user.roleId)}
roles={access.roles}
value={user.roleId || -1}
>
<MenuItem value="" disabled>
Choose role
</MenuItem>
</ProjectRoleSelect>
<PermissionIconButton
permission={UPDATE_PROJECT}
projectId={projectId}
className={styles.iconButton}
edge="end"
aria-label="delete"
title="Remove access"
onClick={() => {
handleRemoveAccess(user);
}}
disabled={access.users.length === 1}
tooltip={
access.users.length === 1
? 'A project must have at least one owner'
: 'Remove access'
}
>
<Delete />
</PermissionIconButton>
</ListItemSecondaryAction>
</ListItem>
);
};

View File

@ -1,19 +1,24 @@
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
import React from 'react';
import IRole from '../../../../interfaces/role';
import { IProjectRole } from '../../../../interfaces/role';
import { useStyles } from '../ProjectAccess.styles';
interface IProjectRoleSelect {
roles: IRole[];
roles: IProjectRole[];
labelId: string;
id: string;
placeholder?: string;
onChange: () => void;
onChange: (
evt: React.ChangeEvent<{
name?: string | undefined;
value: unknown;
}>
) => void;
value: any;
}
const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
export const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
roles,
onChange,
labelId,
@ -39,9 +44,10 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
value={value || ''}
onChange={onChange}
renderValue={roleId => {
return roles?.find(role => {
const role = roles?.find(role => {
return role.id === roleId;
}).name;
});
return role?.name || '';
}}
>
{children}
@ -66,5 +72,3 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
</FormControl>
);
};
export default ProjectRoleSelect;

View File

@ -1,162 +0,0 @@
import React, { useEffect, useState } from 'react';
import projectApi from '../../store/project/api';
import PropTypes from 'prop-types';
import {
TextField,
CircularProgress,
Grid,
Button,
InputAdornment,
} from '@material-ui/core';
import { Search } from '@material-ui/icons';
import Autocomplete from '@material-ui/lab/Autocomplete';
import { Alert } from '@material-ui/lab';
import ProjectRoleSelect from './ProjectAccess/ProjectRoleSelect/ProjectRoleSelect';
function AddUserComponent({ roles, addUserToRole }) {
const [user, setUser] = useState();
const [role, setRole] = useState({});
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
const [select, setSelect] = 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 users = await projectApi.searchProjectUser(q);
setOptions([...users]);
} else {
setOptions([]);
}
setLoading(false);
};
const handleQueryUpdate = evt => {
const q = evt.target.value;
search(q);
if (options.length === 1) {
setSelect(true);
return;
}
setSelect(false);
};
const handleSelectUser = (evt, selectedUser) => {
setOptions([]);
if (selectedUser?.id) {
setUser(selectedUser);
}
};
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 (
<>
<Alert severity="info" style={{ marginBottom: '20px' }}>
The user must have an Unleash root role before added to the
project.
</Alert>
<Grid container spacing={3} alignItems="flex-end">
<Grid item>
<Autocomplete
id="add-user-component"
style={{ width: 300 }}
noOptionsText="No users found."
onChange={handleSelectUser}
autoSelect={select}
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"
variant="outlined"
size="small"
name="search"
onChange={handleQueryUpdate}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress
color="inherit"
size={20}
/>
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
</Grid>
<Grid item>
<ProjectRoleSelect
labelId="add-user-select-role-label"
id="add-user-select-role"
placeholder="Project role"
value={role.id || ''}
onChange={handleRoleChange}
roles={roles}
/>
</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;

View File

@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import Component from './ProjectAccess/ProjectAccess';
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;

View File

@ -163,14 +163,19 @@ const useAPI = ({
if (res.status > 399) {
const response = await res.json();
if (response?.details?.length > 0) {
if (response?.details?.length > 0 && propagateErrors) {
const error = response.details[0];
if (propagateErrors) {
throw new Error(error.message);
throw new Error(error.message || error.msg);
}
return error;
}
if (response?.length > 0 && propagateErrors) {
const error = response[0];
throw new Error(error.message || error.msg);
}
if (propagateErrors) {
throw new Error('Action could not be performed');
}

View File

@ -107,6 +107,54 @@ const useProjectApi = () => {
}
};
const addUserToRole = async (
projectId: string,
roleId: number,
userId: number
) => {
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
const req = createRequest(path, { method: 'POST' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const removeUserFromRole = async (
projectId: string,
roleId: number,
userId: number
) => {
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
const req = createRequest(path, { method: 'DELETE' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
const searchProjectUser = async (query: string): Promise<Response> => {
const path = `api/admin/user-admin/search?q=${query}`;
const req = createRequest(path, { method: 'GET' });
try {
const res = await makeRequest(req.caller, req.id);
return res;
} catch (e) {
throw e;
}
};
return {
createProject,
validateId,
@ -114,8 +162,11 @@ const useProjectApi = () => {
deleteProject,
addEnvironmentToProject,
removeEnvironmentFromProject,
addUserToRole,
removeUserFromRole,
errors,
loading,
searchProjectUser,
};
};

View File

@ -0,0 +1,61 @@
import useSWR, { mutate, SWRConfiguration } from 'swr';
import { useState, useEffect } from 'react';
import { formatApiPath } from '../../../../utils/format-path';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IProjectRole } from '../../../../interfaces/role';
export interface IProjectAccessUser {
id: number;
imageUrl: string;
isAPI: boolean;
roleId: number;
username?: string;
name?: string;
email?: string;
}
export interface IProjectAccessOutput {
users: IProjectAccessUser[];
roles: IProjectRole[];
}
const useProjectAccess = (
projectId: string,
options: SWRConfiguration = {}
) => {
const path = formatApiPath(`api/admin/projects/${projectId}/users`);
const fetcher = () => {
return fetch(path, {
method: 'GET',
})
.then(handleErrorResponses('project access'))
.then(res => res.json());
};
const CACHE_KEY = `api/admin/projects/${projectId}/users`;
const { data, error } = useSWR<IProjectAccessOutput>(
CACHE_KEY,
fetcher,
options
);
const [loading, setLoading] = useState(!error && !data);
const refetchProjectAccess = () => {
mutate(CACHE_KEY);
};
useEffect(() => {
setLoading(!error && !data);
}, [data, error]);
return {
access: data ? data : { roles: [], users: [] },
error,
loading,
refetchProjectAccess,
};
};
export default useProjectAccess;

View File

@ -19,7 +19,7 @@ const usePagination = (
const result = paginate(dataToPaginate, limit);
setPaginatedData(result);
/* eslint-disable-next-line */
}, [data, limit]);
}, [JSON.stringify(data), limit]);
const nextPage = () => {
if (pageIndex < paginatedData.length - 1) {

View File

@ -3,3 +3,7 @@ export interface IFeatureViewParams {
featureId: string;
activeTab: string;
}
export interface IProjectViewParams {
id: string;
}

View File

@ -10,6 +10,7 @@ export interface IProjectRole {
id: number;
name: string;
description: string;
type: string;
}
export default IRole;