diff --git a/website/src/components/Layout.tsx b/website/src/components/Layout.tsx index 484d16ec..55085550 100644 --- a/website/src/components/Layout.tsx +++ b/website/src/components/Layout.tsx @@ -2,7 +2,7 @@ import { Box, Grid } from "@chakra-ui/react"; import type { NextPage } from "next"; -import { FiBarChart2, FiLayout, FiMessageSquare, FiUsers } from "react-icons/fi"; +import { FiBarChart2, FiLayout, FiMessageSquare, FiUsers, FiActivity } from "react-icons/fi"; import { Header } from "src/components/Header"; import { SlimFooter } from "./Dashboard/SlimFooter"; @@ -75,6 +75,12 @@ export const getAdminLayout = (page: React.ReactElement) => ( desc: "Users Dashboard", icon: FiUsers, }, + { + label: "Status", + pathname: "/admin/status", + desc: "Status Dashboard", + icon: FiActivity, + }, ]} > {page} diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts index 1e74b020..44d8ae7c 100644 --- a/website/src/lib/oasst_api_client.ts +++ b/website/src/lib/oasst_api_client.ts @@ -148,6 +148,27 @@ export class OasstApiClient { }); } + /** + * Returns the tasks availability information for given `user`. + */ + async fetch_tasks_availability(user: object): Promise { + return this.post("/api/v1/tasks/availability", user); + } + + /** + * Returns the message stats from the backend. + */ + async fetch_stats(): Promise { + return this.get("/api/v1/stats/"); + } + + /** + * Returns the tree manager stats from the backend. + */ + async fetch_tree_manager(): Promise { + return this.get("/api/v1/stats/tree_manager"); + } + /** * Returns the `BackendUser` associated with `user_id` */ diff --git a/website/src/pages/admin/status/index.tsx b/website/src/pages/admin/status/index.tsx new file mode 100644 index 00000000..12eb0785 --- /dev/null +++ b/website/src/pages/admin/status/index.tsx @@ -0,0 +1,174 @@ +import { + Box, + Card, + CardBody, + CircularProgress, + SimpleGrid, + Text, + Table, + TableCaption, + TableContainer, + Tbody, + Td, + Th, + Thead, + Tr, + useColorMode, +} from "@chakra-ui/react"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { useSession } from "next-auth/react"; +import { useEffect } from "react"; +import useSWRImmutable from "swr/immutable"; +import { getAdminLayout } from "src/components/Layout"; +import { get } from "src/lib/api"; + +/** + * Provides the admin status page that shows result of calls to several backend API endpoints, + * namely /api/v1/tasks/availability, /api/v1/stats/, /api/v1/stats/tree_manager + */ + +const StatusIndex = () => { + const router = useRouter(); + const { data: session, status } = useSession(); + + const { colorMode } = useColorMode(); + const dataBackgroundColor = colorMode === "light" ? "gray.100" : "gray.800"; + // Check when the user session is loaded and re-route if the user is not an + // admin. This follows the suggestion by NextJS for handling private pages: + // https://nextjs.org/docs/api-reference/next/router#usage + // + // All admin pages should use the same check and routing steps. + useEffect(() => { + if (status === "loading") { + return; + } + if (session?.user?.role === "admin") { + return; + } + router.push("/"); + }, [router, session, status]); + + const { + data: dataStatus, + error: errorStatus, + isLoading: isLoadingStatus, + } = useSWRImmutable("/api/admin/status", get); + + const { tasksAvailability, stats, treeManager } = dataStatus || {}; + + return ( + <> + + Status - Open Assistant + + + + + + + + /api/v1/tasks/availability + + + {tasksAvailability?.status === "fulfilled" ? ( +
{JSON.stringify(tasksAvailability.value, null, 2)}
+ ) : tasksAvailability?.status === "rejected" ? ( +
{JSON.stringify(tasksAvailability.reason, null, 2)}
+ ) : errorStatus ? ( +
{JSON.stringify(errorStatus, null, 2)}
+ ) : ( + + )} +
+
+
+ + + + + /api/v1/stats/ + + + {stats?.status === "fulfilled" ? ( +
{JSON.stringify(stats.value, null, 2)}
+ ) : stats?.status === "rejected" ? ( +
{JSON.stringify(stats.reason, null, 2)}
+ ) : errorStatus ? ( +
{JSON.stringify(errorStatus, null, 2)}
+ ) : ( + + )} +
+
+
+
+
+ + + + /api/v1/stats/tree_manager + + {treeManager?.status === "fulfilled" ? ( + + + state_counts + + +
{JSON.stringify(treeManager.value.state_counts, null, 2)}
+
+ +
+ + message_counts + + + Tree Manager + + + + + + + + + + + + + {treeManager.value.message_counts.map( + ({ message_tree_id, state, depth, oldest, youngest, count, goal_tree_size }) => ( + + + + + + + + + + ) + )} + +
Message Tree IDStateDepthOldestYoungestCountGoal Tree Size
{message_tree_id}{state}{depth}{oldest}{youngest}{count}{goal_tree_size}
+
+
+ ) : treeManager?.status === "rejected" ? ( +
{JSON.stringify(treeManager.reason, null, 2)}
+ ) : errorStatus ? ( +
{JSON.stringify(errorStatus, null, 2)}
+ ) : ( + + )} +
+
+ + ); +}; + +StatusIndex.getLayout = getAdminLayout; + +export default StatusIndex; diff --git a/website/src/pages/api/admin/status.ts b/website/src/pages/api/admin/status.ts new file mode 100644 index 00000000..1da03da8 --- /dev/null +++ b/website/src/pages/api/admin/status.ts @@ -0,0 +1,30 @@ +import { getToken } from "next-auth/jwt"; +import { withRole } from "src/lib/auth"; +import { oasstApiClient } from "src/lib/oasst_api_client"; +import { getBackendUserCore } from "src/lib/users"; + +/** + * Returns tasks availability, stats, and tree manager stats. + */ +const handler = withRole("admin", async (req, res) => { + const dummyUser = { + id: "__dummy_user__", + display_name: "Dummy User", + auth_method: "local", + }; + const [tasksAvailabilityOutcome, statsOutcome, treeManagerOutcome] = await Promise.allSettled([ + oasstApiClient.fetch_tasks_availability(dummyUser), + oasstApiClient.fetch_stats(), + oasstApiClient.fetch_tree_manager(), + ]); + + const status = { + tasksAvailability: tasksAvailabilityOutcome, + stats: statsOutcome, + treeManager: treeManagerOutcome, + }; + + res.status(200).json(status); +}); + +export default handler;