@scality/data-browser-library 1.1.10 → 1.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/components/__tests__/BucketCorsPage.test.js +14 -6
  2. package/dist/components/__tests__/BucketLifecycleFormPage.test.js +129 -0
  3. package/dist/components/__tests__/BucketLifecycleList.test.js +78 -0
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +15 -3
  5. package/dist/components/buckets/BucketCorsPage.js +5 -7
  6. package/dist/components/buckets/BucketLifecycleFormPage.js +18 -12
  7. package/dist/components/buckets/BucketLifecycleList.js +5 -5
  8. package/dist/components/buckets/BucketOverview.js +1 -1
  9. package/dist/components/buckets/BucketPolicyPage.js +4 -7
  10. package/dist/components/buckets/BucketReplicationFormPage.js +12 -6
  11. package/dist/components/buckets/BucketVersioning.js +1 -1
  12. package/dist/components/buckets/notifications/BucketNotificationFormPage.js +2 -2
  13. package/dist/components/buckets/notifications/BucketNotificationList.js +1 -1
  14. package/dist/components/objects/DeleteObjectButton.js +1 -1
  15. package/dist/components/objects/GetPresignedUrlButton.js +1 -1
  16. package/dist/components/objects/ObjectDetails/ObjectMetadata.js +1 -1
  17. package/dist/components/objects/ObjectDetails/ObjectSummary.js +118 -7
  18. package/dist/components/objects/ObjectDetails/ObjectTags.js +1 -1
  19. package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.js +142 -3
  20. package/dist/components/objects/ObjectList.js +42 -16
  21. package/dist/components/objects/ObjectLock/ObjectLockSettings.js +1 -1
  22. package/dist/components/objects/__tests__/GetPresignedUrlButton.test.js +30 -1
  23. package/dist/components/providers/QueryProvider.js +2 -1
  24. package/dist/config/types.d.ts +4 -0
  25. package/dist/hooks/bucketConfiguration.js +15 -15
  26. package/dist/hooks/bucketOperations.js +1 -1
  27. package/dist/hooks/factories/__tests__/useCreateS3FunctionMutationHook.test.js +7 -35
  28. package/dist/hooks/factories/__tests__/useCreateS3InfiniteQueryHook.test.js +1 -1
  29. package/dist/hooks/factories/__tests__/useCreateS3MutationHook.test.js +1 -1
  30. package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +1 -1
  31. package/dist/hooks/factories/useCreateS3InfiniteQueryHook.d.ts +1 -1
  32. package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +2 -2
  33. package/dist/hooks/factories/useCreateS3MutationHook.d.ts +3 -3
  34. package/dist/hooks/factories/useCreateS3MutationHook.js +20 -8
  35. package/dist/hooks/factories/useCreateS3QueryHook.d.ts +1 -1
  36. package/dist/hooks/factories/useCreateS3QueryHook.js +2 -2
  37. package/dist/hooks/objectOperations.js +6 -6
  38. package/dist/hooks/presignedOperations.js +1 -1
  39. package/dist/hooks/useDeleteFolder.js +1 -1
  40. package/dist/test/utils/errorHandling.test.js +45 -33
  41. package/dist/utils/__tests__/coldStorage.test.d.ts +1 -0
  42. package/dist/utils/__tests__/coldStorage.test.js +24 -0
  43. package/dist/utils/coldStorage.d.ts +12 -0
  44. package/dist/utils/coldStorage.js +23 -0
  45. package/dist/utils/errorHandling.d.ts +2 -1
  46. package/dist/utils/errorHandling.js +8 -7
  47. package/package.json +1 -1
@@ -306,16 +306,24 @@ describe('BucketCorsPage', ()=>{
306
306
  replace: true
307
307
  });
308
308
  });
