Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
16f6e2b
gen API and stub ip pool utilization endpoint
david-crespo Mar 18, 2024
253c2d5
we're having fun here
david-crespo Mar 18, 2024
001662c
tooltips on abbreviated numbers
david-crespo Mar 19, 2024
bc547f4
lower case eng notation e
david-crespo Mar 19, 2024
bcdcea6
don't bother displaying IPv6 utilization for now
david-crespo Mar 20, 2024
9f64b6d
rather elaborately get actual IP range lengths and counts
david-crespo Mar 20, 2024
13d5616
put back v6 range display on the off chance there is one + for testing
david-crespo Mar 20, 2024
11f38d3
Merge main into ip-pools-util
david-crespo Mar 20, 2024
42582a3
draft: utilization on ip pool detail
david-crespo Mar 20, 2024
ba6ec57
Merge main into ip-pools-util
david-crespo Mar 20, 2024
059aa5f
fix e2e by invalidating ipPoolUtilizationView on add/remove range
david-crespo Mar 20, 2024
d345796
capacity bar, fix ip addresses in mock data
david-crespo Mar 20, 2024
541be68
show capacity bar on ip pool detail
david-crespo Mar 20, 2024
c6797f1
use parse bigint helper in the other spot
david-crespo Mar 20, 2024
87417eb
extract and test number/bignum percentage logic
david-crespo Mar 21, 2024
43c4ab3
use BigNum (adds tooltip) in CapacityBar instead of displayBigNum
david-crespo Mar 21, 2024
5429a44
splitDecimal doesn't need to handle bigint anymore, fix de-DE comma
david-crespo Mar 21, 2024
7a76bb4
make ip-pool-1 bigger and pull floating IP addrs from it
david-crespo Mar 21, 2024
b38dc41
e2e test that deleting floating IP decrements utilization. caught a bug!
david-crespo Mar 21, 2024
b774c89
fix failing e2e due to changed address on floating IP
david-crespo Mar 21, 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
13 changes: 13 additions & 0 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
DiskState,
Instance,
InstanceState,
IpPoolUtilization,
Measurement,
SiloUtilization,
Sled,
Expand Down Expand Up @@ -210,3 +211,15 @@ export function synthesizeData(

return result
}

// do this by hand instead of getting elaborate in the client generator.
// see https://github.com/oxidecomputer/oxide.ts/pull/231
export function parseIpUtilization({ ipv4, ipv6 }: IpPoolUtilization) {
return {
ipv4,
ipv6: {
allocated: BigInt(ipv6.allocated),
capacity: BigInt(ipv6.capacity),
},
}
}
42 changes: 21 additions & 21 deletions app/components/CapacityBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,30 @@
* Copyright Oxide Computer Company
*/

import { splitDecimal } from '~/util/math'
import { BigNum } from '~/ui/lib/BigNum'
import { percentage, splitDecimal } from '~/util/math'

