@scality/data-browser-library 1.0.4 → 1.0.6

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.
Files changed (60) hide show
  1. package/dist/components/DataBrowserUI.js +18 -8
  2. package/dist/components/__tests__/BucketCreate.test.js +60 -20
  3. package/dist/components/__tests__/BucketList.test.js +91 -6
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +54 -19
  5. package/dist/components/__tests__/BucketReplicationFormPage.test.js +183 -61
  6. package/dist/components/__tests__/MetadataSearch.test.js +18 -12
  7. package/dist/components/__tests__/ObjectList.test.js +94 -2
  8. package/dist/components/buckets/BucketCreate.d.ts +1 -0
  9. package/dist/components/buckets/BucketCreate.js +57 -7
  10. package/dist/components/buckets/BucketDetails.js +0 -1
  11. package/dist/components/buckets/BucketLifecycleFormPage.js +209 -213
  12. package/dist/components/buckets/BucketList.js +25 -4
  13. package/dist/components/buckets/BucketReplicationFormPage.js +9 -3
  14. package/dist/components/buckets/DeleteBucketConfigRuleButton.js +1 -1
  15. package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
  16. package/dist/components/objects/DeleteObjectButton.d.ts +1 -0
  17. package/dist/components/objects/DeleteObjectButton.js +11 -5
  18. package/dist/components/objects/ObjectDetails/FormComponents.d.ts +9 -0
  19. package/dist/components/objects/ObjectDetails/FormComponents.js +37 -0
  20. package/dist/components/objects/ObjectDetails/ObjectMetadata.js +182 -204
  21. package/dist/components/objects/ObjectDetails/ObjectSummary.js +22 -5
  22. package/dist/components/objects/ObjectDetails/ObjectTags.js +109 -154
  23. package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +3 -3
  24. package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.d.ts +1 -0
  25. package/dist/components/objects/ObjectDetails/__tests__/ObjectMetadata.test.js +230 -0
  26. package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.d.ts +1 -0
  27. package/dist/components/objects/ObjectDetails/__tests__/ObjectTags.test.js +342 -0
  28. package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.d.ts +1 -0
  29. package/dist/components/objects/ObjectDetails/__tests__/formUtils.test.js +202 -0
  30. package/dist/components/objects/ObjectDetails/index.d.ts +2 -1
  31. package/dist/components/objects/ObjectDetails/index.js +12 -16
  32. package/dist/components/objects/ObjectList.d.ts +3 -2
  33. package/dist/components/objects/ObjectList.js +204 -104
  34. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +78 -26
  35. package/dist/components/objects/ObjectPage.js +22 -5
  36. package/dist/components/ui/ArrayFieldActions.js +0 -2
  37. package/dist/components/ui/FilterFormSection.js +17 -36
  38. package/dist/components/ui/FormGrid.d.ts +7 -0
  39. package/dist/components/ui/FormGrid.js +37 -0
  40. package/dist/components/ui/Table.elements.js +1 -0
  41. package/dist/config/__tests__/factory.test.js +29 -1
  42. package/dist/config/factory.d.ts +2 -0
  43. package/dist/config/factory.js +3 -1
  44. package/dist/config/types.d.ts +45 -2
  45. package/dist/hooks/__tests__/usePresigningS3Client.test.d.ts +1 -0
  46. package/dist/hooks/__tests__/usePresigningS3Client.test.js +104 -0
  47. package/dist/hooks/factories/index.d.ts +1 -1
  48. package/dist/hooks/factories/index.js +2 -2
  49. package/dist/hooks/factories/useCreateS3MutationHook.d.ts +2 -1
  50. package/dist/hooks/factories/useCreateS3MutationHook.js +10 -3
  51. package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -0
  52. package/dist/hooks/factories/useCreateS3QueryHook.js +9 -6
  53. package/dist/hooks/index.d.ts +1 -0
  54. package/dist/hooks/index.js +2 -1
  55. package/dist/hooks/presignedOperations.js +4 -4
  56. package/dist/hooks/useBucketLocations.d.ts +6 -0
  57. package/dist/hooks/useBucketLocations.js +45 -0
  58. package/dist/hooks/usePresigningS3Client.d.ts +13 -0
  59. package/dist/hooks/usePresigningS3Client.js +21 -0
  60. package/package.json +4 -3
