aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web/app
diff options
context:
space:
mode:
authorMohamedBassem <me@mbassem.com>2024-03-01 23:17:27 +0000
committerMohamedBassem <me@mbassem.com>2024-03-01 23:19:03 +0000
commit7ddcfb630d3dec3d9fecbfd6a498ca7c572809ec (patch)
treec0339e9181b35d0819bc0bfd1219ccdb262e54d2 /packages/web/app
parenta5434730ede1272f195d6a4b13207b840a5ac2cf (diff)
downloadkarakeep-7ddcfb630d3dec3d9fecbfd6a498ca7c572809ec.tar.zst
feature: Add an admin page showing server stats and actions
Diffstat (limited to 'packages/web/app')
-rw-r--r--packages/web/app/dashboard/admin/page.tsx123
-rw-r--r--packages/web/app/dashboard/components/Sidebar.tsx8
-rw-r--r--packages/web/app/layout.tsx6
3 files changed, 135 insertions, 2 deletions
diff --git a/packages/web/app/dashboard/admin/page.tsx b/packages/web/app/dashboard/admin/page.tsx
new file mode 100644
index 00000000..26b6447b
--- /dev/null
+++ b/packages/web/app/dashboard/admin/page.tsx
@@ -0,0 +1,123 @@
+"use client";
+
+import { ActionButton } from "@/components/ui/action-button";
+import LoadingSpinner from "@/components/ui/spinner";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { toast } from "@/components/ui/use-toast";
+import { api } from "@/lib/trpc";
+import { keepPreviousData } from "@tanstack/react-query";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+
+export default function AdminPage() {
+ const router = useRouter();
+ const { data: session, status } = useSession();
+
+ if (status == "loading") {
+ return <LoadingSpinner />;
+ }
+
+ if (!session || session.user.role != "admin") {
+ router.push("/");
+ return;
+ }
+
+ const { data } = api.admin.stats.useQuery(undefined, {
+ refetchInterval: 1000,
+ placeholderData: keepPreviousData,
+ });
+
+ const { mutate: recrawlLinks, isPending: isRecrawlPending } =
+ api.admin.recrawlAllLinks.useMutation({
+ onSuccess: () => {
+ toast({
+ description: "Recrawl enqueued",
+ });
+ },
+ onError: (e) => {
+ toast({
+ variant: "destructive",
+ description: e.message,
+ });
+ },
+ });
+
+ const { mutate: reindexBookmarks, isPending: isReindexPending } =
+ api.admin.reindexAllBookmarks.useMutation({
+ onSuccess: () => {
+ toast({
+ description: "Reindex enqueued",
+ });
+ },
+ onError: (e) => {
+ toast({
+ variant: "destructive",
+ description: e.message,
+ });
+ },
+ });
+
+ return (
+ <div className="m-4 flex flex-col gap-5 rounded-md border bg-white p-4">
+ <p className="text-2xl">Admin</p>
+ <hr />
+ {data ? (
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>Stats</TableHead>
+ <TableHead>Value</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ <TableRow>
+ <TableCell>Num Users</TableCell>
+ <TableCell>{data.numUsers}</TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell>Num Bookmarks</TableCell>
+ <TableCell>{data.numBookmarks}</TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell>Pending Crawling Jobs</TableCell>
+ <TableCell>{data.pendingCrawls}</TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell>Pending Indexing Jobs</TableCell>
+ <TableCell>{data.pendingIndexing}</TableCell>
+ </TableRow>
+ <TableRow>
+ <TableCell>Pending OpenAI Jobs</TableCell>
+ <TableCell>{data.pendingOpenai}</TableCell>
+ </TableRow>
+ </TableBody>
+ </Table>
+ ) : (
+ <LoadingSpinner />
+ )}
+ <hr />
+ <p className="text-xl">Actions</p>
+ <ActionButton
+ variant="destructive"
+ loading={isRecrawlPending}
+ onClick={() => recrawlLinks()}
+ >
+ Recrawl All Links
+ </ActionButton>
+ <ActionButton
+ variant="destructive"
+ loading={isReindexPending}
+ onClick={() => reindexBookmarks()}
+ >
+ Reindex All Bookmarks
+ </ActionButton>
+ </div>
+ );
+}
diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx
index 010ee103..29e5baed 100644
--- a/packages/web/app/dashboard/components/Sidebar.tsx
+++ b/packages/web/app/dashboard/components/Sidebar.tsx
@@ -6,6 +6,7 @@ import {
PackageOpen,
Settings,
Search,
+ Shield,
} from "lucide-react";
import { redirect } from "next/navigation";
import SidebarItem from "./SidebarItem";
@@ -61,6 +62,13 @@ export default async function Sidebar() {
name="Settings"
path="/dashboard/settings"
/>
+ {session.user.role == "admin" && (
+ <SidebarItem
+ logo={<Shield />}
+ name="Admin"
+ path="/dashboard/admin"
+ />
+ )}
</ul>
</div>
<Separator />
diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx
index d597063b..a8423031 100644
--- a/packages/web/app/layout.tsx
+++ b/packages/web/app/layout.tsx
@@ -5,6 +5,7 @@ import React from "react";
import { Toaster } from "@/components/ui/toaster";
import Providers from "@/lib/providers";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
+import { getServerAuthSession } from "@/server/auth";
const inter = Inter({ subsets: ["latin"] });
@@ -13,15 +14,16 @@ export const metadata: Metadata = {
description: "Your AI powered second brain",
};
-export default function RootLayout({
+export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ const session = await getServerAuthSession();
return (
<html lang="en">
<body className={inter.className}>
- <Providers>
+ <Providers session={session}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</Providers>