195 lines
5.1 KiB
TypeScript
195 lines
5.1 KiB
TypeScript
import * as plugins from '../plugins.js';
|
|
import * as shared from './shared/index.js';
|
|
import * as appstate from '../appstate.js';
|
|
|
|
import {
|
|
DeesElement,
|
|
customElement,
|
|
html,
|
|
state,
|
|
css,
|
|
cssManager,
|
|
} from '@design.estate/dees-element';
|
|
|
|
@customElement('ops-view-logs')
|
|
export class OpsViewLogs extends DeesElement {
|
|
@state()
|
|
accessor logState: appstate.ILogState = {
|
|
recentLogs: [],
|
|
isStreaming: false,
|
|
filters: {},
|
|
};
|
|
|
|
@state()
|
|
accessor filterLevel: string | undefined;
|
|
|
|
@state()
|
|
accessor filterCategory: string | undefined;
|
|
|
|
@state()
|
|
accessor filterLimit: number = 100;
|
|
|
|
private lastPushedCount = 0;
|
|
|
|
constructor() {
|
|
super();
|
|
const subscription = appstate.logStatePart
|
|
.select((stateArg) => stateArg)
|
|
.subscribe((logState) => {
|
|
this.logState = logState;
|
|
});
|
|
this.rxSubscriptions.push(subscription);
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
shared.viewHostCss,
|
|
css`
|
|
.controls {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-bottom: 24px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filterGroup {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
`,
|
|
];
|
|
|
|
public render() {
|
|
return html`
|
|
<ops-sectionheading>Logs</ops-sectionheading>
|
|
|
|
<div class="controls">
|
|
<div class="filterGroup">
|
|
<dees-button
|
|
@click=${() => this.fetchLogs()}
|
|
>
|
|
Refresh Logs
|
|
</dees-button>
|
|
</div>
|
|
|
|
<div class="filterGroup">
|
|
<label>Level:</label>
|
|
<dees-input-dropdown
|
|
.options=${['all', 'debug', 'info', 'warn', 'error']}
|
|
.selectedOption=${'all'}
|
|
@selectedOption=${(e: any) => this.updateFilter('level', e.detail)}
|
|
></dees-input-dropdown>
|
|
</div>
|
|
|
|
<div class="filterGroup">
|
|
<label>Category:</label>
|
|
<dees-input-dropdown
|
|
.options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
|
|
.selectedOption=${'all'}
|
|
@selectedOption=${(e: any) => this.updateFilter('category', e.detail)}
|
|
></dees-input-dropdown>
|
|
</div>
|
|
|
|
<div class="filterGroup">
|
|
<label>Limit:</label>
|
|
<dees-input-dropdown
|
|
.options=${['50', '100', '200', '500']}
|
|
.selectedOption=${'100'}
|
|
@selectedOption=${(e: any) => this.updateFilter('limit', e.detail)}
|
|
></dees-input-dropdown>
|
|
</div>
|
|
</div>
|
|
|
|
<dees-chart-log
|
|
.label=${'Application Logs'}
|
|
.autoScroll=${true}
|
|
.maxEntries=${2000}
|
|
.showMetrics=${true}
|
|
></dees-chart-log>
|
|
`;
|
|
}
|
|
|
|
async connectedCallback() {
|
|
super.connectedCallback();
|
|
this.lastPushedCount = 0;
|
|
// Only fetch if state is empty (streaming will handle new entries)
|
|
if (this.logState.recentLogs.length === 0) {
|
|
this.fetchLogs();
|
|
}
|
|
}
|
|
|
|
async updated(changedProperties: Map<string, any>) {
|
|
super.updated(changedProperties);
|
|
if (changedProperties.has('logState')) {
|
|
this.pushLogsToChart();
|
|
}
|
|
}
|
|
|
|
private async pushLogsToChart() {
|
|
const chartLog = this.shadowRoot?.querySelector('dees-chart-log') as any;
|
|
if (!chartLog) return;
|
|
|
|
// Ensure the chart element has finished its own initialization
|
|
await chartLog.updateComplete;
|
|
|
|
// Wait for xterm terminal to finish initializing (CDN load)
|
|
if (!chartLog.terminalReady) {
|
|
await new Promise<void>((resolve) => {
|
|
const check = () => {
|
|
if (chartLog.terminalReady) { resolve(); return; }
|
|
setTimeout(check, 50);
|
|
};
|
|
check();
|
|
});
|
|
}
|
|
|
|
const allEntries = this.getMappedLogEntries();
|
|
if (this.lastPushedCount === 0 && allEntries.length > 0) {
|
|
// Initial load: push all entries
|
|
chartLog.updateLog(allEntries);
|
|
this.lastPushedCount = allEntries.length;
|
|
} else if (allEntries.length > this.lastPushedCount) {
|
|
// Incremental: only push new entries
|
|
const newEntries = allEntries.slice(this.lastPushedCount);
|
|
chartLog.updateLog(newEntries);
|
|
this.lastPushedCount = allEntries.length;
|
|
}
|
|
}
|
|
|
|
private getMappedLogEntries() {
|
|
return this.logState.recentLogs.map((log) => ({
|
|
timestamp: new Date(log.timestamp).toISOString(),
|
|
level: log.level as 'debug' | 'info' | 'warn' | 'error',
|
|
message: log.message,
|
|
source: log.category,
|
|
}));
|
|
}
|
|
|
|
private async fetchLogs() {
|
|
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
|
limit: this.filterLimit,
|
|
level: this.filterLevel as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
|
category: this.filterCategory as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
|
});
|
|
}
|
|
|
|
private updateFilter(type: string, value: string) {
|
|
const resolved = value === 'all' ? undefined : value;
|
|
|
|
switch (type) {
|
|
case 'level':
|
|
this.filterLevel = resolved;
|
|
break;
|
|
case 'category':
|
|
this.filterCategory = resolved;
|
|
break;
|
|
case 'limit':
|
|
this.filterLimit = resolved ? parseInt(resolved, 10) : 100;
|
|
break;
|
|
}
|
|
|
|
this.fetchLogs();
|
|
}
|
|
}
|