@scality/data-browser-library 1.0.6 → 1.0.8
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/dist/components/__tests__/BucketLifecycleFormPage.test.js +72 -0
- package/dist/components/__tests__/BucketPolicyPage.test.js +5 -0
- package/dist/components/__tests__/BucketReplicationFormPage.test.js +76 -0
- package/dist/components/buckets/BucketLifecycleFormPage.js +50 -3
- package/dist/components/buckets/BucketReplicationFormPage.js +50 -4
- package/dist/components/buckets/__tests__/BucketVersioning.test.js +2 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +3 -3
- package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.js +8 -0
- package/dist/components/objects/UploadButton.js +25 -15
- package/dist/config/__tests__/resolveBrandingTheme.test.d.ts +1 -0
- package/dist/config/__tests__/resolveBrandingTheme.test.js +96 -0
- package/dist/config/resolveBrandingTheme.d.ts +16 -0
- package/dist/config/resolveBrandingTheme.js +23 -0
- package/dist/config/types.d.ts +36 -0
- package/dist/hooks/__tests__/useISVBucketDetection.test.js +22 -1
- package/dist/hooks/useISVBucketDetection.d.ts +1 -0
- package/dist/hooks/useISVBucketDetection.js +5 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/test/testUtils.d.ts +1 -0
- package/dist/test/testUtils.js +1 -0
- package/dist/utils/constants.d.ts +2 -0
- package/dist/utils/constants.js +2 -1
- package/package.json +2 -2
|
@@ -3,14 +3,19 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import user_event from "@testing-library/user-event";
|
|
4
4
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
5
5
|
import { useGetBucketLifecycle, useSetBucketLifecycle } from "../../hooks/bucketConfiguration.js";
|
|
6
|
+
import { useISVBucketStatus } from "../../hooks/useISVBucketDetection.js";
|
|
6
7
|
import { createTestWrapper, findToggleByLabel, mockErrorSubmit, mockOffsetSize, mockSuccessSubmit, submitForm } from "../../test/testUtils.js";
|
|
7
8
|
import { BucketLifecycleFormPage } from "../buckets/BucketLifecycleFormPage.js";
|
|
8
9
|
jest.mock('../../hooks/bucketConfiguration', ()=>({
|
|
9
10
|
useGetBucketLifecycle: jest.fn(),
|
|
10
11
|
useSetBucketLifecycle: jest.fn()
|
|
11
12
|
}));
|
|
13
|
+
jest.mock('../../hooks/useISVBucketDetection', ()=>({
|
|
14
|
+
useISVBucketStatus: jest.fn()
|
|
15
|
+
}));
|
|
12
16
|
const mockUseGetBucketLifecycle = jest.mocked(useGetBucketLifecycle);
|
|
13
17
|
const mockUseSetBucketLifecycle = jest.mocked(useSetBucketLifecycle);
|
|
18
|
+
const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
|
|
14
19
|
const mockNavigate = jest.fn();
|
|
15
20
|
const mockShowToast = jest.fn();
|
|
16
21
|
jest.mock('react-router', ()=>({
|
|
@@ -69,6 +74,15 @@ describe('BucketLifecycleFormPage', ()=>{
|
|
|
69
74
|
mutate: mockMutate,
|
|
70
75
|
isPending: false
|
|
71
76
|
});
|
|
77
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
78
|
+
isVeeamBucket: false,
|
|
79
|
+
isCommvaultBucket: false,
|
|
80
|
+
isKastenBucket: false,
|
|
81
|
+
isISVManaged: false,
|
|
82
|
+
isvApplication: void 0,
|
|
83
|
+
isLoading: false,
|
|
84
|
+
bucketTagsStatus: 'success'
|
|
85
|
+
});
|
|
72
86
|
});
|
|
73
87
|
describe('Page Rendering', ()=>{
|
|
74
88
|
it('renders create mode with correct title', ()=>{
|
|
@@ -615,4 +629,62 @@ describe('BucketLifecycleFormPage', ()=>{
|
|
|
615
629
|
});
|
|
616
630
|
});
|
|
617
631
|
});
|
|
632
|
+
describe('ISV Bucket Warning', ()=>{
|
|
633
|
+
const setupISVMock = (isvApplication = 'Veeam')=>{
|
|
634
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
635
|
+
isVeeamBucket: 'Veeam' === isvApplication,
|
|
636
|
+
isCommvaultBucket: 'Commvault' === isvApplication,
|
|
637
|
+
isKastenBucket: 'Kasten' === isvApplication,
|
|
638
|
+
isISVManaged: true,
|
|
639
|
+
isvApplication,
|
|
640
|
+
isLoading: false,
|
|
641
|
+
bucketTagsStatus: 'success'
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
it('renders ISV warning banner and checkbox when bucket is ISV-managed', ()=>{
|
|
645
|
+
setupISVMock('Veeam');
|
|
646
|
+
renderBucketLifecycleFormPage();
|
|
647
|
+
expect(screen.getByText(/bucket used for external integration with Veeam/i)).toBeInTheDocument();
|
|
648
|
+
expect(screen.getByText(/may conflict with Veeam's backup strategy/i)).toBeInTheDocument();
|
|
649
|
+
expect(screen.getByLabelText(/i understand what i'm doing/i)).toBeInTheDocument();
|
|
650
|
+
});
|
|
651
|
+
it('does not render ISV warning for non-ISV buckets', ()=>{
|
|
652
|
+
renderBucketLifecycleFormPage();
|
|
653
|
+
expect(screen.queryByText(/bucket used for external integration with/i)).not.toBeInTheDocument();
|
|
654
|
+
expect(screen.queryByLabelText(/i understand what i'm doing/i)).not.toBeInTheDocument();
|
|
655
|
+
});
|
|
656
|
+
it('blocks form submission when ISV checkbox is unchecked', async ()=>{
|
|
657
|
+
setupISVMock('Veeam');
|
|
658
|
+
renderBucketLifecycleFormPage();
|
|
659
|
+
await fillRequiredFields('isv-rule');
|
|
660
|
+
await waitFor(()=>{
|
|
661
|
+
const createButton = screen.getByRole('button', {
|
|
662
|
+
name: /create/i
|
|
663
|
+
});
|
|
664
|
+
expect(createButton).toBeDisabled();
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
it('allows form submission when ISV checkbox is checked', async ()=>{
|
|
668
|
+
setupISVMock('Veeam');
|
|
669
|
+
renderBucketLifecycleFormPage();
|
|
670
|
+
await fillRequiredFields('isv-rule');
|
|
671
|
+
const checkbox = screen.getByLabelText(/i understand what i'm doing/i);
|
|
672
|
+
await user_event.click(checkbox);
|
|
673
|
+
mockSuccessSubmit(mockMutate);
|
|
674
|
+
await submitForm('create');
|
|
675
|
+
await waitFor(()=>{
|
|
676
|
+
expect(mockMutate).toHaveBeenCalled();
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
it('displays correct application name for Commvault ISV bucket', ()=>{
|
|
680
|
+
setupISVMock('Commvault');
|
|
681
|
+
renderBucketLifecycleFormPage();
|
|
682
|
+
expect(screen.getByText(/bucket used for external integration with Commvault/i)).toBeInTheDocument();
|
|
683
|
+
});
|
|
684
|
+
it('displays correct application name for Kasten ISV bucket', ()=>{
|
|
685
|
+
setupISVMock('Kasten');
|
|
686
|
+
renderBucketLifecycleFormPage();
|
|
687
|
+
expect(screen.getByText(/bucket used for external integration with Kasten/i)).toBeInTheDocument();
|
|
688
|
+
});
|
|
689
|
+
});
|
|
618
690
|
});
|
|
@@ -73,6 +73,7 @@ describe('BucketPolicyPage', ()=>{
|
|
|
73
73
|
mockUseISVBucketStatus.mockReturnValue({
|
|
74
74
|
isVeeamBucket: false,
|
|
75
75
|
isCommvaultBucket: false,
|
|
76
|
+
isKastenBucket: false,
|
|
76
77
|
isISVManaged: false,
|
|
77
78
|
isvApplication: void 0,
|
|
78
79
|
isLoading: false,
|
|
@@ -204,6 +205,7 @@ describe('BucketPolicyPage', ()=>{
|
|
|
204
205
|
mockUseISVBucketStatus.mockReturnValue({
|
|
205
206
|
isVeeamBucket: false,
|
|
206
207
|
isCommvaultBucket: false,
|
|
208
|
+
isKastenBucket: false,
|
|
207
209
|
isISVManaged: false,
|
|
208
210
|
isvApplication: void 0,
|
|
209
211
|
isLoading: true,
|
|
@@ -217,6 +219,7 @@ describe('BucketPolicyPage', ()=>{
|
|
|
217
219
|
mockUseISVBucketStatus.mockReturnValue({
|
|
218
220
|
isVeeamBucket: true,
|
|
219
221
|
isCommvaultBucket: false,
|
|
222
|
+
isKastenBucket: false,
|
|
220
223
|
isISVManaged: true,
|
|
221
224
|
isvApplication: 'Veeam',
|
|
222
225
|
isLoading: false,
|
|
@@ -234,6 +237,7 @@ describe('BucketPolicyPage', ()=>{
|
|
|
234
237
|
mockUseISVBucketStatus.mockReturnValue({
|
|
235
238
|
isVeeamBucket: false,
|
|
236
239
|
isCommvaultBucket: true,
|
|
240
|
+
isKastenBucket: false,
|
|
237
241
|
isISVManaged: true,
|
|
238
242
|
isvApplication: 'Commvault',
|
|
239
243
|
isLoading: false,
|
|
@@ -251,6 +255,7 @@ describe('BucketPolicyPage', ()=>{
|
|
|
251
255
|
mockUseISVBucketStatus.mockReturnValue({
|
|
252
256
|
isVeeamBucket: false,
|
|
253
257
|
isCommvaultBucket: false,
|
|
258
|
+
isKastenBucket: false,
|
|
254
259
|
isISVManaged: false,
|
|
255
260
|
isvApplication: void 0,
|
|
256
261
|
isLoading: false,
|
|
@@ -4,6 +4,7 @@ import user_event from "@testing-library/user-event";
|
|
|
4
4
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
5
5
|
import { useGetBucketReplication, useSetBucketReplication } from "../../hooks/bucketConfiguration.js";
|
|
6
6
|
import { useBuckets } from "../../hooks/bucketOperations.js";
|
|
7
|
+
import { useISVBucketStatus } from "../../hooks/useISVBucketDetection.js";
|
|
7
8
|
import { createTestWrapper, findToggleByLabel, mockErrorSubmit, mockOffsetSize, mockSuccessSubmit, submitForm } from "../../test/testUtils.js";
|
|
8
9
|
import { BucketReplicationFormPage } from "../buckets/BucketReplicationFormPage.js";
|
|
9
10
|
jest.mock('../../hooks/bucketConfiguration', ()=>({
|
|
@@ -13,9 +14,13 @@ jest.mock('../../hooks/bucketConfiguration', ()=>({
|
|
|
13
14
|
jest.mock('../../hooks/bucketOperations', ()=>({
|
|
14
15
|
useBuckets: jest.fn()
|
|
15
16
|
}));
|
|
17
|
+
jest.mock('../../hooks/useISVBucketDetection', ()=>({
|
|
18
|
+
useISVBucketStatus: jest.fn()
|
|
19
|
+
}));
|
|
16
20
|
const mockUseGetBucketReplication = jest.mocked(useGetBucketReplication);
|
|
17
21
|
const mockUseSetBucketReplication = jest.mocked(useSetBucketReplication);
|
|
18
22
|
const mockUseBuckets = jest.mocked(useBuckets);
|
|
23
|
+
const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
|
|
19
24
|
const mockNavigate = jest.fn();
|
|
20
25
|
const mockShowToast = jest.fn();
|
|
21
26
|
jest.mock('react-router', ()=>({
|
|
@@ -96,6 +101,15 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
96
101
|
},
|
|
97
102
|
status: 'success'
|
|
98
103
|
});
|
|
104
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
105
|
+
isVeeamBucket: false,
|
|
106
|
+
isCommvaultBucket: false,
|
|
107
|
+
isKastenBucket: false,
|
|
108
|
+
isISVManaged: false,
|
|
109
|
+
isvApplication: void 0,
|
|
110
|
+
isLoading: false,
|
|
111
|
+
bucketTagsStatus: 'success'
|
|
112
|
+
});
|
|
99
113
|
});
|
|
100
114
|
describe('Page Rendering', ()=>{
|
|
101
115
|
it('renders create mode with correct title', ()=>{
|
|
@@ -1876,4 +1890,66 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
1876
1890
|
});
|
|
1877
1891
|
});
|
|
1878
1892
|
});
|
|
1893
|
+
describe('ISV Bucket Warning', ()=>{
|
|
1894
|
+
const setupISVMock = (isvApplication = 'Veeam')=>{
|
|
1895
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
1896
|
+
isVeeamBucket: 'Veeam' === isvApplication,
|
|
1897
|
+
isCommvaultBucket: 'Commvault' === isvApplication,
|
|
1898
|
+
isKastenBucket: 'Kasten' === isvApplication,
|
|
1899
|
+
isISVManaged: true,
|
|
1900
|
+
isvApplication,
|
|
1901
|
+
isLoading: false,
|
|
1902
|
+
bucketTagsStatus: 'success'
|
|
1903
|
+
});
|
|
1904
|
+
};
|
|
1905
|
+
it('renders ISV warning banner and checkbox when bucket is ISV-managed', ()=>{
|
|
1906
|
+
setupISVMock('Veeam');
|
|
1907
|
+
renderBucketReplicationFormPage();
|
|
1908
|
+
expect(screen.getByText(/bucket used for external integration with Veeam/i)).toBeInTheDocument();
|
|
1909
|
+
expect(screen.getByText(/rendering the replicated data unusable for recovery/i)).toBeInTheDocument();
|
|
1910
|
+
expect(screen.getByLabelText(/i understand what i'm doing/i)).toBeInTheDocument();
|
|
1911
|
+
});
|
|
1912
|
+
it('does not render ISV warning for non-ISV buckets', ()=>{
|
|
1913
|
+
renderBucketReplicationFormPage();
|
|
1914
|
+
expect(screen.queryByText(/bucket used for external integration with/i)).not.toBeInTheDocument();
|
|
1915
|
+
expect(screen.queryByLabelText(/i understand what i'm doing/i)).not.toBeInTheDocument();
|
|
1916
|
+
});
|
|
1917
|
+
it('blocks form submission when ISV checkbox is unchecked', async ()=>{
|
|
1918
|
+
setupISVMock('Veeam');
|
|
1919
|
+
renderBucketReplicationFormPage();
|
|
1920
|
+
await fillRequiredFields({
|
|
1921
|
+
ruleId: 'isv-rule'
|
|
1922
|
+
});
|
|
1923
|
+
await waitFor(()=>{
|
|
1924
|
+
const createButton = screen.getByRole('button', {
|
|
1925
|
+
name: /create/i
|
|
1926
|
+
});
|
|
1927
|
+
expect(createButton).toBeDisabled();
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
it('allows form submission when ISV checkbox is checked', async ()=>{
|
|
1931
|
+
setupISVMock('Veeam');
|
|
1932
|
+
renderBucketReplicationFormPage();
|
|
1933
|
+
await fillRequiredFields({
|
|
1934
|
+
ruleId: 'isv-rule'
|
|
1935
|
+
});
|
|
1936
|
+
const checkbox = screen.getByLabelText(/i understand what i'm doing/i);
|
|
1937
|
+
await user_event.click(checkbox);
|
|
1938
|
+
mockSuccessSubmit(mockMutate);
|
|
1939
|
+
await submitForm('create');
|
|
1940
|
+
await waitFor(()=>{
|
|
1941
|
+
expect(mockMutate).toHaveBeenCalled();
|
|
1942
|
+
});
|
|
1943
|
+
});
|
|
1944
|
+
it('displays correct application name for Commvault ISV bucket', ()=>{
|
|
1945
|
+
setupISVMock('Commvault');
|
|
1946
|
+
renderBucketReplicationFormPage();
|
|
1947
|
+
expect(screen.getByText(/bucket used for external integration with Commvault/i)).toBeInTheDocument();
|
|
1948
|
+
});
|
|
1949
|
+
it('displays correct application name for Kasten ISV bucket', ()=>{
|
|
1950
|
+
setupISVMock('Kasten');
|
|
1951
|
+
renderBucketReplicationFormPage();
|
|
1952
|
+
expect(screen.getByText(/bucket used for external integration with Kasten/i)).toBeInTheDocument();
|
|
1953
|
+
});
|
|
1954
|
+
});
|
|
1879
1955
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { joiResolver } from "@hookform/resolvers/joi";
|
|
3
|
-
import { Form, FormGroup, FormSection, Icon, Loader, Stack, Text, Toggle, spacing, useToast } from "@scality/core-ui";
|
|
3
|
+
import { Banner, Checkbox, Form, FormGroup, FormSection, Icon, Loader, Stack, Text, Toggle, spacing, useToast } from "@scality/core-ui";
|
|
4
4
|
import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
|
|
5
5
|
import { Box, Button, Input, Select } from "@scality/core-ui/dist/next";
|
|
6
6
|
import joi from "joi";
|
|
@@ -9,6 +9,7 @@ import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-for
|
|
|
9
9
|
import { useParams } from "react-router";
|
|
10
10
|
import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
|
|
11
11
|
import { useGetBucketLifecycle, useSetBucketLifecycle } from "../../hooks/bucketConfiguration.js";
|
|
12
|
+
import { useISVBucketStatus } from "../../hooks/useISVBucketDetection.js";
|
|
12
13
|
import { useDataBrowserNavigate } from "../../hooks/useDataBrowserNavigate.js";
|
|
13
14
|
import { AWS_RULE_LIMITS, STATUS_OPTIONS, buildS3Filter } from "../../utils/s3RuleUtils.js";
|
|
14
15
|
import { ArrayFieldActions } from "../ui/ArrayFieldActions.js";
|
|
@@ -151,6 +152,9 @@ const createSchema = (hasCustomStorageClassSelector)=>{
|
|
|
151
152
|
'number.min': `Days must be at least ${LIFECYCLE_LIMITS.ABORT_MPU_MIN_DAYS}`
|
|
152
153
|
}),
|
|
153
154
|
otherwise: joi.any()
|
|
155
|
+
}),
|
|
156
|
+
understandISVRisk: joi.boolean().invalid(false).messages({
|
|
157
|
+
'any.invalid': 'You must acknowledge the risk'
|
|
154
158
|
})
|
|
155
159
|
}).custom((value, helpers)=>{
|
|
156
160
|
const hasAtLeastOneAction = value.transitionsEnabled || value.expirationEnabled || value.expiredObjectDeleteMarker || value.noncurrentTransitionsEnabled || value.noncurrentExpirationEnabled || value.abortMpuEnabled;
|
|
@@ -291,6 +295,7 @@ function BucketLifecycleFormPage() {
|
|
|
291
295
|
const navigate = useDataBrowserNavigate();
|
|
292
296
|
const { showToast } = useToast();
|
|
293
297
|
const isEditMode = !!ruleId;
|
|
298
|
+
const { isISVManaged, isvApplication, isLoading: isISVLoading } = useISVBucketStatus(bucketName);
|
|
294
299
|
const { data: lifecycleData, status: lifecycleStatus } = useGetBucketLifecycle({
|
|
295
300
|
Bucket: bucketName
|
|
296
301
|
});
|
|
@@ -338,7 +343,8 @@ function BucketLifecycleFormPage() {
|
|
|
338
343
|
noncurrentExpirationDays: 30,
|
|
339
344
|
noncurrentNewerVersions: 0,
|
|
340
345
|
abortMpuEnabled: false,
|
|
341
|
-
abortMpuDays: 7
|
|
346
|
+
abortMpuDays: 7,
|
|
347
|
+
understandISVRisk: true
|
|
342
348
|
}
|
|
343
349
|
});
|
|
344
350
|
const { handleSubmit, register, control, watch, reset, formState: { isValid, isDirty, errors } } = methods;
|
|
@@ -391,6 +397,14 @@ function BucketLifecycleFormPage() {
|
|
|
391
397
|
existingRule,
|
|
392
398
|
reset
|
|
393
399
|
]);
|
|
400
|
+
useEffect(()=>{
|
|
401
|
+
methods.setValue('understandISVRisk', !isISVManaged, {
|
|
402
|
+
shouldValidate: true
|
|
403
|
+
});
|
|
404
|
+
}, [
|
|
405
|
+
isISVManaged,
|
|
406
|
+
methods
|
|
407
|
+
]);
|
|
394
408
|
const prevTransitionsEnabledRef = useRef(null);
|
|
395
409
|
const prevNoncurrentTransitionsEnabledRef = useRef(null);
|
|
396
410
|
useEffect(()=>{
|
|
@@ -532,7 +546,7 @@ function BucketLifecycleFormPage() {
|
|
|
532
546
|
isEditMode,
|
|
533
547
|
existingRule
|
|
534
548
|
]);
|
|
535
|
-
if ('pending' === lifecycleStatus) return /*#__PURE__*/ jsx(Loader, {
|
|
549
|
+
if ('pending' === lifecycleStatus || isISVLoading) return /*#__PURE__*/ jsx(Loader, {
|
|
536
550
|
centered: true,
|
|
537
551
|
children: /*#__PURE__*/ jsx(Text, {
|
|
538
552
|
children: "Loading..."
|
|
@@ -578,6 +592,39 @@ function BucketLifecycleFormPage() {
|
|
|
578
592
|
]
|
|
579
593
|
}),
|
|
580
594
|
children: [
|
|
595
|
+
isISVManaged && /*#__PURE__*/ jsxs(Stack, {
|
|
596
|
+
direction: "vertical",
|
|
597
|
+
gap: "r16",
|
|
598
|
+
children: [
|
|
599
|
+
/*#__PURE__*/ jsxs(Banner, {
|
|
600
|
+
variant: "warning",
|
|
601
|
+
title: `Bucket used for external integration with
|
|
602
|
+
${isvApplication}`,
|
|
603
|
+
icon: /*#__PURE__*/ jsx(Icon, {
|
|
604
|
+
color: "statusWarning",
|
|
605
|
+
name: "Exclamation-circle"
|
|
606
|
+
}),
|
|
607
|
+
children: [
|
|
608
|
+
"This bucket was provisioned specifically for ",
|
|
609
|
+
isvApplication,
|
|
610
|
+
", which manages its own data retention.",
|
|
611
|
+
/*#__PURE__*/ jsx("br", {}),
|
|
612
|
+
"Configuring manual lifecycle rules here may conflict with ",
|
|
613
|
+
isvApplication,
|
|
614
|
+
"'s backup strategy, potentially leading to data corruption or unintended deletion."
|
|
615
|
+
]
|
|
616
|
+
}),
|
|
617
|
+
/*#__PURE__*/ jsx(Controller, {
|
|
618
|
+
control: control,
|
|
619
|
+
name: "understandISVRisk",
|
|
620
|
+
render: ({ field: { onChange, value } })=>/*#__PURE__*/ jsx(Checkbox, {
|
|
621
|
+
label: "I understand what I'm doing",
|
|
622
|
+
checked: !!value,
|
|
623
|
+
onChange: (e)=>onChange(e.target.checked)
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
]
|
|
627
|
+
}),
|
|
581
628
|
/*#__PURE__*/ jsxs(FormSection, {
|
|
582
629
|
title: {
|
|
583
630
|
name: 'Rule Scope'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { joiResolver } from "@hookform/resolvers/joi";
|
|
3
|
-
import { Form, FormGroup, FormSection, Icon, Loader, Stack, Text, Toggle, spacing, useToast } from "@scality/core-ui";
|
|
3
|
+
import { Banner, Checkbox, Form, FormGroup, FormSection, Icon, Loader, Stack, Text, Toggle, spacing, useToast } from "@scality/core-ui";
|
|
4
4
|
import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
|
|
5
5
|
import { Box, Button, Input, Select } from "@scality/core-ui/dist/next";
|
|
6
6
|
import joi from "joi";
|
|
@@ -10,6 +10,7 @@ import { useParams } from "react-router";
|
|
|
10
10
|
import { useDataBrowserUICustomization } from "../../contexts/DataBrowserUICustomizationContext.js";
|
|
11
11
|
import { useGetBucketReplication, useSetBucketReplication } from "../../hooks/bucketConfiguration.js";
|
|
12
12
|
import { useBuckets } from "../../hooks/bucketOperations.js";
|
|
13
|
+
import { useISVBucketStatus } from "../../hooks/useISVBucketDetection.js";
|
|
13
14
|
import { useDataBrowserNavigate } from "../../hooks/useDataBrowserNavigate.js";
|
|
14
15
|
import { AWS_RULE_LIMITS, STATUS_OPTIONS, buildS3Filter } from "../../utils/s3RuleUtils.js";
|
|
15
16
|
import { FilterFormSection, createFilterValidationSchema } from "../ui/FilterFormSection.js";
|
|
@@ -94,7 +95,10 @@ const createSchema = (hasExistingRules)=>joi.object({
|
|
|
94
95
|
otherwise: joi.boolean()
|
|
95
96
|
}),
|
|
96
97
|
deleteMarkerReplication: joi.boolean(),
|
|
97
|
-
switchObjectOwnership: joi.boolean()
|
|
98
|
+
switchObjectOwnership: joi.boolean(),
|
|
99
|
+
understandISVRisk: joi.boolean().invalid(false).messages({
|
|
100
|
+
'any.invalid': 'You must acknowledge the risk'
|
|
101
|
+
})
|
|
98
102
|
});
|
|
99
103
|
const ruleToFormValues = (rule, role)=>{
|
|
100
104
|
const formValues = {
|
|
@@ -252,6 +256,7 @@ function BucketReplicationFormPage() {
|
|
|
252
256
|
const navigate = useDataBrowserNavigate();
|
|
253
257
|
const { showToast } = useToast();
|
|
254
258
|
const isEditMode = !!ruleId;
|
|
259
|
+
const { isISVManaged, isvApplication, isLoading: isISVLoading } = useISVBucketStatus(bucketName);
|
|
255
260
|
const { data: replicationData, status: replicationStatus } = useGetBucketReplication({
|
|
256
261
|
Bucket: bucketName
|
|
257
262
|
});
|
|
@@ -312,7 +317,8 @@ function BucketReplicationFormPage() {
|
|
|
312
317
|
enforceRTC: false,
|
|
313
318
|
enableRTCNotification: false,
|
|
314
319
|
deleteMarkerReplication: false,
|
|
315
|
-
switchObjectOwnership: false
|
|
320
|
+
switchObjectOwnership: false,
|
|
321
|
+
understandISVRisk: true
|
|
316
322
|
}
|
|
317
323
|
});
|
|
318
324
|
const { handleSubmit, register, control, watch, reset, formState: { isValid, isDirty, errors } } = methods;
|
|
@@ -403,6 +409,14 @@ function BucketReplicationFormPage() {
|
|
|
403
409
|
enforceRTC,
|
|
404
410
|
methods
|
|
405
411
|
]);
|
|
412
|
+
useEffect(()=>{
|
|
413
|
+
methods.setValue('understandISVRisk', !isISVManaged, {
|
|
414
|
+
shouldValidate: true
|
|
415
|
+
});
|
|
416
|
+
}, [
|
|
417
|
+
isISVManaged,
|
|
418
|
+
methods
|
|
419
|
+
]);
|
|
406
420
|
const handleCancel = useCallback(()=>{
|
|
407
421
|
navigate(`/buckets/${bucketName}?tab=replication`);
|
|
408
422
|
}, [
|
|
@@ -450,7 +464,7 @@ function BucketReplicationFormPage() {
|
|
|
450
464
|
isEditMode,
|
|
451
465
|
existingRule
|
|
452
466
|
]);
|
|
453
|
-
if ('pending' === replicationStatus) return /*#__PURE__*/ jsx(Loader, {
|
|
467
|
+
if ('pending' === replicationStatus || isISVLoading) return /*#__PURE__*/ jsx(Loader, {
|
|
454
468
|
centered: true,
|
|
455
469
|
children: /*#__PURE__*/ jsx(Text, {
|
|
456
470
|
children: "Loading..."
|
|
@@ -496,6 +510,38 @@ function BucketReplicationFormPage() {
|
|
|
496
510
|
]
|
|
497
511
|
}),
|
|
498
512
|
children: [
|
|
513
|
+
isISVManaged && /*#__PURE__*/ jsxs(Stack, {
|
|
514
|
+
direction: "vertical",
|
|
515
|
+
gap: "r16",
|
|
516
|
+
children: [
|
|
517
|
+
/*#__PURE__*/ jsxs(Banner, {
|
|
518
|
+
variant: "warning",
|
|
519
|
+
title: `Bucket used for external integration with ${isvApplication}`,
|
|
520
|
+
icon: /*#__PURE__*/ jsx(Icon, {
|
|
521
|
+
color: "statusWarning",
|
|
522
|
+
name: "Exclamation-circle"
|
|
523
|
+
}),
|
|
524
|
+
children: [
|
|
525
|
+
"This bucket was provisioned specifically for ",
|
|
526
|
+
isvApplication,
|
|
527
|
+
". Manual replication workflows created here may be redundant or invisible to the application, rendering the replicated data unusable for recovery.",
|
|
528
|
+
/*#__PURE__*/ jsx("br", {}),
|
|
529
|
+
"To ensure data consistency, manage all replication tasks directly within ",
|
|
530
|
+
isvApplication,
|
|
531
|
+
"."
|
|
532
|
+
]
|
|
533
|
+
}),
|
|
534
|
+
/*#__PURE__*/ jsx(Controller, {
|
|
535
|
+
control: control,
|
|
536
|
+
name: "understandISVRisk",
|
|
537
|
+
render: ({ field: { onChange, value } })=>/*#__PURE__*/ jsx(Checkbox, {
|
|
538
|
+
label: "I understand what I'm doing",
|
|
539
|
+
checked: !!value,
|
|
540
|
+
onChange: (e)=>onChange(e.target.checked)
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
]
|
|
544
|
+
}),
|
|
499
545
|
/*#__PURE__*/ jsx(FormSection, {
|
|
500
546
|
title: {
|
|
501
547
|
name: 'Role'
|
|
@@ -54,6 +54,7 @@ const mockHookDefaults = ()=>{
|
|
|
54
54
|
mockUseISVBucketStatus.mockReturnValue({
|
|
55
55
|
isVeeamBucket: false,
|
|
56
56
|
isCommvaultBucket: false,
|
|
57
|
+
isKastenBucket: false,
|
|
57
58
|
isISVManaged: false,
|
|
58
59
|
isvApplication: void 0,
|
|
59
60
|
isLoading: false,
|
|
@@ -141,6 +142,7 @@ describe('BucketVersioning', ()=>{
|
|
|
141
142
|
mockUseISVBucketStatus.mockReturnValue({
|
|
142
143
|
isVeeamBucket: true,
|
|
143
144
|
isCommvaultBucket: false,
|
|
145
|
+
isKastenBucket: false,
|
|
144
146
|
isISVManaged: true,
|
|
145
147
|
isvApplication: 'Veeam',
|
|
146
148
|
isLoading: false,
|
|
@@ -5,7 +5,7 @@ import { cleanup, render, screen } from "@testing-library/react";
|
|
|
5
5
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
6
6
|
import { DataBrowserUICustomizationProvider } from "../../../../contexts/DataBrowserUICustomizationContext.js";
|
|
7
7
|
var __webpack_modules__ = {
|
|
8
|
-
"
|
|
8
|
+
"../index" (module) {
|
|
9
9
|
module.exports = __rspack_external__index_js_95fdb65a;
|
|
10
10
|
}
|
|
11
11
|
};
|
|
@@ -19,7 +19,7 @@ function __webpack_require__(moduleId) {
|
|
|
19
19
|
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
20
20
|
return module.exports;
|
|
21
21
|
}
|
|
22
|
-
var external_index_js_ = __webpack_require__("
|
|
22
|
+
var external_index_js_ = __webpack_require__("../index");
|
|
23
23
|
jest.mock('../ObjectSummary', ()=>({
|
|
24
24
|
ObjectSummary: ()=>/*#__PURE__*/ jsx("div", {
|
|
25
25
|
"data-testid": "object-summary",
|
|
@@ -398,7 +398,7 @@ describe('ObjectDetails', ()=>{
|
|
|
398
398
|
expect(screen.getByTestId('object-summary')).toBeInTheDocument();
|
|
399
399
|
});
|
|
400
400
|
it('should throw error when useObjectDetailsContext is used outside provider', ()=>{
|
|
401
|
-
const { useObjectDetailsContext } = __webpack_require__("
|
|
401
|
+
const { useObjectDetailsContext } = __webpack_require__("../index");
|
|
402
402
|
const TestComponent = ()=>{
|
|
403
403
|
useObjectDetailsContext();
|
|
404
404
|
return null;
|
|
@@ -33,6 +33,7 @@ beforeEach(()=>{
|
|
|
33
33
|
mockUseISVBucketStatus.mockReturnValue({
|
|
34
34
|
isVeeamBucket: false,
|
|
35
35
|
isCommvaultBucket: false,
|
|
36
|
+
isKastenBucket: false,
|
|
36
37
|
isISVManaged: false,
|
|
37
38
|
isvApplication: void 0,
|
|
38
39
|
isLoading: false,
|
|
@@ -73,6 +74,7 @@ it('is disabled for Veeam Backup & Replication buckets', ()=>{
|
|
|
73
74
|
mockUseISVBucketStatus.mockReturnValue({
|
|
74
75
|
isVeeamBucket: true,
|
|
75
76
|
isCommvaultBucket: false,
|
|
77
|
+
isKastenBucket: false,
|
|
76
78
|
isISVManaged: true,
|
|
77
79
|
isvApplication: 'Veeam',
|
|
78
80
|
isLoading: false,
|
|
@@ -88,6 +90,7 @@ it('is disabled for Veeam Office 365 v6/v7 buckets', ()=>{
|
|
|
88
90
|
mockUseISVBucketStatus.mockReturnValue({
|
|
89
91
|
isVeeamBucket: true,
|
|
90
92
|
isCommvaultBucket: false,
|
|
93
|
+
isKastenBucket: false,
|
|
91
94
|
isISVManaged: true,
|
|
92
95
|
isvApplication: 'Veeam',
|
|
93
96
|
isLoading: false,
|
|
@@ -103,6 +106,7 @@ it('is disabled for Veeam Office 365 v8+ buckets', ()=>{
|
|
|
103
106
|
mockUseISVBucketStatus.mockReturnValue({
|
|
104
107
|
isVeeamBucket: true,
|
|
105
108
|
isCommvaultBucket: false,
|
|
109
|
+
isKastenBucket: false,
|
|
106
110
|
isISVManaged: true,
|
|
107
111
|
isvApplication: 'Veeam',
|
|
108
112
|
isLoading: false,
|
|
@@ -118,6 +122,7 @@ it('is disabled for Commvault buckets', ()=>{
|
|
|
118
122
|
mockUseISVBucketStatus.mockReturnValue({
|
|
119
123
|
isVeeamBucket: false,
|
|
120
124
|
isCommvaultBucket: true,
|
|
125
|
+
isKastenBucket: false,
|
|
121
126
|
isISVManaged: true,
|
|
122
127
|
isvApplication: 'Commvault',
|
|
123
128
|
isLoading: false,
|
|
@@ -133,6 +138,7 @@ it('is disabled for ISV buckets tagged as Veeam Backup for Microsoft 365', ()=>{
|
|
|
133
138
|
mockUseISVBucketStatus.mockReturnValue({
|
|
134
139
|
isVeeamBucket: true,
|
|
135
140
|
isCommvaultBucket: false,
|
|
141
|
+
isKastenBucket: false,
|
|
136
142
|
isISVManaged: true,
|
|
137
143
|
isvApplication: 'Veeam',
|
|
138
144
|
isLoading: false,
|
|
@@ -148,6 +154,7 @@ it('is disabled for ISV buckets tagged as Veeam Backup & Replication', ()=>{
|
|
|
148
154
|
mockUseISVBucketStatus.mockReturnValue({
|
|
149
155
|
isVeeamBucket: true,
|
|
150
156
|
isCommvaultBucket: false,
|
|
157
|
+
isKastenBucket: false,
|
|
151
158
|
isISVManaged: true,
|
|
152
159
|
isvApplication: 'Veeam',
|
|
153
160
|
isLoading: false,
|
|
@@ -191,6 +198,7 @@ it('shows loading state when fetching bucket tags', ()=>{
|
|
|
191
198
|
mockUseISVBucketStatus.mockReturnValue({
|
|
192
199
|
isVeeamBucket: false,
|
|
193
200
|
isCommvaultBucket: false,
|
|
201
|
+
isKastenBucket: false,
|
|
194
202
|
isISVManaged: false,
|
|
195
203
|
isvApplication: void 0,
|
|
196
204
|
isLoading: true,
|
|
@@ -9,7 +9,7 @@ const DropZone = styled_components.div`
|
|
|
9
9
|
flex: 1;
|
|
10
10
|
display: flex;
|
|
11
11
|
flex-direction: column;
|
|
12
|
-
height:
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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__*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 };
|
package/dist/config/types.d.ts
CHANGED
|
@@ -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 {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderHook } from "@testing-library/react";
|
|
2
2
|
import { createTestWrapper } from "../../test/testUtils.js";
|
|
3
|
-
import { BUCKET_TAG_APPLICATION, BUCKET_TAG_VEEAM_APPLICATION, COMMVAULT_APPLICATION, VEEAM_BACKUP_REPLICATION, VEEAM_OFFICE_365, VEEAM_OFFICE_365_V8, VEEAM_VBO_APPLICATION } from "../../utils/constants.js";
|
|
3
|
+
import { BUCKET_TAG_APPLICATION, BUCKET_TAG_VEEAM_APPLICATION, COMMVAULT_APPLICATION, KASTEN_APPLICATION, VEEAM_BACKUP_REPLICATION, VEEAM_OFFICE_365, VEEAM_OFFICE_365_V8, VEEAM_VBO_APPLICATION } from "../../utils/constants.js";
|
|
4
4
|
import { useGetBucketTagging } from "../bucketConfiguration.js";
|
|
5
5
|
import { useFeatures } from "../useFeatures.js";
|
|
6
6
|
import { useISVBucketStatus } from "../useISVBucketDetection.js";
|
|
@@ -144,6 +144,27 @@ describe('useISVBucketStatus', ()=>{
|
|
|
144
144
|
expect(result.current.isISVManaged).toBe(true);
|
|
145
145
|
expect(result.current.isvApplication).toBe('Commvault');
|
|
146
146
|
});
|
|
147
|
+
it('should detect Kasten bucket', ()=>{
|
|
148
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
149
|
+
data: {
|
|
150
|
+
TagSet: [
|
|
151
|
+
{
|
|
152
|
+
Key: BUCKET_TAG_APPLICATION,
|
|
153
|
+
Value: KASTEN_APPLICATION
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
status: 'success'
|
|
158
|
+
});
|
|
159
|
+
const { result } = renderHook(()=>useISVBucketStatus('test-bucket'), {
|
|
160
|
+
wrapper: createTestWrapper()
|
|
161
|
+
});
|
|
162
|
+
expect(result.current.isVeeamBucket).toBe(false);
|
|
163
|
+
expect(result.current.isCommvaultBucket).toBe(false);
|
|
164
|
+
expect(result.current.isKastenBucket).toBe(true);
|
|
165
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
166
|
+
expect(result.current.isvApplication).toBe('Kasten');
|
|
167
|
+
});
|
|
147
168
|
it('should return false for non-ISV bucket', ()=>{
|
|
148
169
|
mockUseGetBucketTagging.mockReturnValue({
|
|
149
170
|
data: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BUCKET_TAG_APPLICATION, BUCKET_TAG_VEEAM_APPLICATION, COMMVAULT_APPLICATION, VEEAM_BACKUP_REPLICATION, VEEAM_OFFICE_365, VEEAM_OFFICE_365_V8, VEEAM_VBO_APPLICATION } from "../utils/constants.js";
|
|
1
|
+
import { BUCKET_TAG_APPLICATION, BUCKET_TAG_VEEAM_APPLICATION, COMMVAULT_APPLICATION, KASTEN_APPLICATION, VEEAM_BACKUP_REPLICATION, VEEAM_OFFICE_365, VEEAM_OFFICE_365_V8, VEEAM_VBO_APPLICATION } from "../utils/constants.js";
|
|
2
2
|
import { useGetBucketTagging } from "./bucketConfiguration.js";
|
|
3
3
|
import { useFeatures } from "./useFeatures.js";
|
|
4
4
|
const useISVBucketStatus = (bucketName)=>{
|
|
@@ -13,13 +13,15 @@ const useISVBucketStatus = (bucketName)=>{
|
|
|
13
13
|
const isVeeamBucket = veeamTagApplication === VEEAM_BACKUP_REPLICATION || veeamTagApplication === VEEAM_OFFICE_365 || veeamTagApplication === VEEAM_OFFICE_365_V8;
|
|
14
14
|
const isISVBucketTagAsVeeam = ISVApplicationTag === VEEAM_BACKUP_REPLICATION || ISVApplicationTag === VEEAM_VBO_APPLICATION;
|
|
15
15
|
const isCommvaultBucket = ISVApplicationTag === COMMVAULT_APPLICATION;
|
|
16
|
+
const isKastenBucket = ISVApplicationTag === KASTEN_APPLICATION;
|
|
16
17
|
const isVeeam = isVeeamBucket || isISVBucketTagAsVeeam;
|
|
17
|
-
const isISVManaged = isVeeam || isCommvaultBucket;
|
|
18
|
+
const isISVManaged = isVeeam || isCommvaultBucket || isKastenBucket;
|
|
18
19
|
return {
|
|
19
20
|
isVeeamBucket: isVeeam,
|
|
20
21
|
isCommvaultBucket,
|
|
22
|
+
isKastenBucket,
|
|
21
23
|
isISVManaged,
|
|
22
|
-
isvApplication: isCommvaultBucket ? 'Commvault' : isVeeam ? 'Veeam' : void 0,
|
|
24
|
+
isvApplication: isKastenBucket ? 'Kasten' : isCommvaultBucket ? 'Commvault' : isVeeam ? 'Veeam' : void 0,
|
|
23
25
|
isLoading: isISVFeatureEnabled && 'pending' === bucketTagsStatus,
|
|
24
26
|
bucketTagsStatus
|
|
25
27
|
};
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/test/testUtils.d.ts
CHANGED
|
@@ -158,6 +158,7 @@ export type MockQueryResult<T> = {
|
|
|
158
158
|
export type ISVBucketStatusMock = {
|
|
159
159
|
isVeeamBucket: boolean;
|
|
160
160
|
isCommvaultBucket: boolean;
|
|
161
|
+
isKastenBucket: boolean;
|
|
161
162
|
isISVManaged: boolean;
|
|
162
163
|
isvApplication: string | undefined;
|
|
163
164
|
isLoading: boolean;
|
package/dist/test/testUtils.js
CHANGED
|
@@ -10,6 +10,8 @@ export declare const VEEAM_OFFICE_365 = "Veeam Backup for Microsoft 365 (v6, v7)
|
|
|
10
10
|
export declare const VEEAM_OFFICE_365_V8 = "Veeam Backup for Microsoft 365 (v8+)";
|
|
11
11
|
/** Generic identifier for Commvault */
|
|
12
12
|
export declare const COMMVAULT_APPLICATION = "Commvault";
|
|
13
|
+
/** Generic identifier for Kasten */
|
|
14
|
+
export declare const KASTEN_APPLICATION = "Kasten";
|
|
13
15
|
export declare const BUCKET_ROUTES: {
|
|
14
16
|
readonly bucketPolicy: (bucketName: string) => string;
|
|
15
17
|
readonly bucketCors: (bucketName: string) => string;
|
package/dist/utils/constants.js
CHANGED
|
@@ -6,6 +6,7 @@ const VEEAM_VBO_APPLICATION = 'Veeam Backup for Microsoft 365';
|
|
|
6
6
|
const VEEAM_OFFICE_365 = 'Veeam Backup for Microsoft 365 (v6, v7)';
|
|
7
7
|
const VEEAM_OFFICE_365_V8 = 'Veeam Backup for Microsoft 365 (v8+)';
|
|
8
8
|
const COMMVAULT_APPLICATION = 'Commvault';
|
|
9
|
+
const KASTEN_APPLICATION = 'Kasten';
|
|
9
10
|
const BUCKET_ROUTES = {
|
|
10
11
|
bucketPolicy: (bucketName)=>`/buckets/${bucketName}/policy`,
|
|
11
12
|
bucketCors: (bucketName)=>`/buckets/${bucketName}/cors`,
|
|
@@ -16,4 +17,4 @@ const BUCKET_ROUTES = {
|
|
|
16
17
|
notificationCreate: (bucketName)=>`/buckets/${bucketName}/notifications/create`,
|
|
17
18
|
notificationEdit: (bucketName, ruleId)=>`/buckets/${bucketName}/notifications/edit/${encodeURIComponent(ruleId)}`
|
|
18
19
|
};
|
|
19
|
-
export { BUCKET_ROUTES, BUCKET_TAG_APPLICATION, BUCKET_TAG_VEEAM_APPLICATION, COMMVAULT_APPLICATION, VEEAM_BACKUP_REPLICATION, VEEAM_IMMUTABLE_POLICY_NAME, VEEAM_OFFICE_365, VEEAM_OFFICE_365_V8, VEEAM_VBO_APPLICATION };
|
|
20
|
+
export { BUCKET_ROUTES, BUCKET_TAG_APPLICATION, BUCKET_TAG_VEEAM_APPLICATION, COMMVAULT_APPLICATION, KASTEN_APPLICATION, VEEAM_BACKUP_REPLICATION, VEEAM_IMMUTABLE_POLICY_NAME, VEEAM_OFFICE_365, VEEAM_OFFICE_365_V8, VEEAM_VBO_APPLICATION };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scality/data-browser-library",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A modular React component library for browsing S3 buckets and objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"react-hook-form": "^7.48.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@scality/core-ui": ">=0.
|
|
43
|
+
"@scality/core-ui": ">=0.198.0",
|
|
44
44
|
"react": ">=18.0.0",
|
|
45
45
|
"react-dom": ">=18.0.0",
|
|
46
46
|
"react-router": ">=7.1.3",
|