Skip to content

Commit 74005f8

Browse files
authored
chore: Start converting to RRv7 framework mode (#2702)
* get some route modules into framework shape * move all route props inside route modules * serial console
1 parent e8371db commit 74005f8

22 files changed

+315
-218
lines changed

.eslintrc.cjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ module.exports = {
102102
ignorePatterns: ['dist/', 'node_modules/', 'tools/deno/'],
103103
overrides: [
104104
{
105-
// default export is needed in config files
106-
files: ['*.config.ts'],
105+
// default exports are needed in the route modules and the config files,
106+
// but we want to avoid them anywhere else
107+
files: ['app/pages/**/*', 'app/layouts/**/*', '*.config.ts'],
107108
rules: { 'import/no-default-export': 'off' },
108109
},
109110
{

.oxlintrc.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
{
22
"$schema": "./node_modules/oxlint/configuration_schema.json",
3+
"plugins": [
4+
"import",
5+
// defaults
6+
"react",
7+
"unicorn",
8+
"typescript",
9+
"oxc"
10+
],
311
"rules": {
412
// only worry about console.log
513
"no-console": ["error", { "allow": ["warn", "error", "info", "table"] }],
@@ -16,7 +24,18 @@
1624
"react/jsx-boolean-value": "error",
1725

1826
"react-hooks/exhaustive-deps": "error",
19-
"react-hooks/rules-of-hooks": "error"
27+
"react-hooks/rules-of-hooks": "error",
28+
"import/no-default-export": "error"
2029
},
30+
"overrides": [
31+
{
32+
// default exports are needed in the route modules and the config files,
33+
// but we want to avoid them anywhere else
34+
"files": ["app/pages/**/*", "app/layouts/**/*", "*.config.ts", "*.config.mjs"],
35+
"rules": {
36+
"import/no-default-export": "off"
37+
}
38+
}
39+
],
2140
"ignorePatterns": ["dist/", "node_modules/"]
2241
}

app/components/Terminal.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,7 @@ interface TerminalProps {
6161
ws: WebSocket
6262
}
6363

64-
// default export is most convenient for dynamic import
65-
// eslint-disable-next-line import/no-default-export
66-
export default function Terminal({ ws }: TerminalProps) {
64+
export function Terminal({ ws }: TerminalProps) {
6765
const [term, setTerm] = useState<XTerm | null>(null)
6866
const terminalRef = useRef<HTMLDivElement>(null)
6967

app/components/TopBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '@oxide/design-system/icons/react'
1919

2020
import { useCrumbs } from '~/hooks/use-crumbs'
21-
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
21+
import { useCurrentUser } from '~/hooks/use-current-user'
2222
import { buttonStyle } from '~/ui/lib/Button'
2323
import * as DropdownMenu from '~/ui/lib/DropdownMenu'
2424
import { Identicon } from '~/ui/lib/Identicon'

app/hooks/use-crumbs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useMatches, type Params, type UIMatch } from 'react-router'
99

1010
import { invariant } from '~/util/invariant'
1111

12-
type Crumb = {
12+
export type Crumb = {
1313
crumb: MakeStr
1414
/**
1515
* Side modal forms have their own routes and their own crumbs that we want

app/hooks/use-current-user.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
import { useApiQueryErrorsAllowed, usePrefetchedApiQuery } from '~/api/client'
10+
import { invariant } from '~/util/invariant'
11+
12+
/**
13+
* Access all the data fetched by the loader. Because of the `shouldRevalidate`
14+
* trick, that loader runs on every authenticated page, which means callers do
15+
* not have to worry about hitting these endpoints themselves in their own
16+
* loaders.
17+
*/
18+
export function useCurrentUser() {
19+
const { data: me } = usePrefetchedApiQuery('currentUserView', {})
20+
const { data: myGroups } = usePrefetchedApiQuery('currentUserGroups', {})
21+
22+
// User can only get to system routes if they have viewer perms (at least) on
23+
// the fleet. The natural place to find out whether they have such perms is
24+
// the fleet (system) policy, but if the user doesn't have fleet read, we'll
25+
// get a 403 from that endpoint. So we simply check whether that endpoint 200s
26+
// or not to determine whether the user is a fleet viewer.
27+
const { data: systemPolicy } = useApiQueryErrorsAllowed('systemPolicyView', {})
28+
// don't use usePrefetchedApiQuery because it's not worth making an errors
29+
// allowed version of that
30+
invariant(systemPolicy, 'System policy must be prefetched')
31+
const isFleetViewer = systemPolicy.type === 'success'
32+
33+
return { me, myGroups, isFleetViewer }
34+
}

app/hooks/use-quick-actions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useEffect, useMemo } from 'react'
99
import { useLocation, useNavigate } from 'react-router'
1010
import { create } from 'zustand'
1111

12-
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
12+
import { useCurrentUser } from '~/hooks/use-current-user'
1313
import { ActionMenu, type QuickActionItem } from '~/ui/lib/ActionMenu'
1414
import { invariant } from '~/util/invariant'
1515
import { pb } from '~/util/path-builder'

app/layouts/AuthenticatedLayout.tsx

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,23 @@
77
*/
88
import { Outlet } from 'react-router'
99

10-
import { apiQueryClient, useApiQueryErrorsAllowed, usePrefetchedApiQuery } from '@oxide/api'
10+
import { apiQueryClient } from '@oxide/api'
1111

12+
import { RouterDataErrorBoundary } from '~/components/ErrorBoundary'
1213
import { QuickActions } from '~/hooks/use-quick-actions'
13-
import { invariant } from '~/util/invariant'
14+
15+
/** very important. see `currentUserLoader` and `useCurrentUser` */
16+
export const shouldRevalidate = () => true
17+
18+
export function ErrorBoundary() {
19+
return <RouterDataErrorBoundary />
20+
}
1421

1522
/**
1623
* We use `shouldRevalidate={() => true}` to force this to re-run on every nav,
1724
* but the longer-than-default `staleTime` avoids fetching too much.
1825
*/
19-
AuthenticatedLayout.loader = async () => {
26+
export async function clientLoader() {
2027
const staleTime = 60000
2128
await Promise.all([
2229
apiQueryClient.prefetchQuery('currentUserView', {}, { staleTime }),
@@ -38,35 +45,11 @@ AuthenticatedLayout.loader = async () => {
3845
}
3946

4047
/** Wraps all authenticated routes. */
41-
export function AuthenticatedLayout() {
48+
export default function AuthenticatedLayout() {
4249
return (
4350
<>
4451
<QuickActions />
4552
<Outlet />
4653
</>
4754
)
4855
}
49-
50-
/**
51-
* Access all the data fetched by the loader. Because of the `shouldRevalidate`
52-
* trick, that loader runs on every authenticated page, which means callers do
53-
* not have to worry about hitting these endpoints themselves in their own
54-
* loaders.
55-
*/
56-
export function useCurrentUser() {
57-
const { data: me } = usePrefetchedApiQuery('currentUserView', {})
58-
const { data: myGroups } = usePrefetchedApiQuery('currentUserGroups', {})
59-
60-
// User can only get to system routes if they have viewer perms (at least) on
61-
// the fleet. The natural place to find out whether they have such perms is
62-
// the fleet (system) policy, but if the user doesn't have fleet read, we'll
63-
// get a 403 from that endpoint. So we simply check whether that endpoint 200s
64-
// or not to determine whether the user is a fleet viewer.
65-
const { data: systemPolicy } = useApiQueryErrorsAllowed('systemPolicyView', {})
66-
// don't use usePrefetchedApiQuery because it's not worth making an errors
67-
// allowed version of that
68-
invariant(systemPolicy, 'System policy must be prefetched')
69-
const isFleetViewer = systemPolicy.type === 'success'
70-
71-
return { me, myGroups, isFleetViewer }
72-
}

app/layouts/ProjectLayout.tsx

Lines changed: 8 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -5,113 +5,17 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { useMemo, type ReactElement } from 'react'
9-
import { useLocation, useNavigate, type LoaderFunctionArgs } from 'react-router'
108

11-
import { apiq, queryClient, usePrefetchedQuery } from '@oxide/api'
129
import {
13-
Access16Icon,
14-
Folder16Icon,
15-
Images16Icon,
16-
Instances16Icon,
17-
IpGlobal16Icon,
18-
Networking16Icon,
19-
Snapshots16Icon,
20-
Storage16Icon,
21-
} from '@oxide/design-system/icons/react'
10+
ProjectLayoutBase,
11+
projectLayoutHandle,
12+
projectLayoutLoader,
13+
} from './ProjectLayoutBase.tsx'
2214

23-
import { TopBar } from '~/components/TopBar'
24-
import { getProjectSelector, useProjectSelector } from '~/hooks/use-params'
25-
import { useQuickActions } from '~/hooks/use-quick-actions'
26-
import { Divider } from '~/ui/lib/Divider'
27-
import { pb } from '~/util/path-builder'
28-
import type * as PP from '~/util/path-params'
15+
export const clientLoader = projectLayoutLoader
2916

30-
import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar'
31-
import { ContentPane, PageContainer } from './helpers'
17+
export const handle = projectLayoutHandle
3218

33-
type ProjectLayoutProps = {
34-
/** Sometimes we need a different layout for the content pane. Like
35-
* `<ContentPane />`, the element passed here should contain an `<Outlet />`.
36-
*/
37-
overrideContentPane?: ReactElement
38-
}
39-
40-
const projectView = ({ project }: PP.Project) => apiq('projectView', { path: { project } })
41-
42-
ProjectLayout.loader = async ({ params }: LoaderFunctionArgs) => {
43-
const { project } = getProjectSelector(params)
44-
await queryClient.prefetchQuery(projectView({ project }))
45-
return null
46-
}
47-
48-
export function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) {
49-
const navigate = useNavigate()
50-
// project will always be there, instance may not
51-
const projectSelector = useProjectSelector()
52-
const { data: project } = usePrefetchedQuery(projectView(projectSelector))
53-
54-
const { pathname } = useLocation()
55-
useQuickActions(
56-
useMemo(
57-
() =>
58-
[
59-
{ value: 'Instances', path: pb.instances(projectSelector) },
60-
{ value: 'Disks', path: pb.disks(projectSelector) },
61-
{ value: 'Snapshots', path: pb.snapshots(projectSelector) },
62-
{ value: 'Images', path: pb.projectImages(projectSelector) },
63-
{ value: 'VPCs', path: pb.vpcs(projectSelector) },
64-
{ value: 'Floating IPs', path: pb.floatingIps(projectSelector) },
65-
{ value: 'Access', path: pb.projectAccess(projectSelector) },
66-
]
67-
// filter out the entry for the path we're currently on
68-
.filter((i) => i.path !== pathname)
69-
.map((i) => ({
70-
navGroup: `Project '${project.name}'`,
71-
value: i.value,
72-
onSelect: () => navigate(i.path),
73-
})),
74-
[pathname, navigate, project.name, projectSelector]
75-
)
76-
)
77-
78-
return (
79-
<PageContainer>
80-
<TopBar systemOrSilo="silo" />
81-
<Sidebar>
82-
<Sidebar.Nav>
83-
<NavLinkItem to={pb.projects()} end>
84-
<Folder16Icon />
85-
Projects
86-
</NavLinkItem>
87-
<DocsLinkItem />
88-
</Sidebar.Nav>
89-
<Divider />
90-
<Sidebar.Nav heading={project.name}>
91-
<NavLinkItem to={pb.instances(projectSelector)}>
92-
<Instances16Icon /> Instances
93-
</NavLinkItem>
94-
<NavLinkItem to={pb.disks(projectSelector)}>
95-
<Storage16Icon /> Disks
96-
</NavLinkItem>
97-
<NavLinkItem to={pb.snapshots(projectSelector)}>
98-
<Snapshots16Icon /> Snapshots
99-
</NavLinkItem>
100-
<NavLinkItem to={pb.projectImages(projectSelector)}>
101-
<Images16Icon title="images" /> Images
102-
</NavLinkItem>
103-
<NavLinkItem to={pb.vpcs(projectSelector)}>
104-
<Networking16Icon /> VPCs
105-
</NavLinkItem>
106-
<NavLinkItem to={pb.floatingIps(projectSelector)}>
107-
<IpGlobal16Icon /> Floating IPs
108-
</NavLinkItem>
109-
<NavLinkItem to={pb.projectAccess(projectSelector)}>
110-
<Access16Icon title="Access" /> Access
111-
</NavLinkItem>
112-
</Sidebar.Nav>
113-
</Sidebar>
114-
{overrideContentPane || <ContentPane />}
115-
</PageContainer>
116-
)
19+
export default function ProjectLayout() {
20+
return <ProjectLayoutBase />
11721
}

0 commit comments

Comments
 (0)