Release rolling / release (push) Successful in 7m47s
Validate port mappings for empty host ports, in-form duplicates, privileged low ports, and peer-side host port conflicts. Runs on template deploy, deploy view, and add-container before create RPC.
597 lines
21 KiB
JavaScript
597 lines
21 KiB
JavaScript
/**
|
|
* Add container page — blank create form (separate from Deploy templates).
|
|
* Field IDs use the addc- prefix. Submit goes through deployContainerWithSteps.
|
|
*/
|
|
import {
|
|
showStatusIndicator,
|
|
hideStatusIndicator,
|
|
showAlert,
|
|
} from './uiUtils.js'
|
|
import { presentError } from '../client/errors.js'
|
|
import {
|
|
precheckDeployNetworking,
|
|
formatNetworkingPrecheckMessage,
|
|
} from '../client/deployNetworkPrecheck.js'
|
|
|
|
let portSeq = 0
|
|
let volSeq = 0
|
|
let envSeq = 0
|
|
let labelSeq = 0
|
|
let dnsSeq = 0
|
|
let hostSeq = 0
|
|
let capSeq = 0
|
|
|
|
function el(id) {
|
|
return document.getElementById(id)
|
|
}
|
|
|
|
function val(id) {
|
|
return el(id)?.value?.trim?.() || ''
|
|
}
|
|
|
|
function checked(id) {
|
|
return Boolean(el(id)?.checked)
|
|
}
|
|
|
|
function numOrNull(id, parse = parseFloat) {
|
|
const raw = el(id)?.value
|
|
if (raw === '' || raw == null) return null
|
|
const n = parse(raw)
|
|
return Number.isFinite(n) && n > 0 ? n : null
|
|
}
|
|
|
|
/**
|
|
* Clear dynamic rows and reset primary fields.
|
|
*/
|
|
export function resetAddContainerForm() {
|
|
for (const id of [
|
|
'addc-ports',
|
|
'addc-volumes',
|
|
'addc-env',
|
|
'addc-labels',
|
|
'addc-dns',
|
|
'addc-extra-hosts',
|
|
'addc-capabilities',
|
|
]) {
|
|
const c = el(id)
|
|
if (c) c.innerHTML = ''
|
|
}
|
|
const form = el('add-container-form')
|
|
if (form) form.reset()
|
|
// Defaults after reset
|
|
const always = el('addc-always-pull')
|
|
if (always) always.checked = true
|
|
const detach = el('addc-detach')
|
|
if (detach) detach.checked = true
|
|
const net = el('addc-network-mode')
|
|
if (net) net.value = 'bridge'
|
|
const restart = el('addc-restart-policy')
|
|
if (restart) restart.value = 'no'
|
|
toggleAddcCustomNetwork()
|
|
}
|
|
|
|
export function addcAddPort(portData = null) {
|
|
const container = el('addc-ports')
|
|
if (!container) return
|
|
const id = `addc-port-${++portSeq}`
|
|
let hostPort = ''
|
|
let containerPort = ''
|
|
let protocol = 'tcp'
|
|
if (portData != null) {
|
|
const portStr = String(portData).trim()
|
|
if (portStr.includes(':')) {
|
|
const [h, rest] = portStr.split(':')
|
|
hostPort = h.trim()
|
|
if (rest.includes('/')) {
|
|
const [c, p] = rest.split('/')
|
|
containerPort = c.trim()
|
|
protocol = p?.toLowerCase() === 'udp' ? 'udp' : 'tcp'
|
|
} else containerPort = rest.trim()
|
|
} else if (portStr.includes('/')) {
|
|
const [c, p] = portStr.split('/')
|
|
containerPort = c.trim()
|
|
protocol = p?.toLowerCase() === 'udp' ? 'udp' : 'tcp'
|
|
} else if (portStr) containerPort = portStr
|
|
}
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item port-mapping-item'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<div class="port-mapping-fields">
|
|
<div class="port-field-group">
|
|
<label class="port-field-label">Host Port</label>
|
|
<input type="number" class="form-control bg-dark text-white port-host-input" placeholder="8080" min="1" max="65535" value="${escapeAttr(hostPort)}">
|
|
</div>
|
|
<div class="port-connector"><i class="fas fa-arrow-right"></i></div>
|
|
<div class="port-field-group">
|
|
<label class="port-field-label">Container Port</label>
|
|
<input type="number" class="form-control bg-dark text-white port-container-input" placeholder="80" min="1" max="65535" value="${escapeAttr(containerPort)}" required>
|
|
</div>
|
|
<div class="port-field-group" style="max-width:6rem">
|
|
<label class="port-field-label">Protocol</label>
|
|
<select class="form-select bg-dark text-white port-protocol-input">
|
|
<option value="tcp" ${protocol === 'tcp' ? 'selected' : ''}>TCP</option>
|
|
<option value="udp" ${protocol === 'udp' ? 'selected' : ''}>UDP</option>
|
|
</select>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger align-self-end" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>`
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function addcAddVolume() {
|
|
const container = el('addc-volumes')
|
|
if (!container) return
|
|
const id = `addc-vol-${++volSeq}`
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item volume-mount-item'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<div class="volume-mount-fields">
|
|
<div class="volume-field-group" style="max-width:8rem">
|
|
<label class="volume-field-label">Type</label>
|
|
<select class="form-select bg-dark text-white volume-type-input">
|
|
<option value="bind">Bind</option>
|
|
<option value="named">Named volume</option>
|
|
</select>
|
|
</div>
|
|
<div class="volume-field-group volume-host-wrap flex-grow-1">
|
|
<label class="volume-field-label volume-src-label">Host path</label>
|
|
<input type="text" class="form-control bg-dark text-white volume-host-input" placeholder="/path/on/host or volume-name">
|
|
</div>
|
|
<div class="volume-field-group flex-grow-1">
|
|
<label class="volume-field-label">Container path</label>
|
|
<input type="text" class="form-control bg-dark text-white volume-container-input" placeholder="/data" required>
|
|
</div>
|
|
<div class="volume-field-group" style="max-width:6rem">
|
|
<label class="volume-field-label">Mode</label>
|
|
<select class="form-select bg-dark text-white volume-mode-input">
|
|
<option value="rw">RW</option>
|
|
<option value="ro">RO</option>
|
|
</select>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-danger align-self-end" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>`
|
|
const typeSel = item.querySelector('.volume-type-input')
|
|
const srcLabel = item.querySelector('.volume-src-label')
|
|
typeSel?.addEventListener('change', () => {
|
|
if (srcLabel) srcLabel.textContent = typeSel.value === 'named' ? 'Volume name' : 'Host path'
|
|
})
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function addcAddEnv() {
|
|
const container = el('addc-env')
|
|
if (!container) return
|
|
const id = `addc-env-${++envSeq}`
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item mb-2 d-flex gap-2 align-items-center'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<input type="text" class="form-control bg-dark text-white" placeholder="KEY" data-env-key="${id}" style="flex:0 0 36%">
|
|
<input type="text" class="form-control bg-dark text-white" placeholder="value" data-env-value="${id}" style="flex:1">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>`
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function addcAddLabel() {
|
|
const container = el('addc-labels')
|
|
if (!container) return
|
|
const id = `addc-label-${++labelSeq}`
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<input type="text" class="form-control bg-dark text-white" placeholder="key=value" data-label-id="${id}">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>`
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function addcAddDns() {
|
|
const container = el('addc-dns')
|
|
if (!container) return
|
|
const id = `addc-dns-${++dnsSeq}`
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<input type="text" class="form-control bg-dark text-white" placeholder="8.8.8.8" data-dns-id="${id}">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>`
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function addcAddExtraHost() {
|
|
const container = el('addc-extra-hosts')
|
|
if (!container) return
|
|
const id = `addc-host-${++hostSeq}`
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<input type="text" class="form-control bg-dark text-white" placeholder="hostname:ip" data-host-id="${id}">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>`
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function addcAddCapability() {
|
|
const container = el('addc-capabilities')
|
|
if (!container) return
|
|
const id = `addc-cap-${++capSeq}`
|
|
const item = document.createElement('div')
|
|
item.className = 'array-item d-flex gap-2 align-items-center mb-2'
|
|
item.dataset.addcRow = id
|
|
item.innerHTML = `
|
|
<input type="text" class="form-control bg-dark text-white" placeholder="NET_ADMIN" data-cap-id="${id}">
|
|
<button type="button" class="btn btn-sm btn-outline-danger" data-addc-remove title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>`
|
|
item.querySelector('[data-addc-remove]')?.addEventListener('click', () => item.remove())
|
|
container.appendChild(item)
|
|
}
|
|
|
|
export function toggleAddcCustomNetwork() {
|
|
const mode = val('addc-network-mode') || 'bridge'
|
|
const wrap = el('addc-custom-network-wrap')
|
|
const label = el('addc-custom-network-label')
|
|
const hint = el('addc-custom-network-hint')
|
|
const input = el('addc-custom-network')
|
|
if (!wrap) return
|
|
if (mode === 'host' || mode === 'none') {
|
|
wrap.style.display = 'none'
|
|
return
|
|
}
|
|
wrap.style.display = ''
|
|
if (mode === 'container') {
|
|
if (label) label.textContent = 'Container ID / name'
|
|
if (hint) hint.textContent = 'Share the network stack of this container'
|
|
if (input) input.placeholder = 'container-name-or-id'
|
|
} else {
|
|
if (label) label.textContent = 'Attach to network'
|
|
if (hint) hint.textContent = 'Named network to join after create (optional)'
|
|
if (input) input.placeholder = 'optional network name'
|
|
}
|
|
}
|
|
|
|
function collectPorts() {
|
|
const container = el('addc-ports')
|
|
if (!container) return []
|
|
const ports = []
|
|
container.querySelectorAll('.port-mapping-item').forEach((item) => {
|
|
const host = item.querySelector('.port-host-input')?.value.trim() || ''
|
|
const cPort = item.querySelector('.port-container-input')?.value.trim() || ''
|
|
const protocol = item.querySelector('.port-protocol-input')?.value || 'tcp'
|
|
if (!cPort) return
|
|
ports.push(host ? `${host}:${cPort}/${protocol}` : `${cPort}/${protocol}`)
|
|
})
|
|
return ports
|
|
}
|
|
|
|
function collectVolumes() {
|
|
const container = el('addc-volumes')
|
|
if (!container) return []
|
|
const volumes = []
|
|
container.querySelectorAll('.volume-mount-item').forEach((item) => {
|
|
const src = item.querySelector('.volume-host-input')?.value.trim() || ''
|
|
const dest = item.querySelector('.volume-container-input')?.value.trim() || ''
|
|
const mode = item.querySelector('.volume-mode-input')?.value || 'rw'
|
|
if (!src || !dest) return
|
|
volumes.push(`${src}:${dest}:${mode}`)
|
|
})
|
|
return volumes
|
|
}
|
|
|
|
function collectEnv() {
|
|
const container = el('addc-env')
|
|
if (!container) return []
|
|
const env = []
|
|
container.querySelectorAll('[data-env-key]').forEach((keyInput) => {
|
|
const name = keyInput.value.trim()
|
|
if (!name) return
|
|
const id = keyInput.getAttribute('data-env-key')
|
|
const valueInput = container.querySelector(`[data-env-value="${id}"]`)
|
|
env.push({ name, value: valueInput?.value ?? '' })
|
|
})
|
|
return env
|
|
}
|
|
|
|
function collectLabels() {
|
|
const container = el('addc-labels')
|
|
if (!container) return {}
|
|
/** @type {Record<string, string>} */
|
|
const labels = {}
|
|
container.querySelectorAll('[data-label-id]').forEach((input) => {
|
|
const raw = input.value.trim()
|
|
if (!raw || !raw.includes('=')) return
|
|
const eq = raw.indexOf('=')
|
|
const k = raw.slice(0, eq).trim()
|
|
const v = raw.slice(eq + 1)
|
|
if (k) labels[k] = v
|
|
})
|
|
return labels
|
|
}
|
|
|
|
function collectSimpleList(containerId, attr) {
|
|
const container = el(containerId)
|
|
if (!container) return []
|
|
const out = []
|
|
container.querySelectorAll(`[${attr}]`).forEach((input) => {
|
|
const v = input.value.trim()
|
|
if (v) out.push(v)
|
|
})
|
|
return out
|
|
}
|
|
|
|
/**
|
|
* Build deployContainer payload from the add-container form.
|
|
* @returns {object}
|
|
*/
|
|
export function collectAddContainerForm() {
|
|
const networkMode = val('addc-network-mode') || 'bridge'
|
|
const customNet = val('addc-custom-network')
|
|
const labels = collectLabels()
|
|
let customNetwork = null
|
|
if (customNet && networkMode !== 'host' && networkMode !== 'none') {
|
|
customNetwork = customNet
|
|
}
|
|
|
|
const data = {
|
|
source: 'add-container',
|
|
containerName: val('addc-name'),
|
|
image: val('addc-image'),
|
|
alwaysPull: checked('addc-always-pull'),
|
|
publishAllPorts: checked('addc-publish-all'),
|
|
command: val('addc-command') || null,
|
|
entrypoint: val('addc-entrypoint') || null,
|
|
workingDir: val('addc-workdir') || null,
|
|
networkMode,
|
|
customNetwork,
|
|
hostname: val('addc-hostname') || null,
|
|
domainname: val('addc-domainname') || null,
|
|
ports: collectPorts(),
|
|
dns: collectSimpleList('addc-dns', 'data-dns-id'),
|
|
extraHosts: collectSimpleList('addc-extra-hosts', 'data-host-id'),
|
|
volumes: collectVolumes(),
|
|
env: collectEnv(),
|
|
labels: Object.keys(labels).length ? labels : null,
|
|
user: val('addc-user') || null,
|
|
privileged: checked('addc-privileged'),
|
|
readonlyRootfs: checked('addc-readonly-rootfs'),
|
|
capabilities: collectSimpleList('addc-capabilities', 'data-cap-id'),
|
|
restartPolicy: val('addc-restart-policy') || 'no',
|
|
restartMaxRetries: parseInt(el('addc-restart-max-retries')?.value, 10) || null,
|
|
autoRemove: checked('addc-auto-remove'),
|
|
tty: checked('addc-tty'),
|
|
stdinOpen: checked('addc-stdin-open'),
|
|
init: checked('addc-init'),
|
|
cpuLimit: numOrNull('addc-cpu-limit'),
|
|
memoryLimit: numOrNull('addc-memory-limit', (x) => parseInt(x, 10)),
|
|
memoryReservation: numOrNull('addc-memory-reservation', (x) => parseInt(x, 10)),
|
|
shmSize: numOrNull('addc-shm-size', (x) => parseInt(x, 10)),
|
|
healthCmd: val('addc-health-cmd') || null,
|
|
logDriver: val('addc-log-driver') || null,
|
|
}
|
|
|
|
Object.keys(data).forEach((key) => {
|
|
if (
|
|
data[key] === null ||
|
|
data[key] === '' ||
|
|
(Array.isArray(data[key]) && data[key].length === 0)
|
|
) {
|
|
delete data[key]
|
|
}
|
|
})
|
|
// Keep boolean alwaysPull even when false
|
|
data.alwaysPull = checked('addc-always-pull')
|
|
data.publishAllPorts = checked('addc-publish-all')
|
|
data.source = 'add-container'
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* @param {object} payload
|
|
*/
|
|
async function confirmReplaceIfNeeded(payload) {
|
|
if (payload.replace) return payload
|
|
if (typeof window.peardockOps?.askUserConfirm !== 'function') return payload
|
|
// Server returns conflict; caller may also pre-check. Keep replace off until conflict.
|
|
return payload
|
|
}
|
|
|
|
/**
|
|
* Submit create form via job stepper.
|
|
* @returns {Promise<object>}
|
|
*/
|
|
export async function submitAddContainer() {
|
|
let payload = collectAddContainerForm()
|
|
if (!payload.containerName || !payload.image) {
|
|
throw new Error(
|
|
'Container name and image are required — How to fix: fill both fields before deploying.'
|
|
)
|
|
}
|
|
|
|
// Networking precheck before any RPC (empty host ports, conflicts, etc.)
|
|
const netCheck = await precheckDeployNetworking(payload)
|
|
if (!netCheck.ok) {
|
|
throw new Error(
|
|
formatNetworkingPrecheckMessage(netCheck) ||
|
|
'Networking configuration is invalid.'
|
|
)
|
|
}
|
|
if (netCheck.warnings?.length) {
|
|
try {
|
|
showAlert('warning', netCheck.warnings.join(' '), { toast: true, tray: false })
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
payload._networkingPrechecked = true
|
|
|
|
// Reuse template deploy replace helper when available
|
|
if (typeof window.deployDockerContainer === 'function') {
|
|
// Prefer shared path for name-conflict handling + job tray
|
|
const result = await window.deployDockerContainer(payload)
|
|
return result
|
|
}
|
|
|
|
if (typeof window.peardockOps?.deployContainerWithSteps !== 'function') {
|
|
throw new Error('Deploy helpers are not ready')
|
|
}
|
|
|
|
payload = await confirmReplaceIfNeeded(payload)
|
|
try {
|
|
const job = await window.peardockOps.deployContainerWithSteps(payload)
|
|
return {
|
|
success: true,
|
|
viaJob: true,
|
|
message:
|
|
job?.result?.message ||
|
|
`Container "${payload.containerName}" created from image "${payload.image}"`,
|
|
id: job?.result?.id,
|
|
}
|
|
} catch (err) {
|
|
if (
|
|
err?.code === 'CONTAINER_NAME_CONFLICT' ||
|
|
/already exists|name.*already|already in use/i.test(String(err?.message || ''))
|
|
) {
|
|
let replace = false
|
|
if (window.peardockOps?.askUserConfirm) {
|
|
replace = await window.peardockOps.askUserConfirm(
|
|
'Container name already in use',
|
|
`A container named "${payload.containerName}" already exists. Replace it? This stops and removes the old container, then creates a new one with your settings.`,
|
|
{ confirmLabel: 'Replace', danger: true, icon: 'fa-redo' }
|
|
)
|
|
}
|
|
if (replace) {
|
|
const job = await window.peardockOps.deployContainerWithSteps({
|
|
...payload,
|
|
replace: true,
|
|
})
|
|
return {
|
|
success: true,
|
|
viaJob: true,
|
|
message:
|
|
job?.result?.message ||
|
|
`Container "${payload.containerName}" replaced successfully`,
|
|
id: job?.result?.id,
|
|
replaced: true,
|
|
}
|
|
}
|
|
}
|
|
if (err && typeof err === 'object') err.viaJob = true
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wire buttons and network mode toggle once.
|
|
*/
|
|
export function initAddContainerPage() {
|
|
const form = el('add-container-form')
|
|
if (!form || form.dataset.addcBound === '1') return
|
|
form.dataset.addcBound = '1'
|
|
|
|
el('addc-network-mode')?.addEventListener('change', toggleAddcCustomNetwork)
|
|
el('addc-add-port')?.addEventListener('click', () => addcAddPort())
|
|
el('addc-add-volume')?.addEventListener('click', () => addcAddVolume())
|
|
el('addc-add-env')?.addEventListener('click', () => addcAddEnv())
|
|
el('addc-add-label')?.addEventListener('click', () => addcAddLabel())
|
|
el('addc-add-dns')?.addEventListener('click', () => addcAddDns())
|
|
el('addc-add-extra-host')?.addEventListener('click', () => addcAddExtraHost())
|
|
el('addc-add-capability')?.addEventListener('click', () => addcAddCapability())
|
|
|
|
el('addc-cancel-btn')?.addEventListener('click', () => {
|
|
if (typeof window.navigateToView === 'function') window.navigateToView('containers')
|
|
})
|
|
|
|
el('addc-reset-btn')?.addEventListener('click', () => resetAddContainerForm())
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault()
|
|
await handleAddContainerSubmit()
|
|
})
|
|
|
|
el('addc-deploy-btn')?.addEventListener('click', async (e) => {
|
|
e.preventDefault()
|
|
await handleAddContainerSubmit()
|
|
})
|
|
|
|
toggleAddcCustomNetwork()
|
|
}
|
|
|
|
async function handleAddContainerSubmit() {
|
|
const btn = el('addc-deploy-btn')
|
|
if (btn) btn.disabled = true
|
|
try {
|
|
showStatusIndicator('Creating container…')
|
|
const result = await submitAddContainer()
|
|
// Job drawer owns success feedback when viaJob
|
|
if (!result?.viaJob) {
|
|
showAlert('success', result?.message || 'Container created')
|
|
}
|
|
resetAddContainerForm()
|
|
if (typeof window.navigateToView === 'function') {
|
|
window.navigateToView('containers')
|
|
}
|
|
if (typeof window.sendCommand === 'function') {
|
|
window.sendCommand('listContainers')
|
|
}
|
|
} catch (err) {
|
|
if (err?.code === 'DEPLOY_CANCELLED') return
|
|
// Job tray owns multi-line error when viaJob
|
|
if (!err?.viaJob) {
|
|
presentError(err, 'deployContainer', { showAlert })
|
|
}
|
|
} finally {
|
|
if (btn) btn.disabled = false
|
|
hideStatusIndicator()
|
|
}
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
return String(s ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/"/g, '"')
|
|
.replace(/</g, '<')
|
|
}
|
|
|
|
// Window exports for inline handlers if needed
|
|
if (typeof window !== 'undefined') {
|
|
window.addcAddPort = addcAddPort
|
|
window.addcAddVolume = addcAddVolume
|
|
window.addcAddEnv = addcAddEnv
|
|
window.addcAddLabel = addcAddLabel
|
|
window.addcAddDns = addcAddDns
|
|
window.addcAddExtraHost = addcAddExtraHost
|
|
window.addcAddCapability = addcAddCapability
|
|
window.resetAddContainerForm = resetAddContainerForm
|
|
window.collectAddContainerForm = collectAddContainerForm
|
|
window.submitAddContainer = submitAddContainer
|
|
window.initAddContainerPage = initAddContainerPage
|
|
window.toggleAddcCustomNetwork = toggleAddcCustomNetwork
|
|
}
|
|
|
|
export default {
|
|
initAddContainerPage,
|
|
resetAddContainerForm,
|
|
collectAddContainerForm,
|
|
submitAddContainer,
|
|
}
|