feat (ui): exec fase 3

This commit is contained in:
kodi
2026-03-06 18:13:55 +01:00
parent 39a33e5711
commit a4099867a5
3 changed files with 121 additions and 6 deletions
+12
View File
@@ -467,6 +467,14 @@ pre{
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.execTerminalHost{
min-height: 48vh;
max-height: 64vh;
border: 1px solid var(--soft-line);
border-radius: 12px;
background: #0a0f1d;
overflow: hidden;
}
.execOutput{ .execOutput{
margin: 0; margin: 0;
white-space: pre-wrap; white-space: pre-wrap;
@@ -482,6 +490,10 @@ pre{
max-height: 64vh; max-height: 64vh;
overflow: auto; overflow: auto;
} }
.execTerminalHost .xterm,
.execTerminalHost .xterm-viewport{
height: 100%;
}
.execInputRow{ .execInputRow{
display: flex; display: flex;
align-items: center; align-items: center;
+104 -5
View File
@@ -404,6 +404,9 @@ let execTerminalState = {
container: '', container: '',
sessionId: '', sessionId: '',
source: null, source: null,
term: null,
fitAddon: null,
useXterm: false,
reconnectTimer: null, reconnectTimer: null,
reconnectAttempts: 0, reconnectAttempts: 0,
reconnectEnabled: false, reconnectEnabled: false,
@@ -433,6 +436,8 @@ function _execEls() {
back: document.getElementById('execModalBack'), back: document.getElementById('execModalBack'),
title: document.getElementById('execModalTitle'), title: document.getElementById('execModalTitle'),
output: document.getElementById('execTerminalOutput'), output: document.getElementById('execTerminalOutput'),
termHost: document.getElementById('execTerminalHost'),
inputRow: document.getElementById('execInputRow'),
bufferInfo: document.getElementById('execTerminalBufferInfo'), bufferInfo: document.getElementById('execTerminalBufferInfo'),
input: document.getElementById('execTerminalInput'), input: document.getElementById('execTerminalInput'),
sendBtn: document.getElementById('execTerminalSendBtn'), sendBtn: document.getElementById('execTerminalSendBtn'),
@@ -447,12 +452,88 @@ function _execSetStatus(msg) {
if (status) status.textContent = msg || ''; if (status) status.textContent = msg || '';
} }
function _execCanUseXterm() {
return !!(window.Terminal && window.FitAddon && window.FitAddon.FitAddon);
}
function _execSetTerminalUiMode(useXterm) {
const { termHost, output, inputRow, rawBtn } = _execEls();
if (termHost) termHost.style.display = useXterm ? 'block' : 'none';
if (output) output.style.display = useXterm ? 'none' : 'block';
if (inputRow) inputRow.style.display = useXterm ? 'none' : 'flex';
if (rawBtn) rawBtn.style.display = useXterm ? 'none' : 'inline-flex';
}
function _execDestroyTerminal() {
if (execTerminalState.term) {
try { execTerminalState.term.dispose(); } catch (_) {}
}
execTerminalState.term = null;
execTerminalState.fitAddon = null;
execTerminalState.useXterm = false;
}
function _execInitTerminal() {
const { termHost } = _execEls();
if (!_execCanUseXterm() || !termHost) {
_execDestroyTerminal();
_execSetTerminalUiMode(false);
return false;
}
_execDestroyTerminal();
try {
const fitAddon = new window.FitAddon.FitAddon();
const term = new window.Terminal({
cursorBlink: true,
convertEol: false,
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 13,
scrollback: 5000,
theme: {
background: '#0a0f1d',
foreground: '#d9e6ff',
},
});
term.loadAddon(fitAddon);
term.open(termHost);
fitAddon.fit();
term.onData((data) => {
const payload = String(data || '');
if (!payload) return;
const p = payload.length > EXEC_PASTE_CHUNK_SIZE
? _execEnqueueChunkedInput(payload)
: _execEnqueueInput(payload);
p.catch((e) => {
_execAppendOutput(`\n[ERROR] ${e.message || e}\n`);
_execSwitchToLineFallback('xterm input error');
});
});
execTerminalState.term = term;
execTerminalState.fitAddon = fitAddon;
execTerminalState.useXterm = true;
_execSetTerminalUiMode(true);
term.focus();
return true;
} catch (_) {
_execDestroyTerminal();
_execSetTerminalUiMode(false);
return false;
}
}
function _execConnectedStatus() { function _execConnectedStatus() {
const sid = execTerminalState.sessionId || ''; const sid = execTerminalState.sessionId || '';
return sid ? `Connected (${sid})` : 'Connected'; return sid ? `Connected (${sid})` : 'Connected';
} }
function _execAppendOutput(txt) { function _execAppendOutput(txt) {
if (execTerminalState.useXterm && execTerminalState.term) {
try { execTerminalState.term.write(String(txt || '')); } catch (_) {}
return;
}
const { output } = _execEls(); const { output } = _execEls();
if (!output || !txt) return; if (!output || !txt) return;
@@ -469,6 +550,9 @@ function _execAppendOutput(txt) {
} }
function _execClearOutput() { function _execClearOutput() {
if (execTerminalState.useXterm && execTerminalState.term) {
try { execTerminalState.term.reset(); } catch (_) {}
}
const { output } = _execEls(); const { output } = _execEls();
execTerminalState.outputText = ''; execTerminalState.outputText = '';
execTerminalState.outputLineCount = 0; execTerminalState.outputLineCount = 0;
@@ -478,6 +562,7 @@ function _execClearOutput() {
} }
function _execUpdateBufferInfo() { function _execUpdateBufferInfo() {
if (execTerminalState.useXterm) return;
const { bufferInfo } = _execEls(); const { bufferInfo } = _execEls();
if (!bufferInfo) return; if (!bufferInfo) return;
if (execTerminalState.outputTruncated) { if (execTerminalState.outputTruncated) {
@@ -611,10 +696,11 @@ function _execAttachStream(afterSeq = 0, wasReconnect = false) {
} }
function _execComputeSize() { function _execComputeSize() {
const { output } = _execEls(); const { output, termHost } = _execEls();
if (!output) return { rows: 24, cols: 80 }; const target = (execTerminalState.useXterm && termHost) ? termHost : output;
const w = output.clientWidth || 640; if (!target) return { rows: 24, cols: 80 };
const h = output.clientHeight || 360; const w = target.clientWidth || 640;
const h = target.clientHeight || 360;
const cols = Math.max(20, Math.floor(w / 8)); const cols = Math.max(20, Math.floor(w / 8));
const rows = Math.max(8, Math.floor(h / 18)); const rows = Math.max(8, Math.floor(h / 18));
return { rows, cols }; return { rows, cols };
@@ -623,6 +709,9 @@ function _execComputeSize() {
async function _execResizeNow() { async function _execResizeNow() {
const sid = execTerminalState.sessionId; const sid = execTerminalState.sessionId;
if (!sid) return; if (!sid) return;
if (execTerminalState.useXterm && execTerminalState.fitAddon) {
try { execTerminalState.fitAddon.fit(); } catch (_) {}
}
const { rows, cols } = _execComputeSize(); const { rows, cols } = _execComputeSize();
try { try {
await api(`/containers/exec/${encodeURIComponent(sid)}/resize`, 'POST', { rows, cols }); await api(`/containers/exec/${encodeURIComponent(sid)}/resize`, 'POST', { rows, cols });
@@ -654,6 +743,8 @@ function _execUpdateInputModeUi() {
} }
function _execSwitchToLineFallback(reason) { function _execSwitchToLineFallback(reason) {
_execDestroyTerminal();
_execSetTerminalUiMode(false);
if (!execTerminalState.rawMode) return; if (!execTerminalState.rawMode) return;
execTerminalState.rawMode = false; execTerminalState.rawMode = false;
_execUpdateInputModeUi(); _execUpdateInputModeUi();
@@ -799,6 +890,7 @@ async function containerExecOpen(name) {
execTerminalState.reconnectEnabled = false; execTerminalState.reconnectEnabled = false;
_execCancelReconnect(); _execCancelReconnect();
execTerminalState.rawMode = true; execTerminalState.rawMode = true;
_execInitTerminal();
_execUpdateInputModeUi(); _execUpdateInputModeUi();
_execResetInputQueue(); _execResetInputQueue();
_execCloseEventSource(); _execCloseEventSource();
@@ -829,7 +921,11 @@ async function containerExecOpen(name) {
execTerminalState.resizeObserver.observe(els.modal); execTerminalState.resizeObserver.observe(els.modal);
} }
if (els.input) els.input.focus(); if (execTerminalState.useXterm && execTerminalState.term) {
execTerminalState.term.focus();
} else if (els.input) {
els.input.focus();
}
} catch (e) { } catch (e) {
_execSetStatus('Start failed'); _execSetStatus('Start failed');
_execAppendOutput(`\n[ERROR] ${e.message || e}\n`); _execAppendOutput(`\n[ERROR] ${e.message || e}\n`);
@@ -890,6 +986,7 @@ function containerExecHandleInputBlur() {
} }
function containerExecToggleRawMode() { function containerExecToggleRawMode() {
if (execTerminalState.useXterm) return;
execTerminalState.rawMode = !execTerminalState.rawMode; execTerminalState.rawMode = !execTerminalState.rawMode;
_execUpdateInputModeUi(); _execUpdateInputModeUi();
_execFocusInputSoon(0); _execFocusInputSoon(0);
@@ -902,6 +999,8 @@ async function containerExecClose() {
execTerminalState.reconnectEnabled = false; execTerminalState.reconnectEnabled = false;
_execCancelReconnect(); _execCancelReconnect();
_execCloseEventSource(); _execCloseEventSource();
_execDestroyTerminal();
_execSetTerminalUiMode(false);
if (execTerminalState.resizeObserver) { if (execTerminalState.resizeObserver) {
try { execTerminalState.resizeObserver.disconnect(); } catch (_) {} try { execTerminalState.resizeObserver.disconnect(); } catch (_) {}
execTerminalState.resizeObserver = null; execTerminalState.resizeObserver = null;
+5 -1
View File
@@ -8,6 +8,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/yaml/yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css">
<title>MVP Control UI</title> <title>MVP Control UI</title>
<link rel="icon" type="image/x-icon" href="assets/icons/podman.io-favicon.ico"> <link rel="icon" type="image/x-icon" href="assets/icons/podman.io-favicon.ico">
<link rel="stylesheet" href="assets/css/app.css"> <link rel="stylesheet" href="assets/css/app.css">
@@ -371,8 +372,9 @@
</div> </div>
</div> </div>
<div class="modalBody execBody"> <div class="modalBody execBody">
<div id="execTerminalHost" class="execTerminalHost"></div>
<pre id="execTerminalOutput" class="execOutput"></pre> <pre id="execTerminalOutput" class="execOutput"></pre>
<div class="execInputRow"> <div class="execInputRow" id="execInputRow">
<span class="statusline" id="execTerminalBufferInfo"></span> <span class="statusline" id="execTerminalBufferInfo"></span>
<input <input
class="input mono" class="input mono"
@@ -469,6 +471,8 @@
</div> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
<script src="assets/js/tabs/containers.js"></script> <script src="assets/js/tabs/containers.js"></script>
<script src="assets/js/tabs/files.js"></script> <script src="assets/js/tabs/files.js"></script>
<script> <script>