@sparkstudio/storage-ui 1.0.13 → 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 +399 -184
- package/dist/index.d.cts +27 -18
- package/dist/index.d.ts +27 -18
- package/dist/index.js +405 -190
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -25,13 +25,13 @@ __export(index_exports, {
|
|
|
25
25
|
ContainerDTO: () => ContainerDTO,
|
|
26
26
|
ContainerType: () => ContainerType,
|
|
27
27
|
ContainerUploadPanel: () => ContainerUploadPanel,
|
|
28
|
+
DesktopFileIcon: () => DesktopFileIcon,
|
|
28
29
|
Home: () => Home,
|
|
29
30
|
HomeView: () => HomeView,
|
|
30
31
|
S3: () => S3,
|
|
31
32
|
SparkStudioStorageSDK: () => SparkStudioStorageSDK,
|
|
32
33
|
UploadContainer: () => UploadContainer,
|
|
33
34
|
UploadDropzone: () => UploadDropzone,
|
|
34
|
-
UploadFilePicker: () => UploadFilePicker,
|
|
35
35
|
UploadFileToS3: () => UploadFileToS3,
|
|
36
36
|
UploadProgressList: () => UploadProgressList,
|
|
37
37
|
UseContainers: () => UseContainers,
|
|
@@ -223,6 +223,7 @@ var ContainerDTO = class {
|
|
|
223
223
|
ContainerType;
|
|
224
224
|
Name;
|
|
225
225
|
ContentType;
|
|
226
|
+
PublicUrl;
|
|
226
227
|
CreatedDate;
|
|
227
228
|
FileSize;
|
|
228
229
|
UserId;
|
|
@@ -232,6 +233,7 @@ var ContainerDTO = class {
|
|
|
232
233
|
this.ContainerType = init.ContainerType;
|
|
233
234
|
this.Name = init.Name;
|
|
234
235
|
this.ContentType = init.ContentType;
|
|
236
|
+
this.PublicUrl = init.PublicUrl;
|
|
235
237
|
this.CreatedDate = init.CreatedDate;
|
|
236
238
|
this.FileSize = init.FileSize;
|
|
237
239
|
this.UserId = init.UserId;
|
|
@@ -260,92 +262,63 @@ var UploadDropzone = ({
|
|
|
260
262
|
isDragging,
|
|
261
263
|
onDragOver,
|
|
262
264
|
onDragLeave,
|
|
263
|
-
onDrop
|
|
265
|
+
onDrop,
|
|
266
|
+
className = "",
|
|
267
|
+
style,
|
|
268
|
+
children
|
|
264
269
|
}) => {
|
|
265
|
-
|
|
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();
|
|
273
|
+
const handleDragOver = (e) => {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
if (onDragOver) onDragOver(e);
|
|
276
|
+
};
|
|
277
|
+
const handleDragLeave = (e) => {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
if (onDragLeave) onDragLeave(e);
|
|
280
|
+
};
|
|
281
|
+
const handleDrop = (e) => {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
if (onDrop) onDrop(e);
|
|
284
|
+
};
|
|
285
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
266
286
|
"div",
|
|
267
287
|
{
|
|
268
|
-
className:
|
|
269
|
-
style: {
|
|
270
|
-
onDragOver,
|
|
271
|
-
onDragLeave,
|
|
272
|
-
onDrop,
|
|
273
|
-
children
|
|
274
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "bi bi-cloud-arrow-up fs-1 mb-2" }),
|
|
275
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mb-2", children: "Drop files here" }),
|
|
276
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("small", { className: "text-muted", children: "or click the button below" })
|
|
277
|
-
]
|
|
288
|
+
className: combinedClassName,
|
|
289
|
+
style: { minHeight: "140px", ...style },
|
|
290
|
+
onDragOver: handleDragOver,
|
|
291
|
+
onDragLeave: handleDragLeave,
|
|
292
|
+
onDrop: handleDrop,
|
|
293
|
+
children
|
|
278
294
|
}
|
|
279
295
|
);
|
|
280
296
|
};
|
|
281
297
|
|
|
282
|
-
// src/components/UploadFilePicker.tsx
|
|
283
|
-
var import_react2 = require("react");
|
|
284
|
-
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
285
|
-
var UploadFilePicker = ({
|
|
286
|
-
multiple,
|
|
287
|
-
accept,
|
|
288
|
-
fileNames,
|
|
289
|
-
onFileChange
|
|
290
|
-
}) => {
|
|
291
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "d-flex gap-2 align-items-center", children: [
|
|
292
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { className: "btn btn-primary mb-0", children: [
|
|
293
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("i", { className: "bi bi-folder2-open me-2" }),
|
|
294
|
-
"Browse files",
|
|
295
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
296
|
-
"input",
|
|
297
|
-
{
|
|
298
|
-
type: "file",
|
|
299
|
-
className: "d-none",
|
|
300
|
-
multiple,
|
|
301
|
-
accept,
|
|
302
|
-
onChange: onFileChange
|
|
303
|
-
}
|
|
304
|
-
)
|
|
305
|
-
] }),
|
|
306
|
-
fileNames.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex-grow-1", children: [
|
|
307
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "small text-muted", children: "Selected:" }),
|
|
308
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("li", { children: name }, name)) })
|
|
309
|
-
] })
|
|
310
|
-
] });
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// src/components/UploadProgressList.tsx
|
|
314
|
-
var import_react3 = require("react");
|
|
315
|
-
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
316
|
-
var UploadProgressList = ({
|
|
317
|
-
uploads
|
|
318
|
-
}) => {
|
|
319
|
-
if (uploads.length === 0) return null;
|
|
320
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mb-2", children: [
|
|
321
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "d-flex justify-content-between small mb-1", children: [
|
|
322
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
323
|
-
u.file.name,
|
|
324
|
-
" - ",
|
|
325
|
-
u?.publicUrl ?? ""
|
|
326
|
-
] }),
|
|
327
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
|
|
328
|
-
] }),
|
|
329
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
330
|
-
"div",
|
|
331
|
-
{
|
|
332
|
-
className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
|
|
333
|
-
role: "progressbar",
|
|
334
|
-
style: { width: `${u.progress}%` },
|
|
335
|
-
"aria-valuenow": u.progress,
|
|
336
|
-
"aria-valuemin": 0,
|
|
337
|
-
"aria-valuemax": 100
|
|
338
|
-
}
|
|
339
|
-
) }),
|
|
340
|
-
u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
|
|
341
|
-
] }, u.id)) });
|
|
342
|
-
};
|
|
343
|
-
|
|
344
298
|
// src/hooks/UseUploadManager.ts
|
|
345
|
-
var
|
|
299
|
+
var import_react2 = require("react");
|
|
346
300
|
|
|
347
301
|
// src/engines/UploadFileToS3.ts
|
|
348
|
-
async function UploadFileToS3(file, presignedUrl, onProgress) {
|
|
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) {
|
|
349
322
|
return new Promise((resolve, reject) => {
|
|
350
323
|
const xhr = new XMLHttpRequest();
|
|
351
324
|
xhr.open("PUT", presignedUrl);
|
|
@@ -360,15 +333,11 @@ async function UploadFileToS3(file, presignedUrl, onProgress) {
|
|
|
360
333
|
resolve();
|
|
361
334
|
} else {
|
|
362
335
|
reject(
|
|
363
|
-
new Error(
|
|
364
|
-
`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`
|
|
365
|
-
)
|
|
336
|
+
new Error(`S3 upload failed: ${xhr.status} ${xhr.statusText}`)
|
|
366
337
|
);
|
|
367
338
|
}
|
|
368
339
|
};
|
|
369
|
-
xhr.onerror = () =>
|
|
370
|
-
reject(new Error("Network error while uploading to S3"));
|
|
371
|
-
};
|
|
340
|
+
xhr.onerror = () => reject(new Error("Network error while uploading to S3"));
|
|
372
341
|
xhr.setRequestHeader(
|
|
373
342
|
"Content-Type",
|
|
374
343
|
file.type || "application/octet-stream"
|
|
@@ -384,14 +353,14 @@ function UseUploadManager({
|
|
|
384
353
|
onUploadComplete,
|
|
385
354
|
onUploadError
|
|
386
355
|
}) {
|
|
387
|
-
const [uploads, setUploads] = (0,
|
|
356
|
+
const [uploads, setUploads] = (0, import_react2.useState)([]);
|
|
388
357
|
const createUploadStates = (files) => Array.from(files).map((file) => ({
|
|
389
358
|
id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
|
|
390
359
|
file,
|
|
391
360
|
progress: 0,
|
|
392
361
|
status: "pending"
|
|
393
362
|
}));
|
|
394
|
-
const uploadFile = (0,
|
|
363
|
+
const uploadFile = (0, import_react2.useCallback)(
|
|
395
364
|
async (upload) => {
|
|
396
365
|
setUploads(
|
|
397
366
|
(prev) => prev.map(
|
|
@@ -402,9 +371,7 @@ function UseUploadManager({
|
|
|
402
371
|
if (!getPresignedUrl) {
|
|
403
372
|
throw new Error("getPresignedUrl is not provided.");
|
|
404
373
|
}
|
|
405
|
-
const presignedUrl = await
|
|
406
|
-
upload.file
|
|
407
|
-
);
|
|
374
|
+
const presignedUrl = await getPresignedUrlWithRetry(upload.file, 10);
|
|
408
375
|
const url = presignedUrl?.PresignedUrl ?? "";
|
|
409
376
|
await UploadFileToS3(upload.file, url, (progress) => {
|
|
410
377
|
setUploads(
|
|
@@ -445,7 +412,21 @@ function UseUploadManager({
|
|
|
445
412
|
},
|
|
446
413
|
[getPresignedUrl, onUploadComplete, onUploadError]
|
|
447
414
|
);
|
|
448
|
-
|
|
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");
|
|
428
|
+
}
|
|
429
|
+
const startUploadsIfNeeded = (0, import_react2.useCallback)(
|
|
449
430
|
(files) => {
|
|
450
431
|
if (!autoUpload || !getPresignedUrl) return;
|
|
451
432
|
const newUploads = createUploadStates(files);
|
|
@@ -456,7 +437,7 @@ function UseUploadManager({
|
|
|
456
437
|
},
|
|
457
438
|
[autoUpload, getPresignedUrl, uploadFile]
|
|
458
439
|
);
|
|
459
|
-
const resetUploads = (0,
|
|
440
|
+
const resetUploads = (0, import_react2.useCallback)(() => setUploads([]), []);
|
|
460
441
|
return {
|
|
461
442
|
uploads,
|
|
462
443
|
startUploadsIfNeeded,
|
|
@@ -464,23 +445,267 @@ function UseUploadManager({
|
|
|
464
445
|
};
|
|
465
446
|
}
|
|
466
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
|
+
|
|
467
695
|
// src/components/UploadContainer.tsx
|
|
468
696
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
469
697
|
var UploadContainer = ({
|
|
470
|
-
title = "Upload files",
|
|
471
|
-
description = "Drag and drop files here, or click the button to browse.",
|
|
472
|
-
multiple = true,
|
|
473
|
-
accept,
|
|
474
698
|
onFilesSelected,
|
|
475
699
|
existingFiles = [],
|
|
700
|
+
existingFilesLoading = false,
|
|
476
701
|
onExistingFileClick,
|
|
702
|
+
onDeleteFile,
|
|
477
703
|
autoUpload = false,
|
|
478
704
|
getPresignedUrl,
|
|
479
705
|
onUploadComplete,
|
|
480
706
|
onUploadError
|
|
481
707
|
}) => {
|
|
482
708
|
const [isDragging, setIsDragging] = (0, import_react5.useState)(false);
|
|
483
|
-
const [fileNames, setFileNames] = (0, import_react5.useState)([]);
|
|
484
709
|
const { uploads, startUploadsIfNeeded } = UseUploadManager({
|
|
485
710
|
autoUpload,
|
|
486
711
|
getPresignedUrl,
|
|
@@ -500,23 +725,16 @@ var UploadContainer = ({
|
|
|
500
725
|
setIsDragging(false);
|
|
501
726
|
const files = e.dataTransfer.files;
|
|
502
727
|
if (!files || files.length === 0) return;
|
|
503
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
504
|
-
onFilesSelected?.(files);
|
|
505
|
-
startUploadsIfNeeded(files);
|
|
506
|
-
};
|
|
507
|
-
const handleFileChange = (e) => {
|
|
508
|
-
const files = e.target.files;
|
|
509
|
-
if (!files || files.length === 0) return;
|
|
510
|
-
setFileNames(Array.from(files).map((f) => f.name));
|
|
511
728
|
onFilesSelected?.(files);
|
|
512
729
|
startUploadsIfNeeded(files);
|
|
513
730
|
};
|
|
514
|
-
const
|
|
731
|
+
const handleExistingFileOpen = (file) => {
|
|
515
732
|
if (onExistingFileClick) {
|
|
516
733
|
onExistingFileClick(file);
|
|
517
734
|
return;
|
|
518
735
|
}
|
|
519
736
|
const a = document.createElement("a");
|
|
737
|
+
a.href = file.PublicUrl ?? "";
|
|
520
738
|
a.download = file.Name ?? "";
|
|
521
739
|
a.target = "_blank";
|
|
522
740
|
a.rel = "noopener noreferrer";
|
|
@@ -524,48 +742,38 @@ var UploadContainer = ({
|
|
|
524
742
|
a.click();
|
|
525
743
|
document.body.removeChild(a);
|
|
526
744
|
};
|
|
527
|
-
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
UploadFilePicker,
|
|
560
|
-
{
|
|
561
|
-
multiple,
|
|
562
|
-
accept,
|
|
563
|
-
fileNames,
|
|
564
|
-
onFileChange: handleFileChange
|
|
565
|
-
}
|
|
566
|
-
),
|
|
567
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads })
|
|
568
|
-
] });
|
|
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
|
+
);
|
|
569
777
|
};
|
|
570
778
|
|
|
571
779
|
// src/hooks/UseContainers.ts
|
|
@@ -579,7 +787,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
|
|
|
579
787
|
setError(null);
|
|
580
788
|
try {
|
|
581
789
|
const sdkDb = new SparkStudioStorageSDK(
|
|
582
|
-
|
|
790
|
+
apiBaseUrl
|
|
583
791
|
);
|
|
584
792
|
const result = await sdkDb.container.ReadRootContainers();
|
|
585
793
|
setContainers(result);
|
|
@@ -604,28 +812,36 @@ function UseContainers({ apiBaseUrl, parentId }) {
|
|
|
604
812
|
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
605
813
|
var ContainerUploadPanel = ({
|
|
606
814
|
containerApiBaseUrl,
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
description = "Drop files to upload. Existing files are listed below."
|
|
815
|
+
storageApiBaseUrl,
|
|
816
|
+
parentContainerId
|
|
610
817
|
}) => {
|
|
611
|
-
const { containers,
|
|
818
|
+
const { containers, reload, loading } = UseContainers({
|
|
612
819
|
apiBaseUrl: containerApiBaseUrl,
|
|
613
820
|
parentId: parentContainerId
|
|
614
821
|
});
|
|
615
822
|
const getPresignedUrl = async (file) => {
|
|
616
|
-
const sdkDb = new SparkStudioStorageSDK(
|
|
617
|
-
|
|
618
|
-
);
|
|
619
|
-
const sdkS3 = new SparkStudioStorageSDK(
|
|
620
|
-
"https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
|
|
621
|
-
);
|
|
823
|
+
const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
|
|
824
|
+
const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
|
|
622
825
|
const containerDTO = await sdkDb.container.CreateFileContainer(
|
|
623
826
|
file.name,
|
|
624
827
|
file.size,
|
|
625
828
|
encodeURIComponent(file.type)
|
|
626
829
|
);
|
|
627
|
-
|
|
628
|
-
|
|
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);
|
|
629
845
|
};
|
|
630
846
|
const handleUploadComplete = async (file, s3Url) => {
|
|
631
847
|
console.log("Upload complete:", file.name, s3Url);
|
|
@@ -634,10 +850,16 @@ var ContainerUploadPanel = ({
|
|
|
634
850
|
const handleUploadError = (file, err) => {
|
|
635
851
|
console.error("Upload failed:", file.name, err);
|
|
636
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
|
+
};
|
|
637
860
|
const handleExistingFileClick = (file) => {
|
|
638
|
-
const downloadUrl = `${containerApiBaseUrl}/api/Container/Download/${file.Id}`;
|
|
639
861
|
const a = document.createElement("a");
|
|
640
|
-
a.href =
|
|
862
|
+
a.href = file.PublicUrl ?? "";
|
|
641
863
|
a.download = file.Name ?? "";
|
|
642
864
|
a.target = "_blank";
|
|
643
865
|
a.rel = "noopener noreferrer";
|
|
@@ -645,26 +867,19 @@ var ContainerUploadPanel = ({
|
|
|
645
867
|
a.click();
|
|
646
868
|
document.body.removeChild(a);
|
|
647
869
|
};
|
|
648
|
-
return /* @__PURE__ */ (0, import_jsx_runtime5.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
autoUpload: true,
|
|
662
|
-
getPresignedUrl,
|
|
663
|
-
onUploadComplete: handleUploadComplete,
|
|
664
|
-
onUploadError: handleUploadError
|
|
665
|
-
}
|
|
666
|
-
)
|
|
667
|
-
] });
|
|
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
|
+
);
|
|
668
883
|
};
|
|
669
884
|
|
|
670
885
|
// src/views/HomeView.tsx
|
|
@@ -688,8 +903,8 @@ function HomeContent() {
|
|
|
688
903
|
user && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
689
904
|
ContainerUploadPanel,
|
|
690
905
|
{
|
|
691
|
-
containerApiBaseUrl: "https://
|
|
692
|
-
storageApiBaseUrl: "https://
|
|
906
|
+
containerApiBaseUrl: "https://localhost:5001",
|
|
907
|
+
storageApiBaseUrl: "https://localhost:5001"
|
|
693
908
|
}
|
|
694
909
|
)
|
|
695
910
|
] });
|
|
@@ -701,13 +916,13 @@ function HomeContent() {
|
|
|
701
916
|
ContainerDTO,
|
|
702
917
|
ContainerType,
|
|
703
918
|
ContainerUploadPanel,
|
|
919
|
+
DesktopFileIcon,
|
|
704
920
|
Home,
|
|
705
921
|
HomeView,
|
|
706
922
|
S3,
|
|
707
923
|
SparkStudioStorageSDK,
|
|
708
924
|
UploadContainer,
|
|
709
925
|
UploadDropzone,
|
|
710
|
-
UploadFilePicker,
|
|
711
926
|
UploadFileToS3,
|
|
712
927
|
UploadProgressList,
|
|
713
928
|
UseContainers,
|