Skip to content

Commit 8dd94f5

Browse files
authored
Moved EditBucketReplication to screen (#3037)
1 parent 1dc21b9 commit 8dd94f5

File tree

5 files changed

+430
-7
lines changed

5 files changed

+430
-7
lines changed

portal-ui/src/common/SecureComponent/permissions.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export const IAM_PAGES = {
140140
BUCKETS: "/buckets",
141141
ADD_BUCKETS: "add-bucket",
142142
BUCKETS_ADMIN_VIEW: ":bucketName/admin/*",
143+
BUCKETS_EDIT_REPLICATION: "/buckets/edit-replication",
143144
/* Object Browser */
144145
OBJECT_BROWSER_VIEW: "/browser",
145146
OBJECT_BROWSER_BUCKET_VIEW: "/browser/:bucketName",
@@ -293,6 +294,9 @@ export const IAM_PAGES_PERMISSIONS = {
293294
[IAM_PAGES.ADD_BUCKETS]: [
294295
IAM_SCOPES.S3_CREATE_BUCKET, // create bucket page
295296
],
297+
[IAM_PAGES.BUCKETS_EDIT_REPLICATION]: [
298+
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // edit bucket replication bucket page
299+
],
296300
[IAM_PAGES.BUCKETS_ADMIN_VIEW]: [
297301
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // bucket admin page
298302
],

portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import React, { Fragment, useEffect, useState } from "react";
1818
import { useSelector } from "react-redux";
19-
import { useParams } from "react-router-dom";
19+
import { useNavigate, useParams } from "react-router-dom";
2020
import {
2121
AddIcon,
2222
Box,
@@ -153,10 +153,12 @@ const BucketReplicationPanel = () => {
153153
setDeleteSelectedRules(true);
154154
setDeleteReplicationModal(true);
155155
};
156-
156+
const navigate = useNavigate();
157157
const editReplicationRule = (replication: BucketReplicationRule) => {
158158
setSelectedRRule(replication.id);
159-
setEditReplicationModal(true);
159+
navigate(
160+
`/buckets/edit-replication?bucketName=${bucketName}&ruleID=${replication.id}`,
161+
);
160162
};
161163

162164
const ruleDestDisplay = (events: BucketReplicationDestination) => {
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// This file is part of MinIO Console Server
2+
// Copyright (c) 2023 MinIO, Inc.
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import React, { Fragment, useEffect, useState } from "react";
18+
import { useNavigate } from "react-router-dom";
19+
import {
20+
BackLink,
21+
Box,
22+
BucketReplicationIcon,
23+
Button,
24+
FormLayout,
25+
Grid,
26+
HelpBox,
27+
InputBox,
28+
PageLayout,
29+
ReadBox,
30+
Switch,
31+
} from "mds";
32+
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
33+
import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
34+
import { useAppDispatch } from "../../../../store";
35+
import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
36+
import HelpMenu from "../../HelpMenu";
37+
import { api } from "api";
38+
import { errorToHandler } from "api/errors";
39+
import QueryMultiSelector from "screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
40+
41+
const EditBucketReplication = () => {
42+
const dispatch = useAppDispatch();
43+
const navigate = useNavigate();
44+
let params = new URLSearchParams(document.location.search);
45+
46+
const bucketName = params.get("bucketName") || "";
47+
const ruleID = params.get("ruleID") || "";
48+
49+
useEffect(() => {
50+
dispatch(setHelpName("bucket-replication-edit"));
51+
// eslint-disable-next-line react-hooks/exhaustive-deps
52+
}, []);
53+
54+
const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`;
55+
56+
const [editLoading, setEditLoading] = useState<boolean>(true);
57+
const [saveEdit, setSaveEdit] = useState<boolean>(false);
58+
const [priority, setPriority] = useState<string>("1");
59+
const [destination, setDestination] = useState<string>("");
60+
const [prefix, setPrefix] = useState<string>("");
61+
const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(false);
62+
const [metadataSync, setMetadataSync] = useState<boolean>(false);
63+
const [initialTags, setInitialTags] = useState<string>("");
64+
const [tags, setTags] = useState<string>("");
65+
const [targetStorageClass, setTargetStorageClass] = useState<string>("");
66+
const [repExisting, setRepExisting] = useState<boolean>(false);
67+
const [repDelete, setRepDelete] = useState<boolean>(false);
68+
const [ruleState, setRuleState] = useState<boolean>(false);
69+
70+
useEffect(() => {
71+
if (editLoading && bucketName && ruleID) {
72+
api.buckets
73+
74+
.getBucketReplicationRule(bucketName, ruleID)
75+
.then((res) => {
76+
setPriority(res.data.priority ? res.data.priority.toString() : "");
77+
const pref = res.data.prefix || "";
78+
const tag = res.data.tags || "";
79+
setPrefix(pref);
80+
setInitialTags(tag);
81+
setTags(tag);
82+
setDestination(res.data.destination?.bucket || "");
83+
setRepDeleteMarker(res.data.delete_marker_replication || false);
84+
setTargetStorageClass(res.data.storageClass || "");
85+
setRepExisting(!!res.data.existingObjects);
86+
setRepDelete(!!res.data.deletes_replication);
87+
setRuleState(res.data.status === "Enabled");
88+
setMetadataSync(!!res.data.metadata_replication);
89+
90+
setEditLoading(false);
91+
})
92+
.catch((err) => {
93+
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
94+
setEditLoading(false);
95+
});
96+
}
97+
}, [editLoading, dispatch, bucketName, ruleID]);
98+
99+
useEffect(() => {
100+
if (saveEdit && bucketName && ruleID) {
101+
const remoteBucketsInfo = {
102+
arn: destination,
103+
ruleState: ruleState,
104+
prefix: prefix,
105+
tags: tags,
106+
replicateDeleteMarkers: repDeleteMarker,
107+
replicateDeletes: repDelete,
108+
replicateExistingObjects: repExisting,
109+
replicateMetadata: metadataSync,
110+
priority: parseInt(priority),
111+
storageClass: targetStorageClass,
112+
};
113+
114+
api.buckets
115+
.updateMultiBucketReplication(bucketName, ruleID, remoteBucketsInfo)
116+
.then(() => {
117+
navigate(backLink);
118+
})
119+
.catch((err) => {
120+
dispatch(setErrorSnackMessage(errorToHandler(err.error)));
121+
setSaveEdit(false);
122+
});
123+
}
124+
// eslint-disable-next-line react-hooks/exhaustive-deps
125+
}, [
126+
saveEdit,
127+
bucketName,
128+
ruleID,
129+
destination,
130+
prefix,
131+
tags,
132+
repDeleteMarker,
133+
priority,
134+
repDelete,
135+
repExisting,
136+
ruleState,
137+
metadataSync,
138+
targetStorageClass,
139+
dispatch,
140+
]);
141+
142+
return (
143+
<Fragment>
144+
<PageHeaderWrapper
145+
label={
146+
<BackLink
147+
label={"Edit Bucket Replication"}
148+
onClick={() => navigate(backLink)}
149+
/>
150+
}
151+
actions={<HelpMenu />}
152+
/>
153+
<PageLayout>
154+
<form
155+
noValidate
156+
autoComplete="off"
157+
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
158+
e.preventDefault();
159+
setSaveEdit(true);
160+
}}
161+
>
162+
<FormLayout
163+
containerPadding={false}
164+
withBorders={false}
165+
helpBox={
166+
<HelpBox
167+
iconComponent={<BucketReplicationIcon />}
168+
title="Bucket Replication Configuration"
169+
help={
170+
<Fragment>
171+
<Box sx={{ paddingTop: "10px" }}>
172+
For each write operation to the bucket, MinIO checks all
173+
configured replication rules for the bucket and applies
174+
the matching rule with highest configured priority.
175+
</Box>
176+
<Box sx={{ paddingTop: "10px" }}>
177+
MinIO supports enabling replication of existing objects in
178+
a bucket.
179+
</Box>
180+
<Box sx={{ paddingTop: "10px" }}>
181+
MinIO does not enable existing object replication by
182+
default. Objects created before replication was configured
183+
or while replication is disabled are not synchronized to
184+
the target deployment unless replication of existing
185+
objects is enabled.
186+
</Box>
187+
<Box sx={{ paddingTop: "10px" }}>
188+
MinIO supports replicating delete operations, where MinIO
189+
synchronizes deleting specific object versions and new
190+
delete markers. Delete operation replication uses the same
191+
replication process as all other replication operations.
192+
</Box>{" "}
193+
</Fragment>
194+
}
195+
/>
196+
}
197+
>
198+
<Switch
199+
checked={ruleState}
200+
id="ruleState"
201+
name="ruleState"
202+
label="Rule State"
203+
onChange={(e) => {
204+
setRuleState(e.target.checked);
205+
}}
206+
/>
207+
<ReadBox label={"Destination"} sx={{ width: "100%" }}>
208+
{destination}
209+
</ReadBox>
210+
<InputBox
211+
id="priority"
212+
name="priority"
213+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
214+
if (e.target.validity.valid) {
215+
setPriority(e.target.value);
216+
}
217+
}}
218+
label="Priority"
219+
value={priority}
220+
pattern={"[0-9]*"}
221+
/>
222+
<InputBox
223+
id="storageClass"
224+
name="storageClass"
225+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
226+
setTargetStorageClass(e.target.value);
227+
}}
228+
placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
229+
label="Storage Class"
230+
value={targetStorageClass}
231+
/>
232+
<fieldset className={"inputItem"}>
233+
<legend>Object Filters</legend>
234+
<InputBox
235+
id="prefix"
236+
name="prefix"
237+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
238+
setPrefix(e.target.value);
239+
}}
240+
placeholder="prefix"
241+
label="Prefix"
242+
value={prefix}
243+
/>
244+
<QueryMultiSelector
245+
name="tags"
246+
label="Tags"
247+
elements={initialTags}
248+
onChange={(vl: string) => {
249+
setTags(vl);
250+
}}
251+
keyPlaceholder="Tag Key"
252+
valuePlaceholder="Tag Value"
253+
withBorder
254+
/>
255+
</fieldset>
256+
<fieldset className={"inputItem"}>
257+
<legend>Replication Options</legend>
258+
<Switch
259+
checked={repExisting}
260+
id="repExisting"
261+
name="repExisting"
262+
label="Existing Objects"
263+
onChange={(e) => {
264+
setRepExisting(e.target.checked);
265+
}}
266+
description={"Replicate existing objects"}
267+
/>
268+
<Switch
269+
checked={metadataSync}
270+
id="metadatataSync"
271+
name="metadatataSync"
272+
label="Metadata Sync"
273+
onChange={(e) => {
274+
setMetadataSync(e.target.checked);
275+
}}
276+
description={"Metadata Sync"}
277+
/>
278+
<Switch
279+
checked={repDeleteMarker}
280+
id="deleteMarker"
281+
name="deleteMarker"
282+
label="Delete Marker"
283+
onChange={(e) => {
284+
setRepDeleteMarker(e.target.checked);
285+
}}
286+
description={"Replicate soft deletes"}
287+
/>
288+
<Switch
289+
checked={repDelete}
290+
id="repDelete"
291+
name="repDelete"
292+
label="Deletes"
293+
onChange={(e) => {
294+
setRepDelete(e.target.checked);
295+
}}
296+
description={"Replicate versioned deletes"}
297+
/>
298+
</fieldset>
299+
<Grid
300+
item
301+
xs={12}
302+
sx={{
303+
display: "flex",
304+
flexDirection: "row",
305+
justifyContent: "end",
306+
gap: 10,
307+
paddingTop: 10,
308+
}}
309+
>
310+
<Button
311+
id={"cancel-edit-replication"}
312+
type="button"
313+
variant="regular"
314+
disabled={editLoading || saveEdit}
315+
onClick={() => {
316+
navigate(backLink);
317+
}}
318+
label={"Cancel"}
319+
/>
320+
<Button
321+
id={"save-replication"}
322+
type="submit"
323+
variant="callAction"
324+
disabled={editLoading || saveEdit}
325+
label={"Save"}
326+
/>
327+
</Grid>
328+
</FormLayout>
329+
</form>
330+
</PageLayout>
331+
</Fragment>
332+
);
333+
};
334+
335+
export default EditBucketReplication;

0 commit comments

Comments
 (0)