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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
3
+ import user_event from "@testing-library/user-event";
4
+ import { MemoryRouter, Route, Routes } from "react-router-dom";
5
+ import { createTestWrapper } from "../../test/testUtils.js";
6
+ import { BucketNotificationCreatePage } from "../buckets/notifications/BucketNotificationCreatePage.js";
7
+ import { useGetBucketNotification, useSetBucketNotification } from "../../hooks/index.js";
8
+ jest.mock("../../hooks", ()=>({
9
+ useGetBucketNotification: jest.fn(),
10
+ useSetBucketNotification: jest.fn()
11
+ }));
12
+ const mockUseGetBucketNotification = jest.mocked(useGetBucketNotification);
13
+ const mockUseSetBucketNotification = jest.mocked(useSetBucketNotification);
14
+ const mockNavigate = jest.fn();
15
+ jest.mock("react-router-dom", ()=>({
16
+ ...jest.requireActual("react-router-dom"),
17
+ useNavigate: ()=>mockNavigate
18
+ }));
19
+ const renderBucketNotificationCreatePage = (bucketName = "test-bucket")=>{
20
+ const Wrapper = createTestWrapper();
21
+ return render(/*#__PURE__*/ jsx(MemoryRouter, {
22
+ initialEntries: [
23
+ `/buckets/${bucketName}/notifications/create`
24
+ ],
25
+ children: /*#__PURE__*/ jsx(Wrapper, {
26
+ children: /*#__PURE__*/ jsx(Routes, {
27
+ children: /*#__PURE__*/ jsx(Route, {
28
+ path: "/buckets/:bucketName/notifications/create",
29
+ element: /*#__PURE__*/ jsx(BucketNotificationCreatePage, {})
30
+ })
31
+ })
32
+ })
33
+ }));
34
+ };
35
+ describe("BucketNotificationCreatePage", ()=>{
36
+ const mockMutate = jest.fn();
37
+ beforeEach(()=>{
38
+ jest.clearAllMocks();
39
+ mockNavigate.mockClear();
40
+ mockUseGetBucketNotification.mockReturnValue({
41
+ data: void 0
42
+ });
43
+ mockUseSetBucketNotification.mockReturnValue({
44
+ mutate: mockMutate,
45
+ isPending: false
46
+ });
47
+ });
48
+ it("renders create notification form with all fields", ()=>{
49
+ renderBucketNotificationCreatePage();
50
+ expect(screen.getByText("Create Bucket Notification")).toBeInTheDocument();
51
+ expect(screen.getByLabelText(/rule name/i)).toBeInTheDocument();
52
+ expect(screen.getByText("Events")).toBeInTheDocument();
53
+ expect(screen.getByLabelText(/destination queue/i)).toBeInTheDocument();
54
+ expect(screen.getByLabelText(/prefix/i)).toBeInTheDocument();
55
+ expect(screen.getByLabelText(/suffix/i)).toBeInTheDocument();
56
+ });
57
+ it("disables create button when form is pristine", ()=>{
58
+ renderBucketNotificationCreatePage();
59
+ expect(screen.getByRole("button", {
60
+ name: /create/i
61
+ })).toBeDisabled();
62
+ });
63
+ it("shows validation error when rule name is empty", async ()=>{
64
+ renderBucketNotificationCreatePage();
65
+ const ruleNameInput = screen.getByLabelText(/rule name/i);
66
+ await user_event.type(ruleNameInput, "a");
67
+ await user_event.clear(ruleNameInput);
68
+ await waitFor(()=>{
69
+ expect(screen.getByText(/this field is required/i)).toBeInTheDocument();
70
+ });
71
+ });
72
+ it("shows validation error when rule name already exists", async ()=>{
73
+ const existingConfig = {
74
+ QueueConfigurations: [
75
+ {
76
+ Id: "existing-rule",
77
+ QueueArn: "arn:aws:sqs:us-east-1:123:queue",
78
+ Events: [
79
+ "s3:ObjectCreated:*"
80
+ ]
81
+ }
82
+ ]
83
+ };
84
+ mockUseGetBucketNotification.mockReturnValue({
85
+ data: existingConfig
86
+ });
87
+ renderBucketNotificationCreatePage();
88
+ const ruleNameInput = screen.getByLabelText(/rule name/i);
89
+ await user_event.type(ruleNameInput, "existing-rule");
90
+ await waitFor(()=>{
91
+ expect(screen.getByText(/a rule with this name already exists/i)).toBeInTheDocument();
92
+ });
93
+ });
94
+ it("shows validation error when queue ARN is invalid", async ()=>{
95
+ renderBucketNotificationCreatePage();
96
+ const queueArnInput = screen.getByLabelText(/destination queue/i);
97
+ await user_event.type(queueArnInput, "invalid-arn");
98
+ await waitFor(()=>{
99
+ expect(screen.getByText(/must be a valid arn/i)).toBeInTheDocument();
100
+ });
101
+ });
102
+ it("shows validation error when no event is selected", async ()=>{
103
+ renderBucketNotificationCreatePage();
104
+ const ruleNameInput = screen.getByLabelText(/rule name/i);
105
+ await user_event.type(ruleNameInput, "test-rule");
106
+ const queueArnInput = screen.getByLabelText(/destination queue/i);
107
+ await user_event.type(queueArnInput, "arn:aws:sqs:us-east-1:123:queue");
108
+ await waitFor(()=>{
109
+ expect(screen.getByRole("button", {
110
+ name: /create/i
111
+ })).toBeDisabled();
112
+ });
113
+ });
114
+ it("enables create button when form is valid", async ()=>{
115
+ renderBucketNotificationCreatePage();
116
+ await user_event.type(screen.getByLabelText(/rule name/i), "test-rule");
117
+ await user_event.type(screen.getByLabelText(/destination queue/i), "arn:aws:sqs:us-east-1:123:queue");
118
+ const checkbox = screen.getByRole("checkbox", {
119
+ name: /s3:ObjectCreated:\*/i
120
+ });
121
+ fireEvent.click(checkbox);
122
+ await waitFor(()=>{
123
+ expect(screen.getByRole("button", {
124
+ name: /create/i
125
+ })).toBeEnabled();
126
+ });
127
+ });
128
+ it("creates notification with all fields and navigates on success", async ()=>{
129
+ renderBucketNotificationCreatePage();
130
+ await user_event.type(screen.getByLabelText(/rule name/i), "test-rule");
131
+ await user_event.type(screen.getByLabelText(/destination queue/i), "arn:aws:sqs:us-east-1:123:my-queue");
132
+ fireEvent.click(screen.getByRole("checkbox", {
133
+ name: /s3:ObjectCreated:Put/i
134
+ }));
135
+ await user_event.type(screen.getByLabelText(/prefix/i), "uploads/");
136
+ await user_event.type(screen.getByLabelText(/suffix/i), ".jpg");
137
+ mockMutate.mockImplementation((_, options)=>{
138
+ options?.onSuccess?.();
139
+ });
140
+ const createButton = screen.getByRole("button", {
141
+ name: /create/i
142
+ });
143
+ await waitFor(()=>expect(createButton).toBeEnabled());
144
+ fireEvent.click(createButton);
145
+ await waitFor(()=>{
146
+ expect(mockMutate).toHaveBeenCalledWith({
147
+ Bucket: "test-bucket",
148
+ NotificationConfiguration: {
149
+ QueueConfigurations: [
150
+ {
151
+ Id: "test-rule",
152
+ QueueArn: "arn:aws:sqs:us-east-1:123:my-queue",
153
+ Events: [
154
+ "s3:ObjectCreated:Put"
155
+ ],
156
+ Filter: {
157
+ Key: {
158
+ FilterRules: [
159
+ {
160
+ Name: "prefix",
161
+ Value: "uploads/"
162
+ },
163
+ {
164
+ Name: "suffix",
165
+ Value: ".jpg"
166
+ }
167
+ ]
168
+ }
169
+ }
170
+ }
171
+ ]
172
+ }
173
+ }, expect.any(Object));
174
+ expect(mockNavigate).toHaveBeenCalledWith("/buckets/test-bucket");
175
+ });
176
+ });
177
+ it("creates notification without filters when not provided", async ()=>{
178
+ renderBucketNotificationCreatePage();
179
+ await user_event.type(screen.getByLabelText(/rule name/i), "test-rule");
180
+ await user_event.type(screen.getByLabelText(/destination queue/i), "arn:aws:sqs:us-east-1:123:my-queue");
181
+ fireEvent.click(screen.getByRole("checkbox", {
182
+ name: /s3:ObjectRemoved:\*/i
183
+ }));
184
+ mockMutate.mockImplementation((_, options)=>{
185
+ options?.onSuccess?.();
186
+ });
187
+ const createButton = screen.getByRole("button", {
188
+ name: /create/i
189
+ });
190
+ await waitFor(()=>expect(createButton).toBeEnabled());
191
+ fireEvent.click(createButton);
192
+ await waitFor(()=>{
193
+ expect(mockMutate).toHaveBeenCalledWith({
194
+ Bucket: "test-bucket",
195
+ NotificationConfiguration: {
196
+ QueueConfigurations: [
197
+ {
198
+ Id: "test-rule",
199
+ QueueArn: "arn:aws:sqs:us-east-1:123:my-queue",
200
+ Events: [
201
+ "s3:ObjectRemoved:*"
202
+ ]
203
+ }
204
+ ]
205
+ }
206
+ }, expect.any(Object));
207
+ });
208
+ });
209
+ it("preserves existing queue configurations when creating new notification", async ()=>{
210
+ const existingConfig = {
211
+ QueueConfigurations: [
212
+ {
213
+ Id: "existing-rule",
214
+ QueueArn: "arn:aws:sqs:us-east-1:123:existing-queue",
215
+ Events: [
216
+ "s3:ObjectCreated:*"
217
+ ]
218
+ }
219
+ ]
220
+ };
221
+ mockUseGetBucketNotification.mockReturnValue({
222
+ data: existingConfig
223
+ });
224
+ renderBucketNotificationCreatePage();
225
+ await user_event.type(screen.getByLabelText(/rule name/i), "new-rule");
226
+ await user_event.type(screen.getByLabelText(/destination queue/i), "arn:aws:sqs:us-east-1:123:new-queue");
227
+ fireEvent.click(screen.getByRole("checkbox", {
228
+ name: /^s3:ObjectRemoved:Delete$/i
229
+ }));
230
+ mockMutate.mockImplementation((_, options)=>{
231
+ options?.onSuccess?.();
232
+ });
233
+ const createButton = screen.getByRole("button", {
234
+ name: /create/i
235
+ });
236
+ await waitFor(()=>expect(createButton).toBeEnabled());
237
+ fireEvent.click(createButton);
238
+ await waitFor(()=>{
239
+ const call = mockMutate.mock.calls[0][0];
240
+ expect(call.NotificationConfiguration.QueueConfigurations).toHaveLength(2);
241
+ expect(call.NotificationConfiguration.QueueConfigurations[0]).toEqual(existingConfig.QueueConfigurations[0]);
242
+ expect(call.NotificationConfiguration.QueueConfigurations[1]).toMatchObject({
243
+ Id: "new-rule",
244
+ QueueArn: "arn:aws:sqs:us-east-1:123:new-queue",
245
+ Events: [
246
+ "s3:ObjectRemoved:Delete"
247
+ ]
248
+ });
249
+ });
250
+ });
251
+ it("displays error message when creation fails", async ()=>{
252
+ renderBucketNotificationCreatePage();
253
+ await user_event.type(screen.getByLabelText(/rule name/i), "test-rule");
254
+ await user_event.type(screen.getByLabelText(/destination queue/i), "arn:aws:sqs:us-east-1:123:my-queue");
255
+ fireEvent.click(screen.getByRole("checkbox", {
256
+ name: /s3:ObjectCreated:\*/i
257
+ }));
258
+ const error = new Error("Access Denied");
259
+ mockMutate.mockImplementation((_, options)=>{
260
+ options?.onError?.(error);
261
+ });
262
+ const createButton = screen.getByRole("button", {
263
+ name: /create/i
264
+ });
265
+ await waitFor(()=>expect(createButton).toBeEnabled());
266
+ fireEvent.click(createButton);
267
+ await waitFor(()=>{
268
+ expect(mockNavigate).not.toHaveBeenCalled();
269
+ });
270
+ });
271
+ it("handles cancel button click and navigates back", ()=>{
272
+ renderBucketNotificationCreatePage();
273
+ const cancelButton = screen.getByRole("button", {
274
+ name: /cancel/i
275
+ });
276
+ fireEvent.click(cancelButton);
277
+ expect(mockNavigate).toHaveBeenCalledWith("/buckets/test-bucket");
278
+ });
279
+ it("allows selecting multiple events", async ()=>{
280
+ renderBucketNotificationCreatePage();
281
+ await user_event.type(screen.getByLabelText(/rule name/i), "multi-event");
282
+ await user_event.type(screen.getByLabelText(/destination queue/i), "arn:aws:sqs:us-east-1:123:queue");
283
+ fireEvent.click(screen.getByRole("checkbox", {
284
+ name: /^s3:ObjectCreated:Put$/i
285
+ }));
286
+ fireEvent.click(screen.getByRole("checkbox", {
287
+ name: /^s3:ObjectCreated:Copy$/i
288
+ }));
289
+ fireEvent.click(screen.getByRole("checkbox", {
290
+ name: /^s3:ObjectRemoved:Delete$/i
291
+ }));
292
+ mockMutate.mockImplementation((_, options)=>{
293
+ options?.onSuccess?.();
294
+ });
295
+ const createButton = screen.getByRole("button", {
296
+ name: /create/i
297
+ });
298
+ await waitFor(()=>expect(createButton).toBeEnabled());
299
+ fireEvent.click(createButton);
300
+ await waitFor(()=>{
301
+ expect(mockMutate).toHaveBeenCalledWith(expect.objectContaining({
302
+ NotificationConfiguration: {
303
+ QueueConfigurations: [
304
+ expect.objectContaining({
305
+ Events: expect.arrayContaining([
306
+ "s3:ObjectCreated:Put",
307
+ "s3:ObjectCreated:Copy",
308
+ "s3:ObjectRemoved:Delete"
309
+ ])
310
+ })
311
+ ]
312
+ }
313
+ }), expect.any(Object));
314
+ });
315
+ });
316
+ });
@@ -0,0 +1 @@
1
+ export declare function BucketNotificationCreatePage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,234 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { joiResolver } from "@hookform/resolvers/joi";
3
+ import { Form, FormGroup, FormSection, Loader, Stack, Text, useToast } from "@scality/core-ui";
4
+ import { convertRemToPixels } from "@scality/core-ui/dist/components/tablev2/TableUtils";
5
+ import { Button, Input } from "@scality/core-ui/dist/next";
6
+ import { array, object, string } from "joi";
7
+ import { useCallback } from "react";
8
+ import { FormProvider, useForm } from "react-hook-form";
9
+ import { useNavigate, useParams } from "react-router-dom";
10
+ import { useGetBucketNotification, useSetBucketNotification } from "../../../hooks/index.js";
11
+ import { EventsSection } from "./EventsSection.js";
12
+ const schema = object({
13
+ ruleName: string().required().messages({
14
+ "string.empty": "This field is required"
15
+ }),
16
+ queueArn: string().required().pattern(/^arn:aws:sqs:[a-z0-9-]+:\d+:.+$/).messages({
17
+ "string.empty": "This field is required",
18
+ "string.pattern.base": "Must be a valid ARN (e.g., arn:aws:sqs:region:account-id:queue-name)"
19
+ }),
20
+ events: array().min(1).required().messages({
21
+ "array.min": "At least one event must be selected"
22
+ }),
23
+ prefix: string().allow("").optional(),
24
+ suffix: string().allow("").optional()
25
+ });
26
+ function BucketNotificationCreatePage() {
27
+ const { bucketName } = useParams();
28
+ const navigate = useNavigate();
29
+ const { showToast } = useToast();
30
+ const { data: existingNotificationData, status: notificationStatus } = useGetBucketNotification({
31
+ Bucket: bucketName
32
+ });
33
+ const existingRuleNames = existingNotificationData?.QueueConfigurations?.map((config)=>config.Id) || [];
34
+ const dynamicSchema = schema.keys({
35
+ ruleName: string().required().invalid(...existingRuleNames).messages({
36
+ "string.empty": "This field is required",
37
+ "any.invalid": "A rule with this name already exists"
38
+ })
39
+ });
40
+ const methods = useForm({
41
+ resolver: joiResolver(dynamicSchema),
42
+ mode: "onChange",
43
+ defaultValues: {
44
+ ruleName: "",
45
+ queueArn: "",
46
+ events: [],
47
+ prefix: "",
48
+ suffix: ""
49
+ }
50
+ });
51
+ const { mutate: setNotification, isPending: isSaving } = useSetBucketNotification();
52
+ const { handleSubmit, register, formState: { isValid, isDirty, errors } } = methods;
53
+ const handleCancel = useCallback(()=>{
54
+ navigate(`/buckets/${bucketName}`);
55
+ }, [
56
+ navigate,
57
+ bucketName
58
+ ]);
59
+ const onSubmit = useCallback((data)=>{
60
+ if (!bucketName) return;
61
+ const filterRules = [
62
+ ...data.prefix ? [
63
+ {
64
+ Name: "prefix",
65
+ Value: data.prefix
66
+ }
67
+ ] : [],
68
+ ...data.suffix ? [
69
+ {
70
+ Name: "suffix",
71
+ Value: data.suffix
72
+ }
73
+ ] : []
74
+ ];
75
+ const filteredEvents = data.events.filter((event)=>{
76
+ if (event.endsWith("*")) return true;
77
+ if (event.startsWith("s3:ObjectCreated:")) return !data.events.includes("s3:ObjectCreated:*");
78
+ if (event.startsWith("s3:ObjectRemoved:")) return !data.events.includes("s3:ObjectRemoved:*");
79
+ if (event.startsWith("s3:LifecycleExpiration:")) return !data.events.includes("s3:LifecycleExpiration:*");
80
+ return true;
81
+ });
82
+ const newQueueConfiguration = {
83
+ Id: data.ruleName,
84
+ QueueArn: data.queueArn,
85
+ Events: filteredEvents,
86
+ ...filterRules.length > 0 && {
87
+ Filter: {
88
+ Key: {
89
+ FilterRules: filterRules
90
+ }
91
+ }
92
+ }
93
+ };
94
+ setNotification({
95
+ Bucket: bucketName,
96
+ NotificationConfiguration: {
97
+ QueueConfigurations: [
98
+ ...existingNotificationData?.QueueConfigurations || [],
99
+ newQueueConfiguration
100
+ ]
101
+ }
102
+ }, {
103
+ onSuccess: ()=>{
104
+ showToast({
105
+ open: true,
106
+ message: "Notification created successfully",
107
+ status: "success"
108
+ });
109
+ navigate(`/buckets/${bucketName}`);
110
+ },
111
+ onError: (error)=>{
112
+ const errorMessage = error instanceof Error ? error.message : "Failed to create notification";
113
+ showToast({
114
+ open: true,
115
+ message: errorMessage,
116
+ status: "error"
117
+ });
118
+ }
119
+ });
120
+ }, [
121
+ bucketName,
122
+ setNotification,
123
+ navigate,
124
+ showToast,
125
+ existingNotificationData
126
+ ]);
127
+ if ("pending" === notificationStatus) return /*#__PURE__*/ jsx(Loader, {
128
+ centered: true,
129
+ children: /*#__PURE__*/ jsx(Text, {
130
+ children: "Loading..."
131
+ })
132
+ });
133
+ return /*#__PURE__*/ jsx(FormProvider, {
134
+ ...methods,
135
+ children: /*#__PURE__*/ jsxs(Form, {
136
+ layout: {
137
+ kind: "page",
138
+ title: "Create Bucket Notification"
139
+ },
140
+ requireMode: "partial",
141
+ onSubmit: handleSubmit(onSubmit),
142
+ rightActions: /*#__PURE__*/ jsxs(Stack, {
143
+ gap: "r16",
144
+ children: [
145
+ /*#__PURE__*/ jsx(Button, {
146
+ id: "cancel-btn",
147
+ variant: "outline",
148
+ type: "button",
149
+ label: "Cancel",
150
+ onClick: handleCancel,
151
+ disabled: isSaving
152
+ }),
153
+ /*#__PURE__*/ jsx(Button, {
154
+ id: "create-notification-btn",
155
+ type: "submit",
156
+ variant: "primary",
157
+ label: "Create",
158
+ disabled: !isDirty || !isValid || isSaving
159
+ })
160
+ ]
161
+ }),
162
+ children: [
163
+ /*#__PURE__*/ jsx(FormSection, {
164
+ forceLabelWidth: convertRemToPixels(13),
165
+ children: /*#__PURE__*/ jsx(FormGroup, {
166
+ label: "Rule name",
167
+ id: "ruleName",
168
+ direction: "horizontal",
169
+ error: errors?.ruleName?.message,
170
+ helpErrorPosition: "bottom",
171
+ labelHelpTooltip: /*#__PURE__*/ jsx(Fragment, {}),
172
+ required: true,
173
+ content: /*#__PURE__*/ jsx(Input, {
174
+ id: "ruleName",
175
+ ...register("ruleName")
176
+ })
177
+ })
178
+ }),
179
+ /*#__PURE__*/ jsx(EventsSection, {}),
180
+ /*#__PURE__*/ jsx(FormSection, {
181
+ forceLabelWidth: convertRemToPixels(13),
182
+ children: /*#__PURE__*/ jsx(FormGroup, {
183
+ label: "Destination Queue",
184
+ id: "queueArn",
185
+ direction: "horizontal",
186
+ error: errors?.queueArn?.message,
187
+ helpErrorPosition: "bottom",
188
+ labelHelpTooltip: /*#__PURE__*/ jsx(Fragment, {}),
189
+ required: true,
190
+ content: /*#__PURE__*/ jsx(Input, {
191
+ id: "queueArn",
192
+ placeholder: "arn:aws:sqs:us-east-1:1xx:name",
193
+ ...register("queueArn")
194
+ })
195
+ })
196
+ }),
197
+ /*#__PURE__*/ jsx(FormSection, {
198
+ title: {
199
+ name: "Filters"
200
+ },
201
+ children: /*#__PURE__*/ jsxs(Stack, {
202
+ direction: "horizontal",
203
+ gap: "r16",
204
+ children: [
205
+ /*#__PURE__*/ jsx(FormGroup, {
206
+ label: "Prefix",
207
+ id: "prefix",
208
+ direction: "horizontal",
209
+ content: /*#__PURE__*/ jsx(Input, {
210
+ id: "prefix",
211
+ placeholder: "",
212
+ size: "2/3",
213
+ ...register("prefix")
214
+ })
215
+ }),
216
+ /*#__PURE__*/ jsx(FormGroup, {
217
+ label: "Suffix",
218
+ id: "suffix",
219
+ direction: "horizontal",
220
+ content: /*#__PURE__*/ jsx(Input, {
221
+ id: "suffix",
222
+ placeholder: "",
223
+ size: "2/3",
224
+ ...register("suffix")
225
+ })
226
+ })
227
+ ]
228
+ })
229
+ })
230
+ ]
231
+ })
232
+ });
233
+ }
234
+ export { BucketNotificationCreatePage };
@@ -0,0 +1 @@
1
+ export declare function EventsSection(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,123 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Checkbox, FormGroup, FormSection, Stack, Text, Wrap } from "@scality/core-ui";
3
+ import { useCallback } from "react";
4
+ import { Controller, useFormContext } from "react-hook-form";
5
+ import { objectCreationEvents, objectDeletionEvents, othersEvents } from "./events.js";
6
+ import { Box } from "@scality/core-ui/dist/next";
7
+ function EventsSection() {
8
+ const { control, formState: { errors } } = useFormContext();
9
+ const toggleEvent = useCallback((currentEvents, eventValue, onChange)=>{
10
+ const isWildcard = eventValue.endsWith("*");
11
+ const isSelected = currentEvents.includes(eventValue);
12
+ if (isWildcard) if (isSelected) {
13
+ const relatedEvents = eventValue.startsWith("s3:ObjectCreated") ? objectCreationEvents.events : eventValue.startsWith("s3:ObjectRemoved") ? objectDeletionEvents.events : eventValue.startsWith("s3:LifecycleExpiration") ? othersEvents.events.filter((e)=>e.startsWith("s3:LifecycleExpiration")) : [
14
+ eventValue
15
+ ];
16
+ onChange(currentEvents.filter((e)=>!relatedEvents.includes(e)));
17
+ } else {
18
+ const relatedEvents = eventValue.startsWith("s3:ObjectCreated") ? objectCreationEvents.events : eventValue.startsWith("s3:ObjectRemoved") ? objectDeletionEvents.events : eventValue.startsWith("s3:LifecycleExpiration") ? othersEvents.events.filter((e)=>e.startsWith("s3:LifecycleExpiration")) : [
19
+ eventValue
20
+ ];
21
+ const newEvents = [
22
+ ...currentEvents.filter((e)=>!relatedEvents.includes(e)),
23
+ ...relatedEvents
24
+ ];
25
+ onChange(newEvents);
26
+ }
27
+ else onChange(isSelected ? currentEvents.filter((e)=>e !== eventValue) : [
28
+ ...currentEvents,
29
+ eventValue
30
+ ]);
31
+ }, []);
32
+ const isEventDisabled = (event, selectedEvents)=>{
33
+ if (event.endsWith("*")) return false;
34
+ if (event.startsWith("s3:ObjectCreated")) return selectedEvents.includes("s3:ObjectCreated:*");
35
+ if (event.startsWith("s3:ObjectRemoved")) return selectedEvents.includes("s3:ObjectRemoved:*");
36
+ if (event.startsWith("s3:LifecycleExpiration")) return selectedEvents.includes("s3:LifecycleExpiration:*");
37
+ return false;
38
+ };
39
+ return /*#__PURE__*/ jsxs(FormSection, {
40
+ children: [
41
+ /*#__PURE__*/ jsx(Wrap, {
42
+ children: /*#__PURE__*/ jsx(Box, {
43
+ display: "flex",
44
+ flexDirection: "row",
45
+ gap: "r8",
46
+ children: /*#__PURE__*/ jsx(Text, {
47
+ isEmphazed: true,
48
+ color: "textPrimary",
49
+ children: "Events"
50
+ })
51
+ })
52
+ }),
53
+ /*#__PURE__*/ jsx(FormGroup, {
54
+ id: "events",
55
+ label: "",
56
+ direction: "vertical",
57
+ error: errors?.events?.message,
58
+ content: /*#__PURE__*/ jsx(Controller, {
59
+ control: control,
60
+ name: "events",
61
+ render: ({ field: { value, onChange } })=>/*#__PURE__*/ jsxs(Stack, {
62
+ direction: "vertical",
63
+ gap: "r16",
64
+ children: [
65
+ /*#__PURE__*/ jsxs(Stack, {
66
+ direction: "vertical",
67
+ gap: "r8",
68
+ children: [
69
+ /*#__PURE__*/ jsx(Text, {
70
+ color: "textPrimary",
71
+ children: objectCreationEvents.label
72
+ }),
73
+ objectCreationEvents.events.map((event)=>/*#__PURE__*/ jsx(Checkbox, {
74
+ id: event,
75
+ label: event,
76
+ checked: value.includes(event),
77
+ disabled: isEventDisabled(event, value),
78
+ onChange: ()=>toggleEvent(value, event, onChange)
79
+ }, event))
80
+ ]
81
+ }),
82
+ /*#__PURE__*/ jsxs(Stack, {
83
+ direction: "vertical",
84
+ gap: "r8",
85
+ children: [
86
+ /*#__PURE__*/ jsx(Text, {
87
+ color: "textPrimary",
88
+ children: objectDeletionEvents.label
89
+ }),
90
+ objectDeletionEvents.events.map((event)=>/*#__PURE__*/ jsx(Checkbox, {
91
+ id: event,
92
+ label: event,
93
+ checked: value.includes(event),
94
+ disabled: isEventDisabled(event, value),
95
+ onChange: ()=>toggleEvent(value, event, onChange)
96
+ }, event))
97
+ ]
98
+ }),
99
+ /*#__PURE__*/ jsxs(Stack, {
100
+ direction: "vertical",
101
+ gap: "r8",
102
+ children: [
103
+ /*#__PURE__*/ jsx(Text, {
104
+ color: "textPrimary",
105
+ children: othersEvents.label
106
+ }),
107
+ othersEvents.events.map((event)=>/*#__PURE__*/ jsx(Checkbox, {
108
+ id: event,
109
+ label: event,
110
+ checked: value.includes(event),
111
+ disabled: isEventDisabled(event, value),
112
+ onChange: ()=>toggleEvent(value, event, onChange)
113
+ }, event))
114
+ ]
115
+ })
116
+ ]
117
+ })
118
+ })
119
+ })
120
+ ]
121
+ });
122
+ }
123
+ export { EventsSection };
@@ -0,0 +1,12 @@
1
+ export declare const objectCreationEvents: {
2
+ label: string;
3
+ events: string[];
4
+ };
5
+ export declare const objectDeletionEvents: {
6
+ label: string;
7
+ events: string[];
8
+ };
9
+ export declare const othersEvents: {
10
+ label: string;
11
+ events: string[];
12
+ };
@@ -0,0 +1,27 @@
1
+ const objectCreationEvents = {
2
+ label: "Object creation",
3
+ events: [
4
+ "s3:ObjectCreated:*",
5
+ "s3:ObjectCreated:Put",
6
+ "s3:ObjectCreated:Copy",
7
+ "s3:ObjectCreated:CompleteMultipartUpload"
8
+ ]
9
+ };
10
+ const objectDeletionEvents = {
11
+ label: "Object deletion",
12
+ events: [
13
+ "s3:ObjectRemoved:*",
14
+ "s3:ObjectRemoved:Delete",
15
+ "s3:ObjectRemoved:DeleteMarkerCreated"
16
+ ]
17
+ };
18
+ const othersEvents = {
19
+ label: "Others",
20
+ events: [
21
+ "s3:Replication:OperationFailedReplication",
22
+ "s3:LifecycleExpiration:*",
23
+ "s3:LifecycleExpiration:DeleteMarkerCreated",
24
+ "s3:LifecycleExpiration:Delete"
25
+ ]
26
+ };
27
+ export { objectCreationEvents, objectDeletionEvents, othersEvents };
@@ -3,6 +3,7 @@ export { BucketList } from "./buckets/BucketList";
3
3
  export { BucketOverview } from "./buckets/BucketOverview";