export const CapacityBar = ({
export const CapacityBar = <T extends number | bigint>({
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

generic is so we can let provisioned and allocated be number | bigint while enforcing that they are the same type

icon,
title,
unit,
provisioned,
allocated,
allocatedLabel,
capacity,
capacityLabel,
provisionedLabel = 'Provisioned',
includeUnit = true,
}: {
icon: JSX.Element
title: 'CPU' | 'Memory' | 'Storage'
unit: 'nCPUs' | 'GiB' | 'TiB'
provisioned: number
allocated: number
allocatedLabel: string
title: string
unit: string
provisioned: T
capacity: T
provisionedLabel?: string
capacityLabel: string
includeUnit?: boolean
}) => {
const percentOfAllocatedUsed = (provisioned / allocated) * 100

const [wholeNumber, decimal] = splitDecimal(percentOfAllocatedUsed)

const formattedPercentUsed = `${percentOfAllocatedUsed}%`
const pct = percentage(provisioned, capacity)
const [wholeNumber, decimal] = splitDecimal(pct)

return (
<div className="w-full min-w-min rounded-lg border border-default">
Expand All @@ -39,7 +39,7 @@ export const CapacityBar = ({
{icon}
</div>
<div className="flex flex-grow items-start">
<span className="text-mono-sm text-secondary">{title}</span>
<span className="!normal-case text-mono-sm text-secondary">{title}</span>
<span className="ml-1 !normal-case text-mono-sm text-quaternary">({unit})</span>
</div>
<div className="flex -translate-y-0.5 items-baseline">
Expand All @@ -52,24 +52,24 @@ export const CapacityBar = ({
<div className="flex w-full gap-0.5">
<div
className="h-3 rounded-l border bg-accent-secondary border-accent-secondary"
style={{ width: formattedPercentUsed }}
style={{ width: `${pct.toFixed(2)}%` }}
></div>
<div className="h-3 grow rounded-r border bg-info-secondary border-info-secondary"></div>
</div>
</div>
<div>
<div className="flex justify-between border-t border-secondary">
<div className="p-3 text-mono-sm">
<div className="text-quaternary">Provisioned</div>
<div className="text-quaternary">{provisionedLabel}</div>
<div className="text-secondary">
{provisioned.toLocaleString()}
<BigNum num={provisioned} />
<span className="normal-case">{includeUnit ? ' ' + unit : ''}</span>
</div>
</div>
<div className="p-3 text-mono-sm">
<div className="text-quaternary">{allocatedLabel}</div>
<div className="text-secondary">
{allocated.toLocaleString()}
<div className="text-quaternary">{capacityLabel}</div>
<div className="!normal-case text-secondary">
<BigNum num={capacity} />
<span className="normal-case">{includeUnit ? ' ' + unit : ''}</span>
</div>
</div>
Expand Down
16 changes: 8 additions & 8 deletions app/components/CapacityBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,25 @@ export const CapacityBars = ({
title="CPU"
unit="nCPUs"
provisioned={provisioned.cpus}
allocated={allocated.cpus}
capacity={allocated.cpus}
includeUnit={false}
allocatedLabel={allocatedLabel}
capacityLabel={allocatedLabel}
/>
<CapacityBar
icon={<Ram16Icon />}
title="Memory"
title="MEMORY"
unit="GiB"
provisioned={bytesToGiB(provisioned.memory)}
allocated={bytesToGiB(allocated.memory)}
allocatedLabel={allocatedLabel}
capacity={bytesToGiB(allocated.memory)}
capacityLabel={allocatedLabel}
/>
<CapacityBar
icon={<Ssd16Icon />}
title="Storage"
title="STORAGE"
unit="TiB"
provisioned={bytesToTiB(provisioned.storage)}
allocated={bytesToTiB(allocated.storage)}
allocatedLabel={allocatedLabel}
capacity={bytesToTiB(allocated.storage)}
capacityLabel={allocatedLabel}
/>
</div>
)
Expand Down
50 changes: 50 additions & 0 deletions app/components/IpPoolUtilization.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { parseIpUtilization, type IpPoolUtilization } from '~/api'
import { Badge } from '~/ui/lib/Badge'
import { BigNum } from '~/ui/lib/BigNum'

const IpUtilFrac = (props: { allocated: number | bigint; capacity: number | bigint }) => (
<>
<BigNum className="text-default" num={props.allocated} /> /{' '}
<BigNum className="text-tertiary" num={props.capacity} />
</>
)

export function IpUtilCell(util: IpPoolUtilization) {
const { ipv4, ipv6 } = parseIpUtilization(util)

if (ipv6.capacity === 0n) {
return (
<div className="space-y-1">
<IpUtilFrac {...ipv4} />
</div>
)
}
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 should be what users see because they almost certainly have no IPv6 ranges.


// the API doesn't let you add IPv6 ranges, but there's a remote possibility
// a pool already exists with IPv6 ranges, so we might as well show that. also
// this is nice for e2e testing the utilization logic
return (
<div className="space-y-1">
<div>
<Badge color="neutral" className="mr-2 !normal-case">
v4
</Badge>
<IpUtilFrac {...ipv4} />
</div>
<div>
<Badge color="neutral" className="mr-2 !normal-case">
v6
</Badge>
<IpUtilFrac {...ipv6} />
</div>
</div>
)
}
1 change: 1 addition & 0 deletions app/forms/floating-ip-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function CreateFloatingIpSideModalForm() {
const createFloatingIp = useApiMutation('floatingIpCreate', {
onSuccess() {
queryClient.invalidateQueries('floatingIpList')
queryClient.invalidateQueries('ipPoolUtilizationView')
addToast({ content: 'Your Floating IP has been created' })
navigate(pb.floatingIps(projectSelector))
},
Expand Down
1 change: 1 addition & 0 deletions app/forms/ip-pool-range-add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export function IpPoolAddRangeSideModalForm() {
onSuccess(_range) {
// refetch list of projects in sidebar
queryClient.invalidateQueries('ipPoolRangeList')
queryClient.invalidateQueries('ipPoolUtilizationView')
addToast({ content: 'IP range added' })
onDismiss()
},
Expand Down
1 change: 1 addition & 0 deletions app/pages/project/floating-ips/FloatingIpsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export function FloatingIpsPage() {
const deleteFloatingIp = useApiMutation('floatingIpDelete', {
onSuccess() {
queryClient.invalidateQueries('floatingIpList')
queryClient.invalidateQueries('ipPoolUtilizationView')
addToast({ content: 'Your floating IP has been deleted' })
},
})
Expand Down
50 changes: 49 additions & 1 deletion app/pages/system/networking/IpPoolPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,21 @@ import { Link, Outlet, type LoaderFunctionArgs } from 'react-router-dom'

import {
apiQueryClient,
parseIpUtilization,
useApiMutation,
useApiQuery,
useApiQueryClient,
usePrefetchedApiQuery,
type IpPoolRange,
type IpPoolSiloLink,
} from '@oxide/api'
import { Networking24Icon, Success12Icon } from '@oxide/design-system/icons/react'
import {
IpGlobal16Icon,
Networking24Icon,
Success12Icon,
} from '@oxide/design-system/icons/react'

import { CapacityBar } from '~/components/CapacityBar'
import { ExternalLink } from '~/components/ExternalLink'
import { ListboxField } from '~/components/form/fields/ListboxField'
import { HL } from '~/components/HL'
Expand Down Expand Up @@ -55,6 +61,9 @@ IpPoolPage.loader = async function ({ params }: LoaderFunctionArgs) {
path: { pool },
query: { limit: 25 }, // match QueryTable
}),
apiQueryClient.prefetchQuery('ipPoolUtilizationView', {
path: { pool },
}),

// fetch silos and preload into RQ cache so fetches by ID in SiloNameFromId
// can be mostly instant yet gracefully fall back to fetching individually
Expand All @@ -76,6 +85,7 @@ export function IpPoolPage() {
<PageHeader>
<PageTitle icon={<Networking24Icon />}>{pool.name}</PageTitle>
</PageHeader>
<UtilizationBars />
<QueryParamTabs className="full-width" defaultValue="ranges">
<Tabs.List>
<Tabs.Trigger value="ranges">IP ranges</Tabs.Trigger>
Expand All @@ -93,6 +103,43 @@ export function IpPoolPage() {
)
}

function UtilizationBars() {
const { pool } = useIpPoolSelector()
const { data } = usePrefetchedApiQuery('ipPoolUtilizationView', { path: { pool } })
const { ipv4, ipv6 } = parseIpUtilization(data)

if (ipv4.capacity === 0 && ipv6.capacity === 0n) return null

return (
<div className="-mt-8 mb-8 flex min-w-min flex-col gap-3 lg+:flex-row">
{ipv4.capacity > 0 && (
<CapacityBar
icon={<IpGlobal16Icon />}
title="IPv4"
provisioned={ipv4.allocated}
capacity={ipv4.capacity}
provisionedLabel="Allocated"
capacityLabel="Capacity"
unit="IPs"
includeUnit={false}
/>
)}
{ipv6.capacity > 0 && (
<CapacityBar
icon={<IpGlobal16Icon />}
title="IPv6"
provisioned={ipv6.allocated}
capacity={ipv6.capacity}
provisionedLabel="Allocated"
capacityLabel="Capacity"
unit="IPs"
includeUnit={false}
/>
)}
</div>
)
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

took a slightly different approach here than the possibly too clever "if there is IPv6, show both, otherwise just show IPv4 with no v4 label" in the table cell. Here we just show whichever ones are present, including possibly both or none.

function IpRangesTable() {
const { pool } = useIpPoolSelector()
const { Table, Column } = useQueryTable('ipPoolRangeList', { path: { pool } })
Expand All @@ -101,6 +148,7 @@ function IpRangesTable() {
const removeRange = useApiMutation('ipPoolRangeRemove', {
onSuccess() {
queryClient.invalidateQueries('ipPoolRangeList')
queryClient.invalidateQueries('ipPoolUtilizationView')
},
})
const emptyState = (
Expand Down
16 changes: 16 additions & 0 deletions app/pages/system/networking/IpPoolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ import { Link, Outlet, useNavigate } from 'react-router-dom'
import {
apiQueryClient,
useApiMutation,
useApiQuery,
usePrefetchedApiQuery,
type IpPool,
} from '@oxide/api'
import { Networking24Icon } from '@oxide/design-system/icons/react'

import { IpUtilCell } from '~/components/IpPoolUtilization'
import { useQuickActions } from '~/hooks'
import { confirmDelete } from '~/stores/confirm-delete'
import { DateCell } from '~/table/cells/DateCell'
import { SkeletonCell } from '~/table/cells/EmptyCell'
import { linkCell } from '~/table/cells/LinkCell'
import type { MenuAction } from '~/table/columns/action-col'
import { useQueryTable } from '~/table/QueryTable'
Expand All @@ -37,6 +40,13 @@ const EmptyState = () => (
/>
)

function UtilizationCell({ pool }: { pool: string }) {
const { data } = useApiQuery('ipPoolUtilizationView', { path: { pool } })

if (!data) return <SkeletonCell />
return <IpUtilCell {...data} />
}

IpPoolsTab.loader = async function () {
await apiQueryClient.prefetchQuery('ipPoolList', { query: { limit: 25 } })
return null
Expand Down Expand Up @@ -99,6 +109,12 @@ export function IpPoolsTab() {
<Table emptyState={<EmptyState />} makeActions={makeActions}>
<Column accessor="name" cell={linkCell((pool) => pb.ipPool({ pool }))} />
<Column accessor="description" />
<Column
accessor="name"
id="Utilization"
header="Utilization"
cell={({ value }) => <UtilizationCell pool={value} />}
/>
<Column accessor="timeCreated" header="Created" cell={DateCell} />
</Table>
<Outlet />
Expand Down
25 changes: 25 additions & 0 deletions app/ui/lib/BigNum.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 { displayBigNum } from '~/util/math'

import { Tooltip } from './Tooltip'

/**
* Possibly abbreviate number if it's big enough, and if it is, wrap it in a
* tooltip showing the unabbreviated value.
*/
export function BigNum({ num, className }: { num: number | bigint; className?: string }) {
const [display, abbreviated] = displayBigNum(num)

const inner = <span className={className}>{display}</span>

if (!abbreviated) return inner

return <Tooltip content={num.toLocaleString()}>{inner}</Tooltip>
}
Loading