@@ -52,7 +56,7 @@ const MainLayout = ({ children, location, uiConfig }) => {
position: 'fixed',
right: '0',
bottom: '-4px',
- zIndex: '1',
+ zIndex: 1,
}}
>
@@ -64,9 +68,3 @@ const MainLayout = ({ children, location, uiConfig }) => {
>
);
};
-
-MainLayout.propTypes = {
- location: PropTypes.object.isRequired,
-};
-
-export default MainLayout;
diff --git a/frontend/src/component/layout/MainLayout/index.js b/frontend/src/component/layout/MainLayout/index.js
deleted file mode 100644
index 04ae6cce09..0000000000
--- a/frontend/src/component/layout/MainLayout/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { connect } from 'react-redux';
-import MainLayout from './MainLayout';
-
-const mapStateToProps = (state, ownProps) => ({
- uiConfig: state.uiConfig.toJS(),
- location: ownProps.location,
- children: ownProps.children,
-});
-
-export default connect(mapStateToProps)(MainLayout);
diff --git a/frontend/src/component/menu/Header/Header.styles.ts b/frontend/src/component/menu/Header/Header.styles.ts
index 091b89226a..410b6787e6 100644
--- a/frontend/src/component/menu/Header/Header.styles.ts
+++ b/frontend/src/component/menu/Header/Header.styles.ts
@@ -7,7 +7,7 @@ export const useStyles = makeStyles(theme => ({
padding: '0.5rem',
boxShadow: 'none',
position: 'relative',
- zIndex: '300',
+ zIndex: 300,
},
links: {
display: 'flex',
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
index 062bc1ea4c..f643fa4014 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes-test.jsx.snap
@@ -286,7 +286,7 @@ Array [
"layout": "main",
"menu": Object {},
"parent": "/addons",
- "path": "/addons/create/:provider",
+ "path": "/addons/create/:providerId",
"title": "Create",
"type": "protected",
},
@@ -295,7 +295,7 @@ Array [
"layout": "main",
"menu": Object {},
"parent": "/addons",
- "path": "/addons/edit/:id",
+ "path": "/addons/edit/:addonId",
"title": "Edit",
"type": "protected",
},
diff --git a/frontend/src/component/menu/routes.js b/frontend/src/component/menu/routes.js
index b0724648e5..22d66f43a1 100644
--- a/frontend/src/component/menu/routes.js
+++ b/frontend/src/component/menu/routes.js
@@ -6,10 +6,8 @@ import Strategies from '../../page/strategies';
import HistoryPage from '../../page/history';
import HistoryTogglePage from '../../page/history/toggle';
import { ArchiveListContainer } from '../archive/ArchiveListContainer';
-import ListTagTypes from '../../page/tag-types';
-import Addons from '../../page/addons';
-import AddonsCreate from '../../page/addons/create';
-import AddonsEdit from '../../page/addons/edit';
+import { TagTypeList } from '../tags/TagTypeList/TagTypeList';
+import { AddonList } from '../addons/AddonList/AddonList';
import Admin from '../admin';
import AdminApi from '../admin/api';
import AdminInvoice from '../admin/invoice/InvoiceAdminPage';
@@ -35,8 +33,8 @@ import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironme
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
import CreateContext from '../context/CreateContext/CreateContext';
import EditContext from '../context/EditContext/EditContext';
-import EditTagType from '../tagTypes/EditTagType/EditTagType';
-import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
+import EditTagType from '../tags/EditTagType/EditTagType';
+import CreateTagType from '../tags/CreateTagType/CreateTagType';
import EditProject from '../project/Project/EditProject/EditProject';
import CreateProject from '../project/Project/CreateProject/CreateProject';
import CreateFeature from '../feature/CreateFeature/CreateFeature';
@@ -45,6 +43,8 @@ import { ApplicationEdit } from '../application/ApplicationEdit/ApplicationEdit'
import { ApplicationList } from '../application/ApplicationList/ApplicationList';
import ContextList from '../context/ContextList/ContextList';
import RedirectFeatureView from '../feature/RedirectFeatureView/RedirectFeatureView';
+import { CreateAddon } from '../addons/CreateAddon/CreateAddon';
+import { EditAddon } from '../addons/EditAddon/EditAddon';
export const routes = [
// Project
@@ -314,7 +314,7 @@ export const routes = [
{
path: '/tag-types',
title: 'Tag types',
- component: ListTagTypes,
+ component: TagTypeList,
type: 'protected',
layout: 'main',
menu: { mobile: true, advanced: true },
@@ -322,19 +322,19 @@ export const routes = [
// Addons
{
- path: '/addons/create/:provider',
+ path: '/addons/create/:providerId',
parent: '/addons',
title: 'Create',
- component: AddonsCreate,
+ component: CreateAddon,
type: 'protected',
layout: 'main',
menu: {},
},
{
- path: '/addons/edit/:id',
+ path: '/addons/edit/:addonId',
parent: '/addons',
title: 'Edit',
- component: AddonsEdit,
+ component: EditAddon,
type: 'protected',
layout: 'main',
menu: {},
@@ -342,7 +342,7 @@ export const routes = [
{
path: '/addons',
title: 'Addons',
- component: Addons,
+ component: AddonList,
hidden: false,
type: 'protected',
layout: 'main',
diff --git a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx
index c22425b5ad..eed75ece87 100644
--- a/frontend/src/component/project/Project/CreateProject/CreateProject.tsx
+++ b/frontend/src/component/project/Project/CreateProject/CreateProject.tsx
@@ -10,7 +10,6 @@ import PermissionButton from '../../../common/PermissionButton/PermissionButton'
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
const CreateProject = () => {
- /* @ts-ignore */
const { setToastData, setToastApiError } = useToast();
const { refetch } = useUser();
const { uiConfig } = useUiConfig();
diff --git a/frontend/src/component/project/Project/Project.tsx b/frontend/src/component/project/Project/Project.tsx
index 70c60bd618..f05acc4c40 100644
--- a/frontend/src/component/project/Project/Project.tsx
+++ b/frontend/src/component/project/Project/Project.tsx
@@ -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';
diff --git a/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts b/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts
index e5e5e19a86..6b8a221e3a 100644
--- a/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts
+++ b/frontend/src/component/project/Project/ProjectForm/ProjectForm.style.ts
@@ -38,7 +38,7 @@ export const useStyles = makeStyles(theme => ({
position: 'relative',
},
errorMessage: {
- //@ts-ignore
+ // @ts-expect-error
fontSize: theme.fontSizes.smallBody,
color: theme.palette.error.main,
position: 'absolute',
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts b/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts
index 388a9a9da1..d34ce9b1d1 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccess.styles.ts
@@ -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',
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx
index fee3f58a6d..48ac53420d 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccess.tsx
@@ -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
();
+export const ProjectAccess = () => {
+ const { id: projectId } = useParams();
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();
if (isOss()) {
return (
-
+ }>
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 (
-
-
-
-
-
- {page.map(user => {
- const labelId = `checkbox-list-secondary-label-${user.id}`;
- return (
-
-
-
-
-
-
-
-
-
+ }
+ className={styles.pageContent}
+ >
+
- {
- setUser(user);
- setShowDelDialogue(true);
- }}
- disabled={users.length === 1}
- tooltip={
- users.length === 1
- ? 'A project must have at least one owner'
- : 'Remove access'
- }
- >
-
-
-
-
- );
- })}
+
+
{
prevPage={prevPage}
style={{ bottom: '-21px' }}
/>
-
+
+
{
- setUser({});
+ setUser(undefined);
setShowDelDialogue(false);
}}
title="Really remove user from this project"
@@ -244,5 +134,3 @@ const ProjectAccess = () => {
);
};
-
-export default ProjectAccess;
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessAddUser/ProjectAccessAddUser.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessAddUser/ProjectAccessAddUser.tsx
new file mode 100644
index 0000000000..26116a29f9
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessAddUser/ProjectAccessAddUser.tsx
@@ -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();
+ const [role, setRole] = useState();
+ 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 (
+ <>
+
+ The user must have an Unleash root role before added to the
+ project.
+
+
+
+ handleBlur()}
+ value={user || ''}
+ freeSolo
+ getOptionSelected={() => true}
+ filterOptions={o => o}
+ getOptionLabel={getOptionLabel}
+ options={options}
+ loading={loading}
+ renderInput={params => (
+
+
+
+ ),
+ endAdornment: (
+ <>
+
+ }
+ />
+
+ {params.InputProps.endAdornment}
+ >
+ ),
+ }}
+ />
+ )}
+ />
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessList.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessList.tsx
new file mode 100644
index 0000000000..42c15e3b42
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessList.tsx
@@ -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 = ({
+ 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 (
+
+ {sortUsers(page).map(user => {
+ return (
+
+ );
+ })}
+ {children}
+
+ );
+};
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.styles.ts b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.styles.ts
new file mode 100644
index 0000000000..72f94047e0
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.styles.ts
@@ -0,0 +1,11 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(() => ({
+ iconButton: {
+ marginLeft: '0.5rem',
+ },
+ actionList: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+}));
diff --git a/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.tsx b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.tsx
new file mode 100644
index 0000000000..be16175c34
--- /dev/null
+++ b/frontend/src/component/project/ProjectAccess/ProjectAccessList/ProjectAccessListItem/ProjectAccessListItem.tsx
@@ -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();
+ const styles = useStyles();
+
+ const labelId = `checkbox-list-secondary-label-${user.id}`;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {
+ handleRemoveAccess(user);
+ }}
+ disabled={access.users.length === 1}
+ tooltip={
+ access.users.length === 1
+ ? 'A project must have at least one owner'
+ : 'Remove access'
+ }
+ >
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx b/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx
index deae3eda52..c8f7dcf257 100644
--- a/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx
+++ b/frontend/src/component/project/ProjectAccess/ProjectRoleSelect/ProjectRoleSelect.tsx
@@ -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 = ({
+export const ProjectRoleSelect: React.FC = ({
roles,
onChange,
labelId,
@@ -39,9 +44,10 @@ const ProjectRoleSelect: React.FC = ({
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 = ({
);
};
-
-export default ProjectRoleSelect;
diff --git a/frontend/src/component/project/access-add-user.tsx b/frontend/src/component/project/access-add-user.tsx
deleted file mode 100644
index e5029160c9..0000000000
--- a/frontend/src/component/project/access-add-user.tsx
+++ /dev/null
@@ -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 (
- <>
-
- The user must have an Unleash root role before added to the
- project.
-
-
-
- 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 => (
-
-
-
- ),
- endAdornment: (
-
- {loading ? (
-
- ) : null}
- {params.InputProps.endAdornment}
-
- ),
- }}
- />
- )}
- />
-
-
-
-
-
-
-
-
- >
- );
-}
-
-AddUserComponent.propTypes = {
- roles: PropTypes.array.isRequired,
- addUserToRole: PropTypes.func.isRequired,
-};
-
-export default AddUserComponent;
diff --git a/frontend/src/component/project/access-container.js b/frontend/src/component/project/access-container.js
deleted file mode 100644
index 692a07f425..0000000000
--- a/frontend/src/component/project/access-container.js
+++ /dev/null
@@ -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;
diff --git a/frontend/src/component/tag-types/TagTypeList/index.jsx b/frontend/src/component/tag-types/TagTypeList/index.jsx
deleted file mode 100644
index 20507a5dfc..0000000000
--- a/frontend/src/component/tag-types/TagTypeList/index.jsx
+++ /dev/null
@@ -1,3 +0,0 @@
-import TagTypeList from './TagTypeList';
-
-export default TagTypeList;
diff --git a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap b/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap
deleted file mode 100644
index 29b3b14a98..0000000000
--- a/frontend/src/component/tag-types/__tests__/__snapshots__/tag-type-create-component-test.js.snap
+++ /dev/null
@@ -1,157 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`it supports editMode 1`] = `
-
-
-
-
-
- Update Tag type
-
-
-
-
-
-
-
- Tag types allow you to group tags together in the management UI
-
-
-
-
-
-`;
-
-exports[`renders correctly for creating 1`] = `
-
-
-
-
-
- Create Tag type
-
-
-
-
-
-
-
- Tag types allow you to group tags together in the management UI
-
-
-
-
-
-`;
-
-exports[`renders correctly for creating without permissions 1`] = `
-
-
-
-
-
- Create Tag type
-
-
-
-
-
-
-
- Tag types allow you to group tags together in the management UI
-
-
-
-
-
-`;
diff --git a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js b/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js
deleted file mode 100644
index f43bb99bde..0000000000
--- a/frontend/src/component/tag-types/__tests__/tag-type-create-component-test.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import React from 'react';
-import { ThemeProvider } from '@material-ui/core';
-import TagTypes from '../form-tag-type-component';
-import renderer from 'react-test-renderer';
-import theme from '../../../themes/main-theme';
-import AccessProvider from '../../providers/AccessProvider/AccessProvider';
-import { createFakeStore } from '../../../accessStoreFake';
-import {
- CREATE_TAG_TYPE,
- UPDATE_TAG_TYPE,
-} from '../../providers/AccessProvider/permissions';
-
-jest.mock('@material-ui/core/TextField');
-
-test('renders correctly for creating', () => {
- const tree = renderer
- .create(
-
-
- Promise.resolve(true)}
- tagType={{ name: '', description: '', icon: '' }}
- editMode={false}
- submit={jest.fn()}
- />
-
-
- )
- .toJSON();
- expect(tree).toMatchSnapshot();
-});
-
-test('renders correctly for creating without permissions', () => {
- const tree = renderer
- .create(
-
-
- Promise.resolve(true)}
- tagType={{ name: '', description: '', icon: '' }}
- editMode={false}
- submit={jest.fn()}
- />
-
-
- )
- .toJSON();
- expect(tree).toMatchSnapshot();
-});
-
-test('it supports editMode', () => {
- const tree = renderer
- .create(
-
-
- Promise.resolve(true)}
- tagType={{ name: '', description: '', icon: '' }}
- editMode
- submit={jest.fn()}
- />
-
-
- )
- .toJSON();
- expect(tree).toMatchSnapshot();
-});
diff --git a/frontend/src/component/tag-types/create-tag-type-container.js b/frontend/src/component/tag-types/create-tag-type-container.js
deleted file mode 100644
index a8cd3c253e..0000000000
--- a/frontend/src/component/tag-types/create-tag-type-container.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import { connect } from 'react-redux';
-import TagTypeComponent from './form-tag-type-component';
-import { createTagType, validateName } from '../../store/tag-type/actions';
-
-const mapStateToProps = () => ({
- tagType: { name: '', description: '', icon: '' },
- editMode: false,
-});
-
-const mapDispatchToProps = dispatch => ({
- validateName: name => validateName(name),
- submit: tagType => createTagType(tagType)(dispatch),
-});
-
-const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(TagTypeComponent);
-
-export default FormAddContainer;
diff --git a/frontend/src/component/tag-types/edit-tag-type-container.js b/frontend/src/component/tag-types/edit-tag-type-container.js
deleted file mode 100644
index 4372dc799b..0000000000
--- a/frontend/src/component/tag-types/edit-tag-type-container.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { connect } from 'react-redux';
-import Component from './form-tag-type-component';
-import { updateTagType } from '../../store/tag-type/actions';
-
-const mapStateToProps = (state, props) => {
- const tagTypeBase = { name: '', description: '', icon: '' };
- const realTagType = state.tagTypes.toJS().find(n => n.name === props.tagTypeName);
- const tagType = Object.assign(tagTypeBase, realTagType);
-
- return {
- tagType,
- editMode: true,
- };
-};
-
-const mapDispatchToProps = dispatch => ({
- validateName: () => {},
- submit: tagType => {
- updateTagType(tagType)(dispatch);
- },
-});
-
-const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
-
-export default FormAddContainer;
diff --git a/frontend/src/component/tag-types/form-tag-type-component.js b/frontend/src/component/tag-types/form-tag-type-component.js
deleted file mode 100644
index 0f07f700d7..0000000000
--- a/frontend/src/component/tag-types/form-tag-type-component.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import React, { useContext, useState } from 'react';
-import PropTypes from 'prop-types';
-import classnames from 'classnames';
-
-import { FormButtons } from '../common';
-import PageContent from '../common/PageContent/PageContent';
-import { Typography, TextField } from '@material-ui/core';
-
-import styles from './TagType.module.scss';
-import commonStyles from '../common/common.module.scss';
-import AccessContext from '../../contexts/AccessContext';
-import {
- CREATE_TAG_TYPE,
- UPDATE_TAG_TYPE,
-} from '../providers/AccessProvider/permissions';
-import ConditionallyRender from '../common/ConditionallyRender';
-
-const AddTagTypeComponent = ({
- tagType,
- validateName,
- submit,
- history,
- editMode,
-}) => {
- const [tagTypeName, setTagTypeName] = useState(tagType.name || '');
- const [tagTypeDescription, setTagTypeDescription] = useState(
- tagType.description || ''
- );
- const [errors, setErrors] = useState({
- general: undefined,
- name: undefined,
- description: undefined,
- });
- const { hasAccess } = useContext(AccessContext);
-
- const onValidateName = async evt => {
- evt.preventDefault();
- const name = evt.target.value;
- try {
- await validateName(name);
- setErrors({ name: undefined });
- } catch (err) {
- setErrors({ name: err.message });
- }
- };
-
- const onCancel = evt => {
- evt.preventDefault();
- history.push('/tag-types');
- };
-
- const onSubmit = async evt => {
- evt.preventDefault();
- try {
- await submit({
- name: tagTypeName,
- description: tagTypeDescription,
- });
- history.push('/tag-types');
- } catch (e) {
- setErrors({ general: e.message });
- }
- };
- const submitText = editMode ? 'Update' : 'Create';
- return (
-
-
-
- Tag types allow you to group tags together in the
- management UI
-
-
- }
- elseShow={
-