import React from 'react'; import {connect} from 'react-redux'; import type {RedirectProps} from 'react-router-dom'; import {Redirect, Route, Switch, useLocation} from 'react-router-dom'; import {Unauthenticated} from '../../components/Errors/401'; import {AccessDenied} from '../../components/Errors/403'; import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; import {useSlots} from '../../components/slots'; import type {SlotMap} from '../../components/slots/SlotMap'; import type {SlotComponent} from '../../components/slots/types'; import routes, {CLUSTER, getClusterPath} from '../../routes'; import type {RootState} from '../../store'; import { useAllCapabilitiesLoaded, useCapabilitiesQuery, useClusterWithoutAuthInUI, useMetaCapabilitiesLoaded, useMetaCapabilitiesQuery, } from '../../store/reducers/capabilities/hooks'; import {nodesListApi} from '../../store/reducers/nodesList'; import {cn} from '../../utils/cn'; import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery'; import {useMetaAuthUnavailable} from '../../utils/hooks/useMetaAuth'; import {lazyComponent} from '../../utils/lazyComponent'; import {isForbiddenError, isRedirectToAuth, isUnauthenticatedError} from '../../utils/response'; import Authentication from '../Authentication/Authentication'; import {GetUser} from '../GetUserWrapper/GetUserWrapper'; import {Header} from '../Header/Header'; import { ClusterSlot, HomePageSlot, NodeSlot, PDiskPageSlot, RedirectSlot, RoutesSlot, StorageGroupSlot, TabletSlot, TenantSlot, VDiskPageSlot, } from './appSlots'; import {shouldRedirectClusterRouteToRoot} from './clusterRouteGuard'; import {getLegacyClusterTenantsRedirect, getLegacyTenantRedirect} from './legacyRedirects'; import './App.scss'; const b = cn('app'); type RouteSlot = { path: string; slot: SlotComponent; component: React.ComponentType; wrapper?: React.ComponentType; exact?: boolean; }; const routesSlots: RouteSlot[] = [ { path: routes.cluster, slot: ClusterSlot, component: lazyComponent(() => import('../Cluster/Cluster'), 'Cluster'), wrapper: DataWrapper, }, { path: routes.tenant, slot: TenantSlot, component: lazyComponent(() => import('../Tenant/Tenant'), 'Tenant'), wrapper: DataWrapper, }, { path: routes.node, slot: NodeSlot, component: lazyComponent(() => import('../Node/Node'), 'Node'), wrapper: DataWrapper, }, { path: routes.pDisk, slot: PDiskPageSlot, component: lazyComponent(() => import('../PDiskPage/PDiskPage'), 'PDiskPage'), wrapper: DataWrapper, }, { path: routes.vDisk, slot: VDiskPageSlot, component: lazyComponent(() => import('../VDiskPage/VDiskPage'), 'VDiskPage'), wrapper: DataWrapper, }, { path: routes.storageGroup, slot: StorageGroupSlot, component: lazyComponent( () => import('../StorageGroupPage/StorageGroupPage'), 'StorageGroupPage', ), wrapper: DataWrapper, }, { path: routes.tablet, slot: TabletSlot, component: lazyComponent(() => import('../Tablet'), 'Tablet'), wrapper: DataWrapper, }, ]; const multiClusterRoutes: RouteSlot[] = [ { path: routes.homePage, exact: true, component: lazyComponent(() => import('../HomePage/HomePage'), 'HomePage'), slot: HomePageSlot, wrapper: HomePageDataWrapper, }, ]; function renderRouteSlot(slots: SlotMap, route: RouteSlot) { return ( { const slot = slots.get(route.slot); let content; if (!slot) { const Component = route.component; content = ; } else if (typeof slot.rendered === 'function') { content = slot.rendered({component: route.component, ...props}); } else { content = slot.rendered; } const Wrapper = route.wrapper ?? React.Fragment; return (
{content}
); }} /> ); } interface ContentProps { singleClusterMode: boolean; children: React.ReactNode; } export function Content(props: ContentProps) { const {singleClusterMode} = props; const slots = useSlots(props); const currentLocation = useLocation(); const shouldRedirectToRoot = shouldRedirectClusterRouteToRoot({ singleClusterMode, pathname: currentLocation.pathname, search: currentLocation.search, }); const additionalRoutes = slots.get(RoutesSlot); const redirect = slots.get(RedirectSlot); const redirectProps: RedirectProps = redirect?.props ?? (singleClusterMode ? {to: getClusterPath()} : {to: routes.homePage}); return ( {additionalRoutes?.rendered} {shouldRedirectToRoot ? : null}
{/* Backwards compatibility: redirect legacy `/cluster/tenants` URL to `/cluster/databases`, preserving any query params and hash from old bookmarks. Must appear before the cluster route so it wins the Switch match (otherwise the cluster page's internal unknown-tab fallback would redirect via `getClusterPath()` and drop search/hash). */} ( )} /> {/* Backwards compatibility: redirect legacy `/tenant` URL to `/database`. */} } /> {singleClusterMode ? null : multiClusterRoutes.map((route) => { return renderRouteSlot(slots, route); })} {/* Single cluster routes */} {routesSlots.map((route) => { return renderRouteSlot(slots, route); })} } /> ); } function DataWrapper({children}: {children: React.ReactNode}) { return ( // capabilities is going to work without authentication, but not all running systems are supporting this yet {/* this GetCapabilities will be removed */} {children} ); } function HomePageDataWrapper({children}: {children: React.ReactNode}) { // Home page content is wrapped with GetMetaUser inside component return {children}; } function GetNodesList() { const database = useDatabaseFromQuery(); nodesListApi.useGetNodesListQuery({database}, undefined); return null; } function GetCapabilities({children}: {children: React.ReactNode}) { const {error} = useCapabilitiesQuery(); useMetaCapabilitiesQuery(); const capabilitiesLoaded = useAllCapabilitiesLoaded(); //do nothing, authentication is in progress upon redirect if (isRedirectToAuth(error)) { return null; } if (isUnauthenticatedError(error)) { return ; } if (isForbiddenError(error)) { return ; } return ( {children} ); } // Only for Clusters page, there is no need to request cluster capabilities there (GetCapabilities) // This wrapper is not used in GetCapabilities so the page does not wait for 2 consecutive capabilities requests function GetMetaCapabilities({children}: {children: React.ReactNode}) { useMetaCapabilitiesQuery(); // It is always true if there is no meta, since request finishes with an error const metaCapabilitiesLoaded = useMetaCapabilitiesLoaded(); return ( {children} ); } interface ContentWrapperProps { singleClusterMode: boolean; isAuthenticated: boolean; children: React.ReactNode; } function ContentWrapper(props: ContentWrapperProps) { const {singleClusterMode, isAuthenticated} = props; const authUnavailable = useClusterWithoutAuthInUI(); const metaAuthUnavailable = useMetaAuthUnavailable(); const renderNotAuthenticated = () => { if (authUnavailable || metaAuthUnavailable) { return ; } return ( ); }; return ( {!authUnavailable && !metaAuthUnavailable && ( )}
{isAuthenticated ? props.children : renderNotAuthenticated()}
); } function mapStateToProps(state: RootState) { return { isAuthenticated: state.authentication.isAuthenticated, singleClusterMode: state.singleClusterMode, }; } export default connect(mapStateToProps)(ContentWrapper);