@scality/data-browser-library 1.0.6 → 1.0.7
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/ObjectLock/__tests__/EditRetentionButton.test.js +8 -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/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 +1 -1
|
@@ -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,
|
|
@@ -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,
|
|
@@ -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/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 };
|