From 4f60bb2d65cef92ea623b7bac2d92f06ca807520 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Dec 2023 14:02:17 -0800 Subject: [PATCH 1/5] Add onBlur to better handle empty IP Address field --- app/forms/network-interface-create.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index edec5d5650..138a0ea53c 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -80,7 +80,16 @@ export default function CreateNetworkInterfaceForm({ required control={form.control} /> - + { + if (form.getValues('ip')?.trim() === '') { + form.setValue('ip', undefined) + } + }} + /> ) } From 8220dd318e03cdf899d1f25564d19c8675d614d5 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Dec 2023 15:01:17 -0800 Subject: [PATCH 2/5] Add test for network-interface-create flow --- app/test/e2e/network-interface-create.e2e.ts | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 app/test/e2e/network-interface-create.e2e.ts diff --git a/app/test/e2e/network-interface-create.e2e.ts b/app/test/e2e/network-interface-create.e2e.ts new file mode 100644 index 0000000000..e0dd193f40 --- /dev/null +++ b/app/test/e2e/network-interface-create.e2e.ts @@ -0,0 +1,79 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { test } from '@playwright/test' + +import { expect } from './utils' + +test('can create a NIC with a specified IP address', async ({ page }) => { + // go to an instance’s Network Interfaces page + await page.goto('/projects/mock-project/instances/db1/network-interfaces') + + // stop the instance + await page.getByRole('button', { name: 'Instance actions' }).click() + await page.getByRole('menuitem', { name: 'Stop' }).click() + + // open the add network interface side modal + await page.getByRole('button', { name: 'Add network interface' }).click() + + // fill out the form + await page.getByLabel('Name').fill('nic-1') + await page.getByRole('button', { name: 'VPC' }).click() + await page.getByRole('option', { name: 'mock-vpc' }).click() + await page.getByRole('button', { name: 'Subnet' }).click() + await page.getByRole('option', { name: 'mock-subnet' }).click() + await page.getByLabel('IP Address').fill('1.2.3.4') + + const sidebar = page.getByRole('dialog', { name: 'Add network interface' }) + + // test that the form can be submitted and a new network interface is created + await sidebar.getByRole('button', { name: 'Add network interface' }).click() + const lastRow = page.getByRole('table').locator('tr:last-child') + + await expect(lastRow.getByText('1.2.3.4')).toBeVisible() +}) + +test('can create a NIC with a blank IP address', async ({ page }) => { + // go to an instance’s Network Interfaces page + await page.goto('/projects/mock-project/instances/db1/network-interfaces') + + // stop the instance + await page.getByRole('button', { name: 'Instance actions' }).click() + await page.getByRole('menuitem', { name: 'Stop' }).click() + + // open the add network interface side modal + await page.getByRole('button', { name: 'Add network interface' }).click() + + // fill out the form + await page.getByLabel('Name').fill('nic-2') + await page.getByRole('button', { name: 'VPC' }).click() + await page.getByRole('option', { name: 'mock-vpc' }).click() + await page.getByRole('button', { name: 'Subnet' }).click() + await page.getByRole('option', { name: 'mock-subnet' }).click() + + // make sure the IP address field has a non-conforming bit of text in it + await page.getByLabel('IP Address').fill('x') + + // try to submit it + const sidebar = page.getByRole('dialog', { name: 'Add network interface' }) + await sidebar.getByRole('button', { name: 'Add network interface' }).click() + + // it should error out + // todo: improve error message from API + await expect(sidebar.getByText('Unknown server error')).toBeVisible() + + // make sure the IP address field has spaces in it + await page.getByLabel('IP Address').fill(' ') + + // test that the form can be submitted and a new network interface is created + await sidebar.getByRole('button', { name: 'Add network interface' }).click() + const lastRow = page.getByRole('table').locator('tr:last-child') + + await expect(lastRow.getByText('nic-2')).toBeVisible() + // it should use the default value + await expect(lastRow.getByText('123.45.68.8')).toBeVisible() +}) From 4c799750f4c54ee575f4393f7186875e08ad752d Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 12 Dec 2023 22:27:19 -0800 Subject: [PATCH 3/5] Use transform rather than onChange --- app/components/form/fields/TextField.tsx | 12 +++++++++++- app/forms/network-interface-create.tsx | 6 +----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/components/form/fields/TextField.tsx b/app/components/form/fields/TextField.tsx index dc53ff6f3c..014d214228 100644 --- a/app/components/form/fields/TextField.tsx +++ b/app/components/form/fields/TextField.tsx @@ -56,6 +56,11 @@ export interface TextFieldProps< units?: string validate?: Validate, TFieldValues> control: Control + /** + * This function can be provided to alter the value of the input + * as the input is changed + */ + transform?: (value: string) => string | undefined } export function TextField< @@ -75,7 +80,7 @@ export function TextField< return (
- + {label} {units && ({units})} {helpText && ( @@ -119,6 +124,7 @@ export const TextFieldInner = < description, required, id: idProp, + transform, ...props }: TextFieldProps & UITextAreaProps) => { const generatedId = useId() @@ -144,6 +150,10 @@ export const TextFieldInner = < // for the calling code despite the actual input value necessarily // being a string. onChange={(e) => { + if (transform) { + onChange(transform(e.target.value)) + return + } if (type === 'number') { if (e.target.value.trim() === '') { onChange(0) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 138a0ea53c..a459c9aa84 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -84,11 +84,7 @@ export default function CreateNetworkInterfaceForm({ name="ip" label="IP Address" control={form.control} - onBlur={() => { - if (form.getValues('ip')?.trim() === '') { - form.setValue('ip', undefined) - } - }} + transform={(ip) => (ip.trim() === '' ? undefined : ip)} /> ) From 6331dbb5ac60051f338cc3335eead797e81f85f5 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Tue, 23 Jan 2024 13:45:05 -0800 Subject: [PATCH 4/5] More succinct prop description --- app/components/form/fields/TextField.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/components/form/fields/TextField.tsx b/app/components/form/fields/TextField.tsx index 4d9c75bdf9..da1f1cf923 100644 --- a/app/components/form/fields/TextField.tsx +++ b/app/components/form/fields/TextField.tsx @@ -56,10 +56,7 @@ export interface TextFieldProps< units?: string validate?: Validate, TFieldValues> control: Control - /** - * This function can be provided to alter the value of the input - * as the input is changed - */ + /** Alters the value of the input during the field's onChange event. */ transform?: (value: string) => string | undefined } From 69d0178abbccc320cef2372e17dca347e5ba26ad Mon Sep 17 00:00:00 2001 From: David Crespo Date: Wed, 24 Jan 2024 16:18:37 -0600 Subject: [PATCH 5/5] small tweaks --- app/components/form/fields/TextField.tsx | 2 +- app/test/e2e/network-interface-create.e2e.ts | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/components/form/fields/TextField.tsx b/app/components/form/fields/TextField.tsx index da1f1cf923..e341be08aa 100644 --- a/app/components/form/fields/TextField.tsx +++ b/app/components/form/fields/TextField.tsx @@ -57,7 +57,7 @@ export interface TextFieldProps< validate?: Validate, TFieldValues> control: Control /** Alters the value of the input during the field's onChange event. */ - transform?: (value: string) => string | undefined + transform?: (value: string) => FieldPathValue } export function TextField< diff --git a/app/test/e2e/network-interface-create.e2e.ts b/app/test/e2e/network-interface-create.e2e.ts index e0dd193f40..21c01ae935 100644 --- a/app/test/e2e/network-interface-create.e2e.ts +++ b/app/test/e2e/network-interface-create.e2e.ts @@ -7,7 +7,7 @@ */ import { test } from '@playwright/test' -import { expect } from './utils' +import { expect, expectRowVisible } from './utils' test('can create a NIC with a specified IP address', async ({ page }) => { // go to an instance’s Network Interfaces page @@ -32,9 +32,9 @@ test('can create a NIC with a specified IP address', async ({ page }) => { // test that the form can be submitted and a new network interface is created await sidebar.getByRole('button', { name: 'Add network interface' }).click() - const lastRow = page.getByRole('table').locator('tr:last-child') + await expect(sidebar).toBeHidden() - await expect(lastRow.getByText('1.2.3.4')).toBeVisible() + await expectRowVisible(page.getByRole('table'), { name: 'nic-1', ip: '1.2.3.4' }) }) test('can create a NIC with a blank IP address', async ({ page }) => { @@ -71,9 +71,8 @@ test('can create a NIC with a blank IP address', async ({ page }) => { // test that the form can be submitted and a new network interface is created await sidebar.getByRole('button', { name: 'Add network interface' }).click() - const lastRow = page.getByRole('table').locator('tr:last-child') + await expect(sidebar).toBeHidden() - await expect(lastRow.getByText('nic-2')).toBeVisible() - // it should use the default value - await expect(lastRow.getByText('123.45.68.8')).toBeVisible() + // ip address is auto-assigned + await expectRowVisible(page.getByRole('table'), { name: 'nic-2', ip: '123.45.68.8' }) })