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