Skip to content

Commit 4065ef0

Browse files
committed
fix crumbs of pageless route nodes, very zany snapshot test for the crumbs
1 parent 7e90d2e commit 4065ef0

File tree

6 files changed

+1104
-71
lines changed

6 files changed

+1104
-71
lines changed

app/hooks/use-crumbs.ts

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,35 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMatches } from 'react-router-dom'
9-
import type { Merge } from 'type-fest'
8+
import { useMatches, type UIMatch } from 'react-router-dom'
109

1110
import { invariant } from '~/util/invariant'
1211

13-
type UseMatchesMatch = ReturnType<typeof useMatches>[number]
12+
type Handle = {
13+
crumb: string | CrumbFunc
14+
/**
15+
* Side modal forms have their own routes and their own crumbs that we want
16+
* in the page title, but it's weird for them to affect the nav breadcrumbs
17+
* because the side modal form opens on top of the page with an overlay
18+
* covering the background and not interactive. It feels weird for the
19+
* breadcrumbs to change in the background when you open a form. So we use
20+
* `titleOnly` to mark the form crumbs as not part of the nav breadcrumbs.
21+
*/
22+
titleOnly?: true
23+
/**
24+
* Some route nodes don't have their own pages, but rather just redirect immediately
25+
* to their first child node. In this case, we need the crumb to link directly to
26+
* that child, otherwise we get a weird flash due to linking to the parent node and waiting
27+
* for the redirect.
28+
*/
29+
crumbPath?: string | CrumbFunc
30+
}
1431

15-
type MatchWithCrumb = Merge<
16-
UseMatchesMatch,
17-
{
18-
handle: {
19-
crumb: string | CrumbFunc
20-
/**
21-
* Side modal forms have their own routes and their own crumbs that we want
22-
* in the page title, but it's weird for them to affect the nav breadcrumbs
23-
* because the side modal form opens on top of the page with an overlay
24-
* covering the background and not interactive. It feels weird for the
25-
* breadcrumbs to change in the background when you open a form. So we use
26-
* `titleOnly` to mark the form crumbs as not part of the nav breadcrumbs.
27-
*/
28-
titleOnly?: true
29-
}
30-
}
31-
>
32+
type MatchWithCrumb = UIMatch<unknown, Handle>
3233

3334
export type CrumbFunc = (m: MatchWithCrumb) => string
3435

