diff --git a/app/components/form/SideModalForm.tsx b/app/components/form/SideModalForm.tsx index c11a9ebd73..e066f1bd3f 100644 --- a/app/components/form/SideModalForm.tsx +++ b/app/components/form/SideModalForm.tsx @@ -5,7 +5,7 @@ * * Copyright Oxide Computer Company */ -import { useEffect, type ReactNode } from 'react' +import { useEffect, useId, type ReactNode } from 'react' import type { FieldValues, UseFormReturn } from 'react-hook-form' import { useNavigationType } from 'react-router-dom' @@ -14,8 +14,19 @@ import type { ApiError } from '@oxide/api' import { Button } from '~/ui/lib/Button' import { SideModal } from '~/ui/lib/SideModal' +type CreateFormProps = { + formType: 'create' + /** Only needed if you need to override the default button text (`Create ${resourceName}`) */ + submitLabel?: string +} + +type EditFormProps = { + formType: 'edit' + /** Not permitted, as all edit form buttons should read `Update ${resourceName}` */ + submitLabel?: never +} + type SideModalFormProps = { - id: string form: UseFormReturn /** * A function that returns the fields. @@ -27,16 +38,17 @@ type SideModalFormProps = { */ children: ReactNode onDismiss: () => void + resourceName: string /** Must be provided with a reason describing why it's disabled */ submitDisabled?: string /** Error from the API call */ submitError: ApiError | null loading?: boolean - title: string + /** Only needed if you need to override the default title (Create/Edit ${resourceName}) */ + title?: string subtitle?: ReactNode onSubmit?: (values: TFieldValues) => void - submitLabel?: string -} +} & (CreateFormProps | EditFormProps) /** * Only animate the modal in when we're navigating by a client-side click. @@ -49,10 +61,11 @@ export function useShouldAnimateModal() { } export function SideModalForm({ - id, form, + formType, children, onDismiss, + resourceName, submitDisabled, submitError, title, @@ -61,6 +74,7 @@ export function SideModalForm({ loading, subtitle, }: SideModalFormProps) { + const id = useId() const { isSubmitting } = form.formState useEffect(() => { @@ -70,11 +84,16 @@ export function SideModalForm({ } }, [submitError, form]) + const label = + formType === 'edit' + ? `Update ${resourceName}` + : submitLabel || title || `Create ${resourceName}` + return ( ({ loading={loading || isSubmitting} form={id} > - {submitLabel || title} + {label} )} diff --git a/app/forms/access-util.tsx b/app/forms/access-util.tsx index 58087231c1..62af6c9e0c 100644 --- a/app/forms/access-util.tsx +++ b/app/forms/access-util.tsx @@ -50,6 +50,7 @@ export type AddRoleModalProps = { } export type EditRoleModalProps = AddRoleModalProps & { + name?: string identityId: string identityType: IdentityType defaultValues: { roleName: RoleKey } diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx index 60ec1f417c..94f75e86ef 100644 --- a/app/forms/disk-attach.tsx +++ b/app/forms/disk-attach.tsx @@ -46,9 +46,10 @@ export function AttachDiskSideModalForm({ return ( onDismiss(navigate)} onSubmit={({ size, ...rest }) => { const body = { size: size * GiB, ...rest } diff --git a/app/forms/firewall-rules-create.tsx b/app/forms/firewall-rules-create.tsx index be4ec6dc00..43afafdf1e 100644 --- a/app/forms/firewall-rules-create.tsx +++ b/app/forms/firewall-rules-create.tsx @@ -504,9 +504,10 @@ export function CreateFirewallRuleForm({ return ( { // TODO: this silently overwrites existing rules with the current name. diff --git a/app/forms/firewall-rules-edit.tsx b/app/forms/firewall-rules-edit.tsx index 5fe192f074..e23428bdc2 100644 --- a/app/forms/firewall-rules-edit.tsx +++ b/app/forms/firewall-rules-edit.tsx @@ -65,9 +65,9 @@ export function EditFirewallRuleForm({ return ( { // note different filter logic from create: filter out the rule with the @@ -86,7 +86,6 @@ export function EditFirewallRuleForm({ // validateOnBlur loading={updateRules.isPending} submitError={updateRules.error} - submitLabel="Update rule" > diff --git a/app/forms/floating-ip-create.tsx b/app/forms/floating-ip-create.tsx index 4b138a5941..0ad8e21545 100644 --- a/app/forms/floating-ip-create.tsx +++ b/app/forms/floating-ip-create.tsx @@ -83,9 +83,9 @@ export function CreateFloatingIpSideModalForm() { return ( navigate(pb.floatingIps(projectSelector))} onSubmit={({ ip, ...rest }) => { createFloatingIp.mutate({ diff --git a/app/forms/floating-ip-edit.tsx b/app/forms/floating-ip-edit.tsx index cab8e03019..94b568adfa 100644 --- a/app/forms/floating-ip-edit.tsx +++ b/app/forms/floating-ip-edit.tsx @@ -55,9 +55,9 @@ export function EditFloatingIpSideModalForm() { return ( { editFloatingIp.mutate({ @@ -68,7 +68,6 @@ export function EditFloatingIpSideModalForm() { }} loading={editFloatingIp.isPending} submitError={editFloatingIp.error} - submitLabel="Save changes" > diff --git a/app/forms/idp/create.tsx b/app/forms/idp/create.tsx index d8b185109d..8bed8ea2db 100644 --- a/app/forms/idp/create.tsx +++ b/app/forms/idp/create.tsx @@ -61,9 +61,9 @@ export function CreateIdpSideModalForm() { return ( navigate(dismissLink)} subtitle={ diff --git a/app/forms/image-from-snapshot.tsx b/app/forms/image-from-snapshot.tsx index da0c61eb8c..14126fc975 100644 --- a/app/forms/image-from-snapshot.tsx +++ b/app/forms/image-from-snapshot.tsx @@ -69,9 +69,10 @@ export function CreateImageFromSnapshotSideModalForm() { return ( diff --git a/app/forms/image-upload.tsx b/app/forms/image-upload.tsx index 1969114be4..e4f450bd28 100644 --- a/app/forms/image-upload.tsx +++ b/app/forms/image-upload.tsx @@ -471,8 +471,9 @@ export function CreateImageSideModalForm() { return ( { diff --git a/app/forms/ip-pool-create.tsx b/app/forms/ip-pool-create.tsx index af360cc8bc..f3d633fbc1 100644 --- a/app/forms/ip-pool-create.tsx +++ b/app/forms/ip-pool-create.tsx @@ -39,9 +39,9 @@ export function CreateIpPoolSideModalForm() { return ( { createPool.mutate({ body: { name, description } }) diff --git a/app/forms/ip-pool-edit.tsx b/app/forms/ip-pool-edit.tsx index 22407ad158..da2467ebf8 100644 --- a/app/forms/ip-pool-edit.tsx +++ b/app/forms/ip-pool-edit.tsx @@ -49,16 +49,15 @@ export function EditIpPoolSideModalForm() { return ( { editPool.mutate({ path: poolSelector, body: { name, description } }) }} loading={editPool.isPending} submitError={editPool.error} - submitLabel="Save changes" > diff --git a/app/forms/ip-pool-range-add.tsx b/app/forms/ip-pool-range-add.tsx index 9c16060e1a..332a6cf904 100644 --- a/app/forms/ip-pool-range-add.tsx +++ b/app/forms/ip-pool-range-add.tsx @@ -77,8 +77,9 @@ export function IpPoolAddRangeSideModalForm() { return ( addRange.mutate({ path: { pool }, body })} diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 19732a6009..ffedcce784 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -52,9 +52,10 @@ export default function CreateNetworkInterfaceForm({ return ( { const interfaceName = defaultValues.name @@ -56,7 +56,6 @@ export default function EditNetworkInterfaceForm({ }} loading={editNetworkInterface.isPending} submitError={editNetworkInterface.error} - submitLabel="Save changes" > diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx index 02f83b75e6..9ac407e749 100644 --- a/app/forms/project-access.tsx +++ b/app/forms/project-access.tsx @@ -42,8 +42,9 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa return ( { // can't happen because roleName is validated not to be '', but TS // wants to be sure @@ -82,6 +83,7 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa export function ProjectAccessEditUserSideModal({ onDismiss, + name, identityId, identityType, policy, @@ -102,9 +104,10 @@ export function ProjectAccessEditUserSideModal({ return ( { updatePolicy.mutate({ path: { project }, @@ -113,7 +116,6 @@ export function ProjectAccessEditUserSideModal({ }} loading={updatePolicy.isPending} submitError={updatePolicy.error} - submitLabel="Update role" onDismiss={onDismiss} > { createProject.mutate({ body: { name, description } }) diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx index 68985c4218..300df72b92 100644 --- a/app/forms/project-edit.tsx +++ b/app/forms/project-edit.tsx @@ -55,16 +55,15 @@ export function EditProjectSideModalForm() { return ( { editProject.mutate({ path: projectSelector, body: { name, description } }) }} loading={editProject.isPending} submitError={editProject.error} - submitLabel="Save changes" > diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx index 9771ce5965..d1bd8c3f0f 100644 --- a/app/forms/silo-access.tsx +++ b/app/forms/silo-access.tsx @@ -39,10 +39,11 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr return ( { // can't happen because roleName is validated not to be '', but TS // wants to be sure @@ -80,6 +81,7 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr export function SiloAccessEditUserSideModal({ onDismiss, + name, identityId, identityType, policy, @@ -97,9 +99,10 @@ export function SiloAccessEditUserSideModal({ return ( { updatePolicy.mutate({ body: updateRole({ identityId, identityType, roleName }, policy), @@ -107,7 +110,6 @@ export function SiloAccessEditUserSideModal({ }} loading={updatePolicy.isPending} submitError={updatePolicy.error} - submitLabel="Update role" onDismiss={onDismiss} > { createSnapshot.mutate({ query: projectSelector, body: values }) diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx index 65f4332cc1..8b4044f179 100644 --- a/app/forms/ssh-key-create.tsx +++ b/app/forms/ssh-key-create.tsx @@ -44,9 +44,10 @@ export function CreateSSHKeySideModalForm({ return ( createSshKey.mutate({ body })} loading={createSshKey.isPending} diff --git a/app/forms/subnet-create.tsx b/app/forms/subnet-create.tsx index 4b74f87a14..a240c61544 100644 --- a/app/forms/subnet-create.tsx +++ b/app/forms/subnet-create.tsx @@ -39,9 +39,9 @@ export function CreateSubnetForm({ onDismiss }: CreateSubnetFormProps) { return ( createSubnet.mutate({ query: vpcSelector, body })} loading={createSubnet.isPending} diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx index 7810960097..ed93841a54 100644 --- a/app/forms/subnet-edit.tsx +++ b/app/forms/subnet-edit.tsx @@ -35,10 +35,10 @@ export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) { return ( { updateSubnet.mutate({ path: { subnet: editing.name }, @@ -48,7 +48,6 @@ export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) { }} loading={updateSubnet.isPending} submitError={updateSubnet.error} - submitLabel="Update subnet" > diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx index da5db90b2d..e89683e40d 100644 --- a/app/forms/vpc-create.tsx +++ b/app/forms/vpc-create.tsx @@ -47,8 +47,8 @@ export function CreateVpcSideModalForm() { return ( createVpc.mutate({ query: projectSelector, body: values })} onDismiss={() => navigate(pb.vpcs(projectSelector))} loading={createVpc.isPending} diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx index 7b2e5877e5..169098cd51 100644 --- a/app/forms/vpc-edit.tsx +++ b/app/forms/vpc-edit.tsx @@ -56,9 +56,9 @@ export function EditVpcSideModalForm() { return ( { editVpc.mutate({ @@ -68,7 +68,6 @@ export function EditVpcSideModalForm() { }) }} loading={editVpc.isPending} - submitLabel="Save changes" submitError={editVpc.error} > diff --git a/app/pages/SiloAccessPage.tsx b/app/pages/SiloAccessPage.tsx index 2fd87ea148..0ba0d4f014 100644 --- a/app/pages/SiloAccessPage.tsx +++ b/app/pages/SiloAccessPage.tsx @@ -172,6 +172,7 @@ export function SiloAccessPage() { setEditingUserRow(null)} policy={siloPolicy} + name={editingUserRow.name} identityId={editingUserRow.id} identityType={editingUserRow.identityType} defaultValues={{ roleName: editingUserRow.siloRole }} diff --git a/app/pages/project/access/ProjectAccessPage.tsx b/app/pages/project/access/ProjectAccessPage.tsx index 6bc38461e4..a83756d870 100644 --- a/app/pages/project/access/ProjectAccessPage.tsx +++ b/app/pages/project/access/ProjectAccessPage.tsx @@ -199,6 +199,7 @@ export function ProjectAccessPage() { setEditingUserRow(null)} policy={projectPolicy} + name={editingUserRow.name} identityId={editingUserRow.id} identityType={editingUserRow.identityType} defaultValues={{ roleName: editingUserRow.projectRole }} diff --git a/app/ui/lib/SideModal.tsx b/app/ui/lib/SideModal.tsx index 341afb585f..f1404a4a4b 100644 --- a/app/ui/lib/SideModal.tsx +++ b/app/ui/lib/SideModal.tsx @@ -142,7 +142,10 @@ SideModal.Title = ({ subtitle?: ReactNode }) => (
-

+

{title}

{subtitle} diff --git a/test/e2e/click-everything.e2e.ts b/test/e2e/click-everything.e2e.ts index 50bfae8091..8db2064c09 100644 --- a/test/e2e/click-everything.e2e.ts +++ b/test/e2e/click-everything.e2e.ts @@ -48,12 +48,12 @@ test('Click through disks page', async ({ page }) => { // Create disk form await page.click('role=link[name="New Disk"]') await expectVisible(page, [ - 'role=heading[name*="Create Disk"]', + 'role=heading[name*="Create disk"]', 'role=textbox[name="Name"]', 'role=textbox[name="Description"]', 'role=radiogroup[name="Block size (Bytes)"]', 'role=textbox[name="Size (GiB)"]', - 'role=button[name="Create Disk"]', + 'role=button[name="Create disk"]', ]) await page.goBack() }) diff --git a/test/e2e/floating-ip-create.e2e.ts b/test/e2e/floating-ip-create.e2e.ts index 35a821c6ab..04d902696e 100644 --- a/test/e2e/floating-ip-create.e2e.ts +++ b/test/e2e/floating-ip-create.e2e.ts @@ -22,11 +22,11 @@ test('can create a floating IP', async ({ page }) => { await page.locator('text="New Floating IP"').click() await expectVisible(page, [ - 'role=heading[name*="Create Floating IP"]', + 'role=heading[name*="Create floating IP"]', 'role=textbox[name="Name"]', 'role=textbox[name="Description"]', 'role=button[name="Advanced"]', - 'role=button[name="Create Floating IP"]', + 'role=button[name="Create floating IP"]', ]) const floatingIpName = 'my-floating-ip' @@ -51,14 +51,14 @@ test('can create a floating IP', async ({ page }) => { await poolListbox.click() await page.getByRole('option', { name: 'ip-pool-1' }).click() await ipTextbox.fill('256.256.256.256') - await page.getByRole('button', { name: 'Create Floating IP' }).click() + await page.getByRole('button', { name: 'Create floating IP' }).click() await expect(page.getByText('Not a valid IP address').first()).toBeVisible() // correct IP and submit await ipTextbox.clear() await ipTextbox.fill('12.34.56.78') - await page.getByRole('button', { name: 'Create Floating IP' }).click() + await page.getByRole('button', { name: 'Create floating IP' }).click() await expect(page).toHaveURL(floatingIpsPage) diff --git a/test/e2e/floating-ip-update.e2e.ts b/test/e2e/floating-ip-update.e2e.ts index 0453358eeb..68bcf0d05d 100644 --- a/test/e2e/floating-ip-update.e2e.ts +++ b/test/e2e/floating-ip-update.e2e.ts @@ -16,7 +16,7 @@ const expectedFormElements = [ 'role=heading[name*="Edit floating IP"]', 'role=textbox[name="Name"]', 'role=textbox[name="Description"]', - 'role=button[name="Save changes"]', + 'role=button[name="Update floating IP"]', ] test('can update a floating IP', async ({ page }) => { @@ -26,7 +26,7 @@ test('can update a floating IP', async ({ page }) => { await page.fill('input[name=name]', updatedName) await page.getByRole('textbox', { name: 'Description' }).fill(updatedDescription) - await page.getByRole('button', { name: 'Save changes' }).click() + await page.getByRole('button', { name: 'Update floating IP' }).click() await expect(page).toHaveURL(floatingIpsPage) await expectRowVisible(page.getByRole('table'), { name: updatedName, @@ -41,7 +41,7 @@ test('can update *just* the floating IP description', async ({ page }) => { await expectVisible(page, expectedFormElements) await page.getByRole('textbox', { name: 'Description' }).fill(updatedDescription) - await page.getByRole('button', { name: 'Save changes' }).click() + await page.getByRole('button', { name: 'Update floating IP' }).click() await expect(page).toHaveURL(floatingIpsPage) await expectRowVisible(page.getByRole('table'), { name: originalName, diff --git a/test/e2e/instance/disks.e2e.ts b/test/e2e/instance/disks.e2e.ts index 98e377162d..a9d4279f0e 100644 --- a/test/e2e/instance/disks.e2e.ts +++ b/test/e2e/instance/disks.e2e.ts @@ -46,7 +46,7 @@ test('Attach disk', async ({ page }) => { 'role=textbox[name="Description"]', 'role=radiogroup[name="Block size (Bytes)"]', 'role=textbox[name="Size (GiB)"]', - 'role=button[name="Create Disk"]', + 'role=button[name="Create disk"]', ]) await page.click('role=button[name="Cancel"]') diff --git a/test/e2e/instance/networking.e2e.ts b/test/e2e/instance/networking.e2e.ts index fe0ccba638..43ef5064de 100644 --- a/test/e2e/instance/networking.e2e.ts +++ b/test/e2e/instance/networking.e2e.ts @@ -66,7 +66,7 @@ test('Instance networking tab', async ({ page }) => { // Make an edit to the network interface await clickRowAction(page, 'nic-2', 'Edit') await page.fill('role=textbox[name="Name"]', 'nic-3') - await page.click('role=button[name="Save changes"]') + await page.click('role=button[name="Update network interface"]') await expectNotVisible(page, ['role=cell[name="nic-2"]']) const nic3 = page.getByRole('cell', { name: 'nic-3' }) await expect(nic3).toBeVisible() diff --git a/test/e2e/networking.e2e.ts b/test/e2e/networking.e2e.ts index 999ef18907..e2f6637302 100644 --- a/test/e2e/networking.e2e.ts +++ b/test/e2e/networking.e2e.ts @@ -40,10 +40,10 @@ test('Create and edit VPC', async ({ page }) => { 'role=textbox[name="Name"]', 'role=textbox[name="Description"]', 'role=textbox[name="DNS name"]', - 'role=button[name="Save changes"]', + 'role=button[name="Update VPC"]', ]) await page.fill('role=textbox[name="Name"]', 'new-vpc') - await page.click('role=button[name="Save changes"]') + await page.click('role=button[name="Update VPC"]') // Close toast, it holds up the test for some reason await page.click('role=button[name="Dismiss notification"]') diff --git a/test/e2e/project-access.e2e.ts b/test/e2e/project-access.e2e.ts index a99a0db1a4..a6481688bc 100644 --- a/test/e2e/project-access.e2e.ts +++ b/test/e2e/project-access.e2e.ts @@ -78,7 +78,7 @@ test('Click through project access page', async ({ page }) => { .click() await page.click('role=menuitem[name="Change role"]') - await expectVisible(page, ['role=heading[name*="Change user role"]']) + await expectVisible(page, ['role=heading[name*="Change role for Simone de Beauvoir"]']) await expectVisible(page, ['button:has-text("Collaborator")']) await page.click('role=button[name*="Role"]') diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index 8ce9827175..aaf6142c59 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -67,7 +67,7 @@ test('Click through silo access page', async ({ page }) => { .click() await page.click('role=menuitem[name="Change role"]') - await expectVisible(page, ['role=heading[name*="Change user role"]']) + await expectVisible(page, ['role=heading[name*="Change role for Jacob Klein"]']) await expectVisible(page, ['button:has-text("Collaborator")']) await page.click('role=button[name*="Role"]')