@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,302 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import { createTestWrapper } from "../../test/testUtils.js";
|
|
4
|
+
import { EmptyBucketButton } from "../buckets/EmptyBucketButton.js";
|
|
5
|
+
import { useIsBucketEmpty } from "../../hooks/useIsBucketEmpty.js";
|
|
6
|
+
import { useEmptyBucket } from "../../hooks/useEmptyBucket.js";
|
|
7
|
+
import { useGetBucketObjectLockConfiguration } from "../../hooks/bucketConfiguration.js";
|
|
8
|
+
jest.mock("../../hooks/useIsBucketEmpty");
|
|
9
|
+
jest.mock("../../hooks/useEmptyBucket");
|
|
10
|
+
jest.mock("../../hooks/bucketConfiguration");
|
|
11
|
+
const mockUseIsBucketEmpty = jest.mocked(useIsBucketEmpty);
|
|
12
|
+
const mockUseEmptyBucket = jest.mocked(useEmptyBucket);
|
|
13
|
+
const mockUseGetBucketObjectLockConfiguration = jest.mocked(useGetBucketObjectLockConfiguration);
|
|
14
|
+
const renderEmptyBucketButton = (props = {})=>{
|
|
15
|
+
const Wrapper = createTestWrapper();
|
|
16
|
+
return render(/*#__PURE__*/ jsx(Wrapper, {
|
|
17
|
+
children: /*#__PURE__*/ jsx(EmptyBucketButton, {
|
|
18
|
+
bucketName: "test-bucket",
|
|
19
|
+
...props
|
|
20
|
+
})
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
const mockHookDefaults = ()=>{
|
|
24
|
+
mockUseIsBucketEmpty.mockReturnValue({
|
|
25
|
+
isEmpty: false,
|
|
26
|
+
isLoading: false,
|
|
27
|
+
error: null
|
|
28
|
+
});
|
|
29
|
+
mockUseEmptyBucket.mockReturnValue({
|
|
30
|
+
emptyBucket: jest.fn(),
|
|
31
|
+
isEmptying: false,
|
|
32
|
+
error: null,
|
|
33
|
+
result: null,
|
|
34
|
+
reset: jest.fn()
|
|
35
|
+
});
|
|
36
|
+
mockUseGetBucketObjectLockConfiguration.mockReturnValue({
|
|
37
|
+
data: void 0,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
error: null
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
describe("EmptyBucketButton", ()=>{
|
|
43
|
+
beforeEach(()=>{
|
|
44
|
+
jest.clearAllMocks();
|
|
45
|
+
mockHookDefaults();
|
|
46
|
+
});
|
|
47
|
+
it("renders empty button with correct label", ()=>{
|
|
48
|
+
renderEmptyBucketButton();
|
|
49
|
+
expect(screen.getByRole("button", {
|
|
50
|
+
name: /empty bucket/i
|
|
51
|
+
})).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
it("disables button when bucket is empty", ()=>{
|
|
54
|
+
mockUseIsBucketEmpty.mockReturnValue({
|
|
55
|
+
isEmpty: true,
|
|
56
|
+
isLoading: false,
|
|
57
|
+
error: null
|
|
58
|
+
});
|
|
59
|
+
renderEmptyBucketButton();
|
|
60
|
+
const button = screen.getByRole("button", {
|
|
61
|
+
name: /empty bucket/i
|
|
62
|
+
});
|
|
63
|
+
expect(button).toBeDisabled();
|
|
64
|
+
});
|
|
65
|
+
it("enables button when bucket is not empty", ()=>{
|
|
66
|
+
mockUseIsBucketEmpty.mockReturnValue({
|
|
67
|
+
isEmpty: false,
|
|
68
|
+
isLoading: false,
|
|
69
|
+
error: null
|
|
70
|
+
});
|
|
71
|
+
renderEmptyBucketButton();
|
|
72
|
+
const button = screen.getByRole("button", {
|
|
73
|
+
name: /empty bucket/i
|
|
74
|
+
});
|
|
75
|
+
expect(button).not.toBeDisabled();
|
|
76
|
+
});
|
|
77
|
+
it("opens confirmation modal when button is clicked", ()=>{
|
|
78
|
+
renderEmptyBucketButton();
|
|
79
|
+
const button = screen.getByRole("button", {
|
|
80
|
+
name: /empty bucket/i
|
|
81
|
+
});
|
|
82
|
+
fireEvent.click(button);
|
|
83
|
+
expect(screen.getByText(/Empty Bucket 'test-bucket'\?/i)).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText(/Emptying a bucket removes all contents and cannot be reversed/i)).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
it("shows information list in modal", ()=>{
|
|
87
|
+
renderEmptyBucketButton();
|
|
88
|
+
const button = screen.getByRole("button", {
|
|
89
|
+
name: /empty bucket/i
|
|
90
|
+
});
|
|
91
|
+
fireEvent.click(button);
|
|
92
|
+
expect(screen.getByText(/Emptying a bucket removes all contents and cannot be reversed/i)).toBeInTheDocument();
|
|
93
|
+
expect(screen.getByText(/New objects added during the empty action may also be removed/i)).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
it("shows confirmation input field", ()=>{
|
|
96
|
+
renderEmptyBucketButton();
|
|
97
|
+
const button = screen.getByRole("button", {
|
|
98
|
+
name: /empty bucket/i
|
|
99
|
+
});
|
|
100
|
+
fireEvent.click(button);
|
|
101
|
+
const input = screen.getByPlaceholderText(/test-bucket/i);
|
|
102
|
+
expect(input).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
it("disables empty button until bucket name is typed", ()=>{
|
|
105
|
+
renderEmptyBucketButton();
|
|
106
|
+
const button = screen.getByRole("button", {
|
|
107
|
+
name: /empty bucket/i
|
|
108
|
+
});
|
|
109
|
+
fireEvent.click(button);
|
|
110
|
+
const emptyButton = screen.getByRole("button", {
|
|
111
|
+
name: /^empty$/i
|
|
112
|
+
});
|
|
113
|
+
expect(emptyButton).toBeDisabled();
|
|
114
|
+
const input = screen.getByPlaceholderText(/test-bucket/i);
|
|
115
|
+
fireEvent.change(input, {
|
|
116
|
+
target: {
|
|
117
|
+
value: "test-bucket"
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
expect(emptyButton).not.toBeDisabled();
|
|
121
|
+
});
|
|
122
|
+
it("closes modal when cancel is clicked", ()=>{
|
|
123
|
+
renderEmptyBucketButton();
|
|
124
|
+
const deleteButton = screen.getByRole("button", {
|
|
125
|
+
name: /empty bucket/i
|
|
126
|
+
});
|
|
127
|
+
fireEvent.click(deleteButton);
|
|
128
|
+
const cancelButton = screen.getByRole("button", {
|
|
129
|
+
name: /cancel/i
|
|
130
|
+
});
|
|
131
|
+
fireEvent.click(cancelButton);
|
|
132
|
+
expect(screen.queryByText(/Empty Bucket 'test-bucket'\?/i)).not.toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
it("resets state when modal is cancelled", ()=>{
|
|
135
|
+
renderEmptyBucketButton();
|
|
136
|
+
const button = screen.getByRole("button", {
|
|
137
|
+
name: /empty bucket/i
|
|
138
|
+
});
|
|
139
|
+
fireEvent.click(button);
|
|
140
|
+
const input = screen.getByPlaceholderText(/test-bucket/i);
|
|
141
|
+
fireEvent.change(input, {
|
|
142
|
+
target: {
|
|
143
|
+
value: "test-bucket"
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
expect(input).toHaveValue("test-bucket");
|
|
147
|
+
const cancelButton = screen.getByRole("button", {
|
|
148
|
+
name: /cancel/i
|
|
149
|
+
});
|
|
150
|
+
fireEvent.click(cancelButton);
|
|
151
|
+
fireEvent.click(button);
|
|
152
|
+
const inputAfterReopen = screen.getByPlaceholderText(/test-bucket/i);
|
|
153
|
+
expect(inputAfterReopen).toHaveValue("");
|
|
154
|
+
});
|
|
155
|
+
it("displays correct bucket name in modal title", ()=>{
|
|
156
|
+
renderEmptyBucketButton({
|
|
157
|
+
bucketName: "my-special-bucket"
|
|
158
|
+
});
|
|
159
|
+
const button = screen.getByRole("button", {
|
|
160
|
+
name: /empty bucket/i
|
|
161
|
+
});
|
|
162
|
+
fireEvent.click(button);
|
|
163
|
+
expect(screen.getByText(/Empty Bucket 'my-special-bucket'\?/i)).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
it("shows loading state during deletion", async ()=>{
|
|
166
|
+
const mockEmptyBucket = jest.fn();
|
|
167
|
+
mockUseEmptyBucket.mockReturnValue({
|
|
168
|
+
emptyBucket: mockEmptyBucket,
|
|
169
|
+
isEmptying: true,
|
|
170
|
+
error: null,
|
|
171
|
+
result: null,
|
|
172
|
+
reset: jest.fn()
|
|
173
|
+
});
|
|
174
|
+
renderEmptyBucketButton();
|
|
175
|
+
const button = screen.getByRole("button", {
|
|
176
|
+
name: /empty bucket/i
|
|
177
|
+
});
|
|
178
|
+
fireEvent.click(button);
|
|
179
|
+
expect(screen.getByText(/Deletion in progress.../i)).toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
it("completes empty bucket operation successfully and shows summary", async ()=>{
|
|
182
|
+
const mockEmptyBucket = jest.fn().mockResolvedValue({
|
|
183
|
+
success: true,
|
|
184
|
+
deletedCount: 100,
|
|
185
|
+
errors: [],
|
|
186
|
+
limitReached: false
|
|
187
|
+
});
|
|
188
|
+
mockUseEmptyBucket.mockReturnValue({
|
|
189
|
+
emptyBucket: mockEmptyBucket,
|
|
190
|
+
isEmptying: false,
|
|
191
|
+
error: null,
|
|
192
|
+
result: null,
|
|
193
|
+
reset: jest.fn()
|
|
194
|
+
});
|
|
195
|
+
renderEmptyBucketButton();
|
|
196
|
+
const button = screen.getByRole("button", {
|
|
197
|
+
name: /empty bucket/i
|
|
198
|
+
});
|
|
199
|
+
fireEvent.click(button);
|
|
200
|
+
const input = screen.getByPlaceholderText(/test-bucket/i);
|
|
201
|
+
fireEvent.change(input, {
|
|
202
|
+
target: {
|
|
203
|
+
value: "test-bucket"
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
const emptyButton = screen.getByRole("button", {
|
|
207
|
+
name: /^empty$/i
|
|
208
|
+
});
|
|
209
|
+
fireEvent.click(emptyButton);
|
|
210
|
+
await waitFor(()=>{
|
|
211
|
+
expect(mockEmptyBucket).toHaveBeenCalledWith("test-bucket");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
it("displays error when deletion fails", ()=>{
|
|
215
|
+
mockUseEmptyBucket.mockReturnValue({
|
|
216
|
+
emptyBucket: jest.fn(),
|
|
217
|
+
isEmptying: false,
|
|
218
|
+
error: new Error("Network error"),
|
|
219
|
+
result: null,
|
|
220
|
+
reset: jest.fn()
|
|
221
|
+
});
|
|
222
|
+
renderEmptyBucketButton();
|
|
223
|
+
const button = screen.getByRole("button", {
|
|
224
|
+
name: /empty bucket/i
|
|
225
|
+
});
|
|
226
|
+
fireEvent.click(button);
|
|
227
|
+
expect(screen.getByText(/Network error/i)).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
it("calls emptyBucket when user confirms", async ()=>{
|
|
230
|
+
const mockEmptyBucket = jest.fn().mockResolvedValue({
|
|
231
|
+
success: false,
|
|
232
|
+
deletedCount: 50,
|
|
233
|
+
errors: [
|
|
234
|
+
{
|
|
235
|
+
key: "file1.txt",
|
|
236
|
+
code: "AccessDenied",
|
|
237
|
+
message: "Access denied"
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
limitReached: false
|
|
241
|
+
});
|
|
242
|
+
mockUseEmptyBucket.mockReturnValue({
|
|
243
|
+
emptyBucket: mockEmptyBucket,
|
|
244
|
+
isEmptying: false,
|
|
245
|
+
error: null,
|
|
246
|
+
result: null,
|
|
247
|
+
reset: jest.fn()
|
|
248
|
+
});
|
|
249
|
+
renderEmptyBucketButton();
|
|
250
|
+
const button = screen.getByRole("button", {
|
|
251
|
+
name: /empty bucket/i
|
|
252
|
+
});
|
|
253
|
+
fireEvent.click(button);
|
|
254
|
+
const input = screen.getByPlaceholderText(/test-bucket/i);
|
|
255
|
+
fireEvent.change(input, {
|
|
256
|
+
target: {
|
|
257
|
+
value: "test-bucket"
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
const emptyButton = screen.getByRole("button", {
|
|
261
|
+
name: /^empty$/i
|
|
262
|
+
});
|
|
263
|
+
fireEvent.click(emptyButton);
|
|
264
|
+
await waitFor(()=>{
|
|
265
|
+
expect(mockEmptyBucket).toHaveBeenCalledWith("test-bucket");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
it("displays Object Lock warning when enabled", ()=>{
|
|
269
|
+
mockUseGetBucketObjectLockConfiguration.mockReturnValue({
|
|
270
|
+
data: {
|
|
271
|
+
ObjectLockConfiguration: {
|
|
272
|
+
ObjectLockEnabled: "Enabled"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
isLoading: false,
|
|
276
|
+
error: null
|
|
277
|
+
});
|
|
278
|
+
renderEmptyBucketButton();
|
|
279
|
+
const button = screen.getByRole("button", {
|
|
280
|
+
name: /empty bucket/i
|
|
281
|
+
});
|
|
282
|
+
fireEvent.click(button);
|
|
283
|
+
expect(screen.getByText(/locked in governance mode/i)).toBeInTheDocument();
|
|
284
|
+
});
|
|
285
|
+
it("does not show Object Lock warning when disabled", ()=>{
|
|
286
|
+
mockUseGetBucketObjectLockConfiguration.mockReturnValue({
|
|
287
|
+
data: {
|
|
288
|
+
ObjectLockConfiguration: {
|
|
289
|
+
ObjectLockEnabled: "Disabled"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
isLoading: false,
|
|
293
|
+
error: null
|
|
294
|
+
});
|
|
295
|
+
renderEmptyBucketButton();
|
|
296
|
+
const button = screen.getByRole("button", {
|
|
297
|
+
name: /empty bucket/i
|
|
298
|
+
});
|
|
299
|
+
fireEvent.click(button);
|
|
300
|
+
expect(screen.queryByText(/locked in governance mode/i)).not.toBeInTheDocument();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import Joi from "joi";
|
|
2
|
+
import { ObjectLockRetentionMode } from "@aws-sdk/client-s3";
|
|
3
|
+
export declare const bucketErrorMessage = "Bucket names can include only lowercase letters, numbers, dots (.), and hyphens (-)";
|
|
4
|
+
export declare const bucketNameValidationSchema: Joi.StringSchema<string>;
|
|
5
|
+
export declare const baseBucketCreateSchema: Joi.ObjectSchema<any>;
|
|
6
|
+
export type BucketCreateFormData = {
|
|
7
|
+
name: string;
|
|
8
|
+
isVersioning: boolean;
|
|
9
|
+
isObjectLockEnabled: boolean;
|
|
10
|
+
isDefaultRetentionEnabled: boolean;
|
|
11
|
+
retentionMode: ObjectLockRetentionMode;
|
|
12
|
+
retentionPeriod: number;
|
|
13
|
+
retentionPeriodFrequencyChoice: string;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
type BucketCreateProps = {
|
|
17
|
+
subTitle?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Custom validation schema - if not provided, uses baseBucketCreateSchema
|
|
20
|
+
*
|
|
21
|
+
* To enable uniqueness validation, provide context with existing bucket names:
|
|
22
|
+
* ```ts
|
|
23
|
+
* const schema = baseBucketCreateSchema.append({...});
|
|
24
|
+
* <BucketCreate
|
|
25
|
+
* validationSchema={schema}
|
|
26
|
+
* validationContext={{ existingBuckets: ['bucket1', 'bucket2'] }}
|
|
27
|
+
* />
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
validationSchema?: Joi.ObjectSchema;
|
|
31
|
+
/**
|
|
32
|
+
* Validation context passed to Joi schema
|
|
33
|
+
* Use this to provide existing bucket names for uniqueness validation
|
|
34
|
+
*/
|
|
35
|
+
validationContext?: {
|
|
36
|
+
existingBuckets?: string[];
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
/** Default form values - merged with base defaults */
|
|
40
|
+
defaultValues?: Partial<BucketCreateFormData>;
|
|
41
|
+
/** Children rendered between bucket name and versioning sections */
|
|
42
|
+
children?: React.ReactNode;
|
|
43
|
+
/** Callback when form is submitted with valid data */
|
|
44
|
+
onSubmit?: (data: BucketCreateFormData) => void;
|
|
45
|
+
/** Callback when cancel is clicked */
|
|
46
|
+
onCancel?: () => void;
|
|
47
|
+
};
|
|
48
|
+
export declare const BucketCreate: ({ subTitle, validationSchema, validationContext, defaultValues: customDefaultValues, children, onSubmit: onSubmitProp, onCancel: onCancelProp, }: BucketCreateProps) => import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
export default BucketCreate;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { joiResolver } from "@hookform/resolvers/joi";
|
|
3
|
+
import { Checkbox, Form, FormGroup, FormSection, Stack, useToast } from "@scality/core-ui";
|
|
4
|
+
import { Button, Input } from "@scality/core-ui/dist/next";
|
|
5
|
+
import { convertRemToPixels } from "@scality/core-ui/dist/utils";
|
|
6
|
+
import joi from "joi";
|
|
7
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
8
|
+
import { useNavigate } from "react-router";
|
|
9
|
+
import ObjectLockRetentionSettings from "../objects/ObjectLock/ObjectLockRetentionSettings.js";
|
|
10
|
+
import { objectLockRetentionSettingsValidationRules } from "../objects/ObjectLock/ObjectLockSettings.js";
|
|
11
|
+
import { useCreateBucket, useSetBucketObjectLockConfiguration, useSetBucketVersioning } from "../../hooks/index.js";
|
|
12
|
+
const bucketErrorMessage = "Bucket names can include only lowercase letters, numbers, dots (.), and hyphens (-)";
|
|
13
|
+
const bucketNameValidationSchema = joi.string().label("Bucket Name").required().min(3).max(63).pattern(/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/, {
|
|
14
|
+
name: "bucketName",
|
|
15
|
+
invert: false
|
|
16
|
+
}).messages({
|
|
17
|
+
"string.pattern.name": bucketErrorMessage
|
|
18
|
+
}).custom((value, helpers)=>{
|
|
19
|
+
if (value.includes("..")) return helpers.message({
|
|
20
|
+
custom: "Bucket names cannot contain two adjacent periods"
|
|
21
|
+
});
|
|
22
|
+
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(value)) return helpers.message({
|
|
23
|
+
custom: "Bucket names must not be formatted as an IP address"
|
|
24
|
+
});
|
|
25
|
+
const forbiddenPrefixes = [
|
|
26
|
+
"xn--",
|
|
27
|
+
"sthree-",
|
|
28
|
+
"amzn-s3-demo-"
|
|
29
|
+
];
|
|
30
|
+
if (forbiddenPrefixes.some((prefix)=>value.startsWith(prefix))) return helpers.message({
|
|
31
|
+
custom: `Bucket names must not start with ${forbiddenPrefixes.join(", ")}`
|
|
32
|
+
});
|
|
33
|
+
const forbiddenSuffixes = [
|
|
34
|
+
"-s3alias",
|
|
35
|
+
"--ol-s3",
|
|
36
|
+
"--x-s3",
|
|
37
|
+
".mrap"
|
|
38
|
+
];
|
|
39
|
+
if (forbiddenSuffixes.some((suffix)=>value.endsWith(suffix))) return helpers.message({
|
|
40
|
+
custom: `Bucket names must not end with ${forbiddenSuffixes.join(", ")}`
|
|
41
|
+
});
|
|
42
|
+
const existingBuckets = helpers.prefs.context?.existingBuckets;
|
|
43
|
+
if (Array.isArray(existingBuckets)) {
|
|
44
|
+
const isDuplicate = existingBuckets.some((bucketName)=>bucketName.toLowerCase() === value.toLowerCase());
|
|
45
|
+
if (isDuplicate) return helpers.message({
|
|
46
|
+
custom: "A bucket with this name already exists"
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}, "AWS S3 bucket naming rules");
|
|
51
|
+
const baseBucketCreateSchema = joi.object({
|
|
52
|
+
name: bucketNameValidationSchema,
|
|
53
|
+
isVersioning: joi.boolean(),
|
|
54
|
+
...objectLockRetentionSettingsValidationRules
|
|
55
|
+
});
|
|
56
|
+
const BucketCreate = ({ subTitle, validationSchema, validationContext, defaultValues: customDefaultValues, children, onSubmit: onSubmitProp, onCancel: onCancelProp })=>{
|
|
57
|
+
const navigate = useNavigate();
|
|
58
|
+
const { showToast } = useToast();
|
|
59
|
+
const baseDefaultValues = {
|
|
60
|
+
name: "",
|
|
61
|
+
isVersioning: false,
|
|
62
|
+
isObjectLockEnabled: false,
|
|
63
|
+
isDefaultRetentionEnabled: false,
|
|
64
|
+
retentionMode: "GOVERNANCE",
|
|
65
|
+
retentionPeriod: 1,
|
|
66
|
+
retentionPeriodFrequencyChoice: "DAYS",
|
|
67
|
+
...customDefaultValues
|
|
68
|
+
};
|
|
69
|
+
const useFormMethods = useForm({
|
|
70
|
+
mode: "all",
|
|
71
|
+
resolver: joiResolver(validationSchema || baseBucketCreateSchema, {
|
|
72
|
+
context: validationContext || {}
|
|
73
|
+
}),
|
|
74
|
+
defaultValues: baseDefaultValues,
|
|
75
|
+
context: validationContext
|
|
76
|
+
});
|
|
77
|
+
const { register, handleSubmit, formState, watch } = useFormMethods;
|
|
78
|
+
const { isValid, errors } = formState;
|
|
79
|
+
const isObjectLockEnabled = watch("isObjectLockEnabled");
|
|
80
|
+
const { mutate: createBucket, isPending: isCreatingBucket } = useCreateBucket();
|
|
81
|
+
const { mutate: setBucketVersioning } = useSetBucketVersioning();
|
|
82
|
+
const { mutate: setObjectLockConfig } = useSetBucketObjectLockConfiguration();
|
|
83
|
+
const handleCancel = ()=>{
|
|
84
|
+
if (onCancelProp) onCancelProp();
|
|
85
|
+
else navigate("/buckets");
|
|
86
|
+
};
|
|
87
|
+
const handleSuccess = (bucketName)=>{
|
|
88
|
+
showToast({
|
|
89
|
+
open: true,
|
|
90
|
+
message: `Bucket "${bucketName}" created successfully`,
|
|
91
|
+
status: "success"
|
|
92
|
+
});
|
|
93
|
+
navigate(`/buckets/${bucketName}`);
|
|
94
|
+
};
|
|
95
|
+
const handleError = (error, operation)=>{
|
|
96
|
+
showToast({
|
|
97
|
+
open: true,
|
|
98
|
+
message: error instanceof Error ? error.message : `Failed to ${operation}`,
|
|
99
|
+
status: "error"
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const onSubmit = (data)=>{
|
|
103
|
+
if (onSubmitProp) return void onSubmitProp(data);
|
|
104
|
+
const bucketName = data.name;
|
|
105
|
+
const needsExplicitVersioning = data.isVersioning && !data.isObjectLockEnabled;
|
|
106
|
+
const needsRetentionConfig = data.isObjectLockEnabled && data.isDefaultRetentionEnabled;
|
|
107
|
+
createBucket({
|
|
108
|
+
Bucket: bucketName,
|
|
109
|
+
ObjectLockEnabledForBucket: data.isObjectLockEnabled || void 0
|
|
110
|
+
}, {
|
|
111
|
+
onSuccess: ()=>{
|
|
112
|
+
if (needsExplicitVersioning) setBucketVersioning({
|
|
113
|
+
Bucket: bucketName,
|
|
114
|
+
VersioningConfiguration: {
|
|
115
|
+
Status: "Enabled"
|
|
116
|
+
}
|
|
117
|
+
}, {
|
|
118
|
+
onSuccess: ()=>handleSuccess(bucketName),
|
|
119
|
+
onError: (error)=>handleError(error, "enable versioning")
|
|
120
|
+
});
|
|
121
|
+
else if (needsRetentionConfig) {
|
|
122
|
+
const retentionConfig = {
|
|
123
|
+
Bucket: bucketName,
|
|
124
|
+
ObjectLockConfiguration: {
|
|
125
|
+
ObjectLockEnabled: "Enabled",
|
|
126
|
+
Rule: {
|
|
127
|
+
DefaultRetention: {
|
|
128
|
+
Mode: data.retentionMode,
|
|
129
|
+
Days: "DAYS" === data.retentionPeriodFrequencyChoice ? data.retentionPeriod : void 0,
|
|
130
|
+
Years: "YEARS" === data.retentionPeriodFrequencyChoice ? data.retentionPeriod : void 0
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
setObjectLockConfig(retentionConfig, {
|
|
136
|
+
onSuccess: ()=>handleSuccess(bucketName),
|
|
137
|
+
onError: (error)=>handleError(error, "configure object lock retention")
|
|
138
|
+
});
|
|
139
|
+
} else handleSuccess(bucketName);
|
|
140
|
+
},
|
|
141
|
+
onError: (error)=>handleError(error, "create bucket")
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
return /*#__PURE__*/ jsx(FormProvider, {
|
|
145
|
+
...useFormMethods,
|
|
146
|
+
children: /*#__PURE__*/ jsxs(Form, {
|
|
147
|
+
layout: {
|
|
148
|
+
kind: "page",
|
|
149
|
+
title: "Create a New Bucket",
|
|
150
|
+
subTitle
|
|
151
|
+
},
|
|
152
|
+
requireMode: "partial",
|
|
153
|
+
onSubmit: handleSubmit(onSubmit),
|
|
154
|
+
rightActions: /*#__PURE__*/ jsxs(Stack, {
|
|
155
|
+
gap: "r16",
|
|
156
|
+
children: [
|
|
157
|
+
/*#__PURE__*/ jsx(Button, {
|
|
158
|
+
id: "cancel-btn",
|
|
159
|
+
variant: "outline",
|
|
160
|
+
onClick: handleCancel,
|
|
161
|
+
type: "button",
|
|
162
|
+
label: "Cancel"
|
|
163
|
+
}),
|
|
164
|
+
/*#__PURE__*/ jsx(Button, {
|
|
165
|
+
disabled: !isValid,
|
|
166
|
+
isLoading: isCreatingBucket,
|
|
167
|
+
id: "create-bucket-btn",
|
|
168
|
+
type: "submit",
|
|
169
|
+
variant: "primary",
|
|
170
|
+
label: "Create"
|
|
171
|
+
})
|
|
172
|
+
]
|
|
173
|
+
}),
|
|
174
|
+
children: [
|
|
175
|
+
/*#__PURE__*/ jsx(FormSection, {
|
|
176
|
+
forceLabelWidth: convertRemToPixels(17.5),
|
|
177
|
+
children: /*#__PURE__*/ jsxs(Fragment, {
|
|
178
|
+
children: [
|
|
179
|
+
/*#__PURE__*/ jsx(FormGroup, {
|
|
180
|
+
id: "name",
|
|
181
|
+
label: "Bucket Name",
|
|
182
|
+
required: true,
|
|
183
|
+
direction: "horizontal",
|
|
184
|
+
content: /*#__PURE__*/ jsx(Input, {
|
|
185
|
+
id: "name",
|
|
186
|
+
autoFocus: true,
|
|
187
|
+
...register("name")
|
|
188
|
+
}),
|
|
189
|
+
labelHelpTooltip: /*#__PURE__*/ jsxs("ul", {
|
|
190
|
+
children: [
|
|
191
|
+
/*#__PURE__*/ jsx("li", {
|
|
192
|
+
children: "Must be unique"
|
|
193
|
+
}),
|
|
194
|
+
/*#__PURE__*/ jsx("li", {
|
|
195
|
+
children: "Cannot be modified after creation"
|
|
196
|
+
}),
|
|
197
|
+
/*#__PURE__*/ jsx("li", {
|
|
198
|
+
children: bucketErrorMessage
|
|
199
|
+
})
|
|
200
|
+
]
|
|
201
|
+
}),
|
|
202
|
+
helpErrorPosition: "bottom",
|
|
203
|
+
error: errors.name ? errors.name?.type === "string.pattern.base" ? bucketErrorMessage : errors.name?.message : void 0
|
|
204
|
+
}),
|
|
205
|
+
children,
|
|
206
|
+
/*#__PURE__*/ jsx(FormGroup, {
|
|
207
|
+
id: "isVersioning",
|
|
208
|
+
label: "Versioning",
|
|
209
|
+
disabled: isObjectLockEnabled,
|
|
210
|
+
labelHelpTooltip: /*#__PURE__*/ jsxs("ul", {
|
|
211
|
+
children: [
|
|
212
|
+
/*#__PURE__*/ jsx("li", {
|
|
213
|
+
children: "Versioning keeps multiple versions of each objects in your bucket. You can restore deleted or overwritten objects as a result of unintended user actions or application failures."
|
|
214
|
+
}),
|
|
215
|
+
/*#__PURE__*/ jsx("li", {
|
|
216
|
+
children: "It's possible to enable and suspend versioning at the bucket level after the bucket creation."
|
|
217
|
+
})
|
|
218
|
+
]
|
|
219
|
+
}),
|
|
220
|
+
help: isObjectLockEnabled ? "Automatically activated when Object-lock is Enabled" : void 0,
|
|
221
|
+
helpErrorPosition: "bottom",
|
|
222
|
+
content: /*#__PURE__*/ jsx(Checkbox, {
|
|
223
|
+
id: "isVersioning",
|
|
224
|
+
disabled: isObjectLockEnabled,
|
|
225
|
+
...register("isVersioning")
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
]
|
|
229
|
+
})
|
|
230
|
+
}),
|
|
231
|
+
/*#__PURE__*/ jsx(ObjectLockRetentionSettings, {})
|
|
232
|
+
]
|
|
233
|
+
})
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
const buckets_BucketCreate = BucketCreate;
|
|
237
|
+
export { BucketCreate, baseBucketCreateSchema, bucketErrorMessage, bucketNameValidationSchema, buckets_BucketCreate as default };
|