@@ -5,7 +5,9 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@c
55import { Button } from '@comp/ui/button' ;
66import { Card , CardContent , CardHeader , CardTitle } from '@comp/ui/card' ;
77import { cn } from '@comp/ui/cn' ;
8- import { CheckCircle2 , Circle , Download , XCircle } from 'lucide-react' ;
8+ import { Progress } from '@comp/ui/progress' ;
9+ import JSZip from 'jszip' ;
10+ import { CheckCircle2 , Circle , Download , Loader2 , XCircle } from 'lucide-react' ;
911import Image from 'next/image' ;
1012import { useState } from 'react' ;
1113import { toast } from 'sonner' ;
@@ -17,21 +19,27 @@ interface DeviceAgentAccordionItemProps {
1719 fleetPolicies ?: FleetPolicy [ ] ;
1820}
1921
22+ type DownloadStatus = 'idle' | 'preparing' | 'downloading' | 'creating-zip' | 'complete' ;
23+
2024export function DeviceAgentAccordionItem ( {
2125 member,
2226 host,
2327 fleetPolicies = [ ] ,
2428} : DeviceAgentAccordionItemProps ) {
25- const [ isDownloading , setIsDownloading ] = useState ( false ) ;
29+ const [ downloadStatus , setDownloadStatus ] = useState < DownloadStatus > ( 'idle' ) ;
30+ const [ downloadProgress , setDownloadProgress ] = useState ( 0 ) ;
2631
2732 const hasInstalledAgent = host !== null ;
2833 const allPoliciesPass =
2934 fleetPolicies . length === 0 || fleetPolicies . every ( ( policy ) => policy . response === 'pass' ) ;
3035 const isCompleted = hasInstalledAgent && allPoliciesPass ;
3136
3237 const handleDownload = async ( ) => {
33- setIsDownloading ( true ) ;
38+ setDownloadStatus ( 'preparing' ) ;
39+ setDownloadProgress ( 0 ) ;
40+
3441 try {
42+ // Step 1: Get download URL and script content from the API
3543 const response = await fetch ( '/api/download-agent' , {
3644 method : 'POST' ,
3745 headers : { 'Content-Type' : 'application/json' } ,
@@ -43,23 +51,166 @@ export function DeviceAgentAccordionItem({
4351
4452 if ( ! response . ok ) {
4553 const errorText = await response . text ( ) ;
46- throw new Error ( errorText || 'Failed to download agent.' ) ;
54+ throw new Error ( errorText || 'Failed to get download information.' ) ;
55+ }
56+
57+ const { scriptContent, scriptFilename, packageDownloadUrl, packageFilename } =
58+ await response . json ( ) ;
59+
60+ // Step 2: Download the package file
61+ setDownloadStatus ( 'downloading' ) ;
62+ setDownloadProgress ( 10 ) ;
63+
64+ const packageResponse = await fetch ( packageDownloadUrl ) ;
65+
66+ if ( ! packageResponse . ok ) {
67+ throw new Error ( 'Failed to download agent package.' ) ;
68+ }
69+
70+ // Get the content length for progress tracking
71+ const contentLength = packageResponse . headers . get ( 'content-length' ) ;
72+ const total = contentLength ? parseInt ( contentLength , 10 ) : 0 ;
73+
74+ // Read the response with progress tracking
75+ const reader = packageResponse . body ?. getReader ( ) ;
76+ if ( ! reader ) {
77+ throw new Error ( 'Failed to read package data.' ) ;
4778 }
4879
49- const blob = await response . blob ( ) ;
50- const url = window . URL . createObjectURL ( blob ) ;
80+ const chunks : Uint8Array [ ] = [ ] ;
81+ let receivedLength = 0 ;
82+
83+ while ( true ) {
84+ const { done, value } = await reader . read ( ) ;
85+
86+ if ( done ) break ;
87+
88+ chunks . push ( value ) ;
89+ receivedLength += value . length ;
90+
91+ if ( total > 0 ) {
92+ // Update progress (10-70% range for download)
93+ const downloadPercent = ( receivedLength / total ) * 60 + 10 ;
94+ setDownloadProgress ( Math . round ( downloadPercent ) ) ;
95+ }
96+ }
97+
98+ // Combine chunks into a single Uint8Array
99+ const chunksAll = new Uint8Array ( receivedLength ) ;
100+ let position = 0 ;
101+ for ( const chunk of chunks ) {
102+ chunksAll . set ( chunk , position ) ;
103+ position += chunk . length ;
104+ }
105+
106+ const packageBlob = new Blob ( [ chunksAll ] ) ;
107+
108+ // Step 3: Create zip file using JSZip
109+ setDownloadStatus ( 'creating-zip' ) ;
110+ setDownloadProgress ( 75 ) ;
111+
112+ const zip = new JSZip ( ) ;
113+
114+ // Add the script file
115+ const scriptBlob = new Blob ( [ scriptContent ] , { type : 'text/plain' } ) ;
116+ zip . file ( scriptFilename , scriptBlob ) ;
117+
118+ // Add the package file
119+ zip . file ( packageFilename , packageBlob ) ;
120+
121+ // Add README
122+ const readmeContent = `Comp AI Device Agent Installation Instructions
123+
124+ 1. Extract this zip file to a folder on your computer
125+ 2. Run the "Install Me First" file first
126+ 3. Then run the Fleet installer package
127+
128+ For macOS:
129+ - Run: ./${ scriptFilename }
130+ - Then open the .pkg file
131+
132+ For Windows:
133+ - Run: ${ scriptFilename }
134+ - Then run the .msi installer
135+
136+ If you have any issues, please contact your IT administrator.` ;
137+
138+ zip . file ( 'README.txt' , readmeContent ) ;
139+
140+ setDownloadProgress ( 85 ) ;
141+
142+ // Step 4: Generate and download the zip
143+ const zipBlob = await zip . generateAsync ( {
144+ type : 'blob' ,
145+ compression : 'DEFLATE' ,
146+ compressionOptions : { level : 9 } ,
147+ } ) ;
148+
149+ setDownloadProgress ( 100 ) ;
150+
151+ // Create download link
152+ const url = window . URL . createObjectURL ( zipBlob ) ;
51153 const a = document . createElement ( 'a' ) ;
52154 a . href = url ;
53155 a . download = 'compai-device-agent.zip' ;
54156 document . body . appendChild ( a ) ;
55157 a . click ( ) ;
56158 a . remove ( ) ;
57159 window . URL . revokeObjectURL ( url ) ;
160+
161+ setDownloadStatus ( 'complete' ) ;
162+ toast . success ( 'Download completed successfully!' ) ;
163+
164+ // Reset after a delay
165+ setTimeout ( ( ) => {
166+ setDownloadStatus ( 'idle' ) ;
167+ setDownloadProgress ( 0 ) ;
168+ } , 3000 ) ;
58169 } catch ( error ) {
59170 console . error ( error ) ;
60171 toast . error ( error instanceof Error ? error . message : 'An unknown error occurred.' ) ;
61- } finally {
62- setIsDownloading ( false ) ;
172+ setDownloadStatus ( 'idle' ) ;
173+ setDownloadProgress ( 0 ) ;
174+ }
175+ } ;
176+
177+ const getButtonContent = ( ) => {
178+ switch ( downloadStatus ) {
179+ case 'preparing' :
180+ return (
181+ < >
182+ < Loader2 className = "h-4 w-4 animate-spin" />
183+ Preparing download...
184+ </ >
185+ ) ;
186+ case 'downloading' :
187+ return (
188+ < >
189+ < Loader2 className = "h-4 w-4 animate-spin" />
190+ Downloading package...
191+ </ >
192+ ) ;
193+ case 'creating-zip' :
194+ return (
195+ < >
196+ < Loader2 className = "h-4 w-4 animate-spin" />
197+ Creating installer...
198+ </ >
199+ ) ;
200+ case 'complete' :
201+ return (
202+ < >
203+ < CheckCircle2 className = "h-4 w-4" />
204+ Download complete!
205+ </ >
206+ ) ;
207+ default :
208+ return (
209+ < >
210+ < Download className = "h-4 w-4" />
211+ Download Agent
212+ </ >
213+ ) ;
63214 }
64215 } ;
65216
@@ -101,12 +252,14 @@ export function DeviceAgentAccordionItem({
101252 size = "sm"
102253 variant = "default"
103254 onClick = { handleDownload }
104- disabled = { isDownloading || hasInstalledAgent }
255+ disabled = { downloadStatus !== 'idle' || hasInstalledAgent }
105256 className = "gap-2 mt-2"
106257 >
107- < Download className = "h-4 w-4" />
108- { isDownloading ? 'Downloading...' : 'Download Agent' }
258+ { getButtonContent ( ) }
109259 </ Button >
260+ { downloadStatus !== 'idle' && downloadStatus !== 'complete' && (
261+ < Progress value = { downloadProgress } className = "mt-2 h-2" />
262+ ) }
110263 </ li >
111264 < li >
112265 < strong > Run the "Install Me First" file</ strong >
0 commit comments