@scality/data-browser-library 1.0.5 → 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.
@@ -47,27 +47,33 @@ describe('MetadataSearch', ()=>{
47
47
  await user_event.type(input, 'test query');
48
48
  expect(searchButton).toBeEnabled();
49
49
  });
50
- it('shows search icon by default', ()=>{
50
+ it('shows search icon by default', async ()=>{
51
51
  renderMetadataSearch();
52
- const input = getInput();
53
- const searchIcon = input.parentElement?.querySelector('[aria-label*="Search"]');
54
- expect(searchIcon).toBeInTheDocument();
52
+ await waitFor(()=>{
53
+ expect(screen.getByRole('img', {
54
+ hidden: true
55
+ })).toBeInTheDocument();
56
+ });
55
57
  });
56
- it('shows check icon when metadata search is active', ()=>{
58
+ it('shows check icon when metadata search is active', async ()=>{
57
59
  renderMetadataSearch({}, [
58
60
  '/bucket/test-bucket?metadatasearch=test'
59
61
  ]);
60
- const input = getInput();
61
- const checkIcon = input.parentElement?.querySelector('[aria-label*="Check"]');
62
- expect(checkIcon).toBeInTheDocument();
62
+ await waitFor(()=>{
63
+ expect(screen.getByRole('img', {
64
+ hidden: true
65
+ })).toBeInTheDocument();
66
+ });
63
67
  });
64
- it('shows error icon when error prop is true', ()=>{
68
+ it('shows error icon when error prop is true', async ()=>{
65
69
  renderMetadataSearch({
66
70
  isError: true
67
71
  });
68
- const input = getInput();
69
- const errorIcon = input.parentElement?.querySelector('[aria-label*="Close"]');
70
- expect(errorIcon).toBeInTheDocument();
72
+ await waitFor(()=>{
73
+ expect(screen.getByRole('img', {
74
+ hidden: true
75
+ })).toBeInTheDocument();
76
+ });
71
77
  });
72
78
  it('populates input with existing search query from URL', ()=>{
73
79
  renderMetadataSearch({}, [
@@ -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,
@@ -59,7 +59,9 @@ describe('ObjectLockSettings', ()=>{
59
59
  await waitFor(()=>{
60
60
  expect(screen.getByText('Object-lock settings')).toBeInTheDocument();
61
61
  });
62
- expect(screen.getByLabelText(/object-lock/i)).toBeInTheDocument();
62
+ expect(screen.getByRole('checkbox', {
63
+ name: /object-lock/i
64
+ })).toBeInTheDocument();
63
65
  });
64
66
  it('disables save button when Object Lock is not enabled', async ()=>{
65
67
  renderObjectLockSettings();
@@ -81,9 +83,15 @@ describe('ObjectLockSettings', ()=>{
81
83
  });
82
84
  renderObjectLockSettings();
83
85
  await waitFor(()=>{
84
- expect(screen.getByText('Default Retention')).toBeInTheDocument();
85
- expect(screen.getByText('Retention mode')).toBeInTheDocument();
86
- expect(screen.getByText('Retention period')).toBeInTheDocument();
86
+ expect(screen.getByRole('checkbox', {
87
+ name: /default retention/i
88
+ })).toBeInTheDocument();
89
+ expect(screen.getByRole('radio', {
90
+ name: /governance/i
91
+ })).toBeInTheDocument();
92
+ expect(screen.getByRole('spinbutton', {
93
+ name: /retention period/i
94
+ })).toBeInTheDocument();
87
95
  }, {
88
96
  timeout: 3000
89
97
  });
@@ -118,13 +126,19 @@ describe('ObjectLockSettings', ()=>{
118
126
  });
119
127
  renderObjectLockSettings();
120
128
  await waitFor(()=>{
121
- expect(screen.getByText('Default Retention')).toBeInTheDocument();
129
+ expect(screen.getByRole('checkbox', {
130
+ name: /default retention/i
131
+ })).toBeInTheDocument();
132
+ });
133
+ const defaultRetentionCheckbox = screen.getByRole('checkbox', {
134
+ name: /default retention/i
122
135
  });
123
- const defaultRetentionCheckbox = screen.getByLabelText(/default retention/i);
124
136
  expect(defaultRetentionCheckbox).not.toBeChecked();
125
137
  fireEvent.click(defaultRetentionCheckbox);
126
138
  await waitFor(()=>{
127
- const governanceRadio = screen.getByLabelText(/governance/i);
139
+ const governanceRadio = screen.getByRole('radio', {
140
+ name: /governance/i
141
+ });
128
142
  expect(governanceRadio).not.toBeDisabled();
129
143
  }, {
130
144
  timeout: 3000
@@ -147,11 +161,15 @@ describe('ObjectLockSettings', ()=>{
147
161
  });
148
162
  renderObjectLockSettings();
149
163
  await waitFor(()=>{
150
- const defaultRetentionCheckbox = screen.getByLabelText(/default retention/i);
164
+ const defaultRetentionCheckbox = screen.getByRole('checkbox', {
165
+ name: /default retention/i
166
+ });
151
167
  fireEvent.click(defaultRetentionCheckbox);
152
168
  });
153
169
  await waitFor(()=>{
154
- const governanceRadio = screen.getByLabelText(/governance/i);
170
+ const governanceRadio = screen.getByRole('radio', {
171
+ name: /governance/i
172
+ });
155
173
  expect(governanceRadio).toBeDisabled();
156
174
  });
157
175
  });
@@ -166,15 +184,23 @@ describe('ObjectLockSettings', ()=>{
166
184
  });
167
185
  renderObjectLockSettings();
168
186
  await waitFor(()=>{
169
- expect(screen.getByText('Default Retention')).toBeInTheDocument();
187
+ expect(screen.getByRole('checkbox', {
188
+ name: /default retention/i
189
+ })).toBeInTheDocument();
190
+ });
191
+ const defaultRetentionCheckbox = screen.getByRole('checkbox', {
192
+ name: /default retention/i
170
193
  });
171
- const defaultRetentionCheckbox = screen.getByLabelText(/default retention/i);
172
194
  fireEvent.click(defaultRetentionCheckbox);
173
195
  await waitFor(()=>{
174
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
196
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
197
+ name: /retention period/i
198
+ });
175
199
  expect(retentionPeriodInput).toBeInTheDocument();
176
200
  });
177
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
201
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
202
+ name: /retention period/i
203
+ });
178
204
  await user_event.clear(retentionPeriodInput);
179
205
  await user_event.type(retentionPeriodInput, '0');
180
206
  await waitFor(()=>{
@@ -197,18 +223,28 @@ describe('ObjectLockSettings', ()=>{
197
223
  });
198
224
  renderObjectLockSettings();
199
225
  await waitFor(()=>{
200
- expect(screen.getByText('Default Retention')).toBeInTheDocument();
226
+ expect(screen.getByRole('checkbox', {
227
+ name: /default retention/i
228
+ })).toBeInTheDocument();
229
+ });
230
+ const defaultRetentionCheckbox = screen.getByRole('checkbox', {
231
+ name: /default retention/i
201
232
  });
202
- const defaultRetentionCheckbox = screen.getByLabelText(/default retention/i);
203
233
  fireEvent.click(defaultRetentionCheckbox);
204
234
  await waitFor(()=>{
205
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
235
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
236
+ name: /retention period/i
237
+ });
206
238
  expect(retentionPeriodInput).toBeInTheDocument();
207
239
  });
208
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
240
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
241
+ name: /retention period/i
242
+ });
209
243
  await user_event.clear(retentionPeriodInput);
210
244
  await user_event.type(retentionPeriodInput, '30');
211
- const governanceRadio = screen.getByLabelText(/governance/i);
245
+ const governanceRadio = screen.getByRole('radio', {
246
+ name: /governance/i
247
+ });
212
248
  fireEvent.click(governanceRadio);
213
249
  mockMutate.mockImplementation((_, options)=>{
214
250
  options?.onSuccess?.();
@@ -241,15 +277,23 @@ describe('ObjectLockSettings', ()=>{
241
277
  });
242
278
  renderObjectLockSettings();
243
279
  await waitFor(()=>{
244
- expect(screen.getByText('Default Retention')).toBeInTheDocument();
280
+ expect(screen.getByRole('checkbox', {
281
+ name: /default retention/i
282
+ })).toBeInTheDocument();
283
+ });
284
+ const defaultRetentionCheckbox = screen.getByRole('checkbox', {
285
+ name: /default retention/i
245
286
  });
246
- const defaultRetentionCheckbox = screen.getByLabelText(/default retention/i);
247
287
  fireEvent.click(defaultRetentionCheckbox);
248
288
  await waitFor(()=>{
249
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
289
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
290
+ name: /retention period/i
291
+ });
250
292
  expect(retentionPeriodInput).toBeInTheDocument();
251
293
  });
252
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
294
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
295
+ name: /retention period/i
296
+ });
253
297
  await user_event.clear(retentionPeriodInput);
254
298
  await user_event.type(retentionPeriodInput, '2');
255
299
  mockMutate.mockImplementation((_, options)=>{
@@ -361,14 +405,22 @@ describe('ObjectLockSettings', ()=>{
361
405
  });
362
406
  renderObjectLockSettings();
363
407
  await waitFor(()=>{
364
- const objectLockCheckbox = screen.getByLabelText(/object-lock/i);
408
+ const objectLockCheckbox = screen.getByRole('checkbox', {
409
+ name: /object-lock/i
410
+ });
365
411
  expect(objectLockCheckbox).toBeChecked();
366
412
  });
367
- const defaultRetentionCheckbox = screen.getByLabelText(/default retention/i);
413
+ const defaultRetentionCheckbox = screen.getByRole('checkbox', {
414
+ name: /default retention/i
415
+ });
368
416
  expect(defaultRetentionCheckbox).toBeChecked();
369
- const complianceRadio = screen.getByLabelText(/compliance/i);
417
+ const complianceRadio = screen.getByRole('radio', {
418
+ name: /compliance/i
419
+ });
370
420
  expect(complianceRadio).toBeChecked();
371
- const retentionPeriodInput = screen.getByLabelText(/retention period/i);
421
+ const retentionPeriodInput = screen.getByRole('spinbutton', {
422
+ name: /retention period/i
423
+ });
372
424
  expect(retentionPeriodInput).toHaveValue(60);
373
425
  });
374
426
  });
@@ -212,7 +212,9 @@ describe('s3RuntimeConfigSchema', ()=>{
212
212
  endpoint: 'https://s3.amazonaws.com',
213
213
  region: 'us-east-1',
214
214
  forcePathStyle: true
215
- }
215
+ },
216
+ allowCustomEndpoint: true,
217
+ allowSessionToken: true
216
218
  };
217
219
  const { error } = s3RuntimeConfigSchema.validate(config);
218
220
  expect(error).toBeUndefined();
@@ -282,6 +284,32 @@ describe('s3RuntimeConfigSchema', ()=>{
282
284
  expect(error).toBeDefined();
283
285
  expect(error?.message).toContain('boolean');
284
286
  });
287
+ it('should reject config with wrong type for allowCustomEndpoint', ()=>{
288
+ const config = {
289
+ s3: {
290
+ region: 'us-east-1'
291
+ },
292
+ allowCustomEndpoint: 'yes'
293
+ };
294
+ const { error } = s3RuntimeConfigSchema.validate(config, {
295
+ convert: false
296
+ });
297
+ expect(error).toBeDefined();
298
+ expect(error?.message).toContain('boolean');
299
+ });
300
+ it('should reject config with wrong type for allowSessionToken', ()=>{
301
+ const config = {
302
+ s3: {
303
+ region: 'us-east-1'
304
+ },
305
+ allowSessionToken: 'yes'
306
+ };
307
+ const { error } = s3RuntimeConfigSchema.validate(config, {
308
+ convert: false
309
+ });
310
+ expect(error).toBeDefined();
311
+ expect(error?.message).toContain('boolean');
312
+ });
285
313
  });
