const Config = { API_BASE_URL: '/api/v1', RULE_METRICS_PATH: '/rule_metrics/', BYTE_TO_MB: 1024 * 1024, CHART_COLORS: { connectionCount: 'rgba(255, 99, 132, 1)', handshakeDuration: 'rgba(54, 162, 235, 1)', pingLatency: 'rgba(255, 206, 86, 1)', networkTransmitBytes: 'rgba(75, 192, 192, 1)', }, TIME_WINDOW: 30, // minutes AUTO_REFRESH_INTERVAL: 5000, // milliseconds }; class ApiService { static async fetchData(path, params = {}) { const url = new URL(Config.API_BASE_URL + path, window.location.origin); Object.entries(params).forEach(([key, value]) => url.searchParams.append(key, value)); try { const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error:', error); return null; } } static async fetchRuleMetrics(startTs, endTs, label = '', remote = '') { const params = { start_ts: startTs, end_ts: endTs }; if (label) params.label = label; if (remote) params.remote = remote; return await this.fetchData(Config.RULE_METRICS_PATH, params); } static async fetchConfig() { return await this.fetchData('/config/'); } static async fetchLabelsAndRemotes() { const config = await this.fetchConfig(); if (!config || !config.relay_configs) { return { labels: [], remotes: [] }; } const labels = new Set(); const remotes = new Set(); config.relay_configs.forEach((relayConfig) => { if (relayConfig.label) labels.add(relayConfig.label); if (relayConfig.remotes) { relayConfig.remotes.forEach((remote) => remotes.add(remote)); } }); return { labels: Array.from(labels), remotes: Array.from(remotes), }; } } class ChartManager { constructor() { this.charts = {}; } initializeCharts() { this.charts = { connectionCount: this.initChart('connectionCountChart', 'line', 'Connection Count', 'Count'), handshakeDuration: this.initChart('handshakeDurationChart', 'line', 'Handshake Duration', 'ms'), pingLatency: this.initChart('pingLatencyChart', 'line', 'Ping Latency', 'ms'), networkTransmitBytes: this.initStackedAreaChart('networkTransmitBytesChart', 'Network Transmit', 'MB'), }; } initChart(canvasId, type, title, unit) { const ctx = $(`#${canvasId}`)[0].getContext('2d'); const color = Config.CHART_COLORS[canvasId.replace('Chart', '')]; const metricType = canvasId.replace('Chart', ''); return new Chart(ctx, { type: type, data: { labels: [], datasets: [ { label: title, borderColor: color, backgroundColor: color.replace('1)', '0.2)'), borderWidth: 2, data: [], pointRadius: 0, // Hide individual points tension: 0.1, // Add slight curve to lines }, ], }, options: this.getChartOptions(title, unit, metricType), }); } initStackedAreaChart(canvasId, title, unit) { const ctx = $(`#${canvasId}`)[0].getContext('2d'); return new Chart(ctx, { type: 'line', data: { labels: [], datasets: [], }, options: this.getStackedAreaChartOptions(title, unit), }); } getChartOptions(title, unit, metricType) { const baseOptions = { responsive: true, plugins: { title: { display: true, text: title, font: { size: 16, weight: 'bold' }, }, tooltip: { callbacks: { label: (context) => `${context.dataset.label}: ${context.parsed.y.toFixed(2)} ${unit}`, }, }, }, scales: { x: { type: 'time', time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, title: { display: true, text: 'Time' }, }, y: { beginAtZero: true, title: { display: true, text: unit }, }, }, }; // We'll update suggestedMin and suggestedMax dynamically in updateCharts method switch (metricType) { case 'connectionCount': baseOptions.scales.y.ticks = { stepSize: 1 }; baseOptions.scales.y.title.text = 'Number of Connections'; break; case 'handshakeDuration': baseOptions.scales.y.title.text = 'Duration (ms)'; break; case 'pingLatency': baseOptions.scales.y.title.text = 'Latency (ms)'; break; case 'networkTransmitBytes': baseOptions.scales.y = { beginAtZero: true, title: { display: true, text: 'Data Transmitted (MB)' }, ticks: { callback: (value) => value.toFixed(2) + ' MB', }, }; baseOptions.plugins.tooltip = { callbacks: { label: (context) => `${context.dataset.label}: ${context.parsed.y.toFixed(2)} MB`, }, }; break; } return baseOptions; } getStackedAreaChartOptions(title, unit) { return { responsive: true, plugins: { title: { display: true, text: title, font: { size: 16, weight: 'bold' }, }, tooltip: { mode: 'index', intersect: false, callbacks: { label: (context) => `${context.dataset.label}: ${(context.parsed.y / Config.BYTE_TO_MB).toFixed(2)} MB`, }, }, }, scales: { x: { type: 'time', time: { unit: 'minute', displayFormats: { minute: 'HH:mm' } }, title: { display: true, text: 'Time' }, }, y: { stacked: true, beginAtZero: true, title: { display: true, text: 'Data Transmitted (MB)' }, ticks: { callback: (value) => (value / Config.BYTE_TO_MB).toFixed(2) + ' MB', }, }, }, interaction: { mode: 'nearest', axis: 'x', intersect: false, }, }; } adjustColor(color, amount) { return color.replace( /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/, (match, r, g, b, a) => `rgba(${Math.min(255, Math.max(0, parseInt(r) + amount))}, ${Math.min(255, Math.max(0, parseInt(g) + amount))}, ${Math.min( 255, Math.max(0, parseInt(b) + amount) )}, ${a || 1})` ); } fillMissingDataPoints(data, startTime, endTime) { const filledData = []; let currentTime = new Date(startTime); const endTimeDate = new Date(endTime); while (currentTime <= endTimeDate) { const existingPoint = data.find((point) => Math.abs(point.x.getTime() - currentTime.getTime()) < 60000); if (existingPoint) { filledData.push(existingPoint); } else { filledData.push({ x: new Date(currentTime), y: null }); } currentTime.setMinutes(currentTime.getMinutes() + 1); } return filledData; } updateCharts(metrics, startTime, endTime) { if (!metrics) { Object.values(this.charts).forEach((chart) => { chart.data.datasets = []; chart.update(); }); return; } metrics.sort((a, b) => a.timestamp - b.timestamp); const groupedMetrics = this.groupMetricsByLabelRemote(metrics); // Calculate min and max values for each metric type const ranges = this.calculateMetricRanges(groupedMetrics); Object.entries(this.charts).forEach(([key, chart]) => { if (key === 'networkTransmitBytes') { chart.data.datasets = []; groupedMetrics.forEach((group, groupIndex) => { const tcpData = []; const udpData = []; group.metrics.forEach((m) => { const timestamp = new Date(m.timestamp * 1000); tcpData.push({ x: timestamp, y: m.tcp_network_transmit_bytes }); udpData.push({ x: timestamp, y: m.udp_network_transmit_bytes }); }); const baseColor = this.getColor(groupIndex); chart.data.datasets.push( { label: `${group.label} - ${group.remote} (TCP)`, borderColor: baseColor, backgroundColor: baseColor.replace('1)', '0.5)'), borderWidth: 1, data: this.fillMissingDataPoints(tcpData, startTime, endTime), fill: true, }, { label: `${group.label} - ${group.remote} (UDP)`, borderColor: this.adjustColor(baseColor, -40), backgroundColor: this.adjustColor(baseColor, -40).replace('1)', '0.5)'), borderWidth: 1, data: this.fillMissingDataPoints(udpData, startTime, endTime), fill: true, } ); }); } else { chart.data.datasets = groupedMetrics.map((group, index) => { const data = group.metrics.map((m) => ({ x: new Date(m.timestamp * 1000), y: this.getMetricValue(key, m), })); const filledData = this.fillMissingDataPoints(data, startTime, endTime); return { label: `${group.label} - ${group.remote}`, borderColor: this.getColor(index), backgroundColor: this.getColor(index, 0.2), borderWidth: 2, data: filledData, }; }); } // Update chart options with calculated ranges chart.options.scales.y.suggestedMin = ranges[key].min; chart.options.scales.y.suggestedMax = ranges[key].max; chart.update(); }); } calculateMetricRanges(groupedMetrics) { const ranges = { connectionCount: { min: Infinity, max: -Infinity }, handshakeDuration: { min: Infinity, max: -Infinity }, pingLatency: { min: Infinity, max: -Infinity }, networkTransmitBytes: { min: Infinity, max: -Infinity }, }; groupedMetrics.forEach((group) => { group.metrics.forEach((metric) => { // Connection Count const connectionCount = metric.tcp_connection_count + metric.udp_connection_count; ranges.connectionCount.min = Math.min(ranges.connectionCount.min, connectionCount); ranges.connectionCount.max = Math.max(ranges.connectionCount.max, connectionCount); // Handshake Duration const handshakeDuration = Math.max(metric.tcp_handshake_duration, metric.udp_handshake_duration); ranges.handshakeDuration.min = Math.min(ranges.handshakeDuration.min, handshakeDuration); ranges.handshakeDuration.max = Math.max(ranges.handshakeDuration.max, handshakeDuration); // Ping Latency ranges.pingLatency.min = Math.min(ranges.pingLatency.min, metric.ping_latency); ranges.pingLatency.max = Math.max(ranges.pingLatency.max, metric.ping_latency); // Network Transmit Bytes const networkTransmitBytes = (metric.tcp_network_transmit_bytes + metric.udp_network_transmit_bytes) / Config.BYTE_TO_MB; ranges.networkTransmitBytes.min = Math.min(ranges.networkTransmitBytes.min, networkTransmitBytes); ranges.networkTransmitBytes.max = Math.max(ranges.networkTransmitBytes.max, networkTransmitBytes); }); }); // Add some padding to the ranges Object.keys(ranges).forEach((key) => { const range = ranges[key].max - ranges[key].min; ranges[key].min = Math.max(0, ranges[key].min - range * 0.1); ranges[key].max += range * 0.1; }); return ranges; } groupMetricsByLabelRemote(metrics) { const groups = {}; metrics.forEach((metric) => { const key = `${metric.label}-${metric.remote}`; if (!groups[key]) { groups[key] = { label: metric.label, remote: metric.remote, metrics: [] }; } groups[key].metrics.push(metric); }); return Object.values(groups); } getMetricValue(metricType, metric) { switch (metricType) { case 'connectionCount': return metric.tcp_connection_count + metric.udp_connection_count; case 'handshakeDuration': return Math.max(metric.tcp_handshake_duration, metric.udp_handshake_duration); case 'pingLatency': return metric.ping_latency; case 'networkTransmitBytes': return (metric.tcp_network_transmit_bytes + metric.udp_network_transmit_bytes) / Config.BYTE_TO_MB; default: return 0; } } getColor(index, alpha = 1) { const colors = [ `rgba(255, 99, 132, ${alpha})`, `rgba(54, 162, 235, ${alpha})`, `rgba(255, 206, 86, ${alpha})`, `rgba(75, 192, 192, ${alpha})`, `rgba(153, 102, 255, ${alpha})`, `rgba(255, 159, 64, ${alpha})`, ]; return colors[index % colors.length]; } } class FilterManager { constructor(chartManager, dateRangeManager) { this.chartManager = chartManager; this.dateRangeManager = dateRangeManager; this.$labelFilter = $('#labelFilter'); this.$remoteFilter = $('#remoteFilter'); this.relayConfigs = []; this.currentStartDate = null; this.currentEndDate = null; this.setupEventListeners(); this.loadFilters(); } setupEventListeners() { this.$labelFilter.on('change', () => this.onLabelChange()); this.$remoteFilter.on('change', () => this.applyFilters()); } async loadFilters() { const config = await ApiService.fetchConfig(); if (config && config.relay_configs) { this.relayConfigs = config.relay_configs; this.populateLabelFilter(); this.onLabelChange(); // Initialize remotes for the first label } } populateLabelFilter() { const labels = [...new Set(this.relayConfigs.map((config) => config.label))]; this.populateFilter(this.$labelFilter, labels); } onLabelChange() { const selectedLabel = this.$labelFilter.val(); const remotes = this.getRemotesForLabel(selectedLabel); this.populateFilter(this.$remoteFilter, remotes); this.applyFilters(); } getRemotesForLabel(label) { const config = this.relayConfigs.find((c) => c.label === label); return config ? config.remotes : []; } populateFilter($select, options) { $select.empty().append($('