From c8b3f625cac4771d7998a83782f950ba7ff12ad6 Mon Sep 17 00:00:00 2001 From: Md Saban Date: Sat, 22 Jun 2024 23:09:54 +0530 Subject: [PATCH 1/5] ui: refactor admin ui --- apps/web/app/dashboard/admin/page.tsx | 276 +----------------- .../dashboard/admin/AdminActions.tsx | 77 +++++ .../dashboard/admin/ServerStats.tsx | 133 +++++++++ apps/web/components/dashboard/admin/Users.tsx | 73 +++++ 4 files changed, 293 insertions(+), 266 deletions(-) create mode 100644 apps/web/components/dashboard/admin/AdminActions.tsx create mode 100644 apps/web/components/dashboard/admin/ServerStats.tsx create mode 100644 apps/web/components/dashboard/admin/Users.tsx diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx index 155b2e17..8b066d3a 100644 --- a/apps/web/app/dashboard/admin/page.tsx +++ b/apps/web/app/dashboard/admin/page.tsx @@ -1,266 +1,11 @@ "use client"; import { useRouter } from "next/navigation"; -import { ActionButton } from "@/components/ui/action-button"; -import { Separator } from "@/components/ui/separator"; +import ServerStats from "@/components/dashboard/admin/ServerStats"; +import Users from "@/components/dashboard/admin/Users"; 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 { useClientConfig } from "@/lib/clientConfig"; -import { api } from "@/lib/trpc"; -import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { Trash } from "lucide-react"; import { useSession } from "next-auth/react"; -const REPO_LATEST_RELEASE_API = - "https://api.github.com/repos/hoarder-app/hoarder/releases/latest"; -const REPO_RELEASE_PAGE = "https://github.com/hoarder-app/hoarder/releases"; - -function useLatestRelease() { - const { data } = useQuery({ - queryKey: ["latest-release"], - queryFn: async () => { - const res = await fetch(REPO_LATEST_RELEASE_API); - if (!res.ok) { - return undefined; - } - const data = (await res.json()) as { name: string }; - return data.name; - }, - staleTime: 60 * 60 * 1000, - enabled: !useClientConfig().disableNewReleaseCheck, - }); - return data; -} - -function ReleaseInfo() { - const currentRelease = useClientConfig().serverVersion ?? "not set"; - const latestRelease = useLatestRelease(); - - let newRelease; - if (latestRelease && currentRelease != latestRelease) { - newRelease = ( - - (New release available: {latestRelease}) - - ); - } - return ( -

- {currentRelease} {newRelease} -

- ); -} - -function ActionsSection() { - const { mutate: recrawlLinks, isPending: isRecrawlPending } = - api.admin.recrawlLinks.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 ( - <> -

Actions

- - recrawlLinks({ crawlStatus: "failure", runInference: true }) - } - > - Recrawl Failed Links Only - - recrawlLinks({ crawlStatus: "all", runInference: true })} - > - Recrawl All Links - - - recrawlLinks({ crawlStatus: "all", runInference: false }) - } - > - Recrawl All Links (Without Inference) - - reindexBookmarks()} - > - Reindex All Bookmarks - - - ); -} - -function ServerStatsSection() { - const { data: serverStats } = api.admin.stats.useQuery(undefined, { - refetchInterval: 1000, - placeholderData: keepPreviousData, - }); - - if (!serverStats) { - return ; - } - - return ( - <> -

Server Stats

- - - - Num Users - {serverStats.numUsers} - - - Num Bookmarks - {serverStats.numBookmarks} - - - Server Version - - - - - -
- -

Background Jobs

- - - Job - Queued - Pending - Failed - - - - Crawling Jobs - {serverStats.crawlStats.queuedInRedis} - {serverStats.crawlStats.pending} - {serverStats.crawlStats.failed} - - - Indexing Jobs - {serverStats.indexingStats.queuedInRedis} - - - - - - - Inference Jobs - {serverStats.inferenceStats.queuedInRedis} - {serverStats.inferenceStats.pending} - {serverStats.inferenceStats.failed} - - -
- - ); -} - -function UsersSection() { - const { data: session } = useSession(); - const invalidateUserList = api.useUtils().users.list.invalidate; - const { data: users } = api.users.list.useQuery(); - const { mutate: deleteUser, isPending: isDeletionPending } = - api.users.delete.useMutation({ - onSuccess: () => { - toast({ - description: "User deleted", - }); - invalidateUserList(); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: `Something went wrong: ${e.message}`, - }); - }, - }); - - if (!users) { - return ; - } - - return ( - <> -

Users

- - - Name - Email - Role - Action - - - {users.users.map((u) => ( - - {u.name} - {u.email} - {u.role} - - deleteUser({ userId: u.id })} - loading={isDeletionPending} - disabled={session!.user.id == u.id} - > - - - - - ))} - -
- - ); -} - export default function AdminPage() { const router = useRouter(); const { data: session, status } = useSession(); @@ -275,14 +20,13 @@ export default function AdminPage() { } return ( -
-

Admin

- - - - - - -
+ <> +
+ +
+
+ +
+ ); } diff --git a/apps/web/components/dashboard/admin/AdminActions.tsx b/apps/web/components/dashboard/admin/AdminActions.tsx new file mode 100644 index 00000000..c3dad327 --- /dev/null +++ b/apps/web/components/dashboard/admin/AdminActions.tsx @@ -0,0 +1,77 @@ +import { ActionButton } from "@/components/ui/action-button"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; + +export default function AdminActions() { + const { mutate: recrawlLinks, isPending: isRecrawlPending } = + api.admin.recrawlLinks.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 ( +
+
Actions
+
+ + recrawlLinks({ crawlStatus: "failure", runInference: true }) + } + > + Recrawl Failed Links Only + + + recrawlLinks({ crawlStatus: "all", runInference: true }) + } + > + Recrawl All Links + + + recrawlLinks({ crawlStatus: "all", runInference: false }) + } + > + Recrawl All Links (Without Inference) + + reindexBookmarks()} + > + Reindex All Bookmarks + +
+
+ ); +} diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx new file mode 100644 index 00000000..9a028ea4 --- /dev/null +++ b/apps/web/components/dashboard/admin/ServerStats.tsx @@ -0,0 +1,133 @@ +import AdminActions from "@/components/dashboard/admin/AdminActions"; +import LoadingSpinner from "@/components/ui/spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useClientConfig } from "@/lib/clientConfig"; +import { api } from "@/lib/trpc"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +const REPO_LATEST_RELEASE_API = + "https://api.github.com/repos/hoarder-app/hoarder/releases/latest"; +const REPO_RELEASE_PAGE = "https://github.com/hoarder-app/hoarder/releases"; + +function useLatestRelease() { + const { data } = useQuery({ + queryKey: ["latest-release"], + queryFn: async () => { + const res = await fetch(REPO_LATEST_RELEASE_API); + if (!res.ok) { + return undefined; + } + const data = (await res.json()) as { name: string }; + return data.name; + }, + staleTime: 60 * 60 * 1000, + enabled: !useClientConfig().disableNewReleaseCheck, + }); + return data; +} + +function ReleaseInfo() { + const currentRelease = useClientConfig().serverVersion ?? "NA"; + const latestRelease = useLatestRelease(); + + let newRelease; + if (latestRelease && currentRelease != latestRelease) { + newRelease = ( + + (New release available: {latestRelease}) + + ); + } + return ( +
+ {currentRelease} + {newRelease} +
+ ); +} + +export default function ServerStats() { + const { data: serverStats } = api.admin.stats.useQuery(undefined, { + refetchInterval: 1000, + placeholderData: keepPreviousData, + }); + + if (!serverStats) { + return ; + } + + return ( + <> +
Server Stats
+
+
+
Total Users
+
{serverStats.numUsers}
+
+
+
+ Total Bookmarks +
+
+ {serverStats.numBookmarks} +
+
+
+
+ Server Version +
+ +
+
+ +
+
+
Background Jobs
+ + + Job + Queued + Pending + Failed + + + + Crawling Jobs + {serverStats.crawlStats.queuedInRedis} + {serverStats.crawlStats.pending} + {serverStats.crawlStats.failed} + + + Indexing Jobs + {serverStats.indexingStats.queuedInRedis} + - + - + + + Inference Jobs + + {serverStats.inferenceStats.queuedInRedis} + + {serverStats.inferenceStats.pending} + {serverStats.inferenceStats.failed} + + +
+
+ +
+ + ); +} diff --git a/apps/web/components/dashboard/admin/Users.tsx b/apps/web/components/dashboard/admin/Users.tsx new file mode 100644 index 00000000..8c621e0e --- /dev/null +++ b/apps/web/components/dashboard/admin/Users.tsx @@ -0,0 +1,73 @@ +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 { Trash } from "lucide-react"; +import { useSession } from "next-auth/react"; + +export default function UsersSection() { + const { data: session } = useSession(); + const invalidateUserList = api.useUtils().users.list.invalidate; + const { data: users } = api.users.list.useQuery(); + const { mutate: deleteUser, isPending: isDeletionPending } = + api.users.delete.useMutation({ + onSuccess: () => { + toast({ + description: "User deleted", + }); + invalidateUserList(); + }, + onError: (e) => { + toast({ + variant: "destructive", + description: `Something went wrong: ${e.message}`, + }); + }, + }); + + if (!users) { + return ; + } + + return ( + <> +
Users list
+ + + + Name + Email + Role + Action + + + {users.users.map((u) => ( + + {u.name} + {u.email} + {u.role} + + deleteUser({ userId: u.id })} + loading={isDeletionPending} + disabled={session!.user.id == u.id} + > + + + + + ))} + +
+ + ); +} From df03fb2c84350200c60620fd0329b0d102197a17 Mon Sep 17 00:00:00 2001 From: Md Saban Date: Tue, 25 Jun 2024 09:41:13 +0530 Subject: [PATCH 2/5] fix: pr comments --- apps/web/app/dashboard/admin/page.tsx | 6 +- .../dashboard/admin/AdminActions.tsx | 2 +- .../dashboard/admin/ServerStats.tsx | 65 +++++++++---------- .../admin/{Users.tsx => UserList.tsx} | 0 4 files changed, 35 insertions(+), 38 deletions(-) rename apps/web/components/dashboard/admin/{Users.tsx => UserList.tsx} (100%) diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx index 8b066d3a..b20625fc 100644 --- a/apps/web/app/dashboard/admin/page.tsx +++ b/apps/web/app/dashboard/admin/page.tsx @@ -1,8 +1,9 @@ "use client"; import { useRouter } from "next/navigation"; +import AdminActions from "@/components/dashboard/admin/AdminActions"; import ServerStats from "@/components/dashboard/admin/ServerStats"; -import Users from "@/components/dashboard/admin/Users"; +import UserList from "@/components/dashboard/admin/UserList"; import LoadingSpinner from "@/components/ui/spinner"; import { useSession } from "next-auth/react"; @@ -23,9 +24,10 @@ export default function AdminPage() { <>
+
- +
); diff --git a/apps/web/components/dashboard/admin/AdminActions.tsx b/apps/web/components/dashboard/admin/AdminActions.tsx index c3dad327..24f201fe 100644 --- a/apps/web/components/dashboard/admin/AdminActions.tsx +++ b/apps/web/components/dashboard/admin/AdminActions.tsx @@ -36,7 +36,7 @@ export default function AdminActions() { return (
Actions
-
+
-
-
-
Background Jobs
- - - Job - Queued - Pending - Failed - - - - Crawling Jobs - {serverStats.crawlStats.queuedInRedis} - {serverStats.crawlStats.pending} - {serverStats.crawlStats.failed} - - - Indexing Jobs - {serverStats.indexingStats.queuedInRedis} - - - - - - - Inference Jobs - - {serverStats.inferenceStats.queuedInRedis} - - {serverStats.inferenceStats.pending} - {serverStats.inferenceStats.failed} - - -
-
- +
+
Background Jobs
+ + + Job + Queued + Pending + Failed + + + + Crawling Jobs + {serverStats.crawlStats.queuedInRedis} + {serverStats.crawlStats.pending} + {serverStats.crawlStats.failed} + + + Indexing Jobs + {serverStats.indexingStats.queuedInRedis} + - + - + + + Inference Jobs + {serverStats.inferenceStats.queuedInRedis} + {serverStats.inferenceStats.pending} + {serverStats.inferenceStats.failed} + + +
); diff --git a/apps/web/components/dashboard/admin/Users.tsx b/apps/web/components/dashboard/admin/UserList.tsx similarity index 100% rename from apps/web/components/dashboard/admin/Users.tsx rename to apps/web/components/dashboard/admin/UserList.tsx From 03d35e0898c056961a9019f9da7a4ba9c4a55490 Mon Sep 17 00:00:00 2001 From: Md Saban Date: Tue, 25 Jun 2024 09:44:54 +0530 Subject: [PATCH 3/5] chore: lint fix --- apps/web/components/dashboard/admin/ServerStats.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx index bb6092f3..13ab32b0 100644 --- a/apps/web/components/dashboard/admin/ServerStats.tsx +++ b/apps/web/components/dashboard/admin/ServerStats.tsx @@ -1,4 +1,3 @@ -import AdminActions from "@/components/dashboard/admin/AdminActions"; import LoadingSpinner from "@/components/ui/spinner"; import { Table, From 26ff78cc00286f8e916fecaa8177965274f28b93 Mon Sep 17 00:00:00 2001 From: Md Saban Date: Wed, 26 Jun 2024 20:17:57 +0530 Subject: [PATCH 4/5] chore: refactor --- apps/web/app/dashboard/admin/page.tsx | 19 +------------------ .../dashboard/admin/AdminActions.tsx | 2 ++ .../dashboard/admin/ServerStats.tsx | 2 ++ .../components/dashboard/admin/UserList.tsx | 11 ++++++++++- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx index b20625fc..5129f3fb 100644 --- a/apps/web/app/dashboard/admin/page.tsx +++ b/apps/web/app/dashboard/admin/page.tsx @@ -1,25 +1,8 @@ -"use client"; - -import { useRouter } from "next/navigation"; import AdminActions from "@/components/dashboard/admin/AdminActions"; import ServerStats from "@/components/dashboard/admin/ServerStats"; import UserList from "@/components/dashboard/admin/UserList"; -import LoadingSpinner from "@/components/ui/spinner"; -import { useSession } from "next-auth/react"; - -export default function AdminPage() { - const router = useRouter(); - const { data: session, status } = useSession(); - - if (status == "loading") { - return ; - } - - if (!session || session.user.role != "admin") { - router.push("/"); - return; - } +export default async function AdminPage() { return ( <>
diff --git a/apps/web/components/dashboard/admin/AdminActions.tsx b/apps/web/components/dashboard/admin/AdminActions.tsx index 24f201fe..783f7e76 100644 --- a/apps/web/components/dashboard/admin/AdminActions.tsx +++ b/apps/web/components/dashboard/admin/AdminActions.tsx @@ -1,3 +1,5 @@ +"use client"; + import { ActionButton } from "@/components/ui/action-button"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx index 13ab32b0..5d3881f4 100644 --- a/apps/web/components/dashboard/admin/ServerStats.tsx +++ b/apps/web/components/dashboard/admin/ServerStats.tsx @@ -1,3 +1,5 @@ +"use client"; + import LoadingSpinner from "@/components/ui/spinner"; import { Table, diff --git a/apps/web/components/dashboard/admin/UserList.tsx b/apps/web/components/dashboard/admin/UserList.tsx index 8c621e0e..62b3231e 100644 --- a/apps/web/components/dashboard/admin/UserList.tsx +++ b/apps/web/components/dashboard/admin/UserList.tsx @@ -1,3 +1,6 @@ +"use client"; + +import { useRouter } from "next/navigation"; import { ActionButton } from "@/components/ui/action-button"; import LoadingSpinner from "@/components/ui/spinner"; import { @@ -15,6 +18,7 @@ import { useSession } from "next-auth/react"; export default function UsersSection() { const { data: session } = useSession(); + const router = useRouter(); const invalidateUserList = api.useUtils().users.list.invalidate; const { data: users } = api.users.list.useQuery(); const { mutate: deleteUser, isPending: isDeletionPending } = @@ -33,6 +37,11 @@ export default function UsersSection() { }, }); + if (!session || session.user.role != "admin") { + router.push("/"); + return; + } + if (!users) { return ; } @@ -59,7 +68,7 @@ export default function UsersSection() { variant="outline" onClick={() => deleteUser({ userId: u.id })} loading={isDeletionPending} - disabled={session!.user.id == u.id} + disabled={session.user.id == u.id} > From 655aa4d5e1fa4abb6463fd194618fd24a85efcea Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sat, 29 Jun 2024 22:12:58 +0000 Subject: [PATCH 5/5] minor tweaks --- apps/web/app/dashboard/admin/page.tsx | 6 ++++++ apps/web/components/dashboard/admin/ServerStats.tsx | 3 ++- apps/web/components/dashboard/admin/UserList.tsx | 11 ++--------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/web/app/dashboard/admin/page.tsx b/apps/web/app/dashboard/admin/page.tsx index 5129f3fb..18efc889 100644 --- a/apps/web/app/dashboard/admin/page.tsx +++ b/apps/web/app/dashboard/admin/page.tsx @@ -1,8 +1,14 @@ +import { redirect } from "next/navigation"; import AdminActions from "@/components/dashboard/admin/AdminActions"; import ServerStats from "@/components/dashboard/admin/ServerStats"; import UserList from "@/components/dashboard/admin/UserList"; +import { getServerAuthSession } from "@/server/auth"; export default async function AdminPage() { + const session = await getServerAuthSession(); + if (!session || session.user.role !== "admin") { + redirect("/"); + } return ( <>
diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx index 5d3881f4..06e3421f 100644 --- a/apps/web/components/dashboard/admin/ServerStats.tsx +++ b/apps/web/components/dashboard/admin/ServerStats.tsx @@ -46,8 +46,9 @@ function ReleaseInfo() { target="_blank" className="text-blue-500" rel="noreferrer" + title="Update available" > - (New release available: {latestRelease}) + ({latestRelease} ⬆️) ); } diff --git a/apps/web/components/dashboard/admin/UserList.tsx b/apps/web/components/dashboard/admin/UserList.tsx index 62b3231e..024325a3 100644 --- a/apps/web/components/dashboard/admin/UserList.tsx +++ b/apps/web/components/dashboard/admin/UserList.tsx @@ -1,6 +1,5 @@ "use client"; -import { useRouter } from "next/navigation"; import { ActionButton } from "@/components/ui/action-button"; import LoadingSpinner from "@/components/ui/spinner"; import { @@ -18,7 +17,6 @@ import { useSession } from "next-auth/react"; export default function UsersSection() { const { data: session } = useSession(); - const router = useRouter(); const invalidateUserList = api.useUtils().users.list.invalidate; const { data: users } = api.users.list.useQuery(); const { mutate: deleteUser, isPending: isDeletionPending } = @@ -37,18 +35,13 @@ export default function UsersSection() { }, }); - if (!session || session.user.role != "admin") { - router.push("/"); - return; - } - if (!users) { return ; } return ( <> -
Users list
+
Users List
@@ -68,7 +61,7 @@ export default function UsersSection() { variant="outline" onClick={() => deleteUser({ userId: u.id })} loading={isDeletionPending} - disabled={session.user.id == u.id} + disabled={session!.user.id == u.id} >