storage-explorer 0.1.0

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.
@@ -0,0 +1,1022 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __toESM = (mod, isNodeMode, target) => {
7
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
8
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
9
+ for (let key of __getOwnPropNames(mod))
10
+ if (!__hasOwnProp.call(to, key))
11
+ __defProp(to, key, {
12
+ get: () => mod[key],
13
+ enumerable: true
14
+ });
15
+ return to;
16
+ };
17
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
18
+ var __export = (target, all) => {
19
+ for (var name in all)
20
+ __defProp(target, name, {
21
+ get: all[name],
22
+ enumerable: true,
23
+ configurable: true,
24
+ set: (newValue) => all[name] = () => newValue
25
+ });
26
+ };
27
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
28
+ var __jsonParse = (a) => JSON.parse(a);
29
+ var __require = import.meta.require;
30
+
31
+ // src/frontend.tsx
32
+ import { StrictMode } from "react";
33
+ import { createRoot } from "react-dom/client";
34
+
35
+ // src/App.tsx
36
+ import { useEffect as useEffect2, useState as useState2 } from "react";
37
+
38
+ // src/features/buckets/BucketPanel.tsx
39
+ import { jsx, jsxs } from "react/jsx-runtime";
40
+ function BucketPanel(props) {
41
+ const {
42
+ profileValid,
43
+ loadingBuckets,
44
+ loadingObjects,
45
+ buckets,
46
+ selectedBucket,
47
+ manualBucketName,
48
+ onManualBucketNameChange,
49
+ onLoadBuckets,
50
+ onOpenBucket,
51
+ onOpenManualBucket
52
+ } = props;
53
+ return /* @__PURE__ */ jsxs("section", {
54
+ className: "panel explorer-panel",
55
+ children: [
56
+ /* @__PURE__ */ jsx("div", {
57
+ className: "panel-step",
58
+ children: "Step 2"
59
+ }),
60
+ /* @__PURE__ */ jsxs("div", {
61
+ className: "explorer-header",
62
+ children: [
63
+ /* @__PURE__ */ jsxs("div", {
64
+ children: [
65
+ /* @__PURE__ */ jsx("h2", {
66
+ children: "Buckets"
67
+ }),
68
+ /* @__PURE__ */ jsx("p", {
69
+ children: "Load visible buckets or open one directly by name."
70
+ })
71
+ ]
72
+ }),
73
+ /* @__PURE__ */ jsx("button", {
74
+ type: "button",
75
+ className: "primary-button",
76
+ onClick: onLoadBuckets,
77
+ disabled: loadingBuckets || !profileValid,
78
+ children: loadingBuckets ? "Loading..." : "Load Buckets"
79
+ })
80
+ ]
81
+ }),
82
+ /* @__PURE__ */ jsxs("form", {
83
+ className: "manual-bucket-form",
84
+ onSubmit: onOpenManualBucket,
85
+ children: [
86
+ /* @__PURE__ */ jsx("label", {
87
+ htmlFor: "manual-bucket-input",
88
+ children: "Known bucket name"
89
+ }),
90
+ /* @__PURE__ */ jsxs("div", {
91
+ className: "manual-bucket-row",
92
+ children: [
93
+ /* @__PURE__ */ jsx("input", {
94
+ id: "manual-bucket-input",
95
+ type: "text",
96
+ value: manualBucketName,
97
+ onChange: (event) => onManualBucketNameChange(event.target.value),
98
+ placeholder: "my-private-bucket",
99
+ autoComplete: "off"
100
+ }),
101
+ /* @__PURE__ */ jsx("button", {
102
+ type: "submit",
103
+ className: "secondary-button",
104
+ disabled: loadingObjects || !profileValid,
105
+ children: "Open Bucket"
106
+ })
107
+ ]
108
+ }),
109
+ /* @__PURE__ */ jsx("p", {
110
+ className: "helper-copy",
111
+ children: "Useful when your key can access a bucket but cannot list all buckets."
112
+ })
113
+ ]
114
+ }),
115
+ /* @__PURE__ */ jsx("div", {
116
+ className: "buckets-grid",
117
+ children: buckets.length === 0 ? /* @__PURE__ */ jsx("p", {
118
+ className: "empty-copy",
119
+ children: "No buckets loaded yet."
120
+ }) : buckets.map((bucket) => /* @__PURE__ */ jsxs("button", {
121
+ type: "button",
122
+ className: `bucket-item ${bucket.name === selectedBucket ? "active" : ""}`,
123
+ onClick: () => onOpenBucket(bucket.name),
124
+ disabled: loadingObjects,
125
+ children: [
126
+ /* @__PURE__ */ jsx("span", {
127
+ children: bucket.name
128
+ }),
129
+ bucket.creationDate && /* @__PURE__ */ jsx("small", {
130
+ children: new Date(bucket.creationDate).toLocaleString()
131
+ })
132
+ ]
133
+ }, bucket.name))
134
+ })
135
+ ]
136
+ });
137
+ }
138
+
139
+ // src/features/objects/PathBreadcrumb.tsx
140
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
141
+ function PathBreadcrumb(props) {
142
+ const { bucket, prefix, loading, onNavigatePrefix } = props;
143
+ if (!bucket) {
144
+ return /* @__PURE__ */ jsx2("div", {
145
+ className: "path-breadcrumb muted",
146
+ children: /* @__PURE__ */ jsx2("span", {
147
+ children: "Bucket path will appear here."
148
+ })
149
+ });
150
+ }
151
+ const segments = prefix.split("/").filter(Boolean);
152
+ return /* @__PURE__ */ jsxs2("div", {
153
+ className: "path-breadcrumb",
154
+ children: [
155
+ /* @__PURE__ */ jsx2("button", {
156
+ type: "button",
157
+ className: "breadcrumb-link",
158
+ onClick: () => onNavigatePrefix(""),
159
+ disabled: loading,
160
+ children: bucket
161
+ }),
162
+ segments.map((segment, index) => {
163
+ const targetPrefix = `${segments.slice(0, index + 1).join("/")}/`;
164
+ return /* @__PURE__ */ jsxs2("span", {
165
+ className: "breadcrumb-segment",
166
+ children: [
167
+ /* @__PURE__ */ jsx2("span", {
168
+ className: "breadcrumb-separator",
169
+ children: "/"
170
+ }),
171
+ /* @__PURE__ */ jsx2("button", {
172
+ type: "button",
173
+ className: "breadcrumb-link",
174
+ onClick: () => onNavigatePrefix(targetPrefix),
175
+ disabled: loading,
176
+ children: segment
177
+ })
178
+ ]
179
+ }, targetPrefix);
180
+ })
181
+ ]
182
+ });
183
+ }
184
+
185
+ // src/features/objects/ObjectExplorer.tsx
186
+ import { jsx as jsx3, jsxs as jsxs3, Fragment } from "react/jsx-runtime";
187
+ function formatObjectName(key, prefix) {
188
+ if (!prefix) {
189
+ return key;
190
+ }
191
+ return key.startsWith(prefix) ? key.slice(prefix.length) : key;
192
+ }
193
+ function formatFolderName(prefixValue, currentPrefix) {
194
+ const clean = prefixValue.replace(/\/$/, "");
195
+ const withoutCurrent = currentPrefix && clean.startsWith(currentPrefix) ? clean.slice(currentPrefix.length) : clean;
196
+ const parts = withoutCurrent.split("/").filter(Boolean);
197
+ return parts.at(-1) ?? clean;
198
+ }
199
+ function ObjectExplorer(props) {
200
+ const {
201
+ selectedBucket,
202
+ prefix,
203
+ objects,
204
+ loadingObjects,
205
+ isNextPage,
206
+ nextToken,
207
+ onUpOneLevel,
208
+ onLoadFirstPage,
209
+ onLoadNextPage,
210
+ onOpenFolder,
211
+ onNavigatePrefix
212
+ } = props;
213
+ return /* @__PURE__ */ jsxs3("section", {
214
+ className: "panel object-panel",
215
+ children: [
216
+ /* @__PURE__ */ jsx3("div", {
217
+ className: "panel-step",
218
+ children: "Step 3"
219
+ }),
220
+ /* @__PURE__ */ jsxs3("div", {
221
+ className: "objects-header",
222
+ children: [
223
+ /* @__PURE__ */ jsxs3("div", {
224
+ children: [
225
+ /* @__PURE__ */ jsx3("h3", {
226
+ children: "Objects"
227
+ }),
228
+ /* @__PURE__ */ jsx3("p", {
229
+ children: "Read-only view of folders and files."
230
+ })
231
+ ]
232
+ }),
233
+ /* @__PURE__ */ jsxs3("div", {
234
+ className: "objects-actions",
235
+ children: [
236
+ /* @__PURE__ */ jsx3("button", {
237
+ type: "button",
238
+ className: "secondary-button",
239
+ onClick: onUpOneLevel,
240
+ disabled: !selectedBucket || loadingObjects,
241
+ children: "Up One Level"
242
+ }),
243
+ /* @__PURE__ */ jsx3("button", {
244
+ type: "button",
245
+ className: "secondary-button",
246
+ onClick: onLoadFirstPage,
247
+ disabled: !selectedBucket || loadingObjects || !isNextPage,
248
+ children: "First Page"
249
+ }),
250
+ /* @__PURE__ */ jsx3("button", {
251
+ type: "button",
252
+ className: "secondary-button",
253
+ onClick: onLoadNextPage,
254
+ disabled: !nextToken || loadingObjects,
255
+ children: loadingObjects ? "Loading..." : "Next Page"
256
+ })
257
+ ]
258
+ })
259
+ ]
260
+ }),
261
+ /* @__PURE__ */ jsx3(PathBreadcrumb, {
262
+ bucket: selectedBucket,
263
+ prefix,
264
+ loading: loadingObjects,
265
+ onNavigatePrefix
266
+ }),
267
+ /* @__PURE__ */ jsxs3("div", {
268
+ className: "objects-list",
269
+ children: [
270
+ !objects && /* @__PURE__ */ jsx3("p", {
271
+ className: "empty-copy",
272
+ children: "Open a bucket to browse objects."
273
+ }),
274
+ objects && /* @__PURE__ */ jsxs3(Fragment, {
275
+ children: [
276
+ objects.folders.length === 0 && objects.files.length === 0 && /* @__PURE__ */ jsx3("p", {
277
+ className: "empty-copy",
278
+ children: "This path is empty."
279
+ }),
280
+ objects.folders.map((folder) => /* @__PURE__ */ jsxs3("button", {
281
+ type: "button",
282
+ className: "object-row folder",
283
+ onClick: () => onOpenFolder(folder),
284
+ disabled: loadingObjects,
285
+ children: [
286
+ /* @__PURE__ */ jsxs3("span", {
287
+ className: "object-name",
288
+ children: [
289
+ formatFolderName(folder, prefix),
290
+ "/"
291
+ ]
292
+ }),
293
+ /* @__PURE__ */ jsx3("span", {
294
+ className: "object-meta",
295
+ children: "folder"
296
+ }),
297
+ /* @__PURE__ */ jsx3("span", {
298
+ className: "object-meta",
299
+ children: "-"
300
+ })
301
+ ]
302
+ }, folder)),
303
+ objects.files.map((file) => /* @__PURE__ */ jsxs3("div", {
304
+ className: "object-row",
305
+ children: [
306
+ /* @__PURE__ */ jsx3("span", {
307
+ className: "object-name",
308
+ children: formatObjectName(file.key, prefix)
309
+ }),
310
+ /* @__PURE__ */ jsxs3("span", {
311
+ className: "object-meta",
312
+ children: [
313
+ file.size.toLocaleString(),
314
+ " bytes"
315
+ ]
316
+ }),
317
+ /* @__PURE__ */ jsx3("span", {
318
+ className: "object-meta",
319
+ children: file.lastModified ? new Date(file.lastModified).toLocaleString() : "-"
320
+ })
321
+ ]
322
+ }, file.key))
323
+ ]
324
+ })
325
+ ]
326
+ })
327
+ ]
328
+ });
329
+ }
330
+
331
+ // src/features/profiles/ProfileSidebar.tsx
332
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
333
+ function ProfileSidebar(props) {
334
+ const {
335
+ profiles,
336
+ selectedProfileId,
337
+ isEditingExisting,
338
+ form,
339
+ showSecret,
340
+ statusMessage,
341
+ statusError,
342
+ testingConnection,
343
+ onCreateNewProfile,
344
+ onSelectProfile,
345
+ onDeleteProfile,
346
+ onSaveProfile,
347
+ onTestConnection,
348
+ onToggleSecret,
349
+ onFormChange
350
+ } = props;
351
+ return /* @__PURE__ */ jsxs4("aside", {
352
+ className: "panel profile-panel",
353
+ children: [
354
+ /* @__PURE__ */ jsx4("div", {
355
+ className: "panel-step",
356
+ children: "Step 1"
357
+ }),
358
+ /* @__PURE__ */ jsxs4("div", {
359
+ className: "panel-header",
360
+ children: [
361
+ /* @__PURE__ */ jsx4("h1", {
362
+ children: "Connection Profile"
363
+ }),
364
+ /* @__PURE__ */ jsx4("p", {
365
+ children: "Save and reuse credentials in your browser."
366
+ })
367
+ ]
368
+ }),
369
+ /* @__PURE__ */ jsxs4("div", {
370
+ className: "profiles-toolbar",
371
+ children: [
372
+ /* @__PURE__ */ jsx4("strong", {
373
+ children: "Saved Profiles"
374
+ }),
375
+ /* @__PURE__ */ jsx4("button", {
376
+ type: "button",
377
+ className: "ghost-button",
378
+ onClick: onCreateNewProfile,
379
+ children: "New"
380
+ })
381
+ ]
382
+ }),
383
+ /* @__PURE__ */ jsxs4("div", {
384
+ className: "profiles-list",
385
+ role: "list",
386
+ "aria-label": "Saved profiles",
387
+ children: [
388
+ profiles.length === 0 && /* @__PURE__ */ jsx4("p", {
389
+ className: "empty-copy",
390
+ children: "No saved profiles yet."
391
+ }),
392
+ profiles.map((profile) => /* @__PURE__ */ jsxs4("div", {
393
+ className: `profile-card ${profile.id === selectedProfileId ? "active" : ""}`,
394
+ role: "listitem",
395
+ children: [
396
+ /* @__PURE__ */ jsxs4("button", {
397
+ type: "button",
398
+ className: "profile-select",
399
+ onClick: () => onSelectProfile(profile),
400
+ children: [
401
+ /* @__PURE__ */ jsx4("span", {
402
+ className: "profile-name",
403
+ children: profile.name || "Unnamed profile"
404
+ }),
405
+ /* @__PURE__ */ jsx4("span", {
406
+ className: "profile-endpoint",
407
+ children: profile.endpoint
408
+ })
409
+ ]
410
+ }),
411
+ /* @__PURE__ */ jsx4("button", {
412
+ type: "button",
413
+ className: "delete-button",
414
+ onClick: () => onDeleteProfile(profile.id),
415
+ "aria-label": `Delete ${profile.name || "profile"}`,
416
+ children: "Delete"
417
+ })
418
+ ]
419
+ }, profile.id))
420
+ ]
421
+ }),
422
+ /* @__PURE__ */ jsxs4("form", {
423
+ className: "profile-form",
424
+ onSubmit: onSaveProfile,
425
+ children: [
426
+ /* @__PURE__ */ jsxs4("label", {
427
+ children: [
428
+ "Profile Name",
429
+ /* @__PURE__ */ jsx4("input", {
430
+ type: "text",
431
+ value: form.name,
432
+ onChange: (event) => onFormChange("name", event.target.value),
433
+ placeholder: "MinIO dev, staging S3"
434
+ })
435
+ ]
436
+ }),
437
+ /* @__PURE__ */ jsxs4("label", {
438
+ children: [
439
+ "Endpoint URL *",
440
+ /* @__PURE__ */ jsx4("input", {
441
+ type: "url",
442
+ value: form.endpoint,
443
+ onChange: (event) => onFormChange("endpoint", event.target.value),
444
+ placeholder: "https://s3.amazonaws.com",
445
+ required: true
446
+ })
447
+ ]
448
+ }),
449
+ /* @__PURE__ */ jsxs4("label", {
450
+ children: [
451
+ "Region",
452
+ /* @__PURE__ */ jsx4("input", {
453
+ type: "text",
454
+ value: form.region,
455
+ onChange: (event) => onFormChange("region", event.target.value),
456
+ placeholder: "us-east-1"
457
+ })
458
+ ]
459
+ }),
460
+ /* @__PURE__ */ jsxs4("label", {
461
+ children: [
462
+ "Access Key ID *",
463
+ /* @__PURE__ */ jsx4("input", {
464
+ type: "text",
465
+ value: form.accessKeyId,
466
+ onChange: (event) => onFormChange("accessKeyId", event.target.value),
467
+ autoComplete: "off",
468
+ required: true
469
+ })
470
+ ]
471
+ }),
472
+ /* @__PURE__ */ jsxs4("label", {
473
+ children: [
474
+ "Secret Access Key *",
475
+ /* @__PURE__ */ jsxs4("div", {
476
+ className: "secret-row",
477
+ children: [
478
+ /* @__PURE__ */ jsx4("input", {
479
+ type: showSecret ? "text" : "password",
480
+ value: form.secretAccessKey,
481
+ onChange: (event) => onFormChange("secretAccessKey", event.target.value),
482
+ autoComplete: "off",
483
+ required: true
484
+ }),
485
+ /* @__PURE__ */ jsx4("button", {
486
+ type: "button",
487
+ className: "ghost-button",
488
+ onClick: onToggleSecret,
489
+ children: showSecret ? "Hide" : "Show"
490
+ })
491
+ ]
492
+ })
493
+ ]
494
+ }),
495
+ /* @__PURE__ */ jsxs4("label", {
496
+ className: "checkbox-row",
497
+ children: [
498
+ /* @__PURE__ */ jsx4("input", {
499
+ type: "checkbox",
500
+ checked: form.forcePathStyle,
501
+ onChange: (event) => onFormChange("forcePathStyle", event.target.checked)
502
+ }),
503
+ "Force path style"
504
+ ]
505
+ }),
506
+ /* @__PURE__ */ jsxs4("div", {
507
+ className: "form-actions",
508
+ children: [
509
+ /* @__PURE__ */ jsx4("button", {
510
+ type: "submit",
511
+ className: "primary-button",
512
+ children: isEditingExisting ? "Update Profile" : "Save Profile"
513
+ }),
514
+ /* @__PURE__ */ jsx4("button", {
515
+ type: "button",
516
+ className: "secondary-button",
517
+ onClick: onTestConnection,
518
+ disabled: testingConnection,
519
+ children: testingConnection ? "Testing..." : "Test Connection"
520
+ })
521
+ ]
522
+ })
523
+ ]
524
+ }),
525
+ statusMessage && /* @__PURE__ */ jsx4("p", {
526
+ className: "status-ok",
527
+ children: statusMessage
528
+ }),
529
+ statusError && /* @__PURE__ */ jsx4("p", {
530
+ className: "status-error",
531
+ children: statusError
532
+ })
533
+ ]
534
+ });
535
+ }
536
+
537
+ // src/shared/api/s3Api.ts
538
+ async function postJson(url, body) {
539
+ const response = await fetch(url, {
540
+ method: "POST",
541
+ headers: {
542
+ "Content-Type": "application/json"
543
+ },
544
+ body: JSON.stringify(body)
545
+ });
546
+ const payload = await response.json();
547
+ if (!response.ok || payload.ok === false) {
548
+ const message = payload.ok === false ? payload.error?.message : "Request failed.";
549
+ throw new Error(message || "Request failed.");
550
+ }
551
+ return payload.data;
552
+ }
553
+ function testConnection(profile) {
554
+ return postJson("/api/s3/test-connection", { profile });
555
+ }
556
+ function listBuckets(profile) {
557
+ return postJson("/api/s3/list-buckets", { profile });
558
+ }
559
+ function listObjects(input) {
560
+ return postJson("/api/s3/list-objects", input);
561
+ }
562
+
563
+ // src/shared/hooks/useProfilesStorage.ts
564
+ import { useCallback, useEffect, useMemo, useState } from "react";
565
+ var PROFILES_KEY = "s3-explorer:profiles:v1";
566
+ var LAST_PROFILE_KEY = "s3-explorer:last-profile:v1";
567
+ var PROFILE_VIEW_KEY = "s3-explorer:profile-view:v1";
568
+ var emptyView = {
569
+ bucket: "",
570
+ prefix: "",
571
+ manualBucketName: ""
572
+ };
573
+ function parseStoredProfiles(raw) {
574
+ if (!raw) {
575
+ return [];
576
+ }
577
+ try {
578
+ const parsed = JSON.parse(raw);
579
+ if (!Array.isArray(parsed)) {
580
+ return [];
581
+ }
582
+ return parsed.filter((item) => typeof item === "object" && item !== null).map((item) => {
583
+ const record = item;
584
+ return {
585
+ id: String(record.id ?? crypto.randomUUID()),
586
+ name: String(record.name ?? ""),
587
+ endpoint: String(record.endpoint ?? ""),
588
+ region: String(record.region ?? "us-east-1"),
589
+ accessKeyId: String(record.accessKeyId ?? ""),
590
+ secretAccessKey: String(record.secretAccessKey ?? ""),
591
+ forcePathStyle: record.forcePathStyle !== false
592
+ };
593
+ }).filter((profile) => profile.endpoint && profile.accessKeyId && profile.secretAccessKey);
594
+ } catch {
595
+ return [];
596
+ }
597
+ }
598
+ function parseStoredViews(raw) {
599
+ if (!raw) {
600
+ return {};
601
+ }
602
+ try {
603
+ const parsed = JSON.parse(raw);
604
+ if (typeof parsed !== "object" || parsed === null) {
605
+ return {};
606
+ }
607
+ const record = parsed;
608
+ const next = {};
609
+ for (const [profileId, value] of Object.entries(record)) {
610
+ if (typeof value !== "object" || value === null) {
611
+ continue;
612
+ }
613
+ const view = value;
614
+ next[profileId] = {
615
+ bucket: typeof view.bucket === "string" ? view.bucket : "",
616
+ prefix: typeof view.prefix === "string" ? view.prefix : "",
617
+ manualBucketName: typeof view.manualBucketName === "string" ? view.manualBucketName : ""
618
+ };
619
+ }
620
+ return next;
621
+ } catch {
622
+ return {};
623
+ }
624
+ }
625
+ function useProfilesStorage() {
626
+ const [profiles, setProfiles] = useState(() => parseStoredProfiles(localStorage.getItem(PROFILES_KEY)));
627
+ const [profileViews, setProfileViews] = useState(() => parseStoredViews(localStorage.getItem(PROFILE_VIEW_KEY)));
628
+ const [selectedProfileId, setSelectedProfileId] = useState(() => {
629
+ const loadedProfiles = parseStoredProfiles(localStorage.getItem(PROFILES_KEY));
630
+ const storedProfileId = localStorage.getItem(LAST_PROFILE_KEY);
631
+ const fallbackProfile = loadedProfiles[0]?.id ?? null;
632
+ return loadedProfiles.some((profile) => profile.id === storedProfileId) ? storedProfileId : fallbackProfile;
633
+ });
634
+ useEffect(() => {
635
+ localStorage.setItem(PROFILES_KEY, JSON.stringify(profiles));
636
+ }, [profiles]);
637
+ useEffect(() => {
638
+ localStorage.setItem(PROFILE_VIEW_KEY, JSON.stringify(profileViews));
639
+ }, [profileViews]);
640
+ useEffect(() => {
641
+ if (selectedProfileId) {
642
+ localStorage.setItem(LAST_PROFILE_KEY, selectedProfileId);
643
+ } else {
644
+ localStorage.removeItem(LAST_PROFILE_KEY);
645
+ }
646
+ }, [selectedProfileId]);
647
+ const selectedProfile = useMemo(() => profiles.find((profile) => profile.id === selectedProfileId) ?? null, [profiles, selectedProfileId]);
648
+ const saveProfile = useCallback((profile) => {
649
+ setProfiles((prev) => {
650
+ const existing = prev.findIndex((entry) => entry.id === profile.id);
651
+ if (existing === -1) {
652
+ return [profile, ...prev];
653
+ }
654
+ const next = [...prev];
655
+ next[existing] = profile;
656
+ return next;
657
+ });
658
+ }, []);
659
+ const deleteProfile = useCallback((profileId) => {
660
+ setProfiles((prev) => prev.filter((profile) => profile.id !== profileId));
661
+ setProfileViews((prev) => {
662
+ const next = { ...prev };
663
+ delete next[profileId];
664
+ return next;
665
+ });
666
+ setSelectedProfileId((current) => current === profileId ? null : current);
667
+ }, []);
668
+ const updateProfileView = useCallback((profileId, patch) => {
669
+ setProfileViews((prev) => ({
670
+ ...prev,
671
+ [profileId]: {
672
+ ...prev[profileId] ?? emptyView,
673
+ ...patch
674
+ }
675
+ }));
676
+ }, []);
677
+ const getProfileView = useCallback((profileId) => {
678
+ if (!profileId) {
679
+ return emptyView;
680
+ }
681
+ return profileViews[profileId] ?? emptyView;
682
+ }, [profileViews]);
683
+ return {
684
+ profiles,
685
+ selectedProfileId,
686
+ selectedProfile,
687
+ setSelectedProfileId,
688
+ saveProfile,
689
+ deleteProfile,
690
+ updateProfileView,
691
+ getProfileView
692
+ };
693
+ }
694
+
695
+ // src/App.tsx
696
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
697
+ var DEFAULT_MAX_KEYS = 200;
698
+ var emptyProfile = {
699
+ name: "",
700
+ endpoint: "",
701
+ region: "us-east-1",
702
+ accessKeyId: "",
703
+ secretAccessKey: "",
704
+ forcePathStyle: true
705
+ };
706
+ function toEditable(profile) {
707
+ return {
708
+ name: profile.name,
709
+ endpoint: profile.endpoint,
710
+ region: profile.region,
711
+ accessKeyId: profile.accessKeyId,
712
+ secretAccessKey: profile.secretAccessKey,
713
+ forcePathStyle: profile.forcePathStyle
714
+ };
715
+ }
716
+ function deriveName(endpoint) {
717
+ try {
718
+ const normalized = endpoint.startsWith("http") ? endpoint : `https://${endpoint}`;
719
+ const host = new URL(normalized).host;
720
+ return host || endpoint;
721
+ } catch {
722
+ return endpoint;
723
+ }
724
+ }
725
+ function App() {
726
+ const {
727
+ profiles,
728
+ selectedProfileId,
729
+ selectedProfile,
730
+ setSelectedProfileId,
731
+ saveProfile,
732
+ deleteProfile,
733
+ updateProfileView,
734
+ getProfileView
735
+ } = useProfilesStorage();
736
+ const [form, setForm] = useState2(emptyProfile);
737
+ const [showSecret, setShowSecret] = useState2(false);
738
+ const [statusMessage, setStatusMessage] = useState2("");
739
+ const [statusError, setStatusError] = useState2("");
740
+ const [testingConnection, setTestingConnection] = useState2(false);
741
+ const [loadingBuckets, setLoadingBuckets] = useState2(false);
742
+ const [loadingObjects, setLoadingObjects] = useState2(false);
743
+ const [buckets, setBuckets] = useState2([]);
744
+ const [manualBucketName, setManualBucketName] = useState2("");
745
+ const [selectedBucket, setSelectedBucket] = useState2("");
746
+ const [prefix, setPrefix] = useState2("");
747
+ const [objects, setObjects] = useState2(null);
748
+ const [nextToken, setNextToken] = useState2(null);
749
+ const [isNextPage, setIsNextPage] = useState2(false);
750
+ useEffect2(() => {
751
+ if (!selectedProfile) {
752
+ setForm(emptyProfile);
753
+ setManualBucketName("");
754
+ setSelectedBucket("");
755
+ setPrefix("");
756
+ setObjects(null);
757
+ setNextToken(null);
758
+ setIsNextPage(false);
759
+ return;
760
+ }
761
+ const profileView = getProfileView(selectedProfile.id);
762
+ setForm(toEditable(selectedProfile));
763
+ setManualBucketName(profileView.manualBucketName || profileView.bucket);
764
+ setSelectedBucket(profileView.bucket);
765
+ setPrefix(profileView.prefix);
766
+ setObjects(null);
767
+ setNextToken(null);
768
+ setIsNextPage(false);
769
+ }, [selectedProfileId, selectedProfile]);
770
+ const profileValid = Boolean(form.endpoint.trim()) && Boolean(form.accessKeyId.trim()) && Boolean(form.secretAccessKey.trim());
771
+ const profilePayload = (value = form) => ({
772
+ endpoint: value.endpoint.trim(),
773
+ region: value.region.trim() || "us-east-1",
774
+ accessKeyId: value.accessKeyId.trim(),
775
+ secretAccessKey: value.secretAccessKey.trim(),
776
+ forcePathStyle: value.forcePathStyle
777
+ });
778
+ const onFormChange = (field, value) => {
779
+ setForm((prev) => ({ ...prev, [field]: value }));
780
+ };
781
+ const onCreateNewProfile = () => {
782
+ setSelectedProfileId(null);
783
+ setForm(emptyProfile);
784
+ setShowSecret(false);
785
+ setStatusMessage("");
786
+ setStatusError("");
787
+ setBuckets([]);
788
+ setManualBucketName("");
789
+ setSelectedBucket("");
790
+ setPrefix("");
791
+ setObjects(null);
792
+ setNextToken(null);
793
+ setIsNextPage(false);
794
+ };
795
+ const onSelectProfile = (profile) => {
796
+ setSelectedProfileId(profile.id);
797
+ setStatusMessage("");
798
+ setStatusError("");
799
+ };
800
+ const onSaveProfile = (event) => {
801
+ event.preventDefault();
802
+ if (!profileValid) {
803
+ setStatusError("Endpoint, access key ID, and secret key are required.");
804
+ setStatusMessage("");
805
+ return;
806
+ }
807
+ const normalized = {
808
+ id: selectedProfile?.id ?? crypto.randomUUID(),
809
+ name: form.name.trim() || deriveName(form.endpoint.trim()),
810
+ ...profilePayload(form)
811
+ };
812
+ saveProfile(normalized);
813
+ setSelectedProfileId(normalized.id);
814
+ setStatusError("");
815
+ setStatusMessage(selectedProfile ? "Profile updated." : "Profile saved.");
816
+ };
817
+ const onDeleteProfile = (profileId) => {
818
+ deleteProfile(profileId);
819
+ if (selectedProfileId === profileId) {
820
+ setForm(emptyProfile);
821
+ setManualBucketName("");
822
+ setSelectedBucket("");
823
+ setPrefix("");
824
+ setObjects(null);
825
+ setNextToken(null);
826
+ setIsNextPage(false);
827
+ }
828
+ setStatusMessage("Profile deleted.");
829
+ setStatusError("");
830
+ };
831
+ const onTestConnection = async () => {
832
+ if (!profileValid) {
833
+ setStatusError("Fill in endpoint, access key ID, and secret key first.");
834
+ setStatusMessage("");
835
+ return;
836
+ }
837
+ setTestingConnection(true);
838
+ setStatusError("");
839
+ setStatusMessage("");
840
+ try {
841
+ const result = await testConnection(profilePayload());
842
+ setStatusMessage(result.message || "Connection successful.");
843
+ } catch (err) {
844
+ setStatusError(err instanceof Error ? err.message : "Connection failed.");
845
+ } finally {
846
+ setTestingConnection(false);
847
+ }
848
+ };
849
+ const onLoadBuckets = async () => {
850
+ if (!profileValid) {
851
+ setStatusError("Fill in endpoint, access key ID, and secret key first.");
852
+ setStatusMessage("");
853
+ return;
854
+ }
855
+ setLoadingBuckets(true);
856
+ setStatusError("");
857
+ try {
858
+ const result = await listBuckets(profilePayload());
859
+ setBuckets(result.buckets.filter((bucket) => Boolean(bucket.name)));
860
+ setStatusMessage(`Loaded ${result.buckets.length} bucket(s).`);
861
+ } catch (err) {
862
+ setStatusError(err instanceof Error ? err.message : "Failed to load buckets.");
863
+ setStatusMessage("");
864
+ } finally {
865
+ setLoadingBuckets(false);
866
+ }
867
+ };
868
+ const loadObjects = async (options) => {
869
+ const { bucket, targetPrefix = "", continuationToken = null, profileOverride } = options;
870
+ setLoadingObjects(true);
871
+ setStatusError("");
872
+ try {
873
+ const data = await listObjects({
874
+ profile: profilePayload(profileOverride ?? form),
875
+ bucket,
876
+ prefix: targetPrefix,
877
+ continuationToken,
878
+ maxKeys: DEFAULT_MAX_KEYS
879
+ });
880
+ setSelectedBucket(bucket);
881
+ setPrefix(targetPrefix);
882
+ setObjects(data);
883
+ setNextToken(data.nextContinuationToken);
884
+ setIsNextPage(Boolean(continuationToken));
885
+ setStatusMessage(`Loaded ${data.folders.length + data.files.length} item(s) from ${bucket}.`);
886
+ if (selectedProfileId) {
887
+ updateProfileView(selectedProfileId, {
888
+ bucket,
889
+ prefix: targetPrefix,
890
+ manualBucketName: manualBucketName.trim() || bucket
891
+ });
892
+ }
893
+ } catch (err) {
894
+ setStatusError(err instanceof Error ? err.message : "Failed to load objects.");
895
+ setStatusMessage("");
896
+ } finally {
897
+ setLoadingObjects(false);
898
+ }
899
+ };
900
+ const onOpenBucket = async (bucket) => {
901
+ setManualBucketName(bucket);
902
+ await loadObjects({ bucket, targetPrefix: "", continuationToken: null });
903
+ };
904
+ const onOpenManualBucket = async (event) => {
905
+ event.preventDefault();
906
+ if (!profileValid) {
907
+ setStatusError("Fill in endpoint, access key ID, and secret key first.");
908
+ setStatusMessage("");
909
+ return;
910
+ }
911
+ const bucket = manualBucketName.trim();
912
+ if (!bucket) {
913
+ setStatusError("Enter a bucket name to open.");
914
+ setStatusMessage("");
915
+ return;
916
+ }
917
+ await onOpenBucket(bucket);
918
+ };
919
+ const onOpenFolder = async (folderPrefix) => {
920
+ if (!selectedBucket) {
921
+ return;
922
+ }
923
+ await loadObjects({ bucket: selectedBucket, targetPrefix: folderPrefix, continuationToken: null });
924
+ };
925
+ const onUpOneLevel = async () => {
926
+ if (!selectedBucket) {
927
+ return;
928
+ }
929
+ if (!prefix) {
930
+ await loadObjects({ bucket: selectedBucket, targetPrefix: "", continuationToken: null });
931
+ return;
932
+ }
933
+ const trimmed = prefix.replace(/\/$/, "");
934
+ const segments = trimmed.split("/").filter(Boolean);
935
+ const parentPrefix = segments.length > 1 ? `${segments.slice(0, -1).join("/")}/` : "";
936
+ await loadObjects({ bucket: selectedBucket, targetPrefix: parentPrefix, continuationToken: null });
937
+ };
938
+ const onNavigatePrefix = async (targetPrefix) => {
939
+ if (!selectedBucket) {
940
+ return;
941
+ }
942
+ await loadObjects({ bucket: selectedBucket, targetPrefix, continuationToken: null });
943
+ };
944
+ const onLoadNextPage = async () => {
945
+ if (!selectedBucket || !nextToken) {
946
+ return;
947
+ }
948
+ await loadObjects({
949
+ bucket: selectedBucket,
950
+ targetPrefix: prefix,
951
+ continuationToken: nextToken
952
+ });
953
+ };
954
+ const onLoadFirstPage = async () => {
955
+ if (!selectedBucket) {
956
+ return;
957
+ }
958
+ await loadObjects({ bucket: selectedBucket, targetPrefix: prefix, continuationToken: null });
959
+ };
960
+ return /* @__PURE__ */ jsxs5("div", {
961
+ className: "app-shell",
962
+ children: [
963
+ /* @__PURE__ */ jsx5(ProfileSidebar, {
964
+ profiles,
965
+ selectedProfileId,
966
+ isEditingExisting: Boolean(selectedProfile),
967
+ form,
968
+ showSecret,
969
+ statusMessage,
970
+ statusError,
971
+ testingConnection,
972
+ onCreateNewProfile,
973
+ onSelectProfile,
974
+ onDeleteProfile,
975
+ onSaveProfile,
976
+ onTestConnection,
977
+ onToggleSecret: () => setShowSecret((prev) => !prev),
978
+ onFormChange
979
+ }),
980
+ /* @__PURE__ */ jsxs5("main", {
981
+ className: "main-column",
982
+ children: [
983
+ /* @__PURE__ */ jsx5(BucketPanel, {
984
+ profileValid,
985
+ loadingBuckets,
986
+ loadingObjects,
987
+ buckets,
988
+ selectedBucket,
989
+ manualBucketName,
990
+ onManualBucketNameChange: setManualBucketName,
991
+ onLoadBuckets,
992
+ onOpenBucket,
993
+ onOpenManualBucket
994
+ }),
995
+ /* @__PURE__ */ jsx5(ObjectExplorer, {
996
+ selectedBucket,
997
+ prefix,
998
+ objects,
999
+ loadingObjects,
1000
+ isNextPage,
1001
+ nextToken,
1002
+ onUpOneLevel,
1003
+ onLoadFirstPage,
1004
+ onLoadNextPage,
1005
+ onOpenFolder,
1006
+ onNavigatePrefix
1007
+ })
1008
+ ]
1009
+ })
1010
+ ]
1011
+ });
1012
+ }
1013
+
1014
+ // src/frontend.tsx
1015
+ import { jsx as jsx6 } from "react/jsx-runtime";
1016
+ var elem = document.getElementById("root");
1017
+ var app = /* @__PURE__ */ jsx6(StrictMode, {
1018
+ children: /* @__PURE__ */ jsx6(App, {})
1019
+ });
1020
+ if (undefined) {} else {
1021
+ createRoot(elem).render(app);
1022
+ }