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:
parent
9c2ac3e55b
commit
08c4b60cef
@ -15,6 +15,7 @@ interface IPaginateUIProps {
|
|||||||
prevPage: () => void;
|
prevPage: () => void;
|
||||||
setPageIndex: (idx: number) => void;
|
setPageIndex: (idx: number) => void;
|
||||||
nextPage: () => void;
|
nextPage: () => void;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PaginateUI = ({
|
const PaginateUI = ({
|
||||||
|
@ -3,13 +3,19 @@ import { useContext } from 'react';
|
|||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
|
||||||
interface IPermissionIconButtonProps
|
interface IPermissionIconButtonProps
|
||||||
extends React.HTMLProps<HTMLButtonElement> {
|
extends React.DetailedHTMLProps<
|
||||||
|
React.HTMLAttributes<HTMLButtonElement>,
|
||||||
|
HTMLButtonElement
|
||||||
|
> {
|
||||||
permission: string;
|
permission: string;
|
||||||
Icon?: React.ElementType;
|
Icon?: React.ElementType;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
onClick?: (e: any) => void;
|
onClick?: (e: any) => void;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
environmentId?: string;
|
environmentId?: string;
|
||||||
|
edge?: string;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
|
const PermissionIconButton: React.FC<IPermissionIconButtonProps> = ({
|
||||||
|
@ -12,7 +12,7 @@ import useQueryParams from '../../../hooks/useQueryParams';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useTabs from '../../../hooks/useTabs';
|
import useTabs from '../../../hooks/useTabs';
|
||||||
import TabPanel from '../../common/TabNav/TabPanel';
|
import TabPanel from '../../common/TabNav/TabPanel';
|
||||||
import ProjectAccess from '../access-container';
|
import { ProjectAccess } from '../ProjectAccess/ProjectAccess';
|
||||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
||||||
import ProjectOverview from './ProjectOverview';
|
import ProjectOverview from './ProjectOverview';
|
||||||
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
||||||
|
@ -11,18 +11,11 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
backgroundColor: '#efefef',
|
backgroundColor: '#efefef',
|
||||||
marginTop: '2rem',
|
marginTop: '2rem',
|
||||||
},
|
},
|
||||||
actionList: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
inputLabel: { backgroundColor: '#fff' },
|
inputLabel: { backgroundColor: '#fff' },
|
||||||
roleName: {
|
roleName: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
padding: '5px 0px',
|
padding: '5px 0px',
|
||||||
},
|
},
|
||||||
iconButton: {
|
|
||||||
marginLeft: '0.5rem',
|
|
||||||
},
|
|
||||||
menuItem: {
|
menuItem: {
|
||||||
width: '340px',
|
width: '340px',
|
||||||
whiteSpace: 'normal',
|
whiteSpace: 'normal',
|
||||||
|
@ -1,71 +1,40 @@
|
|||||||
/* eslint-disable react/jsx-no-target-blank */
|
/* eslint-disable react/jsx-no-target-blank */
|
||||||
import { useEffect, useState } from 'react';
|
import React, { 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 { Alert } from '@material-ui/lab';
|
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 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 { useStyles } from './ProjectAccess.styles';
|
||||||
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { IFeatureViewParams } from '../../../interfaces/params';
|
import { IProjectViewParams } from '../../../interfaces/params';
|
||||||
import ProjectRoleSelect from './ProjectRoleSelect/ProjectRoleSelect';
|
|
||||||
import usePagination from '../../../hooks/usePagination';
|
import usePagination from '../../../hooks/usePagination';
|
||||||
import PaginateUI from '../../common/PaginateUI/PaginateUI';
|
import PaginateUI from '../../common/PaginateUI/PaginateUI';
|
||||||
import useToast from '../../../hooks/useToast';
|
import useToast from '../../../hooks/useToast';
|
||||||
import ConfirmDialogue from '../../common/Dialogue';
|
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 = () => {
|
export const ProjectAccess = () => {
|
||||||
const { id } = useParams<IFeatureViewParams>();
|
const { id: projectId } = useParams<IProjectViewParams>();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [roles, setRoles] = useState([]);
|
const { access, refetchProjectAccess } = useProjectAccess(projectId);
|
||||||
const [users, setUsers] = useState([]);
|
const { setToastData } = useToast();
|
||||||
const [error, setError] = useState();
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
|
||||||
const { isOss } = useUiConfig();
|
const { isOss } = useUiConfig();
|
||||||
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
const { page, pages, nextPage, prevPage, setPageIndex, pageIndex } =
|
||||||
usePagination(users, 10);
|
usePagination(access.users, 10);
|
||||||
|
const { removeUserFromRole, addUserToRole } = useProjectApi();
|
||||||
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
const [showDelDialogue, setShowDelDialogue] = useState(false);
|
||||||
const [user, setUser] = useState({});
|
const [user, setUser] = useState<IProjectAccessUser | undefined>();
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isOss()) {
|
if (isOss()) {
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent headerContent={<HeaderTitle title="Project Access" />}>
|
||||||
<Alert severity="error">
|
<Alert severity="error">
|
||||||
Controlling access to projects requires a paid version of
|
Controlling access to projects requires a paid version of
|
||||||
Unleash. Check out{' '}
|
Unleash. Check out{' '}
|
||||||
@ -78,23 +47,25 @@ const ProjectAccess = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRoleChange = (userId, currRoleId) => async evt => {
|
const handleRoleChange =
|
||||||
const roleId = evt.target.value;
|
(userId: number, currRoleId: number) =>
|
||||||
|
async (
|
||||||
|
evt: React.ChangeEvent<{
|
||||||
|
name?: string;
|
||||||
|
value: unknown;
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const roleId = Number(evt.target.value);
|
||||||
try {
|
try {
|
||||||
await projectApi.removeUserFromRole(id, currRoleId, userId);
|
await removeUserFromRole(projectId, currRoleId, userId);
|
||||||
await projectApi.addUserToRole(id, roleId, userId).then(() => {
|
await addUserToRole(projectId, roleId, userId);
|
||||||
|
refetchProjectAccess();
|
||||||
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'User role changed successfully',
|
title: 'User role changed successfully',
|
||||||
});
|
});
|
||||||
});
|
} catch (err: any) {
|
||||||
const newUsers = users.map(u => {
|
|
||||||
if (u.id === userId) {
|
|
||||||
return { ...u, roleId };
|
|
||||||
} else return u;
|
|
||||||
});
|
|
||||||
setUsers(newUsers);
|
|
||||||
} catch (err) {
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: err.message || 'Server problems when adding users.',
|
title: err.message || 'Server problems when adding users.',
|
||||||
@ -102,34 +73,23 @@ const ProjectAccess = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addUser = async (userId, roleId) => {
|
const handleRemoveAccess = (user: IProjectAccessUser) => {
|
||||||
try {
|
setUser(user);
|
||||||
await projectApi.addUserToRole(id, roleId, userId);
|
setShowDelDialogue(true);
|
||||||
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 = (userId: number, roleId: number) => async () => {
|
const removeAccess = (user: IProjectAccessUser | undefined) => async () => {
|
||||||
|
if (!user) return;
|
||||||
|
const { id, roleId } = user;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await projectApi.removeUserFromRole(id, roleId, userId).then(() => {
|
await removeUserFromRole(projectId, roleId, id);
|
||||||
|
refetchProjectAccess();
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'User have been removed from project',
|
title: 'The user has been removed from project',
|
||||||
});
|
});
|
||||||
});
|
} catch (err: any) {
|
||||||
const newUsers = users.filter(u => u.id !== userId);
|
|
||||||
setUsers(newUsers);
|
|
||||||
} catch (err) {
|
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: err.message || 'Server problems when adding users.',
|
title: err.message || 'Server problems when adding users.',
|
||||||
@ -138,91 +98,20 @@ const ProjectAccess = () => {
|
|||||||
setShowDelDialogue(false);
|
setShowDelDialogue(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseError = () => {
|
|
||||||
setError(undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent className={styles.pageContent}>
|
<PageContent
|
||||||
<AddUserComponent roles={roles} addUserToRole={addUser} />
|
headerContent={<HeaderTitle title="Project Roles"></HeaderTitle>}
|
||||||
<Dialog
|
className={styles.pageContent}
|
||||||
open={!!error}
|
|
||||||
onClose={handleCloseError}
|
|
||||||
aria-labelledby="alert-dialog-title"
|
|
||||||
aria-describedby="alert-dialog-description"
|
|
||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">{'Error'}</DialogTitle>
|
<ProjectAccessAddUser roles={access?.roles} />
|
||||||
<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>
|
<div className={styles.divider}></div>
|
||||||
<List>
|
<ProjectAccessList
|
||||||
{page.map(user => {
|
handleRoleChange={handleRoleChange}
|
||||||
const labelId = `checkbox-list-secondary-label-${user.id}`;
|
handleRemoveAccess={handleRemoveAccess}
|
||||||
return (
|
page={page}
|
||||||
<ListItem key={user.id} button>
|
access={access}
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<PaginateUI
|
<PaginateUI
|
||||||
pages={pages}
|
pages={pages}
|
||||||
pageIndex={pageIndex}
|
pageIndex={pageIndex}
|
||||||
@ -231,12 +120,13 @@ const ProjectAccess = () => {
|
|||||||
prevPage={prevPage}
|
prevPage={prevPage}
|
||||||
style={{ bottom: '-21px' }}
|
style={{ bottom: '-21px' }}
|
||||||
/>
|
/>
|
||||||
</List>
|
</ProjectAccessList>
|
||||||
|
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue
|
||||||
open={showDelDialogue}
|
open={showDelDialogue}
|
||||||
onClick={removeAccess(user.id, user.roleId)}
|
onClick={removeAccess(user)}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setUser({});
|
setUser(undefined);
|
||||||
setShowDelDialogue(false);
|
setShowDelDialogue(false);
|
||||||
}}
|
}}
|
||||||
title="Really remove user from this project"
|
title="Really remove user from this project"
|
||||||
@ -244,5 +134,3 @@ const ProjectAccess = () => {
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectAccess;
|
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(() => ({
|
||||||
|
iconButton: {
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
},
|
||||||
|
actionList: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}));
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,19 +1,24 @@
|
|||||||
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
|
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import IRole from '../../../../interfaces/role';
|
import { IProjectRole } from '../../../../interfaces/role';
|
||||||
|
|
||||||
import { useStyles } from '../ProjectAccess.styles';
|
import { useStyles } from '../ProjectAccess.styles';
|
||||||
|
|
||||||
interface IProjectRoleSelect {
|
interface IProjectRoleSelect {
|
||||||
roles: IRole[];
|
roles: IProjectRole[];
|
||||||
labelId: string;
|
labelId: string;
|
||||||
id: string;
|
id: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
onChange: () => void;
|
onChange: (
|
||||||
|
evt: React.ChangeEvent<{
|
||||||
|
name?: string | undefined;
|
||||||
|
value: unknown;
|
||||||
|
}>
|
||||||
|
) => void;
|
||||||
value: any;
|
value: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
export const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
||||||
roles,
|
roles,
|
||||||
onChange,
|
onChange,
|
||||||
labelId,
|
labelId,
|
||||||
@ -39,9 +44,10 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
|||||||
value={value || ''}
|
value={value || ''}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
renderValue={roleId => {
|
renderValue={roleId => {
|
||||||
return roles?.find(role => {
|
const role = roles?.find(role => {
|
||||||
return role.id === roleId;
|
return role.id === roleId;
|
||||||
}).name;
|
});
|
||||||
|
return role?.name || '';
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -66,5 +72,3 @@ const ProjectRoleSelect: React.FC<IProjectRoleSelect> = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectRoleSelect;
|
|
||||||
|
@ -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;
|
|
@ -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;
|
|
@ -163,14 +163,19 @@ const useAPI = ({
|
|||||||
|
|
||||||
if (res.status > 399) {
|
if (res.status > 399) {
|
||||||
const response = await res.json();
|
const response = await res.json();
|
||||||
|
if (response?.details?.length > 0 && propagateErrors) {
|
||||||
if (response?.details?.length > 0) {
|
|
||||||
const error = response.details[0];
|
const error = response.details[0];
|
||||||
if (propagateErrors) {
|
if (propagateErrors) {
|
||||||
throw new Error(error.message);
|
throw new Error(error.message || error.msg);
|
||||||
}
|
}
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response?.length > 0 && propagateErrors) {
|
||||||
|
const error = response[0];
|
||||||
|
throw new Error(error.message || error.msg);
|
||||||
|
}
|
||||||
|
|
||||||
if (propagateErrors) {
|
if (propagateErrors) {
|
||||||
throw new Error('Action could not be performed');
|
throw new Error('Action could not be performed');
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
return {
|
||||||
createProject,
|
createProject,
|
||||||
validateId,
|
validateId,
|
||||||
@ -114,8 +162,11 @@ const useProjectApi = () => {
|
|||||||
deleteProject,
|
deleteProject,
|
||||||
addEnvironmentToProject,
|
addEnvironmentToProject,
|
||||||
removeEnvironmentFromProject,
|
removeEnvironmentFromProject,
|
||||||
|
addUserToRole,
|
||||||
|
removeUserFromRole,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
|
searchProjectUser,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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;
|
@ -19,7 +19,7 @@ const usePagination = (
|
|||||||
const result = paginate(dataToPaginate, limit);
|
const result = paginate(dataToPaginate, limit);
|
||||||
setPaginatedData(result);
|
setPaginatedData(result);
|
||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, [data, limit]);
|
}, [JSON.stringify(data), limit]);
|
||||||
|
|
||||||
const nextPage = () => {
|
const nextPage = () => {
|
||||||
if (pageIndex < paginatedData.length - 1) {
|
if (pageIndex < paginatedData.length - 1) {
|
||||||
|
@ -3,3 +3,7 @@ export interface IFeatureViewParams {
|
|||||||
featureId: string;
|
featureId: string;
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IProjectViewParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ export interface IProjectRole {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IRole;
|
export default IRole;
|
||||||
|
Loading…
Reference in New Issue
Block a user