@sparkstudio/storage-ui 1.0.7 → 1.0.9
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 +148 -19
- package/dist/index.d.cts +34 -17
- package/dist/index.d.ts +34 -17
- package/dist/index.js +147 -18
- package/package.json +4 -2
package/dist/index.cjs
CHANGED
|
@@ -20,7 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
|
|
23
|
+
AWSPresignedUrlDTO: () => AWSPresignedUrlDTO,
|
|
24
24
|
Container: () => Container,
|
|
25
25
|
ContainerDTO: () => ContainerDTO,
|
|
26
26
|
ContainerType: () => ContainerType,
|
|
@@ -123,8 +123,8 @@ var Container = class {
|
|
|
123
123
|
if (!response.ok) throw new Error(await response.text());
|
|
124
124
|
return await response.json();
|
|
125
125
|
}
|
|
126
|
-
async
|
|
127
|
-
const url = `${this.baseUrl}/api/Container/
|
|
126
|
+
async GetPreSignedUrl(fileName, size, contentType) {
|
|
127
|
+
const url = `${this.baseUrl}/api/Container/GetPreSignedUrl/` + fileName + `/` + size + `/` + contentType;
|
|
128
128
|
const token = localStorage.getItem("auth_token");
|
|
129
129
|
const requestOptions = {
|
|
130
130
|
method: "GET",
|
|
@@ -157,19 +157,15 @@ var SparkStudioStorageSDK = class {
|
|
|
157
157
|
}
|
|
158
158
|
};
|
|
159
159
|
|
|
160
|
-
// src/api/DTOs/
|
|
161
|
-
var
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
BucketName;
|
|
166
|
-
Region;
|
|
160
|
+
// src/api/DTOs/AWSPresignedUrlDTO.ts
|
|
161
|
+
var AWSPresignedUrlDTO = class {
|
|
162
|
+
PresignedUrl;
|
|
163
|
+
PublicUrl;
|
|
164
|
+
Key;
|
|
167
165
|
constructor(init) {
|
|
168
|
-
this.
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
171
|
-
this.BucketName = init.BucketName;
|
|
172
|
-
this.Region = init.Region;
|
|
166
|
+
this.PresignedUrl = init.PresignedUrl;
|
|
167
|
+
this.PublicUrl = init.PublicUrl;
|
|
168
|
+
this.Key = init.Key;
|
|
173
169
|
}
|
|
174
170
|
};
|
|
175
171
|
|
|
@@ -178,6 +174,7 @@ var ContainerDTO = class {
|
|
|
178
174
|
Id;
|
|
179
175
|
ContainerType;
|
|
180
176
|
Name;
|
|
177
|
+
ContentType;
|
|
181
178
|
CreatedDate;
|
|
182
179
|
FileSize;
|
|
183
180
|
UserId;
|
|
@@ -186,6 +183,7 @@ var ContainerDTO = class {
|
|
|
186
183
|
this.Id = init.Id;
|
|
187
184
|
this.ContainerType = init.ContainerType;
|
|
188
185
|
this.Name = init.Name;
|
|
186
|
+
this.ContentType = init.ContentType;
|
|
189
187
|
this.CreatedDate = init.CreatedDate;
|
|
190
188
|
this.FileSize = init.FileSize;
|
|
191
189
|
this.UserId = init.UserId;
|
|
@@ -209,10 +207,67 @@ var UploadContainer = ({
|
|
|
209
207
|
description = "Drag and drop files here, or click the button to browse.",
|
|
210
208
|
multiple = true,
|
|
211
209
|
accept,
|
|
212
|
-
onFilesSelected
|
|
210
|
+
onFilesSelected,
|
|
211
|
+
autoUpload = false,
|
|
212
|
+
getPresignedUrl,
|
|
213
|
+
onUploadComplete,
|
|
214
|
+
onUploadError
|
|
213
215
|
}) => {
|
|
214
216
|
const [isDragging, setIsDragging] = (0, import_react.useState)(false);
|
|
215
217
|
const [fileNames, setFileNames] = (0, import_react.useState)([]);
|
|
218
|
+
const [uploads, setUploads] = (0, import_react.useState)([]);
|
|
219
|
+
const startUploadsIfNeeded = (files) => {
|
|
220
|
+
if (!autoUpload || !getPresignedUrl) return;
|
|
221
|
+
const newUploads = Array.from(files).map((file) => ({
|
|
222
|
+
id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
|
|
223
|
+
file,
|
|
224
|
+
progress: 0,
|
|
225
|
+
status: "pending"
|
|
226
|
+
}));
|
|
227
|
+
setUploads((prev) => [...prev, ...newUploads]);
|
|
228
|
+
newUploads.forEach((upload) => {
|
|
229
|
+
uploadFile(upload);
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
const uploadFile = async (upload) => {
|
|
233
|
+
setUploads(
|
|
234
|
+
(prev) => prev.map(
|
|
235
|
+
(u) => u.id === upload.id ? { ...u, status: "uploading", progress: 0 } : u
|
|
236
|
+
)
|
|
237
|
+
);
|
|
238
|
+
try {
|
|
239
|
+
if (!getPresignedUrl) {
|
|
240
|
+
throw new Error("getPresignedUrl is not provided.");
|
|
241
|
+
}
|
|
242
|
+
const presignedUrl = await getPresignedUrl(upload.file);
|
|
243
|
+
const url = presignedUrl?.PresignedUrl ?? "";
|
|
244
|
+
await uploadFileToS3(upload.file, url, (progress) => {
|
|
245
|
+
setUploads(
|
|
246
|
+
(prev) => prev.map(
|
|
247
|
+
(u) => u.id === upload.id ? { ...u, progress } : u
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
const fileUrl = url.split("?")[0];
|
|
252
|
+
setUploads(
|
|
253
|
+
(prev) => prev.map(
|
|
254
|
+
(u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl, publicUrl: presignedUrl.PublicUrl } : u
|
|
255
|
+
)
|
|
256
|
+
);
|
|
257
|
+
onUploadComplete?.(upload.file, fileUrl);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
let message = "Upload failed";
|
|
260
|
+
if (err instanceof Error) {
|
|
261
|
+
message = err.message;
|
|
262
|
+
}
|
|
263
|
+
setUploads(
|
|
264
|
+
(prev) => prev.map(
|
|
265
|
+
(u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
|
|
266
|
+
)
|
|
267
|
+
);
|
|
268
|
+
onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
|
|
269
|
+
}
|
|
270
|
+
};
|
|
216
271
|
const handleDragOver = (e) => {
|
|
217
272
|
e.preventDefault();
|
|
218
273
|
setIsDragging(true);
|
|
@@ -228,12 +283,14 @@ var UploadContainer = ({
|
|
|
228
283
|
if (!files || files.length === 0) return;
|
|
229
284
|
setFileNames(Array.from(files).map((f) => f.name));
|
|
230
285
|
onFilesSelected?.(files);
|
|
286
|
+
startUploadsIfNeeded(files);
|
|
231
287
|
};
|
|
232
288
|
const handleFileChange = (e) => {
|
|
233
289
|
const files = e.target.files;
|
|
234
290
|
if (!files || files.length === 0) return;
|
|
235
291
|
setFileNames(Array.from(files).map((f) => f.name));
|
|
236
292
|
onFilesSelected?.(files);
|
|
293
|
+
startUploadsIfNeeded(files);
|
|
237
294
|
};
|
|
238
295
|
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: [
|
|
239
296
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h5", { className: "card-title mb-2", children: title }),
|
|
@@ -272,9 +329,60 @@ var UploadContainer = ({
|
|
|
272
329
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "small text-muted", children: "Selected:" }),
|
|
273
330
|
/* @__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)) })
|
|
274
331
|
] })
|
|
275
|
-
] })
|
|
332
|
+
] }),
|
|
333
|
+
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: [
|
|
334
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "d-flex justify-content-between small mb-1", children: [
|
|
335
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
336
|
+
u.file.name,
|
|
337
|
+
" - ",
|
|
338
|
+
u?.publicUrl ?? ""
|
|
339
|
+
] }),
|
|
340
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
|
|
341
|
+
] }),
|
|
342
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
343
|
+
"div",
|
|
344
|
+
{
|
|
345
|
+
className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
|
|
346
|
+
role: "progressbar",
|
|
347
|
+
style: { width: `${u.progress}%` },
|
|
348
|
+
"aria-valuenow": u.progress,
|
|
349
|
+
"aria-valuemin": 0,
|
|
350
|
+
"aria-valuemax": 100
|
|
351
|
+
}
|
|
352
|
+
) }),
|
|
353
|
+
u.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
|
|
354
|
+
] }, u.id)) })
|
|
276
355
|
] }) }) });
|
|
277
356
|
};
|
|
357
|
+
async function uploadFileToS3(file, presignedUrl, onProgress) {
|
|
358
|
+
return new Promise((resolve, reject) => {
|
|
359
|
+
const xhr = new XMLHttpRequest();
|
|
360
|
+
xhr.open("PUT", presignedUrl);
|
|
361
|
+
xhr.upload.onprogress = (event) => {
|
|
362
|
+
if (!event.lengthComputable) return;
|
|
363
|
+
const percent = Math.round(event.loaded / event.total * 100);
|
|
364
|
+
onProgress(percent);
|
|
365
|
+
};
|
|
366
|
+
xhr.onload = () => {
|
|
367
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
368
|
+
onProgress(100);
|
|
369
|
+
resolve();
|
|
370
|
+
} else {
|
|
371
|
+
reject(
|
|
372
|
+
new Error(`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
xhr.onerror = () => {
|
|
377
|
+
reject(new Error("Network error while uploading to S3"));
|
|
378
|
+
};
|
|
379
|
+
xhr.setRequestHeader(
|
|
380
|
+
"Content-Type",
|
|
381
|
+
file.type || "application/octet-stream"
|
|
382
|
+
);
|
|
383
|
+
xhr.send(file);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
278
386
|
|
|
279
387
|
// src/views/HomeView.tsx
|
|
280
388
|
var import_authentication_ui = require("@sparkstudio/authentication-ui");
|
|
@@ -283,6 +391,11 @@ function HomeView() {
|
|
|
283
391
|
function handleOnLoginSuccess(user) {
|
|
284
392
|
alert(user?.Id);
|
|
285
393
|
}
|
|
394
|
+
const getPresignedUrl = async (file) => {
|
|
395
|
+
const sdk = new SparkStudioStorageSDK("https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod");
|
|
396
|
+
const result = await sdk.container.GetPreSignedUrl(file.name, file.size, encodeURIComponent(file.type));
|
|
397
|
+
return result;
|
|
398
|
+
};
|
|
286
399
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
287
400
|
import_authentication_ui.AuthenticatorProvider,
|
|
288
401
|
{
|
|
@@ -292,14 +405,30 @@ function HomeView() {
|
|
|
292
405
|
onLoginSuccess: handleOnLoginSuccess,
|
|
293
406
|
children: [
|
|
294
407
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_authentication_ui.UserInfoCard, {}),
|
|
295
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
408
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
409
|
+
UploadContainer,
|
|
410
|
+
{
|
|
411
|
+
title: "Upload your files to S3",
|
|
412
|
+
description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
|
|
413
|
+
multiple: true,
|
|
414
|
+
accept: "*/*",
|
|
415
|
+
autoUpload: true,
|
|
416
|
+
getPresignedUrl,
|
|
417
|
+
onUploadComplete: (file, s3Url) => {
|
|
418
|
+
console.log("Uploaded", file.name, "to", s3Url);
|
|
419
|
+
},
|
|
420
|
+
onUploadError: (file, error) => {
|
|
421
|
+
console.error("Failed to upload", file.name, error);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
)
|
|
296
425
|
]
|
|
297
426
|
}
|
|
298
427
|
);
|
|
299
428
|
}
|
|
300
429
|
// Annotate the CommonJS export names for ESM import in node:
|
|
301
430
|
0 && (module.exports = {
|
|
302
|
-
|
|
431
|
+
AWSPresignedUrlDTO,
|
|
303
432
|
Container,
|
|
304
433
|
ContainerDTO,
|
|
305
434
|
ContainerType,
|
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,7 @@ interface IContainerDTO {
|
|
|
14
14
|
Id: string;
|
|
15
15
|
ContainerType: ContainerType;
|
|
16
16
|
Name?: string;
|
|
17
|
+
ContentType?: string;
|
|
17
18
|
CreatedDate: string;
|
|
18
19
|
FileSize: number;
|
|
19
20
|
UserId: string;
|
|
@@ -24,6 +25,7 @@ declare class ContainerDTO implements IContainerDTO {
|
|
|
24
25
|
Id: string;
|
|
25
26
|
ContainerType: ContainerType;
|
|
26
27
|
Name?: string;
|
|
28
|
+
ContentType?: string;
|
|
27
29
|
CreatedDate: string;
|
|
28
30
|
FileSize: number;
|
|
29
31
|
UserId: string;
|
|
@@ -32,23 +34,19 @@ declare class ContainerDTO implements IContainerDTO {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
* Represents an Auto-generated model for
|
|
37
|
+
* Represents an Auto-generated model for AWSPresignedUrlDTO.
|
|
36
38
|
*/
|
|
37
|
-
interface
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
BucketName?: string;
|
|
42
|
-
Region?: string;
|
|
39
|
+
interface IAWSPresignedUrlDTO {
|
|
40
|
+
PresignedUrl?: string;
|
|
41
|
+
PublicUrl?: string;
|
|
42
|
+
Key?: string;
|
|
43
43
|
}
|
|
44
|
-
type
|
|
45
|
-
declare class
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Region?: string;
|
|
51
|
-
constructor(init: AWSCredentialsDTOInit);
|
|
44
|
+
type AWSPresignedUrlDTOInit = Partial<IAWSPresignedUrlDTO>;
|
|
45
|
+
declare class AWSPresignedUrlDTO implements IAWSPresignedUrlDTO {
|
|
46
|
+
PresignedUrl?: string;
|
|
47
|
+
PublicUrl?: string;
|
|
48
|
+
Key?: string;
|
|
49
|
+
constructor(init: AWSPresignedUrlDTOInit);
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
/**
|
|
@@ -63,7 +61,7 @@ declare class Container {
|
|
|
63
61
|
Create(containerDTO: ContainerDTO): Promise<ContainerDTO>;
|
|
64
62
|
Update(containerDTO: ContainerDTO): Promise<ContainerDTO>;
|
|
65
63
|
Delete(id: string): Promise<ContainerDTO>;
|
|
66
|
-
|
|
64
|
+
GetPreSignedUrl(fileName: string, size: number, contentType: string): Promise<AWSPresignedUrlDTO>;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
/**
|
|
@@ -89,9 +87,28 @@ interface UploadContainerProps {
|
|
|
89
87
|
multiple?: boolean;
|
|
90
88
|
accept?: string;
|
|
91
89
|
onFilesSelected?: (files: FileList) => void;
|
|
90
|
+
/**
|
|
91
|
+
* When true, the component will immediately start uploading to S3
|
|
92
|
+
* using getPresignedUrl for each file.
|
|
93
|
+
*/
|
|
94
|
+
autoUpload?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Your function that returns a pre-signed URL for a given file.
|
|
97
|
+
* Typically calls your backend: /api/uploads/presign
|
|
98
|
+
*/
|
|
99
|
+
getPresignedUrl?: (file: File) => Promise<AWSPresignedUrlDTO>;
|
|
100
|
+
/**
|
|
101
|
+
* Called when a file has successfully finished uploading.
|
|
102
|
+
* s3Url is usually the final S3 object URL or key (depends on your backend).
|
|
103
|
+
*/
|
|
104
|
+
onUploadComplete?: (file: File, s3Url: string) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Called when a file upload fails.
|
|
107
|
+
*/
|
|
108
|
+
onUploadError?: (file: File, error: Error) => void;
|
|
92
109
|
}
|
|
93
110
|
declare const UploadContainer: React.FC<UploadContainerProps>;
|
|
94
111
|
|
|
95
112
|
declare function HomeView(): react_jsx_runtime.JSX.Element;
|
|
96
113
|
|
|
97
|
-
export {
|
|
114
|
+
export { AWSPresignedUrlDTO, Container, ContainerDTO, ContainerType, Home, HomeView, type IAWSPresignedUrlDTO, type IContainerDTO, SparkStudioStorageSDK, UploadContainer, type UploadContainerProps };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ interface IContainerDTO {
|
|
|
14
14
|
Id: string;
|
|
15
15
|
ContainerType: ContainerType;
|
|
16
16
|
Name?: string;
|
|
17
|
+
ContentType?: string;
|
|
17
18
|
CreatedDate: string;
|
|
18
19
|
FileSize: number;
|
|
19
20
|
UserId: string;
|
|
@@ -24,6 +25,7 @@ declare class ContainerDTO implements IContainerDTO {
|
|
|
24
25
|
Id: string;
|
|
25
26
|
ContainerType: ContainerType;
|
|
26
27
|
Name?: string;
|
|
28
|
+
ContentType?: string;
|
|
27
29
|
CreatedDate: string;
|
|
28
30
|
FileSize: number;
|
|
29
31
|
UserId: string;
|
|
@@ -32,23 +34,19 @@ declare class ContainerDTO implements IContainerDTO {
|
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
|
-
* Represents an Auto-generated model for
|
|
37
|
+
* Represents an Auto-generated model for AWSPresignedUrlDTO.
|
|
36
38
|
*/
|
|
37
|
-
interface
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
BucketName?: string;
|
|
42
|
-
Region?: string;
|
|
39
|
+
interface IAWSPresignedUrlDTO {
|
|
40
|
+
PresignedUrl?: string;
|
|
41
|
+
PublicUrl?: string;
|
|
42
|
+
Key?: string;
|
|
43
43
|
}
|
|
44
|
-
type
|
|
45
|
-
declare class
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Region?: string;
|
|
51
|
-
constructor(init: AWSCredentialsDTOInit);
|
|
44
|
+
type AWSPresignedUrlDTOInit = Partial<IAWSPresignedUrlDTO>;
|
|
45
|
+
declare class AWSPresignedUrlDTO implements IAWSPresignedUrlDTO {
|
|
46
|
+
PresignedUrl?: string;
|
|
47
|
+
PublicUrl?: string;
|
|
48
|
+
Key?: string;
|
|
49
|
+
constructor(init: AWSPresignedUrlDTOInit);
|
|
52
50
|
}
|
|
53
51
|
|
|
54
52
|
/**
|
|
@@ -63,7 +61,7 @@ declare class Container {
|
|
|
63
61
|
Create(containerDTO: ContainerDTO): Promise<ContainerDTO>;
|
|
64
62
|
Update(containerDTO: ContainerDTO): Promise<ContainerDTO>;
|
|
65
63
|
Delete(id: string): Promise<ContainerDTO>;
|
|
66
|
-
|
|
64
|
+
GetPreSignedUrl(fileName: string, size: number, contentType: string): Promise<AWSPresignedUrlDTO>;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
/**
|
|
@@ -89,9 +87,28 @@ interface UploadContainerProps {
|
|
|
89
87
|
multiple?: boolean;
|
|
90
88
|
accept?: string;
|
|
91
89
|
onFilesSelected?: (files: FileList) => void;
|
|
90
|
+
/**
|
|
91
|
+
* When true, the component will immediately start uploading to S3
|
|
92
|
+
* using getPresignedUrl for each file.
|
|
93
|
+
*/
|
|
94
|
+
autoUpload?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Your function that returns a pre-signed URL for a given file.
|
|
97
|
+
* Typically calls your backend: /api/uploads/presign
|
|
98
|
+
*/
|
|
99
|
+
getPresignedUrl?: (file: File) => Promise<AWSPresignedUrlDTO>;
|
|
100
|
+
/**
|
|
101
|
+
* Called when a file has successfully finished uploading.
|
|
102
|
+
* s3Url is usually the final S3 object URL or key (depends on your backend).
|
|
103
|
+
*/
|
|
104
|
+
onUploadComplete?: (file: File, s3Url: string) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Called when a file upload fails.
|
|
107
|
+
*/
|
|
108
|
+
onUploadError?: (file: File, error: Error) => void;
|
|
92
109
|
}
|
|
93
110
|
declare const UploadContainer: React.FC<UploadContainerProps>;
|
|
94
111
|
|
|
95
112
|
declare function HomeView(): react_jsx_runtime.JSX.Element;
|
|
96
113
|
|
|
97
|
-
export {
|
|
114
|
+
export { AWSPresignedUrlDTO, Container, ContainerDTO, ContainerType, Home, HomeView, type IAWSPresignedUrlDTO, type IContainerDTO, SparkStudioStorageSDK, UploadContainer, type UploadContainerProps };
|
package/dist/index.js
CHANGED
|
@@ -90,8 +90,8 @@ var Container = class {
|
|
|
90
90
|
if (!response.ok) throw new Error(await response.text());
|
|
91
91
|
return await response.json();
|
|
92
92
|
}
|
|
93
|
-
async
|
|
94
|
-
const url = `${this.baseUrl}/api/Container/
|
|
93
|
+
async GetPreSignedUrl(fileName, size, contentType) {
|
|
94
|
+
const url = `${this.baseUrl}/api/Container/GetPreSignedUrl/` + fileName + `/` + size + `/` + contentType;
|
|
95
95
|
const token = localStorage.getItem("auth_token");
|
|
96
96
|
const requestOptions = {
|
|
97
97
|
method: "GET",
|
|
@@ -124,19 +124,15 @@ var SparkStudioStorageSDK = class {
|
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
-
// src/api/DTOs/
|
|
128
|
-
var
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
BucketName;
|
|
133
|
-
Region;
|
|
127
|
+
// src/api/DTOs/AWSPresignedUrlDTO.ts
|
|
128
|
+
var AWSPresignedUrlDTO = class {
|
|
129
|
+
PresignedUrl;
|
|
130
|
+
PublicUrl;
|
|
131
|
+
Key;
|
|
134
132
|
constructor(init) {
|
|
135
|
-
this.
|
|
136
|
-
this.
|
|
137
|
-
this.
|
|
138
|
-
this.BucketName = init.BucketName;
|
|
139
|
-
this.Region = init.Region;
|
|
133
|
+
this.PresignedUrl = init.PresignedUrl;
|
|
134
|
+
this.PublicUrl = init.PublicUrl;
|
|
135
|
+
this.Key = init.Key;
|
|
140
136
|
}
|
|
141
137
|
};
|
|
142
138
|
|
|
@@ -145,6 +141,7 @@ var ContainerDTO = class {
|
|
|
145
141
|
Id;
|
|
146
142
|
ContainerType;
|
|
147
143
|
Name;
|
|
144
|
+
ContentType;
|
|
148
145
|
CreatedDate;
|
|
149
146
|
FileSize;
|
|
150
147
|
UserId;
|
|
@@ -153,6 +150,7 @@ var ContainerDTO = class {
|
|
|
153
150
|
this.Id = init.Id;
|
|
154
151
|
this.ContainerType = init.ContainerType;
|
|
155
152
|
this.Name = init.Name;
|
|
153
|
+
this.ContentType = init.ContentType;
|
|
156
154
|
this.CreatedDate = init.CreatedDate;
|
|
157
155
|
this.FileSize = init.FileSize;
|
|
158
156
|
this.UserId = init.UserId;
|
|
@@ -176,10 +174,67 @@ var UploadContainer = ({
|
|
|
176
174
|
description = "Drag and drop files here, or click the button to browse.",
|
|
177
175
|
multiple = true,
|
|
178
176
|
accept,
|
|
179
|
-
onFilesSelected
|
|
177
|
+
onFilesSelected,
|
|
178
|
+
autoUpload = false,
|
|
179
|
+
getPresignedUrl,
|
|
180
|
+
onUploadComplete,
|
|
181
|
+
onUploadError
|
|
180
182
|
}) => {
|
|
181
183
|
const [isDragging, setIsDragging] = useState(false);
|
|
182
184
|
const [fileNames, setFileNames] = useState([]);
|
|
185
|
+
const [uploads, setUploads] = useState([]);
|
|
186
|
+
const startUploadsIfNeeded = (files) => {
|
|
187
|
+
if (!autoUpload || !getPresignedUrl) return;
|
|
188
|
+
const newUploads = Array.from(files).map((file) => ({
|
|
189
|
+
id: `${file.name}-${file.size}-${file.lastModified}-${Math.random()}`,
|
|
190
|
+
file,
|
|
191
|
+
progress: 0,
|
|
192
|
+
status: "pending"
|
|
193
|
+
}));
|
|
194
|
+
setUploads((prev) => [...prev, ...newUploads]);
|
|
195
|
+
newUploads.forEach((upload) => {
|
|
196
|
+
uploadFile(upload);
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
const uploadFile = async (upload) => {
|
|
200
|
+
setUploads(
|
|
201
|
+
(prev) => prev.map(
|
|
202
|
+
(u) => u.id === upload.id ? { ...u, status: "uploading", progress: 0 } : u
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
try {
|
|
206
|
+
if (!getPresignedUrl) {
|
|
207
|
+
throw new Error("getPresignedUrl is not provided.");
|
|
208
|
+
}
|
|
209
|
+
const presignedUrl = await getPresignedUrl(upload.file);
|
|
210
|
+
const url = presignedUrl?.PresignedUrl ?? "";
|
|
211
|
+
await uploadFileToS3(upload.file, url, (progress) => {
|
|
212
|
+
setUploads(
|
|
213
|
+
(prev) => prev.map(
|
|
214
|
+
(u) => u.id === upload.id ? { ...u, progress } : u
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
const fileUrl = url.split("?")[0];
|
|
219
|
+
setUploads(
|
|
220
|
+
(prev) => prev.map(
|
|
221
|
+
(u) => u.id === upload.id ? { ...u, status: "success", progress: 100, s3Url: fileUrl, publicUrl: presignedUrl.PublicUrl } : u
|
|
222
|
+
)
|
|
223
|
+
);
|
|
224
|
+
onUploadComplete?.(upload.file, fileUrl);
|
|
225
|
+
} catch (err) {
|
|
226
|
+
let message = "Upload failed";
|
|
227
|
+
if (err instanceof Error) {
|
|
228
|
+
message = err.message;
|
|
229
|
+
}
|
|
230
|
+
setUploads(
|
|
231
|
+
(prev) => prev.map(
|
|
232
|
+
(u) => u.id === upload.id ? { ...u, status: "error", error: message } : u
|
|
233
|
+
)
|
|
234
|
+
);
|
|
235
|
+
onUploadError?.(upload.file, err instanceof Error ? err : new Error(message));
|
|
236
|
+
}
|
|
237
|
+
};
|
|
183
238
|
const handleDragOver = (e) => {
|
|
184
239
|
e.preventDefault();
|
|
185
240
|
setIsDragging(true);
|
|
@@ -195,12 +250,14 @@ var UploadContainer = ({
|
|
|
195
250
|
if (!files || files.length === 0) return;
|
|
196
251
|
setFileNames(Array.from(files).map((f) => f.name));
|
|
197
252
|
onFilesSelected?.(files);
|
|
253
|
+
startUploadsIfNeeded(files);
|
|
198
254
|
};
|
|
199
255
|
const handleFileChange = (e) => {
|
|
200
256
|
const files = e.target.files;
|
|
201
257
|
if (!files || files.length === 0) return;
|
|
202
258
|
setFileNames(Array.from(files).map((f) => f.name));
|
|
203
259
|
onFilesSelected?.(files);
|
|
260
|
+
startUploadsIfNeeded(files);
|
|
204
261
|
};
|
|
205
262
|
return /* @__PURE__ */ jsx("div", { className: "container my-3", children: /* @__PURE__ */ jsx("div", { className: "card shadow-sm", children: /* @__PURE__ */ jsxs("div", { className: "card-body", children: [
|
|
206
263
|
/* @__PURE__ */ jsx("h5", { className: "card-title mb-2", children: title }),
|
|
@@ -239,9 +296,60 @@ var UploadContainer = ({
|
|
|
239
296
|
/* @__PURE__ */ jsx("div", { className: "small text-muted", children: "Selected:" }),
|
|
240
297
|
/* @__PURE__ */ jsx("ul", { className: "mb-0 small", children: fileNames.map((name) => /* @__PURE__ */ jsx("li", { children: name }, name)) })
|
|
241
298
|
] })
|
|
242
|
-
] })
|
|
299
|
+
] }),
|
|
300
|
+
uploads.length > 0 && /* @__PURE__ */ jsx("div", { className: "mt-3", children: uploads.map((u) => /* @__PURE__ */ jsxs("div", { className: "mb-2", children: [
|
|
301
|
+
/* @__PURE__ */ jsxs("div", { className: "d-flex justify-content-between small mb-1", children: [
|
|
302
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
303
|
+
u.file.name,
|
|
304
|
+
" - ",
|
|
305
|
+
u?.publicUrl ?? ""
|
|
306
|
+
] }),
|
|
307
|
+
/* @__PURE__ */ jsx("span", { children: u.status === "success" ? "Completed" : u.status === "error" ? "Error" : `${u.progress}%` })
|
|
308
|
+
] }),
|
|
309
|
+
/* @__PURE__ */ jsx("div", { className: "progress", style: { height: "6px" }, children: /* @__PURE__ */ jsx(
|
|
310
|
+
"div",
|
|
311
|
+
{
|
|
312
|
+
className: "progress-bar " + (u.status === "success" ? "bg-success" : u.status === "error" ? "bg-danger" : ""),
|
|
313
|
+
role: "progressbar",
|
|
314
|
+
style: { width: `${u.progress}%` },
|
|
315
|
+
"aria-valuenow": u.progress,
|
|
316
|
+
"aria-valuemin": 0,
|
|
317
|
+
"aria-valuemax": 100
|
|
318
|
+
}
|
|
319
|
+
) }),
|
|
320
|
+
u.status === "error" && /* @__PURE__ */ jsx("div", { className: "text-danger small mt-1", children: u.error ?? "Upload failed" })
|
|
321
|
+
] }, u.id)) })
|
|
243
322
|
] }) }) });
|
|
244
323
|
};
|
|
324
|
+
async function uploadFileToS3(file, presignedUrl, onProgress) {
|
|
325
|
+
return new Promise((resolve, reject) => {
|
|
326
|
+
const xhr = new XMLHttpRequest();
|
|
327
|
+
xhr.open("PUT", presignedUrl);
|
|
328
|
+
xhr.upload.onprogress = (event) => {
|
|
329
|
+
if (!event.lengthComputable) return;
|
|
330
|
+
const percent = Math.round(event.loaded / event.total * 100);
|
|
331
|
+
onProgress(percent);
|
|
332
|
+
};
|
|
333
|
+
xhr.onload = () => {
|
|
334
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
335
|
+
onProgress(100);
|
|
336
|
+
resolve();
|
|
337
|
+
} else {
|
|
338
|
+
reject(
|
|
339
|
+
new Error(`S3 upload failed with status ${xhr.status}: ${xhr.statusText}`)
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
xhr.onerror = () => {
|
|
344
|
+
reject(new Error("Network error while uploading to S3"));
|
|
345
|
+
};
|
|
346
|
+
xhr.setRequestHeader(
|
|
347
|
+
"Content-Type",
|
|
348
|
+
file.type || "application/octet-stream"
|
|
349
|
+
);
|
|
350
|
+
xhr.send(file);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
245
353
|
|
|
246
354
|
// src/views/HomeView.tsx
|
|
247
355
|
import { AppSettings, AuthenticatorProvider, UserInfoCard } from "@sparkstudio/authentication-ui";
|
|
@@ -250,6 +358,11 @@ function HomeView() {
|
|
|
250
358
|
function handleOnLoginSuccess(user) {
|
|
251
359
|
alert(user?.Id);
|
|
252
360
|
}
|
|
361
|
+
const getPresignedUrl = async (file) => {
|
|
362
|
+
const sdk = new SparkStudioStorageSDK("https://lf9zyufpuk.execute-api.us-east-2.amazonaws.com/Prod");
|
|
363
|
+
const result = await sdk.container.GetPreSignedUrl(file.name, file.size, encodeURIComponent(file.type));
|
|
364
|
+
return result;
|
|
365
|
+
};
|
|
253
366
|
return /* @__PURE__ */ jsxs2(
|
|
254
367
|
AuthenticatorProvider,
|
|
255
368
|
{
|
|
@@ -259,13 +372,29 @@ function HomeView() {
|
|
|
259
372
|
onLoginSuccess: handleOnLoginSuccess,
|
|
260
373
|
children: [
|
|
261
374
|
/* @__PURE__ */ jsx2(UserInfoCard, {}),
|
|
262
|
-
/* @__PURE__ */ jsx2(
|
|
375
|
+
/* @__PURE__ */ jsx2(
|
|
376
|
+
UploadContainer,
|
|
377
|
+
{
|
|
378
|
+
title: "Upload your files to S3",
|
|
379
|
+
description: "Drag & drop or browse files. Each file will upload with its own progress bar.",
|
|
380
|
+
multiple: true,
|
|
381
|
+
accept: "*/*",
|
|
382
|
+
autoUpload: true,
|
|
383
|
+
getPresignedUrl,
|
|
384
|
+
onUploadComplete: (file, s3Url) => {
|
|
385
|
+
console.log("Uploaded", file.name, "to", s3Url);
|
|
386
|
+
},
|
|
387
|
+
onUploadError: (file, error) => {
|
|
388
|
+
console.error("Failed to upload", file.name, error);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
)
|
|
263
392
|
]
|
|
264
393
|
}
|
|
265
394
|
);
|
|
266
395
|
}
|
|
267
396
|
export {
|
|
268
|
-
|
|
397
|
+
AWSPresignedUrlDTO,
|
|
269
398
|
Container,
|
|
270
399
|
ContainerDTO,
|
|
271
400
|
ContainerType,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkstudio/storage-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
"build": "npm run generateIndex && tsup src/index.ts --format cjs,esm --dts --tsconfig tsconfig.lib.json && sass --load-path=node_modules src/index.scss dist/index.css",
|
|
29
29
|
"lint": "eslint .",
|
|
30
30
|
"preview": "vite preview",
|
|
31
|
-
"start": "vite"
|
|
31
|
+
"start": "npm run generateIndex && vite"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"react": "18.3.1",
|
|
35
35
|
"react-dom": "18.3.1"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@aws-sdk/client-s3": "^3.958.0",
|
|
39
|
+
"@aws-sdk/s3-request-presigner": "^3.958.0",
|
|
38
40
|
"@sparkstudio/authentication-ui": "^1.0.29",
|
|
39
41
|
"@sparkstudio/common-ui": "^1.0.5",
|
|
40
42
|
"barrelsby": "^2.8.1"
|