github.com/minio/console@v1.4.1/web-app/src/screens/Console/ObjectBrowser/OBBucketList.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2022 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 19 import { useNavigate } from "react-router-dom"; 20 import { 21 ActionLink, 22 BucketsIcon, 23 Button, 24 DataTable, 25 HelpBox, 26 PageLayout, 27 ProgressBar, 28 RefreshIcon, 29 Grid, 30 HelpTip, 31 } from "mds"; 32 import { actionsTray } from "../Common/FormComponents/common/styleLibrary"; 33 import { SecureComponent } from "../../../common/SecureComponent"; 34 import { 35 CONSOLE_UI_RESOURCE, 36 IAM_PAGES, 37 IAM_SCOPES, 38 permissionTooltipHelper, 39 } from "../../../common/SecureComponent/permissions"; 40 import SearchBox from "../Common/SearchBox"; 41 import hasPermission from "../../../common/SecureComponent/accessControl"; 42 import { setErrorSnackMessage, setHelpName } from "../../../systemSlice"; 43 import { useAppDispatch } from "../../../store"; 44 import { useSelector } from "react-redux"; 45 import { selFeatures } from "../consoleSlice"; 46 import AutoColorIcon from "../Common/Components/AutoColorIcon"; 47 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 48 import { niceBytesInt } from "../../../common/utils"; 49 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 50 import { Bucket } from "../../../api/consoleApi"; 51 import { api } from "../../../api"; 52 import { errorToHandler } from "../../../api/errors"; 53 import HelpMenu from "../HelpMenu"; 54 import { usageClarifyingContent } from "../Dashboard/BasicDashboard/ReportedUsage"; 55 56 const OBListBuckets = () => { 57 const dispatch = useAppDispatch(); 58 const navigate = useNavigate(); 59 60 const [records, setRecords] = useState<Bucket[]>([]); 61 const [loading, setLoading] = useState<boolean>(true); 62 const [clickOverride, setClickOverride] = useState<boolean>(false); 63 const [filterBuckets, setFilterBuckets] = useState<string>(""); 64 65 const features = useSelector(selFeatures); 66 const obOnly = !!features?.includes("object-browser-only"); 67 68 useEffect(() => { 69 if (loading) { 70 const fetchRecords = () => { 71 setLoading(true); 72 api.buckets 73 .listBuckets() 74 .then((res) => { 75 if (res.data) { 76 setLoading(false); 77 setRecords(res.data.buckets || []); 78 } 79 }) 80 .catch((err) => { 81 setLoading(false); 82 dispatch(setErrorSnackMessage(errorToHandler(err))); 83 }); 84 }; 85 fetchRecords(); 86 } 87 }, [loading, dispatch]); 88 89 const filteredRecords = records.filter((b: Bucket) => { 90 if (filterBuckets === "") { 91 return true; 92 } else { 93 return b.name.indexOf(filterBuckets) >= 0; 94 } 95 }); 96 97 const hasBuckets = records.length > 0; 98 99 const canListBuckets = hasPermission("*", [ 100 IAM_SCOPES.S3_LIST_BUCKET, 101 IAM_SCOPES.S3_ALL_LIST_BUCKET, 102 ]); 103 104 const tableActions = [ 105 { 106 type: "view", 107 onClick: (bucket: Bucket) => { 108 !clickOverride && 109 navigate(`${IAM_PAGES.OBJECT_BROWSER_VIEW}/${bucket.name}`); 110 }, 111 }, 112 ]; 113 114 useEffect(() => { 115 dispatch(setHelpName("object_browser")); 116 }, [dispatch]); 117 118 return ( 119 <Fragment> 120 {!obOnly && ( 121 <PageHeaderWrapper label={"Object Browser"} actions={<HelpMenu />} /> 122 )} 123 124 <PageLayout> 125 <Grid item xs={12} sx={{ ...actionsTray.actionsTray, display: "flex" }}> 126 {obOnly && ( 127 <Grid item xs> 128 <AutoColorIcon marginRight={15} marginTop={10} /> 129 </Grid> 130 )} 131 {hasBuckets && ( 132 <SearchBox 133 onChange={setFilterBuckets} 134 placeholder="Filter Buckets" 135 value={filterBuckets} 136 sx={{ 137 minWidth: 380, 138 "@media (max-width: 900px)": { 139 minWidth: 220, 140 }, 141 }} 142 /> 143 )} 144 145 <Grid 146 item 147 xs={12} 148 sx={{ 149 display: "flex", 150 alignItems: "center", 151 justifyContent: "flex-end", 152 gap: 8, 153 }} 154 > 155 <TooltipWrapper tooltip={"Refresh"}> 156 <Button 157 id={"refresh-buckets"} 158 onClick={() => { 159 setLoading(true); 160 }} 161 icon={<RefreshIcon />} 162 variant={"regular"} 163 /> 164 </TooltipWrapper> 165 </Grid> 166 </Grid> 167 168 {loading && <ProgressBar />} 169 {!loading && ( 170 <Grid 171 item 172 xs={12} 173 sx={{ 174 marginTop: 25, 175 height: "calc(100vh - 211px)", 176 "&.isEmbedded": { 177 height: "calc(100vh - 128px)", 178 }, 179 }} 180 className={obOnly ? "isEmbedded" : ""} 181 > 182 {filteredRecords.length !== 0 && ( 183 <DataTable 184 isLoading={loading} 185 records={filteredRecords} 186 entityName={"Buckets"} 187 idField={"name"} 188 columns={[ 189 { 190 label: "Name", 191 elementKey: "name", 192 renderFunction: (label) => ( 193 <div style={{ display: "flex" }}> 194 <BucketsIcon 195 style={{ width: 15, marginRight: 5, minWidth: 15 }} 196 /> 197 <span 198 id={`browse-${label}`} 199 style={{ 200 whiteSpace: "nowrap", 201 overflow: "hidden", 202 textOverflow: "ellipsis", 203 minWidth: 0, 204 }} 205 > 206 {label} 207 </span> 208 </div> 209 ), 210 }, 211 { 212 label: "Objects", 213 elementKey: "objects", 214 renderFunction: (size: number | null) => 215 size ? size.toLocaleString() : 0, 216 }, 217 { 218 label: "Size", 219 elementKey: "size", 220 renderFunction: (size: number) => ( 221 <div 222 onMouseEnter={() => setClickOverride(true)} 223 onMouseLeave={() => setClickOverride(false)} 224 > 225 <HelpTip 226 content={usageClarifyingContent} 227 placement="right" 228 > 229 {niceBytesInt(size || 0)} 230 </HelpTip> 231 </div> 232 ), 233 }, 234 { 235 label: "Access", 236 elementKey: "rw_access", 237 renderFullObject: true, 238 renderFunction: (bucket: Bucket) => { 239 let access = []; 240 if (bucket.rw_access?.read) { 241 access.push("R"); 242 } 243 if (bucket.rw_access?.write) { 244 access.push("W"); 245 } 246 return <span>{access.join("/")}</span>; 247 }, 248 }, 249 ]} 250 itemActions={tableActions} 251 /> 252 )} 253 {filteredRecords.length === 0 && filterBuckets !== "" && ( 254 <Grid 255 container 256 sx={{ 257 justifyContent: "center", 258 alignContent: "center", 259 alignItems: "center", 260 }} 261 > 262 <Grid item xs={8}> 263 <HelpBox 264 iconComponent={<BucketsIcon />} 265 title={"No Results"} 266 help={ 267 <Fragment> 268 No buckets match the filtering condition 269 </Fragment> 270 } 271 /> 272 </Grid> 273 </Grid> 274 )} 275 {!hasBuckets && ( 276 <Grid 277 container 278 sx={{ 279 justifyContent: "center", 280 alignContent: "center", 281 alignItems: "center", 282 }} 283 > 284 <Grid item xs={8}> 285 <HelpBox 286 iconComponent={<BucketsIcon />} 287 title={"Buckets"} 288 help={ 289 <Fragment> 290 MinIO uses buckets to organize objects. A bucket is 291 similar to a folder or directory in a filesystem, where 292 each bucket can hold an arbitrary number of objects. 293 <br /> 294 {canListBuckets ? ( 295 "" 296 ) : ( 297 <Fragment> 298 <br /> 299 {permissionTooltipHelper( 300 [ 301 IAM_SCOPES.S3_LIST_BUCKET, 302 IAM_SCOPES.S3_ALL_LIST_BUCKET, 303 ], 304 "view the buckets on this server", 305 )} 306 <br /> 307 </Fragment> 308 )} 309 <SecureComponent 310 scopes={[IAM_SCOPES.S3_CREATE_BUCKET]} 311 resource={CONSOLE_UI_RESOURCE} 312 > 313 <br /> 314 To get started, 315 <ActionLink 316 onClick={() => { 317 navigate(IAM_PAGES.ADD_BUCKETS); 318 }} 319 > 320 Create a Bucket. 321 </ActionLink> 322 </SecureComponent> 323 </Fragment> 324 } 325 /> 326 </Grid> 327 </Grid> 328 )} 329 </Grid> 330 )} 331 </PageLayout> 332 </Fragment> 333 ); 334 }; 335 336 export default OBListBuckets;