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;