@sqlrooms/s3-browser 0.26.0 → 0.26.1-rc.1

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/README.md CHANGED
@@ -24,6 +24,115 @@ yarn add @sqlrooms/s3-browser
24
24
 
25
25
  ## Usage
26
26
 
27
+ ### Complete Example
28
+
29
+ ```tsx
30
+ import {useState} from 'react';
31
+ import {S3FileBrowser, S3CredentialsForm, S3State} from '@sqlrooms/s3-browser';
32
+ import {S3FileOrDirectory, S3Config, S3Connection} from '@sqlrooms/s3-utils';
33
+
34
+ type S3BrowserProps = {
35
+ listS3Files: (args: {
36
+ s3Config: S3Config;
37
+ prefix: string;
38
+ }) => Promise<S3FileOrDirectory[]>;
39
+ loadS3Files: (args: {
40
+ s3Config: S3Config;
41
+ prefix: string;
42
+ files: string[];
43
+ }) => Promise<void>;
44
+ s3: S3State['s3'];
45
+ saveS3Credentials: (s3Config: S3Config) => Promise<void>;
46
+ loadS3Credentials: () => Promise<S3Connection[]>;
47
+ deleteS3Credentials: (id: string) => Promise<void>;
48
+ };
49
+
50
+ export const S3Browser = ({
51
+ listS3Files,
52
+ s3,
53
+ loadS3Files,
54
+ saveS3Credentials,
55
+ loadS3Credentials,
56
+ deleteS3Credentials,
57
+ }: S3BrowserProps) => {
58
+ const [isConnecting, setIsConnecting] = useState(false);
59
+ const [error, setError] = useState('');
60
+ const [files, setFiles] = useState<S3FileOrDirectory[] | null>(null);
61
+ const [selectedDirectory, onChangeSelectedDirectory] = useState('');
62
+ const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
63
+ const {setCurrentS3Config, clearCurrentS3Config, currentS3Config} = s3;
64
+
65
+ const listFiles = async (s3Config: S3Config, prefix: string) => {
66
+ try {
67
+ const files = await listS3Files({
68
+ s3Config,
69
+ prefix,
70
+ });
71
+ setCurrentS3Config(s3Config);
72
+ setFiles(files);
73
+ setError('');
74
+ onChangeSelectedDirectory(prefix);
75
+ } catch (error) {
76
+ setError((error as Error).message);
77
+ }
78
+ setIsConnecting(false);
79
+ };
80
+
81
+ const handleLoadFiles = async () => {
82
+ if (!currentS3Config) return;
83
+ await loadS3Files({
84
+ s3Config: currentS3Config,
85
+ prefix: selectedDirectory,
86
+ files: selectedFiles,
87
+ });
88
+ };
89
+
90
+ return (
91
+ <div className="flex h-full flex-col items-center gap-4">
92
+ {/* Connection Panel */}
93
+ {!files ? (
94
+ <S3CredentialsForm
95
+ onConnect={(values) => {
96
+ setIsConnecting(true);
97
+ listFiles(values, '');
98
+ }}
99
+ isLoading={isConnecting}
100
+ saveS3Credentials={saveS3Credentials}
101
+ loadS3Credentials={loadS3Credentials}
102
+ deleteS3Credentials={deleteS3Credentials}
103
+ />
104
+ ) : (
105
+ <div className="flex h-full w-full flex-col items-start justify-start gap-2">
106
+ <S3FileBrowser
107
+ files={files}
108
+ selectedFiles={selectedFiles}
109
+ selectedDirectory={selectedDirectory}
110
+ onChangeSelectedFiles={setSelectedFiles}
111
+ onChangeSelectedDirectory={(directory) => {
112
+ setIsConnecting(true);
113
+ if (!currentS3Config) return;
114
+ listFiles(currentS3Config, directory);
115
+ }}
116
+ onCanConfirmChange={() => {}}
117
+ />
118
+ <button disabled={!selectedFiles.length} onClick={handleLoadFiles}>
119
+ Add Selected
120
+ </button>
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ };
126
+ ```
127
+
128
+ This example demonstrates:
129
+
130
+ - Integrating both `S3FileBrowser` and `S3CredentialsForm` components
131
+ - Managing S3 connection state
132
+ - Handling file listing and selection
133
+ - Error handling and loading states
134
+ - File loading functionality
135
+
27
136
  ### S3FileBrowser Component
28
137
 
29
138
  The `S3FileBrowser` component provides a familiar file explorer interface for navigating and selecting files from an S3-like storage.
@@ -60,33 +169,108 @@ function MyS3Browser() {
60
169
  }
61
170
  ```
62
171
 
63
- ### S3 Utility Functions
172
+ ### S3CredentialsForm Component
64
173
 
65
- The package also provides utility functions for working with S3:
174
+ The `S3CredentialsForm` component provides a form interface for managing S3 credentials and saved connections.
66
175
 
67
176
  ```tsx
