66 * Copyright Oxide Computer Company
77 */
88import * as Accordion from '@radix-ui/react-accordion'
9- import { useEffect , useState } from 'react'
10- import { useWatch } from 'react-hook-form'
9+ import cn from 'classnames'
10+ import { useEffect , useRef , useState } from 'react'
11+ import { useWatch , type Control } from 'react-hook-form'
1112import { useNavigate , type LoaderFunctionArgs } from 'react-router-dom'
1213import type { SetRequired } from 'type-fest'
1314
@@ -421,44 +422,7 @@ export function CreateInstanceForm() {
421422 < FormDivider />
422423 < Form . Heading id = "advanced" > Advanced</ Form . Heading >
423424
424- < Accordion . Root type = "multiple" className = "mt-12" >
425- < Accordion . Item value = "networking" >
426- < AccordionHeader id = "networking" > Networking</ AccordionHeader >
427- < AccordionContent >
428- < NetworkInterfaceField control = { control } disabled = { isSubmitting } />
429-
430- < TextField
431- name = "hostname"
432- tooltipText = "Will be generated if not provided"
433- control = { control }
434- disabled = { isSubmitting }
435- />
436- </ AccordionContent >
437- </ Accordion . Item >
438- < Accordion . Item value = "configuration" >
439- < AccordionHeader id = "configuration" > Configuration</ AccordionHeader >
440- < AccordionContent >
441- < FileField
442- id = "user-data-input"
443- description = {
444- < >
445- Data or scripts to be passed to cloud-init as{ ' ' }
446- < a href = { links . cloudInitFormat } target = "_blank" rel = "noreferrer" >
447- user data
448- </ a > { ' ' }
449- < a href = { links . cloudInitExamples } target = "_blank" rel = "noreferrer" >
450- (examples)
451- </ a > { ' ' }
452- if the selected boot image supports it. Maximum size 32 KiB.
453- </ >
454- }
455- name = "userData"
456- label = "User Data"
457- control = { control }
458- />
459- </ AccordionContent >
460- </ Accordion . Item >
461- </ Accordion . Root >
425+ < AdvancedAccordion control = { control } isSubmitting = { isSubmitting } />
462426
463427 < Form . Actions >
464428 < Form . Submit loading = { createInstance . isPending } > Create instance</ Form . Submit >
@@ -468,20 +432,90 @@ export function CreateInstanceForm() {
468432 )
469433}
470434
471- const AccordionHeader = ( { id, children } : { id : string ; children : React . ReactNode } ) => (
472- < Accordion . Header id = { id } className = "max-w-lg" >
473- < Accordion . Trigger className = "group flex w-full items-center justify-between border-t py-2 text-sans-xl border-secondary [&>svg]:data-[state=open]:rotate-90" >
474- < div className = "text-secondary" > { children } </ div >
475- < DirectionRightIcon className = "transition-all text-secondary" />
476- </ Accordion . Trigger >
477- </ Accordion . Header >
478- )
435+ const AdvancedAccordion = ( {
436+ control,
437+ isSubmitting,
438+ } : {
439+ control : Control < InstanceCreateInput >
440+ isSubmitting : boolean
441+ } ) => {
442+ // we track this state manually for the sole reason that we need to be able to
443+ // tell, inside AccordionItem, when an accordion is opened so we can scroll its
444+ // contents into view
445+ const [ openItems , setOpenItems ] = useState < string [ ] > ( [ ] )
479446
480- const AccordionContent = ( { children } : { children : React . ReactNode } ) => (
481- < Accordion . Content className = "AccordionContent max-w-lg overflow-hidden" >
482- < div className = "ox-accordion-content py-8" > { children } </ div >
483- </ Accordion . Content >
484- )
447+ return (
448+ < Accordion . Root
449+ type = "multiple"
450+ className = "mt-12 max-w-lg"
451+ value = { openItems }
452+ onValueChange = { setOpenItems }
453+ >
454+ < AccordionItem
455+ value = "networking"
456+ label = "Networking"
457+ isOpen = { openItems . includes ( 'networking' ) }
458+ >
459+ < NetworkInterfaceField control = { control } disabled = { isSubmitting } />
460+
461+ < TextField
462+ name = "hostname"
463+ tooltipText = "Will be generated if not provided"
464+ control = { control }
465+ disabled = { isSubmitting }
466+ />
467+ </ AccordionItem >
468+ < AccordionItem
469+ value = "configuration"
470+ label = "Configuration"
471+ isOpen = { openItems . includes ( 'configuration' ) }
472+ >
473+ < FileField
474+ id = "user-data-input"
475+ description = { < UserDataDescription /> }
476+ name = "userData"
477+ label = "User Data"
478+ control = { control }
479+ />
480+ </ AccordionItem >
481+ </ Accordion . Root >
482+ )
483+ }
484+
485+ type AccordionItemProps = {
486+ value : string
487+ isOpen : boolean
488+ label : string
489+ children : React . ReactNode
490+ }
491+
492+ function AccordionItem ( { value, label, children, isOpen } : AccordionItemProps ) {
493+ const contentRef = useRef < HTMLDivElement > ( null )
494+
495+ useEffect ( ( ) => {
496+ if ( isOpen && contentRef . current ) {
497+ contentRef . current . scrollIntoView ( { behavior : 'smooth' } )
498+ }
499+ } , [ isOpen ] )
500+
501+ return (
502+ < Accordion . Item value = { value } >
503+ < Accordion . Header className = "max-w-lg" >
504+ < Accordion . Trigger className = "group flex w-full items-center justify-between border-t py-2 text-sans-xl border-secondary [&>svg]:data-[state=open]:rotate-90" >
505+ < div className = "text-secondary" > { label } </ div >
506+ < DirectionRightIcon className = "transition-all text-secondary" />
507+ </ Accordion . Trigger >
508+ </ Accordion . Header >
509+ < Accordion . Content
510+ ref = { contentRef }
511+ forceMount
512+ className = { cn ( 'ox-accordion-content overflow-hidden py-8' , { hidden : ! isOpen } ) }
513+ >
514+ { children }
515+ </ Accordion . Content >
516+ </ Accordion . Item >
517+ )
518+ }
485519
486520const SshKeysTable = ( ) => {
487521 const keys = usePrefetchedApiQuery ( 'currentUserSshKeyList' , { } ) . data ?. items || [ ]
@@ -580,3 +614,16 @@ const PRESETS = [
580614
581615 { category : 'custom' , id : 'custom' , memory : 0 , ncpus : 0 } ,
582616] as const
617+
618+ const UserDataDescription = ( ) => (
619+ < >
620+ Data or scripts to be passed to cloud-init as{ ' ' }
621+ < a href = { links . cloudInitFormat } target = "_blank" rel = "noreferrer" >
622+ user data
623+ </ a > { ' ' }
624+ < a href = { links . cloudInitExamples } target = "_blank" rel = "noreferrer" >
625+ (examples)
626+ </ a > { ' ' }
627+ if the selected boot image supports it. Maximum size 32 KiB.
628+ </ >
629+ )
0 commit comments