From c06c1c0882ff8004e0ccdfa68a9dd2ad4e1bed23 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 11 Apr 2022 18:21:36 -0400 Subject: [PATCH 1/8] Move DisksTableField to components/fields dir --- app/components/{ => fields}/DisksTableField.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename app/components/{ => fields}/DisksTableField.tsx (97%) diff --git a/app/components/DisksTableField.tsx b/app/components/fields/DisksTableField.tsx similarity index 97% rename from app/components/DisksTableField.tsx rename to app/components/fields/DisksTableField.tsx index 87ce56e718..59033f0f26 100644 --- a/app/components/DisksTableField.tsx +++ b/app/components/fields/DisksTableField.tsx @@ -9,7 +9,7 @@ import { MiniTable, SideModal, } from '@oxide/ui' -import type { FormValues } from '../forms' +import type { FormValues } from '../../forms' type DiskTableItem = | (FormValues<'disk-create'> & { type: 'create' }) @@ -38,7 +38,7 @@ export function DisksTableField() { {items.map((item, index) => ( Date: Mon, 11 Apr 2022 18:21:58 -0400 Subject: [PATCH 2/8] Add ability to use onChange w/ radio group --- libs/ui/lib/radio-group/RadioGroup.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/ui/lib/radio-group/RadioGroup.tsx b/libs/ui/lib/radio-group/RadioGroup.tsx index 22a70e3205..f645221e23 100644 --- a/libs/ui/lib/radio-group/RadioGroup.tsx +++ b/libs/ui/lib/radio-group/RadioGroup.tsx @@ -58,6 +58,8 @@ export type RadioGroupProps = { // For vertical layout of regular Radios. Leave it off for RadioCards. column?: boolean className?: string + + onChange?: (event: React.ChangeEvent) => void } export const RadioGroup = ({ @@ -67,6 +69,7 @@ export const RadioGroup = ({ disabled, column, className, + onChange, }: RadioGroupProps) => (
{React.Children.map(children, (radio) => React.cloneElement(radio, { name, required, disabled }) From f82f8dda5d1d7169341f27b378b1b48e1ff5ce7d Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 11 Apr 2022 18:22:32 -0400 Subject: [PATCH 3/8] Ensure sidemodal has space for last input when UI scrolled --- libs/ui/lib/side-modal/SideModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/ui/lib/side-modal/SideModal.tsx b/libs/ui/lib/side-modal/SideModal.tsx index f101de5ba7..d67c632bb1 100644 --- a/libs/ui/lib/side-modal/SideModal.tsx +++ b/libs/ui/lib/side-modal/SideModal.tsx @@ -65,7 +65,7 @@ SideModal.Title = ({ id, children }: SideModalTitleProps) => { ) } -SideModal.Body = classed.div`body relative overflow-y-auto h-full` +SideModal.Body = classed.div`body relative overflow-y-auto h-full pb-6` SideModal.Section = classed.div`p-8 space-y-6 border-secondary` From ab1110b46fbeef6b6967e96b5a565c9c8494c76a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 11 Apr 2022 18:22:51 -0400 Subject: [PATCH 4/8] Add network interface field to instance create form --- .../fields/NetworkInterfaceField.tsx | 124 ++++++++++++++++++ app/forms/instance-create.tsx | 6 +- app/forms/network-interface-create.tsx | 86 ++++++++++++ 3 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 app/components/fields/NetworkInterfaceField.tsx create mode 100644 app/forms/network-interface-create.tsx diff --git a/app/components/fields/NetworkInterfaceField.tsx b/app/components/fields/NetworkInterfaceField.tsx new file mode 100644 index 0000000000..1884df2b42 --- /dev/null +++ b/app/components/fields/NetworkInterfaceField.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from 'react' +import { useField } from 'formik' +import { Button, Error16Icon, MiniTable, Radio, SideModal } from '@oxide/ui' +import type { + InstanceNetworkInterfaceAttachment, + NetworkInterfaceCreate, +} from '@oxide/api' +import { RadioField } from '@oxide/form' +import CreateNetworkInterfaceForm from 'app/forms/network-interface-create' + +export function NetworkInterfaceField() { + const [showForm, setShowForm] = useState(false) + + /** + * Used to preserve previous user choices in case they accidentally + * change the radio selection + */ + const [oldParams, setOldParams] = useState([]) + + const [, { value }, { setValue }] = + useField({ name: 'networkInterfaces' }) + + useEffect(() => { + if (value.type === 'Create') { + setOldParams(value.params) + } + }, [value]) + + return ( + <> +
+ { + const newType = event.target + .value as InstanceNetworkInterfaceAttachment['type'] + + newType === 'Create' + ? setValue({ type: newType, params: oldParams }) + : setValue({ + type: newType, + }) + }} + > + None + Default + Custom + + {value.type === 'Create' && ( + <> + {value.params.length > 0 && ( + + + Name + VPC + Subnet + {/* For remove button */} + + + + {value.params.map((item, index) => ( + + {item.name} + {item.vpcName} + {item.subnetName} + + + + + ))} + + + )} + + setShowForm(false)} + > + { + setValue({ + type: 'Create', + params: [...value.params, networkInterface], + }) + setShowForm(false) + }} + /> + +
+ +
+ + )} +
+ + ) +} diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 8652ecc98b..2462f3d702 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -25,8 +25,9 @@ import { WindowsResponsiveIcon, } from '@oxide/ui' import { useParams, useToast } from 'app/hooks' -import { DisksTableField } from 'app/components/DisksTableField' +import { DisksTableField } from 'app/components/fields/DisksTableField' import filesize from 'filesize' +import { NetworkInterfaceField } from 'app/components/fields/NetworkInterfaceField' const values = { name: '', @@ -36,6 +37,7 @@ const values = { hostname: '', disks: [], attachedDisks: [], + networkInterfaces: { type: 'Default' }, } export default function CreateInstanceForm({ @@ -210,6 +212,8 @@ export default function CreateInstanceForm({ Networking + + ) { + const queryClient = useApiQueryClient() + const pathParams = useParams('orgName', 'projectName') + + const createNetworkInterface = useApiMutation( + 'instanceNetworkInterfacesPost', + { + onSuccess(data) { + const { instanceName, ...others } = pathParams + invariant( + instanceName, + 'instanceName is required when posting a network interface' + ) + queryClient.invalidateQueries('instanceNetworkInterfacesGet', { + instanceName, + ...others, + }) + onSuccess?.(data) + }, + onError, + } + ) + + return ( +
{ + const { instanceName, ...others } = pathParams + invariant( + instanceName, + 'instanceName is required when posting a network interface' + ) + createNetworkInterface.mutate({ instanceName, ...others, body }) + }) + } + mutation={createNetworkInterface} + {...props} + > + + + + + {/* TODO: Convert these into combo boxes */} + + + + + + {title} + + + + ) +} + +export default CreateNetworkInterfaceForm From 4627679e90885526f6e50408f1f71688f77dad05 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 11 Apr 2022 18:55:31 -0400 Subject: [PATCH 5/8] Add workaround for radio default selection state --- app/components/fields/NetworkInterfaceField.tsx | 3 ++- app/forms/instance-create.tsx | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/components/fields/NetworkInterfaceField.tsx b/app/components/fields/NetworkInterfaceField.tsx index 1884df2b42..98c9a41664 100644 --- a/app/components/fields/NetworkInterfaceField.tsx +++ b/app/components/fields/NetworkInterfaceField.tsx @@ -30,8 +30,9 @@ export function NetworkInterfaceField() { <>
{ diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 2462f3d702..b859b8b0b0 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -38,6 +38,11 @@ const values = { disks: [], attachedDisks: [], networkInterfaces: { type: 'Default' }, + /** + * This is a hack to ensure the network interface radio has a default selection. + * We actually don't care about this value outside of that. + */ + networkInterfaceType: 'Default', } export default function CreateInstanceForm({ From 66d1f8290efc0ae9aa3098bcb81323460f12177a Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 11 Apr 2022 19:03:12 -0400 Subject: [PATCH 6/8] use tiny-invariant --- app/forms/network-interface-create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index b6a209214b..1d9451f60c 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -6,7 +6,7 @@ import { useApiMutation, useApiQueryClient } from '@oxide/api' import type { PrebuiltFormProps } from 'app/forms' import { useParams } from 'app/hooks' -import { invariant } from '@oxide/util' +import invariant from 'tiny-invariant' const values: NetworkInterfaceCreate = { name: '', From cc2777afda0e4cd2e6e4de85546dd129bf58f13f Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Mon, 11 Apr 2022 19:40:19 -0400 Subject: [PATCH 7/8] Update form-types --- app/forms/index.ts | 2 ++ app/forms/network-interface-create.tsx | 4 +--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/forms/index.ts b/app/forms/index.ts index 62b4fa8939..2bf8b84b78 100644 --- a/app/forms/index.ts +++ b/app/forms/index.ts @@ -7,6 +7,7 @@ import type { CreateDiskForm } from './disk-create' import type { CreateProjectForm } from './project-create' import type CreateInstanceForm from './instance-create' import type AttachDiskForm from './disk-attach' +import type CreateNetworkInterfaceForm from './network-interface-create' import type { FormProps } from '@oxide/form' import type { ErrorResponse } from '@oxide/api' @@ -25,6 +26,7 @@ export interface FormTypes { 'disk-create': typeof CreateDiskForm 'subnet-create': typeof CreateSubnetForm 'subnet-edit': typeof EditSubnetForm + 'network-interface-create': typeof CreateNetworkInterfaceForm } export type FormValues = ExtractFormValues< diff --git a/app/forms/network-interface-create.tsx b/app/forms/network-interface-create.tsx index 1d9451f60c..7968ea14f6 100644 --- a/app/forms/network-interface-create.tsx +++ b/app/forms/network-interface-create.tsx @@ -16,7 +16,7 @@ const values: NetworkInterfaceCreate = { vpcName: '', } -export function CreateNetworkInterfaceForm({ +export default function CreateNetworkInterfaceForm({ id = 'create-network-interface-form', title = 'Add Network Interface', initialValues = values, @@ -82,5 +82,3 @@ export function CreateNetworkInterfaceForm({ ) } - -export default CreateNetworkInterfaceForm From f227133e53d6fa60bcc9d3b1bca6520e174c8e12 Mon Sep 17 00:00:00 2001 From: Justin Bennett Date: Tue, 12 Apr 2022 11:02:13 -0400 Subject: [PATCH 8/8] Address PR feedback --- .../fields/NetworkInterfaceField.tsx | 192 +++++++++--------- 1 file changed, 94 insertions(+), 98 deletions(-) diff --git a/app/components/fields/NetworkInterfaceField.tsx b/app/components/fields/NetworkInterfaceField.tsx index 98c9a41664..406e545bd1 100644 --- a/app/components/fields/NetworkInterfaceField.tsx +++ b/app/components/fields/NetworkInterfaceField.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import { useField } from 'formik' import { Button, Error16Icon, MiniTable, Radio, SideModal } from '@oxide/ui' import type { @@ -20,106 +20,102 @@ export function NetworkInterfaceField() { const [, { value }, { setValue }] = useField({ name: 'networkInterfaces' }) - useEffect(() => { - if (value.type === 'Create') { - setOldParams(value.params) - } - }, [value]) - return ( - <> -
- { - const newType = event.target - .value as InstanceNetworkInterfaceAttachment['type'] +
+ { + const newType = event.target + .value as InstanceNetworkInterfaceAttachment['type'] - newType === 'Create' - ? setValue({ type: newType, params: oldParams }) - : setValue({ - type: newType, - }) - }} - > - None - Default - Custom - - {value.type === 'Create' && ( - <> - {value.params.length > 0 && ( - - - Name - VPC - Subnet - {/* For remove button */} - - - - {value.params.map((item, index) => ( - - {item.name} - {item.vpcName} - {item.subnetName} - - - - - ))} - - - )} + if (value.type === 'Create') { + setOldParams(value.params) + } - setShowForm(false)} + newType === 'Create' + ? setValue({ type: newType, params: oldParams }) + : setValue({ + type: newType, + }) + }} + > + None + Default + Custom + + {value.type === 'Create' && ( + <> + {value.params.length > 0 && ( + + + Name + VPC + Subnet + {/* For remove button */} + + + + {value.params.map((item, index) => ( + + {item.name} + {item.vpcName} + {item.subnetName} + + + + + ))} + + + )} + + setShowForm(false)} + > + { + setValue({ + type: 'Create', + params: [...value.params, networkInterface], + }) + setShowForm(false) + }} + /> + +
+ -
- - )} -
- + Add network interface + +
+ + )} +
) }