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
53 changes: 53 additions & 0 deletions app/pages/system/silos/SiloFleetRolesTab.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TableEmptyBox>
<EmptyMessage
icon={<Cloud24Icon />}
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."
/>
</TableEmptyBox>
)
}

return (
<>
<p className="mb-4 text-default">Silo roles can automatically grant a fleet role.</p>
<ul className="space-y-3">
{roleMapPairs.map(([siloRole, fleetRole]) => (
<li key={siloRole + '|' + fleetRole} className="flex items-center">
<Badge>Silo {siloRole}</Badge>
<NextArrow12Icon className="mx-3 text-default" aria-label="maps to" />
<span className="text-sans-md text-default">Fleet {fleetRole}</span>
</li>
))}
</ul>
</>
)
}
14 changes: 10 additions & 4 deletions app/pages/system/silos/SiloIdpsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -30,7 +30,13 @@ const colHelper = createColumnHelper<IdentityProvider>()
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(
Expand Down
19 changes: 16 additions & 3 deletions app/pages/system/silos/SiloIpPoolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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()
Expand Down
79 changes: 12 additions & 67 deletions app/pages/system/silos/SiloPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,21 @@
*/
import { type LoaderFunctionArgs } from 'react-router'

import { apiQueryClient, queryClient, usePrefetchedApiQuery } from '@oxide/api'
import { Cloud16Icon, Cloud24Icon, NextArrow12Icon } from '@oxide/design-system/icons/react'
import { Cloud16Icon, Cloud24Icon } from '@oxide/design-system/icons/react'

import { apiq, queryClient, usePrefetchedQuery } from '~/api'
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 { siloIdpList, SiloIdpsTab } from './SiloIdpsTab'
import { siloIpPoolsQuery, SiloIpPoolsTab } from './SiloIpPoolsTab'
import { SiloQuotasTab } from './SiloQuotasTab'
import { pb } from '~/util/path-builder'

export async function clientLoader({ params }: 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()),
])
await queryClient.prefetchQuery(apiq('siloView', { path: { silo } }))
return null
}

Expand All @@ -42,12 +30,7 @@ export const handle = makeCrumb((p) => p.silo!)
export default function SiloPage() {
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])
)
const { data: silo } = usePrefetchedQuery(apiq('siloView', { path: siloSelector }))

return (
<>
Expand All @@ -73,50 +56,12 @@ export default function SiloPage() {
<PropertiesTable.DateRow date={silo.timeModified} label="Last Modified" />
</PropertiesTable>

<QueryParamTabs className="full-width" defaultValue="idps">
<Tabs.List>
<Tabs.Trigger value="idps">Identity Providers</Tabs.Trigger>
<Tabs.Trigger value="ip-pools">IP Pools</Tabs.Trigger>
<Tabs.Trigger value="quotas">Quotas</Tabs.Trigger>
<Tabs.Trigger value="fleet-roles">Fleet roles</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="idps">
<SiloIdpsTab />
</Tabs.Content>
<Tabs.Content value="ip-pools">
<SiloIpPoolsTab />
</Tabs.Content>
<Tabs.Content value="quotas">
<SiloQuotasTab />
</Tabs.Content>
<Tabs.Content value="fleet-roles">
{/* TODO: better empty state explaining that no roles are mapped so nothing will happen */}
{roleMapPairs.length === 0 ? (
<TableEmptyBox>
<EmptyMessage
icon={<Cloud24Icon />}
title="Mapped fleet roles"
body="Silo roles can automatically grant a fleet role. This silo has no role mappings configured."
/>
</TableEmptyBox>
) : (
<>
<p className="mb-4 text-default">
Silo roles can automatically grant a fleet role.
</p>
<ul className="space-y-3">
{roleMapPairs.map(([siloRole, fleetRole]) => (
<li key={siloRole + '|' + fleetRole} className="flex items-center">
<Badge>Silo {siloRole}</Badge>
<NextArrow12Icon className="mx-3 text-default" aria-label="maps to" />
<span className="text-sans-md text-default">Fleet {fleetRole}</span>
</li>
))}
</ul>
</>
)}
</Tabs.Content>
</QueryParamTabs>
<RouteTabs fullWidth>
<Tab to={pb.siloIdps(siloSelector)}>Identity Providers</Tab>
<Tab to={pb.siloIpPools(siloSelector)}>IP Pools</Tab>
<Tab to={pb.siloQuotas(siloSelector)}>Quotas</Tab>
<Tab to={pb.siloFleetRoles(siloSelector)}>Fleet roles</Tab>
</RouteTabs>
</>
)
}
32 changes: 21 additions & 11 deletions app/pages/system/silos/SiloQuotasTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@

import { useState } from 'react'
import { useForm } from 'react-hook-form'
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'
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'
Expand All @@ -29,11 +31,17 @@ 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 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

Expand Down Expand Up @@ -91,9 +99,11 @@ export 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
Expand All @@ -107,7 +117,7 @@ function EditQuotasForm({ onDismiss }: { onDismiss: () => void }) {

const updateQuotas = useApiMutation('siloQuotasUpdate', {
onSuccess() {
apiQueryClient.invalidateQueries('siloUtilizationView')
queryClient.invalidateEndpoint('siloUtilizationView')
addToast({ content: 'Quotas updated' })
onDismiss()
},
Expand Down
25 changes: 21 additions & 4 deletions app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,30 @@ 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*/}
<Route index element={<Navigate to="idps" replace />} />
<Route lazy={() => import('./pages/system/silos/SiloIdpsTab').then(convert)}>
<Route path="idps" element={null} />
<Route
path="idps-new"
lazy={() => import('./forms/idp/create').then(convert)}
/>
<Route
path="idps/saml/:provider"
lazy={() => import('./forms/idp/edit').then(convert)}
/>
</Route>
<Route
path="ip-pools"
lazy={() => import('./pages/system/silos/SiloIpPoolsTab').then(convert)}
/>
<Route
path="idps-new"
lazy={() => import('./forms/idp/create').then(convert)}
path="quotas"
lazy={() => import('./pages/system/silos/SiloQuotasTab').then(convert)}
/>
<Route
path="idps/saml/:provider"
lazy={() => import('./forms/idp/edit').then(convert)}
path="fleet-roles"
lazy={() => import('./pages/system/silos/SiloFleetRolesTab').then(convert)}
/>
</Route>
</Route>
Expand Down
34 changes: 32 additions & 2 deletions app/util/__snapshots__/path-builder.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
Loading
Loading