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) => ( ([]) + + const [, { value }, { setValue }] = + useField({ name: 'networkInterfaces' }) + + return ( +
+ { + const newType = event.target + .value as InstanceNetworkInterfaceAttachment['type'] + + if (value.type === 'Create') { + setOldParams(value.params) + } + + 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/index.ts b/app/forms/index.ts index 6154e968f0..d108b81355 100644 --- a/app/forms/index.ts +++ b/app/forms/index.ts @@ -8,6 +8,7 @@ import type { CreateProjectForm } from './project-create' import type { CreateVpcForm } from './vpc-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' @@ -26,6 +27,7 @@ export interface FormTypes { 'disk-create': typeof CreateDiskForm 'subnet-create': typeof CreateSubnetForm 'subnet-edit': typeof EditSubnetForm + 'network-interface-create': typeof CreateNetworkInterfaceForm 'vpc-create': typeof CreateVpcForm } diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 8652ecc98b..b859b8b0b0 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,12 @@ const values = { hostname: '', 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({ @@ -210,6 +217,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} + + + + ) +} 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 }) 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`