68
- import {S3Client} from '@aws-sdk/client-s3';
69
- import {
70
- listFilesAndDirectoriesWithPrefix,
71
- deleteS3Files,
72
- } from '@sqlrooms/s3-browser';
73
-
74
- // Initialize S3 client
75
- const s3Client = new S3Client({region: 'us-east-1'});
76
-
77
- // List files and directories
78
- async function listFiles() {
79
- const files = await listFilesAndDirectoriesWithPrefix(
80
- s3Client,
81
- 'my-bucket',
82
- 'path/to/directory',
177
+ import {S3CredentialsForm} from '@sqlrooms/s3-browser';
178
+ import {S3Config, S3Connection} from '@sqlrooms/s3-utils';
179
+
180
+ function MyS3ConnectionManager() {
181
+ const handleConnect = async (credentials: S3Config) => {
182
+ try {
183
+ // Use the credentials to establish connection
184
+ console.log('Connecting with:', credentials);
185
+ // Example: initializeS3Client(credentials);
186
+ } catch (error) {
187
+ console.error('Connection failed:', error);
188
+ }
189
+ };
190
+
191
+ const handleSaveCredential = async (config: S3Config) => {
192
+ try {
193
+ // Save credential to your storage (e.g., local storage, database)
194
+ const savedCredential = await saveToStorage({
195
+ ...config,
196
+ id: generateId(),
197
+ createdAt: new Date().toISOString(),
198
+ });
199
+ return savedCredential;
200
+ } catch (error) {
201
+ console.error('Failed to save credential:', error);
202
+ }
203
+ };
204
+
205
+ const handleLoadCredentials = async (): Promise<S3Connection[]> => {
206
+ try {
207
+ // Load saved credentials from your storage
208
+ const credentials = await loadFromStorage();
209
+ return credentials;
210
+ } catch (error) {
211
+ console.error('Failed to load credentials:', error);
212
+ return [];
213
+ }
214
+ };
215
+
216
+ const handleDeleteCredential = async (id: string) => {
217
+ try {
218
+ // Delete credential from your storage
219
+ await deleteFromStorage(id);
220
+ } catch (error) {
221
+ console.error('Failed to delete credential:', error);
222
+ }
223
+ };
224
+
225
+ return (
226
+ <S3CredentialsForm
227
+ onConnect={handleConnect}
228
+ isLoading={false}
229
+ saveS3Credentials={handleSaveCredential}
230
+ loadS3Credentials={handleLoadCredentials}
231
+ deleteS3Credentials={handleDeleteCredential}
232
+ />
83
233
  );
84
- console.log(files);
85
234
  }
235
+ ```
236
+
237
+ Features:
86
238
 
87
- // Delete files with a prefix
88
- async function deleteFiles() {
89
- await deleteS3Files(s3Client, 'my-bucket', 'path/to/delete');
239
+ - Input fields for S3 credentials (access key, secret key, region, bucket)
240
+ - Option to save connections for later use
241
+ - Auto-fill from AWS CLI exports or credential process output
242
+ - Management of saved connections (view, connect, delete)
243
+ - Secure handling of sensitive credentials
244
+ - Support for session tokens (temporary credentials)
245
+
246
+ #### Props
247
+
248
+ ```tsx
249
+ interface S3CredentialsFormProps {
250
+ /**
251
+ * Callback fired when the connect button is clicked
252
+ */
253
+ onConnect: (data: S3Config) => void;
254
+
255
+ /**
256
+ * Loading state for the connect button
257
+ */
258
+ isLoading?: boolean;
259
+
260
+ /**
261
+ * Callback to save a new S3 credential
262
+ */
263
+ saveS3Credentials: (data: S3Config) => Promise<void>;
264
+
265
+ /**
266
+ * Callback to load saved S3 credentials
267
+ */
268
+ loadS3Credentials: () => Promise<S3Connection[]>;
269
+
270
+ /**
271
+ * Optional callback to delete a saved credential
272
+ */
273
+ deleteS3Credentials?: (id: string) => Promise<void>;
90
274
  }
91
275
  ```
92
276
 
@@ -128,49 +312,17 @@ interface S3FileBrowserProps {
128
312
  }
129
313
  ```
130
314
 
131
- ### S3FileOrDirectory
132
-
133
- ```tsx
134
- type S3FileOrDirectory =
135
- | {
136
- key: string;
137
- isDirectory: true;
138
- }
139
- | {
140
- key: string;
141
- isDirectory: false;
142
- lastModified?: Date;
143
- size?: number;
144
- contentType?: string;
145
- };
146
- ```
147
-
148
- ### Utility Functions
149
-
150
- ```tsx
151
- /**
152
- * Lists files and directories with a given prefix
153
- */
154
- function listFilesAndDirectoriesWithPrefix(
155
- S3: S3Client,
156
- bucket: string,
157
- prefix?: string,
158
- ): Promise<S3FileOrDirectory[]>;
159
-
160
- /**
161
- * Delete all files with the given prefix
162
- */
163
- function deleteS3Files(
164
- S3: S3Client,
165
- bucket: string,
166
- prefix: string,
167
- ): Promise<void>;
168
- ```
169
-
170
315
  ## Dependencies
171
316
 
172
- - @aws-sdk/client-s3
173
- - React
174
- - @sqlrooms/ui (for UI components)
175
- - @sqlrooms/utils (for formatting utilities)
176
- - zod (for type validation)
317
+ - `react` ^18.0.0
318
+ - `react-dom` ^18.0.0
319
+ - `@sqlrooms/ui` - UI component library
320
+ - `@sqlrooms/utils` - Utility functions
321
+ - `@sqlrooms/s3` - S3 client and types
322
+ - `@hookform/resolvers` - Form validation resolvers
323
+ - `react-hook-form` - Form handling
324
+ - `zod` - Runtime type checking and validation
325
+ - `lucide-react` - Icon components
326
+ - `class-variance-authority` - Utility for managing component variants
327
+ - `clsx` - Utility for constructing className strings
328
+ - `tailwindcss` - CSS framework
@@ -0,0 +1,11 @@
1
+ import { StateCreator } from 'zustand';
2
+ import { S3Config } from '@sqlrooms/s3-browser-config';
3
+ export type S3BrowserState = {
4
+ s3Browser: {
5
+ currentS3Config: S3Config | null;
6
+ setCurrentS3Config: (s3Config: S3Config) => void;
7
+ clearCurrentS3Config: () => void;
8
+ };
9
+ };
10
+ export declare function createS3BrowserSlice(): StateCreator<S3BrowserState>;
11
+ //# sourceMappingURL=S3BrowserSlice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3BrowserSlice.d.ts","sourceRoot":"","sources":["../src/S3BrowserSlice.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,YAAY,EAAC,MAAM,SAAS,CAAC;AACrC,OAAO,EAAC,QAAQ,EAAC,MAAM,6BAA6B,CAAC;AAErD,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE;QAET,eAAe,EAAE,QAAQ,GAAG,IAAI,CAAC;QAEjC,kBAAkB,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;QACjD,oBAAoB,EAAE,MAAM,IAAI,CAAC;KAClC,CAAC;CACH,CAAC;AAGF,wBAAgB,oBAAoB,IAAI,YAAY,CAAC,cAAc,CAAC,CAuBnE"}
@@ -0,0 +1,23 @@
1
+ import { createSlice } from '@sqlrooms/room-shell';
2
+ import { produce } from 'immer';
3
+ // Create the store
4
+ export function createS3BrowserSlice() {
5
+ return createSlice((set) => ({
6
+ // Initial state
7
+ s3Browser: {
8
+ currentS3Config: null,
9
+ // Actions
10
+ setCurrentS3Config: (s3Config) => {
11
+ set((state) => produce(state, (draft) => {
12
+ draft.s3Browser.currentS3Config = s3Config;
13
+ }));
14
+ },
15
+ clearCurrentS3Config: () => {
16
+ set((state) => produce(state, (draft) => {
17
+ draft.s3Browser.currentS3Config = null;
18
+ }));
19
+ },
20
+ },
21
+ }));
22
+ }
23
+ //# sourceMappingURL=S3BrowserSlice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3BrowserSlice.js","sourceRoot":"","sources":["../src/S3BrowserSlice.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,WAAW,EAAC,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAC,OAAO,EAAC,MAAM,OAAO,CAAC;AAc9B,mBAAmB;AACnB,MAAM,UAAU,oBAAoB;IAClC,OAAO,WAAW,CAAiB,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC3C,gBAAgB;QAChB,SAAS,EAAE;YACT,eAAe,EAAE,IAAI;YAErB,UAAU;YACV,kBAAkB,EAAE,CAAC,QAAQ,EAAE,EAAE;gBAC/B,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACZ,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;oBACvB,KAAK,CAAC,SAAS,CAAC,eAAe,GAAG,QAAQ,CAAC;gBAC7C,CAAC,CAAC,CACH,CAAC;YACJ,CAAC;YACD,oBAAoB,EAAE,GAAG,EAAE;gBACzB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACZ,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;oBACvB,KAAK,CAAC,SAAS,CAAC,eAAe,GAAG,IAAI,CAAC;gBACzC,CAAC,CAAC,CACH,CAAC;YACJ,CAAC;SACF;KACF,CAAC,CAAC,CAAC;AACN,CAAC","sourcesContent":["import {createSlice} from '@sqlrooms/room-shell';\nimport {produce} from 'immer';\nimport {StateCreator} from 'zustand';\nimport {S3Config} from '@sqlrooms/s3-browser-config';\n\nexport type S3BrowserState = {\n s3Browser: {\n // current in credential form\n currentS3Config: S3Config | null;\n // Credential management\n setCurrentS3Config: (s3Config: S3Config) => void;\n clearCurrentS3Config: () => void;\n };\n};\n\n// Create the store\nexport function createS3BrowserSlice(): StateCreator<S3BrowserState> {\n return createSlice<S3BrowserState>((set) => ({\n // Initial state\n s3Browser: {\n currentS3Config: null,\n\n // Actions\n setCurrentS3Config: (s3Config) => {\n set((state) =>\n produce(state, (draft) => {\n draft.s3Browser.currentS3Config = s3Config;\n }),\n );\n },\n clearCurrentS3Config: () => {\n set((state) =>\n produce(state, (draft) => {\n draft.s3Browser.currentS3Config = null;\n }),\n );\n },\n },\n }));\n}\n"]}
@@ -0,0 +1,65 @@
1
+ import * as z from 'zod';
2
+ import { S3Config, S3Credentials } from '@sqlrooms/s3-browser-config';
3
+ declare const formSchema: z.ZodObject<{
4
+ accessKeyId: z.ZodString;
5
+ secretAccessKey: z.ZodString;
6
+ sessionToken: z.ZodOptional<z.ZodString>;
7
+ region: z.ZodString;
8
+ bucket: z.ZodString;
9
+ name: z.ZodOptional<z.ZodString>;
10
+ saveCredential: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
11
+ }, z.core.$strip>;
12
+ type FormData = z.infer<typeof formSchema>;
13
+ /**
14
+ * A form component for managing S3 credentials and connections.
15
+ *
16
+ * This component provides:
17
+ * - Input fields for S3 credentials (access key, secret key, region, bucket)
18
+ * - Option to save credentials for later use
19
+ * - Ability to paste AWS credentials export format
20
+ * - Management of saved credentials
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const handleConnect = async (credentials) => {
25
+ * // Handle the connection
26
+ * console.log('Connecting with:', credentials);
27
+ * };
28
+ *
29
+ * const handleSaveCredential = async (config) => {
30
+ * // Save the credential to your storage
31
+ * await saveToStorage(config);
32
+ * };
33
+ *
34
+ * const handleLoadCredentials = async () => {
35
+ * // Load saved credentials from your storage
36
+ * return await loadFromStorage();
37
+ * };
38
+ *
39
+ * const handleDeleteCredential = async (id) => {
40
+ * // Delete a saved credential
41
+ * await deleteFromStorage(id);
42
+ * };
43
+ *
44
+ * return (
45
+ * <S3CredentialsForm
46
+ * onConnect={handleConnect}
47
+ * isLoading={false}
48
+ * saveS3Credentials={handleSaveCredential}
49
+ * loadS3Credentials={handleLoadCredentials}
50
+ * deleteS3Credentials={handleDeleteCredential}
51
+ * />
52
+ * );
53
+ * ```
54
+ */
55
+ export type S3CredentialsFormProps = {
56
+ onConnect: (data: FormData) => void;
57
+ isLoading?: boolean;
58
+ saveS3Credentials: (data: S3Config) => Promise<void>;
59
+ loadS3Credentials: () => Promise<S3Credentials[]>;
60
+ deleteS3Credentials?: (id: string) => Promise<void>;
61
+ onInputChange?: () => void;
62
+ };
63
+ export declare function S3CredentialsForm({ onConnect, isLoading, saveS3Credentials, loadS3Credentials, deleteS3Credentials, onInputChange, }: S3CredentialsFormProps): import("react/jsx-runtime").JSX.Element;
64
+ export {};
65
+ //# sourceMappingURL=S3CredentialsForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3CredentialsForm.d.ts","sourceRoot":"","sources":["../src/S3CredentialsForm.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AAEzB,OAAO,EAAC,QAAQ,EAAE,aAAa,EAAC,MAAM,6BAA6B,CAAC;AA8CpE,QAAA,MAAM,UAAU;;;;;;;;iBAQd,CAAC;AAEH,KAAK,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AA2D3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IACpC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,iBAAiB,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,iBAAiB,EAAE,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAClD,mBAAmB,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAChC,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,aAAa,GACd,EAAE,sBAAsB,2CA8exB"}
@@ -0,0 +1,208 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect } from 'react';
3
+ import { useForm } from 'react-hook-form';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import * as z from 'zod';
6
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@sqlrooms/ui';
7
+ import { Input, Textarea, Button, Checkbox, Form, FormControl, FormField, FormItem, FormLabel, FormMessage, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Tooltip, TooltipTrigger, TooltipContent, } from '@sqlrooms/ui';
8
+ import { Eye, EyeOff, Upload, Check, AlertCircle, Info, PlusIcon, Database, Trash2, } from 'lucide-react';
9
+ const S3_REGIONS = [
10
+ { value: 'us-east-1', label: 'us-east-1' },
11
+ { value: 'us-east-2', label: 'us-east-2' },
12
+ { value: 'us-west-1', label: 'us-west-1' },
13
+ { value: 'us-west-2', label: 'us-west-2' },
14
+ { value: 'eu-west-1', label: 'eu-west-1' },
15
+ { value: 'eu-central-1', label: 'eu-central-1' },
16
+ { value: 'ap-northeast-1', label: 'ap-northeast-1' },
17
+ { value: 'ap-southeast-1', label: 'ap-southeast-1' },
18
+ ];
19
+ const formSchema = z.object({
20
+ accessKeyId: z.string().min(1, 'Required'),
21
+ secretAccessKey: z.string().min(1, 'Required'),
22
+ sessionToken: z.string().optional(),
23
+ region: z.string().min(1, 'Required'),
24
+ bucket: z.string().min(1, 'Required'),
25
+ name: z.string().max(100, 'Max 100 characters').optional(),
26
+ saveCredential: z.boolean().default(false).optional(),
27
+ });
28
+ const parseAWSExport = (text) => {
29
+ const lines = text.split('\n');
30
+ const parsed = {};
31
+ for (const line of lines) {
32
+ const trimmed = line.trim();
33
+ if (trimmed.startsWith('export AWS_ACCESS_KEY_ID=')) {
34
+ parsed.accessKeyId = trimmed.split('=')[1]?.replace(/['"]/g, '') || '';
35
+ }
36
+ else if (trimmed.startsWith('export AWS_SECRET_ACCESS_KEY=')) {
37
+ parsed.secretAccessKey =
38
+ trimmed.split('=')[1]?.replace(/['"]/g, '') || '';
39
+ }
40
+ else if (trimmed.startsWith('export AWS_SESSION_TOKEN=')) {
41
+ parsed.sessionToken = trimmed.split('=')[1]?.replace(/['"]/g, '') || '';
42
+ }
43
+ else if (trimmed.startsWith('export AWS_DEFAULT_REGION=')) {
44
+ parsed.region = trimmed.split('=')[1]?.replace(/['"]/g, '') || '';
45
+ }
46
+ }
47
+ return parsed;
48
+ };
49
+ const parseCredentialProcess = (text) => {
50
+ try {
51
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
52
+ if (jsonMatch) {
53
+ const jsonStr = jsonMatch[0];
54
+ const raw = JSON.parse(jsonStr);
55
+ const parsed = {};
56
+ if (raw.AccessKeyId) {
57
+ parsed.accessKeyId = raw.AccessKeyId;
58
+ }
59
+ if (raw.SecretAccessKey) {
60
+ parsed.secretAccessKey = raw.SecretAccessKey;
61
+ }
62
+ if (raw.SessionToken) {
63
+ parsed.sessionToken = raw.SessionToken;
64
+ }
65
+ if (raw.region) {
66
+ // Check if region is present
67
+ parsed.region = raw.region;
68
+ }
69
+ return parsed;
70
+ }
71
+ }
72
+ catch (e) {
73
+ console.error('Failed to parse JSON:', e);
74
+ }
75
+ return {};
76
+ };
77
+ export function S3CredentialsForm({ onConnect, isLoading, saveS3Credentials, loadS3Credentials, deleteS3Credentials, onInputChange, }) {
78
+ const [showSecrets, setShowSecrets] = useState({
79
+ secretAccessKey: false,
80
+ sessionToken: false,
81
+ });
82
+ const [pasteText, setPasteText] = useState('');
83
+ const [notification, setNotification] = useState({
84
+ show: false,
85
+ message: '',
86
+ type: 'success',
87
+ });
88
+ const [savedCredentials, setSavedCredentials] = useState([]);
89
+ const [tab, setTab] = useState('new');
90
+ // Load saved connections on component mount
91
+ useEffect(() => {
92
+ let isMounted = true;
93
+ async function fetchConnections() {
94
+ // You can await here
95
+ const response = await loadS3Credentials();
96
+ if (response && response.length > 0 && isMounted) {
97
+ // Assuming loadS3Credentials returns an array of connections
98
+ setSavedCredentials(response);
99
+ }
100
+ }
101
+ fetchConnections();
102
+ return () => {
103
+ isMounted = false;
104
+ };
105
+ }, []);
106
+ const resolver = zodResolver(formSchema);
107
+ const form = useForm({
108
+ resolver,
109
+ defaultValues: {
110
+ accessKeyId: '',
111
+ secretAccessKey: '',
112
+ sessionToken: '',
113
+ region: 'us-east-1',
114
+ bucket: '',
115
+ saveCredential: false,
116
+ },
117
+ });
118
+ const toggleVisibility = useCallback((field) => {
119
+ setShowSecrets((prev) => ({
120
+ ...prev,
121
+ [field]: !prev[field],
122
+ }));
123
+ }, [setShowSecrets]);
124
+ const showNotification = useCallback((message, type = 'success') => {
125
+ setNotification({ show: true, message, type });
126
+ setTimeout(() => setNotification({ show: false, message: '', type: 'success' }), 3000);
127
+ }, [setNotification]);
128
+ const handleAutofill = useCallback(() => {
129
+ if (!pasteText.trim()) {
130
+ showNotification('Please paste in aws credential first', 'error');
131
+ return;
132
+ }
133
+ let parsed = null;
134
+ // Try parsing as export format first
135
+ if (pasteText.includes('export AWS_')) {
136
+ parsed = parseAWSExport(pasteText);
137
+ }
138
+ // Try parsing as credential_process JSON format
139
+ else if (pasteText.includes('AccessKeyId')) {
140
+ parsed = parseCredentialProcess(pasteText);
141
+ }
142
+ if (parsed) {
143
+ for (const key in parsed) {
144
+ if (key in parsed) {
145
+ form.setValue(key, parsed[key] || '');
146
+ }
147
+ }
148
+ showNotification('Credentials auto-filled successfully!');
149
+ setPasteText('');
150
+ onInputChange?.();
151
+ }
152
+ else {
153
+ showNotification('Could not parse credentials. Please check the format.', 'error');
154
+ }
155
+ }, [pasteText, form, showNotification, onInputChange]);
156
+ const handleSubmit = useCallback(async (data) => {
157
+ onConnect(data);
158
+ if (data.saveCredential) {
159
+ try {
160
+ await saveS3Credentials({
161
+ accessKeyId: data.accessKeyId,
162
+ secretAccessKey: data.secretAccessKey,
163
+ sessionToken: data.sessionToken || undefined,
164
+ region: data.region,
165
+ bucket: data.bucket,
166
+ name: data.name || `${data.bucket}-${data.region}`,
167
+ });
168
+ showNotification('Connection saved successfully!');
169
+ }
170
+ catch (err) {
171
+ showNotification(`Error saving to S3: ${err}`, 'error');
172
+ }
173
+ }
174
+ }, [onConnect, saveS3Credentials, showNotification]);
175
+ const deleteCredential = useCallback(async (id) => {
176
+ if (deleteS3Credentials) {
177
+ await deleteS3Credentials(id);
178
+ const credentials = await loadS3Credentials();
179
+ setSavedCredentials(credentials);
180
+ }
181
+ }, [deleteS3Credentials, setSavedCredentials, loadS3Credentials]);
182
+ // Add a handler for input changes
183
+ const handleInputChange = useCallback(() => {
184
+ onInputChange?.();
185
+ }, [onInputChange]);
186
+ return (_jsxs(Tabs, { value: tab, onValueChange: setTab, orientation: "vertical", defaultValue: "new", className: "flex w-full gap-6 px-4", children: [_jsxs(TabsList, { className: "mb-4 flex h-auto flex-col items-stretch justify-start gap-4 border-r bg-transparent pr-6", children: [_jsxs(TabsTrigger, { value: "new", className: "data-[state=active]:border-primary data-[state=active]:text-primary rounded-md border px-4 py-2 data-[state=active]:shadow-none", children: [_jsx(PlusIcon, { size: 16, className: "mr-2" }), "New Credential"] }), _jsxs(TabsTrigger, { value: "saved", className: "data-[state=active]:border-primary data-[state=active]:text-primary rounded-md border px-4 py-2 data-[state=active]:shadow-none", children: [_jsx(Database, { size: 16, className: "mr-2" }), "Saved Credential"] })] }), _jsx(TabsContent, { value: "new", className: "mt-0 w-full", children: _jsx(Form, { ...form, children: _jsx("form", { onSubmit: form.handleSubmit(handleSubmit), className: "w-full", children: _jsxs("div", { className: "grid h-full w-full grid-cols-[240px_1fr] gap-8", children: [_jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(FormField, { control: form.control, name: "bucket", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Bucket Name" }), _jsx(FormControl, { children: _jsx(Input, { ...field, type: "text", placeholder: "my-bucket-name", onChange: (e) => {
187
+ field.onChange(e);
188
+ handleInputChange();
189
+ } }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "region", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Region" }), _jsx(FormControl, { children: _jsxs(Select, { onValueChange: (e) => {
190
+ field.onChange(e);
191
+ handleInputChange();
192
+ }, defaultValue: field.value, children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Select a Region" }) }), _jsx(SelectContent, { children: S3_REGIONS.map((region) => (_jsx(SelectItem, { value: region.value, children: region.label }, region.value))) })] }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "name", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Connection Name (Optional)" }), _jsx(FormControl, { children: _jsx(Input, { ...field, type: "text", placeholder: "My S3 Connection" }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "saveCredential", render: ({ field }) => (_jsxs(FormItem, { className: "flex items-center space-x-2 space-y-0", children: [_jsx(FormControl, { children: _jsx(Checkbox, { onCheckedChange: field.onChange, checked: field.value, className: "h-4 w-4" }) }), _jsx(FormLabel, { children: "Save credential" }), _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Info, { size: 16, className: "text-muted-foreground hover:cursor-pointer" }) }), _jsx(TooltipContent, { side: "bottom", children: _jsx("pre", { className: "w-[300px] text-wrap break-words text-xs", children: "Save this credential securely on your computer for future use. Credentials will be encrypted and stored locally" }) })] }), _jsx(FormMessage, {})] })) }), notification.show && (_jsxs("div", { className: `mb-4 flex items-center gap-2 rounded-lg p-3 text-sm ${notification.type === 'error'
193
+ ? 'text-destructive-foreground bg-destructive'
194
+ : 'bg-green-50 text-green-700'}`, children: [notification.type === 'error' ? (_jsx(AlertCircle, { size: 16 })) : (_jsx(Check, { size: 16 })), notification.message] }))] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-sm font-medium leading-none", children: "Auto-fill Credentials" }), _jsx(Textarea, { value: pasteText, onChange: (e) => setPasteText(e.target.value), placeholder: 'Paste AWS cli command output here...\n\nExample export:\nexport AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nexport AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/b...\nexport AWS_SESSION_TOKEN=AQoEXAMPLEH4aoAH0gNCAPy...\n\nExample credential_process output:\n{\n "AccessKeyId": "AKIAIOSFODNN7EXAMPLE",\n "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCY...",\n "SessionToken": "AQoEXAMPLEH4aoAH0gNCAPy..."\n}', className: "h-40" })] }), _jsxs("div", { className: "flex gap-3", children: [_jsxs(Button, { variant: "secondary", onClick: (e) => {
195
+ e.preventDefault();
196
+ handleAutofill();
197
+ }, children: [_jsx(Upload, { size: 16 }), "Auto-fill Credentials"] }), _jsx(Button, { variant: "outline", onClick: () => setPasteText(''), children: "Clear" })] }), _jsx(FormField, { control: form.control, name: "accessKeyId", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Access Key Id" }), _jsx(FormControl, { children: _jsx(Input, { ...field, type: "text", className: "w-full rounded-md border px-3 py-2", placeholder: "AKIAXXXXXXXXXXXXXXXX", onChange: (e) => {
198
+ field.onChange(e);
199
+ handleInputChange();
200
+ } }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "secretAccessKey", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Secret Access Key" }), _jsx(FormControl, { children: _jsxs("div", { className: "relative", children: [_jsx(Input, { ...field, type: showSecrets.secretAccessKey ? 'text' : 'password', placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", className: "pr-8", onChange: (e) => {
201
+ field.onChange(e);
202
+ handleInputChange();
203
+ } }), _jsx("div", { className: "text-muted-foreground absolute right-3 top-0 flex h-9 gap-1", children: _jsx("button", { type: "button", onClick: () => toggleVisibility('secretAccessKey'), className: "ml-1", children: showSecrets.secretAccessKey ? (_jsx(EyeOff, { size: 16 })) : (_jsx(Eye, { size: 16 })) }) })] }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "sessionToken", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Session Token (Optional - for temporary credentials)" }), _jsx(FormControl, { children: _jsxs("div", { className: "relative", children: [_jsx(Input, { ...field, type: showSecrets.sessionToken ? 'text' : 'password', placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", className: "pr-8", onChange: (e) => {
204
+ field.onChange(e);
205
+ handleInputChange();
206
+ } }), _jsx("div", { className: "text-muted-foreground absolute right-3 top-0 flex h-9 gap-1", children: _jsx("button", { type: "button", onClick: () => toggleVisibility('sessionToken'), className: "ml-1", children: showSecrets.sessionToken ? (_jsx(EyeOff, { size: 16 })) : (_jsx(Eye, { size: 16 })) }) })] }) }), _jsx(FormMessage, {})] })) }), _jsx(Button, { type: "submit", disabled: form.formState.isSubmitting || !form.formState.isValid, className: "w-full", children: isLoading ? 'Connecting...' : 'Connect to S3' })] })] }) }) }) }), _jsx(TabsContent, { value: "saved", className: "mt-0 w-full", children: _jsx("div", { className: "space-y-4", children: savedCredentials.length === 0 ? (_jsxs("div", { className: "flex flex-col items-center justify-between py-12", children: [_jsx(Database, { className: "text-muted-foreground mx-auto mb-4 h-8 w-8" }), _jsx("h3", { className: "mb-2 text-lg font-medium", children: "No saved credentials" }), _jsx("p", { className: "text-muted-foreground", children: "Create your first credential to get started" })] })) : (_jsx("div", { className: "grid gap-4", children: savedCredentials.map((credential) => (_jsx("div", { className: "rounded-lg border p-4 transition-shadow hover:cursor-pointer hover:shadow-sm", children: _jsxs("div", { className: "flex items-start justify-between", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 font-semibold", children: credential.name }), _jsx("p", { className: "text-muted-foreground mb-2 text-sm", children: credential.bucket }), _jsxs("div", { className: "text-muted-foreground flex items-center space-x-4 text-xs", children: [_jsxs("span", { children: ["Region: ", credential.region] }), _jsxs("span", { children: ["Created:", ' ', new Date(credential.createdAt).toLocaleDateString()] })] })] }), _jsxs("div", { className: "flex space-x-2", children: [_jsx("button", { onClick: () => onConnect(credential), className: "text-primary rounded-md bg-blue-100 px-3 py-1 text-sm hover:bg-blue-200", children: "Connect" }), deleteCredential ? (_jsx("button", { onClick: () => deleteCredential(credential.id), className: "rounded-md bg-red-100 px-3 py-1 text-sm text-red-700 hover:bg-red-200", children: _jsx(Trash2, { className: "h-4 w-4" }) })) : null] })] }) }, credential.id))) })) }) })] }));
207
+ }
208
+ //# sourceMappingURL=S3CredentialsForm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"S3CredentialsForm.js","sourceRoot":"","sources":["../src/S3CredentialsForm.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, {useState, useCallback, useEffect} from 'react';\nimport {useForm, SubmitHandler} from 'react-hook-form';\nimport {zodResolver} from '@hookform/resolvers/zod';\nimport * as z from 'zod';\nimport {Tabs, TabsList, TabsTrigger, TabsContent} from '@sqlrooms/ui';\nimport {S3Config, S3Credentials} from '@sqlrooms/s3-browser-config';\n\nimport {\n Input,\n Textarea,\n Button,\n Checkbox,\n Form,\n FormControl,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n Select,\n SelectTrigger,\n SelectValue,\n SelectContent,\n SelectItem,\n Tooltip,\n TooltipTrigger,\n TooltipContent,\n} from '@sqlrooms/ui';\n\nimport {\n Eye,\n EyeOff,\n Upload,\n Check,\n AlertCircle,\n Info,\n PlusIcon,\n Database,\n Trash2,\n} from 'lucide-react';\n\nconst S3_REGIONS = [\n {value: 'us-east-1', label: 'us-east-1'},\n {value: 'us-east-2', label: 'us-east-2'},\n {value: 'us-west-1', label: 'us-west-1'},\n {value: 'us-west-2', label: 'us-west-2'},\n {value: 'eu-west-1', label: 'eu-west-1'},\n {value: 'eu-central-1', label: 'eu-central-1'},\n {value: 'ap-northeast-1', label: 'ap-northeast-1'},\n {value: 'ap-southeast-1', label: 'ap-southeast-1'},\n];\n\nconst formSchema = z.object({\n accessKeyId: z.string().min(1, 'Required'),\n secretAccessKey: z.string().min(1, 'Required'),\n sessionToken: z.string().optional(),\n region: z.string().min(1, 'Required'),\n bucket: z.string().min(1, 'Required'),\n name: z.string().max(100, 'Max 100 characters').optional(),\n saveCredential: z.boolean().default(false).optional(),\n});\n\ntype FormData = z.infer<typeof formSchema>;\n\ntype ParsedType = {\n accessKeyId?: string;\n secretAccessKey?: string;\n sessionToken?: string;\n region?: string;\n};\n\nconst parseAWSExport = (text: string): ParsedType => {\n const lines = text.split('\\n');\n const parsed = {} as Record<string, string>;\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (trimmed.startsWith('export AWS_ACCESS_KEY_ID=')) {\n parsed.accessKeyId = trimmed.split('=')[1]?.replace(/['\"]/g, '') || '';\n } else if (trimmed.startsWith('export AWS_SECRET_ACCESS_KEY=')) {\n parsed.secretAccessKey =\n trimmed.split('=')[1]?.replace(/['\"]/g, '') || '';\n } else if (trimmed.startsWith('export AWS_SESSION_TOKEN=')) {\n parsed.sessionToken = trimmed.split('=')[1]?.replace(/['\"]/g, '') || '';\n } else if (trimmed.startsWith('export AWS_DEFAULT_REGION=')) {\n parsed.region = trimmed.split('=')[1]?.replace(/['\"]/g, '') || '';\n }\n }\n\n return parsed;\n};\n\nconst parseCredentialProcess = (text: string): ParsedType => {\n try {\n const jsonMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n const jsonStr = jsonMatch[0];\n const raw = JSON.parse(jsonStr);\n const parsed = {} as Record<string, string>;\n\n if (raw.AccessKeyId) {\n parsed.accessKeyId = raw.AccessKeyId;\n }\n if (raw.SecretAccessKey) {\n parsed.secretAccessKey = raw.SecretAccessKey;\n }\n if (raw.SessionToken) {\n parsed.sessionToken = raw.SessionToken;\n }\n if (raw.region) {\n // Check if region is present\n parsed.region = raw.region;\n }\n return parsed;\n }\n } catch (e) {\n console.error('Failed to parse JSON:', e);\n }\n return {};\n};\n\n/**\n * A form component for managing S3 credentials and connections.\n *\n * This component provides:\n * - Input fields for S3 credentials (access key, secret key, region, bucket)\n * - Option to save credentials for later use\n * - Ability to paste AWS credentials export format\n * - Management of saved credentials\n *\n * @example\n * ```tsx\n * const handleConnect = async (credentials) => {\n * // Handle the connection\n * console.log('Connecting with:', credentials);\n * };\n *\n * const handleSaveCredential = async (config) => {\n * // Save the credential to your storage\n * await saveToStorage(config);\n * };\n *\n * const handleLoadCredentials = async () => {\n * // Load saved credentials from your storage\n * return await loadFromStorage();\n * };\n *\n * const handleDeleteCredential = async (id) => {\n * // Delete a saved credential\n * await deleteFromStorage(id);\n * };\n *\n * return (\n * <S3CredentialsForm\n * onConnect={handleConnect}\n * isLoading={false}\n * saveS3Credentials={handleSaveCredential}\n * loadS3Credentials={handleLoadCredentials}\n * deleteS3Credentials={handleDeleteCredential}\n * />\n * );\n * ```\n */\nexport type S3CredentialsFormProps = {\n onConnect: (data: FormData) => void;\n isLoading?: boolean;\n saveS3Credentials: (data: S3Config) => Promise<void>;\n loadS3Credentials: () => Promise<S3Credentials[]>;\n deleteS3Credentials?: (id: string) => Promise<void>;\n onInputChange?: () => void;\n};\n\nexport function S3CredentialsForm({\n onConnect,\n isLoading,\n saveS3Credentials,\n loadS3Credentials,\n deleteS3Credentials,\n onInputChange,\n}: S3CredentialsFormProps) {\n const [showSecrets, setShowSecrets] = useState({\n secretAccessKey: false,\n sessionToken: false,\n });\n const [pasteText, setPasteText] = useState('');\n const [notification, setNotification] = useState({\n show: false,\n message: '',\n type: 'success',\n });\n const [savedCredentials, setSavedCredentials] = useState<S3Credentials[]>([]);\n const [tab, setTab] = useState('new');\n\n // Load saved connections on component mount\n useEffect(() => {\n let isMounted = true;\n async function fetchConnections() {\n // You can await here\n const response = await loadS3Credentials();\n if (response && response.length > 0 && isMounted) {\n // Assuming loadS3Credentials returns an array of connections\n setSavedCredentials(response);\n }\n }\n fetchConnections();\n return () => {\n isMounted = false;\n };\n }, []);\n\n const resolver = zodResolver(formSchema);\n const form = useForm<FormData>({\n resolver,\n defaultValues: {\n accessKeyId: '',\n secretAccessKey: '',\n sessionToken: '',\n region: 'us-east-1',\n bucket: '',\n saveCredential: false,\n },\n });\n const toggleVisibility = useCallback(\n (field: 'secretAccessKey' | 'sessionToken') => {\n setShowSecrets((prev) => ({\n ...prev,\n [field]: !prev[field],\n }));\n },\n [setShowSecrets],\n );\n const showNotification = useCallback(\n (message: string, type = 'success') => {\n setNotification({show: true, message, type});\n setTimeout(\n () => setNotification({show: false, message: '', type: 'success'}),\n 3000,\n );\n },\n [setNotification],\n );\n\n const handleAutofill = useCallback(() => {\n if (!pasteText.trim()) {\n showNotification('Please paste in aws credential first', 'error');\n return;\n }\n\n let parsed = null;\n\n // Try parsing as export format first\n if (pasteText.includes('export AWS_')) {\n parsed = parseAWSExport(pasteText);\n }\n // Try parsing as credential_process JSON format\n else if (pasteText.includes('AccessKeyId')) {\n parsed = parseCredentialProcess(pasteText);\n }\n\n if (parsed) {\n for (const key in parsed) {\n if (key in parsed) {\n form.setValue(\n key as keyof ParsedType,\n parsed[key as keyof ParsedType] || '',\n );\n }\n }\n\n showNotification('Credentials auto-filled successfully!');\n setPasteText('');\n onInputChange?.();\n } else {\n showNotification(\n 'Could not parse credentials. Please check the format.',\n 'error',\n );\n }\n }, [pasteText, form, showNotification, onInputChange]);\n\n const handleSubmit: SubmitHandler<FormData> = useCallback(\n async (data: FormData) => {\n onConnect(data);\n if (data.saveCredential) {\n try {\n await saveS3Credentials({\n accessKeyId: data.accessKeyId,\n secretAccessKey: data.secretAccessKey,\n sessionToken: data.sessionToken || undefined,\n region: data.region,\n bucket: data.bucket,\n name: data.name || `${data.bucket}-${data.region}`,\n });\n showNotification('Connection saved successfully!');\n } catch (err) {\n showNotification(`Error saving to S3: ${err}`, 'error');\n }\n }\n },\n [onConnect, saveS3Credentials, showNotification],\n );\n\n const deleteCredential = useCallback(\n async (id: string) => {\n if (deleteS3Credentials) {\n await deleteS3Credentials(id);\n const credentials = await loadS3Credentials();\n setSavedCredentials(credentials);\n }\n },\n [deleteS3Credentials, setSavedCredentials, loadS3Credentials],\n );\n\n // Add a handler for input changes\n const handleInputChange = useCallback(() => {\n onInputChange?.();\n }, [onInputChange]);\n\n return (\n <Tabs\n value={tab}\n onValueChange={setTab}\n orientation=\"vertical\"\n defaultValue=\"new\"\n className=\"flex w-full gap-6 px-4\"\n >\n <TabsList className=\"mb-4 flex h-auto flex-col items-stretch justify-start gap-4 border-r bg-transparent pr-6\">\n <TabsTrigger\n value=\"new\"\n className=\"data-[state=active]:border-primary data-[state=active]:text-primary rounded-md border px-4 py-2 data-[state=active]:shadow-none\"\n >\n <PlusIcon size={16} className=\"mr-2\" />\n New Credential\n </TabsTrigger>\n <TabsTrigger\n value=\"saved\"\n className=\"data-[state=active]:border-primary data-[state=active]:text-primary rounded-md border px-4 py-2 data-[state=active]:shadow-none\"\n >\n <Database size={16} className=\"mr-2\" />\n Saved Credential\n </TabsTrigger>\n </TabsList>\n <TabsContent value=\"new\" className=\"mt-0 w-full\">\n <Form<FormData> {...form}>\n <form onSubmit={form.handleSubmit(handleSubmit)} className=\"w-full\">\n <div className=\"grid h-full w-full grid-cols-[240px_1fr] gap-8\">\n <div className=\"flex flex-col gap-4\">\n <FormField<FormData, 'bucket'>\n control={form.control}\n name=\"bucket\"\n render={({field}) => (\n <FormItem>\n <FormLabel>Bucket Name</FormLabel>\n <FormControl>\n <Input\n {...field}\n type=\"text\"\n placeholder=\"my-bucket-name\"\n onChange={(e) => {\n field.onChange(e);\n handleInputChange();\n }}\n />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField<FormData, 'region'>\n control={form.control}\n name=\"region\"\n render={({field}) => (\n <FormItem>\n <FormLabel>Region</FormLabel>\n <FormControl>\n <Select\n onValueChange={(e) => {\n field.onChange(e);\n handleInputChange();\n }}\n defaultValue={field.value}\n >\n <SelectTrigger>\n <SelectValue placeholder=\"Select a Region\" />\n </SelectTrigger>\n <SelectContent>\n {S3_REGIONS.map((region) => (\n <SelectItem\n key={region.value}\n value={region.value}\n >\n {region.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField<FormData, 'name'>\n control={form.control}\n name=\"name\"\n render={({field}) => (\n <FormItem>\n <FormLabel>Connection Name (Optional)</FormLabel>\n <FormControl>\n <Input\n {...field}\n type=\"text\"\n placeholder=\"My S3 Connection\"\n />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField<FormData, 'saveCredential'>\n control={form.control}\n name=\"saveCredential\"\n render={({field}) => (\n <FormItem className=\"flex items-center space-x-2 space-y-0\">\n <FormControl>\n <Checkbox\n onCheckedChange={field.onChange}\n checked={field.value}\n className=\"h-4 w-4\"\n />\n </FormControl>\n <FormLabel>Save credential</FormLabel>\n <Tooltip>\n <TooltipTrigger asChild>\n <Info\n size={16}\n className=\"text-muted-foreground hover:cursor-pointer\"\n />\n </TooltipTrigger>\n <TooltipContent side=\"bottom\">\n <pre className=\"w-[300px] text-wrap break-words text-xs\">\n Save this credential securely on your computer for\n future use. Credentials will be encrypted and stored\n locally\n </pre>\n </TooltipContent>\n </Tooltip>\n <FormMessage />\n </FormItem>\n )}\n />\n\n {notification.show && (\n <div\n className={`mb-4 flex items-center gap-2 rounded-lg p-3 text-sm ${\n notification.type === 'error'\n ? 'text-destructive-foreground bg-destructive'\n : 'bg-green-50 text-green-700'\n }`}\n >\n {notification.type === 'error' ? (\n <AlertCircle size={16} />\n ) : (\n <Check size={16} />\n )}\n {notification.message}\n </div>\n )}\n </div>\n\n {/* From Section */}\n <div className=\"flex flex-col gap-4\">\n {/* Auto-fill Section */}\n <div className=\"space-y-2\">\n <label className=\"text-sm font-medium leading-none\">\n Auto-fill Credentials\n </label>\n <Textarea\n value={pasteText}\n onChange={(e) => setPasteText(e.target.value)}\n placeholder='Paste AWS cli command output here...&#10;&#10;Example export:&#10;export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE&#10;export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/b...&#10;export AWS_SESSION_TOKEN=AQoEXAMPLEH4aoAH0gNCAPy...&#10;&#10;Example credential_process output:&#10;{&#10; \"AccessKeyId\": \"AKIAIOSFODNN7EXAMPLE\",&#10; \"SecretAccessKey\": \"wJalrXUtnFEMI/K7MDENG/bPxRfiCY...\",&#10; \"SessionToken\": \"AQoEXAMPLEH4aoAH0gNCAPy...\"&#10;}'\n className=\"h-40\"\n />\n </div>\n <div className=\"flex gap-3\">\n <Button\n variant=\"secondary\"\n onClick={(e) => {\n e.preventDefault();\n handleAutofill();\n }}\n >\n <Upload size={16} />\n Auto-fill Credentials\n </Button>\n <Button variant=\"outline\" onClick={() => setPasteText('')}>\n Clear\n </Button>\n </div>\n <FormField<FormData, 'accessKeyId'>\n control={form.control}\n name=\"accessKeyId\"\n render={({field}) => (\n <FormItem>\n <FormLabel>Access Key Id</FormLabel>\n <FormControl>\n <Input\n {...field}\n type=\"text\"\n className=\"w-full rounded-md border px-3 py-2\"\n placeholder=\"AKIAXXXXXXXXXXXXXXXX\"\n onChange={(e) => {\n field.onChange(e);\n handleInputChange();\n }}\n />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <FormField<FormData, 'secretAccessKey'>\n control={form.control}\n name=\"secretAccessKey\"\n render={({field}) => (\n <FormItem>\n <FormLabel>Secret Access Key</FormLabel>\n <FormControl>\n <div className=\"relative\">\n <Input\n {...field}\n type={\n showSecrets.secretAccessKey ? 'text' : 'password'\n }\n placeholder=\"••••••••••••••••••••••••\"\n className=\"pr-8\"\n onChange={(e) => {\n field.onChange(e);\n handleInputChange();\n }}\n />\n <div className=\"text-muted-foreground absolute right-3 top-0 flex h-9 gap-1\">\n <button\n type=\"button\"\n onClick={() =>\n toggleVisibility('secretAccessKey')\n }\n className=\"ml-1\"\n >\n {showSecrets.secretAccessKey ? (\n <EyeOff size={16} />\n ) : (\n <Eye size={16} />\n )}\n </button>\n </div>\n </div>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField<FormData, 'sessionToken'>\n control={form.control}\n name=\"sessionToken\"\n render={({field}) => (\n <FormItem>\n <FormLabel>\n Session Token (Optional - for temporary credentials)\n </FormLabel>\n <FormControl>\n <div className=\"relative\">\n <Input\n {...field}\n type={\n showSecrets.sessionToken ? 'text' : 'password'\n }\n placeholder=\"••••••••••••••••••••••••\"\n className=\"pr-8\"\n onChange={(e) => {\n field.onChange(e);\n handleInputChange();\n }}\n />\n <div className=\"text-muted-foreground absolute right-3 top-0 flex h-9 gap-1\">\n <button\n type=\"button\"\n onClick={() => toggleVisibility('sessionToken')}\n className=\"ml-1\"\n >\n {showSecrets.sessionToken ? (\n <EyeOff size={16} />\n ) : (\n <Eye size={16} />\n )}\n </button>\n </div>\n </div>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n <Button\n type=\"submit\"\n disabled={\n form.formState.isSubmitting || !form.formState.isValid\n }\n className=\"w-full\"\n >\n {isLoading ? 'Connecting...' : 'Connect to S3'}\n </Button>\n </div>\n </div>\n </form>\n </Form>\n </TabsContent>\n <TabsContent value=\"saved\" className=\"mt-0 w-full\">\n <div className=\"space-y-4\">\n {savedCredentials.length === 0 ? (\n <div className=\"flex flex-col items-center justify-between py-12\">\n <Database className=\"text-muted-foreground mx-auto mb-4 h-8 w-8\" />\n <h3 className=\"mb-2 text-lg font-medium\">No saved credentials</h3>\n <p className=\"text-muted-foreground\">\n Create your first credential to get started\n </p>\n </div>\n ) : (\n <div className=\"grid gap-4\">\n {savedCredentials.map((credential) => (\n <div\n key={credential.id}\n className=\"rounded-lg border p-4 transition-shadow hover:cursor-pointer hover:shadow-sm\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex-1\">\n <h3 className=\"mb-1 font-semibold\">{credential.name}</h3>\n <p className=\"text-muted-foreground mb-2 text-sm\">\n {credential.bucket}\n </p>\n <div className=\"text-muted-foreground flex items-center space-x-4 text-xs\">\n <span>Region: {credential.region}</span>\n <span>\n Created:{' '}\n {new Date(credential.createdAt).toLocaleDateString()}\n </span>\n </div>\n </div>\n <div className=\"flex space-x-2\">\n <button\n onClick={() => onConnect(credential)}\n className=\"text-primary rounded-md bg-blue-100 px-3 py-1 text-sm hover:bg-blue-200\"\n >\n Connect\n </button>\n {deleteCredential ? (\n <button\n onClick={() => deleteCredential(credential.id)}\n className=\"rounded-md bg-red-100 px-3 py-1 text-sm text-red-700 hover:bg-red-200\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n ) : null}\n </div>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </TabsContent>\n </Tabs>\n );\n}\n"]}
@@ -1,5 +1,5 @@
1
+ import { S3FileOrDirectory } from '@sqlrooms/s3-browser-config';
1
2
  import { FC } from 'react';
2
- import { S3FileOrDirectory } from './S3FileOrDirectory';
3
3
  /**
4
4
  * A file browser component for navigating and selecting files from an S3-like storage.
5
5
  *
@@ -52,6 +52,7 @@ declare const S3FileBrowser: FC<{
52
52
  onCanConfirmChange: (canConfirm: boolean) => void;
53
53
  onChangeSelectedDirectory: (directory: string) => void;
54
54
  onChangeSelectedFiles: (files: string[]) => void;
55
+ renderFileActions?: () => React.ReactNode;
55
56
  }>;
56
57
  export default S3FileBrowser;
57
58
  //# sourceMappingURL=S3FileBrowser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"S3FileBrowser.d.ts","sourceRoot":"","sources":["../src/S3FileBrowser.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAAC,EAAE,EAAkC,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,QAAA,MAAM,aAAa,EAAE,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,yBAAyB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAClD,CA6LA,CAAC;AAEF,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"S3FileBrowser.d.ts","sourceRoot":"","sources":["../src/S3FileBrowser.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAC,iBAAiB,EAAC,MAAM,6BAA6B,CAAC;AAG9D,OAAO,EAAC,EAAE,EAA4C,MAAM,OAAO,CAAC;AA4BpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,QAAA,MAAM,aAAa,EAAE,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAC5B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,EAAE,CAAC,UAAU,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,yBAAyB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;CAC3C,CAmOA,CAAC;AAEF,eAAe,aAAa,CAAC"}
@@ -1,8 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator, Button, Checkbox, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@sqlrooms/ui';
3
- import { Undo2Icon, FolderIcon } from 'lucide-react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator, Button, Checkbox, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, } from '@sqlrooms/ui';
3
+ import { Undo2Icon, FolderIcon, Search } from 'lucide-react';
4
4
  import { formatBytes, formatTimeRelative } from '@sqlrooms/utils';
5
- import { useCallback, useEffect, useMemo } from 'react';
5
+ import { useCallback, useEffect, useMemo, useState } from 'react';
6
+ function renderFileName(key, searchQuery, isDirectory) {
7
+ return (_jsx(_Fragment, { children: searchQuery ? (_jsx("span", { dangerouslySetInnerHTML: {
8
+ __html: key.replace(new RegExp(`(${searchQuery})`, 'gi'), '<mark class="bg-yellow-200">$1</mark>') + (isDirectory ? '/' : ''),
9
+ } })) : (_jsxs("span", { children: [key, isDirectory ? '/' : ''] })) }));
10
+ }
6
11
  /**
7
12
  * A file browser component for navigating and selecting files from an S3-like storage.
8
13
  *
@@ -49,10 +54,17 @@ import { useCallback, useEffect, useMemo } from 'react';
49
54
  * @param props.onChangeSelectedFiles - Callback fired when file selection changes
50
55
  */
51
56
  const S3FileBrowser = (props) => {
52
- const { files, selectedDirectory, selectedFiles, onCanConfirmChange, onChangeSelectedFiles, onChangeSelectedDirectory, } = props;
57
+ const { files, selectedDirectory, selectedFiles, onCanConfirmChange, onChangeSelectedFiles, onChangeSelectedDirectory, renderFileActions, } = props;
53
58
  useEffect(() => {
54
59
  onCanConfirmChange(Boolean(selectedFiles?.length));
55
60
  }, [selectedFiles, onCanConfirmChange]);
61
+ const [searchQuery, setSearchQuery] = useState('');
62
+ const filteredFiles = useMemo(() => {
63
+ if (!searchQuery.trim() || !files) {
64
+ return files;
65
+ }
66
+ return files.filter((file) => file.key.toLowerCase().includes(searchQuery.toLowerCase()));
67
+ }, [files, searchQuery]);
56
68
  const handleSelectFile = useCallback((key) => {
57
69
  if (selectedFiles.includes(key)) {
58
70
  onChangeSelectedFiles(selectedFiles.filter((id) => id !== key));
@@ -73,41 +85,49 @@ const S3FileBrowser = (props) => {
73
85
  onChangeSelectedFiles(filesInDirectory.map(({ key }) => key) ?? []);
74
86
  }
75
87
  }, [filesInDirectory, onChangeSelectedFiles, selectedFiles.length]);
88
+ const handleSearchInputChange = useCallback((e) => {
89
+ const value = e.target.value;
90
+ setSearchQuery(value);
91
+ if (value.trim() === '') {
92
+ onChangeSelectedFiles([]);
93
+ }
94
+ }, [onChangeSelectedFiles, setSearchQuery]);
76
95
  const parentDirectory = useMemo(() => {
77
96
  const dir = selectedDirectory.split('/').slice(0, -2).join('/');
78
97
  return dir ? `${dir}/` : '';
79
98
  }, [selectedDirectory]);
80
- return (_jsx("div", { className: "relative h-full w-full overflow-hidden", children: _jsx("div", { className: "absolute flex h-full w-full flex-col items-start overflow-x-auto overflow-y-auto py-0", children: _jsx("div", { className: "w-full overflow-y-auto rounded-lg border border-gray-600", children: _jsxs(Table, { disableWrapper: true, children: [_jsxs(TableHeader, { children: [selectedDirectory ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "bg-gray-800 py-3 text-gray-100", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => onChangeSelectedDirectory(parentDirectory), children: [_jsx(Undo2Icon, { className: "mr-1 h-3 w-3" }), ".."] }), _jsx(Breadcrumb, { children: _jsxs(BreadcrumbList, { children: [_jsx(BreadcrumbItem, { children: _jsx(BreadcrumbLink, { onClick: () => onChangeSelectedDirectory(''), className: "text-xs text-blue-400", children: "Home" }) }), selectedDirectory.split('/').map((directory, i) => {
81
- if (!directory)
82
- return null;
83
- const path = selectedDirectory
84
- .split('/')
85
- .slice(0, i + 1)
86
- .join('/')
87
- .concat('/');
88
- const isCurrent = path === selectedDirectory;
89
- return (_jsxs(BreadcrumbItem, { children: [_jsx(BreadcrumbSeparator, {}), _jsx(BreadcrumbLink, { className: cn('text-xs text-blue-400', isCurrent &&
90
- 'cursor-default hover:no-underline'), onClick: () => {
91
- if (!isCurrent) {
92
- onChangeSelectedDirectory(path);
93
- }
94
- }, children: directory })] }, i));
95
- })] }) })] }) }) })) : null, _jsxs(TableRow, { className: "sticky top-0 z-[2] bg-gray-600", children: [_jsx(TableHead, { className: "w-[1%]", children: _jsx(Checkbox, { checked: selectedFiles.length === filesInDirectory.length, onCheckedChange: handleSelectAll }) }), _jsx(TableHead, { className: "text-foreground py-2", children: "Name" }), _jsx(TableHead, { className: "text-foreground py-2", children: "Type" }), _jsx(TableHead, { className: "text-foreground text-right", children: "Size" }), _jsx(TableHead, { className: "text-foreground text-right", children: "Modified" })] })] }), _jsx(TableBody, { children: files?.map((object) => {
96
- const { key, isDirectory } = object;
97
- return (_jsxs(TableRow, { className: "hover:text-foreground cursor-pointer text-blue-300 hover:bg-blue-700", onClick: (evt) => {
98
- if (isDirectory) {
99
- handleSelectDirectory(key);
100
- }
101
- else {
102
- handleSelectFile(key);
103
- evt.preventDefault(); // prevent double change when clicking checkbox
104
- }
105
- }, children: [_jsx(TableCell, { children: _jsx(Checkbox, { disabled: isDirectory, checked: selectedFiles.includes(key) }) }), _jsx(TableCell, { className: "text-xs", children: isDirectory ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FolderIcon, { className: "h-4 w-4" }), _jsx("span", { children: `${key}/` })] })) : (key) }), _jsx(TableCell, { className: "text-xs", children: isDirectory ? 'Directory' : object.contentType }), _jsx(TableCell, { className: "text-right text-xs", children: !isDirectory && object.size !== undefined
106
- ? formatBytes(object.size)
107
- : '' }), _jsx(TableCell, { className: "text-right text-xs", children: !isDirectory && object.lastModified
108
- ? formatTimeRelative(object.lastModified)
109
- : '' })] }, key));
110
- }) })] }) }) }) }));
99
+ return (_jsxs("div", { className: "relative h-full w-full overflow-hidden", children: [_jsxs("div", { className: "flex w-full justify-end px-[1px] py-2", children: [renderFileActions ? renderFileActions() : null, _jsxs("div", { className: "relative w-[240px] shrink-0", children: [_jsx("div", { className: "pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3", children: _jsx(Search, { className: "h-5 w-5 text-gray-400" }) }), _jsx(Input, { type: "text", placeholder: "Search files by name", value: searchQuery, onChange: handleSearchInputChange, className: "h-8 py-2 pl-10 text-xs leading-5 md:text-xs" })] })] }), _jsx("div", { className: "absolute flex h-full w-full flex-col items-start overflow-x-auto overflow-y-auto py-0", children: _jsx("div", { className: "border-border w-full overflow-y-auto rounded-lg border", children: _jsxs(Table, { disableWrapper: true, children: [_jsxs(TableHeader, { children: [selectedDirectory ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5, className: "bg-secondary text-secondary-foreground", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { size: "sm", variant: "outline", onClick: () => onChangeSelectedDirectory(parentDirectory), children: [_jsx(Undo2Icon, { className: "mr-1 h-3 w-3" }), ".."] }), _jsx(Breadcrumb, { children: _jsxs(BreadcrumbList, { children: [_jsx(BreadcrumbItem, { children: _jsx(BreadcrumbLink, { onClick: () => onChangeSelectedDirectory(''), className: "text-primary text-xs", children: "Home" }) }), selectedDirectory.split('/').map((directory, i) => {
100
+ if (!directory)
101
+ return null;
102
+ const path = selectedDirectory
103
+ .split('/')
104
+ .slice(0, i + 1)
105
+ .join('/')
106
+ .concat('/');
107
+ const isCurrent = path === selectedDirectory;
108
+ return (_jsxs(BreadcrumbItem, { children: [_jsx(BreadcrumbSeparator, {}), _jsx(BreadcrumbLink, { className: cn('text-primary text-xs', isCurrent
109
+ ? 'cursor-default hover:no-underline'
110
+ : 'cursor-pointer'), onClick: () => {
111
+ if (!isCurrent) {
112
+ onChangeSelectedDirectory(path);
113
+ }
114
+ }, children: directory })] }, i));
115
+ })] }) })] }) }) })) : null, _jsxs(TableRow, { className: "bg-accent text-primary-foreground sticky top-0 z-[2]", children: [_jsx(TableHead, { className: "w-[1%]", children: _jsx(Checkbox, { checked: selectedFiles.length === filesInDirectory.length, onCheckedChange: handleSelectAll }) }), _jsx(TableHead, { className: "text-foreground py-2", children: "Name" }), _jsx(TableHead, { className: "text-foreground py-2", children: "Type" }), _jsx(TableHead, { className: "text-foreground text-right", children: "Size" }), _jsx(TableHead, { className: "text-foreground text-right", children: "Modified" })] })] }), _jsx(TableBody, { children: filteredFiles?.map((object) => {
116
+ const { key, isDirectory } = object;
117
+ return (_jsxs(TableRow, { className: "text-foreground hover:bg-accent cursor-pointer", onClick: (evt) => {
118
+ if (isDirectory) {
119
+ handleSelectDirectory(key);
120
+ }
121
+ else {
122
+ handleSelectFile(key);
123
+ evt.preventDefault(); // prevent double change when clicking checkbox
124
+ }
125
+ }, children: [_jsx(TableCell, { children: _jsx(Checkbox, { disabled: isDirectory, checked: selectedFiles.includes(key) }) }), _jsx(TableCell, { className: "text-xs", children: isDirectory ? (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(FolderIcon, { className: "h-4 w-4" }), renderFileName(key, searchQuery, true)] })) : (renderFileName(key, searchQuery, false)) }), _jsx(TableCell, { className: "text-xs", children: isDirectory ? 'Directory' : object.contentType }), _jsx(TableCell, { className: "text-right text-xs", children: !isDirectory && object.size !== undefined
126
+ ? formatBytes(object.size)
127
+ : '' }), _jsx(TableCell, { className: "text-right text-xs", children: !isDirectory && object.lastModified
128
+ ? formatTimeRelative(object.lastModified)
129
+ : '' })] }, key));
130
+ }) })] }) }) })] }));
111
131
  };
