Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions app/api/__tests__/hooks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
*
* Copyright Oxide Computer Company
*/
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query'
import { act, render, renderHook, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'

import { project } from '@oxide/api-mocks'

import { useApiMutation, useApiQuery } from '..'
import { apiq, useApiMutation } from '..'
import type { DiskCreate } from '../__generated__/Api'
import { overrideOnce } from '../../../test/unit/server'

// TODO: rethink whether these tests need to exist when they are so well-covered
// by playwright tests

// because useApiQuery and useApiMutation are almost entirely typed wrappers
// around React Query's useQuery and useMutation, these tests are mostly about
// testing the one bit of real logic in there: error parsing
Expand All @@ -32,12 +35,12 @@ export function Wrapper({ children }: { children: React.ReactNode }) {

const config = { wrapper: Wrapper }

const renderProjectList = () => renderHook(() => useApiQuery('projectList', {}), config)
const renderProjectList = () => renderHook(() => useQuery(apiq('projectList', {})), config)

// 503 is a special key in the MSW server that returns a 503
const renderGetProject503 = () =>
renderHook(
() => useApiQuery('projectView', { path: { project: 'project-error-503' } }),
() => useQuery(apiq('projectView', { path: { project: 'project-error-503' } })),
config
)

Expand Down Expand Up @@ -117,7 +120,7 @@ describe('useApiQuery', () => {
function BadApiCall() {
try {
// oxlint-disable-next-line react-hooks/rules-of-hooks
useApiQuery('projectView', { path: { project: 'nonexistent' } })
useQuery(apiq('projectView', { path: { project: 'nonexistent' } }))
} catch (e) {
onError(e)
}
Expand All @@ -139,10 +142,12 @@ describe('useApiQuery', () => {
it('default throw behavior can be overridden to use query error state', async () => {
const { result } = renderHook(
() =>
useApiQuery(
'projectView',
{ path: { project: 'nonexistent' } },
{ throwOnError: false } // <----- the point
useQuery(
apiq(
'projectView',
{ path: { project: 'nonexistent' } },
{ throwOnError: false }
)
),
config
)
Expand Down
2 changes: 0 additions & 2 deletions app/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
getApiQueryOptionsErrorsAllowed,
getListQueryOptionsFn,
getUseApiMutation,
getUseApiQuery,
getUsePrefetchedApiQuery,
wrapQueryClient,
} from './hooks'
Expand Down Expand Up @@ -56,7 +55,6 @@ export const apiqErrorsAllowed = getApiQueryOptionsErrorsAllowed(api.methods)
* `useQueryTable`.
*/
export const getListQFn = getListQueryOptionsFn(api.methods)
export const useApiQuery = getUseApiQuery(api.methods)
/**
* Same as `useApiQuery`, except we use `invariant(data)` to ensure the data is
* already there in the cache at request time, which means it has been
Expand Down
9 changes: 0 additions & 9 deletions app/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,15 +177,6 @@ export const getListQueryOptionsFn =
}
}

export const getUseApiQuery =
<A extends ApiClient>(api: A) =>
<M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: UseQueryOtherOptions<Result<A[M]>> = {}
) =>
useQuery(getApiQueryOptions(api)(method, params, options))

export const getUsePrefetchedApiQuery =
<A extends ApiClient>(api: A) =>
<M extends string & keyof A>(
Expand Down
10 changes: 5 additions & 5 deletions app/components/ExternalIps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
* Copyright Oxide Computer Company
*/

import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router'
import * as R from 'remeda'

import { useApiQuery, type ExternalIp } from '@oxide/api'
import { apiq, type ExternalIp } from '@oxide/api'

import { EmptyCell, SkeletonCell } from '~/table/cells/EmptyCell'
import { CopyableIp } from '~/ui/lib/CopyableIp'
Expand All @@ -23,10 +24,9 @@ const IP_ORDER = { floating: 0, ephemeral: 1, snat: 2 } as const
export const orderIps = (ips: ExternalIp[]) => R.sortBy(ips, (a) => IP_ORDER[a.kind])

export function ExternalIps({ project, instance }: PP.Instance) {
const { data, isPending } = useApiQuery('instanceExternalIpList', {
path: { instance },
query: { project },
})
const { data, isPending } = useQuery(
apiq('instanceExternalIpList', { path: { instance }, query: { project } })
)
if (isPending) return <SkeletonCell />

// Exclude SNAT IPs from the properties table because they are rarely going
Expand Down
60 changes: 26 additions & 34 deletions app/components/SystemMetric.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { useMemo, useRef } from 'react'

import {
synthesizeData,
useApiQuery,
type ChartDatum,
type SystemMetricName,
} from '@oxide/api'
import { apiq, synthesizeData, type ChartDatum, type SystemMetricName } from '@oxide/api'

import { ChartContainer, ChartHeader, TimeSeriesChart } from './TimeSeriesChart'

Expand Down Expand Up @@ -53,23 +49,21 @@ export function SiloMetric({
}: SiloMetricProps) {
// TODO: we're only pulling the first page. Should we bump the cap to 10k?
// Fetch multiple pages if 10k is not enough? That's a bit much.
const inRange = useApiQuery(
'siloMetric',
{
path: { metricName },
query: { project, startTime, endTime, limit: 3000 },
},
{ placeholderData: (x) => x }
const inRange = useQuery(
apiq(
'siloMetric',
{ path: { metricName }, query: { project, startTime, endTime, limit: 3000 } },
{ placeholderData: (x) => x }
)
)

// get last point before startTime to use as first point in graph
const beforeStart = useApiQuery(
'siloMetric',
{
path: { metricName },
query: { project, endTime: startTime, ...staticParams },
},
{ placeholderData: (x) => x }
const beforeStart = useQuery(
apiq(
'siloMetric',
{ path: { metricName }, query: { project, endTime: startTime, ...staticParams } },
{ placeholderData: (x) => x }
)
)

const ref = useRef<ChartDatum[] | undefined>(undefined)
Expand Down Expand Up @@ -124,23 +118,21 @@ export function SystemMetric({
}: SystemMetricProps) {
// TODO: we're only pulling the first page. Should we bump the cap to 10k?
// Fetch multiple pages if 10k is not enough? That's a bit much.
const inRange = useApiQuery(
'systemMetric',
{
path: { metricName },
query: { silo, startTime, endTime, limit: 3000 },
},
{ placeholderData: (x) => x }
const inRange = useQuery(
apiq(
'systemMetric',
{ path: { metricName }, query: { silo, startTime, endTime, limit: 3000 } },
{ placeholderData: (x) => x }
)
)

// get last point before startTime to use as first point in graph
const beforeStart = useApiQuery(
'systemMetric',
{
path: { metricName },
query: { silo, endTime: startTime, ...staticParams },
},
{ placeholderData: (x) => x }
const beforeStart = useQuery(
apiq(
'systemMetric',
{ path: { metricName }, query: { silo, endTime: startTime, ...staticParams } },
{ placeholderData: (x) => x }
)
)

const ref = useRef<ChartDatum[] | undefined>(undefined)
Expand Down
13 changes: 8 additions & 5 deletions app/components/form/fields/SubnetListbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { useWatch, type FieldPath, type FieldValues } from 'react-hook-form'

import { useApiQuery } from '@oxide/api'
import { apiq } from '@oxide/api'

import { useProjectSelector } from '~/hooks/use-params'

Expand Down Expand Up @@ -40,10 +41,12 @@ export function SubnetListbox<

// TODO: error handling other than fallback to empty list?
const subnets =
useApiQuery(
'vpcSubnetList',
{ query: { ...projectSelector, vpc: vpcName } },
{ enabled: vpcExists, throwOnError: false }
useQuery(
apiq(
'vpcSubnetList',
{ query: { ...projectSelector, vpc: vpcName } },
{ enabled: vpcExists, throwOnError: false }
)
).data?.items || []

return (
Expand Down
6 changes: 4 additions & 2 deletions app/components/form/fields/useItemsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
* Copyright Oxide Computer Company
*/

import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'

import { useApiQuery } from '~/api'
import { apiq } from '@oxide/api'

import { useVpcSelector } from '~/hooks/use-params'

/**
Expand All @@ -30,7 +32,7 @@ export function customRouterDataToForm(value: string | undefined | null): string

export const useCustomRouterItems = () => {
const vpcSelector = useVpcSelector()
const { data, isLoading } = useApiQuery('vpcRouterList', { query: vpcSelector })
const { data, isLoading } = useQuery(apiq('vpcRouterList', { query: vpcSelector }))

const routerItems = useMemo(() => {
const items = (data?.items || [])
Expand Down
9 changes: 5 additions & 4 deletions app/forms/disk-attach.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useForm } from 'react-hook-form'

import { useApiQuery, type ApiError } from '@oxide/api'
import { apiq, type ApiError } from '@oxide/api'

import { ComboboxField } from '~/components/form/fields/ComboboxField'
import { ModalForm } from '~/components/form/ModalForm'
Expand Down Expand Up @@ -40,9 +41,9 @@ export function AttachDiskModalForm({
}: AttachDiskProps) {
const { project } = useProjectSelector()

const { data, isPending } = useApiQuery('diskList', {
query: { project, limit: ALL_ISH },
})
const { data, isPending } = useQuery(
apiq('diskList', { query: { project, limit: ALL_ISH } })
)
const detachedDisks = useMemo(
() =>
toComboboxItems(
Expand Down
18 changes: 8 additions & 10 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { filesize } from 'filesize'
import { useMemo } from 'react'
import { useController, useForm, type Control } from 'react-hook-form'

import {
apiq,
useApiMutation,
useApiQuery,
useApiQueryClient,
type BlockSize,
type Disk,
Expand Down Expand Up @@ -82,8 +83,8 @@ export function CreateDiskSideModalForm({

const form = useForm({ defaultValues })
const { project } = useProjectSelector()
const projectImages = useApiQuery('imageList', { query: { project } })
const siloImages = useApiQuery('imageList', {})
const projectImages = useQuery(apiq('imageList', { query: { project } }))
const siloImages = useQuery(apiq('imageList', {}))

// put project images first because if there are any, there probably aren't
// very many and they're probably relevant
Expand All @@ -93,7 +94,7 @@ export function CreateDiskSideModalForm({
)
const areImagesLoading = projectImages.isPending || siloImages.isPending

const snapshotsQuery = useApiQuery('snapshotList', { query: { project } })
const snapshotsQuery = useQuery(apiq('snapshotList', { query: { project } }))
const snapshots = snapshotsQuery.data?.items || []

// validate disk source size
Expand Down Expand Up @@ -235,11 +236,8 @@ const DiskSourceField = ({
}

const DiskNameFromId = ({ disk }: { disk: string }) => {
const { data, isPending, isError } = useApiQuery(
'diskView',
{ path: { disk } },
// this can 404 if the source disk has been deleted, and that's fine
{ throwOnError: false }
const { data, isPending, isError } = useQuery(
apiq('diskView', { path: { disk } }, { throwOnError: false })
)

if (isPending || isError) return null
Expand All @@ -248,7 +246,7 @@ const DiskNameFromId = ({ disk }: { disk: string }) => {

const SnapshotSelectField = ({ control }: { control: Control<DiskCreate> }) => {
const { project } = useProjectSelector()
const snapshotsQuery = useApiQuery('snapshotList', { query: { project } })
const snapshotsQuery = useQuery(apiq('snapshotList', { query: { project } }))

const snapshots = snapshotsQuery.data?.items || []
const diskSizeField = useController({ control, name: 'size' }).field
Expand Down
12 changes: 5 additions & 7 deletions app/forms/floating-ip-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@
* Copyright Oxide Computer Company
*/
import * as Accordion from '@radix-ui/react-accordion'
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router'

import {
useApiMutation,
useApiQuery,
useApiQueryClient,
type FloatingIpCreate,
} from '@oxide/api'
import { apiq, useApiMutation, useApiQueryClient, type FloatingIpCreate } from '@oxide/api'

import { AccordionItem } from '~/components/AccordionItem'
import { DescriptionField } from '~/components/form/fields/DescriptionField'
Expand All @@ -42,7 +38,9 @@ export const handle = titleCrumb('New Floating IP')
export default function CreateFloatingIpSideModalForm() {
// Fetch 1000 to we can be sure to get them all. Don't bother prefetching
// because the list is hidden under the Advanced accordion.
const { data: allPools } = useApiQuery('projectIpPoolList', { query: { limit: ALL_ISH } })
const { data: allPools } = useQuery(
apiq('projectIpPoolList', { query: { limit: ALL_ISH } })
)

const queryClient = useApiQueryClient()
const projectSelector = useProjectSelector()
Expand Down
5 changes: 3 additions & 2 deletions app/forms/network-interface-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useForm } from 'react-hook-form'
import type { SetNonNullable, SetRequired } from 'type-fest'

import { useApiQuery, type ApiError, type InstanceNetworkInterfaceCreate } from '@oxide/api'
import { apiq, type ApiError, type InstanceNetworkInterfaceCreate } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { ListboxField } from '~/components/form/fields/ListboxField'
Expand Down Expand Up @@ -47,7 +48,7 @@ export function CreateNetworkInterfaceForm({
}: CreateNetworkInterfaceFormProps) {
const projectSelector = useProjectSelector()

const { data: vpcsData } = useApiQuery('vpcList', { query: projectSelector })
const { data: vpcsData } = useQuery(apiq('vpcList', { query: projectSelector }))
const vpcs = useMemo(() => vpcsData?.items || [], [vpcsData])

const form = useForm({ defaultValues })
Expand Down
Loading
Loading