github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/ListBuckets.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 { useNavigate } from "react-router-dom"; 19 import { 20 AddIcon, 21 BucketsIcon, 22 Button, 23 HelpBox, 24 LifecycleConfigIcon, 25 MultipleBucketsIcon, 26 PageLayout, 27 RefreshIcon, 28 SelectAllIcon, 29 SelectMultipleIcon, 30 Grid, 31 breakPoints, 32 ProgressBar, 33 ActionLink, 34 } from "mds"; 35 36 import { actionsTray } from "../../Common/FormComponents/common/styleLibrary"; 37 import { SecureComponent } from "../../../../common/SecureComponent"; 38 import { 39 CONSOLE_UI_RESOURCE, 40 IAM_PAGES, 41 IAM_PERMISSIONS, 42 IAM_ROLES, 43 IAM_SCOPES, 44 permissionTooltipHelper, 45 } from "../../../../common/SecureComponent/permissions"; 46 import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice"; 47 import { useAppDispatch } from "../../../../store"; 48 import { useSelector } from "react-redux"; 49 import { selFeatures } from "../../consoleSlice"; 50 import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper"; 51 import { api } from "../../../../api"; 52 import { Bucket } from "../../../../api/consoleApi"; 53 import { errorToHandler } from "../../../../api/errors"; 54 import HelpMenu from "../../HelpMenu"; 55 import AutoColorIcon from "../../Common/Components/AutoColorIcon"; 56 import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper"; 57 import SearchBox from "../../Common/SearchBox"; 58 import VirtualizedList from "../../Common/VirtualizedList/VirtualizedList"; 59 import BulkLifecycleModal from "./BulkLifecycleModal"; 60 import hasPermission from "../../../../common/SecureComponent/accessControl"; 61 import BucketListItem from "./BucketListItem"; 62 import BulkReplicationModal from "./BulkReplicationModal"; 63 64 const ListBuckets = () => { 65 const dispatch = useAppDispatch(); 66 const navigate = useNavigate(); 67 68 const [records, setRecords] = useState<Bucket[]>([]); 69 const [loading, setLoading] = useState<boolean>(true); 70 const [filterBuckets, setFilterBuckets] = useState<string>(""); 71 const [selectedBuckets, setSelectedBuckets] = useState<string[]>([]); 72 const [replicationModalOpen, setReplicationModalOpen] = 73 useState<boolean>(false); 74 const [lifecycleModalOpen, setLifecycleModalOpen] = useState<boolean>(false); 75 const [canPutLifecycle, setCanPutLifecycle] = useState<boolean>(false); 76 const [bulkSelect, setBulkSelect] = useState<boolean>(false); 77 78 const features = useSelector(selFeatures); 79 const obOnly = !!features?.includes("object-browser-only"); 80 81 useEffect(() => { 82 dispatch(setHelpName("ob_bucket_list")); 83 }, [dispatch]); 84 85 useEffect(() => { 86 if (loading) { 87 const fetchRecords = () => { 88 setLoading(true); 89 api.buckets.listBuckets().then((res) => { 90 if (res.data) { 91 setLoading(false); 92 setRecords(res.data.buckets || []); 93 } else if (res.error) { 94 setLoading(false); 95 dispatch(setErrorSnackMessage(errorToHandler(res.error))); 96 } 97 }); 98 }; 99 fetchRecords(); 100 } 101 }, [loading, dispatch]); 102 103 const filteredRecords = records.filter((b: Bucket) => { 104 if (filterBuckets === "") { 105 return true; 106 } else { 107 return b.name.indexOf(filterBuckets) >= 0; 108 } 109 }); 110 111 const hasBuckets = records.length > 0; 112 113 const selectListBuckets = (e: React.ChangeEvent<HTMLInputElement>) => { 114 const targetD = e.target; 115 const value = targetD.value; 116 const checked = targetD.checked; 117 118 let elements: string[] = [...selectedBuckets]; // We clone the selectedBuckets array 119 120 if (checked) { 121 // If the user has checked this field we need to push this to selectedBucketsList 122 elements.push(value); 123 } else { 124 // User has unchecked this field, we need to remove it from the list 125 elements = elements.filter((element) => element !== value); 126 } 127 setSelectedBuckets(elements); 128 129 return elements; 130 }; 131 132 const closeBulkReplicationModal = (unselectAll: boolean) => { 133 setReplicationModalOpen(false); 134 135 if (unselectAll) { 136 setSelectedBuckets([]); 137 } 138 }; 139 140 const closeBulkLifecycleModal = (unselectAll: boolean) => { 141 setLifecycleModalOpen(false); 142 143 if (unselectAll) { 144 setSelectedBuckets([]); 145 } 146 }; 147 148 useEffect(() => { 149 var failLifecycle = false; 150 selectedBuckets.forEach((bucket: string) => { 151 hasPermission(bucket, IAM_PERMISSIONS[IAM_ROLES.BUCKET_LIFECYCLE], true) 152 ? setCanPutLifecycle(true) 153 : (failLifecycle = true); 154 }); 155 failLifecycle ? setCanPutLifecycle(false) : setCanPutLifecycle(true); 156 }, [selectedBuckets]); 157 158 const renderItemLine = (index: number) => { 159 const bucket = filteredRecords[index] || null; 160 if (bucket) { 161 return ( 162 <BucketListItem 163 bucket={bucket} 164 onSelect={selectListBuckets} 165 selected={selectedBuckets.includes(bucket.name)} 166 bulkSelect={bulkSelect} 167 /> 168 ); 169 } 170 return null; 171 }; 172 173 const selectAllBuckets = () => { 174 if (selectedBuckets.length === filteredRecords.length) { 175 setSelectedBuckets([]); 176 return; 177 } 178 179 const selectAllBuckets = filteredRecords.map((bucket) => { 180 return bucket.name; 181 }); 182 183 setSelectedBuckets(selectAllBuckets); 184 }; 185 186 const canCreateBucket = hasPermission("*", [IAM_SCOPES.S3_CREATE_BUCKET]); 187 const canListBuckets = hasPermission("*", [ 188 IAM_SCOPES.S3_LIST_BUCKET, 189 IAM_SCOPES.S3_ALL_LIST_BUCKET, 190 ]); 191 192 return ( 193 <Fragment> 194 {replicationModalOpen && ( 195 <BulkReplicationModal 196 open={replicationModalOpen} 197 buckets={selectedBuckets} 198 closeModalAndRefresh={closeBulkReplicationModal} 199 /> 200 )} 201 {lifecycleModalOpen && ( 202 <BulkLifecycleModal 203 buckets={selectedBuckets} 204 closeModalAndRefresh={closeBulkLifecycleModal} 205 open={lifecycleModalOpen} 206 /> 207 )} 208 {!obOnly && ( 209 <PageHeaderWrapper label={"Buckets"} actions={<HelpMenu />} /> 210 )} 211 212 <PageLayout> 213 <Grid item xs={12} sx={actionsTray.actionsTray}> 214 {obOnly && ( 215 <Grid item xs> 216 <AutoColorIcon marginRight={15} marginTop={10} /> 217 </Grid> 218 )} 219 {hasBuckets && ( 220 <SearchBox 221 onChange={setFilterBuckets} 222 placeholder="Search Buckets" 223 value={filterBuckets} 224 sx={{ 225 minWidth: 380, 226 [`@media (max-width: ${breakPoints.md}px)`]: { 227 minWidth: 220, 228 }, 229 }} 230 /> 231 )} 232 233 <Grid 234 item 235 xs={12} 236 sx={{ 237 display: "flex", 238 alignItems: "center", 239 justifyContent: "flex-end", 240 gap: 5, 241 }} 242 > 243 {!obOnly && ( 244 <Fragment> 245 <TooltipWrapper 246 tooltip={ 247 !hasBuckets 248 ? "" 249 : bulkSelect 250 ? "Unselect Buckets" 251 : "Select Multiple Buckets" 252 } 253 > 254 <Button 255 id={"multiple-bucket-seection"} 256 onClick={() => { 257 setBulkSelect(!bulkSelect); 258 setSelectedBuckets([]); 259 }} 260 icon={<SelectMultipleIcon />} 261 variant={bulkSelect ? "callAction" : "regular"} 262 disabled={!hasBuckets} 263 /> 264 </TooltipWrapper> 265 266 {bulkSelect && ( 267 <TooltipWrapper 268 tooltip={ 269 !hasBuckets 270 ? "" 271 : selectedBuckets.length === filteredRecords.length 272 ? "Unselect All Buckets" 273 : "Select All Buckets" 274 } 275 > 276 <Button 277 id={"select-all-buckets"} 278 onClick={selectAllBuckets} 279 icon={<SelectAllIcon />} 280 variant={"regular"} 281 /> 282 </TooltipWrapper> 283 )} 284 285 <TooltipWrapper 286 tooltip={ 287 !hasBuckets 288 ? "" 289 : !canPutLifecycle 290 ? permissionTooltipHelper( 291 IAM_PERMISSIONS[IAM_ROLES.BUCKET_LIFECYCLE], 292 "configure lifecycle for the selected buckets", 293 ) 294 : selectedBuckets.length === 0 295 ? bulkSelect 296 ? "Please select at least one bucket on which to configure Lifecycle" 297 : "Use the Select Multiple Buckets button to choose buckets on which to configure Lifecycle" 298 : "Set Lifecycle" 299 } 300 > 301 <Button 302 id={"set-lifecycle"} 303 onClick={() => { 304 setLifecycleModalOpen(true); 305 }} 306 icon={<LifecycleConfigIcon />} 307 variant={"regular"} 308 disabled={selectedBuckets.length === 0 || !canPutLifecycle} 309 /> 310 </TooltipWrapper> 311 312 <TooltipWrapper 313 tooltip={ 314 !hasBuckets 315 ? "" 316 : selectedBuckets.length === 0 317 ? bulkSelect 318 ? "Please select at least one bucket on which to configure Replication" 319 : "Use the Select Multiple Buckets button to choose buckets on which to configure Replication" 320 : "Set Replication" 321 } 322 > 323 <Button 324 id={"set-replication"} 325 onClick={() => { 326 setReplicationModalOpen(true); 327 }} 328 icon={<MultipleBucketsIcon />} 329 variant={"regular"} 330 disabled={selectedBuckets.length === 0} 331 /> 332 </TooltipWrapper> 333 </Fragment> 334 )} 335 336 <TooltipWrapper tooltip={"Refresh"}> 337 <Button 338 id={"refresh-buckets"} 339 onClick={() => { 340 setLoading(true); 341 }} 342 icon={<RefreshIcon />} 343 variant={"regular"} 344 /> 345 </TooltipWrapper> 346 347 {!obOnly && ( 348 <TooltipWrapper 349 tooltip={ 350 canCreateBucket 351 ? "" 352 : permissionTooltipHelper( 353 [IAM_SCOPES.S3_CREATE_BUCKET], 354 "create a bucket", 355 ) 356 } 357 > 358 <Button 359 id={"create-bucket"} 360 onClick={() => { 361 navigate(IAM_PAGES.ADD_BUCKETS); 362 }} 363 icon={<AddIcon />} 364 variant={"callAction"} 365 disabled={!canCreateBucket} 366 label={"Create Bucket"} 367 /> 368 </TooltipWrapper> 369 )} 370 </Grid> 371 </Grid> 372 373 {loading && <ProgressBar />} 374 {!loading && ( 375 <Grid 376 item 377 xs={12} 378 sx={{ 379 marginTop: 25, 380 height: "calc(100vh - 211px)", 381 "&.isEmbedded": { 382 height: "calc(100vh - 128px)", 383 }, 384 }} 385 className={obOnly ? "isEmbedded" : ""} 386 > 387 {filteredRecords.length !== 0 && ( 388 <VirtualizedList 389 rowRenderFunction={renderItemLine} 390 totalItems={filteredRecords.length} 391 /> 392 )} 393 {filteredRecords.length === 0 && filterBuckets !== "" && ( 394 <Grid container> 395 <Grid item xs={8}> 396 <HelpBox 397 iconComponent={<BucketsIcon />} 398 title={"No Results"} 399 help={ 400 <Fragment> 401 No buckets match the filtering condition 402 </Fragment> 403 } 404 /> 405 </Grid> 406 </Grid> 407 )} 408 {!hasBuckets && ( 409 <Grid container> 410 <Grid item xs={8}> 411 <HelpBox 412 iconComponent={<BucketsIcon />} 413 title={"Buckets"} 414 help={ 415 <Fragment> 416 MinIO uses buckets to organize objects. A bucket is 417 similar to a folder or directory in a filesystem, where 418 each bucket can hold an arbitrary number of objects. 419 <br /> 420 {canListBuckets ? ( 421 "" 422 ) : ( 423 <Fragment> 424 <br /> 425 {permissionTooltipHelper( 426 [ 427 IAM_SCOPES.S3_LIST_BUCKET, 428 IAM_SCOPES.S3_ALL_LIST_BUCKET, 429 ], 430 "view the buckets on this server", 431 )} 432 <br /> 433 </Fragment> 434 )} 435 <SecureComponent 436 scopes={[IAM_SCOPES.S3_CREATE_BUCKET]} 437 resource={CONSOLE_UI_RESOURCE} 438 > 439 <br /> 440 To get started, 441 <ActionLink 442 onClick={() => { 443 navigate(IAM_PAGES.ADD_BUCKETS); 444 }} 445 > 446 Create a Bucket. 447 </ActionLink> 448 </SecureComponent> 449 </Fragment> 450 } 451 /> 452 </Grid> 453 </Grid> 454 )} 455 </Grid> 456 )} 457 </PageLayout> 458 </Fragment> 459 ); 460 }; 461 462 export default ListBuckets;