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` `;
+ return html`
+
+
+
+ ${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]);
}
}