@sqlrooms/s3-browser 0.26.0 → 0.26.1-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -65
- package/dist/S3BrowserSlice.d.ts +11 -0
- package/dist/S3BrowserSlice.d.ts.map +1 -0
- package/dist/S3BrowserSlice.js +23 -0
- package/dist/S3BrowserSlice.js.map +1 -0
- package/dist/S3CredentialsForm.d.ts +65 -0
- package/dist/S3CredentialsForm.d.ts.map +1 -0
- package/dist/S3CredentialsForm.js +208 -0
- package/dist/S3CredentialsForm.js.map +1 -0
- package/dist/S3FileBrowser.d.ts +2 -1
- package/dist/S3FileBrowser.d.ts.map +1 -1
- package/dist/S3FileBrowser.js +56 -36
- package/dist/S3FileBrowser.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +12 -7
- package/dist/S3FileOrDirectory.d.ts +0 -13
- package/dist/S3FileOrDirectory.d.ts.map +0 -1
- package/dist/S3FileOrDirectory.js +0 -15
- package/dist/S3FileOrDirectory.js.map +0 -1
- package/dist/s3.d.ts +0 -9
- package/dist/s3.d.ts.map +0 -1
- package/dist/s3.js +0 -75
- package/dist/s3.js.map +0 -1
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
|
-
###
|
|
172
|
+
### S3CredentialsForm Component
|
|
64
173
|
|
|
65
|
-
The
|
|
174
|
+
The `S3CredentialsForm` component provides a form interface for managing S3 credentials and saved connections.
|
|
66
175
|
|
|
67
176
|
```tsx
|
|
68
|
-
import {
|
|
69
|
-
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
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... Example export: export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/b... export AWS_SESSION_TOKEN=AQoEXAMPLEH4aoAH0gNCAPy... Example credential_process output: { \"AccessKeyId\": \"AKIAIOSFODNN7EXAMPLE\", \"SecretAccessKey\": \"wJalrXUtnFEMI/K7MDENG/bPxRfiCY...\", \"SessionToken\": \"AQoEXAMPLEH4aoAH0gNCAPy...\" }'\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"]}
|
package/dist/S3FileBrowser.d.ts
CHANGED
|
@@ -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":"
|
|
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"}
|
package/dist/S3FileBrowser.js
CHANGED
|
@@ -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 (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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 * \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":"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","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 * \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
|
|
7
|
-
export
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
7
|
-
export
|
|
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,
|
|
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.0",
|
|
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
|
-
"@
|
|
22
|
-
"@sqlrooms/
|
|
23
|
-
"@sqlrooms/
|
|
24
|
-
"
|
|
25
|
-
"
|
|
21
|
+
"@hookform/resolvers": "^3.10.0",
|
|
22
|
+
"@sqlrooms/room-shell": "0.26.1-rc.0",
|
|
23
|
+
"@sqlrooms/s3-browser-config": "0.26.1-rc.0",
|
|
24
|
+
"@sqlrooms/ui": "0.26.1-rc.0",
|
|
25
|
+
"@sqlrooms/utils": "0.26.1-rc.0",
|
|
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": "
|
|
43
|
+
"gitHead": "89c252c460ccd29d108cfabf7b34c4a679f85609"
|
|
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"]}
|