From cd1dad747c75624bdb52d25c7911ec65ad28a265 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Oct 2025 08:48:31 -0400 Subject: [PATCH 01/10] convert silo page tabs to path-based routing --- app/pages/system/silos/SiloFleetRolesTab.tsx | 53 ++++++++++ app/pages/system/silos/SiloIdpsTab.tsx | 14 ++- app/pages/system/silos/SiloIpPoolsTab.tsx | 19 +++- app/pages/system/silos/SiloPage.tsx | 101 ++++++------------- app/pages/system/silos/SiloQuotasTab.tsx | 11 +- app/routes.tsx | 17 ++++ app/util/path-builder.spec.ts | 5 +- app/util/path-builder.ts | 5 +- test/e2e/ip-pools.e2e.ts | 2 +- test/e2e/silos.e2e.ts | 4 +- 10 files changed, 149 insertions(+), 82 deletions(-) create mode 100644 app/pages/system/silos/SiloFleetRolesTab.tsx diff --git a/app/pages/system/silos/SiloFleetRolesTab.tsx b/app/pages/system/silos/SiloFleetRolesTab.tsx new file mode 100644 index 0000000000..713691b291 --- /dev/null +++ b/app/pages/system/silos/SiloFleetRolesTab.tsx @@ -0,0 +1,53 @@ +/* + * 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 { usePrefetchedApiQuery } from '@oxide/api' +import { Cloud24Icon, NextArrow12Icon } from '@oxide/design-system/icons/react' + +import { useSiloSelector } from '~/hooks/use-params' +import { Badge } from '~/ui/lib/Badge' +import { EmptyMessage } from '~/ui/lib/EmptyMessage' +import { TableEmptyBox } from '~/ui/lib/Table' + +export default function SiloFleetRolesTab() { + const siloSelector = useSiloSelector() + const { data: silo } = usePrefetchedApiQuery('siloView', { path: siloSelector }) + + const roleMapPairs = Object.entries(silo.mappedFleetRoles).flatMap( + ([fleetRole, siloRoles]) => + siloRoles.map((siloRole) => [siloRole, fleetRole] as [string, string]) + ) + + if (roleMapPairs.length === 0) { + return ( + + } + title="Mapped fleet roles" + // TODO: better empty state explaining that no roles are mapped so nothing will happen + body="Silo roles can automatically grant a fleet role. This silo has no role mappings configured." + /> + + ) + } + + return ( + <> +

Silo roles can automatically grant a fleet role.

+ + + ) +} diff --git a/app/pages/system/silos/SiloIdpsTab.tsx b/app/pages/system/silos/SiloIdpsTab.tsx index d457c66d71..539e19ca96 100644 --- a/app/pages/system/silos/SiloIdpsTab.tsx +++ b/app/pages/system/silos/SiloIdpsTab.tsx @@ -7,12 +7,12 @@ */ import { createColumnHelper } from '@tanstack/react-table' import { useMemo } from 'react' -import { Outlet } from 'react-router' +import { Outlet, type LoaderFunctionArgs } from 'react-router' import { Cloud24Icon } from '@oxide/design-system/icons/react' -import { getListQFn, type IdentityProvider } from '~/api' -import { useSiloSelector } from '~/hooks/use-params' +import { getListQFn, queryClient, type IdentityProvider } from '~/api' +import { getSiloSelector, useSiloSelector } from '~/hooks/use-params' import { LinkCell } from '~/table/cells/LinkCell' import { Columns } from '~/table/columns/common' import { useQueryTable } from '~/table/QueryTable' @@ -30,7 +30,13 @@ const colHelper = createColumnHelper() export const siloIdpList = (silo: string) => getListQFn('siloIdentityProviderList', { query: { silo } }) -export function SiloIdpsTab() { +export async function clientLoader({ params }: LoaderFunctionArgs) { + const { silo } = getSiloSelector(params) + await queryClient.prefetchQuery(siloIdpList(silo).optionsFn()) + return null +} + +export default function SiloIdpsTab() { const { silo } = useSiloSelector() const columns = useMemo( diff --git a/app/pages/system/silos/SiloIpPoolsTab.tsx b/app/pages/system/silos/SiloIpPoolsTab.tsx index 726820a6f9..ddeb6feb64 100644 --- a/app/pages/system/silos/SiloIpPoolsTab.tsx +++ b/app/pages/system/silos/SiloIpPoolsTab.tsx @@ -10,13 +10,20 @@ import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' +import { type LoaderFunctionArgs } from 'react-router' -import { getListQFn, useApiMutation, useApiQueryClient, type SiloIpPool } from '@oxide/api' +import { + getListQFn, + queryClient, + useApiMutation, + useApiQueryClient, + type SiloIpPool, +} from '@oxide/api' import { Networking24Icon } from '@oxide/design-system/icons/react' import { ComboboxField } from '~/components/form/fields/ComboboxField' import { HL } from '~/components/HL' -import { useSiloSelector } from '~/hooks/use-params' +import { getSiloSelector, useSiloSelector } from '~/hooks/use-params' import { confirmAction } from '~/stores/confirm-action' import { addToast } from '~/stores/toast' import { DefaultPoolCell } from '~/table/cells/DefaultPoolCell' @@ -62,7 +69,13 @@ const allSiloPoolsQuery = (silo: string) => export const siloIpPoolsQuery = (silo: string) => getListQFn('siloIpPoolList', { path: { silo } }) -export function SiloIpPoolsTab() { +export async function clientLoader({ params }: LoaderFunctionArgs) { + const { silo } = getSiloSelector(params) + await queryClient.prefetchQuery(siloIpPoolsQuery(silo).optionsFn()) + return null +} + +export default function SiloIpPoolsTab() { const { silo } = useSiloSelector() const [showLinkModal, setShowLinkModal] = useState(false) const queryClient = useApiQueryClient() diff --git a/app/pages/system/silos/SiloPage.tsx b/app/pages/system/silos/SiloPage.tsx index 50ac9653af..02923534d3 100644 --- a/app/pages/system/silos/SiloPage.tsx +++ b/app/pages/system/silos/SiloPage.tsx @@ -5,35 +5,43 @@ * * Copyright Oxide Computer Company */ -import { type LoaderFunctionArgs } from 'react-router' +import { redirect, type LoaderFunctionArgs } from 'react-router' -import { apiQueryClient, queryClient, usePrefetchedApiQuery } from '@oxide/api' -import { Cloud16Icon, Cloud24Icon, NextArrow12Icon } from '@oxide/design-system/icons/react' +import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' +import { Cloud16Icon, Cloud24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' -import { QueryParamTabs } from '~/components/QueryParamTabs' +import { RouteTabs, Tab } from '~/components/RouteTabs' import { makeCrumb } from '~/hooks/use-crumbs' import { getSiloSelector, useSiloSelector } from '~/hooks/use-params' -import { Badge } from '~/ui/lib/Badge' -import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { PropertiesTable } from '~/ui/lib/PropertiesTable' -import { TableEmptyBox } from '~/ui/lib/Table' -import { Tabs } from '~/ui/lib/Tabs' import { docLinks } from '~/util/links' +import { pb } from '~/util/path-builder' -import { siloIdpList, SiloIdpsTab } from './SiloIdpsTab' -import { siloIpPoolsQuery, SiloIpPoolsTab } from './SiloIpPoolsTab' -import { SiloQuotasTab } from './SiloQuotasTab' - -export async function clientLoader({ params }: LoaderFunctionArgs) { +export async function clientLoader({ params, request }: LoaderFunctionArgs) { const { silo } = getSiloSelector(params) - await Promise.all([ - apiQueryClient.prefetchQuery('siloView', { path: { silo } }), - apiQueryClient.prefetchQuery('siloUtilizationView', { path: { silo } }), - queryClient.prefetchQuery(siloIdpList(silo).optionsFn()), - queryClient.prefetchQuery(siloIpPoolsQuery(silo).optionsFn()), - ]) + + // Handle old query param-based URLs for backwards compatibility + const url = new URL(request.url) + const tab = url.searchParams.get('tab') + if (tab) { + const tabRoutes: Record = { + idps: pb.siloIdps({ silo }), + 'ip-pools': pb.siloIpPools({ silo }), + quotas: pb.siloQuotas({ silo }), + 'fleet-roles': pb.siloFleetRoles({ silo }), + } + // Redirect to new route-based URL + if (tabRoutes[tab]) { + return redirect(tabRoutes[tab]) + } + // Unknown tab, redirect to default + return redirect(pb.siloIdps({ silo })) + } + + // Only load data needed by the parent page. Tab-specific data is loaded by each tab's loader. + await apiQueryClient.prefetchQuery('siloView', { path: { silo } }) return null } @@ -44,11 +52,6 @@ export default function SiloPage() { const { data: silo } = usePrefetchedApiQuery('siloView', { path: siloSelector }) - const roleMapPairs = Object.entries(silo.mappedFleetRoles).flatMap( - ([fleetRole, siloRoles]) => - siloRoles.map((siloRole) => [siloRole, fleetRole] as [string, string]) - ) - return ( <> @@ -73,50 +76,12 @@ export default function SiloPage() { - - - Identity Providers - IP Pools - Quotas - Fleet roles - - - - - - - - - - - - {/* TODO: better empty state explaining that no roles are mapped so nothing will happen */} - {roleMapPairs.length === 0 ? ( - - } - title="Mapped fleet roles" - body="Silo roles can automatically grant a fleet role. This silo has no role mappings configured." - /> - - ) : ( - <> -

- Silo roles can automatically grant a fleet role. -

-
    - {roleMapPairs.map(([siloRole, fleetRole]) => ( -
  • - Silo {siloRole} - - Fleet {fleetRole} -
  • - ))} -
- - )} -
-
+ + Identity Providers + IP Pools + Quotas + Fleet roles + ) } diff --git a/app/pages/system/silos/SiloQuotasTab.tsx b/app/pages/system/silos/SiloQuotasTab.tsx index 5d513cd939..f0d29da7e2 100644 --- a/app/pages/system/silos/SiloQuotasTab.tsx +++ b/app/pages/system/silos/SiloQuotasTab.tsx @@ -8,6 +8,7 @@ import { useState } from 'react' import { useForm } from 'react-hook-form' +import { type LoaderFunctionArgs } from 'react-router' import type { SetNonNullable } from 'type-fest' import { @@ -18,7 +19,7 @@ import { } from '~/api' import { NumberField } from '~/components/form/fields/NumberField' import { SideModalForm } from '~/components/form/SideModalForm' -import { useSiloSelector } from '~/hooks/use-params' +import { getSiloSelector, useSiloSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { Button } from '~/ui/lib/Button' import { Message } from '~/ui/lib/Message' @@ -29,7 +30,13 @@ import { bytesToGiB, GiB } from '~/util/units' const Unit = classed.span`ml-1 text-secondary` -export function SiloQuotasTab() { +export async function clientLoader({ params }: LoaderFunctionArgs) { + const { silo } = getSiloSelector(params) + await apiQueryClient.prefetchQuery('siloUtilizationView', { path: { silo } }) + return null +} + +export default function SiloQuotasTab() { const { silo } = useSiloSelector() const { data: utilization } = usePrefetchedApiQuery('siloUtilizationView', { path: { silo: silo }, diff --git a/app/routes.tsx b/app/routes.tsx index f653b18af8..7594c3e24b 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -129,6 +129,23 @@ export const routes = createRoutesFromElements( path=":silo" lazy={() => import('./pages/system/silos/SiloPage').then(convert)} > + } /> + import('./pages/system/silos/SiloIdpsTab').then(convert)} + /> + import('./pages/system/silos/SiloIpPoolsTab').then(convert)} + /> + import('./pages/system/silos/SiloQuotasTab').then(convert)} + /> + import('./pages/system/silos/SiloFleetRolesTab').then(convert)} + /> import('./forms/idp/create').then(convert)} diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index 202994c026..b20320c036 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -81,10 +81,13 @@ test('path builder', () => { "serialConsole": "/projects/p/instances/i/serial-console", "silo": "/system/silos/s", "siloAccess": "/access", + "siloFleetRoles": "/system/silos/s/fleet-roles", + "siloIdps": "/system/silos/s/idps", "siloIdpsNew": "/system/silos/s/idps-new", "siloImageEdit": "/images/im/edit", "siloImages": "/images", - "siloIpPools": "/system/silos/s?tab=ip-pools", + "siloIpPools": "/system/silos/s/ip-pools", + "siloQuotas": "/system/silos/s/quotas", "siloUtilization": "/utilization", "silos": "/system/silos", "silosNew": "/system/silos-new", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 1a75b7354b..98323f1652 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -123,8 +123,11 @@ export const pb = { silos: () => '/system/silos', silosNew: () => '/system/silos-new', silo: ({ silo }: PP.Silo) => `/system/silos/${silo}`, - siloIpPools: (params: PP.Silo) => `${pb.silo(params)}?tab=ip-pools`, + siloIdps: (params: PP.Silo) => `${pb.silo(params)}/idps`, siloIdpsNew: (params: PP.Silo) => `${pb.silo(params)}/idps-new`, + siloIpPools: (params: PP.Silo) => `${pb.silo(params)}/ip-pools`, + siloQuotas: (params: PP.Silo) => `${pb.silo(params)}/quotas`, + siloFleetRoles: (params: PP.Silo) => `${pb.silo(params)}/fleet-roles`, samlIdp: (params: PP.IdentityProvider) => `${pb.silo(params)}/idps/saml/${params.provider}`, diff --git a/test/e2e/ip-pools.e2e.ts b/test/e2e/ip-pools.e2e.ts index 07742b3f91..d85be83733 100644 --- a/test/e2e/ip-pools.e2e.ts +++ b/test/e2e/ip-pools.e2e.ts @@ -74,7 +74,7 @@ test('IP pool silo list', async ({ page }) => { // clicking silo takes you to silo page const siloLink = page.getByRole('link', { name: 'maze-war' }) await siloLink.click() - await expect(page).toHaveURL('/system/silos/maze-war?tab=ip-pools') + await expect(page).toHaveURL('/system/silos/maze-war/ip-pools') await page.goBack() // unlink silo and the row is gone diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index e0b8dd1dc8..86d45e5471 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -257,7 +257,7 @@ test('Identity providers', async ({ page }) => { }) test('Silo IP pools', async ({ page }) => { - await page.goto('/system/silos/maze-war?tab=ip-pools') + await page.goto('/system/silos/maze-war/ip-pools') const table = page.getByRole('table') await expectRowVisible(table, { name: 'ip-pool-1', Default: 'default' }) @@ -305,7 +305,7 @@ test('Silo IP pools', async ({ page }) => { }) test('Silo IP pools link pool', async ({ page }) => { - await page.goto('/system/silos/maze-war?tab=ip-pools') + await page.goto('/system/silos/maze-war/ip-pools') const table = page.getByRole('table') await expectRowVisible(table, { name: 'ip-pool-1', Default: 'default' }) From dd38ed7d3bf47e0adfc61300caeedbd58a54af2f Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Oct 2025 09:25:23 -0400 Subject: [PATCH 02/10] update snapshots for routes --- .../__snapshots__/path-builder.spec.ts.snap | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 2393aa324b..5d49b9b339 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -553,6 +553,26 @@ exports[`breadcrumbs 2`] = ` "path": "/access", }, ], + "siloFleetRoles (/system/silos/s/fleet-roles)": [ + { + "label": "Silos", + "path": "/system/silos", + }, + { + "label": "s", + "path": "/system/silos/s", + }, + ], + "siloIdps (/system/silos/s/idps)": [ + { + "label": "Silos", + "path": "/system/silos", + }, + { + "label": "s", + "path": "/system/silos/s", + }, + ], "siloIdpsNew (/system/silos/s/idps-new)": [ { "label": "Silos", @@ -575,7 +595,17 @@ exports[`breadcrumbs 2`] = ` "path": "/images", }, ], - "siloIpPools (/system/silos/s?tab=ip-pools)": [ + "siloIpPools (/system/silos/s/ip-pools)": [ + { + "label": "Silos", + "path": "/system/silos", + }, + { + "label": "s", + "path": "/system/silos/s", + }, + ], + "siloQuotas (/system/silos/s/quotas)": [ { "label": "Silos", "path": "/system/silos", From cab16d08e3dfd1fe847c23dbd692fe9696d51f97 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Oct 2025 09:45:11 -0400 Subject: [PATCH 03/10] Update navigation within test --- test/e2e/silos.e2e.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index 86d45e5471..af58b00523 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -155,7 +155,8 @@ test('Create silo', async ({ page }) => { await expectRowVisible(table, { Resource: 'Memory', Quota: '58 GiB' }) await expectRowVisible(table, { Resource: 'Storage', Quota: '735 GiB' }) - await page.goBack() + // Go back to the silos list page to delete the silo + await page.goto('/system/silos') // now delete it await clickRowAction(page, 'other-silo', 'Delete') From 576705822379152870278cb45303a843fef3921d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 Oct 2025 10:20:42 -0400 Subject: [PATCH 04/10] Update tests --- test/e2e/silos.e2e.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index af58b00523..accbff581c 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -155,8 +155,14 @@ test('Create silo', async ({ page }) => { await expectRowVisible(table, { Resource: 'Memory', Quota: '58 GiB' }) await expectRowVisible(table, { Resource: 'Storage', Quota: '735 GiB' }) - // Go back to the silos list page to delete the silo - await page.goto('/system/silos') + // Go back to the silos list page to delete the silo using breadcrumbs + await page + .getByRole('navigation', { name: 'Breadcrumbs' }) + .getByRole('link', { name: 'Silos' }) + .click() + + // Wait for the row to be visible before trying to delete it + await expect(page.getByRole('cell', { name: 'other-silo' })).toBeVisible() // now delete it await clickRowAction(page, 'other-silo', 'Delete') From 31416df5da40d3459454a455f98c6750818170ed Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 10 Oct 2025 10:58:14 -0500 Subject: [PATCH 05/10] don't need to wait for visible, lockfile changes --- package-lock.json | 52 ++++++++++++++++++++++++------------------- test/e2e/silos.e2e.ts | 3 --- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index be45e16dd7..f2f269aa1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -198,7 +198,6 @@ "resolved": "https://registry.npmjs.org/@asciidoctor/opal-runtime/-/opal-runtime-3.0.1.tgz", "integrity": "sha512-iW7ACahOG0zZft4A/4CqDcc7JX+fWRNjV5tFAVkNCzwZD+EnFolPaUOPYt8jzadc0+Bgd80cQTtRMQnaaV1kkg==", "license": "MIT", - "peer": true, "dependencies": { "glob": "8.1.0", "unxhr": "1.2.0" @@ -1108,6 +1107,7 @@ "integrity": "sha512-F0/Hrcfpy8WuxlQyAWJTEren/uxKhYonOGY4OyWmwRdeTvkh9mMSCxowZLjNkhwi/2ipqCgtXwwOk7tW0mWXkA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@babel/generator": "^7.26.2", "@babel/parser": "^7.26.2", @@ -4358,6 +4358,7 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz", "integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==", "license": "MIT", + "peer": true, "dependencies": { "@tanstack/query-core": "5.56.2" }, @@ -4860,6 +4861,7 @@ "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4870,6 +4872,7 @@ "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4970,6 +4973,7 @@ "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", @@ -5344,7 +5348,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", @@ -5373,6 +5378,7 @@ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5870,6 +5876,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001669", "electron-to-chromium": "^1.5.41", @@ -5998,9 +6005,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001716", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", + "version": "1.0.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", "dev": true, "funding": [ { @@ -6909,7 +6916,6 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -6936,7 +6942,6 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "domelementtype": "^2.3.0" }, @@ -6952,7 +6957,6 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", - "peer": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -7324,6 +7328,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -7380,6 +7385,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8622,7 +8628,6 @@ "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "deprecated": "Glob versions prior to v9 are no longer supported", "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -8654,7 +8659,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8881,7 +8885,6 @@ "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.0.13.tgz", "integrity": "sha512-B7JonBuAfG32I7fDouUQEogBrz3jK9gAuN1r1AaXpED6dIhtg/JwiSRhjGL7aOJwRz3HU4efowCjQBaoXiREqg==", "license": "MIT", - "peer": true, "dependencies": { "domhandler": "5.0.3", "htmlparser2": "10.0.0" @@ -8937,7 +8940,6 @@ "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.2.tgz", "integrity": "sha512-yA5012CJGSFWYZsgYzfr6HXJgDap38/AEP4ra8Cw+WHIi2ZRDXRX/QVYdumRf1P8zKyScKd6YOrWYvVEiPfGKg==", "license": "MIT", - "peer": true, "dependencies": { "domhandler": "5.0.3", "html-dom-parser": "5.0.13", @@ -8976,7 +8978,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -8989,7 +8990,6 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -9152,8 +9152,7 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/internal-slot": { "version": "1.1.0", @@ -10484,6 +10483,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@bundled-es-modules/cookie": "^2.0.1", "@bundled-es-modules/statuses": "^1.0.1", @@ -11457,6 +11457,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11655,6 +11656,7 @@ "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11708,6 +11710,7 @@ "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11977,6 +11980,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12035,6 +12039,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -12090,8 +12095,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz", "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-remove-scroll": { "version": "2.6.3", @@ -13410,7 +13414,6 @@ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", "license": "MIT", - "peer": true, "dependencies": { "style-to-object": "1.0.8" } @@ -13420,7 +13423,6 @@ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", "license": "MIT", - "peer": true, "dependencies": { "inline-style-parser": "0.2.4" } @@ -13536,6 +13538,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -13709,6 +13712,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -13934,6 +13938,7 @@ "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -14114,6 +14119,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14146,8 +14152,7 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/unist-util-is": { "version": "6.0.0", @@ -14242,7 +14247,6 @@ "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.2.0.tgz", "integrity": "sha512-6cGpm8NFXPD9QbSNx0cD2giy7teZ6xOkCUH3U89WKVkL9N9rBrWjlCwhR94Re18ZlAop4MOc3WU1M3Hv/bgpIw==", "license": "MIT", - "peer": true, "engines": { "node": ">=8.11" } @@ -14468,6 +14472,7 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -14647,6 +14652,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index accbff581c..a7d1885fdf 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -161,9 +161,6 @@ test('Create silo', async ({ page }) => { .getByRole('link', { name: 'Silos' }) .click() - // Wait for the row to be visible before trying to delete it - await expect(page.getByRole('cell', { name: 'other-silo' })).toBeVisible() - // now delete it await clickRowAction(page, 'other-silo', 'Delete') await page.getByRole('button', { name: 'Confirm' }).click() From 6bca84edbf042c9a91dd041569f592ef677f7ad7 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 10 Oct 2025 10:58:14 -0500 Subject: [PATCH 06/10] make silo base route private, make IdPs route canonical --- app/util/__snapshots__/path-builder.spec.ts.snap | 2 +- app/util/path-builder.spec.ts | 2 +- app/util/path-builder.ts | 16 +++++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/util/__snapshots__/path-builder.spec.ts.snap b/app/util/__snapshots__/path-builder.spec.ts.snap index 5d49b9b339..b52f8f530e 100644 --- a/app/util/__snapshots__/path-builder.spec.ts.snap +++ b/app/util/__snapshots__/path-builder.spec.ts.snap @@ -537,7 +537,7 @@ exports[`breadcrumbs 2`] = ` "path": "/projects/p/instances/i/serial-console", }, ], - "silo (/system/silos/s)": [ + "silo (/system/silos/s/idps)": [ { "label": "Silos", "path": "/system/silos", diff --git a/app/util/path-builder.spec.ts b/app/util/path-builder.spec.ts index b20320c036..2a94d2262d 100644 --- a/app/util/path-builder.spec.ts +++ b/app/util/path-builder.spec.ts @@ -79,7 +79,7 @@ test('path builder', () => { "projectsNew": "/projects-new", "samlIdp": "/system/silos/s/idps/saml/pr", "serialConsole": "/projects/p/instances/i/serial-console", - "silo": "/system/silos/s", + "silo": "/system/silos/s/idps", "siloAccess": "/access", "siloFleetRoles": "/system/silos/s/fleet-roles", "siloIdps": "/system/silos/s/idps", diff --git a/app/util/path-builder.ts b/app/util/path-builder.ts index 98323f1652..18a1744fa7 100644 --- a/app/util/path-builder.ts +++ b/app/util/path-builder.ts @@ -18,6 +18,7 @@ const vpcBase = ({ project, vpc }: PP.Vpc) => `${pb.vpcs({ project })}/${vpc}` export const instanceMetricsBase = ({ project, instance }: PP.Instance) => `${instanceBase({ project, instance })}/metrics` export const inventoryBase = () => '/system/inventory' +const siloBase = ({ silo }: PP.Silo) => `/system/silos/${silo}` export const pb = { projects: () => `/projects`, @@ -122,14 +123,15 @@ export const pb = { silos: () => '/system/silos', silosNew: () => '/system/silos-new', - silo: ({ silo }: PP.Silo) => `/system/silos/${silo}`, - siloIdps: (params: PP.Silo) => `${pb.silo(params)}/idps`, - siloIdpsNew: (params: PP.Silo) => `${pb.silo(params)}/idps-new`, - siloIpPools: (params: PP.Silo) => `${pb.silo(params)}/ip-pools`, - siloQuotas: (params: PP.Silo) => `${pb.silo(params)}/quotas`, - siloFleetRoles: (params: PP.Silo) => `${pb.silo(params)}/fleet-roles`, + // canonical route for silo is first tab + silo: (params: PP.Silo) => pb.siloIdps(params), + siloIdps: (params: PP.Silo) => `${siloBase(params)}/idps`, + siloIdpsNew: (params: PP.Silo) => `${siloBase(params)}/idps-new`, + siloIpPools: (params: PP.Silo) => `${siloBase(params)}/ip-pools`, + siloQuotas: (params: PP.Silo) => `${siloBase(params)}/quotas`, + siloFleetRoles: (params: PP.Silo) => `${siloBase(params)}/fleet-roles`, samlIdp: (params: PP.IdentityProvider) => - `${pb.silo(params)}/idps/saml/${params.provider}`, + `${siloBase(params)}/idps/saml/${params.provider}`, profile: () => '/settings/profile', sshKeys: () => '/settings/ssh-keys', From 6554de57f21ee0bef12841303fe20c22c9dfaf05 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 10 Oct 2025 11:16:36 -0500 Subject: [PATCH 07/10] fix idps tab content disappearing when side modal is open --- app/routes.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/routes.tsx b/app/routes.tsx index 7594c3e24b..a085800cf9 100644 --- a/app/routes.tsx +++ b/app/routes.tsx @@ -129,11 +129,19 @@ export const routes = createRoutesFromElements( path=":silo" lazy={() => import('./pages/system/silos/SiloPage').then(convert)} > + {/* Nesting keeps IdPs tab contents rendered when side modals are open*/} } /> - import('./pages/system/silos/SiloIdpsTab').then(convert)} - /> + import('./pages/system/silos/SiloIdpsTab').then(convert)}> + + import('./forms/idp/create').then(convert)} + /> + import('./forms/idp/edit').then(convert)} + /> + import('./pages/system/silos/SiloIpPoolsTab').then(convert)} @@ -146,14 +154,6 @@ export const routes = createRoutesFromElements( path="fleet-roles" lazy={() => import('./pages/system/silos/SiloFleetRolesTab').then(convert)} /> - import('./forms/idp/create').then(convert)} - /> - import('./forms/idp/edit').then(convert)} - /> From 5fbc8fb5affee4abe87586b56edd2a936481dd98 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 10 Oct 2025 12:05:44 -0500 Subject: [PATCH 08/10] not worried about backwards compatibility with query params --- app/pages/system/silos/SiloPage.tsx | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/app/pages/system/silos/SiloPage.tsx b/app/pages/system/silos/SiloPage.tsx index 02923534d3..a8bec0bb6b 100644 --- a/app/pages/system/silos/SiloPage.tsx +++ b/app/pages/system/silos/SiloPage.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { redirect, type LoaderFunctionArgs } from 'react-router' +import { type LoaderFunctionArgs } from 'react-router' import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' import { Cloud16Icon, Cloud24Icon } from '@oxide/design-system/icons/react' @@ -19,28 +19,8 @@ import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { docLinks } from '~/util/links' import { pb } from '~/util/path-builder' -export async function clientLoader({ params, request }: LoaderFunctionArgs) { +export async function clientLoader({ params }: LoaderFunctionArgs) { const { silo } = getSiloSelector(params) - - // Handle old query param-based URLs for backwards compatibility - const url = new URL(request.url) - const tab = url.searchParams.get('tab') - if (tab) { - const tabRoutes: Record = { - idps: pb.siloIdps({ silo }), - 'ip-pools': pb.siloIpPools({ silo }), - quotas: pb.siloQuotas({ silo }), - 'fleet-roles': pb.siloFleetRoles({ silo }), - } - // Redirect to new route-based URL - if (tabRoutes[tab]) { - return redirect(tabRoutes[tab]) - } - // Unknown tab, redirect to default - return redirect(pb.siloIdps({ silo })) - } - - // Only load data needed by the parent page. Tab-specific data is loaded by each tab's loader. await apiQueryClient.prefetchQuery('siloView', { path: { silo } }) return null } From e89e30506dc910adc60413ba59acd908500a84d1 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 10 Oct 2025 12:09:49 -0500 Subject: [PATCH 09/10] might as well use apiq in new code --- app/pages/system/silos/SiloPage.tsx | 6 +++--- app/pages/system/silos/SiloQuotasTab.tsx | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/pages/system/silos/SiloPage.tsx b/app/pages/system/silos/SiloPage.tsx index a8bec0bb6b..1088509fc8 100644 --- a/app/pages/system/silos/SiloPage.tsx +++ b/app/pages/system/silos/SiloPage.tsx @@ -7,9 +7,9 @@ */ import { type LoaderFunctionArgs } from 'react-router' -import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' import { Cloud16Icon, Cloud24Icon } from '@oxide/design-system/icons/react' +import { apiq, queryClient, usePrefetchedQuery } from '~/api' import { DocsPopover } from '~/components/DocsPopover' import { RouteTabs, Tab } from '~/components/RouteTabs' import { makeCrumb } from '~/hooks/use-crumbs' @@ -21,7 +21,7 @@ import { pb } from '~/util/path-builder' export async function clientLoader({ params }: LoaderFunctionArgs) { const { silo } = getSiloSelector(params) - await apiQueryClient.prefetchQuery('siloView', { path: { silo } }) + await queryClient.prefetchQuery(apiq('siloView', { path: { silo } })) return null } @@ -30,7 +30,7 @@ export const handle = makeCrumb((p) => p.silo!) export default function SiloPage() { const siloSelector = useSiloSelector() - const { data: silo } = usePrefetchedApiQuery('siloView', { path: siloSelector }) + const { data: silo } = usePrefetchedQuery(apiq('siloView', { path: siloSelector })) return ( <> diff --git a/app/pages/system/silos/SiloQuotasTab.tsx b/app/pages/system/silos/SiloQuotasTab.tsx index f0d29da7e2..d97c3d0a9d 100644 --- a/app/pages/system/silos/SiloQuotasTab.tsx +++ b/app/pages/system/silos/SiloQuotasTab.tsx @@ -12,9 +12,10 @@ import { type LoaderFunctionArgs } from 'react-router' import type { SetNonNullable } from 'type-fest' import { - apiQueryClient, + apiq, + queryClient, useApiMutation, - usePrefetchedApiQuery, + usePrefetchedQuery, type SiloQuotasUpdate, } from '~/api' import { NumberField } from '~/components/form/fields/NumberField' @@ -32,15 +33,15 @@ const Unit = classed.span`ml-1 text-secondary` export async function clientLoader({ params }: LoaderFunctionArgs) { const { silo } = getSiloSelector(params) - await apiQueryClient.prefetchQuery('siloUtilizationView', { path: { silo } }) + await queryClient.prefetchQuery(apiq('siloUtilizationView', { path: { silo } })) return null } export default function SiloQuotasTab() { const { silo } = useSiloSelector() - const { data: utilization } = usePrefetchedApiQuery('siloUtilizationView', { - path: { silo: silo }, - }) + const { data: utilization } = usePrefetchedQuery( + apiq('siloUtilizationView', { path: { silo } }) + ) const { allocated: quotas, provisioned } = utilization @@ -98,9 +99,11 @@ export default function SiloQuotasTab() { function EditQuotasForm({ onDismiss }: { onDismiss: () => void }) { const { silo } = useSiloSelector() - const { data: utilization } = usePrefetchedApiQuery('siloUtilizationView', { - path: { silo: silo }, - }) + const { data: utilization } = usePrefetchedQuery( + apiq('siloUtilizationView', { + path: { silo: silo }, + }) + ) const quotas = utilization.allocated // required because we need to rule out undefined because NumberField hates that @@ -114,7 +117,7 @@ function EditQuotasForm({ onDismiss }: { onDismiss: () => void }) { const updateQuotas = useApiMutation('siloQuotasUpdate', { onSuccess() { - apiQueryClient.invalidateQueries('siloUtilizationView') + queryClient.invalidateQueries({ queryKey: ['siloUtilizationView'] }) addToast({ content: 'Quotas updated' }) onDismiss() }, From 63fe2d102f7ed26da8bc385ef5ad0783ba430b11 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 10 Oct 2025 12:23:11 -0500 Subject: [PATCH 10/10] use invalidateEndpoint --- app/pages/system/silos/SiloQuotasTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pages/system/silos/SiloQuotasTab.tsx b/app/pages/system/silos/SiloQuotasTab.tsx index d97c3d0a9d..c65ca10554 100644 --- a/app/pages/system/silos/SiloQuotasTab.tsx +++ b/app/pages/system/silos/SiloQuotasTab.tsx @@ -117,7 +117,7 @@ function EditQuotasForm({ onDismiss }: { onDismiss: () => void }) { const updateQuotas = useApiMutation('siloQuotasUpdate', { onSuccess() { - queryClient.invalidateQueries({ queryKey: ['siloUtilizationView'] }) + queryClient.invalidateEndpoint('siloUtilizationView') addToast({ content: 'Quotas updated' }) onDismiss() },