44 Please, refer to the LICENSE file in the root directory.
55 SPDX-License-Identifier: BSD-3-Clause
66*************************************************************************/
7- import React , { useEffect , useState } from 'react'
7+ import React , { useEffect , useMemo , useState } from 'react'
88// dialogs
99import CodeBlock from '~/components/codes/CodeBlock'
1010import TemplatedCodeBlock from '~/components/codes/TemplatedCodeBlock'
@@ -18,71 +18,141 @@ import { LanguegeType } from '~/types/language'
1818
1919interface TransferUploadResultDialogProps {
2020 targetPath : string
21- transferResult : GetTransferUploadResponse
21+ transferResult : GetTransferUploadResponse | null
2222 open : boolean
2323 onClose : ( ) => void
2424}
2525
26+ const DEFAULT_PATH = '/path/to/local/file'
27+
2628const TransferUploadResultDialog : React . FC < TransferUploadResultDialogProps > = ( {
2729 targetPath,
2830 transferResult,
2931 open,
3032 onClose,
31- } : TransferUploadResultDialogProps ) => {
32- const [ scriptTemplate , setScriptTemplate ] = useState < string > ( '' )
33+ } ) => {
34+ // ---------------------------
35+ // 🧩 Hooks (must always run)
36+ // ---------------------------
37+ const [ templateRaw , setTemplateRaw ] = useState < string > ( '' )
38+ const [ scriptFilled , setScriptFilled ] = useState < string > ( '' )
39+ const [ filePath , setFilePath ] = useState < string > ( DEFAULT_PATH )
3340
41+ // ---------------------------
42+ // 📦 Helpers
43+ // ---------------------------
3444 const getTransferDirectives = ( ) => {
45+ if ( ! transferResult ) return null
3546 const { transferDirectives } = transferResult
36- // const { partsUploadUrls, completeUploadUrl, maxPartSize } = transferDirectives
3747 const { parts_upload_urls, complete_upload_url, max_part_size } = transferDirectives
38-
39- const data = {
48+ return {
4049 partsUploadUrls : parts_upload_urls ,
4150 completeUploadUrl : complete_upload_url ,
4251 maxPartSize : max_part_size ,
4352 blocSize : '1048576' , // 1MB
4453 }
45- return data
4654 }
4755
56+ // ---------------------------
57+ // 📄 Load the script template once per transfer result
58+ // ---------------------------
4859 useEffect ( ( ) => {
49- const loadScriptTemplate = async ( ) => {
50- if ( ! transferResult || transferResult === null ) {
51- return
52- }
60+ const loadTemplate = async ( ) => {
61+ if ( ! transferResult ) return
5362 try {
54- // Load the file from the public folder
55- const templateScriptResponse = await fetch ( '/file_upload_script_template.txt' )
56- const templateScriptText = await templateScriptResponse . text ( )
57-
58- const data = getTransferDirectives ( )
59-
60- const { partsUploadUrls, completeUploadUrl, maxPartSize, blocSize } = data
61-
62- const bashData = {
63- partsUploadUrls : formatArray ( partsUploadUrls , LanguegeType . bash ) ,
64- completeUploadUrl : JSON . stringify ( completeUploadUrl , null , 2 ) ,
65- maxPartSize : String ( maxPartSize ) ,
66- blocSize : '1048576' , // 1MB
67- }
68-
69- const filled = templateScriptText . replace (
70- / { { ( .* ?) } } / g,
71- ( _ , key ) => bashData [ key . trim ( ) ] ?? '' ,
72- )
73- setScriptTemplate ( filled )
74- } catch ( error ) {
75- console . error ( 'Failed to load template:' , error )
63+ const res = await fetch ( '/file_upload_script_template.txt' )
64+ const txt = await res . text ( )
65+ setTemplateRaw ( txt )
66+ } catch ( err ) {
67+ console . error ( 'Failed to load template:' , err )
7668 }
7769 }
78-
79- loadScriptTemplate ( )
70+ loadTemplate ( )
8071 } , [ transferResult ] )
8172
82- if ( ! transferResult || transferResult === null ) {
83- return null
73+ // ---------------------------
74+ // 🧠 Compute bash data for replacement
75+ // ---------------------------
76+ const bashData = useMemo ( ( ) => {
77+ if ( ! transferResult ) return null
78+ const data = getTransferDirectives ( )
79+ if ( ! data ) return null
80+ const { partsUploadUrls, completeUploadUrl, maxPartSize, blocSize } = data
81+ return {
82+ partsUploadUrls : formatArray ( partsUploadUrls , LanguegeType . bash ) ,
83+ completeUploadUrl : JSON . stringify ( completeUploadUrl , null , 2 ) ,
84+ maxPartSize : String ( maxPartSize ) ,
85+ blocSize : String ( blocSize ) ,
86+ outputFilePath : filePath ,
87+ }
88+ } , [ transferResult , filePath ] )
89+
90+ // ---------------------------
91+ // 🧩 Fill template when data changes
92+ // ---------------------------
93+ useEffect ( ( ) => {
94+ if ( ! templateRaw || ! bashData ) return
95+ const filled = templateRaw . replace ( / { { ( .* ?) } } / g, ( _ , key ) => {
96+ const k = String ( key ) . trim ( )
97+ return ( bashData as any ) [ k ] ?? ''
98+ } )
99+ setScriptFilled ( filled )
100+ } , [ templateRaw , bashData ] )
101+
102+ // ---------------------------
103+ // 📥 Download & Copy actions
104+ // ---------------------------
105+ const downloadFileName = useMemo ( ( ) => {
106+ const base = filePath . trim ( ) . split ( '/' ) . filter ( Boolean ) . pop ( )
107+ return base || 'firecrest-upload.sh'
108+ } , [ filePath ] )
109+
110+ const handleCopyCode = async ( ) => {
111+ try {
112+ await navigator . clipboard . writeText ( scriptFilled )
113+ alert ( '✅ Script copied to clipboard.' )
114+ } catch {
115+ alert ( 'Could not copy. Please select and copy manually.' )
116+ }
117+ }
118+
119+ const handleDownload = ( ) => {
120+ const blob = new Blob ( [ scriptFilled ] , { type : 'text/x-sh' } )
121+ const url = URL . createObjectURL ( blob )
122+ const a = document . createElement ( 'a' )
123+ a . href = url
124+ a . download = downloadFileName
125+ document . body . appendChild ( a )
126+ a . click ( )
127+ a . remove ( )
128+ URL . revokeObjectURL ( url )
129+ }
130+
131+ const usageCommands = useMemo ( ( ) => {
132+ const p = filePath || DEFAULT_PATH
133+ return [
134+ '# Make it executable' ,
135+ `chmod +x /path/to/your/script_file` ,
136+ '# 3) Run it' ,
137+ '/path/to/your/script_file' ,
138+ ] . join ( '\n' )
139+ } , [ filePath ] )
140+
141+ const handleCopyUsage = async ( ) => {
142+ try {
143+ await navigator . clipboard . writeText ( usageCommands )
144+ alert ( '✅ Usage commands copied.' )
145+ } catch {
146+ alert ( 'Could not copy usage commands.' )
147+ }
84148 }
85149
150+ // ---------------------------
151+ // 🖼️ Render
152+ // ---------------------------
153+ // (all hooks have run above — safe to early-return now)
154+ if ( ! transferResult ) return null
155+
86156 return (
87157 < SimpleDialog
88158 title = 'Transfer Upload Operation'
@@ -93,7 +163,7 @@ const TransferUploadResultDialog: React.FC<TransferUploadResultDialogProps> = ({
93163 closeButtonName = 'Close'
94164 >
95165 < div className = 'space-y-8 text-sm text-gray-700' >
96- { /* Success message */ }
166+ { /* ✅ Success message */ }
97167 < div className = 'rounded-md bg-green-50 border border-green-200 p-4' >
98168 < p >
99169 ✅ The upload operation to the destination path{ ' ' }
@@ -102,15 +172,15 @@ const TransferUploadResultDialog: React.FC<TransferUploadResultDialogProps> = ({
102172 </ p >
103173 </ div >
104174
105- { /* Section: Multipart upload info */ }
175+ { /* 📖 Multipart upload info */ }
106176 < section >
107177 < h3 className = 'text-base font-semibold text-gray-900 mb-3' >
108178 Uploading large files using S3 multipart protocol
109179 </ h3 >
110180 < div className = 'space-y-3 leading-relaxed' >
111181 < p >
112182 For large file uploads, FirecREST provides upload URLs based on the S3 multipart
113- protocol. The number of URLs depends on the file size and the FirecREST settings.
183+ protocol. The number of URLs depends on the file size and FirecREST settings.
114184 </ p >
115185 < p >
116186 📘 Learn more in the{ ' ' }
@@ -124,18 +194,61 @@ const TransferUploadResultDialog: React.FC<TransferUploadResultDialogProps> = ({
124194 </ a >
125195 .
126196 </ p >
197+ < p >
198+ We provide below a bash script template that you can use to upload your file in parts.
199+ </ p >
200+ </ div >
201+ </ section >
202+
203+ { /* 📝 File path field */ }
204+ < section >
205+ < h3 className = 'text-base font-semibold text-gray-900 mb-2' > 1. Complete the script</ h3 >
206+ < p className = 'text-gray-600 py-2' >
207+ Complete the script by specifying the path to your local file below.
208+ </ p >
209+ < div className = 'flex flex-col gap-2' >
210+ < input
211+ type = 'text'
212+ value = { filePath }
213+ onChange = { ( e ) => setFilePath ( e . target . value ) }
214+ className = 'w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-xs text-gray-900'
215+ placeholder = { DEFAULT_PATH }
216+ />
217+ < p className = 'text-gray-600' >
218+ This path will appear in the commands below and (if supported) inside the script
219+ template.
220+ </ p >
127221 </ div >
128222 </ section >
129223
224+ { /* 🧰 Script example + actions */ }
130225 < section >
131226 < h3 className = 'text-base font-semibold text-gray-900 mb-3' >
132- Usage example (using < code > dd </ code > )
227+ 2. Copy or download the upload script
133228 </ h3 >
134229 < p className = 'leading-relaxed mb-4' >
135- The script example below uses < code > dd</ code > and has the part upload URLs and the
136- complete URL defined in the header.
230+ The script below uses < code > dd</ code > and defines part upload URLs and completion URL in
231+ the header.
137232 </ p >
138- < TemplatedCodeBlock code = { scriptTemplate } />
233+
234+ < div className = 'flex flex-wrap items-center gap-2 mb-3' >
235+ < button
236+ onClick = { handleDownload }
237+ className = 'rounded-md bg-white text-gray-800 px-3 py-1.5 text-xs border border-gray-300 hover:bg-gray-50'
238+ title = { `Download as ${ downloadFileName } ` }
239+ >
240+ Download script
241+ </ button >
242+ </ div >
243+
244+ < TemplatedCodeBlock code = { scriptFilled } />
245+ </ section >
246+
247+ { /* 💡 How to use */ }
248+ < section >
249+ < h3 className = 'text-base font-semibold text-gray-900 mb-2 py-3' > 3. Run the script</ h3 >
250+
251+ < TemplatedCodeBlock code = { usageCommands } />
139252 </ section >
140253 </ div >
141254 </ SimpleDialog >
0 commit comments