Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
de66425
basic IP pools page and list silos for pool
david-crespo Jan 23, 2024
adef77b
Stubbing out tabs for silo IP pools
charliepark Jan 24, 2024
4bd7ff7
Stubbing out tabs for silo IP pools
charliepark Jan 24, 2024
5b88a8e
Switch to query param-based URL and tabs
charliepark Jan 24, 2024
7447f00
Copy change
charliepark Jan 24, 2024
d5a1e85
Cleanup on IP Pools page, but not there yet
charliepark Jan 24, 2024
4fb4acd
use the right API endpoint
david-crespo Jan 25, 2024
a0afdcf
Merge branch 'main' into ip-pools-ui
david-crespo Jan 25, 2024
159e627
make/clear default row action
david-crespo Jan 25, 2024
0c3c848
use silo page loader for API calls backing the tabs
david-crespo Jan 25, 2024
33c1527
tweak display
david-crespo Jan 25, 2024
5ff5d6c
move fleet role mapping into a tab
david-crespo Jan 25, 2024
c8b7a81
add properties table to silo detail
david-crespo Jan 25, 2024
f8b5968
unlink pool
david-crespo Jan 25, 2024
4380a91
make e2e tests pass, improve empty fleet role mapping state
david-crespo Jan 25, 2024
d791ca4
Merge main into ip-pools-ui
david-crespo Jan 30, 2024
d21d0ce
add IP ranges
david-crespo Jan 30, 2024
f870531
make /system/ip-pools the top-level route for now
david-crespo Jan 30, 2024
04cfc28
add apology for incomplete UI, will hopefully get far enough to remove
david-crespo Jan 30, 2024
e4f5b9a
create pool form
david-crespo Jan 30, 2024
71b9c81
delete IP pool
david-crespo Jan 30, 2024
0245432
ip pool edit form
david-crespo Jan 30, 2024
4fd02e9
update path-builder test
david-crespo Jan 30, 2024
8e49838
basic e2e tests
david-crespo Jan 30, 2024
e4b7bdc
Merge main into ip-pools-ui
david-crespo Jan 30, 2024
944d2a8
silo names on IP pools silo list, link to silo, help text
david-crespo Jan 31, 2024
d2df6fe
make *NameFromId rendering logic more uniform
david-crespo Jan 31, 2024
38844e8
Merge branch 'main' into ip-pools-ui
david-crespo Jan 31, 2024
afc72a6
change IP pools page back to Networking with one tab
david-crespo Jan 31, 2024
1421a81
confirm actions on silo IP pools list
david-crespo Jan 31, 2024
bef5353
add docs links, remove apology callout
david-crespo Jan 31, 2024
7bb3711
confirm unlink in ip pool silos list
david-crespo Jan 31, 2024
5ae447c
link silo from ip pool silos list
david-crespo Jan 31, 2024
9b8654a
link pool from silo pools list
david-crespo Jan 31, 2024
153142e
error toast and loading states on link modals
david-crespo Jan 31, 2024
bbae349
fix e2e test with confirm
david-crespo Jan 31, 2024
ce722af
Merge branch 'main' into ip-pools-ui
david-crespo Jan 31, 2024
1d4989d
fix already exists check on mock ip pool update handler
david-crespo Jan 31, 2024
b9c6eda
add disabled range add/remove buttons to suggest using API instead
david-crespo Jan 31, 2024
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
27 changes: 27 additions & 0 deletions app/components/ExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 cn from 'classnames'

type ExternalLinkProps = {
href: string
className?: string
children: React.ReactNode
}

export function ExternalLink({ href, className, children }: ExternalLinkProps) {
return (
<a
href={href}
className={cn('underline text-accent-secondary hover:text-accent', className)}
target="_blank"
rel="noreferrer"
>
{children}
</a>
)
}
10 changes: 10 additions & 0 deletions app/components/HL.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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 { classed } from '@oxide/util'

export const HL = classed.span`text-sans-semi-md text-default`
23 changes: 22 additions & 1 deletion app/components/TopBarPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
Wrap,
} from '@oxide/ui'

import { useInstanceSelector, useSiloSelector } from 'app/hooks'
import { useInstanceSelector, useIpPoolSelector, useSiloSelector } from 'app/hooks'
import { useCurrentUser } from 'app/layouts/AuthenticatedLayout'
import { pb } from 'app/util/path-builder'

Expand Down Expand Up @@ -228,6 +228,27 @@ export function SiloPicker() {
)
}

/** Used when drilling down into a pool from the System/Networking view. */
export function IpPoolPicker() {
// picker only shows up when a pool is in scope
const { pool: poolName } = useIpPoolSelector()
const { data } = useApiQuery('ipPoolList', { query: { limit: 10 } })
const items = (data?.items || []).map((pool) => ({
label: pool.name,
to: pb.ipPool({ pool: pool.name }),
}))

return (
<TopBarPicker
aria-label="Switch pool"
category="IP Pools"
current={poolName}
items={items}
noItemsText="No IP pools found"
/>
)
}