4
4
  export { BucketPage } from "./buckets/BucketPage";
5
5
  export { BucketPolicyPage } from "./buckets/BucketPolicyPage";
6
+ export { BucketNotificationCreatePage } from "./buckets/notifications/BucketNotificationCreatePage";
6
7
  export { UploadButton } from "./objects/UploadButton";
7
8
  export { CreateFolderButton } from "./objects/CreateFolderButton";
8
9
  export { ObjectDetails } from "./objects/ObjectDetails";
@@ -3,6 +3,7 @@ import { BucketList } from "./buckets/BucketList.js";
3
3
  import { BucketOverview } from "./buckets/BucketOverview.js";
4
4
  import { BucketPage } from "./buckets/BucketPage.js";
5
5
  import { BucketPolicyPage } from "./buckets/BucketPolicyPage.js";
6
+ import { BucketNotificationCreatePage } from "./buckets/notifications/BucketNotificationCreatePage.js";
6
7
  import { UploadButton } from "./objects/UploadButton.js";
7
8
  import { CreateFolderButton } from "./objects/CreateFolderButton.js";
8
9
  import { ObjectDetails } from "./objects/ObjectDetails/index.js";
@@ -10,4 +11,4 @@ import { ObjectList } from "./objects/ObjectList.js";
10
11
  import { ObjectPage } from "./objects/ObjectPage.js";
