diff --git a/AGENTS.md b/AGENTS.md index 6681fc422..a7af733ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - Before starting a feature, skim an existing page or form with similar behavior and mirror the conventions—this codebase is intentionally conventional. Look for similar pages in `app/pages` and forms in `app/forms` to use as templates. - `@oxide/api` is at `app/api` and `@oxide/api-mocks` is at `mock-api/index.ts`. - The language server often has out of date errors. tsgo is extremely fast, so confirm errors that come from the language server by running `npm run tsc` +- Format with `npm run fmt` (oxfmt). Never use prettier directly—it is not the project formatter. - Use Node.js 22+, then install deps and start the mock-backed dev server (skip if `npm run dev` is already running in another terminal): ```sh diff --git a/app/forms/anti-affinity-group-create.tsx b/app/forms/anti-affinity-group-create.tsx index dfaaaa037..38eb34a6f 100644 --- a/app/forms/anti-affinity-group-create.tsx +++ b/app/forms/anti-affinity-group-create.tsx @@ -79,7 +79,11 @@ export default function CreateAntiAffinityGroupForm() { { value: 'fail', label: 'Fail' }, ]} /> - + ) } diff --git a/app/forms/anti-affinity-group-edit.tsx b/app/forms/anti-affinity-group-edit.tsx index d238827f8..1c1c43b25 100644 --- a/app/forms/anti-affinity-group-edit.tsx +++ b/app/forms/anti-affinity-group-edit.tsx @@ -83,7 +83,11 @@ export default function EditAntiAffintyGroupForm() { > - + ) } diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx index 2e8f182d4..9a799f491 100644 --- a/app/forms/disk-create.tsx +++ b/app/forms/disk-create.tsx @@ -222,7 +222,7 @@ export function CreateDiskSideModalForm({ images={images} areImagesLoading={areImagesLoading} /> - + ) } diff --git a/app/forms/external-subnet-create.tsx b/app/forms/external-subnet-create.tsx index bd7bd1006..34f299802 100644 --- a/app/forms/external-subnet-create.tsx +++ b/app/forms/external-subnet-create.tsx @@ -153,7 +153,11 @@ export default function CreateExternalSubnetSideModalForm() { description="The subnet to reserve, e.g., 10.128.1.0/24" /> )} - + ) } diff --git a/app/forms/external-subnet-edit.tsx b/app/forms/external-subnet-edit.tsx index 0efa2622c..6b2c635fb 100644 --- a/app/forms/external-subnet-edit.tsx +++ b/app/forms/external-subnet-edit.tsx @@ -114,7 +114,11 @@ export default function EditExternalSubnetSideModalForm() { - + ) } diff --git a/app/forms/fleet-access.tsx b/app/forms/fleet-access.tsx index 018097f55..4df4413ed 100644 --- a/app/forms/fleet-access.tsx +++ b/app/forms/fleet-access.tsx @@ -77,7 +77,11 @@ export function FleetAccessAddUserSideModal({ control={form.control} /> - + ) } @@ -122,7 +126,11 @@ export function FleetAccessEditUserSideModal({ }} > - + ) } diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index b36b218fe..18fb35d6d 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -108,7 +108,11 @@ export default function CreateFloatingIpSideModalForm() { placeholder="Select a pool" noItemsPlaceholder="No pools available" /> - + ) } diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index 960f2e71e..678f81975 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -115,7 +115,11 @@ export default function EditFloatingIpSideModalForm() { - + ) } diff --git a/app/forms/image-from-snapshot.tsx b/app/forms/image-from-snapshot.tsx index b6f7767e8..4fc20d96f 100644 --- a/app/forms/image-from-snapshot.tsx +++ b/app/forms/image-from-snapshot.tsx @@ -102,7 +102,11 @@ export default function CreateImageFromSnapshotSideModalForm() { - + ) } diff --git a/app/forms/image-upload.tsx b/app/forms/image-upload.tsx index bade80684..911cb4ea1 100644 --- a/app/forms/image-upload.tsx +++ b/app/forms/image-upload.tsx @@ -709,7 +709,11 @@ export default function ImageCreate() { /> )} - + ) } diff --git a/app/forms/ip-pool-create.tsx b/app/forms/ip-pool-create.tsx index 67bd8e9d3..500d852d9 100644 --- a/app/forms/ip-pool-create.tsx +++ b/app/forms/ip-pool-create.tsx @@ -89,7 +89,11 @@ export default function CreateIpPoolSideModalForm() { ]} /> */} - + ) } diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index 15c6b7505..a4d31bd77 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -76,7 +76,11 @@ export default function EditIpPoolSideModalForm() { - + ) } diff --git a/app/forms/ip-pool-range-add.tsx b/app/forms/ip-pool-range-add.tsx index ba3c740e2..a38f927fd 100644 --- a/app/forms/ip-pool-range-add.tsx +++ b/app/forms/ip-pool-range-add.tsx @@ -94,7 +94,11 @@ export default function IpPoolAddRange() { validate={(value) => validateAddress(value, poolData.ipVersion)} {...noPasswordManager} /> - + ) } diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 0694e5f29..e8aa4703c 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -180,7 +180,11 @@ export function CreateNetworkInterfaceForm({ placeholder="Leave blank for auto-assignment" /> )} - + ) } diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx index 8bd206ccd..f64af5983 100644 --- a/app/forms/network-interface-edit.tsx +++ b/app/forms/network-interface-edit.tsx @@ -178,7 +178,11 @@ export function EditNetworkInterfaceForm({ variant="info" content={`This network interface supports ${supportedVersions} transit IPs.`} /> - + ) } diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx index 15566bc56..7e1ea5033 100644 --- a/app/forms/project-access.tsx +++ b/app/forms/project-access.tsx @@ -76,7 +76,11 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa control={form.control} /> - + ) } @@ -123,7 +127,11 @@ export function ProjectAccessEditUserSideModal({ onDismiss={onDismiss} > - + ) } diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx index 46c0fec48..7a33c4182 100644 --- a/app/forms/project-create.tsx +++ b/app/forms/project-create.tsx @@ -61,7 +61,11 @@ export default function ProjectCreateSideModalForm() { > - + ) } diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 43144736a..634834b83 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -70,7 +70,11 @@ export default function EditProjectSideModalForm() { > - + ) } diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx index 6bc711230..c375f3f93 100644 --- a/app/forms/silo-access.tsx +++ b/app/forms/silo-access.tsx @@ -73,7 +73,11 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr control={form.control} /> - + ) } @@ -118,7 +122,11 @@ export function SiloAccessEditUserSideModal({ }} > - + ) } diff --git a/app/forms/silo-create.tsx b/app/forms/silo-create.tsx index de7e01223..05c694f9c 100644 --- a/app/forms/silo-create.tsx +++ b/app/forms/silo-create.tsx @@ -183,7 +183,11 @@ export default function CreateSiloSideModalForm() { - + ) } diff --git a/app/forms/snapshot-create.tsx b/app/forms/snapshot-create.tsx index 8079c59f3..ef7634ff0 100644 --- a/app/forms/snapshot-create.tsx +++ b/app/forms/snapshot-create.tsx @@ -90,7 +90,11 @@ export default function SnapshotCreate() { required control={form.control} /> - + ) } diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx index 01e05201c..0a0058ae7 100644 --- a/app/forms/ssh-key-create.tsx +++ b/app/forms/ssh-key-create.tsx @@ -76,7 +76,11 @@ export function SSHKeyCreate({ onDismiss, onSuccess, message }: Props) { control={form.control} /> {message} - + ) } diff --git a/app/forms/subnet-create.tsx b/app/forms/subnet-create.tsx index ddfe14501..de55d8b68 100644 --- a/app/forms/subnet-create.tsx +++ b/app/forms/subnet-create.tsx @@ -96,7 +96,11 @@ export default function CreateSubnetForm() { control={form.control} required /> - + ) } diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx index 8b5bcb61d..2f5035f5c 100644 --- a/app/forms/subnet-edit.tsx +++ b/app/forms/subnet-edit.tsx @@ -107,7 +107,11 @@ export default function EditSubnetForm() { control={form.control} required /> - + ) } diff --git a/app/forms/subnet-pool-create.tsx b/app/forms/subnet-pool-create.tsx index 9767a2fb0..cc80ab4f5 100644 --- a/app/forms/subnet-pool-create.tsx +++ b/app/forms/subnet-pool-create.tsx @@ -71,7 +71,11 @@ export default function CreateSubnetPoolSideModalForm() { { value: 'v6', label: 'v6' }, ]} /> - + ) } diff --git a/app/forms/subnet-pool-edit.tsx b/app/forms/subnet-pool-edit.tsx index 21049c539..56624876e 100644 --- a/app/forms/subnet-pool-edit.tsx +++ b/app/forms/subnet-pool-edit.tsx @@ -75,7 +75,11 @@ export default function EditSubnetPoolSideModalForm() { - + ) } diff --git a/app/forms/subnet-pool-member-add.tsx b/app/forms/subnet-pool-member-add.tsx index b1709fe41..de989141a 100644 --- a/app/forms/subnet-pool-member-add.tsx +++ b/app/forms/subnet-pool-member-add.tsx @@ -177,7 +177,11 @@ export default function SubnetPoolMemberAdd() { } deps="minPrefixLength" /> - + ) } diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx index 8a22867ba..1692a2e22 100644 --- a/app/forms/vpc-create.tsx +++ b/app/forms/vpc-create.tsx @@ -65,7 +65,7 @@ export default function CreateVpcSideModalForm() { - + ) } diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx index a2e5c60e1..991539937 100644 --- a/app/forms/vpc-edit.tsx +++ b/app/forms/vpc-edit.tsx @@ -78,7 +78,7 @@ export default function EditVpcSideModalForm() { - + ) } diff --git a/app/forms/vpc-router-create.tsx b/app/forms/vpc-router-create.tsx index 2578df6b0..cbcdfb6b9 100644 --- a/app/forms/vpc-router-create.tsx +++ b/app/forms/vpc-router-create.tsx @@ -57,7 +57,11 @@ export default function RouterCreate() { > - + ) } diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx index 4feccb194..a8ac48eb3 100644 --- a/app/forms/vpc-router-edit.tsx +++ b/app/forms/vpc-router-edit.tsx @@ -80,7 +80,11 @@ export default function EditRouterSideModalForm() { > - + ) } diff --git a/app/forms/vpc-router-route-common.tsx b/app/forms/vpc-router-route-common.tsx index 4043f5f49..85375c2fa 100644 --- a/app/forms/vpc-router-route-common.tsx +++ b/app/forms/vpc-router-route-common.tsx @@ -222,6 +222,10 @@ export const RouteFormFields = ({ form, disabled }: RouteFormFieldsProps) => { ) } -export const RouteFormDocs = () => ( - +export const RouteFormDocs = ({ apiOp, cliCmd }: { apiOp: string; cliCmd: string }) => ( + ) diff --git a/app/forms/vpc-router-route-create.tsx b/app/forms/vpc-router-route-create.tsx index 764497144..a78414f51 100644 --- a/app/forms/vpc-router-route-create.tsx +++ b/app/forms/vpc-router-route-create.tsx @@ -83,7 +83,7 @@ export default function CreateRouterRouteSideModalForm() { submitError={createRouterRoute.error} > - + ) } diff --git a/app/forms/vpc-router-route-edit.tsx b/app/forms/vpc-router-route-edit.tsx index f0d2dbd24..6fcd1ca2e 100644 --- a/app/forms/vpc-router-route-edit.tsx +++ b/app/forms/vpc-router-route-edit.tsx @@ -100,7 +100,7 @@ export default function EditRouterRouteSideModalForm() { submitDisabled={disabled ? routeFormMessage.vpcSubnetNotModifiable : undefined} > - + ) } diff --git a/app/ui/lib/ModalLinks.tsx b/app/ui/lib/ModalLinks.tsx index d75794347..44e9e5867 100644 --- a/app/ui/lib/ModalLinks.tsx +++ b/app/ui/lib/ModalLinks.tsx @@ -11,6 +11,8 @@ import { OpenLink12Icon } from '@oxide/design-system/icons/react' import { FormDivider } from '~/ui/lib/Divider' +const DOC_BASE = 'https://docs.oxide.computer' + export const ModalLinks = ({ heading, children, @@ -41,13 +43,59 @@ export const ModalLink = ({ to, label }: { to: string; label: string }) => ( type DocLink = { href: string; linkText: string } -export const SideModalFormDocs = ({ docs }: { docs: DocLink[] }) => ( +/** + * `apiOp` is a snake_case operation ID, e.g., "project_create". + * `cliCmd` is a slash-delimited CLI path, e.g., "project/create". + */ +export const SideModalFormDocs = ({ + docs, + apiOp, + cliCmd, +}: { + docs: DocLink[] + apiOp?: string + cliCmd?: string +}) => ( <> {docs.map(({ href, linkText }) => ( ))} + {apiOp && ( +
  • + + + + API:{' '} + {apiOp} + + +
  • + )} + {cliCmd && ( +
  • + + + + CLI:{' '} + + oxide {cliCmd.replaceAll('/', ' ')} + + + +
  • + )}
    )