From 4b1b328f072381ce42f5100ad45336a86c1e8bdb Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Mon, 3 Mar 2025 20:45:40 +0100
Subject: [PATCH 1/6] FE-6 Mock chart data and add fetch API methods

---
 src/app/api/configuration.api.ts              |  2 +-
 src/app/api/dashboard.api.ts                  | 56 ++++++++++++++-
 src/app/interceptor/interceptor.ts            | 36 +++++++++-
 src/app/interceptor/mocks/frames-current.json |  4 ++
 .../interceptor/mocks/frames-historical.json  | 64 +++++++++++++++++
 src/app/interceptor/mocks/ipv4-current.json   |  4 ++
 .../interceptor/mocks/ipv4-historical.json    | 68 +++++++++++++++++++
 src/app/interceptor/mocks/ir-current.json     |  4 ++
 src/app/interceptor/mocks/ir-historical.json  | 52 ++++++++++++++
 src/app/models/chart-frames.ts                |  4 ++
 src/app/models/chart-information-rate.ts      |  4 ++
 src/app/models/chart-protocol.ts              |  4 ++
 12 files changed, 295 insertions(+), 7 deletions(-)
 create mode 100644 src/app/interceptor/mocks/frames-current.json
 create mode 100644 src/app/interceptor/mocks/frames-historical.json
 create mode 100644 src/app/interceptor/mocks/ipv4-current.json
 create mode 100644 src/app/interceptor/mocks/ipv4-historical.json
 create mode 100644 src/app/interceptor/mocks/ir-current.json
 create mode 100644 src/app/interceptor/mocks/ir-historical.json
 create mode 100644 src/app/models/chart-frames.ts
 create mode 100644 src/app/models/chart-information-rate.ts
 create mode 100644 src/app/models/chart-protocol.ts

diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts
index 064fd85..b134ce5 100644
--- a/src/app/api/configuration.api.ts
+++ b/src/app/api/configuration.api.ts
@@ -9,7 +9,7 @@ import {
 } from '../models/configuration';
 import { ApiResponse } from '../models/api-response';
 
