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 ? (
+
+ ) : (
+ <>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 (
<>
-
+
+
+
+
+
+ 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
+
+
+
+
>
diff --git a/apps/web/src/app/(main)/admin/users/components.tsx b/apps/web/src/app/(main)/admin/users/components.tsx
new file mode 100644
index 0000000..e58992d
--- /dev/null
+++ b/apps/web/src/app/(main)/admin/users/components.tsx
@@ -0,0 +1,108 @@
+import { SubmitButton } from "@/components/submit-button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import authClient from "@/lib/auth-client";
+import type { SelectUser } from "@stardust/db";
+import { toast } from "sonner";
+import { resetPassword, updateUser } from "./actions";
+
+export interface Props {
+ user: SelectUser;
+ setOpen: React.Dispatch>;
+ open: boolean;
+}
+export function ResetPasswordDialog({ user, open, setOpen }: Props) {
+ return (
+
+ );
+}
+export function UpdateUserDialog({ user, open, setOpen }: Props) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/app/(main)/admin/workspaces/actions.ts b/apps/web/src/app/(main)/admin/workspaces/actions.ts
index aa6f2d7..2ff8f66 100644
--- a/apps/web/src/app/(main)/admin/workspaces/actions.ts
+++ b/apps/web/src/app/(main)/admin/workspaces/actions.ts
@@ -7,7 +7,6 @@ import { getConfig } from "@stardust/config";
import db, { type SelectWorkspace, session, workspace } from "@stardust/db";
import { eq } from "@stardust/db/utils";
import { revalidatePath } from "next/cache";
-import { redirect } from "next/navigation";
export async function updateWorkspace(data: FormData) {
await check();
@@ -25,7 +24,7 @@ export async function updateWorkspace(data: FormData) {
.update(workspace)
.set(fields)
.where(eq(workspace.dockerImage, data.get("dockerImage")?.toString() as string));
- redirect("/admin/workspaces");
+ revalidatePath("/admin/workspaces");
}
export async function deleteWorkspace(w: SelectWorkspace) {
await check();
diff --git a/apps/web/src/app/(main)/admin/workspaces/components.tsx b/apps/web/src/app/(main)/admin/workspaces/components.tsx
index 0e2b5d5..69a22a2 100644
--- a/apps/web/src/app/(main)/admin/workspaces/components.tsx
+++ b/apps/web/src/app/(main)/admin/workspaces/components.tsx
@@ -31,7 +31,16 @@ export function UpdateDialog({ workspace, open, setOpen }: Props) {
Edit {workspace.friendlyName} ({workspace.dockerImage})
-