mirror of
https://github.com/tbnobody/OpenDTU.git
synced 2025-12-11 09:20:34 +01:00
Feature: Rewrite virutal console to support new line break and colored highlighting
This commit is contained in:
@@ -380,7 +380,8 @@
|
||||
"VirtualDebugConsole": "Εικονική κονσόλα εντοπισμού σφαλμάτων",
|
||||
"EnableAutoScroll": "Ενεργοποίηση αυτόματης κύλισης",
|
||||
"ClearConsole": "Εκκαθάριση κονσόλας",
|
||||
"CopyToClipboard": "Αντιγραφή στο πρόχειρο"
|
||||
"CopyToClipboard": "Αντιγραφή στο πρόχειρο",
|
||||
"Download": "Download"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "Είσοδος {num}",
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
"VirtualDebugConsole": "Consola de Depuración Virtual",
|
||||
"EnableAutoScroll": "Habilitar Desplazamiento Automático",
|
||||
"ClearConsole": "Limpiar Consola",
|
||||
"CopyToClipboard": "Copiar al Portapapeles"
|
||||
"CopyToClipboard": "Copiar al Portapapeles",
|
||||
"Download": "Download"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "Cadena {num}",
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
"VirtualDebugConsole": "Virtual Debug Console",
|
||||
"EnableAutoScroll": "Abilita AutoScroll",
|
||||
"ClearConsole": "Pulisci Console",
|
||||
"CopyToClipboard": "Copia nella clipboard"
|
||||
"CopyToClipboard": "Copia nella clipboard",
|
||||
"Download": "Download"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "Stringa {num}",
|
||||
|
||||
@@ -380,7 +380,8 @@
|
||||
"VirtualDebugConsole": "Wirtualna konsola debugowania",
|
||||
"EnableAutoScroll": "Włącz automatyczne przewijani",
|
||||
"ClearConsole": "Wyczyść konsolę",
|
||||
"CopyToClipboard": "Kopiuj do schowka"
|
||||
"CopyToClipboard": "Kopiuj do schowka",
|
||||
"Download": "Download"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "Numer panelu {num}",
|
||||
|
||||
@@ -365,7 +365,8 @@
|
||||
"VirtualDebugConsole": "Virtuelle Debug-Konsole",
|
||||
"EnableAutoScroll": "Automatisches Scrollen aktivieren",
|
||||
"ClearConsole": "Konsole leeren",
|
||||
"CopyToClipboard": "In die Zwischenablage kopieren"
|
||||
"CopyToClipboard": "In die Zwischenablage kopieren",
|
||||
"Download": "Herunterladen"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "String {num}",
|
||||
|
||||
@@ -365,7 +365,8 @@
|
||||
"VirtualDebugConsole": "Virtual Debug Console",
|
||||
"EnableAutoScroll": "Enable Auto Scroll",
|
||||
"ClearConsole": "Clear Console",
|
||||
"CopyToClipboard": "Copy to clipboard"
|
||||
"CopyToClipboard": "Copy to clipboard",
|
||||
"Download": "Download"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "String {num}",
|
||||
|
||||
@@ -347,7 +347,8 @@
|
||||
"VirtualDebugConsole": "Console de débogage",
|
||||
"EnableAutoScroll": "Activer le défilement automatique",
|
||||
"ClearConsole": "Vider la console",
|
||||
"CopyToClipboard": "Copier dans le presse-papiers"
|
||||
"CopyToClipboard": "Copier dans le presse-papiers",
|
||||
"Download": "Download"
|
||||
},
|
||||
"inverterchannelinfo": {
|
||||
"String": "Ligne {num}",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
type="checkbox"
|
||||
role="switch"
|
||||
id="autoScroll"
|
||||
v-model="isAutoScroll"
|
||||
v-model="autoScroll"
|
||||
/>
|
||||
<label class="form-check-label" for="autoScroll">
|
||||
{{ $t('console.EnableAutoScroll') }}
|
||||
@@ -24,10 +24,19 @@
|
||||
<button type="button" class="btn btn-secondary" :onClick="copyConsole">
|
||||
{{ $t('console.CopyToClipboard') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" :onClick="exportConsole">
|
||||
{{ $t('console.Download') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="console" class="form-control" rows="24" v-model="consoleBuffer" readonly></textarea>
|
||||
|
||||
<div id="log" ref="logRef" class="log-output" @scroll="handleScroll">
|
||||
<div v-for="(line, index) in lines" :key="index" class="log-line" :class="getLineClass(line.text)">
|
||||
<span class="timestamp">[{{ formatTimestamp(line.timestamp) }}]</span>
|
||||
{{ line.text }}
|
||||
</div>
|
||||
</div>
|
||||
</CardElement>
|
||||
</BasePage>
|
||||
</template>
|
||||
@@ -38,6 +47,11 @@ import CardElement from '@/components/CardElement.vue';
|
||||
import { authUrl } from '@/utils/authentication';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
interface LogLine {
|
||||
text: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
BasePage,
|
||||
@@ -48,9 +62,16 @@ export default defineComponent({
|
||||
socket: {} as WebSocket,
|
||||
heartInterval: 0,
|
||||
dataLoading: true,
|
||||
consoleBuffer: '',
|
||||
isAutoScroll: true,
|
||||
endWithNewline: false,
|
||||
autoScroll: true,
|
||||
lines: [] as LogLine[],
|
||||
buffer: '',
|
||||
levelMap: {
|
||||
'E (': 'error',
|
||||
'W (': 'warning',
|
||||
'I (': 'info',
|
||||
'D (': 'debug',
|
||||
'V (': 'verbose',
|
||||
} as Record<string, string>,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -60,16 +81,6 @@ export default defineComponent({
|
||||
unmounted() {
|
||||
this.closeSocket();
|
||||
},
|
||||
watch: {
|
||||
consoleBuffer() {
|
||||
if (this.isAutoScroll) {
|
||||
const textarea = this.$el.querySelector('#console');
|
||||
setTimeout(() => {
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
initSocket() {
|
||||
console.log('Starting connection to WebSocket Server');
|
||||
@@ -84,15 +95,25 @@ export default defineComponent({
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event);
|
||||
|
||||
let outstr = String(event.data);
|
||||
let removedNewline = false;
|
||||
if (outstr.endsWith('\n')) {
|
||||
outstr = outstr.substring(0, outstr.length - 1);
|
||||
removedNewline = true;
|
||||
}
|
||||
this.consoleBuffer +=
|
||||
(this.endWithNewline ? this.getOutDate() : '') + outstr.replaceAll('\n', '\n' + this.getOutDate());
|
||||
this.endWithNewline = removedNewline;
|
||||
this.buffer += event.data;
|
||||
const splitLines = this.buffer.split('\n');
|
||||
|
||||
this.buffer = splitLines.pop() || ''; // Save the incomplete line
|
||||
|
||||
splitLines.forEach((line) => {
|
||||
this.lines.push({
|
||||
text: line,
|
||||
timestamp: new Date(), // assign time of message arrival
|
||||
});
|
||||
this.$nextTick(() => {
|
||||
if (this.autoScroll) {
|
||||
const el = this.$refs.logRef as HTMLDivElement;
|
||||
if (el) {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
this.heartCheck(); // Reset heartbeat detection
|
||||
};
|
||||
|
||||
@@ -106,6 +127,25 @@ export default defineComponent({
|
||||
this.closeSocket();
|
||||
};
|
||||
},
|
||||
getLineClass(line: string): string {
|
||||
for (const tag in this.levelMap) {
|
||||
if (line.includes(tag)) {
|
||||
return this.levelMap[tag];
|
||||
}
|
||||
}
|
||||
return 'default';
|
||||
},
|
||||
handleScroll() {
|
||||
const el = this.$refs.logRef as HTMLDivElement;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const threshold = 20; // px from bottom to consider "at bottom"
|
||||
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - threshold;
|
||||
|
||||
this.autoScroll = atBottom;
|
||||
},
|
||||
// Send heartbeat packets regularly * 5s Send a heartbeat
|
||||
heartCheck() {
|
||||
if (this.heartInterval) {
|
||||
@@ -133,40 +173,111 @@ export default defineComponent({
|
||||
clearInterval(this.heartInterval);
|
||||
}
|
||||
},
|
||||
getOutDate(): string {
|
||||
const u = new Date();
|
||||
formatTimestamp(date: Date): string {
|
||||
return (
|
||||
('0' + u.getHours()).slice(-2) +
|
||||
('0' + date.getHours()).slice(-2) +
|
||||
':' +
|
||||
('0' + u.getMinutes()).slice(-2) +
|
||||
('0' + date.getMinutes()).slice(-2) +
|
||||
':' +
|
||||
('0' + u.getSeconds()).slice(-2) +
|
||||
('0' + date.getSeconds()).slice(-2) +
|
||||
'.' +
|
||||
(u.getMilliseconds() / 1000).toFixed(3).slice(2, 5) +
|
||||
' > '
|
||||
(date.getMilliseconds() / 1000).toFixed(3).slice(2, 5)
|
||||
);
|
||||
},
|
||||
clearConsole() {
|
||||
this.consoleBuffer = '';
|
||||
this.lines = [];
|
||||
this.buffer = '';
|
||||
},
|
||||
copyConsole() {
|
||||
const input = document.createElement('textarea');
|
||||
input.innerHTML = this.consoleBuffer;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
const content = this.lines
|
||||
.map((line) => `[${this.formatTimestamp(line.timestamp)}] ${line.text}`)
|
||||
.join('\n');
|
||||
navigator.clipboard
|
||||
.writeText(content)
|
||||
.then(() => {
|
||||
console.log('Copied to clipboard!');
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
},
|
||||
exportConsole() {
|
||||
const content = this.lines
|
||||
.map((line) => `[${this.formatTimestamp(line.timestamp)}] ${line.text}`)
|
||||
.join('\n');
|
||||
const timestamp = this.getFileTimestamp();
|
||||
const filename = `opendtu-log-${timestamp}.txt`;
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
getFileTimestamp(): string {
|
||||
const now = new Date();
|
||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
||||
|
||||
const year = now.getFullYear();
|
||||
const month = pad(now.getMonth() + 1);
|
||||
const day = pad(now.getDate());
|
||||
const hour = pad(now.getHours());
|
||||
const min = pad(now.getMinutes());
|
||||
const sec = pad(now.getSeconds());
|
||||
|
||||
return `${year}-${month}-${day}_${hour}-${min}-${sec}`;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#console {
|
||||
background-color: #0c0c0c;
|
||||
color: #cccccc;
|
||||
.log-output {
|
||||
height: 500px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
font-family: courier new;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.log-line {
|
||||
margin: 3px 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #888;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff5555;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.info {
|
||||
color: #a8ff60;
|
||||
}
|
||||
|
||||
.debug {
|
||||
color: #57c7ff;
|
||||
}
|
||||
|
||||
.verbose {
|
||||
color: #bbbbbb;
|
||||
}
|
||||
|
||||
.default {
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user