Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. TypeScript 7 is extremely fast, so confirm errors that come from the language server by running `npm run tsc`
- This repo uses oxfmt and oxlint, not prettier or eslint
- 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
Expand Down Expand Up @@ -48,6 +49,7 @@
# Mutations & UI flow

- Wrap writes in `useApiMutation`, use `confirmAction` to guard destructive intent, and surface results with `addToast`.
- When a form's `onSuccess` always navigates away, pass `loading={mutation.isPending || mutation.isSuccess}` to the form shell. `isPending` alone flips false before the navigation unmounts the modal, so the button's spinner animates back out right before close. Skip `isSuccess` if the form can stay open and be reused after success, or if the mutation lives in a component that survives the modal (e.g., a tab page with `{open && <Modal/>}`) — there success closes the modal synchronously so `isPending` alone is glitch-free, and a sticky `isSuccess` would strand a spinner on next open.
- Mutation error display depends on context. In forms, errors display inline via `submitError={mutation.error}` — do not add `onError` with a toast to the `useApiMutation` call. In `confirmAction`/`confirmDelete` flows, the confirm modal catches the error and shows a toast using `errorTitle` — do not also add `onError` on the mutation, or the user will see two toasts. For standalone actions (fire-and-forget `mutate` calls not wrapped in a confirm modal or form), use `onError` on the mutation to show an error toast.
- Keep page scaffolding consistent: `PageHeader`, `PageTitle`, `DocsPopover`, `RefreshButton`, `PropertiesTable`, and `CardBlock` provide the expected layout for new system pages.
- When a page should be discoverable from the command palette, extend `useQuickActions` with the new entry so it appears in the quick actions menu (see `app/pages/ProjectsPage.tsx:100-115`).
Expand Down
2 changes: 1 addition & 1 deletion app/components/AttachEphemeralIpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const AttachEphemeralIpModal = ({
submitLabel="Attach"
submitDisabled={submitDisabled}
submitError={instanceEphemeralIpAttach.error}
loading={instanceEphemeralIpAttach.isPending}
loading={instanceEphemeralIpAttach.isPending || instanceEphemeralIpAttach.isSuccess}
onSubmit={({ pool }) => {
instanceEphemeralIpAttach.mutate({
path: { instance },
Expand Down
2 changes: 1 addition & 1 deletion app/components/AttachFloatingIpModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export const AttachFloatingIpModal = ({
onDismiss={onDismiss}
submitLabel="Attach floating IP"
submitError={floatingIpAttach.error}
loading={floatingIpAttach.isPending}
loading={floatingIpAttach.isPending || floatingIpAttach.isSuccess}
title="Attach floating IP"
onSubmit={() =>
floatingIpAttach.mutate({
Expand Down
2 changes: 1 addition & 1 deletion app/forms/anti-affinity-group-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function CreateAntiAffinityGroupForm() {
body: { ...values, failureDomain: 'sled' },
})
}
loading={createAntiAffinityGroup.isPending}
loading={createAntiAffinityGroup.isPending || createAntiAffinityGroup.isSuccess}
submitError={createAntiAffinityGroup.error}
submitLabel="Add group"
>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/anti-affinity-group-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function EditAntiAffintyGroupForm() {
body: values,
})
}}
loading={editAntiAffinityGroup.isPending}
loading={editAntiAffinityGroup.isPending || editAntiAffinityGroup.isSuccess}
submitError={editAntiAffinityGroup.error}
submitLabel="Edit group"
>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export function CreateDiskSideModalForm({
createDisk.mutate({ query: { project }, body })
}
}}
loading={createDisk.isPending}
loading={createDisk.isPending || createDisk.isSuccess}
submitError={createDisk.error}
>
<NameField
Expand Down
2 changes: 1 addition & 1 deletion app/forms/external-subnet-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export default function CreateExternalSubnetSideModalForm() {
body: { name, description, allocator },
})
}}
loading={createExternalSubnet.isPending}
loading={createExternalSubnet.isPending || createExternalSubnet.isSuccess}
submitError={createExternalSubnet.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/external-subnet-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function EditExternalSubnetSideModalForm() {
body: { name, description },
})
}}
loading={editExternalSubnet.isPending}
loading={editExternalSubnet.isPending || editExternalSubnet.isSuccess}
submitError={editExternalSubnet.error}
>
<FormMetadata resource={subnet}>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/firewall-rules-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export default function CreateFirewallRuleForm() {
},
})
}}
loading={updateRules.isPending}
loading={updateRules.isPending || updateRules.isSuccess}
submitError={updateRules.error}
submitLabel="Add rule"
>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/firewall-rules-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default function EditFirewallRuleForm() {
}
// validationSchema={validationSchema}
// validateOnBlur
loading={updateRules.isPending}
loading={updateRules.isPending || updateRules.isSuccess}
submitError={updateRules.error}
>
<FormMetadata resource={originalRule} />
Expand Down
4 changes: 2 additions & 2 deletions app/forms/fleet-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function FleetAccessAddUserSideModal({
body: updateRole({ identityId, identityType, roleName }, policy),
})
}}
loading={updatePolicy.isPending}
loading={updatePolicy.isPending || updatePolicy.isSuccess}
submitError={updatePolicy.error}
>
<ListboxField
Expand Down Expand Up @@ -114,7 +114,7 @@ export function FleetAccessEditUserSideModal({
body: updateRole({ identityId, identityType, roleName }, policy),
})
}}
loading={updatePolicy.isPending}
loading={updatePolicy.isPending || updatePolicy.isSuccess}
submitError={updatePolicy.error}
onDismiss={() => {
updatePolicy.reset() // clear API error state so it doesn't persist on next open
Expand Down
2 changes: 1 addition & 1 deletion app/forms/floating-ip-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default function CreateFloatingIpSideModalForm() {
}
createFloatingIp.mutate({ query: projectSelector, body })
}}
loading={createFloatingIp.isPending}
loading={createFloatingIp.isPending || createFloatingIp.isSuccess}
submitError={createFloatingIp.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/floating-ip-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export default function EditFloatingIpSideModalForm() {
body: { name, description },
})
}}
loading={editFloatingIp.isPending}
loading={editFloatingIp.isPending || editFloatingIp.isSuccess}
submitError={editFloatingIp.error}
>
<FormMetadata resource={floatingIp}>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/idp/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export default function CreateIdpSideModalForm() {
},
})
}}
loading={createIdp.isPending}
loading={createIdp.isPending || createIdp.isSuccess}
submitError={createIdp.error}
submitLabel="Create provider"
>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/image-from-snapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default function CreateImageFromSnapshotSideModalForm() {
})
}
submitError={createImage.error}
loading={createImage.isPending}
loading={createImage.isPending || createImage.isSuccess}
>
<PropertiesTable>
<PropertiesTable.Row label="Snapshot">{data.name}</PropertiesTable.Row>
Expand Down
6 changes: 4 additions & 2 deletions app/forms/instance-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ export default function CreateInstanceForm() {
},
})
}}
loading={createInstance.isPending}
loading={createInstance.isPending || createInstance.isSuccess}
submitError={createInstance.error}
>
<NameField name="name" control={control} disabled={isSubmitting} />
Expand Down Expand Up @@ -854,7 +854,9 @@ export default function CreateInstanceForm() {
disabled={isSubmitting}
/>
<Form.Actions>
<Form.Submit loading={createInstance.isPending}>Create instance</Form.Submit>
<Form.Submit loading={createInstance.isPending || createInstance.isSuccess}>
Create instance
</Form.Submit>
<Form.Cancel onClick={() => navigate(pb.instances({ project }))} />
</Form.Actions>
</FullPageForm>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/ip-pool-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export default function CreateIpPoolSideModalForm() {
onSubmit={({ name, description, ipVersion, poolType }) => {
createPool.mutate({ body: { name, description, ipVersion, poolType } })
}}
loading={createPool.isPending}
loading={createPool.isPending || createPool.isSuccess}
submitError={createPool.error}
>
<IpPoolVisibilityMessage />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/ip-pool-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default function EditIpPoolSideModalForm() {
onSubmit={({ name, description }) => {
editPool.mutate({ path: poolSelector, body: { name, description } })
}}
loading={editPool.isPending}
loading={editPool.isPending || editPool.isSuccess}
submitError={editPool.error}
>
<FormMetadata resource={pool} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/ip-pool-range-add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default function IpPoolAddRange() {
title="Add IP range"
onDismiss={onDismiss}
onSubmit={(body) => addRange.mutate({ path: { pool }, body })}
loading={addRange.isPending}
loading={addRange.isPending || addRange.isSuccess}
submitError={addRange.error}
>
<Message
Expand Down
2 changes: 1 addition & 1 deletion app/forms/network-interface-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function EditNetworkInterfaceForm({
body,
})
}}
loading={editNetworkInterface.isPending}
loading={editNetworkInterface.isPending || editNetworkInterface.isSuccess}
submitError={editNetworkInterface.error}
>
<FormMetadata resource={editing}>
Expand Down
4 changes: 2 additions & 2 deletions app/forms/project-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa
body: updateRole({ identityId, identityType, roleName }, policy),
})
}}
loading={updatePolicy.isPending}
loading={updatePolicy.isPending || updatePolicy.isSuccess}
submitError={updatePolicy.error}
onDismiss={onDismiss}
>
Expand Down Expand Up @@ -118,7 +118,7 @@ export function ProjectAccessEditUserSideModal({
body: updateRole({ identityId, identityType, roleName }, policy),
})
}}
loading={updatePolicy.isPending}
loading={updatePolicy.isPending || updatePolicy.isSuccess}
submitError={updatePolicy.error}
onDismiss={onDismiss}
>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/project-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function ProjectCreateSideModalForm() {
onSubmit={({ name, description }) => {
createProject.mutate({ body: { name, description } })
}}
loading={createProject.isPending}
loading={createProject.isPending || createProject.isSuccess}
submitError={createProject.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/project-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function EditProjectSideModalForm() {
onSubmit={({ name, description }) => {
editProject.mutate({ path: projectSelector, body: { name, description } })
}}
loading={editProject.isPending}
loading={editProject.isPending || editProject.isSuccess}
submitError={editProject.error}
>
<FormMetadata resource={project} />
Expand Down
4 changes: 2 additions & 2 deletions app/forms/silo-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr
body: updateRole({ identityId, identityType, roleName }, policy),
})
}}
loading={updatePolicy.isPending}
loading={updatePolicy.isPending || updatePolicy.isSuccess}
submitError={updatePolicy.error}
>
<ListboxField
Expand Down Expand Up @@ -136,7 +136,7 @@ export function SiloAccessEditUserSideModal({
}
updatePolicy.mutate({ body })
}}
loading={updatePolicy.isPending}
loading={updatePolicy.isPending || updatePolicy.isSuccess}
submitError={updatePolicy.error}
onDismiss={() => {
updatePolicy.reset() // clear API error state so it doesn't persist on next open
Expand Down
2 changes: 1 addition & 1 deletion app/forms/silo-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function CreateSiloSideModalForm() {
},
})
}}
loading={createSilo.isPending}
loading={createSilo.isPending || createSilo.isSuccess}
submitError={createSilo.error}
>
<Message variant="info" content={<HelpMessage />} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/snapshot-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default function SnapshotCreate() {
createSnapshot.mutate({ query: projectSelector, body: values })
}}
submitError={createSnapshot.error}
loading={createSnapshot.isPending}
loading={createSnapshot.isPending || createSnapshot.isSuccess}
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/ssh-key-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function SSHKeyCreate({ onDismiss, onSuccess, message }: Props) {
title="Add SSH key"
onDismiss={handleDismiss}
onSubmit={(body) => createSshKey.mutate({ body })}
loading={createSshKey.isPending}
loading={createSshKey.isPending || createSshKey.isSuccess}
submitError={createSshKey.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/subnet-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function CreateSubnetForm() {
},
})
}
loading={createSubnet.isPending}
loading={createSubnet.isPending || createSubnet.isSuccess}
submitError={createSubnet.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/subnet-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export default function EditSubnetForm() {
},
})
}}
loading={updateSubnet.isPending}
loading={updateSubnet.isPending || updateSubnet.isSuccess}
submitError={updateSubnet.error}
>
<FormMetadata resource={subnet}>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/subnet-pool-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function CreateSubnetPoolSideModalForm() {
onSubmit={({ name, description, ipVersion }) => {
createPool.mutate({ body: { name, description, ipVersion } })
}}
loading={createPool.isPending}
loading={createPool.isPending || createPool.isSuccess}
submitError={createPool.error}
>
<SubnetPoolVisibilityMessage />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/subnet-pool-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default function EditSubnetPoolSideModalForm() {
body: { name, description },
})
}}
loading={editPool.isPending}
loading={editPool.isPending || editPool.isSuccess}
submitError={editPool.error}
>
<FormMetadata resource={pool} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/subnet-pool-member-add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default function SubnetPoolMemberAdd() {
},
})
}}
loading={addMember.isPending}
loading={addMember.isPending || addMember.isSuccess}
submitError={addMember.error}
>
<Message
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function CreateVpcSideModalForm() {
})
}
onDismiss={() => navigate(pb.vpcs(projectSelector))}
loading={createVpc.isPending}
loading={createVpc.isPending || createVpc.isSuccess}
submitError={createVpc.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function EditVpcSideModalForm() {
body: { name, description, dnsName },
})
}}
loading={editVpc.isPending}
loading={editVpc.isPending || editVpc.isSuccess}
submitError={editVpc.error}
>
<FormMetadata resource={vpc} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-router-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function RouterCreate() {
resourceName="router"
onDismiss={onDismiss}
onSubmit={(body) => createRouter.mutate({ query: vpcSelector, body })}
loading={createRouter.isPending}
loading={createRouter.isPending || createRouter.isSuccess}
submitError={createRouter.error}
>
<NameField name="name" control={form.control} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-router-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function EditRouterSideModalForm() {
body,
})
}
loading={editRouter.isPending}
loading={editRouter.isPending || editRouter.isSuccess}
submitError={editRouter.error}
>
<FormMetadata resource={routerData}>
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-router-route-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default function CreateRouterRouteSideModalForm() {
},
})
}
loading={createRouterRoute.isPending}
loading={createRouterRoute.isPending || createRouterRoute.isSuccess}
submitError={createRouterRoute.error}
>
<RouteFormFields form={form} />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/vpc-router-route-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function EditRouterRouteSideModalForm() {
},
})
}
loading={updateRouterRoute.isPending}
loading={updateRouterRoute.isPending || updateRouterRoute.isSuccess}
submitError={updateRouterRoute.error}
submitDisabled={disabled ? routeFormMessage.vpcSubnetNotModifiable : undefined}
>
Expand Down
Loading
Loading