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
2 changes: 1 addition & 1 deletion OMICRON_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a9d9f1e1a17736aecc8d644c8d8c6174c1a5bc14
c52ed36e07d70b6f968c177b27f606d9a91ca279
6 changes: 6 additions & 0 deletions app/api/__generated__/Api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion app/api/__generated__/OMICRON_VERSION

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion app/api/__generated__/validate.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 0 additions & 34 deletions app/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,40 +305,6 @@ export const wrapQueryClient = <A extends ApiClient>(api: A, queryClient: QueryC
queryFn: () => api[method](params).then(handleResult(method)),
...options,
}),
/**
* Loader analog to `useApiQueryErrorsAllowed`. Prefetch a query that can
* error, converting the error to a valid result so RQ will cache it.
*/
prefetchQueryErrorsAllowed: <M extends string & keyof A>(
method: M,
params: Params<A[M]>,
options: FetchQueryOtherOptions<ErrorsAllowed<Result<A[M]>, ApiError>> & {
/**
* HTTP errors will show up unexplained in the browser console. It can be
* helpful to reassure people they're normal.
*/
explanation: string
expectedStatusCode: 403 | 404
}
) =>
queryClient.prefetchQuery({
queryKey: [method, params, ERRORS_ALLOWED],
queryFn: () =>
api[method](params)
.then(handleResult(method))
.then((data) => ({ type: 'success' as const, data }))
.catch((data: ApiError) => {
// if we get an unexpected error, we're still throwing
if (data.statusCode !== options.expectedStatusCode) {
// data is the result of handleResult, so it's ready to through
// directly without further processing
throw data
}
console.info(options.explanation)
return { type: 'error' as const, data }
}),
...options,
}),
})

/*
Expand Down
8 changes: 4 additions & 4 deletions app/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { intersperse } from '~/util/array'
import { pb } from '~/util/path-builder'

export function TopBar({ systemOrSilo }: { systemOrSilo: 'system' | 'silo' }) {
const { isFleetViewer } = useCurrentUser()
const { me } = useCurrentUser()
// The height of this component is governed by the `PageContainer`
// It's important that this component returns two distinct elements (wrapped in a fragment).
// Each element will occupy one of the top column slots provided by `PageContainer`.
Expand All @@ -42,7 +42,7 @@ export function TopBar({ systemOrSilo }: { systemOrSilo: 'system' | 'silo' }) {
<Breadcrumbs />
</div>
<div className="flex items-center gap-2">
{isFleetViewer && <SiloSystemPicker level={systemOrSilo} />}
{me.fleetViewer && <SiloSystemPicker level={systemOrSilo} />}
<UserMenu />
</div>
</div>
Expand Down Expand Up @@ -155,8 +155,8 @@ function UserMenu() {

/**
* Choose between System and Silo-scoped route trees, or if the user doesn't
* have access to system routes (i.e., if systemPolicyView 403s) show the
* current silo.
* have access to system routes (i.e., if /v1/me has fleetViewer: false) show
* the current silo.
*/
function SiloSystemPicker({ level }: { level: 'silo' | 'system' }) {
return (
Expand Down
23 changes: 4 additions & 19 deletions app/hooks/use-current-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
* Copyright Oxide Computer Company
*/

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

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

/**
* Access all the data fetched by the loader. Because of the `shouldRevalidate`
Expand All @@ -18,19 +15,7 @@ import { invariant } from '~/util/invariant'
* loaders.
*/
export function useCurrentUser() {
const { data: me } = usePrefetchedApiQuery('currentUserView', {})
const { data: myGroups } = usePrefetchedApiQuery('currentUserGroups', {})

// User can only get to system routes if they have viewer perms (at least) on
// the fleet. The natural place to find out whether they have such perms is
// 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 } = 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')
const isFleetViewer = systemPolicy.type === 'success'

return { me, myGroups, isFleetViewer }
const { data: me } = usePrefetchedQuery(apiq('currentUserView', {}))
const { data: myGroups } = usePrefetchedQuery(apiq('currentUserGroups', {}))
return { me, myGroups }
}
6 changes: 3 additions & 3 deletions app/hooks/use-quick-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function closeQuickActions() {
function useGlobalActions() {
const location = useLocation()
const navigate = useNavigate()
const { isFleetViewer } = useCurrentUser()
const { me } = useCurrentUser()

return useMemo(() => {
const actions = []
Expand All @@ -69,15 +69,15 @@ function useGlobalActions() {
onSelect: () => navigate(pb.profile()),
})
}
if (isFleetViewer && !location.pathname.startsWith('/system/')) {
if (me.fleetViewer && !location.pathname.startsWith('/system/')) {
actions.push({
navGroup: 'System',
value: 'Manage system',
onSelect: () => navigate(pb.silos()),
})
}
return actions
}, [location.pathname, navigate, isFleetViewer])
}, [location.pathname, navigate, me.fleetViewer])
}