286
314
  describe('Schema behavior', ()=>{
287
315
  it('should return all validation errors when abortEarly is false', ()=>{
@@ -5,6 +5,8 @@ export type S3RuntimeConfig = {
5
5
  region: string;
6
6
  forcePathStyle?: boolean;
7
7
  };
8
+ allowCustomEndpoint?: boolean;
9
+ allowSessionToken?: boolean;
8
10
  };
9
11
  export declare const s3RuntimeConfigSchema: Joi.ObjectSchema<any>;
10
12
  /**
@@ -4,7 +4,9 @@ const s3RuntimeConfigSchema = joi.object({
4
4
  endpoint: joi.string().optional().allow('origin'),
5
5
  region: joi.string().min(1).required(),
6
6
  forcePathStyle: joi.boolean().optional()
7
- }).required()
7
+ }).required(),
8
+ allowCustomEndpoint: joi.boolean().optional(),
9
+ allowSessionToken: joi.boolean().optional()
8
10
  });
9
11
  async function loadRuntimeConfig(configUrl) {
10
12
  const response = await fetch(configUrl);
@@ -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: {
@@ -8,6 +8,7 @@
8
8
  export declare const useISVBucketStatus: (bucketName: string) => {
9
9
  isVeeamBucket: boolean;
10
10
  isCommvaultBucket: boolean;
11
+ isKastenBucket: boolean;
11
12
  isISVManaged: boolean;
12
13
  isvApplication: string | undefined;
13
14
  isLoading: boolean | undefined;
@@ -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
  };