feat (ui): exec fase 3
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user