@scality/data-browser-library 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/components/__tests__/BucketCorsPage.test.js +67 -9
  2. package/dist/components/__tests__/BucketDetails.test.js +1 -0
  3. package/dist/components/__tests__/BucketLifecycleFormPage.test.js +16 -11
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +45 -0
  5. package/dist/components/__tests__/BucketOverview.test.js +92 -2
  6. package/dist/components/__tests__/BucketPolicyPage.test.js +70 -51
  7. package/dist/components/__tests__/BucketReplicationFormPage.test.js +18 -24
  8. package/dist/components/__tests__/ObjectList.test.js +43 -2
  9. package/dist/components/buckets/BucketConfigEditButton.d.ts +2 -0
  10. package/dist/components/buckets/BucketConfigEditButton.js +9 -3
  11. package/dist/components/buckets/BucketCorsPage.js +57 -20
  12. package/dist/components/buckets/BucketDetails.js +27 -2
  13. package/dist/components/buckets/BucketLifecycleFormPage.js +310 -270
  14. package/dist/components/buckets/BucketOverview.js +21 -18
  15. package/dist/components/buckets/BucketPolicyPage.js +119 -83
  16. package/dist/components/buckets/BucketReplicationFormPage.js +39 -29
  17. package/dist/components/buckets/BucketVersioning.js +16 -10
  18. package/dist/components/buckets/__tests__/BucketVersioning.test.js +76 -23
  19. package/dist/components/buckets/notifications/BucketNotificationFormPage.js +13 -5
  20. package/dist/components/objects/ObjectList.js +22 -25
  21. package/dist/components/objects/ObjectLock/EditRetentionButton.js +2 -2
  22. package/dist/components/objects/UploadButton.js +25 -15
  23. package/dist/config/__tests__/resolveBrandingTheme.test.d.ts +1 -0
  24. package/dist/config/__tests__/resolveBrandingTheme.test.js +96 -0
  25. package/dist/config/resolveBrandingTheme.d.ts +16 -0
  26. package/dist/config/resolveBrandingTheme.js +23 -0
  27. package/dist/config/types.d.ts +36 -0
  28. package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +2 -0
  29. package/dist/hooks/index.d.ts +1 -1
  30. package/dist/hooks/objectOperations.d.ts +3 -3
  31. package/dist/hooks/objectOperations.js +3 -3
  32. package/dist/hooks/useBucketConfigEditor.d.ts +4 -4
  33. package/dist/hooks/useBucketConfigEditor.js +16 -31
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +1 -0
  36. package/dist/test/mocks/esmOnlyModules.js +4 -0
  37. package/dist/types/index.d.ts +0 -1
  38. package/dist/utils/__tests__/proxyMiddleware.test.js +34 -0
  39. package/dist/utils/proxyMiddleware.js +2 -0
  40. package/package.json +4 -4
  41. package/dist/components/Editor.d.ts +0 -12
  42. package/dist/components/Editor.js +0 -28
  43. package/dist/types/monaco.d.ts +0 -13
  44. package/dist/types/monaco.js +0 -0
@@ -16,23 +16,22 @@ const mockUseGetBucketVersioning = jest.mocked(useGetBucketVersioning);
16
16
  const mockUseGetBucketObjectLockConfiguration = jest.mocked(useGetBucketObjectLockConfiguration);
17
17
  const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
18
18
  const mockUseSetBucketVersioning = jest.mocked(useSetBucketVersioning);