35-
function hasCrumb(m: UseMatchesMatch): m is MatchWithCrumb {
36+
function hasCrumb(m: UIMatch): m is MatchWithCrumb {
3637
return !!(m.handle && typeof m.handle === 'object' && 'crumb' in m.handle)
3738
}
3839

@@ -51,16 +52,14 @@ function checkCrumbType(m: MatchWithCrumb): MatchWithCrumb {
5152
return m
5253
}
5354

55+
export const matchToCrumb = (m: MatchWithCrumb) => ({
56+
label: typeof m.handle.crumb === 'function' ? m.handle.crumb(m) : m.handle.crumb,
57+
path:
58+
typeof m.handle.crumbPath === 'function'
59+
? m.handle.crumbPath(m)
60+
: m.handle.crumbPath || m.pathname,
61+
titleOnly: !!m.handle.titleOnly,
62+
})
63+
5464
export const useCrumbs = () =>
55-
useMatches()
56-
.filter(hasCrumb)
57-
.map(checkCrumbType)
58-
.map((m) => {
59-
const label =
60-
typeof m.handle.crumb === 'function' ? m.handle.crumb(m) : m.handle.crumb
61-
return {
62-
label,
63-
path: m.pathname,
64-
titleOnly: !!m.handle.titleOnly,
65-
}
66-
})
65+
useMatches().filter(hasCrumb).map(checkCrumbType).map(matchToCrumb)

app/layouts/SystemLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export function SystemLayout() {
6363
const systemLinks = [
6464
{ value: 'Silos', path: pb.silos() },
6565
{ value: 'Utilization', path: pb.systemUtilization() },
66-
{ value: 'Inventory', path: pb.inventory() },
66+
{ value: 'Inventory', path: pb.sledInventory() },
6767
{ value: 'IP Pools', path: pb.ipPools() },
6868
]
6969
// filter out the entry for the path we're currently on
@@ -102,7 +102,7 @@ export function SystemLayout() {
102102
<NavLinkItem to={pb.systemUtilization()}>
103103
<Metrics16Icon /> Utilization
104104
</NavLinkItem>
105-
<NavLinkItem to={pb.inventory()}>
105+
<NavLinkItem to={pb.sledInventory()}>
106106
<Servers16Icon /> Inventory
107107
</NavLinkItem>
108108
<NavLinkItem to={pb.ipPools()}>

app/routes.tsx

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { createRoutesFromElements, Navigate, Route } from 'react-router-dom'
8+
import { createRoutesFromElements, Navigate, Route, type UIMatch } from 'react-router-dom'
99

1010
import { RouterDataErrorBoundary } from './components/ErrorBoundary'
1111
import { NotFound } from './components/ErrorPage'
@@ -118,7 +118,11 @@ export const routes = createRoutesFromElements(
118118
// very important. see `currentUserLoader` and `useCurrentUser`
119119
shouldRevalidate={() => true}
120120
>
121-
<Route path="settings" handle={{ crumb: 'Settings' }} element={<SettingsLayout />}>
121+
<Route
122+
path="settings"
123+
handle={{ crumb: 'Settings', crumbPath: pb.profile() }}
124+
element={<SettingsLayout />}
125+
>
122126
<Route index element={<Navigate to="profile" replace />} />
123127
<Route path="profile" element={<ProfilePage />} handle={{ crumb: 'Profile' }} />
124128
<Route
@@ -136,8 +140,12 @@ export const routes = createRoutesFromElements(
136140
</Route>
137141

138142
<Route path="system" element={<SystemLayout />} loader={SystemLayout.loader}>
139-
<Route element={<SilosPage />} loader={SilosPage.loader}>
140-
<Route path="silos" element={null} handle={{ crumb: 'Silos' }} />
143+
<Route
144+
element={<SilosPage />}
145+
loader={SilosPage.loader}
146+
handle={{ crumb: 'Silos', crumbPath: pb.silos() }}
147+
>
148+
<Route path="silos" element={null} />
141149
<Route path="silos-new" element={<CreateSiloSideModalForm />} />
142150
</Route>
143151
<Route path="silos" handle={{ crumb: 'Silos' }}>
@@ -167,7 +175,7 @@ export const routes = createRoutesFromElements(
167175
path="inventory"
168176
element={<InventoryPage />}
169177
loader={InventoryPage.loader}
170-
handle={{ crumb: 'Inventory' }}
178+
handle={{ crumb: 'Inventory', crumbPath: pb.sledInventory() }}
171179
>
172180
<Route index element={<Navigate to="sleds" replace />} loader={SledsTab.loader} />
173181
<Route
@@ -242,9 +250,6 @@ export const routes = createRoutesFromElements(
242250

243251
<Route index element={<Navigate to={pb.projects()} replace />} />
244252

245-
{/* These are done here instead of nested so we don't flash a layout on 404s */}
246-
<Route path="projects/:project" element={<Navigate to="instances" replace />} />
247-
248253
<Route element={<SiloLayout />}>
249254
<Route
250255
path="images"
@@ -311,7 +316,10 @@ export const routes = createRoutesFromElements(
311316
path=":project"
312317
element={<ProjectLayout overrideContentPane={<SerialConsoleContentPane />} />}
313318
loader={ProjectLayout.loader}
314-
handle={{ crumb: projectCrumb }}
319+
handle={{
320+
crumb: projectCrumb,
321+
crumbPath: ({ params }: UIMatch) => pb.project({ project: params.project! }),
322+
}}
315323
>
316324
<Route path="instances" handle={{ crumb: 'Instances' }}>
317325
<Route path=":instance" handle={{ crumb: instanceCrumb }}>
@@ -329,8 +337,12 @@ export const routes = createRoutesFromElements(
329337
path=":project"
330338
element={<ProjectLayout />}
331339
loader={ProjectLayout.loader}
332-
handle={{ crumb: projectCrumb }}
340+
handle={{
341+
crumb: projectCrumb,
342+
crumbPath: ({ params }: UIMatch) => pb.project({ project: params.project! }),
343+
}}
333344
>
345+
<Route index element={<Navigate to="instances" replace />} />
334346
<Route
335347
path="instances-new"
336348
element={<CreateInstanceForm />}
@@ -339,7 +351,14 @@ export const routes = createRoutesFromElements(
339351
/>
340352
<Route path="instances" handle={{ crumb: 'Instances' }}>
341353
<Route index element={<InstancesPage />} loader={InstancesPage.loader} />
342-
<Route path=":instance" handle={{ crumb: instanceCrumb }}>
354+
<Route
355+
path=":instance"
356+
handle={{
357+
crumb: instanceCrumb,
358+
crumbPath: ({ params }: UIMatch) =>
359+
pb.instance({ project: params.project!, instance: params.instance! }),
360+
}}
361+
>
343362
<Route index element={<Navigate to="storage" replace />} />
344363
<Route element={<InstancePage />} loader={InstancePage.loader}>
345364
<Route
@@ -370,7 +389,14 @@ export const routes = createRoutesFromElements(
370389
</Route>
371390
</Route>
372391

373-
<Route loader={VpcsPage.loader} handle={{ crumb: 'VPCs' }} element={<VpcsPage />}>
392+
<Route
393+
loader={VpcsPage.loader}
394+
handle={{
395+
crumb: 'VPCs',
396+
crumbPath: (m: UIMatch) => pb.vpcs({ project: m.params.project! }),
397+
}}
398+
element={<VpcsPage />}
399+
>
374400
<Route path="vpcs" element={null} />
375401
<Route
376402
path="vpcs-new"
@@ -380,7 +406,14 @@ export const routes = createRoutesFromElements(
380406
</Route>
381407

382408
<Route path="vpcs" handle={{ crumb: 'VPCs' }}>
383-
<Route path=":vpc" handle={{ crumb: vpcCrumb }}>
409+
<Route
410+
path=":vpc"
411+
handle={{
412+
crumb: vpcCrumb,
413+
crumbPath: ({ params }: UIMatch) =>
414+
pb.vpc({ project: params.project!, vpc: params.vpc! }),
415+
}}
416+
>
384417
<Route element={<VpcPage />} loader={VpcPage.loader}>
385418
<Route
386419
index
@@ -422,7 +455,7 @@ export const routes = createRoutesFromElements(
422455
loader={VpcSubnetsTab.loader}
423456
handle={{ crumb: 'Subnets' }}
424457
>
425-
<Route path="subnets" />
458+
<Route path="subnets" element={null} />
426459
<Route
427460
path="subnets-new"
428461
element={<CreateSubnetForm />}
@@ -445,7 +478,7 @@ export const routes = createRoutesFromElements(
445478
path=":router/edit"
446479
element={<EditRouterSideModalForm />}
447480
loader={EditRouterSideModalForm.loader}
448-
handle={{ crumb: 'Edit Router' }}
481+
handle={{ crumb: 'Edit Router', titleOnly: true }}
449482
/>
450483
</Route>
451484
<Route
@@ -488,7 +521,10 @@ export const routes = createRoutesFromElements(
488521
<Route
489522
element={<FloatingIpsPage />}
490523
loader={FloatingIpsPage.loader}
491-
handle={{ crumb: 'Floating IPs' }}
524+
handle={{
525+
crumb: 'Floating IPs',
526+
crumbPath: (m: UIMatch) => pb.floatingIps({ project: m.params.project! }),
527+
}}
492528
>
493529
<Route path="floating-ips" element={null} />
494530
<Route
@@ -506,7 +542,10 @@ export const routes = createRoutesFromElements(
506542

507543
<Route
508544
element={<DisksPage />}
509-
handle={{ crumb: 'Disks' }}
545+
handle={{
546+
crumb: 'Disks',
547+
crumbPath: (m: UIMatch) => pb.disks({ project: m.params.project! }),
548+
}}
510549
loader={DisksPage.loader}
511550
>
512551
<Route path="disks" element={null} />
@@ -523,7 +562,10 @@ export const routes = createRoutesFromElements(
523562

524563
<Route
525564
element={<SnapshotsPage />}
526-
handle={{ crumb: 'Snapshots' }}
565+
handle={{
566+
crumb: 'Snapshots',
567+
crumbPath: (m: UIMatch) => pb.snapshots({ project: m.params.project! }),
568+
}}
527569
loader={SnapshotsPage.loader}
528570
>
529571
<Route path="snapshots" element={null} />
@@ -542,7 +584,10 @@ export const routes = createRoutesFromElements(
542584

543585
<Route
544586
element={<ImagesPage />}
545-
handle={{ crumb: 'Images' }}
587+
handle={{
588+
crumb: 'Images',
589+
crumbPath: (m: UIMatch) => pb.projectImages({ project: m.params.project! }),
590+
}}
546591
loader={ImagesPage.loader}
547592
>
548593
<Route path="images" element={null} />

0 commit comments

Comments
 (0)