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
175c2a63
Commit
175c2a63
authored
Oct 17, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(exports): filter only exportable crawl jobs and auto-download on export creation
parent
d4e84772
Pipeline
#53757
passed with stage
in 2 minutes and 10 seconds
Changes
2
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
109 additions
and
35 deletions
+109
-35
page.tsx
src/app/crawl-jobs/[id]/page.tsx
+2
-0
create-export-modal.tsx
src/components/exports/create-export-modal.tsx
+107
-35
No files found.
src/app/crawl-jobs/[id]/page.tsx
View file @
175c2a63
...
...
@@ -1311,6 +1311,8 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
onClose=
{
()
=>
setIsExportModalOpen
(
false
)
}
defaultJobId=
{
jobId
}
defaultJobUrl=
{
job
?.
domain
||
job
?.
startUrl
}
defaultSuccessPages=
{
job
?.
successPages
}
defaultStatus=
{
job
?.
status
}
/>
</
div
>
);
...
...
src/components/exports/create-export-modal.tsx
View file @
175c2a63
...
...
@@ -12,6 +12,7 @@ import {
Check
,
Globe
,
Sparkles
,
AlertCircle
,
}
from
"lucide-react"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Input
}
from
"@/components/ui/input"
;
...
...
@@ -19,13 +20,36 @@ import { Label } from "@/components/ui/label";
import
{
useLanguage
}
from
"@/providers/language-provider"
;
import
{
useCrawlJobs
}
from
"@/hooks/use-crawl-jobs"
;
import
{
useCreateCrawlExport
}
from
"@/hooks/use-crawl-exports"
;
import
{
crawlExportService
}
from
"@/services/crawl-export.service"
;
import
{
ExportType
}
from
"@/types/export"
;
import
{
formatDate
}
from
"@/lib/utils"
;
interface
CreateExportModalProps
{
export
interface
CreateExportModalProps
{
isOpen
:
boolean
;
onClose
:
()
=>
void
;
defaultJobId
?:
string
;
defaultJobUrl
?:
string
;
defaultSuccessPages
?:
number
;
defaultStatus
?:
string
;
}
/**
* Kiểm tra xem tác vụ cào có đủ điều kiện để xuất dữ liệu không:
* 1. Phải có ít nhất 1 trang thu thập thành công (successPages > 0)
* 2. Không ở trạng thái đang chạy hoặc chờ (RUNNING, PROCESSING_EXPORT, PENDING, QUEUED)
*/
export
function
isJobExportable
(
job
?:
{
status
?:
string
;
successPages
?:
number
|
null
;
}):
boolean
{
if
(
!
job
)
return
false
;
const
successPages
=
job
.
successPages
??
0
;
if
(
successPages
<=
0
)
return
false
;
return
(
job
.
status
===
"COMPLETED"
||
job
.
status
===
"CANCELED"
||
job
.
status
===
"FAILED"
);
}
const
EXPORT_FORMATS
:
{
...
...
@@ -77,37 +101,68 @@ export function CreateExportModal({
onClose
,
defaultJobId
=
""
,
defaultJobUrl
=
""
,
defaultSuccessPages
,
defaultStatus
,
}:
CreateExportModalProps
)
{
const
{
t
}
=
useLanguage
();
const
{
data
:
jobsData
}
=
useCrawlJobs
({
limit
:
10
});
const
{
t
,
locale
}
=
useLanguage
();
const
{
data
:
jobsData
}
=
useCrawlJobs
({
limit
:
50
,
sortBy
:
"createdAt"
,
order
:
"desc"
,
});
const
createExportMutation
=
useCreateCrawlExport
();
const
[
userSelectedJobId
,
setUserSelectedJobId
]
=
useState
<
string
|
null
>
(
null
);
const
selectedJobId
=
userSelectedJobId
??
defaultJobId
;
const
setSelectedJobId
=
(
id
:
string
)
=>
setUserSelectedJobId
(
id
);
const
[
selectedFormat
,
setSelectedFormat
]
=
useState
<
ExportType
>
(
"CSV"
);
const
[
customFileName
,
setCustomFileName
]
=
useState
(
""
);
const
jobs
=
jobsData
?.
items
||
[];
const
completedJobs
=
jobs
.
filter
((
j
)
=>
j
.
status
===
"COMPLETED"
);
const
isDefaultInCompleted
=
completedJobs
.
some
((
j
)
=>
j
.
id
===
defaultJobId
);
const
exportableJobs
=
useMemo
(()
=>
{
return
jobs
.
filter
(
isJobExportable
);
},
[
jobs
]);
const
displayJobs
=
useMemo
<
Array
<
{
id
:
string
;
startUrl
:
string
;
successPages
?:
number
;
status
?:
string
}
>
Array
<
{
id
:
string
;
startUrl
:
string
;
successPages
?:
number
;
status
?:
string
;
createdAt
?:
string
;
}
>
>
(()
=>
{
if
(
defaultJobId
&&
!
isDefaultInCompleted
)
{
return
[
{
id
:
defaultJobId
,
startUrl
:
defaultJobUrl
||
`Job:
${
defaultJobId
.
slice
(
0
,
16
)}
...`
,
successPages
:
0
,
status
:
"COMPLETED"
,
},
...
completedJobs
,
];
if
(
defaultJobId
)
{
const
isDefaultExportable
=
isJobExportable
({
status
:
defaultStatus
||
"COMPLETED"
,
successPages
:
defaultSuccessPages
!==
undefined
?
defaultSuccessPages
:
1
,
});
if
(
isDefaultExportable
)
{
const
exists
=
exportableJobs
.
some
((
j
)
=>
j
.
id
===
defaultJobId
);
if
(
!
exists
)
{
return
[
{
id
:
defaultJobId
,
startUrl
:
defaultJobUrl
||
`Job:
${
defaultJobId
.
slice
(
0
,
16
)}
...`
,
successPages
:
defaultSuccessPages
??
1
,
status
:
defaultStatus
||
"COMPLETED"
,
},
...
exportableJobs
,
];
}
}
}
return
exportableJobs
;
},
[
exportableJobs
,
defaultJobId
,
defaultJobUrl
,
defaultSuccessPages
,
defaultStatus
]);
const
selectedJobId
=
useMemo
(()
=>
{
if
(
userSelectedJobId
&&
displayJobs
.
some
((
j
)
=>
j
.
id
===
userSelectedJobId
))
{
return
userSelectedJobId
;
}
if
(
defaultJobId
&&
displayJobs
.
some
((
j
)
=>
j
.
id
===
defaultJobId
))
{
return
defaultJobId
;
}
return
completedJobs
;
},
[
completedJobs
,
defaultJobId
,
defaultJobUrl
,
isDefaultInCompleted
]);
return
displayJobs
[
0
]?.
id
||
""
;
},
[
userSelectedJobId
,
defaultJobId
,
displayJobs
]);
if
(
!
isOpen
)
return
null
;
...
...
@@ -117,11 +172,16 @@ export function CreateExportModal({
const
trimmedFileName
=
customFileName
.
trim
();
setCustomFileName
(
""
);
await
createExportMutation
.
mutateAsync
({
const
result
=
await
createExportMutation
.
mutateAsync
({
jobId
:
selectedJobId
,
exportType
:
selectedFormat
,
fileName
:
trimmedFileName
?
trimmedFileName
:
undefined
,
});
// Tự động kích hoạt tải xuống tệp dữ liệu đã xuất về máy tính
if
(
result
?.
id
)
{
crawlExportService
.
downloadExport
(
result
.
id
,
result
.
fileName
);
}
onClose
();
};
...
...
@@ -155,11 +215,18 @@ export function CreateExportModal({
<
form
onSubmit=
{
handleSubmit
}
className=
"py-4 space-y-4"
>
{
/* Job Selection */
}
<
div
className=
"space-y-1.5"
>
<
Label
className=
"text-xs font-semibold"
>
{
t
.
exports
.
modal
.
selectJob
}
</
Label
>
<
div
className=
"flex items-center justify-between"
>
<
Label
className=
"text-xs font-semibold"
>
{
t
.
exports
.
modal
.
selectJob
}
</
Label
>
{
displayJobs
.
length
>
0
&&
(
<
span
className=
"text-[11px] text-muted-foreground font-medium"
>
(
{
displayJobs
.
length
}
tác vụ có thể xuất)
</
span
>
)
}
</
div
>
{
displayJobs
.
length
>
0
?
(
<
div
className=
"space-y-2 max-h-
36
overflow-y-auto pr-1"
>
<
div
className=
"space-y-2 max-h-
40
overflow-y-auto pr-1"
>
{
displayJobs
.
map
((
job
)
=>
(
<
button
key=
{
job
.
id
}
...
...
@@ -172,9 +239,16 @@ export function CreateExportModal({
}`
}
>
<
div
className=
"truncate min-w-0 pr-2"
>
<
p
className=
"font-semibold text-foreground truncate"
>
{
job
.
startUrl
}
</
p
>
<
p
className=
"text-[10px] text-muted-foreground font-mono truncate"
>
ID:
{
job
.
id
.
slice
(
0
,
16
)
}
...
{
job
.
successPages
?
` • ${job.successPages} trang`
:
""
}
<
div
className=
"flex items-center gap-2"
>
<
p
className=
"font-semibold text-foreground truncate max-w-[280px] sm:max-w-md"
>
{
job
.
startUrl
}
</
p
>
<
span
className=
"text-[10px] font-semibold px-2 py-0.5 rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 shrink-0"
>
{
job
.
successPages
??
0
}
trang
</
span
>
</
div
>
<
p
className=
"text-[10px] text-muted-foreground font-mono truncate pt-0.5"
>
ID:
{
job
.
id
.
slice
(
0
,
16
)
}
...
{
job
.
status
?
` • ${job.status === "COMPLETED" ? "Hoàn tất" : job.status}`
:
""
}{
job
.
createdAt
?
` • ${formatDate(job.createdAt, locale)}`
:
""
}
</
p
>
</
div
>
{
selectedJobId
===
job
.
id
&&
(
...
...
@@ -184,12 +258,10 @@ export function CreateExportModal({
))
}
</
div
>
)
:
(
<
Input
placeholder=
"Nhập mã Crawl Job ID..."
value=
{
selectedJobId
}
onChange=
{
(
e
)
=>
setSelectedJobId
(
e
.
target
.
value
)
}
className=
"rounded-2xl text-xs"
/>
<
div
className=
"rounded-2xl border border-amber-500/30 bg-amber-500/10 p-3.5 text-xs text-amber-700 dark:text-amber-400 flex items-center gap-2.5"
>
<
AlertCircle
className=
"h-4 w-4 shrink-0 text-amber-500"
/>
<
span
>
Không có tác vụ cào nào có trang thu thập thành công để xuất dữ liệu.
</
span
>
</
div
>
)
}
</
div
>
...
...
@@ -258,7 +330,7 @@ export function CreateExportModal({
</
Button
>
<
Button
type=
"submit"
disabled=
{
!
selectedJobId
||
createExportMutation
.
isPending
}
disabled=
{
!
selectedJobId
||
displayJobs
.
length
===
0
||
createExportMutation
.
isPending
}
className=
"rounded-2xl text-xs h-9 px-5 bg-emerald-600 hover:bg-emerald-700 text-white shadow-sm shadow-emerald-600/20 cursor-pointer"
>
{
createExportMutation
.
isPending
...
...
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