github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/AddReplicationModal.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 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, { useEffect, useState } from "react"; 18 import get from "lodash/get"; 19 import { 20 Box, 21 BucketReplicationIcon, 22 Button, 23 FormLayout, 24 Grid, 25 InputBox, 26 Select, 27 Switch, 28 } from "mds"; 29 import { api } from "api"; 30 import { errorToHandler } from "api/errors"; 31 import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary"; 32 import { BucketReplicationRule } from "../types"; 33 import { getBytes, k8sScalarUnitsExcluding } from "../../../../common/utils"; 34 import { setModalErrorSnackMessage } from "../../../../systemSlice"; 35 import { useAppDispatch } from "../../../../store"; 36 import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper"; 37 import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector"; 38 import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; 39 40 interface IReplicationModal { 41 open: boolean; 42 closeModalAndRefresh: () => any; 43 bucketName: string; 44 45 setReplicationRules: BucketReplicationRule[]; 46 } 47 48 const AddReplicationModal = ({ 49 open, 50 closeModalAndRefresh, 51 bucketName, 52 setReplicationRules, 53 }: IReplicationModal) => { 54 const dispatch = useAppDispatch(); 55 const [addLoading, setAddLoading] = useState<boolean>(false); 56 const [priority, setPriority] = useState<string>("1"); 57 const [accessKey, setAccessKey] = useState<string>(""); 58 const [secretKey, setSecretKey] = useState<string>(""); 59 const [targetURL, setTargetURL] = useState<string>(""); 60 const [targetStorageClass, setTargetStorageClass] = useState<string>(""); 61 const [prefix, setPrefix] = useState<string>(""); 62 const [targetBucket, setTargetBucket] = useState<string>(""); 63 const [region, setRegion] = useState<string>(""); 64 const [useTLS, setUseTLS] = useState<boolean>(true); 65 const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true); 66 const [repDelete, setRepDelete] = useState<boolean>(true); 67 const [metadataSync, setMetadataSync] = useState<boolean>(true); 68 const [tags, setTags] = useState<string>(""); 69 const [replicationMode, setReplicationMode] = useState<"async" | "sync">( 70 "async", 71 ); 72 const [bandwidthScalar, setBandwidthScalar] = useState<string>("100"); 73 const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi"); 74 const [healthCheck, setHealthCheck] = useState<string>("60"); 75 76 useEffect(() => { 77 if (setReplicationRules.length === 0) { 78 setPriority("1"); 79 return; 80 } 81 82 const greatestValue = setReplicationRules.reduce((prevAcc, currValue) => { 83 if (currValue.priority > prevAcc) { 84 return currValue.priority; 85 } 86 return prevAcc; 87 }, 0); 88 89 const nextPriority = greatestValue + 1; 90 setPriority(nextPriority.toString()); 91 }, [setReplicationRules]); 92 93 const addRecord = () => { 94 const replicate = [ 95 { 96 originBucket: bucketName, 97 destinationBucket: targetBucket, 98 }, 99 ]; 100 101 const hc = parseInt(healthCheck); 102 103 const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`; 104 105 const remoteBucketsInfo = { 106 accessKey: accessKey, 107 secretKey: secretKey, 108 targetURL: endURL, 109 region: region, 110 bucketsRelation: replicate, 111 syncMode: replicationMode, 112 bandwidth: 113 replicationMode === "async" 114 ? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true)) 115 : 0, 116 healthCheckPeriod: hc, 117 prefix: prefix, 118 tags: tags, 119 replicateDeleteMarkers: repDeleteMarker, 120 replicateDeletes: repDelete, 121 priority: parseInt(priority), 122 storageClass: targetStorageClass, 123 replicateMetadata: metadataSync, 124 }; 125 126 api.bucketsReplication 127 .setMultiBucketReplication(remoteBucketsInfo) 128 .then((res) => { 129 setAddLoading(false); 130 131 const states = get(res.data, "replicationState", []); 132 133 if (states.length > 0) { 134 const itemVal = states[0]; 135 136 setAddLoading(false); 137 138 if (itemVal.errorString && itemVal.errorString !== "") { 139 dispatch( 140 setModalErrorSnackMessage({ 141 errorMessage: itemVal.errorString, 142 detailedError: "", 143 }), 144 ); 145 return; 146 } 147 148 closeModalAndRefresh(); 149 150 return; 151 } 152 dispatch( 153 setModalErrorSnackMessage({ 154 errorMessage: "No changes applied", 155 detailedError: "", 156 }), 157 ); 158 }) 159 .catch((err) => { 160 setAddLoading(false); 161 dispatch(setModalErrorSnackMessage(errorToHandler(err.error))); 162 }); 163 }; 164 165 return ( 166 <ModalWrapper 167 modalOpen={open} 168 onClose={() => { 169 closeModalAndRefresh(); 170 }} 171 title="Set Bucket Replication" 172 titleIcon={<BucketReplicationIcon />} 173 > 174 <form 175 noValidate 176 autoComplete="off" 177 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 178 e.preventDefault(); 179 setAddLoading(true); 180 addRecord(); 181 }} 182 > 183 <FormLayout withBorders={false} containerPadding={false}> 184 <InputBox 185 id="priority" 186 name="priority" 187 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 188 if (e.target.validity.valid) { 189 setPriority(e.target.value); 190 } 191 }} 192 label="Priority" 193 value={priority} 194 pattern={"[0-9]*"} 195 /> 196 197 <InputBox 198 id="targetURL" 199 name="targetURL" 200 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 201 setTargetURL(e.target.value); 202 }} 203 placeholder="play.min.io" 204 label="Target URL" 205 value={targetURL} 206 /> 207 208 <Switch 209 checked={useTLS} 210 id="useTLS" 211 name="useTLS" 212 label="Use TLS" 213 onChange={(e) => { 214 setUseTLS(e.target.checked); 215 }} 216 value="yes" 217 /> 218 219 <InputBox 220 id="accessKey" 221 name="accessKey" 222 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 223 setAccessKey(e.target.value); 224 }} 225 label="Access Key" 226 value={accessKey} 227 /> 228 229 <InputBox 230 id="secretKey" 231 name="secretKey" 232 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 233 setSecretKey(e.target.value); 234 }} 235 label="Secret Key" 236 value={secretKey} 237 /> 238 239 <InputBox 240 id="targetBucket" 241 name="targetBucket" 242 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 243 setTargetBucket(e.target.value); 244 }} 245 label="Target Bucket" 246 value={targetBucket} 247 /> 248 249 <InputBox 250 id="region" 251 name="region" 252 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 253 setRegion(e.target.value); 254 }} 255 label="Region" 256 value={region} 257 /> 258 259 <Select 260 id="replication_mode" 261 name="replication_mode" 262 onChange={(value) => { 263 setReplicationMode(value as "async" | "sync"); 264 }} 265 label="Replication Mode" 266 value={replicationMode} 267 options={[ 268 { label: "Asynchronous", value: "async" }, 269 { label: "Synchronous", value: "sync" }, 270 ]} 271 /> 272 273 {replicationMode === "async" && ( 274 <Box className={"inputItem"}> 275 <InputBox 276 type="number" 277 id="bandwidth_scalar" 278 name="bandwidth_scalar" 279 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 280 if (e.target.validity.valid) { 281 setBandwidthScalar(e.target.value as string); 282 } 283 }} 284 label="Bandwidth" 285 value={bandwidthScalar} 286 min="0" 287 pattern={"[0-9]*"} 288 overlayObject={ 289 <InputUnitMenu 290 id={"quota_unit"} 291 onUnitChange={(newValue) => { 292 setBandwidthUnit(newValue); 293 }} 294 unitSelected={bandwidthUnit} 295 unitsList={k8sScalarUnitsExcluding(["Ki"])} 296 disabled={false} 297 /> 298 } 299 /> 300 </Box> 301 )} 302 303 <InputBox 304 id="healthCheck" 305 name="healthCheck" 306 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 307 setHealthCheck(e.target.value as string); 308 }} 309 label="Health Check Duration" 310 value={healthCheck} 311 /> 312 313 <InputBox 314 id="storageClass" 315 name="storageClass" 316 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 317 setTargetStorageClass(e.target.value); 318 }} 319 placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc" 320 label="Storage Class" 321 value={targetStorageClass} 322 /> 323 324 <fieldset className={"inputItem"}> 325 <legend>Object Filters</legend> 326 <InputBox 327 id="prefix" 328 name="prefix" 329 onChange={(e: React.ChangeEvent<HTMLInputElement>) => { 330 setPrefix(e.target.value); 331 }} 332 placeholder="prefix" 333 label="Prefix" 334 value={prefix} 335 /> 336 <QueryMultiSelector 337 name="tags" 338 label="Tags" 339 elements={""} 340 onChange={(vl: string) => { 341 setTags(vl); 342 }} 343 keyPlaceholder="Tag Key" 344 valuePlaceholder="Tag Value" 345 withBorder 346 /> 347 </fieldset> 348 <fieldset className={"inputItem"}> 349 <legend>Replication Options</legend> 350 <Switch 351 checked={metadataSync} 352 id="metadatataSync" 353 name="metadatataSync" 354 label="Metadata Sync" 355 onChange={(e) => { 356 setMetadataSync(e.target.checked); 357 }} 358 description={"Metadata Sync"} 359 /> 360 <Switch 361 checked={repDeleteMarker} 362 id="deleteMarker" 363 name="deleteMarker" 364 label="Delete Marker" 365 onChange={(e) => { 366 setRepDeleteMarker(e.target.checked); 367 }} 368 description={"Replicate soft deletes"} 369 /> 370 <Switch 371 checked={repDelete} 372 id="repDelete" 373 name="repDelete" 374 label="Deletes" 375 onChange={(e) => { 376 setRepDelete(e.target.checked); 377 }} 378 description={"Replicate versioned deletes"} 379 /> 380 </fieldset> 381 <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}> 382 <Button 383 id={"cancel"} 384 type="button" 385 variant="regular" 386 disabled={addLoading} 387 onClick={() => { 388 closeModalAndRefresh(); 389 }} 390 label={"Cancel"} 391 /> 392 <Button 393 id={"submit"} 394 type="submit" 395 variant="callAction" 396 color="primary" 397 disabled={addLoading} 398 label={"Save"} 399 /> 400 </Grid> 401 </FormLayout> 402 </form> 403 </ModalWrapper> 404 ); 405 }; 406 407 export default AddReplicationModal;