Skip to content

Commit 2a2a5a3

Browse files
author
Francesco Pagnamenta
committed
TransferUploadResultDialog improved instructions based on the feedback
1 parent 27c8e51 commit 2a2a5a3

File tree

2 files changed

+160
-47
lines changed

2 files changed

+160
-47
lines changed

app/modules/filesystem/components/dialogs/TransferUploadResultDialog.tsx

Lines changed: 158 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
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
99
import CodeBlock from '~/components/codes/CodeBlock'
1010
import TemplatedCodeBlock from '~/components/codes/TemplatedCodeBlock'
@@ -18,71 +18,141 @@ import { LanguegeType } from '~/types/language'
1818

1919
interface 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+
2628
const 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>

public/file_upload_script_template.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ set -E
33
# -----------------------------------------------------------------------------
44

55
# Template software to be used to upload a large file
6-
data_file="/path/to/local/file" # set the path to your local file
7-
part_file="/path/to/local/file.part" # set the path to the part file
6+
data_file={{outputFilePath}} # set the path to your local file
7+
part_file={{outputFilePath}}.part # set the path to the part file
88
parts_upload_urls={{partsUploadUrls}}
99
complete_upload_url={{completeUploadUrl}}
1010
max_part_size={{maxPartSize}}

0 commit comments

Comments
 (0)