diff --git a/app/pages/system/SiloImageEdit.tsx b/app/pages/SiloImageEdit.tsx similarity index 100% rename from app/pages/system/SiloImageEdit.tsx rename to app/pages/SiloImageEdit.tsx diff --git a/app/pages/system/SiloImagesPage.tsx b/app/pages/SiloImagesPage.tsx similarity index 98% rename from app/pages/system/SiloImagesPage.tsx rename to app/pages/SiloImagesPage.tsx index 977131244a..82f0ca89dd 100644 --- a/app/pages/system/SiloImagesPage.tsx +++ b/app/pages/SiloImagesPage.tsx @@ -51,11 +51,13 @@ const EmptyState = () => ( const imageList = getListQFn('imageList', {}) -export async function loader() { +export async function clientLoader() { await queryClient.prefetchQuery(imageList.optionsFn()) return null } +export const handle = { crumb: 'Images' } + const colHelper = createColumnHelper() const staticCols = [ colHelper.accessor('name', { @@ -66,8 +68,7 @@ const staticCols = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -Component.displayName = 'SiloImagesPage' -export function Component() { +export default function SiloImagesPage() { const [showModal, setShowModal] = useState(false) const [demoteImage, setDemoteImage] = useState(null) diff --git a/app/pages/project/vpcs/VpcPage.tsx b/app/pages/project/vpcs/VpcPage.tsx index 9f9c489f54..6518c6f04e 100644 --- a/app/pages/project/vpcs/VpcPage.tsx +++ b/app/pages/project/vpcs/VpcPage.tsx @@ -27,12 +27,12 @@ import { VpcDocsPopover } from './VpcsPage' const vpcView = ({ project, vpc }: PP.Vpc) => apiq('vpcView', { path: { vpc }, query: { project } }) -VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { await queryClient.prefetchQuery(vpcView(getVpcSelector(params))) return null } -export function VpcPage() { +export default function VpcPage() { const navigate = useNavigate() const vpcSelector = useVpcSelector() const { project, vpc: vpcName } = vpcSelector diff --git a/app/pages/project/vpcs/VpcRoutersTab.tsx b/app/pages/project/vpcs/VpcRoutersTab.tsx index 62288118af..daadeabe4b 100644 --- a/app/pages/project/vpcs/VpcRoutersTab.tsx +++ b/app/pages/project/vpcs/VpcRoutersTab.tsx @@ -35,14 +35,15 @@ const colHelper = createColumnHelper() const vpcRouterList = (query: PP.Vpc) => getListQFn('vpcRouterList', { query }) -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project, vpc } = getVpcSelector(params) await queryClient.prefetchQuery(vpcRouterList({ project, vpc }).optionsFn()) return null } -Component.displayName = 'VpcRoutersTab' -export function Component() { +export const handle = { crumb: 'Routers' } + +export default function VpcRoutersTab() { const vpcSelector = useVpcSelector() const navigate = useNavigate() const { project, vpc } = vpcSelector diff --git a/app/pages/project/vpcs/VpcsPage.tsx b/app/pages/project/vpcs/VpcsPage.tsx index 72406192f2..659d5a02a7 100644 --- a/app/pages/project/vpcs/VpcsPage.tsx +++ b/app/pages/project/vpcs/VpcsPage.tsx @@ -15,6 +15,7 @@ import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/r import { DocsPopover } from '~/components/DocsPopover' import { HL } from '~/components/HL' +import { makeCrumb } from '~/hooks/use-crumbs' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { useQuickActions } from '~/hooks/use-quick-actions' import { confirmDelete } from '~/stores/confirm-delete' @@ -65,13 +66,15 @@ const colHelper = createColumnHelper() // just as in the vpcList call for the quick actions menu, include limit to make // sure it matches the call in the QueryTable -VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { project } = getProjectSelector(params) await queryClient.prefetchQuery(vpcList(project).optionsFn()) return null } -export function VpcsPage() { +export const handle = makeCrumb('VPCs', (p) => pb.vpcs(getProjectSelector(p))) + +export default function VpcsPage() { const { project } = useProjectSelector() const navigate = useNavigate() diff --git a/app/pages/system/inventory/DisksTab.tsx b/app/pages/system/inventory/DisksTab.tsx index 36a442688b..02025e3ecc 100644 --- a/app/pages/system/inventory/DisksTab.tsx +++ b/app/pages/system/inventory/DisksTab.tsx @@ -40,11 +40,13 @@ const EmptyState = () => ( const diskList = getListQFn('physicalDiskList', {}) -export async function loader() { +export async function clientLoader() { await queryClient.prefetchQuery(diskList.optionsFn()) return null } +export const handle = { crumb: 'Disks' } + const colHelper = createColumnHelper() const staticCols = [ colHelper.accessor('id', {}), @@ -69,8 +71,7 @@ const staticCols = [ }), ] -Component.displayName = 'DisksTab' -export function Component() { +export default function DisksTab() { const emptyState = const { table } = useQueryTable({ query: diskList, columns: staticCols, emptyState }) return table diff --git a/app/pages/system/inventory/InventoryPage.tsx b/app/pages/system/inventory/InventoryPage.tsx index 8e1a10df9b..215c444836 100644 --- a/app/pages/system/inventory/InventoryPage.tsx +++ b/app/pages/system/inventory/InventoryPage.tsx @@ -11,18 +11,21 @@ import { Servers16Icon, Servers24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' import { RouteTabs, Tab } from '~/components/RouteTabs' +import { makeCrumb } from '~/hooks/use-crumbs' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' const rackList = getListQFn('rackList', {}) -InventoryPage.loader = async () => { +export async function clientLoader() { await queryClient.prefetchQuery(rackList.optionsFn()) return null } -export function InventoryPage() { +export const handle = makeCrumb('Inventory', pb.sledInventory()) + +export default function InventoryPage() { const { data: racks } = usePrefetchedQuery(rackList.optionsFn()) const rack = racks?.items[0] diff --git a/app/pages/system/inventory/SledsTab.tsx b/app/pages/system/inventory/SledsTab.tsx index 4051a05052..68b94f344c 100644 --- a/app/pages/system/inventory/SledsTab.tsx +++ b/app/pages/system/inventory/SledsTab.tsx @@ -30,11 +30,13 @@ const STATE_BADGE_COLORS: Record = { const sledList = getListQFn('sledList', {}) -export async function loader() { +export async function clientLoader() { await queryClient.prefetchQuery(sledList.optionsFn()) return null } +export const handle = { crumb: 'Sleds' } + const colHelper = createColumnHelper() const staticCols = [ colHelper.accessor('id', { @@ -88,8 +90,7 @@ const staticCols = [ }), ] -Component.displayName = 'SledsTab' -export function Component() { +export default function SledsTab() { const emptyState = } title="No sleds found" /> const { table } = useQueryTable({ query: sledList, columns: staticCols, emptyState }) return table diff --git a/app/pages/system/inventory/sled/SledInstancesTab.tsx b/app/pages/system/inventory/sled/SledInstancesTab.tsx index 59c80b724d..6440767e6e 100644 --- a/app/pages/system/inventory/sled/SledInstancesTab.tsx +++ b/app/pages/system/inventory/sled/SledInstancesTab.tsx @@ -33,12 +33,14 @@ const EmptyState = () => { ) } -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { sledId } = requireSledParams(params) await queryClient.prefetchQuery(sledInstanceList(sledId).optionsFn()) return null } +export const handle = { crumb: 'Instances' } + // passing in empty function because we still want the copy ID button const makeActions = (): MenuAction[] => [] @@ -69,8 +71,7 @@ const staticCols = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -Component.displayName = 'SledInstancesTab' -export function Component() { +export default function SledInstancesTab() { const { sledId } = useSledParams() const columns = useColsWithActions(staticCols, makeActions) const { table } = useQueryTable({ diff --git a/app/pages/system/inventory/sled/SledPage.tsx b/app/pages/system/inventory/sled/SledPage.tsx index 4119b67e3c..b150a158ce 100644 --- a/app/pages/system/inventory/sled/SledPage.tsx +++ b/app/pages/system/inventory/sled/SledPage.tsx @@ -12,21 +12,26 @@ import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' import { Servers24Icon } from '@oxide/design-system/icons/react' import { RouteTabs, Tab } from '~/components/RouteTabs' +import { makeCrumb } from '~/hooks/use-crumbs' import { requireSledParams, useSledParams } from '~/hooks/use-params' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { PropertiesTable } from '~/ui/lib/PropertiesTable' +import { truncate } from '~/ui/lib/Truncate' import { pb } from '~/util/path-builder' -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { sledId } = requireSledParams(params) await apiQueryClient.prefetchQuery('sledView', { path: { sledId }, }) return null } +export const handle = makeCrumb( + (p) => truncate(p.sledId!, 12, 'middle'), + (p) => pb.sled({ sledId: p.sledId! }) +) -Component.displayName = 'SledPage' -export function Component() { +export default function SledPage() { const { sledId } = useSledParams() const { data: sled } = usePrefetchedApiQuery('sledView', { path: { sledId } }) diff --git a/app/pages/system/networking/IpPoolPage.tsx b/app/pages/system/networking/IpPoolPage.tsx index dd05e1855f..cea1eab308 100644 --- a/app/pages/system/networking/IpPoolPage.tsx +++ b/app/pages/system/networking/IpPoolPage.tsx @@ -33,6 +33,7 @@ import { ComboboxField } from '~/components/form/fields/ComboboxField' import { HL } from '~/components/HL' import { MoreActionsMenu } from '~/components/MoreActionsMenu' import { QueryParamTabs } from '~/components/QueryParamTabs' +import { makeCrumb } from '~/hooks/use-crumbs' import { getIpPoolSelector, useIpPoolSelector } from '~/hooks/use-params' import { confirmAction } from '~/stores/confirm-action' import { confirmDelete } from '~/stores/confirm-delete' @@ -59,7 +60,7 @@ const ipPoolView = (pool: string) => apiq('ipPoolView', { path: { pool } }) const ipPoolSiloList = (pool: string) => getListQFn('ipPoolSiloList', { path: { pool } }) const ipPoolRangeList = (pool: string) => getListQFn('ipPoolRangeList', { path: { pool } }) -export async function loader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { pool } = getIpPoolSelector(params) await Promise.all([ queryClient.prefetchQuery(ipPoolView(pool)), @@ -79,8 +80,9 @@ export async function loader({ params }: LoaderFunctionArgs) { return null } -Component.displayName = 'IpPoolPage' -export function Component() { +export const handle = makeCrumb((p) => p.pool!) + +export default function IpPoolpage() { const poolSelector = useIpPoolSelector() const { data: pool } = usePrefetchedQuery(ipPoolView(poolSelector.pool)) const { data: ranges } = usePrefetchedQuery( diff --git a/app/pages/system/networking/IpPoolsPage.tsx b/app/pages/system/networking/IpPoolsPage.tsx index f112eb3e49..17c83e6cd7 100644 --- a/app/pages/system/networking/IpPoolsPage.tsx +++ b/app/pages/system/networking/IpPoolsPage.tsx @@ -69,13 +69,14 @@ const staticColumns = [ const ipPoolList = () => getListQFn('ipPoolList', {}) -export async function loader() { +export async function clientLoader() { await queryClient.prefetchQuery(ipPoolList().optionsFn()) return null } -Component.displayName = 'IpPoolsPage' -export function Component() { +export const handle = { crumb: 'IP Pools' } + +export default function IpPoolsPage() { const navigate = useNavigate() const { mutateAsync: deletePool } = useApiMutation('ipPoolDelete', { diff --git a/app/routes.tsx b/app/routes.tsx index 9425f9e7e7..c69d1e7d9d 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -16,19 +16,7 @@ import { import { NotFound } from './components/ErrorPage' import { makeCrumb, type Crumb } from './hooks/use-crumbs' -import { getInstanceSelector, getProjectSelector, getVpcSelector } from './hooks/use-params' -import * as VpcRoutersTab from './pages/project/vpcs//VpcRoutersTab' -import { VpcPage } from './pages/project/vpcs/VpcPage' -import { VpcsPage } from './pages/project/vpcs/VpcsPage' -import * as DisksTab from './pages/system/inventory/DisksTab' -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 IpPool from './pages/system/networking/IpPoolPage' -import * as IpPools from './pages/system/networking/IpPoolsPage' -import * as SiloImages from './pages/system/SiloImagesPage' -import { truncate } from './ui/lib/Truncate' +import { getInstanceSelector, getVpcSelector } from './hooks/use-params' import { pb } from './util/path-builder' // hack because RR doesn't export the redirect type @@ -129,37 +117,58 @@ export const routes = createRoutesFromElements( /> } - loader={InventoryPage.loader} - handle={makeCrumb('Inventory', pb.sledInventory())} + lazy={() => import('./pages/system/inventory/InventoryPage.tsx').then(convert)} > - } loader={SledsTab.loader} /> - - + + import('./pages/system/inventory/SledsTab') + .then(convert) + .then(({ loader }) => ({ + loader, + Component: () => , + })) + } + /> + import('./pages/system/inventory/SledsTab').then(convert)} + /> + import('./pages/system/inventory/DisksTab').then(convert)} + /> {/* a crumb for the sled ID looks ridiculous, unfortunately */} truncate(p.sledId!, 12, 'middle'), - (p) => pb.sled({ sledId: p.sledId! }) - )} + lazy={() => import('./pages/system/inventory/sled/SledPage').then(convert)} > } - loader={SledInstances.loader} + lazy={() => + import('./pages/system/inventory/sled/SledInstancesTab') + .then(convert) + .then(({ loader }) => ({ + loader, + Component: () => , + })) + } + /> + + import('./pages/system/inventory/sled/SledInstancesTab').then(convert) + } /> - } /> - + import('./pages/system/networking/IpPoolsPage').then(convert)}> - p.pool!)}> + import('./pages/system/networking/IpPoolPage').then(convert)} + > import('./forms/ip-pool-edit').then(convert)} /> } /> import('./layouts/SiloLayout').then(convert)}> - + import('./pages/SiloImagesPage.tsx').then(convert)} + > import('./pages/system/SiloImageEdit').then(convert)} + lazy={() => import('./pages/SiloImageEdit.tsx').then(convert)} /> - pb.vpcs(getProjectSelector(p)))} - element={} - > + import('./pages/project/vpcs/VpcsPage').then(convert)}> pb.vpc(getVpcSelector(p)) )} > - } loader={VpcPage.loader}> + import('./pages/project/vpcs/VpcPage').then(convert)}> import('./forms/subnet-edit').then(convert)} /> - + import('./pages/project/vpcs/VpcRoutersTab').then(convert)} + > getCrumbs(page)).toEqual(crumbs) + await expect.poll(() => getCrumbs(page), { timeout: 10000 }).toEqual(crumbs) } const projectCrumbs: Pair[] = [ diff --git a/test/e2e/inventory.e2e.ts b/test/e2e/inventory.e2e.ts index 387fa3f82b..b77f9e65d3 100644 --- a/test/e2e/inventory.e2e.ts +++ b/test/e2e/inventory.e2e.ts @@ -55,6 +55,7 @@ test('Sled inventory page', async ({ page }) => { await sledsTable.getByRole('link').first().click() await expectVisible(page, ['role=heading[name*="Sled"]']) + await expect(page.getByText('serialBRM02222869')).toBeVisible() const instancesTab = page.getByRole('tab', { name: 'Instances' }) await expect(instancesTab).toBeVisible() diff --git a/vite.config.ts b/vite.config.ts index 87af9e0a99..6a1607f4a7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -110,7 +110,7 @@ export default defineConfig(({ mode }) => ({ // but some end up being like 300 bytes. It feels silly to have several // hundred of those, so we set a minimum size to end up with fewer. // https://rollupjs.org/configuration-options/#output-experimentalminchunksize - experimentalMinChunkSize: 5 * KiB, + experimentalMinChunkSize: 30 * KiB, }, }, // prevent inlining assets as `data:`, which is not permitted by our Content-Security-Policy