From e3d9eb8f0cf307951d694578c5230c977163bbb9 Mon Sep 17 00:00:00 2001 From: tgt Date: Sun, 16 Feb 2025 15:03:44 -0500 Subject: [PATCH] feat: admins can edit user, other bug fixes and imporvements in admin dash, homepage css changes --- apps/web/src/app/(main)/admin/page.tsx | 24 ++- .../web/src/app/(main)/admin/users/actions.ts | 31 +++- .../src/app/(main)/admin/users/columns.tsx | 137 ++++++++---------- .../src/app/(main)/admin/users/components.tsx | 108 ++++++++++++++ .../app/(main)/admin/workspaces/actions.ts | 3 +- .../(main)/admin/workspaces/components.tsx | 11 +- apps/web/src/app/(main)/page.tsx | 4 +- apps/web/src/app/(main)/sessions/page.tsx | 2 +- 8 files changed, 234 insertions(+), 86 deletions(-) create mode 100644 apps/web/src/app/(main)/admin/users/components.tsx diff --git a/apps/web/src/app/(main)/admin/page.tsx b/apps/web/src/app/(main)/admin/page.tsx index 48c45d8..87bc96b 100644 --- a/apps/web/src/app/(main)/admin/page.tsx +++ b/apps/web/src/app/(main)/admin/page.tsx @@ -1,7 +1,9 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import auth from "@stardust/common/auth"; +import { stardustConnector } from "@stardust/common/daemon/client"; +import { getConfig } from "@stardust/config"; import db, { workspace, user } from "@stardust/db"; -import { Container, Layers, Users } from "lucide-react"; +import { Boxes, Container, Layers, Users } from "lucide-react"; import { headers } from "next/headers"; function mode(arr: Array) { return arr.sort((a, b) => arr.filter((v) => v === a).length - arr.filter((v) => v === b).length).pop(); @@ -18,6 +20,16 @@ export default async function AdminPage() { const workspaces = await tx.select().from(workspace); return { users, sessions, workspaces }; }); + const nodes = await Promise.all( + getConfig().nodes.map(async (n) => { + const node = stardustConnector(n); + return { + ...n, + health: (await node.healthcheck.get()).data, + }; + }), + ); + const averageCpuUsage = nodes.reduce((acc, node) => acc + Number(node.health?.cpu), 0) / nodes.length; const activeUsers = [...new Set(sessions.map((s) => s.userId))]; const admins = users.filter((u) => u.role === "admin"); return ( @@ -60,6 +72,16 @@ export default async function AdminPage() {

+ + + Nodes + + + +
{nodes.length}
+

Average CPU usage is {averageCpuUsage.toFixed(2)}%

+
+
); diff --git a/apps/web/src/app/(main)/admin/users/actions.ts b/apps/web/src/app/(main)/admin/users/actions.ts index 169646a..bde51e3 100644 --- a/apps/web/src/app/(main)/admin/users/actions.ts +++ b/apps/web/src/app/(main)/admin/users/actions.ts @@ -3,7 +3,7 @@ import { check } from "@/lib/admin-check"; import { deleteSession } from "@/lib/session/manage"; import auth from "@stardust/common/auth"; import { hashPassword } from "@stardust/common/auth/lib"; -import db, { account, session } from "@stardust/db"; +import db, { account, session, user } from "@stardust/db"; import { and, eq } from "@stardust/db/utils"; import { revalidatePath } from "next/cache"; import { headers } from "next/headers"; @@ -74,3 +74,32 @@ export async function resetPassword(id: string, data: FormData) { return { error: (e as Error).message }; } } +export async function updateUser(id: string, data: FormData) { + await check(); + try { + const where = eq(user.id, id); + const [dbEntry] = await db.select().from(user).where(where); + if (!dbEntry) throw new Error("no user found"); + const name = data.get("name")?.toString(); + const email = data.get("email")?.toString(); + const image = data.get("image")?.toString(); + const role = data.get("role")?.toString(); + const res = await db + .update(user) + .set({ + name, + email, + image, + role, + }) + .where(where) + .returning(); + revalidateHandler(); + return { + error: undefined, + ...res[0], + }; + } catch (e) { + return { error: (e as Error).message }; + } +} diff --git a/apps/web/src/app/(main)/admin/users/columns.tsx b/apps/web/src/app/(main)/admin/users/columns.tsx index 0c05c80..20015db 100644 --- a/apps/web/src/app/(main)/admin/users/columns.tsx +++ b/apps/web/src/app/(main)/admin/users/columns.tsx @@ -1,9 +1,16 @@ "use client"; import { DataTableColumnHeader } from "@/components/data-table/column-header"; -import { SubmitButton } from "@/components/submit-button"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuCheckboxItem, @@ -13,16 +20,16 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import authClient from "@/lib/auth-client"; import type { ErrorContext } from "@stardust/common/auth/lib"; import type { SelectUserRelation } from "@stardust/db/relational-types"; import type { ColumnDef } from "@tanstack/react-table"; import { MoreHorizontal } from "lucide-react"; +import Image from "next/image"; import { useState } from "react"; import { toast } from "sonner"; -import { deleteUserSessions, resetPassword, revalidateHandler, safeDeleteUser } from "./actions"; +import { deleteUserSessions, revalidateHandler, safeDeleteUser } from "./actions"; +import { ResetPasswordDialog, UpdateUserDialog } from "./components"; const clientOptions = { onSuccess() { revalidateHandler(); @@ -36,6 +43,22 @@ export const columns: ColumnDef[] = [ accessorKey: "name", header: ({ column }) => , }, + { + accessorKey: "image", + header: "Profile Picture", + cell: ({ row }) => + row.original.image ? ( + {row.original.name} + ) : ( + <>None + ), + }, { accessorKey: "email", header: ({ column }) => , @@ -55,47 +78,34 @@ export const columns: ColumnDef[] = [ id: "actions", cell: ({ row: { original: user } }) => { const [resetDialogOpen, setResetDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [updateDialogOpen, setUpdateDialogOpen] = useState(false); return ( <> - - - - Reset password for {user.name || user.email} - -
- toast.promise( - async () => { - const res = await resetPassword(user.id, data); - if (res?.error) throw new Error(res.error); - return res; - }, - { - loading: "Resetting password...", - success: "Password reset", - error: (error) => `Failed to reset password: ${error.message}`, - }, - ) - } - className="flex flex-col gap-2 w-full" - > - - -
- - -
- Submit -
-
-
+ + + + + + Delete User + Are you sure you want to delete {user.email}? + + + Nevermind + + toast.promise(() => safeDeleteUser(user.id), { + loading: "Deleting user...", + success: "User deleted", + error: (error) => `Failed to delete user: ${error.message}`, + }) + } + > + Yes, delete + + + +