11
12
  import { MetadataSearch } from "./search/MetadataSearch.js";
12
13
  import { DataBrowserProvider, useDataBrowserContext, useDataBrowserTheme } from "./providers/DataBrowserProvider.js";
13
- export { BucketList, BucketOverview, BucketPage, BucketPolicyPage, CreateFolderButton, DataBrowserProvider, DeleteBucketButton, MetadataSearch, ObjectDetails, ObjectList, ObjectPage, UploadButton, useDataBrowserContext, useDataBrowserTheme };
14
+ export { BucketList, BucketNotificationCreatePage, BucketOverview, BucketPage, BucketPolicyPage, CreateFolderButton, DataBrowserProvider, DeleteBucketButton, MetadataSearch, ObjectDetails, ObjectList, ObjectPage, UploadButton, useDataBrowserContext, useDataBrowserTheme };
@@ -23,8 +23,8 @@ describe("useIsBucketEmpty", ()=>{
23
23
  data: {
24
24
  pages: [
25
25
  {
26
- objects: [],
27
- commonPrefixes: []
26
+ Contents: [],
27
+ CommonPrefixes: []
28
28
  }
29
29
  ]
30
30
  },
@@ -40,12 +40,12 @@ describe("useIsBucketEmpty", ()=>{
40
40
  data: {
41
41
  pages: [
42
42
  {
43
- objects: [
43
+ Contents: [
44
44
  {
45
45
  Key: "file.txt"
46
46
  }
47
47
  ],
48
- commonPrefixes: []
48
+ CommonPrefixes: []
49
49
  }
50
50
  ]
51
51
  },
@@ -61,8 +61,8 @@ describe("useIsBucketEmpty", ()=>{
61
61
  data: {
62
62
  pages: [
63
63
  {
64
- objects: [],
65
- commonPrefixes: [
64
+ Contents: [],
65
+ CommonPrefixes: [
66
66
  {
67
67
  Prefix: "folder/"
68
68
  }
@@ -94,7 +94,7 @@ describe("useIsBucketEmpty", ()=>{
94
94
  data: {
95
95
  pages: [
96
96
  {
97
- objects: [
97
+ Contents: [
98
98
  {
99
99
  Key: void 0
100
100
  },
@@ -102,7 +102,7 @@ describe("useIsBucketEmpty", ()=>{
102
102
  Key: ""
103
103
  }
104
104
  ],
105
- commonPrefixes: [
105
+ CommonPrefixes: [
106
106
  {
107
107
  Prefix: null
108
108
  },
@@ -13,12 +13,12 @@ const useIsBucketEmpty = (bucketName)=>{
13
13
  if ("pending" === status || !data) return null;
14
14
  if (!isInfiniteData(data)) return null;
15
15
  for (const page of data.pages){
16
- if (page?.objects && page.objects.length > 0) {
17
- const validObjects = page.objects.filter((obj)=>obj?.Key);
16
+ if (page?.Contents && page.Contents.length > 0) {
17
+ const validObjects = page.Contents.filter((obj)=>obj?.Key);
18
18
  if (validObjects.length > 0) return false;
19
19
  }
20
- if (page?.commonPrefixes && page.commonPrefixes.length > 0) {
21
- const validPrefixes = page.commonPrefixes.filter((cp)=>cp?.Prefix);
20
+ if (page?.CommonPrefixes && page.CommonPrefixes.length > 0) {
21
+ const validPrefixes = page.CommonPrefixes.filter((cp)=>cp?.Prefix);
22
22
  if (validPrefixes.length > 0) return false;
23
23
  }
24
24
  }
@@ -22,7 +22,7 @@ var __webpack_require__ = {};
22
22
  const testConfig = {
23
23
  endpoint: "https://s3.amazonaws.com",
24
24
  region: "us-east-1",
25
- forcePathStyle: false
25
+ forcePathStyle: true
26
26
  };
27
27
  const testCredentials = {
28
28
  accessKeyId: "test-access-key",
@@ -8,6 +8,8 @@ export interface S3BrowserConfig extends Omit<S3ClientConfig, "credentials"> {
8
8
  useDevProxy?: boolean;
9
9
  realS3Host?: string;
10
10
  proxyPath?: string;
11
+ proxyHost?: string;
12
+ proxyPort?: number;
11
13
  publicAclIndicator?: string;
12
14
  }
13
15
  export interface S3Credentials extends AwsCredentialIdentity {
@@ -1,35 +1,36 @@
1
1
  import { S3Client } from "@aws-sdk/client-s3";
2
2
  import { createProxyMiddleware } from "./proxyMiddleware.js";
3
- import { getBuildInfo, getProxyConfig, shouldUseProxy } from "../config/factory.js";
3
+ import { S3ConfigurationFactory } from "../config/factory.js";
4
+ function extractProxyConfig(config) {
5
+ if (config.useDevProxy && config.realS3Host && config.proxyPath) return {
6
+ realS3Host: config.realS3Host,
7
+ proxyBasePath: config.proxyPath,
8
+ proxyHost: config.proxyHost || "localhost",
9
+ proxyPort: config.proxyPort || 8084
10
+ };
11
+ const proxyConfig = S3ConfigurationFactory.createProxyConfiguration();
12
+ if (proxyConfig.useProxy && proxyConfig.proxyConfig) return {
13
+ realS3Host: proxyConfig.proxyConfig.realHost,
14
+ proxyBasePath: proxyConfig.proxyConfig.proxyBasePath,
15
+ proxyHost: proxyConfig.proxyConfig.proxyHost,
16
+ proxyPort: proxyConfig.proxyConfig.proxyPort
17
+ };
18
+ return null;
19
+ }
4
20
  const createS3Client = (config)=>{
5
21
  const client = new S3Client({
6
22
  ...config,
7
23
  credentials: config.credentials,
8
- forcePathStyle: true,
24
+ forcePathStyle: config.forcePathStyle ?? true,
9
25
  region: config.region
10
26
  });
11
- if (shouldUseProxy()) {
12
- const proxyConfig = getProxyConfig();
13
- if (proxyConfig.proxyConfig) {
14
- client.middlewareStack.use(createProxyMiddleware({
15
- realS3Host: proxyConfig.proxyConfig.realHost,
16
- proxyBasePath: proxyConfig.proxyConfig.proxyBasePath,
17
- proxyHost: proxyConfig.proxyConfig.proxyHost,
18
- proxyPort: proxyConfig.proxyConfig.proxyPort
19
- }));
20
- const buildInfo = getBuildInfo();
21
- console.log("S3Client configured with proxy middleware", {
22
- buildInfo,
23
- proxyConfig: proxyConfig.proxyConfig
24
- });
25
- }
26
- } else {
27
- const buildInfo = getBuildInfo();
28
- console.log("S3Client configured for direct connection", {
29
- buildInfo,
30
- endpoint: config.endpoint
31
- });
32
- }
27
+ const proxyConfig = extractProxyConfig(config);
28
+ if (proxyConfig) client.middlewareStack.use(createProxyMiddleware({
29
+ realS3Host: proxyConfig.realS3Host,
30
+ proxyBasePath: proxyConfig.proxyBasePath,
31
+ proxyHost: proxyConfig.proxyHost,
32
+ proxyPort: proxyConfig.proxyPort
33
+ }));
33
34
  return client;
34
35
  };
35
36
  export { createS3Client };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scality/data-browser-library",
3
- "version": "1.0.0-preview.5",
3
+ "version": "1.0.0-preview.7",
4
4
  "description": "A modular React component library for browsing S3 buckets and objects",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -27,21 +27,26 @@
27
27
  "@aws-sdk/protocol-http": "^3.370.0",
28
28
  "@aws-sdk/s3-presigned-post": "^3.888.0",
29
29
  "@aws-sdk/s3-request-presigner": "^3.478.0",
30
+ "@hookform/resolvers": "^5.2.2",
30
31
  "@monaco-editor/react": "^4.7.0",
31
32
  "@tanstack/react-query": "^5.8.0",
32
33
  "@tanstack/react-query-devtools": "^5.8.0",
33
34
  "@testing-library/user-event": "^14.6.1",
35
+ "joi": "^18.0.1",
34
36
  "react-dropzone": "^14.2.0",
35
- "react-hook-form": "^7.48.0"
37
+ "react-hook-form": "^7.48.0",
38
+ "@scality/zenkoclient": "^2.0.0-preview.1"
36
39
  },
37
40
  "peerDependencies": {
38
- "@scality/core-ui": "0.174.0",
41
+ "@scality/core-ui": "^0.174.0",
39
42
  "react": ">=18.0.0",
40
- "react-dom": ">=18.0.0"
43
+ "react-dom": ">=18.0.0",
44
+ "react-router-dom": ">=6.0.0",
45
+ "styled-components": "^5.0.0"
41
46
  },
42
47
  "devDependencies": {
43
- "@rslib/core": "^0.14.0",
44
48
  "@rsbuild/plugin-react": "^1.4.1",
49
+ "@rslib/core": "^0.14.0",
45
50
  "@testing-library/jest-dom": "^6.8.0",
46
51
  "@testing-library/react": "^15.0.6",
47
52
  "@testing-library/user-event": "^14.6.1",