112
132
  export default S3FileBrowser;
113
133
  //# sourceMappingURL=S3FileBrowser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"S3FileBrowser.js","sourceRoot":"","sources":["../src/S3FileBrowser.tsx"],"names":[],"mappings":"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","sourcesContent":["import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbSeparator,\n Button,\n Checkbox,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n cn,\n} from '@sqlrooms/ui';\nimport {Undo2Icon, FolderIcon} from 'lucide-react';\nimport {formatBytes, formatTimeRelative} from '@sqlrooms/utils';\nimport {FC, useCallback, useEffect, useMemo} from 'react';\nimport {S3FileOrDirectory} from './S3FileOrDirectory';\n\n/**\n * A file browser component for navigating and selecting files from an S3-like storage.\n *\n * This component provides a familiar file explorer interface with features like:\n * - Directory navigation with breadcrumbs\n * - File and directory listing\n * - Multiple file selection\n * - File metadata display (size, type, last modified)\n *\n * ![S3 File Browser Interface](https://github.com/user-attachments/assets/dd79fbb9-c487-4050-96ef-81cff39930d3)\n *\n * @example\n * ```tsx\n * const [selectedFiles, setSelectedFiles] = useState<string[]>([]);\n * const [selectedDirectory, setSelectedDirectory] = useState('');\n *\n * return (\n * <S3FileBrowser\n * files={[\n * { key: 'documents', isDirectory: true },\n * {\n * key: 'example.txt',\n * isDirectory: false,\n * size: 1024,\n * contentType: 'text/plain',\n * lastModified: new Date()\n * }\n * ]}\n * selectedFiles={selectedFiles}\n * selectedDirectory={selectedDirectory}\n * onCanConfirmChange={(canConfirm) => console.log('Can confirm:', canConfirm)}\n * onChangeSelectedDirectory={setSelectedDirectory}\n * onChangeSelectedFiles={setSelectedFiles}\n * />\n * );\n * ```\n *\n * @param props - The component props\n * @param props.files - Array of files and directories to display\n * @param props.selectedFiles - Array of currently selected file keys\n * @param props.selectedDirectory - Current directory path (empty string for root)\n * @param props.onCanConfirmChange - Callback fired when selection state changes\n * @param props.onChangeSelectedDirectory - Callback fired when directory navigation occurs\n * @param props.onChangeSelectedFiles - Callback fired when file selection changes\n */\nconst S3FileBrowser: FC<{\n files?: S3FileOrDirectory[];\n selectedFiles: string[];\n selectedDirectory: string;\n onCanConfirmChange: (canConfirm: boolean) => void;\n onChangeSelectedDirectory: (directory: string) => void;\n onChangeSelectedFiles: (files: string[]) => void;\n}> = (props) => {\n const {\n files,\n selectedDirectory,\n selectedFiles,\n onCanConfirmChange,\n onChangeSelectedFiles,\n onChangeSelectedDirectory,\n } = props;\n\n useEffect(() => {\n onCanConfirmChange(Boolean(selectedFiles?.length));\n }, [selectedFiles, onCanConfirmChange]);\n\n const handleSelectFile = useCallback(\n (key: string) => {\n if (selectedFiles.includes(key)) {\n onChangeSelectedFiles(selectedFiles.filter((id) => id !== key));\n } else {\n onChangeSelectedFiles([...selectedFiles, key]);\n }\n },\n [onChangeSelectedFiles, selectedFiles],\n );\n\n const handleSelectDirectory = useCallback(\n (key: string) => {\n onChangeSelectedDirectory(`${selectedDirectory}${key}/`);\n },\n [selectedDirectory, onChangeSelectedDirectory],\n );\n\n const filesInDirectory = useMemo(\n () => files?.filter(({isDirectory}) => !isDirectory) ?? [],\n [files],\n );\n\n const handleSelectAll = useCallback(() => {\n if (selectedFiles.length === filesInDirectory.length) {\n onChangeSelectedFiles([]);\n } else {\n onChangeSelectedFiles(filesInDirectory.map(({key}) => key) ?? []);\n }\n }, [filesInDirectory, onChangeSelectedFiles, selectedFiles.length]);\n\n const parentDirectory = useMemo(() => {\n const dir = selectedDirectory.split('/').slice(0, -2).join('/');\n return dir ? `${dir}/` : '';\n }, [selectedDirectory]);\n\n return (\n <div className=\"relative h-full w-full overflow-hidden\">\n <div className=\"absolute flex h-full w-full flex-col items-start overflow-x-auto overflow-y-auto py-0\">\n <div className=\"w-full overflow-y-auto rounded-lg border border-gray-600\">\n <Table disableWrapper>\n <TableHeader>\n {selectedDirectory ? (\n <TableRow>\n <TableCell\n colSpan={5}\n className=\"bg-gray-800 py-3 text-gray-100\"\n >\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n onChangeSelectedDirectory(parentDirectory)\n }\n >\n <Undo2Icon className=\"mr-1 h-3 w-3\" />\n ..\n </Button>\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbLink\n onClick={() => onChangeSelectedDirectory('')}\n className=\"text-xs text-blue-400\"\n >\n Home\n </BreadcrumbLink>\n </BreadcrumbItem>\n\n {selectedDirectory.split('/').map((directory, i) => {\n if (!directory) return null;\n const path = selectedDirectory\n .split('/')\n .slice(0, i + 1)\n .join('/')\n .concat('/');\n const isCurrent = path === selectedDirectory;\n return (\n <BreadcrumbItem key={i}>\n <BreadcrumbSeparator />\n <BreadcrumbLink\n className={cn(\n 'text-xs text-blue-400',\n isCurrent &&\n 'cursor-default hover:no-underline',\n )}\n onClick={() => {\n if (!isCurrent) {\n onChangeSelectedDirectory(path);\n }\n }}\n >\n {directory}\n </BreadcrumbLink>\n </BreadcrumbItem>\n );\n })}\n </BreadcrumbList>\n </Breadcrumb>\n </div>\n </TableCell>\n </TableRow>\n ) : null}\n <TableRow className=\"sticky top-0 z-[2] bg-gray-600\">\n <TableHead className=\"w-[1%]\">\n <Checkbox\n checked={selectedFiles.length === filesInDirectory.length}\n onCheckedChange={handleSelectAll}\n />\n </TableHead>\n <TableHead className=\"text-foreground py-2\">Name</TableHead>\n <TableHead className=\"text-foreground py-2\">Type</TableHead>\n <TableHead className=\"text-foreground text-right\">\n Size\n </TableHead>\n <TableHead className=\"text-foreground text-right\">\n Modified\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {files?.map((object) => {\n const {key, isDirectory} = object;\n return (\n <TableRow\n key={key}\n className=\"hover:text-foreground cursor-pointer text-blue-300 hover:bg-blue-700\"\n onClick={(evt) => {\n if (isDirectory) {\n handleSelectDirectory(key);\n } else {\n handleSelectFile(key);\n evt.preventDefault(); // prevent double change when clicking checkbox\n }\n }}\n >\n <TableCell>\n <Checkbox\n disabled={isDirectory}\n checked={selectedFiles.includes(key)}\n />\n </TableCell>\n <TableCell className=\"text-xs\">\n {isDirectory ? (\n <div className=\"flex items-center gap-2\">\n <FolderIcon className=\"h-4 w-4\" />\n <span>{`${key}/`}</span>\n </div>\n ) : (\n key\n )}\n </TableCell>\n <TableCell className=\"text-xs\">\n {isDirectory ? 'Directory' : object.contentType}\n </TableCell>\n <TableCell className=\"text-right text-xs\">\n {!isDirectory && object.size !== undefined\n ? formatBytes(object.size)\n : ''}\n </TableCell>\n <TableCell className=\"text-right text-xs\">\n {!isDirectory && object.lastModified\n ? formatTimeRelative(object.lastModified)\n : ''}\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n </div>\n );\n};\n\nexport default S3FileBrowser;\n"]}
