597 lines
18 KiB
TypeScript
597 lines
18 KiB
TypeScript
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 networkRequests: INetworkRequest[] = [];
|
|
|
|
@state()
|
|
private trafficDataIn: Array<{ x: string | number; y: number }> = [];
|
|
|
|
@state()
|
|
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
|
|
|
|
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;
|
|
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
|
|
|
// Removed byte tracking - now using real-time data from SmartProxy
|
|
|
|
constructor() {
|
|
super();
|
|
this.subscribeToStateParts();
|
|
this.initializeTrafficData();
|
|
this.updateNetworkData();
|
|
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();
|
|
});
|
|
}
|
|
|
|
private initializeTrafficData() {
|
|
const now = Date.now();
|
|
// Fixed 5 minute time range
|
|
const range = 5 * 60 * 1000; // 5 minutes
|
|
const bucketSize = range / 60; // 60 data points
|
|
|
|
// Initialize with empty data points for both in and out
|
|
const emptyData = Array.from({ length: 60 }, (_, i) => {
|
|
const time = now - ((59 - i) * bucketSize);
|
|
return {
|
|
x: new Date(time).toISOString(),
|
|
y: 0,
|
|
};
|
|
});
|
|
|
|
this.trafficDataIn = [...emptyData];
|
|
this.trafficDataOut = emptyData.map(point => ({ ...point }));
|
|
|
|
this.lastTrafficUpdateTime = now;
|
|
}
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
viewHostCss,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
padding: 24px;
|
|
}
|
|
|
|
.networkContainer {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
}
|
|
|
|
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">
|
|
<!-- Stats Grid -->
|
|
${this.renderNetworkStats()}
|
|
|
|
<!-- Traffic Chart -->
|
|
<dees-chart-area
|
|
.label=${'Network Traffic'}
|
|
.series=${[
|
|
{
|
|
name: 'Inbound',
|
|
data: this.trafficDataIn,
|
|
color: '#22c55e', // Green for download
|
|
},
|
|
{
|
|
name: 'Outbound',
|
|
data: this.trafficDataOut,
|
|
color: '#8b5cf6', // Purple for upload
|
|
}
|
|
]}
|
|
.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.networkRequests}
|
|
.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="Recent 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 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 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 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);
|
|
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;
|
|
|
|
// Use request count history for the requests/sec trend
|
|
const trendData = [...this.requestsPerSecHistory];
|
|
|
|
// If we don't have enough data, pad with zeros
|
|
while (trendData.length < 20) {
|
|
trendData.unshift(0);
|
|
}
|
|
|
|
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,
|
|
description: `Average over last minute`,
|
|
},
|
|
{
|
|
id: 'throughputIn',
|
|
title: 'Throughput In',
|
|
value: this.formatBitsPerSecond(throughput.in),
|
|
unit: '',
|
|
type: 'number',
|
|
icon: 'download',
|
|
color: '#22c55e',
|
|
},
|
|
{
|
|
id: 'throughputOut',
|
|
title: 'Throughput Out',
|
|
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 renderTopIPs(): TemplateResult {
|
|
if (this.networkState.topIPs.length === 0) {
|
|
return html``;
|
|
}
|
|
|
|
// Calculate total connections across all top IPs
|
|
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
|
|
|
|
return html`
|
|
<dees-table
|
|
.data=${this.networkState.topIPs}
|
|
.displayFunction=${(ipData: { ip: string; count: number }) => ({
|
|
'IP Address': ipData.ip,
|
|
'Connections': ipData.count,
|
|
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
|
})}
|
|
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();
|
|
// Fixed 5 minute time range
|
|
const range = 5 * 60 * 1000; // 5 minutes
|
|
const bucketSize = range / 60; // 60 data points // 60 data points
|
|
|
|
// 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,
|
|
currentDataPoints: this.trafficDataIn.length
|
|
});
|
|
|
|
if (!shouldAddNewPoint && this.trafficDataIn.length > 0) {
|
|
// Not enough time has passed, don't update
|
|
return;
|
|
}
|
|
|
|
// Use real-time throughput data from SmartProxy (same as throughput tiles)
|
|
const throughput = this.calculateThroughput();
|
|
|
|
// Convert to Mbps (bytes * 8 / 1,000,000)
|
|
const throughputInMbps = (throughput.in * 8) / 1000000;
|
|
const throughputOutMbps = (throughput.out * 8) / 1000000;
|
|
|
|
console.log('Throughput calculation:', {
|
|
bytesInPerSecond: throughput.in,
|
|
bytesOutPerSecond: throughput.out,
|
|
throughputInMbps,
|
|
throughputOutMbps,
|
|
throughputTileValue: `${this.formatBitsPerSecond(throughput.in)} IN, ${this.formatBitsPerSecond(throughput.out)} OUT`
|
|
});
|
|
|
|
if (this.trafficDataIn.length === 0) {
|
|
// Initialize if empty
|
|
this.initializeTrafficData();
|
|
} else {
|
|
// 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
|
|
};
|
|
|
|
// Create new arrays with existing data plus new points
|
|
const newTrafficDataIn = [...this.trafficDataIn, newDataPointIn];
|
|
const newTrafficDataOut = [...this.trafficDataOut, newDataPointOut];
|
|
|
|
// Keep only the last 60 points
|
|
if (newTrafficDataIn.length > 60) {
|
|
newTrafficDataIn.shift(); // Remove oldest point
|
|
newTrafficDataOut.shift();
|
|
}
|
|
|
|
this.trafficDataIn = newTrafficDataIn;
|
|
this.trafficDataOut = newTrafficDataOut;
|
|
this.lastTrafficUpdateTime = now;
|
|
|
|
console.log('Added new traffic data points:', {
|
|
timestamp: timestamp,
|
|
throughputInMbps: newDataPointIn.y,
|
|
throughputOutMbps: newDataPointOut.y,
|
|
totalPoints: this.trafficDataIn.length
|
|
});
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
} |