You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/09/11 15:30:09 UTC
(airavata-custos) 03/03: Allow adding cluster admin role without portal admin selection
This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch user-onboard-web
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
commit e732e2753fbb968bc8827b11a2821f49f3c092ca
Author: lahiruj <la...@gmail.com>
AuthorDate: Mon Sep 7 23:12:46 2026 -0400
Allow adding cluster admin role without portal admin selection
---
.../admin/users/management/AddUserDialog.tsx | 38 +++++++++++++---------
.../management/__tests__/AddUserDialog.test.tsx | 35 ++++++++++++++++++++
2 files changed, 58 insertions(+), 15 deletions(-)
diff --git a/web/src/app/(portal)/admin/users/management/AddUserDialog.tsx b/web/src/app/(portal)/admin/users/management/AddUserDialog.tsx
index a11cf660c..4a2a4c02d 100644
--- a/web/src/app/(portal)/admin/users/management/AddUserDialog.tsx
+++ b/web/src/app/(portal)/admin/users/management/AddUserDialog.tsx
@@ -61,6 +61,7 @@ export function AddUserDialog({
const [lastName, setLastName] = React.useState("");
const [username, setUsername] = React.useState("");
const [allocationId, setAllocationId] = React.useState(NO_ALLOCATION);
+ const [portalAdmin, setPortalAdmin] = React.useState(true);
const [clusterAdmin, setClusterAdmin] = React.useState(false);
React.useEffect(() => {
@@ -71,22 +72,37 @@ export function AddUserDialog({
setLastName("");
setUsername("");
setAllocationId(NO_ALLOCATION);
+ setPortalAdmin(true);
setClusterAdmin(false);
}
}, [open]);
const isAdmin = userType === "admin";
+ const noAdminAccess = isAdmin && !portalAdmin && !clusterAdmin;
+ const adminNotice = noAdminAccess
+ ? { title: "Pick at least one kind of admin access", detail: "Otherwise there is nothing to grant." }
+ : portalAdmin && clusterAdmin
+ ? {
+ title: "This user will be a portal and cluster admin",
+ detail: "Admin access to the portal, and sudo on the cluster.",
+ }
+ : clusterAdmin
+ ? {
+ title: "This user will be a cluster admin",
+ detail: "Sudo on the cluster, with no admin access in the portal.",
+ }
+ : { title: "This user will be a portal admin", detail: "Admin access to the portal." };
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
- if (isPending) return;
+ if (isPending || noAdminAccess) return;
const payload: CreateUserPayload = {
email: email.trim(),
first_name: firstName.trim(),
last_name: lastName.trim(),
};
if (isAdmin) {
- payload.portal_admin = true;
+ if (portalAdmin) payload.portal_admin = true;
if (clusterAdmin) payload.cluster_admin = true;
} else {
if (username.trim()) payload.username = username.trim();
@@ -216,8 +232,8 @@ export function AddUserDialog({
<label className="flex cursor-pointer items-start gap-2.5 p-3">
<input
type="checkbox"
- checked
- readOnly
+ checked={portalAdmin}
+ onChange={(e) => setPortalAdmin(e.target.checked)}
className="mt-0.5 size-4 rounded border-input accent-[color:var(--brand)]"
/>
<span>
@@ -245,16 +261,8 @@ export function AddUserDialog({
<div className="flex items-start gap-2.5 rounded-md border border-[color:var(--tone-warn-fg)]/30 bg-[color:var(--tone-warn-bg)] p-3 text-xs">
<ShieldCheck className="mt-0.5 size-4 shrink-0 text-[color:var(--tone-warn-fg)]" />
<span>
- <span className="block text-sm font-medium">
- {clusterAdmin
- ? "This user will be a portal and cluster admin"
- : "This user will be a portal admin"}
- </span>
- <span className="text-muted-foreground">
- {clusterAdmin
- ? "Admin access to the portal, and sudo on the cluster."
- : "Admin access to the portal."}
- </span>
+ <span className="block text-sm font-medium">{adminNotice.title}</span>
+ <span className="text-muted-foreground">{adminNotice.detail}</span>
</span>
</div>
</div>
@@ -264,7 +272,7 @@ export function AddUserDialog({
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
- <Button type="submit" variant="brand" disabled={isPending}>
+ <Button type="submit" variant="brand" disabled={isPending || noAdminAccess}>
{isPending ? "Adding…" : isAdmin ? "Add admin user" : "Add user"}
</Button>
</DialogFooter>
diff --git a/web/src/app/(portal)/admin/users/management/__tests__/AddUserDialog.test.tsx b/web/src/app/(portal)/admin/users/management/__tests__/AddUserDialog.test.tsx
index 75788fb89..49d1d1728 100644
--- a/web/src/app/(portal)/admin/users/management/__tests__/AddUserDialog.test.tsx
+++ b/web/src/app/(portal)/admin/users/management/__tests__/AddUserDialog.test.tsx
@@ -117,6 +117,41 @@ describe("AddUserDialog", () => {
});
});
+ it("submits a cluster admin without portal admin", () => {
+ const { onSubmit } = renderDialog();
+ fireEvent.click(screen.getByRole("radio", { name: /Admin/ }));
+
+ const box = (text: RegExp) =>
+ screen.getByText(text).closest("label")?.querySelector("input") as HTMLInputElement;
+ fireEvent.click(box(/Admin access on the cluster itself/));
+ fireEvent.click(box(/Manage users, allocations, and settings/));
+
+ fireEvent.change(screen.getByLabelText("Email"), {
+ target: { value: "sandrade@sdsc.edu" },
+ });
+ fireEvent.click(screen.getByRole("button", { name: "Add admin user" }));
+
+ expect(onSubmit).toHaveBeenCalledWith({
+ email: "sandrade@sdsc.edu",
+ first_name: "",
+ last_name: "",
+ cluster_admin: true,
+ });
+ });
+
+ it("blocks submit when neither kind of admin access is picked", () => {
+ const { onSubmit } = renderDialog();
+ fireEvent.click(screen.getByRole("radio", { name: /Admin/ }));
+ fireEvent.click(
+ screen
+ .getByText(/Manage users, allocations, and settings/)
+ .closest("label")
+ ?.querySelector("input") as HTMLInputElement,
+ );
+ expect(screen.getByRole("button", { name: "Add admin user" })).toBeDisabled();
+ expect(onSubmit).not.toHaveBeenCalled();
+ });
+
it("leaves cluster_admin off when the box is not ticked", () => {
const { onSubmit } = renderDialog();
fireEvent.click(screen.getByRole("radio", { name: /Admin/ }));