github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.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, { Fragment, useEffect, useState } from "react"; 18 import { useSelector } from "react-redux"; 19 import { useNavigate, useParams } from "react-router-dom"; 20 import { 21 AddIcon, 22 Box, 23 BucketsIcon, 24 Button, 25 DataTable, 26 Grid, 27 HelpBox, 28 SectionTitle, 29 TrashIcon, 30 HelpTip, 31 } from "mds"; 32 import api from "../../../../common/api"; 33 import { 34 BucketReplication, 35 BucketReplicationDestination, 36 BucketReplicationRule, 37 } from "../types"; 38 import { ErrorResponseHandler } from "../../../../common/types"; 39 import { 40 hasPermission, 41 SecureComponent, 42 } from "../../../../common/SecureComponent"; 43 import { 44 IAM_PAGES, 45 IAM_SCOPES, 46 } from "../../../../common/SecureComponent/permissions"; 47 import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice"; 48 import { selBucketDetailsLoading } from "./bucketDetailsSlice"; 49 import { useAppDispatch } from "../../../../store"; 50 import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper"; 51 import withSuspense from "../../Common/Components/withSuspense"; 52 53 const EditReplicationModal = withSuspense( 54 React.lazy(() => import("./EditReplicationModal")), 55 ); 56 const AddReplicationModal = withSuspense( 57 React.lazy(() => import("./AddReplicationModal")), 58 ); 59 const DeleteReplicationRule = withSuspense( 60 React.lazy(() => import("./DeleteReplicationRule")), 61 ); 62 63 const BucketReplicationPanel = () => { 64 const dispatch = useAppDispatch(); 65 const params = useParams(); 66 67 const loadingBucket = useSelector(selBucketDetailsLoading); 68 69 const [loadingReplication, setLoadingReplication] = useState<boolean>(true); 70 const [replicationRules, setReplicationRules] = useState< 71 BucketReplicationRule[] 72 >([]); 73 const [deleteReplicationModal, setDeleteReplicationModal] = 74 useState<boolean>(false); 75 const [openSetReplication, setOpenSetReplication] = useState<boolean>(false); 76 const [editReplicationModal, setEditReplicationModal] = 77 useState<boolean>(false); 78 const [selectedRRule, setSelectedRRule] = useState<string>(""); 79 const [selectedRepRules, setSelectedRepRules] = useState<string[]>([]); 80 const [deleteSelectedRules, setDeleteSelectedRules] = 81 useState<boolean>(false); 82 83 const bucketName = params.bucketName || ""; 84 85 const displayReplicationRules = hasPermission(bucketName, [ 86 IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, 87 IAM_SCOPES.S3_GET_ACTIONS, 88 ]); 89 useEffect(() => { 90 dispatch(setHelpName("bucket_detail_replication")); 91 // eslint-disable-next-line react-hooks/exhaustive-deps 92 }, []); 93 94 useEffect(() => { 95 if (loadingBucket) { 96 setLoadingReplication(true); 97 } 98 }, [loadingBucket, setLoadingReplication]); 99 100 useEffect(() => { 101 if (loadingReplication) { 102 if (displayReplicationRules) { 103 api 104 .invoke("GET", `/api/v1/buckets/${bucketName}/replication`) 105 .then((res: BucketReplication) => { 106 const r = res.rules ? res.rules : []; 107 108 r.sort((a, b) => a.priority - b.priority); 109 110 setReplicationRules(r); 111 setLoadingReplication(false); 112 }) 113 .catch((err: ErrorResponseHandler) => { 114 dispatch(setErrorSnackMessage(err)); 115 setLoadingReplication(false); 116 }); 117 } else { 118 setLoadingReplication(false); 119 } 120 } 121 }, [loadingReplication, dispatch, bucketName, displayReplicationRules]); 122 123 const closeAddReplication = () => { 124 setOpenReplicationOpen(false); 125 setLoadingReplication(true); 126 }; 127 128 const setOpenReplicationOpen = (open = false) => { 129 setOpenSetReplication(open); 130 }; 131 132 const closeReplicationModalDelete = (refresh: boolean) => { 133 setDeleteReplicationModal(false); 134 135 if (refresh) { 136 setLoadingReplication(true); 137 } 138 }; 139 140 const closeEditReplication = (refresh: boolean) => { 141 setEditReplicationModal(false); 142 143 if (refresh) { 144 setLoadingReplication(true); 145 } 146 }; 147 148 const confirmDeleteReplication = (replication: BucketReplicationRule) => { 149 setSelectedRRule(replication.id); 150 setDeleteSelectedRules(false); 151 setDeleteReplicationModal(true); 152 }; 153 154 const confirmDeleteSelectedReplicationRules = () => { 155 setSelectedRRule("selectedRules"); 156 setDeleteSelectedRules(true); 157 setDeleteReplicationModal(true); 158 }; 159 const navigate = useNavigate(); 160 const editReplicationRule = (replication: BucketReplicationRule) => { 161 setSelectedRRule(replication.id); 162 navigate( 163 `/buckets/edit-replication?bucketName=${bucketName}&ruleID=${replication.id}`, 164 ); 165 }; 166 167 const ruleDestDisplay = (events: BucketReplicationDestination) => { 168 return <Fragment>{events.bucket.replace("arn:aws:s3:::", "")}</Fragment>; 169 }; 170 171 const tagDisplay = (events: BucketReplicationRule) => { 172 return <Fragment>{events && events.tags !== "" ? "Yes" : "No"}</Fragment>; 173 }; 174 175 const selectAllItems = () => { 176 if (selectedRepRules.length === replicationRules.length) { 177 setSelectedRepRules([]); 178 return; 179 } 180 setSelectedRepRules(replicationRules.map((x) => x.id)); 181 }; 182 183 const selectRules = (e: React.ChangeEvent<HTMLInputElement>) => { 184 const targetD = e.target; 185 const value = targetD.value; 186 const checked = targetD.checked; 187 188 let elements: string[] = [...selectedRepRules]; // We clone the selectedSAs array 189 if (checked) { 190 // If the user has checked this field we need to push this to selectedSAs 191 elements.push(value); 192 } else { 193 // User has unchecked this field, we need to remove it from the list 194 elements = elements.filter((element) => element !== value); 195 } 196 setSelectedRepRules(elements); 197 return elements; 198 }; 199 200 const replicationTableActions: any = [ 201 { 202 type: "delete", 203 onClick: confirmDeleteReplication, 204 }, 205 { 206 type: "view", 207 onClick: editReplicationRule, 208 disableButtonFunction: !hasPermission( 209 bucketName, 210 [ 211 IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, 212 IAM_SCOPES.S3_PUT_ACTIONS, 213 ], 214 true, 215 ), 216 }, 217 ]; 218 219 return ( 220 <Fragment> 221 {openSetReplication && ( 222 <AddReplicationModal 223 closeModalAndRefresh={closeAddReplication} 224 open={openSetReplication} 225 bucketName={bucketName} 226 setReplicationRules={replicationRules} 227 /> 228 )} 229 230 {deleteReplicationModal && ( 231 <DeleteReplicationRule 232 deleteOpen={deleteReplicationModal} 233 selectedBucket={bucketName} 234 closeDeleteModalAndRefresh={closeReplicationModalDelete} 235 ruleToDelete={selectedRRule} 236 rulesToDelete={selectedRepRules} 237 remainingRules={replicationRules.length} 238 allSelected={ 239 replicationRules.length > 0 && 240 selectedRepRules.length === replicationRules.length 241 } 242 deleteSelectedRules={deleteSelectedRules} 243 /> 244 )} 245 246 {editReplicationModal && ( 247 <EditReplicationModal 248 closeModalAndRefresh={closeEditReplication} 249 open={editReplicationModal} 250 bucketName={bucketName} 251 ruleID={selectedRRule} 252 /> 253 )} 254 <SectionTitle 255 separator 256 sx={{ marginBottom: 15 }} 257 actions={ 258 <Box style={{ display: "flex", gap: 10 }}> 259 <SecureComponent 260 scopes={[ 261 IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, 262 IAM_SCOPES.S3_PUT_ACTIONS, 263 ]} 264 resource={bucketName} 265 matchAll 266 errorProps={{ disabled: true }} 267 > 268 <TooltipWrapper tooltip={"Remove Selected Replication Rules"}> 269 <Button 270 id={"remove-bucket-replication-rule"} 271 onClick={() => { 272 confirmDeleteSelectedReplicationRules(); 273 }} 274 label={"Remove Selected Rules"} 275 icon={<TrashIcon />} 276 color={"secondary"} 277 disabled={ 278 selectedRepRules.length === 0 || 279 replicationRules.length === 0 280 } 281 variant={"secondary"} 282 /> 283 </TooltipWrapper> 284 </SecureComponent> 285 <SecureComponent 286 scopes={[ 287 IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, 288 IAM_SCOPES.S3_PUT_ACTIONS, 289 ]} 290 resource={bucketName} 291 matchAll 292 errorProps={{ disabled: true }} 293 > 294 <TooltipWrapper tooltip={"Add Replication Rule"}> 295 <Button 296 id={"add-bucket-replication-rule"} 297 onClick={() => { 298 navigate( 299 IAM_PAGES.BUCKETS_ADD_REPLICATION + 300 `?bucketName=${bucketName}&nextPriority=${ 301 replicationRules.length + 1 302 }`, 303 ); 304 }} 305 label={"Add Replication Rule"} 306 icon={<AddIcon />} 307 variant={"callAction"} 308 /> 309 </TooltipWrapper> 310 </SecureComponent> 311 </Box> 312 } 313 > 314 <HelpTip 315 content={ 316 <Fragment> 317 MinIO{" "} 318 <a 319 target="blank" 320 href="https://min.io/docs/minio/kubernetes/upstream/administration/bucket-replication.html" 321 > 322 server-side bucket replication 323 </a>{" "} 324 is an automatic bucket-level configuration that synchronizes 325 objects between a source and destination bucket. 326 </Fragment> 327 } 328 placement="right" 329 > 330 Replication 331 </HelpTip> 332 </SectionTitle> 333 <Grid container> 334 <Grid item xs={12}> 335 <SecureComponent 336 scopes={[ 337 IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, 338 IAM_SCOPES.S3_GET_ACTIONS, 339 ]} 340 resource={bucketName} 341 errorProps={{ disabled: true }} 342 > 343 <DataTable 344 itemActions={replicationTableActions} 345 columns={[ 346 { 347 label: "Priority", 348 elementKey: "priority", 349 width: 55, 350 contentTextAlign: "center", 351 }, 352 { 353 label: "Destination", 354 elementKey: "destination", 355 renderFunction: ruleDestDisplay, 356 }, 357 { 358 label: "Prefix", 359 elementKey: "prefix", 360 width: 200, 361 }, 362 { 363 label: "Tags", 364 elementKey: "tags", 365 renderFunction: tagDisplay, 366 width: 60, 367 }, 368 { label: "Status", elementKey: "status", width: 100 }, 369 ]} 370 isLoading={loadingReplication} 371 records={replicationRules} 372 entityName="Replication Rules" 373 idField="id" 374 customPaperHeight={"400px"} 375 textSelectable 376 selectedItems={selectedRepRules} 377 onSelect={(e) => selectRules(e)} 378 onSelectAll={selectAllItems} 379 /> 380 </SecureComponent> 381 </Grid> 382 <Grid item xs={12}> 383 <br /> 384 <HelpBox 385 title={"Replication"} 386 iconComponent={<BucketsIcon />} 387 help={ 388 <Fragment> 389 MinIO supports server-side and client-side replication of 390 objects between source and destination buckets. 391 <br /> 392 <br /> 393 You can learn more at our{" "} 394 <a 395 href="https://min.io/docs/minio/linux/administration/bucket-replication.html?ref=con" 396 target="_blank" 397 rel="noopener" 398 > 399 documentation 400 </a> 401 . 402 </Fragment> 403 } 404 /> 405 </Grid> 406 </Grid> 407 </Fragment> 408 ); 409 }; 410 411 export default BucketReplicationPanel;