6
6
* Copyright Oxide Computer Company
7
7
*/
8
8
import * 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'
11
12
import { useNavigate , type LoaderFunctionArgs } from 'react-router-dom'
12
13
import type { SetRequired } from 'type-fest'
13
14
@@ -421,44 +422,7 @@ export function CreateInstanceForm() {
421
422
< FormDivider />
422
423
< Form . Heading id = "advanced" > Advanced</ Form . Heading >
423
424
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 } />
462
426
463
427
< Form . Actions >
464
428
< Form . Submit loading = { createInstance . isPending } > Create instance</ Form . Submit >
@@ -468,20 +432,90 @@ export function CreateInstanceForm() {
468
432
)
469
433
}
470
434
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 [ ] > ( [ ] )
479
446
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
+ }
485
519
486
520
const SshKeysTable = ( ) => {
487
521
const keys = usePrefetchedApiQuery ( 'currentUserSshKeyList' , { } ) . data ?. items || [ ]
@@ -580,3 +614,16 @@ const PRESETS = [
580
614
581
615
{ category : 'custom' , id : 'custom' , memory : 0 , ncpus : 0 } ,
582
616
] 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