309
- it('shows error message when fetching CORS fails with unexpected error', ()=>{
310
- const accessDeniedError = new Error('Access Denied');
311
- accessDeniedError.name = 'AccessDenied';
309
+ it('shows permission message when fetching CORS fails with 403 error', ()=>{
310
+ const authError = new EnhancedS3Error("You don't have permission to load CORS configuration. Contact your administrator.", 'AccessDenied', ErrorCategory.AUTHORIZATION, new Error('Access Denied'), 403);
312
311
  mockUseGetBucketCors.mockReturnValue(createMockQueryResult({
313
312
  status: 'error',
314
- error: accessDeniedError
313
+ error: authError
315
314
  }));
316
315
  renderBucketCorsPage();
317
- expect(screen.getByText(/Failed to load CORS configuration/i)).toBeInTheDocument();
318
- expect(screen.getByText(/Access Denied/i)).toBeInTheDocument();
316
+ expect(screen.getByText("You don't have permission to load CORS configuration. Contact your administrator.")).toBeInTheDocument();
317
+ expect(screen.queryByTestId('cors-editor')).not.toBeInTheDocument();
318
+ });
319
+ it('shows original error message when fetching CORS fails with non-403 error', ()=>{
320
+ const serverError = new EnhancedS3Error('Internal Server Error', 'InternalError', ErrorCategory.SERVER_ERROR, new Error('Internal Server Error'), 500);
321
+ mockUseGetBucketCors.mockReturnValue(createMockQueryResult({
322
+ status: 'error',
323
+ error: serverError
324
+ }));
325
+ renderBucketCorsPage();
326
+ expect(screen.getByText('Internal Server Error')).toBeInTheDocument();
319
327
  expect(screen.queryByTestId('cors-editor')).not.toBeInTheDocument();
320
328
  });
321
329
  });
@@ -441,6 +441,31 @@ describe('BucketLifecycleFormPage', ()=>{
441
441
  });
442
442
  });
