327 lines
9.8 KiB
TypeScript
327 lines
9.8 KiB
TypeScript
import {
|
|
DeesElement,
|
|
css,
|
|
cssManager,
|
|
customElement,
|
|
html,
|
|
property,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import * as domtools from '@design.estate/dees-domtools';
|
|
import { demoFunc } from './dees-chart-log.demo.js';
|
|
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'dees-chart-log': DeesChartLog;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
@property()
|
|
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: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
.mainbox {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 400px;
|
|
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%)')};
|
|
border-radius: 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(0 0% 7%)')};
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.title {
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.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: 6px;
|
|
padding: 6px 12px;
|
|
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;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
}
|
|
|
|
.control-button:hover {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
|
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
|
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
|
|
}
|
|
|
|
.control-button.active {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 93.9%)')};
|
|
color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
|
|
}
|
|
|
|
.logContainer {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
padding: 16px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.logEntry {
|
|
margin-bottom: 4px;
|
|
display: flex;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.timestamp {
|
|
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
|
|
margin-right: 12px;
|
|
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('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
background: ${cssManager.bdTheme('hsl(0 0% 45.1% / 0.1)', 'hsl(0 0% 63.9% / 0.1)')};
|
|
}
|
|
|
|
.level.info {
|
|
color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
|
background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
|
|
}
|
|
|
|
.level.warn {
|
|
color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
|
|
background: ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')};
|
|
}
|
|
|
|
.level.error {
|
|
color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
|
|
background: ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
|
|
}
|
|
|
|
.level.success {
|
|
color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
|
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
|
|
}
|
|
|
|
.source {
|
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
margin-right: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.message {
|
|
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
|
|
flex: 1;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 100%;
|
|
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.logContainer::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.logContainer::-webkit-scrollbar-track {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 10%)')};
|
|
}
|
|
|
|
.logContainer::-webkit-scrollbar-thumb {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 70%)', 'hsl(0 0% 30%)')};
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.logContainer::-webkit-scrollbar-thumb:hover {
|
|
background: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 40%)')};
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<div class="mainbox">
|
|
<div class="header">
|
|
<div class="title">${this.label}</div>
|
|
<div class="controls">
|
|
<button
|
|
class="control-button ${this.autoScroll ? 'active' : ''}"
|
|
@click=${() => { this.autoScroll = !this.autoScroll; }}
|
|
>
|
|
Auto Scroll
|
|
</button>
|
|
<button
|
|
class="control-button"
|
|
@click=${() => { this.clearLogs(); }}
|
|
>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="logContainer">
|
|
${this.logEntries.length === 0
|
|
? html`<div class="empty-state">No logs to display</div>`
|
|
: this.logEntries.map(entry => this.renderLogEntry(entry))
|
|
}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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`
|
|
<div class="logEntry">
|
|
<span class="timestamp">${timestamp}</span>
|
|
<span class="level ${entry.level}">${entry.level}</span>
|
|
${entry.source ? html`<span class="source">[${entry.source}]</span>` : ''}
|
|
<span class="message">${entry.message}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
public async firstUpdated() {
|
|
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();
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|