Commit ff1b6c4d authored by ThinhNC's avatar ThinhNC

Merge branch 'fix/progress-calculation-and-crawled-pages-ux' into 'develop'

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

See merge request !11
parents 54cfeda8 90e09b6c
......@@ -148,10 +148,10 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
const targetPages = job?.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages;
const processedPages = (job?.successPages || 0) + (job?.failedPages || 0);
const isJobRunning = job?.status === "RUNNING" || job?.status === "PROCESSING_EXPORT";
const progressPercent = job?.status === "COMPLETED"
const progressPercent = job?.status === "COMPLETED" || (targetPages > 0 && processedPages >= targetPages)
? 100
: targetPages > 0
? Math.min(isJobRunning ? 99 : 100, Math.round((processedPages / targetPages) * 100))
? Math.min(99, Math.round((processedPages / targetPages) * 100))
: 0;
// Filtered Logs
......@@ -264,7 +264,7 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
<Button
size="sm"
variant="outline"
disabled={isJobRunning}
disabled={(isJobRunning && processedPages < targetPages) || (job?.successPages || 0) === 0}
onClick={() => setIsExportModalOpen(true)}
className="rounded-2xl border-border/80 text-xs font-semibold gap-1.5 hover:bg-muted/40 cursor-pointer"
title={t.jobDetail.exportData}
......@@ -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.wordsCol}</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>
</thead>
<tbody className="divide-y divide-border/60">
{isPagesLoading && (
<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" />
{t.jobDetail.pages.loading}
</td>
......@@ -726,7 +725,7 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
{!isPagesLoading && (!pagesData?.items || pagesData.items.length === 0) && (
<tr>
<td colSpan={5} className="py-12 text-center text-muted-foreground">
<td colSpan={4} className="py-12 text-center text-muted-foreground">
{isJobRunning ? (
<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">
......@@ -749,15 +748,27 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
{pagesData?.items?.map((pageItem) => {
const isOk = pageItem.statusCode === 200;
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">
<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}
</div>
<a
href={pageItem.url}
target="_blank"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-[11px] text-emerald-600 dark:text-emerald-400 hover:underline truncate block"
>
{pageItem.url}
......@@ -777,20 +788,6 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
<td className="py-3 px-4 text-muted-foreground">
{pageItem.crawledAt ? formatDate(pageItem.crawledAt, locale) : t.jobDetail.justNow}
</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>
);
})}
......
......@@ -326,10 +326,10 @@ export function CrawlerTaskTable() {
const targetPages = job.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages;
const processedPages = (job.successPages || 0) + (job.failedPages || 0);
const isRunning = job.status === "RUNNING" || job.status === "PROCESSING_EXPORT";
const progressPct = job.status === "COMPLETED"
const progressPct = job.status === "COMPLETED" || (targetPages > 0 && processedPages >= targetPages)
? 100
: targetPages > 0
? Math.min(isRunning ? 99 : 100, Math.round((processedPages / targetPages) * 100))
? Math.min(99, Math.round((processedPages / targetPages) * 100))
: 0;
const isJobActive =
job.status === "RUNNING" ||
......@@ -419,7 +419,7 @@ export function CrawlerTaskTable() {
<TableCell className="max-[767px]:hidden">
<div className="space-y-1.5">
<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}
</span>
<span className="font-semibold text-foreground">{progressPct}%</span>
......
......@@ -174,8 +174,14 @@ export function RecentJobsWidget() {
<TableBody>
{jobs.map((job) => {
const maxPages = Math.max(1, job.maxPages || 20);
const currentPages = job.totalPages || job.successPages || 0;
const progressPct = Math.min(100, Math.round((currentPages / maxPages) * 100));
const targetPages = job.totalPages && job.totalPages > 0 ? Math.min(maxPages, job.totalPages) : maxPages;
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 (
<TableRow
......@@ -257,8 +263,8 @@ export function RecentJobsWidget() {
<TableCell className="max-[767px]:hidden">
<div className="space-y-1">
<div className="flex justify-between text-xs text-muted-foreground">
<span>
{currentPages} / {job.maxPages} {t.table.pagesUnit}
<span title={`Đã xử lý ${processedPages} / ${targetPages} trang (Giới hạn: ${maxPages} trang)`}>
{processedPages} / {targetPages} {t.table.pagesUnit}
</span>
<span className="font-semibold text-foreground">{progressPct}%</span>
</div>
......
......@@ -4,6 +4,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { crawlJobService } from "@/services/crawl-job.service";
import { CrawlJob, CrawlJobQueryDto, CreateCrawlJobDto } from "@/types/crawl-job";
import { toast } from "sonner";
import { USAGE_QUERY_KEYS } from "./use-usage";
export const CRAWL_JOBS_QUERY_KEYS = {
all: ["crawl-jobs"] as const,
......@@ -59,6 +60,7 @@ export function useCreateCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
},
onError: (err: Error) => {
toast.error("Không thể khởi tạo tác vụ", {
......@@ -97,6 +99,7 @@ export function useRerunCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
},
onError: (err: Error) => {
toast.error("Không thể chạy lại tác vụ", {
......@@ -122,6 +125,7 @@ export function useCancelCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
},
onError: (err: Error) => {
toast.error("Không thể hủy tác vụ", {
......@@ -144,6 +148,7 @@ export function useDeleteCrawlJob() {
queryClient.invalidateQueries({ queryKey: CRAWL_JOBS_QUERY_KEYS.lists() });
queryClient.invalidateQueries({ queryKey: ["crawler", "tasks"] });
queryClient.invalidateQueries({ queryKey: ["crawler", "stats"] });
queryClient.invalidateQueries({ queryKey: USAGE_QUERY_KEYS.usage });
},
onError: (err: Error) => {
toast.error("Không thể xóa tác vụ", {
......
......@@ -7,7 +7,7 @@ import { toast } from "sonner";
export const PROFILE_KEYS = {
me: ["auth", "me"] as const,
usage: ["auth", "usage"] as const,
usage: ["auth", "me", "usage"] as const,
};
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