diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts index b134ce55e56951497fbafb46c727d9739d27cad6..fd761e3194002159711e4140edf82603bbbc1736 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 = '/configurations'; +export const CONFIGURATION_API_URL = '/api/configurations'; @Injectable({ providedIn: 'root', diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts index 42a065716b18753ed0e7c7182cdf2485e2d4d673..2706cacd2ff445925922db8d36199a93ccdf73ae 100644 --- a/src/app/api/dashboard.api.ts +++ b/src/app/api/dashboard.api.ts @@ -1,12 +1,17 @@ -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, tap } from 'rxjs'; import { ChartInformationRate } from '../models/chart-information-rate'; import { ChartProtocol } from '../models/chart-protocol'; +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { + DashboardStatistics, + DashboardStatisticsDto, + dtoToDashboardStatistics, +} from '../models/dashboard-statistics'; -export const DASHBOARD_API_URL = '/statistics'; +export const DASHBOARD_API_URL = '/api/statistics'; @Injectable({ providedIn: 'root', @@ -14,6 +19,12 @@ export const DASHBOARD_API_URL = '/statistics'; export class DashboardApi { private readonly httpClient = inject(HttpClient); + fetchStatistics(): Observable<DashboardStatistics> { + return this.httpClient + .get<ApiResponse<DashboardStatisticsDto>>(DASHBOARD_API_URL) + .pipe(map(dto => dtoToDashboardStatistics(dto.data))); + } + fetchFramesHistory(interval: number): Observable<ChartFrames[]> { return this.httpClient .get<ApiResponse<ChartFrames[]>>( diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index 58830a5b714c0d3dddbfbbf12d3b2585619eeabc..910833bff1914b10cf030a4b681c3ae0eed406db 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -1,9 +1,9 @@ import { - afterNextRender, ChangeDetectionStrategy, Component, effect, inject, + OnInit, signal, } from '@angular/core'; import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component'; @@ -50,7 +50,7 @@ enum ConfigurationPageMode { styleUrl: './configuration.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ConfigurationComponent { +export class ConfigurationComponent implements OnInit { configurationApi = inject(ConfigurationApi); pageMode = signal<ConfigurationPageMode>(ConfigurationPageMode.READ); @@ -70,27 +70,15 @@ export class ConfigurationComponent { }); } }); + } - afterNextRender(() => { - this.fetchConfigurationOptions().subscribe(configurations => { - const appliedConfiguration = configurations.find(c => c.is_applied); - if (appliedConfiguration) { - this.chosenConfiguration.set(appliedConfiguration.id); - } - this.configurationOptions.set(configurations); - }); - }); + ngOnInit() { + this.updateConfigurationOptions(); } onDeleteConfiguration() { this.configurationApi.delete(this.chosenConfiguration()!).subscribe(() => { - this.fetchConfigurationOptions().subscribe(configurations => { - const appliedConfiguration = configurations.find(c => c.is_applied); - if (appliedConfiguration) { - this.chosenConfiguration.set(appliedConfiguration.id); - } - this.configurationOptions.set(configurations); - }); + this.updateConfigurationOptions(); }); } @@ -128,5 +116,15 @@ export class ConfigurationComponent { ); } + private updateConfigurationOptions() { + this.fetchConfigurationOptions().subscribe(configurations => { + const appliedConfiguration = configurations.find(c => c.is_applied); + if (appliedConfiguration) { + this.chosenConfiguration.set(appliedConfiguration.id); + } + this.configurationOptions.set(configurations); + }); + } + protected readonly ConfigurationPageMode = ConfigurationPageMode; } diff --git a/src/app/components/dashboard-statistics/dashboard-statistics.component.html b/src/app/components/dashboard-statistics/dashboard-statistics.component.html index 12e92e1088a4c2ca23c5013fbb799f1c49dbcb2e..bd5d0df4699d5f06ed48f2a9e093d3c5bfcfa6ae 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.html +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.html @@ -1 +1,77 @@ -<ngx-skeleton-loader count="10" /> +<div class="statistics"> + @let statistics = dashboardStatistics | async; + @if (!statistics) { + <div class="statistics__loader"> + @for (_ of [].constructor(4); track $index) { + <ngx-skeleton-loader count="7" /> + } + </div> + } + @else { + <div class="statistics__table"> + <div class="statistics__table__time"> + <p>Total time: {{ statistics.total_time | time }} </p> + </div> + <table class="statistics__table__header"> + <thead class="statistics__table__header-title"> + <tr> + <th></th> + <th>Total packets</th> + <th>Packets per second</th> + <th>Total bytes</th> + <th>Bytes per second</th> + </tr> + </thead> + <tbody> + @let protocolEth = getETHStatistics(statistics.protocols); + <tr class="statistics__table__header-eth"> + <td>{{ protocolEth.name }}</td> + <td>{{ protocolEth.total_packets | decimal }}</td> + <td> + {{ getPerSecond(protocolEth.total_packets, statistics.total_time) | decimal }} + </td> + <td>{{ protocolEth.total_bytes | decimal }}</td> + <td> + {{ getPerSecond(protocolEth.total_bytes, statistics.total_time) | decimal }} + </td> + </tr> + </tbody> + </table> + <table class="statistics__table__protocols"> + <tbody> + @for (protocol of getProtocols(statistics.protocols); track protocol.name) { + <tr> + <td>{{ protocol.name }}</td> + <td>{{ protocol.total_packets | decimal }}</td> + <td> + {{ getPerSecond(protocol.total_packets, statistics.total_time) | decimal }} + </td> + <td>{{ protocol.total_bytes | decimal }}</td> + <td> + {{ getPerSecond(protocol.total_bytes, statistics.total_time) | decimal }} + </td> + </tr> + } + </tbody> + </table> + <table class="statistics__table__ir"> + <thead class="statistics__table__ir-header"> + <tr> + <th></th> + <th>Min</th> + <th>Max</th> + <th>Current</th> + </tr> + </thead> + <tbody> + <tr class="statistics__table_ir-content"> + <td>Information Rate</td> + <td>{{ statistics.information_rate.min | decimal }}</td> + <td>{{ statistics.information_rate.max | decimal }}</td> + <td>{{ statistics.information_rate.current | decimal }}</td> + </tr> + </tbody> + </table> + </div> + } +</div> diff --git a/src/app/components/dashboard-statistics/dashboard-statistics.component.scss b/src/app/components/dashboard-statistics/dashboard-statistics.component.scss index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e11b5db12cf23fcea68df59b4eba90d9989305d9 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.scss +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.scss @@ -0,0 +1,89 @@ +@use '../../../vars'; +@use 'sass:map'; + +:host { + ngx-skeleton-loader { + display: flex; + flex-direction: row; + gap: map.get(vars.$spacing, 'md'); + ::ng-deep * { + height: 50px; + } + } + + .statistics { + display: flex; + flex-direction: column; + justify-content: space-between; + font-weight: 500; + font-size: map.get(vars.$text, 'lg'); + + &__table { + &__time { + font-size: map.get(vars.$text, 'md'); + } + + &__header { + width: 100%; + border-radius: map.get(vars.$radius, 'xs'); + background-color: map.get(vars.$grey, 0); + padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xl'); + + &-title { + border-radius: map.get(vars.$radius, 'xs'); + background-color: map.get(vars.$grey, 0); + padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xl'); + } + + &-eth td { + border-top: 1px solid map.get(vars.$grey, 30); + } + + th, td { + width: 20%; + padding: map.get(vars.$spacing, 'sm'); + text-align: left; + } + + } + + &__protocols { + border-radius: map.get(vars.$radius, 'xs'); + background-color: map.get(vars.$grey, 0); + padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xl'); + + margin-top: map.get(vars.$text, 'lg'); + width: 100%; + + th, td { + width: 20%; + padding: map.get(vars.$spacing, 'sm'); + text-align: left; + } + + tr:not(:last-child) td { + border-bottom: 1px solid map.get(vars.$grey, 30); + } + } + + &__ir { + border-radius: map.get(vars.$radius, 'xs'); + background-color: map.get(vars.$grey, 0); + padding: map.get(vars.$spacing, 'none') map.get(vars.$spacing, 'xl'); + + margin-top: map.get(vars.$spacing, 'lg'); + width: 100%; + + tbody td { + border-top: 1px solid map.get(vars.$grey, 30); + } + + th, td { + width: 25%; + padding: map.get(vars.$spacing, 'sm'); + text-align: left; + } + } + } + } +} diff --git a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts index 975f49c33dc444132cf28267b37526124407a9b7..2b6f6289f1cb04b49be36b8caebc8bea25d85bfa 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts @@ -1,11 +1,37 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; +import { AsyncPipe } from '@angular/common'; +import { DashboardApi } from '../../api/dashboard.api'; +import { interval, shareReplay, switchMap } from 'rxjs'; +import { ProtocolStatistics } from '../../models/dashboard-statistics'; +import { TimePipe } from '../../pipes/time.pipe'; +import { DecimalPipe } from '../../pipes/decimal.pipe'; @Component({ selector: 'app-dashboard-statistics', - imports: [NgxSkeletonLoaderComponent], + imports: [NgxSkeletonLoaderComponent, AsyncPipe, TimePipe, DecimalPipe], templateUrl: './dashboard-statistics.component.html', styleUrl: './dashboard-statistics.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardStatisticsComponent {} +export class DashboardStatisticsComponent { + dashboardApi = inject(DashboardApi); + + dashboardStatistics = interval(1000).pipe( + switchMap(() => this.dashboardApi.fetchStatistics()), + shareReplay(1) + ); + + getPerSecond(value: number, time: Date): number { + const totalSeconds = time.getTime() / 1000; + return value / totalSeconds; + } + + getETHStatistics(protocols: ProtocolStatistics[]): ProtocolStatistics { + return protocols.find(protocol => protocol.name === 'ETH')!; + } + + getProtocols(protocols: ProtocolStatistics[]): ProtocolStatistics[] { + return protocols.filter(protocol => protocol.name !== 'ETH'); + } +} diff --git a/src/app/components/dashboard/dashboard.component.scss b/src/app/components/dashboard/dashboard.component.scss index 1938814a9ffa012336e43c56745f11cf1ea4b54c..d11c2b9c25b202716ae17166d1acf99a1ccdeb1e 100644 --- a/src/app/components/dashboard/dashboard.component.scss +++ b/src/app/components/dashboard/dashboard.component.scss @@ -29,6 +29,10 @@ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); border-bottom: 1px solid map.get(vars.$grey, 30); + &-title { + font-size: map.get(vars.$text, 'lg'); + } + &:last-child { border-bottom: none; } diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts index 21905b02b2b875fa4dbfb9ede7656b5ba6ec3fa1..f83b6314c8851c00741fab2c06a14b6822c7d7ca 100644 --- a/src/app/interceptor/interceptor.ts +++ b/src/app/interceptor/interceptor.ts @@ -7,42 +7,48 @@ 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'; +import * as dashboardStats from './mocks/dashboard-statistics.json'; +import * as dashboardStats2 from './mocks/dashboard-statistics-2.json'; export const urls = [ { - url: '/configurations/1abc', - json: configuration1, + url: '/api/configuration/1abc', + json: () => configuration1, }, { - url: '/configurations/2def', - json: configuration2, + url: '/api/configuration/2def', + json: () => configuration2, }, { - url: '/configurations', - json: configurationList, + url: '/api/configuration', + json: () => configurationList, }, { - url: '/statistics/frames/current', - json: framesCurrent, + url: '/api/statistics/frames/current', + json: () => framesCurrent, }, { - url: '/statistics/frames/historical', - json: framesHistorical, + url: '/api/statistics/frames/historical', + json: () => framesHistorical, }, { - url: '/statistics/ir/current', - json: irCurrent, + url: '/api/statistics/ir/current', + json: () => irCurrent, }, { - url: '/statistics/ir/historical', - json: irHistorical, + url: '/api/statistics/ir/historical', + json: () => irHistorical, }, { - url: '/statistics/ipv4/current', - json: ipv4Current, + url: '/api/statistics/ipv4/current', + json: () => ipv4Current, }, { - url: '/statistics/ipv4/historical', - json: ipv4Historical, + url: '/api/statistics/ipv4/historical', + json: () => ipv4Historical, + }, + { + url: '/api/statistics', + json: () => (Math.random() < 0.5 ? dashboardStats2 : dashboardStats), }, ]; diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts index a7c8d7b51b8951c31a20f285dcda4e588a9610d8..f4dc1a11cecbfd449140b9e199758083ddcdcd46 100644 --- a/src/app/interceptor/mock-interceptor.ts +++ b/src/app/interceptor/mock-interceptor.ts @@ -8,12 +8,12 @@ export const MockInterceptor: HttpInterceptorFn = (req, next) => { const { url, method } = req; for (const element of urls) { - let body = (element.json as any).default; + 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, body); + console.log('Loaded from json for url: ' + url, element.json(), body); return of(new HttpResponse({ status: 200, body: body })).pipe(delay(300)); } } diff --git a/src/app/interceptor/mocks/dashboard-statistics-2.json b/src/app/interceptor/mocks/dashboard-statistics-2.json new file mode 100644 index 0000000000000000000000000000000000000000..9d58d0f611f90c47edf6bbd676fde91d8058d2ea --- /dev/null +++ b/src/app/interceptor/mocks/dashboard-statistics-2.json @@ -0,0 +1,33 @@ +{ + "data": { + "id": "statistics", + "total-time": 160402, + "protocols": [ + { + "name": "ETH", + "total-packets": 1237890, + "total-bytes": 123678901 + }, + { + "name": "TCP", + "total-packets": 323344, + "total-bytes": 32112345 + }, + { + "name": "IPv4", + "total-packets": 3399, + "total-bytes": 1000 + }, + { + "name": "IPv6", + "total-packets": 13, + "total-bytes": 778 + } + ], + "information-rate": { + "min": 0, + "max": 10.53, + "current": 10.53 + } + } +} diff --git a/src/app/interceptor/mocks/dashboard-statistics.json b/src/app/interceptor/mocks/dashboard-statistics.json new file mode 100644 index 0000000000000000000000000000000000000000..b05d511f7456fb992cea4866fc92d248c29de429 --- /dev/null +++ b/src/app/interceptor/mocks/dashboard-statistics.json @@ -0,0 +1,33 @@ +{ + "data": { + "id": "statistics", + "total-time": 160401, + "protocols": [ + { + "name": "ETH", + "total-packets": 1234567, + "total-bytes": 123456789 + }, + { + "name": "TCP", + "total-packets": 321312, + "total-bytes": 32143245 + }, + { + "name": "IPv4", + "total-packets": 3333, + "total-bytes": 777 + }, + { + "name": "IPv6", + "total-packets": 0, + "total-bytes": 0 + } + ], + "information-rate": { + "min": 0, + "max": 0, + "current": 0 + } + } +} diff --git a/src/app/models/dashboard-statistics.ts b/src/app/models/dashboard-statistics.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a8befc246d094db7c2a652aa599b2a4e49273cd --- /dev/null +++ b/src/app/models/dashboard-statistics.ts @@ -0,0 +1,47 @@ +export interface ProtocolStatistics { + name: string; + total_packets: number; + total_bytes: number; +} + +export interface ProtocolStatisticsDto { + name: string; + 'total-packets': number; + 'total-bytes': number; +} + +export interface InformationRate { + min: number; + max: number; + current: number; +} + +export interface DashboardStatisticsDto { + 'total-time': number; + protocols: ProtocolStatisticsDto[]; + 'information-rate': InformationRate; +} + +export interface DashboardStatistics { + total_time: Date; + protocols: ProtocolStatistics[]; + information_rate: InformationRate; +} + +export function dtoToDashboardStatistics( + dto: DashboardStatisticsDto +): DashboardStatistics { + return { + total_time: new Date(dto['total-time'] * 1000), + protocols: dto.protocols.map((protocol: ProtocolStatisticsDto) => ({ + name: protocol.name, + total_packets: protocol['total-packets'], + total_bytes: protocol['total-bytes'], + })), + information_rate: { + min: dto['information-rate'].min, + max: dto['information-rate'].max, + current: dto['information-rate'].current, + }, + }; +} diff --git a/src/app/pipes/decimal.pipe.ts b/src/app/pipes/decimal.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..8311eabce33825a94a7fae095311bd91c3beb30f --- /dev/null +++ b/src/app/pipes/decimal.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'decimal', +}) +export class DecimalPipe implements PipeTransform { + transform(value: number | string, decimalPlaces: number = 2): string { + if (value == null || isNaN(Number(value))) { + return ''; + } + + return Number(value).toLocaleString('pl-PL', { + maximumFractionDigits: decimalPlaces, + }); + } +} diff --git a/src/app/pipes/time.pipe.ts b/src/app/pipes/time.pipe.ts new file mode 100644 index 0000000000000000000000000000000000000000..3213400b643d473c4d78b29282cb866ca4ffd76a --- /dev/null +++ b/src/app/pipes/time.pipe.ts @@ -0,0 +1,15 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'time', +}) +export class TimePipe implements PipeTransform { + transform(date: Date): string { + let totalSeconds = Math.floor(date.getTime() / 1000); + const hours = Math.floor(totalSeconds / 3600); + totalSeconds %= 3600; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } +}