@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 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,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 handleExistingFileClickInternal = (file) => {
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)(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
- ] });
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
- "https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod"
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
- parentContainerId,
608
- title = "Upload files",
609
- description = "Drop files to upload. Existing files are listed below."
815
+ storageApiBaseUrl,
816
+ parentContainerId
610
817
  }) => {
611
- const { containers, loading, error, reload } = UseContainers({
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
- "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
- );
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
- const resultS3 = await sdkS3.s3.GetPreSignedUrl(containerDTO);
628
- return resultS3;
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 = downloadUrl;
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.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
- ] });
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://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod",
692
- storageApiBaseUrl: "https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod"
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,