1
+ {"version":3,"file":"S3FileBrowser.js","sourceRoot":"","sources":["../src/S3FileBrowser.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,UAAU,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,MAAM,EACN,QAAQ,EACR,KAAK,EACL,KAAK,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,WAAW,EACX,QAAQ,EACR,EAAE,GACH,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAC,SAAS,EAAE,UAAU,EAAE,MAAM,EAAC,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAC,WAAW,EAAE,kBAAkB,EAAC,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAK,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAC,MAAM,OAAO,CAAC;AAEpE,SAAS,cAAc,CACrB,GAAW,EACX,WAAmB,EACnB,WAAoB;IAEpB,OAAO,CACL,4BACG,WAAW,CAAC,CAAC,CAAC,CACb,eACE,uBAAuB,EAAE;gBACvB,MAAM,EACJ,GAAG,CAAC,OAAO,CACT,IAAI,MAAM,CAAC,IAAI,WAAW,GAAG,EAAE,IAAI,CAAC,EACpC,uCAAuC,CACxC,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/B,GACD,CACH,CAAC,CAAC,CAAC,CACF,2BACG,GAAG,EACH,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAClB,CACR,GACA,CACJ,CAAC;AACJ,CAAC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,aAAa,GAQd,CAAC,KAAK,EAAE,EAAE;IACb,MAAM,EACJ,KAAK,EACL,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,qBAAqB,EACrB,yBAAyB,EACzB,iBAAiB,GAClB,GAAG,KAAK,CAAC;IAEV,SAAS,CAAC,GAAG,EAAE;QACb,kBAAkB,CAAC,OAAO,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC;IACrD,CAAC,EAAE,CAAC,aAAa,EAAE,kBAAkB,CAAC,CAAC,CAAC;IACxC,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAEnD,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE;QACjC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAC3B,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,CAC3D,CAAC;IACJ,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC;IAEzB,MAAM,gBAAgB,GAAG,WAAW,CAClC,CAAC,GAAW,EAAE,EAAE;QACd,IAAI,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,qBAAqB,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAClE,CAAC;aAAM,CAAC;YACN,qBAAqB,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,CAAC,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,EACD,CAAC,qBAAqB,EAAE,aAAa,CAAC,CACvC,CAAC;IAEF,MAAM,qBAAqB,GAAG,WAAW,CACvC,CAAC,GAAW,EAAE,EAAE;QACd,yBAAyB,CAAC,GAAG,iBAAiB,GAAG,GAAG,GAAG,CAAC,CAAC;IAC3D,CAAC,EACD,CAAC,iBAAiB,EAAE,yBAAyB,CAAC,CAC/C,CAAC;IAEF,MAAM,gBAAgB,GAAG,OAAO,CAC9B,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,EAAC,WAAW,EAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,EAC1D,CAAC,KAAK,CAAC,CACR,CAAC;IAEF,MAAM,eAAe,GAAG,WAAW,CAAC,GAAG,EAAE;QACvC,IAAI,aAAa,CAAC,MAAM,KAAK,gBAAgB,CAAC,MAAM,EAAE,CAAC;YACrD,qBAAqB,CAAC,EAAE,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,qBAAqB,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,EAAC,GAAG,EAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QACpE,CAAC;IACH,CAAC,EAAE,CAAC,gBAAgB,EAAE,qBAAqB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IACpE,MAAM,uBAAuB,GAAG,WAAW,CACzC,CAAC,CAAsC,EAAE,EAAE;QACzC,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;QAC7B,cAAc,CAAC,KAAK,CAAC,CAAC;QACtB,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACxB,qBAAqB,CAAC,EAAE,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC,EACD,CAAC,qBAAqB,EAAE,cAAc,CAAC,CACxC,CAAC;IACF,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,EAAE;QACnC,MAAM,GAAG,GAAG,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChE,OAAO,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC;IAExB,OAAO,CACL,eAAK,SAAS,EAAC,wCAAwC,aAErD,eAAK,SAAS,EAAC,uCAAuC,aACnD,iBAAiB,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,IAAI,EAC/C,eAAK,SAAS,EAAC,6BAA6B,aAC1C,cAAK,SAAS,EAAC,sEAAsE,YACnF,KAAC,MAAM,IAAC,SAAS,EAAC,uBAAuB,GAAG,GACxC,EACN,KAAC,KAAK,IACJ,IAAI,EAAC,MAAM,EACX,WAAW,EAAC,sBAAsB,EAClC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,uBAAuB,EACjC,SAAS,EAAC,6CAA6C,GACvD,IACE,IACF,EACN,cAAK,SAAS,EAAC,uFAAuF,YACpG,cAAK,SAAS,EAAC,wDAAwD,YACrE,MAAC,KAAK,IAAC,cAAc,mBACnB,MAAC,WAAW,eACT,iBAAiB,CAAC,CAAC,CAAC,CACnB,KAAC,QAAQ,cACP,KAAC,SAAS,IACR,OAAO,EAAE,CAAC,EACV,SAAS,EAAC,wCAAwC,YAElD,eAAK,SAAS,EAAC,yBAAyB,aACtC,MAAC,MAAM,IACL,IAAI,EAAC,IAAI,EACT,OAAO,EAAC,SAAS,EACjB,OAAO,EAAE,GAAG,EAAE,CACZ,yBAAyB,CAAC,eAAe,CAAC,aAG5C,KAAC,SAAS,IAAC,SAAS,EAAC,cAAc,GAAG,UAE/B,EACT,KAAC,UAAU,cACT,MAAC,cAAc,eACb,KAAC,cAAc,cACb,KAAC,cAAc,IACb,OAAO,EAAE,GAAG,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAC,EAC5C,SAAS,EAAC,sBAAsB,qBAGjB,GACF,EAEhB,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,CAAC,EAAE,EAAE;oEACjD,IAAI,CAAC,SAAS;wEAAE,OAAO,IAAI,CAAC;oEAC5B,MAAM,IAAI,GAAG,iBAAiB;yEAC3B,KAAK,CAAC,GAAG,CAAC;yEACV,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;yEACf,IAAI,CAAC,GAAG,CAAC;yEACT,MAAM,CAAC,GAAG,CAAC,CAAC;oEACf,MAAM,SAAS,GAAG,IAAI,KAAK,iBAAiB,CAAC;oEAC7C,OAAO,CACL,MAAC,cAAc,eACb,KAAC,mBAAmB,KAAG,EACvB,KAAC,cAAc,IACb,SAAS,EAAE,EAAE,CACX,sBAAsB,EACtB,SAAS;oFACP,CAAC,CAAC,mCAAmC;oFACrC,CAAC,CAAC,gBAAgB,CACrB,EACD,OAAO,EAAE,GAAG,EAAE;oFACZ,IAAI,CAAC,SAAS,EAAE,CAAC;wFACf,yBAAyB,CAAC,IAAI,CAAC,CAAC;oFAClC,CAAC;gFACH,CAAC,YAEA,SAAS,GACK,KAhBE,CAAC,CAiBL,CAClB,CAAC;gEACJ,CAAC,CAAC,IACa,GACN,IACT,GACI,GACH,CACZ,CAAC,CAAC,CAAC,IAAI,EACR,MAAC,QAAQ,IAAC,SAAS,EAAC,sDAAsD,aACxE,KAAC,SAAS,IAAC,SAAS,EAAC,QAAQ,YAC3B,KAAC,QAAQ,IACP,OAAO,EAAE,aAAa,CAAC,MAAM,KAAK,gBAAgB,CAAC,MAAM,EACzD,eAAe,EAAE,eAAe,GAChC,GACQ,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,sBAAsB,qBAAiB,EAC5D,KAAC,SAAS,IAAC,SAAS,EAAC,sBAAsB,qBAAiB,EAC5D,KAAC,SAAS,IAAC,SAAS,EAAC,4BAA4B,qBAErC,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,4BAA4B,yBAErC,IACH,IACC,EACd,KAAC,SAAS,cACP,aAAa,EAAE,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;oCAC7B,MAAM,EAAC,GAAG,EAAE,WAAW,EAAC,GAAG,MAAM,CAAC;oCAClC,OAAO,CACL,MAAC,QAAQ,IAEP,SAAS,EAAC,gDAAgD,EAC1D,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;4CACf,IAAI,WAAW,EAAE,CAAC;gDAChB,qBAAqB,CAAC,GAAG,CAAC,CAAC;4CAC7B,CAAC;iDAAM,CAAC;gDACN,gBAAgB,CAAC,GAAG,CAAC,CAAC;gDACtB,GAAG,CAAC,cAAc,EAAE,CAAC,CAAC,+CAA+C;4CACvE,CAAC;wCACH,CAAC,aAED,KAAC,SAAS,cACR,KAAC,QAAQ,IACP,QAAQ,EAAE,WAAW,EACrB,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,GACpC,GACQ,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,YAC3B,WAAW,CAAC,CAAC,CAAC,CACb,eAAK,SAAS,EAAC,yBAAyB,aACtC,KAAC,UAAU,IAAC,SAAS,EAAC,SAAS,GAAG,EACjC,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,CAAC,IACnC,CACP,CAAC,CAAC,CAAC,CACF,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,CACxC,GACS,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,SAAS,YAC3B,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,GACrC,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,oBAAoB,YACtC,CAAC,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;oDACxC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC;oDAC1B,CAAC,CAAC,EAAE,GACI,EACZ,KAAC,SAAS,IAAC,SAAS,EAAC,oBAAoB,YACtC,CAAC,WAAW,IAAI,MAAM,CAAC,YAAY;oDAClC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,YAAY,CAAC;oDACzC,CAAC,CAAC,EAAE,GACI,KAvCP,GAAG,CAwCC,CACZ,CAAC;gCACJ,CAAC,CAAC,GACQ,IACN,GACJ,GACF,IACF,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,aAAa,CAAC","sourcesContent":["import {\n Breadcrumb,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbList,\n BreadcrumbSeparator,\n Button,\n Checkbox,\n Input,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n cn,\n} from '@sqlrooms/ui';\nimport {S3FileOrDirectory} from '@sqlrooms/s3-browser-config';\nimport {Undo2Icon, FolderIcon, Search} from 'lucide-react';\nimport {formatBytes, formatTimeRelative} from '@sqlrooms/utils';\nimport {FC, useCallback, useEffect, useMemo, useState} from 'react';\n\nfunction renderFileName(\n key: string,\n searchQuery: string,\n isDirectory: boolean,\n) {\n return (\n <>\n {searchQuery ? (\n <span\n dangerouslySetInnerHTML={{\n __html:\n key.replace(\n new RegExp(`(${searchQuery})`, 'gi'),\n '<mark class=\"bg-yellow-200\">$1</mark>',\n ) + (isDirectory ? '/' : ''),\n }}\n />\n ) : (\n <span>\n {key}\n {isDirectory ? '/' : ''}\n </span>\n )}\n </>\n );\n}\n/**\n * A file browser component for navigating and selecting files from an S3-like storage.\n *\n * This component provides a familiar file explorer interface with features like:\n * - Directory navigation with breadcrumbs\n * - File and directory listing\n * - Multiple file selection\n * - File metadata display (size, type, last modified)\n *\n * ![S3 File Browser Interface](https://github.com/user-attachments/assets/dd79fbb9-c487-4050-96ef-81cff39930d3)\n *\n * @example\n * ```tsx\n * const [selectedFiles, setSelectedFiles] = useState<string[]>([]);\n * const [selectedDirectory, setSelectedDirectory] = useState('');\n *\n * return (\n * <S3FileBrowser\n * files={[\n * { key: 'documents', isDirectory: true },\n * {\n * key: 'example.txt',\n * isDirectory: false,\n * size: 1024,\n * contentType: 'text/plain',\n * lastModified: new Date()\n * }\n * ]}\n * selectedFiles={selectedFiles}\n * selectedDirectory={selectedDirectory}\n * onCanConfirmChange={(canConfirm) => console.log('Can confirm:', canConfirm)}\n * onChangeSelectedDirectory={setSelectedDirectory}\n * onChangeSelectedFiles={setSelectedFiles}\n * />\n * );\n * ```\n *\n * @param props - The component props\n * @param props.files - Array of files and directories to display\n * @param props.selectedFiles - Array of currently selected file keys\n * @param props.selectedDirectory - Current directory path (empty string for root)\n * @param props.onCanConfirmChange - Callback fired when selection state changes\n * @param props.onChangeSelectedDirectory - Callback fired when directory navigation occurs\n * @param props.onChangeSelectedFiles - Callback fired when file selection changes\n */\nconst S3FileBrowser: FC<{\n files?: S3FileOrDirectory[];\n selectedFiles: string[];\n selectedDirectory: string;\n onCanConfirmChange: (canConfirm: boolean) => void;\n onChangeSelectedDirectory: (directory: string) => void;\n onChangeSelectedFiles: (files: string[]) => void;\n renderFileActions?: () => React.ReactNode;\n}> = (props) => {\n const {\n files,\n selectedDirectory,\n selectedFiles,\n onCanConfirmChange,\n onChangeSelectedFiles,\n onChangeSelectedDirectory,\n renderFileActions,\n } = props;\n\n useEffect(() => {\n onCanConfirmChange(Boolean(selectedFiles?.length));\n }, [selectedFiles, onCanConfirmChange]);\n const [searchQuery, setSearchQuery] = useState('');\n\n const filteredFiles = useMemo(() => {\n if (!searchQuery.trim() || !files) {\n return files;\n }\n\n return files.filter((file) =>\n file.key.toLowerCase().includes(searchQuery.toLowerCase()),\n );\n }, [files, searchQuery]);\n\n const handleSelectFile = useCallback(\n (key: string) => {\n if (selectedFiles.includes(key)) {\n onChangeSelectedFiles(selectedFiles.filter((id) => id !== key));\n } else {\n onChangeSelectedFiles([...selectedFiles, key]);\n }\n },\n [onChangeSelectedFiles, selectedFiles],\n );\n\n const handleSelectDirectory = useCallback(\n (key: string) => {\n onChangeSelectedDirectory(`${selectedDirectory}${key}/`);\n },\n [selectedDirectory, onChangeSelectedDirectory],\n );\n\n const filesInDirectory = useMemo(\n () => files?.filter(({isDirectory}) => !isDirectory) ?? [],\n [files],\n );\n\n const handleSelectAll = useCallback(() => {\n if (selectedFiles.length === filesInDirectory.length) {\n onChangeSelectedFiles([]);\n } else {\n onChangeSelectedFiles(filesInDirectory.map(({key}) => key) ?? []);\n }\n }, [filesInDirectory, onChangeSelectedFiles, selectedFiles.length]);\n const handleSearchInputChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value;\n setSearchQuery(value);\n if (value.trim() === '') {\n onChangeSelectedFiles([]);\n }\n },\n [onChangeSelectedFiles, setSearchQuery],\n );\n const parentDirectory = useMemo(() => {\n const dir = selectedDirectory.split('/').slice(0, -2).join('/');\n return dir ? `${dir}/` : '';\n }, [selectedDirectory]);\n\n return (\n <div className=\"relative h-full w-full overflow-hidden\">\n {/* Search Box */}\n <div className=\"flex w-full justify-end px-[1px] py-2\">\n {renderFileActions ? renderFileActions() : null}\n <div className=\"relative w-[240px] shrink-0\">\n <div className=\"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3\">\n <Search className=\"h-5 w-5 text-gray-400\" />\n </div>\n <Input\n type=\"text\"\n placeholder=\"Search files by name\"\n value={searchQuery}\n onChange={handleSearchInputChange}\n className=\"h-8 py-2 pl-10 text-xs leading-5 md:text-xs\"\n />\n </div>\n </div>\n <div className=\"absolute flex h-full w-full flex-col items-start overflow-x-auto overflow-y-auto py-0\">\n <div className=\"border-border w-full overflow-y-auto rounded-lg border\">\n <Table disableWrapper>\n <TableHeader>\n {selectedDirectory ? (\n <TableRow>\n <TableCell\n colSpan={5}\n className=\"bg-secondary text-secondary-foreground\"\n >\n <div className=\"flex items-center gap-2\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n onChangeSelectedDirectory(parentDirectory)\n }\n >\n <Undo2Icon className=\"mr-1 h-3 w-3\" />\n ..\n </Button>\n <Breadcrumb>\n <BreadcrumbList>\n <BreadcrumbItem>\n <BreadcrumbLink\n onClick={() => onChangeSelectedDirectory('')}\n className=\"text-primary text-xs\"\n >\n Home\n </BreadcrumbLink>\n </BreadcrumbItem>\n\n {selectedDirectory.split('/').map((directory, i) => {\n if (!directory) return null;\n const path = selectedDirectory\n .split('/')\n .slice(0, i + 1)\n .join('/')\n .concat('/');\n const isCurrent = path === selectedDirectory;\n return (\n <BreadcrumbItem key={i}>\n <BreadcrumbSeparator />\n <BreadcrumbLink\n className={cn(\n 'text-primary text-xs',\n isCurrent\n ? 'cursor-default hover:no-underline'\n : 'cursor-pointer',\n )}\n onClick={() => {\n if (!isCurrent) {\n onChangeSelectedDirectory(path);\n }\n }}\n >\n {directory}\n </BreadcrumbLink>\n </BreadcrumbItem>\n );\n })}\n </BreadcrumbList>\n </Breadcrumb>\n </div>\n </TableCell>\n </TableRow>\n ) : null}\n <TableRow className=\"bg-accent text-primary-foreground sticky top-0 z-[2]\">\n <TableHead className=\"w-[1%]\">\n <Checkbox\n checked={selectedFiles.length === filesInDirectory.length}\n onCheckedChange={handleSelectAll}\n />\n </TableHead>\n <TableHead className=\"text-foreground py-2\">Name</TableHead>\n <TableHead className=\"text-foreground py-2\">Type</TableHead>\n <TableHead className=\"text-foreground text-right\">\n Size\n </TableHead>\n <TableHead className=\"text-foreground text-right\">\n Modified\n </TableHead>\n </TableRow>\n </TableHeader>\n <TableBody>\n {filteredFiles?.map((object) => {\n const {key, isDirectory} = object;\n return (\n <TableRow\n key={key}\n className=\"text-foreground hover:bg-accent cursor-pointer\"\n onClick={(evt) => {\n if (isDirectory) {\n handleSelectDirectory(key);\n } else {\n handleSelectFile(key);\n evt.preventDefault(); // prevent double change when clicking checkbox\n }\n }}\n >\n <TableCell>\n <Checkbox\n disabled={isDirectory}\n checked={selectedFiles.includes(key)}\n />\n </TableCell>\n <TableCell className=\"text-xs\">\n {isDirectory ? (\n <div className=\"flex items-center gap-2\">\n <FolderIcon className=\"h-4 w-4\" />\n {renderFileName(key, searchQuery, true)}\n </div>\n ) : (\n renderFileName(key, searchQuery, false)\n )}\n </TableCell>\n <TableCell className=\"text-xs\">\n {isDirectory ? 'Directory' : object.contentType}\n </TableCell>\n <TableCell className=\"text-right text-xs\">\n {!isDirectory && object.size !== undefined\n ? formatBytes(object.size)\n : ''}\n </TableCell>\n <TableCell className=\"text-right text-xs\">\n {!isDirectory && object.lastModified\n ? formatTimeRelative(object.lastModified)\n : ''}\n </TableCell>\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n </div>\n );\n};\n\nexport default S3FileBrowser;\n"]}
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  export { default as S3FileBrowser } from './S3FileBrowser';
6
- export * from './s3';
7
- export * from './S3FileOrDirectory';
6
+ export { S3CredentialsForm } from './S3CredentialsForm';
7
+ export { createS3BrowserSlice, type S3BrowserState } from './S3BrowserSlice';
8
+ export * from '@sqlrooms/s3-browser-config';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAC,OAAO,IAAI,aAAa,EAAC,MAAM,iBAAiB,CAAC;AACzD,cAAc,MAAM,CAAC;AACrB,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAC,OAAO,IAAI,aAAa,EAAC,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAC,oBAAoB,EAAE,KAAK,cAAc,EAAC,MAAM,kBAAkB,CAAC;AAC3E,cAAc,6BAA6B,CAAC"}
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  export { default as S3FileBrowser } from './S3FileBrowser';
6
- export * from './s3';
7
- export * from './S3FileOrDirectory';
6
+ export { S3CredentialsForm } from './S3CredentialsForm';
7
+ export { createS3BrowserSlice } from './S3BrowserSlice';
8
+ export * from '@sqlrooms/s3-browser-config';
8
9
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAC,OAAO,IAAI,aAAa,EAAC,MAAM,iBAAiB,CAAC;AACzD,cAAc,MAAM,CAAC;AACrB,cAAc,qBAAqB,CAAC","sourcesContent":["/**\n * {@include ../README.md}\n * @packageDocumentation\n */\nexport {default as S3FileBrowser} from './S3FileBrowser';\nexport * from './s3';\nexport * from './S3FileOrDirectory';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAC,OAAO,IAAI,aAAa,EAAC,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAC,oBAAoB,EAAsB,MAAM,kBAAkB,CAAC;AAC3E,cAAc,6BAA6B,CAAC","sourcesContent":["/**\n * {@include ../README.md}\n * @packageDocumentation\n */\nexport {default as S3FileBrowser} from './S3FileBrowser';\nexport {S3CredentialsForm} from './S3CredentialsForm';\nexport {createS3BrowserSlice, type S3BrowserState} from './S3BrowserSlice';\nexport * from '@sqlrooms/s3-browser-config';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqlrooms/s3-browser",
3
- "version": "0.26.0",
3
+ "version": "0.26.1-rc.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/index.js",
@@ -18,11 +18,16 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
- "@aws-sdk/client-s3": "^3.894.0",
22
- "@sqlrooms/ui": "0.26.0",
23
- "@sqlrooms/utils": "0.26.0",
24
- "lucide-react": "^0.544.0",
25
- "zod": "^4.1.8"
21
+ "@hookform/resolvers": "^3.10.0",
22
+ "@sqlrooms/room-shell": "0.26.1-rc.1",
23
+ "@sqlrooms/s3-browser-config": "0.26.1-rc.1",
24
+ "@sqlrooms/ui": "0.26.1-rc.1",
25
+ "@sqlrooms/utils": "0.26.1-rc.1",
26
+ "immer": "^10.1.3",
27
+ "lucide-react": "^0.555.0",
28
+ "react-hook-form": "^7.63.0",
29
+ "zod": "^4.1.8",
30
+ "zustand": "^5.0.8"
26
31
  },