@@ -10,11 +10,13 @@ const ObjectPage = ()=>{
10
10
  const navigate = useDataBrowserNavigate();
11
11
  const [searchParams] = useSearchParams();
12
12
  const [item, setItem] = useState(null);
13
+ const [multipleSelected, setMultipleSelected] = useState(false);
13
14
  const prefix = searchParams.get('prefix') || '';
14
15
  const handlePrefixChange = useCallback((newPrefix)=>{
15
16
  const newSearchParams = new URLSearchParams(searchParams);
16
17
  newSearchParams.set('prefix', newPrefix);
17
18
  setItem(null);
19
+ setMultipleSelected(false);
18
20
  navigate(`?${newSearchParams.toString()}`, {
19
21
  replace: true
20
22
  });
@@ -22,12 +24,25 @@ const ObjectPage = ()=>{
22
24
  navigate,
23
25
  searchParams
24
26
  ]);
27
+ const handleObjectSelect = useCallback((object)=>{
28
+ setItem(object);
29
+ setMultipleSelected(false);
30
+ }, []);
31
+ const handleSelectedObjectsChange = useCallback((objects)=>{
32
+ if (1 === objects.length) {
33
+ setItem(objects[0]);
34
+ setMultipleSelected(false);
35
+ } else if (objects.length > 1) {
36
+ setItem(null);
37
+ setMultipleSelected(true);
38
+ } else {
39
+ setItem(null);
40
+ setMultipleSelected(false);
41
+ }
42
+ }, []);
25
43
  if (!bucketName) return /*#__PURE__*/ jsx("div", {
26
44
  children: "Bucket name is required"
27
45
  });
28
- const handleObjectSelect = (object)=>{
29
- setItem(object);
30
- };
31
46
  return /*#__PURE__*/ jsx(BrowserPageLayout, {
32
47
  iconName: "Bucket",
33
48
  title: bucketName,
@@ -35,10 +50,12 @@ const ObjectPage = ()=>{
35
50
  bucketName: bucketName,
36
51
  prefix: prefix,
37
52
  onObjectSelect: handleObjectSelect,
38
- onPrefixChange: handlePrefixChange
53
+ onPrefixChange: handlePrefixChange,
54
+ onSelectedObjectsChange: handleSelectedObjectsChange
39
55
  }),
40
56
  rightPanel: /*#__PURE__*/ jsx(ObjectDetails, {
41
- item: item
57
+ item: item,
58
+ multipleSelected: multipleSelected
42
59
  }),
43
60
  withArrowNavigation: true,
44
61
  arrowNavigationPath: `/buckets/${bucketName}`
@@ -1,11 +1,9 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { Icon, spacing } from "@scality/core-ui";
3
- import { convertSizeToRem } from "@scality/core-ui/dist/components/inputv2/inputv2";
4
3
  import { Box, Button } from "@scality/core-ui/dist/next";
5
4
  function ArrayFieldActions({ onRemove, onAdd, canRemove, canAdd = true, showAdd, removeLabel = 'Remove', addLabel = 'Add' }) {
6
5
  return /*#__PURE__*/ jsxs(Box, {
7
6
  display: "flex",
8
- width: convertSizeToRem('1/2'),
9
7
  gap: spacing.r8,
10
8
  justifyContent: "flex-start",
11
9
  children: [
@@ -1,8 +1,9 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { FormGroup, FormSection, Stack, Text } from "@scality/core-ui";
2
+ import { FormGroup, FormSection, Stack } from "@scality/core-ui";
3
3
  import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
4
- import { Box, Input, Select } from "@scality/core-ui/dist/next";
4
+ import { Input, Select } from "@scality/core-ui/dist/next";
5
5
  import { ArrayFieldActions } from "./ArrayFieldActions.js";
6
+ import { FormCell, FormColumnHeaders, FormRow } from "./FormGrid.js";
6
7
  const filterTypeOptions = [
7
8
  {
8
9
  value: 'none',
@@ -68,42 +69,22 @@ function FilterFormSection({ filterType, onFilterTypeChange, prefixRegister, tag
68
69
  direction: "vertical",
69
70
  gap: "r8",
70
71
  children: [
71
- /*#__PURE__*/ jsxs(Stack, {
72
- gap: "r8",
73
- children: [
74
- /*#__PURE__*/ jsx(Box, {
75
- flex: "1",
76
- children: /*#__PURE__*/ jsx(Text, {
77
- color: "textSecondary",
78
- children: "Key"
79
- })
80
- }),
81
- /*#__PURE__*/ jsx(Box, {
82
- flex: "1",
83
- children: /*#__PURE__*/ jsx(Text, {
84
- color: "textSecondary",
85
- children: "Value"
86
- })
87
- }),
88
- /*#__PURE__*/ jsx(Box, {
89
- width: "80px"
90
- })
91
- ]
92
- }),
93
- tagFields.map((field, index)=>/*#__PURE__*/ jsxs(Stack, {
94
- gap: "r8",
72
+ /*#__PURE__*/ jsx(FormColumnHeaders, {}),
73
+ tagFields.map((field, index)=>/*#__PURE__*/ jsxs(FormRow, {
95
74
  children: [
96
- /*#__PURE__*/ jsx(Input, {
97
- id: `tag-key-${index}`,
98
- placeholder: "Key",
99
- size: "1/2",
100
- ...tagKeyRegister(index)
75
+ /*#__PURE__*/ jsx(FormCell, {
76
+ children: /*#__PURE__*/ jsx(Input, {
77
+ id: `tag-key-${index}`,
78
+ placeholder: "Key",
79
+ ...tagKeyRegister(index)
80
+ })
101
81
  }),
102
- /*#__PURE__*/ jsx(Input, {
103
- id: `tag-value-${index}`,
104
- placeholder: "Value",
105
- size: "1/2",
106
- ...tagValueRegister(index)
82
+ /*#__PURE__*/ jsx(FormCell, {
83
+ children: /*#__PURE__*/ jsx(Input, {
84
+ id: `tag-value-${index}`,
85
+ placeholder: "Value",
86
+ ...tagValueRegister(index)
87
+ })
107
88
  }),
108
89
  /*#__PURE__*/ jsx(ArrayFieldActions, {
109
90
  showAdd: index === tagFields.length - 1,
@@ -0,0 +1,7 @@
1
+ export declare const FormRow: import("styled-components").StyledComponent<"div", any, {
2
+ columns?: string;
3
+ }, never>;
4
+ export declare const FormCell: import("styled-components").StyledComponent<"div", any, {}, never>;
5
+ export declare const FormColumnHeaders: ({ wideKey }: {
6
+ wideKey?: boolean;
7
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,37 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Text, spacing } from "@scality/core-ui";
3
+ import styled_components from "styled-components";
4
+ const ACTIONS_WIDTH = `calc(${spacing.r32} * 2 + ${spacing.r8})`;
5
+ const FormRow = styled_components.div`
6
+ display: grid;
7
+ grid-template-columns: ${({ columns })=>columns || '1fr 1fr'} ${ACTIONS_WIDTH};
8
+ gap: ${spacing.r8};
9
+ align-items: center;
10
+ `;
11
+ const FormCell = styled_components.div`
12
+ min-width: 0;
13
+ > * {
14
+ width: 100% !important;
15
+ }
16
+ `;
17
+ const FormColumnHeaders = ({ wideKey })=>/*#__PURE__*/ jsxs(FormRow, {
18
+ columns: wideKey ? '3fr auto 2fr' : void 0,
19
+ children: [
20
+ /*#__PURE__*/ jsx(Text, {
21
+ color: "textPrimary",
22
+ children: "Key"
23
+ }),
24
+ wideKey && /*#__PURE__*/ jsx("span", {
25
+ style: {
26
+ visibility: 'hidden'
27
+ },
28
+ children: ":"
29
+ }),
30
+ /*#__PURE__*/ jsx(Text, {
31
+ color: "textPrimary",
32
+ children: "Value"
33
+ }),
34
+ /*#__PURE__*/ jsx("span", {})
35
+ ]
36
+ });
37
+ export { FormCell, FormColumnHeaders, FormRow };
@@ -6,6 +6,7 @@ const Table = ({ children, ...props })=>/*#__PURE__*/ jsx(Box, {
6
6
  height: "100%",
7
7
  overflow: "auto",
8
8
  width: "100%",
9
+ paddingRight: spacing.r16,
9
10
  ...props,
10
11
  children: children
11
12
  });
@@ -212,7 +212,9 @@ describe('s3RuntimeConfigSchema', ()=>{
212
212
  endpoint: 'https://s3.amazonaws.com',
213
213
  region: 'us-east-1',
214
214
  forcePathStyle: true
215
- }
215
+ },
216
+ allowCustomEndpoint: true,
217
+ allowSessionToken: true
216
218
  };
217
219
  const { error } = s3RuntimeConfigSchema.validate(config);
218
220
  expect(error).toBeUndefined();
@@ -282,6 +284,32 @@ describe('s3RuntimeConfigSchema', ()=>{
282
284
  expect(error).toBeDefined();
283
285
  expect(error?.message).toContain('boolean');
284
286
  });
287
+ it('should reject config with wrong type for allowCustomEndpoint', ()=>{
288
+ const config = {
289
+ s3: {
290
+ region: 'us-east-1'
291
+ },
292
+ allowCustomEndpoint: 'yes'
293
+ };
294
+ const { error } = s3RuntimeConfigSchema.validate(config, {
295
+ convert: false
296
+ });
297
+ expect(error).toBeDefined();
298
+ expect(error?.message).toContain('boolean');
299
+ });
300
+ it('should reject config with wrong type for allowSessionToken', ()=>{
301
+ const config = {
302
+ s3: {
303
+ region: 'us-east-1'
304
+ },
305
+ allowSessionToken: 'yes'
306
+ };
307
+ const { error } = s3RuntimeConfigSchema.validate(config, {
308
+ convert: false
309
+ });
310
+ expect(error).toBeDefined();
311
+ expect(error?.message).toContain('boolean');
312
+ });
285
313
  });
286
314
  describe('Schema behavior', ()=>{
287
315
  it('should return all validation errors when abortEarly is false', ()=>{
@@ -5,6 +5,8 @@ export type S3RuntimeConfig = {
5
5
  region: string;
6
6
  forcePathStyle?: boolean;
7
7
  };
8
+ allowCustomEndpoint?: boolean;
9
+ allowSessionToken?: boolean;
8
10
  };
9
11
  export declare const s3RuntimeConfigSchema: Joi.ObjectSchema<any>;
10
12
  /**
@@ -4,7 +4,9 @@ const s3RuntimeConfigSchema = joi.object({
4
4
  endpoint: joi.string().optional().allow('origin'),
5
5
  region: joi.string().min(1).required(),
6
6
  forcePathStyle: joi.boolean().optional()
7
- }).required()
7
+ }).required(),
8
+ allowCustomEndpoint: joi.boolean().optional(),
9
+ allowSessionToken: joi.boolean().optional()
8
10
  });
9
11
  async function loadRuntimeConfig(configUrl) {
10
12
  const response = await fetch(configUrl);
@@ -144,12 +144,40 @@ export interface SectionConfig {
144
144
  render: () => React.ReactNode;
145
145
  }
146
146
  /**
147
- * Storage class selector props
147
+ * Base props for custom selector components injected into the library.
148
+ * `value` is the current selection; `onChange` is called when the user picks a new option.
148
149
  */
149
- export interface StorageClassSelectorProps {
150
+ interface SelectorProps {
150
151
  value: string;
151
152
  onChange: (newValue: string) => void;
152
153
  }
154
+ /**
155
+ * Props for a custom storage class selector used in replication and lifecycle forms.
156
+ * The component receives the current storage class value and must call `onChange` with the new value.
157
+ *
158
+ * `context` indicates which form is rendering the selector, allowing the consumer
159
+ * to apply different filtering logic (e.g. replication targets vs transition targets).
160
+ */
161
+ export interface StorageClassSelectorProps extends SelectorProps {
162
+ context: 'replication' | 'lifecycle';
163
+ }
164
+ /**
165
+ * Props for a custom location selector in bucket creation (maps to S3 `LocationConstraint`).
166
+ * The component receives the current location value and must call `onChange` with the new value.
167
+ */
168
+ export interface LocationSelectorProps extends SelectorProps {
169
+ }
170
+ /**
171
+ * Props passed to the custom versioning renderer in bucket creation.
172
+ * Provides commonly-needed form state as props for convenience.
173
+ *
174
+ * The component is rendered inside a `FormProvider`, so consumers can also call
175
+ * `useFormContext()` for full form access (register, setValue, watch, etc.).
176
+ */
177
+ export interface BucketCreateVersioningProps {
178
+ isVersioning: boolean;
179
+ isObjectLockEnabled: boolean;
180
+ }
153
181
  /**
154
182
  * Main DataBrowserUI component props
155
183
  */
@@ -161,6 +189,20 @@ export interface DataBrowserUIProps {
161
189
  */
162
190
  header?: React.ReactNode;
163
191
  storageClassSelector?: React.ComponentType<StorageClassSelectorProps>;
192
+ /** Label for the storage class selector. Defaults to "Storage Class". */
193
+ storageClassLabel?: string;
194
+ /** Custom location selector for bucket creation (LocationConstraint). */
195
+ locationSelector?: React.ComponentType<LocationSelectorProps>;
196
+ /** Extra form fields rendered after the location selector inside bucket creation. Components can use useFormContext() to access form state. */
197
+ bucketCreateExtraFields?: React.ReactNode;
198
+ /**
199
+ * Transform form data before the default bucket creation logic runs.
200
+ * The function receives all form values and must return the full object —
201
+ * only modify the fields you need (e.g. append ':ingest' suffix to locationConstraint).
202
+ */
203
+ transformBucketCreateData?: <T extends Record<string, unknown>>(data: T) => T;
204
+ /** Custom versioning section for bucket creation. When provided, replaces the default versioning FormGroup entirely. */
205
+ bucketCreateVersioning?: React.ComponentType<BucketCreateVersioningProps>;
164
206
  extraBucketListColumns?: ColumnConfig<Bucket>[];
165
207
  extraBucketListActions?: ActionConfig[];
166
208
  extraBucketTabs?: TabConfig[];
@@ -222,3 +264,4 @@ export interface DataBrowserUIProps {
222
264
  }
223
265
  export type S3EventType = 's3:ObjectCreated:*' | 's3:ObjectCreated:Put' | 's3:ObjectCreated:Post' | 's3:ObjectCreated:Copy' | 's3:ObjectCreated:CompleteMultipartUpload' | 's3:ObjectRemoved:*' | 's3:ObjectRemoved:Delete' | 's3:ObjectRemoved:DeleteMarkerCreated' | 's3:ObjectRestore:*' | 's3:ObjectRestore:Post' | 's3:ObjectRestore:Completed' | 's3:ObjectRestore:Delete' | 's3:LifecycleExpiration:*' | 's3:LifecycleExpiration:Delete' | 's3:LifecycleExpiration:DeleteMarkerCreated' | 's3:LifecycleTransition' | 's3:Replication:*' | 's3:Replication:OperationFailedReplication' | 's3:Replication:OperationMissedThreshold' | 's3:Replication:OperationReplicatedAfterThreshold' | 's3:Replication:OperationNotTracked' | 's3:ObjectTagging:*' | 's3:ObjectTagging:Put' | 's3:ObjectTagging:Delete' | 's3:ReducedRedundancyLostObject' | 's3:IntelligentTiering' | 's3:ObjectAcl:Put' | 's3:TestEvent';
224
266
  export type S3EventCategory = 'Object Creation' | 'Object Deletion' | 'Object Restoration' | 'Lifecycle' | 'Replication' | 'Object Tagging' | 'Storage & Access' | 'Testing';
267
+ export {};
@@ -0,0 +1,104 @@
1
+ import { S3Client } from "@aws-sdk/client-s3";
2
+ import { renderHook } from "@testing-library/react";
3
+ import { createTestWrapper, testConfig, testCredentials } from "../../test/testUtils.js";
4
+ import { usePresigningS3Client } from "../usePresigningS3Client.js";
5
+ jest.mock('@aws-sdk/client-s3');
6
+ const MockedS3Client = S3Client;
7
+ const proxyConfig = {
8
+ ...testConfig,
9
+ proxy: {
10
+ enabled: true,
11
+ endpoint: 'https://proxy.example.com/s3',
12
+ target: 'http://internal-s3.cluster.local'
13
+ }
14
+ };
15
+ describe('usePresigningS3Client', ()=>{
16
+ beforeEach(()=>{
17
+ jest.clearAllMocks();
18
+ MockedS3Client.mockImplementation(()=>({
19
+ config: {},
20
+ middlewareStack: {
21
+ use: jest.fn(),
22
+ add: jest.fn()
23
+ }
24
+ }));
25
+ });
26
+ it('should create an S3 client with the direct endpoint when no proxy is configured', ()=>{
27
+ renderHook(()=>usePresigningS3Client(), {
28
+ wrapper: createTestWrapper(testConfig, testCredentials)
29
+ });
30
+ expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
31
+ endpoint: testConfig.endpoint,
32
+ region: testConfig.region,
33
+ forcePathStyle: true
34
+ }));
35
+ });
36
+ it('should create an S3 client with the proxy endpoint (not target) when proxy is enabled', ()=>{
37
+ renderHook(()=>usePresigningS3Client(), {
38
+ wrapper: createTestWrapper(proxyConfig, testCredentials)
39
+ });
40
+ expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
41
+ endpoint: 'https://proxy.example.com/s3',
42
+ region: testConfig.region,
43
+ forcePathStyle: true
44
+ }));
45
+ const constructorCall = MockedS3Client.mock.calls[0][0];
46
+ expect(constructorCall?.endpoint).not.toContain('internal-s3.cluster.local');
47
+ });
48
+ it('should not attach proxy middleware even when proxy is enabled', ()=>{
49
+ const mockUse = jest.fn();
50
+ MockedS3Client.mockImplementation(()=>({
51
+ config: {},
52
+ middlewareStack: {
53
+ use: mockUse,
54
+ add: jest.fn()
55
+ }
56
+ }));
57
+ renderHook(()=>usePresigningS3Client(), {
58
+ wrapper: createTestWrapper(proxyConfig, testCredentials)
59
+ });
60
+ expect(mockUse).not.toHaveBeenCalled();
61
+ });
62
+ it('should use origin-relative proxy endpoint resolved with window.location.origin', ()=>{
63
+ const originRelativeConfig = {
64
+ ...testConfig,
65
+ proxy: {
66
+ enabled: true,
67
+ endpoint: '/zenko/s3',
68
+ target: 'http://artesca-data-connector-s3api.zenko.svc.cluster.local'
69
+ }
70
+ };
71
+ const originalOrigin = window.location.origin;
72
+ renderHook(()=>usePresigningS3Client(), {
73
+ wrapper: createTestWrapper(originRelativeConfig, testCredentials)
74
+ });
75
+ expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
76
+ endpoint: `${originalOrigin}/zenko/s3`
77
+ }));
78
+ });
79
+ it('should use direct endpoint when proxy is disabled', ()=>{
80
+ const disabledProxyConfig = {
81
+ ...testConfig,
82
+ proxy: {
83
+ enabled: false,
84
+ endpoint: 'https://proxy.example.com/s3'
85
+ }
86
+ };
87
+ renderHook(()=>usePresigningS3Client(), {
88
+ wrapper: createTestWrapper(disabledProxyConfig, testCredentials)
89
+ });
90
+ expect(MockedS3Client).toHaveBeenCalledWith(expect.objectContaining({
91
+ endpoint: testConfig.endpoint
92
+ }));
93
+ });
94
+ it('should return the same client instance on re-render when s3ConfigIdentifier is unchanged', ()=>{
95
+ const { result, rerender } = renderHook(()=>usePresigningS3Client(), {
96
+ wrapper: createTestWrapper(testConfig, testCredentials)
97
+ });
98
+ const firstClient = result.current;
99
+ rerender();
100
+ const secondClient = result.current;
101
+ expect(firstClient).toBe(secondClient);
102
+ expect(MockedS3Client).toHaveBeenCalledTimes(1);
103
+ });
104
+ });
@@ -14,5 +14,5 @@
14
14
  */
15
15
  export { useCreateS3InfiniteQueryHook } from './useCreateS3InfiniteQueryHook';
16
16
  export { useCreateS3LoginHook } from './useCreateS3LoginHook';
17
- export { useCreateS3FunctionMutationHook, useCreateS3MutationHook, } from './useCreateS3MutationHook';
17
+ export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook, } from './useCreateS3MutationHook';
18
18
  export { useCreateS3QueryHook } from './useCreateS3QueryHook';
@@ -1,5 +1,5 @@
1
1
  import { useCreateS3InfiniteQueryHook } from "./useCreateS3InfiniteQueryHook.js";
2
2
  import { useCreateS3LoginHook } from "./useCreateS3LoginHook.js";
3
- import { useCreateS3FunctionMutationHook, useCreateS3MutationHook } from "./useCreateS3MutationHook.js";
3
+ import { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook } from "./useCreateS3MutationHook.js";
4
4
  import { useCreateS3QueryHook } from "./useCreateS3QueryHook.js";
5
- export { useCreateS3FunctionMutationHook, useCreateS3InfiniteQueryHook, useCreateS3LoginHook, useCreateS3MutationHook, useCreateS3QueryHook };
5
+ export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3InfiniteQueryHook, useCreateS3LoginHook, useCreateS3MutationHook, useCreateS3QueryHook };
@@ -2,4 +2,5 @@ import type { S3Client } from '@aws-sdk/client-s3';
2
2
  import { type UseMutationOptions, type UseMutationResult } from '@tanstack/react-query';
3
3
  import { type EnhancedS3Error } from '../../utils/errorHandling';
4
4
  export declare function useCreateS3MutationHook<TInput extends object, TOutput>(Command: new (input: TInput) => any, operationName: string, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
5
- export declare function useCreateS3FunctionMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput>, "mutationFn">) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
5
+ export declare function useCreateS3FunctionMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput, unknown>, "mutationFn"> | undefined) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
6
+ export declare function useCreatePresigningMutationHook<TInput, TOutput>(operation: (s3Client: S3Client, input: TInput) => Promise<TOutput>, invalidationKeys?: string[]): (options?: Omit<UseMutationOptions<TOutput, EnhancedS3Error, TInput, unknown>, "mutationFn"> | undefined) => UseMutationResult<TOutput, EnhancedS3Error, TInput>;
@@ -1,6 +1,7 @@
1
1
  import { useMutation, useQueryClient } from "@tanstack/react-query";
2
2
  import { useDataBrowserContext } from "../../components/providers/DataBrowserProvider.js";
3
3
  import { createS3OperationError } from "../../utils/errorHandling.js";
4
+ import { usePresigningS3Client } from "../usePresigningS3Client.js";
4
5
  import { useS3Client } from "../useS3Client.js";
5
6
  function useCreateS3MutationHook(Command, operationName, invalidationKeys) {
6
7
  return (options)=>{
@@ -31,10 +32,10 @@ function useCreateS3MutationHook(Command, operationName, invalidationKeys) {
31
32
  });
32
33
  };
33
34
  }
34
- function useCreateS3FunctionMutationHook(operation, invalidationKeys) {
35
+ function createFunctionMutationHook(operation, useClient, invalidationKeys) {
35
36
  return (options)=>{
36
37
  const { s3ConfigIdentifier } = useDataBrowserContext();
37
- const s3Client = useS3Client();
38
+ const s3Client = useClient();
38
39
  const queryClient = useQueryClient();
39
40
  return useMutation({
40
41
  mutationFn: async (params)=>operation(s3Client, params),
@@ -52,4 +53,10 @@ function useCreateS3FunctionMutationHook(operation, invalidationKeys) {
52
53
  });
53
54
  };
54
55
  }
55
- export { useCreateS3FunctionMutationHook, useCreateS3MutationHook };
56
+ function useCreateS3FunctionMutationHook(operation, invalidationKeys) {
57
+ return createFunctionMutationHook(operation, useS3Client, invalidationKeys);
58
+ }
59
+ function useCreatePresigningMutationHook(operation, invalidationKeys) {
60
+ return createFunctionMutationHook(operation, usePresigningS3Client, invalidationKeys);
61
+ }
62
+ export { useCreatePresigningMutationHook, useCreateS3FunctionMutationHook, useCreateS3MutationHook };
@@ -1,3 +1,4 @@
1
1
  import { type QueryKey, type UseQueryOptions, type UseQueryResult } from '@tanstack/react-query';
2
2
  import { type EnhancedS3Error } from '../../utils/errorHandling';
3
+ export declare function createS3QueryKey(s3ConfigIdentifier: string, operationName: string, params?: object): QueryKey;
3
4
  export declare function useCreateS3QueryHook<TInput extends object, TOutput>(Command: new (input: TInput) => any, operationName: string): (params?: TInput, options?: Omit<UseQueryOptions<TOutput, EnhancedS3Error, TOutput, QueryKey>, "queryKey" | "queryFn">) => UseQueryResult<TOutput, EnhancedS3Error>;
@@ -21,15 +21,18 @@ function getEmptyConfigForOperation(operationName) {
21
21
  return null;
22
22
  }
23
23
  }
24
+ function createS3QueryKey(s3ConfigIdentifier, operationName, params) {
25
+ return [
26
+ s3ConfigIdentifier,
27
+ operationName,
28
+ params
29
+ ];
30
+ }
24
31
  function useCreateS3QueryHook(Command, operationName) {
25
32
  return (params, options)=>{
26
33
  const { s3ConfigIdentifier } = useDataBrowserContext();
27
34
  const s3Client = useS3Client();
28
- const queryKey = [
29
- s3ConfigIdentifier,
30
- operationName,
31
- params
32
- ];
35
+ const queryKey = createS3QueryKey(s3ConfigIdentifier, operationName, params);
33
36
  return useQuery({
34
37
  queryKey,
35
38
  queryFn: async ({ signal })=>{
@@ -53,4 +56,4 @@ function useCreateS3QueryHook(Command, operationName) {
53
56
  });
54
57
  };
55
58
  }
56
- export { useCreateS3QueryHook };
59
+ export { createS3QueryKey, useCreateS3QueryHook };
@@ -7,6 +7,7 @@ export { useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, }
7
7
  export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, } from './useAccessibleBuckets';
8
8
  export { useBatchObjectLegalHold } from './useBatchObjectLegalHold';
9
9
  export { type BucketConfigEditorConfig, type BucketConfigEditorResult, useBucketConfigEditor, } from './useBucketConfigEditor';
10
+ export { useBucketLocations } from './useBucketLocations';
10
11
  export { useDeleteBucketConfigRule } from './useDeleteBucketConfigRule';
11
12
  export { useEmptyBucket } from './useEmptyBucket';
12
13
  export { useFeatures } from './useFeatures';
@@ -6,6 +6,7 @@ import { useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload } f
6
6
  import { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets } from "./useAccessibleBuckets.js";
7
7
  import { useBatchObjectLegalHold } from "./useBatchObjectLegalHold.js";
8
8
  import { useBucketConfigEditor } from "./useBucketConfigEditor.js";
9
+ import { useBucketLocations } from "./useBucketLocations.js";
9
10
  import { useDeleteBucketConfigRule } from "./useDeleteBucketConfigRule.js";
10
11
  import { useEmptyBucket } from "./useEmptyBucket.js";
11
12
  import { useFeatures } from "./useFeatures.js";
@@ -16,4 +17,4 @@ import { useS3Client } from "./useS3Client.js";
16
17
  import { useS3ConfigSwitch } from "./useS3ConfigSwitch.js";
17
18
  import { useSupportedNotificationEvents } from "./useSupportedNotificationEvents.js";
18
19
  import { useTableRowSelection } from "./useTableRowSelection.js";
19
- export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
20
+ export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, useBatchObjectLegalHold, useBucketConfigEditor, useBucketLocations, useBuckets, useCopyObject, useCreateBucket, useCreateFolder, useDeleteBucket, useDeleteBucketConfigRule, useDeleteBucketCors, useDeleteBucketLifecycle, useDeleteBucketPolicy, useDeleteBucketReplication, useDeleteBucketTagging, useDeleteObject, useDeleteObjectTagging, useDeleteObjects, useEmptyBucket, useFeatures, useGetBucketAcl, useGetBucketCors, useGetBucketEncryption, useGetBucketLifecycle, useGetBucketLocation, useGetBucketNotification, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketReplication, useGetBucketTagging, useGetBucketVersioning, useGetObject, useGetObjectAttributes, useGetObjectTorrent, useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, useGetPublicAccessBlock, useISVBucketStatus, useIsBucketEmpty, useLimitedAccessFlow, useListMultipartUploads, useListObjectVersions, useListObjects, useLoginMutation, useObjectAcl, useObjectLegalHold, useObjectMetadata, useObjectRetention, useObjectTagging, usePutObject, useRestoreObject, useS3Client, useS3ConfigSwitch, useSearchObjects, useSearchObjectsVersions, useSelectObjectContent, useSetBucketAcl, useSetBucketCors, useSetBucketEncryption, useSetBucketLifecycle, useSetBucketNotification, useSetBucketObjectLockConfiguration, useSetBucketPolicy, useSetBucketReplication, useSetBucketTagging, useSetBucketVersioning, useSetObjectAcl, useSetObjectLegalHold, useSetObjectRetention, useSetObjectTagging, useSupportedNotificationEvents, useTableRowSelection, useUploadObjects, useValidateBucketAccess };
@@ -2,7 +2,7 @@ import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
2
2
  import { createPresignedPost } from "@aws-sdk/s3-presigned-post";
3
3
  import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
4
4
  import { createS3OperationError } from "../utils/errorHandling.js";
5
- import { useCreateS3FunctionMutationHook } from "./factories/index.js";
5
+ import { useCreatePresigningMutationHook } from "./factories/index.js";
6
6
  const generatePresignedDownloadUrl = async (client, config)=>{
7
7
  try {
8
8
  const { Bucket, Key, expiresIn = 3600, ...awsOptions } = config;
@@ -66,7 +66,7 @@ const generatePresignedPost = async (client, config)=>{
66
66
  throw createS3OperationError(error, 'GeneratePresignedPost', config.Bucket, config.Key);
67
67
  }
68
68
  };
69
- const useGetPresignedDownload = useCreateS3FunctionMutationHook(generatePresignedDownloadUrl);
70
- const useGetPresignedUpload = useCreateS3FunctionMutationHook(generatePresignedUploadUrl);
71
- const useGetPresignedPost = useCreateS3FunctionMutationHook(generatePresignedPost);
69
+ const useGetPresignedDownload = useCreatePresigningMutationHook(generatePresignedDownloadUrl);
70
+ const useGetPresignedUpload = useCreatePresigningMutationHook(generatePresignedUploadUrl);
71
+ const useGetPresignedPost = useCreatePresigningMutationHook(generatePresignedPost);
72
72
  export { useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Hook for fetching locations of all provided buckets in parallel.
3
+ * Shares query cache with useGetBucketLocation (same query keys via createS3QueryKey).
4
+ * Returns a Map<bucketName, locationConstraint>.
5
+ */
6
+ export declare const useBucketLocations: (bucketNames: string[]) => Map<string, string>;
@@ -0,0 +1,45 @@
1
+ import { GetBucketLocationCommand } from "@aws-sdk/client-s3";
2
+ import { useQueries } from "@tanstack/react-query";
3
+ import { useMemo } from "react";
4
+ import { useDataBrowserContext } from "../components/providers/DataBrowserProvider.js";
5
+ import { createS3OperationError, shouldRetryError } from "../utils/errorHandling.js";
6
+ import { createS3QueryKey } from "./factories/useCreateS3QueryHook.js";
7
+ import { useS3Client } from "./useS3Client.js";
8
+ const useBucketLocations = (bucketNames)=>{
9
+ const { s3ConfigIdentifier } = useDataBrowserContext();
10
+ const s3Client = useS3Client();
11
+ const results = useQueries({
12
+ queries: bucketNames.map((name)=>({
13
+ queryKey: createS3QueryKey(s3ConfigIdentifier, 'GetBucketLocation', {
14
+ Bucket: name
15
+ }),
16
+ queryFn: async ({ signal })=>{
17
+ try {
18
+ return await s3Client.send(new GetBucketLocationCommand({
19
+ Bucket: name
20
+ }), {
21
+ abortSignal: signal
22
+ });
23
+ } catch (error) {
24
+ throw createS3OperationError(error, 'GetBucketLocation', name);
25
+ }
26
+ },
27
+ retry: (failureCount, error)=>shouldRetryError(error, failureCount)
28
+ }))
29
+ });
30
+ const locationsKey = results.map((r, i)=>{
31
+ if ('success' === r.status && r.data) return `${bucketNames[i]}=${r.data.LocationConstraint || 'us-east-1'}`;
32
+ return `${bucketNames[i]}:${r.status}`;
33
+ }).join(',');
34
+ return useMemo(()=>{
35
+ const map = new Map();
36
+ for(let i = 0; i < bucketNames.length; i++){
37
+ const result = results[i];
38
+ if ('success' === result.status && result.data) map.set(bucketNames[i], result.data.LocationConstraint || 'us-east-1');
39
+ }
40
+ return map;
41
+ }, [
42
+ locationsKey
43
+ ]);
44
+ };
45
+ export { useBucketLocations };