Skip to content

Commit 35dd06c

Browse files
committed
fix(api): fix admin/editor level permissions for v2 DM api
1 parent 7798f72 commit 35dd06c

File tree

24 files changed

+247
-169
lines changed

24 files changed

+247
-169
lines changed

components/ManageUsers.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import { Autocomplete, Chip, TextField } from "@mui/material";
66
import type { AutocompleteChangeReason } from "@mui/material/useAutocomplete";
77

88
export interface ManageUsersProps {
9-
/**
10-
* User's username
11-
*/
12-
currentUsername: string;
139
/**
1410
* Array of current users
1511
*/
@@ -38,7 +34,6 @@ export interface ManageUsersProps {
3834
* The current user is assumed to always be included.
3935
*/
4036
export const ManageUsers: FC<ManageUsersProps> = ({
41-
currentUsername,
4237
users,
4338
isLoading = false,
4439
title,
@@ -72,7 +67,6 @@ export const ManageUsers: FC<ManageUsersProps> = ({
7267
fullWidth
7368
multiple
7469
disabled={loading}
75-
getOptionDisabled={(option) => option === currentUsername}
7670
id={title.toLowerCase().replace(/\s/g, "")}
7771
loading={loading}
7872
options={availableUsers.map((user) => user.username)}
@@ -84,14 +78,14 @@ export const ManageUsers: FC<ManageUsersProps> = ({
8478
<Chip
8579
label={option}
8680
variant="outlined"
87-
onDelete={option !== currentUsername ? onDelete : undefined}
81+
onDelete={onDelete}
8882
{...chipProps}
8983
key={option}
9084
/>
9185
);
9286
})
9387
}
94-
value={[currentUsername, ...users]}
88+
value={users}
9589
onChange={(_, value, reason) => updateUsers(value, reason)}
9690
/>
9791
);

components/instances/ResultApplicationCard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { InstanceGetResponse, InstanceSummary } from "@squonk/data-manager-
22

33
import { CardContent, ListItem, ListItemText } from "@mui/material";
44

5-
import { useIsEditorOfCurrentProject, useProjectFromId } from "../../hooks/projectHooks";
5+
import { useIsUserAdminOrEditorOfCurrentProject, useProjectFromId } from "../../hooks/projectHooks";
66
import { HrefButton } from "../HrefButton";
77
import { ProjectListItem } from "../projects/ProjectListItem";
88
import { ResultCard } from "../results/ResultCard";
@@ -36,14 +36,14 @@ export const ResultApplicationCard = ({
3636

3737
const associatedProject = useProjectFromId(instance.project_id);
3838

39-
const isEditor = useIsEditorOfCurrentProject();
39+
const hasPermission = useIsUserAdminOrEditorOfCurrentProject();
4040

4141
return (
4242
<ResultCard
4343
actions={({ setSlideIn }) => (
4444
<>
4545
<TerminateInstance
46-
disabled={!isEditor}
46+
disabled={!hasPermission}
4747
instanceId={instanceId}
4848
phase={instance.phase}
4949
projectId={instance.project_id}

components/instances/ResultJobCard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Alert, CardContent, ListItem, ListItemText } from "@mui/material";
55
import { LogsButton } from "../../components/results/LogsButton";
66
import { RerunJobButton } from "../../components/results/RerunJobButton";
77
import { ResultCard } from "../../components/results/ResultCard";
8-
import { useIsEditorOfCurrentProject, useProjectFromId } from "../../hooks/projectHooks";
8+
import { useIsUserAdminOrEditorOfCurrentProject, useProjectFromId } from "../../hooks/projectHooks";
99
import { ProjectListItem } from "../projects/ProjectListItem";
1010
import { ArchivedStatus } from "./ArchivedStatus";
1111
import { ArchiveInstance } from "./ArchiveInstance";
@@ -40,7 +40,7 @@ export const ResultJobCard = ({
4040

4141
const associatedProject = useProjectFromId(instance.project_id);
4242

43-
const isEditor = useIsEditorOfCurrentProject();
43+
const hasPermission = useIsUserAdminOrEditorOfCurrentProject();
4444

4545
if (instance.job_id === undefined) {
4646
return <Alert severity="error">Instance is missing a job ID</Alert>;
@@ -51,13 +51,13 @@ export const ResultJobCard = ({
5151
actions={({ setSlideIn }) => (
5252
<>
5353
<TerminateInstance
54-
disabled={!isEditor}
54+
disabled={!hasPermission}
5555
instanceId={instanceId}
5656
phase={instance.phase}
5757
projectId={instance.project_id}
5858
onTermination={() => setSlideIn(false)}
5959
/>
60-
<RerunJobButton disabled={!isEditor} instance={instance} />
60+
<RerunJobButton disabled={!hasPermission} instance={instance} />
6161
<LogsButton instance={instance} instanceId={instanceId} />
6262
<ArchiveInstance archived={instance.archived} instanceId={instanceId} />
6363
</>

components/projects/EditProjectButton/EditProjectButton.tsx

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import type { ProjectDetail } from "@squonk/data-manager-client";
55
import { Edit as EditIcon } from "@mui/icons-material";
66
import { Box, IconButton, Tooltip, Typography } from "@mui/material";
77

8-
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
98
import { ModalWrapper } from "../../modals/ModalWrapper";
109
import { PrivateProjectToggle } from "./PrivateProjectToggle";
10+
import { ProjectAdministrators } from "./ProjectAdministrators";
1111
import { ProjectEditors } from "./ProjectEditors";
1212
import { ProjectObservers } from "./ProjectObservers";
1313

@@ -24,20 +24,16 @@ export interface EditProjectButtonProps {
2424
export const EditProjectButton = ({ project }: EditProjectButtonProps) => {
2525
const [open, setOpen] = useState(false);
2626

27-
const { user } = useKeycloakUser();
28-
const isEditor = !!user.username && !!project.editors.includes(user.username);
29-
3027
return (
3128
<>
32-
{isEditor && (
33-
<Tooltip title={"Edit Project"}>
34-
<span>
35-
<IconButton size="small" sx={{ p: "1px" }} onClick={() => setOpen(!open)}>
36-
<EditIcon />
37-
</IconButton>
38-
</span>
39-
</Tooltip>
40-
)}
29+
<Tooltip title={"Edit Project"}>
30+
<span>
31+
<IconButton size="small" sx={{ p: "1px" }} onClick={() => setOpen(!open)}>
32+
<EditIcon />
33+
</IconButton>
34+
</span>
35+
</Tooltip>
36+
4137
<ModalWrapper
4238
DialogProps={{ maxWidth: "sm", fullWidth: true }}
4339
id="edit-project"
@@ -50,13 +46,12 @@ export const EditProjectButton = ({ project }: EditProjectButtonProps) => {
5046
{project.name}
5147
</Typography>
5248

53-
<Typography gutterBottom>
54-
<b>Owner</b>: {project.owner}
55-
</Typography>
49+
<Typography gutterBottom>{/* <b>Owner</b>: {project.owner} */}</Typography>
5650

5751
<PrivateProjectToggle isPrivate={project.private} projectId={project.project_id} />
5852

5953
<Box display="flex" flexDirection="column" gap={2}>
54+
<ProjectAdministrators project={project} />
6055
<ProjectEditors project={project} />
6156
<ProjectObservers project={project} />
6257
</Box>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import type { ProjectDetail } from "@squonk/data-manager-client";
2+
import {
3+
getGetProjectQueryKey,
4+
getGetProjectsQueryKey,
5+
useAddAdministratorToProject,
6+
useGetProjects,
7+
useRemoveAdministratorFromProject,
8+
} from "@squonk/data-manager-client/project";
9+
10+
import { useQueryClient } from "@tanstack/react-query";
11+
12+
import { ProjectMemberSelection } from "./ProjectMemberSelection";
13+
14+
export interface ProjectAdministratorsProps {
15+
/**
16+
* Project to be edited.
17+
*/
18+
project: ProjectDetail;
19+
}
20+
21+
/**
22+
* MuiAutocomplete to manage the current administrators of the selected project
23+
*/
24+
export const ProjectAdministrators = ({ project }: ProjectAdministratorsProps) => {
25+
const { isLoading: isProjectsLoading } = useGetProjects();
26+
27+
const { mutateAsync: addAdministrator, isPending: isAdding } = useAddAdministratorToProject();
28+
const { mutateAsync: removeAdministrator, isPending: isRemoving } =
29+
useRemoveAdministratorFromProject();
30+
const queryClient = useQueryClient();
31+
32+
return (
33+
<ProjectMemberSelection
34+
addMember={(userId) => addAdministrator({ projectId: project.project_id, userId })}
35+
isLoading={isAdding || isRemoving || isProjectsLoading}
36+
memberList={project.administrators}
37+
removeMember={(userId) => removeAdministrator({ projectId: project.project_id, userId })}
38+
title="Administrators"
39+
onSettled={() =>
40+
Promise.all([
41+
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) }),
42+
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() }),
43+
])
44+
}
45+
/>
46+
);
47+
};
Lines changed: 18 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DmError, ProjectDetail } from "@squonk/data-manager-client";
1+
import type { ProjectDetail } from "@squonk/data-manager-client";
22
import {
33
getGetProjectQueryKey,
44
getGetProjectsQueryKey,
@@ -9,9 +9,7 @@ import {
99

1010
import { useQueryClient } from "@tanstack/react-query";
1111

12-
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
13-
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
14-
import { ManageUsers } from "../../ManageUsers";
12+
import { ProjectMemberSelection } from "./ProjectMemberSelection";
1513

1614
export interface ProjectEditorsProps {
1715
/**
@@ -24,54 +22,25 @@ export interface ProjectEditorsProps {
2422
* MuiAutocomplete to manage the current editors of the selected project
2523
*/
2624
export const ProjectEditors = ({ project }: ProjectEditorsProps) => {
27-
const { user: currentUser } = useKeycloakUser();
28-
2925
const { isLoading: isProjectsLoading } = useGetProjects();
26+
3027
const { mutateAsync: addEditor, isPending: isAdding } = useAddEditorToProject();
3128
const { mutateAsync: removeEditor, isPending: isRemoving } = useRemoveEditorFromProject();
3229
const queryClient = useQueryClient();
3330

34-
const { enqueueError, enqueueSnackbar } = useEnqueueError<DmError>();
35-
36-
if (currentUser.username) {
37-
return (
38-
<ManageUsers
39-
currentUsername={currentUser.username}
40-
isLoading={isAdding || isRemoving || isProjectsLoading}
41-
title="Editors"
42-
users={project.editors.filter((user) => user !== currentUser.username)}
43-
onRemove={async (value) => {
44-
const username = project.editors.find((editor) => !value.includes(editor));
45-
if (username) {
46-
try {
47-
await removeEditor({ projectId: project.project_id, userId: username });
48-
} catch (error) {
49-
enqueueError(error);
50-
}
51-
// DM Queries
52-
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) });
53-
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() });
54-
} else {
55-
enqueueSnackbar("Username not found", { variant: "warning" });
56-
}
57-
}}
58-
onSelect={async (value) => {
59-
const username = value.find((user) => !project.editors.includes(user));
60-
if (username) {
61-
try {
62-
await addEditor({ projectId: project.project_id, userId: username });
63-
} catch (error) {
64-
enqueueError(error);
65-
}
66-
// DM Queries
67-
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) });
68-
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() });
69-
} else {
70-
enqueueSnackbar("Username not found", { variant: "warning" });
71-
}
72-
}}
73-
/>
74-
);
75-
}
76-
return null;
31+
return (
32+
<ProjectMemberSelection
33+
addMember={(userId) => addEditor({ projectId: project.project_id, userId })}
34+
isLoading={isAdding || isRemoving || isProjectsLoading}
35+
memberList={project.editors}
36+
removeMember={(userId) => removeEditor({ projectId: project.project_id, userId })}
37+
title="Editors"
38+
onSettled={() =>
39+
Promise.all([
40+
queryClient.invalidateQueries({ queryKey: getGetProjectQueryKey(project.project_id) }),
41+
queryClient.invalidateQueries({ queryKey: getGetProjectsQueryKey() }),
42+
])
43+
}
44+
/>
45+
);
7746
};
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { DmError } from "@squonk/data-manager-client";
2+
3+
import { useEnqueueError } from "../../../hooks/useEnqueueStackError";
4+
import { useKeycloakUser } from "../../../hooks/useKeycloakUser";
5+
import { ManageUsers } from "../../ManageUsers";
6+
7+
type Callback = (userId: string) => Promise<void>;
8+
9+
export interface ProjectMemberSelectionProps {
10+
title: string;
11+
/**
12+
* Project to be edited.
13+
*/
14+
memberList: string[];
15+
addMember: Callback;
16+
removeMember: Callback;
17+
onSettled: () => Promise<unknown>;
18+
/**
19+
* Loading state of async operations
20+
*/
21+
isLoading: boolean;
22+
}
23+
24+
/**
25+
* MuiAutocomplete to manage the current editors of the selected project
26+
*/
27+
export const ProjectMemberSelection = ({
28+
title,
29+
memberList,
30+
addMember,
31+
removeMember,
32+
onSettled,
33+
isLoading,
34+
}: ProjectMemberSelectionProps) => {
35+
const { user: currentUser } = useKeycloakUser();
36+
37+
const { enqueueError, enqueueSnackbar } = useEnqueueError<DmError>();
38+
39+
if (currentUser.username) {
40+
return (
41+
<ManageUsers
42+
isLoading={isLoading}
43+
title={title}
44+
users={memberList}
45+
onRemove={async (value) => {
46+
const username = memberList.find((editor) => !value.includes(editor));
47+
if (username) {
48+
try {
49+
await removeMember(username);
50+
} catch (error) {
51+
enqueueError(error);
52+
}
53+
await onSettled();
54+
} else {
55+
enqueueSnackbar("Username not found", { variant: "warning" });
56+
}
57+
}}
58+
onSelect={async (value) => {
59+
const username = value.slice(-1).pop();
60+
if (username) {
61+
try {
62+
await addMember(username);
63+
} catch (error) {
64+
enqueueError(error);
65+
}
66+
await onSettled();
67+
} else {
68+
enqueueSnackbar("Username not found", { variant: "warning" });
69+
}
70+
}}
71+
/>
72+
);
73+
}
74+
return null;
75+
};

0 commit comments

Comments
 (0)