27
32
  "peerDependencies": {
28
33
  "react": ">=18",
@@ -35,5 +40,5 @@
35
40
  "typecheck": "tsc --noEmit",
36
41
  "typedoc": "typedoc"
37
42
  },
38
- "gitHead": "3376e76ddfa3c54097b79a20b88a1c37814dca61"
43
+ "gitHead": "e41ac3e033289faaaab0c2dbce552f1a7d0aea2b"
39
44
  }
@@ -1,13 +0,0 @@
1
- import { z } from 'zod';
2
- export declare const S3FileOrDirectory: z.ZodUnion<readonly [z.ZodObject<{
3
- key: z.ZodString;
4
- isDirectory: z.ZodLiteral<true>;
5
- }, z.core.$strip>, z.ZodObject<{
6
- key: z.ZodString;
7
- isDirectory: z.ZodLiteral<false>;
8
- lastModified: z.ZodOptional<z.ZodDate>;
9
- size: z.ZodOptional<z.ZodNumber>;
10
- contentType: z.ZodOptional<z.ZodString>;
11
- }, z.core.$strip>]>;
12
- export type S3FileOrDirectory = z.infer<typeof S3FileOrDirectory>;
13
- //# sourceMappingURL=S3FileOrDirectory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"S3FileOrDirectory.d.ts","sourceRoot":"","sources":["../src/S3FileOrDirectory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,eAAO,MAAM,iBAAiB;;;;;;;;;mBAY5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC"}
@@ -1,15 +0,0 @@
1
- import { z } from 'zod';
2
- export const S3FileOrDirectory = z.union([
3
- z.object({
4
- key: z.string(),
5
- isDirectory: z.literal(true),
6
- }),
7
- z.object({
8
- key: z.string(),
9
- isDirectory: z.literal(false),
10
- lastModified: z.date().optional(),
11
- size: z.number().optional(),
12
- contentType: z.string().optional(),
13
- }),
14
- ]);
15
- //# sourceMappingURL=S3FileOrDirectory.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"S3FileOrDirectory.js","sourceRoot":"","sources":["../src/S3FileOrDirectory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC;IACvC,CAAC,CAAC,MAAM,CAAC;QACP,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;KAC7B,CAAC;IACF,CAAC,CAAC,MAAM,CAAC;QACP,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;QACf,WAAW,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;QAC7B,YAAY,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE;QACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;KACnC,CAAC;CACH,CAAC,CAAC","sourcesContent":["import {z} from 'zod';\n\nexport const S3FileOrDirectory = z.union([\n z.object({\n key: z.string(),\n isDirectory: z.literal(true),\n }),\n z.object({\n key: z.string(),\n isDirectory: z.literal(false),\n lastModified: z.date().optional(),\n size: z.number().optional(),\n contentType: z.string().optional(),\n }),\n]);\nexport type S3FileOrDirectory = z.infer<typeof S3FileOrDirectory>;\n"]}
package/dist/s3.d.ts DELETED
@@ -1,9 +0,0 @@
1
- import { S3Client } from '@aws-sdk/client-s3';
2
- import { S3FileOrDirectory } from './S3FileOrDirectory';
3
- export declare function listFilesAndDirectoriesWithPrefix(S3: S3Client, bucket: string, prefix?: string): Promise<S3FileOrDirectory[]>;
4
- /**
5
- * Delete all files with the given prefix
6
- * @param prefix
7
- */
8
- export declare function deleteS3Files(S3: S3Client, bucket: string, prefix: string): Promise<void>;
9
- //# sourceMappingURL=s3.d.ts.map
package/dist/s3.d.ts.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"s3.d.ts","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,QAAQ,EACT,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,iBAAiB,EAAC,MAAM,qBAAqB,CAAC;AAEtD,wBAAsB,iCAAiC,CACrD,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAwD9B;AAgBD;;;GAGG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,QAAQ,EACZ,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,iBAUf"}
package/dist/s3.js DELETED
@@ -1,75 +0,0 @@
1
- import { DeleteObjectCommand, HeadObjectCommand, ListObjectsCommand, ListObjectsV2Command, } from '@aws-sdk/client-s3';
2
- export async function listFilesAndDirectoriesWithPrefix(S3, bucket, prefix) {
3
- const command = new ListObjectsV2Command({
4
- Bucket: bucket,
5
- Prefix: prefix ? `${prefix}${prefix.endsWith('/') ? '' : '/'}` : '',
6
- Delimiter: '/',
7
- });
8
- const response = await S3.send(command);
9
- const objects = [];
10
- const removePrefix = (key) => {
11
- if (!prefix) {
12
- return key;
13
- }
14
- return key.replace(prefix ?? '', '');
15
- };
16
- if (response.CommonPrefixes) {
17
- for (const commonPrefix of response.CommonPrefixes) {
18
- if (commonPrefix.Prefix) {
19
- // Extract the directory name from the CommonPrefixes
20
- const directoryName = removePrefix(commonPrefix.Prefix).slice(0, -1);
21
- objects.push({ key: directoryName, isDirectory: true });
22
- }
23
- }
24
- }
25
- if (response.Contents) {
26
- for (const content of response.Contents) {
27
- const key = content.Key;
28
- if (key) {
29
- // Exclude subdirectories by checking if the Key ends with '/'
30
- if (!key.endsWith('/')) {
31
- const fileName = removePrefix(key);
32
- const headCommand = new HeadObjectCommand({
33
- Bucket: bucket,
34
- Key: key,
35
- });
36
- const headResponse = await S3.send(headCommand);
37
- objects.push({
38
- key: fileName,
39
- isDirectory: false,
40
- lastModified: content.LastModified,
41
- size: content.Size,
42
- contentType: headResponse.ContentType,
43
- });
44
- }
45
- }
46
- }
47
- }
48
- return objects;
49
- }
50
- // async function listBucketContents(
51
- // prefix: string,
52
- // ): Promise<ListObjectsCommandOutput> {
53
- // if (!prefix.length) {
54
- // throw new Error('Prefix cannot be empty');
55
- // }
56
- // const listObjectsCommand = new ListObjectsCommand({
57
- // Bucket: S3_BUCKET_NAME,
58
- // Prefix: `${prefix}/`,
59
- // });
60
- // const response = await S3.send(listObjectsCommand);
61
- // return response;
62
- // }
63
- /**
64
- * Delete all files with the given prefix
65
- * @param prefix
66
- */
67
- export async function deleteS3Files(S3, bucket, prefix) {
68
- const data = await S3.send(new ListObjectsCommand({ Bucket: bucket, Prefix: `${prefix}/` }));
69
- if (data.Contents?.length) {
70
- for (const obj of data.Contents) {
71
- await S3.send(new DeleteObjectCommand({ Bucket: bucket, Key: obj.Key }));
72
- }
73
- }
74
- }
75
- //# sourceMappingURL=s3.js.map
package/dist/s3.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"s3.js","sourceRoot":"","sources":["../src/s3.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n DeleteObjectCommand,\n HeadObjectCommand,\n ListObjectsCommand,\n ListObjectsV2Command,\n S3Client,\n} from '@aws-sdk/client-s3';\nimport {S3FileOrDirectory} from './S3FileOrDirectory';\n\nexport async function listFilesAndDirectoriesWithPrefix(\n S3: S3Client,\n bucket: string,\n prefix?: string,\n): Promise<S3FileOrDirectory[]> {\n const command = new ListObjectsV2Command({\n Bucket: bucket,\n Prefix: prefix ? `${prefix}${prefix.endsWith('/') ? '' : '/'}` : '',\n Delimiter: '/',\n });\n\n const response = await S3.send(command);\n\n const objects: S3FileOrDirectory[] = [];\n\n const removePrefix = (key: string) => {\n if (!prefix) {\n return key;\n }\n return key.replace(prefix ?? '', '');\n };\n\n if (response.CommonPrefixes) {\n for (const commonPrefix of response.CommonPrefixes) {\n if (commonPrefix.Prefix) {\n // Extract the directory name from the CommonPrefixes\n const directoryName = removePrefix(commonPrefix.Prefix).slice(0, -1);\n objects.push({key: directoryName, isDirectory: true});\n }\n }\n }\n\n if (response.Contents) {\n for (const content of response.Contents) {\n const key = content.Key;\n if (key) {\n // Exclude subdirectories by checking if the Key ends with '/'\n if (!key.endsWith('/')) {\n const fileName = removePrefix(key);\n\n const headCommand = new HeadObjectCommand({\n Bucket: bucket,\n Key: key,\n });\n\n const headResponse = await S3.send(headCommand);\n\n objects.push({\n key: fileName,\n isDirectory: false,\n lastModified: content.LastModified,\n size: content.Size,\n contentType: headResponse.ContentType,\n });\n }\n }\n }\n }\n\n return objects;\n}\n\n// async function listBucketContents(\n// prefix: string,\n// ): Promise<ListObjectsCommandOutput> {\n// if (!prefix.length) {\n// throw new Error('Prefix cannot be empty');\n// }\n// const listObjectsCommand = new ListObjectsCommand({\n// Bucket: S3_BUCKET_NAME,\n// Prefix: `${prefix}/`,\n// });\n// const response = await S3.send(listObjectsCommand);\n// return response;\n// }\n\n/**\n * Delete all files with the given prefix\n * @param prefix\n */\nexport async function deleteS3Files(\n S3: S3Client,\n bucket: string,\n prefix: string,\n) {\n const data = await S3.send(\n new ListObjectsCommand({Bucket: bucket, Prefix: `${prefix}/`}),\n );\n if (data.Contents?.length) {\n for (const obj of data.Contents) {\n await S3.send(new DeleteObjectCommand({Bucket: bucket, Key: obj.Key}));\n }\n }\n}\n"]}