diff --git a/app/components/form/Form.tsx b/app/components/form/Form.tsx
index a9cfb94d7f..b677823f72 100644
--- a/app/components/form/Form.tsx
+++ b/app/components/form/Form.tsx
@@ -12,7 +12,9 @@ import './form.css'
interface FormActionsProps {
formId?: string
children: React.ReactNode
- submitDisabled?: boolean
+ /** Must be provided with a reason why the submit button is disabled */
+ submitDisabled?: string
+ loading?: boolean
error?: Error | null
className?: string
}
@@ -27,9 +29,10 @@ export const Form = {
Actions: ({
children,
formId,
- submitDisabled = true,
+ submitDisabled,
error,
className,
+ loading,
}: FormActionsProps) => {
const childArray = flattenChildren(children)
@@ -49,7 +52,12 @@ export const Form = {
className
)}
>
- {cloneElement(submit, { form: formId, disabled: submitDisabled })}
+ {cloneElement(submit, {
+ form: formId,
+ disabled: !!submitDisabled,
+ disabledReason: submitDisabled,
+ loading,
+ })}
{childArray}
{error && (
diff --git a/app/components/form/FullPageForm.tsx b/app/components/form/FullPageForm.tsx
index b5562ae51b..5cd30169a2 100644
--- a/app/components/form/FullPageForm.tsx
+++ b/app/components/form/FullPageForm.tsx
@@ -14,9 +14,11 @@ interface FullPageFormProps
{
id: string
title: string
icon: ReactElement
- submitDisabled?: boolean
+ /** Must provide a reason for submit being disabled */
+ submitDisabled?: string
error?: Error
formOptions: UseFormProps
+ loading?: boolean
onSubmit: (values: TFieldValues) => void
/** Error from the API call */
submitError: ErrorResult | null
@@ -37,14 +39,15 @@ export function FullPageForm({
id,
title,
children,
- submitDisabled = false,
+ submitDisabled,
error,
icon,
+ loading,
formOptions,
onSubmit,
}: FullPageFormProps) {
const form = useForm(formOptions)
- const { isSubmitting, isDirty } = form.formState
+ const { isSubmitting, isSubmitSuccessful } = form.formState
const childArray = flattenChildren(children(form))
const actions = pluckFirstOfType(childArray, Form.Actions)
@@ -62,7 +65,8 @@ export function FullPageForm({
{cloneElement(actions, {
formId: id,
- submitDisabled: submitDisabled || isSubmitting || !isDirty,
+ submitDisabled,
+ loading: loading || isSubmitting || isSubmitSuccessful,
error,
})}
diff --git a/app/components/form/SideModalForm.tsx b/app/components/form/SideModalForm.tsx
index 96abc8724e..682ff8159f 100644
--- a/app/components/form/SideModalForm.tsx
+++ b/app/components/form/SideModalForm.tsx
@@ -20,9 +20,11 @@ type SideModalFormProps = {
*/
children: (form: UseFormReturn) => ReactNode
onDismiss: () => void
- submitDisabled?: boolean
+ /** Must be provided with a reason describing why it's disabled */
+ submitDisabled?: string
/** Error from the API call */
submitError: ErrorResult | null
+ loading?: boolean
title: string
onSubmit: (values: TFieldValues) => void
submitLabel?: string
@@ -33,19 +35,18 @@ export function SideModalForm({
formOptions,
children,
onDismiss,
- submitDisabled = false,
+ submitDisabled,
submitError,
title,
onSubmit,
submitLabel,
+ loading,
}: SideModalFormProps) {
// TODO: RHF docs warn about the performance impact of validating on every
// change
const form = useForm({ mode: 'all', ...formOptions })
- const { isDirty, isValid } = form.formState
-
- const canSubmit = isDirty && isValid
+ const { isSubmitting, isSubmitSuccessful } = form.formState
/**
* Only animate the modal in when we're navigating by a client-side click.
@@ -87,7 +88,14 @@ export function SideModalForm({
-
diff --git a/app/forms/disk-attach.tsx b/app/forms/disk-attach.tsx
index 16f9951c26..0dcaf8f83a 100644
--- a/app/forms/disk-attach.tsx
+++ b/app/forms/disk-attach.tsx
@@ -58,7 +58,7 @@ export function AttachDiskSideModalForm({
})
})
}
- submitDisabled={attachDisk.isLoading}
+ loading={attachDisk.isLoading}
submitError={attachDisk.error}
onDismiss={onDismiss}
>
diff --git a/app/forms/disk-create.tsx b/app/forms/disk-create.tsx
index aca0ca8080..3cacc70790 100644
--- a/app/forms/disk-create.tsx
+++ b/app/forms/disk-create.tsx
@@ -73,7 +73,7 @@ export function CreateDiskSideModalForm({
const body = { size: size * GiB, ...rest }
onSubmit ? onSubmit(body) : createDisk.mutate({ path: pathParams, body })
}}
- submitDisabled={createDisk.isLoading}
+ loading={createDisk.isLoading}
submitError={createDisk.error}
>
{({ control }) => (
diff --git a/app/forms/firewall-rules-create.tsx b/app/forms/firewall-rules-create.tsx
index d591479cab..24ed574023 100644
--- a/app/forms/firewall-rules-create.tsx
+++ b/app/forms/firewall-rules-create.tsx
@@ -451,7 +451,7 @@ export function CreateFirewallRuleForm({
},
})
}}
- submitDisabled={updateRules.isLoading}
+ loading={updateRules.isLoading}
submitError={updateRules.error}
submitLabel="Add rule"
>
diff --git a/app/forms/firewall-rules-edit.tsx b/app/forms/firewall-rules-edit.tsx
index d548a61676..cd1bd58a13 100644
--- a/app/forms/firewall-rules-edit.tsx
+++ b/app/forms/firewall-rules-edit.tsx
@@ -67,7 +67,7 @@ export function EditFirewallRuleForm({
}}
// validationSchema={validationSchema}
// validateOnBlur
- submitDisabled={updateRules.isLoading}
+ loading={updateRules.isLoading}
submitError={updateRules.error}
submitLabel="Update rule"
>
diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx
index 7ea48200e7..814ef52517 100644
--- a/app/forms/instance-create.tsx
+++ b/app/forms/instance-create.tsx
@@ -154,7 +154,7 @@ export function CreateInstanceForm() {
},
})
}}
- submitDisabled={createInstance.isLoading}
+ loading={createInstance.isLoading}
submitError={createInstance.error}
>
{({ control }) => (
diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx
index ed0d632926..01457a4997 100644
--- a/app/forms/network-interface-create.tsx
+++ b/app/forms/network-interface-create.tsx
@@ -69,7 +69,7 @@ export default function CreateNetworkInterfaceForm({
})
})
}
- submitDisabled={createNetworkInterface.isLoading}
+ loading={createNetworkInterface.isLoading}
submitError={createNetworkInterface.error}
>
{({ control }) => (
diff --git a/app/forms/network-interface-edit.tsx b/app/forms/network-interface-edit.tsx
index cb396db7fc..452b580351 100644
--- a/app/forms/network-interface-edit.tsx
+++ b/app/forms/network-interface-edit.tsx
@@ -44,7 +44,7 @@ export default function EditNetworkInterfaceForm({
body,
})
}}
- submitDisabled={editNetworkInterface.isLoading}
+ loading={editNetworkInterface.isLoading}
submitError={editNetworkInterface.error}
submitLabel="Save changes"
>
diff --git a/app/forms/org-access.tsx b/app/forms/org-access.tsx
index c030be9317..b36e345e9c 100644
--- a/app/forms/org-access.tsx
+++ b/app/forms/org-access.tsx
@@ -41,7 +41,7 @@ export function OrgAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPro
body: setUserRole(userId, roleName, policy),
})
}}
- submitDisabled={updatePolicy.isLoading}
+ loading={updatePolicy.isLoading}
submitError={updatePolicy.error}
submitLabel="Add user"
>
@@ -95,7 +95,7 @@ export function OrgAccessEditUserSideModal({
body: setUserRole(userId, roleName, policy),
})
}}
- submitDisabled={updatePolicy.isLoading || !policy}
+ loading={updatePolicy.isLoading}
submitError={updatePolicy.error}
onDismiss={onDismiss}
submitLabel="Update role"
diff --git a/app/forms/org-create.tsx b/app/forms/org-create.tsx
index 88d04a40fb..550b776d1d 100644
--- a/app/forms/org-create.tsx
+++ b/app/forms/org-create.tsx
@@ -40,7 +40,7 @@ export function CreateOrgSideModalForm() {
title="Create organization"
onDismiss={() => navigate(pb.orgs())}
onSubmit={(values) => createOrg.mutate({ body: values })}
- submitDisabled={createOrg.isLoading}
+ loading={createOrg.isLoading}
submitError={createOrg.error}
>
{({ control }) => (
diff --git a/app/forms/org-edit.tsx b/app/forms/org-edit.tsx
index d440ad2409..fbc3635142 100644
--- a/app/forms/org-edit.tsx
+++ b/app/forms/org-edit.tsx
@@ -52,7 +52,7 @@ export function EditOrgSideModalForm() {
body: { name, description },
})
}
- submitDisabled={updateOrg.isLoading}
+ loading={updateOrg.isLoading}
submitError={updateOrg.error}
submitLabel="Save changes"
>
diff --git a/app/forms/project-access.tsx b/app/forms/project-access.tsx
index 310c11ef98..705eacd97a 100644
--- a/app/forms/project-access.tsx
+++ b/app/forms/project-access.tsx
@@ -41,7 +41,7 @@ export function ProjectAccessAddUserSideModal({ onDismiss, policy }: AddRoleModa
body: setUserRole(userId, roleName, policy),
})
}}
- submitDisabled={updatePolicy.isLoading}
+ loading={updatePolicy.isLoading}
submitError={updatePolicy.error}
submitLabel="Add user"
onDismiss={onDismiss}
@@ -96,7 +96,7 @@ export function ProjectAccessEditUserSideModal({
body: setUserRole(userId, roleName, policy),
})
}}
- submitDisabled={updatePolicy.isLoading || !policy}
+ loading={updatePolicy.isLoading}
submitError={updatePolicy.error}
submitLabel="Update role"
onDismiss={onDismiss}
diff --git a/app/forms/project-create.tsx b/app/forms/project-create.tsx
index b0478a7632..610365e864 100644
--- a/app/forms/project-create.tsx
+++ b/app/forms/project-create.tsx
@@ -51,7 +51,7 @@ export function CreateProjectSideModalForm() {
body: { name, description },
})
}}
- submitDisabled={createProject.isLoading}
+ loading={createProject.isLoading}
submitError={createProject.error}
>
{({ control }) => (
diff --git a/app/forms/project-edit.tsx b/app/forms/project-edit.tsx
index 5d84bf828e..dc1d01d9cb 100644
--- a/app/forms/project-edit.tsx
+++ b/app/forms/project-edit.tsx
@@ -57,7 +57,7 @@ export function EditProjectSideModalForm() {
body: { name, description },
})
}}
- submitDisabled={editProject.isLoading}
+ loading={editProject.isLoading}
submitError={editProject.error}
submitLabel="Save changes"
>
diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx
index 3d45106502..973260167b 100644
--- a/app/forms/silo-access.tsx
+++ b/app/forms/silo-access.tsx
@@ -37,7 +37,7 @@ export function SiloAccessAddUserSideModal({ onDismiss, policy }: AddRoleModalPr
body: setUserRole(userId, roleName, policy),
})
}}
- submitDisabled={updatePolicy.isLoading}
+ loading={updatePolicy.isLoading}
submitError={updatePolicy.error}
submitLabel="Add user"
>
@@ -88,7 +88,7 @@ export function SiloAccessEditUserSideModal({
body: setUserRole(userId, roleName, policy),
})
}}
- submitDisabled={updatePolicy.isLoading || !policy}
+ loading={updatePolicy.isLoading}
submitError={updatePolicy.error}
submitLabel="Update role"
onDismiss={onDismiss}
diff --git a/app/forms/silo-create.tsx b/app/forms/silo-create.tsx
index 746c1603bf..e1058f5ed1 100644
--- a/app/forms/silo-create.tsx
+++ b/app/forms/silo-create.tsx
@@ -52,7 +52,7 @@ export function CreateSiloSideModalForm() {
body: { name, description, discoverable, identityMode },
})
}
- submitDisabled={createSilo.isLoading}
+ loading={createSilo.isLoading}
submitError={createSilo.error}
>
{({ control }) => (
diff --git a/app/forms/ssh-key-create.tsx b/app/forms/ssh-key-create.tsx
index 7136499f4e..42136d0d1b 100644
--- a/app/forms/ssh-key-create.tsx
+++ b/app/forms/ssh-key-create.tsx
@@ -32,7 +32,7 @@ export function CreateSSHKeySideModalForm() {
formOptions={{ defaultValues }}
onDismiss={onDismiss}
onSubmit={(body) => createSshKey.mutate({ body })}
- submitDisabled={createSshKey.isLoading}
+ loading={createSshKey.isLoading}
submitError={createSshKey.error}
>
{({ control }) => (
diff --git a/app/forms/subnet-create.tsx b/app/forms/subnet-create.tsx
index 4b30749e9f..7a9163d492 100644
--- a/app/forms/subnet-create.tsx
+++ b/app/forms/subnet-create.tsx
@@ -32,7 +32,7 @@ export function CreateSubnetForm({ onDismiss }: CreateSubnetFormProps) {
formOptions={{ defaultValues }}
onDismiss={onDismiss}
onSubmit={(body) => createSubnet.mutate({ path: parentNames, body })}
- submitDisabled={createSubnet.isLoading}
+ loading={createSubnet.isLoading}
submitError={createSubnet.error}
>
{({ control }) => (
diff --git a/app/forms/subnet-edit.tsx b/app/forms/subnet-edit.tsx
index dea0a0b18d..2196c4f69c 100644
--- a/app/forms/subnet-edit.tsx
+++ b/app/forms/subnet-edit.tsx
@@ -35,7 +35,7 @@ export function EditSubnetForm({ onDismiss, editing }: EditSubnetFormProps) {
body,
})
}}
- submitDisabled={updateSubnet.isLoading}
+ loading={updateSubnet.isLoading}
submitError={updateSubnet.error}
submitLabel="Update subnet"
>
diff --git a/app/forms/vpc-create.tsx b/app/forms/vpc-create.tsx
index d1fcdbc99f..5b4bdbb548 100644
--- a/app/forms/vpc-create.tsx
+++ b/app/forms/vpc-create.tsx
@@ -42,7 +42,7 @@ export function CreateVpcSideModalForm() {
formOptions={{ defaultValues }}
onSubmit={(values) => createVpc.mutate({ path: parentNames, body: values })}
onDismiss={() => navigate(pb.vpcs(parentNames))}
- submitDisabled={createVpc.isLoading}
+ loading={createVpc.isLoading}
submitError={createVpc.error}
>
{({ control }) => (
diff --git a/app/forms/vpc-edit.tsx b/app/forms/vpc-edit.tsx
index 74b109522d..d0fcaf515e 100644
--- a/app/forms/vpc-edit.tsx
+++ b/app/forms/vpc-edit.tsx
@@ -55,7 +55,7 @@ export function EditVpcSideModalForm() {
body: { name, description, dnsName },
})
}}
- submitDisabled={editVpc.isLoading}
+ loading={editVpc.isLoading}
submitLabel="Save changes"
submitError={editVpc.error}
>
diff --git a/app/forms/vpc-router-create.tsx b/app/forms/vpc-router-create.tsx
index 77ef60b962..a922ef7ff1 100644
--- a/app/forms/vpc-router-create.tsx
+++ b/app/forms/vpc-router-create.tsx
@@ -46,7 +46,7 @@ export function CreateVpcRouterForm({ onDismiss }: CreateVpcRouterFormProps) {
onSubmit={({ name, description }) =>
createRouter.mutate({ path: parentNames, body: { name, description } })
}
- submitDisabled={createRouter.isLoading}
+ loading={createRouter.isLoading}
submitError={createRouter.error}
>
{({ control }) => (
diff --git a/app/forms/vpc-router-edit.tsx b/app/forms/vpc-router-edit.tsx
index 52cfc639d5..af51b2a486 100644
--- a/app/forms/vpc-router-edit.tsx
+++ b/app/forms/vpc-router-edit.tsx
@@ -35,7 +35,7 @@ export function EditVpcRouterForm({ onDismiss, editing }: EditVpcRouterFormProps
body: { name, description },
})
}}
- submitDisabled={updateRouter.isLoading}
+ loading={updateRouter.isLoading}
submitError={updateRouter.error}
>
{({ control }) => (
diff --git a/app/pages/__tests__/click-everything.e2e.ts b/app/pages/__tests__/click-everything.e2e.ts
index 2710dbe5d2..23e6e1be67 100644
--- a/app/pages/__tests__/click-everything.e2e.ts
+++ b/app/pages/__tests__/click-everything.e2e.ts
@@ -60,7 +60,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
'role=textbox[name="Description"]',
'role=radiogroup[name="Block size (Bytes)"]',
'role=spinbutton[name="Size (GiB)"]',
- 'role=button[name="Create Disk"][disabled]',
+ 'role=button[name="Create Disk"]',
])
await page.goBack()
@@ -104,7 +104,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
'role=textbox[name="Description"]',
'role=textbox[name="DNS name"]',
'role=textbox[name="IPV6 prefix"]',
- 'role=button[name="Create VPC"][disabled]',
+ 'role=button[name="Create VPC"]',
])
await page.goBack()
@@ -119,7 +119,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
'role=textbox[name="Name"]',
'role=textbox[name="Description"]',
'role=textbox[name="DNS name"]',
- 'role=button[name="Save changes"][disabled]',
+ 'role=button[name="Save changes"]',
])
await page.fill('role=textbox[name="Name"]', 'new-vpc')
await page.click('role=button[name="Save changes"]')
@@ -146,7 +146,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
await page.click('role=button[name="New subnet"]')
await expectVisible(page, [
'role=heading[name="Create subnet"]',
- 'role=button[name="Create subnet"][disabled]',
+ 'role=button[name="Create subnet"]',
])
await page.fill('role=textbox[name="Name"]', 'new-subnet')
await page.fill('role=textbox[name="IPv4 block"]', '10.1.1.1/24')
@@ -162,7 +162,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
await expectVisible(page, [
'role=heading[name="Edit subnet"]',
- 'role=button[name="Update subnet"][disabled]',
+ 'role=button[name="Update subnet"]',
])
await page.fill('role=textbox[name="Name"]', 'edited-subnet')
await page.fill('role=textbox[name="Description"]', 'behold')
@@ -181,7 +181,7 @@ test("Click through everything and make it's all there", async ({ page }) => {
await page.click('role=button[name="New router"]')
await expectVisible(page, [
'role=heading[name="Create VPC Router"]',
- 'role=button[name="Create VPC Router"][disabled]',
+ 'role=button[name="Create VPC Router"]',
])
await page.fill('role=textbox[name="Name"]', 'new-router')
await page.click('role=button[name="Create VPC Router"]')
diff --git a/app/pages/__tests__/instance/attach-disk.e2e.ts b/app/pages/__tests__/instance/attach-disk.e2e.ts
index 9480d56f39..d1fb00d3c1 100644
--- a/app/pages/__tests__/instance/attach-disk.e2e.ts
+++ b/app/pages/__tests__/instance/attach-disk.e2e.ts
@@ -17,7 +17,7 @@ test('Attach disk', async ({ page }) => {
'role=textbox[name="Description"]',
'role=radiogroup[name="Block size (Bytes)"]',
'role=spinbutton[name="Size (GiB)"]',
- 'role=button[name="Create Disk"][disabled]',
+ 'role=button[name="Create Disk"]',
])
await page.click('role=button[name="Cancel"]')
diff --git a/app/pages/__tests__/orgs.e2e.ts b/app/pages/__tests__/orgs.e2e.ts
index 7a43a51fe6..a95c17b7ca 100644
--- a/app/pages/__tests__/orgs.e2e.ts
+++ b/app/pages/__tests__/orgs.e2e.ts
@@ -17,7 +17,7 @@ test('Orgs list and detail click work', async ({ page }) => {
'role=heading[name*="Create organization"]',
'role=textbox[name="Name"]',
'role=textbox[name="Description"]',
- 'role=button[name="Create organization"][disabled]',
+ 'role=button[name="Create organization"]',
])
await page.goBack()
diff --git a/app/pages/__tests__/project-create.e2e.tsx b/app/pages/__tests__/project-create.e2e.tsx
index d1cbb0b523..c0d0dea1fe 100644
--- a/app/pages/__tests__/project-create.e2e.tsx
+++ b/app/pages/__tests__/project-create.e2e.tsx
@@ -12,7 +12,7 @@ test.describe('Project create', () => {
'role=heading[name*="Create project"]', // TODO: standardize capitalization
'role=textbox[name="Name"]',
'role=textbox[name="Description"]',
- 'role=button[name="Create project"][disabled]',
+ 'role=button[name="Create project"]',
])
})
@@ -25,8 +25,6 @@ test.describe('Project create', () => {
test('shows field-level validation error and does not POST', async ({ page }) => {
await page.fill('role=textbox[name="Name"]', 'Invalid name')
- await expect(page.locator('role=button[name="Create project"]')).toBeDisabled()
-
await page.click('role=textbox[name="Description"]') // just to blur name input
await expectVisible(page, ['text="Must start with a lower-case letter"'])
})
diff --git a/app/test/instance-create.e2e.ts b/app/test/instance-create.e2e.ts
index 7c87d9ca6f..74dcbe71b5 100644
--- a/app/test/instance-create.e2e.ts
+++ b/app/test/instance-create.e2e.ts
@@ -28,7 +28,7 @@ test('can invoke instance create form from instances page', async ({
'role=spinbutton[name="Disk size (GiB)"]',
'role=radiogroup[name="Network interface"]',
'role=textbox[name="Hostname"]',
- 'role=button[name="Create instance"][disabled]',
+ 'role=button[name="Create instance"]',
])
const instanceName = genName('instance')
diff --git a/libs/ui/lib/button/Button.tsx b/libs/ui/lib/button/Button.tsx
index 78430f1683..a31762b07b 100644
--- a/libs/ui/lib/button/Button.tsx
+++ b/libs/ui/lib/button/Button.tsx
@@ -70,7 +70,6 @@ export const Button = forwardRef(
innerClassName,
disabled,
onClick,
- 'aria-disabled': ariaDisabled,
disabledReason,
// needs to be a spread because we pass this component to Reach
// with the `as` prop and get passed arbitrary props
@@ -78,20 +77,21 @@ export const Button = forwardRef(
},
ref
) => {
+ const isDisabled = disabled || loading
return (
}
>
{loading && }