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
20 changes: 18 additions & 2 deletions app/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { type ApiError } from './errors'
import {
ensurePrefetched,
getApiQueryOptions,
getApiQueryOptionsErrorsAllowed,
getListQueryOptionsFn,
getUseApiMutation,
getUseApiQuery,
getUseApiQueryErrorsAllowed,
getUsePrefetchedApiQuery,
wrapQueryClient,
} from './hooks'
Expand All @@ -33,6 +33,23 @@ export type ApiMethods = typeof api.methods

/** API-specific query options helper. */
export const apiq = getApiQueryOptions(api.methods)
/**
* Variant of `apiq` that allows error responses as a valid result,
* which importantly means they can be cached by RQ. This means we can prefetch
* an endpoint that might error (see `prefetchQueryErrorsAllowed`) and use this
* hook to retrieve the error result.
*
* Concretely, the difference from the usual query function is that we turn all
* errors into successes. Instead of throwing the error, we return it as a valid
* result. This means `data` has a type that includes the possibility of error,
* plus a discriminant to let us handle both sides properly in the calling code.
*
* We also use a special query key to distinguish these from normal API queries.
* If we hit a given endpoint twice on the same page, once the normal way and
* once with errors allowed, the responses have different shapes, so we do not
* want to share the cache and mix them up.
*/
export const apiqErrorsAllowed = getApiQueryOptionsErrorsAllowed(api.methods)
/**
* Query options helper that only supports list endpoints. Returns
* a function `(limit, pageToken) => QueryOptions` for use with
Expand All @@ -47,7 +64,6 @@ export const useApiQuery = getUseApiQuery(api.methods)
* test loading the page to exercise the invariant in CI.
*/
export const usePrefetchedApiQuery = getUsePrefetchedApiQuery(api.methods)
export const useApiQueryErrorsAllowed = getUseApiQueryErrorsAllowed(api.methods)
export const useApiMutation = getUseApiMutation(api.methods)

export const usePrefetchedQuery = <TData>(options: UseQueryOptions<TData, ApiError>) =>
Expand Down
20 changes: 4 additions & 16 deletions app/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,27 +229,16 @@ const ERRORS_ALLOWED = 'errors-allowed'
/** Result that includes both success and error so it can be cached by RQ */
type ErrorsAllowed<T, E> = { type: 'success'; data: T } | { type: 'error'; data: E }

/**
* Variant of `getUseApiQuery` that allows error responses as a valid result,
* which importantly means they can be cached by RQ. This means we can prefetch
* an endpoint that might error (see `prefetchQueryErrorsAllowed`) and use this
* hook to retrieve the error result.
*
* Concretely, the only difference from `getUseApiQuery`: we turn all errors
* into successes. Instead of throwing the error, we return it as a valid
* result. This means `data` has a type that includes the possibility of error,
* plus a discriminant to let us handle both sides properly in the calling code.
*/
export const getUseApiQueryErrorsAllowed =
export const getApiQueryOptionsErrorsAllowed =
<A extends ApiClient>(api: A) =>
<M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: UseQueryOtherOptions<ErrorsAllowed<Result<A[M]>, ApiError>> = {}
) => {
return useQuery({
) =>
queryOptions({
// extra bit of key is important to distinguish from normal query. if we
// hit a a given endpoint twice on the same page, once the normal way and
// hit a given endpoint twice on the same page, once the normal way and
// once with errors allowed the responses have different shapes, so we do
// not want to share the cache and mix them up
queryKey: [method, params, ERRORS_ALLOWED],
Expand All @@ -260,7 +249,6 @@ export const getUseApiQueryErrorsAllowed =
.catch((data) => ({ type: 'error' as const, data })),
...options,
})
}

export const getUseApiMutation =
<A extends ApiClient>(api: A) =>
Expand Down
6 changes: 4 additions & 2 deletions app/hooks/use-current-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* Copyright Oxide Computer Company
*/

import { useApiQueryErrorsAllowed, usePrefetchedApiQuery } from '~/api/client'
import { useQuery } from '@tanstack/react-query'

import { apiqErrorsAllowed, usePrefetchedApiQuery } from '~/api/client'
import { invariant } from '~/util/invariant'

/**
Expand All @@ -24,7 +26,7 @@ export function useCurrentUser() {
// the fleet (system) policy, but if the user doesn't have fleet read, we'll
// get a 403 from that endpoint. So we simply check whether that endpoint 200s
// or not to determine whether the user is a fleet viewer.
const { data: systemPolicy } = useApiQueryErrorsAllowed('systemPolicyView', {})
const { data: systemPolicy } = useQuery(apiqErrorsAllowed('systemPolicyView', {}))
// don't use usePrefetchedApiQuery because it's not worth making an errors
// allowed version of that
invariant(systemPolicy, 'System policy must be prefetched')
Expand Down
5 changes: 3 additions & 2 deletions app/pages/project/snapshots/SnapshotsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
*
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
import { useCallback } from 'react'
import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router'

import {
apiqErrorsAllowed,
apiQueryClient,
getListQFn,
queryClient,
useApiMutation,
useApiQueryClient,
useApiQueryErrorsAllowed,
type Snapshot,
} from '@oxide/api'
import { Snapshots16Icon, Snapshots24Icon } from '@oxide/design-system/icons/react'
Expand All @@ -38,7 +39,7 @@ import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'

const DiskNameFromId = ({ value }: { value: string }) => {
const { data } = useApiQueryErrorsAllowed('diskView', { path: { disk: value } })
const { data } = useQuery(apiqErrorsAllowed('diskView', { path: { disk: value } }))

if (!data) return <SkeletonCell />
if (data.type === 'error') return <Badge color="neutral">Deleted</Badge>
Expand Down
10 changes: 6 additions & 4 deletions app/table/cells/IpPoolCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
*
* Copyright Oxide Computer Company
*/
import { useApiQueryErrorsAllowed } from '~/api'
import { useQuery } from '@tanstack/react-query'

import { apiqErrorsAllowed } from '~/api'
import { Tooltip } from '~/ui/lib/Tooltip'

import { EmptyCell, SkeletonCell } from './EmptyCell'

export const IpPoolCell = ({ ipPoolId }: { ipPoolId: string }) => {
const { data: result } = useApiQueryErrorsAllowed('projectIpPoolView', {
path: { pool: ipPoolId },
})
const { data: result } = useQuery(
apiqErrorsAllowed('projectIpPoolView', { path: { pool: ipPoolId } })
)
if (!result) return <SkeletonCell />
// this should essentially never happen, but it's probably better than blowing
// up the whole page if the pool is not found
Expand Down
Loading