feat (ui): exec fase 1
This commit is contained in:
@@ -456,6 +456,40 @@ pre{
|
||||
.menuItem.ok{ border-color: rgba(45,212,191,.35); }
|
||||
.menuItem.warn{ border-color: rgba(251,191,36,.35); }
|
||||
.menuItem.bad{ border-color: rgba(251,113,133,.35); }
|
||||
.menuItem.info{ border-color: rgba(96,165,250,.45); }
|
||||
|
||||
/* Exec terminal modal */
|
||||
.modalExec{
|
||||
width: min(1100px, 100%);
|
||||
}
|
||||
.execBody{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.execOutput{
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--mono);
|
||||
font-size: 12.5px;
|
||||
color: var(--pre-text);
|
||||
background: var(--pre-bg);
|
||||
border: 1px solid var(--soft-line);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
min-height: 48vh;
|
||||
max-height: 64vh;
|
||||
overflow: auto;
|
||||
}
|
||||
.execInputRow{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.execInputRow .input{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* css voor herziene Netwerken pagina */
|
||||
/* Toolbar controls */
|
||||
|
||||
@@ -29,13 +29,17 @@ function _setCollapsed(pod, v) {
|
||||
localStorage.setItem('pod_group_collapsed:' + pod, v ? '1' : '0');
|
||||
}
|
||||
|
||||
function renderActionsDropdown(menuId, actionFn, targetEsc) {
|
||||
function renderActionsDropdown(menuId, actionFn, targetEsc, includeExec = false) {
|
||||
// actionFn is string: "containerAction" of "podAction"
|
||||
// targetEsc is al esc(...) dus veilig in onclick
|
||||
const execBtn = includeExec
|
||||
? `<button class="menuItem info" onclick="containerExecOpen('${targetEsc}'); closeAllMenus();">Exec shell</button>`
|
||||
: '';
|
||||
return `
|
||||
<span class="actions-menu">
|
||||
<button class="btn icon" title="Acties" onclick="toggleMenu('${menuId}')">⋮</button>
|
||||
<div class="menuPanel" id="${menuId}">
|
||||
${execBtn}
|
||||
<button class="menuItem ok" onclick="${actionFn}('start','${targetEsc}'); closeAllMenus();">Start</button>
|
||||
<button class="menuItem warn" onclick="${actionFn}('restart','${targetEsc}'); closeAllMenus();">Restart</button>
|
||||
<button class="menuItem bad" onclick="${actionFn}('stop','${targetEsc}'); closeAllMenus();">Stop</button>
|
||||
@@ -71,7 +75,7 @@ function renderContainerRow(c) {
|
||||
<div class="flex">
|
||||
<button class="btn icon" title="Inspect" onclick="containerInspect(decodeURIComponent('${encodeURIComponent(name)}'))">🔍</button>
|
||||
<button class="btn icon" title="Logs" onclick="containerLogs(decodeURIComponent('${encodeURIComponent(name)}'))">📄</button>
|
||||
${renderActionsDropdown(menuId, 'containerAction', esc(name))}
|
||||
${renderActionsDropdown(menuId, 'containerAction', esc(name), true)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -172,7 +176,7 @@ function renderContainersGrouped(list, tbody, podStatus) {
|
||||
<button class="btn icon" disabled style="visibility:hidden;" aria-hidden="true" tabindex="-1">🔍</button>
|
||||
<button class="btn icon" disabled style="visibility:hidden;" aria-hidden="true" tabindex="-1">📄</button>
|
||||
|
||||
${renderActionsDropdown(podMenuId, 'podAction', esc(pod))}
|
||||
${renderActionsDropdown(podMenuId, 'podAction', esc(pod), false)}
|
||||
</div>
|
||||
` : '<span class="muted">-</span>'}
|
||||
</td>
|
||||
@@ -395,3 +399,177 @@ function formatBytes(b) {
|
||||
if (n >= 1024) return (n / 1024).toFixed(0) + "KiB";
|
||||
return n + "B";
|
||||
}
|
||||
|
||||
let execTerminalState = {
|
||||
container: '',
|
||||
sessionId: '',
|
||||
source: null,
|
||||
resizeTimer: null,
|
||||
resizeObserver: null,
|
||||
};
|
||||
|
||||
function _execEls() {
|
||||
return {
|
||||
back: document.getElementById('execModalBack'),
|
||||
title: document.getElementById('execModalTitle'),
|
||||
output: document.getElementById('execTerminalOutput'),
|
||||
input: document.getElementById('execTerminalInput'),
|
||||
status: document.getElementById('execTerminalStatus'),
|
||||
modal: document.getElementById('execTerminalModal'),
|
||||
};
|
||||
}
|
||||
|
||||
function _execSetStatus(msg) {
|
||||
const { status } = _execEls();
|
||||
if (status) status.textContent = msg || '';
|
||||
}
|
||||
|
||||
function _execAppendOutput(txt) {
|
||||
const { output } = _execEls();
|
||||
if (!output || !txt) return;
|
||||
output.textContent += txt;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
|
||||
function _execClearOutput() {
|
||||
const { output } = _execEls();
|
||||
if (output) output.textContent = '';
|
||||
}
|
||||
|
||||
function _execCloseEventSource() {
|
||||
if (execTerminalState.source) {
|
||||
try { execTerminalState.source.close(); } catch (_) {}
|
||||
execTerminalState.source = null;
|
||||
}
|
||||
}
|
||||
|
||||
function _execComputeSize() {
|
||||
const { output } = _execEls();
|
||||
if (!output) return { rows: 24, cols: 80 };
|
||||
const w = output.clientWidth || 640;
|
||||
const h = output.clientHeight || 360;
|
||||
const cols = Math.max(20, Math.floor(w / 8));
|
||||
const rows = Math.max(8, Math.floor(h / 18));
|
||||
return { rows, cols };
|
||||
}
|
||||
|
||||
async function _execResizeNow() {
|
||||
const sid = execTerminalState.sessionId;
|
||||
if (!sid) return;
|
||||
const { rows, cols } = _execComputeSize();
|
||||
try {
|
||||
await api(`/containers/exec/${encodeURIComponent(sid)}/resize`, 'POST', { rows, cols });
|
||||
} catch (_) {
|
||||
// ignore while session is not fully running yet
|
||||
}
|
||||
}
|
||||
|
||||
function _execScheduleResize() {
|
||||
if (execTerminalState.resizeTimer) clearTimeout(execTerminalState.resizeTimer);
|
||||
execTerminalState.resizeTimer = setTimeout(() => {
|
||||
execTerminalState.resizeTimer = null;
|
||||
_execResizeNow();
|
||||
}, 120);
|
||||
}
|
||||
|
||||
async function containerExecOpen(name) {
|
||||
const els = _execEls();
|
||||
execTerminalState.container = name;
|
||||
execTerminalState.sessionId = '';
|
||||
_execCloseEventSource();
|
||||
_execClearOutput();
|
||||
_execSetStatus('Starting exec session...');
|
||||
|
||||
if (els.title) els.title.textContent = `Exec: ${name}`;
|
||||
if (els.back) els.back.style.display = 'flex';
|
||||
|
||||
try {
|
||||
const res = await api(`/containers/${encodeURIComponent(name)}/exec/start`, 'POST', { cmd: ['/bin/sh'], tty: true });
|
||||
const sid = res?.session_id;
|
||||
if (!sid) throw new Error('Geen session_id ontvangen');
|
||||
execTerminalState.sessionId = sid;
|
||||
_execSetStatus(`Connected (${sid})`);
|
||||
|
||||
const es = new EventSource(`/api/containers/exec/${encodeURIComponent(sid)}/stream`);
|
||||
execTerminalState.source = es;
|
||||
|
||||
es.addEventListener('exec', (ev) => {
|
||||
try {
|
||||
const msg = JSON.parse(ev.data || '{}');
|
||||
if (msg?.type === 'stdout') _execAppendOutput(msg.data || '');
|
||||
if (msg?.type === 'closed') _execSetStatus(`Session closed: ${msg.data || 'closed'}`);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
es.addEventListener('ping', () => {});
|
||||
|
||||
es.onerror = () => {
|
||||
_execSetStatus('Stream disconnected');
|
||||
};
|
||||
|
||||
_execScheduleResize();
|
||||
|
||||
if (execTerminalState.resizeObserver) {
|
||||
try { execTerminalState.resizeObserver.disconnect(); } catch (_) {}
|
||||
execTerminalState.resizeObserver = null;
|
||||
}
|
||||
if (typeof ResizeObserver !== 'undefined' && els.modal) {
|
||||
execTerminalState.resizeObserver = new ResizeObserver(() => _execScheduleResize());
|
||||
execTerminalState.resizeObserver.observe(els.modal);
|
||||
}
|
||||
|
||||
if (els.input) els.input.focus();
|
||||
} catch (e) {
|
||||
_execSetStatus('Start failed');
|
||||
_execAppendOutput(`\n[ERROR] ${e.message || e}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function containerExecSendInput() {
|
||||
const els = _execEls();
|
||||
const sid = execTerminalState.sessionId;
|
||||
if (!sid || !els.input) return;
|
||||
const raw = els.input.value || '';
|
||||
if (!raw) return;
|
||||
els.input.value = '';
|
||||
try {
|
||||
await api(`/containers/exec/${encodeURIComponent(sid)}/input`, 'POST', { data: raw + '\n' });
|
||||
} catch (e) {
|
||||
_execAppendOutput(`\n[ERROR] ${e.message || e}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function containerExecHandleInputKey(ev) {
|
||||
if (ev.key !== 'Enter') return;
|
||||
ev.preventDefault();
|
||||
containerExecSendInput();
|
||||
}
|
||||
|
||||
async function containerExecClose() {
|
||||
const els = _execEls();
|
||||
const sid = execTerminalState.sessionId;
|
||||
|
||||
_execCloseEventSource();
|
||||
if (execTerminalState.resizeObserver) {
|
||||
try { execTerminalState.resizeObserver.disconnect(); } catch (_) {}
|
||||
execTerminalState.resizeObserver = null;
|
||||
}
|
||||
if (execTerminalState.resizeTimer) {
|
||||
clearTimeout(execTerminalState.resizeTimer);
|
||||
execTerminalState.resizeTimer = null;
|
||||
}
|
||||
|
||||
execTerminalState.sessionId = '';
|
||||
if (els.back) els.back.style.display = 'none';
|
||||
|
||||
if (sid) {
|
||||
try {
|
||||
await api(`/containers/exec/${encodeURIComponent(sid)}/stop`, 'POST');
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function closeExecModal(ev) {
|
||||
if (ev.target.id !== 'execModalBack') return;
|
||||
containerExecClose();
|
||||
}
|
||||
|
||||
@@ -359,6 +359,31 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Exec Terminal Modal -->
|
||||
<div class="modalBack" id="execModalBack" style="display:none;" onclick="closeExecModal(event)">
|
||||
<div class="modal modalExec" id="execTerminalModal" onclick="event.stopPropagation()">
|
||||
<div class="modalHeader">
|
||||
<div class="modalTitle" id="execModalTitle">Exec</div>
|
||||
<div class="flex">
|
||||
<span class="statusline" id="execTerminalStatus">Disconnected</span>
|
||||
<button class="btn small ghost" onclick="containerExecClose()">Sluiten</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modalBody execBody">
|
||||
<pre id="execTerminalOutput" class="execOutput"></pre>
|
||||
<div class="execInputRow">
|
||||
<input
|
||||
class="input mono"
|
||||
id="execTerminalInput"
|
||||
placeholder="Type command and press Enter..."
|
||||
onkeydown="containerExecHandleInputKey(event)"
|
||||
/>
|
||||
<button class="btn" onclick="containerExecSendInput()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Build Modal -->
|
||||
<div class="modalBack" id="buildModalBack" style="display:none;" onclick="closeBuildModal(event)">
|
||||
<div class="modal" onclick="event.stopPropagation()" style="width:700px;">
|
||||
|
||||
Reference in New Issue
Block a user