299 lines
9.5 KiB
TypeScript
299 lines
9.5 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-stats')
|
||
|
export class OpsViewStats extends DeesElement {
|
||
|
@state()
|
||
|
private statsState: appstate.IStatsState = {
|
||
|
serverStats: null,
|
||
|
emailStats: null,
|
||
|
dnsStats: null,
|
||
|
securityMetrics: null,
|
||
|
lastUpdated: 0,
|
||
|
isLoading: false,
|
||
|
error: null,
|
||
|
};
|
||
|
|
||
|
@state()
|
||
|
private uiState: appstate.IUiState = {
|
||
|
activeView: 'dashboard',
|
||
|
sidebarCollapsed: false,
|
||
|
autoRefresh: true,
|
||
|
refreshInterval: 30000,
|
||
|
theme: 'light',
|
||
|
};
|
||
|
|
||
|
constructor() {
|
||
|
super();
|
||
|
const statsSubscription = appstate.statsStatePart
|
||
|
.select((stateArg) => stateArg)
|
||
|
.subscribe((statsState) => {
|
||
|
this.statsState = statsState;
|
||
|
});
|
||
|
this.rxSubscriptions.push(statsSubscription);
|
||
|
|
||
|
const uiSubscription = appstate.uiStatePart
|
||
|
.select((stateArg) => stateArg)
|
||
|
.subscribe((uiState) => {
|
||
|
this.uiState = uiState;
|
||
|
});
|
||
|
this.rxSubscriptions.push(uiSubscription);
|
||
|
}
|
||
|
|
||
|
public static styles = [
|
||
|
cssManager.defaultStyles,
|
||
|
shared.viewHostCss,
|
||
|
css`
|
||
|
.controls {
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
align-items: center;
|
||
|
margin-bottom: 24px;
|
||
|
padding: 16px;
|
||
|
background: #f8f9fa;
|
||
|
border-radius: 8px;
|
||
|
}
|
||
|
|
||
|
.refreshButton {
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
gap: 8px;
|
||
|
}
|
||
|
|
||
|
.lastUpdated {
|
||
|
font-size: 14px;
|
||
|
color: #666;
|
||
|
}
|
||
|
|
||
|
.statsSection {
|
||
|
margin-bottom: 48px;
|
||
|
}
|
||
|
|
||
|
.sectionTitle {
|
||
|
font-size: 24px;
|
||
|
font-weight: 600;
|
||
|
margin-bottom: 24px;
|
||
|
color: #333;
|
||
|
}
|
||
|
|
||
|
.metricsGrid {
|
||
|
display: grid;
|
||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
|
gap: 16px;
|
||
|
}
|
||
|
|
||
|
.metricCard {
|
||
|
background: white;
|
||
|
border: 1px solid #e9ecef;
|
||
|
border-radius: 8px;
|
||
|
padding: 20px;
|
||
|
transition: all 0.2s ease;
|
||
|
}
|
||
|
|
||
|
.metricCard:hover {
|
||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
|
transform: translateY(-2px);
|
||
|
}
|
||
|
|
||
|
.metricLabel {
|
||
|
font-size: 14px;
|
||
|
color: #666;
|
||
|
margin-bottom: 8px;
|
||
|
}
|
||
|
|
||
|
.metricValue {
|
||
|
font-size: 28px;
|
||
|
font-weight: 700;
|
||
|
color: #2196F3;
|
||
|
}
|
||
|
|
||
|
.metricUnit {
|
||
|
font-size: 16px;
|
||
|
color: #999;
|
||
|
margin-left: 4px;
|
||
|
}
|
||
|
|
||
|
.chartContainer {
|
||
|
background: white;
|
||
|
border: 1px solid #e9ecef;
|
||
|
border-radius: 8px;
|
||
|
padding: 24px;
|
||
|
margin-top: 24px;
|
||
|
}
|
||
|
`,
|
||
|
];
|
||
|
|
||
|
public render() {
|
||
|
return html`
|
||
|
<ops-sectionheading>Statistics</ops-sectionheading>
|
||
|
|
||
|
<div class="controls">
|
||
|
<div class="refreshButton">
|
||
|
<dees-button
|
||
|
@click=${() => appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null)}
|
||
|
.disabled=${this.statsState.isLoading}
|
||
|
>
|
||
|
${this.statsState.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
|
||
|
</dees-button>
|
||
|
<dees-button
|
||
|
@click=${() => appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)}
|
||
|
.type=${this.uiState.autoRefresh ? 'highlighted' : 'normal'}
|
||
|
>
|
||
|
Auto-refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
|
||
|
</dees-button>
|
||
|
</div>
|
||
|
<div class="lastUpdated">
|
||
|
${this.statsState.lastUpdated ? html`
|
||
|
Last updated: ${new Date(this.statsState.lastUpdated).toLocaleTimeString()}
|
||
|
` : ''}
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
${this.statsState.serverStats ? html`
|
||
|
<div class="statsSection">
|
||
|
<h2 class="sectionTitle">Server Metrics</h2>
|
||
|
<div class="metricsGrid">
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Uptime</div>
|
||
|
<div class="metricValue">${this.formatUptime(this.statsState.serverStats.uptime)}</div>
|
||
|
</div>
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">CPU Usage</div>
|
||
|
<div class="metricValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}<span class="metricUnit">%</span></div>
|
||
|
</div>
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Memory Used</div>
|
||
|
<div class="metricValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
|
||
|
</div>
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Active Connections</div>
|
||
|
<div class="metricValue">${this.statsState.serverStats.activeConnections}</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
|
||
|
<div class="chartContainer">
|
||
|
<dees-chart-area
|
||
|
.label=${'Server Performance (Last 24 Hours)'}
|
||
|
.data=${[]}
|
||
|
></dees-chart-area>
|
||
|
</div>
|
||
|
</div>
|
||
|
` : ''}
|
||
|
|
||
|
${this.statsState.emailStats ? html`
|
||
|
<div class="statsSection">
|
||
|
<h2 class="sectionTitle">Email Statistics</h2>
|
||
|
<dees-table
|
||
|
.heading1=${'Email Metrics'}
|
||
|
.heading2=${'Current statistics for email processing'}
|
||
|
.data=${[
|
||
|
{ metric: 'Total Sent', value: this.statsState.emailStats.sent, unit: 'emails' },
|
||
|
{ metric: 'Total Received', value: this.statsState.emailStats.received, unit: 'emails' },
|
||
|
{ metric: 'Failed Deliveries', value: this.statsState.emailStats.failed, unit: 'emails' },
|
||
|
{ metric: 'Currently Queued', value: this.statsState.emailStats.queued, unit: 'emails' },
|
||
|
{ metric: 'Average Delivery Time', value: this.statsState.emailStats.averageDeliveryTime, unit: 'ms' },
|
||
|
{ metric: 'Delivery Rate', value: `${Math.round(this.statsState.emailStats.deliveryRate * 100)}`, unit: '%' },
|
||
|
]}
|
||
|
.displayFunction=${(item) => ({
|
||
|
Metric: item.metric,
|
||
|
Value: `${item.value} ${item.unit}`,
|
||
|
})}
|
||
|
></dees-table>
|
||
|
</div>
|
||
|
` : ''}
|
||
|
|
||
|
${this.statsState.dnsStats ? html`
|
||
|
<div class="statsSection">
|
||
|
<h2 class="sectionTitle">DNS Statistics</h2>
|
||
|
<div class="metricsGrid">
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Total Queries</div>
|
||
|
<div class="metricValue">${this.formatNumber(this.statsState.dnsStats.totalQueries)}</div>
|
||
|
</div>
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Cache Hit Rate</div>
|
||
|
<div class="metricValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}<span class="metricUnit">%</span></div>
|
||
|
</div>
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Average Response Time</div>
|
||
|
<div class="metricValue">${this.statsState.dnsStats.averageResponseTime}<span class="metricUnit">ms</span></div>
|
||
|
</div>
|
||
|
<div class="metricCard">
|
||
|
<div class="metricLabel">Domains Configured</div>
|
||
|
<div class="metricValue">${this.statsState.dnsStats.activeDomains}</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
` : ''}
|
||
|
|
||
|
${this.statsState.securityMetrics ? html`
|
||
|
<div class="statsSection">
|
||
|
<h2 class="sectionTitle">Security Metrics</h2>
|
||
|
<dees-table
|
||
|
.heading1=${'Security Events'}
|
||
|
.heading2=${'Recent security-related activities'}
|
||
|
.data=${[
|
||
|
{ metric: 'Blocked IPs', value: this.statsState.securityMetrics.blockedIPs.length, severity: 'high' },
|
||
|
{ metric: 'Failed Authentications', value: this.statsState.securityMetrics.authenticationFailures, severity: 'medium' },
|
||
|
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'low' },
|
||
|
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'medium' },
|
||
|
{ metric: 'Malware Detected', value: this.statsState.securityMetrics.malwareDetected, severity: 'high' },
|
||
|
{ metric: 'Phishing Detected', value: this.statsState.securityMetrics.phishingDetected, severity: 'high' },
|
||
|
]}
|
||
|
.displayFunction=${(item) => ({
|
||
|
'Security Metric': item.metric,
|
||
|
'Count': item.value,
|
||
|
'Severity': item.severity,
|
||
|
})}
|
||
|
></dees-table>
|
||
|
</div>
|
||
|
` : ''}
|
||
|
`;
|
||
|
}
|
||
|
|
||
|
private formatUptime(seconds: number): string {
|
||
|
const days = Math.floor(seconds / 86400);
|
||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||
|
|
||
|
if (days > 0) {
|
||
|
return `${days}d ${hours}h`;
|
||
|
} else if (hours > 0) {
|
||
|
return `${hours}h ${minutes}m`;
|
||
|
} else {
|
||
|
return `${minutes}m`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private formatBytes(bytes: number): string {
|
||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
|
let size = bytes;
|
||
|
let unitIndex = 0;
|
||
|
|
||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
|
size /= 1024;
|
||
|
unitIndex++;
|
||
|
}
|
||
|
|
||
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||
|
}
|
||
|
|
||
|
private formatNumber(num: number): string {
|
||
|
if (num >= 1000000) {
|
||
|
return `${(num / 1000000).toFixed(1)}M`;
|
||
|
} else if (num >= 1000) {
|
||
|
return `${(num / 1000).toFixed(1)}K`;
|
||
|
}
|
||
|
return num.toString();
|
||
|
}
|
||
|
}
|