Commit 90e09b6c authored by ThinhNC's avatar ThinhNC

fix(crawl-jobs): harmonize progress percentage, unlock export, and streamline crawled pages table

parent e60d8925
...@@ -148,10 +148,10 @@ export default function CrawlJobDetailPage({ params }: PageProps) { ...@@ -148,10 +148,10 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
const targetPages = job?.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages; const targetPages = job?.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages;
const processedPages = (job?.successPages || 0) + (job?.failedPages || 0); const processedPages = (job?.successPages || 0) + (job?.failedPages || 0);
const isJobRunning = job?.status === "RUNNING" || job?.status === "PROCESSING_EXPORT"; const isJobRunning = job?.status === "RUNNING" || job?.status === "PROCESSING_EXPORT";
const progressPercent = job?.status === "COMPLETED" const progressPercent = job?.status === "COMPLETED" || (targetPages > 0 && processedPages >= targetPages)
? 100 ? 100
: targetPages > 0 : targetPages > 0
? Math.min(isJobRunning ? 99 : 100, Math.round((processedPages / targetPages) * 100)) ? Math.min(99, Math.round((processedPages / targetPages) * 100))
: 0; : 0;
// Filtered Logs // Filtered Logs
...@@ -264,7 +264,7 @@ export default function CrawlJobDetailPage({ params }: PageProps) { ...@@ -264,7 +264,7 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
disabled={isJobRunning} disabled={(isJobRunning && processedPages < targetPages) || (job?.successPages || 0) === 0}
onClick={() => setIsExportModalOpen(true)} onClick={() => setIsExportModalOpen(true)}
className="rounded-2xl border-border/80 text-xs font-semibold gap-1.5 hover:bg-muted/40 cursor-pointer" className="rounded-2xl border-border/80 text-xs font-semibold gap-1.5 hover:bg-muted/40 cursor-pointer"
title={t.jobDetail.exportData} title={t.jobDetail.exportData}
...@@ -711,13 +711,12 @@ export default function CrawlJobDetailPage({ params }: PageProps) { ...@@ -711,13 +711,12 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
<th className="py-3 px-4">{t.jobDetail.pages.statusCol}</th> <th className="py-3 px-4">{t.jobDetail.pages.statusCol}</th>
<th className="py-3 px-4">{t.jobDetail.pages.wordsCol}</th> <th className="py-3 px-4">{t.jobDetail.pages.wordsCol}</th>
<th className="py-3 px-4">{t.jobDetail.pages.timeCol}</th> <th className="py-3 px-4">{t.jobDetail.pages.timeCol}</th>
<th className="py-3 px-4 text-right">{t.jobDetail.pages.actionCol}</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-border/60"> <tbody className="divide-y divide-border/60">
{isPagesLoading && ( {isPagesLoading && (
<tr> <tr>
<td colSpan={5} className="py-12 text-center text-muted-foreground"> <td colSpan={4} className="py-12 text-center text-muted-foreground">
<Loader2 className="h-6 w-6 animate-spin mx-auto text-emerald-500 mb-2" /> <Loader2 className="h-6 w-6 animate-spin mx-auto text-emerald-500 mb-2" />
{t.jobDetail.pages.loading} {t.jobDetail.pages.loading}
</td> </td>
...@@ -726,7 +725,7 @@ export default function CrawlJobDetailPage({ params }: PageProps) { ...@@ -726,7 +725,7 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
{!isPagesLoading && (!pagesData?.items || pagesData.items.length === 0) && ( {!isPagesLoading && (!pagesData?.items || pagesData.items.length === 0) && (
<tr> <tr>
<td colSpan={5} className="py-12 text-center text-muted-foreground"> <td colSpan={4} className="py-12 text-center text-muted-foreground">
{isJobRunning ? ( {isJobRunning ? (
<div className="flex flex-col items-center justify-center gap-2.5 max-w-md mx-auto py-2"> <div className="flex flex-col items-center justify-center gap-2.5 max-w-md mx-auto py-2">
<div className="flex items-center gap-2 text-emerald-600 dark:text-emerald-400 font-semibold text-sm"> <div className="flex items-center gap-2 text-emerald-600 dark:text-emerald-400 font-semibold text-sm">
...@@ -749,15 +748,27 @@ export default function CrawlJobDetailPage({ params }: PageProps) { ...@@ -749,15 +748,27 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
{pagesData?.items?.map((pageItem) => { {pagesData?.items?.map((pageItem) => {
const isOk = pageItem.statusCode === 200; const isOk = pageItem.statusCode === 200;
return ( return (
<tr key={pageItem.id} className="hover:bg-muted/20 transition-colors"> <tr
key={pageItem.id}
onClick={() => {
setPreviewPage(pageItem);
setPreviewTab("markdown");
}}
className="hover:bg-muted/40 transition-colors cursor-pointer group"
title={t.jobDetail.pages.previewBtn || "Xem chi tiết nội dung trang"}
>
<td className="py-3 px-4 max-w-sm"> <td className="py-3 px-4 max-w-sm">
<div className="font-semibold text-foreground truncate" title={pageItem.title || ""}> <div
className="font-semibold text-foreground group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors truncate"
title={pageItem.title || ""}
>
{pageItem.title || pageItem.url} {pageItem.title || pageItem.url}
</div> </div>
<a <a
href={pageItem.url} href={pageItem.url}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-[11px] text-emerald-600 dark:text-emerald-400 hover:underline truncate block" className="text-[11px] text-emerald-600 dark:text-emerald-400 hover:underline truncate block"
> >
{pageItem.url} {pageItem.url}
...@@ -777,20 +788,6 @@ export default function CrawlJobDetailPage({ params }: PageProps) { ...@@ -777,20 +788,6 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
<td className="py-3 px-4 text-muted-foreground"> <td className="py-3 px-4 text-muted-foreground">
{pageItem.crawledAt ? formatDate(pageItem.crawledAt, locale) : t.jobDetail.justNow} {pageItem.crawledAt ? formatDate(pageItem.crawledAt, locale) : t.jobDetail.justNow}
</td> </td>
<td className="py-3 px-4 text-right">
<Button
size="sm"
variant="secondary"
onClick={() => {
setPreviewPage(pageItem);
setPreviewTab("markdown");
}}
className="rounded-xl h-7 px-2.5 text-xs font-semibold gap-1 text-emerald-600 dark:text-emerald-400 hover:bg-emerald-500/10 cursor-pointer"
>
<Eye className="h-3 w-3" />
<span>{t.jobDetail.pages.previewBtn}</span>
</Button>
</td>
</tr> </tr>
); );
})} })}
......
...@@ -326,10 +326,10 @@ export function CrawlerTaskTable() { ...@@ -326,10 +326,10 @@ export function CrawlerTaskTable() {
const targetPages = job.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages; const targetPages = job.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages;
const processedPages = (job.successPages || 0) + (job.failedPages || 0); const processedPages = (job.successPages || 0) + (job.failedPages || 0);
const isRunning = job.status === "RUNNING" || job.status === "PROCESSING_EXPORT"; const isRunning = job.status === "RUNNING" || job.status === "PROCESSING_EXPORT";
const progressPct = job.status === "COMPLETED" const progressPct = job.status === "COMPLETED" || (targetPages > 0 && processedPages >= targetPages)
? 100 ? 100
: targetPages > 0 : targetPages > 0
? Math.min(isRunning ? 99 : 100, Math.round((processedPages / targetPages) * 100)) ? Math.min(99, Math.round((processedPages / targetPages) * 100))
: 0; : 0;
const isJobActive = const isJobActive =
job.status === "RUNNING" || job.status === "RUNNING" ||
...@@ -419,7 +419,7 @@ export function CrawlerTaskTable() { ...@@ -419,7 +419,7 @@ export function CrawlerTaskTable() {
<TableCell className="max-[767px]:hidden"> <TableCell className="max-[767px]:hidden">
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex justify-between text-xs text-muted-foreground"> <div className="flex justify-between text-xs text-muted-foreground">
<span> <span title={`Đã xử lý ${processedPages} / ${targetPages} trang (Giới hạn: ${maxPages} trang)`}>
{processedPages} / {targetPages} {t.table.pagesUnit} {processedPages} / {targetPages} {t.table.pagesUnit}
</span> </span>
<span className="font-semibold text-foreground">{progressPct}%</span> <span className="font-semibold text-foreground">{progressPct}%</span>
......
...@@ -174,8 +174,14 @@ export function RecentJobsWidget() { ...@@ -174,8 +174,14 @@ export function RecentJobsWidget() {
<TableBody> <TableBody>
{jobs.map((job) => { {jobs.map((job) => {
const maxPages = Math.max(1, job.maxPages || 20); const maxPages = Math.max(1, job.maxPages || 20);
const currentPages = job.totalPages || job.successPages || 0; const targetPages = job.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages;
const progressPct = Math.min(100, Math.round((currentPages / maxPages) * 100)); const processedPages = (job.successPages || 0) + (job.failedPages || 0);
const isRunning = job.status === "RUNNING" || job.status === "PROCESSING_EXPORT";
const progressPct = job.status === "COMPLETED" || (targetPages > 0 && processedPages >= targetPages)
? 100
: targetPages > 0
? Math.min(99, Math.round((processedPages / targetPages) * 100))
: 0;
return ( return (
<TableRow <TableRow
...@@ -257,8 +263,8 @@ export function RecentJobsWidget() { ...@@ -257,8 +263,8 @@ export function RecentJobsWidget() {
<TableCell className="max-[767px]:hidden"> <TableCell className="max-[767px]:hidden">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground"> <div className="flex justify-between text-xs text-muted-foreground">
<span> <span title={`Đã xử lý ${processedPages} / ${targetPages} trang (Giới hạn: ${maxPages} trang)`}>
{currentPages} / {job.maxPages} {t.table.pagesUnit} {processedPages} / {targetPages} {t.table.pagesUnit}
</span> </span>
<span className="font-semibold text-foreground">{progressPct}%</span> <span className="font-semibold text-foreground">{progressPct}%</span>
</div> </div>
......
...@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; ...@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { crawlJobService } from "@/services/crawl-job.service"; import { crawlJobService } from "@/services/crawl-job.service";
import { CrawlJob, CrawlJobQueryDto, CreateCrawlJobDto } from "@/types/crawl-job"; import { CrawlJob, CrawlJobQueryDto, CreateCrawlJobDto } from "@/types/crawl-job";
import { toast } from "sonner"; import { toast } from "sonner";
import { USAGE_QUERY_KEYS } from "./use-usage";
export const CRAWL_JOBS_QUERY_KEYS = { export const CRAWL_JOBS_QUERY_KEYS = {
all: ["crawl-jobs"] as const, all: ["crawl-jobs"] as const,
...@@ -59,6 +60,7 @@ export function useCreateCrawlJob() { ...@@ -59,6 +60,7 @@ export function useCreateCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() }); queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
}, },
onError: (err: Error) => { onError: (err: Error) => {
toast.error("Không thể khởi tạo tác vụ", { toast.error("Không thể khởi tạo tác vụ", {
...@@ -97,6 +99,7 @@ export function useRerunCrawlJob() { ...@@ -97,6 +99,7 @@ export function useRerunCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() }); queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
}, },
onError: (err: Error) => { onError: (err: Error) => {
toast.error("Không thể chạy lại tác vụ", { toast.error("Không thể chạy lại tác vụ", {
...@@ -122,6 +125,7 @@ export function useCancelCrawlJob() { ...@@ -122,6 +125,7 @@ export function useCancelCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() }); queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
}, },
onError: (err: Error) => { onError: (err: Error) => {
toast.error("Không thể hủy tác vụ", { toast.error("Không thể hủy tác vụ", {
...@@ -144,6 +148,7 @@ export function useDeleteCrawlJob() { ...@@ -144,6 +148,7 @@ export function useDeleteCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() }); queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] }); queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
}, },
onError: (err: Error) => { onError: (err: Error) => {
toast.error("Không thể xóa tác vụ", { toast.error("Không thể xóa tác vụ", {
......
...@@ -7,7 +7,7 @@ import { toast } from "sonner"; ...@@ -7,7 +7,7 @@ import { toast } from "sonner";
export const PROFILE_KEYS = { export const PROFILE_KEYS = {
me: ["auth", "me"] as const, me: ["auth", "me"] as const,
usage: ["auth", "usage"] as const, usage: ["auth", "me", "usage"] as const,
}; };
export function useUserProfile() { export function useUserProfile() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment