diff --git a/app/components/MswBanner.tsx b/app/components/MswBanner.tsx index a22f9bd7a..58cf8eec1 100644 --- a/app/components/MswBanner.tsx +++ b/app/components/MswBanner.tsx @@ -26,7 +26,24 @@ function ExternalLink({ href, children }: { href: string; children: ReactNode }) ) } -export function MswBanner() { +type Props = { + /** + * HACK to avoid the user opening the modal while on the loading skeleton + * -- it immediately closes when the page finishes loading because the + * banner is dropped when the HydrateFallback unmounts and re-rendered in + * RootLayout. A more ideal solution would be to render the banner outside + * the RouterProvider and therefore have it be the same banner in both the + * HydrateFallback and normal page situations, but it's a lot more work to + * get the layout right in that case with respect to things like the loading + * bar. When we switch to framework mode, we can manage all this in the root + * route using the Layout export. In the meantime, this is tolerable and only + * applies to the preview deploys, and only burdens someone who manages to + * click the Learn More button in the half second before the content loads. + */ + disableButton?: boolean +} + +export function MswBanner({ disableButton }: Props) { const [isOpen, setIsOpen] = useState(false) const closeModal = () => setIsOpen(false) return ( @@ -38,6 +55,7 @@ export function MswBanner() { type="button" className="ml-2 flex items-center gap-0.5 text-sans-md hover:text-info" onClick={() => setIsOpen(true)} + disabled={disableButton} > Learn more diff --git a/app/components/PageSkeleton.tsx b/app/components/PageSkeleton.tsx new file mode 100644 index 000000000..3a89672b5 --- /dev/null +++ b/app/components/PageSkeleton.tsx @@ -0,0 +1,59 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ + +import { useLocation } from 'react-router' + +import { PageContainer } from '~/layouts/helpers' +import { classed } from '~/util/classed' + +import { MswBanner } from './MswBanner' + +const Block = classed.div`motion-safe:animate-pulse2 rounded bg-tertiary` + +export function PageSkeleton({ skipPaths }: { skipPaths?: RegExp[] }) { + const { pathname } = useLocation() + + // HACK: we can only hang a HydrateFallback off the root route/layout, so in + // order to avoid rendering this skeleton on pages that don't have this grid + // layout, all we can do is match the path + if (skipPaths?.some((regex) => regex.test(pathname))) return null + + // we need the msw banner here so it doesn't pop in on load + return ( + <> + {process.env.MSW_BANNER ? : null} + +
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+ + + + +
+
+
+ + + ) +} diff --git a/app/routes.tsx b/app/routes.tsx index efb61e145..14cb4625c 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -9,12 +9,13 @@ import type { ReactElement } from 'react' import { createRoutesFromElements, Navigate, + redirect, Route, type LoaderFunctionArgs, - type redirect, } from 'react-router' import { NotFound } from './components/ErrorPage' +import { PageSkeleton } from './components/PageSkeleton.tsx' import { makeCrumb, type Crumb } from './hooks/use-crumbs' import { getInstanceSelector, getVpcSelector } from './hooks/use-params' import { pb } from './util/path-builder' @@ -29,6 +30,7 @@ type RouteModule = { shouldRevalidate?: () => boolean ErrorBoundary?: () => ReactElement handle?: Crumb + hydrateFallbackElement?: ReactElement // trick to get a nice type error when we forget to convert loader to // clientLoader in the module loader?: never @@ -51,7 +53,20 @@ const redirectWithLoader = (to: string) => (mod: RouteModule) => ({ }) export const routes = createRoutesFromElements( - import('./layouts/RootLayout').then(convert)}> + import('./layouts/RootLayout').then(convert)} + // This only works here, not on any lower layouts. In framework mode they + // make clearer that only the root can have a `HydrateFallback` -- that + // restriction appears to be in place implicitly in library mode. This is + // why we need skipPaths: there are layouts that don't have the grid that + // matches skeleton, so we can't show the skeleton on those pages. + // + // Also notable: this only works when explicitly added here as a prop, + // not as an export from the lazy-loaded route module, which makes sense + // because the loading of that route module is itself part of "hydration" + // (confusingly, not what React calls hydration). + hydrateFallbackElement={} + > } /> import('./layouts/LoginLayout.tsx').then(convert)}> - } /> + redirect(pb.projects())} /> import('./layouts/SiloLayout').then(convert)}>