feat(users): Convert the user and group management UI/UX from a native view to a modal
Build And Publish Doktainer Image / docker (push) Canceled after 0s
Build And Publish Doktainer Image / docker (push) Canceled after 0s
This commit is contained in:
1 parent
14411e0cf8
commit
bbfe4c6680
2 files changed
+97
-278
No files matched your search
@@ -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 = (
|
const requestSshAccessUpdateConfirm = (
|
||||||
options: ServerSshAccessUpdateBody,
|
options: ServerSshAccessUpdateBody,
|
||||||
) => {
|
) => {
|
||||||
@@ -1008,7 +987,6 @@ export default function ServerConfigModal({
|
|||||||
requestSystemUserSshKeyRevokeConfirm
|
requestSystemUserSshKeyRevokeConfirm
|
||||||
}
|
}
|
||||||
onRequestDeleteUserConfirm={requestSystemUserDeleteConfirm}
|
onRequestDeleteUserConfirm={requestSystemUserDeleteConfirm}
|
||||||
onRequestDeleteGroupConfirm={requestSystemGroupDeleteConfirm}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "ssh-access":
|
case "ssh-access":
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
@@ -13,6 +14,7 @@ import {
|
|||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
Trash2,
|
Trash2,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
ServerConfigSnapshot,
|
ServerConfigSnapshot,
|
||||||
@@ -63,28 +65,10 @@ interface ServerConfigUsersPanelProps {
|
|||||||
confirmation: string;
|
confirmation: string;
|
||||||
removeHome: boolean;
|
removeHome: boolean;
|
||||||
}) => void;
|
}) => void;
|
||||||
onRequestDeleteGroupConfirm: (options: {
|
|
||||||
groupName: string;
|
|
||||||
gid: number;
|
|
||||||
members: string[];
|
|
||||||
primaryUsers: string[];
|
|
||||||
confirmation: string;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountNamePattern = /^[a-z_][a-z0-9_-]{0,31}$/;
|
const accountNamePattern = /^[a-z_][a-z0-9_-]{0,31}$/;
|
||||||
const sensitiveGroups = new Set(["docker", "root", "sudo", "wheel"]);
|
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+.*)?$/;
|
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({
|
export default function ServerConfigUsersPanel({
|
||||||
@@ -101,9 +85,9 @@ export default function ServerConfigUsersPanel({
|
|||||||
onRequestSshKeyAddConfirm,
|
onRequestSshKeyAddConfirm,
|
||||||
onRequestSshKeyRevokeConfirm,
|
onRequestSshKeyRevokeConfirm,
|
||||||
onRequestDeleteUserConfirm,
|
onRequestDeleteUserConfirm,
|
||||||
onRequestDeleteGroupConfirm,
|
|
||||||
}: ServerConfigUsersPanelProps) {
|
}: ServerConfigUsersPanelProps) {
|
||||||
const [previewMode, setPreviewMode] = useState<"user" | "group">("user");
|
const [previewMode, setPreviewMode] = useState<"user" | "group">("user");
|
||||||
|
const [managementModalOpen, setManagementModalOpen] = useState(false);
|
||||||
const [previewUsername, setPreviewUsername] = useState("deploy");
|
const [previewUsername, setPreviewUsername] = useState("deploy");
|
||||||
const [previewGroupName, setPreviewGroupName] = useState("docker");
|
const [previewGroupName, setPreviewGroupName] = useState("docker");
|
||||||
const [userPurpose, setUserPurpose] = useState<"ssh" | "local">("ssh");
|
const [userPurpose, setUserPurpose] = useState<"ssh" | "local">("ssh");
|
||||||
@@ -129,12 +113,6 @@ export default function ServerConfigUsersPanel({
|
|||||||
);
|
);
|
||||||
const [deleteUserConfirmation, setDeleteUserConfirmation] = useState("");
|
const [deleteUserConfirmation, setDeleteUserConfirmation] = useState("");
|
||||||
const [deleteUserHome, setDeleteUserHome] = useState(false);
|
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 [passwordEditorOpen, setPasswordEditorOpen] = useState(false);
|
||||||
const [editPassword, setEditPassword] = useState("");
|
const [editPassword, setEditPassword] = useState("");
|
||||||
const [editPasswordConfirm, setEditPasswordConfirm] = useState("");
|
const [editPasswordConfirm, setEditPasswordConfirm] = useState("");
|
||||||
@@ -145,6 +123,19 @@ export default function ServerConfigUsersPanel({
|
|||||||
const [sshKeyLabel, setSshKeyLabel] = useState("Personal laptop");
|
const [sshKeyLabel, setSshKeyLabel] = useState("Personal laptop");
|
||||||
const [sshPublicKey, setSshPublicKey] = useState("");
|
const [sshPublicKey, setSshPublicKey] = useState("");
|
||||||
const [sshKeyError, setSshKeyError] = useState<string | null>(null);
|
const [sshKeyError, setSshKeyError] = useState<string | null>(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(
|
const users = useMemo(
|
||||||
() => [
|
() => [
|
||||||
...(snapshot.rootUser ? [snapshot.rootUser] : []),
|
...(snapshot.rootUser ? [snapshot.rootUser] : []),
|
||||||
@@ -219,24 +210,6 @@ export default function ServerConfigUsersPanel({
|
|||||||
: loginMethod === "password"
|
: loginMethod === "password"
|
||||||
? `Create ${normalizedUsername || "<username>"} and prepare password-based SSH login.`
|
? `Create ${normalizedUsername || "<username>"} and prepare password-based SSH login.`
|
||||||
: `Create ${normalizedUsername || "<username>"} and install an SSH public key.`;
|
: `Create ${normalizedUsername || "<username>"} 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) => {
|
const toggleGroup = (group: string) => {
|
||||||
setSelectedGroups((current) =>
|
setSelectedGroups((current) =>
|
||||||
@@ -292,6 +265,7 @@ export default function ServerConfigUsersPanel({
|
|||||||
remoteLogin,
|
remoteLogin,
|
||||||
credential,
|
credential,
|
||||||
});
|
});
|
||||||
|
setManagementModalOpen(false);
|
||||||
setGuidedSetupError(null);
|
setGuidedSetupError(null);
|
||||||
setSetupPassword("");
|
setSetupPassword("");
|
||||||
setSetupPasswordConfirm("");
|
setSetupPasswordConfirm("");
|
||||||
@@ -408,57 +382,78 @@ export default function ServerConfigUsersPanel({
|
|||||||
style={{ display: "grid", gap: 16 }}
|
style={{ display: "grid", gap: 16 }}
|
||||||
hidden={snapshotLoadError != null}
|
hidden={snapshotLoadError != null}
|
||||||
>
|
>
|
||||||
<div className="card" style={{ padding: 18 }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 12,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<strong style={{ color: "var(--text-primary)", fontSize: 14 }}>
|
|
||||||
User Inventory
|
|
||||||
</strong>
|
|
||||||
<p
|
|
||||||
style={{ marginTop: 6, color: "var(--text-muted)", fontSize: 13 }}
|
|
||||||
>
|
|
||||||
Showing root and non-root accounts together with their detected
|
|
||||||
groups.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: 8,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserBadge
|
|
||||||
label={`${snapshot.hasRootUser ? 1 : 0} root`}
|
|
||||||
tone="danger"
|
|
||||||
/>
|
|
||||||
<UserBadge
|
|
||||||
label={`${snapshot.nonRootUsers.length} non-root`}
|
|
||||||
tone="info"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className="card"
|
className="card"
|
||||||
style={{
|
style={{
|
||||||
padding: 18,
|
padding: 18,
|
||||||
display: "grid",
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
gap: 16,
|
gap: 16,
|
||||||
|
flexWrap: "wrap",
|
||||||
borderColor: "rgba(59,130,246,0.24)",
|
borderColor: "rgba(59,130,246,0.24)",
|
||||||
background:
|
background:
|
||||||
"linear-gradient(180deg, rgba(59,130,246,0.045), var(--bg-card) 68%)",
|
"linear-gradient(180deg, rgba(59,130,246,0.045), transparent 68%), var(--bg-card)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<strong style={{ color: "var(--text-primary)", fontSize: 14 }}>
|
||||||
|
User & Group Management
|
||||||
|
</strong>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
marginTop: 6,
|
||||||
|
color: "var(--text-muted)",
|
||||||
|
fontSize: 13,
|
||||||
|
maxWidth: 620,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create host accounts, create groups, and assign supplementary
|
||||||
|
group access without opening a terminal.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => setManagementModalOpen(true)}
|
||||||
|
style={{ minHeight: 44, justifyContent: "center" }}
|
||||||
|
>
|
||||||
|
<UserPlus size={14} /> Manage Users & Groups
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{managementModalOpen && typeof document !== "undefined"
|
||||||
|
? createPortal(
|
||||||
|
<div className="modal-overlay" style={{ zIndex: 1003 }}>
|
||||||
|
<div className="modal-shell" style={{ maxWidth: 920 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="modal-close"
|
||||||
|
onClick={() => setManagementModalOpen(false)}
|
||||||
|
aria-label="Close user and group management modal"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
<X size={22} />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
className="modal animate-slide-in"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="user-group-management-title"
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: 920,
|
||||||
|
maxHeight: "90vh",
|
||||||
|
padding: 24,
|
||||||
|
display: "grid",
|
||||||
|
gap: 16,
|
||||||
|
overflowY: "auto",
|
||||||
|
borderColor: "rgba(59,130,246,0.24)",
|
||||||
|
background:
|
||||||
|
"linear-gradient(180deg, rgba(59,130,246,0.045), transparent 68%), var(--bg-card)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -469,7 +464,10 @@ export default function ServerConfigUsersPanel({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<strong style={{ color: "var(--text-primary)", fontSize: 14 }}>
|
<strong
|
||||||
|
id="user-group-management-title"
|
||||||
|
style={{ color: "var(--text-primary)", fontSize: 14 }}
|
||||||
|
>
|
||||||
User & Group Management
|
User & Group Management
|
||||||
</strong>
|
</strong>
|
||||||
<p
|
<p
|
||||||
@@ -480,8 +478,8 @@ export default function ServerConfigUsersPanel({
|
|||||||
maxWidth: 620,
|
maxWidth: 620,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Create passwordless host accounts, create groups, and assign
|
Create host accounts, create groups, and assign supplementary
|
||||||
supplementary group access without opening a terminal.
|
group access without opening a terminal.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UserBadge label="Audited action" tone="success" />
|
<UserBadge label="Audited action" tone="success" />
|
||||||
@@ -490,7 +488,8 @@ export default function ServerConfigUsersPanel({
|
|||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
|
gridTemplateColumns:
|
||||||
|
"repeat(auto-fit, minmax(min(260px, 100%), 1fr))",
|
||||||
gap: 16,
|
gap: 16,
|
||||||
alignItems: "start",
|
alignItems: "start",
|
||||||
}}
|
}}
|
||||||
@@ -501,7 +500,7 @@ export default function ServerConfigUsersPanel({
|
|||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 1fr",
|
gridTemplateColumns: "1fr 1fr",
|
||||||
gap: 8,
|
gap: 8,
|
||||||
height: 40,
|
minHeight: 44,
|
||||||
alignSelf: "start",
|
alignSelf: "start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -518,8 +517,7 @@ export default function ServerConfigUsersPanel({
|
|||||||
className="btn"
|
className="btn"
|
||||||
onClick={() => setPreviewMode(item.key as "user" | "group")}
|
onClick={() => setPreviewMode(item.key as "user" | "group")}
|
||||||
style={{
|
style={{
|
||||||
height: 40,
|
minHeight: 44,
|
||||||
minHeight: 40,
|
|
||||||
padding: "0 12px",
|
padding: "0 12px",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
borderColor: active ? "var(--accent)" : "var(--border)",
|
borderColor: active ? "var(--accent)" : "var(--border)",
|
||||||
@@ -1035,6 +1033,7 @@ export default function ServerConfigUsersPanel({
|
|||||||
if (previewMode === "user") {
|
if (previewMode === "user") {
|
||||||
stageGuidedUserSetup();
|
stageGuidedUserSetup();
|
||||||
} else {
|
} else {
|
||||||
|
setManagementModalOpen(false);
|
||||||
onRequestCreateGroupConfirm(normalizedGroupName);
|
onRequestCreateGroupConfirm(normalizedGroupName);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1068,170 +1067,12 @@ export default function ServerConfigUsersPanel({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className="card"
|
|
||||||
style={{
|
|
||||||
padding: 18,
|
|
||||||
display: "grid",
|
|
||||||
gap: 14,
|
|
||||||
borderColor: "rgba(239,68,68,0.18)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: 12,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<strong style={{ color: "var(--text-primary)", fontSize: 14 }}>
|
|
||||||
Existing Groups
|
|
||||||
</strong>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
marginTop: 5,
|
|
||||||
color: "var(--text-muted)",
|
|
||||||
fontSize: 12,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Inspect group usage before opening the protected delete flow.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<UserBadge label="Audited action" tone="success" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
|
|
||||||
gap: 14,
|
|
||||||
alignItems: "start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "grid", gap: 10 }}>
|
|
||||||
<SearchableSelect
|
|
||||||
value={managedGroup}
|
|
||||||
options={snapshot.systemGroupDetails.map((group) => ({
|
|
||||||
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 ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: 7,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserBadge
|
|
||||||
label={managedGroupBlockReason ? "Deletion locked" : "Candidate"}
|
|
||||||
tone={managedGroupBlockReason ? "warning" : "success"}
|
|
||||||
/>
|
|
||||||
<UserBadge
|
|
||||||
label={`${managedGroupUsers.length} detected member${managedGroupUsers.length === 1 ? "" : "s"}`}
|
|
||||||
tone="neutral"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>,
|
||||||
</div>
|
document.body,
|
||||||
|
)
|
||||||
<div
|
: null}
|
||||||
style={{
|
|
||||||
display: "grid",
|
|
||||||
gap: 10,
|
|
||||||
padding: 12,
|
|
||||||
borderRadius: 9,
|
|
||||||
border: managedGroupBlockReason
|
|
||||||
? "1px solid rgba(245,158,11,0.24)"
|
|
||||||
: "1px solid rgba(239,68,68,0.24)",
|
|
||||||
background: managedGroupBlockReason
|
|
||||||
? "rgba(245,158,11,0.055)"
|
|
||||||
: "rgba(239,68,68,0.035)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
||||||
{managedGroupBlockReason ? (
|
|
||||||
<LockKeyhole size={15} color="#f59e0b" />
|
|
||||||
) : (
|
|
||||||
<Trash2 size={15} color="#ef4444" />
|
|
||||||
)}
|
|
||||||
<strong style={{ color: "var(--text-primary)", fontSize: 13 }}>
|
|
||||||
Delete group
|
|
||||||
</strong>
|
|
||||||
</div>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
color: managedGroupBlockReason ? "#b45309" : "var(--text-muted)",
|
|
||||||
fontSize: 11,
|
|
||||||
lineHeight: 1.5,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{managedGroupBlockReason ??
|
|
||||||
"No members were detected. The live feature will re-check GID_MIN, primary-group usage, and membership on the host immediately before deletion."}
|
|
||||||
</p>
|
|
||||||
{!managedGroupBlockReason && managedGroup ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
className="input"
|
|
||||||
value={deleteGroupConfirmation}
|
|
||||||
onChange={(event) =>
|
|
||||||
setDeleteGroupConfirmation(event.target.value)
|
|
||||||
}
|
|
||||||
placeholder={`Type ${managedGroup} to confirm`}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm"
|
|
||||||
disabled={
|
|
||||||
deleteGroupConfirmation !== managedGroup ||
|
|
||||||
!canManageSystemAccounts ||
|
|
||||||
!managedGroupDetails ||
|
|
||||||
isActionRunning(`system-group:delete:${managedGroup}`)
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (!managedGroupDetails) return;
|
|
||||||
onRequestDeleteGroupConfirm({
|
|
||||||
groupName: managedGroupDetails.name,
|
|
||||||
gid: managedGroupDetails.gid,
|
|
||||||
members: managedGroupDetails.members,
|
|
||||||
primaryUsers: managedGroupDetails.primaryUsers,
|
|
||||||
confirmation: deleteGroupConfirmation,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
justifyContent: "center",
|
|
||||||
color: "#ef4444",
|
|
||||||
borderColor: "rgba(239,68,68,0.32)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 size={13} /> Review Group Deletion
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{users.length > 0 ? (
|
{users.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
Reference in new issue
Block a user