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)}>