@sparkstudio/storage-ui 1.0.13 → 1.0.15

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 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
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
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: "border rounded-3 p-4 text-center mb-3 d-flex flex-column align-items-center justify-content-center " + (isDragging ? "bg-light border-primary" : "border-secondary border-dashed"),
269
- style: { cursor: "pointer", minHeight: "140px" },
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 import_react4 = require("react");
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, import_react4.useState)([]);
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, import_react4.useCallback)(
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 getPresignedUrl(
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
- const startUploadsIfNeeded = (0, import_react4.useCallback)(
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, import_react4.useCallback)(() => setUploads([]), []);
440
+ const resetUploads = (0, import_react2.useCallback)(() => setUploads([]), []);
460
441
  return {
461
442
  uploads,
462
443
  startUploadsIfNeeded,
@@ -464,23 +445,290 @@ 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 === "uploading" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
482
+ "div",
483
+ {
484
+ className: "position-absolute top-50 start-50 translate-middle",
485
+ style: { pointerEvents: "none" },
486
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "spinner-border spinner-border-sm text-primary" })
487
+ }
488
+ ),
489
+ 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: "!" })
490
+ ]
491
+ }
492
+ ),
493
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
494
+ "div",
495
+ {
496
+ className: "small text-center text-truncate",
497
+ style: { width: "100%" },
498
+ children: u.file.name
499
+ }
500
+ ),
501
+ /* @__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)(
502
+ "div",
503
+ {
504
+ className: "progress-bar " + (u.status === "error" ? "bg-danger" : ""),
505
+ role: "progressbar",
506
+ style: { width: `${u.progress}%` },
507
+ "aria-valuenow": u.progress,
508
+ "aria-valuemin": 0,
509
+ "aria-valuemax": 100
510
+ }
511
+ ) }) }),
512
+ u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
513
+ "div",
514
+ {
515
+ className: "text-danger small mt-1 text-center",
516
+ style: { width: "100%" },
517
+ children: u.error ?? "Upload failed"
518
+ }
519
+ )
520
+ ]
521
+ },
522
+ u.id
523
+ ))
524
+ }
525
+ );
526
+ };
527
+
528
+ // src/components/DesktopFileIcon.tsx
529
+ var import_react4 = require("react");
530
+ var import_jsx_runtime3 = require("react/jsx-runtime");
531
+ var DesktopFileIcon = ({
532
+ name,
533
+ sizeBytes,
534
+ downloadUrl,
535
+ onOpen,
536
+ onDelete
537
+ }) => {
538
+ const [contextMenuPos, setContextMenuPos] = (0, import_react4.useState)(
539
+ null
540
+ );
541
+ const [isHovered, setIsHovered] = (0, import_react4.useState)(false);
542
+ const [isDeleting, setIsDeleting] = (0, import_react4.useState)(false);
543
+ const iconRef = (0, import_react4.useRef)(null);
544
+ const menuRef = (0, import_react4.useRef)(null);
545
+ const handleDoubleClick = () => {
546
+ if (isDeleting) return;
547
+ if (onOpen) {
548
+ onOpen();
549
+ return;
550
+ }
551
+ if (downloadUrl) {
552
+ const a = document.createElement("a");
553
+ a.href = downloadUrl;
554
+ a.download = name ?? "";
555
+ a.target = "_blank";
556
+ a.rel = "noopener noreferrer";
557
+ document.body.appendChild(a);
558
+ a.click();
559
+ document.body.removeChild(a);
560
+ }
561
+ };
562
+ const handleContextMenu = (e) => {
563
+ if (isDeleting) return;
564
+ e.preventDefault();
565
+ setContextMenuPos({ x: e.clientX, y: e.clientY });
566
+ };
567
+ const closeMenu = () => setContextMenuPos(null);
568
+ const handleDownload = () => {
569
+ closeMenu();
570
+ if (!downloadUrl || isDeleting) return;
571
+ const a = document.createElement("a");
572
+ a.href = downloadUrl;
573
+ a.download = name ?? "";
574
+ a.target = "_blank";
575
+ a.rel = "noopener noreferrer";
576
+ document.body.appendChild(a);
577
+ a.click();
578
+ document.body.removeChild(a);
579
+ };
580
+ const handleCopyUrl = async () => {
581
+ closeMenu();
582
+ if (!downloadUrl || isDeleting) return;
583
+ try {
584
+ await navigator.clipboard?.writeText(downloadUrl);
585
+ } catch (err) {
586
+ console.error("Failed to copy URL", err);
587
+ }
588
+ };
589
+ const handleDelete = async () => {
590
+ closeMenu();
591
+ if (!onDelete) return;
592
+ try {
593
+ setIsDeleting(true);
594
+ await Promise.resolve(onDelete());
595
+ } catch (err) {
596
+ console.error("Delete failed", err);
597
+ }
598
+ };
599
+ const formattedSize = typeof sizeBytes === "number" ? `${(sizeBytes / 1024).toFixed(1)} KB` : void 0;
600
+ (0, import_react4.useEffect)(() => {
601
+ if (!contextMenuPos) return;
602
+ const handleGlobalClick = (e) => {
603
+ const target = e.target;
604
+ if (menuRef.current && menuRef.current.contains(target)) {
605
+ return;
606
+ }
607
+ if (iconRef.current && iconRef.current.contains(target)) {
608
+ return;
609
+ }
610
+ closeMenu();
611
+ };
612
+ document.addEventListener("mousedown", handleGlobalClick);
613
+ return () => {
614
+ document.removeEventListener("mousedown", handleGlobalClick);
615
+ };
616
+ }, [contextMenuPos]);
617
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
618
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
619
+ "div",
620
+ {
621
+ ref: iconRef,
622
+ className: "d-flex flex-column align-items-center rounded-3 p-1 " + (isHovered || contextMenuPos ? "bg-light border-primary" : "border-transparent"),
623
+ style: {
624
+ width: 96,
625
+ cursor: isDeleting ? "default" : "pointer",
626
+ userSelect: "none",
627
+ transition: "background-color 0.1s ease, border-color 0.1s ease, opacity 0.1s ease",
628
+ opacity: isDeleting ? 0.6 : 1,
629
+ borderWidth: 1,
630
+ borderStyle: "solid"
631
+ },
632
+ onDoubleClick: handleDoubleClick,
633
+ onContextMenu: handleContextMenu,
634
+ title: name ?? void 0,
635
+ onClick: () => {
636
+ if (contextMenuPos) {
637
+ closeMenu();
638
+ }
639
+ },
640
+ onMouseEnter: () => setIsHovered(true),
641
+ onMouseLeave: () => setIsHovered(false),
642
+ children: [
643
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
644
+ "div",
645
+ {
646
+ className: "bg-white border rounded-3 d-flex align-items-center justify-content-center mb-1 shadow-sm position-relative",
647
+ style: {
648
+ width: 64,
649
+ height: 64
650
+ },
651
+ children: [
652
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("i", { className: "bi bi-file-earmark fs-2" }),
653
+ isDeleting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "position-absolute top-50 start-50 translate-middle", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "spinner-border spinner-border-sm text-danger" }) })
654
+ ]
655
+ }
656
+ ),
657
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
658
+ "div",
659
+ {
660
+ className: "small text-center text-truncate",
661
+ style: { width: "100%" },
662
+ children: name
663
+ }
664
+ ),
665
+ formattedSize && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("small", { className: "text-muted mt-1", children: formattedSize })
666
+ ]
667
+ }
668
+ ),
669
+ contextMenuPos && !isDeleting && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
670
+ "div",
671
+ {
672
+ ref: menuRef,
673
+ className: "position-fixed dropdown-menu show shadow-sm",
674
+ style: {
675
+ top: contextMenuPos.y,
676
+ left: contextMenuPos.x,
677
+ zIndex: 1050,
678
+ minWidth: 180
679
+ },
680
+ onClick: (e) => e.stopPropagation(),
681
+ children: [
682
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
683
+ "button",
684
+ {
685
+ type: "button",
686
+ className: "dropdown-item small",
687
+ onClick: handleDownload,
688
+ disabled: !downloadUrl,
689
+ children: "Download file"
690
+ }
691
+ ),
692
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
693
+ "button",
694
+ {
695
+ type: "button",
696
+ className: "dropdown-item small",
697
+ onClick: handleCopyUrl,
698
+ disabled: !downloadUrl,
699
+ children: "Copy download URL"
700
+ }
701
+ ),
702
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dropdown-divider" }),
703
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
704
+ "button",
705
+ {
706
+ type: "button",
707
+ className: "dropdown-item text-danger small",
708
+ onClick: handleDelete,
709
+ children: "Delete file"
710
+ }
711
+ )
712
+ ]
713
+ }
714
+ )
715
+ ] });
716
+ };
717
+
467
718
  // src/components/UploadContainer.tsx
468
719
  var import_jsx_runtime4 = require("react/jsx-runtime");
469
720
  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
721
  onFilesSelected,
475
722
  existingFiles = [],
723
+ existingFilesLoading = false,
476
724
  onExistingFileClick,
725
+ onDeleteFile,
477
726
  autoUpload = false,
478
727
  getPresignedUrl,
479
728
  onUploadComplete,
480
729
  onUploadError
481
730
  }) => {
482
731
  const [isDragging, setIsDragging] = (0, import_react5.useState)(false);
483
- const [fileNames, setFileNames] = (0, import_react5.useState)([]);
484
732
  const { uploads, startUploadsIfNeeded } = UseUploadManager({
485
733
  autoUpload,
486
734
  getPresignedUrl,
@@ -500,23 +748,16 @@ var UploadContainer = ({
500
748
  setIsDragging(false);
501
749
  const files = e.dataTransfer.files;
502
750
  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
751
  onFilesSelected?.(files);
512
752
  startUploadsIfNeeded(files);
513
753
  };
514
- const handleExistingFileClickInternal = (file) => {
754
+ const handleExistingFileOpen = (file) => {
515
755
  if (onExistingFileClick) {
516
756
  onExistingFileClick(file);
517
757
  return;
518
758
  }
519
759
  const a = document.createElement("a");
760
+ a.href = file.PublicUrl ?? "";
520
761
  a.download = file.Name ?? "";
521
762
  a.target = "_blank";
522
763
  a.rel = "noopener noreferrer";
@@ -524,48 +765,38 @@ var UploadContainer = ({
524
765
  a.click();
525
766
  document.body.removeChild(a);
526
767
  };
527
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
528
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h5", { className: "card-title mb-2", children: title }),
529
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "card-text text-muted", children: description }),
530
- existingFiles.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "mb-3", children: [
531
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h6", { className: "mb-2", children: "Existing files" }),
532
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: "list-group", children: existingFiles.map((file) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
533
- "li",
534
- {
535
- className: "list-group-item d-flex justify-content-between align-items-center",
536
- style: { cursor: "pointer" },
537
- onClick: () => handleExistingFileClickInternal(file),
538
- children: [
539
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: file.Name }),
540
- typeof file.FileSize === "number" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("small", { className: "text-muted", children: [
541
- (file.FileSize / 1024).toFixed(1),
542
- " KB"
543
- ] })
544
- ]
545
- },
546
- file.Id
547
- )) })
548
- ] }),
549
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
550
- UploadDropzone,
551
- {
552
- isDragging,
553
- onDragOver: handleDragOver,
554
- onDragLeave: handleDragLeave,
555
- onDrop: handleDrop
556
- }
557
- ),
558
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
559
- UploadFilePicker,
560
- {
561
- multiple,
562
- accept,
563
- fileNames,
564
- onFileChange: handleFileChange
565
- }
566
- ),
567
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(UploadProgressList, { uploads })
568
- ] });
768
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
769
+ UploadDropzone,
770
+ {
771
+ isDragging,
772
+ onDragOver: handleDragOver,
773
+ onDragLeave: handleDragLeave,
774
+ onDrop: handleDrop,
775
+ className: "w-100",
776
+ style: { minHeight: "260px", alignItems: "stretch" },
777
+ children: [
778
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "w-100 mb-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 }) }) }) }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
780
+ "div",
781
+ {
782
+ className: "w-100 d-flex flex-wrap gap-4 align-content-start",
783
+ style: { minHeight: "140px" },
784
+ 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)(
785
+ DesktopFileIcon,
786
+ {
787
+ name: file.Name,
788
+ sizeBytes: file.FileSize,
789
+ downloadUrl: file.PublicUrl,
790
+ onOpen: () => handleExistingFileOpen(file),
791
+ onDelete: () => onDeleteFile?.(file)
792
+ },
793
+ file.Id
794
+ ))
795
+ }
796
+ )
797
+ ]
798
+ }
799
+ );
569
800
  };
570
801
 
571
802
  // src/hooks/UseContainers.ts
@@ -579,7 +810,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
579
810
  setError(null);
580
811
  try {
581
812
  const sdkDb = new SparkStudioStorageSDK(
582
- "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod"
813
+ apiBaseUrl
583
814
  );
584
815
  const result = await sdkDb.container.ReadRootContainers();
585
816
  setContainers(result);
@@ -594,6 +825,7 @@ function UseContainers({ apiBaseUrl, parentId }) {
594
825
  }, [apiBaseUrl, parentId]);
595
826
  return {
596
827
  containers,
828
+ setContainers,
597
829
  loading,
598
830
  error,
599
831
  reload: load
@@ -604,28 +836,37 @@ function UseContainers({ apiBaseUrl, parentId }) {
604
836
  var import_jsx_runtime5 = require("react/jsx-runtime");
605
837
  var ContainerUploadPanel = ({
606
838
  containerApiBaseUrl,
607
- parentContainerId,
608
- title = "Upload files",
609
- description = "Drop files to upload. Existing files are listed below."
839
+ storageApiBaseUrl,
840
+ parentContainerId
610
841
  }) => {
611
- const { containers, loading, error, reload } = UseContainers({
842
+ const { containers, setContainers, reload, loading } = UseContainers({
612
843
  apiBaseUrl: containerApiBaseUrl,
613
844
  parentId: parentContainerId
614
845
  });
615
846
  const getPresignedUrl = async (file) => {
616
- const sdkDb = new SparkStudioStorageSDK(
617
- "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod"
618
- );
619
- const sdkS3 = new SparkStudioStorageSDK(
620
- "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
621
- );
847
+ const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
848
+ const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
849
+ const contentType = file.type || "application/octet-stream";
622
850
  const containerDTO = await sdkDb.container.CreateFileContainer(
623
851
  file.name,
624
852
  file.size,
625
- encodeURIComponent(file.type)
853
+ encodeURIComponent(contentType)
626
854
  );
627
- const resultS3 = await sdkS3.s3.GetPreSignedUrl(containerDTO);
628
- return resultS3;
855
+ async function getPresignedUrlWithRetry(container, attempts = 3) {
856
+ let lastError;
857
+ for (let i = 1; i <= attempts; i++) {
858
+ try {
859
+ return await sdkS3.s3.GetPreSignedUrl(container);
860
+ } catch (err) {
861
+ lastError = err;
862
+ if (i < attempts) {
863
+ await new Promise((r) => setTimeout(r, 500 * i));
864
+ }
865
+ }
866
+ }
867
+ throw lastError instanceof Error ? lastError : new Error("Failed to fetch presigned URL");
868
+ }
869
+ return getPresignedUrlWithRetry(containerDTO);
629
870
  };
630
871
  const handleUploadComplete = async (file, s3Url) => {
631
872
  console.log("Upload complete:", file.name, s3Url);
@@ -635,9 +876,8 @@ var ContainerUploadPanel = ({
635
876
  console.error("Upload failed:", file.name, err);
636
877
  };
637
878
  const handleExistingFileClick = (file) => {
638
- const downloadUrl = `${containerApiBaseUrl}/api/Container/Download/${file.Id}`;
639
879
  const a = document.createElement("a");
640
- a.href = downloadUrl;
880
+ a.href = file.PublicUrl ?? "";
641
881
  a.download = file.Name ?? "";
642
882
  a.target = "_blank";
643
883
  a.rel = "noopener noreferrer";
@@ -645,26 +885,26 @@ var ContainerUploadPanel = ({
645
885
  a.click();
646
886
  document.body.removeChild(a);
647
887
  };
648
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
649
- loading && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { children: "Loading existing files\u2026" }),
650
- error && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("p", { className: "text-danger", children: [
651
- "Failed to load containers: ",
652
- error.message
653
- ] }),
654
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
655
- UploadContainer,
656
- {
657
- title,
658
- description,
659
- existingFiles: containers,
660
- onExistingFileClick: handleExistingFileClick,
661
- autoUpload: true,
662
- getPresignedUrl,
663
- onUploadComplete: handleUploadComplete,
664
- onUploadError: handleUploadError
665
- }
666
- )
667
- ] });
888
+ const handleDeleteFile = async (file) => {
889
+ const sdkDb = new SparkStudioStorageSDK(containerApiBaseUrl);
890
+ const sdkS3 = new SparkStudioStorageSDK(storageApiBaseUrl);
891
+ await sdkDb.container.DeleteContainer(file.Id);
892
+ await sdkS3.s3.DeleteS3(file);
893
+ setContainers((prev) => prev.filter((c) => c.Id !== file.Id));
894
+ };
895
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
896
+ UploadContainer,
897
+ {
898
+ existingFiles: containers,
899
+ existingFilesLoading: loading,
900
+ onExistingFileClick: handleExistingFileClick,
901
+ autoUpload: true,
902
+ getPresignedUrl,
903
+ onUploadComplete: handleUploadComplete,
904
+ onUploadError: handleUploadError,
905
+ onDeleteFile: handleDeleteFile
906
+ }
907
+ );
668
908
  };
669
909
 
670
910
  // src/views/HomeView.tsx
@@ -688,8 +928,8 @@ function HomeContent() {
688
928
  user && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
689
929
  ContainerUploadPanel,
690
930
  {
691
- containerApiBaseUrl: "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
692
- storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
931
+ containerApiBaseUrl: "https://localhost:5001",
932
+ storageApiBaseUrl: "https://localhost:5001"
693
933
  }
694
934
  )
695
935
  ] });
@@ -701,13 +941,13 @@ function HomeContent() {
701
941
  ContainerDTO,
702
942
  ContainerType,
703
943
  ContainerUploadPanel,
944
+ DesktopFileIcon,
704
945
  Home,
705
946
  HomeView,
706
947
  S3,
707
948
  SparkStudioStorageSDK,
708
949
  UploadContainer,
709
950
  UploadDropzone,
710
- UploadFilePicker,
711
951
  UploadFileToS3,
712
952
  UploadProgressList,
713
953
  UseContainers,