From e2c303acb087cab2e1ed74b08796693e2108bc7e Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Thu, 21 Aug 2025 18:24:03 +0300 Subject: feat: A redesigned background jobs page. #1551 --- apps/web/components/admin/AdminCard.tsx | 16 +- apps/web/components/admin/BackgroundJobs.tsx | 590 +++++++++++++++++++-------- 2 files changed, 434 insertions(+), 172 deletions(-) (limited to 'apps/web/components/admin') diff --git a/apps/web/components/admin/AdminCard.tsx b/apps/web/components/admin/AdminCard.tsx index 3a52b5e5..87c10076 100644 --- a/apps/web/components/admin/AdminCard.tsx +++ b/apps/web/components/admin/AdminCard.tsx @@ -1,3 +1,15 @@ -export function AdminCard({ children }: { children: React.ReactNode }) { - return
{children}
; +import { cn } from "@/lib/utils"; + +export function AdminCard({ + className, + children, +}: { + className?: string; + children: React.ReactNode; +}) { + return ( +
+ {children} +
+ ); } diff --git a/apps/web/components/admin/BackgroundJobs.tsx b/apps/web/components/admin/BackgroundJobs.tsx index ac5885ef..56d3531f 100644 --- a/apps/web/components/admin/BackgroundJobs.tsx +++ b/apps/web/components/admin/BackgroundJobs.tsx @@ -1,23 +1,249 @@ "use client"; import { ActionButton } from "@/components/ui/action-button"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import { api } from "@/lib/trpc"; import { keepPreviousData } from "@tanstack/react-query"; - -import LoadingSpinner from "../ui/spinner"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "../ui/table"; + Activity, + AlertTriangle, + Clock, + Database, + Globe, + HelpCircle, + Image, + RefreshCw, + Rss, + Search, + Sparkle, + Video, + Webhook, +} from "lucide-react"; + +import { AdminCard } from "./AdminCard"; + +interface JobStats { + queued: number; + pending?: number; + failed?: number; +} + +interface JobAction { + label: string; + onClick: () => void; + loading: boolean; +} + +function JobStatusExplanation() { + const { t } = useTranslation(); + + return ( + + + + + {t("admin.background_jobs.status.title")} + + + +
+
+
+ +
+
+

+ {t("admin.background_jobs.status.queued.title")} +

+

+ {t("admin.background_jobs.status.queued.description")} +

+
+
+
+
+ +
+
+

+ {t("admin.background_jobs.status.unprocessed.title")} +

+

+ {t("admin.background_jobs.status.unprocessed.description")} +

+
+
+
+
+ +
+
+

+ {t("admin.background_jobs.status.failed.title")} +

+

+ {t("admin.background_jobs.status.failed.description")} +

+
+
+
+
+
+ ); +} + +function JobCardSkeleton() { + return ( + + +
+
+ + +
+ +
+ +
+ +
+
+ + + +
+
+ + + +
+
+ +
+
+ + +
+ +
-function AdminActions() { +
+ +
+ + +
+
+
+
+ ); +} + +function JobCard({ + title, + icon: Icon, + stats, + description, + actions = [], +}: { + title: string; + icon: React.ComponentType<{ className?: string }>; + stats: JobStats; + description: string; + actions?: JobAction[]; +}) { + const { t } = useTranslation(); + const total = stats.queued + (stats.pending || 0) + (stats.failed || 0); + const hasActivity = total > 0; + + return ( + + +
+
+ + {title} +
+ {hasActivity && ( + + + {t("admin.background_jobs.active")} + + )} +
+ + {description} + +
+ +
+
+ + + {t("admin.background_jobs.status.queued.title")} + + {stats.queued} +
+ {stats.pending !== undefined && ( +
+ + + {t("admin.background_jobs.status.unprocessed.title")} + + {stats.pending} +
+ )} + {stats.failed !== undefined && stats.failed > 0 && ( +
+ + + {t("admin.background_jobs.status.failed.title")} + + {stats.failed} +
+ )} +
+ + {actions.length > 0 && ( +
+

+ {t("admin.background_jobs.available_actions")} +

+
+ {actions.map((action, index) => ( + + {action.label} + + ))} +
+
+ )} +
+
+ ); +} + +function useJobActions() { const { t } = useTranslation(); + const { mutate: recrawlLinks, isPending: isRecrawlPending } = api.admin.recrawlLinks.useMutation({ onSuccess: () => { @@ -95,93 +321,88 @@ function AdminActions() { }, }); - return ( -
- - recrawlLinks({ crawlStatus: "failure", runInference: true }) - } - > - {t("admin.actions.recrawl_failed_links_only")} - - recrawlLinks({ crawlStatus: "all", runInference: true })} - > - {t("admin.actions.recrawl_all_links")} - - - recrawlLinks({ crawlStatus: "all", runInference: false }) - } - > - {t("admin.actions.recrawl_all_links")} ( - {t("admin.actions.without_inference")}) - - - reRunInferenceOnAllBookmarks({ type: "tag", status: "failure" }) - } - > - {t("admin.actions.regenerate_ai_tags_for_failed_bookmarks_only")} - - - reRunInferenceOnAllBookmarks({ type: "tag", status: "all" }) - } - > - {t("admin.actions.regenerate_ai_tags_for_all_bookmarks")} - - - reRunInferenceOnAllBookmarks({ type: "summarize", status: "failure" }) - } - > - {t("admin.actions.regenerate_ai_summaries_for_failed_bookmarks_only")} - - - reRunInferenceOnAllBookmarks({ type: "summarize", status: "all" }) - } - > - {t("admin.actions.regenerate_ai_summaries_for_all_bookmarks")} - - reindexBookmarks()} - > - {t("admin.actions.reindex_all_bookmarks")} - - reprocessAssetsFixMode()} - > - {t("admin.actions.reprocess_assets_fix_mode")} - - tidyAssets()} - > - {t("admin.actions.compact_assets")} - -
- ); + return { + crawlActions: [ + { + label: t("admin.background_jobs.actions.recrawl_failed_links_only"), + onClick: () => + recrawlLinks({ crawlStatus: "failure", runInference: true }), + variant: "secondary" as const, + loading: isRecrawlPending, + }, + { + label: t("admin.background_jobs.actions.recrawl_all_links"), + onClick: () => recrawlLinks({ crawlStatus: "all", runInference: true }), + loading: isRecrawlPending, + }, + { + label: `${t("admin.background_jobs.actions.recrawl_all_links")} (${t("admin.background_jobs.actions.without_inference")})`, + onClick: () => + recrawlLinks({ crawlStatus: "all", runInference: false }), + loading: isRecrawlPending, + }, + ], + inferenceActions: [ + { + label: t( + "admin.background_jobs.actions.regenerate_ai_tags_for_failed_bookmarks_only", + ), + onClick: () => + reRunInferenceOnAllBookmarks({ type: "tag", status: "failure" }), + variant: "secondary" as const, + loading: isInferencePending, + }, + { + label: t( + "admin.background_jobs.actions.regenerate_ai_tags_for_all_bookmarks", + ), + onClick: () => + reRunInferenceOnAllBookmarks({ type: "tag", status: "all" }), + loading: isInferencePending, + }, + { + label: t( + "admin.background_jobs.actions.regenerate_ai_summaries_for_failed_bookmarks_only", + ), + onClick: () => + reRunInferenceOnAllBookmarks({ + type: "summarize", + status: "failure", + }), + variant: "secondary" as const, + loading: isInferencePending, + }, + { + label: t( + "admin.background_jobs.actions.regenerate_ai_summaries_for_all_bookmarks", + ), + onClick: () => + reRunInferenceOnAllBookmarks({ type: "summarize", status: "all" }), + loading: isInferencePending, + }, + ], + indexingActions: [ + { + label: t("admin.background_jobs.actions.reindex_all_bookmarks"), + onClick: () => reindexBookmarks(), + loading: isReindexPending, + }, + ], + assetPreprocessingActions: [ + { + label: t("admin.background_jobs.actions.reprocess_assets_fix_mode"), + onClick: () => reprocessAssetsFixMode(), + loading: isReprocessingPending, + }, + ], + tidyAssetsActions: [ + { + label: t("admin.background_jobs.actions.clean_assets"), + onClick: () => tidyAssets(), + loading: isTidyAssetsPending, + }, + ], + }; } export default function BackgroundJobs() { @@ -194,84 +415,113 @@ export default function BackgroundJobs() { }, ); + const actions = useJobActions(); + if (!serverStats) { - return ; + return ( +
+
+ + +
+ +
+ {Array.from({ length: 8 }).map((_, index) => ( + + ))} +
+
+ ); } + const jobs = [ + { + title: t("admin.background_jobs.jobs.crawler.title"), + icon: Globe, + stats: serverStats.crawlStats, + description: t("admin.background_jobs.jobs.crawler.description"), + actions: actions.crawlActions, + }, + { + title: t("admin.background_jobs.jobs.inference.title"), + icon: Sparkle, + stats: serverStats.inferenceStats, + description: t("admin.background_jobs.jobs.inference.description"), + actions: actions.inferenceActions, + }, + { + title: t("admin.background_jobs.jobs.indexing.title"), + icon: Search, + stats: { queued: serverStats.indexingStats.queued }, + description: t("admin.background_jobs.jobs.indexing.description"), + actions: actions.indexingActions, + }, + { + title: t("admin.background_jobs.jobs.asset_preprocessing.title"), + icon: Image, + stats: { queued: serverStats.assetPreprocessingStats.queued }, + description: t( + "admin.background_jobs.jobs.asset_preprocessing.description", + ), + actions: actions.assetPreprocessingActions, + }, + { + title: t("admin.background_jobs.jobs.tidy_assets.title"), + icon: Database, + stats: { queued: serverStats.tidyAssetsStats.queued }, + description: t("admin.background_jobs.jobs.tidy_assets.description"), + actions: actions.tidyAssetsActions, + }, + { + title: t("admin.background_jobs.jobs.video.title"), + icon: Video, + stats: { queued: serverStats.videoStats.queued }, + description: t("admin.background_jobs.jobs.video.description"), + actions: [], + }, + { + title: t("admin.background_jobs.jobs.webhook.title"), + icon: Webhook, + stats: { queued: serverStats.webhookStats.queued }, + description: t("admin.background_jobs.jobs.webhook.description"), + actions: [], + }, + { + title: t("admin.background_jobs.jobs.feed.title"), + icon: Rss, + stats: { queued: serverStats.feedStats.queued }, + description: t("admin.background_jobs.jobs.feed.description"), + actions: [], + }, + ]; + return ( -
-
- {t("admin.background_jobs.background_jobs")} -
-
- - - {t("admin.background_jobs.job")} - {t("admin.background_jobs.queued")} - {t("admin.background_jobs.pending")} - {t("admin.background_jobs.failed")} - - - - - {t("admin.background_jobs.crawler_jobs")} - - {serverStats.crawlStats.queued} - {serverStats.crawlStats.pending} - {serverStats.crawlStats.failed} - - - {t("admin.background_jobs.indexing_jobs")} - {serverStats.indexingStats.queued} - - - - - - - {t("admin.background_jobs.inference_jobs")} - {serverStats.inferenceStats.queued} - {serverStats.inferenceStats.pending} - {serverStats.inferenceStats.failed} - - - - {t("admin.background_jobs.tidy_assets_jobs")} - - {serverStats.tidyAssetsStats.queued} - - - - - - - {t("admin.background_jobs.video_jobs")} - {serverStats.videoStats.queued} - - - - - - - {t("admin.background_jobs.webhook_jobs")} - {serverStats.webhookStats.queued} - - - - - - - - {t("admin.background_jobs.asset_preprocessing_jobs")} - - - {serverStats.assetPreprocessingStats.queued} - - - - - - - - {t("admin.background_jobs.feed_jobs")} - {serverStats.feedStats.queued} - - - - - - -
+
+ +
+

+ {t("admin.background_jobs.background_jobs")} +

+

+ {t("admin.background_jobs.monitor_and_manage")} +

+
+ + +
+ +
+ {jobs.map((job, index) => ( + + ))}
-
); } -- cgit v1.2.3-70-g09d2