// retoor <retoor@molodetz.nl>
import { Http } from "./Http.js";
import { Poller } from "./Poller.js";
import { escapeHtml, wireSearch, wireBootToggle } from "./ContainerFormControls.js";
export class ContainerList {
constructor(root) {
this.root = root;
this.endpoint = root.dataset.endpoint;
this.body = document.getElementById("cm-admin-rows");
this.pollMs = 4000;
}
init() {
this.bindActions();
this.bindCreate();
this.poller = new Poller(() => this.refresh(), 20000, { immediate: false });
const pubsub = window.app && window.app.pubsub;
if (pubsub) {
pubsub.subscribe("container.list", (data) => this.render(data.instances || []));
}
}
toast(message, type) {
if (window.app && window.app.toast) window.app.toast.show(message, { type: type || "info" });
}
escape(value) {
return escapeHtml(value);
}
bindActions() {
if (!this.body) return;
this.body.addEventListener("click", (e) => {
const btn = e.target.closest("[data-cm-action]");
if (!btn) return;
e.preventDefault();
const row = btn.closest(".cm-row");
if (!row) return;
this.runAction(btn.dataset.cmAction, row.dataset.uid, row.dataset.slug, row.dataset.name);
});
}
async runAction(action, uid, slug, name) {
if (action === "terminal") {
if (window.app && window.app.containerTerminals && slug) {
window.app.containerTerminals.open(slug, uid, name);
} else {
this.toast("Terminal needs a project slug", "error");
}
return;
}
if (action === "delete") {
const ok = window.app && window.app.dialog
? await window.app.dialog.confirm(`Delete container instance "${name}"? This cannot be undone.`)
: window.confirm(`Delete container instance "${name}"?`);
if (!ok) return;
}
try {
await Http.send(`/admin/containers/${uid}/${action}`, {});
this.toast(`Instance ${action} requested`, "success");
await this.refresh();
} catch (err) {
return;
}
}
bindCreate() {
this.form = document.getElementById("cm-admin-create-form");
if (!this.form) return;
wireSearch(this.form, "project", "/admin/containers/projects/search", "project_slug", (r) => r.slug, (r) => r.title);
wireSearch(this.form, "user", "/admin/containers/users/search", "run_as_uid", (r) => r.uid, (r) => r.username);
wireBootToggle(this.form);
this.form.addEventListener("submit", (e) => { e.preventDefault(); this.create(); });
}
async create() {
const f = this.form;
const slug = f.elements.project_slug.value.trim();
if (!slug) { this.toast("Pick a project first", "error"); return; }
const params = {
project_slug: slug,
name: f.elements.name.value.trim(),
run_as_uid: f.elements.run_as_uid.value.trim(),
boot_language: f.elements.boot_language.value,
boot_script: f.elements.boot_script.value,
boot_command: f.elements.boot_command.value.trim(),
restart_policy: f.elements.restart_policy.value,
ports: f.elements.ports.value.trim(),
env: f.elements.env.value,
cpu_limit: f.elements.cpu_limit.value.trim(),
mem_limit: f.elements.mem_limit.value.trim(),
ingress_slug: f.elements.ingress_slug.value.trim(),
ingress_port: f.elements.ingress_port.value.trim(),
start_on_boot: f.elements.start_on_boot.checked ? "true" : "false",
autostart: f.elements.autostart.checked ? "true" : "false",
};
try {
await Http.send("/admin/containers/create", params);
const modal = document.getElementById("cm-admin-create-modal");
if (modal) modal.classList.remove("visible");
f.reset();
wireBootToggle(this.form);
this.toast("Instance created", "success");
await this.refresh();
} catch (err) {
return;
}
}
async refresh() {
this.render((await Http.getJson(this.endpoint)).instances || []);
}
render(instances) {
if (!this.body) return;
if (!instances.length) {
this.body.innerHTML = `<tr><td colspan="8" class="admin-empty">No container instances yet. Create one above or open a project's container manager.</td></tr>`;
return;
}
this.body.innerHTML = instances.map((inst) => this.row(inst)).join("");
}
row(inst) {
const uid = this.escape(inst.uid);
const slug = this.escape(inst.project_slug || "");
const name = this.escape(inst.name);
const project = inst.project_slug
? `<a class="cm-row-project" href="/projects/${slug}/containers">${this.escape(inst.project_title || inst.project_slug)}</a>`
: `<span class="cm-muted">-</span>`;
const ingress = inst.ingress_slug
? `<a href="/p/${this.escape(inst.ingress_slug)}" target="_blank" rel="noopener">/p/${this.escape(inst.ingress_slug)}</a>`
: `<span class="cm-muted">-</span>`;
const boot = inst.start_on_boot
? `<span class="cm-badge cm-running">on</span>`
: `<span class="cm-muted">off</span>`;
return `<tr class="cm-row" data-uid="${uid}" data-slug="${slug}" data-name="${name}">
<td><a class="cm-row-name" href="/admin/containers/${uid}">${name}</a></td>
<td>${project}</td>
<td><span class="cm-badge cm-${this.escape(inst.status)}">${this.escape(inst.status)}</span></td>
<td>${this.escape(inst.boot_language || "none")}</td>
<td>${ingress}</td>
<td>${this.escape(inst.restart_policy || "never")}</td>
<td>${boot}</td>
<td class="cm-actions">
<button class="admin-btn admin-btn-sm" data-cm-action="start">Start</button>
<button class="admin-btn admin-btn-sm" data-cm-action="stop">Stop</button>
<button class="admin-btn admin-btn-sm" data-cm-action="restart">Restart</button>
<button class="admin-btn admin-btn-sm" data-cm-action="terminal">Terminal</button>
<a class="admin-btn admin-btn-sm" href="/admin/containers/${uid}/edit">Edit</a>
<button class="admin-btn admin-btn-sm admin-btn-danger" data-cm-action="delete">Delete</button>
</td>
</tr>`;
}
}