Skip to content

Commit 6990481

Browse files
committed
Merge branch 'main' into buttonmageddon
# Conflicts: # app/pages/project/instances/instance/SerialConsolePage.tsx
2 parents d258c9d + dab1496 commit 6990481

File tree

16 files changed

+217
-85
lines changed

16 files changed

+217
-85
lines changed

app/components/RouteTabs.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import cn from 'classnames'
2+
import type { ReactNode } from 'react'
3+
import { Link, Outlet } from 'react-router-dom'
4+
5+
import { useIsActivePath } from 'app/hooks/use-is-active-path'
6+
7+
const selectTab = (e: React.KeyboardEvent<HTMLDivElement>) => {
8+
const target = e.target as HTMLDivElement
9+
if (e.key === 'ArrowLeft') {
10+
e.stopPropagation()
11+
e.preventDefault()
12+
13+
const sibling = (target.previousSibling ??
14+
target.parentElement!.lastChild!) as HTMLDivElement
15+
16+
sibling.focus()
17+
sibling.click()
18+
} else if (e.key === 'ArrowRight') {
19+
e.stopPropagation()
20+
e.preventDefault()
21+
22+
const sibling = (target.nextSibling ??
23+
target.parentElement!.firstChild!) as HTMLDivElement
24+
25+
sibling.focus()
26+
sibling.click()
27+
}
28+
}
29+
30+
export interface RouteTabsProps {
31+
children: ReactNode
32+
fullWidth?: boolean
33+
}
34+
export function RouteTabs({ children, fullWidth }: RouteTabsProps) {
35+
return (
36+
<div className={cn('ox-tabs', { 'full-width': fullWidth })}>
37+
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
38+
<div role="tablist" className="ox-tabs-list flex" onKeyDown={selectTab}>
39+
{children}
40+
</div>
41+
{/* TODO: Add aria-describedby for active tab */}
42+
<div className="ox-tabs-panel" role="tabpanel" tabIndex={0}>
43+
<Outlet />
44+
</div>
45+
</div>
46+
)
47+
}
48+
49+
export interface TabProps {
50+
to: string
51+
children: ReactNode
52+
}
53+
export const Tab = ({ to, children }: TabProps) => {
54+
const isActive = useIsActivePath({ to })
55+
return (
56+
<Link
57+
role="tab"
58+
to={to}
59+
className={cn('ox-tab', { 'is-selected': isActive })}
60+
tabIndex={isActive ? 0 : -1}
61+
aria-selected={isActive}
62+
>
63+
<div>{children}</div>
64+
</Link>
65+
)
66+
}

app/forms/instance-create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function CreateInstanceForm() {
9393
title: 'Success!',
9494
content: 'Your instance has been created.',
9595
})
96-
navigate(pb.instance({ ...pageParams, instanceName: instance.name }))
96+
navigate(pb.instancePage({ ...pageParams, instanceName: instance.name }))
9797
},
9898
})
9999

app/hooks/use-is-active-path.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useLocation, useResolvedPath } from 'react-router-dom'
2+
3+
interface ActivePathOptions {
4+
to: string
5+
end?: boolean
6+
}
7+
/**
8+
* Returns true if the provided path is currently active.
9+
*
10+
* This implementation is based on logic from React Router's NavLink component.
11+
*
12+
* @see https://github.com/remix-run/react-router/blob/67f16e73603765158c63a27afb70d3a4b3e823d3/packages/react-router-dom/index.tsx#L448-L467
13+
*
14+
* @param to The path to check
15+
* @param options.end Ensure this path isn't matched as "active" when its descendant paths are matched.
16+
*/
17+
export const useIsActivePath = ({ to, end }: ActivePathOptions) => {
18+
const path = useResolvedPath(to)
19+
const location = useLocation()
20+
21+
const toPathname = path.pathname
22+
const locationPathname = location.pathname
23+
24+
return (
25+
locationPathname === toPathname ||
26+
(!end &&
27+
locationPathname.startsWith(toPathname) &&
28+
locationPathname.charAt(toPathname.length) === '/')
29+
)
30+
}

app/pages/__tests__/click-everything.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
1717
'role=heading[name*=db1]',
1818
'role=tab[name="Storage"]',
1919
'role=tab[name="Metrics"]',
20-
'role=tab[name="Networking"]',
20+
'role=tab[name="Network Interfaces"]',
2121
'role=table[name="Boot disk"] >> role=cell[name="disk-1"]',
2222
'role=table[name="Attached disks"] >> role=cell[name="disk-2"]',
2323
// buttons disabled while instance is running

