github.com/minio/console@v1.4.1/web-app/src/screens/Console/Console.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, { 18 Fragment, 19 Suspense, 20 useEffect, 21 useLayoutEffect, 22 useState, 23 } from "react"; 24 import { Box, Button, MainContainer, ProgressBar, Snackbar } from "mds"; 25 import debounce from "lodash/debounce"; 26 import { Navigate, Route, Routes, useLocation } from "react-router-dom"; 27 import { useSelector } from "react-redux"; 28 import { selFeatures, selSession } from "./consoleSlice"; 29 import { api } from "api"; 30 import { AppState, useAppDispatch } from "../../store"; 31 import MainError from "./Common/MainError/MainError"; 32 import { 33 CONSOLE_UI_RESOURCE, 34 IAM_PAGES, 35 IAM_PAGES_PERMISSIONS, 36 IAM_SCOPES, 37 S3_ALL_RESOURCES, 38 } from "../../common/SecureComponent/permissions"; 39 import { hasPermission } from "../../common/SecureComponent"; 40 import { IRouteRule } from "./Menu/types"; 41 import { 42 menuOpen, 43 selDistSet, 44 serverIsLoading, 45 setServerNeedsRestart, 46 setSnackBarMessage, 47 } from "../../systemSlice"; 48 import MenuWrapper from "./Menu/MenuWrapper"; 49 import LoadingComponent from "../../common/LoadingComponent"; 50 import ComponentsScreen from "./Common/ComponentsScreen"; 51 52 const Trace = React.lazy(() => import("./Trace/Trace")); 53 const Watch = React.lazy(() => import("./Watch/Watch")); 54 const HealthInfo = React.lazy(() => import("./HealthInfo/HealthInfo")); 55 56 const EventDestinations = React.lazy( 57 () => import("./EventDestinations/EventDestinations"), 58 ); 59 const AddEventDestination = React.lazy( 60 () => import("./EventDestinations/AddEventDestination"), 61 ); 62 const EventTypeSelector = React.lazy( 63 () => import("./EventDestinations/EventTypeSelector"), 64 ); 65 66 const ListTiersConfiguration = React.lazy( 67 () => import("./Configurations/TiersConfiguration/ListTiersConfiguration"), 68 ); 69 const TierTypeSelector = React.lazy( 70 () => import("./Configurations/TiersConfiguration/TierTypeSelector"), 71 ); 72 const AddTierConfiguration = React.lazy( 73 () => import("./Configurations/TiersConfiguration/AddTierConfiguration"), 74 ); 75 76 const ErrorLogs = React.lazy(() => import("./Logs/ErrorLogs/ErrorLogs")); 77 const LogsSearchMain = React.lazy( 78 () => import("./Logs/LogSearch/LogsSearchMain"), 79 ); 80 const GroupsDetails = React.lazy(() => import("./Groups/GroupsDetails")); 81 82 const Tools = React.lazy(() => import("./Tools/Tools")); 83 const IconsScreen = React.lazy(() => import("./Common/IconsScreen")); 84 85 const Speedtest = React.lazy(() => import("./Speedtest/Speedtest")); 86 87 const ObjectManager = React.lazy( 88 () => import("./Common/ObjectManager/ObjectManager"), 89 ); 90 91 const ObjectBrowser = React.lazy(() => import("./ObjectBrowser/ObjectBrowser")); 92 93 const Buckets = React.lazy(() => import("./Buckets/Buckets")); 94 95 const EditBucketReplication = React.lazy( 96 () => import("./Buckets/BucketDetails/EditBucketReplication"), 97 ); 98 const AddBucketReplication = React.lazy( 99 () => import("./Buckets/BucketDetails/AddBucketReplication"), 100 ); 101 const Policies = React.lazy(() => import("./Policies/Policies")); 102 103 const AddPolicyScreen = React.lazy(() => import("./Policies/AddPolicyScreen")); 104 const Dashboard = React.lazy(() => import("./Dashboard/Dashboard")); 105 106 const Account = React.lazy(() => import("./Account/Account")); 107 108 const AccountCreate = React.lazy( 109 () => import("./Account/AddServiceAccountScreen"), 110 ); 111 112 const Users = React.lazy(() => import("./Users/Users")); 113 const Groups = React.lazy(() => import("./Groups/Groups")); 114 const IDPOpenIDConfigurations = React.lazy( 115 () => import("./IDP/IDPOpenIDConfigurations"), 116 ); 117 const AddIDPOpenIDConfiguration = React.lazy( 118 () => import("./IDP/AddIDPOpenIDConfiguration"), 119 ); 120 const IDPLDAPConfigurationDetails = React.lazy( 121 () => import("./IDP/LDAP/IDPLDAPConfigurationDetails"), 122 ); 123 const IDPOpenIDConfigurationDetails = React.lazy( 124 () => import("./IDP/IDPOpenIDConfigurationDetails"), 125 ); 126 127 const License = React.lazy(() => import("./License/License")); 128 const ConfigurationOptions = React.lazy( 129 () => import("./Configurations/ConfigurationPanels/ConfigurationOptions"), 130 ); 131 132 const AddGroupScreen = React.lazy(() => import("./Groups/AddGroupScreen")); 133 const SiteReplication = React.lazy( 134 () => import("./Configurations/SiteReplication/SiteReplication"), 135 ); 136 const SiteReplicationStatus = React.lazy( 137 () => import("./Configurations/SiteReplication/SiteReplicationStatus"), 138 ); 139 140 const AddReplicationSites = React.lazy( 141 () => import("./Configurations/SiteReplication/AddReplicationSites"), 142 ); 143 144 const KMSRoutes = React.lazy(() => import("./KMS/KMSRoutes")); 145 146 const Console = () => { 147 const dispatch = useAppDispatch(); 148 const { pathname = "" } = useLocation(); 149 const open = useSelector((state: AppState) => state.system.sidebarOpen); 150 const session = useSelector(selSession); 151 const features = useSelector(selFeatures); 152 const distributedSetup = useSelector(selDistSet); 153 const snackBarMessage = useSelector( 154 (state: AppState) => state.system.snackBar, 155 ); 156 const needsRestart = useSelector( 157 (state: AppState) => state.system.serverNeedsRestart, 158 ); 159 const isServerLoading = useSelector( 160 (state: AppState) => state.system.serverIsLoading, 161 ); 162 const loadingProgress = useSelector( 163 (state: AppState) => state.system.loadingProgress, 164 ); 165 166 const [openSnackbar, setOpenSnackbar] = useState<boolean>(false); 167 168 const ldapIsEnabled = (features && features.includes("ldap-idp")) || false; 169 const kmsIsEnabled = (features && features.includes("kms")) || false; 170 const obOnly = !!features?.includes("object-browser-only"); 171 172 useEffect(() => { 173 dispatch({ type: "socket/OBConnect" }); 174 }, [dispatch]); 175 176 const restartServer = () => { 177 dispatch(serverIsLoading(true)); 178 api.service 179 .restartService({}) 180 .then(() => { 181 console.log("success restarting service"); 182 dispatch(serverIsLoading(false)); 183 dispatch(setServerNeedsRestart(false)); 184 }) 185 .catch((err) => { 186 if (err.error.errorMessage === "Error 502") { 187 dispatch(setServerNeedsRestart(false)); 188 } 189 dispatch(serverIsLoading(false)); 190 console.log("failure restarting service"); 191 console.error(err.error); 192 }); 193 }; 194 195 // Layout effect to be executed after last re-render for resizing only 196 useLayoutEffect(() => { 197 // Debounce to not execute constantly 198 const debounceSize = debounce(() => { 199 if (open && window.innerWidth <= 1024) { 200 dispatch(menuOpen(false)); 201 } 202 }, 300); 203 204 // Added event listener for window resize 205 window.addEventListener("resize", debounceSize); 206 207 // We remove the listener on component unmount 208 return () => window.removeEventListener("resize", debounceSize); 209 }); 210 211 const consoleAdminRoutes: IRouteRule[] = [ 212 { 213 component: ObjectBrowser, 214 path: IAM_PAGES.OBJECT_BROWSER_VIEW, 215 forceDisplay: true, 216 customPermissionFnc: () => { 217 const path = window.location.pathname; 218 const resource = path.match(/browser\/(.*)\//); 219 return ( 220 resource && 221 resource.length > 0 && 222 hasPermission( 223 resource[1], 224 IAM_PAGES_PERMISSIONS[IAM_PAGES.OBJECT_BROWSER_VIEW], 225 ) 226 ); 227 }, 228 }, 229 { 230 component: Buckets, 231 path: IAM_PAGES.BUCKETS, 232 forceDisplay: true, 233 }, 234 { 235 component: Dashboard, 236 path: IAM_PAGES.DASHBOARD, 237 }, 238 { 239 component: Buckets, 240 path: IAM_PAGES.ADD_BUCKETS, 241 customPermissionFnc: () => { 242 return hasPermission("*", IAM_PAGES_PERMISSIONS[IAM_PAGES.ADD_BUCKETS]); 243 }, 244 }, 245 { 246 component: AddBucketReplication, 247 path: IAM_PAGES.BUCKETS_ADD_REPLICATION, 248 customPermissionFnc: () => { 249 return hasPermission( 250 "*", 251 IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_ADD_REPLICATION], 252 ); 253 }, 254 }, 255 { 256 component: EditBucketReplication, 257 path: IAM_PAGES.BUCKETS_EDIT_REPLICATION, 258 customPermissionFnc: () => { 259 return hasPermission( 260 "*", 261 IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_EDIT_REPLICATION], 262 ); 263 }, 264 }, 265 { 266 component: Buckets, 267 path: IAM_PAGES.BUCKETS_ADMIN_VIEW, 268 customPermissionFnc: () => { 269 const path = window.location.pathname; 270 const resource = path.match(/buckets\/(.*)\/admin*/); 271 return ( 272 resource && 273 resource.length > 0 && 274 hasPermission( 275 resource[1], 276 IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_ADMIN_VIEW], 277 ) 278 ); 279 }, 280 }, 281 282 { 283 component: Watch, 284 path: IAM_PAGES.TOOLS_WATCH, 285 }, 286 { 287 component: Speedtest, 288 path: IAM_PAGES.TOOLS_SPEEDTEST, 289 }, 290 { 291 component: Users, 292 path: IAM_PAGES.USERS, 293 fsHidden: ldapIsEnabled, 294 customPermissionFnc: () => 295 hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.ADMIN_LIST_USERS]) || 296 hasPermission(S3_ALL_RESOURCES, [IAM_SCOPES.ADMIN_CREATE_USER]), 297 }, 298 { 299 component: Groups, 300 path: IAM_PAGES.GROUPS, 301 fsHidden: ldapIsEnabled, 302 }, 303 { 304 component: AddGroupScreen, 305 path: IAM_PAGES.GROUPS_ADD, 306 }, 307 { 308 component: GroupsDetails, 309 path: IAM_PAGES.GROUPS_VIEW, 310 }, 311 { 312 component: Policies, 313 path: IAM_PAGES.POLICIES_VIEW, 314 }, 315 { 316 component: AddPolicyScreen, 317 path: IAM_PAGES.POLICY_ADD, 318 }, 319 { 320 component: Policies, 321 path: IAM_PAGES.POLICIES, 322 }, 323 { 324 component: IDPLDAPConfigurationDetails, 325 path: IAM_PAGES.IDP_LDAP_CONFIGURATIONS, 326 }, 327 { 328 component: IDPOpenIDConfigurations, 329 path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS, 330 }, 331 { 332 component: AddIDPOpenIDConfiguration, 333 path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_ADD, 334 }, 335 { 336 component: IDPOpenIDConfigurationDetails, 337 path: IAM_PAGES.IDP_OPENID_CONFIGURATIONS_VIEW, 338 }, 339 { 340 component: Trace, 341 path: IAM_PAGES.TOOLS_TRACE, 342 }, 343 { 344 component: HealthInfo, 345 path: IAM_PAGES.TOOLS_DIAGNOSTICS, 346 }, 347 { 348 component: ErrorLogs, 349 path: IAM_PAGES.TOOLS_LOGS, 350 }, 351 { 352 component: LogsSearchMain, 353 path: IAM_PAGES.TOOLS_AUDITLOGS, 354 }, 355 { 356 component: Tools, 357 path: IAM_PAGES.TOOLS, 358 }, 359 { 360 component: ConfigurationOptions, 361 path: IAM_PAGES.SETTINGS, 362 }, 363 { 364 component: AddEventDestination, 365 path: IAM_PAGES.EVENT_DESTINATIONS_ADD_SERVICE, 366 }, 367 { 368 component: EventTypeSelector, 369 path: IAM_PAGES.EVENT_DESTINATIONS_ADD, 370 }, 371 { 372 component: EventDestinations, 373 path: IAM_PAGES.EVENT_DESTINATIONS, 374 }, 375 { 376 component: AddTierConfiguration, 377 path: IAM_PAGES.TIERS_ADD_SERVICE, 378 fsHidden: !distributedSetup, 379 }, 380 { 381 component: TierTypeSelector, 382 path: IAM_PAGES.TIERS_ADD, 383 fsHidden: !distributedSetup, 384 }, 385 { 386 component: ListTiersConfiguration, 387 path: IAM_PAGES.TIERS, 388 }, 389 { 390 component: SiteReplication, 391 path: IAM_PAGES.SITE_REPLICATION, 392 }, 393 { 394 component: SiteReplicationStatus, 395 path: IAM_PAGES.SITE_REPLICATION_STATUS, 396 }, 397 { 398 component: AddReplicationSites, 399 path: IAM_PAGES.SITE_REPLICATION_ADD, 400 }, 401 { 402 component: Account, 403 path: IAM_PAGES.ACCOUNT, 404 forceDisplay: true, 405 // user has implicit access to service-accounts 406 }, 407 { 408 component: AccountCreate, 409 path: IAM_PAGES.ACCOUNT_ADD, 410 forceDisplay: true, // user has implicit access to service-accounts 411 }, 412 { 413 component: License, 414 path: IAM_PAGES.LICENSE, 415 forceDisplay: true, 416 }, 417 { 418 component: KMSRoutes, 419 path: IAM_PAGES.KMS, 420 fsHidden: !kmsIsEnabled, 421 }, 422 ]; 423 424 let routes = consoleAdminRoutes; 425 426 const allowedRoutes = routes.filter((route: any) => 427 obOnly 428 ? route.path.includes("browser") 429 : (route.forceDisplay || 430 (route.customPermissionFnc 431 ? route.customPermissionFnc() 432 : hasPermission( 433 CONSOLE_UI_RESOURCE, 434 IAM_PAGES_PERMISSIONS[route.path], 435 ))) && 436 !route.fsHidden, 437 ); 438 439 const closeSnackBar = () => { 440 setOpenSnackbar(false); 441 dispatch(setSnackBarMessage("")); 442 }; 443 444 useEffect(() => { 445 if (snackBarMessage.message === "") { 446 setOpenSnackbar(false); 447 return; 448 } 449 // Open SnackBar 450 if (snackBarMessage.type !== "error") { 451 setOpenSnackbar(true); 452 } 453 }, [snackBarMessage]); 454 455 let hideMenu = false; 456 if (features?.includes("hide-menu") || pathname.endsWith("/hop") || obOnly) { 457 hideMenu = true; 458 } 459 460 return ( 461 <Fragment> 462 {session && session.status === "ok" ? ( 463 <MainContainer 464 menu={!hideMenu ? <MenuWrapper /> : <Fragment />} 465 mobileModeAuto={false} 466 > 467 <Fragment> 468 {needsRestart && ( 469 <Snackbar 470 onClose={() => {}} 471 open={needsRestart} 472 variant={"warning"} 473 message={ 474 <Box 475 sx={{ 476 display: "flex", 477 gap: 8, 478 justifyContent: "center", 479 alignItems: "center", 480 width: "100%", 481 }} 482 > 483 {isServerLoading ? ( 484 <Fragment> 485 <ProgressBar 486 barHeight={3} 487 transparentBG 488 sx={{ 489 width: "100%", 490 position: "absolute", 491 top: 0, 492 left: 0, 493 }} 494 /> 495 <span>The server is restarting.</span> 496 </Fragment> 497 ) : ( 498 <Fragment> 499 The instance needs to be restarted for configuration 500 changes to take effect.{" "} 501 <Button 502 id={"restart-server"} 503 variant="secondary" 504 onClick={() => { 505 restartServer(); 506 }} 507 label={"Restart"} 508 /> 509 </Fragment> 510 )} 511 </Box> 512 } 513 autoHideDuration={0} 514 /> 515 )} 516 {loadingProgress < 100 && ( 517 <ProgressBar 518 barHeight={3} 519 variant="determinate" 520 value={loadingProgress} 521 sx={{ width: "100%", position: "absolute", top: 0, left: 0 }} 522 /> 523 )} 524 <MainError /> 525 <Snackbar 526 onClose={closeSnackBar} 527 open={openSnackbar} 528 message={snackBarMessage.message} 529 variant={snackBarMessage.type === "error" ? "error" : "default"} 530 autoHideDuration={snackBarMessage.type === "error" ? 10 : 5} 531 condensed 532 /> 533 <Suspense fallback={<LoadingComponent />}> 534 <ObjectManager /> 535 </Suspense> 536 <Routes> 537 {allowedRoutes.map((route: any) => ( 538 <Route 539 key={route.path} 540 path={`${route.path}/*`} 541 element={ 542 <Suspense fallback={<LoadingComponent />}> 543 <route.component {...route.props} /> 544 </Suspense> 545 } 546 /> 547 ))} 548 <Route 549 key={"icons"} 550 path={"icons"} 551 element={ 552 <Suspense fallback={<LoadingComponent />}> 553 <IconsScreen /> 554 </Suspense> 555 } 556 /> 557 <Route 558 key={"components"} 559 path={"components"} 560 element={ 561 <Suspense fallback={<LoadingComponent />}> 562 <ComponentsScreen /> 563 </Suspense> 564 } 565 /> 566 <Route 567 path={"*"} 568 element={ 569 <Fragment> 570 {allowedRoutes.length > 0 ? ( 571 <Navigate to={allowedRoutes[0].path} /> 572 ) : ( 573 <Fragment /> 574 )} 575 </Fragment> 576 } 577 /> 578 </Routes> 579 </Fragment> 580 </MainContainer> 581 ) : null} 582 </Fragment> 583 ); 584 }; 585 586 export default Console;