@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 +425 -185
- package/dist/index.d.cts +34 -23
- package/dist/index.d.ts +34 -23
- package/dist/index.js +431 -191
- 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,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
|
|
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)(
|
|
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
|
-
] });
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
description = "Drop files to upload. Existing files are listed below."
|
|
839
|
+
storageApiBaseUrl,
|
|
840
|
+
parentContainerId
|
|
610
841
|
}) => {
|
|
611
|
-
const { containers,
|
|
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
|
-
|
|
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(
|
|
853
|
+
encodeURIComponent(contentType)
|
|
626
854
|
);
|
|
627
|
-
|
|
628
|
-
|
|
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 =
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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://
|
|
692
|
-
storageApiBaseUrl: "https://
|
|
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,
|