Skip to content

Update instance-create to handle both v4 and v6 ephemeral IP options#3057

Merged
david-crespo merged 24 commits intomainfrom
dual_ephemeral_ips
Feb 27, 2026
Merged

Update instance-create to handle both v4 and v6 ephemeral IP options#3057
david-crespo merged 24 commits intomainfrom
dual_ephemeral_ips

Conversation

@charliepark
Copy link
Contributor

@charliepark charliepark commented Feb 7, 2026

Instances can now have both IPv4- and IPv6-backed ephemeral IPs. This updates the instance create flow to account for that.

It checks / unchecks the ephemeral IP box for the IP version(s) specified in the Network Interfaces section of the form.

Default NICs
Screenshot 2026-02-06 at 4 47 32 PM
Screenshot 2026-02-06 at 4 47 49 PM
Screenshot 2026-02-06 at 4 47 56 PM

Custom NICs
Screenshot 2026-02-06 at 4 48 16 PM
Screenshot 2026-02-06 at 4 48 34 PM
Screenshot 2026-02-06 at 4 48 53 PM

No NIC
Screenshot 2026-02-06 at 4 49 01 PM

Closes #3041

@vercel
Copy link

vercel bot commented Feb 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
console Ready Ready Preview Feb 27, 2026 11:28pm

Request Review

@david-crespo
Copy link
Collaborator

At first I thought maybe we can do without the label, but we do need to indicate somehow what the pool is.

image

What if we put it in the label? Like Allocate IPv6 address from pool:

image

Claude was smart and figured out that the "from pool" should be conditional:

image

The diff is really small, it's like 90% about plumbing through aria-label to make sure we still have a label on the field for screenreaders.

Diff to produce the above ```diff diff --git a/app/components/form/fields/IpPoolSelector.tsx b/app/components/form/fields/IpPoolSelector.tsx index e40b696b22..67893f53f4 100644 --- a/app/components/form/fields/IpPoolSelector.tsx +++ b/app/components/form/fields/IpPoolSelector.tsx @@ -56,6 +56,7 @@ required?: boolean hideOptionalTag?: boolean label?: string + 'aria-label'?: string }

export function IpPoolSelector<
@@ -71,6 +72,7 @@
required = true,
hideOptionalTag = false,
label = 'Pool',

  • 'aria-label': ariaLabel,
    }: IpPoolSelectorProps<TFieldValues, TName>) {
    // Note: pools are already filtered by poolType before being passed to this component
    const sortedPools = useMemo(() => {
    @@ -89,6 +91,7 @@
    name={poolFieldName}
    items={sortedPools.map(toIpPoolItem)}
    label={label}
  •    aria-label={ariaLabel}
       noItemsPlaceholder="No pools available"
       control={control}
       placeholder="Select a pool"
    

diff --git a/app/components/form/fields/ListboxField.tsx b/app/components/form/fields/ListboxField.tsx
index 445c8e810c..d93b6488f9 100644
--- a/app/components/form/fields/ListboxField.tsx
+++ b/app/components/form/fields/ListboxField.tsx
@@ -26,6 +26,8 @@
placeholder?: string
className?: string
label?: string

  • /** Accessible label for the button when no visible label is rendered */
  • 'aria-label'?: string
    required?: boolean
    description?: string | React.ReactNode
    control: Control
    @@ -54,6 +56,7 @@
    isLoading,
    noItemsPlaceholder,
    hideOptionalTag,
  • 'aria-label': ariaLabel,
    }: ListboxFieldProps<TFieldValues, TName>) {
    // TODO: recreate this logic
    // validate: (v) => (required && !v ? ${name} is required : undefined),
    @@ -63,6 +66,7 @@
    <Listbox
    description={description}
    label={label}
  •    aria-label={ariaLabel}
       required={required}
       placeholder={placeholder}
       noItemsPlaceholder={noItemsPlaceholder}
    

diff --git a/app/forms/instance-create.tsx b/app/forms/instance-create.tsx
index 4b1e9b923c..c2b2f72701 100644
--- a/app/forms/instance-create.tsx
+++ b/app/forms/instance-create.tsx
@@ -1000,12 +1000,13 @@
name={checkboxName}
disabled={!canAttach || isSubmitting}
>

  •          Allocate and attach an ephemeral {displayVersion} address
    
  •          Allocate {displayVersion} address
    
  •          {checked && ' from pool:'}
           </CheckboxField>
         </span>
       </Wrap>
       {checked && (
    
  •      <div className="ml-6">
    
  •      <div className="my-2 ml-6">
           <IpPoolSelector
             control={control}
             poolFieldName={poolFieldName}
    

@@ -1013,7 +1014,8 @@
disabled={isSubmitting}
required={false}
hideOptionalTag

  •          label={`${displayVersion} pool`}
    
  •          label=""
    
  •          aria-label={`${displayVersion} pool`}
           />
         </div>
       )}
    

diff --git a/app/ui/lib/Listbox.tsx b/app/ui/lib/Listbox.tsx
index 0691124bf6..8bf631ff67 100644
--- a/app/ui/lib/Listbox.tsx
+++ b/app/ui/lib/Listbox.tsx
@@ -37,6 +37,8 @@
hasError?: boolean
name?: string
label?: React.ReactNode

  • /** Accessible label for the button when no visible label is rendered */
  • 'aria-label'?: string
    description?: React.ReactNode
    required?: boolean
    isLoading?: boolean
    @@ -63,6 +65,7 @@
    buttonRef,
    hideOptionalTag,
    hideSelected = false,
  • 'aria-label': ariaLabel,
    ...props
    }: ListboxProps) => {
    const selectedItem = selected && items.find((i) => i.value === selected)
    @@ -114,6 +117,7 @@
    hideSelected ? 'w-auto' : 'w-full'
    )}
    ref={buttonRef}
  •          aria-label={ariaLabel}
             {...props}
           >
             {!hideSelected && (
    

</details>

@david-crespo
Copy link
Collaborator

With the refactors, this came in at barely a net add in app code — all the additions are tests, which is great!

$ jj diff -f 'fork_point(trunk() | @)' --stat app
app/components/form/fields/IpPoolSelector.tsx |  11 +-
app/components/form/fields/ListboxField.tsx   |   4 +
app/forms/instance-create.tsx                 | 337 ++++++++++++++--------------
app/ui/lib/Listbox.tsx                        |   8 +-
4 files changed, 194 insertions(+), 166 deletions(-)

@david-crespo david-crespo merged commit 2fb10dc into main Feb 27, 2026
7 checks passed
@david-crespo david-crespo deleted the dual_ephemeral_ips branch February 27, 2026 23:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Instance create: allow creating ephemeral IPs of both versions

2 participants