@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.
- package/dist/components/__tests__/BucketCreate.test.d.ts +1 -0
- package/dist/components/__tests__/BucketCreate.test.js +408 -0
- package/dist/components/__tests__/BucketLifecycleFormPage.test.d.ts +1 -0
- package/dist/components/__tests__/BucketLifecycleFormPage.test.js +618 -0
- package/dist/components/__tests__/BucketLifecycleList.test.d.ts +1 -0
- package/dist/components/__tests__/BucketLifecycleList.test.js +325 -0
- package/dist/components/__tests__/BucketList.test.js +190 -0
- package/dist/components/__tests__/BucketOverview.test.js +298 -8
- package/dist/components/__tests__/BucketReplicationFormPage.test.d.ts +1 -0
- package/dist/components/__tests__/BucketReplicationFormPage.test.js +1757 -0
- package/dist/components/__tests__/BucketReplicationList.test.d.ts +1 -0
- package/dist/components/__tests__/BucketReplicationList.test.js +344 -0
- package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.d.ts +1 -0
- package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.js +196 -0
- package/dist/components/__tests__/EmptyBucketButton.test.d.ts +1 -0
- package/dist/components/__tests__/EmptyBucketButton.test.js +302 -0
- package/dist/components/buckets/BucketCreate.d.ts +49 -0
- package/dist/components/buckets/BucketCreate.js +237 -0
- package/dist/components/buckets/BucketDetails.js +62 -10
- package/dist/components/buckets/BucketLifecycleFormPage.d.ts +15 -0
- package/dist/components/buckets/BucketLifecycleFormPage.js +1070 -0
- package/dist/components/buckets/BucketLifecycleList.d.ts +10 -0
- package/dist/components/buckets/BucketLifecycleList.js +270 -0
- package/dist/components/buckets/BucketList.d.ts +5 -2
- package/dist/components/buckets/BucketList.js +38 -28
- package/dist/components/buckets/BucketOverview.d.ts +65 -4
- package/dist/components/buckets/BucketOverview.js +261 -179
- package/dist/components/buckets/BucketPage.js +1 -1
- package/dist/components/buckets/BucketReplicationFormPage.d.ts +1 -0
- package/dist/components/buckets/BucketReplicationFormPage.js +834 -0
- package/dist/components/buckets/BucketReplicationList.d.ts +11 -0
- package/dist/components/buckets/BucketReplicationList.js +189 -0
- package/dist/components/buckets/DeleteBucketConfigRuleButton.d.ts +18 -0
- package/dist/components/buckets/DeleteBucketConfigRuleButton.js +53 -0
- package/dist/components/buckets/EmptyBucketButton.d.ts +5 -0
- package/dist/components/buckets/EmptyBucketButton.js +232 -0
- package/dist/components/buckets/EmptyBucketSummary.d.ts +9 -0
- package/dist/components/buckets/EmptyBucketSummary.js +60 -0
- package/dist/components/buckets/EmptyBucketSummaryList.d.ts +13 -0
- package/dist/components/buckets/EmptyBucketSummaryList.js +140 -0
- package/dist/components/buckets/notifications/BucketNotificationCreatePage.js +8 -8
- package/dist/components/index.d.ts +8 -1
- package/dist/components/index.js +9 -2
- package/dist/components/objects/ObjectLock/EditRetentionButton.d.ts +4 -0
- package/dist/components/objects/ObjectLock/EditRetentionButton.js +32 -0
- package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.d.ts +3 -0
- package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.js +211 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettings.d.ts +9 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettings.js +158 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.d.ts +8 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.js +39 -0
- package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.d.ts +1 -0
- package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.js +204 -0
- package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.d.ts +1 -0
- package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +374 -0
- package/dist/components/ui/ArrayFieldActions.d.ts +36 -0
- package/dist/components/ui/ArrayFieldActions.js +38 -0
- package/dist/components/ui/ConfirmDeleteRuleModal.d.ts +16 -0
- package/dist/components/ui/ConfirmDeleteRuleModal.js +43 -0
- package/dist/components/ui/FilterFormSection.d.ts +44 -0
- package/dist/components/ui/FilterFormSection.js +159 -0
- package/dist/config/factory.d.ts +13 -2
- package/dist/config/factory.js +9 -6
- package/dist/hooks/__tests__/useISVBucketDetection.test.d.ts +1 -0
- package/dist/hooks/__tests__/useISVBucketDetection.test.js +188 -0
- package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +44 -1
- package/dist/hooks/factories/useCreateS3QueryHook.js +22 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +5 -1
- package/dist/hooks/useDeleteBucketConfigRule.d.ts +26 -0
- package/dist/hooks/useDeleteBucketConfigRule.js +46 -0
- package/dist/hooks/useEmptyBucket.d.ts +27 -0
- package/dist/hooks/useEmptyBucket.js +116 -0
- package/dist/hooks/useISVBucketDetection.d.ts +15 -0
- package/dist/hooks/useISVBucketDetection.js +27 -0
- package/dist/hooks/useTableRowSelection.d.ts +9 -0
- package/dist/hooks/useTableRowSelection.js +45 -0
- package/dist/test/setup.js +8 -0
- package/dist/test/testUtils.d.ts +99 -17
- package/dist/test/testUtils.js +64 -16
- package/dist/test/utils/errorHandling.test.js +39 -1
- package/dist/utils/constants.d.ts +12 -0
- package/dist/utils/constants.js +9 -0
- package/dist/utils/errorHandling.d.ts +9 -0
- package/dist/utils/errorHandling.js +6 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/s3RuleUtils.d.ts +53 -0
- package/dist/utils/s3RuleUtils.js +101 -0
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Button } from "@scality/core-ui/dist/next";
|
|
3
|
+
import { Icon } from "@scality/core-ui";
|
|
4
|
+
function ArrayFieldActions({ onRemove, onAdd, canRemove, canAdd = true, showAdd, removeLabel = "Remove", addLabel = "Add" }) {
|
|
5
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
6
|
+
children: [
|
|
7
|
+
/*#__PURE__*/ jsx(Button, {
|
|
8
|
+
icon: /*#__PURE__*/ jsx(Icon, {
|
|
9
|
+
name: "Remove-minus"
|
|
10
|
+
}),
|
|
11
|
+
variant: "danger",
|
|
12
|
+
type: "button",
|
|
13
|
+
onClick: onRemove,
|
|
14
|
+
"aria-label": removeLabel,
|
|
15
|
+
disabled: !canRemove,
|
|
16
|
+
tooltip: {
|
|
17
|
+
overlay: removeLabel,
|
|
18
|
+
placement: "top"
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
showAdd && /*#__PURE__*/ jsx(Button, {
|
|
22
|
+
icon: /*#__PURE__*/ jsx(Icon, {
|
|
23
|
+
name: "Add-plus"
|
|
24
|
+
}),
|
|
25
|
+
variant: "outline",
|
|
26
|
+
type: "button",
|
|
27
|
+
onClick: onAdd,
|
|
28
|
+
"aria-label": addLabel,
|
|
29
|
+
tooltip: {
|
|
30
|
+
overlay: addLabel,
|
|
31
|
+
placement: "top"
|
|
32
|
+
},
|
|
33
|
+
disabled: !canAdd
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export { ArrayFieldActions };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface ConfirmDeleteRuleModalProps {
|
|
2
|
+
/** Whether the modal is open */
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
/** Rule ID to display in confirmation message */
|
|
5
|
+
ruleId: string;
|
|
6
|
+
/** Type of rule (e.g., "lifecycle", "replication") */
|
|
7
|
+
ruleType: "lifecycle" | "replication";
|
|
8
|
+
/** Whether deletion is in progress */
|
|
9
|
+
isDeleting: boolean;
|
|
10
|
+
/** Callback when user confirms deletion */
|
|
11
|
+
onConfirm: () => void;
|
|
12
|
+
/** Callback when user cancels deletion */
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function ConfirmDeleteRuleModal({ isOpen, ruleId, ruleType, isDeleting, onConfirm, onCancel, }: ConfirmDeleteRuleModalProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Loader, Modal, Stack, Wrap } from "@scality/core-ui";
|
|
3
|
+
import { Button } from "@scality/core-ui/dist/next";
|
|
4
|
+
function ConfirmDeleteRuleModal({ isOpen, ruleId, ruleType, isDeleting, onConfirm, onCancel }) {
|
|
5
|
+
const ruleTypeLabel = "lifecycle" === ruleType ? "Lifecycle" : "Replication";
|
|
6
|
+
return /*#__PURE__*/ jsxs(Modal, {
|
|
7
|
+
close: onCancel,
|
|
8
|
+
isOpen: isOpen,
|
|
9
|
+
footer: /*#__PURE__*/ jsxs(Wrap, {
|
|
10
|
+
children: [
|
|
11
|
+
/*#__PURE__*/ jsx("p", {}),
|
|
12
|
+
/*#__PURE__*/ jsxs(Stack, {
|
|
13
|
+
children: [
|
|
14
|
+
/*#__PURE__*/ jsx(Button, {
|
|
15
|
+
variant: "outline",
|
|
16
|
+
onClick: onCancel,
|
|
17
|
+
label: "Cancel",
|
|
18
|
+
disabled: isDeleting
|
|
19
|
+
}),
|
|
20
|
+
/*#__PURE__*/ jsx(Button, {
|
|
21
|
+
variant: "danger",
|
|
22
|
+
onClick: onConfirm,
|
|
23
|
+
icon: isDeleting && /*#__PURE__*/ jsx(Loader, {
|
|
24
|
+
size: "larger"
|
|
25
|
+
}),
|
|
26
|
+
label: "Delete",
|
|
27
|
+
disabled: isDeleting
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
})
|
|
31
|
+
]
|
|
32
|
+
}),
|
|
33
|
+
title: `Delete ${ruleTypeLabel} Rule`,
|
|
34
|
+
children: [
|
|
35
|
+
"Are you sure you want to delete the ",
|
|
36
|
+
ruleType,
|
|
37
|
+
' rule "',
|
|
38
|
+
ruleId,
|
|
39
|
+
'"?'
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export { ConfirmDeleteRuleModal };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { UseFormRegisterReturn } from "react-hook-form";
|
|
2
|
+
export interface FilterFormValues {
|
|
3
|
+
filterType: "none" | "prefix" | "tags" | "and";
|
|
4
|
+
prefix: string;
|
|
5
|
+
tags: Array<{
|
|
6
|
+
key: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}>;
|
|
9
|
+
}
|
|
10
|
+
interface FilterFormSectionProps {
|
|
11
|
+
filterType: string;
|
|
12
|
+
onFilterTypeChange: (value: string) => void;
|
|
13
|
+
prefixRegister: UseFormRegisterReturn;
|
|
14
|
+
tagFields: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
key: string;
|
|
17
|
+
value: string;
|
|
18
|
+
}>;
|
|
19
|
+
tagKeyRegister: (index: number) => UseFormRegisterReturn;
|
|
20
|
+
tagValueRegister: (index: number) => UseFormRegisterReturn;
|
|
21
|
+
getTagKeyValue: (index: number) => string;
|
|
22
|
+
getTagValueValue: (index: number) => string;
|
|
23
|
+
appendTag: (value: {
|
|
24
|
+
key: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
removeTag: (index: number) => void;
|
|
28
|
+
errors?: {
|
|
29
|
+
prefix?: {
|
|
30
|
+
message?: string;
|
|
31
|
+
};
|
|
32
|
+
tags?: {
|
|
33
|
+
message?: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
forceLabelWidth?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function FilterFormSection({ filterType, onFilterTypeChange, prefixRegister, tagFields, tagKeyRegister, tagValueRegister, getTagKeyValue, getTagValueValue, appendTag, removeTag, errors, forceLabelWidth, }: FilterFormSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export declare const createFilterValidationSchema: (Joi: typeof import("joi")) => {
|
|
40
|
+
filterType: import("joi").StringSchema<string>;
|
|
41
|
+
prefix: import("joi").AlternativesSchema<any>;
|
|
42
|
+
tags: import("joi").AlternativesSchema<any>;
|
|
43
|
+
};
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { FormGroup, FormSection, Stack, Text } from "@scality/core-ui";
|
|
3
|
+
import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
|
|
4
|
+
import { Box, Input, Select } from "@scality/core-ui/dist/next";
|
|
5
|
+
import { ArrayFieldActions } from "./ArrayFieldActions.js";
|
|
6
|
+
const filterTypeOptions = [
|
|
7
|
+
{
|
|
8
|
+
value: "none",
|
|
9
|
+
label: "All objects"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
value: "prefix",
|
|
13
|
+
label: "Prefix filter"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
value: "tags",
|
|
17
|
+
label: "Tags filter"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
value: "and",
|
|
21
|
+
label: "Prefix and tags filter"
|
|
22
|
+
}
|
|
23
|
+
];
|
|
24
|
+
const shouldShowPrefix = (filterType)=>"prefix" === filterType || "and" === filterType;
|
|
25
|
+
const shouldShowTags = (filterType)=>"tags" === filterType || "and" === filterType;
|
|
26
|
+
function FilterFormSection({ filterType, onFilterTypeChange, prefixRegister, tagFields, tagKeyRegister, tagValueRegister, getTagKeyValue, getTagValueValue, appendTag, removeTag, errors, forceLabelWidth = convertRemToPixels(15) }) {
|
|
27
|
+
return /*#__PURE__*/ jsxs(FormSection, {
|
|
28
|
+
title: {
|
|
29
|
+
name: "Filter"
|
|
30
|
+
},
|
|
31
|
+
forceLabelWidth: forceLabelWidth,
|
|
32
|
+
children: [
|
|
33
|
+
/*#__PURE__*/ jsx(FormGroup, {
|
|
34
|
+
label: "Filter",
|
|
35
|
+
id: "filterType",
|
|
36
|
+
direction: "horizontal",
|
|
37
|
+
content: /*#__PURE__*/ jsx(Select, {
|
|
38
|
+
id: "filterType",
|
|
39
|
+
value: filterType,
|
|
40
|
+
onChange: onFilterTypeChange,
|
|
41
|
+
children: filterTypeOptions.map((option)=>/*#__PURE__*/ jsx(Select.Option, {
|
|
42
|
+
value: option.value,
|
|
43
|
+
children: option.label
|
|
44
|
+
}, option.value))
|
|
45
|
+
})
|
|
46
|
+
}),
|
|
47
|
+
shouldShowPrefix(filterType) ? /*#__PURE__*/ jsx(FormGroup, {
|
|
48
|
+
label: "Prefix",
|
|
49
|
+
id: "prefix",
|
|
50
|
+
direction: "horizontal",
|
|
51
|
+
error: errors?.prefix?.message,
|
|
52
|
+
helpErrorPosition: "bottom",
|
|
53
|
+
required: "prefix" === filterType,
|
|
54
|
+
content: /*#__PURE__*/ jsx(Input, {
|
|
55
|
+
id: "prefix",
|
|
56
|
+
placeholder: "folder/",
|
|
57
|
+
...prefixRegister
|
|
58
|
+
})
|
|
59
|
+
}) : /*#__PURE__*/ jsx(Fragment, {}),
|
|
60
|
+
shouldShowTags(filterType) ? /*#__PURE__*/ jsx(FormGroup, {
|
|
61
|
+
label: "Tags",
|
|
62
|
+
id: "tags",
|
|
63
|
+
direction: "horizontal",
|
|
64
|
+
error: errors?.tags?.message,
|
|
65
|
+
helpErrorPosition: "bottom",
|
|
66
|
+
required: true,
|
|
67
|
+
content: /*#__PURE__*/ jsxs(Stack, {
|
|
68
|
+
direction: "vertical",
|
|
69
|
+
gap: "r8",
|
|
70
|
+
children: [
|
|
71
|
+
/*#__PURE__*/ jsxs(Stack, {
|
|
72
|
+
gap: "r8",
|
|
73
|
+
children: [
|
|
74
|
+
/*#__PURE__*/ jsx(Box, {
|
|
75
|
+
flex: "1",
|
|
76
|
+
children: /*#__PURE__*/ jsx(Text, {
|
|
77
|
+
color: "textSecondary",
|
|
78
|
+
children: "Key"
|
|
79
|
+
})
|
|
80
|
+
}),
|
|
81
|
+
/*#__PURE__*/ jsx(Box, {
|
|
82
|
+
flex: "1",
|
|
83
|
+
children: /*#__PURE__*/ jsx(Text, {
|
|
84
|
+
color: "textSecondary",
|
|
85
|
+
children: "Value"
|
|
86
|
+
})
|
|
87
|
+
}),
|
|
88
|
+
/*#__PURE__*/ jsx(Box, {
|
|
89
|
+
width: "80px"
|
|
90
|
+
})
|
|
91
|
+
]
|
|
92
|
+
}),
|
|
93
|
+
tagFields.map((field, index)=>/*#__PURE__*/ jsxs(Stack, {
|
|
94
|
+
gap: "r8",
|
|
95
|
+
children: [
|
|
96
|
+
/*#__PURE__*/ jsx(Input, {
|
|
97
|
+
id: `tag-key-${index}`,
|
|
98
|
+
placeholder: "Key",
|
|
99
|
+
size: "1/2",
|
|
100
|
+
...tagKeyRegister(index)
|
|
101
|
+
}),
|
|
102
|
+
/*#__PURE__*/ jsx(Input, {
|
|
103
|
+
id: `tag-value-${index}`,
|
|
104
|
+
placeholder: "Value",
|
|
105
|
+
size: "1/2",
|
|
106
|
+
...tagValueRegister(index)
|
|
107
|
+
}),
|
|
108
|
+
/*#__PURE__*/ jsx(ArrayFieldActions, {
|
|
109
|
+
showAdd: index === tagFields.length - 1,
|
|
110
|
+
onRemove: ()=>removeTag(index),
|
|
111
|
+
onAdd: ()=>appendTag({
|
|
112
|
+
key: "",
|
|
113
|
+
value: ""
|
|
114
|
+
}),
|
|
115
|
+
canRemove: tagFields.length > 1,
|
|
116
|
+
canAdd: !!getTagKeyValue(index) && !!getTagValueValue(index),
|
|
117
|
+
removeLabel: "Remove tag",
|
|
118
|
+
addLabel: "Add tag"
|
|
119
|
+
})
|
|
120
|
+
]
|
|
121
|
+
}, field.id))
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
}) : /*#__PURE__*/ jsx(Fragment, {})
|
|
125
|
+
]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const createFilterValidationSchema = (Joi)=>({
|
|
129
|
+
filterType: Joi.string().valid("none", "prefix", "tags", "and").required(),
|
|
130
|
+
prefix: Joi.when("filterType", {
|
|
131
|
+
is: "prefix",
|
|
132
|
+
then: Joi.string().required().min(1).messages({
|
|
133
|
+
"string.empty": "Prefix is required when using prefix filter",
|
|
134
|
+
"string.min": "Prefix is required when using prefix filter"
|
|
135
|
+
}),
|
|
136
|
+
otherwise: Joi.when("filterType", {
|
|
137
|
+
is: "and",
|
|
138
|
+
then: Joi.string().allow("").optional(),
|
|
139
|
+
otherwise: Joi.any()
|
|
140
|
+
})
|
|
141
|
+
}),
|
|
142
|
+
tags: Joi.when("filterType", {
|
|
143
|
+
is: Joi.valid("tags", "and"),
|
|
144
|
+
then: Joi.array().items(Joi.object({
|
|
145
|
+
key: Joi.string().required().max(128).messages({
|
|
146
|
+
"string.empty": "Tag key is required",
|
|
147
|
+
"string.max": "Tag key must not exceed 128 characters"
|
|
148
|
+
}),
|
|
149
|
+
value: Joi.string().required().max(256).messages({
|
|
150
|
+
"string.empty": "Tag value is required",
|
|
151
|
+
"string.max": "Tag value must not exceed 256 characters"
|
|
152
|
+
})
|
|
153
|
+
})).min(1).messages({
|
|
154
|
+
"array.min": "At least one tag is required when using tag filter"
|
|
155
|
+
}),
|
|
156
|
+
otherwise: Joi.array().optional()
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
export { FilterFormSection, createFilterValidationSchema };
|
package/dist/config/factory.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { S3ClientConfiguration } from "./types";
|
|
2
2
|
import type { S3BrowserConfig, S3Credentials } from "../types";
|
|
3
|
+
import { CoreUIThemeName } from "@scality/core-ui/dist/style/theme";
|
|
4
|
+
interface RuntimeConfig {
|
|
5
|
+
s3: {
|
|
6
|
+
endpoint?: string;
|
|
7
|
+
region: string;
|
|
8
|
+
forcePathStyle?: boolean;
|
|
9
|
+
};
|
|
10
|
+
theme?: CoreUIThemeName;
|
|
11
|
+
basePath?: string;
|
|
12
|
+
}
|
|
3
13
|
/**
|
|
4
14
|
* Configuration factory that uses build-time constants
|
|
5
15
|
* No runtime environment detection or hardcoded values
|
|
@@ -9,7 +19,7 @@ export declare class S3ConfigurationFactory {
|
|
|
9
19
|
* Load runtime configuration from config.json
|
|
10
20
|
* Should be called at app startup
|
|
11
21
|
*/
|
|
12
|
-
static loadRuntimeConfig(): Promise<
|
|
22
|
+
static loadRuntimeConfig(configUrl: string): Promise<RuntimeConfig | null>;
|
|
13
23
|
private static getBuildTimeConfig;
|
|
14
24
|
/**
|
|
15
25
|
* Create S3 client configuration based on build-time settings
|
|
@@ -49,4 +59,5 @@ export declare const getBuildInfo: () => {
|
|
|
49
59
|
s3Endpoint: string;
|
|
50
60
|
proxyEndpoint: string | undefined;
|
|
51
61
|
};
|
|
52
|
-
export declare const loadRuntimeConfig: () => Promise<
|
|
62
|
+
export declare const loadRuntimeConfig: (configUrl: string) => Promise<RuntimeConfig | null>;
|
|
63
|
+
export {};
|
package/dist/config/factory.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
let runtimeConfig = null;
|
|
2
2
|
class S3ConfigurationFactory {
|
|
3
|
-
static async loadRuntimeConfig() {
|
|
3
|
+
static async loadRuntimeConfig(configUrl) {
|
|
4
4
|
try {
|
|
5
|
-
const response = await fetch(
|
|
5
|
+
const response = await fetch(configUrl);
|
|
6
6
|
if (response.ok) {
|
|
7
7
|
const data = await response.json();
|
|
8
|
-
runtimeConfig = data
|
|
9
|
-
console.log("Loaded runtime
|
|
8
|
+
runtimeConfig = data;
|
|
9
|
+
console.log("Loaded runtime configuration:", runtimeConfig);
|
|
10
|
+
return data;
|
|
10
11
|
}
|
|
12
|
+
return null;
|
|
11
13
|
} catch (error) {
|
|
12
14
|
console.warn("Could not load runtime config, using build-time config:", error);
|
|
15
|
+
return null;
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
18
|
static getBuildTimeConfig() {
|
|
@@ -22,7 +25,7 @@ class S3ConfigurationFactory {
|
|
|
22
25
|
}
|
|
23
26
|
static createClientConfiguration(credentials) {
|
|
24
27
|
const buildConfig = this.getBuildTimeConfig();
|
|
25
|
-
const s3Config = buildConfig.isProduction && runtimeConfig ? runtimeConfig : buildConfig.s3;
|
|
28
|
+
const s3Config = buildConfig.isProduction && runtimeConfig?.s3 ? runtimeConfig.s3 : buildConfig.s3;
|
|
26
29
|
const baseConfig = {
|
|
27
30
|
credentials,
|
|
28
31
|
region: s3Config.region,
|
|
@@ -66,5 +69,5 @@ const createS3Config = (credentials)=>S3ConfigurationFactory.createClientConfigu
|
|
|
66
69
|
const shouldUseProxy = ()=>S3ConfigurationFactory.shouldUseProxyMiddleware();
|
|
67
70
|
const getProxyConfig = ()=>S3ConfigurationFactory.createProxyConfiguration();
|
|
68
71
|
const getBuildInfo = ()=>S3ConfigurationFactory.getBuildInfo();
|
|
69
|
-
const loadRuntimeConfig = ()=>S3ConfigurationFactory.loadRuntimeConfig();
|
|
72
|
+
const loadRuntimeConfig = (configUrl)=>S3ConfigurationFactory.loadRuntimeConfig(configUrl);
|
|
70
73
|
export { S3ConfigurationFactory, createS3Config, getBuildInfo, getProxyConfig, loadRuntimeConfig, shouldUseProxy };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { useISVBucketStatus } from "../useISVBucketDetection.js";
|
|
3
|
+
import { useGetBucketTagging } from "../bucketConfiguration.js";
|
|
4
|
+
import { useFeatures } from "../../utils/useFeatures.js";
|
|
5
|
+
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";
|
|
6
|
+
import { createTestWrapper } from "../../test/testUtils.js";
|
|
7
|
+
jest.mock("../bucketConfiguration");
|
|
8
|
+
jest.mock("../../utils/useFeatures");
|
|
9
|
+
const mockUseGetBucketTagging = useGetBucketTagging;
|
|
10
|
+
const mockUseFeatures = useFeatures;
|
|
11
|
+
describe("useISVBucketStatus", ()=>{
|
|
12
|
+
beforeEach(()=>{
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
mockUseFeatures.mockReturnValue(true);
|
|
15
|
+
});
|
|
16
|
+
it("should return false for all flags when ISV feature is disabled", ()=>{
|
|
17
|
+
mockUseFeatures.mockReturnValue(false);
|
|
18
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
19
|
+
data: void 0,
|
|
20
|
+
status: "success"
|
|
21
|
+
});
|
|
22
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
23
|
+
wrapper: createTestWrapper()
|
|
24
|
+
});
|
|
25
|
+
expect(result.current.isVeeamBucket).toBe(false);
|
|
26
|
+
expect(result.current.isCommvaultBucket).toBe(false);
|
|
27
|
+
expect(result.current.isISVManaged).toBe(false);
|
|
28
|
+
expect(result.current.isvApplication).toBeUndefined();
|
|
29
|
+
expect(result.current.isLoading).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it("should detect Veeam Backup & Replication bucket", ()=>{
|
|
32
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
33
|
+
data: {
|
|
34
|
+
TagSet: [
|
|
35
|
+
{
|
|
36
|
+
Key: BUCKET_TAG_VEEAM_APPLICATION,
|
|
37
|
+
Value: VEEAM_BACKUP_REPLICATION
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
status: "success"
|
|
42
|
+
});
|
|
43
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
44
|
+
wrapper: createTestWrapper()
|
|
45
|
+
});
|
|
46
|
+
expect(result.current.isVeeamBucket).toBe(true);
|
|
47
|
+
expect(result.current.isCommvaultBucket).toBe(false);
|
|
48
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
49
|
+
expect(result.current.isvApplication).toBe("Veeam");
|
|
50
|
+
});
|
|
51
|
+
it("should detect Veeam Office 365 v6/v7 bucket", ()=>{
|
|
52
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
53
|
+
data: {
|
|
54
|
+
TagSet: [
|
|
55
|
+
{
|
|
56
|
+
Key: BUCKET_TAG_VEEAM_APPLICATION,
|
|
57
|
+
Value: VEEAM_OFFICE_365
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
status: "success"
|
|
62
|
+
});
|
|
63
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
64
|
+
wrapper: createTestWrapper()
|
|
65
|
+
});
|
|
66
|
+
expect(result.current.isVeeamBucket).toBe(true);
|
|
67
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
68
|
+
expect(result.current.isvApplication).toBe("Veeam");
|
|
69
|
+
});
|
|
70
|
+
it("should detect Veeam Office 365 v8+ bucket", ()=>{
|
|
71
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
72
|
+
data: {
|
|
73
|
+
TagSet: [
|
|
74
|
+
{
|
|
75
|
+
Key: BUCKET_TAG_VEEAM_APPLICATION,
|
|
76
|
+
Value: VEEAM_OFFICE_365_V8
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
status: "success"
|
|
81
|
+
});
|
|
82
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
83
|
+
wrapper: createTestWrapper()
|
|
84
|
+
});
|
|
85
|
+
expect(result.current.isVeeamBucket).toBe(true);
|
|
86
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
87
|
+
expect(result.current.isvApplication).toBe("Veeam");
|
|
88
|
+
});
|
|
89
|
+
it("should detect ISV bucket tagged as Veeam Backup & Replication", ()=>{
|
|
90
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
91
|
+
data: {
|
|
92
|
+
TagSet: [
|
|
93
|
+
{
|
|
94
|
+
Key: BUCKET_TAG_APPLICATION,
|
|
95
|
+
Value: VEEAM_BACKUP_REPLICATION
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
status: "success"
|
|
100
|
+
});
|
|
101
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
102
|
+
wrapper: createTestWrapper()
|
|
103
|
+
});
|
|
104
|
+
expect(result.current.isVeeamBucket).toBe(true);
|
|
105
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
106
|
+
expect(result.current.isvApplication).toBe("Veeam");
|
|
107
|
+
});
|
|
108
|
+
it("should detect ISV bucket tagged as Veeam VBO", ()=>{
|
|
109
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
110
|
+
data: {
|
|
111
|
+
TagSet: [
|
|
112
|
+
{
|
|
113
|
+
Key: BUCKET_TAG_APPLICATION,
|
|
114
|
+
Value: VEEAM_VBO_APPLICATION
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
status: "success"
|
|
119
|
+
});
|
|
120
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
121
|
+
wrapper: createTestWrapper()
|
|
122
|
+
});
|
|
123
|
+
expect(result.current.isVeeamBucket).toBe(true);
|
|
124
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
125
|
+
expect(result.current.isvApplication).toBe("Veeam");
|
|
126
|
+
});
|
|
127
|
+
it("should detect Commvault bucket", ()=>{
|
|
128
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
129
|
+
data: {
|
|
130
|
+
TagSet: [
|
|
131
|
+
{
|
|
132
|
+
Key: BUCKET_TAG_APPLICATION,
|
|
133
|
+
Value: COMMVAULT_APPLICATION
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
status: "success"
|
|
138
|
+
});
|
|
139
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
140
|
+
wrapper: createTestWrapper()
|
|
141
|
+
});
|
|
142
|
+
expect(result.current.isVeeamBucket).toBe(false);
|
|
143
|
+
expect(result.current.isCommvaultBucket).toBe(true);
|
|
144
|
+
expect(result.current.isISVManaged).toBe(true);
|
|
145
|
+
expect(result.current.isvApplication).toBe("Commvault");
|
|
146
|
+
});
|
|
147
|
+
it("should return false for non-ISV bucket", ()=>{
|
|
148
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
149
|
+
data: {
|
|
150
|
+
TagSet: [
|
|
151
|
+
{
|
|
152
|
+
Key: "SomeOtherTag",
|
|
153
|
+
Value: "SomeValue"
|
|
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.isISVManaged).toBe(false);
|
|
165
|
+
expect(result.current.isvApplication).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
it("should return loading state when fetching bucket tags", ()=>{
|
|
168
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
169
|
+
data: void 0,
|
|
170
|
+
status: "pending"
|
|
171
|
+
});
|
|
172
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
173
|
+
wrapper: createTestWrapper()
|
|
174
|
+
});
|
|
175
|
+
expect(result.current.isLoading).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
it("should not be loading when ISV feature is disabled", ()=>{
|
|
178
|
+
mockUseFeatures.mockReturnValue(false);
|
|
179
|
+
mockUseGetBucketTagging.mockReturnValue({
|
|
180
|
+
data: void 0,
|
|
181
|
+
status: "pending"
|
|
182
|
+
});
|
|
183
|
+
const { result } = renderHook(()=>useISVBucketStatus("test-bucket"), {
|
|
184
|
+
wrapper: createTestWrapper()
|
|
185
|
+
});
|
|
186
|
+
expect(result.current.isLoading).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { renderHook, waitFor } from "@testing-library/react";
|
|
2
2
|
import { useCreateS3QueryHook } from "../useCreateS3QueryHook.js";
|
|
3
3
|
import { useS3Client } from "../../useS3Client.js";
|
|
4
|
-
import { createS3OperationError } from "../../../utils/errorHandling.js";
|
|
4
|
+
import { createS3OperationError, isNotFoundError } from "../../../utils/errorHandling.js";
|
|
5
5
|
import { createQueryWrapper, validateFactoryHook, validateHookResult } from "../../../test/testUtils.js";
|
|
6
6
|
jest.mock("../../useS3Client");
|
|
7
7
|
jest.mock("../../../utils/errorHandling");
|
|
8
8
|
const mockUseS3Client = useS3Client;
|
|
9
9
|
const mockCreateS3OperationError = createS3OperationError;
|
|
10
|
+
const mockIsNotFoundError = isNotFoundError;
|
|
10
11
|
class MockCommand {
|
|
11
12
|
input;
|
|
12
13
|
constructor(input){
|
|
@@ -133,4 +134,46 @@ describe("useCreateS3QueryHook - Factory Specific", ()=>{
|
|
|
133
134
|
expect(capturedAbortSignal).toBeInstanceOf(AbortSignal);
|
|
134
135
|
expect(capturedAbortSignal?.aborted).toBe(false);
|
|
135
136
|
});
|
|
137
|
+
it("should return empty config for supported operations when NOT_FOUND error occurs", async ()=>{
|
|
138
|
+
const error = new Error("Configuration does not exist");
|
|
139
|
+
error.name = "NoSuchConfiguration";
|
|
140
|
+
error.$metadata = {
|
|
141
|
+
httpStatusCode: 404
|
|
142
|
+
};
|
|
143
|
+
mockS3Client.send.mockRejectedValueOnce(error);
|
|
144
|
+
mockIsNotFoundError.mockReturnValueOnce(true);
|
|
145
|
+
const useTestQuery = useCreateS3QueryHook(MockCommand, "GetBucketLifecycleConfiguration");
|
|
146
|
+
const { result } = renderHook(()=>useTestQuery({
|
|
147
|
+
Bucket: "test-bucket"
|
|
148
|
+
}), {
|
|
149
|
+
wrapper: createQueryWrapper()
|
|
150
|
+
});
|
|
151
|
+
await waitFor(()=>{
|
|
152
|
+
expect(result.current.isSuccess).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
expect(result.current.data).toBeDefined();
|
|
155
|
+
expect(mockIsNotFoundError).toHaveBeenCalledWith(error);
|
|
156
|
+
});
|
|
157
|
+
it("should throw error when NOT_FOUND occurs for unsupported operations", async ()=>{
|
|
158
|
+
const error = new Error("Bucket not found");
|
|
159
|
+
error.name = "NoSuchBucket";
|
|
160
|
+
error.$metadata = {
|
|
161
|
+
httpStatusCode: 404
|
|
162
|
+
};
|
|
163
|
+
const enhancedError = new Error("Enhanced error");
|
|
164
|
+
mockS3Client.send.mockRejectedValueOnce(error);
|
|
165
|
+
mockIsNotFoundError.mockReturnValueOnce(true);
|
|
166
|
+
mockCreateS3OperationError.mockReturnValueOnce(enhancedError);
|
|
167
|
+
const useTestQuery = useCreateS3QueryHook(MockCommand, "GetBucketAcl");
|
|
168
|
+
const { result } = renderHook(()=>useTestQuery({
|
|
169
|
+
Bucket: "test-bucket"
|
|
170
|
+
}), {
|
|
171
|
+
wrapper: createQueryWrapper()
|
|
172
|
+
});
|
|
173
|
+
await waitFor(()=>{
|
|
174
|
+
expect(result.current.isError).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
expect(result.current.error).toBe(enhancedError);
|
|
177
|
+
expect(mockIsNotFoundError).toHaveBeenCalledWith(error);
|
|
178
|
+
});
|
|
136
179
|
});
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
2
|
import { useS3Client } from "../useS3Client.js";
|
|
3
|
-
import { createS3OperationError, shouldRetryError } from "../../utils/errorHandling.js";
|
|
3
|
+
import { createS3OperationError, isNotFoundError, shouldRetryError } from "../../utils/errorHandling.js";
|
|
4
|
+
function getEmptyConfigForOperation(operationName) {
|
|
5
|
+
switch(operationName){
|
|
6
|
+
case "GetBucketLifecycleConfiguration":
|
|
7
|
+
return {
|
|
8
|
+
Rules: []
|
|
9
|
+
};
|
|
10
|
+
case "GetBucketReplication":
|
|
11
|
+
return {
|
|
12
|
+
ReplicationConfiguration: {
|
|
13
|
+
Rules: [],
|
|
14
|
+
Role: ""
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
default:
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
4
21
|
function useCreateS3QueryHook(Command, operationName) {
|
|
5
22
|
return (params, options)=>{
|
|
6
23
|
const queryKey = [
|
|
@@ -18,6 +35,10 @@ function useCreateS3QueryHook(Command, operationName) {
|
|
|
18
35
|
});
|
|
19
36
|
} catch (error) {
|
|
20
37
|
const bucketName = params && "Bucket" in params ? params.Bucket : void 0;
|
|
38
|
+
if (isNotFoundError(error)) {
|
|
39
|
+
const emptyConfig = getEmptyConfigForOperation(operationName);
|
|
40
|
+
if (null !== emptyConfig) return emptyConfig;
|
|
41
|
+
}
|
|
21
42
|
throw createS3OperationError(error, operationName, bucketName);
|
|
22
43
|
}
|
|
23
44
|
},
|