Files
dcrouter/ts_web/elements/ops-view-network.ts

716 lines
21 KiB
TypeScript
Raw Normal View History

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 networkState = appstate.networkStatePart.getState();
@state()
private selectedTimeRange: '1m' | '5m' | '15m' | '1h' | '24h' = '5m';
@state()
private selectedProtocol: 'all' | 'http' | 'https' | 'smtp' | 'dns' = 'all';
@state()
private networkRequests: INetworkRequest[] = [];
@state()
2025-06-23 00:19:47 +00:00
private trafficDataIn: Array<{ x: string | number; y: number }> = [];
@state()
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
@state()
private isLoading = false;
2025-06-22 23:40:02 +00:00
private lastTrafficUpdateTime = 0;
private trafficUpdateInterval = 1000; // Update every 1 second
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
private trafficUpdateTimer: any = null;
2025-06-23 00:19:47 +00:00
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
2025-06-22 23:40:02 +00:00
2025-06-23 00:19:47 +00:00
// Removed byte tracking - now using real-time data from SmartProxy
constructor() {
super();
this.subscribeToStateParts();
2025-06-22 23:40:02 +00:00
this.initializeTrafficData();
this.updateNetworkData();
2025-06-22 23:40:02 +00:00
this.startTrafficUpdateTimer();
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.stopTrafficUpdateTimer();
}
private subscribeToStateParts() {
appstate.statsStatePart.state.subscribe((state) => {
this.statsState = state;
this.updateNetworkData();
});
appstate.networkStatePart.state.subscribe((state) => {
this.networkState = state;
this.updateNetworkData();
});
}
2025-06-22 23:40:02 +00:00
private initializeTrafficData() {
const now = Date.now();
const timeRanges = {
'1m': 60 * 1000,
'5m': 5 * 60 * 1000,
'15m': 15 * 60 * 1000,
'1h': 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000,
};
const range = timeRanges[this.selectedTimeRange];
const bucketSize = range / 60;
2025-06-23 00:19:47 +00:00
// Initialize with empty data points for both in and out
const emptyData = Array.from({ length: 60 }, (_, i) => {
2025-06-22 23:40:02 +00:00
const time = now - ((59 - i) * bucketSize);
return {
x: new Date(time).toISOString(),
y: 0,
};
});
2025-06-23 00:19:47 +00:00
this.trafficDataIn = [...emptyData];
this.trafficDataOut = emptyData.map(point => ({ ...point }));
2025-06-22 23:40:02 +00:00
this.lastTrafficUpdateTime = now;
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
:host {
display: block;
padding: 24px;
}
.networkContainer {
display: flex;
flex-direction: column;
gap: 24px;
}
.controlBar {
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 24px;
}
.controlGroup {
display: flex;
gap: 8px;
align-items: center;
}
.controlLabel {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
margin-right: 8px;
}
dees-statsgrid {
margin-bottom: 24px;
}
dees-chart-area {
margin-bottom: 24px;
}
.protocolBadge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.protocolBadge.http {
background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')};
color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')};
}
.protocolBadge.https {
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
}
.protocolBadge.tcp {
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
}
.protocolBadge.smtp {
background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')};
color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')};
}
.protocolBadge.dns {
background: ${cssManager.bdTheme('#e0f2f1', '#1a3a3a')};
color: ${cssManager.bdTheme('#00796b', '#4db6ac')};
}
.statusBadge {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.statusBadge.success {
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
}
.statusBadge.error {
background: ${cssManager.bdTheme('#ffebee', '#3a1a1a')};
color: ${cssManager.bdTheme('#d32f2f', '#ff6666')};
}
.statusBadge.warning {
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
}
`,
];
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
2025-06-22 23:40:02 +00:00
@click=${() => this.handleTimeRangeChange(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 -->
<dees-chart-area
.label=${'Network Traffic'}
.series=${[
{
2025-06-23 00:19:47 +00:00
name: 'Inbound',
data: this.trafficDataIn,
color: '#22c55e', // Green for download
},
{
name: 'Outbound',
data: this.trafficDataOut,
color: '#8b5cf6', // Purple for upload
}
]}
2025-06-23 00:19:47 +00:00
.stacked=${false}
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
.tooltipFormatter=${(point: any) => {
const mbps = point.y || 0;
const seriesName = point.series?.name || 'Throughput';
const timestamp = new Date(point.x).toLocaleTimeString();
return `
<div style="padding: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">${timestamp}</div>
<div>${seriesName}: ${mbps.toFixed(2)} Mbit/s</div>
</div>
`;
}}
></dees-chart-area>
<!-- Top IPs Section -->
${this.renderTopIPs()}
<!-- Requests Table -->
<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>
`;
}
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);
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]}`;
}
2025-06-23 00:19:47 +00:00
private formatBitsPerSecond(bytesPerSecond: number): string {
const bitsPerSecond = bytesPerSecond * 8; // Convert bytes to bits
const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s'];
let size = bitsPerSecond;
let unitIndex = 0;
while (size >= 1000 && unitIndex < units.length - 1) {
size /= 1000; // Use 1000 for bits (not 1024)
unitIndex++;
}
return `${size.toFixed(1)} ${units[unitIndex]}`;
}
private calculateRequestsPerSecond(): number {
// Calculate from actual request data in the last minute
const oneMinuteAgo = Date.now() - 60000;
const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo);
2025-06-23 00:19:47 +00:00
const reqPerSec = Math.round(recentRequests.length / 60);
// Track history for trend (keep last 20 values)
this.requestsPerSecHistory.push(reqPerSec);
if (this.requestsPerSecHistory.length > 20) {
this.requestsPerSecHistory.shift();
}
return reqPerSec;
}
private calculateThroughput(): { in: number; out: number } {
// Use real throughput data from network state
return {
in: this.networkState.throughputRate.bytesInPerSecond,
out: this.networkState.throughputRate.bytesOutPerSecond,
};
}
private renderNetworkStats(): TemplateResult {
const reqPerSec = this.calculateRequestsPerSecond();
const throughput = this.calculateThroughput();
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
2025-06-23 00:19:47 +00:00
// Use request count history for the requests/sec trend
const trendData = [...this.requestsPerSecHistory];
2025-06-22 23:40:02 +00:00
2025-06-23 00:19:47 +00:00
// If we don't have enough data, pad with zeros
2025-06-22 23:40:02 +00:00
while (trendData.length < 20) {
2025-06-23 00:19:47 +00:00
trendData.unshift(0);
2025-06-22 23:40:02 +00:00
}
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 () => {
},
},
],
},
{
id: 'requests',
title: 'Requests/sec',
value: reqPerSec,
type: 'trend',
icon: 'chartLine',
color: '#3b82f6',
trendData: trendData,
2025-06-23 00:19:47 +00:00
description: `Average over last minute`,
},
{
id: 'throughputIn',
title: 'Throughput In',
2025-06-23 00:19:47 +00:00
value: this.formatBitsPerSecond(throughput.in),
unit: '',
type: 'number',
icon: 'download',
color: '#22c55e',
},
{
id: 'throughputOut',
title: 'Throughput Out',
2025-06-23 00:19:47 +00:00
value: this.formatBitsPerSecond(throughput.out),
unit: '',
type: 'number',
icon: 'upload',
color: '#8b5cf6',
},
];
return html`
<dees-statsgrid
.tiles=${tiles}
.minTileWidth=${200}
.gridActions=${[
{
name: 'Export Data',
iconName: 'fileExport',
action: async () => {
console.log('Export feature coming soon');
},
},
]}
></dees-statsgrid>
`;
}
private async refreshData() {
this.isLoading = true;
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
await this.updateNetworkData();
this.isLoading = false;
}
private renderTopIPs(): TemplateResult {
if (this.networkState.topIPs.length === 0) {
return html``;
}
return html`
<dees-table
.data=${this.networkState.topIPs}
.displayFunction=${(ipData: { ip: string; count: number }) => ({
'IP Address': ipData.ip,
'Connections': ipData.count,
'Percentage': ((ipData.count / this.networkState.connections.length) * 100).toFixed(1) + '%',
})}
heading1="Top Connected IPs"
heading2="IPs with most active connections"
.pagination=${false}
dataName="ip"
></dees-table>
`;
}
private async updateNetworkData() {
// Convert connection data to network requests format
if (this.networkState.connections.length > 0) {
this.networkRequests = this.networkState.connections.map((conn, index) => ({
id: conn.id,
timestamp: conn.startTime,
method: 'GET', // Default method for proxy connections
url: '/',
hostname: conn.remoteAddress,
port: conn.protocol === 'https' ? 443 : 80,
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
statusCode: conn.state === 'connected' ? 200 : undefined,
duration: Date.now() - conn.startTime,
bytesIn: conn.bytesReceived,
bytesOut: conn.bytesSent,
remoteIp: conn.remoteAddress,
route: 'proxy',
}));
} else {
this.networkRequests = [];
}
// Generate traffic data based on request history
this.updateTrafficData();
}
private updateTrafficData() {
const now = Date.now();
const timeRanges = {
'1m': 60 * 1000,
'5m': 5 * 60 * 1000,
'15m': 15 * 60 * 1000,
'1h': 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000,
};
const range = timeRanges[this.selectedTimeRange];
const bucketSize = range / 60; // 60 data points
2025-06-22 23:40:02 +00:00
// Check if enough time has passed to add a new data point
const timeSinceLastUpdate = now - this.lastTrafficUpdateTime;
const shouldAddNewPoint = timeSinceLastUpdate >= this.trafficUpdateInterval;
console.log('UpdateTrafficData called:', {
networkRequestsCount: this.networkRequests.length,
timeSinceLastUpdate,
shouldAddNewPoint,
2025-06-23 00:19:47 +00:00
currentDataPoints: this.trafficDataIn.length
2025-06-22 23:40:02 +00:00
});
2025-06-23 00:19:47 +00:00
if (!shouldAddNewPoint && this.trafficDataIn.length > 0) {
2025-06-22 23:40:02 +00:00
// Not enough time has passed, don't update
return;
}
2025-06-23 00:19:47 +00:00
// Use real-time throughput data from SmartProxy (same as throughput tiles)
const throughput = this.calculateThroughput();
2025-06-22 23:40:02 +00:00
2025-06-23 00:19:47 +00:00
// Convert to Mbps (bytes * 8 / 1,000,000)
const throughputInMbps = (throughput.in * 8) / 1000000;
const throughputOutMbps = (throughput.out * 8) / 1000000;
2025-06-23 00:19:47 +00:00
console.log('Throughput calculation:', {
bytesInPerSecond: throughput.in,
bytesOutPerSecond: throughput.out,
throughputInMbps,
throughputOutMbps,
throughputTileValue: `${this.formatBitsPerSecond(throughput.in)} IN, ${this.formatBitsPerSecond(throughput.out)} OUT`
});
2025-06-23 00:19:47 +00:00
if (this.trafficDataIn.length === 0) {
2025-06-22 23:40:02 +00:00
// Initialize if empty
this.initializeTrafficData();
} else {
2025-06-23 00:19:47 +00:00
// Add new data points for both in and out
const timestamp = new Date(now).toISOString();
const newDataPointIn = {
x: timestamp,
y: Math.round(throughputInMbps * 10) / 10 // Round to 1 decimal place
};
const newDataPointOut = {
x: timestamp,
y: Math.round(throughputOutMbps * 10) / 10 // Round to 1 decimal place
};
2025-06-22 23:40:02 +00:00
2025-06-23 00:19:47 +00:00
// Create new arrays with existing data plus new points
const newTrafficDataIn = [...this.trafficDataIn, newDataPointIn];
const newTrafficDataOut = [...this.trafficDataOut, newDataPointOut];
2025-06-22 23:40:02 +00:00
// Keep only the last 60 points
2025-06-23 00:19:47 +00:00
if (newTrafficDataIn.length > 60) {
newTrafficDataIn.shift(); // Remove oldest point
newTrafficDataOut.shift();
2025-06-22 23:40:02 +00:00
}
2025-06-23 00:19:47 +00:00
this.trafficDataIn = newTrafficDataIn;
this.trafficDataOut = newTrafficDataOut;
2025-06-22 23:40:02 +00:00
this.lastTrafficUpdateTime = now;
2025-06-23 00:19:47 +00:00
console.log('Added new traffic data points:', {
timestamp: timestamp,
throughputInMbps: newDataPointIn.y,
throughputOutMbps: newDataPointOut.y,
totalPoints: this.trafficDataIn.length
2025-06-22 23:40:02 +00:00
});
}
}
private startTrafficUpdateTimer() {
this.stopTrafficUpdateTimer(); // Clear any existing timer
this.trafficUpdateTimer = setInterval(() => {
this.updateTrafficData();
}, 1000); // Check every second, but only update when interval has passed
}
private stopTrafficUpdateTimer() {
if (this.trafficUpdateTimer) {
clearInterval(this.trafficUpdateTimer);
this.trafficUpdateTimer = null;
}
}
private handleTimeRangeChange(range: '1m' | '5m' | '15m' | '1h' | '24h') {
this.selectedTimeRange = range;
// Reinitialize traffic data for new time range
this.initializeTrafficData();
this.updateNetworkData();
}
}