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:
520
ts_web/elements/ops-view-network.ts
Normal file
520
ts_web/elements/ops-view-network.ts
Normal 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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user