From fad7fda2a6a66a0fc0bf86fcd76c7ae30d65d994 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 12 Jun 2025 10:44:21 +0000 Subject: [PATCH] feat(dees-chart-log): Enhance log component with realistic log simulation and improved UI controls --- readme.hints.md | 26 ++- ts_web/elements/dees-chart-log.demo.ts | 139 +++++++++++- ts_web/elements/dees-chart-log.ts | 290 ++++++++++++++++++++++--- 3 files changed, 427 insertions(+), 28 deletions(-) diff --git a/readme.hints.md b/readme.hints.md index adce7e1..ec2dbb9 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -1,4 +1,28 @@ !!! Please pay attention to the following points when writing the readme: !!! * Give a short rundown of components and a few points abputspecific features on each. * Try to list all components in a summary. -* Then list all components with a short description. \ No newline at end of file +* Then list all components with a short description. + +## Chart Components + +### dees-chart-area +- Fully functional area chart component using ApexCharts +- Displays time-series data with gradient fills +- Responsive with ResizeObserver +- Demo shows CPU and memory usage metrics + +### dees-chart-log +- Server log viewer component (not a chart despite the name) +- Terminal-style interface with monospace font +- Supports log levels: debug, info, warn, error, success +- Features: + - Auto-scroll toggle + - Clear logs button + - Colored log levels + - Timestamp with milliseconds + - Source labels for log entries + - Maximum 1000 entries (configurable) + - Light/dark theme support +- Demo includes realistic server log simulation +- Note: In demos, buttons use `@clicked` event (not `@click`) +- Demo uses global reference to access log element (window.__demoLogElement) \ No newline at end of file diff --git a/ts_web/elements/dees-chart-log.demo.ts b/ts_web/elements/dees-chart-log.demo.ts index 6db6b15..2fa968a 100644 --- a/ts_web/elements/dees-chart-log.demo.ts +++ b/ts_web/elements/dees-chart-log.demo.ts @@ -1,6 +1,123 @@ import { html } from '@design.estate/dees-element'; export const demoFunc = () => { + let intervalId: number; + + const serverSources = ['Server', 'Database', 'API', 'Auth', 'Cache', 'Queue', 'WebSocket', 'Scheduler']; + + const logTemplates = { + debug: [ + 'Loading module: {{module}}', + 'Cache hit for key: {{key}}', + 'SQL query executed in {{time}}ms', + 'Request headers: {{headers}}', + 'Environment variable loaded: {{var}}', + ], + info: [ + 'Request received: {{method}} {{path}}', + 'User {{userId}} authenticated successfully', + 'Processing job {{jobId}} from queue', + 'Scheduled task "{{task}}" started', + 'WebSocket connection established from {{ip}}', + ], + warn: [ + 'Slow query detected: {{query}} ({{time}}ms)', + 'Memory usage at {{percent}}%', + 'Rate limit approaching for IP {{ip}}', + 'Deprecated API endpoint called: {{endpoint}}', + 'Certificate expires in {{days}} days', + ], + error: [ + 'Database connection lost: {{error}}', + 'Failed to process request: {{error}}', + 'Authentication failed for user {{user}}', + 'File not found: {{path}}', + 'Service unavailable: {{service}}', + ], + success: [ + 'Server started successfully on port {{port}}', + 'Database migration completed', + 'Backup completed: {{size}} MB', + 'SSL certificate renewed', + 'Health check passed: all systems operational', + ], + }; + + const generateRandomLog = () => { + const logElement = (window as any).__demoLogElement; + if (!logElement) { + console.warn('Log element not ready yet'); + return; + } + const levels: Array<'debug' | 'info' | 'warn' | 'error' | 'success'> = ['debug', 'info', 'warn', 'error', 'success']; + const weights = [0.2, 0.5, 0.15, 0.1, 0.05]; // Weighted probability + + const random = Math.random(); + let cumulative = 0; + let level: typeof levels[0] = 'info'; + + for (let i = 0; i < weights.length; i++) { + cumulative += weights[i]; + if (random < cumulative) { + level = levels[i]; + break; + } + } + + const source = serverSources[Math.floor(Math.random() * serverSources.length)]; + const templates = logTemplates[level]; + const template = templates[Math.floor(Math.random() * templates.length)]; + + // Replace placeholders with random values + const message = template + .replace('{{module}}', ['express', 'mongoose', 'redis', 'socket.io'][Math.floor(Math.random() * 4)]) + .replace('{{key}}', 'user:' + Math.floor(Math.random() * 1000)) + .replace('{{time}}', String(Math.floor(Math.random() * 500) + 50)) + .replace('{{headers}}', 'Content-Type: application/json, Authorization: Bearer ...') + .replace('{{var}}', ['NODE_ENV', 'DATABASE_URL', 'API_KEY', 'PORT'][Math.floor(Math.random() * 4)]) + .replace('{{method}}', ['GET', 'POST', 'PUT', 'DELETE'][Math.floor(Math.random() * 4)]) + .replace('{{path}}', ['/api/users', '/api/auth/login', '/api/products', '/health'][Math.floor(Math.random() * 4)]) + .replace('{{userId}}', String(Math.floor(Math.random() * 10000))) + .replace('{{jobId}}', 'job_' + Math.random().toString(36).substring(2, 11)) + .replace('{{task}}', ['cleanup', 'backup', 'report-generation', 'cache-refresh'][Math.floor(Math.random() * 4)]) + .replace('{{ip}}', `192.168.1.${Math.floor(Math.random() * 255)}`) + .replace('{{query}}', 'SELECT * FROM users WHERE ...') + .replace('{{percent}}', String(Math.floor(Math.random() * 30) + 70)) + .replace('{{endpoint}}', '/api/v1/legacy') + .replace('{{days}}', String(Math.floor(Math.random() * 30) + 1)) + .replace('{{error}}', ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'][Math.floor(Math.random() * 3)]) + .replace('{{user}}', 'user_' + Math.floor(Math.random() * 1000)) + .replace('{{service}}', ['Redis', 'MongoDB', 'ElasticSearch'][Math.floor(Math.random() * 3)]) + .replace('{{port}}', String(3000 + Math.floor(Math.random() * 10))) + .replace('{{size}}', String(Math.floor(Math.random() * 500) + 100)); + + logElement.addLog(level, message, source); + }; + + const startSimulation = () => { + if (!intervalId) { + // Generate logs at random intervals between 500ms and 2500ms + const scheduleNext = () => { + generateRandomLog(); + const nextDelay = Math.random() * 2000 + 500; + intervalId = window.setTimeout(() => { + if (intervalId) { + scheduleNext(); + } + }, nextDelay); + }; + scheduleNext(); + } + }; + + const stopSimulation = () => { + if (intervalId) { + window.clearTimeout(intervalId); + intervalId = null; + } + }; + + return html`
+
+ generateRandomLog()}>Add Single Log + startSimulation()}>Start Simulation + stopSimulation()}>Stop Simulation +
+
Simulating realistic server logs with various levels and sources
`; diff --git a/ts_web/elements/dees-chart-log.ts b/ts_web/elements/dees-chart-log.ts index 25a0a54..8678056 100644 --- a/ts_web/elements/dees-chart-log.ts +++ b/ts_web/elements/dees-chart-log.ts @@ -13,7 +13,6 @@ import { import * as domtools from '@design.estate/dees-domtools'; import { demoFunc } from './dees-chart-log.demo.js'; -import ApexCharts from 'apexcharts'; declare global { interface HTMLElementTagNameMap { @@ -21,69 +20,308 @@ declare global { } } +export interface ILogEntry { + timestamp: string; + level: 'debug' | 'info' | 'warn' | 'error' | 'success'; + message: string; + source?: string; +} + @customElement('dees-chart-log') export class DeesChartLog extends DeesElement { public static demo = demoFunc; - // instance - @state() - public chart: ApexCharts; - @property() - public label: string = 'Untitled Chart'; + public label: string = 'Server Logs'; + + @property({ type: Array }) + public logEntries: ILogEntry[] = []; + + @property({ type: Boolean }) + public autoScroll: boolean = true; + + @property({ type: Number }) + public maxEntries: number = 1000; + + private logContainer: HTMLDivElement; constructor() { super(); domtools.elementBasic.setup(); + } public static styles = [ cssManager.defaultStyles, css` :host { - font-family: 'Geist Sans', sans-serif; + font-family: 'Geist Mono', 'Consolas', 'Monaco', monospace; color: #ccc; - font-weight: 600; font-size: 12px; + line-height: 1.4; } .mainbox { position: relative; width: 100%; height: 400px; - background: #222; + background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; + border: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')}; border-radius: 8px; - padding: 32px 16px 16px 0px; + display: flex; + flex-direction: column; + overflow: hidden; } - .chartTitle { - position: absolute; - top: 0; - left: 0; - width: 100%; - text-align: center; - padding-top: 16px; + .header { + background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')}; + padding: 8px 16px; + border-bottom: 1px solid ${cssManager.bdTheme('#dee2e6', '#333')}; + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; } - .chartContainer { - position: relative; - width: 100%; + + .title { + font-weight: 600; + color: ${cssManager.bdTheme('#212529', '#fff')}; + } + + .controls { + display: flex; + gap: 8px; + } + + .control-button { + background: ${cssManager.bdTheme('#e9ecef', '#2a2a2a')}; + border: 1px solid ${cssManager.bdTheme('#ced4da', '#444')}; + border-radius: 4px; + padding: 4px 8px; + color: ${cssManager.bdTheme('#495057', '#ccc')}; + cursor: pointer; + font-size: 11px; + transition: all 0.2s; + } + + .control-button:hover { + background: ${cssManager.bdTheme('#dee2e6', '#3a3a3a')}; + border-color: ${cssManager.bdTheme('#adb5bd', '#555')}; + } + + .control-button.active { + background: ${cssManager.bdTheme('#007bff', '#4a4a4a')}; + color: ${cssManager.bdTheme('#fff', '#fff')}; + } + + .logContainer { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 8px 16px; + font-size: 12px; + } + + .logEntry { + margin-bottom: 2px; + display: flex; + white-space: pre-wrap; + word-break: break-all; + } + + .timestamp { + color: ${cssManager.bdTheme('#6c757d', '#666')}; + margin-right: 8px; + flex-shrink: 0; + } + + .level { + margin-right: 8px; + padding: 0 6px; + border-radius: 3px; + font-weight: 600; + text-transform: uppercase; + font-size: 10px; + flex-shrink: 0; + } + + .level.debug { + color: ${cssManager.bdTheme('#6c757d', '#999')}; + background: ${cssManager.bdTheme('rgba(108, 117, 125, 0.1)', '#333')}; + } + + .level.info { + color: ${cssManager.bdTheme('#0066cc', '#4a9eff')}; + background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(74, 158, 255, 0.1)')}; + } + + .level.warn { + color: ${cssManager.bdTheme('#ff8800', '#ffb84a')}; + background: ${cssManager.bdTheme('rgba(255, 136, 0, 0.1)', 'rgba(255, 184, 74, 0.1)')}; + } + + .level.error { + color: ${cssManager.bdTheme('#dc3545', '#ff4a4a')}; + background: ${cssManager.bdTheme('rgba(220, 53, 69, 0.1)', 'rgba(255, 74, 74, 0.1)')}; + } + + .level.success { + color: ${cssManager.bdTheme('#28a745', '#4aff88')}; + background: ${cssManager.bdTheme('rgba(40, 167, 69, 0.1)', 'rgba(74, 255, 136, 0.1)')}; + } + + .source { + color: ${cssManager.bdTheme('#6c757d', '#888')}; + margin-right: 8px; + flex-shrink: 0; + } + + .message { + color: ${cssManager.bdTheme('#212529', '#ddd')}; + flex: 1; + } + + .empty-state { + display: flex; + align-items: center; + justify-content: center; height: 100%; + color: ${cssManager.bdTheme('#6c757d', '#666')}; + font-style: italic; + } + + /* Custom scrollbar */ + .logContainer::-webkit-scrollbar { + width: 8px; + } + + .logContainer::-webkit-scrollbar-track { + background: ${cssManager.bdTheme('#e9ecef', '#1a1a1a')}; + } + + .logContainer::-webkit-scrollbar-thumb { + background: ${cssManager.bdTheme('#adb5bd', '#444')}; + border-radius: 4px; + } + + .logContainer::-webkit-scrollbar-thumb:hover { + background: ${cssManager.bdTheme('#6c757d', '#555')}; } `, ]; public render(): TemplateResult { - return html`
-
${this.label}
-
-
`; + return html` +
+
+
${this.label}
+
+ + +
+
+
+ ${this.logEntries.length === 0 + ? html`
No logs to display
` + : this.logEntries.map(entry => this.renderLogEntry(entry)) + } +
+
+ `; + } + + private renderLogEntry(entry: ILogEntry): TemplateResult { + const timestamp = new Date(entry.timestamp).toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3 + }); + + return html` +
+ ${timestamp} + ${entry.level} + ${entry.source ? html`[${entry.source}]` : ''} + ${entry.message} +
+ `; } public async firstUpdated() { const domtoolsInstance = await this.domtoolsPromise; + this.logContainer = this.shadowRoot.querySelector('.logContainer'); + + // Initialize with demo server logs + const demoLogs: ILogEntry[] = [ + { timestamp: new Date().toISOString(), level: 'info', message: 'Server started on port 3000', source: 'Server' }, + { timestamp: new Date().toISOString(), level: 'debug', message: 'Loading configuration from /etc/app/config.json', source: 'Config' }, + { timestamp: new Date().toISOString(), level: 'info', message: 'Connected to MongoDB at mongodb://localhost:27017', source: 'Database' }, + { timestamp: new Date().toISOString(), level: 'success', message: 'Database connection established successfully', source: 'Database' }, + { timestamp: new Date().toISOString(), level: 'warn', message: 'No SSL certificate found, using self-signed certificate', source: 'Security' }, + { timestamp: new Date().toISOString(), level: 'info', message: 'API routes initialized: GET /api/users, POST /api/users, DELETE /api/users/:id', source: 'Router' }, + { timestamp: new Date().toISOString(), level: 'debug', message: 'Middleware stack: cors, bodyParser, authentication, errorHandler', source: 'Middleware' }, + { timestamp: new Date().toISOString(), level: 'info', message: 'WebSocket server listening on ws://localhost:3001', source: 'WebSocket' }, + ]; + + this.logEntries = demoLogs; + this.scrollToBottom(); + // For demo purposes, store reference globally + if ((window as any).__demoLogElement === undefined) { + (window as any).__demoLogElement = this; + } } - public async updateLog() { - + public async updateLog(entries?: ILogEntry[]) { + if (entries) { + // Add new entries + this.logEntries = [...this.logEntries, ...entries]; + + // Trim if exceeds max entries + if (this.logEntries.length > this.maxEntries) { + this.logEntries = this.logEntries.slice(-this.maxEntries); + } + + // Trigger re-render + this.requestUpdate(); + + // Auto-scroll if enabled + await this.updateComplete; + if (this.autoScroll) { + this.scrollToBottom(); + } + } + } + + public clearLogs() { + this.logEntries = []; + this.requestUpdate(); + } + + private scrollToBottom() { + if (this.logContainer) { + this.logContainer.scrollTop = this.logContainer.scrollHeight; + } + } + + public addLog(level: ILogEntry['level'], message: string, source?: string) { + const newEntry: ILogEntry = { + timestamp: new Date().toISOString(), + level, + message, + source + }; + this.updateLog([newEntry]); } }