feat: Implement dees-statsgrid in DCRouter UI for enhanced stats visualization

- Added new readme.statsgrid.md outlining the implementation plan for dees-statsgrid component.
- Replaced custom stats cards in ops-view-overview.ts and ops-view-network.ts with dees-statsgrid for better visualization.
- Introduced consistent color scheme for success, warning, error, and info states.
- Enhanced interactive features including click actions, context menus, and real-time updates.
- Developed ops-view-emails.ts for email management with features like composing, searching, and viewing emails.
- Integrated mock data generation for emails and network requests to facilitate testing.
- Added responsive design elements and improved UI consistency across components.
This commit is contained in:
Juergen Kunz
2025-06-12 08:04:30 +00:00
parent 0eb4963247
commit facae93e4b
9 changed files with 1543 additions and 499 deletions

View File

@ -1,6 +1,7 @@
export * from './ops-dashboard.js';
export * from './ops-view-overview.js';
export * from './ops-view-stats.js';
export * from './ops-view-network.js';
export * from './ops-view-emails.js';
export * from './ops-view-logs.js';
export * from './ops-view-config.js';
export * from './ops-view-security.js';

View File

@ -13,7 +13,8 @@ import {
// Import view components
import { OpsViewOverview } from './ops-view-overview.js';
import { OpsViewStats } from './ops-view-stats.js';
import { OpsViewNetwork } from './ops-view-network.js';
import { OpsViewEmails } from './ops-view-emails.js';
import { OpsViewLogs } from './ops-view-logs.js';
import { OpsViewConfig } from './ops-view-config.js';
import { OpsViewSecurity } from './ops-view-security.js';
@ -84,8 +85,12 @@ export class OpsDashboard extends DeesElement {
element: OpsViewOverview,
},
{
name: 'Statistics',
element: OpsViewStats,
name: 'Network',
element: OpsViewNetwork,
},
{
name: 'Emails',
element: OpsViewEmails,
},
{
name: 'Logs',

View File

@ -0,0 +1,697 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
declare global {
interface HTMLElementTagNameMap {
'ops-view-emails': OpsViewEmails;
}
}
interface IEmail {
id: string;
from: string;
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
body: string;
html?: string;
attachments?: Array<{
filename: string;
size: number;
contentType: string;
}>;
date: number;
read: boolean;
folder: 'inbox' | 'sent' | 'draft' | 'trash';
flags?: string[];
messageId?: string;
inReplyTo?: string;
}
@customElement('ops-view-emails')
export class OpsViewEmails extends DeesElement {
@state()
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
@state()
private emails: IEmail[] = [];
@state()
private selectedEmail: IEmail | null = null;
@state()
private showCompose = false;
@state()
private isLoading = false;
@state()
private searchTerm = '';
constructor() {
super();
this.loadEmails();
}
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
height: 100%;
padding: 24px;
}
.emailContainer {
display: grid;
grid-template-columns: 250px 1fr;
gap: 24px;
height: calc(100vh - 200px);
}
.sidebar {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.folderList {
display: flex;
flex-direction: column;
gap: 8px;
}
.folderItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.folderItem:hover {
background: #f5f5f5;
}
.folderItem.selected {
background: #e3f2fd;
color: #1976d2;
}
.folderIcon {
font-size: 18px;
}
.folderLabel {
flex: 1;
font-weight: 500;
}
.folderCount {
background: #e0e0e0;
color: #666;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.folderItem.selected .folderCount {
background: #1976d2;
color: white;
}
.mainContent {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.emailToolbar {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e9ecef;
background: #fafafa;
}
.searchBox {
flex: 1;
max-width: 400px;
}
.emailList {
flex: 1;
overflow-y: auto;
}
.emailPreview {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
}
.emailHeader {
padding: 20px;
border-bottom: 1px solid #e9ecef;
background: #fafafa;
}
.emailSubject {
font-size: 20px;
font-weight: 600;
margin-bottom: 12px;
}
.emailMeta {
display: flex;
gap: 16px;
font-size: 14px;
color: #666;
}
.emailBody {
padding: 20px;
max-height: 500px;
overflow-y: auto;
}
.emailActions {
display: flex;
gap: 8px;
padding: 16px;
border-top: 1px solid #e9ecef;
background: #fafafa;
}
.composeModal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.composeContent {
background: white;
border-radius: 8px;
width: 90%;
max-width: 800px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.composeHeader {
padding: 20px;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: space-between;
}
.composeTitle {
font-size: 20px;
font-weight: 600;
}
.composeForm {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.composeActions {
padding: 20px;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
}
.emptyIcon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.3;
}
.emptyText {
font-size: 18px;
}
`,
];
public render() {
return html`
<ops-sectionheading>Emails</ops-sectionheading>
<div class="emailContainer">
<!-- Sidebar -->
<div class="sidebar">
<dees-button @click=${() => this.showCompose = true} type="highlighted">
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
Compose
</dees-button>
<div class="folderList">
${this.renderFolderItem('inbox', 'inbox', 'Inbox', this.getEmailCount('inbox'))}
${this.renderFolderItem('sent', 'paperPlane', 'Sent', this.getEmailCount('sent'))}
${this.renderFolderItem('draft', 'file', 'Drafts', this.getEmailCount('draft'))}
${this.renderFolderItem('trash', 'trash', 'Trash', this.getEmailCount('trash'))}
</div>
</div>
<!-- Main Content -->
<div class="mainContent">
<!-- Toolbar -->
<div class="emailToolbar">
<dees-input-text
class="searchBox"
placeholder="Search emails..."
.value=${this.searchTerm}
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
>
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
</dees-input-text>
<dees-button @click=${() => this.refreshEmails()}>
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : html`<dees-icon name="arrowsRotate"></dees-icon>`}
</dees-button>
<dees-button @click=${() => this.markAllAsRead()}>
<dees-icon name="envelopeOpen"></dees-icon>
Mark all read
</dees-button>
</div>
<!-- Email List or Preview -->
${this.selectedEmail ? this.renderEmailPreview() : this.renderEmailList()}
</div>
</div>
<!-- Compose Modal -->
${this.showCompose ? this.renderComposeModal() : ''}
`;
}
private renderFolderItem(folder: string, icon: string, label: string, count: number) {
return html`
<div
class="folderItem ${this.selectedFolder === folder ? 'selected' : ''}"
@click=${() => this.selectFolder(folder as any)}
>
<dees-icon class="folderIcon" name="${icon}"></dees-icon>
<span class="folderLabel">${label}</span>
${count > 0 ? html`<span class="folderCount">${count}</span>` : ''}
</div>
`;
}
private renderEmailList() {
const filteredEmails = this.getFilteredEmails();
if (filteredEmails.length === 0) {
return html`
<div class="emptyState">
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
<div class="emptyText">No emails in ${this.selectedFolder}</div>
</div>
`;
}
return html`
<div class="emailList">
<dees-table
.data=${filteredEmails}
.displayFunction=${(email: IEmail) => ({
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" style="color: ${email.read ? '#999' : '#1976d2'}"></dees-icon>`,
From: email.from,
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
Date: this.formatDate(email.date),
'Attach': html`
${email.attachments?.length ? html`<dees-icon name="paperclip" style="color: #666"></dees-icon>` : ''}
`,
})}
.dataActions=${[
{
name: 'Read',
iconName: 'eye',
type: ['doubleClick', 'inRow'],
actionFunc: async (actionData) => {
this.selectedEmail = actionData.item;
if (!actionData.item.read) {
this.markAsRead(actionData.item.id);
}
}
},
{
name: 'Reply',
iconName: 'reply',
type: ['contextmenu'],
actionFunc: async (actionData) => {
this.replyToEmail(actionData.item);
}
},
{
name: 'Forward',
iconName: 'share',
type: ['contextmenu'],
actionFunc: async (actionData) => {
this.forwardEmail(actionData.item);
}
},
{
name: 'Delete',
iconName: 'trash',
type: ['contextmenu'],
actionFunc: async (actionData) => {
this.deleteEmail(actionData.item.id);
}
}
]}
.selectionMode=${'single'}
></dees-table>
</div>
`;
}
private renderEmailPreview() {
if (!this.selectedEmail) return '';
return html`
<div class="emailPreview">
<div class="emailHeader">
<div class="emailSubject">${this.selectedEmail.subject}</div>
<div class="emailMeta">
<span><strong>From:</strong> ${this.selectedEmail.from}</span>
<span><strong>To:</strong> ${this.selectedEmail.to.join(', ')}</span>
<span><strong>Date:</strong> ${this.formatDate(this.selectedEmail.date)}</span>
</div>
</div>
<div class="emailBody">
${this.selectedEmail.html ?
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
html`<pre style="white-space: pre-wrap; font-family: inherit;">${this.selectedEmail.body}</pre>`
}
</div>
<div class="emailActions">
<dees-button @click=${() => this.selectedEmail = null}>
<dees-icon name="arrowLeft"></dees-icon>
Back
</dees-button>
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
<dees-icon name="reply"></dees-icon>
Reply
</dees-button>
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
<dees-icon name="replyAll"></dees-icon>
Reply All
</dees-button>
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
<dees-icon name="share"></dees-icon>
Forward
</dees-button>
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
<dees-icon name="trash"></dees-icon>
Delete
</dees-button>
</div>
</div>
`;
}
private renderComposeModal() {
return html`
<div class="composeModal" @click=${(e: Event) => {
if (e.target === e.currentTarget) this.showCompose = false;
}}>
<div class="composeContent">
<div class="composeHeader">
<div class="composeTitle">New Email</div>
<dees-button @click=${() => this.showCompose = false} type="ghost">
<dees-icon name="xmark"></dees-icon>
</dees-button>
</div>
<div class="composeForm">
<dees-form @formData=${(e: CustomEvent) => this.sendEmail(e.detail)}>
<dees-input-tags
key="to"
label="To"
placeholder="Enter recipient email addresses..."
required
></dees-input-tags>
<dees-input-tags
key="cc"
label="CC"
placeholder="Enter CC recipients..."
></dees-input-tags>
<dees-input-tags
key="bcc"
label="BCC"
placeholder="Enter BCC recipients..."
></dees-input-tags>
<dees-input-text
key="subject"
label="Subject"
placeholder="Enter email subject..."
required
></dees-input-text>
<dees-editor
key="body"
label="Message"
.mode=${'markdown'}
.height=${300}
required
></dees-editor>
<dees-input-fileupload
key="attachments"
label="Attachments"
multiple
></dees-input-fileupload>
<div class="composeActions">
<dees-button @click=${() => this.showCompose = false} type="secondary">
Cancel
</dees-button>
<dees-form-submit>
<dees-icon name="paperPlane"></dees-icon>
Send Email
</dees-form-submit>
</div>
</dees-form>
</div>
</div>
</div>
`;
}
private getFilteredEmails(): IEmail[] {
let emails = this.emails.filter(e => e.folder === this.selectedFolder);
if (this.searchTerm) {
const search = this.searchTerm.toLowerCase();
emails = emails.filter(e =>
e.subject.toLowerCase().includes(search) ||
e.from.toLowerCase().includes(search) ||
e.body.toLowerCase().includes(search)
);
}
return emails.sort((a, b) => b.date - a.date);
}
private getEmailCount(folder: string): number {
return this.emails.filter(e => e.folder === folder && !e.read).length;
}
private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') {
this.selectedFolder = folder;
this.selectedEmail = null;
}
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = diff / (1000 * 60 * 60);
if (hours < 24) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else if (hours < 168) { // 7 days
return date.toLocaleDateString([], { weekday: 'short' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
}
private async loadEmails() {
// TODO: Load real emails from server
// For now, generate mock data
this.generateMockEmails();
}
private async refreshEmails() {
this.isLoading = true;
await this.loadEmails();
this.isLoading = false;
}
private async sendEmail(formData: any) {
try {
// TODO: Implement actual email sending
console.log('Sending email:', formData);
// Add to sent folder
const newEmail: IEmail = {
id: `email-${Date.now()}`,
from: 'me@dcrouter.local',
to: formData.to,
cc: formData.cc || [],
bcc: formData.bcc || [],
subject: formData.subject,
body: formData.body,
date: Date.now(),
read: true,
folder: 'sent',
};
this.emails = [...this.emails, newEmail];
this.showCompose = false;
// TODO: Implement toast notification when DeesToast.show is available
console.log('Email sent successfully');
} catch (error: any) {
// TODO: Implement toast notification when DeesToast.show is available
console.error('Failed to send email', error);
}
}
private async markAsRead(emailId: string) {
const email = this.emails.find(e => e.id === emailId);
if (email) {
email.read = true;
this.emails = [...this.emails];
}
}
private async markAllAsRead() {
this.emails = this.emails.map(e =>
e.folder === this.selectedFolder ? { ...e, read: true } : e
);
}
private async deleteEmail(emailId: string) {
const email = this.emails.find(e => e.id === emailId);
if (email) {
if (email.folder === 'trash') {
// Permanently delete
this.emails = this.emails.filter(e => e.id !== emailId);
} else {
// Move to trash
email.folder = 'trash';
this.emails = [...this.emails];
}
if (this.selectedEmail?.id === emailId) {
this.selectedEmail = null;
}
}
}
private async replyToEmail(email: IEmail) {
// TODO: Open compose with reply context
this.showCompose = true;
}
private async replyAllToEmail(email: IEmail) {
// TODO: Open compose with reply all context
this.showCompose = true;
}
private async forwardEmail(email: IEmail) {
// TODO: Open compose with forward context
this.showCompose = true;
}
private generateMockEmails() {
const subjects = [
'Server Alert: High CPU Usage',
'Daily Report - Network Activity',
'Security Update Required',
'New User Registration',
'Backup Completed Successfully',
'DNS Query Spike Detected',
'SSL Certificate Renewal Notice',
'Monthly Usage Summary',
];
const senders = [
'monitoring@dcrouter.local',
'alerts@system.local',
'admin@company.com',
'noreply@service.com',
'support@vendor.com',
];
const bodies = [
'This is an automated alert regarding your server status.',
'Please review the attached report for detailed information.',
'Action required: Update your security settings.',
'Your daily summary is ready for review.',
'All systems are operating normally.',
];
this.emails = Array.from({ length: 50 }, (_, i) => ({
id: `email-${i}`,
from: senders[Math.floor(Math.random() * senders.length)],
to: ['admin@dcrouter.local'],
subject: subjects[Math.floor(Math.random() * subjects.length)],
body: bodies[Math.floor(Math.random() * bodies.length)],
date: Date.now() - (i * 3600000), // 1 hour apart
read: Math.random() > 0.3,
folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash',
attachments: Math.random() > 0.8 ? [{
filename: 'report.pdf',
size: 1024 * 1024,
contentType: 'application/pdf',
}] : undefined,
}));
}
}

View File

@ -0,0 +1,520 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
import { viewHostCss } from './shared/css.js';
import { type IStatsTile } from '@design.estate/dees-catalog';
declare global {
interface HTMLElementTagNameMap {
'ops-view-network': OpsViewNetwork;
}
}
interface INetworkRequest {
id: string;
timestamp: number;
method: string;
url: string;
hostname: string;
port: number;
protocol: 'http' | 'https' | 'tcp' | 'udp';
statusCode?: number;
duration: number;
bytesIn: number;
bytesOut: number;
remoteIp: string;
route?: string;
}
@customElement('ops-view-network')
export class OpsViewNetwork extends DeesElement {
@state()
private statsState = appstate.statsStatePart.getState();
@state()
private selectedTimeRange: '1m' | '5m' | '15m' | '1h' | '24h' = '5m';
@state()
private selectedProtocol: 'all' | 'http' | 'https' | 'smtp' | 'dns' = 'all';
@state()
private networkRequests: INetworkRequest[] = [];
@state()
private trafficData: Array<{ x: number; y: number }> = [];
@state()
private isLoading = false;
constructor() {
super();
this.subscribeToStateParts();
this.generateMockData(); // TODO: Replace with real data from metrics
}
private subscribeToStateParts() {
appstate.statsStatePart.state.subscribe((state) => {
this.statsState = state;
this.updateNetworkData();
});
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
:host {
display: block;
padding: 24px;
}
.networkContainer {
display: flex;
flex-direction: column;
gap: 24px;
}
.controlBar {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.controlGroup {
display: flex;
gap: 8px;
align-items: center;
}
.controlLabel {
font-size: 14px;
color: #666;
margin-right: 8px;
}
dees-statsgrid {
margin-bottom: 24px;
}
.chartSection {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 24px;
}
.tableSection {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
}
.protocolBadge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.protocolBadge.http {
background: #e3f2fd;
color: #1976d2;
}
.protocolBadge.https {
background: #e8f5e9;
color: #388e3c;
}
.protocolBadge.tcp {
background: #fff3e0;
color: #f57c00;
}
.protocolBadge.smtp {
background: #f3e5f5;
color: #7b1fa2;
}
.protocolBadge.dns {
background: #e0f2f1;
color: #00796b;
}
.statusBadge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.statusBadge.success {
background: #e8f5e9;
color: #388e3c;
}
.statusBadge.error {
background: #ffebee;
color: #d32f2f;
}
.statusBadge.warning {
background: #fff3e0;
color: #f57c00;
}
`,
];
public render() {
return html`
<ops-sectionheading>Network Activity</ops-sectionheading>
<div class="networkContainer">
<!-- Control Bar -->
<div class="controlBar">
<div class="controlGroup">
<span class="controlLabel">Time Range:</span>
<dees-button-group>
${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html`
<dees-button
@click=${() => this.selectedTimeRange = range}
.type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'}
>
${range}
</dees-button>
`)}
</dees-button-group>
</div>
<div class="controlGroup">
<span class="controlLabel">Protocol:</span>
<dees-input-dropdown
.options=${[
{ key: 'all', label: 'All Protocols' },
{ key: 'http', label: 'HTTP' },
{ key: 'https', label: 'HTTPS' },
{ key: 'smtp', label: 'SMTP' },
{ key: 'dns', label: 'DNS' },
]}
.selectedOption=${{ key: this.selectedProtocol, label: this.getProtocolLabel(this.selectedProtocol) }}
@selectedOption=${(e: CustomEvent) => this.selectedProtocol = e.detail.key}
></dees-input-dropdown>
</div>
<div style="margin-left: auto;">
<dees-button
@click=${() => this.refreshData()}
.disabled=${this.isLoading}
>
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
</dees-button>
</div>
</div>
<!-- Stats Grid -->
${this.renderNetworkStats()}
<!-- Traffic Chart -->
<div class="chartSection">
<dees-chart-area
.label=${'Network Traffic'}
.series=${[
{
name: 'Requests/min',
data: this.trafficData,
}
]}
></dees-chart-area>
</div>
<!-- Requests Table -->
<div class="tableSection">
<dees-table
.data=${this.getFilteredRequests()}
.displayFunction=${(req: INetworkRequest) => ({
Time: new Date(req.timestamp).toLocaleTimeString(),
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
Method: req.method,
'Host:Port': `${req.hostname}:${req.port}`,
Path: this.truncateUrl(req.url),
Status: this.renderStatus(req.statusCode),
Duration: `${req.duration}ms`,
'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
'Remote IP': req.remoteIp,
})}
.dataActions=${[
{
name: 'View Details',
iconName: 'magnifyingGlass',
type: ['inRow', 'doubleClick', 'contextmenu'],
actionFunc: async (actionData) => {
await this.showRequestDetails(actionData.item);
}
}
]}
heading1="Recent Network Activity"
heading2="Last ${this.selectedTimeRange} of network requests"
searchable
.pagination=${true}
.paginationSize=${50}
dataName="request"
></dees-table>
</div>
</div>
`;
}
private async showRequestDetails(request: INetworkRequest) {
const { DeesModal } = await import('@design.estate/dees-catalog');
await DeesModal.createAndShow({
heading: 'Request Details',
content: html`
<div style="padding: 20px;">
<dees-dataview-codebox
.heading=${'Request Information'}
progLang="json"
.codeToDisplay=${JSON.stringify({
id: request.id,
timestamp: new Date(request.timestamp).toISOString(),
protocol: request.protocol,
method: request.method,
url: request.url,
hostname: request.hostname,
port: request.port,
statusCode: request.statusCode,
duration: `${request.duration}ms`,
bytesIn: request.bytesIn,
bytesOut: request.bytesOut,
remoteIp: request.remoteIp,
route: request.route,
}, null, 2)}
></dees-dataview-codebox>
</div>
`,
menuOptions: [
{
name: 'Copy Request ID',
iconName: 'copy',
action: async () => {
await navigator.clipboard.writeText(request.id);
// TODO: Implement toast notification when DeesToast.show is available
console.log('Request ID copied to clipboard');
}
}
]
});
}
private getFilteredRequests(): INetworkRequest[] {
if (this.selectedProtocol === 'all') {
return this.networkRequests;
}
// Map protocol filter to actual protocol values
const protocolMap: Record<string, string[]> = {
'http': ['http'],
'https': ['https'],
'smtp': ['tcp'], // SMTP runs over TCP
'dns': ['udp'], // DNS typically runs over UDP
};
const allowedProtocols = protocolMap[this.selectedProtocol] || [this.selectedProtocol];
return this.networkRequests.filter(req => allowedProtocols.includes(req.protocol));
}
private renderStatus(statusCode?: number): TemplateResult {
if (!statusCode) {
return html`<span class="statusBadge warning">N/A</span>`;
}
const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' :
statusCode >= 400 ? 'error' : 'warning';
return html`<span class="statusBadge ${statusClass}">${statusCode}</span>`;
}
private truncateUrl(url: string, maxLength = 50): string {
if (url.length <= maxLength) return url;
return url.substring(0, maxLength - 3) + '...';
}
private getProtocolLabel(protocol: string): string {
const labels: Record<string, string> = {
'all': 'All Protocols',
'http': 'HTTP',
'https': 'HTTPS',
'smtp': 'SMTP',
'dns': 'DNS',
};
return labels[protocol] || protocol.toUpperCase();
}
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.toFixed(0);
}
private formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private calculateRequestsPerSecond(): number {
// TODO: Calculate from real data based on connection metrics
// For now, return a calculated value based on active connections
return Math.floor((this.statsState.serverStats?.activeConnections || 0) * 0.8);
}
private calculateThroughput(): { in: number; out: number } {
// TODO: Calculate from real connection data
// For now, return estimated values
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
return {
in: activeConnections * 1024 * 10, // 10KB per connection estimate
out: activeConnections * 1024 * 50, // 50KB per connection estimate
};
}
private renderNetworkStats(): TemplateResult {
const reqPerSec = this.calculateRequestsPerSecond();
const throughput = this.calculateThroughput();
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
// Generate trend data for requests per second
const trendData = Array.from({ length: 20 }, (_, i) =>
Math.max(0, reqPerSec + (Math.random() - 0.5) * 10)
);
const tiles: IStatsTile[] = [
{
id: 'connections',
title: 'Active Connections',
value: activeConnections,
type: 'number',
icon: 'plug',
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
actions: [
{
name: 'View Details',
iconName: 'magnifyingGlass',
action: async () => {
// TODO: Show connection details
},
},
],
},
{
id: 'requests',
title: 'Requests/sec',
value: reqPerSec,
type: 'trend',
icon: 'chartLine',
color: '#3b82f6',
trendData: trendData,
description: `${this.formatNumber(reqPerSec)} req/s`,
},
{
id: 'throughputIn',
title: 'Throughput In',
value: this.formatBytes(throughput.in),
unit: '/s',
type: 'number',
icon: 'download',
color: '#22c55e',
},
{
id: 'throughputOut',
title: 'Throughput Out',
value: this.formatBytes(throughput.out),
unit: '/s',
type: 'number',
icon: 'upload',
color: '#8b5cf6',
},
];
return html`
<dees-statsgrid
.tiles=${tiles}
.minTileWidth=${200}
.gridActions=${[
{
name: 'Export Data',
iconName: 'fileExport',
action: async () => {
// TODO: Export network data
// TODO: Implement toast notification when DeesToast.show is available
console.log('Export feature coming soon');
},
},
]}
></dees-statsgrid>
`;
}
private async refreshData() {
this.isLoading = true;
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await this.updateNetworkData();
this.isLoading = false;
}
private async updateNetworkData() {
// TODO: Fetch real network data from the server
// For now, using mock data
this.generateMockData();
}
private generateMockData() {
// Generate mock network requests
const now = Date.now();
const protocols: Array<'http' | 'https' | 'tcp' | 'udp'> = ['http', 'https', 'tcp', 'udp'];
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'];
const hosts = ['api.example.com', 'app.local', 'mail.server.com', 'dns.resolver.net'];
this.networkRequests = Array.from({ length: 100 }, (_, i) => ({
id: `req-${i}`,
timestamp: now - (i * 5000), // 5 seconds apart
method: methods[Math.floor(Math.random() * methods.length)],
url: `/api/v1/resource/${Math.floor(Math.random() * 100)}`,
hostname: hosts[Math.floor(Math.random() * hosts.length)],
port: Math.random() > 0.5 ? 443 : 80,
protocol: protocols[Math.floor(Math.random() * protocols.length)],
statusCode: Math.random() > 0.8 ? 404 : 200,
duration: Math.floor(Math.random() * 500),
bytesIn: Math.floor(Math.random() * 10000),
bytesOut: Math.floor(Math.random() * 50000),
remoteIp: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
route: 'main-route',
}));
// Generate traffic data for chart
this.trafficData = Array.from({ length: 60 }, (_, i) => ({
x: now - (i * 60000), // 1 minute intervals
y: Math.floor(Math.random() * 100) + 50,
})).reverse();
}
}

View File

@ -9,7 +9,9 @@ import {
state,
css,
cssManager,
type TemplateResult,
} from '@design.estate/dees-element';
import { type IStatsTile } from '@design.estate/dees-catalog';
@customElement('ops-view-overview')
export class OpsViewOverview extends DeesElement {
@ -38,39 +40,13 @@ export class OpsViewOverview extends DeesElement {
cssManager.defaultStyles,
shared.viewHostCss,
css`
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 16px;
margin-bottom: 40px;
}
.statCard {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 24px;
}
.statCard h3 {
margin: 0 0 16px 0;
font-size: 18px;
h2 {
margin: 32px 0 16px 0;
font-size: 24px;
font-weight: 600;
color: #333;
}
.statValue {
font-size: 32px;
font-weight: 700;
color: #2196F3;
margin-bottom: 8px;
}
.statLabel {
font-size: 14px;
color: #666;
}
.chartGrid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@ -92,6 +68,10 @@ export class OpsViewOverview extends DeesElement {
color: #c00;
margin: 16px 0;
}
dees-statsgrid {
margin-bottom: 32px;
}
`,
];
@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement {
Error loading statistics: ${this.statsState.error}
</div>
` : html`
<div class="statsGrid">
${this.statsState.serverStats ? html`
<div class="statCard">
<h3>Server Status</h3>
<div class="statValue">${this.statsState.serverStats.uptime ? 'Online' : 'Offline'}</div>
<div class="statLabel">Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}</div>
</div>
<div class="statCard">
<h3>Connections</h3>
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
<div class="statLabel">Active connections</div>
</div>
${this.renderServerStats()}
<div class="statCard">
<h3>Memory Usage</h3>
<div class="statValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
<div class="statLabel">of ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}</div>
</div>
${this.renderEmailStats()}
<div class="statCard">
<h3>CPU Usage</h3>
<div class="statValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%</div>
<div class="statLabel">Average load</div>
</div>
` : ''}
</div>
${this.statsState.emailStats ? html`
<h2>Email Statistics</h2>
<div class="statsGrid">
<div class="statCard">
<h3>Emails Sent</h3>
<div class="statValue">${this.statsState.emailStats.sent}</div>
<div class="statLabel">Total sent</div>
</div>
<div class="statCard">
<h3>Emails Received</h3>
<div class="statValue">${this.statsState.emailStats.received}</div>
<div class="statLabel">Total received</div>
</div>
<div class="statCard">
<h3>Failed Deliveries</h3>
<div class="statValue">${this.statsState.emailStats.failed}</div>
<div class="statLabel">Delivery failures</div>
</div>
<div class="statCard">
<h3>Queued</h3>
<div class="statValue">${this.statsState.emailStats.queued}</div>
<div class="statLabel">In queue</div>
</div>
</div>
` : ''}
${this.statsState.dnsStats ? html`
<h2>DNS Statistics</h2>
<div class="statsGrid">
<div class="statCard">
<h3>DNS Queries</h3>
<div class="statValue">${this.statsState.dnsStats.totalQueries}</div>
<div class="statLabel">Total queries handled</div>
</div>
<div class="statCard">
<h3>Cache Hit Rate</h3>
<div class="statValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%</div>
<div class="statLabel">Cache efficiency</div>
</div>
</div>
` : ''}
${this.renderDnsStats()}
<div class="chartGrid">
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
@ -222,4 +134,171 @@ export class OpsViewOverview extends DeesElement {
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private renderServerStats(): TemplateResult {
if (!this.statsState.serverStats) return html``;
const cpuUsage = Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2);
const memoryUsage = Math.round((this.statsState.serverStats.memoryUsage.heapUsed / this.statsState.serverStats.memoryUsage.heapTotal) * 100);
const tiles: IStatsTile[] = [
{
id: 'status',
title: 'Server Status',
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
type: 'text',
icon: 'server',
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
},
{
id: 'connections',
title: 'Active Connections',
value: this.statsState.serverStats.activeConnections,
type: 'number',
icon: 'networkWired',
color: '#3b82f6',
description: `Total: ${this.statsState.serverStats.totalConnections}`,
},
{
id: 'cpu',
title: 'CPU Usage',
value: cpuUsage,
type: 'gauge',
icon: 'microchip',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: '#22c55e' },
{ value: 60, color: '#f59e0b' },
{ value: 80, color: '#ef4444' },
],
},
},
{
id: 'memory',
title: 'Memory Usage',
value: memoryUsage,
type: 'percentage',
icon: 'memory',
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
description: `${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}`,
},
];
return html`
<dees-statsgrid
.tiles=${tiles}
.gridActions=${[
{
name: 'Refresh',
iconName: 'arrowsRotate',
action: async () => {
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
},
},
]}
></dees-statsgrid>
`;
}
private renderEmailStats(): TemplateResult {
if (!this.statsState.emailStats) return html``;
const deliveryRate = this.statsState.emailStats.deliveryRate || 0;
const bounceRate = this.statsState.emailStats.bounceRate || 0;
const tiles: IStatsTile[] = [
{
id: 'sent',
title: 'Emails Sent',
value: this.statsState.emailStats.sent,
type: 'number',
icon: 'paperPlane',
color: '#22c55e',
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
},
{
id: 'received',
title: 'Emails Received',
value: this.statsState.emailStats.received,
type: 'number',
icon: 'envelope',
color: '#3b82f6',
},
{
id: 'queued',
title: 'Queued',
value: this.statsState.emailStats.queued,
type: 'number',
icon: 'clock',
color: '#f59e0b',
description: 'Pending delivery',
},
{
id: 'failed',
title: 'Failed',
value: this.statsState.emailStats.failed,
type: 'number',
icon: 'triangleExclamation',
color: '#ef4444',
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
},
];
return html`
<h2>Email Statistics</h2>
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
private renderDnsStats(): TemplateResult {
if (!this.statsState.dnsStats) return html``;
const cacheHitRate = Math.round(this.statsState.dnsStats.cacheHitRate * 100);
const tiles: IStatsTile[] = [
{
id: 'queries',
title: 'DNS Queries',
value: this.statsState.dnsStats.totalQueries,
type: 'number',
icon: 'globe',
color: '#3b82f6',
description: 'Total queries handled',
},
{
id: 'cacheRate',
title: 'Cache Hit Rate',
value: cacheHitRate,
type: 'percentage',
icon: 'database',
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
},
{
id: 'domains',
title: 'Active Domains',
value: this.statsState.dnsStats.activeDomains,
type: 'number',
icon: 'sitemap',
color: '#8b5cf6',
},
{
id: 'responseTime',
title: 'Avg Response Time',
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
unit: 'ms',
type: 'number',
icon: 'clockRotateLeft',
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
},
];
return html`
<h2>DNS Statistics</h2>
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
`;
}
}

View File

@ -1,302 +0,0 @@
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);
const secs = Math.floor(seconds % 60);
if (days > 0) {
return `${days}d ${hours}h ${minutes}m ${secs}s`;
} else if (hours > 0) {
return `${hours}h ${minutes}m ${secs}s`;
} else if (minutes > 0) {
return `${minutes}m ${secs}s`;
} else {
return `${secs}s`;
}
}
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();
}
}