github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/BucketDetails.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 { 19 Navigate, 20 Route, 21 Routes, 22 useLocation, 23 useNavigate, 24 useParams, 25 } from "react-router-dom"; 26 import { 27 BackLink, 28 Box, 29 BucketsIcon, 30 Button, 31 FolderIcon, 32 PageLayout, 33 RefreshIcon, 34 ScreenTitle, 35 Tabs, 36 TrashIcon, 37 } from "mds"; 38 import { useSelector } from "react-redux"; 39 import { 40 browseBucketPermissions, 41 deleteBucketPermissions, 42 IAM_PERMISSIONS, 43 IAM_ROLES, 44 IAM_SCOPES, 45 permissionTooltipHelper, 46 } from "../../../../common/SecureComponent/permissions"; 47 48 import { 49 hasPermission, 50 SecureComponent, 51 } from "../../../../common/SecureComponent"; 52 53 import withSuspense from "../../Common/Components/withSuspense"; 54 import { 55 selDistSet, 56 selSiteRep, 57 setErrorSnackMessage, 58 setHelpName, 59 } from "../../../../systemSlice"; 60 import { 61 selBucketDetailsInfo, 62 selBucketDetailsLoading, 63 setBucketDetailsLoad, 64 setBucketInfo, 65 } from "./bucketDetailsSlice"; 66 import { useAppDispatch } from "../../../../store"; 67 import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper"; 68 import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper"; 69 import { api } from "api"; 70 import { errorToHandler } from "api/errors"; 71 import HelpMenu from "../../HelpMenu"; 72 73 const DeleteBucket = withSuspense( 74 React.lazy(() => import("../ListBuckets/DeleteBucket")), 75 ); 76 const AccessRulePanel = withSuspense( 77 React.lazy(() => import("./AccessRulePanel")), 78 ); 79 const AccessDetailsPanel = withSuspense( 80 React.lazy(() => import("./AccessDetailsPanel")), 81 ); 82 const BucketSummaryPanel = withSuspense( 83 React.lazy(() => import("./BucketSummaryPanel")), 84 ); 85 const BucketEventsPanel = withSuspense( 86 React.lazy(() => import("./BucketEventsPanel")), 87 ); 88 const BucketReplicationPanel = withSuspense( 89 React.lazy(() => import("./BucketReplicationPanel")), 90 ); 91 const BucketLifecyclePanel = withSuspense( 92 React.lazy(() => import("./BucketLifecyclePanel")), 93 ); 94 95 const BucketDetails = () => { 96 const dispatch = useAppDispatch(); 97 const navigate = useNavigate(); 98 const params = useParams(); 99 const location = useLocation(); 100 101 const distributedSetup = useSelector(selDistSet); 102 const loadingBucket = useSelector(selBucketDetailsLoading); 103 const bucketInfo = useSelector(selBucketDetailsInfo); 104 const siteReplicationInfo = useSelector(selSiteRep); 105 106 const [iniLoad, setIniLoad] = useState<boolean>(false); 107 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 108 const bucketName = params.bucketName || ""; 109 110 const canDelete = hasPermission(bucketName, deleteBucketPermissions); 111 const canBrowse = hasPermission(bucketName, browseBucketPermissions); 112 113 useEffect(() => { 114 dispatch(setHelpName("bucket_details")); 115 // eslint-disable-next-line react-hooks/exhaustive-deps 116 }, []); 117 118 useEffect(() => { 119 if (!iniLoad) { 120 dispatch(setBucketDetailsLoad(true)); 121 setIniLoad(true); 122 } 123 }, [iniLoad, dispatch, setIniLoad]); 124 125 useEffect(() => { 126 if (loadingBucket) { 127 api.buckets 128 .bucketInfo(bucketName) 129 .then((res) => { 130 dispatch(setBucketDetailsLoad(false)); 131 dispatch(setBucketInfo(res.data)); 132 }) 133 .catch((err) => { 134 dispatch(setBucketDetailsLoad(false)); 135 dispatch(setErrorSnackMessage(errorToHandler(err))); 136 }); 137 } 138 }, [bucketName, loadingBucket, dispatch]); 139 140 let topLevelRoute = `/buckets/${bucketName}`; 141 const defaultRoute = "/admin/summary"; 142 143 const manageBucketRoutes: Record<string, any> = { 144 events: "/admin/events", 145 replication: "/admin/replication", 146 lifecycle: "/admin/lifecycle", 147 access: "/admin/access", 148 prefix: "/admin/prefix", 149 }; 150 151 const getRoutePath = (routeKey: string) => { 152 let path = manageBucketRoutes[routeKey]; 153 if (!path) { 154 path = `${topLevelRoute}${defaultRoute}`; 155 } else { 156 path = `${topLevelRoute}${path}`; 157 } 158 return path; 159 }; 160 161 const closeDeleteModalAndRefresh = (refresh: boolean) => { 162 setDeleteOpen(false); 163 if (refresh) { 164 navigate("/buckets"); 165 } 166 }; 167 168 return ( 169 <Fragment> 170 {deleteOpen && ( 171 <DeleteBucket 172 deleteOpen={deleteOpen} 173 selectedBucket={bucketName} 174 closeDeleteModalAndRefresh={(refresh: boolean) => { 175 closeDeleteModalAndRefresh(refresh); 176 }} 177 /> 178 )} 179 <PageHeaderWrapper 180 label={ 181 <BackLink label={"Buckets"} onClick={() => navigate("/buckets")} /> 182 } 183 actions={ 184 <Fragment> 185 <TooltipWrapper 186 tooltip={ 187 canBrowse 188 ? "Browse Bucket" 189 : permissionTooltipHelper( 190 IAM_PERMISSIONS[IAM_ROLES.BUCKET_VIEWER], 191 "browsing this bucket", 192 ) 193 } 194 > 195 <Button 196 id={"switch-browse-view"} 197 aria-label="Browse Bucket" 198 onClick={() => { 199 navigate(`/browser/${bucketName}`); 200 }} 201 icon={ 202 <FolderIcon 203 style={{ width: 20, height: 20, marginTop: -3 }} 204 /> 205 } 206 style={{ 207 padding: "0 10px", 208 }} 209 disabled={!canBrowse} 210 /> 211 </TooltipWrapper> 212 <HelpMenu /> 213 </Fragment> 214 } 215 /> 216 <PageLayout> 217 <ScreenTitle 218 icon={ 219 <Fragment> 220 <BucketsIcon width={40} /> 221 </Fragment> 222 } 223 title={bucketName} 224 subTitle={ 225 <SecureComponent 226 scopes={[ 227 IAM_SCOPES.S3_GET_BUCKET_POLICY, 228 IAM_SCOPES.S3_GET_ACTIONS, 229 ]} 230 resource={bucketName} 231 > 232 <span style={{ fontSize: 15 }}>Access: </span> 233 <span 234 style={{ 235 fontWeight: 600, 236 fontSize: 15, 237 textTransform: "capitalize", 238 }} 239 > 240 {bucketInfo?.access?.toLowerCase()} 241 </span> 242 </SecureComponent> 243 } 244 actions={ 245 <Fragment> 246 <SecureComponent 247 scopes={deleteBucketPermissions} 248 resource={bucketName} 249 errorProps={{ disabled: true }} 250 > 251 <TooltipWrapper 252 tooltip={ 253 canDelete 254 ? "" 255 : permissionTooltipHelper( 256 [ 257 IAM_SCOPES.S3_DELETE_BUCKET, 258 IAM_SCOPES.S3_FORCE_DELETE_BUCKET, 259 ], 260 "deleting this bucket", 261 ) 262 } 263 > 264 <Button 265 id={"delete-bucket-button"} 266 onClick={() => { 267 setDeleteOpen(true); 268 }} 269 label={"Delete Bucket"} 270 icon={<TrashIcon />} 271 variant={"secondary"} 272 disabled={!canDelete} 273 /> 274 </TooltipWrapper> 275 </SecureComponent> 276 <Button 277 id={"refresh-bucket-info"} 278 onClick={() => { 279 dispatch(setBucketDetailsLoad(true)); 280 }} 281 label={"Refresh"} 282 icon={<RefreshIcon />} 283 /> 284 </Fragment> 285 } 286 sx={{ marginBottom: 15 }} 287 /> 288 <Box> 289 <Tabs 290 currentTabOrPath={location.pathname} 291 useRouteTabs 292 onTabClick={(tab) => { 293 navigate(tab); 294 }} 295 options={[ 296 { 297 tabConfig: { 298 label: "Summary", 299 id: "summary", 300 to: getRoutePath("summary"), 301 }, 302 }, 303 { 304 tabConfig: { 305 label: "Events", 306 id: "events", 307 disabled: !hasPermission(bucketName, [ 308 IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS, 309 IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS, 310 IAM_SCOPES.S3_GET_ACTIONS, 311 IAM_SCOPES.S3_PUT_ACTIONS, 312 ]), 313 to: getRoutePath("events"), 314 }, 315 }, 316 { 317 tabConfig: { 318 label: "Replication", 319 id: "replication", 320 disabled: 321 !distributedSetup || 322 (siteReplicationInfo.enabled && 323 siteReplicationInfo.curSite) || 324 !hasPermission(bucketName, [ 325 IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, 326 IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, 327 IAM_SCOPES.S3_GET_ACTIONS, 328 IAM_SCOPES.S3_PUT_ACTIONS, 329 ]), 330 to: getRoutePath("replication"), 331 }, 332 }, 333 { 334 tabConfig: { 335 label: "Lifecycle", 336 id: "lifecycle", 337 disabled: 338 !distributedSetup || 339 !hasPermission(bucketName, [ 340 IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION, 341 IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION, 342 IAM_SCOPES.S3_GET_ACTIONS, 343 IAM_SCOPES.S3_PUT_ACTIONS, 344 ]), 345 to: getRoutePath("lifecycle"), 346 }, 347 }, 348 { 349 tabConfig: { 350 label: "Access", 351 id: "access", 352 disabled: !hasPermission(bucketName, [ 353 IAM_SCOPES.ADMIN_GET_POLICY, 354 IAM_SCOPES.ADMIN_LIST_USER_POLICIES, 355 IAM_SCOPES.ADMIN_LIST_USERS, 356 ]), 357 to: getRoutePath("access"), 358 }, 359 }, 360 { 361 tabConfig: { 362 label: "Anonymous", 363 id: "anonymous", 364 disabled: !hasPermission(bucketName, [ 365 IAM_SCOPES.S3_GET_BUCKET_POLICY, 366 IAM_SCOPES.S3_GET_ACTIONS, 367 ]), 368 to: getRoutePath("prefix"), 369 }, 370 }, 371 ]} 372 routes={ 373 <Routes> 374 <Route path="summary" element={<BucketSummaryPanel />} /> 375 <Route path="events" element={<BucketEventsPanel />} /> 376 {distributedSetup && ( 377 <Route 378 path="replication" 379 element={<BucketReplicationPanel />} 380 /> 381 )} 382 {distributedSetup && ( 383 <Route path="lifecycle" element={<BucketLifecyclePanel />} /> 384 )} 385 386 <Route path="access" element={<AccessDetailsPanel />} /> 387 <Route path="prefix" element={<AccessRulePanel />} /> 388 <Route 389 path="*" 390 element={ 391 <Navigate to={`/buckets/${bucketName}/admin/summary`} /> 392 } 393 /> 394 </Routes> 395 } 396 /> 397 </Box> 398 </PageLayout> 399 </Fragment> 400 ); 401 }; 402 403 export default BucketDetails;