@sparkstudio/storage-ui 1.0.12 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +628 -181
- package/dist/index.d.cts +86 -18
- package/dist/index.d.ts +86 -18
- package/dist/index.js +627 -182
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -24,11 +24,18 @@ __export(index_exports, {
|
|
|
24
24
|
Container: () => Container,
|
|
25
25
|
ContainerDTO: () => ContainerDTO,
|
|
26
26
|
ContainerType: () => ContainerType,
|
|
27
|
+
ContainerUploadPanel: () => ContainerUploadPanel,
|
|
28
|
+
DesktopFileIcon: () => DesktopFileIcon,
|
|
27
29
|
Home: () => Home,
|
|
28
30
|
HomeView: () => HomeView,
|
|
29
31
|
S3: () => S3,
|
|
30
32
|
SparkStudioStorageSDK: () => SparkStudioStorageSDK,
|
|
31
|
-
UploadContainer: () => UploadContainer
|
|
33
|
+
UploadContainer: () => UploadContainer,
|
|
34
|
+
UploadDropzone: () => UploadDropzone,
|
|
35
|
+
UploadFileToS3: () => UploadFileToS3,
|
|
36
|
+
UploadProgressList: () => UploadProgressList,
|
|
37
|
+
UseContainers: () => UseContainers,
|
|
38
|
+
UseUploadManager: () => UseUploadManager
|
|
32
39
|
});
|
|
33
40
|
module.exports = __toCommonJS(index_exports);
|
|
34
41
|
|
|
@@ -216,6 +223,7 @@ var ContainerDTO = class {
|
|
|
216
223
|
ContainerType;
|
|
217
224
|
Name;
|
|
218
225
|
ContentType;
|
|
226
|
+
PublicUrl;
|
|
219
227
|
CreatedDate;
|
|
220
228
|
FileSize;
|
|
221
229
|
UserId;
|
|
@@ -225,6 +233,7 @@ var ContainerDTO = class {
|
|
|
225
233
|
this.ContainerType = init.ContainerType;
|
|
226
234
|
this.Name = init.Name;
|
|
227
235
|
this.ContentType = init.ContentType;
|
|
236
|
+
this.PublicUrl = init.PublicUrl;
|
|
228
237
|
this.CreatedDate = init.CreatedDate;
|
|
229
238
|
this.FileSize = init.FileSize;
|
|
230
239
|
this.UserId = init.UserId;
|
|
@@ -240,162 +249,76 @@ var ContainerType = /* @__PURE__ */ ((ContainerType2) => {
|
|
|
240
249
|
return ContainerType2;
|
|
241
250
|
})(ContainerType || {});
|
|
242
251
|
|
|
252
|
+
// src/components/ContainerUploadPanel.tsx
|
|
253
|
+
var import_react7 = require("react");
|
|
254
|
+
|
|
243
255
|
// src/components/UploadContainer.tsx
|
|
256
|
+
var import_react5 = require("react");
|
|
257
|
+
|
|
258
|
+
// src/components/UploadDropzone.tsx
|
|
244
259
|
var import_react = require("react");
|
|
245
260
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
246
|
-
var
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
onUploadComplete,
|
|
255
|
-
onUploadError
|
|
261
|
+
var UploadDropzone = ({
|
|
262
|
+
isDragging,
|
|
263
|
+
onDragOver,
|
|
264
|
+
onDragLeave,
|
|
265
|
+
onDrop,
|
|
266
|
+
className = "",
|
|
267
|
+
style,
|
|
268
|
+
children
|
|
256
269
|
}) => {
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const startUploadsIfNeeded = (files) => {
|
|
261
|
-
if (!autoUpload || !getPresignedUrl) return;
|
|
262
|
-
const newUploads = Array.from(files).map((file) => ({
|
|
263
|
-
id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
|
|
264
|
-
file,
|
|
265
|
-
progress: 0,
|
|
266
|
-
status: "pending"
|
|
267
|
-
}));
|
|
268
|
-
setUploads((prev) => [...prev, ...newUploads]);
|
|
269
|
-
newUploads.forEach((upload) => {
|
|
270
|
-
uploadFile(upload);
|
|
271
|
-
});
|
|
272
|
-
};
|
|
273
|
-
const uploadFile = async (upload) => {
|
|
274
|
-
setUploads(
|
|
275
|
-
(prev) => prev.map(
|
|
276
|
-
(u) => u.id === upload.id ? { ...u, status: "uploading", progress: 0 } : u
|
|
277
|
-
)
|
|
278
|
-
);
|
|
279
|
-
try {
|
|
280
|
-
if (!getPresignedUrl) {
|
|
281
|
-
throw new Error("getPresignedUrl is not provided.");
|
|
282
|
-
}
|
|
283
|
-
const presignedUrl = await getPresignedUrl(upload.file);
|
|
284
|
-
const url = presignedUrl?.PresignedUrl ?? "";
|
|
285
|
-
await uploadFileToS3(upload.file, url, (progress) => {
|
|
286
|
-
setUploads(
|
|
287
|
-
(prev) => prev.map(
|
|
288
|
-
(u) => u.id === upload.id ? { ...u, progress } : u
|
|
289
|
-
)
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
const fileUrl = url.split("?")[0];
|
|
293
|
-
setUploads(
|
|
294
|
-
(prev) => prev.map(
|
|
295
|
-
(u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl, publicUrl: presignedUrl.PublicUrl } : u
|
|
296
|
-
)
|
|
297
|
-
);
|
|
298
|
-
onUploadComplete?.(upload.file, fileUrl);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
let message = "Upload failed";
|
|
301
|
-
if (err instanceof Error) {
|
|
302
|
-
message = err.message;
|
|
303
|
-
}
|
|
304
|
-
setUploads(
|
|
305
|
-
(prev) => prev.map(
|
|
306
|
-
(u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
|
|
307
|
-
)
|
|
308
|
-
);
|
|
309
|
-
onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
|
|
310
|
-
}
|
|
311
|
-
};
|
|
270
|
+
const baseClass = "border rounded-3 p-4 mb-3 d-flex flex-column align-items-center justify-content-center ";
|
|
271
|
+
const stateClass = isDragging ? "bg-light border-primary" : "border-secondary border-dashed";
|
|
272
|
+
const combinedClassName = `${baseClass}${stateClass} ${className}`.trim();
|
|
312
273
|
const handleDragOver = (e) => {
|
|
313
274
|
e.preventDefault();
|
|
314
|
-
|
|
275
|
+
if (onDragOver) onDragOver(e);
|
|
315
276
|
};
|
|
316
277
|
const handleDragLeave = (e) => {
|
|
317
278
|
e.preventDefault();
|
|
318
|
-
|
|
279
|
+
if (onDragLeave) onDragLeave(e);
|
|
319
280
|
};
|
|
320
281
|
const handleDrop = (e) => {
|
|
321
282
|
e.preventDefault();
|
|
322
|
-
|
|
323
|
-
const files = e.dataTransfer.files;
|
|
324
|
-
if (!files || files.length === 0) return;
|
|
325
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
326
|
-
onFilesSelected?.(files);
|
|
327
|
-
startUploadsIfNeeded(files);
|
|
328
|
-
};
|
|
329
|
-
const handleFileChange = (e) => {
|
|
330
|
-
const files = e.target.files;
|
|
331
|
-
if (!files || files.length === 0) return;
|
|
332
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
333
|
-
onFilesSelected?.(files);
|
|
334
|
-
startUploadsIfNeeded(files);
|
|
283
|
+
if (onDrop) onDrop(e);
|
|
335
284
|
};
|
|
336
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
children: [
|
|
348
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "bi bi-cloud-arrow-up fs-1 mb-2" }),
|
|
349
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mb-2", children: "Drop files here" }),
|
|
350
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "text-muted", children: "or click the button below" })
|
|
351
|
-
]
|
|
352
|
-
}
|
|
353
|
-
),
|
|
354
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "d-flex gap-2 align-items-center", children: [
|
|
355
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "btn btn-primary mb-0", children: [
|
|
356
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "bi bi-folder2-open me-2" }),
|
|
357
|
-
"Browse files",
|
|
358
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
359
|
-
"input",
|
|
360
|
-
{
|
|
361
|
-
type: "file",
|
|
362
|
-
className: "d-none",
|
|
363
|
-
multiple,
|
|
364
|
-
accept,
|
|
365
|
-
onChange: handleFileChange
|
|
366
|
-
}
|
|
367
|
-
)
|
|
368
|
-
] }),
|
|
369
|
-
fileNames.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex-grow-1", children: [
|
|
370
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "small text-muted", children: "Selected:" }),
|
|
371
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: name }, name)) })
|
|
372
|
-
] })
|
|
373
|
-
] }),
|
|
374
|
-
uploads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-2", children: [
|
|
375
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "d-flex justify-content-between small mb-1", children: [
|
|
376
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
377
|
-
u.file.name,
|
|
378
|
-
" - ",
|
|
379
|
-
u?.publicUrl ?? ""
|
|
380
|
-
] }),
|
|
381
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
|
|
382
|
-
] }),
|
|
383
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
384
|
-
"div",
|
|
385
|
-
{
|
|
386
|
-
className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
|
|
387
|
-
role: "progressbar",
|
|
388
|
-
style: { width: `${u.progress}%` },
|
|
389
|
-
"aria-valuenow": u.progress,
|
|
390
|
-
"aria-valuemin": 0,
|
|
391
|
-
"aria-valuemax": 100
|
|
392
|
-
}
|
|
393
|
-
) }),
|
|
394
|
-
u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
|
|
395
|
-
] }, u.id)) })
|
|
396
|
-
] }) }) });
|
|
285
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
286
|
+
"div",
|
|
287
|
+
{
|
|
288
|
+
className: combinedClassName,
|
|
289
|
+
style: { minHeight: "140px", ...style },
|
|
290
|
+
onDragOver: handleDragOver,
|
|
291
|
+
onDragLeave: handleDragLeave,
|
|
292
|
+
onDrop: handleDrop,
|
|
293
|
+
children
|
|
294
|
+
}
|
|
295
|
+
);
|
|
397
296
|
};
|
|
398
|
-
|
|
297
|
+
|
|
298
|
+
// src/hooks/UseUploadManager.ts
|
|
299
|
+
var import_react2 = require("react");
|
|
300
|
+
|
|
301
|
+
// src/engines/UploadFileToS3.ts
|
|
302
|
+
async function UploadFileToS3(file, presignedUrl, onProgress, maxRetries = 3) {
|
|
303
|
+
let attempt = 0;
|
|
304
|
+
while (true) {
|
|
305
|
+
try {
|
|
306
|
+
await uploadOnce(file, presignedUrl, onProgress);
|
|
307
|
+
return;
|
|
308
|
+
} catch (err) {
|
|
309
|
+
if (attempt >= maxRetries) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Upload failed after ${attempt + 1} attempts: ${err.message}`
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
attempt++;
|
|
315
|
+
const delay = 300 * attempt;
|
|
316
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
317
|
+
console.warn(`Retrying upload (attempt ${attempt + 1})...`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function uploadOnce(file, presignedUrl, onProgress) {
|
|
399
322
|
return new Promise((resolve, reject) => {
|
|
400
323
|
const xhr = new XMLHttpRequest();
|
|
401
324
|
xhr.open("PUT", presignedUrl);
|
|
@@ -410,13 +333,11 @@ async function uploadFileToS3(file, presignedUrl, onProgress) {
|
|
|
410
333
|
resolve();
|
|
411
334
|
} else {
|
|
412
335
|
reject(
|
|
413
|
-
new Error(`S3 upload failed
|
|
336
|
+
new Error(`S3 upload failed: ${xhr.status} ${xhr.statusText}`)
|
|
414
337
|
);
|
|
415
338
|
}
|
|
416
339
|
};
|
|
417
|
-
xhr.onerror = () =>
|
|
418
|
-
reject(new Error("Network error while uploading to S3"));
|
|
419
|
-
};
|
|
340
|
+
xhr.onerror = () => reject(new Error("Network error while uploading to S3"));
|
|
420
341
|
xhr.setRequestHeader(
|
|
421
342
|
"Content-Type",
|
|
422
343
|
file.type || "application/octet-stream"
|
|
@@ -425,59 +346,585 @@ async function uploadFileToS3(file, presignedUrl, onProgress) {
|
|
|
425
346
|
});
|
|
426
347
|
}
|
|
427
348
|
|
|
428
|
-
// src/
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
349
|
+
// src/hooks/UseUploadManager.ts
|
|
350
|
+
function UseUploadManager({
|
|
351
|
+
autoUpload = false,
|
|
352
|
+
getPresignedUrl,
|
|
353
|
+
onUploadComplete,
|
|
354
|
+
onUploadError
|
|
355
|
+
}) {
|
|
356
|
+
const [uploads, setUploads] = (0, import_react2.useState)([]);
|
|
357
|
+
const createUploadStates = (files) => Array.from(files).map((file) => ({
|
|
358
|
+
id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
|
|
359
|
+
file,
|
|
360
|
+
progress: 0,
|
|
361
|
+
status: "pending"
|
|
362
|
+
}));
|
|
363
|
+
const uploadFile = (0, import_react2.useCallback)(
|
|
364
|
+
async (upload) => {
|
|
365
|
+
setUploads(
|
|
366
|
+
(prev) => prev.map(
|
|
367
|
+
(u) => u.id === upload.id ? { ...u, status: "uploading", progress: 0 } : u
|
|
368
|
+
)
|
|
369
|
+
);
|
|
370
|
+
try {
|
|
371
|
+
if (!getPresignedUrl) {
|
|
372
|
+
throw new Error("getPresignedUrl is not provided.");
|
|
373
|
+
}
|
|
374
|
+
const presignedUrl = await getPresignedUrlWithRetry(upload.file, 10);
|
|
375
|
+
const url = presignedUrl?.PresignedUrl ?? "";
|
|
376
|
+
await UploadFileToS3(upload.file, url, (progress) => {
|
|
377
|
+
setUploads(
|
|
378
|
+
(prev) => prev.map(
|
|
379
|
+
(u) => u.id === upload.id ? { ...u, progress } : u
|
|
380
|
+
)
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
const fileUrl = url.split("?")[0];
|
|
384
|
+
setUploads(
|
|
385
|
+
(prev) => prev.map(
|
|
386
|
+
(u) => u.id === upload.id ? {
|
|
387
|
+
...u,
|
|
388
|
+
status: "success",
|
|
389
|
+
progress: 100,
|
|
390
|
+
s3Url: fileUrl,
|
|
391
|
+
// assumes your DTO has PublicUrl
|
|
392
|
+
publicUrl: presignedUrl.PublicUrl
|
|
393
|
+
} : u
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
onUploadComplete?.(upload.file, fileUrl);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
let message = "Upload failed";
|
|
399
|
+
if (err instanceof Error) {
|
|
400
|
+
message = err.message;
|
|
401
|
+
}
|
|
402
|
+
setUploads(
|
|
403
|
+
(prev) => prev.map(
|
|
404
|
+
(u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
|
|
405
|
+
)
|
|
406
|
+
);
|
|
407
|
+
onUploadError?.(
|
|
408
|
+
upload.file,
|
|
409
|
+
err instanceof Error ? err : new Error(message)
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
[getPresignedUrl, onUploadComplete, onUploadError]
|
|
414
|
+
);
|
|
415
|
+
async function getPresignedUrlWithRetry(file, attempts = 3) {
|
|
416
|
+
let lastError;
|
|
417
|
+
for (let i = 1; i <= attempts; i++) {
|
|
418
|
+
try {
|
|
419
|
+
return await getPresignedUrl(file);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
lastError = err;
|
|
422
|
+
if (i < attempts) {
|
|
423
|
+
await new Promise((r) => setTimeout(r, 500 * i));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
|
|
434
428
|
}
|
|
429
|
+
const startUploadsIfNeeded = (0, import_react2.useCallback)(
|
|
430
|
+
(files) => {
|
|
431
|
+
if (!autoUpload || !getPresignedUrl) return;
|
|
432
|
+
const newUploads = createUploadStates(files);
|
|
433
|
+
setUploads((prev) => [...prev, ...newUploads]);
|
|
434
|
+
newUploads.forEach((upload) => {
|
|
435
|
+
void uploadFile(upload);
|
|
436
|
+
});
|
|
437
|
+
},
|
|
438
|
+
[autoUpload, getPresignedUrl, uploadFile]
|
|
439
|
+
);
|
|
440
|
+
const resetUploads = (0, import_react2.useCallback)(() => setUploads([]), []);
|
|
441
|
+
return {
|
|
442
|
+
uploads,
|
|
443
|
+
startUploadsIfNeeded,
|
|
444
|
+
resetUploads
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/components/UploadProgressList.tsx
|
|
449
|
+
var import_react3 = require("react");
|
|
450
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
451
|
+
var UploadProgressList = ({
|
|
452
|
+
uploads
|
|
453
|
+
}) => {
|
|
454
|
+
const visibleUploads = uploads.filter((u) => u.status !== "success");
|
|
455
|
+
if (visibleUploads.length === 0) return null;
|
|
456
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
457
|
+
"div",
|
|
458
|
+
{
|
|
459
|
+
className: "w-100 d-flex flex-wrap gap-4 align-content-start mt-3",
|
|
460
|
+
style: { minHeight: "80px" },
|
|
461
|
+
children: visibleUploads.map((u) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
462
|
+
"div",
|
|
463
|
+
{
|
|
464
|
+
className: "d-flex flex-column align-items-center",
|
|
465
|
+
style: {
|
|
466
|
+
width: 96,
|
|
467
|
+
userSelect: "none"
|
|
468
|
+
},
|
|
469
|
+
title: u.file.name,
|
|
470
|
+
children: [
|
|
471
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
472
|
+
"div",
|
|
473
|
+
{
|
|
474
|
+
className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
|
|
475
|
+
style: {
|
|
476
|
+
width: 64,
|
|
477
|
+
height: 64
|
|
478
|
+
},
|
|
479
|
+
children: [
|
|
480
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("i", { className: "bi bi-file-earmark fs-2" }),
|
|
481
|
+
u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger", children: "!" })
|
|
482
|
+
]
|
|
483
|
+
}
|
|
484
|
+
),
|
|
485
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
486
|
+
"div",
|
|
487
|
+
{
|
|
488
|
+
className: "small text-center text-truncate",
|
|
489
|
+
style: { width: "100%" },
|
|
490
|
+
children: u.file.name
|
|
491
|
+
}
|
|
492
|
+
),
|
|
493
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "w-100 mt-1", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "progress", style: { height: "4px" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
494
|
+
"div",
|
|
495
|
+
{
|
|
496
|
+
className: "progress-bar " + (u.status === "error" ? "bg-danger" : ""),
|
|
497
|
+
role: "progressbar",
|
|
498
|
+
style: { width: `${u.progress}%` },
|
|
499
|
+
"aria-valuenow": u.progress,
|
|
500
|
+
"aria-valuemin": 0,
|
|
501
|
+
"aria-valuemax": 100
|
|
502
|
+
}
|
|
503
|
+
) }) }),
|
|
504
|
+
u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
505
|
+
"div",
|
|
506
|
+
{
|
|
507
|
+
className: "text-danger small mt-1 text-center",
|
|
508
|
+
style: { width: "100%" },
|
|
509
|
+
children: u.error ?? "Upload failed"
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
]
|
|
513
|
+
},
|
|
514
|
+
u.id
|
|
515
|
+
))
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/components/DesktopFileIcon.tsx
|
|
521
|
+
var import_react4 = require("react");
|
|
522
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
523
|
+
var DesktopFileIcon = ({
|
|
524
|
+
name,
|
|
525
|
+
sizeBytes,
|
|
526
|
+
downloadUrl,
|
|
527
|
+
onOpen,
|
|
528
|
+
onDelete
|
|
529
|
+
}) => {
|
|
530
|
+
const [contextMenuPos, setContextMenuPos] = (0, import_react4.useState)(
|
|
531
|
+
null
|
|
532
|
+
);
|
|
533
|
+
const [isHovered, setIsHovered] = (0, import_react4.useState)(false);
|
|
534
|
+
const iconRef = (0, import_react4.useRef)(null);
|
|
535
|
+
const menuRef = (0, import_react4.useRef)(null);
|
|
536
|
+
const handleDoubleClick = () => {
|
|
537
|
+
if (onOpen) {
|
|
538
|
+
onOpen();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (downloadUrl) {
|
|
542
|
+
const a = document.createElement("a");
|
|
543
|
+
a.href = downloadUrl;
|
|
544
|
+
a.download = name ?? "";
|
|
545
|
+
a.target = "_blank";
|
|
546
|
+
a.rel = "noopener noreferrer";
|
|
547
|
+
document.body.appendChild(a);
|
|
548
|
+
a.click();
|
|
549
|
+
document.body.removeChild(a);
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const handleContextMenu = (e) => {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
setContextMenuPos({ x: e.clientX, y: e.clientY });
|
|
555
|
+
};
|
|
556
|
+
const closeMenu = () => setContextMenuPos(null);
|
|
557
|
+
const handleDownload = () => {
|
|
558
|
+
closeMenu();
|
|
559
|
+
if (!downloadUrl) return;
|
|
560
|
+
const a = document.createElement("a");
|
|
561
|
+
a.href = downloadUrl;
|
|
562
|
+
a.download = name ?? "";
|
|
563
|
+
a.target = "_blank";
|
|
564
|
+
a.rel = "noopener noreferrer";
|
|
565
|
+
document.body.appendChild(a);
|
|
566
|
+
a.click();
|
|
567
|
+
document.body.removeChild(a);
|
|
568
|
+
};
|
|
569
|
+
const handleCopyUrl = async () => {
|
|
570
|
+
closeMenu();
|
|
571
|
+
if (!downloadUrl) return;
|
|
572
|
+
try {
|
|
573
|
+
await navigator.clipboard?.writeText(downloadUrl);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
console.error("Failed to copy URL", err);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
const handleDelete = () => {
|
|
579
|
+
closeMenu();
|
|
580
|
+
onDelete?.();
|
|
581
|
+
};
|
|
582
|
+
const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
|
|
583
|
+
(0, import_react4.useEffect)(() => {
|
|
584
|
+
if (!contextMenuPos) return;
|
|
585
|
+
const handleGlobalClick = (e) => {
|
|
586
|
+
const target = e.target;
|
|
587
|
+
if (menuRef.current && menuRef.current.contains(target)) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (iconRef.current && iconRef.current.contains(target)) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
closeMenu();
|
|
594
|
+
};
|
|
595
|
+
document.addEventListener("mousedown", handleGlobalClick);
|
|
596
|
+
return () => {
|
|
597
|
+
document.removeEventListener("mousedown", handleGlobalClick);
|
|
598
|
+
};
|
|
599
|
+
}, [contextMenuPos]);
|
|
600
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
601
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
602
|
+
"div",
|
|
603
|
+
{
|
|
604
|
+
ref: iconRef,
|
|
605
|
+
className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-light border" : ""),
|
|
606
|
+
style: {
|
|
607
|
+
width: 96,
|
|
608
|
+
cursor: "pointer",
|
|
609
|
+
userSelect: "none",
|
|
610
|
+
transition: "background-color 0.1s ease, border-color 0.1s ease"
|
|
611
|
+
},
|
|
612
|
+
onDoubleClick: handleDoubleClick,
|
|
613
|
+
onContextMenu: handleContextMenu,
|
|
614
|
+
title: name ?? void 0,
|
|
615
|
+
onClick: () => {
|
|
616
|
+
if (contextMenuPos) {
|
|
617
|
+
closeMenu();
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
onMouseEnter: () => setIsHovered(true),
|
|
621
|
+
onMouseLeave: () => setIsHovered(false),
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
624
|
+
"div",
|
|
625
|
+
{
|
|
626
|
+
className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm",
|
|
627
|
+
style: {
|
|
628
|
+
width: 64,
|
|
629
|
+
height: 64
|
|
630
|
+
},
|
|
631
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", { className: "bi bi-file-earmark fs-2" })
|
|
632
|
+
}
|
|
633
|
+
),
|
|
634
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
635
|
+
"div",
|
|
636
|
+
{
|
|
637
|
+
className: "small text-center text-truncate",
|
|
638
|
+
style: { width: "100%" },
|
|
639
|
+
children: name
|
|
640
|
+
}
|
|
641
|
+
),
|
|
642
|
+
formattedSize && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { className: "text-muted mt-1", children: formattedSize })
|
|
643
|
+
]
|
|
644
|
+
}
|
|
645
|
+
),
|
|
646
|
+
contextMenuPos && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
647
|
+
"div",
|
|
648
|
+
{
|
|
649
|
+
ref: menuRef,
|
|
650
|
+
className: "position-fixed dropdown-menu show shadow-sm",
|
|
651
|
+
style: {
|
|
652
|
+
top: contextMenuPos.y,
|
|
653
|
+
left: contextMenuPos.x,
|
|
654
|
+
zIndex: 1050,
|
|
655
|
+
minWidth: 180
|
|
656
|
+
},
|
|
657
|
+
onClick: (e) => e.stopPropagation(),
|
|
658
|
+
children: [
|
|
659
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
660
|
+
"button",
|
|
661
|
+
{
|
|
662
|
+
type: "button",
|
|
663
|
+
className: "dropdown-item small",
|
|
664
|
+
onClick: handleDownload,
|
|
665
|
+
disabled: !downloadUrl,
|
|
666
|
+
children: "Download file"
|
|
667
|
+
}
|
|
668
|
+
),
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
670
|
+
"button",
|
|
671
|
+
{
|
|
672
|
+
type: "button",
|
|
673
|
+
className: "dropdown-item small",
|
|
674
|
+
onClick: handleCopyUrl,
|
|
675
|
+
disabled: !downloadUrl,
|
|
676
|
+
children: "Copy download URL"
|
|
677
|
+
}
|
|
678
|
+
),
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dropdown-divider" }),
|
|
680
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
681
|
+
"button",
|
|
682
|
+
{
|
|
683
|
+
type: "button",
|
|
684
|
+
className: "dropdown-item text-danger small",
|
|
685
|
+
onClick: handleDelete,
|
|
686
|
+
children: "Delete file"
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
]
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
] });
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/components/UploadContainer.tsx
|
|
696
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
697
|
+
var UploadContainer = ({
|
|
698
|
+
onFilesSelected,
|
|
699
|
+
existingFiles = [],
|
|
700
|
+
existingFilesLoading = false,
|
|
701
|
+
onExistingFileClick,
|
|
702
|
+
onDeleteFile,
|
|
703
|
+
autoUpload = false,
|
|
704
|
+
getPresignedUrl,
|
|
705
|
+
onUploadComplete,
|
|
706
|
+
onUploadError
|
|
707
|
+
}) => {
|
|
708
|
+
const [isDragging, setIsDragging] = (0, import_react5.useState)(false);
|
|
709
|
+
const { uploads, startUploadsIfNeeded } = UseUploadManager({
|
|
710
|
+
autoUpload,
|
|
711
|
+
getPresignedUrl,
|
|
712
|
+
onUploadComplete,
|
|
713
|
+
onUploadError
|
|
714
|
+
});
|
|
715
|
+
const handleDragOver = (e) => {
|
|
716
|
+
e.preventDefault();
|
|
717
|
+
setIsDragging(true);
|
|
718
|
+
};
|
|
719
|
+
const handleDragLeave = (e) => {
|
|
720
|
+
e.preventDefault();
|
|
721
|
+
setIsDragging(false);
|
|
722
|
+
};
|
|
723
|
+
const handleDrop = (e) => {
|
|
724
|
+
e.preventDefault();
|
|
725
|
+
setIsDragging(false);
|
|
726
|
+
const files = e.dataTransfer.files;
|
|
727
|
+
if (!files || files.length === 0) return;
|
|
728
|
+
onFilesSelected?.(files);
|
|
729
|
+
startUploadsIfNeeded(files);
|
|
730
|
+
};
|
|
731
|
+
const handleExistingFileOpen = (file) => {
|
|
732
|
+
if (onExistingFileClick) {
|
|
733
|
+
onExistingFileClick(file);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const a = document.createElement("a");
|
|
737
|
+
a.href = file.PublicUrl ?? "";
|
|
738
|
+
a.download = file.Name ?? "";
|
|
739
|
+
a.target = "_blank";
|
|
740
|
+
a.rel = "noopener noreferrer";
|
|
741
|
+
document.body.appendChild(a);
|
|
742
|
+
a.click();
|
|
743
|
+
document.body.removeChild(a);
|
|
744
|
+
};
|
|
745
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
746
|
+
UploadDropzone,
|
|
747
|
+
{
|
|
748
|
+
isDragging,
|
|
749
|
+
onDragOver: handleDragOver,
|
|
750
|
+
onDragLeave: handleDragLeave,
|
|
751
|
+
onDrop: handleDrop,
|
|
752
|
+
className: "w-100",
|
|
753
|
+
style: { minHeight: "260px", alignItems: "stretch" },
|
|
754
|
+
children: [
|
|
755
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
756
|
+
"div",
|
|
757
|
+
{
|
|
758
|
+
className: "w-100 d-flex flex-wrap gap-4 align-content-start",
|
|
759
|
+
style: { minHeight: "140px" },
|
|
760
|
+
children: existingFilesLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 d-flex justify-content-center align-items-center", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "spinner-border text-secondary", role: "status", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "visually-hidden", children: "Loading containers..." }) }) }) : existingFiles.map((file) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
761
|
+
DesktopFileIcon,
|
|
762
|
+
{
|
|
763
|
+
name: file.Name,
|
|
764
|
+
sizeBytes: typeof file.FileSize === "number" ? file.FileSize : null,
|
|
765
|
+
downloadUrl: file.PublicUrl ?? null,
|
|
766
|
+
onOpen: () => handleExistingFileOpen(file),
|
|
767
|
+
onDelete: onDeleteFile ? () => onDeleteFile(file) : void 0
|
|
768
|
+
},
|
|
769
|
+
file.Id
|
|
770
|
+
))
|
|
771
|
+
}
|
|
772
|
+
),
|
|
773
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 mt-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-3", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-grow-1 w-100", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads }) }) }) })
|
|
774
|
+
]
|
|
775
|
+
}
|
|
776
|
+
);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/hooks/UseContainers.ts
|
|
780
|
+
var import_react6 = require("react");
|
|
781
|
+
function UseContainers({ apiBaseUrl, parentId }) {
|
|
782
|
+
const [containers, setContainers] = (0, import_react6.useState)([]);
|
|
783
|
+
const [loading, setLoading] = (0, import_react6.useState)(false);
|
|
784
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
785
|
+
const load = async () => {
|
|
786
|
+
setLoading(true);
|
|
787
|
+
setError(null);
|
|
788
|
+
try {
|
|
789
|
+
const sdkDb = new SparkStudioStorageSDK(
|
|
790
|
+
apiBaseUrl
|
|
791
|
+
);
|
|
792
|
+
const result = await sdkDb.container.ReadRootContainers();
|
|
793
|
+
setContainers(result);
|
|
794
|
+
} catch (err) {
|
|
795
|
+
setError(err);
|
|
796
|
+
} finally {
|
|
797
|
+
setLoading(false);
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
(0, import_react6.useEffect)(() => {
|
|
801
|
+
void load();
|
|
802
|
+
}, [apiBaseUrl, parentId]);
|
|
803
|
+
return {
|
|
804
|
+
containers,
|
|
805
|
+
loading,
|
|
806
|
+
error,
|
|
807
|
+
reload: load
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/components/ContainerUploadPanel.tsx
|
|
812
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
813
|
+
var ContainerUploadPanel = ({
|
|
814
|
+
containerApiBaseUrl,
|
|
815
|
+
storageApiBaseUrl,
|
|
816
|
+
parentContainerId
|
|
817
|
+
}) => {
|
|
818
|
+
const { containers, reload, loading } = UseContainers({
|
|
819
|
+
apiBaseUrl: containerApiBaseUrl,
|
|
820
|
+
parentId: parentContainerId
|
|
821
|
+
});
|
|
435
822
|
const getPresignedUrl = async (file) => {
|
|
436
|
-
const sdkDb = new SparkStudioStorageSDK(
|
|
437
|
-
const sdkS3 = new SparkStudioStorageSDK(
|
|
438
|
-
const containerDTO = await sdkDb.container.CreateFileContainer(
|
|
439
|
-
|
|
440
|
-
|
|
823
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
824
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
825
|
+
const containerDTO = await sdkDb.container.CreateFileContainer(
|
|
826
|
+
file.name,
|
|
827
|
+
file.size,
|
|
828
|
+
encodeURIComponent(file.type)
|
|
829
|
+
);
|
|
830
|
+
async function getPresignedUrlWithRetry(container, attempts = 3) {
|
|
831
|
+
let lastError;
|
|
832
|
+
for (let i = 1; i <= attempts; i++) {
|
|
833
|
+
try {
|
|
834
|
+
return await sdkS3.s3.GetPreSignedUrl(container);
|
|
835
|
+
} catch (err) {
|
|
836
|
+
lastError = err;
|
|
837
|
+
if (i < attempts) {
|
|
838
|
+
await new Promise((r) => setTimeout(r, 500 * i));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
|
|
843
|
+
}
|
|
844
|
+
return getPresignedUrlWithRetry(containerDTO);
|
|
845
|
+
};
|
|
846
|
+
const handleUploadComplete = async (file, s3Url) => {
|
|
847
|
+
console.log("Upload complete:", file.name, s3Url);
|
|
848
|
+
await reload();
|
|
441
849
|
};
|
|
442
|
-
|
|
850
|
+
const handleUploadError = (file, err) => {
|
|
851
|
+
console.error("Upload failed:", file.name, err);
|
|
852
|
+
};
|
|
853
|
+
const handleOnDeleteFile = async (file) => {
|
|
854
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
855
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
856
|
+
await sdkDb.container.DeleteContainer(file.Id);
|
|
857
|
+
await sdkS3.s3.DeleteS3(file);
|
|
858
|
+
await reload();
|
|
859
|
+
};
|
|
860
|
+
const handleExistingFileClick = (file) => {
|
|
861
|
+
const a = document.createElement("a");
|
|
862
|
+
a.href = file.PublicUrl ?? "";
|
|
863
|
+
a.download = file.Name ?? "";
|
|
864
|
+
a.target = "_blank";
|
|
865
|
+
a.rel = "noopener noreferrer";
|
|
866
|
+
document.body.appendChild(a);
|
|
867
|
+
a.click();
|
|
868
|
+
document.body.removeChild(a);
|
|
869
|
+
};
|
|
870
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
871
|
+
UploadContainer,
|
|
872
|
+
{
|
|
873
|
+
existingFiles: containers,
|
|
874
|
+
existingFilesLoading: loading,
|
|
875
|
+
onExistingFileClick: handleExistingFileClick,
|
|
876
|
+
autoUpload: true,
|
|
877
|
+
getPresignedUrl,
|
|
878
|
+
onUploadComplete: handleUploadComplete,
|
|
879
|
+
onUploadError: handleUploadError,
|
|
880
|
+
onDeleteFile: handleOnDeleteFile
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// src/views/HomeView.tsx
|
|
886
|
+
var import_authentication_ui = require("@sparkstudio/authentication-ui");
|
|
887
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
888
|
+
function HomeView() {
|
|
889
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
443
890
|
import_authentication_ui.AuthenticatorProvider,
|
|
444
891
|
{
|
|
445
892
|
googleClientId: import_authentication_ui.AppSettings.GoogleClientId,
|
|
446
893
|
authenticationUrl: import_authentication_ui.AppSettings.AuthenticationUrl,
|
|
447
894
|
accountsUrl: import_authentication_ui.AppSettings.AccountsUrl,
|
|
448
|
-
|
|
449
|
-
children: [
|
|
450
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_authentication_ui.UserInfoCard, {}),
|
|
451
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
452
|
-
UploadContainer,
|
|
453
|
-
{
|
|
454
|
-
title: "Upload your files to S3",
|
|
455
|
-
description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
|
|
456
|
-
multiple: true,
|
|
457
|
-
accept: "*/*",
|
|
458
|
-
autoUpload: true,
|
|
459
|
-
getPresignedUrl,
|
|
460
|
-
onUploadComplete: (file, s3Url) => {
|
|
461
|
-
console.log("Uploaded", file.name, "to", s3Url);
|
|
462
|
-
},
|
|
463
|
-
onUploadError: (file, error) => {
|
|
464
|
-
console.error("Failed to upload", file.name, error);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
)
|
|
468
|
-
]
|
|
895
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HomeContent, {})
|
|
469
896
|
}
|
|
470
897
|
);
|
|
471
898
|
}
|
|
899
|
+
function HomeContent() {
|
|
900
|
+
const { user } = (0, import_authentication_ui.useUser)();
|
|
901
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
902
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_authentication_ui.UserInfoCard, {}),
|
|
903
|
+
user && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
904
|
+
ContainerUploadPanel,
|
|
905
|
+
{
|
|
906
|
+
containerApiBaseUrl: "https://localhost:5001",
|
|
907
|
+
storageApiBaseUrl: "https://localhost:5001"
|
|
908
|
+
}
|
|
909
|
+
)
|
|
910
|
+
] });
|
|
911
|
+
}
|
|
472
912
|
// Annotate the CommonJS export names for ESM import in node:
|
|
473
913
|
0 && (module.exports = {
|
|
474
914
|
AWSPresignedUrlDTO,
|
|
475
915
|
Container,
|
|
476
916
|
ContainerDTO,
|
|
477
917
|
ContainerType,
|
|
918
|
+
ContainerUploadPanel,
|
|
919
|
+
DesktopFileIcon,
|
|
478
920
|
Home,
|
|
479
921
|
HomeView,
|
|
480
922
|
S3,
|
|
481
923
|
SparkStudioStorageSDK,
|
|
482
|
-
UploadContainer
|
|
924
|
+
UploadContainer,
|
|
925
|
+
UploadDropzone,
|
|
926
|
+
UploadFileToS3,
|
|
927
|
+
UploadProgressList,
|
|
928
|
+
UseContainers,
|
|
929
|
+
UseUploadManager
|
|
483
930
|
});
|