github.com/minio/console@v1.4.1/web-app/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.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 import { useSelector } from "react-redux"; 19 import CopyToClipboard from "react-copy-to-clipboard"; 20 import styled from "styled-components"; 21 import { Link, useNavigate } from "react-router-dom"; 22 import { encodeURLString, safeDecodeURIComponent } from "../../../common/utils"; 23 import { 24 Button, 25 CopyIcon, 26 NewPathIcon, 27 Tooltip, 28 Breadcrumbs, 29 breakPoints, 30 Box, 31 } from "mds"; 32 import { hasPermission } from "../../../common/SecureComponent"; 33 import { 34 IAM_SCOPES, 35 permissionTooltipHelper, 36 } from "../../../common/SecureComponent/permissions"; 37 import withSuspense from "../Common/Components/withSuspense"; 38 import { setSnackBarMessage } from "../../../systemSlice"; 39 import { AppState, useAppDispatch } from "../../../store"; 40 import { setVersionsModeEnabled } from "./objectBrowserSlice"; 41 import { getSessionGrantsWildCard } from "../Buckets/ListBuckets/UploadPermissionUtils"; 42 43 const CreatePathModal = withSuspense( 44 React.lazy( 45 () => import("../Buckets/ListBuckets/Objects/ListObjects/CreatePathModal"), 46 ), 47 ); 48 49 const BreadcrumbsMain = styled.div(() => ({ 50 display: "flex", 51 "& .additionalOptions": { 52 paddingRight: "10px", 53 display: "flex", 54 alignItems: "center", 55 [`@media (max-width: ${breakPoints.lg}px)`]: { 56 display: "none", 57 }, 58 }, 59 "& .slashSpacingStyle": { 60 margin: "0 5px", 61 }, 62 })); 63 64 interface IObjectBrowser { 65 bucketName: string; 66 internalPaths: string; 67 hidePathButton?: boolean; 68 additionalOptions?: React.ReactNode; 69 } 70 71 const BrowserBreadcrumbs = ({ 72 bucketName, 73 internalPaths, 74 hidePathButton, 75 additionalOptions, 76 }: IObjectBrowser) => { 77 const dispatch = useAppDispatch(); 78 const navigate = useNavigate(); 79 80 const rewindEnabled = useSelector( 81 (state: AppState) => state.objectBrowser.rewind.rewindEnabled, 82 ); 83 const versionsMode = useSelector( 84 (state: AppState) => state.objectBrowser.versionsMode, 85 ); 86 const versionedFile = useSelector( 87 (state: AppState) => state.objectBrowser.versionedFile, 88 ); 89 const anonymousMode = useSelector( 90 (state: AppState) => state.system.anonymousMode, 91 ); 92 93 const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false); 94 const [canCreateSubpath, setCanCreateSubpath] = useState<boolean>(false); 95 96 const putObjectPermScopes = [ 97 IAM_SCOPES.S3_PUT_OBJECT, 98 IAM_SCOPES.S3_PUT_ACTIONS, 99 ]; 100 101 const sessionGrants = useSelector((state: AppState) => 102 state.console.session ? state.console.session.permissions || {} : {}, 103 ); 104 105 let paths = internalPaths; 106 107 if (internalPaths !== "") { 108 paths = `/${internalPaths}`; 109 } 110 111 const splitPaths = paths.split("/").filter((path) => path !== ""); 112 const lastBreadcrumbsIndex = splitPaths.length - 1; 113 114 const pathToCheckPerms = bucketName + paths || bucketName; 115 const sessionGrantWildCards = getSessionGrantsWildCard( 116 sessionGrants, 117 pathToCheckPerms, 118 putObjectPermScopes, 119 ); 120 121 useEffect(() => { 122 setCanCreateSubpath(false); 123 Object.keys(sessionGrants).forEach((grant) => { 124 grant.includes(pathToCheckPerms) && 125 grant.includes("/*") && 126 setCanCreateSubpath(true); 127 }); 128 }, [pathToCheckPerms, internalPaths, sessionGrants]); 129 130 const canCreatePath = 131 hasPermission( 132 [pathToCheckPerms, ...sessionGrantWildCards], 133 putObjectPermScopes, 134 ) || 135 anonymousMode || 136 canCreateSubpath; 137 138 let breadcrumbsMap = splitPaths.map((objectItem: string, index: number) => { 139 const subSplit = `${splitPaths.slice(0, index + 1).join("/")}/`; 140 const route = `/browser/${bucketName}/${ 141 subSplit ? `${encodeURLString(subSplit)}` : `` 142 }`; 143 144 if (index === lastBreadcrumbsIndex && objectItem === versionedFile) { 145 return null; 146 } 147 148 return ( 149 <Fragment key={`breadcrumbs-${index.toString()}`}> 150 <span className={"slashSpacingStyle"}>/</span> 151 {index === lastBreadcrumbsIndex ? ( 152 <span style={{ cursor: "default", whiteSpace: "pre" }}> 153 {safeDecodeURIComponent(objectItem) /*Only for display*/} 154 </span> 155 ) : ( 156 <Link 157 style={{ 158 whiteSpace: "pre", 159 }} 160 to={route} 161 onClick={() => { 162 dispatch( 163 setVersionsModeEnabled({ status: false, objectName: "" }), 164 ); 165 }} 166 > 167 { 168 safeDecodeURIComponent( 169 objectItem, 170 ) /*Only for display to preserve */ 171 } 172 </Link> 173 )} 174 </Fragment> 175 ); 176 }); 177 178 let versionsItem: any[] = []; 179 180 if (versionsMode) { 181 versionsItem = [ 182 <Fragment key={`breadcrumbs-versionedItem`}> 183 <span> 184 <span className={"slashSpacingStyle"}>/</span> 185 {versionedFile} - Versions 186 </span> 187 </Fragment>, 188 ]; 189 } 190 191 const listBreadcrumbs: any[] = [ 192 <Fragment key={`breadcrumbs-root-path`}> 193 <Link 194 to={`/browser/${bucketName}`} 195 onClick={() => { 196 dispatch(setVersionsModeEnabled({ status: false, objectName: "" })); 197 }} 198 > 199 {bucketName} 200 </Link> 201 </Fragment>, 202 ...breadcrumbsMap, 203 ...versionsItem, 204 ]; 205 206 const closeAddFolderModal = () => { 207 setCreateFolderOpen(false); 208 }; 209 210 const goBackFunction = () => { 211 if (versionsMode) { 212 dispatch(setVersionsModeEnabled({ status: false, objectName: "" })); 213 } else { 214 if (splitPaths.length === 0) { 215 navigate("/browser"); 216 217 return; 218 } 219 220 const prevPath = splitPaths.slice(0, -1); 221 222 navigate( 223 `/browser/${bucketName}${ 224 prevPath.length > 0 225 ? `/${encodeURLString(`${prevPath.join("/")}/`)}` 226 : "" 227 }`, 228 ); 229 } 230 }; 231 232 return ( 233 <Fragment> 234 <BreadcrumbsMain> 235 {createFolderOpen && ( 236 <CreatePathModal 237 modalOpen={createFolderOpen} 238 bucketName={bucketName} 239 folderName={internalPaths} 240 onClose={closeAddFolderModal} 241 limitedSubPath={ 242 canCreateSubpath && 243 !( 244 hasPermission( 245 [pathToCheckPerms, ...sessionGrantWildCards], 246 putObjectPermScopes, 247 ) || anonymousMode 248 ) 249 } 250 /> 251 )} 252 <Breadcrumbs 253 sx={{ 254 whiteSpace: "pre", 255 }} 256 goBackFunction={goBackFunction} 257 additionalOptions={ 258 <Fragment> 259 <CopyToClipboard text={`${bucketName}/${splitPaths.join("/")}`}> 260 <Button 261 id={"copy-path"} 262 icon={ 263 <CopyIcon 264 style={{ 265 width: "12px", 266 height: "12px", 267 fill: "#969FA8", 268 marginTop: -1, 269 }} 270 /> 271 } 272 variant={"regular"} 273 onClick={() => { 274 dispatch(setSnackBarMessage("Path copied to clipboard")); 275 }} 276 style={{ 277 width: "28px", 278 height: "28px", 279 color: "#969FA8", 280 border: "#969FA8 1px solid", 281 marginRight: 5, 282 }} 283 /> 284 </CopyToClipboard> 285 <Box className={"additionalOptions"}>{additionalOptions}</Box> 286 </Fragment> 287 } 288 > 289 {listBreadcrumbs} 290 </Breadcrumbs> 291 {!hidePathButton && ( 292 <Tooltip 293 tooltip={ 294 canCreatePath 295 ? "Choose or create a new path" 296 : permissionTooltipHelper( 297 [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS], 298 "create a new path", 299 ) 300 } 301 > 302 <Button 303 id={"new-path"} 304 onClick={() => { 305 setCreateFolderOpen(true); 306 }} 307 disabled={anonymousMode ? false : rewindEnabled || !canCreatePath} 308 icon={<NewPathIcon style={{ fill: "#969FA8" }} />} 309 style={{ 310 whiteSpace: "nowrap", 311 }} 312 variant={"regular"} 313 label={"Create new path"} 314 /> 315 </Tooltip> 316 )} 317 </BreadcrumbsMain> 318 <Box 319 sx={{ 320 display: "none", 321 marginTop: 15, 322 marginBottom: 5, 323 justifyContent: "flex-start", 324 "& > div": { 325 fontSize: 12, 326 fontWeight: "normal", 327 flexDirection: "row", 328 flexWrap: "nowrap", 329 }, 330 [`@media (max-width: ${breakPoints.lg}px)`]: { 331 display: "flex", 332 }, 333 }} 334 > 335 {additionalOptions} 336 </Box> 337 </Fragment> 338 ); 339 }; 340 341 export default BrowserBreadcrumbs;