Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion app/components/MswBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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 <NextArrow12Icon />
</button>
Expand Down
59 changes: 59 additions & 0 deletions app/components/PageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -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 ? <MswBanner disableButton /> : null}
<PageContainer>
<div className="flex items-center gap-2 border-b border-r p-3 border-secondary">
<Block className="h-8 w-8" />
<Block className="h-4 w-24" />
</div>
<div className="flex items-center justify-between gap-2 border-b p-3 border-secondary">
<Block className="h-4 w-24" />
<div className="flex items-center gap-2">
<Block className="h-6 w-16" />
<Block className="h-6 w-32" />
</div>
</div>
<div className="border-r p-4 border-secondary">
<Block className="mb-10 h-4 w-full" />
<div className="mb-6 space-y-2">
<Block className="h-4 w-32" />
<Block className="h-4 w-24" />
</div>
<div className="space-y-2">
<Block className="h-4 w-14" />
<Block className="h-4 w-32" />
<Block className="h-4 w-24" />
<Block className="h-4 w-14" />
</div>
</div>
<div className="" />
</PageContainer>
</>
)
}
21 changes: 18 additions & 3 deletions app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -51,7 +53,20 @@ const redirectWithLoader = (to: string) => (mod: RouteModule) => ({
})

export const routes = createRoutesFromElements(
<Route lazy={() => import('./layouts/RootLayout').then(convert)}>
<Route
lazy={() => 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={<PageSkeleton skipPaths={[/^\/login\//, /^\/device\//]} />}
>
<Route path="*" element={<NotFound />} />
<Route lazy={() => import('./layouts/LoginLayout.tsx').then(convert)}>
<Route
Expand Down Expand Up @@ -192,7 +207,7 @@ export const routes = createRoutesFromElements(
</Route>
</Route>

<Route index element={<Navigate to={pb.projects()} replace />} />
<Route index loader={() => redirect(pb.projects())} />
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this, when loading the root (an extremely common thing to do) you'd see the skeleton flash and then it would go blank wile the loaders for the next route load. I think doing it this way is fine as long we never link to /, which we don't. We'll have to see if there's any weird behavior, though. I think another workaround would be to leave the element thing in place and give this route a loader that just calls the loader from the projects page, that way the loading time is spent with the fallback up.


<Route lazy={() => import('./layouts/SiloLayout').then(convert)}>
<Route
Expand Down
10 changes: 10 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ export default {
animation: {
'spin-slow': 'spin 5s linear infinite',
pulse: 'pulse 2s cubic-bezier(.4,0,.6,1) infinite',
// used by PageSkeleton
pulse2: 'pulse2 1.3s cubic-bezier(.4,0,.6,1) infinite',
},
keyframes: {
// different from pulse in that we go up a little before we go back down.
// pulse starts at opacity 1
pulse2: {
'0%, 100%': { opacity: '0.75' },
'50%': { opacity: '1' },
},
},
},
plugins: [
Expand Down
Loading