diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx
index c44efe44dc..8ec8c6716d 100644
--- a/app/pages/project/floating-ips/FloatingIpsPage.tsx
+++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx
@@ -7,7 +7,7 @@
*/
import { useState } from 'react'
import { useForm } from 'react-hook-form'
-import { Link, Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
+import { Outlet, useNavigate, type LoaderFunctionArgs } from 'react-router-dom'
import {
apiQueryClient,
@@ -19,6 +19,7 @@ import {
} from '@oxide/api'
import { IpGlobal24Icon, Networking24Icon } from '@oxide/design-system/icons/react'
+import { ExternalLink } from '~/components/ExternalLink'
import { HL } from '~/components/HL'
import { getProjectSelector, useProjectSelector } from '~/hooks'
import { confirmAction } from '~/stores/confirm-action'
@@ -27,13 +28,13 @@ import { addToast } from '~/stores/toast'
import { InstanceLinkCell } from '~/table/cells/InstanceLinkCell'
import type { MenuAction } from '~/table/columns/action-col'
import { useQueryTable } from '~/table/QueryTable'
-import { buttonStyle } from '~/ui/lib/Button'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { Listbox } from '~/ui/lib/Listbox'
import { Message } from '~/ui/lib/Message'
import { Modal } from '~/ui/lib/Modal'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
-import { TableActions } from '~/ui/lib/Table'
+import { TableControls, TableControlsLink, TableControlsText } from '~/ui/lib/Table'
+import { links } from '~/util/links'
import { pb } from '~/util/path-builder'
const EmptyState = () => (
@@ -161,11 +162,16 @@ export function FloatingIpsPage() {
}>Floating IPs
-
-
+
+
+ Floating IPs are public IP addresses that can be attached to instances. They allow
+ your instances to be reachable from the internet. Find out more about{' '}
+ managing floating IPs.
+
+
New Floating IP
-
-
+
+
} makeActions={makeActions}>
diff --git a/app/pages/system/networking/IpPoolPage.tsx b/app/pages/system/networking/IpPoolPage.tsx
index b64b777dcd..292769dd3d 100644
--- a/app/pages/system/networking/IpPoolPage.tsx
+++ b/app/pages/system/networking/IpPoolPage.tsx
@@ -33,11 +33,12 @@ import { LinkCell } from '~/table/cells/LinkCell'
import type { MenuAction } from '~/table/columns/action-col'
import { useQueryTable } from '~/table/QueryTable'
import { Badge } from '~/ui/lib/Badge'
-import { Button, buttonStyle } from '~/ui/lib/Button'
+import { buttonStyle } from '~/ui/lib/Button'
import { EmptyMessage } from '~/ui/lib/EmptyMessage'
import { Message } from '~/ui/lib/Message'
import { Modal } from '~/ui/lib/Modal'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
+import { TableControls, TableControlsButton, TableControlsText } from '~/ui/lib/Table'
import { Tabs } from '~/ui/lib/Tabs'
import { links } from '~/util/links'
import { pb } from '~/util/path-builder'
@@ -216,18 +217,18 @@ function LinkedSilosTable() {
return (
<>
-
-
+
+
Users in linked silos can allocate external IPs from this pool for their
instances. A silo can have at most one default pool. IPs are allocated from the
default pool when users ask for one without specifying a pool. Read the docs to
learn more about{' '}
managing IP pools.
-
-
-
+
+
-
-
+
+
Users in this silo can allocate external IPs from these pools for their instances.
A silo can have at most one default pool. IPs are allocated from the default pool
when users ask for one without specifying a pool. Read the docs to learn more
about managing IP pools.
-
-
-
+
+
} makeActions={makeActions}>
pb.ipPool({ pool }))} />
diff --git a/app/ui/lib/Button.tsx b/app/ui/lib/Button.tsx
index 6c7c627e4c..ae972219b0 100644
--- a/app/ui/lib/Button.tsx
+++ b/app/ui/lib/Button.tsx
@@ -35,7 +35,7 @@ export const buttonStyle = ({
variant = 'primary',
}: ButtonStyleProps = {}) => {
return cn(
- 'ox-button elevation-1 rounded inline-flex items-center justify-center align-top disabled:cursor-not-allowed',
+ 'ox-button elevation-1 rounded inline-flex items-center justify-center align-top disabled:cursor-not-allowed shrink-0',
`btn-${variant}`,
sizeStyle[size],
variant === 'danger'
diff --git a/app/ui/lib/Table.tsx b/app/ui/lib/Table.tsx
index 4f3014943d..08c88239e5 100644
--- a/app/ui/lib/Table.tsx
+++ b/app/ui/lib/Table.tsx
@@ -7,9 +7,11 @@
*/
import cn from 'classnames'
import React, { useRef, type ReactElement } from 'react'
+import { Link, type LinkProps } from 'react-router-dom'
import SimpleBar from 'simplebar-react'
import { useIsOverflow } from '~/hooks'
+import { Button, buttonStyle, type ButtonProps } from '~/ui/lib/Button'
import { classed } from '~/util/classed'
export type TableProps = JSX.IntrinsicElements['table']
@@ -124,3 +126,17 @@ Table.Cell = ({ height = 'large', className, children, ...props }: TableCellProp
export const TableActions = classed.div`-mt-11 mb-3 flex justify-end space-x-2`
export const TableEmptyBox = classed.div`flex h-full max-h-[480px] items-center justify-center rounded-lg border border-secondary p-4`
+
+/**
+ * Used _outside_ of the `Table`, this element includes a soon-to-be-removed description of the resource inside the table,
+ * along with a link to more info, and a button to take action on the resource listed in the table.
+ */
+export const TableControls = classed.div`mb-4 flex items-end justify-between space-x-8`
+export const TableControlsText = classed.p`max-w-2xl text-sans-md text-secondary`
+
+export const TableControlsButton = (props: ButtonProps) => (
+
+)
+export const TableControlsLink = (props: LinkProps) => (
+
+)
diff --git a/app/util/links.ts b/app/util/links.ts
index 4418c90a62..a729ba8977 100644
--- a/app/util/links.ts
+++ b/app/util/links.ts
@@ -8,5 +8,6 @@
export const links: Record = {
cloudInitFormat: 'https://cloudinit.readthedocs.io/en/latest/explanation/format.html',
cloudInitExamples: 'https://cloudinit.readthedocs.io/en/latest/reference/examples.html',
+ floatingIpsDocs: 'https://docs.oxide.computer/guides/managing-floating-ips',
ipPoolsDocs: 'https://docs.oxide.computer/guides/operator/ip-pool-management',
}