|
// 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>`;
|
|
}
|
|
}
|