const NoProjectLogo = () => (
<div className="flex h-[34px] w-[34px] items-center justify-center rounded text-secondary bg-secondary">
<Folder16Icon />
Expand Down
54 changes: 54 additions & 0 deletions app/forms/ip-pool-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 { useNavigate } from 'react-router-dom'

import { useApiMutation, useApiQueryClient, type IpPoolCreate } from '@oxide/api'

import { DescriptionField, NameField, SideModalForm } from 'app/components/form'
import { useForm } from 'app/hooks'
import { addToast } from 'app/stores/toast'
import { pb } from 'app/util/path-builder'

const defaultValues: IpPoolCreate = {
name: '',
description: '',
}

export function CreateIpPoolSideModalForm() {
const navigate = useNavigate()
const queryClient = useApiQueryClient()

const onDismiss = () => navigate(pb.ipPools())

const createPool = useApiMutation('ipPoolCreate', {
onSuccess(_pool) {
queryClient.invalidateQueries('ipPoolList')
addToast({ content: 'Your IP pool has been created' })
navigate(pb.ipPools())
},
})

const form = useForm({ defaultValues })

return (
<SideModalForm
id="create-pool-form"
form={form}
title="Create IP pool"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
createPool.mutate({ body: { name, description } })
}}
loading={createPool.isPending}
submitError={createPool.error}
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
</SideModalForm>
)
}
65 changes: 65 additions & 0 deletions app/forms/ip-pool-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
} from '@oxide/api'

import { DescriptionField, NameField, SideModalForm } from 'app/components/form'
import { getIpPoolSelector, useForm, useIpPoolSelector, useToast } from 'app/hooks'
import { pb } from 'app/util/path-builder'

EditIpPoolSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { pool } = getIpPoolSelector(params)
await apiQueryClient.prefetchQuery('ipPoolView', { path: { pool } })
return null
}

export function EditIpPoolSideModalForm() {
const queryClient = useApiQueryClient()
const addToast = useToast()
const navigate = useNavigate()

const poolSelector = useIpPoolSelector()

const onDismiss = () => navigate(pb.ipPools())

const { data: pool } = usePrefetchedApiQuery('ipPoolView', { path: poolSelector })

const editPool = useApiMutation('ipPoolUpdate', {
onSuccess(_pool) {
queryClient.invalidateQueries('ipPoolList')
addToast({ content: 'Your IP pool has been updated' })
onDismiss()
},
})

const form = useForm({ defaultValues: pool })

return (
<SideModalForm
id="edit-pool-form"
form={form}
title="Edit IP pool"
onDismiss={onDismiss}
onSubmit={({ name, description }) => {
editPool.mutate({ path: poolSelector, body: { name, description } })
}}
loading={editPool.isPending}
submitError={editPool.error}
submitLabel="Save changes"
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
</SideModalForm>
)
}
2 changes: 0 additions & 2 deletions app/forms/project-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export function CreateProjectSideModalForm() {
},
})

// TODO: RHF docs warn about the performance impact of validating on every
// change
const form = useForm({ defaultValues })

