feat: Add operations view components for logs, overview, security, and stats
- Implemented `ops-view-logs` for displaying and filtering logs with streaming capabilities. - Created `ops-view-overview` to show server, email, DNS statistics, and charts. - Developed `ops-view-security` for monitoring security metrics, blocked IPs, and authentication attempts. - Added `ops-view-stats` to present comprehensive statistics on server, email, DNS, and security metrics. - Introduced shared styles and components including `ops-sectionheading` for consistent UI.
This commit is contained in:
299
ts_web/elements/ops-view-stats.ts
Normal file
299
ts_web/elements/ops-view-stats.ts
Normal file
@ -0,0 +1,299 @@
|
||||
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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user