From f202070ce9bd66215665b861965cc086b22d86cb Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Fri, 10 May 2024 15:10:06 -0700 Subject: [PATCH 1/4] Add docsPopover prop to FullPageForm, and implementation --- app/components/form/FullPageForm.tsx | 3 +++ app/forms/instance-create.tsx | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/components/form/FullPageForm.tsx b/app/components/form/FullPageForm.tsx index 3f3bc94c96..9d002a2c5c 100644 --- a/app/components/form/FullPageForm.tsx +++ b/app/components/form/FullPageForm.tsx @@ -44,6 +44,7 @@ interface FullPageFormProps { * constrain the `name` prop to paths in the values object. */ children: ReactNode + docsPopover?: ReactNode } const PageActionsContainer = classed.div`flex h-20 items-center gutter` @@ -59,6 +60,7 @@ export function FullPageForm({ form, onSubmit, submitError, + docsPopover, }: FullPageFormProps) { const { isSubmitting, isDirty, isSubmitSuccessful } = form.formState @@ -87,6 +89,7 @@ export function FullPageForm({ <> {title} + {docsPopover}
} + summary="Instances are virtual machines that run on the Oxide platform." + links={[docLinks.instances, docLinks.vms]} + /> + } > From 94a1af17c809a3c119009b68f2d1df9237d32ee6 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 13 May 2024 14:57:02 -0700 Subject: [PATCH 2/4] Move page header to instance-create --- app/components/form/FullPageForm.tsx | 13 +- app/forms/instance-create.tsx | 550 ++++++++++++++------------- 2 files changed, 286 insertions(+), 277 deletions(-) diff --git a/app/components/form/FullPageForm.tsx b/app/components/form/FullPageForm.tsx index 9d002a2c5c..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 @@ -44,23 +41,19 @@ interface FullPageFormProps { * constrain the `name` prop to paths in the values object. */ children: ReactNode - docsPopover?: ReactNode } const PageActionsContainer = classed.div`flex h-20 items-center gutter` export function FullPageForm({ id, - title, children, submitDisabled, error, - icon, loading, form, onSubmit, submitError, - docsPopover, }: FullPageFormProps) { const { isSubmitting, isDirty, isSubmitSuccessful } = form.formState @@ -87,10 +80,6 @@ export function FullPageForm({ return ( <> - - {title} - {docsPopover} - } - 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} - docsPopover={ + <> + + }>Create instance } summary="Instances are virtual machines that run on the Oxide platform." links={[docLinks.instances, docLinks.vms]} /> - } - > - - - - 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) - } + + { + 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} > - - - 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 && ( - - )} - + + - {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 }))} /> + + + ) } From 92b45c492030658f9f079e74023f5581806bb3a3 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 13 May 2024 15:12:16 -0700 Subject: [PATCH 3/4] Add link to Quick Start --- app/pages/project/floating-ips/FloatingIpsPage.tsx | 3 +++ app/util/links.ts | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/app/pages/project/floating-ips/FloatingIpsPage.tsx b/app/pages/project/floating-ips/FloatingIpsPage.tsx index 9f7b14a456..5960ce0450 100644 --- a/app/pages/project/floating-ips/FloatingIpsPage.tsx +++ b/app/pages/project/floating-ips/FloatingIpsPage.tsx @@ -59,6 +59,9 @@ FloatingIpsPage.loader = async ({ params }: LoaderFunctionArgs) => { apiQueryClient.prefetchQuery('instanceList', { query: { project }, }), + apiQueryClient.prefetchQuery('projectIpPoolList', { + query: { limit: 1000 }, + }), ]) return null } 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', From 3f210638b7be455bd432a948e149a580aaf7318e Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 13 May 2024 15:14:36 -0700 Subject: [PATCH 4/4] Finish adding Quick Start --- app/forms/instance-create.tsx | 2 +- app/pages/project/floating-ips/FloatingIpsPage.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx index 581f065d2f..5bbffa4cb9 100644 --- a/app/forms/instance-create.tsx +++ b/app/forms/instance-create.tsx @@ -279,7 +279,7 @@ export function CreateInstanceForm() { heading="instances" icon={} summary="Instances are virtual machines that run on the Oxide platform." - links={[docLinks.instances, docLinks.vms]} + links={[docLinks.instances, docLinks.vms, docLinks.quickStart]} /> { apiQueryClient.prefetchQuery('instanceList', { query: { project }, }), - apiQueryClient.prefetchQuery('projectIpPoolList', { - query: { limit: 1000 }, - }), ]) return null }