return (
Expand Down
2 changes: 2 additions & 0 deletions app/hooks/use-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const getProjectImageSelector = requireParams('project', 'image')
export const getProjectSnapshotSelector = requireParams('project', 'snapshot')
export const requireSledParams = requireParams('sledId')
export const requireUpdateParams = requireParams('version')
export const getIpPoolSelector = requireParams('pool')

/**
* Turn `getThingSelector`, a pure function on a params object, into a hook
Expand Down Expand Up @@ -79,3 +80,4 @@ export const useSiloImageSelector = () => useSelectedParams(getSiloImageSelector
export const useIdpSelector = () => useSelectedParams(getIdpSelector)
export const useSledParams = () => useSelectedParams(requireSledParams)
export const useUpdateParams = () => useSelectedParams(requireUpdateParams)
export const useIpPoolSelector = () => useSelectedParams(getIpPoolSelector)
24 changes: 13 additions & 11 deletions app/layouts/SystemLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ import { useMemo } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'

import { apiQueryClient } from '@oxide/api'
import { Cloud16Icon, Divider, Metrics16Icon, Storage16Icon } from '@oxide/ui'
import {
Cloud16Icon,
Divider,
Metrics16Icon,
Networking16Icon,
Storage16Icon,
} from '@oxide/ui'

import { trigger404 } from 'app/components/ErrorBoundary'
import { DocsLinkItem, NavLinkItem, Sidebar } from 'app/components/Sidebar'
import { TopBar } from 'app/components/TopBar'
import { SiloPicker, SiloSystemPicker } from 'app/components/TopBarPicker'
import { IpPoolPicker, SiloPicker, SiloSystemPicker } from 'app/components/TopBarPicker'
import { useQuickActions } from 'app/hooks'
import { pb } from 'app/util/path-builder'

Expand Down Expand Up @@ -49,7 +55,7 @@ export default function SystemLayout() {
// robust way of doing this would be to make a separate layout for the
// silo-specific routes in the route config, but it's overkill considering
// this is a one-liner. Switch to that approach at the first sign of trouble.
const { silo } = useParams()
const { silo, pool } = useParams()
const navigate = useNavigate()
const { pathname } = useLocation()

Expand All @@ -60,6 +66,7 @@ export default function SystemLayout() {
{ value: 'Silos', path: pb.silos() },
{ value: 'Utilization', path: pb.systemUtilization() },
{ value: 'Inventory', path: pb.inventory() },
{ value: 'Networking', path: pb.ipPools() },
]
// filter out the entry for the path we're currently on
.filter((i) => i.path !== pathname)
Expand All @@ -84,6 +91,7 @@ export default function SystemLayout() {
<TopBar>
<SiloSystemPicker value="system" />
{silo && <SiloPicker />}
{pool && <IpPoolPicker />}
</TopBar>
<Sidebar>
<Sidebar.Nav>
Expand All @@ -103,15 +111,9 @@ export default function SystemLayout() {
<NavLinkItem to={pb.sledInventory()}>
<Storage16Icon /> Inventory
</NavLinkItem>
{/* <NavLinkItem to={pb.systemHealth()} disabled>
<Health16Icon /> Health
</NavLinkItem>
<NavLinkItem to={pb.systemUpdates()} disabled>
<SoftwareUpdate16Icon /> System Update
</NavLinkItem>
<NavLinkItem to={pb.systemNetworking()} disabled>
<NavLinkItem to={pb.ipPools()}>
<Networking16Icon /> Networking
</NavLinkItem> */}
</NavLinkItem>
</Sidebar.Nav>
</Sidebar>
<ContentPane />
Expand Down
3 changes: 2 additions & 1 deletion app/pages/SiloAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ import {
import { groupBy, isTruthy } from '@oxide/util'

import { AccessNameCell } from 'app/components/AccessNameCell'
import { HL } from 'app/components/HL'
import { RoleBadgeCell } from 'app/components/RoleBadgeCell'
import {
SiloAccessAddUserSideModal,
SiloAccessEditUserSideModal,
} from 'app/forms/silo-access'
import { confirmDelete, HL } from 'app/stores/confirm-delete'
import { confirmDelete } from 'app/stores/confirm-delete'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
<TableEmptyBox>
Expand Down
3 changes: 2 additions & 1 deletion app/pages/project/access/ProjectAccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ import {
import { groupBy, isTruthy } from '@oxide/util'

import { AccessNameCell } from 'app/components/AccessNameCell'
import { HL } from 'app/components/HL'
import { RoleBadgeCell } from 'app/components/RoleBadgeCell'
import {
ProjectAccessAddUserSideModal,
ProjectAccessEditUserSideModal,
} from 'app/forms/project-access'
import { getProjectSelector, useProjectSelector } from 'app/hooks'
import { confirmDelete, HL } from 'app/stores/confirm-delete'
import { confirmDelete } from 'app/stores/confirm-delete'

const EmptyState = ({ onClick }: { onClick: () => void }) => (
<TableEmptyBox>
Expand Down
41 changes: 23 additions & 18 deletions app/pages/project/disks/DisksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ import {
useApiQueryClient,
type Disk,
} from '@oxide/api'
import { DateCell, linkCell, SizeCell, useQueryTable, type MenuAction } from '@oxide/table'
import {
DateCell,
LinkCell,
SizeCell,
SkeletonCell,
useQueryTable,
type MenuAction,
} from '@oxide/table'
import {
buttonStyle,
EmptyMessage,
Expand All @@ -33,22 +40,22 @@ import { pb } from 'app/util/path-builder'

import { fancifyStates } from '../instances/instance/tabs/common'

function AttachedInstance({
instanceId,
...projectSelector
}: {
project: string
instanceId: string
}) {
const { data: instance } = useApiQuery('instanceView', {
path: { instance: instanceId },
})

const instanceLinkCell = linkCell((instanceName) =>
pb.instancePage({ ...projectSelector, instance: instanceName })
function InstanceNameFromId({ value: instanceId }: { value: string | null }) {
const { project } = useProjectSelector()
const { data: instance } = useApiQuery(
'instanceView',
{ path: { instance: instanceId! } },
{ enabled: !!instanceId }
)

return instance ? instanceLinkCell({ value: instance.name }) : null
if (!instanceId) return null
if (!instance) return <SkeletonCell />

return (
<LinkCell to={pb.instancePage({ project, instance: instance.name })}>
{instance.name}
</LinkCell>
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is unrelated but I made all the NameForId components use LinkCell and have the nice Skeleton state

}

const EmptyState = () => (
Expand Down Expand Up @@ -143,9 +150,7 @@ export function DisksPage() {
// whether it has an instance field
'instance' in disk.state ? disk.state.instance : null
}
cell={({ value }: { value: string | undefined }) =>
value ? <AttachedInstance {...projectSelector} instanceId={value} /> : null
}
cell={InstanceNameFromId}
/>
<Column header="Size" accessor="size" cell={SizeCell} />
<Column
Expand Down
Loading