33import { useState , useEffect } from "react" ;
44import { Modal } from "@/app/_components/GlobalComponents/UIElements/Modal" ;
55import { Button } from "@/app/_components/GlobalComponents/UIElements/Button" ;
6- import { FileTextIcon , TrashIcon , EyeIcon , XIcon , ArrowsClockwiseIcon , WarningCircleIcon , CheckCircleIcon } from "@phosphor-icons/react" ;
6+ import { FileTextIcon , TrashIcon , EyeIcon , XIcon , ArrowsClockwiseIcon , WarningCircleIcon , CheckCircleIcon , DownloadIcon } from "@phosphor-icons/react" ;
77import { useTranslations } from "next-intl" ;
8+ import { zipSync , strToU8 } from "fflate" ;
89import {
910 getJobLogs ,
1011 getLogContent ,
@@ -44,6 +45,7 @@ export const LogsModal = ({
4445 const [ logContent , setLogContent ] = useState < string > ( "" ) ;
4546 const [ isLoadingLogs , setIsLoadingLogs ] = useState ( false ) ;
4647 const [ isLoadingContent , setIsLoadingContent ] = useState ( false ) ;
48+ const [ isDownloading , setIsDownloading ] = useState ( false ) ;
4749 const [ stats , setStats ] = useState < {
4850 count : number ;
4951 totalSize : number ;
@@ -133,6 +135,28 @@ export const LogsModal = ({
133135 }
134136 } ;
135137
138+ const handleDownloadLogs = async ( ) => {
139+ if ( logs . length === 0 ) return ;
140+ setIsDownloading ( true ) ;
141+ try {
142+ const files : Record < string , Uint8Array > = { } ;
143+ for ( const log of logs ) {
144+ const content = await getLogContent ( jobId , log . filename ) ;
145+ files [ log . filename ] = strToU8 ( content ) ;
146+ }
147+ const zipped = zipSync ( files ) ;
148+ const blob = new Blob ( [ zipped as unknown as ArrayBuffer ] , { type : "application/zip" } ) ;
149+ const url = URL . createObjectURL ( blob ) ;
150+ const a = document . createElement ( "a" ) ;
151+ a . href = url ;
152+ a . download = `${ jobComment || jobId } _logs.zip` ;
153+ a . click ( ) ;
154+ URL . revokeObjectURL ( url ) ;
155+ } finally {
156+ setIsDownloading ( false ) ;
157+ }
158+ } ;
159+
136160 const formatFileSize = ( bytes : number ) : string => {
137161 if ( bytes < 1024 ) return `${ bytes } B` ;
138162 if ( bytes < 1024 * 1024 ) return `${ ( bytes / 1024 ) . toFixed ( 2 ) } KB` ;
@@ -157,43 +181,56 @@ export const LogsModal = ({
157181 return (
158182 < Modal isOpen = { isOpen } onClose = { onClose } title = { t ( "cronjobs.viewLogs" ) } size = "xl" >
159183 < div className = "flex flex-col h-[600px]" >
160- < div className = "flex items-center justify-between mb-4 pb-4 border-b border-border" >
161- < div >
162- < h3 className = "font-semibold text-lg" > { jobComment || jobId } </ h3 >
184+ < div className = "block sm: flex items-center justify-between mb-4 pb-4 border-b border-border" >
185+ < div className = "min-w-0 mb-4 sm:mb-0" >
186+ < h3 className = "font-semibold text-lg truncate " > { jobComment || jobId } </ h3 >
163187 { stats && (
164188 < p className = "text-sm text-muted-foreground" >
165189 { stats . count } { t ( "cronjobs.logs" ) } • { stats . totalSizeMB } MB
166190 </ p >
167191 ) }
168192 </ div >
169- < div className = "flex gap-2" >
193+ < div className = "flex gap-2 flex-shrink-0" >
194+ < Button
195+ onClick = { handleDownloadLogs }
196+ disabled = { logs . length === 0 || isDownloading }
197+ className = "btn-primary glow-primary"
198+ size = "sm"
199+ >
200+ { isDownloading ? (
201+ < ArrowsClockwiseIcon className = "w-4 h-4 sm:mr-2 animate-spin" />
202+ ) : (
203+ < DownloadIcon className = "w-4 h-4 sm:mr-2" />
204+ ) }
205+ < span className = "hidden sm:inline" > { t ( "cronjobs.downloadLog" ) } </ span >
206+ </ Button >
170207 < Button
171208 onClick = { loadLogs }
172209 disabled = { isLoadingLogs }
173210 className = "btn-primary glow-primary"
174211 size = "sm"
175212 >
176213 < ArrowsClockwiseIcon
177- className = { `w-4 h-4 mr-2 ${ isLoadingLogs ? "animate-spin" : ""
214+ className = { `w-4 h-4 sm: mr-2 ${ isLoadingLogs ? "animate-spin" : ""
178215 } `}
179216 />
180- { t ( "common.refresh" ) }
217+ < span className = "hidden sm:inline" > { t ( "common.refresh" ) } </ span >
181218 </ Button >
182219 { logs . length > 0 && (
183220 < Button
184221 onClick = { handleDeleteAllLogs }
185222 variant = "destructive"
186223 size = "sm"
187224 >
188- < TrashIcon className = "w-4 h-4 mr-2" />
189- { t ( "cronjobs.deleteAll" ) }
225+ < TrashIcon className = "w-4 h-4 sm: mr-2" />
226+ < span className = "hidden sm:inline" > { t ( "cronjobs.deleteAll" ) } </ span >
190227 </ Button >
191228 ) }
192229 </ div >
193230 </ div >
194231
195- < div className = "flex-1 flex gap-4 overflow-hidden" >
196- < div className = "w-1/3 flex flex-col border-r border-border pr-4 overflow-hidden" >
232+ < div className = "flex-1 flex flex-col sm:flex-row gap-4 overflow-hidden" >
233+ < div className = "sm: w-1/3 flex flex-col sm: border-r border-b sm: border-b-0 border-border sm: pr-4 pb-4 sm:pb-0 overflow-hidden max-h-[40%] sm:max-h-none " >
197234 < h4 className = "font-semibold mb-2" > { t ( "cronjobs.logFiles" ) } </ h4 >
198235 < div className = "flex-1 overflow-y-auto space-y-2" >
199236 { isLoadingLogs ? (
@@ -288,8 +325,8 @@ export const LogsModal = ({
288325
289326 < div className = "mt-4 pt-4 border-t border-border flex justify-end" >
290327 < Button onClick = { onClose } className = "btn-primary glow-primary" >
291- < XIcon className = "w-4 h-4 mr-2" />
292- { t ( "common.close" ) }
328+ < XIcon className = "w-4 h-4 sm: mr-2" />
329+ < span className = "hidden sm:inline" > { t ( "common.close" ) } </ span >
293330 </ Button >
294331 </ div >
295332 </ div >
0 commit comments