github.com/minio/console@v1.4.1/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>(true); 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: "There was an error", 128 detailedError: itemVal.errorString, 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; this setting is enabled by default. Please note 205 that objects created before replication was configured or 206 while replication is disabled are not synchronized to the 207 target deployment in case this setting is not enabled. 208 </Box> 209 <Box sx={{ paddingTop: "10px" }}> 210 MinIO supports replicating delete operations, where MinIO 211 synchronizes deleting specific object versions and new 212 delete markers. Delete operation replication uses the same 213 replication process as all other replication operations. 214 </Box>{" "} 215 </Fragment> 216 } 217 /> 218 } 219 > 220 <form 221 noValidate 222 autoComplete="off" 223 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 224 e.preventDefault(); 225 setAddLoading(true); 226 addRecord(); 227 }} 228 > 229 <InputBox 230 id="priority" 231 name="priority" 232 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 233 if (e.target.validity.valid) { 234 setPriority(e.target.value); 235 } 236 }} 237 label="Priority" 238 value={priority} 239 pattern={"[0-9]*"} 240 /> 241 242 <InputBox 243 id="targetURL" 244 name="targetURL" 245 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 246 setTargetURL(e.target.value); 247 }} 248 placeholder="play.min.io" 249 label="Target URL" 250 value={targetURL} 251 /> 252 253 <Switch 254 checked={useTLS} 255 id="useTLS" 256 name="useTLS" 257 label="Use TLS" 258 onChange={(e) => { 259 setUseTLS(e.target.checked); 260 }} 261 value="yes" 262 /> 263 264 <InputBox 265 id="accessKey" 266 name="accessKey" 267 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 268 setAccessKey(e.target.value); 269 }} 270 label="Access Key" 271 value={accessKey} 272 /> 273 274 <InputBox 275 id="secretKey" 276 name="secretKey" 277 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 278 setSecretKey(e.target.value); 279 }} 280 label="Secret Key" 281 value={secretKey} 282 /> 283 284 <InputBox 285 id="targetBucket" 286 name="targetBucket" 287 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 288 setTargetBucket(e.target.value); 289 }} 290 label="Target Bucket" 291 value={targetBucket} 292 /> 293 294 <InputBox 295 id="region" 296 name="region" 297 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 298 setRegion(e.target.value); 299 }} 300 label="Region" 301 value={region} 302 /> 303 304 <Select 305 id="replication_mode" 306 name="replication_mode" 307 onChange={(value) => { 308 setReplicationMode(value as "async" | "sync"); 309 }} 310 label="Replication Mode" 311 value={replicationMode} 312 options={[ 313 { label: "Asynchronous", value: "async" }, 314 { label: "Synchronous", value: "sync" }, 315 ]} 316 /> 317 318 {replicationMode === "async" && ( 319 <Box className={"inputItem"}> 320 <InputBox 321 type="number" 322 id="bandwidth_scalar" 323 name="bandwidth_scalar" 324 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 325 if (e.target.validity.valid) { 326 setBandwidthScalar(e.target.value as string); 327 } 328 }} 329 label="Bandwidth" 330 value={bandwidthScalar} 331 min="0" 332 pattern={"[0-9]*"} 333 overlayObject={ 334 <InputUnitMenu 335 id={"quota_unit"} 336 onUnitChange={(newValue) => { 337 setBandwidthUnit(newValue); 338 }} 339 unitSelected={bandwidthUnit} 340 unitsList={k8sScalarUnitsExcluding(["Ki"])} 341 disabled={false} 342 /> 343 } 344 /> 345 </Box> 346 )} 347 348 <InputBox 349 id="healthCheck" 350 name="healthCheck" 351 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 352 setHealthCheck(e.target.value as string); 353 }} 354 label="Health Check Duration" 355 value={healthCheck} 356 /> 357 358 <InputBox 359 id="storageClass" 360 name="storageClass" 361 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 362 setTargetStorageClass(e.target.value); 363 }} 364 placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc" 365 label="Storage Class" 366 value={targetStorageClass} 367 /> 368 369 <fieldset className={"inputItem"}> 370 <legend>Object Filters</legend> 371 <InputBox 372 id="prefix" 373 name="prefix" 374 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 375 setPrefix(e.target.value); 376 }} 377 placeholder="prefix" 378 label="Prefix" 379 value={prefix} 380 /> 381 <QueryMultiSelector 382 name="tags" 383 label="Tags" 384 elements={""} 385 onChange={(vl: string) => { 386 setTags(vl); 387 }} 388 keyPlaceholder="Tag Key" 389 valuePlaceholder="Tag Value" 390 withBorder 391 /> 392 </fieldset> 393 <fieldset className={"inputItem"}> 394 <legend>Replication Options</legend> 395 <Switch 396 checked={repExisting} 397 id="repExisting" 398 name="repExisting" 399 label="Existing Objects" 400 onChange={(e) => { 401 setRepExisting(e.target.checked); 402 }} 403 description={"Replicate existing objects"} 404 /> 405 <Switch 406 checked={metadataSync} 407 id="metadatataSync" 408 name="metadatataSync" 409 label="Metadata Sync" 410 onChange={(e) => { 411 setMetadataSync(e.target.checked); 412 }} 413 description={"Metadata Sync"} 414 /> 415 <Switch 416 checked={repDeleteMarker} 417 id="deleteMarker" 418 name="deleteMarker" 419 label="Delete Marker" 420 onChange={(e) => { 421 setRepDeleteMarker(e.target.checked); 422 }} 423 description={"Replicate soft deletes"} 424 /> 425 <Switch 426 checked={repDelete} 427 id="repDelete" 428 name="repDelete" 429 label="Deletes" 430 onChange={(e) => { 431 setRepDelete(e.target.checked); 432 }} 433 description={"Replicate versioned deletes"} 434 /> 435 </fieldset> 436 <Grid 437 item 438 xs={12} 439 sx={{ 440 display: "flex", 441 flexDirection: "row", 442 justifyContent: "end", 443 gap: 10, 444 paddingTop: 10, 445 }} 446 > 447 <Button 448 id={"cancel"} 449 type="button" 450 variant="regular" 451 disabled={addLoading} 452 onClick={() => { 453 navigate(backLink); 454 }} 455 label={"Cancel"} 456 /> 457 <Button 458 id={"submit"} 459 type="submit" 460 variant="callAction" 461 color="primary" 462 disabled={addLoading || !validated} 463 label={"Save"} 464 /> 465 </Grid> 466 </form> 467 </FormLayout> 468 </PageLayout> 469 </Fragment> 470 ); 471 }; 472 export default AddBucketReplication;