From bbfe4c6680a3a3ab4a17bf00daf34f3e1ff86087 Mon Sep 17 00:00:00 2001 From: KodekaTeam Date: Sun, 13 Sep 2026 12:54:36 +0700 Subject: [PATCH] feat(users): Convert the user and group management UI/UX from a native view to a modal --- .../servers/components/ServerConfigModal.tsx | 22 -- .../server-config/ServerConfigUsersPanel.tsx | 353 +++++------------- 2 files changed, 97 insertions(+), 278 deletions(-) diff --git a/src/app/servers/components/ServerConfigModal.tsx b/src/app/servers/components/ServerConfigModal.tsx index fcbd665..f3b8de4 100644 --- a/src/app/servers/components/ServerConfigModal.tsx +++ b/src/app/servers/components/ServerConfigModal.tsx @@ -924,27 +924,6 @@ export default function ServerConfigModal({ }); }; - const requestSystemGroupDeleteConfirm = (options: { - groupName: string; - gid: number; - members: string[]; - primaryUsers: string[]; - confirmation: string; - }) => { - setPendingConfirm({ - kind: "system-group-delete", - groupName: options.groupName, - expectedGid: options.gid, - expectedMembers: options.members, - expectedPrimaryUsers: options.primaryUsers, - confirmation: options.confirmation, - title: `Delete system group ${options.groupName}`, - description: `Permanently delete the empty group ${options.groupName} (GID ${options.gid}). System, privileged, changed, or in-use groups will be rejected by the host.`, - confirmLabel: "Delete Group", - tone: "danger", - }); - }; - const requestSshAccessUpdateConfirm = ( options: ServerSshAccessUpdateBody, ) => { @@ -1008,7 +987,6 @@ export default function ServerConfigModal({ requestSystemUserSshKeyRevokeConfirm } onRequestDeleteUserConfirm={requestSystemUserDeleteConfirm} - onRequestDeleteGroupConfirm={requestSystemGroupDeleteConfirm} /> ); case "ssh-access": diff --git a/src/app/servers/components/server-config/ServerConfigUsersPanel.tsx b/src/app/servers/components/server-config/ServerConfigUsersPanel.tsx index 8fda78b..53335f2 100644 --- a/src/app/servers/components/server-config/ServerConfigUsersPanel.tsx +++ b/src/app/servers/components/server-config/ServerConfigUsersPanel.tsx @@ -1,6 +1,7 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { createPortal } from "react-dom"; import { Eye, EyeOff, @@ -13,6 +14,7 @@ import { ShieldAlert, Trash2, UserPlus, + X, } from "lucide-react"; import type { ServerConfigSnapshot, @@ -63,28 +65,10 @@ interface ServerConfigUsersPanelProps { confirmation: string; removeHome: boolean; }) => void; - onRequestDeleteGroupConfirm: (options: { - groupName: string; - gid: number; - members: string[]; - primaryUsers: string[]; - confirmation: string; - }) => void; } const accountNamePattern = /^[a-z_][a-z0-9_-]{0,31}$/; const sensitiveGroups = new Set(["docker", "root", "sudo", "wheel"]); -const protectedGroupNames = new Set([ - "root", - "sudo", - "wheel", - "docker", - "adm", - "www-data", - "systemd-journal", - "ssh", - "sshd", -]); const publicKeyPattern = /^(?:ssh-(?:ed25519|rsa)|ecdsa-sha2-nistp(?:256|384|521)|sk-ssh-ed25519@openssh\.com|sk-ecdsa-sha2-nistp256@openssh\.com)\s+[A-Za-z0-9+/]+={0,3}(?:\s+.*)?$/; export default function ServerConfigUsersPanel({ @@ -101,9 +85,9 @@ export default function ServerConfigUsersPanel({ onRequestSshKeyAddConfirm, onRequestSshKeyRevokeConfirm, onRequestDeleteUserConfirm, - onRequestDeleteGroupConfirm, }: ServerConfigUsersPanelProps) { const [previewMode, setPreviewMode] = useState<"user" | "group">("user"); + const [managementModalOpen, setManagementModalOpen] = useState(false); const [previewUsername, setPreviewUsername] = useState("deploy"); const [previewGroupName, setPreviewGroupName] = useState("docker"); const [userPurpose, setUserPurpose] = useState<"ssh" | "local">("ssh"); @@ -129,12 +113,6 @@ export default function ServerConfigUsersPanel({ ); const [deleteUserConfirmation, setDeleteUserConfirmation] = useState(""); const [deleteUserHome, setDeleteUserHome] = useState(false); - const [managedGroup, setManagedGroup] = useState( - snapshot.systemGroups.includes("docker") - ? "docker" - : snapshot.systemGroups[0] ?? "", - ); - const [deleteGroupConfirmation, setDeleteGroupConfirmation] = useState(""); const [passwordEditorOpen, setPasswordEditorOpen] = useState(false); const [editPassword, setEditPassword] = useState(""); const [editPasswordConfirm, setEditPasswordConfirm] = useState(""); @@ -145,6 +123,19 @@ export default function ServerConfigUsersPanel({ const [sshKeyLabel, setSshKeyLabel] = useState("Personal laptop"); const [sshPublicKey, setSshPublicKey] = useState(""); const [sshKeyError, setSshKeyError] = useState(null); + + useEffect(() => { + if (!managementModalOpen) return; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setManagementModalOpen(false); + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [managementModalOpen]); const users = useMemo( () => [ ...(snapshot.rootUser ? [snapshot.rootUser] : []), @@ -219,24 +210,6 @@ export default function ServerConfigUsersPanel({ : loginMethod === "password" ? `Create ${normalizedUsername || ""} and prepare password-based SSH login.` : `Create ${normalizedUsername || ""} and install an SSH public key.`; - const managedGroupDetails = snapshot.systemGroupDetails.find( - (group) => group.name === managedGroup, - ); - const managedGroupUsers = managedGroupDetails?.members ?? []; - const managedGroupPrimaryUsers = managedGroupDetails?.primaryUsers ?? []; - const managedGroupBlockReason = !managedGroup - ? "Select a group to inspect." - : !managedGroupDetails || snapshot.gidMin == null - ? "Group metadata or the host GID_MIN policy is unavailable. Refresh Server Config." - : protectedGroupNames.has(managedGroup) - ? `${managedGroup} is protected because it is a system or privileged group.` - : managedGroupDetails.gid < snapshot.gidMin - ? `GID ${managedGroupDetails.gid} is below the host GID_MIN (${snapshot.gidMin}).` - : managedGroupPrimaryUsers.length > 0 - ? `This is the primary group for ${managedGroupPrimaryUsers.join(", ")}.` - : managedGroupUsers.length > 0 - ? `Remove its members first: ${managedGroupUsers.join(", ")}.` - : null; const toggleGroup = (group: string) => { setSelectedGroups((current) => @@ -292,6 +265,7 @@ export default function ServerConfigUsersPanel({ remoteLogin, credential, }); + setManagementModalOpen(false); setGuidedSetupError(null); setSetupPassword(""); setSetupPasswordConfirm(""); @@ -408,57 +382,78 @@ export default function ServerConfigUsersPanel({ style={{ display: "grid", gap: 16 }} hidden={snapshotLoadError != null} > -
-
-
- - User Inventory - -

- Showing root and non-root accounts together with their detected - groups. -

-
-
- - -
-
-
+
+ + User & Group Management + +

+ Create host accounts, create groups, and assign supplementary + group access without opening a terminal. +

+
+ +
+ + {managementModalOpen && typeof document !== "undefined" + ? createPortal( +
+
+ +
- + User & Group Management

- Create passwordless host accounts, create groups, and assign - supplementary group access without opening a terminal. + Create host accounts, create groups, and assign supplementary + group access without opening a terminal.

@@ -490,7 +488,8 @@ export default function ServerConfigUsersPanel({
@@ -518,8 +517,7 @@ export default function ServerConfigUsersPanel({ className="btn" onClick={() => setPreviewMode(item.key as "user" | "group")} style={{ - height: 40, - minHeight: 40, + minHeight: 44, padding: "0 12px", justifyContent: "center", borderColor: active ? "var(--accent)" : "var(--border)", @@ -1035,6 +1033,7 @@ export default function ServerConfigUsersPanel({ if (previewMode === "user") { stageGuidedUserSetup(); } else { + setManagementModalOpen(false); onRequestCreateGroupConfirm(normalizedGroupName); } }} @@ -1068,170 +1067,12 @@ export default function ServerConfigUsersPanel({ ) : null}
-
-
-
-
- - Existing Groups - -

- Inspect group usage before opening the protected delete flow. -

-
- -
- -
-
- ({ - value: group.name, - label: group.name, - description: protectedGroupNames.has(group.name) || - (snapshot.gidMin != null && group.gid < snapshot.gidMin) - ? "Protected group" - : group.members.length > 0 || group.primaryUsers.length > 0 - ? "In use" - : `GID ${group.gid} ยท eligible for review`, - }))} - onChange={(value) => { - setManagedGroup(value); - setDeleteGroupConfirmation(""); - }} - placeholder="Select an existing group..." - searchPlaceholder="Search group..." - emptyText="No matching group found" - /> - {managedGroup ? ( -
- - +
- ) : null} -
- -
-
- {managedGroupBlockReason ? ( - - ) : ( - - )} - - Delete group - -
-

- {managedGroupBlockReason ?? - "No members were detected. The live feature will re-check GID_MIN, primary-group usage, and membership on the host immediately before deletion."} -

- {!managedGroupBlockReason && managedGroup ? ( - <> - - setDeleteGroupConfirmation(event.target.value) - } - placeholder={`Type ${managedGroup} to confirm`} - autoComplete="off" - /> - - - ) : null} -
-
-
+
, + document.body, + ) + : null} {users.length > 0 ? (