-export const CONFIGURATION_API_URL = '/api/v1/configuration';
+export const CONFIGURATION_API_URL = '/configurations';
 
 @Injectable({
   providedIn: 'root',
diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts
index 3f08285..c6fe7e8 100644
--- a/src/app/api/dashboard.api.ts
+++ b/src/app/api/dashboard.api.ts
@@ -1,8 +1,58 @@
-import { Injectable } from '@angular/core';
+import { inject, Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { ApiResponse } from '../models/api-response';
+import { ChartFrames } from '../models/chart-frames';
+import { map, Observable } from 'rxjs';
+import { ChartInformationRate } from '../models/chart-information-rate';
+import { ChartProtocol } from '../models/chart-protocol';
 
-export const DASHBOARD_API_URL = '/api/v1/dashboard';
+export const DASHBOARD_API_URL = '/statistics';
 
 @Injectable({
   providedIn: 'root',
 })
-export class DashboardApi {}
+export class DashboardApi {
+  private readonly httpClient = inject(HttpClient);
+
+  fetchFramesHistory(): Observable<ChartFrames[]> {
+    return this.httpClient
+      .get<ApiResponse<ChartFrames[]>>(`${DASHBOARD_API_URL}/frames/historical`)
+      .pipe(map(response => response.data));
+  }
+
+  fetchFrames(): Observable<ChartFrames> {
+    return this.httpClient
+      .get<ApiResponse<ChartFrames>>(`${DASHBOARD_API_URL}/frames/current`)
+      .pipe(map(response => response.data));
+  }
+
+  fetchInformationRateHistory(): Observable<ChartInformationRate[]> {
+    return this.httpClient
+      .get<
+        ApiResponse<ChartInformationRate[]>
+      >(`${DASHBOARD_API_URL}/information-rate/historical`)
+      .pipe(map(response => response.data));
+  }
+
+  fetchInformationRate(): Observable<ChartInformationRate> {
+    return this.httpClient
+      .get<
+        ApiResponse<ChartInformationRate>
+      >(`${DASHBOARD_API_URL}/information-rate/current`)
+      .pipe(map(response => response.data));
+  }
+
+  fetchProtocolHistory(): Observable<ChartProtocol[]> {
+    return this.httpClient
+      .get<
+        ApiResponse<ChartProtocol[]>
+      >(`${DASHBOARD_API_URL}/protocols/historical`)
+      .pipe(map(response => response.data));
+  }
+
+  fetchProtocol(): Observable<ChartProtocol> {
+    return this.httpClient
+      .get<ApiResponse<ChartProtocol>>(`${DASHBOARD_API_URL}/protocols/current`)
+      .pipe(map(response => response.data));
+  }
+}
diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts
index 2dfdedf..21905b0 100644
--- a/src/app/interceptor/interceptor.ts
+++ b/src/app/interceptor/interceptor.ts
@@ -1,18 +1,48 @@
 import * as configurationList from './mocks/configurations.json';
 import * as configuration1 from './mocks/configuration-1.json';
 import * as configuration2 from './mocks/configuration-2.json';
+import * as framesCurrent from './mocks/frames-current.json';
+import * as framesHistorical from './mocks/frames-historical.json';
+import * as irCurrent from './mocks/ir-current.json';
+import * as irHistorical from './mocks/ir-historical.json';
+import * as ipv4Current from './mocks/ipv4-current.json';
+import * as ipv4Historical from './mocks/ipv4-historical.json';
 
 export const urls = [
   {
-    url: '/api/v1/configuration/1abc',
+    url: '/configurations/1abc',
     json: configuration1,
   },
   {
-    url: '/api/v1/configuration/2def',
+    url: '/configurations/2def',
     json: configuration2,
   },
   {
-    url: '/api/v1/configuration',
+    url: '/configurations',
     json: configurationList,
   },
+  {
+    url: '/statistics/frames/current',
+    json: framesCurrent,
+  },
+  {
+    url: '/statistics/frames/historical',
+    json: framesHistorical,
+  },
+  {
+    url: '/statistics/ir/current',
+    json: irCurrent,
+  },
+  {
+    url: '/statistics/ir/historical',
+    json: irHistorical,
+  },
+  {
+    url: '/statistics/ipv4/current',
+    json: ipv4Current,
+  },
+  {
+    url: '/statistics/ipv4/historical',
+    json: ipv4Historical,
+  },
 ];
diff --git a/src/app/interceptor/mocks/frames-current.json b/src/app/interceptor/mocks/frames-current.json
new file mode 100644
index 0000000..f336aa5
--- /dev/null
+++ b/src/app/interceptor/mocks/frames-current.json
@@ -0,0 +1,4 @@
+{
+  "valid": 2345,
+  "invalid": 234
+}
diff --git a/src/app/interceptor/mocks/frames-historical.json b/src/app/interceptor/mocks/frames-historical.json
new file mode 100644
index 0000000..134de06
--- /dev/null
+++ b/src/app/interceptor/mocks/frames-historical.json
@@ -0,0 +1,64 @@
+{
+  "frames": [
+    {
+      "valid": 1000,
+      "invalid": 15
+    },
+    {
+      "valid": 1500,
+      "invalid": 0
+    },
+    {
+      "valid": 100,
+      "invalid": 99
+    },
+    {
+      "valid": 1234,
+      "invalid": 123
+    },
+    {
+      "valid": 1555,
+      "invalid": 25
+    },
+    {
+      "valid": 1000,
+      "invalid": 15
+    },
+    {
+      "valid": 1500,
+      "invalid": 0
+    },
+    {
+      "valid": 100,
+      "invalid": 99
+    },
+    {
+      "valid": 1234,
+      "invalid": 123
+    },
+    {
+      "valid": 1555,
+      "invalid": 25
+    },
+    {
+      "valid": 1000,
+      "invalid": 15
+    },
+    {
+      "valid": 1500,
+      "invalid": 0
+    },
+    {
+      "valid": 100,
+      "invalid": 99
+    },
+    {
+      "valid": 1234,
+      "invalid": 123
+    },
+    {
+      "valid": 1555,
+      "invalid": 25
+    }
+  ]
+}
diff --git a/src/app/interceptor/mocks/ipv4-current.json b/src/app/interceptor/mocks/ipv4-current.json
new file mode 100644
index 0000000..7b75083
--- /dev/null
+++ b/src/app/interceptor/mocks/ipv4-current.json
@@ -0,0 +1,4 @@
+{
+  "bytes": 3000,
+  "packets": 58
+}
diff --git a/src/app/interceptor/mocks/ipv4-historical.json b/src/app/interceptor/mocks/ipv4-historical.json
new file mode 100644
index 0000000..d57ce55
--- /dev/null
+++ b/src/app/interceptor/mocks/ipv4-historical.json
@@ -0,0 +1,68 @@
+{
+  "statistics": [
+    {
+      "bytes": 2048,
+      "packets": 24
+    },
+    {
+      "bytes": 1024,
+      "packets": 16
+    },
+    {
+      "bytes": 1526,
+      "packets": 32
+    },
+    {
+      "bytes": 1920,
+      "packets": 48
+    },
+    {
+      "bytes": 2048,
+      "packets": 24
+    },
+    {
+      "bytes": 1024,
+      "packets": 16
+    },
+    {
+      "bytes": 1526,
+      "packets": 32
+    },
+    {
+      "bytes": 1920,
+      "packets": 48
+    },
+    {
+      "bytes": 2048,
+      "packets": 24
+    },
+    {
+      "bytes": 1024,
+      "packets": 16
+    },
+    {
+      "bytes": 1526,
+      "packets": 32
+    },
+    {
+      "bytes": 1920,
+      "packets": 48
+    },
+    {
+      "bytes": 2048,
+      "packets": 24
+    },
+    {
+      "bytes": 1024,
+      "packets": 16
+    },
+    {
+      "bytes": 1526,
+      "packets": 32
+    },
+    {
+      "bytes": 1920,
+      "packets": 48
+    }
+  ]
+}
diff --git a/src/app/interceptor/mocks/ir-current.json b/src/app/interceptor/mocks/ir-current.json
new file mode 100644
index 0000000..0aacc58
--- /dev/null
+++ b/src/app/interceptor/mocks/ir-current.json
@@ -0,0 +1,4 @@
+{
+  "current": 123.45,
+  "average": 123.4
+}
diff --git a/src/app/interceptor/mocks/ir-historical.json b/src/app/interceptor/mocks/ir-historical.json
new file mode 100644
index 0000000..593d659
--- /dev/null
+++ b/src/app/interceptor/mocks/ir-historical.json
@@ -0,0 +1,52 @@
+{
+  "statistics": [
+    {
+      "current": 123.45,
+      "average": 134.6
+    },
+    {
+      "current": 103.45,
+      "average": 114.6
+    },
+    {
+      "current": 153.45,
+      "average": 124.6
+    },
+    {
+      "current": 193.45,
+      "average": 144.6
+    },
+    {
+      "current": 123.45,
+      "average": 134.6
+    },
+    {
+      "current": 103.45,
+      "average": 114.6
+    },
+    {
+      "current": 153.45,
+      "average": 124.6
+    },
+    {
+      "current": 193.45,
+      "average": 144.6
+    },
+    {
+      "current": 123.45,
+      "average": 134.6
+    },
+    {
+      "current": 103.45,
+      "average": 114.6
+    },
+    {
+      "current": 153.45,
+      "average": 124.6
+    },
+    {
+      "current": 193.45,
+      "average": 144.6
+    }
+  ]
+}
diff --git a/src/app/models/chart-frames.ts b/src/app/models/chart-frames.ts
new file mode 100644
index 0000000..03956ce
--- /dev/null
+++ b/src/app/models/chart-frames.ts
@@ -0,0 +1,4 @@
+export interface ChartFrames {
+  valid: number;
+  invalid: number;
+}
diff --git a/src/app/models/chart-information-rate.ts b/src/app/models/chart-information-rate.ts
new file mode 100644
index 0000000..ee2919d
--- /dev/null
+++ b/src/app/models/chart-information-rate.ts
@@ -0,0 +1,4 @@
+export interface ChartInformationRate {
+  current: number;
+  average: number;
+}
diff --git a/src/app/models/chart-protocol.ts b/src/app/models/chart-protocol.ts
new file mode 100644
index 0000000..22844a0
--- /dev/null
+++ b/src/app/models/chart-protocol.ts
@@ -0,0 +1,4 @@
+export interface ChartProtocol {
+  bytes: number;
+  packets: number;
+}
-- 
GitLab


From 048c4e6320493593e975d9eef699dfed68863992 Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Mon, 3 Mar 2025 22:14:40 +0100
Subject: [PATCH 2/6] FE-6 Add dynamic frames chart

---
 package-lock.json                             | 17 ++++
 package.json                                  |  1 +
 src/app/api/dashboard.api.ts                  |  8 +-
 src/app/components/button/button.component.ts |  3 +-
 .../configuration-form.component.ts           |  2 +
 .../configuration-template.component.ts       |  3 +-
 .../configuration/configuration.component.ts  |  2 +
 .../dashboard-chart-frames.component.html     |  3 +
 .../dashboard-chart-frames.component.scss     |  0
 .../dashboard-chart-frames.component.ts       | 82 +++++++++++++++++++
 ...oard-chart-information-rate.component.html |  1 +
 ...oard-chart-information-rate.component.scss |  0
 ...hboard-chart-information-rate.component.ts | 10 +++
 .../dashboard-chart-protocol.component.html   |  1 +
 .../dashboard-chart-protocol.component.scss   |  0
 .../dashboard-chart-protocol.component.ts     | 47 +++++++++++
 .../dashboard-charts.component.html           | 14 +++-
 .../dashboard-charts.component.ts             | 20 ++++-
 .../dashboard-statistics.component.ts         |  3 +-
 .../dashboard/dashboard.component.html        |  2 +-
 .../dashboard/dashboard.component.ts          |  3 +-
 src/app/components/header/header.component.ts |  3 +-
 .../not-found/not-found.component.ts          |  3 +-
 .../page-wrapper/page-wrapper.component.ts    |  3 +-
 src/app/components/pill/pill.component.ts     |  8 +-
 .../components/sidenav/sidenav.component.ts   |  2 +
 src/app/interceptor/mocks/frames-current.json |  6 +-
 .../interceptor/mocks/frames-historical.json  |  2 +-
 src/app/interceptor/mocks/ipv4-current.json   |  6 +-
 .../interceptor/mocks/ipv4-historical.json    |  2 +-
 src/app/interceptor/mocks/ir-current.json     |  6 +-
 src/app/interceptor/mocks/ir-historical.json  |  2 +-
 32 files changed, 240 insertions(+), 25 deletions(-)
 create mode 100644 src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
 create mode 100644 src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.scss
 create mode 100644 src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
 create mode 100644 src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html
 create mode 100644 src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.scss
 create mode 100644 src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
 create mode 100644 src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html
 create mode 100644 src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.scss
 create mode 100644 src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts

diff --git a/package-lock.json b/package-lock.json
index e00ab1f..f9c86e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
         "@angular/platform-browser-dynamic": "^19.0.0",
         "@angular/router": "^19.0.0",
         "@primeng/themes": "^19.0.6",
+        "chart.js": "^4.4.8",
         "ngx-skeleton-loader": "^10.0.0",
         "primeicons": "^7.0.0",
         "primeng": "^19.0.6",
@@ -3070,6 +3071,11 @@
         "tslib": "2"
       }
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
+    },
     "node_modules/@leichtgewicht/ip-codec": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -5804,6 +5810,17 @@
       "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
       "dev": true
     },
