@scality/data-browser-library 1.0.0-preview.7 → 1.0.0-preview.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/components/__tests__/BucketCreate.test.d.ts +1 -0
  2. package/dist/components/__tests__/BucketCreate.test.js +408 -0
  3. package/dist/components/__tests__/BucketLifecycleFormPage.test.d.ts +1 -0
  4. package/dist/components/__tests__/BucketLifecycleFormPage.test.js +618 -0
  5. package/dist/components/__tests__/BucketLifecycleList.test.d.ts +1 -0
  6. package/dist/components/__tests__/BucketLifecycleList.test.js +325 -0
  7. package/dist/components/__tests__/BucketList.test.js +190 -0
  8. package/dist/components/__tests__/BucketOverview.test.js +298 -8
  9. package/dist/components/__tests__/BucketReplicationFormPage.test.d.ts +1 -0
  10. package/dist/components/__tests__/BucketReplicationFormPage.test.js +1757 -0
  11. package/dist/components/__tests__/BucketReplicationList.test.d.ts +1 -0
  12. package/dist/components/__tests__/BucketReplicationList.test.js +344 -0
  13. package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.d.ts +1 -0
  14. package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.js +196 -0
  15. package/dist/components/__tests__/EmptyBucketButton.test.d.ts +1 -0
  16. package/dist/components/__tests__/EmptyBucketButton.test.js +302 -0
  17. package/dist/components/buckets/BucketCreate.d.ts +49 -0
  18. package/dist/components/buckets/BucketCreate.js +237 -0
  19. package/dist/components/buckets/BucketDetails.js +62 -10
  20. package/dist/components/buckets/BucketLifecycleFormPage.d.ts +15 -0
  21. package/dist/components/buckets/BucketLifecycleFormPage.js +1070 -0
  22. package/dist/components/buckets/BucketLifecycleList.d.ts +10 -0
  23. package/dist/components/buckets/BucketLifecycleList.js +270 -0
  24. package/dist/components/buckets/BucketList.d.ts +5 -2
  25. package/dist/components/buckets/BucketList.js +38 -28
  26. package/dist/components/buckets/BucketOverview.d.ts +65 -4
  27. package/dist/components/buckets/BucketOverview.js +261 -179
  28. package/dist/components/buckets/BucketPage.js +1 -1
  29. package/dist/components/buckets/BucketReplicationFormPage.d.ts +1 -0
  30. package/dist/components/buckets/BucketReplicationFormPage.js +834 -0
  31. package/dist/components/buckets/BucketReplicationList.d.ts +11 -0
  32. package/dist/components/buckets/BucketReplicationList.js +189 -0
  33. package/dist/components/buckets/DeleteBucketConfigRuleButton.d.ts +18 -0
  34. package/dist/components/buckets/DeleteBucketConfigRuleButton.js +53 -0
  35. package/dist/components/buckets/EmptyBucketButton.d.ts +5 -0
  36. package/dist/components/buckets/EmptyBucketButton.js +232 -0
  37. package/dist/components/buckets/EmptyBucketSummary.d.ts +9 -0
  38. package/dist/components/buckets/EmptyBucketSummary.js +60 -0
  39. package/dist/components/buckets/EmptyBucketSummaryList.d.ts +13 -0
  40. package/dist/components/buckets/EmptyBucketSummaryList.js +140 -0
  41. package/dist/components/buckets/notifications/BucketNotificationCreatePage.js +8 -8
  42. package/dist/components/index.d.ts +8 -1
  43. package/dist/components/index.js +9 -2
  44. package/dist/components/objects/ObjectLock/EditRetentionButton.d.ts +4 -0
  45. package/dist/components/objects/ObjectLock/EditRetentionButton.js +32 -0
  46. package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.d.ts +3 -0
  47. package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.js +211 -0
  48. package/dist/components/objects/ObjectLock/ObjectLockSettings.d.ts +9 -0
  49. package/dist/components/objects/ObjectLock/ObjectLockSettings.js +158 -0
  50. package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.d.ts +8 -0
  51. package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.js +39 -0
  52. package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.d.ts +1 -0
  53. package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.js +204 -0
  54. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.d.ts +1 -0
  55. package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +374 -0
  56. package/dist/components/ui/ArrayFieldActions.d.ts +36 -0
  57. package/dist/components/ui/ArrayFieldActions.js +38 -0
  58. package/dist/components/ui/ConfirmDeleteRuleModal.d.ts +16 -0
  59. package/dist/components/ui/ConfirmDeleteRuleModal.js +43 -0
  60. package/dist/components/ui/FilterFormSection.d.ts +44 -0
  61. package/dist/components/ui/FilterFormSection.js +159 -0
  62. package/dist/config/factory.d.ts +13 -2
  63. package/dist/config/factory.js +9 -6
  64. package/dist/hooks/__tests__/useISVBucketDetection.test.d.ts +1 -0
  65. package/dist/hooks/__tests__/useISVBucketDetection.test.js +188 -0
  66. package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +44 -1
  67. package/dist/hooks/factories/useCreateS3QueryHook.js +22 -1
  68. package/dist/hooks/index.d.ts +4 -0
  69. package/dist/hooks/index.js +5 -1
  70. package/dist/hooks/useDeleteBucketConfigRule.d.ts +26 -0
  71. package/dist/hooks/useDeleteBucketConfigRule.js +46 -0
  72. package/dist/hooks/useEmptyBucket.d.ts +27 -0
  73. package/dist/hooks/useEmptyBucket.js +116 -0
  74. package/dist/hooks/useISVBucketDetection.d.ts +15 -0
  75. package/dist/hooks/useISVBucketDetection.js +27 -0
  76. package/dist/hooks/useTableRowSelection.d.ts +9 -0
  77. package/dist/hooks/useTableRowSelection.js +45 -0
  78. package/dist/test/setup.js +8 -0
  79. package/dist/test/testUtils.d.ts +99 -17
  80. package/dist/test/testUtils.js +64 -16
  81. package/dist/test/utils/errorHandling.test.js +39 -1
  82. package/dist/utils/constants.d.ts +12 -0
  83. package/dist/utils/constants.js +9 -0
  84. package/dist/utils/errorHandling.d.ts +9 -0
  85. package/dist/utils/errorHandling.js +6 -1
  86. package/dist/utils/index.d.ts +2 -0
  87. package/dist/utils/index.js +2 -0
  88. package/dist/utils/s3RuleUtils.d.ts +53 -0
  89. package/dist/utils/s3RuleUtils.js +101 -0
  90. package/package.json +1 -1
