diff --git a/app/pages/system/inventory/InventoryPage.tsx b/app/pages/system/inventory/InventoryPage.tsx
index 8e1a10df9..0673f6759 100644
--- a/app/pages/system/inventory/InventoryPage.tsx
+++ b/app/pages/system/inventory/InventoryPage.tsx
@@ -43,6 +43,7 @@ export function InventoryPage() {
Sleds
Disks
+ Switches
>
)
diff --git a/app/pages/system/inventory/SwitchesTab.tsx b/app/pages/system/inventory/SwitchesTab.tsx
new file mode 100644
index 000000000..bcd4c9662
--- /dev/null
+++ b/app/pages/system/inventory/SwitchesTab.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { createColumnHelper } from '@tanstack/react-table'
+
+import { getListQFn, queryClient, type Switch } from '@oxide/api'
+import { Servers24Icon } from '@oxide/design-system/icons/react'
+
+import { useQueryTable } from '~/table/QueryTable'
+import { EmptyMessage } from '~/ui/lib/EmptyMessage'
+
+const EmptyState = () => (
+ }
+ title="Something went wrong"
+ body="We expected some switches here, but none were found"
+ />
+)
+
+const switchList = getListQFn('switchList', {})
+
+export async function loader() {
+ await queryClient.prefetchQuery(switchList.optionsFn())
+ return null
+}
+
+const colHelper = createColumnHelper()
+const staticCols = [
+ colHelper.accessor('id', {}),
+ colHelper.accessor('baseboard.part', { header: 'part number' }),
+ colHelper.accessor('baseboard.serial', { header: 'serial number' }),
+ colHelper.accessor('baseboard.revision', { header: 'revision' }),
+]
+
+Component.displayName = 'SwitchesTab'
+export function Component() {
+ const emptyState =
+ const { table } = useQueryTable({ query: switchList, columns: staticCols, emptyState })
+ return table
+}
diff --git a/app/routes.tsx b/app/routes.tsx
index 19e8e671a..ed4d0a508 100644
--- a/app/routes.tsx
+++ b/app/routes.tsx
@@ -81,6 +81,7 @@ import { InventoryPage } from './pages/system/inventory/InventoryPage'
import * as SledInstances from './pages/system/inventory/sled/SledInstancesTab'
import * as SledPage from './pages/system/inventory/sled/SledPage'
import * as SledsTab from './pages/system/inventory/SledsTab'
+import * as SwitchesTab from './pages/system/inventory/SwitchesTab'
import * as IpPool from './pages/system/networking/IpPoolPage'
import * as IpPools from './pages/system/networking/IpPoolsPage'
import * as SiloImages from './pages/system/SiloImagesPage'
@@ -162,6 +163,7 @@ export const routes = createRoutesFromElements(
} loader={SledsTab.loader} />
+
diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap
index debb608d8..01a5a29e4 100644
--- a/app/util/__snapshots__/path-builder.spec.ts.snap
+++ b/app/util/__snapshots__/path-builder.spec.ts.snap
@@ -581,6 +581,16 @@ exports[`breadcrumbs 2`] = `
"path": "/settings/ssh-keys",
},
],
+ "switchInventory (/system/inventory/switches)": [
+ {
+ "label": "Inventory",
+ "path": "/system/inventory/sleds",
+ },
+ {
+ "label": "Switches",
+ "path": "/system/inventory/switches",
+ },
+ ],
"systemUtilization (/system/utilization)": [
{
"label": "Utilization",
diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts
index 291447a1b..a7698083b 100644
--- a/app/util/path-builder.spec.ts
+++ b/app/util/path-builder.spec.ts
@@ -86,6 +86,7 @@ test('path builder', () => {
"sshKeyEdit": "/settings/ssh-keys/ss/edit",
"sshKeys": "/settings/ssh-keys",
"sshKeysNew": "/settings/ssh-keys-new",
+ "switchInventory": "/system/inventory/switches",
"systemUtilization": "/system/utilization",
"vpc": "/projects/p/vpcs/v/firewall-rules",
"vpcEdit": "/projects/p/vpcs/v/edit",
diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts
index f2ec29204..a7071721f 100644
--- a/app/util/path-builder.ts
+++ b/app/util/path-builder.ts
@@ -98,6 +98,7 @@ export const pb = {
sledInventory: () => '/system/inventory/sleds',
diskInventory: () => '/system/inventory/disks',
+ switchInventory: () => '/system/inventory/switches',
sled: ({ sledId }: PP.Sled) => `/system/inventory/sleds/${sledId}/instances`,
sledInstances: ({ sledId }: PP.Sled) => `/system/inventory/sleds/${sledId}/instances`,
diff --git a/mock-api/index.ts b/mock-api/index.ts
index e03311145..4f034680d 100644
--- a/mock-api/index.ts
+++ b/mock-api/index.ts
@@ -21,6 +21,7 @@ export * from './silo'
export * from './sled'
export * from './snapshot'
export * from './sshKeys'
+export * from './switch'
export * from './user'
export * from './user-group'
export * from './user'
diff --git a/mock-api/msw/db.ts b/mock-api/msw/db.ts
index 13efe3d9b..d59019e17 100644
--- a/mock-api/msw/db.ts
+++ b/mock-api/msw/db.ts
@@ -418,6 +418,7 @@ const initDb = {
siloProvisioned: [...mock.siloProvisioned],
identityProviders: [...mock.identityProviders],
sleds: [...mock.sleds],
+ switches: [...mock.switches],
snapshots: [...mock.snapshots],
sshKeys: [...mock.sshKeys],
users: [...mock.users],
diff --git a/mock-api/msw/handlers.ts b/mock-api/msw/handlers.ts
index 4386ebca1..d9284c642 100644
--- a/mock-api/msw/handlers.ts
+++ b/mock-api/msw/handlers.ts
@@ -1501,6 +1501,11 @@ export const handlers = makeHandlers({
return paginated(query, db.users)
},
+ switchList: ({ query, cookies }) => {
+ requireFleetViewer(cookies)
+ return paginated(query, db.switches)
+ },
+
systemPolicyView({ cookies }) {
requireFleetViewer(cookies)
@@ -1587,7 +1592,6 @@ export const handlers = makeHandlers({
sledAdd: NotImplemented,
sledListUninitialized: NotImplemented,
sledSetProvisionPolicy: NotImplemented,
- switchList: NotImplemented,
switchView: NotImplemented,
systemPolicyUpdate: NotImplemented,
systemQuotasList: NotImplemented,
diff --git a/mock-api/switch.ts b/mock-api/switch.ts
new file mode 100644
index 000000000..afa9e9978
--- /dev/null
+++ b/mock-api/switch.ts
@@ -0,0 +1,25 @@
+/*
+ * 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 type { Switch } from '@oxide/api'
+
+import type { Json } from './json-type'
+import { rack } from './rack'
+
+export const switches: Json = [
+ {
+ baseboard: {
+ part: '832-0431906',
+ serial: 'BDS02141689',
+ revision: 1,
+ },
+ id: 'ed66617e-4955-465e-b810-0d0dc55d4511',
+ rack_id: rack.id,
+ time_created: rack.time_created,
+ time_modified: rack.time_modified,
+ },
+]
diff --git a/test/e2e/inventory.e2e.ts b/test/e2e/inventory.e2e.ts
index 5ec3a0a34..79d5e3c38 100644
--- a/test/e2e/inventory.e2e.ts
+++ b/test/e2e/inventory.e2e.ts
@@ -6,7 +6,7 @@
* Copyright Oxide Computer Company
*/
-import { physicalDisks, sleds } from '@oxide/api-mocks'
+import { physicalDisks, sleds, switches } from '@oxide/api-mocks'
import { expect, expectRowVisible, expectVisible, test } from './utils'
@@ -85,3 +85,21 @@ test('Disk inventory page', async ({ page }) => {
state: 'decommissioned',
})
})
+
+test('Switch inventory page', async ({ page }) => {
+ await page.goto('/system/inventory/switches')
+
+ await expectVisible(page, ['role=heading[name*="Inventory"]'])
+
+ const switchesTab = page.getByRole('tab', { name: 'Switches' })
+ await expect(switchesTab).toBeVisible()
+ await expect(switchesTab).toHaveClass(/is-selected/)
+
+ const table = page.getByRole('table')
+ await expectRowVisible(table, {
+ id: switches[0].id,
+ 'part number': '832-0431906',
+ 'serial number': 'BDS02141689',
+ revision: '1',
+ })
+})