diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts index 3f08285d5be8adae1a1350da0cb15e29901dd36e..e81df041320410aa5f893f6c1f7044fbc3286c44 100644 --- a/src/app/api/dashboard.api.ts +++ b/src/app/api/dashboard.api.ts @@ -1,8 +1,24 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map, Observable, tap } from 'rxjs'; +import { + DashboardStatistics, + DashboardStatisticsDto, + dtoToDashboardStatistics, +} from '../models/dashboard-statistics'; +import { ApiResponse } from '../models/api-response'; -export const DASHBOARD_API_URL = '/api/v1/dashboard'; +export const DASHBOARD_API_URL = '/api/statistics'; @Injectable({ providedIn: 'root', }) -export class DashboardApi {} +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))); + } +} 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 4f88f14ff30722c0ebddbef6dfaf55272b6c5cb0..d9040eefc2f0c494965136360d354b45bc934222 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts @@ -1,10 +1,46 @@ -import { Component } from '@angular/core'; +import { + afterNextRender, + Component, + effect, + inject, + signal, +} from '@angular/core'; import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; +import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { DashboardApi } from '../../api/dashboard.api'; +import { interval, map, Observable, shareReplay, switchMap, tap } from 'rxjs'; +import { + DashboardStatistics, + ProtocolStatistics, +} from '../../models/dashboard-statistics'; +import { PillComponent } from '../pill/pill.component'; +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', }) -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 2dfdedf6c4fbf82c4cc97a526eb7533b346baef7..8e088acb883c638b72a88c594d0f501efd77132d 100644 --- a/src/app/interceptor/interceptor.ts +++ b/src/app/interceptor/interceptor.ts @@ -1,6 +1,7 @@ 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 dashboardStats from './mocks/dashboard-statistics.json'; export const urls = [ { @@ -15,4 +16,8 @@ export const urls = [ url: '/api/v1/configuration', json: configurationList, }, + { + url: '/api/statistics', + json: dashboardStats, + }, ]; 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..1d9a6e1a701c08b48bb6e60c15e014a713d7c9cd --- /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 ''; + } + + let num = Number(value).toFixed(decimalPlaces); + + return num.replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + } +} 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')}`; + } +}