diff --git a/app/components/form/FullPageForm.tsx b/app/components/form/FullPageForm.tsx index 3f3bc94c96..4ca6f79b91 100644 --- a/app/components/form/FullPageForm.tsx +++ b/app/components/form/FullPageForm.tsx @@ -5,14 +5,13 @@ * * Copyright Oxide Computer Company */ -import { cloneElement, useEffect, type ReactElement, type ReactNode } from 'react' +import { cloneElement, useEffect, type ReactNode } from 'react' import type { FieldValues, UseFormReturn } from 'react-hook-form' import { useBlocker, type Blocker } from 'react-router-dom' import type { ApiError } from '@oxide/api' import { Modal } from '~/ui/lib/Modal' -import { PageHeader, PageTitle } from '~/ui/lib/PageHeader' import { flattenChildren, pluckFirstOfType } from '~/util/children' import { classed } from '~/util/classed' @@ -21,8 +20,6 @@ import { PageActions } from '../PageActions' interface FullPageFormProps { id: string - title: string - icon: ReactElement /** Must provide a reason for submit being disabled */ submitDisabled?: string error?: Error @@ -50,11 +47,9 @@ const PageActionsContainer = classed.div`flex h-20 items-center gutter` export function FullPageForm({ id, - title, children, submitDisabled, error, - icon, loading, form, onSubmit, @@ -85,9 +80,6 @@ export function FullPageForm({ return ( <> - - {title} -
} - onSubmit={async (values) => { - setIsSubmitting(true) - // we should never have a presetId that's not in the list - const preset = PRESETS.find((option) => option.id === values.presetId)! - const instance = - values.presetId === 'custom' - ? { memory: values.memory, ncpus: values.ncpus } - : { memory: preset.memory, ncpus: preset.ncpus } - - const bootDisk = getBootDiskAttachment(values) - - const userData = values.userData - ? await readBlobAsBase64(values.userData) - : undefined - - await createInstance.mutateAsync({ - query: { project }, - body: { - name: values.name, - hostname: values.hostname || values.name, - description: values.description, - memory: instance.memory * GiB, - ncpus: instance.ncpus, - disks: [bootDisk, ...values.disks], - externalIps: values.externalIps, - start: values.start, - networkInterfaces: values.networkInterfaces, - sshPublicKeys: values.sshPublicKeys, - userData, - }, - }) - }} - loading={createInstance.isPending} - submitError={createInstance.error} - > - - - - Start Instance - - - Hardware - - Pick a pre-configured machine type that offers balanced vCPU and memory for most - workloads or create a custom machine. - - { - // Having an option selected from a non-current tab is confusing, - // especially in combination with the custom inputs. So we auto - // select the first option from the current tab - const firstOption = PRESETS.find((preset) => preset.category === val) - if (firstOption) { - setValue('presetId', firstOption.id) - } - }} - > - - - General Purpose - - - High CPU - - - High Memory - - - Custom - - - - - {renderLargeRadioCards('general')} - - - - - - {renderLargeRadioCards('highCPU')} - - - - - - {renderLargeRadioCards('highMemory')} - - - - - { - if (cpus < 1) { - return `Must be at least 1 vCPU` - } - if (cpus > INSTANCE_MAX_CPU) { - return `CPUs capped to ${INSTANCE_MAX_CPU}` - } - }} - disabled={isSubmitting} - /> - { - if (memory < 1) { - return `Must be at least 1 GiB` - } - if (memory > INSTANCE_MAX_RAM_GiB) { - return `Can be at most ${INSTANCE_MAX_RAM_GiB} GiB` - } - }} - disabled={isSubmitting} - /> - - - - - - Boot disk - { - setValue('bootDiskSourceType', val as BootDiskSourceType) - if (imageSizeGiB && imageSizeGiB > bootDiskSize) { - setValue('bootDiskSize', nearest10(imageSizeGiB)) - } + <> + + }>Create instance + } + summary="Instances are virtual machines that run on the Oxide platform." + links={[docLinks.instances, docLinks.vms, docLinks.quickStart]} + /> + + { + setIsSubmitting(true) + // we should never have a presetId that's not in the list + const preset = PRESETS.find((option) => option.id === values.presetId)! + const instance = + values.presetId === 'custom' + ? { memory: values.memory, ncpus: values.ncpus } + : { memory: preset.memory, ncpus: preset.ncpus } + + const bootDisk = getBootDiskAttachment(values) + + const userData = values.userData + ? await readBlobAsBase64(values.userData) + : undefined + + await createInstance.mutateAsync({ + query: { project }, + body: { + name: values.name, + hostname: values.hostname || values.name, + description: values.description, + memory: instance.memory * GiB, + ncpus: instance.ncpus, + disks: [bootDisk, ...values.disks], + externalIps: values.externalIps, + start: values.start, + networkInterfaces: values.networkInterfaces, + sshPublicKeys: values.sshPublicKeys, + userData, + }, + }) }} + loading={createInstance.isPending} + submitError={createInstance.error} > - - - Silo images - - - Project images - - - Existing disks - - - {allImages.length === 0 && disks.length === 0 && ( - - )} - + + - {siloImages.length === 0 ? ( -
- } - title="No silo images found" - body="Promote a project image to see it here" - /> -
- ) : ( - <> - - {bootDiskSizeAndName} - - )} -
- + + Hardware + + Pick a pre-configured machine type that offers balanced vCPU and memory for most + workloads or create a custom machine. + + { + // Having an option selected from a non-current tab is confusing, + // especially in combination with the custom inputs. So we auto + // select the first option from the current tab + const firstOption = PRESETS.find((preset) => preset.category === val) + if (firstOption) { + setValue('presetId', firstOption.id) + } + }} > - {projectImages.length === 0 ? ( -
- } - title="No project images found" - body="Upload an image to see it here" - buttonText="Upload image" - onClick={() => navigate(pb.projectImagesNew({ project }))} - /> -
- ) : ( - <> - - {bootDiskSizeAndName} - - )} -
- - - {disks.length === 0 ? ( -
- } - title="No detached disks found" - body="Only detached disks can be used as a boot disk" - /> -
- ) : ( - + + General Purpose + + + High CPU + + + High Memory + + + Custom + + + + + {renderLargeRadioCards('general')} + + + + + + {renderLargeRadioCards('highCPU')} + + + + + + {renderLargeRadioCards('highMemory')} + + + + + { + if (cpus < 1) { + return `Must be at least 1 vCPU` + } + if (cpus > INSTANCE_MAX_CPU) { + return `CPUs capped to ${INSTANCE_MAX_CPU}` + } + }} + disabled={isSubmitting} + /> + { + if (memory < 1) { + return `Must be at least 1 GiB` + } + if (memory > INSTANCE_MAX_RAM_GiB) { + return `Can be at most ${INSTANCE_MAX_RAM_GiB} GiB` + } + }} + disabled={isSubmitting} + /> + +
+ + + + Boot disk + { + setValue('bootDiskSourceType', val as BootDiskSourceType) + if (imageSizeGiB && imageSizeGiB > bootDiskSize) { + setValue('bootDiskSize', nearest10(imageSizeGiB)) + } + }} + > + + + Silo images + + + Project images + + + Existing disks + + + {allImages.length === 0 && disks.length === 0 && ( + )} - - - - Additional disks - - - Authentication - - - Advanced - - - Create instance - navigate(pb.instances({ project }))} /> - - + + {siloImages.length === 0 ? ( +
+ } + title="No silo images found" + body="Promote a project image to see it here" + /> +
+ ) : ( + <> + + {bootDiskSizeAndName} + + )} +
+ + {projectImages.length === 0 ? ( +
+ } + title="No project images found" + body="Upload an image to see it here" + buttonText="Upload image" + onClick={() => navigate(pb.projectImagesNew({ project }))} + /> +
+ ) : ( + <> + + {bootDiskSizeAndName} + + )} +
+ + + {disks.length === 0 ? ( +
+ } + title="No detached disks found" + body="Only detached disks can be used as a boot disk" + /> +
+ ) : ( + + )} +
+ + + Additional disks + + + Authentication + + + Advanced + + + Create instance + navigate(pb.instances({ project }))} /> + + + ) } diff --git a/app/util/links.ts b/app/util/links.ts index b9b5a4298a..e1af292667 100644 --- a/app/util/links.ts +++ b/app/util/links.ts @@ -22,6 +22,7 @@ export const links: Record = { keyConceptsProjectsDocs: 'https://docs.oxide.computer/guides/key-entities-and-concepts#_projects', projectsDocs: 'https://docs.oxide.computer/guides/onboarding-projects', + quickStart: 'https://docs.oxide.computer/guides/quickstart', sledDocs: 'https://docs.oxide.computer/guides/architecture/service-processors#_server_sled', snapshotsDocs: @@ -74,6 +75,10 @@ export const docLinks = { href: links.projectsDocs, linkText: 'Managing Projects', }, + quickStart: { + href: links.quickStart, + linkText: 'Quick Start', + }, sleds: { href: links.sledDocs, linkText: 'Server Sleds',