+    "node_modules/chart.js": {
+      "version": "4.4.8",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
+      "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
     "node_modules/chokidar": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
diff --git a/package.json b/package.json
index aa94cd0..ea14e07 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
     "@angular/platform-browser-dynamic": "^19.0.0",
     "@angular/router": "^19.0.0",
     "@primeng/themes": "^19.0.6",
+    "chart.js": "^4.4.8",
     "ngx-skeleton-loader": "^10.0.0",
     "primeicons": "^7.0.0",
     "primeng": "^19.0.6",
diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts
index c6fe7e8..900ccc1 100644
--- a/src/app/api/dashboard.api.ts
+++ b/src/app/api/dashboard.api.ts
@@ -2,7 +2,7 @@ import { inject, Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 import { ApiResponse } from '../models/api-response';
 import { ChartFrames } from '../models/chart-frames';
-import { map, Observable } from 'rxjs';
+import { map, Observable, tap } from 'rxjs';
 import { ChartInformationRate } from '../models/chart-information-rate';
 import { ChartProtocol } from '../models/chart-protocol';
 
@@ -15,9 +15,13 @@ export class DashboardApi {
   private readonly httpClient = inject(HttpClient);
 
   fetchFramesHistory(): Observable<ChartFrames[]> {
+    console.log('fetchFramesHistory');
     return this.httpClient
       .get<ApiResponse<ChartFrames[]>>(`${DASHBOARD_API_URL}/frames/historical`)
-      .pipe(map(response => response.data));
+      .pipe(
+        map(response => response.data),
+        tap(console.log)
+      );
   }
 
   fetchFrames(): Observable<ChartFrames> {
diff --git a/src/app/components/button/button.component.ts b/src/app/components/button/button.component.ts
index cc2d400..3682c84 100644
--- a/src/app/components/button/button.component.ts
+++ b/src/app/components/button/button.component.ts
@@ -1,4 +1,4 @@
-import { Component, input } from '@angular/core';
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
 import { MatButton } from '@angular/material/button';
 import { NgClass } from '@angular/common';
 
@@ -7,6 +7,7 @@ import { NgClass } from '@angular/common';
   imports: [MatButton, NgClass],
   templateUrl: './button.component.html',
   styleUrl: './button.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ButtonComponent {
   secondary = input<string | null>(null);
diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts
index 0996c77..456bd9d 100644
--- a/src/app/components/configuration-form/configuration-form.component.ts
+++ b/src/app/components/configuration-form/configuration-form.component.ts
@@ -1,4 +1,5 @@
 import {
+  ChangeDetectionStrategy,
   Component,
   inject,
   input,
@@ -43,6 +44,7 @@ import { ButtonComponent } from '../button/button.component';
   ],
   templateUrl: './configuration-form.component.html',
   styleUrl: './configuration-form.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ConfigurationFormComponent implements OnInit {
   configurationFormBuilder = inject(ConfigurationFormBuilder);
diff --git a/src/app/components/configuration-template/configuration-template.component.ts b/src/app/components/configuration-template/configuration-template.component.ts
index d6f45a2..4139825 100644
--- a/src/app/components/configuration-template/configuration-template.component.ts
+++ b/src/app/components/configuration-template/configuration-template.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { MatDivider } from '@angular/material/divider';
 
 @Component({
@@ -6,5 +6,6 @@ import { MatDivider } from '@angular/material/divider';
   imports: [MatDivider],
   templateUrl: './configuration-template.component.html',
   styleUrl: './configuration-template.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ConfigurationTemplateComponent {}
diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts
index 5645562..58830a5 100644
--- a/src/app/components/configuration/configuration.component.ts
+++ b/src/app/components/configuration/configuration.component.ts
@@ -1,5 +1,6 @@
 import {
   afterNextRender,
+  ChangeDetectionStrategy,
   Component,
   effect,
   inject,
@@ -47,6 +48,7 @@ enum ConfigurationPageMode {
   ],
   templateUrl: './configuration.component.html',
   styleUrl: './configuration.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class ConfigurationComponent {
   configurationApi = inject(ConfigurationApi);
diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
new file mode 100644
index 0000000..3f4d449
--- /dev/null
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
@@ -0,0 +1,3 @@
+<p-chart type="line" [data]="data()" [options]="options" />
+<!--@todo: find other way to fix scrolling to the top of the page-->
+<p-skeleton height="290px" />
diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.scss b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
new file mode 100644
index 0000000..4bf877f
--- /dev/null
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
@@ -0,0 +1,82 @@
+import {
+  ChangeDetectionStrategy,
+  ChangeDetectorRef,
+  Component,
+  computed,
+  inject,
+  OnInit,
+  signal,
+} from '@angular/core';
+import { DashboardApi } from '../../api/dashboard.api';
+import { ChartFrames } from '../../models/chart-frames';
+import { UIChart } from 'primeng/chart';
+import { interval, shareReplay, switchMap } from 'rxjs';
+import { Skeleton } from 'primeng/skeleton';
+
+@Component({
+  selector: 'app-dashboard-chart-frames',
+  imports: [UIChart, Skeleton],
+  templateUrl: './dashboard-chart-frames.component.html',
+  styleUrl: './dashboard-chart-frames.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DashboardChartFramesComponent implements OnInit {
+  readonly #dashboardApi = inject(DashboardApi);
+  readonly #cdr = inject(ChangeDetectorRef);
+  framesHistory = signal<ChartFrames[]>([]);
+  data = computed(() => {
+    const frames = this.framesHistory();
+    if (!frames) return;
+    return {
+      labels: this.getFrameLabels().map(date => date.toLocaleTimeString()),
+      datasets: [
+        {
+          label: 'Valid Frames',
+          data: frames.map(frame => frame.valid),
+          fill: true,
+          backgroundColor: 'rgb(76, 175, 80, 0.2)',
+          tension: 0.4,
+          borderColor: '#4CAF50',
+        },
+        {
+          label: 'Invalid Frames',
+          data: frames.map(frame => frame.invalid),
+          fill: true,
+          tension: 0.4,
+          borderColor: '#f34d52',
+          backgroundColor: 'rgb(243, 77, 82, 0.2)',
+        },
+      ],
+    };
+  });
+
+  options = {
+    animation: {
+      duration: 0,
+    },
+  };
+
+  ngOnInit() {
+    this.#dashboardApi
+      .fetchFramesHistory()
+      .subscribe(response => this.framesHistory.set(response));
+
+    interval(1000)
+      .pipe(
+        switchMap(() => this.#dashboardApi.fetchFrames()),
+        shareReplay(1)
+      )
+      .subscribe(frame => {
+        this.framesHistory.update(frames => [...frames.splice(1), frame]);
+      });
+  }
+
+  private getFrameLabels(): Date[] {
+    const labelsNumber = this.framesHistory().length;
+    const now = new Date().getTime();
+    return Array.from(
+      { length: labelsNumber },
+      (_, index) => new Date(now - (labelsNumber - index) * 1000)
+    );
+  }
+}
diff --git a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html
new file mode 100644
index 0000000..4f2c3e0
--- /dev/null
+++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html
@@ -0,0 +1 @@
+<p>dashboard-chart-information-rate works!</p>
diff --git a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.scss b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.scss
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
new file mode 100644
index 0000000..2851ba1
--- /dev/null
+++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
@@ -0,0 +1,10 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+
+@Component({
+  selector: 'app-dashboard-chart-information-rate',
+  imports: [],
+  templateUrl: './dashboard-chart-information-rate.component.html',
+  styleUrl: './dashboard-chart-information-rate.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class DashboardChartInformationRateComponent {}
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
new file mode 100644
index 0000000..d0159e3
--- /dev/null
+++ b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html
@@ -0,0 +1 @@
+<p-chart type="line" [data]="data" />
diff --git a/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.scss b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.scss
new file mode 100644
index 0000000..e69de29
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
new file mode 100644
index 0000000..e9586f8
--- /dev/null
+++ b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts
@@ -0,0 +1,47 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { UIChart } from 'primeng/chart';
+
+@Component({
+  selector: 'app-dashboard-chart-protocol',
+  imports: [UIChart],
+  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'],
+      datasets: [
+        {
+          label: 'First Dataset',
+          data: [65, 59, 80, 81, 56, 55, 40],
+          fill: false,
+          tension: 0.4,
+          borderColor: documentStyle.getPropertyValue('--p-cyan-500'),
+        },
+        {
+          label: 'Second Dataset',
+          data: [28, 48, 40, 19, 86, 27, 90],
+          fill: false,
+          borderDash: [5, 5],
+          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)',
+        },
+      ],
+    };
+  }
+}
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.html b/src/app/components/dashboard-charts/dashboard-charts.component.html
index 12e92e1..1189f85 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.html
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.html
@@ -1 +1,13 @@
-<ngx-skeleton-loader count="10" />
+<mat-tab-group>
+  <mat-tab label="Frames">
+    <app-dashboard-chart-frames />
+  </mat-tab>
+  <mat-tab label="Information Rate">
+    <app-dashboard-chart-information-rate />
+  </mat-tab>
+  @for (protocol of protocols(); track $index) {
+    <mat-tab [label]="protocol">
+      <app-dashboard-chart-protocol [protocol]="protocol" />
+    </mat-tab>
+  }
+</mat-tab-group>
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.ts b/src/app/components/dashboard-charts/dashboard-charts.component.ts
index ddf7cf2..c861ca5 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.ts
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.ts
@@ -1,10 +1,22 @@
-import { Component } from '@angular/core';
-import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { MatTab, MatTabGroup } from '@angular/material/tabs';
+import { DashboardChartFramesComponent } from '../dashboard-chart-frames/dashboard-chart-frames.component';
+import { DashboardChartInformationRateComponent } from '../dashboard-chart-information-rate/dashboard-chart-information-rate.component';
+import { DashboardChartProtocolComponent } from '../dashboard-chart-protocol/dashboard-chart-protocol.component';
 
 @Component({
   selector: 'app-dashboard-charts',
-  imports: [NgxSkeletonLoaderComponent],
+  imports: [
+    MatTabGroup,
+    MatTab,
+    DashboardChartFramesComponent,
+    DashboardChartInformationRateComponent,
+    DashboardChartProtocolComponent,
+  ],
   templateUrl: './dashboard-charts.component.html',
   styleUrl: './dashboard-charts.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class DashboardChartsComponent {}
+export class DashboardChartsComponent {
+  protocols = input.required<string[]>();
+}
diff --git a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts
index 4f88f14..975f49c 100644
--- a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts
+++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
 
 @Component({
@@ -6,5 +6,6 @@ import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
   imports: [NgxSkeletonLoaderComponent],
   templateUrl: './dashboard-statistics.component.html',
   styleUrl: './dashboard-statistics.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DashboardStatisticsComponent {}
diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html
index c8259a3..c86a0de 100644
--- a/src/app/components/dashboard/dashboard.component.html
+++ b/src/app/components/dashboard/dashboard.component.html
@@ -22,7 +22,7 @@
           Charts
         </mat-panel-title>
       </mat-expansion-panel-header>
-      <app-dashboard-charts />
+      <app-dashboard-charts [protocols]="['ipv4']" />
     </mat-expansion-panel>
   </mat-accordion>
 </app-page-wrapper>
diff --git a/src/app/components/dashboard/dashboard.component.ts b/src/app/components/dashboard/dashboard.component.ts
index ab1f72f..c9ea330 100644
--- a/src/app/components/dashboard/dashboard.component.ts
+++ b/src/app/components/dashboard/dashboard.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component';
 import { MatExpansionModule } from '@angular/material/expansion';
 import { MatIcon } from '@angular/material/icon';
@@ -18,6 +18,7 @@ import { DashboardChartsComponent } from '../dashboard-charts/dashboard-charts.c
   ],
   templateUrl: './dashboard.component.html',
   styleUrl: './dashboard.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DashboardComponent {
   openSettings() {}
diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts
index 267d2a7..f13bffa 100644
--- a/src/app/components/header/header.component.ts
+++ b/src/app/components/header/header.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { DividerModule } from 'primeng/divider';
 import { MatDivider } from '@angular/material/divider';
 import { MatIcon } from '@angular/material/icon';
@@ -9,5 +9,6 @@ import { MatButton } from '@angular/material/button';
   imports: [DividerModule, MatDivider, MatIcon, MatButton],
   templateUrl: './header.component.html',
   styleUrl: './header.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class HeaderComponent {}
diff --git a/src/app/components/not-found/not-found.component.ts b/src/app/components/not-found/not-found.component.ts
index 03ff4dc..a6ac140 100644
--- a/src/app/components/not-found/not-found.component.ts
+++ b/src/app/components/not-found/not-found.component.ts
@@ -1,9 +1,10 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 
 @Component({
   selector: 'app-not-found',
   imports: [],
   templateUrl: './not-found.component.html',
   styleUrl: './not-found.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class NotFoundComponent {}
diff --git a/src/app/components/page-wrapper/page-wrapper.component.ts b/src/app/components/page-wrapper/page-wrapper.component.ts
index 3046fbc..2531cf8 100644
--- a/src/app/components/page-wrapper/page-wrapper.component.ts
+++ b/src/app/components/page-wrapper/page-wrapper.component.ts
@@ -1,4 +1,4 @@
-import { Component } from '@angular/core';
+import { ChangeDetectionStrategy, Component } from '@angular/core';
 import { MatDivider } from '@angular/material/divider';
 
 @Component({
@@ -6,5 +6,6 @@ import { MatDivider } from '@angular/material/divider';
   imports: [MatDivider],
   templateUrl: './page-wrapper.component.html',
   styleUrl: './page-wrapper.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class PageWrapperComponent {}
diff --git a/src/app/components/pill/pill.component.ts b/src/app/components/pill/pill.component.ts
index f43c8ac..be975e0 100644
--- a/src/app/components/pill/pill.component.ts
+++ b/src/app/components/pill/pill.component.ts
@@ -1,4 +1,9 @@
-import { Component, input, output } from '@angular/core';
+import {
+  ChangeDetectionStrategy,
+  Component,
+  input,
+  output,
+} from '@angular/core';
 import { MatIcon } from '@angular/material/icon';
 
 @Component({
@@ -6,6 +11,7 @@ import { MatIcon } from '@angular/material/icon';
   imports: [MatIcon],
   templateUrl: './pill.component.html',
   styleUrl: './pill.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class PillComponent {
   removable = input<boolean | null>(null);
diff --git a/src/app/components/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts
index a630fe2..dc9691e 100644
--- a/src/app/components/sidenav/sidenav.component.ts
+++ b/src/app/components/sidenav/sidenav.component.ts
@@ -1,5 +1,6 @@
 import {
   afterNextRender,
+  ChangeDetectionStrategy,
   Component,
   inject,
   signal,
@@ -24,6 +25,7 @@ import { MatIcon } from '@angular/material/icon';
   ],
   templateUrl: './sidenav.component.html',
   styleUrl: './sidenav.component.scss',
+  changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class SidenavComponent {
   router = inject(Router);
diff --git a/src/app/interceptor/mocks/frames-current.json b/src/app/interceptor/mocks/frames-current.json
index f336aa5..1df35e6 100644
--- a/src/app/interceptor/mocks/frames-current.json
+++ b/src/app/interceptor/mocks/frames-current.json
@@ -1,4 +1,6 @@
 {
-  "valid": 2345,
-  "invalid": 234
+  "data": {
+    "valid": 2345,
+    "invalid": 234
+  }
 }
diff --git a/src/app/interceptor/mocks/frames-historical.json b/src/app/interceptor/mocks/frames-historical.json
index 134de06..1599d22 100644
--- a/src/app/interceptor/mocks/frames-historical.json
+++ b/src/app/interceptor/mocks/frames-historical.json
@@ -1,5 +1,5 @@
 {
-  "frames": [
+  "data": [
     {
       "valid": 1000,
       "invalid": 15
diff --git a/src/app/interceptor/mocks/ipv4-current.json b/src/app/interceptor/mocks/ipv4-current.json
index 7b75083..789f453 100644
--- a/src/app/interceptor/mocks/ipv4-current.json
+++ b/src/app/interceptor/mocks/ipv4-current.json
@@ -1,4 +1,6 @@
 {
-  "bytes": 3000,
-  "packets": 58
+  "data": {
+    "bytes": 3000,
+    "packets": 58
+  }
 }
diff --git a/src/app/interceptor/mocks/ipv4-historical.json b/src/app/interceptor/mocks/ipv4-historical.json
index d57ce55..1c3fe57 100644
--- a/src/app/interceptor/mocks/ipv4-historical.json
+++ b/src/app/interceptor/mocks/ipv4-historical.json
@@ -1,5 +1,5 @@
 {
-  "statistics": [
+  "data": [
     {
       "bytes": 2048,
       "packets": 24
diff --git a/src/app/interceptor/mocks/ir-current.json b/src/app/interceptor/mocks/ir-current.json
index 0aacc58..946c342 100644
--- a/src/app/interceptor/mocks/ir-current.json
+++ b/src/app/interceptor/mocks/ir-current.json
@@ -1,4 +1,6 @@
 {
-  "current": 123.45,
-  "average": 123.4
+  "data": {
+    "current": 123.45,
+    "average": 123.4
+  }
 }
diff --git a/src/app/interceptor/mocks/ir-historical.json b/src/app/interceptor/mocks/ir-historical.json
index 593d659..2663f3a 100644
--- a/src/app/interceptor/mocks/ir-historical.json
+++ b/src/app/interceptor/mocks/ir-historical.json
@@ -1,5 +1,5 @@
 {
-  "statistics": [
+  "data": [
     {
       "current": 123.45,
       "average": 134.6
-- 
GitLab


From 05c266eb91907dad6be10a0a674af09566928ec7 Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Wed, 5 Mar 2025 22:27:31 +0100
Subject: [PATCH 3/6] FE-6 Add interval selection; randomize mock data

---
 src/app/api/dashboard.api.ts                  | 12 +++--
 .../dashboard-chart-frames.component.html     |  2 -
 .../dashboard-chart-frames.component.ts       | 51 +++++++++++--------
 .../dashboard-charts.component.html           | 22 +++++++-
 .../dashboard-charts.component.scss           | 20 ++++++++
 .../dashboard-charts.component.ts             | 24 ++++++++-
 src/app/interceptor/mock-interceptor.ts       | 22 ++++++--
 7 files changed, 122 insertions(+), 31 deletions(-)

diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts
index 900ccc1..42a0657 100644
--- a/src/app/api/dashboard.api.ts
+++ b/src/app/api/dashboard.api.ts
@@ -14,10 +14,16 @@ export const DASHBOARD_API_URL = '/statistics';
 export class DashboardApi {
   private readonly httpClient = inject(HttpClient);
 
-  fetchFramesHistory(): Observable<ChartFrames[]> {
-    console.log('fetchFramesHistory');
+  fetchFramesHistory(interval: number): Observable<ChartFrames[]> {
     return this.httpClient
-      .get<ApiResponse<ChartFrames[]>>(`${DASHBOARD_API_URL}/frames/historical`)
+      .get<ApiResponse<ChartFrames[]>>(
+        `${DASHBOARD_API_URL}/frames/historical`,
+        {
+          params: {
+            interval: interval.toString(),
+          },
+        }
+      )
       .pipe(
         map(response => response.data),
         tap(console.log)
diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
index 3f4d449..0e75030 100644
--- a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
@@ -1,3 +1 @@
 <p-chart type="line" [data]="data()" [options]="options" />
-<!--@todo: find other way to fix scrolling to the top of the page-->
-<p-skeleton height="290px" />
diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
index 4bf877f..7924741 100644
--- a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
@@ -1,28 +1,28 @@
 import {
   ChangeDetectionStrategy,
-  ChangeDetectorRef,
   Component,
   computed,
+  effect,
   inject,
-  OnInit,
+  input,
   signal,
 } from '@angular/core';
 import { DashboardApi } from '../../api/dashboard.api';
 import { ChartFrames } from '../../models/chart-frames';
 import { UIChart } from 'primeng/chart';
-import { interval, shareReplay, switchMap } from 'rxjs';
-import { Skeleton } from 'primeng/skeleton';
+import { interval, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
 
 @Component({
   selector: 'app-dashboard-chart-frames',
-  imports: [UIChart, Skeleton],
+  imports: [UIChart],
   templateUrl: './dashboard-chart-frames.component.html',
   styleUrl: './dashboard-chart-frames.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class DashboardChartFramesComponent implements OnInit {
+export class DashboardChartFramesComponent {
   readonly #dashboardApi = inject(DashboardApi);
-  readonly #cdr = inject(ChangeDetectorRef);
+  recordsInterval = input<number>(1);
+  recordsLimit = input<number>(20);
   framesHistory = signal<ChartFrames[]>([]);
   data = computed(() => {
     const frames = this.framesHistory();
@@ -56,19 +56,29 @@ export class DashboardChartFramesComponent implements OnInit {
     },
   };
 
-  ngOnInit() {
-    this.#dashboardApi
-      .fetchFramesHistory()
-      .subscribe(response => this.framesHistory.set(response));
+  private stopFetching = new Subject();
 
-    interval(1000)
-      .pipe(
-        switchMap(() => this.#dashboardApi.fetchFrames()),
-        shareReplay(1)
-      )
-      .subscribe(frame => {
-        this.framesHistory.update(frames => [...frames.splice(1), frame]);
-      });
+  constructor() {
+    effect(() => {
+      this.stopFetching.next(undefined);
+
+      this.#dashboardApi
+        .fetchFramesHistory(this.recordsInterval())
+        .subscribe(response => this.framesHistory.set(response));
+
+      interval(this.recordsInterval() * 1000)
+        .pipe(
+          takeUntil(this.stopFetching),
+          switchMap(() => this.#dashboardApi.fetchFrames()),
+          shareReplay(1)
+        )
+        .subscribe(frame => {
+          this.framesHistory.update(frames => {
+            if (frames.length < this.recordsLimit()) return [...frames, frame];
+            return [...frames.splice(1), frame];
+          });
+        });
+    });
   }
 
   private getFrameLabels(): Date[] {
@@ -76,7 +86,8 @@ export class DashboardChartFramesComponent implements OnInit {
     const now = new Date().getTime();
     return Array.from(
       { length: labelsNumber },
-      (_, index) => new Date(now - (labelsNumber - index) * 1000)
+      (_, 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 1189f85..f91a638 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.html
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.html
@@ -1,13 +1,33 @@
 <mat-tab-group>
   <mat-tab label="Frames">
-    <app-dashboard-chart-frames />
+    <ng-container *ngTemplateOutlet="selectInterval" />
+    <app-dashboard-chart-frames [recordsInterval]="interval" />
   </mat-tab>
   <mat-tab label="Information Rate">
+    <ng-container *ngTemplateOutlet="selectInterval" />
     <app-dashboard-chart-information-rate />
   </mat-tab>
   @for (protocol of protocols(); track $index) {
     <mat-tab [label]="protocol">
+      <ng-container *ngTemplateOutlet="selectInterval" />
       <app-dashboard-chart-protocol [protocol]="protocol" />
     </mat-tab>
   }
 </mat-tab-group>
+
+<ng-template #selectInterval>
+  <div class="dashboard-charts__interval">
+    <mat-form-field>
+      <mat-label>Interval</mat-label>
+      <input matInput type="number" [(ngModel)]="interval" />
+    </mat-form-field>
+    <div class="dashboard-charts__interval-slider">
+      <mat-slider [max]="100" [min]="1" [discrete]="true">
+        <input matSliderThumb [(ngModel)]="interval" />
+      </mat-slider>
+    </div>
+  </div>
+</ng-template>
+
+<!--@todo: find other way to fix scrolling to the top of the page-->
+<p-skeleton height="300px" />
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.scss b/src/app/components/dashboard-charts/dashboard-charts.component.scss
index e69de29..0a4803f 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.scss
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.scss
@@ -0,0 +1,20 @@
+@use '../../../vars';
+@use 'sass:map';
+
+.dashboard-charts {
+  &__interval {
+    display: grid;
+    grid-template-columns: 200px 1fr;
+    gap: map.get(vars.$spacing, 'xxl');
+    padding: map.get(vars.$spacing, 'xl');
+
+    &-slider {
+      display: grid;
+      align-items: center;
+      padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xxl');
+      background-color: map.get(vars.$grey, 0);
+      border-radius: map.get(vars.$radius, 'md');
+      margin-bottom: map.get(vars.$spacing, 'lg');
+    }
+  }
+}
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.ts b/src/app/components/dashboard-charts/dashboard-charts.component.ts
index c861ca5..7ec60c2 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.ts
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.ts
@@ -1,8 +1,20 @@
-import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import {
+  ChangeDetectionStrategy,
+  Component,
+  input,
+  signal,
+} from '@angular/core';
 import { MatTab, MatTabGroup } from '@angular/material/tabs';
 import { DashboardChartFramesComponent } from '../dashboard-chart-frames/dashboard-chart-frames.component';
 import { DashboardChartInformationRateComponent } from '../dashboard-chart-information-rate/dashboard-chart-information-rate.component';
 import { DashboardChartProtocolComponent } from '../dashboard-chart-protocol/dashboard-chart-protocol.component';
+import { SliderModule } from 'primeng/slider';
+import { FormsModule } from '@angular/forms';
+import { MatSlider, MatSliderThumb } from '@angular/material/slider';
+import { MatFormField, MatLabel } from '@angular/material/form-field';
+import { MatInput } from '@angular/material/input';
+import { Skeleton } from 'primeng/skeleton';
+import { NgTemplateOutlet } from '@angular/common';
 
 @Component({
   selector: 'app-dashboard-charts',
@@ -12,6 +24,15 @@ import { DashboardChartProtocolComponent } from '../dashboard-chart-protocol/das
     DashboardChartFramesComponent,
     DashboardChartInformationRateComponent,
     DashboardChartProtocolComponent,
+    SliderModule,
+    FormsModule,
+    MatSlider,
+    MatSliderThumb,
+    MatFormField,
+    MatInput,
+    MatLabel,
+    Skeleton,
+    NgTemplateOutlet,
   ],
   templateUrl: './dashboard-charts.component.html',
   styleUrl: './dashboard-charts.component.scss',
@@ -19,4 +40,5 @@ import { DashboardChartProtocolComponent } from '../dashboard-chart-protocol/das
 })
 export class DashboardChartsComponent {
   protocols = input.required<string[]>();
+  interval: number = 1;
 }
diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts
index d1ae561..a7c8d7b 100644
--- a/src/app/interceptor/mock-interceptor.ts
+++ b/src/app/interceptor/mock-interceptor.ts
@@ -1,17 +1,31 @@
 import { HttpInterceptorFn, HttpResponse } from '@angular/common/http';
 import { delay, of } from 'rxjs';
 import { urls } from './interceptor';
+import { ChartFrames } from '../models/chart-frames';
+import { DASHBOARD_API_URL } from '../api/dashboard.api';
 
 export const MockInterceptor: HttpInterceptorFn = (req, next) => {
   const { url, method } = req;
 
   for (const element of urls) {
+    let body = (element.json as any).default;
+    if (url === DASHBOARD_API_URL + '/frames/current') {
+      body = getRandomFrameRecord();
+    }
     if (url.includes(element.url)) {
-      console.log('Loaded from json for url: ' + url, element.json);
-      return of(
-        new HttpResponse({ status: 200, body: (element.json as any).default })
-      ).pipe(delay(300));
+      console.log('Loaded from json for url: ' + url, element.json, body);
+      return of(new HttpResponse({ status: 200, body: body })).pipe(delay(300));
     }
   }
   return next(req);
 };
+
+function getRandomFrameRecord(): { data: ChartFrames } {
+  const validNumber = Math.floor(Math.random() * 5000) + 200;
+  return {
+    data: {
+      valid: validNumber,
+      invalid: Math.floor(Math.random() * validNumber * 0.3),
+    },
+  };
+}
-- 
GitLab


From 4537d63cb9c2042e2cf7042bce9c2039b74e0387 Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Thu, 6 Mar 2025 22:33:25 +0100
Subject: [PATCH 4/6] FE-6 Add IR chart; separate interval pickers of different
 charts

---
 src/app/api/dashboard.api.ts                  |  33 +++---
 .../dashboard-chart-frames.component.html     |  19 ++++
 .../dashboard-chart-frames.component.ts       |  17 ++-
 ...oard-chart-information-rate.component.html |  21 +++-
 ...hboard-chart-information-rate.component.ts | 102 +++++++++++++++++-
 .../dashboard-charts.component.html           |  19 +---
 .../dashboard-charts.component.scss           |  20 ----
 .../dashboard-charts.component.ts             |  18 +---
 src/app/interceptor/interceptor.ts            |   4 +-
 src/app/interceptor/mock-interceptor.ts       |  16 ++-
 src/styles.scss                               |  18 ++++
 11 files changed, 211 insertions(+), 76 deletions(-)

diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts
index 2706cac..a6090f0 100644
--- a/src/app/api/dashboard.api.ts
+++ b/src/app/api/dashboard.api.ts
@@ -35,10 +35,7 @@ export class DashboardApi {
           },
         }
       )
-      .pipe(
-        map(response => response.data),
-        tap(console.log)
-      );
+      .pipe(map(response => response.data));
   }
 
   fetchFrames(): Observable<ChartFrames> {
@@ -47,11 +44,18 @@ export class DashboardApi {
       .pipe(map(response => response.data));
   }
 
-  fetchInformationRateHistory(): Observable<ChartInformationRate[]> {
+  fetchInformationRateHistory(
+    interval: number
+  ): Observable<ChartInformationRate[]> {
     return this.httpClient
-      .get<
-        ApiResponse<ChartInformationRate[]>
-      >(`${DASHBOARD_API_URL}/information-rate/historical`)
+      .get<ApiResponse<ChartInformationRate[]>>(
+        `${DASHBOARD_API_URL}/information-rate/historical`,
+        {
+          params: {
+            interval: interval.toString(),
+          },
+        }
+      )
       .pipe(map(response => response.data));
   }
 
@@ -63,11 +67,16 @@ export class DashboardApi {
       .pipe(map(response => response.data));
   }
 
-  fetchProtocolHistory(): Observable<ChartProtocol[]> {
+  fetchProtocolHistory(interval: number): Observable<ChartProtocol[]> {
     return this.httpClient
-      .get<
-        ApiResponse<ChartProtocol[]>
-      >(`${DASHBOARD_API_URL}/protocols/historical`)
+      .get<ApiResponse<ChartProtocol[]>>(
+        `${DASHBOARD_API_URL}/protocols/historical`,
+        {
+          params: {
+            interval: interval.toString(),
+          },
+        }
+      )
       .pipe(map(response => response.data));
   }
 
diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
index 0e75030..ce1f83f 100644
--- a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html
@@ -1 +1,20 @@
+<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-frames/dashboard-chart-frames.component.ts b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
index 7924741..b713d95 100644
--- a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
@@ -11,18 +11,31 @@ import { DashboardApi } from '../../api/dashboard.api';
 import { ChartFrames } from '../../models/chart-frames';
 import { UIChart } from 'primeng/chart';
 import { interval, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
+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';
 
 @Component({
   selector: 'app-dashboard-chart-frames',
-  imports: [UIChart],
+  imports: [
+    UIChart,
+    MatFormField,
+    MatInput,
+    MatLabel,
+    MatSlider,
+    MatSliderThumb,
+    ReactiveFormsModule,
+    FormsModule,
+  ],
   templateUrl: './dashboard-chart-frames.component.html',
   styleUrl: './dashboard-chart-frames.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class DashboardChartFramesComponent {
   readonly #dashboardApi = inject(DashboardApi);
-  recordsInterval = input<number>(1);
   recordsLimit = input<number>(20);
+  recordsInterval = signal<number>(1);
   framesHistory = signal<ChartFrames[]>([]);
   data = computed(() => {
     const frames = this.framesHistory();
diff --git a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html
index 4f2c3e0..ce1f83f 100644
--- a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html
+++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html
@@ -1 +1,20 @@
-<p>dashboard-chart-information-rate works!</p>
+<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-information-rate/dashboard-chart-information-rate.component.ts b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
index 2851ba1..c60ee31 100644
--- a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
+++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
@@ -1,10 +1,106 @@
-import { ChangeDetectionStrategy, Component } from '@angular/core';
+import {
+  ChangeDetectionStrategy,
+  Component,
+  computed,
+  effect,
+  inject,
+  input,
+  signal,
+} from '@angular/core';
+import { UIChart } from 'primeng/chart';
+import { DashboardApi } from '../../api/dashboard.api';
+import { ChartFrames } from '../../models/chart-frames';
+import { interval, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
+import { ChartInformationRate } from '../../models/chart-information-rate';
+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';
 
 @Component({
   selector: 'app-dashboard-chart-information-rate',
-  imports: [],
+  imports: [
+    UIChart,
+    MatFormField,
+    MatInput,
+    MatLabel,
+    MatSlider,
+    MatSliderThumb,
+    ReactiveFormsModule,
+    FormsModule,
+  ],
   templateUrl: './dashboard-chart-information-rate.component.html',
   styleUrl: './dashboard-chart-information-rate.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class DashboardChartInformationRateComponent {}
+export class DashboardChartInformationRateComponent {
+  readonly #dashboardApi = inject(DashboardApi);
+  recordsLimit = input<number>(20);
+  recordsInterval = signal<number>(1);
+  irHistory = signal<ChartInformationRate[]>([]);
+  data = computed(() => {
+    const informationRates = this.irHistory();
+    if (!informationRates) return;
+    return {
+      labels: this.getFrameLabels().map(date => date.toLocaleTimeString()),
+      datasets: [
+        {
+          label: 'Current IR',
+          data: informationRates.map(ir => ir.current),
+          fill: true,
+          backgroundColor: 'rgba(144,147,239,0.32)',
+          tension: 0.4,
+          borderColor: '#5b70f8',
+        },
+        {
+          label: 'Average IR',
+          data: informationRates.map(ir => ir.average),
+          tension: 0.4,
+          borderColor: '#535353',
+        },
+      ],
+    };
+  });
+
+  options = {
+    animation: {
+      duration: 0,
+    },
+  };
+
+  private stopFetching = new Subject();
+
+  constructor() {
+    effect(() => {
+      this.stopFetching.next(undefined);
+
+      this.#dashboardApi
+        .fetchInformationRateHistory(this.recordsInterval())
+        .subscribe(response => this.irHistory.set(response));
+
+      interval(this.recordsInterval() * 1000)
+        .pipe(
+          takeUntil(this.stopFetching),
+          switchMap(() => this.#dashboardApi.fetchInformationRate()),
+          shareReplay(1)
+        )
+        .subscribe(ir => {
+          this.irHistory.update(informationRates => {
+            if (informationRates.length < this.recordsLimit())
+              return [...informationRates, ir];
+            return [...informationRates.splice(1), ir];
+          });
+        });
+    });
+  }
+
+  private getFrameLabels(): Date[] {
+    const labelsNumber = this.irHistory().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 f91a638..3ce6565 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.html
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.html
@@ -1,33 +1,16 @@
 <mat-tab-group>
   <mat-tab label="Frames">
-    <ng-container *ngTemplateOutlet="selectInterval" />
-    <app-dashboard-chart-frames [recordsInterval]="interval" />
+    <app-dashboard-chart-frames />
   </mat-tab>
   <mat-tab label="Information Rate">
-    <ng-container *ngTemplateOutlet="selectInterval" />
     <app-dashboard-chart-information-rate />
   </mat-tab>
   @for (protocol of protocols(); track $index) {
     <mat-tab [label]="protocol">
-      <ng-container *ngTemplateOutlet="selectInterval" />
       <app-dashboard-chart-protocol [protocol]="protocol" />
     </mat-tab>
   }
 </mat-tab-group>
 
-<ng-template #selectInterval>
-  <div class="dashboard-charts__interval">
-    <mat-form-field>
-      <mat-label>Interval</mat-label>
-      <input matInput type="number" [(ngModel)]="interval" />
-    </mat-form-field>
-    <div class="dashboard-charts__interval-slider">
-      <mat-slider [max]="100" [min]="1" [discrete]="true">
-        <input matSliderThumb [(ngModel)]="interval" />
-      </mat-slider>
-    </div>
-  </div>
-</ng-template>
-
 <!--@todo: find other way to fix scrolling to the top of the page-->
 <p-skeleton height="300px" />
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.scss b/src/app/components/dashboard-charts/dashboard-charts.component.scss
index 0a4803f..e69de29 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.scss
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.scss
@@ -1,20 +0,0 @@
-@use '../../../vars';
-@use 'sass:map';
-
-.dashboard-charts {
-  &__interval {
-    display: grid;
-    grid-template-columns: 200px 1fr;
-    gap: map.get(vars.$spacing, 'xxl');
-    padding: map.get(vars.$spacing, 'xl');
-
-    &-slider {
-      display: grid;
-      align-items: center;
-      padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xxl');
-      background-color: map.get(vars.$grey, 0);
-      border-radius: map.get(vars.$radius, 'md');
-      margin-bottom: map.get(vars.$spacing, 'lg');
-    }
-  }
-}
diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.ts b/src/app/components/dashboard-charts/dashboard-charts.component.ts
index 7ec60c2..f2da7bd 100644
--- a/src/app/components/dashboard-charts/dashboard-charts.component.ts
+++ b/src/app/components/dashboard-charts/dashboard-charts.component.ts
@@ -1,20 +1,11 @@
-import {
-  ChangeDetectionStrategy,
-  Component,
-  input,
-  signal,
-} from '@angular/core';
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
 import { MatTab, MatTabGroup } from '@angular/material/tabs';
 import { DashboardChartFramesComponent } from '../dashboard-chart-frames/dashboard-chart-frames.component';
 import { DashboardChartInformationRateComponent } from '../dashboard-chart-information-rate/dashboard-chart-information-rate.component';
 import { DashboardChartProtocolComponent } from '../dashboard-chart-protocol/dashboard-chart-protocol.component';
 import { SliderModule } from 'primeng/slider';
 import { FormsModule } from '@angular/forms';
-import { MatSlider, MatSliderThumb } from '@angular/material/slider';
-import { MatFormField, MatLabel } from '@angular/material/form-field';
-import { MatInput } from '@angular/material/input';
 import { Skeleton } from 'primeng/skeleton';
-import { NgTemplateOutlet } from '@angular/common';
 
 @Component({
   selector: 'app-dashboard-charts',
@@ -26,13 +17,7 @@ import { NgTemplateOutlet } from '@angular/common';
     DashboardChartProtocolComponent,
     SliderModule,
     FormsModule,
-    MatSlider,
-    MatSliderThumb,
-    MatFormField,
-    MatInput,
-    MatLabel,
     Skeleton,
-    NgTemplateOutlet,
   ],
   templateUrl: './dashboard-charts.component.html',
   styleUrl: './dashboard-charts.component.scss',
@@ -40,5 +25,4 @@ import { NgTemplateOutlet } from '@angular/common';
 })
 export class DashboardChartsComponent {
   protocols = input.required<string[]>();
-  interval: number = 1;
 }
diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts
index f83b631..93d051b 100644
--- a/src/app/interceptor/interceptor.ts
+++ b/src/app/interceptor/interceptor.ts
@@ -32,11 +32,11 @@ export const urls = [
     json: () => framesHistorical,
   },
   {
-    url: '/api/statistics/ir/current',
+    url: '/api/statistics/information-rate/current',
     json: () => irCurrent,
   },
   {
-    url: '/api/statistics/ir/historical',
+    url: '/api/statistics/information-rate/historical',
     json: () => irHistorical,
   },
   {
diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts
index f4dc1a1..e2da970 100644
--- a/src/app/interceptor/mock-interceptor.ts
+++ b/src/app/interceptor/mock-interceptor.ts
@@ -3,17 +3,21 @@ import { delay, of } from 'rxjs';
 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';
 
 export const MockInterceptor: HttpInterceptorFn = (req, next) => {
   const { url, method } = req;
 
   for (const element of urls) {
     let body = (element.json() as any).default;
+
     if (url === DASHBOARD_API_URL + '/frames/current') {
       body = getRandomFrameRecord();
+    } else if (url === DASHBOARD_API_URL + '/information-rate/current') {
+      body = getRandomInformationRateRecord();
     }
+
     if (url.includes(element.url)) {
-      console.log('Loaded from json for url: ' + url, element.json(), body);
       return of(new HttpResponse({ status: 200, body: body })).pipe(delay(300));
     }
   }
@@ -29,3 +33,13 @@ function getRandomFrameRecord(): { data: ChartFrames } {
     },
   };
 }
+
+function getRandomInformationRateRecord(): { data: ChartInformationRate } {
+  const current = Math.floor(Math.random() * 1000) + 30;
+  return {
+    data: {
+      current: current,
+      average: Math.floor(Math.random() * current * 0.3),
+    },
+  };
+}
diff --git a/src/styles.scss b/src/styles.scss
index e81a446..ef564f1 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -12,3 +12,21 @@ body {
 mat-divider {
   border-color: map.get(vars.$grey, 30) !important;
 }
+
+.dashboard-charts {
+  &__interval {
+    display: grid;
+    grid-template-columns: 200px 1fr;
+    gap: map.get(vars.$spacing, 'xxl');
+    padding: map.get(vars.$spacing, 'xl');
+
+    &-slider {
+      display: grid;
+      align-items: center;
+      padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xxl');
+      background-color: map.get(vars.$grey, 0);
+      border-radius: map.get(vars.$radius, 'md');
+      margin-bottom: map.get(vars.$spacing, 'lg');
+    }
+  }
+}
-- 
GitLab


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 5/6] 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


From fd163bf46b0dc0686efaf37979400b72870027b5 Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Fri, 7 Mar 2025 20:55:23 +0100
Subject: [PATCH 6/6] FE-6 Fix Y axis for protocol chart; move component logic
 to chart service

---
 .../dashboard-chart-frames.component.ts       |  45 ++----
 ...hboard-chart-information-rate.component.ts |  44 ++----
 .../dashboard-chart-protocol.component.ts     |  64 ++-------
 src/app/interceptor/mock-interceptor.ts       |   4 +-
 src/app/service/.gitkeep                      |   0
 src/app/service/chart.service.ts              | 131 ++++++++++++++++++
 6 files changed, 158 insertions(+), 130 deletions(-)
 delete mode 100644 src/app/service/.gitkeep
 create mode 100644 src/app/service/chart.service.ts

diff --git a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
index b713d95..7c79268 100644
--- a/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
+++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts
@@ -15,6 +15,7 @@ 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 { ChartService } from '../../service/chart.service';
 
 @Component({
   selector: 'app-dashboard-chart-frames',
@@ -34,40 +35,20 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 })
 export class DashboardChartFramesComponent {
   readonly #dashboardApi = inject(DashboardApi);
-  recordsLimit = input<number>(20);
+  readonly #chartService = inject(ChartService);
+  recordsLimit = input<number>(60);
   recordsInterval = signal<number>(1);
   framesHistory = signal<ChartFrames[]>([]);
   data = computed(() => {
     const frames = this.framesHistory();
     if (!frames) return;
-    return {
-      labels: this.getFrameLabels().map(date => date.toLocaleTimeString()),
-      datasets: [
-        {
-          label: 'Valid Frames',
-          data: frames.map(frame => frame.valid),
-          fill: true,
-          backgroundColor: 'rgb(76, 175, 80, 0.2)',
-          tension: 0.4,
-          borderColor: '#4CAF50',
-        },
-        {
-          label: 'Invalid Frames',
-          data: frames.map(frame => frame.invalid),
-          fill: true,
-          tension: 0.4,
-          borderColor: '#f34d52',
-          backgroundColor: 'rgb(243, 77, 82, 0.2)',
-        },
-      ],
-    };
+    return this.#chartService.getFramesChartDataConfig(
+      frames,
+      this.recordsInterval()
+    );
   });
 
-  options = {
-    animation: {
-      duration: 0,
-    },
-  };
+  readonly options = this.#chartService.getDefaultChartOptions();
 
   private stopFetching = new Subject();
 
@@ -93,14 +74,4 @@ export class DashboardChartFramesComponent {
         });
     });
   }
-
-  private getFrameLabels(): Date[] {
-    const labelsNumber = this.framesHistory().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-chart-information-rate/dashboard-chart-information-rate.component.ts b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
index c60ee31..99d01f1 100644
--- a/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
+++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts
@@ -9,13 +9,13 @@ import {
 } from '@angular/core';
 import { UIChart } from 'primeng/chart';
 import { DashboardApi } from '../../api/dashboard.api';
-import { ChartFrames } from '../../models/chart-frames';
 import { interval, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
 import { ChartInformationRate } from '../../models/chart-information-rate';
 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 { ChartService } from '../../service/chart.service';
 
 @Component({
   selector: 'app-dashboard-chart-information-rate',
@@ -35,38 +35,20 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 })
 export class DashboardChartInformationRateComponent {
   readonly #dashboardApi = inject(DashboardApi);
-  recordsLimit = input<number>(20);
+  readonly #chartService = inject(ChartService);
+  recordsLimit = input<number>(60);
   recordsInterval = signal<number>(1);
   irHistory = signal<ChartInformationRate[]>([]);
   data = computed(() => {
     const informationRates = this.irHistory();
     if (!informationRates) return;
-    return {
-      labels: this.getFrameLabels().map(date => date.toLocaleTimeString()),
-      datasets: [
-        {
-          label: 'Current IR',
-          data: informationRates.map(ir => ir.current),
-          fill: true,
-          backgroundColor: 'rgba(144,147,239,0.32)',
-          tension: 0.4,
-          borderColor: '#5b70f8',
-        },
-        {
-          label: 'Average IR',
-          data: informationRates.map(ir => ir.average),
-          tension: 0.4,
-          borderColor: '#535353',
-        },
-      ],
-    };
+    return this.#chartService.getIRChartDataConfig(
+      informationRates,
+      this.recordsInterval()
+    );
   });
 
-  options = {
-    animation: {
-      duration: 0,
-    },
-  };
+  readonly options = this.#chartService.getDefaultChartOptions();
 
   private stopFetching = new Subject();
 
@@ -93,14 +75,4 @@ export class DashboardChartInformationRateComponent {
         });
     });
   }
-
-  private getFrameLabels(): Date[] {
-    const labelsNumber = this.irHistory().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-chart-protocol/dashboard-chart-protocol.component.ts b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts
index 39c14d3..aae9f5d 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
@@ -13,9 +13,9 @@ 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';
+import { interval, shareReplay, Subject, switchMap, takeUntil } from 'rxjs';
+import { ChartService } from '../../service/chart.service';
 
 @Component({
   selector: 'app-dashboard-chart-protocol',
@@ -35,57 +35,21 @@ import { ChartProtocol } from '../../models/chart-protocol';
 })
 export class DashboardChartProtocolComponent {
   readonly #dashboardApi = inject(DashboardApi);
+  readonly #chartService = inject(ChartService);
   protocolName = input.required<string>();
-  recordsLimit = input<number>(20);
+  recordsLimit = input<number>(60);
   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: 'Bytes',
-          data: protocols.map(protocol => protocol.bytes),
-          yAxisId: 'y',
-          tension: 0.4,
-          borderColor: '#4464e3',
-        },
-        // @todo: fix Y1 axis
-        {
-          label: 'Packets',
-          data: protocols.map(protocol => protocol.packets),
-          yAxisId: 'y1',
-          tension: 0.4,
-          borderColor: '#61aa48',
-        },
-      ],
-    };
+    return this.#chartService.getProtocolChartDataConfig(
+      protocols,
+      this.recordsInterval()
+    );
   });
 
-  options = {
-    animation: {
-      duration: 0,
-    },
-    stacked: false,
-    scales: {
-      y: {
-        type: 'linear',
-        display: true,
-        position: 'left',
-      },
-      y1: {
-        type: 'linear',
-        display: true,
-        position: 'right',
-        grid: {
-          drawOnChartArea: false,
-        },
-      },
-    },
-  };
+  readonly options = this.#chartService.getMultipleYAxesChartOptions();
 
   private stopFetching = new Subject();
 
@@ -114,14 +78,4 @@ export class DashboardChartProtocolComponent {
         });
     });
   }
-
-  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/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts
index 3947067..0ce7c4c 100644
--- a/src/app/interceptor/mock-interceptor.ts
+++ b/src/app/interceptor/mock-interceptor.ts
@@ -7,7 +7,7 @@ import { ChartInformationRate } from '../models/chart-information-rate';
 import { ChartProtocol } from '../models/chart-protocol';
 
 export const MockInterceptor: HttpInterceptorFn = (req, next) => {
-  const { url, method } = req;
+  const { url } = req;
 
   for (const element of urls) {
     let body = (element.json() as any).default;
@@ -52,7 +52,7 @@ function getRandomProtocolRecord(): { data: ChartProtocol } {
   return {
     data: {
       packets: packets,
-      bytes: packets * Math.floor(Math.random() * 512),
+      bytes: packets * Math.floor(Math.random() * Math.sqrt(packets) * 16),
     },
   };
 }
diff --git a/src/app/service/.gitkeep b/src/app/service/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/app/service/chart.service.ts b/src/app/service/chart.service.ts
new file mode 100644
index 0000000..861c9e0
--- /dev/null
+++ b/src/app/service/chart.service.ts
@@ -0,0 +1,131 @@
+import { Injectable } from '@angular/core';
+import { ChartInformationRate } from '../models/chart-information-rate';
+import { ChartFrames } from '../models/chart-frames';
+import { ChartProtocol } from '../models/chart-protocol';
+
+@Injectable({
+  providedIn: 'root',
+})
+export class ChartService {
+  getChartLabels(
+    labelsNumber: number,
+    labelTimeInterval: number,
+    labelsStep: number = 5
+  ): string[] {
+    const now = new Date().getTime();
+    return Array.from({ length: labelsNumber }, (_, index) => {
+      if (index % labelsStep && index != 0 && index != labelsNumber - 1) {
+        return '';
+      }
+      return new Date(
+        now - (labelsNumber - index) * 1000 * labelTimeInterval
+      ).toLocaleTimeString();
+    });
+  }
+
+  getIRChartDataConfig(
+    informationRates: ChartInformationRate[],
+    recordsInterval: number
+  ) {
+    return {
+      labels: this.getChartLabels(informationRates.length, recordsInterval),
+      datasets: [
+        {
+          label: 'Current IR',
+          data: informationRates.map(ir => ir.current),
+          fill: true,
+          backgroundColor: 'rgba(144,147,239,0.32)',
+          tension: 0.4,
+          borderColor: '#5b70f8',
+        },
+        {
+          label: 'Average IR',
+          data: informationRates.map(ir => ir.average),
+          tension: 0.4,
+          borderColor: '#535353',
+        },
+      ],
+    };
+  }
+
+  getFramesChartDataConfig(frames: ChartFrames[], recordsInterval: number) {
+    return {
+      labels: this.getChartLabels(frames.length, recordsInterval),
+      datasets: [
+        {
+          label: 'Valid Frames',
+          data: frames.map(frame => frame.valid),
+          fill: true,
+          backgroundColor: 'rgb(76, 175, 80, 0.2)',
+          tension: 0.4,
+          borderColor: '#4CAF50',
+        },
+        {
+          label: 'Invalid Frames',
+          data: frames.map(frame => frame.invalid),
+          fill: true,
+          tension: 0.4,
+          borderColor: '#f34d52',
+          backgroundColor: 'rgb(243, 77, 82, 0.2)',
+        },
+      ],
+    };
+  }
+
+  getProtocolChartDataConfig(
+    protocols: ChartProtocol[],
+    recordsInterval: number
+  ) {
+    return {
+      labels: this.getChartLabels(protocols.length, recordsInterval),
+      datasets: [
+        {
+          label: 'Bytes',
+          fill: false,
+          yAxisID: 'y',
+          tension: 0.4,
+          data: protocols.map(protocol => protocol.bytes),
+        },
+        {
+          label: 'Packets',
+          fill: false,
+          yAxisID: 'y1',
+          tension: 0.4,
+          data: protocols.map(protocol => protocol.packets),
+        },
+      ],
+    };
+  }
+
+  getDefaultChartOptions() {
+    return {
+      animation: {
+        duration: 0,
+      },
+    };
+  }
+
+  getMultipleYAxesChartOptions() {
+    return {
+      animation: {
+        duration: 0,
+      },
+      stacked: false,
+      scales: {
+        y: {
+          type: 'linear',
+          display: true,
+          position: 'left',
+        },
+        y1: {
+          type: 'linear',
+          display: true,
+          position: 'right',
+          grid: {
+            drawOnChartArea: false,
+          },
+        },
+      },
+    };
+  }
+}
-- 
GitLab