443
443
  describe('Rule Data Loading', ()=>{
444
+ it('loads rule with empty prefix filter as "All objects" (no filter)', async ()=>{
445
+ const rule = {
446
+ ID: 'no-filter-rule',
447
+ Status: 'Enabled',
448
+ Filter: {
449
+ Prefix: ''
450
+ },
451
+ Expiration: {
452
+ Days: 30
453
+ }
454
+ };
455
+ mockUseGetBucketLifecycle.mockReturnValue({
456
+ data: {
457
+ Rules: [
458
+ rule
459
+ ]
460
+ },
461
+ status: 'success'
462
+ });
463
+ renderBucketLifecycleFormPage('test-bucket', 'no-filter-rule');
464
+ await waitFor(()=>{
465
+ expect(screen.getByText('All objects')).toBeInTheDocument();
466
+ });
467
+ expect(screen.queryByPlaceholderText('folder/')).not.toBeInTheDocument();
468
+ });
444
469
  it('loads rule with prefix filter correctly', async ()=>{
445
470
  const rule = {
446
471
  ID: 'prefix-rule',
@@ -684,6 +709,110 @@ describe('BucketLifecycleFormPage', ()=>{
684
709
  });
685
710
  });
686
711
  });
712
+ describe('editing immediate transition rules (0 days)', ()=>{
713
+ it('preserves 0 days when editing a current version transition', async ()=>{
714
+ mockUseGetBucketLifecycle.mockReturnValue({
715
+ data: {
716
+ Rules: [
717
+ {
718
+ ID: 'immediate-glacier',
719
+ Status: 'Enabled',
720
+ Filter: {},
721
+ Transitions: [
722
+ {
723
+ Days: 0,
724
+ StorageClass: 'GLACIER'
725
+ }
726
+ ]
727
+ }
728
+ ]
729
+ },
730
+ status: 'success'
731
+ });
732
+ renderBucketLifecycleFormPage('test-bucket', 'immediate-glacier');
733
+ await waitFor(()=>{
734
+ const transitionToggle = findToggleByLabel('Transition current version');
735
+ expect(transitionToggle).toBeChecked();
736
+ const daysInput = document.getElementById('transition-days-0');
737
+ expect(daysInput).toHaveValue(0);
738
+ });
739
+ });
740
+ it('preserves 0 days when editing a noncurrent version transition', async ()=>{
741
+ mockUseGetBucketLifecycle.mockReturnValue({
742
+ data: {
743
+ Rules: [
744
+ {
745
+ ID: 'immediate-noncurrent',
746
+ Status: 'Enabled',
747
+ Filter: {},
748
+ NoncurrentVersionTransitions: [
749
+ {
750
+ NoncurrentDays: 0,
751
+ StorageClass: 'GLACIER'
752
+ }
753
+ ]
754
+ }
755
+ ]
756
+ },
757
+ status: 'success'
758
+ });
759
+ renderBucketLifecycleFormPage('test-bucket', 'immediate-noncurrent');
760
+ await waitFor(()=>{
761
+ const noncurrentToggle = findToggleByLabel('Transition noncurrent version');
762
+ expect(noncurrentToggle).toBeChecked();
763
+ const daysInput = document.getElementById('noncurrent-transition-days-0');
764
+ expect(daysInput).toHaveValue(0);
765
+ });
766
+ });
767
+ it('preserves 0 days when editing a current version expiration', async ()=>{
768
+ mockUseGetBucketLifecycle.mockReturnValue({
769
+ data: {
770
+ Rules: [
771
+ {
772
+ ID: 'immediate-expire',
773
+ Status: 'Enabled',
774
+ Filter: {},
775
+ Expiration: {
776
+ Days: 0
777
+ }
778
+ }
779
+ ]
780
+ },
781
+ status: 'success'
782
+ });
783
+ renderBucketLifecycleFormPage('test-bucket', 'immediate-expire');
784
+ await waitFor(()=>{
785
+ const expirationToggle = findToggleByLabel('Expiration current version');
786
+ expect(expirationToggle).toBeChecked();
787
+ const daysInput = screen.getByLabelText(/^days$/i);
788
+ expect(daysInput).toHaveValue(0);
789
+ });
790
+ });
791
+ it('preserves 0 days when editing a noncurrent version expiration', async ()=>{
792
+ mockUseGetBucketLifecycle.mockReturnValue({
793
+ data: {
794
+ Rules: [
795
+ {
796
+ ID: 'immediate-noncurrent-expire',
797
+ Status: 'Enabled',
798
+ Filter: {},
799
+ NoncurrentVersionExpiration: {
800
+ NoncurrentDays: 0
801
+ }
802
+ }
803
+ ]
804
+ },
805
+ status: 'success'
806
+ });
807
+ renderBucketLifecycleFormPage('test-bucket', 'immediate-noncurrent-expire');
808
+ await waitFor(()=>{
809
+ const noncurrentExpirationToggle = findToggleByLabel('Expiration noncurrent version');
810
+ expect(noncurrentExpirationToggle).toBeChecked();
811
+ const noncurrentDaysInput = screen.getByLabelText(/noncurrent days/i);
812
+ expect(noncurrentDaysInput).toHaveValue(0);
813
+ });
814
+ });
815
+ });
687
816
  describe('Transition Auto-Add in Create Mode', ()=>{
688
817
  it('auto-adds a transition row when enabling transitions', async ()=>{
689
818
  renderBucketLifecycleFormPage();
@@ -314,6 +314,84 @@ describe('BucketLifecycleList', ()=>{
314
314
  });
315
315
  expect(screen.getByText('Delete expired markers')).toBeInTheDocument();
316
316
  });
317
+ describe('immediate transition rules (0 days)', ()=>{
318
+ it("shows current version transition description for immediate transition", ()=>{
319
+ renderBucketLifecycleList({
320
+ lifecycleRules: [
321
+ {
322
+ ID: 'immediate-glacier',
323
+ Status: 'Enabled',
324
+ Transitions: [
325
+ {
326
+ Days: 0,
327
+ StorageClass: 'GLACIER'
328
+ }
329
+ ]
330
+ }
331
+ ]
332
+ });
333
+ expect(screen.getByText(/Transition current \(0 days\)/)).toBeInTheDocument();
334
+ });
335
+ it("shows noncurrent version transition description for immediate transition", ()=>{
336
+ renderBucketLifecycleList({
337
+ lifecycleRules: [
338
+ {
339
+ ID: 'immediate-noncurrent',
340
+ Status: 'Enabled',
341
+ NoncurrentVersionTransitions: [
342
+ {
343
+ NoncurrentDays: 0,
344
+ StorageClass: 'GLACIER'
345
+ }
346
+ ]
347
+ }
348
+ ]
349
+ });
350
+ expect(screen.getByText(/Transition noncurrent \(0 days\)/)).toBeInTheDocument();
351
+ });
352
+ it("shows expiration description for immediate expiration", ()=>{
353
+ renderBucketLifecycleList({
354
+ lifecycleRules: [
355
+ {
356
+ ID: 'immediate-expire',
357
+ Status: 'Enabled',
358
+ Expiration: {
359
+ Days: 0
360
+ }
361
+ }
362
+ ]
363
+ });
364
+ expect(screen.getByText(/Expire current \(0 days\)/)).toBeInTheDocument();
365
+ });
366
+ it("shows noncurrent expiration description for immediate expiration", ()=>{
367
+ renderBucketLifecycleList({
368
+ lifecycleRules: [
369
+ {
370
+ ID: 'immediate-noncurrent-expire',
371
+ Status: 'Enabled',
372
+ NoncurrentVersionExpiration: {
373
+ NoncurrentDays: 0
374
+ }
375
+ }
376
+ ]
377
+ });
378
+ expect(screen.getByText(/Expire noncurrent \(0 days\)/)).toBeInTheDocument();
379
+ });
380
+ it("shows abort MPU description for immediate abort", ()=>{
381
+ renderBucketLifecycleList({
382
+ lifecycleRules: [
383
+ {
384
+ ID: 'immediate-mpu',
385
+ Status: 'Enabled',
386
+ AbortIncompleteMultipartUpload: {
387
+ DaysAfterInitiation: 0
388
+ }
389
+ }
390
+ ]
391
+ });
392
+ expect(screen.getByText(/Abort Incomplete MPU \(0 days\)/)).toBeInTheDocument();
393
+ });
394
+ });
317
395
  it('shows info icon for multiple actions', ()=>{
318
396
  renderBucketLifecycleList({
319
397
  lifecycleRules: mockLifecycleRules
@@ -5,6 +5,7 @@ import { MemoryRouter, Route, Routes } from "react-router";
5
5
  import { useGetBucketNotification, useSetBucketNotification } from "../../hooks/index.js";
6
6
  import { useSupportedNotificationEvents } from "../../hooks/useSupportedNotificationEvents.js";
7
7
  import { createTestWrapper } from "../../test/testUtils.js";
8
+ import { EnhancedS3Error, ErrorCategory } from "../../utils/errorHandling.js";
8
9
  import { BucketNotificationFormPage } from "../buckets/notifications/BucketNotificationFormPage.js";
9
10
  jest.mock('../../hooks', ()=>({
10
11
  useGetBucketNotification: jest.fn(),
@@ -235,14 +236,25 @@ describe('BucketNotificationFormPage', ()=>{
235
236
  renderCreatePage();
236
237
  expect(screen.getByText(/failed to fetch notification configuration/i)).toBeInTheDocument();
237
238
  });
238
- it('shows generic error message when error is not an Error instance', ()=>{
239
+ it('shows permission message when notification data fails to load with 403 error', ()=>{
240
+ const authError = new EnhancedS3Error("You don't have permission to load the notification configuration. Contact your administrator.", 'AccessDenied', ErrorCategory.AUTHORIZATION, new Error('Access Denied'), 403);
239
241
  mockUseGetBucketNotification.mockReturnValue({
240
242
  data: void 0,
241
243
  status: 'error',
242
- error: 'Unknown error'
244
+ error: authError
243
245
  });
244
246
  renderCreatePage();
245
- expect(screen.getByText(/failed to load notification configuration/i)).toBeInTheDocument();
247
+ expect(screen.getByText("You don't have permission to load the notification configuration. Contact your administrator.")).toBeInTheDocument();
248
+ });
249
+ it('shows error message from EnhancedS3Error', ()=>{
250
+ const error = new EnhancedS3Error('Something went wrong', 'InternalError', ErrorCategory.SERVER_ERROR, new Error('Something went wrong'), 500);
251
+ mockUseGetBucketNotification.mockReturnValue({
252
+ data: void 0,
253
+ status: 'error',
254
+ error
255
+ });
256
+ renderCreatePage();
257
+ expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
246
258
  });
247
259
  });
248
260
  describe('Edit Mode', ()=>{
@@ -102,7 +102,7 @@ const BucketCorsPage = ()=>{
102
102
  onError: (error)=>{
103
103
  setError('content', {
104
104
  type: 'server',
105
- message: error instanceof Error ? error.message : 'Failed to delete'
105
+ message: error.message
106
106
  });
107
107
  }
108
108
  });
@@ -123,7 +123,7 @@ const BucketCorsPage = ()=>{
123
123
  onError: (error)=>{
124
124
  setError('content', {
125
125
  type: 'server',
126
- message: error instanceof Error ? error.message : 'Failed to save'
126
+ message: error.message
127
127
  });
128
128
  }
129
129
  });
@@ -163,12 +163,9 @@ const BucketCorsPage = ()=>{
163
163
  size: "2x",
164
164
  color: "statusWarning"
165
165
  }),
166
- /*#__PURE__*/ jsxs(Text, {
166
+ /*#__PURE__*/ jsx(Text, {
167
167
  variant: "Large",
168
- children: [
169
- "Failed to load CORS configuration: ",
170
- corsError.message || 'Unknown error'
171
- ]
168
+ children: corsError.message
172
169
  })
173
170
  ]
174
171
  })
@@ -214,6 +211,7 @@ const BucketCorsPage = ()=>{
214
211
  id: "corsConfiguration",
215
212
  label: "CORS Rules",
216
213
  direction: "vertical",
214
+ helpErrorPosition: "bottom",
217
215
  error: errors.content?.message,
218
216
  content: /*#__PURE__*/ jsxs(Stack, {
219
217
  direction: "horizontal",
@@ -209,7 +209,7 @@ const ruleToFormValues = (rule)=>{
209
209
  key: tag.Key || '',
210
210
  value: tag.Value || ''
211
211
  })) || [];
212
- } else if (void 0 !== rule.Filter.Prefix) {
212
+ } else if (void 0 !== rule.Filter.Prefix && '' !== rule.Filter.Prefix) {
213
213
  formValues.filterType = 'prefix';
214
214
  formValues.prefix = rule.Filter.Prefix;
215
215
  } else if (rule.Filter.Tag) {
@@ -226,13 +226,13 @@ const ruleToFormValues = (rule)=>{
226
226
  formValues.transitionsEnabled = true;
227
227
  formValues.transitions = rule.Transitions.map((t)=>({
228
228
  timeType: void 0 !== t.Days ? 'days' : 'date',
229
- days: t.Days || 30,
229
+ days: t.Days ?? 30,
230
230
  date: t.Date ? new Date(t.Date).toISOString().split('T')[0] : '',
231
231
  storageClass: t.StorageClass || 'GLACIER'
232
232
  }));
233
233
  }
234
234
  if (rule.Expiration) {
235
- if (rule.Expiration.Days) {
235
+ if (null != rule.Expiration.Days) {
236
236
  formValues.expirationEnabled = true;
237
237
  formValues.expirationType = 'days';
238
238
  formValues.expirationDays = rule.Expiration.Days;
@@ -246,19 +246,19 @@ const ruleToFormValues = (rule)=>{
246
246
  if (rule.NoncurrentVersionTransitions && rule.NoncurrentVersionTransitions.length > 0) {
247
247
  formValues.noncurrentTransitionsEnabled = true;
248
248
  formValues.noncurrentTransitions = rule.NoncurrentVersionTransitions.map((t)=>({
249
- noncurrentDays: t.NoncurrentDays || 30,
249
+ noncurrentDays: t.NoncurrentDays ?? 30,
250
250
  storageClass: t.StorageClass || 'GLACIER',
251
251
  newerNoncurrentVersions: t.NewerNoncurrentVersions || 0
252
252
  }));
253
253
  }
254
254
  if (rule.NoncurrentVersionExpiration) {
255
255
  formValues.noncurrentExpirationEnabled = true;
256
- formValues.noncurrentExpirationDays = rule.NoncurrentVersionExpiration.NoncurrentDays || 30;
256
+ formValues.noncurrentExpirationDays = rule.NoncurrentVersionExpiration.NoncurrentDays ?? 30;
257
257
  formValues.noncurrentNewerVersions = rule.NoncurrentVersionExpiration.NewerNoncurrentVersions || 0;
258
258
  }
259
259
  if (rule.AbortIncompleteMultipartUpload) {
260
260
  formValues.abortMpuEnabled = true;
261
- formValues.abortMpuDays = rule.AbortIncompleteMultipartUpload.DaysAfterInitiation || 7;
261
+ formValues.abortMpuDays = rule.AbortIncompleteMultipartUpload.DaysAfterInitiation ?? 7;
262
262
  }
263
263
  return formValues;
264
264
  };
@@ -347,7 +347,8 @@ function BucketLifecycleFormPage() {
347
347
  understandISVRisk: true
348
348
  }
349
349
  });
350
- const { handleSubmit, register, control, watch, reset, formState: { isValid, isDirty, errors } } = methods;
350
+ const { handleSubmit, register, control, watch, reset, formState: { isValid, dirtyFields, errors } } = methods;
351
+ const hasRuleChanges = Object.keys(dirtyFields).some((key)=>'understandISVRisk' !== key);
351
352
  const { fields: transitionFields, append: appendTransition, remove: removeTransition } = useFieldArray({
352
353
  control,
353
354
  name: 'transitions'
@@ -390,18 +391,23 @@ function BucketLifecycleFormPage() {
390
391
  useEffect(()=>{
391
392
  if (isEditMode && existingRule) {
392
393
  const formValues = ruleToFormValues(existingRule);
393
- reset(formValues);
394
+ reset({
395
+ ...formValues,
396
+ understandISVRisk: !isISVManaged
397
+ });
394
398
  }
395
399
  }, [
396
400
  isEditMode,
397
401
  existingRule,
398
- reset
402
+ reset,
403
+ isISVManaged
399
404
  ]);
400
405
  useEffect(()=>{
401
- methods.setValue('understandISVRisk', !isISVManaged, {
406
+ if (!isEditMode) methods.setValue('understandISVRisk', !isISVManaged, {
402
407
  shouldValidate: true
403
408
  });
404
409
  }, [
410
+ isEditMode,
405
411
  isISVManaged,
406
412
  methods
407
413
  ]);
@@ -496,7 +502,7 @@ function BucketLifecycleFormPage() {
496
502
  navigate(`/buckets/${bucketName}?tab=lifecycle`);
497
503
  },
498
504
  onError: (error)=>{
499
- const errorMessage = error instanceof Error ? error.message : `Failed to ${isEditMode ? 'update' : 'create'} lifecycle rule`;
505
+ const errorMessage = error.message;
500
506
  showToast({
501
507
  open: true,
502
508
  message: errorMessage,
@@ -554,7 +560,7 @@ function BucketLifecycleFormPage() {
554
560
  icon: isEditMode ? /*#__PURE__*/ jsx(Icon, {
555
561
  name: "Save"
556
562
  }) : void 0,
557
- disabled: isEditMode ? !isDirty || !isValid || isSaving : !isValid || isSaving
563
+ disabled: isEditMode ? !hasRuleChanges || !isValid || isSaving : !isValid || isSaving
558
564
  })
559
565
  ]
560
566
  }),
@@ -9,7 +9,7 @@ const pluralizeDays = (days)=>1 === days ? `${days} day` : `${days} days`;
9
9
  const formatActions = (rule)=>{
10
10
  const actions = [];
11
11
  if (rule.Expiration) {
12
- if (rule.Expiration.Days) actions.push({
12
+ if (null != rule.Expiration.Days) actions.push({
13
13
  text: `Expire current (${pluralizeDays(rule.Expiration.Days)})`,
14
14
  description: `Expire current versions of objects after ${pluralizeDays(rule.Expiration.Days)}`
15
15
  });
@@ -23,7 +23,7 @@ const formatActions = (rule)=>{
23
23
  description: 'Delete expired object delete markers'
24
24
  });
25
25
  }
26
- if (rule.NoncurrentVersionExpiration?.NoncurrentDays) {
26
+ if (rule.NoncurrentVersionExpiration?.NoncurrentDays != null) {
27
27
  const keepText = rule.NoncurrentVersionExpiration.NewerNoncurrentVersions ? `, keep ${rule.NoncurrentVersionExpiration.NewerNoncurrentVersions}` : '';
28
28
  const keepDescription = rule.NoncurrentVersionExpiration.NewerNoncurrentVersions ? `, keep ${rule.NoncurrentVersionExpiration.NewerNoncurrentVersions} newest versions` : '';
29
29
  actions.push({
@@ -32,7 +32,7 @@ const formatActions = (rule)=>{
32
32
  });
33
33
  }
34
34
  if (rule.Transitions && rule.Transitions.length > 0) rule.Transitions.forEach((transition)=>{
35
- if (transition.Days) actions.push({
35
+ if (null != transition.Days) actions.push({
36
36
  text: `Transition current (${pluralizeDays(transition.Days)})`,
37
37
  description: `Transition current versions of objects after ${pluralizeDays(transition.Days)}`
38
38
  });
@@ -43,7 +43,7 @@ const formatActions = (rule)=>{
43
43
  });
44
44
  });
45
45
  if (rule.NoncurrentVersionTransitions && rule.NoncurrentVersionTransitions.length > 0) rule.NoncurrentVersionTransitions.forEach((transition)=>{
46
- if (transition.NoncurrentDays) {
46
+ if (null != transition.NoncurrentDays) {
47
47
  const keepText = transition.NewerNoncurrentVersions ? `, keep ${transition.NewerNoncurrentVersions}` : '';
48
48
  const keepDescription = transition.NewerNoncurrentVersions ? `, keep ${transition.NewerNoncurrentVersions} newest versions` : '';
49
49
  actions.push({
@@ -52,7 +52,7 @@ const formatActions = (rule)=>{
52
52
  });
53
53
  }
54
54
  });
55
- if (rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation) actions.push({
55
+ if (rule.AbortIncompleteMultipartUpload?.DaysAfterInitiation != null) actions.push({
56
56
  text: `Abort Incomplete MPU (${pluralizeDays(rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)})`,
57
57
  description: `Abort incomplete multipart uploads after ${pluralizeDays(rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)}`
58
58
  });
@@ -476,7 +476,7 @@ const PermissionsSection = /*#__PURE__*/ memo(({ onEditPolicy, onEditCors, owner
476
476
  onError: (error)=>{
477
477
  showToast({
478
478
  open: true,
479
- message: error instanceof Error ? error.message : 'Failed to update bucket visibility',
479
+ message: error.message,
480
480
  status: 'error'
481
481
  });
482
482
  }
@@ -94,7 +94,7 @@ const BucketPolicyPage = ()=>{
94
94
  onError: (error)=>{
95
95
  setError('content', {
96
96
  type: 'server',
97
- message: error instanceof Error ? error.message : 'Failed to delete'
97
+ message: error.message
98
98
  });
99
99
  }
100
100
  });
@@ -114,7 +114,7 @@ const BucketPolicyPage = ()=>{
114
114
  onError: (error)=>{
115
115
  setError('content', {
116
116
  type: 'server',
117
- message: error instanceof Error ? error.message : 'Failed to save'
117
+ message: error.message
118
118
  });
119
119
  }
120
120
  });
@@ -153,12 +153,9 @@ const BucketPolicyPage = ()=>{
153
153
  size: "2x",
154
154
  color: "statusWarning"
155
155
  }),
156
- /*#__PURE__*/ jsxs(Text, {
156
+ /*#__PURE__*/ jsx(Text, {
157
157
  variant: "Large",
158
- children: [
159
- "Failed to load bucket policy: ",
160
- policyError.message || 'Unknown error'
161
- ]
158
+ children: policyError.message
162
159
  })
163
160
  ]
164
161
  })
@@ -450,7 +450,8 @@ function BucketReplicationFormPage() {
450
450
  understandISVRisk: true
451
451
  }
452
452
  });
453
- const { handleSubmit, register, control, watch, reset, formState: { isValid, isDirty, errors } } = methods;
453
+ const { handleSubmit, register, control, watch, reset, formState: { isValid, isDirty, dirtyFields, errors } } = methods;
454
+ const hasRuleChanges = Object.keys(dirtyFields).some((key)=>'understandISVRisk' !== key);
454
455
  const { fields: tagFields, append: appendTag, remove: removeTag } = useFieldArray({
455
456
  control,
456
457
  name: 'tags'
@@ -491,7 +492,10 @@ function BucketReplicationFormPage() {
491
492
  if (isEditMode && existingRule) {
492
493
  if (loadedRuleIdRef.current !== existingRule.ID) {
493
494
  const formValues = ruleToFormValues(existingRule, existingRole);
494
- reset(formValues);
495
+ reset({
496
+ ...formValues,
497
+ understandISVRisk: !isISVManaged
498
+ });
495
499
  loadedRuleIdRef.current = existingRule.ID || null;
496
500
  }
497
501
  } else if (!isEditMode && !isDirty) {
@@ -509,7 +513,8 @@ function BucketReplicationFormPage() {
509
513
  replicationRoleDefault,
510
514
  nextAvailablePriority,
511
515
  isDirty,
512
- reset
516
+ reset,
517
+ isISVManaged
513
518
  ]);
514
519
  const prevFilterTypeRef = useRef();
515
520
  useEffect(()=>{
@@ -564,10 +569,11 @@ function BucketReplicationFormPage() {
564
569
  methods
565
570
  ]);
566
571
  useEffect(()=>{
567
- methods.setValue('understandISVRisk', !isISVManaged, {
572
+ if (!isEditMode) methods.setValue('understandISVRisk', !isISVManaged, {
568
573
  shouldValidate: true
569
574
  });
570
575
  }, [
576
+ isEditMode,
571
577
  isISVManaged,
572
578
  methods
573
579
  ]);
@@ -601,7 +607,7 @@ function BucketReplicationFormPage() {
601
607
  navigate(`/buckets/${bucketName}?tab=replication`);
602
608
  },
603
609
  onError: (error)=>{
604
- const errorMessage = error instanceof Error ? error.message : `Failed to ${isEditMode ? 'update' : 'create'} replication rule`;
610
+ const errorMessage = error.message;
605
611
  showToast({
606
612
  open: true,
607
613
  message: errorMessage,
@@ -660,7 +666,7 @@ function BucketReplicationFormPage() {
660
666
  icon: isEditMode ? /*#__PURE__*/ jsx(Icon, {
661
667
  name: "Save"
662
668
  }) : void 0,
663
- disabled: !isDirty || !isValid || isSaving
669
+ disabled: isEditMode ? !hasRuleChanges || !isValid || isSaving : !isValid || isSaving
664
670
  })
665
671
  ]
666
672
  }),
@@ -35,7 +35,7 @@ function BucketVersioning({ tooltipOverlay }) {
35
35
  onError: (error)=>{
36
36
  showToast({
37
37
  open: true,
38
- message: error instanceof Error ? error.message : 'Failed to update bucket versioning',
38
+ message: error.message,
39
39
  status: 'error'
40
40
  });
41
41
  }
@@ -157,7 +157,7 @@ function BucketNotificationFormPage() {
157
157
  navigate(`/buckets/${bucketName}?tab=notification`);
158
158
  },
159
159
  onError: (error)=>{
160
- const errorMessage = error instanceof Error ? error.message : `Failed to ${isEditMode ? 'update' : 'create'} notification rule`;
160
+ const errorMessage = error.message;
161
161
  showToast({
162
162
  open: true,
163
163
  message: errorMessage,
@@ -181,7 +181,7 @@ function BucketNotificationFormPage() {
181
181
  })
182
182
  });
183
183
  if ('error' === notificationStatus && notificationError) {
184
- const errorMessage = notificationError instanceof Error ? notificationError.message : 'Failed to load notification configuration';
184
+ const errorMessage = notificationError.message;
185
185
  return /*#__PURE__*/ jsx(Form, {
186
186
  layout: {
187
187
  kind: 'page',
@@ -45,7 +45,7 @@ function BucketNotificationList({ bucketName, notificationRules, notificationSta
45
45
  });
46
46
  },
47
47
  onError: (error)=>{
48
- const errorMessage = error instanceof Error ? error.message : 'Failed to delete notification rule';
48
+ const errorMessage = error.message;
49
49
  showToast({
50
50
  open: true,
51
51
  message: errorMessage,
@@ -110,7 +110,7 @@ const DeleteObjectButton = ({ objects, bucketName, onDeleteSuccess })=>{
110
110
  } catch (error) {
111
111
  showToast({
112
112
  open: true,
113
- message: error instanceof Error ? error.message : 'Failed to delete objects',
113
+ message: error instanceof Error ? error.message : 'Unknown error',
114
114
  status: 'error'
115
115
  });
116
116
  } finally{
@@ -62,7 +62,7 @@ const GetPresignedUrlButton = ({ bucketName, objectKey, versionId })=>{
62
62
  } catch (error) {
63
63
  showToast({
64
64
  open: true,
65
- message: error instanceof Error ? error.message : 'Failed to generate presigned URL',
65
+ message: error instanceof Error ? error.message : 'Unknown error',
66
66
  status: 'error'
67
67
  });
68
68
  }
@@ -160,7 +160,7 @@ const ObjectMetadata = ({ bucketName, objectKey, versionId })=>{
160
160
  status: 'success'
161
161
  });
162
162
  } catch (error) {
163
- const errorMessage = error instanceof Error ? error.message : 'Failed to save metadata';
163
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
164
164
  showToast({
165
165
  open: true,
166
166
  message: errorMessage,