Files
dees-catalog/ts_web/elements/00group-chart/dees-chart-log/dees-chart-log.ts

926 lines
26 KiB
TypeScript
Raw Normal View History

2024-02-05 10:07:49 +01:00
import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
state,
2024-02-05 10:07:49 +01:00
type TemplateResult,
} from '@design.estate/dees-element';
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-chart-log.demo.js';
import { themeDefaultStyles } from '../../00theme.js';
import { DeesServiceLibLoader, type IXtermSearchAddon, CDN_BASE, CDN_VERSIONS } from '../../../services/index.js';
2024-02-05 10:07:49 +01:00
// Type imports (no runtime overhead)
import type { Terminal } from 'xterm';
import type { FitAddon } from 'xterm-addon-fit';
2024-02-05 10:07:49 +01:00
declare global {
interface HTMLElementTagNameMap {
'dees-chart-log': DeesChartLog;
}
}
export interface ILogEntry {
timestamp: string;
level: 'debug' | 'info' | 'warn' | 'error' | 'success';
message: string;
source?: string;
}
export interface ILogMetrics {
debug: number;
info: number;
warn: number;
error: number;
success: number;
total: number;
rate: number; // logs per second (rolling average)
}
2024-02-05 10:07:49 +01:00
@customElement('dees-chart-log')
export class DeesChartLog extends DeesElement {
public static demo = demoFunc;
public static demoGroup = 'Chart';
2024-02-05 10:07:49 +01:00
@property()
accessor label: string = 'Server Logs';
@property({ type: String })
accessor mode: 'structured' | 'raw' = 'structured';
@property({ type: Array })
accessor logEntries: ILogEntry[] = [];
@property({ type: Boolean })
accessor autoScroll: boolean = true;
@property({ type: Number })
accessor maxEntries: number = 10000;
@property({ type: Array })
accessor highlightKeywords: string[] = [];
2024-02-05 10:07:49 +01:00
@property({ type: Boolean })
accessor showMetrics: boolean = true;
@state()
accessor searchQuery: string = '';
@state()
accessor filterMode: boolean = false;
@state()
accessor metrics: ILogMetrics = { debug: 0, info: 0, warn: 0, error: 0, success: 0, total: 0, rate: 0 };
@state()
accessor terminalReady: boolean = false;
// Buffer of all log entries for filter mode
private logBuffer: ILogEntry[] = [];
// Track trailing hidden entries count for live updates in filter mode
private trailingHiddenCount: number = 0;
// xterm instances
private terminal: Terminal | null = null;
private fitAddon: FitAddon | null = null;
private searchAddon: IXtermSearchAddon | null = null;
private resizeObserver: ResizeObserver | null = null;
private terminalThemeSubscription: any = null;
private domtoolsInstance: any = null;
// Rate calculation
private rateBuffer: number[] = [];
private rateInterval: ReturnType<typeof setInterval> | null = null;
2024-02-05 10:07:49 +01:00
public static styles = [
themeDefaultStyles,
2024-02-05 10:07:49 +01:00
cssManager.defaultStyles,
css`
:host {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
2025-06-27 15:58:26 +00:00
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
2024-02-05 10:07:49 +01:00
}
2024-02-05 10:07:49 +01:00
.mainbox {
position: relative;
width: 100%;
height: 400px;
2025-06-27 15:58:26 +00:00
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
2024-02-05 10:07:49 +01:00
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
2024-02-05 10:07:49 +01:00
}
.header {
2025-06-27 15:58:26 +00:00
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
padding: 8px 12px;
2025-06-27 15:58:26 +00:00
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
flex-wrap: wrap;
2024-02-05 10:07:49 +01:00
}
.title {
2025-06-27 15:58:26 +00:00
font-weight: 500;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
white-space: nowrap;
}
.search-box {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 150px;
max-width: 300px;
}
.search-box input {
flex: 1;
padding: 4px 8px;
2025-06-27 15:58:26 +00:00
font-size: 12px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
outline: none;
}
.search-box input:focus {
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
}
.search-box input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
}
.search-nav {
display: flex;
gap: 2px;
}
.search-nav button {
padding: 4px 6px;
font-size: 11px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 3px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: pointer;
line-height: 1;
}
.search-nav button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.filter-toggle {
padding: 4px 8px;
font-size: 11px;
font-weight: 500;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
2025-06-27 15:58:26 +00:00
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.filter-toggle:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.filter-toggle.active {
background: ${cssManager.bdTheme('hsl(45 93% 47%)', 'hsl(45 93% 47%)')};
border-color: ${cssManager.bdTheme('hsl(45 93% 47%)', 'hsl(45 93% 47%)')};
color: hsl(0 0% 9%);
}
.controls {
display: flex;
gap: 6px;
margin-left: auto;
}
.control-button {
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 14.9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
padding: 4px 10px;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.15s;
}
.control-button:hover {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 20%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 25%)')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.control-button.active {
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
color: white;
}
.terminal-container {
flex: 1;
overflow: hidden;
padding: 8px;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
}
.terminal-container .xterm {
height: 100%;
}
.loading-state {
display: flex;
align-items: center;
justify-content: center;
2024-02-05 10:07:49 +01:00
height: 100%;
2025-06-27 15:58:26 +00:00
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
font-style: italic;
font-size: 13px;
}
.metrics-bar {
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
padding: 6px 12px;
display: flex;
gap: 16px;
font-size: 11px;
font-weight: 500;
flex-shrink: 0;
}
.metric {
display: flex;
align-items: center;
gap: 4px;
}
.metric::before {
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
}
.metric.error::before {
background: hsl(0 84.2% 60.2%);
}
.metric.warn::before {
background: hsl(25 95% 53%);
}
.metric.info::before {
background: hsl(222.2 47.4% 51.2%);
}
.metric.success::before {
background: hsl(142.1 76.2% 36.3%);
}
.metric.debug::before {
background: hsl(0 0% 63.9%);
}
.metric.rate {
margin-left: auto;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
}
.metric.rate::before {
display: none;
2024-02-05 10:07:49 +01:00
}
`,
];
constructor() {
super();
domtools.elementBasic.setup();
}
2024-02-05 10:07:49 +01:00
public render(): TemplateResult {
return html`
<div class="mainbox">
<div class="header">
<div class="title">${this.label}</div>
<div class="search-box">
<input
type="text"
placeholder="Search logs..."
.value=${this.searchQuery}
@input=${(e: InputEvent) => this.handleSearchInput(e)}
@keydown=${(e: KeyboardEvent) => this.handleSearchKeydown(e)}
/>
<div class="search-nav">
<button @click=${() => this.searchPrevious()} title="Previous match"></button>
<button @click=${() => this.searchNext()} title="Next match"></button>
</div>
<button
class="filter-toggle ${this.filterMode ? 'active' : ''}"
@click=${() => this.toggleFilterMode()}
title="${this.filterMode ? 'Switch to highlight mode' : 'Switch to filter mode'}"
>
${this.filterMode ? 'Filter' : 'Highlight'}
</button>
</div>
<div class="controls">
<button
class="control-button ${this.autoScroll ? 'active' : ''}"
@click=${() => this.toggleAutoScroll()}
>
Auto Scroll
</button>
<button class="control-button" @click=${() => this.clearLogs()}>
Clear
</button>
</div>
</div>
<div class="terminal-container">
${!this.terminalReady
? html`<div class="loading-state">Loading terminal...</div>`
: ''}
</div>
${this.showMetrics
? html`
<div class="metrics-bar">
<span class="metric error">errors: ${this.metrics.error}</span>
<span class="metric warn">warns: ${this.metrics.warn}</span>
<span class="metric info">info: ${this.metrics.info}</span>
<span class="metric success">success: ${this.metrics.success}</span>
<span class="metric debug">debug: ${this.metrics.debug}</span>
<span class="metric rate">${this.metrics.rate.toFixed(1)} logs/sec</span>
</div>
`
: ''}
</div>
`;
}
public async firstUpdated() {
this.domtoolsInstance = await this.domtoolsPromise;
await this.initializeTerminal();
// Process any initial log entries
if (this.logEntries.length > 0) {
for (const entry of this.logEntries) {
this.writeLogEntry(entry);
}
}
}
private async initializeTerminal() {
const libLoader = DeesServiceLibLoader.getInstance();
const [xtermBundle, fitBundle, searchBundle] = await Promise.all([
libLoader.loadXterm(),
libLoader.loadXtermFitAddon(),
libLoader.loadXtermSearchAddon(),
]);
// Inject xterm CSS into shadow root (needed because shadow DOM doesn't inherit from document.head)
await this.injectXtermStylesIntoShadow();
this.terminal = new xtermBundle.Terminal({
cursorBlink: false,
disableStdin: true,
fontSize: 12,
fontFamily: "'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace",
theme: this.getTerminalTheme(),
scrollback: this.maxEntries,
convertEol: true,
});
this.fitAddon = new fitBundle.FitAddon();
this.searchAddon = new searchBundle.SearchAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.loadAddon(this.searchAddon);
const container = this.shadowRoot!.querySelector('.terminal-container') as HTMLElement;
this.terminal.open(container);
// Fit after a small delay to ensure proper sizing
await new Promise((resolve) => requestAnimationFrame(resolve));
this.fitAddon.fit();
// Set up resize observer
this.resizeObserver = new ResizeObserver(() => {
this.fitAddon?.fit();
});
this.resizeObserver.observe(container);
// Subscribe to theme changes
this.terminalThemeSubscription = this.domtoolsInstance.themeManager.themeObservable.subscribe(() => {
if (this.terminal) {
this.terminal.options.theme = this.getTerminalTheme();
}
});
// Start rate calculation interval
this.rateInterval = setInterval(() => this.calculateRate(), 1000);
this.terminalReady = true;
}
private getTerminalTheme() {
const isDark = this.domtoolsInstance?.themeManager?.isDarkMode ?? true;
return isDark
? {
background: '#0a0a0a',
foreground: '#e0e0e0',
cursor: '#e0e0e0',
selectionBackground: '#404040',
black: '#000000',
red: '#ff5555',
green: '#50fa7b',
yellow: '#f1fa8c',
blue: '#6272a4',
magenta: '#ff79c6',
cyan: '#8be9fd',
white: '#f8f8f2',
brightBlack: '#6272a4',
brightRed: '#ff6e6e',
brightGreen: '#69ff94',
brightYellow: '#ffffa5',
brightBlue: '#d6acff',
brightMagenta: '#ff92df',
brightCyan: '#a4ffff',
brightWhite: '#ffffff',
}
: {
background: '#ffffff',
foreground: '#333333',
cursor: '#333333',
selectionBackground: '#add6ff',
black: '#000000',
red: '#cd3131',
green: '#00bc00',
yellow: '#949800',
blue: '#0451a5',
magenta: '#bc05bc',
cyan: '#0598bc',
white: '#555555',
brightBlack: '#666666',
brightRed: '#cd3131',
brightGreen: '#14ce14',
brightYellow: '#b5ba00',
brightBlue: '#0451a5',
brightMagenta: '#bc05bc',
brightCyan: '#0598bc',
brightWhite: '#a5a5a5',
};
}
/**
* Inject xterm CSS styles into shadow root
* This is needed because shadow DOM doesn't inherit styles from document.head
*/
private async injectXtermStylesIntoShadow(): Promise<void> {
const styleId = 'xterm-shadow-styles';
if (this.shadowRoot!.getElementById(styleId)) {
return; // Already injected
}
const cssUrl = `${CDN_BASE}/xterm@${CDN_VERSIONS.xterm}/css/xterm.css`;
const response = await fetch(cssUrl);
const cssText = await response.text();
const style = document.createElement('style');
style.id = styleId;
style.textContent = cssText;
this.shadowRoot!.appendChild(style);
}
// =====================
// Structured Log Methods
// =====================
/**
* Add a single structured log entry
*/
public addLog(level: ILogEntry['level'], message: string, source?: string) {
const entry: ILogEntry = {
timestamp: new Date().toISOString(),
level,
message,
source,
};
// Add to buffer
this.logBuffer.push(entry);
if (this.logBuffer.length > this.maxEntries) {
this.logBuffer.shift();
}
// Handle display based on filter mode
if (!this.filterMode || !this.searchQuery) {
// No filtering - show all entries
this.writeLogEntry(entry);
} else if (this.entryMatchesFilter(entry)) {
// Entry matches filter - reset trailing count and write entry
this.trailingHiddenCount = 0;
this.writeLogEntry(entry);
} else {
// Entry doesn't match - update trailing placeholder
this.updateTrailingPlaceholder();
}
this.updateMetrics(entry.level);
}
/**
* Add multiple structured log entries
*/
public updateLog(entries?: ILogEntry[]) {
if (!entries) return;
for (const entry of entries) {
// Add to buffer
this.logBuffer.push(entry);
if (this.logBuffer.length > this.maxEntries) {
this.logBuffer.shift();
}
// Handle display based on filter mode
if (!this.filterMode || !this.searchQuery) {
// No filtering - show all entries
this.writeLogEntry(entry);
} else if (this.entryMatchesFilter(entry)) {
// Entry matches filter - reset trailing count and write entry
this.trailingHiddenCount = 0;
this.writeLogEntry(entry);
} else {
// Entry doesn't match - update trailing placeholder
this.updateTrailingPlaceholder();
}
this.updateMetrics(entry.level);
}
}
/**
* Update the trailing hidden placeholder in real-time
* Clears the last line if a placeholder already exists, then writes updated count
*/
private updateTrailingPlaceholder() {
if (!this.terminal) return;
if (this.trailingHiddenCount > 0) {
// Clear the previous placeholder line (move up, clear line, move to start)
this.terminal.write('\x1b[1A\x1b[2K\r');
}
this.trailingHiddenCount++;
this.writeHiddenPlaceholder(this.trailingHiddenCount);
if (this.autoScroll) {
this.terminal.scrollToBottom();
}
}
/**
* Check if a log entry matches the current filter
*/
private entryMatchesFilter(entry: ILogEntry): boolean {
if (!this.searchQuery) return true;
const query = this.searchQuery.toLowerCase();
return (
entry.message.toLowerCase().includes(query) ||
entry.level.toLowerCase().includes(query) ||
(entry.source?.toLowerCase().includes(query) ?? false)
);
}
private writeLogEntry(entry: ILogEntry) {
if (!this.terminal) return;
const formatted = this.formatLogEntry(entry);
this.terminal.writeln(formatted);
if (this.autoScroll) {
this.terminal.scrollToBottom();
}
}
private formatLogEntry(entry: ILogEntry): string {
const timestamp = this.formatTimestamp(entry.timestamp);
const levelColors: Record<ILogEntry['level'], string> = {
debug: '\x1b[90m', // Gray
info: '\x1b[36m', // Cyan
warn: '\x1b[33m', // Yellow
error: '\x1b[31m', // Red
success: '\x1b[32m', // Green
};
const reset = '\x1b[0m';
const dim = '\x1b[2m';
const levelStr = `${levelColors[entry.level]}[${entry.level.toUpperCase().padEnd(7)}]${reset}`;
const sourceStr = entry.source ? `${dim}[${entry.source}]${reset} ` : '';
const messageStr = this.applyHighlights(entry.message);
return `${dim}${timestamp}${reset} ${levelStr} ${sourceStr}${messageStr}`;
}
private formatTimestamp(isoString: string): string {
const date = new Date(isoString);
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3,
} as Intl.DateTimeFormatOptions);
}
private applyHighlights(text: string): string {
// Collect all keywords to highlight
const keywords = [...this.highlightKeywords];
// In filter mode, also highlight the search query
if (this.filterMode && this.searchQuery) {
keywords.push(this.searchQuery);
}
if (keywords.length === 0) return text;
let result = text;
for (const keyword of keywords) {
// Escape regex special characters
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escaped})`, 'gi');
// Yellow background, black text for highlights
result = result.replace(regex, '\x1b[43m\x1b[30m$1\x1b[0m');
}
return result;
2024-02-05 10:07:49 +01:00
}
// =====================
// Raw Log Methods
// =====================
/**
* Write raw data to the terminal (for Docker logs, etc.)
*/
public writeRaw(data: string) {
if (!this.terminal) return;
this.terminal.write(data);
this.recordLogEvent();
if (this.autoScroll) {
this.terminal.scrollToBottom();
}
}
/**
* Write a raw line to the terminal
*/
public writelnRaw(line: string) {
if (!this.terminal) return;
this.terminal.writeln(line);
this.recordLogEvent();
if (this.autoScroll) {
this.terminal.scrollToBottom();
}
}
// =====================
// Search Methods
// =====================
private handleSearchInput(e: InputEvent) {
const input = e.target as HTMLInputElement;
const newQuery = input.value;
const queryChanged = this.searchQuery !== newQuery;
this.searchQuery = newQuery;
if (this.filterMode && queryChanged) {
// Re-render with filtered logs
this.reRenderFilteredLogs();
} else if (this.searchQuery) {
// Just highlight/search in current view
this.searchAddon?.findNext(this.searchQuery);
}
}
private handleSearchKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
if (e.shiftKey) {
this.searchPrevious();
} else {
this.searchNext();
}
} else if (e.key === 'Escape') {
this.searchQuery = '';
(e.target as HTMLInputElement).value = '';
}
}
/**
* Search for a query in the terminal
*/
public search(query: string): void {
this.searchQuery = query;
this.searchAddon?.findNext(query);
}
/**
* Find next search match
*/
public searchNext(): void {
if (this.searchQuery) {
this.searchAddon?.findNext(this.searchQuery);
}
}
/**
* Find previous search match
*/
public searchPrevious(): void {
if (this.searchQuery) {
this.searchAddon?.findPrevious(this.searchQuery);
}
}
// =====================
// Control Methods
// =====================
private toggleAutoScroll() {
this.autoScroll = !this.autoScroll;
if (this.autoScroll && this.terminal) {
this.terminal.scrollToBottom();
}
}
/**
* Toggle between filter mode and highlight mode
*/
private toggleFilterMode() {
this.filterMode = !this.filterMode;
this.reRenderFilteredLogs();
}
/**
* Re-render logs based on current filter state
* In filter mode: show matching logs with placeholders for hidden entries
* In highlight mode: show all logs
*/
private reRenderFilteredLogs() {
if (!this.terminal) return;
// Clear terminal and re-render
this.terminal.clear();
// Reset trailing count for fresh render
this.trailingHiddenCount = 0;
if (!this.filterMode || !this.searchQuery) {
// No filtering - show all entries
for (const entry of this.logBuffer) {
const formatted = this.formatLogEntry(entry);
this.terminal.writeln(formatted);
}
} else {
// Filter mode with placeholders for hidden entries
let hiddenCount = 0;
for (const entry of this.logBuffer) {
if (this.entryMatchesFilter(entry)) {
// Output placeholder for hidden entries if any
if (hiddenCount > 0) {
this.writeHiddenPlaceholder(hiddenCount);
hiddenCount = 0;
}
// Output the matching entry
const formatted = this.formatLogEntry(entry);
this.terminal.writeln(formatted);
} else {
hiddenCount++;
}
}
// Handle trailing hidden entries
if (hiddenCount > 0) {
this.writeHiddenPlaceholder(hiddenCount);
// Store trailing count for live updates
this.trailingHiddenCount = hiddenCount;
}
}
if (this.autoScroll) {
this.terminal.scrollToBottom();
}
}
/**
* Write a placeholder line showing how many log entries are hidden by filter
*/
private writeHiddenPlaceholder(count: number) {
const dim = '\x1b[2m';
const reset = '\x1b[0m';
const text = count === 1
? `[1 log line hidden by filter ...]`
: `[${count} log lines hidden by filter ...]`;
this.terminal?.writeln(`${dim}${text}${reset}`);
}
/**
* Clear all logs and reset metrics
*/
public clearLogs() {
this.terminal?.clear();
this.logBuffer = [];
this.trailingHiddenCount = 0;
this.resetMetrics();
}
/**
* Scroll to the bottom of the log
*/
public scrollToBottom() {
this.terminal?.scrollToBottom();
}
// =====================
// Metrics Methods
// =====================
private updateMetrics(level: ILogEntry['level']) {
this.metrics = {
...this.metrics,
[level]: this.metrics[level] + 1,
total: this.metrics.total + 1,
};
this.recordLogEvent();
}
private recordLogEvent() {
this.rateBuffer.push(Date.now());
}
private calculateRate() {
const now = Date.now();
// Keep only events from the last 10 seconds
this.rateBuffer = this.rateBuffer.filter((t) => now - t < 10000);
const rate = this.rateBuffer.length / 10;
if (rate !== this.metrics.rate) {
this.metrics = { ...this.metrics, rate };
}
}
private resetMetrics() {
this.metrics = { debug: 0, info: 0, warn: 0, error: 0, success: 0, total: 0, rate: 0 };
this.rateBuffer = [];
}
// =====================
// Lifecycle
// =====================
async disconnectedCallback() {
await super.disconnectedCallback();
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.terminalThemeSubscription) {
this.terminalThemeSubscription.unsubscribe();
}
if (this.rateInterval) {
clearInterval(this.rateInterval);
}
if (this.terminal) {
this.terminal.dispose();
}
2024-02-05 10:07:49 +01:00
}
}