github.com/minio/console@v1.3.0/web-app/src/screens/Console/Buckets/BucketDetails/AddBucketReplication.tsx (about) 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 Select, 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 import { getBytes, k8sScalarUnitsExcluding } from "common/utils"; 41 import get from "lodash/get"; 42 import InputUnitMenu from "screens/Console/Common/FormComponents/InputUnitMenu/InputUnitMenu"; 43 44 const AddBucketReplication = () => { 45 const dispatch = useAppDispatch(); 46 const navigate = useNavigate(); 47 let params = new URLSearchParams(document.location.search); 48 const bucketName = params.get("bucketName") || ""; 49 const nextPriority = params.get("nextPriority") || "1"; 50 const [addLoading, setAddLoading] = useState<boolean>(false); 51 const [priority, setPriority] = useState<string>(nextPriority); 52 const [accessKey, setAccessKey] = useState<string>(""); 53 const [secretKey, setSecretKey] = useState<string>(""); 54 const [targetURL, setTargetURL] = useState<string>(""); 55 const [targetStorageClass, setTargetStorageClass] = useState<string>(""); 56 const [prefix, setPrefix] = useState<string>(""); 57 const [targetBucket, setTargetBucket] = useState<string>(""); 58 const [region, setRegion] = useState<string>(""); 59 const [useTLS, setUseTLS] = useState<boolean>(true); 60 const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true); 61 const [repDelete, setRepDelete] = useState<boolean>(true); 62 const [metadataSync, setMetadataSync] = useState<boolean>(true); 63 const [repExisting, setRepExisting] = useState<boolean>(false); 64 const [tags, setTags] = useState<string>(""); 65 const [replicationMode, setReplicationMode] = useState<"async" | "sync">( 66 "async", 67 ); 68 const [bandwidthScalar, setBandwidthScalar] = useState<string>("100"); 69 const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi"); 70 const [healthCheck, setHealthCheck] = useState<string>("60"); 71 const [validated, setValidated] = useState<boolean>(false); 72 const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`; 73 useEffect(() => { 74 dispatch(setHelpName("bucket-replication-add")); 75 // eslint-disable-next-line react-hooks/exhaustive-deps 76 }, []); 77 78 const addRecord = () => { 79 const replicate = [ 80 { 81 originBucket: bucketName, 82 destinationBucket: targetBucket, 83 }, 84 ]; 85 86 const hc = parseInt(healthCheck); 87 88 const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`; 89 90 const remoteBucketsInfo = { 91 accessKey: accessKey, 92 secretKey: secretKey, 93 targetURL: endURL, 94 region: region, 95 bucketsRelation: replicate, 96 syncMode: replicationMode, 97 bandwidth: 98 replicationMode === "async" 99 ? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true)) 100 : 0, 101 healthCheckPeriod: hc, 102 prefix: prefix, 103 tags: tags, 104 replicateDeleteMarkers: repDeleteMarker, 105 replicateDeletes: repDelete, 106 replicateExistingObjects: repExisting, 107 priority: parseInt(priority), 108 storageClass: targetStorageClass, 109 replicateMetadata: metadataSync, 110 }; 111 112 api.bucketsReplication 113 .setMultiBucketReplication(remoteBucketsInfo) 114 .then((res) => { 115 setAddLoading(false); 116 117 const states = get(res.data, "replicationState", []); 118 119 if (states.length > 0) { 120 const itemVal = states[0]; 121 122 setAddLoading(false); 123 124 if (itemVal.errorString && itemVal.errorString !== "") { 125 dispatch( 126 setErrorSnackMessage({ 127 errorMessage: itemVal.errorString, 128 detailedError: "There was an error", 129 }), 130 ); 131 // navigate(backLink); 132 return; 133 } 134 navigate(backLink); 135 return; 136 } 137 dispatch( 138 setErrorSnackMessage({ 139 errorMessage: "No changes applied", 140 detailedError: "", 141 }), 142 ); 143 }) 144 .catch((err) => { 145 console.log("this is an error!"); 146 setAddLoading(false); 147 dispatch(setErrorSnackMessage(errorToHandler(err.error))); 148 }); 149 }; 150 151 useEffect(() => { 152 !validated && 153 accessKey.length >= 3 && 154 secretKey.length >= 8 && 155 targetBucket.length >= 3 && 156 targetURL.length > 0 && 157 setValidated(true); 158 }, [targetURL, accessKey, secretKey, targetBucket, validated]); 159 160 useEffect(() => { 161 if ( 162 validated && 163 (accessKey.length < 3 || 164 secretKey.length < 8 || 165 targetBucket.length < 3 || 166 targetURL.length < 1) 167 ) { 168 setValidated(false); 169 } 170 }, [targetURL, accessKey, secretKey, targetBucket, validated]); 171 172 return ( 173 <Fragment> 174 <PageHeaderWrapper 175 label={ 176 <BackLink 177 label={"Add Bucket Replication Rule - " + bucketName} 178 onClick={() => navigate(backLink)} 179 /> 180 } 181 actions={<HelpMenu />} 182 /> 183 <PageLayout> 184 <FormLayout 185 title="Add Replication Rule" 186 icon={<BucketReplicationIcon />} 187 helpBox={ 188 <HelpBox 189 iconComponent={<BucketReplicationIcon />} 190 title="Bucket Replication Configuration" 191 help={ 192 <Fragment> 193 <Box sx={{ paddconngTop: "10px" }}> 194 The bucket selected in this deployment acts as the “source” 195 while the configured remote deployment acts as the “target”. 196 </Box> 197 <Box sx={{ paddingTop: "10px" }}> 198 For each write operation to this "source" bucket, MinIO 199 checks all configured replication rules and applies the 200 matching rule with highest configured priority. 201 </Box> 202 <Box sx={{ paddingTop: "10px" }}> 203 MinIO supports automatically replicating existing objects in 204 a bucket, however it does not enable existing object 205 replication by default. Objects created before replication 206 was configured or while replication is disabled are not 207 synchronized to the target deployment unless replication of 208 existing objects is enabled. 209 </Box> 210 <Box sx={{ paddingTop: "10px" }}> 211 MinIO supports replicating delete operations, where MinIO 212 synchronizes deleting specific object versions and new 213 delete markers. Delete operation replication uses the same 214 replication process as all other replication operations. 215 </Box>{" "} 216 </Fragment> 217 } 218 /> 219 } 220 > 221 <form 222 noValidate 223 autoComplete="off" 224 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 225 e.preventDefault(); 226 setAddLoading(true); 227 addRecord(); 228 }} 229 > 230 <InputBox 231 id="priority" 232 name="priority" 233 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 234 if (e.target.validity.valid) { 235 setPriority(e.target.value); 236 } 237 }} 238 label="Priority" 239 value={priority} 240 pattern={"[0-9]*"} 241 /> 242 243 <InputBox 244 id="targetURL" 245 name="targetURL" 246 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 247 setTargetURL(e.target.value); 248 }} 249 placeholder="play.min.io" 250 label="Target URL" 251 value={targetURL} 252 /> 253 254 <Switch 255 checked={useTLS} 256 id="useTLS" 257 name="useTLS" 258 label="Use TLS" 259 onChange={(e) => { 260 setUseTLS(e.target.checked); 261 }} 262 value="yes" 263 /> 264 265 <InputBox 266 id="accessKey" 267 name="accessKey" 268 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 269 setAccessKey(e.target.value); 270 }} 271 label="Access Key" 272 value={accessKey} 273 /> 274 275 <InputBox 276 id="secretKey" 277 name="secretKey" 278 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 279 setSecretKey(e.target.value); 280 }} 281 label="Secret Key" 282 value={secretKey} 283 /> 284 285 <InputBox 286 id="targetBucket" 287 name="targetBucket" 288 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 289 setTargetBucket(e.target.value); 290 }} 291 label="Target Bucket" 292 value={targetBucket} 293 /> 294 295 <InputBox 296 id="region" 297 name="region" 298 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 299 setRegion(e.target.value); 300 }} 301 label="Region" 302 value={region} 303 /> 304 305 <Select 306 id="replication_mode" 307 name="replication_mode" 308 onChange={(value) => { 309 setReplicationMode(value as "async" | "sync"); 310 }} 311 label="Replication Mode" 312 value={replicationMode} 313 options={[ 314 { label: "Asynchronous", value: "async" }, 315 { label: "Synchronous", value: "sync" }, 316 ]} 317 /> 318 319 {replicationMode === "async" && ( 320 <Box className={"inputItem"}> 321 <InputBox 322 type="number" 323 id="bandwidth_scalar" 324 name="bandwidth_scalar" 325 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 326 if (e.target.validity.valid) { 327 setBandwidthScalar(e.target.value as string); 328 } 329 }} 330 label="Bandwidth" 331 value={bandwidthScalar} 332 min="0" 333 pattern={"[0-9]*"} 334 overlayObject={ 335 <InputUnitMenu 336 id={"quota_unit"} 337 onUnitChange={(newValue) => { 338 setBandwidthUnit(newValue); 339 }} 340 unitSelected={bandwidthUnit} 341 unitsList={k8sScalarUnitsExcluding(["Ki"])} 342 disabled={false} 343 /> 344 } 345 /> 346 </Box> 347 )} 348 349 <InputBox 350 id="healthCheck" 351 name="healthCheck" 352 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 353 setHealthCheck(e.target.value as string); 354 }} 355 label="Health Check Duration" 356 value={healthCheck} 357 /> 358 359 <InputBox 360 id="storageClass" 361 name="storageClass" 362 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 363 setTargetStorageClass(e.target.value); 364 }} 365 placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc" 366 label="Storage Class" 367 value={targetStorageClass} 368 /> 369 370 <fieldset className={"inputItem"}> 371 <legend>Object Filters</legend> 372 <InputBox 373 id="prefix" 374 name="prefix" 375 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 376 setPrefix(e.target.value); 377 }} 378 placeholder="prefix" 379 label="Prefix" 380 value={prefix} 381 /> 382 <QueryMultiSelector 383 name="tags" 384 label="Tags" 385 elements={""} 386 onChange={(vl: string) => { 387 setTags(vl); 388 }} 389 keyPlaceholder="Tag Key" 390 valuePlaceholder="Tag Value" 391 withBorder 392 /> 393 </fieldset> 394 <fieldset className={"inputItem"}> 395 <legend>Replication Options</legend> 396 <Switch 397 checked={repExisting} 398 id="repExisting" 399 name="repExisting" 400 label="Existing Objects" 401 onChange={(e) => { 402 setRepExisting(e.target.checked); 403 }} 404 description={"Replicate existing objects"} 405 /> 406 <Switch 407 checked={metadataSync} 408 id="metadatataSync" 409 name="metadatataSync" 410 label="Metadata Sync" 411 onChange={(e) => { 412 setMetadataSync(e.target.checked); 413 }} 414 description={"Metadata Sync"} 415 /> 416 <Switch 417 checked={repDeleteMarker} 418 id="deleteMarker" 419 name="deleteMarker" 420 label="Delete Marker" 421 onChange={(e) => { 422 setRepDeleteMarker(e.target.checked); 423 }} 424 description={"Replicate soft deletes"} 425 /> 426 <Switch 427 checked={repDelete} 428 id="repDelete" 429 name="repDelete" 430 label="Deletes" 431 onChange={(e) => { 432 setRepDelete(e.target.checked); 433 }} 434 description={"Replicate versioned deletes"} 435 /> 436 </fieldset> 437 <Grid 438 item 439 xs={12} 440 sx={{ 441 display: "flex", 442 flexDirection: "row", 443 justifyContent: "end", 444 gap: 10, 445 paddingTop: 10, 446 }} 447 > 448 <Button 449 id={"cancel"} 450 type="button" 451 variant="regular" 452 disabled={addLoading} 453 onClick={() => { 454 navigate(backLink); 455 }} 456 label={"Cancel"} 457 /> 458 <Button 459 id={"submit"} 460 type="submit" 461 variant="callAction" 462 color="primary" 463 disabled={addLoading || !validated} 464 label={"Save"} 465 /> 466 </Grid> 467 </form> 468 </FormLayout> 469 </PageLayout> 470 </Fragment> 471 ); 472 }; 473 export default AddBucketReplication;