diff options
| author | Mohamed Bassem <me@mbassem.com> | 2024-12-30 11:27:32 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2024-12-30 11:31:35 +0000 |
| commit | 179f00b15525b024b6823088ef8fb94b7106b4f0 (patch) | |
| tree | d64257778930965ed076ff9a081411470343fb3c /apps/web/components/dashboard | |
| parent | aff4e60952321d06dc4cf517ff3b15206aaaebba (diff) | |
| download | karakeep-179f00b15525b024b6823088ef8fb94b7106b4f0.tar.zst | |
feat: Change the admin page to be tabbed similar to that of the settings page
Diffstat (limited to 'apps/web/components/dashboard')
| -rw-r--r-- | apps/web/components/dashboard/admin/AddUserDialog.tsx | 213 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/AdminActions.tsx | 137 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/AdminCard.tsx | 3 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/AdminNotices.tsx | 71 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/ChangeRoleDialog.tsx | 154 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/ResetPasswordDialog.tsx | 145 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/ServerStats.tsx | 148 | ||||
| -rw-r--r-- | apps/web/components/dashboard/admin/UserList.tsx | 130 | ||||
| -rw-r--r-- | apps/web/components/dashboard/header/ProfileOptions.tsx | 4 |
9 files changed, 2 insertions, 1003 deletions
diff --git a/apps/web/components/dashboard/admin/AddUserDialog.tsx b/apps/web/components/dashboard/admin/AddUserDialog.tsx deleted file mode 100644 index a13c6b88..00000000 --- a/apps/web/components/dashboard/admin/AddUserDialog.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { zAdminCreateUserSchema } from "@hoarder/shared/types/admin";
-
-type AdminCreateUserSchema = z.infer<typeof zAdminCreateUserSchema>;
-
-export default function AddUserDialog({
- children,
-}: {
- children?: React.ReactNode;
-}) {
- const apiUtils = api.useUtils();
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<AdminCreateUserSchema>({
- resolver: zodResolver(zAdminCreateUserSchema),
- defaultValues: {
- name: "",
- email: "",
- password: "",
- confirmPassword: "",
- role: "user",
- },
- });
- const { mutate, isPending } = api.admin.createUser.useMutation({
- onSuccess: () => {
- toast({
- description: "User created successfully",
- });
- onOpenChange(false);
- apiUtils.users.list.invalidate();
- apiUtils.admin.userStats.invalidate();
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to create user",
- });
- }
- },
- });
-
- useEffect(() => {
- if (!isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Add User</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Name</FormLabel>
- <FormControl>
- <Input
- type="text"
- placeholder="Name"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="email"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Email</FormLabel>
- <FormControl>
- <Input
- type="email"
- placeholder="Email"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="password"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="confirmPassword"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Confirm Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="role"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Role</FormLabel>
- <FormControl>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <SelectTrigger className="w-full">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="user">User</SelectItem>
- <SelectItem value="admin">Admin</SelectItem>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Create
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/components/dashboard/admin/AdminActions.tsx b/apps/web/components/dashboard/admin/AdminActions.tsx deleted file mode 100644 index 3b95045c..00000000 --- a/apps/web/components/dashboard/admin/AdminActions.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import { ActionButton } from "@/components/ui/action-button"; -import { toast } from "@/components/ui/use-toast"; -import { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; - -export default function AdminActions() { - const { t } = useTranslation(); - 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, - }); - }, - }); - - const { - mutate: reRunInferenceOnAllBookmarks, - isPending: isInferencePending, - } = api.admin.reRunInferenceOnAllBookmarks.useMutation({ - onSuccess: () => { - toast({ - description: "Inference jobs enqueued", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); - - const { mutateAsync: tidyAssets, isPending: isTidyAssetsPending } = - api.admin.tidyAssets.useMutation({ - onSuccess: () => { - toast({ - description: "Tidy assets request has been enqueued!", - }); - }, - onError: (e) => { - toast({ - variant: "destructive", - description: e.message, - }); - }, - }); - - return ( - <div> - <div className="mb-2 mt-8 text-xl font-medium">{t("common.actions")}</div> - <div className="flex flex-col gap-2 sm:w-1/2"> - <ActionButton - variant="destructive" - loading={isRecrawlPending} - onClick={() => - recrawlLinks({ crawlStatus: "failure", runInference: true }) - } - > - {t("admin.actions.recrawl_failed_links_only")} - </ActionButton> - <ActionButton - variant="destructive" - loading={isRecrawlPending} - onClick={() => - recrawlLinks({ crawlStatus: "all", runInference: true }) - } - > - {t("admin.actions.recrawl_all_links")} - </ActionButton> - <ActionButton - variant="destructive" - loading={isRecrawlPending} - onClick={() => - recrawlLinks({ crawlStatus: "all", runInference: false }) - } - > - {t("admin.actions.recrawl_all_links")} ( - {t("admin.actions.without_inference")}) - </ActionButton> - <ActionButton - variant="destructive" - loading={isInferencePending} - onClick={() => - reRunInferenceOnAllBookmarks({ taggingStatus: "failure" }) - } - > - {t("admin.actions.regenerate_ai_tags_for_failed_bookmarks_only")} - </ActionButton> - <ActionButton - variant="destructive" - loading={isInferencePending} - onClick={() => reRunInferenceOnAllBookmarks({ taggingStatus: "all" })} - > - {t("admin.actions.regenerate_ai_tags_for_all_bookmarks")} - </ActionButton> - <ActionButton - variant="destructive" - loading={isReindexPending} - onClick={() => reindexBookmarks()} - > - {t("admin.actions.reindex_all_bookmarks")} - </ActionButton> - <ActionButton - variant="destructive" - loading={isTidyAssetsPending} - onClick={() => tidyAssets()} - > - {t("admin.actions.compact_assets")} - </ActionButton> - </div> - </div> - ); -} diff --git a/apps/web/components/dashboard/admin/AdminCard.tsx b/apps/web/components/dashboard/admin/AdminCard.tsx deleted file mode 100644 index 3a52b5e5..00000000 --- a/apps/web/components/dashboard/admin/AdminCard.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function AdminCard({ children }: { children: React.ReactNode }) { - return <div className="rounded-md border bg-background p-4">{children}</div>; -} diff --git a/apps/web/components/dashboard/admin/AdminNotices.tsx b/apps/web/components/dashboard/admin/AdminNotices.tsx deleted file mode 100644 index 4977736f..00000000 --- a/apps/web/components/dashboard/admin/AdminNotices.tsx +++ /dev/null @@ -1,71 +0,0 @@ -"use client"; - -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { Badge } from "@/components/ui/badge"; -import { api } from "@/lib/trpc"; -import { AlertCircle } from "lucide-react"; - -import { AdminCard } from "./AdminCard"; - -interface AdminNotice { - level: "info" | "warning" | "error"; - message: React.ReactNode; - title: string; -} - -function useAdminNotices() { - const { data } = api.admin.getAdminNoticies.useQuery(); - if (!data) { - return []; - } - const ret: AdminNotice[] = []; - if (data.legacyContainersNotice) { - ret.push({ - level: "warning", - message: ( - <p> - You're using the legacy docker container images. Those will stop - getting supported soon. Please follow{" "} - <a - href="https://docs.hoarder.app/next/Guides/legacy-container-upgrade" - className="underline" - > - this guide - </a>{" "} - to upgrade. - </p> - ), - title: "Legacy Container Images", - }); - } - return ret; -} - -export function AdminNotices() { - const notices = useAdminNotices(); - - if (notices.length === 0) { - return null; - } - return ( - <AdminCard> - <div className="flex flex-col gap-2"> - {notices.map((n, i) => ( - <Alert key={i} variant="destructive"> - <AlertCircle className="h-4 w-4" /> - <AlertTitle>{n.title}</AlertTitle> - <AlertDescription>{n.message}</AlertDescription> - </Alert> - ))} - </div> - </AdminCard> - ); -} - -export function AdminNoticeBadge() { - const notices = useAdminNotices(); - if (notices.length === 0) { - return null; - } - return <Badge variant="destructive">{notices.length}</Badge>; -} diff --git a/apps/web/components/dashboard/admin/ChangeRoleDialog.tsx b/apps/web/components/dashboard/admin/ChangeRoleDialog.tsx deleted file mode 100644 index 26ad5dce..00000000 --- a/apps/web/components/dashboard/admin/ChangeRoleDialog.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { changeRoleSchema } from "@hoarder/shared/types/admin";
-
-type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
-
-interface ChangeRoleDialogProps {
- userId: string;
- currentRole: "user" | "admin";
- children?: React.ReactNode;
-}
-
-export default function ChangeRoleDialog({
- userId,
- currentRole,
- children,
-}: ChangeRoleDialogProps) {
- const apiUtils = api.useUtils();
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<ChangeRoleSchema>({
- resolver: zodResolver(changeRoleSchema),
- defaultValues: {
- userId,
- role: currentRole,
- },
- });
- const { mutate, isPending } = api.admin.changeRole.useMutation({
- onSuccess: () => {
- toast({
- description: "Role changed successfully",
- });
- apiUtils.users.list.invalidate();
- onOpenChange(false);
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to change role",
- });
- }
- },
- });
-
- useEffect(() => {
- if (isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogTrigger asChild></DialogTrigger>
- <DialogHeader>
- <DialogTitle>Change Role</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="role"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Role</FormLabel>
- <FormControl>
- <Select
- onValueChange={field.onChange}
- defaultValue={field.value}
- >
- <SelectTrigger className="w-full">
- <SelectValue />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="user">User</SelectItem>
- <SelectItem value="admin">Admin</SelectItem>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="userId"
- render={({ field }) => (
- <FormItem>
- <FormControl>
- <input type="hidden" {...field} />
- </FormControl>
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Change
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/components/dashboard/admin/ResetPasswordDialog.tsx b/apps/web/components/dashboard/admin/ResetPasswordDialog.tsx deleted file mode 100644 index 32183d1a..00000000 --- a/apps/web/components/dashboard/admin/ResetPasswordDialog.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useEffect, useState } from "react";
-import { ActionButton } from "@/components/ui/action-button";
-import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog";
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form";
-import { Input } from "@/components/ui/input";
-import { toast } from "@/components/ui/use-toast";
-import { api } from "@/lib/trpc"; // Adjust the import path as needed
-import { zodResolver } from "@hookform/resolvers/zod";
-import { TRPCClientError } from "@trpc/client";
-import { useForm } from "react-hook-form";
-import { z } from "zod";
-
-import { resetPasswordSchema } from "@hoarder/shared/types/admin";
-
-interface ResetPasswordDialogProps {
- userId: string;
- children?: React.ReactNode;
-}
-
-type ResetPasswordSchema = z.infer<typeof resetPasswordSchema>;
-
-export default function ResetPasswordDialog({
- children,
- userId,
-}: ResetPasswordDialogProps) {
- const [isOpen, onOpenChange] = useState(false);
- const form = useForm<ResetPasswordSchema>({
- resolver: zodResolver(resetPasswordSchema),
- defaultValues: {
- userId,
- newPassword: "",
- newPasswordConfirm: "",
- },
- });
- const { mutate, isPending } = api.admin.resetPassword.useMutation({
- onSuccess: () => {
- toast({
- description: "Password reset successfully",
- });
- onOpenChange(false);
- },
- onError: (error) => {
- if (error instanceof TRPCClientError) {
- toast({
- variant: "destructive",
- description: error.message,
- });
- } else {
- toast({
- variant: "destructive",
- description: "Failed to reset password",
- });
- }
- },
- });
-
- useEffect(() => {
- if (isOpen) {
- form.reset();
- }
- }, [isOpen, form]);
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogTrigger asChild>{children}</DialogTrigger>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>Reset Password</DialogTitle>
- </DialogHeader>
- <Form {...form}>
- <form onSubmit={form.handleSubmit((val) => mutate(val))}>
- <div className="flex w-full flex-col space-y-2">
- <FormField
- control={form.control}
- name="newPassword"
- render={({ field }) => (
- <FormItem>
- <FormLabel>New Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="New Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="newPasswordConfirm"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Confirm New Password</FormLabel>
- <FormControl>
- <Input
- type="password"
- placeholder="Confirm New Password"
- {...field}
- className="w-full rounded border p-2"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <DialogFooter className="sm:justify-end">
- <DialogClose asChild>
- <Button type="button" variant="secondary">
- Close
- </Button>
- </DialogClose>
- <ActionButton
- type="submit"
- loading={isPending}
- disabled={isPending}
- >
- Reset
- </ActionButton>
- </DialogFooter>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/apps/web/components/dashboard/admin/ServerStats.tsx b/apps/web/components/dashboard/admin/ServerStats.tsx deleted file mode 100644 index da69390b..00000000 --- a/apps/web/components/dashboard/admin/ServerStats.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client"; - -import LoadingSpinner from "@/components/ui/spinner"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { useClientConfig } from "@/lib/clientConfig"; -import { useTranslation } from "@/lib/i18n/client"; -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 = ( - <a - href={REPO_RELEASE_PAGE} - target="_blank" - className="text-blue-500" - rel="noreferrer" - title="Update available" - > - ({latestRelease} ⬆️) - </a> - ); - } - return ( - <div className="text-nowrap"> - <span className="text-3xl font-semibold">{currentRelease}</span> - <span className="ml-1 text-sm">{newRelease}</span> - </div> - ); -} - -export default function ServerStats() { - const { t } = useTranslation(); - const { data: serverStats } = api.admin.stats.useQuery(undefined, { - refetchInterval: 1000, - placeholderData: keepPreviousData, - }); - - if (!serverStats) { - return <LoadingSpinner />; - } - - return ( - <> - <div className="mb-2 text-xl font-medium"> - {t("admin.server_stats.server_stats")} - </div> - <div className="flex flex-col gap-4 sm:flex-row"> - <div className="rounded-md border bg-background p-4 sm:w-1/4"> - <div className="text-sm font-medium text-gray-400"> - {t("admin.server_stats.total_users")} - </div> - <div className="text-3xl font-semibold">{serverStats.numUsers}</div> - </div> - <div className="rounded-md border bg-background p-4 sm:w-1/4"> - <div className="text-sm font-medium text-gray-400"> - {t("admin.server_stats.total_bookmarks")} - </div> - <div className="text-3xl font-semibold"> - {serverStats.numBookmarks} - </div> - </div> - <div className="rounded-md border bg-background p-4 sm:w-1/4"> - <div className="text-sm font-medium text-gray-400"> - {t("admin.server_stats.server_version")} - </div> - <ReleaseInfo /> - </div> - </div> - - <div className="sm:w-1/2"> - <div className="mb-2 mt-8 text-xl font-medium"> - {t("admin.background_jobs.background_jobs")} - </div> - <Table className="rounded-md border"> - <TableHeader className="bg-gray-200"> - <TableHead>{t("admin.background_jobs.job")}</TableHead> - <TableHead>{t("admin.background_jobs.queued")}</TableHead> - <TableHead>{t("admin.background_jobs.pending")}</TableHead> - <TableHead>{t("admin.background_jobs.failed")}</TableHead> - </TableHeader> - <TableBody> - <TableRow> - <TableCell className="lg:w-2/3"> - {t("admin.background_jobs.crawler_jobs")} - </TableCell> - <TableCell>{serverStats.crawlStats.queued}</TableCell> - <TableCell>{serverStats.crawlStats.pending}</TableCell> - <TableCell>{serverStats.crawlStats.failed}</TableCell> - </TableRow> - <TableRow> - <TableCell>{t("admin.background_jobs.indexing_jobs")}</TableCell> - <TableCell>{serverStats.indexingStats.queued}</TableCell> - <TableCell>-</TableCell> - <TableCell>-</TableCell> - </TableRow> - <TableRow> - <TableCell>{t("admin.background_jobs.inference_jobs")}</TableCell> - <TableCell>{serverStats.inferenceStats.queued}</TableCell> - <TableCell>{serverStats.inferenceStats.pending}</TableCell> - <TableCell>{serverStats.inferenceStats.failed}</TableCell> - </TableRow> - <TableRow> - <TableCell> - {t("admin.background_jobs.tidy_assets_jobs")} - </TableCell> - <TableCell>{serverStats.tidyAssetsStats.queued}</TableCell> - <TableCell>-</TableCell> - <TableCell>-</TableCell> - </TableRow> - </TableBody> - </Table> - </div> - </> - ); -} diff --git a/apps/web/components/dashboard/admin/UserList.tsx b/apps/web/components/dashboard/admin/UserList.tsx deleted file mode 100644 index 8c788ef4..00000000 --- a/apps/web/components/dashboard/admin/UserList.tsx +++ /dev/null @@ -1,130 +0,0 @@ -"use client"; - -import { ActionButtonWithTooltip } from "@/components/ui/action-button"; -import { ButtonWithTooltip } from "@/components/ui/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 { useTranslation } from "@/lib/i18n/client"; -import { api } from "@/lib/trpc"; -import { Check, KeyRound, Pencil, Trash, UserPlus, X } from "lucide-react"; -import { useSession } from "next-auth/react"; - -import AddUserDialog from "./AddUserDialog"; -import ChangeRoleDialog from "./ChangeRoleDialog"; -import ResetPasswordDialog from "./ResetPasswordDialog"; - -function toHumanReadableSize(size: number) { - const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; - if (size === 0) return "0 Bytes"; - const i = Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) + " " + sizes[i]; -} - -export default function UsersSection() { - const { t } = useTranslation(); - const { data: session } = useSession(); - const invalidateUserList = api.useUtils().users.list.invalidate; - const { data: users } = api.users.list.useQuery(); - const { data: userStats } = api.admin.userStats.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 || !userStats) { - return <LoadingSpinner />; - } - - return ( - <> - <div className="mb-2 flex items-center justify-between text-xl font-medium"> - <span>{t("admin.users_list.users_list")}</span> - <AddUserDialog> - <ButtonWithTooltip tooltip="Create User" variant="outline"> - <UserPlus size={16} /> - </ButtonWithTooltip> - </AddUserDialog> - </div> - - <Table> - <TableHeader className="bg-gray-200"> - <TableHead>{t("common.name")}</TableHead> - <TableHead>{t("common.email")}</TableHead> - <TableHead>{t("admin.users_list.num_bookmarks")}</TableHead> - <TableHead>{t("admin.users_list.asset_sizes")}</TableHead> - <TableHead>{t("common.role")}</TableHead> - <TableHead>{t("admin.users_list.local_user")}</TableHead> - <TableHead>{t("common.actions")}</TableHead> - </TableHeader> - <TableBody> - {users.users.map((u) => ( - <TableRow key={u.id}> - <TableCell className="py-1">{u.name}</TableCell> - <TableCell className="py-1">{u.email}</TableCell> - <TableCell className="py-1"> - {userStats[u.id].numBookmarks} - </TableCell> - <TableCell className="py-1"> - {toHumanReadableSize(userStats[u.id].assetSizes)} - </TableCell> - <TableCell className="py-1"> - {u.role && t(`common.roles.${u.role}`)} - </TableCell> - <TableCell className="py-1"> - {u.localUser ? <Check /> : <X />} - </TableCell> - <TableCell className="flex gap-1 py-1"> - <ActionButtonWithTooltip - tooltip={t("admin.users_list.delete_user")} - variant="outline" - onClick={() => deleteUser({ userId: u.id })} - loading={isDeletionPending} - disabled={session!.user.id == u.id} - > - <Trash size={16} color="red" /> - </ActionButtonWithTooltip> - <ResetPasswordDialog userId={u.id}> - <ButtonWithTooltip - tooltip={t("admin.users_list.reset_password")} - variant="outline" - disabled={session!.user.id == u.id || !u.localUser} - > - <KeyRound size={16} color="red" /> - </ButtonWithTooltip> - </ResetPasswordDialog> - <ChangeRoleDialog userId={u.id} currentRole={u.role!}> - <ButtonWithTooltip - tooltip={t("admin.users_list.change_role")} - variant="outline" - disabled={session!.user.id == u.id} - > - <Pencil size={16} color="red" /> - </ButtonWithTooltip> - </ChangeRoleDialog> - </TableCell> - </TableRow> - ))} - </TableBody> - </Table> - </> - ); -} diff --git a/apps/web/components/dashboard/header/ProfileOptions.tsx b/apps/web/components/dashboard/header/ProfileOptions.tsx index fc18e9d2..3d125606 100644 --- a/apps/web/components/dashboard/header/ProfileOptions.tsx +++ b/apps/web/components/dashboard/header/ProfileOptions.tsx @@ -16,7 +16,7 @@ import { LogOut, Moon, Paintbrush, Settings, Shield, Sun } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; import { useTheme } from "next-themes"; -import { AdminNoticeBadge } from "../admin/AdminNotices"; +import { AdminNoticeBadge } from "../../admin/AdminNotices"; function DarkModeToggle() { const { t } = useTranslation(); @@ -74,7 +74,7 @@ export default function SidebarProfileOptions() { </DropdownMenuItem> {session.user.role == "admin" && ( <DropdownMenuItem asChild> - <Link href="/dashboard/admin" className="flex justify-between"> + <Link href="/admin" className="flex justify-between"> <div className="items-cente flex gap-2"> <Shield className="size-4" /> {t("admin.admin_settings")} |
