diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts index fd761e3194002159711e4140edf82603bbbc1736..45e99433bce50ca9788685402773a3d1cd337d28 100644 --- a/src/app/api/configuration.api.ts +++ b/src/app/api/configuration.api.ts @@ -1,6 +1,6 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { map, Observable, tap } from 'rxjs'; +import { map, mergeMap, Observable, tap } from 'rxjs'; import { Configuration, ConfigurationDto, @@ -9,7 +9,7 @@ import { } from '../models/configuration'; import { ApiResponse } from '../models/api-response'; -export const CONFIGURATION_API_URL = '/api/configurations'; +export const CONFIGURATION_API_URL = '/api/configuration'; @Injectable({ providedIn: 'root', @@ -23,6 +23,30 @@ export class ConfigurationApi { .pipe(map(dto => dto.data.map(dtoToConfiguration))); } + fetchAppliedConfiguration(): Observable<Configuration> { + return this.httpClient + .get<ApiResponse<ConfigurationDto[]>>(CONFIGURATION_API_URL) + .pipe( + map(dto => dto.data), + + map(configurations => { + const appliedConfiguration = configurations.find( + dto => dto.is_applied + ); + + return appliedConfiguration!.id; + }), + + mergeMap(appliedConfigId => { + return this.httpClient + .get< + ApiResponse<ConfigurationDto> + >(`${CONFIGURATION_API_URL}/${appliedConfigId}`) + .pipe(map(dto => dtoToConfiguration(dto.data))); + }) + ); + } + find(configName: string): Observable<Configuration> { return this.httpClient .get< diff --git a/src/app/api/dashboard.api.ts b/src/app/api/dashboard.api.ts index 3168c8b979db259d18df8d314db390a7804a6023..3573fa35c4e60c39af62246a7136b193d9b4da7b 100644 --- a/src/app/api/dashboard.api.ts +++ b/src/app/api/dashboard.api.ts @@ -1,6 +1,6 @@ import { ApiResponse } from '../models/api-response'; import { ChartFrames } from '../models/chart-frames'; -import { map, Observable, tap } from 'rxjs'; +import { filter, map, Observable } from 'rxjs'; import { ChartInformationRate } from '../models/chart-information-rate'; import { ChartProtocol } from '../models/chart-protocol'; import { inject, Injectable } from '@angular/core'; @@ -88,6 +88,9 @@ export class DashboardApi { .get< ApiResponse<ChartProtocol> >(`${DASHBOARD_API_URL}/${protocolName}/current`) - .pipe(map(response => response.data)); + .pipe( + map((response): ChartProtocol => response.data), + filter(d => d.packets !== undefined && d.bytes !== undefined) // for some reason, the HTTP client returns object of type Settings instead of ChartProtocol + ); } } 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 aae9f5d7d6bd052f1d796e2b7cc2a91bef24452a..5a6edf2717e73fa04237ffcd84e9894c4eb9ac7e 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 @@ -42,7 +42,9 @@ export class DashboardChartProtocolComponent { protocolHistory = signal<ChartProtocol[]>([]); data = computed(() => { const protocols = this.protocolHistory(); - if (!protocols) return; + + if (!protocols.length) return; + return this.#chartService.getProtocolChartDataConfig( protocols, this.recordsInterval() @@ -69,11 +71,11 @@ export class DashboardChartProtocolComponent { ), shareReplay(1) ) - .subscribe(ir => { + .subscribe(cp => { this.protocolHistory.update(protocols => { if (protocols.length < this.recordsLimit()) - return [...protocols, ir]; - return [...protocols.splice(1), ir]; + return [...protocols, cp]; + return [...protocols.splice(1), cp]; }); }); }); diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.html b/src/app/components/dashboard-charts/dashboard-charts.component.html index 9434f6470bf1e5e63db55a41bb2d363dbf717d82..26951b6556e05c0964a3539cb7905345510371d8 100644 --- a/src/app/components/dashboard-charts/dashboard-charts.component.html +++ b/src/app/components/dashboard-charts/dashboard-charts.component.html @@ -1,16 +1,17 @@ <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> + @if (settings().showETH) { + <mat-tab label="Frames"> + <app-dashboard-chart-frames /> + </mat-tab> + } + @if (settings().showCurrentValue) { + <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 [protocolName]="protocol" /> </mat-tab> } </mat-tab-group> - -<!--@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.ts b/src/app/components/dashboard-charts/dashboard-charts.component.ts index f2da7bd11e790b79d3cef2c743378dba0b26bfd6..c8f445557b295f201f5ea25cc762cbb7bc23e1aa 100644 --- a/src/app/components/dashboard-charts/dashboard-charts.component.ts +++ b/src/app/components/dashboard-charts/dashboard-charts.component.ts @@ -1,11 +1,16 @@ -import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + 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 { Skeleton } from 'primeng/skeleton'; +import { Settings } from '../../models/settings'; @Component({ selector: 'app-dashboard-charts', @@ -17,12 +22,16 @@ import { Skeleton } from 'primeng/skeleton'; DashboardChartProtocolComponent, SliderModule, FormsModule, - Skeleton, ], templateUrl: './dashboard-charts.component.html', styleUrl: './dashboard-charts.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class DashboardChartsComponent { - protocols = input.required<string[]>(); + settings = input.required<Settings>(); + protocols = computed(() => + Object.keys(this.settings().protocols).filter( + key => this.settings().protocols[key] + ) + ); } diff --git a/src/app/components/dashboard-settings/dashboard-settings.builder.ts b/src/app/components/dashboard-settings/dashboard-settings.builder.ts new file mode 100644 index 0000000000000000000000000000000000000000..048c153d0f3370aec1b68911141d4db0325241f1 --- /dev/null +++ b/src/app/components/dashboard-settings/dashboard-settings.builder.ts @@ -0,0 +1,62 @@ +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { inject, Injectable } from '@angular/core'; +import { Settings } from '../../models/settings'; +import { interval, shareReplay, switchMap } from 'rxjs'; +import { DashboardApi } from '../../api/dashboard.api'; + +export type SettingsForm = FormGroup<{ + showTotalPackets: FormControl<boolean>; + showPacketsPerSec: FormControl<boolean>; + showTotalBytes: FormControl<boolean>; + showBytesPerSec: FormControl<boolean>; + + showETH: FormControl<boolean>; + protocols: FormGroup<{ [key: string]: FormControl<boolean> }>; + + showMinValue: FormControl<boolean>; + showMaxValue: FormControl<boolean>; + showCurrentValue: FormControl<boolean>; +}>; + +@Injectable({ + providedIn: 'root', +}) +export class SettingsFormBuilder { + formBuilder = inject(FormBuilder); + + createDefaultForm(protocols: string[]): SettingsForm { + const fb = this.formBuilder.nonNullable; + + const protocolControls = protocols.reduce( + (acc, protocolName) => { + acc[protocolName] = new FormControl<boolean>(true, { + nonNullable: true, + }); + return acc; + }, + {} as { [key: string]: FormControl<boolean> } + ); + + const form = fb.group({ + showTotalPackets: fb.control<boolean>(true), + showPacketsPerSec: fb.control<boolean>(true), + showTotalBytes: fb.control<boolean>(true), + showBytesPerSec: fb.control<boolean>(true), + + showETH: fb.control<boolean>(true), + protocols: fb.group<{ [key: string]: FormControl<boolean> }>( + protocolControls + ), + + showMinValue: fb.control<boolean>(true), + showMaxValue: fb.control<boolean>(true), + showCurrentValue: fb.control<boolean>(true), + }); + + return form; + } + + toValue(form: SettingsForm): Settings { + return form.getRawValue(); + } +} diff --git a/src/app/components/dashboard-settings/dashboard-settings.component.html b/src/app/components/dashboard-settings/dashboard-settings.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5c590d28a43cbdfc8c4d27204af9196f466593d7 --- /dev/null +++ b/src/app/components/dashboard-settings/dashboard-settings.component.html @@ -0,0 +1,45 @@ +@if(form) { +<div class="dialog" [formGroup]="form"> + <h2 mat-dialog-title class="dialog__header">Settings</h2> + <mat-dialog-content class="dialog__content"> + <div class="dialog__content__statistics__columns"> + <div class="dialog__content__statistics__columns-title"> + Statistics columns + </div> + <div class="dialog__content__statistics__columns-content"> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showTotalPackets">Total packets</mat-slide-toggle> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showPacketsPerSec">Packets per seconds</mat-slide-toggle> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showTotalBytes">Total bytes</mat-slide-toggle> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showBytesPerSec">Bytes per second</mat-slide-toggle> + </div> + </div> + <div class="dialog__content__statistics__rows"> + <div class="dialog__content__statistics__rows-title"> + Statistics rows & charts + </div> + <div class="dialog__content__statistics__rows-content"> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showETH">ETH</mat-slide-toggle> + @for (entry of Object.entries(form.controls.protocols.controls); track $index) { + <mat-slide-toggle [hideIcon]="true" [formControl]="entry[1]">{{ + entry[0] + }}</mat-slide-toggle> + } + </div> + </div> + <div class="dialog__content__statistics__ir"> + <div class="dialog__content__statistics__ir-title"> + Information Rate + </div> + <div class="dialog__content__statistics__ir-content"> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showMinValue">Min value</mat-slide-toggle> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showMaxValue">Max value</mat-slide-toggle> + <mat-slide-toggle [hideIcon]="true" [formControl]="form.controls.showCurrentValue">Current value</mat-slide-toggle> + </div> + </div> + </mat-dialog-content> + <mat-dialog-actions class="dialog__actions"> + <app-button secondary class="cancel-button" (click)="onCancel()">CANCEL</app-button> + <app-button class="save-button" (click)="onSubmit()">SAVE</app-button> + </mat-dialog-actions> +</div> +} diff --git a/src/app/components/dashboard-settings/dashboard-settings.component.scss b/src/app/components/dashboard-settings/dashboard-settings.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..b3de42627e09db3e0c166f68709e02c323b5d563 --- /dev/null +++ b/src/app/components/dashboard-settings/dashboard-settings.component.scss @@ -0,0 +1,64 @@ +@use '../../../vars'; +@use 'sass:map'; + +.dialog { + background-color: map.get(vars.$grey, 0); + &__header { + border-bottom: 1px solid map.get(vars.$grey, 30); + font-family: Monoton, cursive; + + font-size: map.get(vars.$text, 'xxl'); + padding: map.get(vars.$spacing, 'lg') 0 map.get(vars.$spacing, 'lg') map.get(vars.$spacing, 'xl'); + } + + &__content { + font-weight: 500; + + &__statistics { + &__columns, + &__rows, + &__ir { + + &-title { + font-size: map.get(vars.$text, 'xl'); + padding: map.get(vars.$spacing, 'xxl') 0 map.get(vars.$spacing, 'sm') 0; + } + &-content { + display: grid; + grid-template-columns: repeat(4, 1fr); + + border-radius: map.get(vars.$radius, 'xs'); + background-color: map.get(vars.$grey, 10); + padding: map.get(vars.$spacing, 'sm') 0 map.get(vars.$spacing, 'sm') map.get(vars.$spacing, 'xxl'); + } + } + } + + ::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__shadow { + background-color: map.get(vars.$grey, 10); + } + + ::ng-deep .mat-mdc-slide-toggle.mat-mdc-slide-toggle-checked:not(.mat-disabled) .mdc-switch__track::after { + background-color: vars.$textPrimary !important; + } + + ::ng-deep .mdc-switch__track::before { + background-color: map.get(vars.$grey, 30) !important; + } + + } + &__actions { + border-radius: map.get(vars.$radius, 'xs'); + padding: map.get(vars.$spacing, 'xxl') map.get(vars.$spacing, 'xl') map.get(vars.$spacing, 'xl') map.get(vars.$spacing, 'xl'); + display: flex; + justify-content: space-between; + + ::ng-deep app-button { + button { + width: 450px; + } + margin: map.get(vars.$spacing, 'md') 0 0 0; + } + } +} + diff --git a/src/app/components/dashboard-settings/dashboard-settings.component.ts b/src/app/components/dashboard-settings/dashboard-settings.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..768be1a1b19cf814331added007dd39aa215d18b --- /dev/null +++ b/src/app/components/dashboard-settings/dashboard-settings.component.ts @@ -0,0 +1,70 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + input, + OnInit, +} from '@angular/core'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { ToggleSwitchModule } from 'primeng/toggleswitch'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { ButtonComponent } from '../button/button.component'; +import { SettingsService } from '../../service/settings.service'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { Settings } from '../../models/settings'; +import { + SettingsForm, + SettingsFormBuilder, +} from './dashboard-settings.builder'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + MatDialogModule, + ToggleSwitchModule, + FormsModule, + MatButtonModule, + ButtonComponent, + ReactiveFormsModule, + MatSlideToggle, + ], + selector: 'app-dashboard-settings', + styleUrl: './dashboard-settings.component.scss', + templateUrl: './dashboard-settings.component.html', +}) +export class DashboardSettingsComponent implements OnInit { + settingsFormBuilder = inject(SettingsFormBuilder); + + dialogRef = inject(MatDialogRef<DashboardSettingsComponent>); + + settingsService = inject(SettingsService); + form: SettingsForm | null = null; + + ngOnInit() { + this.settingsService.settings$.subscribe(settings => { + this.initializeForm(settings); + }); + } + + initializeForm(settings: Settings) { + const protocols = Object.keys(settings.protocols); + + this.form = this.settingsFormBuilder.createDefaultForm(protocols); + this.form.patchValue(settings); + } + + onSubmit() { + if (this.form) { + const settings: Settings = this.settingsFormBuilder.toValue(this.form); + this.settingsService.updateSettings(settings); + this.dialogRef.close(); + } + } + + onCancel() { + this.dialogRef.close(); + } + + protected readonly Object = Object; +} diff --git a/src/app/components/dashboard-statistics/dashboard-statistics.component.html b/src/app/components/dashboard-statistics/dashboard-statistics.component.html index bd5d0df4699d5f06ed48f2a9e093d3c5bfcfa6ae..d00a3ab54d33c1f8819fe80930a10ef0370d0977 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.html +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.html @@ -6,72 +6,144 @@ <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> + } @else { + @if (settings()) { + <div class="statistics__table"> + <div class="statistics__table__time"> + <p>Total time: {{ statistics.total_time | time }}</p> + </div> + @if ( + showAnyStatisticsColumn(settings()) && + showStatisticsRowsAndCharts(settings()) + ) { + <table class="statistics__table__header"> + <thead class="statistics__table__header-title"> + <tr> + <th></th> + @if (settings().showTotalPackets) { + <th>Total packets</th> + } + @if (settings().showPacketsPerSec) { + <th>Packets per second</th> + } + @if (settings().showTotalBytes) { + <th>Total bytes</th> + } + @if (settings().showBytesPerSec) { + <th>Bytes per second</th> + } + </tr> + </thead> + @if (settings().showETH) { + <tbody> + @let protocolEth = getETHStatistics(statistics.protocols); + <tr class="statistics__table__header-eth"> + <td>{{ protocolEth.name }}</td> + @if (settings().showTotalPackets) { + <td>{{ protocolEth.total_packets | decimal }}</td> + } + @if (settings().showPacketsPerSec) { + <td> + {{ + getPerSecond( + protocolEth.total_packets, + statistics.total_time + ) | decimal + }} + </td> + } + @if (settings().showTotalBytes) { + <td>{{ protocolEth.total_bytes | decimal }}</td> + } + @if (settings().showBytesPerSec) { + <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 + ) { + @if (settings().protocols[protocol.name]) { + <tr> + <td>{{ protocol.name }}</td> + @if (settings().showTotalPackets) { + <td>{{ protocol.total_packets | decimal }}</td> + } + @if (settings().showPacketsPerSec) { + <td> + {{ + getPerSecond( + protocol.total_packets, + statistics.total_time + ) | decimal + }} + </td> + } + @if (settings().showTotalBytes) { + <td>{{ protocol.total_bytes | decimal }}</td> + } + @if (settings().showBytesPerSec) { + <td> + {{ + getPerSecond( + protocol.total_bytes, + statistics.total_time + ) | decimal + }} + </td> + } + </tr> + } + } + </tbody> + </table> } - </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> + @if (showInformationRate(settings()!)) { + <table class="statistics__table__ir"> + <thead class="statistics__table__ir-header"> + <tr> + <th></th> + @if (settings().showMinValue) { + <th>Min</th> + } + @if (settings().showMaxValue) { + <th>Max</th> + } + @if (settings().showCurrentValue) { + <th>Current</th> + } + </tr> + </thead> + <tbody> + <tr class="statistics__table_ir-content"> + <td>Information Rate</td> + @if (settings().showMinValue) { + <td>{{ statistics.information_rate.min | decimal }}</td> + } + @if (settings().showMaxValue) { + <td>{{ statistics.information_rate.max | decimal }}</td> + } + @if (settings().showCurrentValue) { + <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 e11b5db12cf23fcea68df59b4eba90d9989305d9..6c25990eaae01dad2680ea01405b7a3d1d7c40ce 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.scss +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.scss @@ -6,6 +6,7 @@ display: flex; flex-direction: row; gap: map.get(vars.$spacing, 'md'); + ::ng-deep * { height: 50px; } @@ -74,6 +75,7 @@ margin-top: map.get(vars.$spacing, 'lg'); width: 100%; + tbody td { border-top: 1px solid map.get(vars.$grey, 30); } diff --git a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts index 2b6f6289f1cb04b49be36b8caebc8bea25d85bfa..1e41d2502fce12be7710743452bd4194239db713 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts @@ -1,21 +1,36 @@ -import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + input, +} 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'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Settings } from '../../models/settings'; @Component({ selector: 'app-dashboard-statistics', - imports: [NgxSkeletonLoaderComponent, AsyncPipe, TimePipe, DecimalPipe], + imports: [ + NgxSkeletonLoaderComponent, + AsyncPipe, + TimePipe, + DecimalPipe, + ReactiveFormsModule, + ], templateUrl: './dashboard-statistics.component.html', styleUrl: './dashboard-statistics.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class DashboardStatisticsComponent { dashboardApi = inject(DashboardApi); + settings = input.required<Settings>(); dashboardStatistics = interval(1000).pipe( switchMap(() => this.dashboardApi.fetchStatistics()), @@ -34,4 +49,25 @@ export class DashboardStatisticsComponent { getProtocols(protocols: ProtocolStatistics[]): ProtocolStatistics[] { return protocols.filter(protocol => protocol.name !== 'ETH'); } + + showAnyStatisticsColumn(settings: Settings): boolean { + return ( + settings.showBytesPerSec || + settings.showPacketsPerSec || + settings.showTotalBytes || + settings.showTotalPackets + ); + } + + showStatisticsRowsAndCharts(settings: Settings): boolean { + return settings.showETH || Object.values(settings.protocols).some(v => v); + } + + showInformationRate(settings: Settings): boolean { + return ( + settings.showMinValue || + settings.showMaxValue || + settings.showCurrentValue + ); + } } diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index c86a0de5c150b18f00efc5b00547b72570324abb..ee374a8e181499a0e5c307eae156665632ecc1dd 100644 --- a/src/app/components/dashboard/dashboard.component.html +++ b/src/app/components/dashboard/dashboard.component.html @@ -1,3 +1,4 @@ +@let settings = settings$ | async; <app-page-wrapper> <div title>Monitoring and Analysis</div> @@ -14,15 +15,25 @@ Statistics </mat-panel-title> </mat-expansion-panel-header> - <app-dashboard-statistics /> + + @if (settings) { + <app-dashboard-statistics [settings]="settings" /> + } </mat-expansion-panel> + <mat-expansion-panel class="accordion__panel"> <mat-expansion-panel-header class="accordion__panel-header"> <mat-panel-title class="accordion__panel-title"> Charts </mat-panel-title> </mat-expansion-panel-header> - <app-dashboard-charts [protocols]="['ipv4']" /> + + @if (settings) { + <app-dashboard-charts [settings]="settings" /> + } </mat-expansion-panel> </mat-accordion> + + <!--@todo: find other way to fix scrolling to the top of the page--> + <p-skeleton height="300px" /> </app-page-wrapper> diff --git a/src/app/components/dashboard/dashboard.component.ts b/src/app/components/dashboard/dashboard.component.ts index c9ea330f3a33f59d37829b1e3c2c53d2924a7491..9d0e3807214bf517cf012cb831c731eb362b2063 100644 --- a/src/app/components/dashboard/dashboard.component.ts +++ b/src/app/components/dashboard/dashboard.component.ts @@ -1,10 +1,21 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + inject, + OnInit, +} from '@angular/core'; import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component'; import { MatExpansionModule } from '@angular/material/expansion'; import { MatIcon } from '@angular/material/icon'; import { MatFabButton } from '@angular/material/button'; import { DashboardStatisticsComponent } from '../dashboard-statistics/dashboard-statistics.component'; import { DashboardChartsComponent } from '../dashboard-charts/dashboard-charts.component'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { DashboardSettingsComponent } from '../dashboard-settings/dashboard-settings.component'; +import { ConfigurationApi } from '../../api/configuration.api'; +import { SettingsService } from '../../service/settings.service'; +import { AsyncPipe } from '@angular/common'; +import { Skeleton } from 'primeng/skeleton'; @Component({ selector: 'app-dashboard', @@ -15,11 +26,32 @@ import { DashboardChartsComponent } from '../dashboard-charts/dashboard-charts.c MatFabButton, DashboardStatisticsComponent, DashboardChartsComponent, + AsyncPipe, + Skeleton, ], templateUrl: './dashboard.component.html', styleUrl: './dashboard.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class DashboardComponent { - openSettings() {} +export class DashboardComponent implements OnInit { + readonly dialog = inject(MatDialog); + + configurationApi = inject(ConfigurationApi); + settingsService = inject(SettingsService); + settings$ = this.settingsService.settings$; + + ngOnInit() { + this.configurationApi.fetchAppliedConfiguration().subscribe(config => { + this.settingsService.initSettings(config.protocols); + }); + } + + openSettings() { + const config = new MatDialogConfig(); + + config.minWidth = '1100px'; + config.minHeight = '550px'; + + this.dialog.open(DashboardSettingsComponent, config); + } } diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts index 93d051b41e68a73b5f4e5288ea43755e2b795400..6cab5d5c0d4153206b9e13b59f43d1ca76197f81 100644 --- a/src/app/interceptor/interceptor.ts +++ b/src/app/interceptor/interceptor.ts @@ -40,11 +40,11 @@ export const urls = [ json: () => irHistorical, }, { - url: '/api/statistics/ipv4/current', + url: '/api/statistics/IPv4/current', json: () => ipv4Current, }, { - url: '/api/statistics/ipv4/historical', + url: '/api/statistics/IPv4/historical', json: () => ipv4Historical, }, { diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts index 0ce7c4cf8958d41aa28f5c0a537661328b8adb04..0c336b6ef850ac45f837e1c14b27e58fdcb6419a 100644 --- a/src/app/interceptor/mock-interceptor.ts +++ b/src/app/interceptor/mock-interceptor.ts @@ -16,7 +16,7 @@ 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') { + } else if (url === DASHBOARD_API_URL + '/IPv4/current') { body = getRandomProtocolRecord(); } diff --git a/src/app/interceptor/mocks/configuration-1.json b/src/app/interceptor/mocks/configuration-1.json index 6e971313aa0358f2068b1a5eebec1e90d8cb8ba7..93c1d9b1f5575ffd6fdcbaf52e02e18506e2b6a7 100644 --- a/src/app/interceptor/mocks/configuration-1.json +++ b/src/app/interceptor/mocks/configuration-1.json @@ -2,7 +2,7 @@ "data": { "id": "1abc", "name": "Custom", - "is_applied": false, + "is_applied": true, "mac_source": [ "01:23:45:67:89:00", "01:23:45:67:89:11", @@ -19,6 +19,6 @@ [128, 255], [1024, 1518] ], - "protocols": ["IPv4", "IP6", "UDP"] + "protocols": ["IPv4", "IPv6", "UDP"] } } diff --git a/src/app/interceptor/mocks/configuration-2.json b/src/app/interceptor/mocks/configuration-2.json index d18ca9470f7c6dc9b350038ae2edee502e66b3f7..f047b8b4edb47f698b826edce66221969e05d2dd 100644 --- a/src/app/interceptor/mocks/configuration-2.json +++ b/src/app/interceptor/mocks/configuration-2.json @@ -2,7 +2,7 @@ "data": { "id": "2def", "name": "Everything", - "is_applied": true, + "is_applied": false, "mac_source": [], "mac_destination": [], "frame_ranges": [], diff --git a/src/app/interceptor/mocks/configurations.json b/src/app/interceptor/mocks/configurations.json index b1ed282448df746db14186a2c1aae8cfe1f9a6dc..e7de12535e42398db682bf55103fbbf5cc909593 100644 --- a/src/app/interceptor/mocks/configurations.json +++ b/src/app/interceptor/mocks/configurations.json @@ -3,7 +3,7 @@ { "id": "1abc", "name": "Custom", - "is_applied": false, + "is_applied": true, "mac_source": [ "01:23:45:67:89:00", "01:23:45:67:89:11", @@ -20,12 +20,12 @@ [128, 255], [1024, 1518] ], - "protocols": ["IPv4", "IP6", "UDP"] + "protocols": ["IPv4", "IPv6", "UDP"] }, { "id": "2def", "name": "Everything", - "is_applied": true, + "is_applied": false, "mac_source": [], "mac_destination": [], "frame_ranges": [], diff --git a/src/app/interceptor/mocks/dashboard-statistics-2.json b/src/app/interceptor/mocks/dashboard-statistics-2.json index 9d58d0f611f90c47edf6bbd676fde91d8058d2ea..08e38b504dabcee6fd722b9c954df7f64c12c90c 100644 --- a/src/app/interceptor/mocks/dashboard-statistics-2.json +++ b/src/app/interceptor/mocks/dashboard-statistics-2.json @@ -9,7 +9,7 @@ "total-bytes": 123678901 }, { - "name": "TCP", + "name": "UDP", "total-packets": 323344, "total-bytes": 32112345 }, diff --git a/src/app/interceptor/mocks/dashboard-statistics.json b/src/app/interceptor/mocks/dashboard-statistics.json index b05d511f7456fb992cea4866fc92d248c29de429..d9b9013d965ae9923558580d88323c7493b18f6a 100644 --- a/src/app/interceptor/mocks/dashboard-statistics.json +++ b/src/app/interceptor/mocks/dashboard-statistics.json @@ -9,7 +9,7 @@ "total-bytes": 123456789 }, { - "name": "TCP", + "name": "UDP", "total-packets": 321312, "total-bytes": 32143245 }, diff --git a/src/app/models/settings.ts b/src/app/models/settings.ts new file mode 100644 index 0000000000000000000000000000000000000000..79a2ce659eb9fde1c9d5af1212d9c77c33b62067 --- /dev/null +++ b/src/app/models/settings.ts @@ -0,0 +1,13 @@ +export interface Settings { + showTotalPackets: boolean; + showPacketsPerSec: boolean; + showTotalBytes: boolean; + showBytesPerSec: boolean; + + showETH: boolean; + protocols: { [key: string]: boolean }; + + showMinValue: boolean; + showMaxValue: boolean; + showCurrentValue: boolean; +} diff --git a/src/app/service/settings.service.ts b/src/app/service/settings.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e9ee7ec8b8916ebbf10c42b5253ea6ed8de15ee --- /dev/null +++ b/src/app/service/settings.service.ts @@ -0,0 +1,42 @@ +import { inject, Injectable } from '@angular/core'; +import { + BehaviorSubject, + interval, + shareReplay, + Subject, + switchMap, +} from 'rxjs'; +import { Settings } from '../models/settings'; +import { SettingsFormBuilder } from '../components/dashboard-settings/dashboard-settings.builder'; + +@Injectable({ + providedIn: 'root', +}) +export class SettingsService { + settingsFormBuilder = inject(SettingsFormBuilder); + + private settingsSubject = new BehaviorSubject<Settings>({ + showBytesPerSec: true, + showPacketsPerSec: true, + showTotalBytes: true, + showTotalPackets: true, + showETH: true, + protocols: {}, + showMinValue: true, + showMaxValue: true, + showCurrentValue: true, + }); + + settings$ = this.settingsSubject.asObservable(); + + initSettings(protocols: string[]) { + const defaultSettings = this.settingsFormBuilder.toValue( + this.settingsFormBuilder.createDefaultForm(protocols) + ); + this.settingsSubject.next(defaultSettings); + } + + updateSettings(settings: Settings) { + this.settingsSubject.next(settings); + } +}