-
Notifications
You must be signed in to change notification settings - Fork 16
IP pool utilization #2078
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
IP pool utilization #2078
Changes from all commits
16f6e2b
253c2d5
001662c
bc547f4
bcdcea6
9f64b6d
13d5616
11f38d3
42582a3
ba6ec57
059aa5f
d345796
541be68
c6797f1
87417eb
43c4ab3
5429a44
7a76bb4
b38dc41
b774c89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ) | ||
| } | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
|
|
@@ -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 | ||
|
|
@@ -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> | ||
|
|
@@ -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> | ||
| ) | ||
| } | ||
|
|
||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } }) | ||
|
|
@@ -101,6 +148,7 @@ function IpRangesTable() { | |
| const removeRange = useApiMutation('ipPoolRangeRemove', { | ||
| onSuccess() { | ||
| queryClient.invalidateQueries('ipPoolRangeList') | ||
| queryClient.invalidateQueries('ipPoolUtilizationView') | ||
| }, | ||
| }) | ||
| const emptyState = ( | ||
|
|
||
| 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> | ||
| } |
There was a problem hiding this comment.
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 | bigintwhile enforcing that they are the same type