19
- const renderBucketVersioning = (props = {})=>{
20
- const mockMutate = jest.fn();
19
+ const renderBucketVersioning = (props = {}, { simulateSuccess = false } = {})=>{
20
+ const mockMutate = jest.fn().mockImplementation((_params, options)=>{
21
+ if (simulateSuccess && options?.onSuccess) options.onSuccess();
22
+ });
21
23
  mockUseSetBucketVersioning.mockReturnValue(createMockMutationResult(mockMutate));
22
24
  const Wrapper = createTestWrapper();
23
- return {
24
- ...render(/*#__PURE__*/ jsx(Wrapper, {
25
- children: /*#__PURE__*/ jsx(BucketOverviewContext.Provider, {
26
- value: {
27
- bucketName: 'test-bucket'
28
- },
29
- children: /*#__PURE__*/ jsx(BucketVersioning, {
30
- ...props
31
- })
25
+ return render(/*#__PURE__*/ jsx(Wrapper, {
26
+ children: /*#__PURE__*/ jsx(BucketOverviewContext.Provider, {
27
+ value: {
28
+ bucketName: 'test-bucket'
29
+ },
30
+ children: /*#__PURE__*/ jsx(BucketVersioning, {
31
+ ...props
32
32
  })
33
- })),
34
- mutate: mockMutate
35
- };
33
+ })
34
+ }));
36
35
  };
37
36
  const mockHookDefaults = ()=>{
38
37
  mockUseGetBucketVersioning.mockReturnValue({
@@ -86,10 +85,10 @@ describe('BucketVersioning', ()=>{
86
85
  expect(toggle).not.toBeChecked();
87
86
  });
88
87
  it('calls mutation with Suspended status when disabling versioning', ()=>{
89
- const { mutate } = renderBucketVersioning();
88
+ renderBucketVersioning();
90
89
  const toggle = findToggleByLabel('Active');
91
90
  fireEvent.click(toggle);
92
- expect(mutate).toHaveBeenCalledWith({
91
+ expect(mockUseSetBucketVersioning().mutate).toHaveBeenCalledWith({
93
92
  Bucket: 'test-bucket',
94
93
  VersioningConfiguration: {
95
94
  Status: 'Suspended'
@@ -104,10 +103,10 @@ describe('BucketVersioning', ()=>{
104
103
  status: 'success',
105
104
  error: null
106
105
  });
107
- const { mutate } = renderBucketVersioning();
106
+ renderBucketVersioning();
108
107
  const toggle = findToggleByLabel('Inactive');
109
108
  fireEvent.click(toggle);
110
- expect(mutate).toHaveBeenCalledWith({
109
+ expect(mockUseSetBucketVersioning().mutate).toHaveBeenCalledWith({
111
110
  Bucket: 'test-bucket',
112
111
  VersioningConfiguration: {
113
112
  Status: 'Enabled'
@@ -135,16 +134,35 @@ describe('BucketVersioning', ()=>{
135
134
  });
136
135
  renderBucketVersioning();
137
136
  expect(screen.getByText('Enabled')).toBeInTheDocument();
138
- expect(screen.getByText(/Versioning cannot be suspended because Object-lock is enabled/i)).toBeInTheDocument();
137
+ expect(screen.getByText(/Versioning cannot be disabled because Object-lock is enabled/i)).toBeInTheDocument();
139
138
  expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
140
139
  });
141
- it('disables toggle when bucket is managed by Veeam', ()=>{
142
- mockUseISVBucketStatus.mockReturnValue({
140
+ it.each([
141
+ {
142
+ isvApplication: 'Veeam',
143
143
  isVeeamBucket: true,
144
144
  isCommvaultBucket: false,
145
- isKastenBucket: false,
145
+ isKastenBucket: false
146
+ },
147
+ {
148
+ isvApplication: 'Commvault',
149
+ isVeeamBucket: false,
150
+ isCommvaultBucket: true,
151
+ isKastenBucket: false
152
+ },
153
+ {
154
+ isvApplication: 'Kasten',
155
+ isVeeamBucket: false,
156
+ isCommvaultBucket: false,
157
+ isKastenBucket: true
158
+ }
159
+ ])('disables toggle when bucket is managed by $isvApplication', ({ isvApplication, isVeeamBucket, isCommvaultBucket, isKastenBucket })=>{
160
+ mockUseISVBucketStatus.mockReturnValue({
161
+ isVeeamBucket,
162
+ isCommvaultBucket,
163
+ isKastenBucket,
146
164
  isISVManaged: true,
147
- isvApplication: 'Veeam',
165
+ isvApplication,
148
166
  isLoading: false,
149
167
  bucketTagsStatus: 'success'
150
168
  });
@@ -152,6 +170,41 @@ describe('BucketVersioning', ()=>{
152
170
  const toggle = screen.getByRole('checkbox');
153
171
  expect(toggle).toHaveAttribute('aria-disabled', 'true');
154
172
  });
173
+ it('shows toast with correct message when disabling versioning', ()=>{
174
+ jest.useFakeTimers();
175
+ renderBucketVersioning({}, {
176
+ simulateSuccess: true
177
+ });
178
+ const toggle = findToggleByLabel('Active');
179
+ fireEvent.click(toggle);
180
+ jest.advanceTimersByTime(500);
181
+ expect(mockShowToast).toHaveBeenCalledWith(expect.objectContaining({
182
+ message: 'Bucket versioning disabled',
183
+ status: 'success'
184
+ }));
185
+ jest.useRealTimers();
186
+ });
187
+ it('shows toast with correct message when enabling versioning', ()=>{
188
+ jest.useFakeTimers();
189
+ mockUseGetBucketVersioning.mockReturnValue({
190
+ data: {
191
+ Status: 'Suspended'
192
+ },
193
+ status: 'success',
194
+ error: null
195
+ });
196
+ renderBucketVersioning({}, {
197
+ simulateSuccess: true
198
+ });
199
+ const toggle = findToggleByLabel('Inactive');
200
+ fireEvent.click(toggle);
201
+ jest.advanceTimersByTime(500);
202
+ expect(mockShowToast).toHaveBeenCalledWith(expect.objectContaining({
203
+ message: 'Bucket versioning enabled',
204
+ status: 'success'
205
+ }));
206
+ jest.useRealTimers();
207
+ });
155
208
  it('disables toggle when versioning query errors', ()=>{
156
209
  mockUseGetBucketVersioning.mockReturnValue({
157
210
  data: void 0,
@@ -1,4 +1,4 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { joiResolver } from "@hookform/resolvers/joi";
3
3
  import { Form, FormGroup, FormSection, Icon, Loader, Stack, Text, spacing, useToast } from "@scality/core-ui";
4
4
  import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
@@ -9,6 +9,7 @@ import { FormProvider, useForm } from "react-hook-form";
9
9
  import { useParams } from "react-router";
10
10
  import { useGetBucketNotification, useSetBucketNotification } from "../../../hooks/index.js";
11
11
  import { useDataBrowserNavigate } from "../../../hooks/useDataBrowserNavigate.js";
12
+ import { useSupportedNotificationEvents } from "../../../hooks/useSupportedNotificationEvents.js";
12
13
  import { EventsSection } from "./EventsSection.js";
13
14
  const baseSchema = joi.object({
14
15
  ruleName: joi.string().required().messages({
@@ -29,6 +30,7 @@ function BucketNotificationFormPage() {
29
30
  const navigate = useDataBrowserNavigate();
30
31
  const { showToast } = useToast();
31
32
  const isEditMode = !!ruleId;
33
+ const supportedEvents = useSupportedNotificationEvents();
32
34
  const { data: existingNotificationData, status: notificationStatus, error: notificationError } = useGetBucketNotification({
33
35
  Bucket: bucketName
34
36
  });
@@ -70,10 +72,15 @@ function BucketNotificationFormPage() {
70
72
  if (isEditMode && existingRule) {
71
73
  const prefixRule = existingRule.Filter?.Key?.FilterRules?.find((r)=>'prefix' === r.Name);
72
74
  const suffixRule = existingRule.Filter?.Key?.FilterRules?.find((r)=>'suffix' === r.Name);
75
+ let events = existingRule.Events || [];
76
+ if (supportedEvents?.length) {
77
+ const supported = new Set(supportedEvents);
78
+ events = events.filter((event)=>supported.has(event));
79
+ }
73
80
  reset({
74
81
  ruleName: existingRule.Id || '',
75
82
  queueArn: existingRule.QueueArn || '',
76
- events: existingRule.Events || [],
83
+ events,
77
84
  prefix: prefixRule?.Value || '',
78
85
  suffix: suffixRule?.Value || ''
79
86
  });
@@ -81,7 +88,8 @@ function BucketNotificationFormPage() {
81
88
  }, [
82
89
  isEditMode,
83
90
  existingRule,
84
- reset
91
+ reset,
92
+ supportedEvents
85
93
  ]);
86
94
  const handleCancel = useCallback(()=>{
87
95
  navigate(`/buckets/${bucketName}?tab=notification`);
@@ -248,7 +256,7 @@ function BucketNotificationFormPage() {
248
256
  direction: "horizontal",
249
257
  error: errors?.ruleName?.message,
250
258
  helpErrorPosition: "bottom",
251
- labelHelpTooltip: /*#__PURE__*/ jsx(Fragment, {}),
259
+ labelHelpTooltip: "Must be unique across all notification rules in this bucket. Cannot be changed after creation.",
252
260
  required: true,
253
261
  content: isEditMode ? /*#__PURE__*/ jsx(Text, {
254
262
  children: existingRule?.Id
@@ -267,7 +275,7 @@ function BucketNotificationFormPage() {
267
275
  direction: "horizontal",
268
276
  error: errors?.queueArn?.message,
269
277
  helpErrorPosition: "bottom",
270
- labelHelpTooltip: /*#__PURE__*/ jsx(Fragment, {}),
278
+ labelHelpTooltip: "Expected format: arn:aws:sqs:region:account-id:queue-name",
271
279
  required: true,
272
280
  content: /*#__PURE__*/ jsx(Input, {
273
281
  id: "queueArn",
@@ -5,7 +5,7 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "rea
5
5
  import { useLocation, useNavigate } from "react-router";
6
6
  import styled_components from "styled-components";
7
7
  import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
8
- import { useSearchObjects, useSearchObjectsVersions } from "../../hooks/index.js";
8
+ import { useListObjectVersions, useListObjects, useSearchObjects, useSearchObjectsVersions } from "../../hooks/index.js";
9
9
  import { useGetPresignedDownload } from "../../hooks/presignedOperations.js";
10
10
  import { useBatchObjectLegalHold } from "../../hooks/useBatchObjectLegalHold.js";
11
11
  import { useFeatures } from "../../hooks/useFeatures.js";
@@ -370,28 +370,8 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
370
370
  getPresignedDownload,
371
371
  showToast
372
372
  ]);
373
- const searchParams = useMemo(()=>{
374
- const baseParams = {
375
- Bucket: bucketName,
376
- Prefix: prefix,
377
- MaxKeys: DEFAULT_PAGE_SIZE,
378
- Delimiter: '/'
379
- };
380
- if (isMetadataSearchEnabled && metadataSearchQuery) return {
381
- ...baseParams,
382
- Search: metadataSearchQuery
383
- };
384
- return baseParams;
385
- }, [
386
- bucketName,
387
- prefix,
388
- isMetadataSearchEnabled,
389
- metadataSearchQuery
390
- ]);
391
- const listObjectsQuery = useSearchObjects(searchParams, {
392
- enabled: Boolean(bucketName)
393
- });
394
- const versionSearchParams = useMemo(()=>({
373
+ const isSearchActive = isMetadataSearchEnabled && Boolean(metadataSearchQuery);
374
+ const baseParams = useMemo(()=>({
395
375
  Bucket: bucketName,
396
376
  Prefix: prefix,
397
377
  MaxKeys: DEFAULT_PAGE_SIZE,
@@ -400,9 +380,26 @@ const ObjectList = ({ bucketName, prefix, onObjectSelect, onPrefixChange, onSele
400
380
  bucketName,
401
381
  prefix
402
382
  ]);
403
- const listVersionsQuery = useSearchObjectsVersions(versionSearchParams, {
404
- enabled: showVersions && Boolean(bucketName)
383
+ const regularListQuery = useListObjects(baseParams, {
384
+ enabled: Boolean(bucketName) && !isSearchActive
385
+ });
386
+ const searchListQuery = useSearchObjects({
387
+ ...baseParams,
388
+ Query: metadataSearchQuery || ''
389
+ }, {
390
+ enabled: Boolean(bucketName) && isSearchActive
391
+ });
392
+ const listObjectsQuery = isSearchActive ? searchListQuery : regularListQuery;
393
+ const regularVersionsQuery = useListObjectVersions(baseParams, {
394
+ enabled: showVersions && Boolean(bucketName) && !isSearchActive
395
+ });
396
+ const searchVersionsQuery = useSearchObjectsVersions({
397
+ ...baseParams,
398
+ Query: metadataSearchQuery || ''
399
+ }, {
400
+ enabled: showVersions && Boolean(bucketName) && isSearchActive
405
401
  });
402
+ const listVersionsQuery = isSearchActive ? searchVersionsQuery : regularVersionsQuery;
406
403
  useEffect(()=>{
407
404
  setSelectedObjects([]);
408
405
  onObjectSelectRef.current(null);
@@ -5,8 +5,8 @@ import { useISVBucketStatus } from "../../../hooks/index.js";
5
5
  import { useDataBrowserNavigate } from "../../../hooks/useDataBrowserNavigate.js";
6
6
  const EditRetentionButton = ({ bucketName })=>{
7
7
  const navigate = useDataBrowserNavigate();
8
- const { isVeeamBucket, isCommvaultBucket, isISVManaged, isLoading } = useISVBucketStatus(bucketName);
9
- const tooltipOverlay = isCommvaultBucket ? 'Edition is disabled as it is managed by Commvault.' : isVeeamBucket ? 'Edition is disabled as it is managed by Veeam.' : void 0;
8
+ const { isISVManaged, isvApplication, isLoading } = useISVBucketStatus(bucketName);
9
+ const tooltipOverlay = isISVManaged ? `Edition is disabled as it is managed by ${isvApplication}.` : void 0;
10
10
  return /*#__PURE__*/ jsx(Button, {
11
11
  id: "edit-retention-btn",
12
12
  variant: "outline",
@@ -9,7 +9,7 @@ const DropZone = styled_components.div`
9
9
  flex: 1;
10
10
  display: flex;
11
11
  flex-direction: column;
12
- height: 300px;
12
+ height: 400px;
13
13
  width: 500px;
14
14
  padding: ${spacing.r20};
15
15
  border-width: ${spacing.r2};
@@ -18,8 +18,10 @@ const DropZone = styled_components.div`
18
18
  border-style: dashed;
19
19
  `;
20
20
  const Files = styled_components.div`
21
- height: 250px;
22
- overflow-y: scroll;
21
+ flex: 1;
22
+ min-height: 0;
23
+ overflow-y: auto;
24
+ overflow-x: hidden;
23
25
  margin: ${spacing.r8} 0px;
24
26
  `;
25
27
  const EmptyFile = styled_components.div`
@@ -38,6 +40,10 @@ const FileRow = styled_components.div`
38
40
  `;
39
41
  const FileInfo = styled_components.div`
40
42
  flex: 1;
43
+ min-width: 0;
44
+ overflow: hidden;
45
+ text-overflow: ellipsis;
46
+ white-space: nowrap;
41
47
  `;
42
48
  const RemoveButton = styled_components.button`
43
49
  background: none;
@@ -52,7 +58,12 @@ const RemoveButton = styled_components.button`
52
58
  `;
53
59
  const maybePluralize = (count, word)=>1 === count ? `1 ${word}` : `${count} ${word}s`;
54
60
  const getTitle = (fileCount)=>0 === fileCount ? 'Upload Files' : `Upload ${maybePluralize(fileCount, 'file')}`;
55
- const FileList = ({ acceptedFiles, open, removeFile })=>/*#__PURE__*/ jsxs("div", {
61
+ const FileListWrapper = styled_components.div`
62
+ display: flex;
63
+ flex-direction: column;
64
+ height: 100%;
65
+ `;
66
+ const FileList = ({ acceptedFiles, open, removeFile })=>/*#__PURE__*/ jsxs(FileListWrapper, {
56
67
  children: [
57
68
  /*#__PURE__*/ jsx(Button, {
58
69
  icon: /*#__PURE__*/ jsx(Icon, {
@@ -65,18 +76,17 @@ const FileList = ({ acceptedFiles, open, removeFile })=>/*#__PURE__*/ jsxs("div"
65
76
  /*#__PURE__*/ jsx(Files, {
66
77
  children: acceptedFiles.map((file)=>/*#__PURE__*/ jsxs(FileRow, {
67
78
  children: [
68
- /*#__PURE__*/ jsx(FileInfo, {
69
- children: /*#__PURE__*/ jsxs("div", {
70
- children: [
71
- file.name,
72
- /*#__PURE__*/ jsx("br", {}),
73
- /*#__PURE__*/ jsx("small", {
74
- children: /*#__PURE__*/ jsx(PrettyBytes, {
75
- bytes: file.size
76
- })
79
+ /*#__PURE__*/ jsxs(FileInfo, {
80
+ title: file.name,
81
+ children: [
82
+ file.name,
83
+ /*#__PURE__*/ jsx("br", {}),
84
+ /*#__PURE__*/ jsx("small", {
85
+ children: /*#__PURE__*/ jsx(PrettyBytes, {
86
+ bytes: file.size
77
87
  })
78
- ]
79
- })
88
+ })
89
+ ]
80
90
  }),
81
91
  /*#__PURE__*/ jsx(RemoveButton, {
82
92
  onClick: ()=>removeFile(file.name),
@@ -0,0 +1,96 @@
1
+ import { coreUIAvailableThemes } from "@scality/core-ui/dist/style/theme";
2
+ import { resolveBrandingTheme } from "../resolveBrandingTheme.js";
3
+ describe('resolveBrandingTheme', ()=>{
4
+ it('loads base preset and returns complete theme', ()=>{
5
+ const config = {
6
+ base: 'darkRebrand',
7
+ logo: '/logo.png',
8
+ brandPrimary: '#2563EB',
9
+ brandSecondary: '#1E3A5F'
10
+ };
11
+ const result = resolveBrandingTheme(config);
12
+ expect(result).toHaveProperty('statusHealthy');
13
+ expect(result).toHaveProperty('statusWarning');
14
+ expect(result).toHaveProperty('statusCritical');
15
+ expect(result).toHaveProperty('backgroundLevel1');
16
+ expect(result.backgroundLevel1).toBe(coreUIAvailableThemes.darkRebrand.backgroundLevel1);
17
+ });
18
+ it('applies brandPrimary to selectedActive and buttonPrimary tokens', ()=>{
19
+ const config = {
20
+ base: 'darkRebrand',
21
+ logo: '/logo.png',
22
+ brandPrimary: '#2563EB',
23
+ brandSecondary: '#1E3A5F'
24
+ };
25
+ const result = resolveBrandingTheme(config);
26
+ expect(result.selectedActive).toBe('#2563EB');
27
+ expect(result.buttonPrimary).toBe('#2563EB');
28
+ });
29
+ it('derives highlight color from brandPrimary at 20% opacity', ()=>{
30
+ const config = {
31
+ base: 'darkRebrand',
32
+ logo: '/logo.png',
33
+ brandPrimary: '#2563EB',
34
+ brandSecondary: '#1E3A5F'
35
+ };
36
+ const result = resolveBrandingTheme(config);
37
+ expect(result.highlight).toBe('rgba(37, 99, 235, 0.2)');
38
+ });
39
+ it('applies brandSecondary to navbarBackground', ()=>{
40
+ const config = {
41
+ base: 'darkRebrand',
42
+ logo: '/logo.png',
43
+ brandPrimary: '#2563EB',
44
+ brandSecondary: '#1E3A5F'
45
+ };
46
+ const result = resolveBrandingTheme(config);
47
+ expect(result.navbarBackground).toBe('#1E3A5F');
48
+ });
49
+ it('applies optional overrides on top of generated theme', ()=>{
50
+ const config = {
51
+ base: 'darkRebrand',
52
+ logo: '/logo.png',
53
+ brandPrimary: '#2563EB',
54
+ brandSecondary: '#1E3A5F',
55
+ overrides: {
56
+ textLink: '#71AEFF',
57
+ border: '#333333'
58
+ }
59
+ };
60
+ const result = resolveBrandingTheme(config);
61
+ expect(result.buttonPrimary).toBe('#2563EB');
62
+ expect(result.navbarBackground).toBe('#1E3A5F');
63
+ expect(result.textLink).toBe('#71AEFF');
64
+ expect(result.border).toBe('#333333');
65
+ });
66
+ it('handles 3-character hex shorthand colors', ()=>{
67
+ const config = {
68
+ base: 'darkRebrand',
69
+ logo: '/logo.png',
70
+ brandPrimary: '#FFF',
71
+ brandSecondary: '#000'
72
+ };
73
+ const result = resolveBrandingTheme(config);
74
+ expect(result.buttonPrimary).toBe('#FFF');
75
+ expect(result.highlight).toBe('rgba(255, 255, 255, 0.2)');
76
+ expect(result.navbarBackground).toBe('#000');
77
+ });
78
+ it('throws error when base theme name does not exist', ()=>{
79
+ const config = {
80
+ base: 'foobar',
81
+ logo: '/logo.png',
82
+ brandPrimary: '#2563EB',
83
+ brandSecondary: '#1E3A5F'
84
+ };
85
+ expect(()=>resolveBrandingTheme(config)).toThrow('Unknown base theme "foobar"');
86
+ });
87
+ it('throws error on invalid hex color format', ()=>{
88
+ const config = {
89
+ base: 'darkRebrand',
90
+ logo: '/logo.png',
91
+ brandPrimary: 'not-a-color',
92
+ brandSecondary: '#1E3A5F'
93
+ };
94
+ expect(()=>resolveBrandingTheme(config)).toThrow('Invalid hex color format');
95
+ });
96
+ });
@@ -0,0 +1,16 @@
1
+ import { type CoreUITheme } from '@scality/core-ui/dist/style/theme';
2
+ import type { BrandingModeConfig } from './types';
3
+ /**
4
+ * Resolves a lightweight branding configuration into a complete CoreUITheme.
5
+ *
6
+ * Takes a base preset and brand colors, then generates a full theme by:
7
+ * - Loading all tokens from the specified base preset
8
+ * - Applying brandPrimary to button and active states
9
+ * - Applying brandSecondary to navbar background
10
+ * - Auto-deriving highlight color from brandPrimary
11
+ * - Applying any optional token overrides
12
+ *
13
+ * @param config - Branding configuration with base preset, colors, and optional overrides
14
+ * @returns Complete CoreUITheme ready to use with styled-components ThemeProvider
15
+ */
16
+ export declare function resolveBrandingTheme(config: BrandingModeConfig): CoreUITheme;
@@ -0,0 +1,23 @@
1
+ import { coreUIAvailableThemes } from "@scality/core-ui/dist/style/theme";
2
+ function hexToRgba(hex, opacity) {
3
+ let cleanHex = hex.replace('#', '');
4
+ if (!/^[0-9A-Fa-f]{3}$|^[0-9A-Fa-f]{6}$/.test(cleanHex)) throw new Error(`Invalid hex color format: "${hex}". Expected format: #RGB or #RRGGBB`);
5
+ if (3 === cleanHex.length) cleanHex = cleanHex.split('').map((char)=>char + char).join('');
6
+ const r = Number.parseInt(cleanHex.substring(0, 2), 16);
7
+ const g = Number.parseInt(cleanHex.substring(2, 4), 16);
8
+ const b = Number.parseInt(cleanHex.substring(4, 6), 16);
9
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
10
+ }
11
+ function resolveBrandingTheme(config) {
12
+ const baseTheme = coreUIAvailableThemes[config.base];
13
+ if (!baseTheme) throw new Error(`Unknown base theme "${config.base}". Available themes: ${Object.keys(coreUIAvailableThemes).join(', ')}`);
14
+ return {
15
+ ...baseTheme,
16
+ selectedActive: config.brandPrimary,
17
+ buttonPrimary: config.brandPrimary,
18
+ highlight: hexToRgba(config.brandPrimary, 0.2),
19
+ navbarBackground: config.brandSecondary,
20
+ ...config.overrides
21
+ };
22
+ }
23
+ export { resolveBrandingTheme };
@@ -2,6 +2,7 @@
2
2
  * Runtime configuration types
3
3
  */
4
4
  import type { Bucket } from '@aws-sdk/client-s3';
5
+ import type { CoreUITheme, CoreUIThemeName } from '@scality/core-ui/dist/style/theme';
5
6
  import type { TableItem } from '../components/objects/ObjectList';
6
7
  /**
7
8
  * Proxy configuration for development or custom proxy setups
@@ -264,4 +265,39 @@ export interface DataBrowserUIProps {
264
265
  }
265
266
  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';
266
267
  export type S3EventCategory = 'Object Creation' | 'Object Deletion' | 'Object Restoration' | 'Lifecycle' | 'Replication' | 'Object Tagging' | 'Storage & Access' | 'Testing';
268
+ /**
269
+ * Branding configuration for a single theme mode (dark or light).
270
+ *
271
+ * Simplifies theme customization by requiring only a base preset and two brand colors.
272
+ * The branding engine auto-generates a complete theme by:
273
+ * - Loading all tokens from the base preset
274
+ * - Applying brandPrimary to buttons and active states
275
+ * - Applying brandSecondary to navbar background
276
+ * - Auto-deriving hover states and highlights
277
+ * - Allowing optional token overrides for fine-tuning
278
+ */
279
+ export interface BrandingModeConfig {
280
+ /** Base theme preset that provides default values for all tokens */
281
+ base: CoreUIThemeName;
282
+ /** Path to brand logo displayed in navbar. Supports separate logos for dark/light modes. */
283
+ logo: string;
284
+ /** Main brand color applied to buttons, active states, and auto-generates hover color */
285
+ brandPrimary: string;
286
+ /** Brand color applied to navbar background for immediate brand recognition */
287
+ brandSecondary: string;
288
+ /** Optional overrides for individual design tokens when fine-tuning is needed */
289
+ overrides?: Partial<CoreUITheme>;
290
+ }
291
+ /**
292
+ * Complete branding configuration supporting dark and/or light modes.
293
+ *
294
+ * At least one mode must be provided (dark, light, or both).
295
+ */
296
+ export type BrandingConfig = {
297
+ dark: BrandingModeConfig;
298
+ light?: BrandingModeConfig;
299
+ } | {
300
+ dark?: BrandingModeConfig;
301
+ light: BrandingModeConfig;
302
+ };
267
303
  export {};
@@ -46,6 +46,7 @@ function getPaginationParams(operationName, pageParam) {
46
46
  ContinuationToken: pageParam
47
47
  };
48
48
  case 'ListObjectVersions':
49
+ case 'SearchObjectsVersions':
49
50
  if (pageParam.includes('|')) {
50
51
  const [keyMarker, versionIdMarker] = pageParam.split('|');
51
52
  return {
@@ -71,6 +72,7 @@ function getNextPageToken(operationName, lastPage) {
71
72
  case 'ListObjects':
72
73
  return lastPage.IsTruncated ? lastPage.NextContinuationToken : void 0;
73
74
  case 'ListObjectVersions':
75
+ case 'SearchObjectsVersions':
74
76
  return lastPage.IsTruncated && (lastPage.NextKeyMarker || lastPage.NextVersionIdMarker) ? `${lastPage.NextKeyMarker || ''}|${lastPage.NextVersionIdMarker || ''}` : void 0;
75
77
  default:
76
78
  return lastPage.IsTruncated ? lastPage.NextContinuationToken : void 0;
@@ -6,7 +6,7 @@ export { useCopyObject, useCreateFolder, useDeleteObject, useDeleteObjects, useD
6
6
  export { useGetPresignedDownload, useGetPresignedPost, useGetPresignedUpload, } from './presignedOperations';
7
7
  export { getAccessibleBucketsStorageKey, getLimitedAccessFlagKey, setLimitedAccessFlag, useAccessibleBuckets, } from './useAccessibleBuckets';
8
8
  export { useBatchObjectLegalHold } from './useBatchObjectLegalHold';
9
- export { type BucketConfigEditorConfig, type BucketConfigEditorResult, useBucketConfigEditor, } from './useBucketConfigEditor';
9
+ export { type BucketConfigEditorConfig, type BucketConfigEditorResult, useBucketConfigEditor } from './useBucketConfigEditor';
10
10
  export { useBucketLocations } from './useBucketLocations';
11
11
  export { useDeleteBucketConfigRule } from './useDeleteBucketConfigRule';
12
12
  export { useEmptyBucket } from './useEmptyBucket';
@@ -6,7 +6,7 @@
6
6
  * configurations like retention and legal hold.
7
7
  */
8
8
  import { type CopyObjectCommandInput, type CopyObjectCommandOutput, type DeleteObjectCommandInput, type DeleteObjectCommandOutput, type DeleteObjectsCommandInput, type DeleteObjectsCommandOutput, type DeleteObjectTaggingCommandInput, type DeleteObjectTaggingCommandOutput, type GetObjectAclCommandInput, type GetObjectAclCommandOutput, type GetObjectAttributesCommandInput, type GetObjectAttributesCommandOutput, type GetObjectCommandInput, type GetObjectCommandOutput, type GetObjectLegalHoldCommandInput, type GetObjectLegalHoldCommandOutput, type GetObjectRetentionCommandInput, type GetObjectRetentionCommandOutput, type GetObjectTaggingCommandInput, type GetObjectTaggingCommandOutput, type GetObjectTorrentCommandInput, type GetObjectTorrentCommandOutput, type HeadObjectCommandInput, type HeadObjectCommandOutput, type ListMultipartUploadsCommandInput, type ListMultipartUploadsCommandOutput, type ListObjectsV2CommandInput, type ListObjectVersionsCommandInput, type PutObjectAclCommandInput, type PutObjectAclCommandOutput, type PutObjectCommandInput, type PutObjectCommandOutput, type PutObjectLegalHoldCommandInput, type PutObjectLegalHoldCommandOutput, type PutObjectRetentionCommandInput, type PutObjectRetentionCommandOutput, type PutObjectTaggingCommandInput, type PutObjectTaggingCommandOutput, type RestoreObjectCommandInput, type RestoreObjectCommandOutput, type SelectObjectContentCommandInput, type SelectObjectContentCommandOutput } from '@aws-sdk/client-s3';
9
- import { type SearchObjectsV2CommandInput, type SearchObjectsVersionCommandInput } from '@scality/zenkoclient';
9
+ import { type ListObjectsV2ExtendedInput, type ListObjectVersionsExtendedInput } from '@scality/cloudserverclient';
10
10
  /**
11
11
  * Hook for listing S3 objects with infinite scroll support
12
12
  *
@@ -27,14 +27,14 @@ export declare const useListObjectVersions: (params: ListObjectVersionsCommandIn
27
27
  * Provides paginated search of objects in an S3 bucket using SQL-like query syntax.
28
28
  * Supports advanced filtering and pattern matching on object keys and metadata.
29
29
  */
30
- export declare const useSearchObjects: (params: SearchObjectsV2CommandInput, options?: Partial<import("@tanstack/react-query").UseInfiniteQueryOptions<any, import("..").EnhancedS3Error, any, (string | SearchObjectsV2CommandInput)[], string | undefined>> | undefined) => import("@tanstack/react-query").UseInfiniteQueryResult<any, import("..").EnhancedS3Error>;
30
+ export declare const useSearchObjects: (params: ListObjectsV2ExtendedInput, options?: Partial<import("@tanstack/react-query").UseInfiniteQueryOptions<any, import("..").EnhancedS3Error, any, (string | ListObjectsV2ExtendedInput)[], string | undefined>> | undefined) => import("@tanstack/react-query").UseInfiniteQueryResult<any, import("..").EnhancedS3Error>;
31
31
  /**
32
32
  * Hook for searching S3 object versions with infinite scroll support
33
33
  *
34
34
  * Provides paginated search of object versions in an S3 bucket using SQL-like query syntax.
35
35
  * Supports advanced filtering and pattern matching on object keys and metadata across all versions.
36
36
  */
37
- export declare const useSearchObjectsVersions: (params: SearchObjectsVersionCommandInput, options?: Partial<import("@tanstack/react-query").UseInfiniteQueryOptions<any, import("..").EnhancedS3Error, any, (string | SearchObjectsVersionCommandInput)[], string | undefined>> | undefined) => import("@tanstack/react-query").UseInfiniteQueryResult<any, import("..").EnhancedS3Error>;
37
+ export declare const useSearchObjectsVersions: (params: ListObjectVersionsExtendedInput, options?: Partial<import("@tanstack/react-query").UseInfiniteQueryOptions<any, import("..").EnhancedS3Error, any, (string | ListObjectVersionsExtendedInput)[], string | undefined>> | undefined) => import("@tanstack/react-query").UseInfiniteQueryResult<any, import("..").EnhancedS3Error>;
38
38
  /**
39
39
  * Hook for retrieving S3 object metadata
40
40
  *
@@ -1,12 +1,12 @@
1
1
  import { CopyObjectCommand, DeleteObjectCommand, DeleteObjectTaggingCommand, DeleteObjectsCommand, GetObjectAclCommand, GetObjectAttributesCommand, GetObjectCommand, GetObjectLegalHoldCommand, GetObjectRetentionCommand, GetObjectTaggingCommand, GetObjectTorrentCommand, HeadObjectCommand, ListMultipartUploadsCommand, ListObjectVersionsCommand, ListObjectsV2Command, PutObjectAclCommand, PutObjectCommand, PutObjectLegalHoldCommand, PutObjectRetentionCommand, PutObjectTaggingCommand, RestoreObjectCommand, SelectObjectContentCommand } from "@aws-sdk/client-s3";
2
- import { SearchObjectsV2Command, SearchObjectsVersionCommand } from "@scality/zenkoclient";
2
+ import { ListObjectVersionsExtendedCommand, ListObjectsV2ExtendedCommand } from "@scality/cloudserverclient";
3
3
  import { useCreateS3InfiniteQueryHook } from "./factories/useCreateS3InfiniteQueryHook.js";
4
4
  import { useCreateS3MutationHook } from "./factories/useCreateS3MutationHook.js";
5
5
  import { useCreateS3QueryHook } from "./factories/useCreateS3QueryHook.js";
6
6
  const useListObjects = useCreateS3InfiniteQueryHook(ListObjectsV2Command, 'ListObjects');
7
7
  const useListObjectVersions = useCreateS3InfiniteQueryHook(ListObjectVersionsCommand, 'ListObjectVersions');
8
- const useSearchObjects = useCreateS3InfiniteQueryHook(SearchObjectsV2Command, 'SearchObjects');
9
- const useSearchObjectsVersions = useCreateS3InfiniteQueryHook(SearchObjectsVersionCommand, 'SearchObjectsVersions');
8
+ const useSearchObjects = useCreateS3InfiniteQueryHook(ListObjectsV2ExtendedCommand, 'SearchObjects');
9
+ const useSearchObjectsVersions = useCreateS3InfiniteQueryHook(ListObjectVersionsExtendedCommand, 'SearchObjectsVersions');
10
10
  const useObjectMetadata = useCreateS3QueryHook(HeadObjectCommand, 'HeadObject');
11
11
  const useGetObject = useCreateS3QueryHook(GetObjectCommand, 'GetObject');
12
12
  const usePutObject = useCreateS3MutationHook(PutObjectCommand, 'PutObject', [