diff --git a/vmm/src/console_v1.html b/vmm/src/console_v1.html index 6fd5dd61..2f979e2f 100644 --- a/vmm/src/console_v1.html +++ b/vmm/src/console_v1.html @@ -1534,7 +1534,7 @@ } .port-row { - grid-template-columns: 90px 100px 120px 120px 100px; + grid-template-columns: 90px 100px 120px 120px 160px 100px; } .gpu-config-items { @@ -1771,9 +1771,19 @@

Encrypted Environment Variables

const PortMappingEditorComponent = { name: 'PortMappingEditor', props: { + ports: { type: Array, required: true }, + }, + // normalize on initial load + created() { + this.ports.forEach((p) => this.normalizePort(p)); + }, + // normalize again if parent replaces ports array (e.g. after refresh) + watch: { ports: { - type: Array, - required: true, + deep: true, + handler(newPorts) { + newPorts.forEach((p) => this.normalizePort(p)); + }, }, }, template: /* html */ ` @@ -1784,10 +1794,21 @@

Encrypted Environment Variables

- + + + + + + @@ -1796,10 +1817,30 @@

Encrypted Environment Variables

`, methods: { + // derives mode + custom_ip from host_address when editing existing VMs + normalizePort(port) { + // If mode already set, keep it (don’t fight the user while editing) + if (port.host_address_mode) + return; + if (port.host_address === '127.0.0.1') { + port.host_address_mode = 'local'; + port.custom_ip = ''; + } + else if (port.host_address === '0.0.0.0') { + port.host_address_mode = 'public'; + port.custom_ip = ''; + } + else { + port.host_address_mode = 'custom'; + port.custom_ip = port.host_address; // show dedicated IP in input + } + }, addPort() { this.ports.push({ protocol: 'tcp', host_address: '127.0.0.1', + host_address_mode: 'local', + custom_ip: '', host_port: null, vm_port: null, }); @@ -1807,6 +1848,31 @@

Encrypted Environment Variables

removePort(index) { this.ports.splice(index, 1); }, + onModeChange(port) { + if (port.host_address_mode === 'local') { + port.host_address = '127.0.0.1'; + port.custom_ip = ''; + } + else if (port.host_address_mode === 'public') { + port.host_address = '0.0.0.0'; + port.custom_ip = ''; + } + else if (port.host_address_mode === 'custom') { + // if coming from existing custom, keep it; otherwise start empty + if (!port.custom_ip || port.custom_ip === '') { + // if host_address already contains a non-standard IP, reuse it + if (port.host_address !== '127.0.0.1' && port.host_address !== '0.0.0.0') { + port.custom_ip = port.host_address; + } + } + port.host_address = port.custom_ip || ''; + } + }, + onCustomIPChange(port) { + if (port.host_address_mode === 'custom') { + port.host_address = port.custom_ip || ''; + } + }, }, }; module.exports = PortMappingEditorComponent; @@ -16063,7 +16129,7 @@

Derive VM

}, map: {"protobufjs/minimal":"node_modules/protobufjs/minimal.js"} }, 'build/ts/templates/app.html': { factory: function(module, exports, require) { -module.exports = "\n\n
\n
\n
\n
\n

dstack-vmm

\n \n v{{ version.version }}\n \n \n
\n
\n \n
\n \n
\n \n \n \n \n
\n
\n
\n
\n
\n\n \n\n \n\n \n\n
\n
\n
\n \n \n \n \n \n \n
\n
\n Total Instances:\n {{ totalVMs }}\n
\n
\n
\n
\n \n
\n \n /\n {{ maxPage || 1 }}\n
\n \n \n
\n
\n
\n\n
\n
\n
\n
Name
\n
Status
\n
Uptime
\n
View
\n
Actions
\n
\n\n
\n
\n
\n \n
\n
\n {{ vm.name }}\n
\n
\n \n \n {{ vmStatus(vm) }}\n \n
\n
{{ vm.status !== 'stopped' ? shortUptime(vm.uptime) : '-' }}
\n
\n Logs\n Stderr\n Board\n
\n
\n
\n \n
\n \n \n \n \n \n \n
\n
\n
\n
\n\n
\n
\n
\n VM ID\n
\n {{ vm.id }}\n \n
\n
\n
\n Instance ID\n
\n {{ vm.instance_id }}\n \n
\n -\n
\n
\n App ID\n
\n {{ vm.app_id }}\n \n
\n -\n
\n
\n Image\n {{ vm.configuration?.image }}\n
\n
\n vCPUs\n {{ vm.configuration?.vcpu }}\n
\n
\n Memory\n {{ formatMemory(vm.configuration?.memory) }}\n
\n
\n Swap\n {{ formatMemory(bytesToMB(vm.configuration.swap_size)) }}\n
\n
\n Disk Size\n {{ vm.configuration?.disk_size }} GB\n
\n
\n Disk Type\n {{ vm.configuration?.disk_type || 'virtio-pci' }}\n
\n
\n TEE\n {{ vm.configuration?.no_tee ? 'Disabled' : 'Enabled' }}\n
\n
\n GPUs\n
\n \n All GPUs\n \n
\n
\n \n {{ gpu.slot || gpu.product_id || ('GPU #' + (index + 1)) }}\n \n
\n
\n None\n
\n
\n
\n\n
\n

Port Mappings

\n
\n {{ port.host_address === '127.0.0.1' ? 'Local' : 'Public' }}\n {{ port.protocol.toUpperCase() }}: {{ port.host_port }} → {{ port.vm_port }}\n
\n
\n\n
\n

Features

\n {{ getVmFeatures(vm) }}\n
\n\n
\n

Network Interfaces

\n
\n
\n
\n
\n \n \n \n \n {{ iface.name }}\n
\n
\n
\n
\n MAC Address\n {{ iface.mac || '-' }}\n
\n
\n IP Address\n {{ iface.addresses.map(addr => addr.address + '/' + addr.prefix).join(', ') || '-' }}\n
\n
\n
\n
\n \n \n \n
\n
\n RX\n {{ iface.rx_bytes }} bytes\n 0\">({{ iface.rx_errors }} errors)\n
\n
\n
\n
\n \n \n \n
\n
\n TX\n {{ iface.tx_bytes }} bytes\n 0\">({{ iface.tx_errors }} errors)\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n \n \n WireGuard Info\n

\n
{{ networkInfo[vm.id].wg_info }}
\n
\n
\n\n
\n
\n

App Compose

\n
\n \n \n
\n
\n
\n
{{ vm.appCompose?.docker_compose_file || 'Docker Compose content not available' }}
\n
\n
\n\n
\n
\n

User Config

\n \n
\n
{{ vm.configuration.user_config }}
\n
\n\n
\n \n \n \n
\n
\n
\n
\n\n
\n
\n \n
\n
\n
\n \n
\n
\n
\n {{ errorMessage }}\n \n
\n
\n
\n"; +module.exports = "\n\n
\n
\n
\n
\n

dstack-vmm

\n \n v{{ version.version }}\n \n \n
\n
\n \n
\n \n
\n \n \n \n \n
\n
\n
\n
\n
\n\n \n\n \n\n \n\n
\n
\n
\n \n \n \n \n \n \n
\n
\n Total Instances:\n {{ totalVMs }}\n
\n
\n
\n
\n \n
\n \n /\n {{ maxPage || 1 }}\n
\n \n \n
\n
\n
\n\n
\n
\n
\n
Name
\n
Status
\n
Uptime
\n
View
\n
Actions
\n
\n\n
\n
\n
\n \n
\n
\n {{ vm.name }}\n
\n
\n \n \n {{ vmStatus(vm) }}\n \n
\n
{{ vm.status !== 'stopped' ? shortUptime(vm.uptime) : '-' }}
\n
\n Logs\n Stderr\n Board\n
\n
\n
\n \n
\n \n \n \n \n \n \n
\n
\n
\n
\n\n
\n
\n
\n VM ID\n
\n {{ vm.id }}\n \n
\n
\n
\n Instance ID\n
\n {{ vm.instance_id }}\n \n
\n -\n
\n
\n App ID\n
\n {{ vm.app_id }}\n \n
\n -\n
\n
\n Image\n {{ vm.configuration?.image }}\n
\n
\n vCPUs\n {{ vm.configuration?.vcpu }}\n
\n
\n Memory\n {{ formatMemory(vm.configuration?.memory) }}\n
\n
\n Swap\n {{ formatMemory(bytesToMB(vm.configuration.swap_size)) }}\n
\n
\n Disk Size\n {{ vm.configuration?.disk_size }} GB\n
\n
\n Disk Type\n {{ vm.configuration?.disk_type || 'virtio-pci' }}\n
\n
\n TEE\n {{ vm.configuration?.no_tee ? 'Disabled' : 'Enabled' }}\n
\n
\n GPUs\n
\n \n All GPUs\n \n
\n
\n \n {{ gpu.slot || gpu.product_id || ('GPU #' + (index + 1)) }}\n \n
\n
\n None\n
\n
\n
\n\n
\n

Port Mappings

\n
\n {{\n port.host_address === '127.0.0.1'\n ? 'Local'\n : (port.host_address === '0.0.0.0' ? 'Public' : port.host_address)\n }}\n {{ port.protocol.toUpperCase() }}: {{ port.host_port }} → {{ port.vm_port }}\n
\n
\n\n
\n

Features

\n {{ getVmFeatures(vm) }}\n
\n\n
\n

Network Interfaces

\n
\n
\n
\n
\n \n \n \n \n {{ iface.name }}\n
\n
\n
\n
\n MAC Address\n {{ iface.mac || '-' }}\n
\n
\n IP Address\n {{ iface.addresses.map(addr => addr.address + '/' + addr.prefix).join(', ') || '-' }}\n
\n
\n
\n
\n \n \n \n
\n
\n RX\n {{ iface.rx_bytes }} bytes\n 0\">({{ iface.rx_errors }} errors)\n
\n
\n
\n
\n \n \n \n
\n
\n TX\n {{ iface.tx_bytes }} bytes\n 0\">({{ iface.tx_errors }} errors)\n
\n
\n
\n
\n
\n
\n
\n

\n \n \n \n \n WireGuard Info\n

\n
{{ networkInfo[vm.id].wg_info }}
\n
\n
\n\n
\n
\n

App Compose

\n
\n \n \n
\n
\n
\n
{{ vm.appCompose?.docker_compose_file || 'Docker Compose content not available' }}
\n
\n
\n\n
\n
\n

User Config

\n \n
\n
{{ vm.configuration.user_config }}
\n
\n\n
\n \n \n \n
\n
\n
\n
\n\n
\n
\n \n
\n
\n
\n \n
\n
\n
\n {{ errorMessage }}\n \n
\n
\n
\n"; }, map: {} } }; const cache = {}; diff --git a/vmm/ui/src/components/PortMappingEditor.ts b/vmm/ui/src/components/PortMappingEditor.ts index 8658b8bb..5fcf730a 100644 --- a/vmm/ui/src/components/PortMappingEditor.ts +++ b/vmm/ui/src/components/PortMappingEditor.ts @@ -4,22 +4,40 @@ type PortEntry = { protocol: string; host_address: string; + host_address_mode?: string; // "local" | "public" | "custom" host_port: number | null; vm_port: number | null; + custom_ip?: string; // User-entered IP for custom mode }; +// ... keep your types as-is ... + type ComponentInstance = { ports: PortEntry[]; + normalizePort: (p: PortEntry) => void; }; const PortMappingEditorComponent = { name: 'PortMappingEditor', props: { + ports: { type: Array, required: true }, + }, + + // normalize on initial load + created(this: ComponentInstance) { + this.ports.forEach((p) => this.normalizePort(p)); + }, + + // normalize again if parent replaces ports array (e.g. after refresh) + watch: { ports: { - type: Array, - required: true, + deep: true, + handler(this: ComponentInstance, newPorts: PortEntry[]) { + newPorts.forEach((p) => this.normalizePort(p)); + }, }, }, + template: /* html */ `
@@ -28,10 +46,21 @@ const PortMappingEditorComponent = { - + + + + + + @@ -39,18 +68,64 @@ const PortMappingEditorComponent = {
`, + methods: { + // derives mode + custom_ip from host_address when editing existing VMs + normalizePort(this: ComponentInstance, port: PortEntry) { + // If mode already set, keep it (don’t fight the user while editing) + if (port.host_address_mode) return; + + if (port.host_address === '127.0.0.1') { + port.host_address_mode = 'local'; + port.custom_ip = ''; + } else if (port.host_address === '0.0.0.0') { + port.host_address_mode = 'public'; + port.custom_ip = ''; + } else { + port.host_address_mode = 'custom'; + port.custom_ip = port.host_address; // show dedicated IP in input + } + }, + addPort(this: ComponentInstance) { this.ports.push({ protocol: 'tcp', host_address: '127.0.0.1', + host_address_mode: 'local', + custom_ip: '', host_port: null, vm_port: null, }); }, + removePort(this: ComponentInstance, index: number) { this.ports.splice(index, 1); }, + + onModeChange(port: PortEntry) { + if (port.host_address_mode === 'local') { + port.host_address = '127.0.0.1'; + port.custom_ip = ''; + } else if (port.host_address_mode === 'public') { + port.host_address = '0.0.0.0'; + port.custom_ip = ''; + } else if (port.host_address_mode === 'custom') { + // if coming from existing custom, keep it; otherwise start empty + if (!port.custom_ip || port.custom_ip === '') { + // if host_address already contains a non-standard IP, reuse it + if (port.host_address !== '127.0.0.1' && port.host_address !== '0.0.0.0') { + port.custom_ip = port.host_address; + } + } + port.host_address = port.custom_ip || ''; + } + }, + + onCustomIPChange(port: PortEntry) { + if (port.host_address_mode === 'custom') { + port.host_address = port.custom_ip || ''; + } + }, }, }; diff --git a/vmm/ui/src/styles/main.css b/vmm/ui/src/styles/main.css index 45b267a9..54def8a5 100644 --- a/vmm/ui/src/styles/main.css +++ b/vmm/ui/src/styles/main.css @@ -1522,7 +1522,7 @@ h1, h2, h3, h4, h5, h6 { } .port-row { - grid-template-columns: 90px 100px 120px 120px 100px; + grid-template-columns: 90px 100px 120px 120px 160px 100px; } .gpu-config-items { diff --git a/vmm/ui/src/templates/app.html b/vmm/ui/src/templates/app.html index b55550fa..a9660454 100644 --- a/vmm/ui/src/templates/app.html +++ b/vmm/ui/src/templates/app.html @@ -320,7 +320,11 @@

dstack-vmm

Port Mappings

- {{ port.host_address === '127.0.0.1' ? 'Local' : 'Public' }} + {{ + port.host_address === '127.0.0.1' + ? 'Local' + : (port.host_address === '0.0.0.0' ? 'Public' : port.host_address) + }} {{ port.protocol.toUpperCase() }}: {{ port.host_port }} → {{ port.vm_port }}