Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
U
upgrade-data-crawler-fe
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
upgrade-data-crawler-fe
Commits
90e09b6c
Commit
90e09b6c
authored
Sep 07, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(crawl-jobs): harmonize progress percentage, unlock export, and streamline crawled pages table
parent
e60d8925
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
38 additions
and
30 deletions
+38
-30
page.tsx
src/app/crawl-jobs/[id]/page.tsx
+19
-22
crawler-task-table.tsx
src/components/crawler/crawler-task-table.tsx
+3
-3
recent-jobs-widget.tsx
src/components/dashboard/recent-jobs-widget.tsx
+10
-4
use-crawl-jobs.ts
src/hooks/use-crawl-jobs.ts
+5
-0
use-profile.ts
src/hooks/use-profile.ts
+1
-1
No files found.
src/app/crawl-jobs/[id]/page.tsx
View file @
90e09b6c
...
@@ -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
>
);
);
})
}
})
}
...
...
src/components/crawler/crawler-task-table.tsx
View file @
90e09b6c
...
@@ -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
>
...
...
src/components/dashboard/recent-jobs-widget.tsx
View file @
90e09b6c
...
@@ -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
.
max
Pages
}
{
t
.
table
.
pagesUnit
}
{
processedPages
}
/
{
target
Pages
}
{
t
.
table
.
pagesUnit
}
</
span
>
</
span
>
<
span
className=
"font-semibold text-foreground"
>
{
progressPct
}
%
</
span
>
<
span
className=
"font-semibold text-foreground"
>
{
progressPct
}
%
</
span
>
</
div
>
</
div
>
...
...
src/hooks/use-crawl-jobs.ts
View file @
90e09b6c
...
@@ -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ụ"
,
{
...
...
src/hooks/use-profile.ts
View file @
90e09b6c
...
@@ -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
()
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment