UNPKG

@kinvolk/headlamp-plugin

Version:

The needed infrastructure for building Headlamp plugins.

154 lines (153 loc) 7.08 kB
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime"; /* * Copyright 2025 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { useQuery } from '@tanstack/react-query'; import React, { Suspense } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { Redirect, Route, Switch, useHistory } from 'react-router-dom'; import { getCluster, getSelectedClusters } from '../../lib/cluster'; import { useCluster, useClustersConf } from '../../lib/k8s'; import { testAuth } from '../../lib/k8s/api/v1/clusterApi'; import { NotFoundRoute } from '../../lib/router'; import { createRouteURL } from '../../lib/router/createRouteURL'; import { getDefaultRoutes } from '../../lib/router/getDefaultRoutes'; import { getRoutePath } from '../../lib/router/getRoutePath'; import { getRouteUseClusterURL } from '../../lib/router/getRouteUseClusterURL'; import { useTypedSelector } from '../../redux/hooks'; import { uiSlice } from '../../redux/uiSlice'; import ErrorBoundary from '../common/ErrorBoundary'; import ErrorComponent from '../common/ErrorPage'; import { useSidebarItem } from '../Sidebar'; export default function RouteSwitcher(props) { // The NotFoundRoute always has to be evaluated in the last place. const routes = useTypedSelector(state => state.routes.routes); const routeFilters = useTypedSelector(state => state.routes.routeFilters); const defaultRoutes = Object.values(getDefaultRoutes()).concat(NotFoundRoute); const clusters = useClustersConf(); const filteredRoutes = Object.values(routes) .concat(defaultRoutes) .filter(route => !(routeFilters.length > 0 && routeFilters.filter(f => f(route)).length !== routeFilters.length) && !route.disabled); return (_jsx(Suspense, { fallback: null, children: _jsx(Switch, { children: filteredRoutes.map((route, index) => route.name === 'OidcAuth' ? (_jsx(Route, { path: route.path, component: () => _jsx(RouteComponent, { route: route }) }, index)) : (_jsx(AuthRoute, { path: getRoutePath(route), sidebar: route.sidebar, requiresAuth: !route.noAuthRequired, requiresCluster: getRouteUseClusterURL(route), exact: !!route.exact, clusters: clusters, requiresToken: props.requiresToken, children: _jsx(RouteComponent, { route: route }, `${route.path}-${getCluster()}`) }, `${route.path}-${getCluster()}`))) }) })); } function RouteErrorBoundary(props) { const { error, route } = props; const { t } = useTranslation(); return (_jsx(ErrorComponent, { title: t('Uh-oh! Something went wrong.'), error: error, message: t('translation|Error loading {{ routeName }}', { routeName: route.name }) })); } function RouteComponent({ route }) { const { t } = useTranslation(); const dispatch = useDispatch(); React.useEffect(() => { dispatch(uiSlice.actions.setHideAppBar(route.hideAppBar)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route.hideAppBar]); React.useEffect(() => { dispatch(uiSlice.actions.setIsFullWidth(route.isFullWidth)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route.isFullWidth]); return (_jsx(PageTitle, { title: t(route.name ? route.name : typeof route.sidebar === 'string' ? route.sidebar : route.sidebar?.item || ''), children: _jsx(ErrorBoundary, { fallback: (props) => (_jsx(RouteErrorBoundary, { error: props.error, route: route })), children: _jsx(route.component, {}) }) })); } function PageTitle({ title, children, }) { const cluster = useCluster(); React.useEffect(() => { if (cluster && title) { document.title = `${cluster} - ${title}`; return; } document.title = cluster || title || ''; }, [cluster, title]); return _jsx(_Fragment, { children: children }); } function AuthRoute(props) { const { children, sidebar, requiresAuth = true, requiresCluster = true, computedMatch = {}, ...other } = props; useSidebarItem(sidebar, computedMatch); const cluster = useCluster(); const query = useQuery({ queryKey: ['auth', cluster], queryFn: () => testAuth(cluster), enabled: !!cluster && requiresAuth, retry: 0, }); const clusters = useClustersConf(); const currentCluster = getCluster(); const clusterConf = currentCluster && clusters ? clusters[currentCluster] : null; const authError = query.error; const isExplicitAuthError = [401, 403].includes(authError?.status); let redirectRoute; if (!currentCluster) { redirectRoute = 'chooser'; } else if (clusterConf?.auth_type === 'oidc') { redirectRoute = 'login'; } else if (query.isError && isExplicitAuthError) { redirectRoute = 'token'; } else { redirectRoute = 'login'; } function getRenderer({ location }) { if (!requiresAuth) { return children; } if (requiresCluster) { if (getSelectedClusters().length > 1) { // In multi-cluster mode, we do not know if one of them requires a token. return children; } } if (query.isSuccess) { return children; } if (query.isError) { return (_jsx(Redirect, { to: { pathname: createRouteURL(redirectRoute), state: { from: location }, } })); } return null; } // If no auth is required for the view, or the token is set up, then // render the assigned component. Otherwise redirect to the login route. return _jsx(Route, { ...other, render: getRenderer }); } const PreviousRouteContext = React.createContext(0); export function PreviousRouteProvider({ children }) { const history = useHistory(); const [locationInfo, setLocationInfo] = React.useState(0); React.useEffect(() => { history.listen((location, action) => { if (action === 'PUSH') { setLocationInfo(levels => levels + 1); } else if (action === 'POP') { setLocationInfo(levels => levels - 1); } }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (_jsx(PreviousRouteContext.Provider, { value: locationInfo, children: children })); } export function useHasPreviousRoute() { const routeLevels = React.useContext(PreviousRouteContext); return routeLevels >= 1; }