From a218cb60e0bca70ffdd15b5bb1b772fed8539d0b Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Thu, 6 Mar 2025 23:02:43 +0100
Subject: [PATCH] FE-6 Add chart for protocols

---
 src/app/api/dashboard.api.ts                  |  13 +-
 .../dashboard-chart-protocol.component.html   |  21 ++-
 .../dashboard-chart-protocol.component.ts     | 134 ++++++++++++++----
 .../dashboard-charts.component.html           |   2 +-
 src/app/interceptor/mock-interceptor.ts       |  13 ++
 5 files changed, 150 insertions(+), 33 deletions(-)

diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts
index a6090f0..3168c8b 100644
--- a/src/app/api/dashboard.api.ts
+++ b/src/app/api/dashboard.api.ts
@@ -67,10 +67,13 @@ export class DashboardApi {
       .pipe(map(response => response.data));
   }
 
-  fetchProtocolHistory(interval: number): Observable<ChartProtocol[]> {
+  fetchProtocolHistory(
+    protocolName: string,
+    interval: number
+  ): Observable<ChartProtocol[]> {
     return this.httpClient
       .get<ApiResponse<ChartProtocol[]>>(
-        `${DASHBOARD_API_URL}/protocols/historical`,
+        `${DASHBOARD_API_URL}/${protocolName}/historical`,
         {
           params: {
             interval: interval.toString(),
@@ -80,9 +83,11 @@ export class DashboardApi {
       .pipe(map(response => response.data));
   }
 
-  fetchProtocol(): Observable<ChartProtocol> {
+  fetchProtocol(protocolName: string): Observable<ChartProtocol> {
     return this.httpClient
-      .get<ApiResponse<ChartProtocol>>(`${DASHBOARD_API_URL}/protocols/current`)
+      .get<
+        ApiResponse<ChartProtocol>
+      >(`${DASHBOARD_API_URL}/${protocolName}/current`)
       .pipe(map(response => response.data));
   }
 }
diff --git a/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html
index d0159e3..ce1f83f 100644
--- a/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html
+++ b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html
@@ -1 +1,20 @@
-<p-chart type="line" [data]="data" />
+<div class="dashboard-charts__interval">
+  <mat-form-field>
+    <mat-label>Interval</mat-label>
+    <input
+      matInput
+      type="number"
+      [ngModel]="recordsInterval()"
+      (ngModelChange)="recordsInterval.set($event)" />
+  </mat-form-field>
+  <div class="dashboard-charts__interval-slider">
+    <mat-slider [max]="100" [min]="1" [discrete]="true">
+      <input
+        matSliderThumb
+        [ngModel]="recordsInterval()"
+        (ngModelChange)="recordsInterval.set($event)" />
+    </mat-slider>
+  </div>
+</div>
+
+<p-chart type="line" [data]="data()" [options]="options" />
diff --git a/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts
index e9586f8..39c14d3 100644
--- a/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts
+++ b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts
@@ -1,47 +1,127 @@
-import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import {
+  ChangeDetectionStrategy,
+  Component,
+  computed,
+  effect,
+  inject,
+  input,
+  signal,
+} from '@angular/core';
 import { UIChart } from 'primeng/chart';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { MatSlider, MatSliderThumb } from '@angular/material/slider';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { DashboardApi } from '../../api/dashboard.api';
+import { ChartInformationRate } from '../../models/chart-information-rate';
+import { interval, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
+import { ChartProtocol } from '../../models/chart-protocol';
 
 @Component({
   selector: 'app-dashboard-chart-protocol',
-  imports: [UIChart],
+  imports: [
+    UIChart,
+    MatFormField,
+    MatInput,
+    MatLabel,
+    MatSlider,
+    MatSliderThumb,
+    ReactiveFormsModule,
+    FormsModule,
+  ],
   templateUrl: './dashboard-chart-protocol.component.html',
   styleUrl: './dashboard-chart-protocol.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DashboardChartProtocolComponent {
-  protocol = input.required<string>();
-  data: any;
-
-  ngOnInit() {
-    const documentStyle = getComputedStyle(document.documentElement);
-
-    this.data = {
-      labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
+  readonly #dashboardApi = inject(DashboardApi);
+  protocolName = input.required<string>();
+  recordsLimit = input<number>(20);
+  recordsInterval = signal<number>(1);
+  protocolHistory = signal<ChartProtocol[]>([]);
+  data = computed(() => {
+    const protocols = this.protocolHistory();
+    if (!protocols) return;
+    console.log(protocols);
+    return {
+      labels: this.getFrameLabels().map(date => date.toLocaleTimeString()),
       datasets: [
         {
-          label: 'First Dataset',
-          data: [65, 59, 80, 81, 56, 55, 40],
-          fill: false,
+          label: 'Bytes',
+          data: protocols.map(protocol => protocol.bytes),
+          yAxisId: 'y',
           tension: 0.4,
-          borderColor: documentStyle.getPropertyValue('--p-cyan-500'),
+          borderColor: '#4464e3',
         },
+        // @todo: fix Y1 axis
         {
-          label: 'Second Dataset',
-          data: [28, 48, 40, 19, 86, 27, 90],
-          fill: false,
-          borderDash: [5, 5],
+          label: 'Packets',
+          data: protocols.map(protocol => protocol.packets),
+          yAxisId: 'y1',
           tension: 0.4,
-          borderColor: documentStyle.getPropertyValue('--p-orange-500'),
-        },
-        {
-          label: 'Third Dataset',
-          data: [12, 51, 62, 33, 21, 62, 45],
-          fill: true,
-          borderColor: documentStyle.getPropertyValue('--p-gray-500'),
-          tension: 0.4,
-          backgroundColor: 'rgba(107, 114, 128, 0.2)',
+          borderColor: '#61aa48',
         },
       ],
     };
+  });
+
+  options = {
+    animation: {
+      duration: 0,
+    },
+    stacked: false,
+    scales: {
+      y: {
+        type: 'linear',
+        display: true,
+        position: 'left',
+      },
+      y1: {
+        type: 'linear',
+        display: true,
+        position: 'right',
+        grid: {
+          drawOnChartArea: false,
+        },
+      },
+    },
+  };
+
+  private stopFetching = new Subject();
+
+  constructor() {
+    effect(() => {
+      this.stopFetching.next(undefined);
+
+      this.#dashboardApi
+        .fetchProtocolHistory(this.protocolName(), this.recordsInterval())
+        .subscribe(response => this.protocolHistory.set(response));
+
+      interval(this.recordsInterval() * 1000)
+        .pipe(
+          takeUntil(this.stopFetching),
+          switchMap(() =>
+            this.#dashboardApi.fetchProtocol(this.protocolName())
+          ),
+          shareReplay(1)
+        )
+        .subscribe(ir => {
+          this.protocolHistory.update(protocols => {
+            if (protocols.length < this.recordsLimit())
+              return [...protocols, ir];
+            return [...protocols.splice(1), ir];
+          });
+        });
+    });
+  }
+
+  private getFrameLabels(): Date[] {
+    const labelsNumber = this.protocolHistory().length;
+    const now = new Date().getTime();
+    return Array.from(
+      { length: labelsNumber },
+      (_, index) =>
+        new Date(now - (labelsNumber - index) * 1000 * this.recordsInterval())
+    );
   }
 }
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.html b/src/app/components/dashboard-charts/dashboard-charts.component.html
index 3ce6565..9434f64 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.html
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.html
@@ -7,7 +7,7 @@
   </mat-tab>
   @for (protocol of protocols(); track $index) {
     <mat-tab [label]="protocol">
-      <app-dashboard-chart-protocol [protocol]="protocol" />
+      <app-dashboard-chart-protocol [protocolName]="protocol" />
     </mat-tab>
   }
 </mat-tab-group>
diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts
index e2da970..3947067 100644
--- a/src/app/interceptor/mock-interceptor.ts
+++ b/src/app/interceptor/mock-interceptor.ts
@@ -4,6 +4,7 @@ import { urls } from './interceptor';
 import { ChartFrames } from '../models/chart-frames';
 import { DASHBOARD_API_URL } from '../api/dashboard.api';
 import { ChartInformationRate } from '../models/chart-information-rate';
+import { ChartProtocol } from '../models/chart-protocol';
 
 export const MockInterceptor: HttpInterceptorFn = (req, next) => {
   const { url, method } = req;
@@ -15,6 +16,8 @@ export const MockInterceptor: HttpInterceptorFn = (req, next) => {
       body = getRandomFrameRecord();
     } else if (url === DASHBOARD_API_URL + '/information-rate/current') {
       body = getRandomInformationRateRecord();
+    } else if (url === DASHBOARD_API_URL + '/ipv4/current') {
+      body = getRandomProtocolRecord();
     }
 
     if (url.includes(element.url)) {
@@ -43,3 +46,13 @@ function getRandomInformationRateRecord(): { data: ChartInformationRate } {
     },
   };
 }
+
+function getRandomProtocolRecord(): { data: ChartProtocol } {
+  const packets = Math.floor(Math.random() * 1000) + 30;
+  return {
+    data: {
+      packets: packets,
+      bytes: packets * Math.floor(Math.random() * 512),
+    },
+  };
+}
-- 
GitLab