diff --git a/web-app/src/api/consoleApi.ts b/web-app/src/api/consoleApi.ts index 9680623cc..f0cfbeafc 100644 --- a/web-app/src/api/consoleApi.ts +++ b/web-app/src/api/consoleApi.ts @@ -1743,10 +1743,9 @@ export class HttpClient { ? { "Content-Type": type } : {}), }, - signal: - (cancelToken - ? this.createAbortSignal(cancelToken) - : requestParams.signal) || null, + signal: cancelToken + ? this.createAbortSignal(cancelToken) + : requestParams.signal, body: typeof body === "undefined" || body === null ? null diff --git a/web-app/src/common/SecureComponent/permissions.ts b/web-app/src/common/SecureComponent/permissions.ts index 45593aa5f..7094a1ed4 100644 --- a/web-app/src/common/SecureComponent/permissions.ts +++ b/web-app/src/common/SecureComponent/permissions.ts @@ -139,6 +139,7 @@ export const IAM_PAGES = { /* Buckets */ BUCKETS: "/buckets", ADD_BUCKETS: "add-bucket", + BUCKETS_ADD_REPLICATION: "/buckets/add-replication", BUCKETS_ADMIN_VIEW: ":bucketName/admin/*", BUCKETS_EDIT_REPLICATION: "/buckets/edit-replication", /* Object Browser */ @@ -296,6 +297,9 @@ export const IAM_PAGES_PERMISSIONS = { [IAM_PAGES.BUCKETS_EDIT_REPLICATION]: [ ...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // edit bucket replication bucket page ], + [IAM_PAGES.BUCKETS_ADD_REPLICATION]: [ + ...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // add bucket replication rule + ], [IAM_PAGES.BUCKETS_ADMIN_VIEW]: [ ...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // bucket admin page ], diff --git a/web-app/src/screens/Console/Buckets/BucketDetails/AddBucketReplication.tsx b/web-app/src/screens/Console/Buckets/BucketDetails/AddBucketReplication.tsx new file mode 100644 index 000000000..4753ba5de --- /dev/null +++ b/web-app/src/screens/Console/Buckets/BucketDetails/AddBucketReplication.tsx @@ -0,0 +1,473 @@ +// 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, + Select, + 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"; +import { getBytes, k8sScalarUnitsExcluding } from "common/utils"; +import get from "lodash/get"; +import InputUnitMenu from "screens/Console/Common/FormComponents/InputUnitMenu/InputUnitMenu"; + +const AddBucketReplication = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + let params = new URLSearchParams(document.location.search); + const bucketName = params.get("bucketName") || ""; + const nextPriority = params.get("nextPriority") || "1"; + const [addLoading, setAddLoading] = useState(false); + const [priority, setPriority] = useState(nextPriority); + const [accessKey, setAccessKey] = useState(""); + const [secretKey, setSecretKey] = useState(""); + const [targetURL, setTargetURL] = useState(""); + const [targetStorageClass, setTargetStorageClass] = useState(""); + const [prefix, setPrefix] = useState(""); + const [targetBucket, setTargetBucket] = useState(""); + const [region, setRegion] = useState(""); + const [useTLS, setUseTLS] = useState(true); + const [repDeleteMarker, setRepDeleteMarker] = useState(true); + const [repDelete, setRepDelete] = useState(true); + const [metadataSync, setMetadataSync] = useState(true); + const [repExisting, setRepExisting] = useState(false); + const [tags, setTags] = useState(""); + const [replicationMode, setReplicationMode] = useState<"async" | "sync">( + "async", + ); + const [bandwidthScalar, setBandwidthScalar] = useState("100"); + const [bandwidthUnit, setBandwidthUnit] = useState("Gi"); + const [healthCheck, setHealthCheck] = useState("60"); + const [validated, setValidated] = useState(false); + const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`; + useEffect(() => { + dispatch(setHelpName("bucket-replication-add")); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const addRecord = () => { + const replicate = [ + { + originBucket: bucketName, + destinationBucket: targetBucket, + }, + ]; + + const hc = parseInt(healthCheck); + + const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`; + + const remoteBucketsInfo = { + accessKey: accessKey, + secretKey: secretKey, + targetURL: endURL, + region: region, + bucketsRelation: replicate, + syncMode: replicationMode, + bandwidth: + replicationMode === "async" + ? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true)) + : 0, + healthCheckPeriod: hc, + prefix: prefix, + tags: tags, + replicateDeleteMarkers: repDeleteMarker, + replicateDeletes: repDelete, + replicateExistingObjects: repExisting, + priority: parseInt(priority), + storageClass: targetStorageClass, + replicateMetadata: metadataSync, + }; + + api.bucketsReplication + .setMultiBucketReplication(remoteBucketsInfo) + .then((res) => { + setAddLoading(false); + + const states = get(res.data, "replicationState", []); + + if (states.length > 0) { + const itemVal = states[0]; + + setAddLoading(false); + + if (itemVal.errorString && itemVal.errorString !== "") { + dispatch( + setErrorSnackMessage({ + errorMessage: itemVal.errorString, + detailedError: "There was an error", + }), + ); + // navigate(backLink); + return; + } + navigate(backLink); + return; + } + dispatch( + setErrorSnackMessage({ + errorMessage: "No changes applied", + detailedError: "", + }), + ); + }) + .catch((err) => { + console.log("this is an error!"); + setAddLoading(false); + dispatch(setErrorSnackMessage(errorToHandler(err.error))); + }); + }; + + useEffect(() => { + !validated && + accessKey.length >= 3 && + secretKey.length >= 8 && + targetBucket.length >= 3 && + targetURL.length > 0 && + setValidated(true); + }, [targetURL, accessKey, secretKey, targetBucket, validated]); + + useEffect(() => { + if ( + validated && + (accessKey.length < 3 || + secretKey.length < 8 || + targetBucket.length < 3 || + targetURL.length < 1) + ) { + setValidated(false); + } + }, [targetURL, accessKey, secretKey, targetBucket, validated]); + + return ( + + navigate(backLink)} + /> + } + actions={} + /> + + } + helpBox={ + } + title="Bucket Replication Configuration" + help={ + + + The bucket selected in this deployment acts as the “source” + while the configured remote deployment acts as the “target”. + + + For each write operation to this "source" bucket, MinIO + checks all configured replication rules and applies the + matching rule with highest configured priority. + + + MinIO supports automatically replicating existing objects in + a bucket, however it 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. + {" "} + + } + /> + } + > +
) => { + e.preventDefault(); + setAddLoading(true); + addRecord(); + }} + > + ) => { + if (e.target.validity.valid) { + setPriority(e.target.value); + } + }} + label="Priority" + value={priority} + pattern={"[0-9]*"} + /> + + ) => { + setTargetURL(e.target.value); + }} + placeholder="play.min.io" + label="Target URL" + value={targetURL} + /> + + { + setUseTLS(e.target.checked); + }} + value="yes" + /> + + ) => { + setAccessKey(e.target.value); + }} + label="Access Key" + value={accessKey} + /> + + ) => { + setSecretKey(e.target.value); + }} + label="Secret Key" + value={secretKey} + /> + + ) => { + setTargetBucket(e.target.value); + }} + label="Target Bucket" + value={targetBucket} + /> + + ) => { + setRegion(e.target.value); + }} + label="Region" + value={region} + /> + +