app/pages/__tests__/instance/networking.e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test('Instance networking tab', async ({ page }) => {
88
await page.goto('/orgs/maze-war/projects/mock-project/instances/db1')
99

1010
// Instance networking tab
11-
await page.click('role=tab[name="Networking"]')
11+
await page.click('role=tab[name="Network Interfaces"]')
1212

1313
const table = page.locator('table')
1414
await expectRowVisible(table, { name: 'my-nic', primary: 'primary' })

app/pages/project/disks/DisksPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function AttachedInstance({
4343
return instance ? (
4444
<Link
4545
className="text-sans-semi-md text-accent hover:underline"
46-
to={pb.instance({ orgName, projectName, instanceName: instance.name })}
46+
to={pb.instancePage({ orgName, projectName, instanceName: instance.name })}
4747
>
4848
{instance.name}
4949
</Link>

app/pages/project/instances/InstancesPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ export function InstancesPage() {
6767
{ value: 'New instance', onSelect: () => navigate(pb.instanceNew(projectParams)) },
6868
...(instances?.items || []).map((i) => ({
6969
value: i.name,
70-
onSelect: () => navigate(pb.instance({ ...projectParams, instanceName: i.name })),
70+
onSelect: () =>
71+
navigate(pb.instancePage({ ...projectParams, instanceName: i.name })),
7172
navGroup: 'Go to instance',
7273
})),
7374
],
@@ -103,7 +104,7 @@ export function InstancesPage() {
103104
<Column
104105
accessor="name"
105106
cell={linkCell((instanceName) =>
106-
pb.instance({ orgName, projectName, instanceName })
107+
pb.instancePage({ orgName, projectName, instanceName })
107108
)}
108109
/>
109110
<Column

app/pages/project/instances/instance/InstancePage.tsx

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,19 @@
11
import filesize from 'filesize'
2-
import React, { Suspense, memo, useMemo } from 'react'
2+
import { useMemo } from 'react'
33
import type { LoaderFunctionArgs } from 'react-router-dom'
44
import { useNavigate } from 'react-router-dom'
55

66
import { apiQueryClient, useApiQuery, useApiQueryClient } from '@oxide/api'
7-
import { Instances24Icon, PageHeader, PageTitle, PropertiesTable, Tab } from '@oxide/ui'
7+
import { Instances24Icon, PageHeader, PageTitle, PropertiesTable } from '@oxide/ui'
88
import { pick } from '@oxide/util'
99

1010
import { MoreActionsMenu } from 'app/components/MoreActionsMenu'
11+
import { RouteTabs, Tab } from 'app/components/RouteTabs'
1112
import { InstanceStatusBadge } from 'app/components/StatusBadge'
12-
import { Tabs } from 'app/components/Tabs'
1313
import { requireInstanceParams, useQuickActions, useRequiredParams } from 'app/hooks'
1414
import { pb } from 'app/util/path-builder'
1515

1616
import { useMakeInstanceActions } from '../actions'
17-
import { NetworkingTab } from './tabs/NetworkingTab'
18-
import { SerialConsoleTab } from './tabs/SerialConsoleTab'
19-
import { StorageTab } from './tabs/StorageTab'
20-
21-
const MetricsTab = React.lazy(() => import('./tabs/MetricsTab'))
22-
23-
const InstanceTabs = memo(() => (
24-
<Tabs id="tabs-instance" fullWidth>
25-
<Tab>Storage</Tab>
26-
<Tab.Panel>
27-
<StorageTab />
28-
</Tab.Panel>
29-
<Tab>Metrics</Tab>
30-
<Tab.Panel>
31-
<Suspense fallback={null}>
32-
<MetricsTab />
33-
</Suspense>
34-
</Tab.Panel>
35-
<Tab>Networking</Tab>
36-
<Tab.Panel>
37-
<NetworkingTab />
38-
</Tab.Panel>
39-
<Tab>Serial Console</Tab>
40-
<Tab.Panel>
41-
<SerialConsoleTab />
42-
</Tab.Panel>
43-
</Tabs>
44-
))
4517

4618
InstancePage.loader = async ({ params }: LoaderFunctionArgs) => {
4719
await apiQueryClient.prefetchQuery('instanceView', {
@@ -111,7 +83,12 @@ export function InstancePage() {
11183
</PropertiesTable.Row>
11284
</PropertiesTable>
11385
</PropertiesTable.Group>
114-
<InstanceTabs />
86+
<RouteTabs fullWidth>
87+
<Tab to={pb.instanceStorage(instanceParams)}>Storage</Tab>
88+
<Tab to={pb.instanceMetrics(instanceParams)}>Metrics</Tab>
89+
<Tab to={pb.nics(instanceParams)}>Network Interfaces</Tab>
90+
<Tab to={pb.serialConsole(instanceParams)}>Serial Console</Tab>
91+
</RouteTabs>
11592
</>
11693
)
11794
}

app/pages/project/instances/instance/tabs/MetricsTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ function DiskMetrics({ disks }: { disks: Disk[] }) {
8080

8181
return (
8282
<>
83-
<div className="mt-8 mb-4 flex justify-between">
83+
<div className="mb-4 flex justify-between">
8484
<Listbox
8585
className="w-48"
8686
aria-label="Choose disk"

app/pages/project/instances/instance/tabs/StorageTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export function StorageTab() {
110110
if (!data) return null
111111

112112
return (
113-
<div className="mt-8">
113+
<>
114114
<h2 id={bootLabelId} className="mb-4 text-mono-sm text-secondary">
115115
Boot disk
116116
</h2>
@@ -173,6 +173,6 @@ export function StorageTab() {
173173
{showDiskAttach && (
174174
<AttachDiskSideModalForm onDismiss={() => setShowDiskAttach(false)} />
175175
)}
176-
</div>
176+
</>
177177
)
178178
}

0 commit comments

Comments
 (0)