From 8dd94f5336aae5645944e7838ebdf0047de9b720 Mon Sep 17 00:00:00 2001 From: jinapurapu <65002498+jinapurapu@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:24:07 -0700 Subject: [PATCH] Moved EditBucketReplication to screen (#3037) --- .../src/common/SecureComponent/permissions.ts | 4 + .../BucketDetails/BucketReplicationPanel.tsx | 8 +- .../BucketDetails/EditBucketReplication.tsx | 335 ++++++++++++++++++ portal-ui/src/screens/Console/Console.tsx | 14 + portal-ui/src/screens/Console/helpTopics.json | 76 +++- 5 files changed, 430 insertions(+), 7 deletions(-) create mode 100644 portal-ui/src/screens/Console/Buckets/BucketDetails/EditBucketReplication.tsx diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index 79aa9d7c9..8a68e37b6 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -140,6 +140,7 @@ export const IAM_PAGES = { BUCKETS: "/buckets", ADD_BUCKETS: "add-bucket", BUCKETS_ADMIN_VIEW: ":bucketName/admin/*", + BUCKETS_EDIT_REPLICATION: "/buckets/edit-replication", /* Object Browser */ OBJECT_BROWSER_VIEW: "/browser", OBJECT_BROWSER_BUCKET_VIEW: "/browser/:bucketName", @@ -293,6 +294,9 @@ export const IAM_PAGES_PERMISSIONS = { [IAM_PAGES.ADD_BUCKETS]: [ IAM_SCOPES.S3_CREATE_BUCKET, // create bucket page ], + [IAM_PAGES.BUCKETS_EDIT_REPLICATION]: [ + ...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // edit bucket replication bucket page + ], [IAM_PAGES.BUCKETS_ADMIN_VIEW]: [ ...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // bucket admin page ], diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx index f1b0ec6a0..e026d43a5 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx @@ -16,7 +16,7 @@ import React, { Fragment, useEffect, useState } from "react"; import { useSelector } from "react-redux"; -import { useParams } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { AddIcon, Box, @@ -153,10 +153,12 @@ const BucketReplicationPanel = () => { setDeleteSelectedRules(true); setDeleteReplicationModal(true); }; - + const navigate = useNavigate(); const editReplicationRule = (replication: BucketReplicationRule) => { setSelectedRRule(replication.id); - setEditReplicationModal(true); + navigate( + `/buckets/edit-replication?bucketName=${bucketName}&ruleID=${replication.id}`, + ); }; const ruleDestDisplay = (events: BucketReplicationDestination) => { diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/EditBucketReplication.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/EditBucketReplication.tsx new file mode 100644 index 000000000..42932b1f2 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/EditBucketReplication.tsx @@ -0,0 +1,335 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2023 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import React, { Fragment, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { + BackLink, + Box, + BucketReplicationIcon, + Button, + FormLayout, + Grid, + HelpBox, + InputBox, + PageLayout, + ReadBox, + Switch, +} from "mds"; +import { IAM_PAGES } from "../../../../common/SecureComponent/permissions"; +import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice"; +import { useAppDispatch } from "../../../../store"; +import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper"; +import HelpMenu from "../../HelpMenu"; +import { api } from "api"; +import { errorToHandler } from "api/errors"; +import QueryMultiSelector from "screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector"; + +const EditBucketReplication = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + let params = new URLSearchParams(document.location.search); + + const bucketName = params.get("bucketName") || ""; + const ruleID = params.get("ruleID") || ""; + + useEffect(() => { + dispatch(setHelpName("bucket-replication-edit")); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`; + + const [editLoading, setEditLoading] = useState(true); + const [saveEdit, setSaveEdit] = useState(false); + const [priority, setPriority] = useState("1"); + const [destination, setDestination] = useState(""); + const [prefix, setPrefix] = useState(""); + const [repDeleteMarker, setRepDeleteMarker] = useState(false); + const [metadataSync, setMetadataSync] = useState(false); + const [initialTags, setInitialTags] = useState(""); + const [tags, setTags] = useState(""); + const [targetStorageClass, setTargetStorageClass] = useState(""); + const [repExisting, setRepExisting] = useState(false); + const [repDelete, setRepDelete] = useState(false); + const [ruleState, setRuleState] = useState(false); + + useEffect(() => { + if (editLoading && bucketName && ruleID) { + api.buckets + + .getBucketReplicationRule(bucketName, ruleID) + .then((res) => { + setPriority(res.data.priority ? res.data.priority.toString() : ""); + const pref = res.data.prefix || ""; + const tag = res.data.tags || ""; + setPrefix(pref); + setInitialTags(tag); + setTags(tag); + setDestination(res.data.destination?.bucket || ""); + setRepDeleteMarker(res.data.delete_marker_replication || false); + setTargetStorageClass(res.data.storageClass || ""); + setRepExisting(!!res.data.existingObjects); + setRepDelete(!!res.data.deletes_replication); + setRuleState(res.data.status === "Enabled"); + setMetadataSync(!!res.data.metadata_replication); + + setEditLoading(false); + }) + .catch((err) => { + dispatch(setErrorSnackMessage(errorToHandler(err.error))); + setEditLoading(false); + }); + } + }, [editLoading, dispatch, bucketName, ruleID]); + + useEffect(() => { + if (saveEdit && bucketName && ruleID) { + const remoteBucketsInfo = { + arn: destination, + ruleState: ruleState, + prefix: prefix, + tags: tags, + replicateDeleteMarkers: repDeleteMarker, + replicateDeletes: repDelete, + replicateExistingObjects: repExisting, + replicateMetadata: metadataSync, + priority: parseInt(priority), + storageClass: targetStorageClass, + }; + + api.buckets + .updateMultiBucketReplication(bucketName, ruleID, remoteBucketsInfo) + .then(() => { + navigate(backLink); + }) + .catch((err) => { + dispatch(setErrorSnackMessage(errorToHandler(err.error))); + setSaveEdit(false); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + saveEdit, + bucketName, + ruleID, + destination, + prefix, + tags, + repDeleteMarker, + priority, + repDelete, + repExisting, + ruleState, + metadataSync, + targetStorageClass, + dispatch, + ]); + + return ( + + navigate(backLink)} + /> + } + actions={} + /> + +
) => { + e.preventDefault(); + setSaveEdit(true); + }} + > + } + title="Bucket Replication Configuration" + help={ + + + For each write operation to the bucket, MinIO checks all + configured replication rules for the bucket and applies + the matching rule with highest configured priority. + + + MinIO supports enabling replication of existing objects in + a bucket. + + + MinIO does not enable existing object replication by + default. Objects created before replication was configured + or while replication is disabled are not synchronized to + the target deployment unless replication of existing + objects is enabled. + + + MinIO supports replicating delete operations, where MinIO + synchronizes deleting specific object versions and new + delete markers. Delete operation replication uses the same + replication process as all other replication operations. + {" "} + + } + /> + } + > + { + setRuleState(e.target.checked); + }} + /> + + {destination} + + ) => { + if (e.target.validity.valid) { + setPriority(e.target.value); + } + }} + label="Priority" + value={priority} + pattern={"[0-9]*"} + /> + ) => { + setTargetStorageClass(e.target.value); + }} + placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc" + label="Storage Class" + value={targetStorageClass} + /> +
+ Object Filters + ) => { + setPrefix(e.target.value); + }} + placeholder="prefix" + label="Prefix" + value={prefix} + /> + { + setTags(vl); + }} + keyPlaceholder="Tag Key" + valuePlaceholder="Tag Value" + withBorder + /> +
+
+ Replication Options + { + setRepExisting(e.target.checked); + }} + description={"Replicate existing objects"} + /> + { + setMetadataSync(e.target.checked); + }} + description={"Metadata Sync"} + /> + { + setRepDeleteMarker(e.target.checked); + }} + description={"Replicate soft deletes"} + /> + { + setRepDelete(e.target.checked); + }} + description={"Replicate versioned deletes"} + /> +
+ +