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` Network Activity
Time Range: ${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html` this.selectedTimeRange = range} .type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'} > ${range} `)}
Protocol: this.selectedProtocol = e.detail.key} >
this.refreshData()} .disabled=${this.isLoading} > ${this.isLoading ? html`` : 'Refresh'}
${this.renderNetworkStats()}
({ Time: new Date(req.timestamp).toLocaleTimeString(), Protocol: html`${req.protocol.toUpperCase()}`, 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" >
`; } private async showRequestDetails(request: INetworkRequest) { const { DeesModal } = await import('@design.estate/dees-catalog'); await DeesModal.createAndShow({ heading: 'Request Details', content: html`
`, 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 = { '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`N/A`; } const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' : statusCode >= 400 ? 'error' : 'warning'; return html`${statusCode}`; } 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 = { '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` { // TODO: Export network data // TODO: Implement toast notification when DeesToast.show is available console.log('Export feature coming soon'); }, }, ]} > `; } 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(); } }