From 622bc80c076211d3d1adc10e18a7fa4dc6fa0bc5 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 4 Dec 2024 17:22:12 -0600 Subject: [PATCH 1/8] show what it looks like to get rid of apiQueryClient --- app/api/client.ts | 12 ++++++++++++ app/pages/ProjectsPage.tsx | 14 +++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/api/client.ts b/app/api/client.ts index b1b6bd380..3977be00d 100644 --- a/app/api/client.ts +++ b/app/api/client.ts @@ -65,6 +65,18 @@ export const queryClient = new QueryClient({ // don't have access to context export const apiQueryClient = wrapQueryClient(api.methods, queryClient) +/** + * Invalidate all cached queries for a given endpoint. + * + * Note that we only take a single argument, `method`, rather than allowing + * the full query key `[query, params]` to be specified. This is to avoid + * accidentally overspecifying and therefore failing to match the desired query. + * The params argument can be added in if we ever have a use case for it. + */ +export function invalidate(method: keyof typeof api.methods) { + queryClient.invalidateQueries({ queryKey: [method] }) +} + // used to retrieve the typed query client in components. doesn't need to exist: // we could import apiQueryClient directly everywhere, but the change is noisy export const useApiQueryClient = () => apiQueryClient diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 5b2134d40..1fd0ccc92 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -10,8 +10,9 @@ import { useCallback, useMemo } from 'react' import { Outlet, useNavigate } from 'react-router-dom' import { - apiQueryClient, + apiq, getListQFn, + invalidate, queryClient, useApiMutation, type Project, @@ -64,9 +65,7 @@ export function Component() { const { mutateAsync: deleteProject } = useApiMutation('projectDelete', { onSuccess() { - // TODO: figure out if this is invalidating as expected, can we leave out the query - // altogether, etc. Look at whether limit param matters. - apiQueryClient.invalidateQueries('projectList') + invalidate('projectList') }, }) @@ -77,11 +76,8 @@ export function Component() { onActivate: () => { // the edit view has its own loader, but we can make the modal open // instantaneously by preloading the fetch result - apiQueryClient.setQueryData( - 'projectView', - { path: { project: project.name } }, - project - ) + const queryOptions = apiq('projectView', { path: { project: project.name } }) + queryClient.setQueryData(queryOptions.queryKey, project) navigate(pb.projectEdit({ project: project.name })) }, }, From a134c36cacb5be7f7736027025415edd5d80fe81 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 4 Dec 2024 17:35:04 -0600 Subject: [PATCH 2/8] try invalidateOptions on list options helper --- app/api/hooks.ts | 6 ++++++ app/pages/ProjectsPage.tsx | 11 ++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/api/hooks.ts b/app/api/hooks.ts index 8cac13b43..412011b34 100644 --- a/app/api/hooks.ts +++ b/app/api/hooks.ts @@ -132,6 +132,11 @@ export type PaginatedQuery = { pageToken?: string ) => UseQueryOptions & { queryKey: QueryKey } pageSize: number + /** + * Options to pass to `queryClient.invalidateQueries` that will invalidate all + * cached queries to this endpoint, regardless of parameters. + */ + invalidateOptions: { queryKey: readonly [string] } } /** @@ -174,6 +179,7 @@ export const getListQueryOptionsFn = }) }, pageSize: limit, + invalidateOptions: { queryKey: [method] }, } } diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 1fd0ccc92..576037019 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -9,14 +9,7 @@ import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo } from 'react' import { Outlet, useNavigate } from 'react-router-dom' -import { - apiq, - getListQFn, - invalidate, - queryClient, - useApiMutation, - type Project, -} from '@oxide/api' +import { apiq, getListQFn, queryClient, useApiMutation, type Project } from '@oxide/api' import { Folder16Icon, Folder24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' @@ -65,7 +58,7 @@ export function Component() { const { mutateAsync: deleteProject } = useApiMutation('projectDelete', { onSuccess() { - invalidate('projectList') + queryClient.invalidateQueries(projectList.invalidateOptions) }, }) From 1fba247ec59b679b2982b8006437fe44f3cd2443 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 5 Dec 2024 13:05:01 -0600 Subject: [PATCH 3/8] show what it looks like to get apiQueryClient out of more pages --- app/forms/project-edit.tsx | 21 +++++++++++-------- app/layouts/ProjectLayout.tsx | 12 ++++++----- app/pages/ProjectsPage.tsx | 4 ++-- .../vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx | 20 ++++++++++-------- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 7af23a172..592b06493 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -9,10 +9,11 @@ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import { - apiQueryClient, + apiq, + invalidate, + queryClient, useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, + usePrefetchedQuery, } from '@oxide/api' import { DescriptionField } from '~/components/form/fields/DescriptionField' @@ -23,29 +24,31 @@ import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' +const projectView = ({ project }: { project: string }) => + apiq('projectView', { path: { project } }) + EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { project } = getProjectSelector(params) - await apiQueryClient.prefetchQuery('projectView', { path: { project } }) + await queryClient.prefetchQuery(projectView({ project })) return null } export function EditProjectSideModalForm() { - const queryClient = useApiQueryClient() const navigate = useNavigate() const projectSelector = useProjectSelector() const onDismiss = () => navigate(pb.projects()) - const { data: project } = usePrefetchedApiQuery('projectView', { path: projectSelector }) + const { data: project } = usePrefetchedQuery(projectView(projectSelector)) const editProject = useApiMutation('projectUpdate', { onSuccess(project) { // refetch list of projects in sidebar - // TODO: check this invalidation - queryClient.invalidateQueries('projectList') + invalidate('projectList') // avoid the project fetch when the project page loads since we have the data - queryClient.setQueryData('projectView', { path: { project: project.name } }, project) + const { queryKey } = projectView({ project: project.name }) + queryClient.setQueryData(queryKey, project) addToast(<>Project {project.name} updated) // prettier-ignore onDismiss() }, diff --git a/app/layouts/ProjectLayout.tsx b/app/layouts/ProjectLayout.tsx index 3ba21d951..9142ea86d 100644 --- a/app/layouts/ProjectLayout.tsx +++ b/app/layouts/ProjectLayout.tsx @@ -8,7 +8,7 @@ import { useMemo, type ReactElement } from 'react' import { useLocation, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api' +import { apiq, queryClient, usePrefetchedQuery } from '@oxide/api' import { Access16Icon, Folder16Icon, @@ -36,10 +36,12 @@ type ProjectLayoutProps = { overrideContentPane?: ReactElement } +const projectView = ({ project }: { project: string }) => + apiq('projectView', { path: { project } }) + ProjectLayout.loader = async ({ params }: LoaderFunctionArgs) => { - await apiQueryClient.prefetchQuery('projectView', { - path: getProjectSelector(params), - }) + const { project } = getProjectSelector(params) + await queryClient.prefetchQuery(projectView({ project })) return null } @@ -47,7 +49,7 @@ export function ProjectLayout({ overrideContentPane }: ProjectLayoutProps) { const navigate = useNavigate() // project will always be there, instance may not const projectSelector = useProjectSelector() - const { data: project } = usePrefetchedApiQuery('projectView', { path: projectSelector }) + const { data: project } = usePrefetchedQuery(projectView(projectSelector)) const { pathname } = useLocation() useQuickActions( diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 576037019..10caab42e 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -69,8 +69,8 @@ export function Component() { onActivate: () => { // the edit view has its own loader, but we can make the modal open // instantaneously by preloading the fetch result - const queryOptions = apiq('projectView', { path: { project: project.name } }) - queryClient.setQueryData(queryOptions.queryKey, project) + const { queryKey } = apiq('projectView', { path: { project: project.name } }) + queryClient.setQueryData(queryKey, project) navigate(pb.projectEdit({ project: project.name })) }, }, diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx index e97dcc12a..9b54ab032 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -11,10 +11,11 @@ import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import * as R from 'remeda' import { - apiQueryClient, + apiq, + invalidate, + queryClient, useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, + usePrefetchedQuery, type VpcFirewallRule, } from '@oxide/api' @@ -97,26 +98,27 @@ const staticColumns = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] +const rulesView = (vpcSel: { vpc: string; project: string }) => + apiq('vpcFirewallRulesView', { query: vpcSel }) + VpcFirewallRulesTab.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) - await apiQueryClient.prefetchQuery('vpcFirewallRulesView', { query: { project, vpc } }) + await queryClient.prefetchQuery(rulesView({ project, vpc })) return null } export function VpcFirewallRulesTab() { - const queryClient = useApiQueryClient() const vpcSelector = useVpcSelector() - const { data } = usePrefetchedApiQuery('vpcFirewallRulesView', { - query: vpcSelector, - }) + const { data } = usePrefetchedQuery(rulesView(vpcSelector)) + const rules = useMemo(() => R.sortBy(data.rules, (r) => r.priority), [data]) const navigate = useNavigate() const { mutateAsync: updateRules } = useApiMutation('vpcFirewallRulesUpdate', { onSuccess() { - queryClient.invalidateQueries('vpcFirewallRulesView') + invalidate('vpcFirewallRulesView') }, }) From 985437bf4938832eec4c7ebd565824c79abdc614 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 5 Dec 2024 13:10:06 -0600 Subject: [PATCH 4/8] put invalidate function on queryClient by extending class --- app/api/client.ts | 36 ++++++++++++------- app/forms/project-edit.tsx | 10 ++---- .../vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx | 3 +- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/app/api/client.ts b/app/api/client.ts index 3977be00d..5c1d2dcd1 100644 --- a/app/api/client.ts +++ b/app/api/client.ts @@ -5,7 +5,11 @@ * * Copyright Oxide Computer Company */ -import { QueryClient, useQuery, type UseQueryOptions } from '@tanstack/react-query' +import { + QueryClient as QueryClientOrig, + useQuery, + type UseQueryOptions, +} from '@tanstack/react-query' import { Api } from './__generated__/Api' import { type ApiError } from './errors' @@ -49,6 +53,24 @@ export const useApiMutation = getUseApiMutation(api.methods) export const usePrefetchedQuery = (options: UseQueryOptions) => ensurePrefetched(useQuery(options), options.queryKey) +/** + * Extends React Query's `QueryClient` with a couple of API-specific methods. + * Existing methods are never modified. + */ +class QueryClient extends QueryClientOrig { + /** + * Invalidate all cached queries for a given endpoint. + * + * Note that we only take a single argument, `method`, rather than allowing + * the full query key `[query, params]` to be specified. This is to avoid + * accidentally overspecifying and therefore failing to match the desired query. + * The params argument can be added in if we ever have a use case for it. + */ + invalidateEndpoint(method: keyof typeof api.methods) { + this.invalidateQueries({ queryKey: [method] }) + } +} + // Needs to be defined here instead of in app so we can use it to define // `apiQueryClient`, which provides API-typed versions of QueryClient methods export const queryClient = new QueryClient({ @@ -65,18 +87,6 @@ export const queryClient = new QueryClient({ // don't have access to context export const apiQueryClient = wrapQueryClient(api.methods, queryClient) -/** - * Invalidate all cached queries for a given endpoint. - * - * Note that we only take a single argument, `method`, rather than allowing - * the full query key `[query, params]` to be specified. This is to avoid - * accidentally overspecifying and therefore failing to match the desired query. - * The params argument can be added in if we ever have a use case for it. - */ -export function invalidate(method: keyof typeof api.methods) { - queryClient.invalidateQueries({ queryKey: [method] }) -} - // used to retrieve the typed query client in components. doesn't need to exist: // we could import apiQueryClient directly everywhere, but the change is noisy export const useApiQueryClient = () => apiQueryClient diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 592b06493..1c7999fba 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -8,13 +8,7 @@ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiq, - invalidate, - queryClient, - useApiMutation, - usePrefetchedQuery, -} from '@oxide/api' +import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api' import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' @@ -45,7 +39,7 @@ export function EditProjectSideModalForm() { const editProject = useApiMutation('projectUpdate', { onSuccess(project) { // refetch list of projects in sidebar - invalidate('projectList') + queryClient.invalidateEndpoint('projectList') // avoid the project fetch when the project page loads since we have the data const { queryKey } = projectView({ project: project.name }) queryClient.setQueryData(queryKey, project) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx index 9b54ab032..594ee0e37 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -12,7 +12,6 @@ import * as R from 'remeda' import { apiq, - invalidate, queryClient, useApiMutation, usePrefetchedQuery, @@ -118,7 +117,7 @@ export function VpcFirewallRulesTab() { const { mutateAsync: updateRules } = useApiMutation('vpcFirewallRulesUpdate', { onSuccess() { - invalidate('vpcFirewallRulesView') + queryClient.invalidateEndpoint('vpcFirewallRulesView') }, }) From 8444dc18356e78f378b2183f9eba149acdaaecea Mon Sep 17 00:00:00 2001 From: David Crespo Date: Thu, 5 Dec 2024 22:35:09 -0600 Subject: [PATCH 5/8] convert a few more to see how they look --- app/forms/floating-ip-edit.tsx | 22 ++++++++----------- app/forms/subnet-edit.tsx | 28 ++++++++++++------------- app/forms/vpc-router-edit.tsx | 25 ++++++++++------------ app/pages/project/disks/DisksPage.tsx | 27 ++++++++++-------------- app/pages/project/images/ImagesPage.tsx | 14 +++---------- 5 files changed, 47 insertions(+), 69 deletions(-) diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index 26fe356f9..18a00b892 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -8,12 +8,7 @@ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiQueryClient, - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, -} from '@oxide/api' +import { apiq, queryClient, useApiMutation, usePrefetchedApiQuery } from '@oxide/api' import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' @@ -23,17 +18,18 @@ import { getFloatingIpSelector, useFloatingIpSelector } from '~/hooks/use-params import { addToast } from '~/stores/toast' import { pb } from 'app/util/path-builder' +type FipSelector = { project: string; floatingIp: string } + +const floatingIpView = ({ project, floatingIp }: FipSelector) => + apiq('floatingIpView', { path: { floatingIp }, query: { project } }) + EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { floatingIp, project } = getFloatingIpSelector(params) - await apiQueryClient.prefetchQuery('floatingIpView', { - path: { floatingIp }, - query: { project }, - }) + const selector = getFloatingIpSelector(params) + await queryClient.prefetchQuery(floatingIpView(selector)) return null } export function EditFloatingIpSideModalForm() { - const queryClient = useApiQueryClient() const navigate = useNavigate() const floatingIpSelector = useFloatingIpSelector() @@ -47,7 +43,7 @@ export function EditFloatingIpSideModalForm() { const editFloatingIp = useApiMutation('floatingIpUpdate', { onSuccess(_floatingIp) { - queryClient.invalidateQueries('floatingIpList') + queryClient.invalidateEndpoint('floatingIpList') addToast(<>Floating IP {_floatingIp.name} updated) // prettier-ignore onDismiss() }, diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx index 49ab973fb..956dfe572 100644 --- a/app/forms/subnet-edit.tsx +++ b/app/forms/subnet-edit.tsx @@ -9,10 +9,10 @@ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import { - apiQueryClient, + apiq, + queryClient, useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, + usePrefetchedQuery, type VpcSubnetUpdate, } from '@oxide/api' @@ -31,30 +31,28 @@ import { addToast } from '~/stores/toast' import { FormDivider } from '~/ui/lib/Divider' import { pb } from '~/util/path-builder' +type SubnetSelector = { project: string; vpc: string; subnet: string } +const subnetView = ({ project, vpc, subnet }: SubnetSelector) => + apiq('vpcSubnetView', { query: { project, vpc }, path: { subnet } }) + EditSubnetForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, vpc, subnet } = getVpcSubnetSelector(params) - await apiQueryClient.prefetchQuery('vpcSubnetView', { - query: { project, vpc }, - path: { subnet }, - }) + const selector = getVpcSubnetSelector(params) + await queryClient.prefetchQuery(subnetView(selector)) return null } export function EditSubnetForm() { - const { project, vpc, subnet: subnetName } = useVpcSubnetSelector() - const queryClient = useApiQueryClient() + const subnetSelector = useVpcSubnetSelector() + const { project, vpc } = subnetSelector const navigate = useNavigate() const onDismiss = () => navigate(pb.vpcSubnets({ project, vpc })) - const { data: subnet } = usePrefetchedApiQuery('vpcSubnetView', { - query: { project, vpc }, - path: { subnet: subnetName }, - }) + const { data: subnet } = usePrefetchedQuery(subnetView(subnetSelector)) const updateSubnet = useApiMutation('vpcSubnetUpdate', { onSuccess(subnet) { - queryClient.invalidateQueries('vpcSubnetList') + queryClient.invalidateEndpoint('vpcSubnetList') addToast(<>Subnet {subnet.name} updated) // prettier-ignore onDismiss() }, diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx index 134aadcf2..4de010e20 100644 --- a/app/forms/vpc-router-edit.tsx +++ b/app/forms/vpc-router-edit.tsx @@ -13,10 +13,10 @@ import { } from 'react-router-dom' import { - apiQueryClient, + apiq, + queryClient, useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, + usePrefetchedQuery, type VpcRouterUpdate, } from '@oxide/api' @@ -28,23 +28,20 @@ import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' +type RouterSelector = { project: string; vpc: string; router: string } +const routerView = ({ project, vpc, router }: RouterSelector) => + apiq('vpcRouterView', { path: { router }, query: { project, vpc } }) + EditRouterSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { - const { router, project, vpc } = getVpcRouterSelector(params) - await apiQueryClient.prefetchQuery('vpcRouterView', { - path: { router }, - query: { project, vpc }, - }) + const selector = getVpcRouterSelector(params) + await queryClient.prefetchQuery(routerView(selector)) return null } export function EditRouterSideModalForm() { - const queryClient = useApiQueryClient() const routerSelector = useVpcRouterSelector() const { project, vpc, router } = routerSelector - const { data: routerData } = usePrefetchedApiQuery('vpcRouterView', { - path: { router }, - query: { project, vpc }, - }) + const { data: routerData } = usePrefetchedQuery(routerView(routerSelector)) const navigate = useNavigate() const onDismiss = (navigate: NavigateFunction) => { @@ -53,7 +50,7 @@ export function EditRouterSideModalForm() { const editRouter = useApiMutation('vpcRouterUpdate', { onSuccess(updatedRouter) { - queryClient.invalidateQueries('vpcRouterList') + queryClient.invalidateEndpoint('vpcRouterList') addToast(<>Router {updatedRouter.name} updated) // prettier-ignore navigate(pb.vpcRouters({ project, vpc })) }, diff --git a/app/pages/project/disks/DisksPage.tsx b/app/pages/project/disks/DisksPage.tsx index adaa7e7cc..d2d2f52b1 100644 --- a/app/pages/project/disks/DisksPage.tsx +++ b/app/pages/project/disks/DisksPage.tsx @@ -10,13 +10,12 @@ import { useCallback } from 'react' import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' import { - apiQueryClient, + apiq, diskCan, genName, getListQFn, queryClient, useApiMutation, - useApiQueryClient, type Disk, } from '@oxide/api' import { Storage16Icon, Storage24Icon } from '@oxide/design-system/icons/react' @@ -51,6 +50,8 @@ const EmptyState = () => ( ) const diskList = (project: string) => getListQFn('diskList', { query: { project } }) +const instanceList = (project: string) => + getListQFn('instanceList', { query: { project, limit: 200 } }) DisksPage.loader = async ({ params }: LoaderFunctionArgs) => { const { project } = getProjectSelector(params) @@ -60,17 +61,12 @@ DisksPage.loader = async ({ params }: LoaderFunctionArgs) => { // fetch instances and preload into RQ cache so fetches by ID in // InstanceLinkCell can be mostly instant yet gracefully fall back to // fetching individually if we don't fetch them all here - apiQueryClient - .fetchQuery('instanceList', { query: { project, limit: 200 } }) - .then((instances) => { - for (const instance of instances.items) { - apiQueryClient.setQueryData( - 'instanceView', - { path: { instance: instance.id } }, - instance - ) - } - }), + queryClient.fetchQuery(instanceList(project).optionsFn()).then((instances) => { + for (const instance of instances.items) { + const { queryKey } = apiq('instanceView', { path: { instance: instance.id } }) + queryClient.setQueryData(queryKey, instance) + } + }), ]) return null } @@ -97,19 +93,18 @@ const staticCols = [ ] export function DisksPage() { - const queryClient = useApiQueryClient() const { project } = useProjectSelector() const { mutateAsync: deleteDisk } = useApiMutation('diskDelete', { onSuccess(_data, variables) { - queryClient.invalidateQueries('diskList') + queryClient.invalidateEndpoint('diskList') addToast(<>Disk {variables.path.disk} deleted) // prettier-ignore }, }) const { mutate: createSnapshot } = useApiMutation('snapshotCreate', { onSuccess(_data, variables) { - queryClient.invalidateQueries('snapshotList') + queryClient.invalidateEndpoint('snapshotList') addToast(<>Snapshot {variables.body.name} created) // prettier-ignore }, onError(err) { diff --git a/app/pages/project/images/ImagesPage.tsx b/app/pages/project/images/ImagesPage.tsx index 226848230..cf2defd25 100644 --- a/app/pages/project/images/ImagesPage.tsx +++ b/app/pages/project/images/ImagesPage.tsx @@ -9,14 +9,7 @@ import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo, useState } from 'react' import { Outlet, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiQueryClient, - getListQFn, - queryClient, - useApiMutation, - useApiQueryClient, - type Image, -} from '@oxide/api' +import { getListQFn, queryClient, useApiMutation, type Image } from '@oxide/api' import { Images16Icon, Images24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' @@ -65,7 +58,7 @@ export function ImagesPage() { const { mutateAsync: deleteImage } = useApiMutation('imageDelete', { onSuccess(_data, variables) { addToast(<>Image {variables.path.image} deleted) // prettier-ignore - apiQueryClient.invalidateQueries('imageList') + queryClient.invalidateEndpoint('imageList') }, }) @@ -138,7 +131,6 @@ type PromoteModalProps = { onDismiss: () => void; imageName: string } const PromoteImageModal = ({ onDismiss, imageName }: PromoteModalProps) => { const { project } = useProjectSelector() - const queryClient = useApiQueryClient() const promoteImage = useApiMutation('imagePromote', { onSuccess(data) { @@ -153,7 +145,7 @@ const PromoteImageModal = ({ onDismiss, imageName }: PromoteModalProps) => { link: '/images', }, }) - queryClient.invalidateQueries('imageList') + queryClient.invalidateEndpoint('imageList') }, onError: (err) => { addToast({ title: 'Error', content: err.message, variant: 'error' }) From ae28b8a15029aa891f6513ecb2f3765183e72dd0 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 6 Dec 2024 12:29:26 -0600 Subject: [PATCH 6/8] use the new path params types --- app/forms/floating-ip-edit.tsx | 5 ++--- app/forms/project-edit.tsx | 4 ++-- app/forms/subnet-edit.tsx | 4 ++-- app/forms/vpc-router-edit.tsx | 4 ++-- app/layouts/ProjectLayout.tsx | 4 ++-- app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx | 4 ++-- 6 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index 18a00b892..17469321e 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -16,11 +16,10 @@ import { SideModalForm } from '~/components/form/SideModalForm' import { HL } from '~/components/HL' import { getFloatingIpSelector, useFloatingIpSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' +import type * as PP from '~/util/path-params' import { pb } from 'app/util/path-builder' -type FipSelector = { project: string; floatingIp: string } - -const floatingIpView = ({ project, floatingIp }: FipSelector) => +const floatingIpView = ({ project, floatingIp }: PP.FloatingIp) => apiq('floatingIpView', { path: { floatingIp }, query: { project } }) EditFloatingIpSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 1c7999fba..45f05da9b 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -17,9 +17,9 @@ import { HL } from '~/components/HL' import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' -const projectView = ({ project }: { project: string }) => - apiq('projectView', { path: { project } }) +const projectView = ({ project }: PP.Project) => apiq('projectView', { path: { project } }) EditProjectSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { project } = getProjectSelector(params) diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx index 956dfe572..204767b38 100644 --- a/app/forms/subnet-edit.tsx +++ b/app/forms/subnet-edit.tsx @@ -30,9 +30,9 @@ import { getVpcSubnetSelector, useVpcSubnetSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { FormDivider } from '~/ui/lib/Divider' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' -type SubnetSelector = { project: string; vpc: string; subnet: string } -const subnetView = ({ project, vpc, subnet }: SubnetSelector) => +const subnetView = ({ project, vpc, subnet }: PP.VpcSubnet) => apiq('vpcSubnetView', { query: { project, vpc }, path: { subnet } }) EditSubnetForm.loader = async ({ params }: LoaderFunctionArgs) => { diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx index 4de010e20..fb808b590 100644 --- a/app/forms/vpc-router-edit.tsx +++ b/app/forms/vpc-router-edit.tsx @@ -27,9 +27,9 @@ import { HL } from '~/components/HL' import { getVpcRouterSelector, useVpcRouterSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' -type RouterSelector = { project: string; vpc: string; router: string } -const routerView = ({ project, vpc, router }: RouterSelector) => +const routerView = ({ project, vpc, router }: PP.VpcRouter) => apiq('vpcRouterView', { path: { router }, query: { project, vpc } }) EditRouterSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { diff --git a/app/layouts/ProjectLayout.tsx b/app/layouts/ProjectLayout.tsx index 9142ea86d..979ee2854 100644 --- a/app/layouts/ProjectLayout.tsx +++ b/app/layouts/ProjectLayout.tsx @@ -25,6 +25,7 @@ import { getProjectSelector, useProjectSelector } from '~/hooks/use-params' import { useQuickActions } from '~/hooks/use-quick-actions' import { Divider } from '~/ui/lib/Divider' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' import { DocsLinkItem, NavLinkItem, Sidebar } from '../components/Sidebar' import { ContentPane, PageContainer } from './helpers' @@ -36,8 +37,7 @@ type ProjectLayoutProps = { overrideContentPane?: ReactElement } -const projectView = ({ project }: { project: string }) => - apiq('projectView', { path: { project } }) +const projectView = ({ project }: PP.Project) => apiq('projectView', { path: { project } }) ProjectLayout.loader = async ({ params }: LoaderFunctionArgs) => { const { project } = getProjectSelector(params) diff --git a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx index 594ee0e37..b4cbe0d2e 100644 --- a/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx +++ b/app/pages/project/vpcs/VpcPage/tabs/VpcFirewallRulesTab.tsx @@ -32,6 +32,7 @@ import { CreateLink } from '~/ui/lib/CreateButton' import { EmptyMessage } from '~/ui/lib/EmptyMessage' import { TableEmptyBox } from '~/ui/lib/Table' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' import { titleCase } from '~/util/str' const colHelper = createColumnHelper() @@ -97,8 +98,7 @@ const staticColumns = [ colHelper.accessor('timeCreated', Columns.timeCreated), ] -const rulesView = (vpcSel: { vpc: string; project: string }) => - apiq('vpcFirewallRulesView', { query: vpcSel }) +const rulesView = (query: PP.Vpc) => apiq('vpcFirewallRulesView', { query }) VpcFirewallRulesTab.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) From b3bd45fd49ee5a313bffc480f18f508ccae075f4 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 6 Dec 2024 12:35:49 -0600 Subject: [PATCH 7/8] remove invalidateOptions on paginated query thing --- app/api/hooks.ts | 6 ------ app/pages/ProjectsPage.tsx | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/api/hooks.ts b/app/api/hooks.ts index 412011b34..8cac13b43 100644 --- a/app/api/hooks.ts +++ b/app/api/hooks.ts @@ -132,11 +132,6 @@ export type PaginatedQuery = { pageToken?: string ) => UseQueryOptions & { queryKey: QueryKey } pageSize: number - /** - * Options to pass to `queryClient.invalidateQueries` that will invalidate all - * cached queries to this endpoint, regardless of parameters. - */ - invalidateOptions: { queryKey: readonly [string] } } /** @@ -179,7 +174,6 @@ export const getListQueryOptionsFn = }) }, pageSize: limit, - invalidateOptions: { queryKey: [method] }, } } diff --git a/app/pages/ProjectsPage.tsx b/app/pages/ProjectsPage.tsx index 10caab42e..3357a1574 100644 --- a/app/pages/ProjectsPage.tsx +++ b/app/pages/ProjectsPage.tsx @@ -58,7 +58,7 @@ export function Component() { const { mutateAsync: deleteProject } = useApiMutation('projectDelete', { onSuccess() { - queryClient.invalidateQueries(projectList.invalidateOptions) + queryClient.invalidateEndpoint('projectList') }, }) From 604ecf52c70071159d6df6b4ae580935073f2ea2 Mon Sep 17 00:00:00 2001 From: David Crespo Date: Fri, 6 Dec 2024 12:50:45 -0600 Subject: [PATCH 8/8] convert some more, actually cut some lines! --- app/forms/image-from-snapshot.tsx | 23 +++++------- app/forms/vpc-edit.tsx | 23 +++++------- app/pages/lookups.ts | 14 +++---- .../project/floating-ips/FloatingIpsPage.tsx | 37 +++++++------------ app/pages/project/vpcs/VpcPage/VpcPage.tsx | 22 ++++------- app/pages/project/vpcs/VpcsPage.tsx | 21 +++-------- 6 files changed, 54 insertions(+), 86 deletions(-) diff --git a/app/forms/image-from-snapshot.tsx b/app/forms/image-from-snapshot.tsx index 8d44a1d8e..b6e7e2a55 100644 --- a/app/forms/image-from-snapshot.tsx +++ b/app/forms/image-from-snapshot.tsx @@ -10,10 +10,10 @@ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import { - apiQueryClient, + apiq, + queryClient, useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, + usePrefetchedQuery, type ImageCreate, } from '@oxide/api' @@ -26,6 +26,7 @@ import { getProjectSnapshotSelector, useProjectSnapshotSelector } from '~/hooks/ import { addToast } from '~/stores/toast' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' const defaultValues: Omit = { name: '', @@ -34,29 +35,25 @@ const defaultValues: Omit = { version: '', } +const snapshotView = ({ project, snapshot }: PP.Snapshot) => + apiq('snapshotView', { path: { snapshot }, query: { project } }) + CreateImageFromSnapshotSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { project, snapshot } = getProjectSnapshotSelector(params) - await apiQueryClient.prefetchQuery('snapshotView', { - path: { snapshot }, - query: { project }, - }) + await queryClient.prefetchQuery(snapshotView({ project, snapshot })) return null } export function CreateImageFromSnapshotSideModalForm() { const { snapshot, project } = useProjectSnapshotSelector() - const { data } = usePrefetchedApiQuery('snapshotView', { - path: { snapshot }, - query: { project }, - }) + const { data } = usePrefetchedQuery(snapshotView({ project, snapshot })) const navigate = useNavigate() - const queryClient = useApiQueryClient() const onDismiss = () => navigate(pb.snapshots({ project })) const createImage = useApiMutation('imageCreate', { onSuccess(image) { - queryClient.invalidateQueries('imageList') + queryClient.invalidateEndpoint('imageList') addToast(<>Image {image.name} created) // prettier-ignore onDismiss() }, diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx index 0982d17f1..473eedc64 100644 --- a/app/forms/vpc-edit.tsx +++ b/app/forms/vpc-edit.tsx @@ -8,12 +8,7 @@ import { useForm } from 'react-hook-form' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiQueryClient, - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, -} from '@oxide/api' +import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api' import { DescriptionField } from '~/components/form/fields/DescriptionField' import { NameField } from '~/components/form/fields/NameField' @@ -22,26 +17,26 @@ import { HL } from '~/components/HL' import { getVpcSelector, useVpcSelector } from '~/hooks/use-params' import { addToast } from '~/stores/toast' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' + +const vpcView = ({ project, vpc }: PP.Vpc) => + apiq('vpcView', { path: { vpc }, query: { project } }) EditVpcSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => { const { project, vpc } = getVpcSelector(params) - await apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }) + await queryClient.prefetchQuery(vpcView({ project, vpc })) return null } export function EditVpcSideModalForm() { const { vpc: vpcName, project } = useVpcSelector() - const queryClient = useApiQueryClient() const navigate = useNavigate() - const { data: vpc } = usePrefetchedApiQuery('vpcView', { - path: { vpc: vpcName }, - query: { project }, - }) + const { data: vpc } = usePrefetchedQuery(vpcView({ project, vpc: vpcName })) const editVpc = useApiMutation('vpcUpdate', { onSuccess(updatedVpc) { - queryClient.invalidateQueries('vpcList') + queryClient.invalidateEndpoint('vpcList') navigate(pb.vpc({ project, vpc: updatedVpc.name })) addToast(<>VPC {updatedVpc.name} updated) // prettier-ignore @@ -51,7 +46,7 @@ export function EditVpcSideModalForm() { // page's VPC gets cleared out while we're still on the page. If we're // navigating to a different page, its query will fetch anew regardless. if (vpc.name === updatedVpc.name) { - queryClient.invalidateQueries('vpcView') + queryClient.invalidateEndpoint('vpcView') } }, }) diff --git a/app/pages/lookups.ts b/app/pages/lookups.ts index 52413f2e4..9fa98cd2e 100644 --- a/app/pages/lookups.ts +++ b/app/pages/lookups.ts @@ -7,19 +7,19 @@ */ import { redirect, type LoaderFunctionArgs } from 'react-router-dom' -import { apiQueryClient } from '@oxide/api' +import { apiq, queryClient } from '@oxide/api' import { trigger404 } from '~/components/ErrorBoundary' import { pb } from '~/util/path-builder' export async function instanceLookupLoader({ params }: LoaderFunctionArgs) { try { - const instance = await apiQueryClient.fetchQuery('instanceView', { - path: { instance: params.instance! }, - }) - const project = await apiQueryClient.fetchQuery('projectView', { - path: { project: instance.projectId }, - }) + const instance = await queryClient.fetchQuery( + apiq('instanceView', { path: { instance: params.instance! } }) + ) + const project = await queryClient.fetchQuery( + apiq('projectView', { path: { project: instance.projectId } }) + ) return redirect(pb.instance({ project: project.name, instance: instance.name })) } catch (_e) { throw trigger404 diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index de198518a..39053df5e 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -11,11 +11,10 @@ import { useForm } from 'react-hook-form' import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' import { - apiQueryClient, + apiq, getListQFn, queryClient, useApiMutation, - useApiQueryClient, usePrefetchedQuery, type FloatingIp, type Instance, @@ -67,15 +66,12 @@ FloatingIpsPage.loader = async ({ params }: LoaderFunctionArgs) => { // fetch IP Pools and preload into RQ cache so fetches by ID in // IpPoolCell can be mostly instant yet gracefully fall back to // fetching individually if we don't fetch them all here - apiQueryClient - .fetchQuery('projectIpPoolList', { query: { limit: ALL_ISH } }) + queryClient + .fetchQuery(apiq('projectIpPoolList', { query: { limit: ALL_ISH } })) .then((pools) => { for (const pool of pools.items) { - apiQueryClient.setQueryData( - 'projectIpPoolView', - { path: { pool: pool.id } }, - pool - ) + const { queryKey } = apiq('projectIpPoolView', { path: { pool: pool.id } }) + queryClient.setQueryData(queryKey, pool) } }), ]) @@ -102,14 +98,13 @@ const staticCols = [ export function FloatingIpsPage() { const [floatingIpToModify, setFloatingIpToModify] = useState(null) - const queryClient = useApiQueryClient() const { project } = useProjectSelector() const { data: instances } = usePrefetchedQuery(instanceList(project).optionsFn()) const navigate = useNavigate() const { mutateAsync: floatingIpDetach } = useApiMutation('floatingIpDetach', { onSuccess(floatingIp) { - queryClient.invalidateQueries('floatingIpList') + queryClient.invalidateEndpoint('floatingIpList') addToast(<>Floating IP {floatingIp.name} detached) // prettier-ignore }, onError: (err) => { @@ -118,8 +113,8 @@ export function FloatingIpsPage() { }) const { mutateAsync: deleteFloatingIp } = useApiMutation('floatingIpDelete', { onSuccess(_data, variables) { - queryClient.invalidateQueries('floatingIpList') - queryClient.invalidateQueries('ipPoolUtilizationView') + queryClient.invalidateEndpoint('floatingIpList') + queryClient.invalidateEndpoint('ipPoolUtilizationView') addToast(<>Floating IP {variables.path.floatingIp} deleted) // prettier-ignore }, }) @@ -171,14 +166,11 @@ export function FloatingIpsPage() { { label: 'Edit', onActivate: () => { - apiQueryClient.setQueryData( - 'floatingIpView', - { - path: { floatingIp: floatingIp.name }, - query: { project }, - }, - floatingIp - ) + const { queryKey } = apiq('floatingIpView', { + path: { floatingIp: floatingIp.name }, + query: { project }, + }) + queryClient.setQueryData(queryKey, floatingIp) navigate(pb.floatingIpEdit({ project, floatingIp: floatingIp.name })) }, }, @@ -251,10 +243,9 @@ const AttachFloatingIpModal = ({ project: string onDismiss: () => void }) => { - const queryClient = useApiQueryClient() const floatingIpAttach = useApiMutation('floatingIpAttach', { onSuccess(floatingIp) { - queryClient.invalidateQueries('floatingIpList') + queryClient.invalidateEndpoint('floatingIpList') addToast(<>Floating IP {floatingIp.name} attached) // prettier-ignore onDismiss() }, diff --git a/app/pages/project/vpcs/VpcPage/VpcPage.tsx b/app/pages/project/vpcs/VpcPage/VpcPage.tsx index 97adda95e..dfb6a0f04 100644 --- a/app/pages/project/vpcs/VpcPage/VpcPage.tsx +++ b/app/pages/project/vpcs/VpcPage/VpcPage.tsx @@ -8,12 +8,7 @@ import { useMemo } from 'react' import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiQueryClient, - useApiMutation, - useApiQueryClient, - usePrefetchedApiQuery, -} from '@oxide/api' +import { apiq, queryClient, useApiMutation, usePrefetchedQuery } from '@oxide/api' import { Networking24Icon } from '@oxide/design-system/icons/react' import { HL } from '~/components/HL' @@ -27,28 +22,27 @@ import { DateTime } from '~/ui/lib/DateTime' import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { PropertiesTable } from '~/ui/lib/PropertiesTable' import { pb } from '~/util/path-builder' +import type * as PP from '~/util/path-params' import { VpcDocsPopover } from '../VpcsPage' +const vpcView = ({ project, vpc }: PP.Vpc) => + apiq('vpcView', { path: { vpc }, query: { project } }) + VpcPage.loader = async ({ params }: LoaderFunctionArgs) => { - const { project, vpc } = getVpcSelector(params) - await apiQueryClient.prefetchQuery('vpcView', { path: { vpc }, query: { project } }) + await queryClient.prefetchQuery(vpcView(getVpcSelector(params))) return null } export function VpcPage() { - const queryClient = useApiQueryClient() const navigate = useNavigate() const vpcSelector = useVpcSelector() const { project, vpc: vpcName } = vpcSelector - const { data: vpc } = usePrefetchedApiQuery('vpcView', { - path: { vpc: vpcName }, - query: { project }, - }) + const { data: vpc } = usePrefetchedQuery(vpcView(vpcSelector)) const { mutateAsync: deleteVpc } = useApiMutation('vpcDelete', { onSuccess(_data, variables) { - queryClient.invalidateQueries('vpcList') + queryClient.invalidateEndpoint('vpcList') navigate(pb.vpcs({ project })) addToast(<>VPC {variables.path.vpc} deleted) // prettier-ignore }, diff --git a/app/pages/project/vpcs/VpcsPage.tsx b/app/pages/project/vpcs/VpcsPage.tsx index 1b8282a4a..6f23956f6 100644 --- a/app/pages/project/vpcs/VpcsPage.tsx +++ b/app/pages/project/vpcs/VpcsPage.tsx @@ -5,19 +5,12 @@ * * Copyright Oxide Computer Company */ +import { useQuery } from '@tanstack/react-query' import { createColumnHelper } from '@tanstack/react-table' import { useCallback, useMemo } from 'react' import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom' -import { - apiQueryClient, - getListQFn, - queryClient, - useApiMutation, - useApiQuery, - useApiQueryClient, - type Vpc, -} from '@oxide/api' +import { apiq, getListQFn, queryClient, useApiMutation, type Vpc } from '@oxide/api' import { Networking16Icon, Networking24Icon } from '@oxide/design-system/icons/react' import { DocsPopover } from '~/components/DocsPopover' @@ -61,7 +54,7 @@ export const VpcDocsPopover = () => ( ) const FirewallRuleCount = ({ project, vpc }: PP.Vpc) => { - const { data } = useApiQuery('vpcFirewallRulesView', { query: { project, vpc } }) + const { data } = useQuery(apiq('vpcFirewallRulesView', { query: { project, vpc } })) if (!data) return // loading @@ -79,13 +72,12 @@ VpcsPage.loader = async ({ params }: LoaderFunctionArgs) => { } export function VpcsPage() { - const queryClient = useApiQueryClient() const { project } = useProjectSelector() const navigate = useNavigate() const { mutateAsync: deleteVpc } = useApiMutation('vpcDelete', { onSuccess(_data, variables) { - queryClient.invalidateQueries('vpcList') + queryClient.invalidateEndpoint('vpcList') addToast(<>VPC {variables.path.vpc} deleted) // prettier-ignore }, }) @@ -95,9 +87,8 @@ export function VpcsPage() { { label: 'Edit', onActivate() { - apiQueryClient.setQueryData( - 'vpcView', - { path: { vpc: vpc.name }, query: { project } }, + queryClient.setQueryData( + apiq('vpcView', { path: { vpc: vpc.name }, query: { project } }).queryKey, vpc ) navigate(pb.vpcEdit({ project, vpc: vpc.name }), { state: vpc })