import { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Search, Plus, Clock, QrCode, BarChart3, Pencil, Trash2, Layers, Download, MoreVertical, } from "lucide-react"; import { useDeleteUser, useUsers } from "@/api/hooks"; import type { User, UserStatus } from "@/api/types"; import { Button } from "@/components/ui"; import { Pagination } from "@/components/Pagination"; import { GlassCard, ProtocolBadge, StatusBadge } from "@/components/vortexui"; import { useI18n } from "@/i18n/i18n"; import { useTitle } from "@/lib/useTitle"; import { CreateUserModal } from "@/components/CreateUserModal"; import { BulkCreateModal } from "@/components/BulkCreateModal"; import { ImportUsersModal } from "@/components/ImportUsersModal"; import { EditUserModal } from "@/components/EditUserModal"; import { UserUsageModal } from "@/components/UserUsageModal"; import { UserSubModal } from "@/components/UserSubModal"; import { useConfirm } from "@/components/confirm"; import { useToast } from "@/components/toast"; import { useAuth } from "@/auth/auth"; import { cn, formatBytes } from "@/lib/utils"; import { EmptyState } from "@/components/EmptyState"; import { Users as UsersIcon } from "lucide-react"; type StatusFilter = "" | "active" | "warning" | "inactive"; const STATUS_FILTERS: { value: StatusFilter; labelKey: "users.filterAll" | "users.statActive" | "users.filterWarning" | "users.filterInactive" }[] = [ { value: "", labelKey: "users.filterAll" }, { value: "active", labelKey: "users.statActive" }, { value: "warning", labelKey: "users.filterWarning" }, { value: "inactive", labelKey: "users.filterInactive" }, ]; function statusDisplay(status: UserStatus): { type: string; label: string; pulse: boolean } { switch (status) { case "limited": return { type: "warning", label: "WARNING", pulse: false }; case "expired": return { type: "error", label: "ERROR", pulse: false }; case "disabled": case "on_hold": return { type: "inactive", label: "INACTIVE", pulse: false }; default: return { type: "active", label: "ACTIVE", pulse: true }; } } function userQueryParams(filter: StatusFilter): { status?: string; status_group?: string } { switch (filter) { case "active": return { status: "active" }; case "warning": return { status_group: "warning" }; case "inactive": return { status_group: "inactive" }; default: return {}; } } function devicesLabel(u: User): string { const connected = u.device_count ?? u.allowed_hwids?.length ?? 0; if (u.device_limit > 0) return `${connected}/${u.device_limit}`; return `${connected}/∞`; } function shortUserId(index: number): string { return `#${String(index + 1).padStart(4, "0")}`; } export function Users() { useTitle("Users"); const { can } = useAuth(); const canWrite = can("user:write"); const [search, setSearch] = useState(""); const [statusFilter, setStatusFilter] = useState(""); const [page, setPage] = useState(0); const [pageSize] = useState(20); const [modalOpen, setModalOpen] = useState(false); const [bulkOpen, setBulkOpen] = useState(false); const [importOpen, setImportOpen] = useState(false); const [editing, setEditing] = useState(null); const [viewing, setViewing] = useState(null); const [subbing, setSubbing] = useState(null); const [menuUserId, setMenuUserId] = useState(null); const menuRef = useRef(null); const { data, isLoading, error } = useUsers({ search, limit: pageSize, offset: page * pageSize, ...userQueryParams(statusFilter), }); const users = data?.users ?? []; const total = data?.total ?? 0; const showing = users.length; const { t } = useI18n(); const nav = useNavigate(); const del = useDeleteUser(); const confirm = useConfirm(); const toast = useToast(); useEffect(() => { function onDocClick(e: MouseEvent) { if (menuRef.current && !menuRef.current.contains(e.target as Node)) { setMenuUserId(null); } } document.addEventListener("mousedown", onDocClick); return () => document.removeEventListener("mousedown", onDocClick); }, []); function onSearch(v: string) { setSearch(v); setPage(0); } async function remove(u: User) { setMenuUserId(null); const ok = await confirm({ title: `Delete ${u.username}?`, message: "This removes the user and revokes their access on all nodes.", confirmLabel: "Delete", destructive: true, }); if (!ok) return; try { await del.mutateAsync(u.id); toast.success(`Deleted ${u.username}`); } catch { toast.error("Delete failed"); } } const summaryLabel = t("users.showingOf") .replace("{count}", String(showing)) .replace("{total}", String(total)); return (
setModalOpen(false)} /> setBulkOpen(false)} /> setImportOpen(false)} /> setEditing(null)} /> setViewing(null)} /> setSubbing(null)} />

{t("users.managementTitle")}

{total} {t("users.totalUsers")}

{t("users.showingOf").replace("{count}", String(showing)).replace("{total}", String(total))}

{canWrite && ( <> )}
onSearch(e.target.value)} className="w-full h-9 rounded-lg bg-surface/80 border border-border/60 ps-9 pe-3 text-sm text-fg placeholder:text-fg-subtle focus:outline-none focus:border-primary/40 focus:ring-1 focus:ring-primary/20 transition-all input-surface" />
{STATUS_FILTERS.map((f) => ( ))}
{isLoading &&
{Array.from({ length: 5 }).map((_, i) => (
))}
} {error &&
Failed to load users
} {data && (
{users.map((u, i) => { const usagePct = u.data_limit > 0 ? Math.min(100, (u.used_traffic / u.data_limit) * 100) : 0; const st = statusDisplay(u.status); const globalIndex = page * pageSize + i; return ( ); })} {users.length === 0 && ( )}
{t("users.username")} {t("users.protocol")} {t("users.dataUsage")} {t("users.devices")} {t("common.status")} {t("users.expires")} {t("common.actions")}
{u.username.slice(0, 2).toUpperCase()}

ID: {shortUserId(globalIndex)}

{formatBytes(u.used_traffic, false)} /{" "} {u.data_limit > 0 ? formatBytes(u.data_limit, false) : "∞"}
{u.data_limit > 0 && (
90 || u.status === "limited" ? "bg-warning" : usagePct > 70 ? "bg-warning/80" : "bg-primary", )} style={{ width: `${usagePct}%` }} />
)}
{devicesLabel(u)}
{u.expire_at ? new Date(u.expire_at).toLocaleDateString() : "Never"}
{menuUserId === u.id && (
} onClick={() => { setSubbing(u); setMenuUserId(null); }}> {t("users.subscription")} } onClick={() => { setViewing(u); setMenuUserId(null); }}> {t("users.usage")} {canWrite && ( <> } onClick={() => { setEditing(u); setMenuUserId(null); }}> {t("common.edit")} } className="text-danger hover:bg-danger/10" onClick={() => remove(u)} > {t("common.delete")} )}
)}
)} {data && total > 0 && (
)}
); } function MenuAction({ children, icon, onClick, className, }: { children: React.ReactNode; icon: React.ReactNode; onClick: () => void; className?: string; }) { return ( ); }