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/ }));