@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 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 UploadContainer = ({
247
- title = "Upload files",
248
- description = "Drag and drop files here, or click the button to browse.",
249
- multiple = true,
250
- accept,
251
- onFilesSelected,
252
- autoUpload = false,
253
- getPresignedUrl,
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 [isDragging, setIsDragging] = (0, import_react.useState)(false);
258
- const [fileNames, setFileNames] = (0, import_react.useState)([]);
259
- const [uploads, setUploads] = (0, import_react.useState)([]);
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
- setIsDragging(true);
275
+ if (onDragOver) onDragOver(e);
315
276
  };
316
277
  const handleDragLeave = (e) => {
317
278
  e.preventDefault();
318
- setIsDragging(false);
279
+ if (onDragLeave) onDragLeave(e);
319
280
  };
320
281
  const handleDrop = (e) => {
321
282
  e.preventDefault();
322
- setIsDragging(false);
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)("div", { className: "container my-3", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "card shadow-sm", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "card-body", children: [
337
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h5", { className: "card-title mb-2", children: title }),
338
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "card-text text-muted", children: description }),
339
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
340
- "div",
341
- {
342
- 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"),
343
- style: { cursor: "pointer", minHeight: "140px" },
344
- onDragOver: handleDragOver,
345
- onDragLeave: handleDragLeave,
346
- onDrop: handleDrop,
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
- async function uploadFileToS3(file, presignedUrl, onProgress) {
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 with status ${xhr.status}: ${xhr.statusText}`)
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/views/HomeView.tsx
429
- var import_authentication_ui = require("@sparkstudio/authentication-ui");
430
- var import_jsx_runtime2 = require("react/jsx-runtime");
431
- function HomeView() {
432
- function handleOnLoginSuccess(user) {
433
- alert(user?.Id);
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("https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod");
437
- const sdkS3 = new SparkStudioStorageSDK("https://iq0gmcn0pd.execute-api.us-east-2.amazonaws.com/Prod");
438
- const containerDTO = await sdkDb.container.CreateFileContainer(file.name, file.size, encodeURIComponent(file.type));
439
- const resultS3 = await sdkS3.s3.GetPreSignedUrl(containerDTO);
440
- return resultS3;
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
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
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
- onLoginSuccess: handleOnLoginSuccess,
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
  });