@@ -0,0 +1,834 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { joiResolver } from "@hookform/resolvers/joi";
3
+ import { Form, FormGroup, FormSection, Icon, Loader, Stack, Text, Toggle, spacing, useToast } from "@scality/core-ui";
4
+ import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
5
+ import { Box, Button, Input, Select } from "@scality/core-ui/dist/next";
6
+ import joi from "joi";
7
+ import { useCallback, useEffect, useMemo, useRef } from "react";
8
+ import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form";
9
+ import { useNavigate, useParams } from "react-router-dom";
10
+ import { useGetBucketReplication, useSetBucketReplication } from "../../hooks/bucketConfiguration.js";
11
+ import { useBuckets } from "../../hooks/bucketOperations.js";
12
+ import { FilterFormSection, createFilterValidationSchema } from "../ui/FilterFormSection.js";
13
+ import { AWS_RULE_LIMITS, STATUS_OPTIONS, buildS3Filter } from "../../utils/s3RuleUtils.js";
14
+ const storageClassOptions = [
15
+ {
16
+ value: "",
17
+ label: "Same as source"
18
+ },
19
+ {
20
+ value: "GLACIER",
21
+ label: "Glacier"
22
+ },
23
+ {
24
+ value: "DEEP_ARCHIVE",
25
+ label: "Glacier Deep Archive"
26
+ },
27
+ {
28
+ value: "STANDARD_IA",
29
+ label: "Standard-IA"
30
+ },
31
+ {
32
+ value: "ONEZONE_IA",
33
+ label: "One Zone-IA"
34
+ },
35
+ {
36
+ value: "INTELLIGENT_TIERING",
37
+ label: "Intelligent-Tiering"
38
+ },
39
+ {
40
+ value: "GLACIER_IR",
41
+ label: "Glacier Instant Retrieval"
42
+ }
43
+ ];
44
+ const createSchema = (hasExistingRules)=>joi.object({
45
+ role: hasExistingRules ? joi.string().optional() : joi.string().required().messages({
46
+ "string.empty": "Role ARN is required for first replication rule"
47
+ }),
48
+ ruleId: joi.string().required().max(AWS_RULE_LIMITS.RULE_ID_MAX_LENGTH).messages({
49
+ "string.empty": "Rule ID is required",
50
+ "string.max": `Rule ID must not exceed ${AWS_RULE_LIMITS.RULE_ID_MAX_LENGTH} characters`
51
+ }),
52
+ status: joi.string().valid(...STATUS_OPTIONS.map((o)=>o.value)).required(),
53
+ priority: joi.number().integer().min(0).allow(null).optional().messages({
54
+ "number.base": "Priority must be a number",
55
+ "number.min": "Priority must be at least 0"
56
+ }),
57
+ ...createFilterValidationSchema(joi),
58
+ includeEncryptedObjects: joi.boolean(),
59
+ replicaModifications: joi.boolean(),
60
+ sameAccount: joi.boolean(),
61
+ targetBucket: joi.string().required().messages({
62
+ "string.empty": "Target bucket is required"
63
+ }),
64
+ targetAccountId: joi.when("sameAccount", {
65
+ is: false,
66
+ then: joi.string().required().messages({
67
+ "string.empty": "Target account ID is required for cross-account replication"
68
+ }),
69
+ otherwise: joi.string().allow("").optional()
70
+ }),
71
+ storageClass: joi.string().allow("").optional(),
72
+ encryptReplicatedObjects: joi.boolean().custom((value, helpers)=>{
73
+ const { includeEncryptedObjects } = helpers.state.ancestors[0];
74
+ if (includeEncryptedObjects && !value) return helpers.error("any.invalid", {
75
+ message: "Encryption must be enabled when replicating encrypted objects"
76
+ });
77
+ return value;
78
+ }),
79
+ replicaKmsKeyId: joi.when("encryptReplicatedObjects", {
80
+ is: true,
81
+ then: joi.string().required().messages({
82
+ "string.empty": "Replica KMS Key ID is required when encrypting replicated objects"
83
+ }),
84
+ otherwise: joi.string().allow("").optional()
85
+ }),
86
+ enforceRTC: joi.boolean(),
87
+ enableRTCNotification: joi.when("enforceRTC", {
88
+ is: true,
89
+ then: joi.boolean().valid(true).messages({
90
+ "any.only": "Metrics must be enabled when RTC is enabled"
91
+ }),
92
+ otherwise: joi.boolean()
93
+ }),
94
+ deleteMarkerReplication: joi.boolean(),
95
+ switchObjectOwnership: joi.boolean()
96
+ });
97
+ const ruleToFormValues = (rule, role)=>{
98
+ const formValues = {
99
+ role,
100
+ ruleId: rule.ID || "",
101
+ status: rule.Status || "Enabled",
102
+ priority: rule.Priority ?? null,
103
+ filterType: "none",
104
+ prefix: "",
105
+ tags: [],
106
+ includeEncryptedObjects: false,
107
+ replicaModifications: false,
108
+ sameAccount: true,
109
+ targetBucket: "",
110
+ targetAccountId: "",
111
+ storageClass: "",
112
+ encryptReplicatedObjects: false,
113
+ replicaKmsKeyId: "",
114
+ enforceRTC: false,
115
+ enableRTCNotification: false,
116
+ deleteMarkerReplication: false,
117
+ switchObjectOwnership: false
118
+ };
119
+ if (void 0 !== rule.Prefix && "" !== rule.Prefix) {
120
+ formValues.filterType = "prefix";
121
+ formValues.prefix = rule.Prefix;
122
+ } else if (rule.Filter) {
123
+ if (rule.Filter.And) {
124
+ formValues.filterType = "and";
125
+ formValues.prefix = rule.Filter.And.Prefix || "";
126
+ formValues.tags = rule.Filter.And.Tags?.map((tag)=>({
127
+ key: tag.Key || "",
128
+ value: tag.Value || ""
129
+ })) || [];
130
+ } else if (void 0 !== rule.Filter.Prefix) {
131
+ formValues.filterType = "prefix";
132
+ formValues.prefix = rule.Filter.Prefix;
133
+ } else if (rule.Filter.Tag) {
134
+ formValues.filterType = "tags";
135
+ formValues.tags = [
136
+ {
137
+ key: rule.Filter.Tag.Key || "",
138
+ value: rule.Filter.Tag.Value || ""
139
+ }
140
+ ];
141
+ }
142
+ }
143
+ if (rule.SourceSelectionCriteria) {
144
+ formValues.includeEncryptedObjects = rule.SourceSelectionCriteria.SseKmsEncryptedObjects?.Status === "Enabled";
145
+ formValues.replicaModifications = rule.SourceSelectionCriteria.ReplicaModifications?.Status === "Enabled";
146
+ }
147
+ if (rule.Destination) {
148
+ const bucketArn = rule.Destination.Bucket || "";
149
+ const parts = bucketArn.split(":::");
150
+ formValues.targetBucket = parts[parts.length - 1] || bucketArn;
151
+ if (rule.Destination.Account) {
152
+ formValues.sameAccount = false;
153
+ formValues.targetAccountId = rule.Destination.Account;
154
+ } else formValues.sameAccount = true;
155
+ formValues.storageClass = rule.Destination.StorageClass || "";
156
+ formValues.switchObjectOwnership = rule.Destination.AccessControlTranslation?.Owner === "Destination";
157
+ if (rule.Destination.ReplicationTime) formValues.enforceRTC = "Enabled" === rule.Destination.ReplicationTime.Status;
158
+ if (rule.Destination.Metrics) formValues.enableRTCNotification = "Enabled" === rule.Destination.Metrics.Status;
159
+ if (rule.Destination.EncryptionConfiguration) {
160
+ formValues.encryptReplicatedObjects = true;
161
+ formValues.replicaKmsKeyId = rule.Destination.EncryptionConfiguration.ReplicaKmsKeyID || "";
162
+ }
163
+ }
164
+ if (rule.DeleteMarkerReplication) formValues.deleteMarkerReplication = "Enabled" === rule.DeleteMarkerReplication.Status;
165
+ return formValues;
166
+ };
167
+ const ToggleFormField = ({ name, label, id, control, labelHelpTooltip })=>/*#__PURE__*/ jsx(FormGroup, {
168
+ label: label,
169
+ id: id,
170
+ direction: "horizontal",
171
+ labelHelpTooltip: labelHelpTooltip,
172
+ content: /*#__PURE__*/ jsx(Controller, {
173
+ name: name,
174
+ control: control,
175
+ render: ({ field })=>/*#__PURE__*/ jsx(Toggle, {
176
+ toggle: field.value,
177
+ onChange: field.onChange,
178
+ label: field.value ? "Enabled" : "Disabled"
179
+ })
180
+ })
181
+ });
182
+ const buildDestination = (data)=>({
183
+ Bucket: `arn:aws:s3:::${data.targetBucket}`,
184
+ ...!data.sameAccount && data.targetAccountId && "" !== data.targetAccountId.trim() && {
185
+ Account: data.targetAccountId
186
+ },
187
+ ...data.storageClass && "" !== data.storageClass.trim() && {
188
+ StorageClass: data.storageClass
189
+ },
190
+ ...(data.enforceRTC || data.enableRTCNotification) && {
191
+ ReplicationTime: {
192
+ Status: data.enforceRTC ? "Enabled" : "Disabled",
193
+ Time: {
194
+ Minutes: 15
195
+ }
196
+ },
197
+ Metrics: {
198
+ Status: "Enabled",
199
+ ...data.enforceRTC && {
200
+ EventThreshold: {
201
+ Minutes: 15
202
+ }
203
+ }
204
+ }
205
+ },
206
+ ...data.encryptReplicatedObjects && data.replicaKmsKeyId && "" !== data.replicaKmsKeyId.trim() && {
207
+ EncryptionConfiguration: {
208
+ ReplicaKmsKeyID: data.replicaKmsKeyId
209
+ }
210
+ },
211
+ ...!data.sameAccount && data.switchObjectOwnership && data.targetAccountId && "" !== data.targetAccountId.trim() && {
212
+ AccessControlTranslation: {
213
+ Owner: "Destination"
214
+ }
215
+ }
216
+ });
217
+ const buildSourceSelectionCriteria = (data)=>{
218
+ if (data.includeEncryptedObjects || data.replicaModifications) return {
219
+ ...data.includeEncryptedObjects && {
220
+ SseKmsEncryptedObjects: {
221
+ Status: "Enabled"
222
+ }
223
+ },
224
+ ...data.replicaModifications && {
225
+ ReplicaModifications: {
226
+ Status: "Enabled"
227
+ }
228
+ }
229
+ };
230
+ };
231
+ const buildReplicationRule = (data)=>{
232
+ const rule = {
233
+ ID: data.ruleId,
234
+ Status: data.status,
235
+ Priority: data.priority ?? 0,
236
+ Destination: buildDestination(data)
237
+ };
238
+ const filter = buildS3Filter(data);
239
+ if (filter) rule.Filter = filter;
240
+ const sourceSelectionCriteria = buildSourceSelectionCriteria(data);
241
+ if (sourceSelectionCriteria) rule.SourceSelectionCriteria = sourceSelectionCriteria;
242
+ rule.DeleteMarkerReplication = {
243
+ Status: data.deleteMarkerReplication ? "Enabled" : "Disabled"
244
+ };
245
+ return rule;
246
+ };
247
+ function BucketReplicationFormPage() {
248
+ const { bucketName, ruleId } = useParams();
249
+ const navigate = useNavigate();
250
+ const { showToast } = useToast();
251
+ const isEditMode = !!ruleId;
252
+ const { data: replicationData, status: replicationStatus } = useGetBucketReplication({
253
+ Bucket: bucketName
254
+ });
255
+ const { data: bucketsData } = useBuckets();
256
+ const availableBuckets = bucketsData?.Buckets || [];
257
+ const existingRole = replicationData?.ReplicationConfiguration?.Role || "";
258
+ const hasExistingRules = (replicationData?.ReplicationConfiguration?.Rules || []).length > 0;
259
+ const existingRule = useMemo(()=>{
260
+ if (!isEditMode || !replicationData?.ReplicationConfiguration?.Rules || !ruleId) return null;
261
+ return replicationData.ReplicationConfiguration.Rules.find((rule)=>rule.ID === decodeURIComponent(ruleId)) || null;
262
+ }, [
263
+ isEditMode,
264
+ replicationData,
265
+ ruleId
266
+ ]);
267
+ const existingRuleIds = useMemo(()=>(replicationData?.ReplicationConfiguration?.Rules || []).map((rule)=>rule.ID).filter(Boolean).filter((id)=>!isEditMode || id !== existingRule?.ID), [
268
+ replicationData,
269
+ isEditMode,
270
+ existingRule
271
+ ]);
272
+ const nextAvailablePriority = useMemo(()=>{
273
+ const existingRules = replicationData?.ReplicationConfiguration?.Rules || [];
274
+ if (0 === existingRules.length) return 0;
275
+ const priorities = existingRules.map((rule)=>rule.Priority).filter((p)=>null != p);
276
+ if (0 === priorities.length) return 0;
277
+ return Math.max(...priorities) + 1;
278
+ }, [
279
+ replicationData
280
+ ]);
281
+ const dynamicSchema = useMemo(()=>createSchema(hasExistingRules).keys({
282
+ ruleId: joi.string().required().invalid(...existingRuleIds).messages({
283
+ "string.empty": "Rule ID is required",
284
+ "any.invalid": "A rule with this ID already exists"
285
+ })
286
+ }), [
287
+ existingRuleIds,
288
+ hasExistingRules
289
+ ]);
290
+ const methods = useForm({
291
+ resolver: joiResolver(dynamicSchema),
292
+ mode: "onChange",
293
+ defaultValues: {
294
+ role: "",
295
+ ruleId: "",
296
+ status: "Enabled",
297
+ priority: null,
298
+ filterType: "none",
299
+ prefix: "",
300
+ tags: [],
301
+ includeEncryptedObjects: false,
302
+ replicaModifications: false,
303
+ sameAccount: true,
304
+ targetBucket: "",
305
+ targetAccountId: "",
306
+ storageClass: "",
307
+ encryptReplicatedObjects: false,
308
+ replicaKmsKeyId: "",
309
+ enforceRTC: false,
310
+ enableRTCNotification: false,
311
+ deleteMarkerReplication: false,
312
+ switchObjectOwnership: false
313
+ }
314
+ });
315
+ const { handleSubmit, register, control, watch, reset, formState: { isValid, isDirty, errors } } = methods;
316
+ const { fields: tagFields, append: appendTag, remove: removeTag } = useFieldArray({
317
+ control,
318
+ name: "tags"
319
+ });
320
+ const filterType = watch("filterType");
321
+ const sameAccount = watch("sameAccount");
322
+ const encryptReplicatedObjects = watch("encryptReplicatedObjects");
323
+ const includeEncryptedObjects = watch("includeEncryptedObjects");
324
+ const deleteMarkerReplication = watch("deleteMarkerReplication");
325
+ const enforceRTC = watch("enforceRTC");
326
+ const { mutate: setReplication, isPending: isSaving } = useSetBucketReplication();
327
+ const loadedRuleIdRef = useRef(null);
328
+ useEffect(()=>{
329
+ if (isEditMode && existingRule) {
330
+ if (loadedRuleIdRef.current !== existingRule.ID) {
331
+ const formValues = ruleToFormValues(existingRule, existingRole);
332
+ reset(formValues);
333
+ loadedRuleIdRef.current = existingRule.ID || null;
334
+ }
335
+ } else if (!isEditMode && !isDirty) {
336
+ reset((prev)=>({
337
+ ...prev,
338
+ role: existingRole,
339
+ priority: nextAvailablePriority
340
+ }));
341
+ loadedRuleIdRef.current = null;
342
+ }
343
+ }, [
344
+ isEditMode,
345
+ existingRule,
346
+ existingRole,
347
+ nextAvailablePriority,
348
+ isDirty,
349
+ reset
350
+ ]);
351
+ const prevFilterTypeRef = useRef();
352
+ useEffect(()=>{
353
+ const prevFilterType = prevFilterTypeRef.current;
354
+ prevFilterTypeRef.current = filterType;
355
+ if (("tags" === filterType || "and" === filterType) && 0 === tagFields.length) {
356
+ if (!isEditMode || void 0 !== prevFilterType && prevFilterType !== filterType) appendTag({
357
+ key: "",
358
+ value: ""
359
+ });
360
+ }
361
+ if (void 0 !== prevFilterType && prevFilterType !== filterType) {
362
+ if (("none" === filterType || "prefix" === filterType) && tagFields.length > 0) tagFields.forEach((_, index)=>{
363
+ removeTag(tagFields.length - 1 - index);
364
+ });
365
+ if ("none" === filterType) methods.setValue("prefix", "", {
366
+ shouldDirty: true
367
+ });
368
+ }
369
+ }, [
370
+ isEditMode,
371
+ filterType,
372
+ tagFields,
373
+ appendTag,
374
+ removeTag,
375
+ methods
376
+ ]);
377
+ useEffect(()=>{
378
+ if (includeEncryptedObjects && !encryptReplicatedObjects) methods.setValue("encryptReplicatedObjects", true, {
379
+ shouldValidate: true
380
+ });
381
+ }, [
382
+ includeEncryptedObjects,
383
+ encryptReplicatedObjects,
384
+ methods
385
+ ]);
386
+ useEffect(()=>{
387
+ if (("tags" === filterType || "and" === filterType) && deleteMarkerReplication) methods.setValue("deleteMarkerReplication", false, {
388
+ shouldValidate: true
389
+ });
390
+ }, [
391
+ filterType,
392
+ deleteMarkerReplication,
393
+ methods
394
+ ]);
395
+ useEffect(()=>{
396
+ if (enforceRTC) methods.setValue("enableRTCNotification", true, {
397
+ shouldValidate: true
398
+ });
399
+ }, [
400
+ enforceRTC,
401
+ methods
402
+ ]);
403
+ const handleCancel = useCallback(()=>{
404
+ navigate(`/buckets/${bucketName}?tab=replication`);
405
+ }, [
406
+ navigate,
407
+ bucketName
408
+ ]);
409
+ const onSubmit = useCallback((data)=>{
410
+ if (!bucketName) return;
411
+ const rule = buildReplicationRule(data);
412
+ const existingRules = replicationData?.ReplicationConfiguration?.Rules || [];
413
+ const updatedRules = isEditMode ? existingRules.map((r)=>r.ID === existingRule?.ID ? rule : r) : [
414
+ ...existingRules,
415
+ rule
416
+ ];
417
+ setReplication({
418
+ Bucket: bucketName,
419
+ ReplicationConfiguration: {
420
+ Role: data.role,
421
+ Rules: updatedRules
422
+ }
423
+ }, {
424
+ onSuccess: ()=>{
425
+ showToast({
426
+ open: true,
427
+ message: `Replication rule ${isEditMode ? "updated" : "created"} successfully`,
428
+ status: "success"
429
+ });
430
+ navigate(`/buckets/${bucketName}?tab=replication`);
431
+ },
432
+ onError: (error)=>{
433
+ const errorMessage = error instanceof Error ? error.message : `Failed to ${isEditMode ? "update" : "create"} replication rule`;
434
+ showToast({
435
+ open: true,
436
+ message: errorMessage,
437
+ status: "error"
438
+ });
439
+ }
440
+ });
441
+ }, [
442
+ bucketName,
443
+ setReplication,
444
+ navigate,
445
+ showToast,
446
+ replicationData,
447
+ isEditMode,
448
+ existingRule
449
+ ]);
450
+ if ("pending" === replicationStatus) return /*#__PURE__*/ jsx(Loader, {
451
+ centered: true,
452
+ children: /*#__PURE__*/ jsx(Text, {
453
+ children: "Loading..."
454
+ })
455
+ });
456
+ if (isEditMode && !existingRule) return /*#__PURE__*/ jsx(Box, {
457
+ padding: spacing.r16,
458
+ children: /*#__PURE__*/ jsx(Text, {
459
+ color: "statusCritical",
460
+ children: "Rule not found"
461
+ })
462
+ });
463
+ return /*#__PURE__*/ jsx(FormProvider, {
464
+ ...methods,
465
+ children: /*#__PURE__*/ jsxs(Form, {
466
+ layout: {
467
+ kind: "page",
468
+ title: isEditMode ? "Edit Replication Rule" : "Create Replication Rule"
469
+ },
470
+ requireMode: "partial",
471
+ onSubmit: handleSubmit(onSubmit),
472
+ rightActions: /*#__PURE__*/ jsxs(Stack, {
473
+ gap: "r16",
474
+ children: [
475
+ /*#__PURE__*/ jsx(Button, {
476
+ id: "cancel-btn",
477
+ variant: "outline",
478
+ type: "button",
479
+ label: "Cancel",
480
+ onClick: handleCancel,
481
+ disabled: isSaving
482
+ }),
483
+ /*#__PURE__*/ jsx(Button, {
484
+ id: isEditMode ? "save-replication-btn" : "create-replication-btn",
485
+ type: "submit",
486
+ variant: "primary",
487
+ label: isEditMode ? "Save" : "Create",
488
+ icon: isEditMode ? /*#__PURE__*/ jsx(Icon, {
489
+ name: "Save"
490
+ }) : void 0,
491
+ disabled: !isDirty || !isValid || isSaving
492
+ })
493
+ ]
494
+ }),
495
+ children: [
496
+ /*#__PURE__*/ jsx(FormSection, {
497
+ title: {
498
+ name: "Role"
499
+ },
500
+ forceLabelWidth: convertRemToPixels(15),
501
+ children: /*#__PURE__*/ jsx(FormGroup, {
502
+ label: "Role ARN",
503
+ id: "role",
504
+ direction: "horizontal",
505
+ error: errors?.role?.message,
506
+ helpErrorPosition: "bottom",
507
+ required: !hasExistingRules,
508
+ content: hasExistingRules ? /*#__PURE__*/ jsxs(Stack, {
509
+ direction: "vertical",
510
+ gap: "r8",
511
+ children: [
512
+ /*#__PURE__*/ jsx(Text, {
513
+ children: existingRole || "Not set"
514
+ }),
515
+ /*#__PURE__*/ jsx(Text, {
516
+ color: "textSecondary",
517
+ children: "To change the Role, edit it through bucket configuration or delete all rules first"
518
+ })
519
+ ]
520
+ }) : /*#__PURE__*/ jsx(Input, {
521
+ id: "role",
522
+ placeholder: "arn:aws:iam::123456789012:role/replication-role",
523
+ ...register("role")
524
+ })
525
+ })
526
+ }),
527
+ /*#__PURE__*/ jsxs(FormSection, {
528
+ title: {
529
+ name: "Rule Scope"
530
+ },
531
+ forceLabelWidth: convertRemToPixels(15),
532
+ children: [
533
+ /*#__PURE__*/ jsx(FormGroup, {
534
+ label: "Rule ID / Name",
535
+ id: "ruleId",
536
+ direction: "horizontal",
537
+ error: errors?.ruleId?.message,
538
+ helpErrorPosition: "bottom",
539
+ required: true,
540
+ content: isEditMode ? /*#__PURE__*/ jsx(Text, {
541
+ children: watch("ruleId")
542
+ }) : /*#__PURE__*/ jsx(Input, {
543
+ id: "ruleId",
544
+ ...register("ruleId")
545
+ })
546
+ }),
547
+ /*#__PURE__*/ jsx(FormGroup, {
548
+ label: "Status",
549
+ id: "status",
550
+ direction: "horizontal",
551
+ error: errors?.status?.message,
552
+ helpErrorPosition: "bottom",
553
+ required: true,
554
+ content: /*#__PURE__*/ jsx(Controller, {
555
+ name: "status",
556
+ control: control,
557
+ render: ({ field })=>/*#__PURE__*/ jsx(Select, {
558
+ id: "status",
559
+ value: field.value,
560
+ onChange: field.onChange,
561
+ children: STATUS_OPTIONS.map((option)=>/*#__PURE__*/ jsx(Select.Option, {
562
+ value: option.value,
563
+ children: option.label
564
+ }, option.value))
565
+ })
566
+ })
567
+ }),
568
+ /*#__PURE__*/ jsx(FormGroup, {
569
+ label: "Rule priority",
570
+ id: "priority",
571
+ direction: "horizontal",
572
+ error: errors?.priority?.message,
573
+ helpErrorPosition: "bottom",
574
+ labelHelpTooltip: "Lower numbers have higher priority. Rules are evaluated in priority order when filters overlap.",
575
+ content: /*#__PURE__*/ jsx(Box, {
576
+ flex: "1",
577
+ children: /*#__PURE__*/ jsx(Input, {
578
+ type: "number",
579
+ id: "priority",
580
+ placeholder: isEditMode ? void 0 : `Auto-assigned: ${nextAvailablePriority}`,
581
+ ...register("priority", {
582
+ setValueAs: (v)=>{
583
+ if ("" === v || null == v) return null;
584
+ const parsed = parseInt(v, 10);
585
+ return isNaN(parsed) ? null : parsed;
586
+ }
587
+ })
588
+ })
589
+ })
590
+ })
591
+ ]
592
+ }),
593
+ /*#__PURE__*/ jsx(FilterFormSection, {
594
+ filterType: filterType,
595
+ onFilterTypeChange: (value)=>methods.setValue("filterType", value),
596
+ prefixRegister: register("prefix"),
597
+ tagFields: tagFields,
598
+ tagKeyRegister: (index)=>register(`tags.${index}.key`),
599
+ tagValueRegister: (index)=>register(`tags.${index}.value`),
600
+ getTagKeyValue: (index)=>watch(`tags.${index}.key`),
601
+ getTagValueValue: (index)=>watch(`tags.${index}.value`),
602
+ appendTag: appendTag,
603
+ removeTag: removeTag,
604
+ errors: errors
605
+ }),
606
+ /*#__PURE__*/ jsxs(FormSection, {
607
+ title: {
608
+ name: "Destination"
609
+ },
610
+ forceLabelWidth: convertRemToPixels(15),
611
+ children: [
612
+ /*#__PURE__*/ jsx(FormGroup, {
613
+ label: "Destination",
614
+ id: "sameAccount",
615
+ direction: "horizontal",
616
+ content: /*#__PURE__*/ jsx(Controller, {
617
+ name: "sameAccount",
618
+ control: control,
619
+ render: ({ field })=>/*#__PURE__*/ jsx(Toggle, {
620
+ toggle: field.value,
621
+ onChange: field.onChange,
622
+ label: field.value ? "Same account destination" : "Not same account destination"
623
+ })
624
+ })
625
+ }),
626
+ sameAccount ? /*#__PURE__*/ jsx(Fragment, {}) : /*#__PURE__*/ jsx(FormGroup, {
627
+ label: "Target account ID",
628
+ id: "targetAccountId",
629
+ direction: "horizontal",
630
+ error: errors?.targetAccountId?.message,
631
+ helpErrorPosition: "bottom",
632
+ required: true,
633
+ content: /*#__PURE__*/ jsx(Input, {
634
+ id: "targetAccountId",
635
+ placeholder: "Account ID",
636
+ ...register("targetAccountId")
637
+ })
638
+ }),
639
+ /*#__PURE__*/ jsx(FormGroup, {
640
+ label: "Target Bucket",
641
+ id: "targetBucket",
642
+ direction: "horizontal",
643
+ error: errors?.targetBucket?.message,
644
+ helpErrorPosition: "bottom",
645
+ required: true,
646
+ content: sameAccount ? /*#__PURE__*/ jsx(Controller, {
647
+ name: "targetBucket",
648
+ control: control,
649
+ render: ({ field })=>/*#__PURE__*/ jsx(Select, {
650
+ id: "targetBucket",
651
+ value: field.value,
652
+ onChange: field.onChange,
653
+ placeholder: "Select a bucket",
654
+ children: availableBuckets.filter((bucket)=>bucket.Name !== bucketName).map((bucket)=>/*#__PURE__*/ jsx(Select.Option, {
655
+ value: bucket.Name || "",
656
+ children: bucket.Name
657
+ }, bucket.Name))
658
+ })
659
+ }) : /*#__PURE__*/ jsx(Input, {
660
+ id: "targetBucket",
661
+ placeholder: "Bucket name",
662
+ ...register("targetBucket")
663
+ })
664
+ }),
665
+ /*#__PURE__*/ jsx(FormGroup, {
666
+ label: "Storage Class",
667
+ id: "storageClass",
668
+ direction: "horizontal",
669
+ error: errors?.storageClass?.message,
670
+ helpErrorPosition: "bottom",
671
+ labelHelpTooltip: "Storage class for replicated objects in destination bucket",
672
+ content: /*#__PURE__*/ jsx(Controller, {
673
+ name: "storageClass",
674
+ control: control,
675
+ render: ({ field })=>/*#__PURE__*/ jsx(Select, {
676
+ id: "storageClass",
677
+ value: field.value,
678
+ onChange: field.onChange,
679
+ children: storageClassOptions.map((option)=>/*#__PURE__*/ jsx(Select.Option, {
680
+ value: option.value,
681
+ children: option.label
682
+ }, option.value))
683
+ })
684
+ })
685
+ }),
686
+ sameAccount ? /*#__PURE__*/ jsx(Fragment, {}) : /*#__PURE__*/ jsx(ToggleFormField, {
687
+ name: "switchObjectOwnership",
688
+ label: "Switch Object ownership",
689
+ id: "switchObjectOwnership",
690
+ control: control,
691
+ labelHelpTooltip: "Change replica ownership to destination bucket owner (AccessControlTranslation)"
692
+ })
693
+ ]
694
+ }),
695
+ /*#__PURE__*/ jsxs(FormSection, {
696
+ title: {
697
+ name: "Encryption"
698
+ },
699
+ forceLabelWidth: convertRemToPixels(15),
700
+ children: [
701
+ /*#__PURE__*/ jsx(ToggleFormField, {
702
+ name: "includeEncryptedObjects",
703
+ label: "Replicate encrypted objects",
704
+ id: "includeEncryptedObjects",
705
+ control: control,
706
+ labelHelpTooltip: "Replicate objects encrypted with KMS from source bucket"
707
+ }),
708
+ /*#__PURE__*/ jsx(FormGroup, {
709
+ label: "Encrypt replicated objects",
710
+ id: "encryptReplicatedObjects",
711
+ direction: "horizontal",
712
+ error: errors?.encryptReplicatedObjects?.message,
713
+ helpErrorPosition: "bottom",
714
+ labelHelpTooltip: "Encrypt objects in destination bucket using KMS",
715
+ required: includeEncryptedObjects,
716
+ content: /*#__PURE__*/ jsxs(Stack, {
717
+ direction: "vertical",
718
+ gap: "r8",
719
+ children: [
720
+ /*#__PURE__*/ jsx(Controller, {
721
+ name: "encryptReplicatedObjects",
722
+ control: control,
723
+ render: ({ field })=>/*#__PURE__*/ jsx(Toggle, {
724
+ toggle: field.value,
725
+ onChange: field.onChange,
726
+ label: field.value ? "Enabled" : "Disabled",
727
+ disabled: includeEncryptedObjects
728
+ })
729
+ }),
730
+ includeEncryptedObjects && /*#__PURE__*/ jsx(Text, {
731
+ color: "textSecondary",
732
+ children: "Encryption is required when replicating encrypted objects"
733
+ })
734
+ ]
735
+ })
736
+ }),
737
+ encryptReplicatedObjects ? /*#__PURE__*/ jsx(FormGroup, {
738
+ label: "Replica KMS Key ID",
739
+ id: "replicaKmsKeyId",
740
+ direction: "horizontal",
741
+ error: errors?.replicaKmsKeyId?.message,
742
+ helpErrorPosition: "bottom",
743
+ labelHelpTooltip: "KMS key ID or ARN to use for encrypting replicated objects",
744
+ required: true,
745
+ content: /*#__PURE__*/ jsx(Input, {
746
+ id: "replicaKmsKeyId",
747
+ placeholder: "KMS Key ID or ARN",
748
+ ...register("replicaKmsKeyId")
749
+ })
750
+ }) : /*#__PURE__*/ jsx(Fragment, {})
751
+ ]
752
+ }),
753
+ /*#__PURE__*/ jsxs(FormSection, {
754
+ title: {
755
+ name: "Additional Options"
756
+ },
757
+ forceLabelWidth: convertRemToPixels(15),
758
+ children: [
759
+ /*#__PURE__*/ jsx(ToggleFormField, {
760
+ name: "enforceRTC",
761
+ label: "Replication Time Control (RTC)",
762
+ id: "enforceRTC",
763
+ control: control,
764
+ labelHelpTooltip: "Guarantees replication within 15 minutes with SLA monitoring"
765
+ }),
766
+ /*#__PURE__*/ jsx(FormGroup, {
767
+ label: "RTC metrics and notifications",
768
+ id: "enableRTCNotification",
769
+ direction: "horizontal",
770
+ labelHelpTooltip: "Emit S3 events when replication exceeds 15-minute RTC threshold",
771
+ required: enforceRTC,
772
+ content: /*#__PURE__*/ jsxs(Stack, {
773
+ direction: "vertical",
774
+ gap: "r8",
775
+ children: [
776
+ /*#__PURE__*/ jsx(Controller, {
777
+ name: "enableRTCNotification",
778
+ control: control,
779
+ render: ({ field })=>/*#__PURE__*/ jsx(Toggle, {
780
+ toggle: field.value,
781
+ onChange: field.onChange,
782
+ label: field.value ? "Enabled" : "Disabled",
783
+ disabled: enforceRTC
784
+ })
785
+ }),
786
+ enforceRTC && /*#__PURE__*/ jsx(Text, {
787
+ color: "textSecondary",
788
+ children: "Metrics must be enabled when RTC is enabled"
789
+ })
790
+ ]
791
+ })
792
+ }),
793
+ /*#__PURE__*/ jsx(ToggleFormField, {
794
+ name: "replicaModifications",
795
+ label: "Replica modification sync",
796
+ id: "replicaModifications",
797
+ control: control,
798
+ labelHelpTooltip: "Replicate metadata changes made to replica objects"
799
+ }),
800
+ /*#__PURE__*/ jsx(FormGroup, {
801
+ label: "Delete marker replication",
802
+ id: "deleteMarkerReplication",
803
+ direction: "horizontal",
804
+ error: errors?.deleteMarkerReplication?.message,
805
+ helpErrorPosition: "bottom",
806
+ labelHelpTooltip: "Replicate delete markers created when objects are deleted in versioned buckets",
807
+ content: /*#__PURE__*/ jsxs(Stack, {
808
+ direction: "vertical",
809
+ gap: "r8",
810
+ children: [
811
+ /*#__PURE__*/ jsx(Controller, {
812
+ name: "deleteMarkerReplication",
813
+ control: control,
814
+ render: ({ field })=>/*#__PURE__*/ jsx(Toggle, {
815
+ toggle: field.value,
816
+ onChange: field.onChange,
817
+ label: field.value ? "Enabled" : "Disabled",
818
+ disabled: "tags" === filterType || "and" === filterType
819
+ })
820
+ }),
821
+ ("tags" === filterType || "and" === filterType) && /*#__PURE__*/ jsx(Text, {
822
+ color: "textSecondary",
823
+ children: "Delete marker replication is not supported for tag-based filters"
824
+ })
825
+ ]
826
+ })
827
+ })
828
+ ]
829
+ })
830
+ ]
831
+ })
832
+ });
833
+ }
834
+ export { BucketReplicationFormPage };