/**
Expand Down
12 changes: 0 additions & 12 deletions app/layouts/AuthenticatedLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,6 @@ export async function clientLoader() {
await Promise.all([
apiQueryClient.prefetchQuery('currentUserView', {}, { staleTime }),
apiQueryClient.prefetchQuery('currentUserGroups', {}, { staleTime }),
// Need to prefetch this because every layout hits it when deciding whether
// to show the silo/system picker. It's also fetched by the SystemLayout
// loader to figure out whether to 404, but RQ dedupes the request.
apiQueryClient.prefetchQueryErrorsAllowed(
'systemPolicyView',
{},
{
explanation: '/v1/system/policy 403 is expected if user is not a fleet viewer.',
expectedStatusCode: 403,
staleTime,
}
),
])
return null
}
Expand Down
20 changes: 5 additions & 15 deletions app/layouts/SystemLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,23 +27,13 @@ import { inventoryBase, pb } from '~/util/path-builder'
import { ContentPane, PageContainer } from './helpers'

/**
* If we can see the policy, we're a fleet viewer, and we need to be a fleet
* viewer in order to see any of the routes under this layout. We need to
* `fetchQuery` instead of `prefetchQuery` because the latter doesn't return the
* result, and then we need to `.catch()` because `fetchQuery` throws on request
* error. We're being a little cavalier here with the error. If it's something
* other than a 403, that would be strange and we would want to know.
* We need to be a fleet viewer in order to see any of the routes under this
* layout. We need to `fetchQuery` instead of `prefetchQuery` because the latter
* doesn't return the result.
*/
export async function clientLoader() {
// we don't need to use the ErrorsAllowed version here because we're 404ing
// immediately on error, so we don't need to pick the result up from the cache
const isFleetViewer = await apiQueryClient
.fetchQuery('systemPolicyView', {})
.then(() => true)
.catch(() => false)

if (!isFleetViewer) throw trigger404

const me = await apiQueryClient.fetchQuery('currentUserView', {})
if (!me.fleetViewer) throw trigger404
return null
}

Expand Down
9 changes: 8 additions & 1 deletion mock-api/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
requireRole,
unavailableErr,
updateDesc,
userHasRole,
} from './util'

// Note the *JSON types. Those represent actual API request and response bodies,
Expand Down Expand Up @@ -1428,7 +1429,13 @@ export const handlers = makeHandlers({
return paginated(query, db.racks)
},
currentUserView({ cookies }) {
return { ...currentUser(cookies), silo_name: defaultSilo.name }
const user = currentUser(cookies)
return {
...user,
silo_name: defaultSilo.name,
fleet_viewer: userHasRole(user, 'fleet', FLEET_ID, 'viewer'),
silo_admin: userHasRole(user, 'silo', defaultSilo.id, 'admin'),
}
},
currentUserGroups({ cookies }) {
const user = currentUser(cookies)
Expand Down
Loading