diff --git a/package-lock.json b/package-lock.json index e00ab1ff2f806704e71a481d74e1ae4d11852c03..f9c86e9612a2cf9f6187de22f812bd8a9559b811 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 aa94cd0823438fb7def27972152e47ab9500b9ac..ea14e070392bddcc5d3e6f7470e0bd394e8c8e37 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/configuration.api.ts b/src/app/api/configuration.api.ts index 064fd8503d330dfd8f8e5a438860fbe2ba3e064c..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 = '/api/v1/configuration'; +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 e81df041320410aa5f893f6c1f7044fbc3286c44..3168c8b979db259d18df8d314db390a7804a6023 100644 --- a/src/app/api/dashboard.api.ts +++ b/src/app/api/dashboard.api.ts @@ -1,12 +1,15 @@ +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 { 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/statistics'; @@ -21,4 +24,70 @@ export class DashboardApi { .get<ApiResponse<DashboardStatisticsDto>>(DASHBOARD_API_URL) .pipe(map(dto => dtoToDashboardStatistics(dto.data))); } + + fetchFramesHistory(interval: number): Observable<ChartFrames[]> { + return this.httpClient + .get<ApiResponse<ChartFrames[]>>( + `${DASHBOARD_API_URL}/frames/historical`, + { + params: { + interval: interval.toString(), + }, + } + ) + .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( + interval: number + ): Observable<ChartInformationRate[]> { + return this.httpClient + .get<ApiResponse<ChartInformationRate[]>>( + `${DASHBOARD_API_URL}/information-rate/historical`, + { + params: { + interval: interval.toString(), + }, + } + ) + .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( + protocolName: string, + interval: number + ): Observable<ChartProtocol[]> { + return this.httpClient + .get<ApiResponse<ChartProtocol[]>>( + `${DASHBOARD_API_URL}/${protocolName}/historical`, + { + params: { + interval: interval.toString(), + }, + } + ) + .pipe(map(response => response.data)); + } + + fetchProtocol(protocolName: string): Observable<ChartProtocol> { + return this.httpClient + .get< + ApiResponse<ChartProtocol> + >(`${DASHBOARD_API_URL}/${protocolName}/current`) + .pipe(map(response => response.data)); + } } diff --git a/src/app/components/button/button.component.ts b/src/app/components/button/button.component.ts index cc2d400904cb38c0b53075e7e8d254c8c2b20f6b..3682c840dade7cc8ee306becf87517588f2854b3 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 0996c777d0ac25d824cd26344fc76e5d9722e5e8..456bd9d173b0ed7bbf1ec0ab51e608304e73f516 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 d6f45a2ae79cb75f4cc59ddaea46e445c3975ff5..413982568dd01316e80b76cfaf6bd0ba3d1d7782 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 5645562d5997cbc2a9bb49dd687dcdac4efc6ed2..910833bff1914b10cf030a4b681c3ae0eed406db 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -1,8 +1,9 @@ import { - afterNextRender, + ChangeDetectionStrategy, Component, effect, inject, + OnInit, signal, } from '@angular/core'; import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component'; @@ -47,8 +48,9 @@ enum ConfigurationPageMode { ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ConfigurationComponent { +export class ConfigurationComponent implements OnInit { configurationApi = inject(ConfigurationApi); pageMode = signal<ConfigurationPageMode>(ConfigurationPageMode.READ); @@ -68,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(); }); } @@ -126,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-chart-frames/dashboard-chart-frames.component.html b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html new file mode 100644 index 0000000000000000000000000000000000000000..ce1f83fe6455c4803838efe6afa49606b4d2fa1a --- /dev/null +++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.html @@ -0,0 +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/service/.gitkeep b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.scss similarity index 100% rename from src/app/service/.gitkeep rename to src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.scss 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 0000000000000000000000000000000000000000..7c79268ea5ccbebd1fdb12bdd20bc8fa44adafab --- /dev/null +++ b/src/app/components/dashboard-chart-frames/dashboard-chart-frames.component.ts @@ -0,0 +1,77 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + 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, 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'; +import { ChartService } from '../../service/chart.service'; + +@Component({ + selector: 'app-dashboard-chart-frames', + 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); + 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 this.#chartService.getFramesChartDataConfig( + frames, + this.recordsInterval() + ); + }); + + readonly options = this.#chartService.getDefaultChartOptions(); + + private stopFetching = new Subject(); + + 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]; + }); + }); + }); + } +} 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 0000000000000000000000000000000000000000..ce1f83fe6455c4803838efe6afa49606b4d2fa1a --- /dev/null +++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.html @@ -0,0 +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-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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 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 0000000000000000000000000000000000000000..99d01f1bb58e9bed98d57a68043aff406b708bd4 --- /dev/null +++ b/src/app/components/dashboard-chart-information-rate/dashboard-chart-information-rate.component.ts @@ -0,0 +1,78 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + input, + signal, +} from '@angular/core'; +import { UIChart } from 'primeng/chart'; +import { DashboardApi } from '../../api/dashboard.api'; +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', + 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 { + readonly #dashboardApi = inject(DashboardApi); + 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 this.#chartService.getIRChartDataConfig( + informationRates, + this.recordsInterval() + ); + }); + + readonly options = this.#chartService.getDefaultChartOptions(); + + 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]; + }); + }); + }); + } +} 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 0000000000000000000000000000000000000000..ce1f83fe6455c4803838efe6afa49606b4d2fa1a --- /dev/null +++ b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.html @@ -0,0 +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-protocol/dashboard-chart-protocol.component.scss b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 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 0000000000000000000000000000000000000000..aae9f5d7d6bd052f1d796e2b7cc2a91bef24452a --- /dev/null +++ b/src/app/components/dashboard-chart-protocol/dashboard-chart-protocol.component.ts @@ -0,0 +1,81 @@ +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 { 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', + 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 { + readonly #dashboardApi = inject(DashboardApi); + readonly #chartService = inject(ChartService); + protocolName = input.required<string>(); + recordsLimit = input<number>(60); + recordsInterval = signal<number>(1); + protocolHistory = signal<ChartProtocol[]>([]); + data = computed(() => { + const protocols = this.protocolHistory(); + if (!protocols) return; + return this.#chartService.getProtocolChartDataConfig( + protocols, + this.recordsInterval() + ); + }); + + readonly options = this.#chartService.getMultipleYAxesChartOptions(); + + 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]; + }); + }); + }); + } +} diff --git a/src/app/components/dashboard-charts/dashboard-charts.component.html b/src/app/components/dashboard-charts/dashboard-charts.component.html index 12e92e1088a4c2ca23c5013fbb799f1c49dbcb2e..9434f6470bf1e5e63db55a41bb2d363dbf717d82 100644 --- a/src/app/components/dashboard-charts/dashboard-charts.component.html +++ b/src/app/components/dashboard-charts/dashboard-charts.component.html @@ -1 +1,16 @@ -<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 [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 ddf7cf2b24cee9bfe912857526c60b4b186232cd..f2da7bd11e790b79d3cef2c743378dba0b26bfd6 100644 --- a/src/app/components/dashboard-charts/dashboard-charts.component.ts +++ b/src/app/components/dashboard-charts/dashboard-charts.component.ts @@ -1,10 +1,28 @@ -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'; +import { SliderModule } from 'primeng/slider'; +import { FormsModule } from '@angular/forms'; +import { Skeleton } from 'primeng/skeleton'; @Component({ selector: 'app-dashboard-charts', - imports: [NgxSkeletonLoaderComponent], + imports: [ + MatTabGroup, + MatTab, + DashboardChartFramesComponent, + DashboardChartInformationRateComponent, + DashboardChartProtocolComponent, + SliderModule, + FormsModule, + Skeleton, + ], 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 d9040eefc2f0c494965136360d354b45bc934222..2b6f6289f1cb04b49be36b8caebc8bea25d85bfa 100644 --- a/src/app/components/dashboard-statistics/dashboard-statistics.component.ts +++ b/src/app/components/dashboard-statistics/dashboard-statistics.component.ts @@ -1,19 +1,9 @@ -import { - afterNextRender, - Component, - effect, - inject, - signal, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; -import { AsyncPipe, NgTemplateOutlet } from '@angular/common'; +import { AsyncPipe } 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 { interval, shareReplay, switchMap } from 'rxjs'; +import { ProtocolStatistics } from '../../models/dashboard-statistics'; import { TimePipe } from '../../pipes/time.pipe'; import { DecimalPipe } from '../../pipes/decimal.pipe'; @@ -22,6 +12,7 @@ import { DecimalPipe } from '../../pipes/decimal.pipe'; imports: [NgxSkeletonLoaderComponent, AsyncPipe, TimePipe, DecimalPipe], templateUrl: './dashboard-statistics.component.html', styleUrl: './dashboard-statistics.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DashboardStatisticsComponent { dashboardApi = inject(DashboardApi); diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index c8259a3f08b0d4aa1a6e0eba7a4994163a389196..c86a0de5c150b18f00efc5b00547b72570324abb 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 ab1f72fa34deef45018254ed349cc92a37fd6520..c9ea330f3a33f59d37829b1e3c2c53d2924a7491 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 267d2a72b9dfd64e6f8a61655c60b26dba43e9cc..f13bffabcaa092ec892dd65647d7c5edd05c685c 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 03ff4dc556e1345c1ac569ff4882f5d64878dd8a..a6ac140ff58296a6711904b12b3d311edffa94ac 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 3046fbcd7f1447eb25d537a917fe158b4bcb8223..2531cf897809b6811bd5b7979443995ce8f27867 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 f43c8ace3b9566369df724bce387c1871662b6be..be975e081913cbd517819336ec51dee28a4fb867 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 a630fe2959e7f2106957b7664ff0dc3063d3151d..dc9691e497cfa41019a7a9e8e73d4208598c0ed8 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/interceptor.ts b/src/app/interceptor/interceptor.ts index 8e088acb883c638b72a88c594d0f501efd77132d..93d051b41e68a73b5f4e5288ea43755e2b795400 100644 --- a/src/app/interceptor/interceptor.ts +++ b/src/app/interceptor/interceptor.ts @@ -1,23 +1,54 @@ 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'; import * as dashboardStats from './mocks/dashboard-statistics.json'; +import * as dashboardStats2 from './mocks/dashboard-statistics-2.json'; export const urls = [ { - url: '/api/v1/configuration/1abc', - json: configuration1, + url: '/api/configuration/1abc', + json: () => configuration1, }, { - url: '/api/v1/configuration/2def', - json: configuration2, + url: '/api/configuration/2def', + json: () => configuration2, }, { - url: '/api/v1/configuration', - json: configurationList, + url: '/api/configuration', + json: () => configurationList, + }, + { + url: '/api/statistics/frames/current', + json: () => framesCurrent, + }, + { + url: '/api/statistics/frames/historical', + json: () => framesHistorical, + }, + { + url: '/api/statistics/information-rate/current', + json: () => irCurrent, + }, + { + url: '/api/statistics/information-rate/historical', + json: () => irHistorical, + }, + { + url: '/api/statistics/ipv4/current', + json: () => ipv4Current, + }, + { + url: '/api/statistics/ipv4/historical', + json: () => ipv4Historical, }, { url: '/api/statistics', - json: dashboardStats, + json: () => (Math.random() < 0.5 ? dashboardStats2 : dashboardStats), }, ]; diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts index d1ae561c1d7b35f3c2ae8fb39c14f7865087fc90..0ce7c4cf8958d41aa28f5c0a537661328b8adb04 100644 --- a/src/app/interceptor/mock-interceptor.ts +++ b/src/app/interceptor/mock-interceptor.ts @@ -1,17 +1,58 @@ 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'; +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; + + if (url === DASHBOARD_API_URL + '/frames/current') { + 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)) { - 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)); + 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), + }, + }; +} + +function getRandomInformationRateRecord(): { data: ChartInformationRate } { + const current = Math.floor(Math.random() * 1000) + 30; + return { + data: { + current: current, + average: Math.floor(Math.random() * current * 0.3), + }, + }; +} + +function getRandomProtocolRecord(): { data: ChartProtocol } { + const packets = Math.floor(Math.random() * 1000) + 30; + return { + data: { + packets: packets, + bytes: packets * Math.floor(Math.random() * Math.sqrt(packets) * 16), + }, + }; +} 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/frames-current.json b/src/app/interceptor/mocks/frames-current.json new file mode 100644 index 0000000000000000000000000000000000000000..1df35e6c7b10e07e3568e4774defd33219831325 --- /dev/null +++ b/src/app/interceptor/mocks/frames-current.json @@ -0,0 +1,6 @@ +{ + "data": { + "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 0000000000000000000000000000000000000000..1599d221acb9309c9a8b90181e80d1464cd74e5f --- /dev/null +++ b/src/app/interceptor/mocks/frames-historical.json @@ -0,0 +1,64 @@ +{ + "data": [ + { + "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 0000000000000000000000000000000000000000..789f453a211be42d1dc937d02a3f7fd8c2c3139e --- /dev/null +++ b/src/app/interceptor/mocks/ipv4-current.json @@ -0,0 +1,6 @@ +{ + "data": { + "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 0000000000000000000000000000000000000000..1c3fe5775e9236f584493416b004898898f158e5 --- /dev/null +++ b/src/app/interceptor/mocks/ipv4-historical.json @@ -0,0 +1,68 @@ +{ + "data": [ + { + "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 0000000000000000000000000000000000000000..946c342cb8000270b79a0b423ba2f40fa579671c --- /dev/null +++ b/src/app/interceptor/mocks/ir-current.json @@ -0,0 +1,6 @@ +{ + "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 new file mode 100644 index 0000000000000000000000000000000000000000..2663f3aec160b96c9be42f318666e844002d69fa --- /dev/null +++ b/src/app/interceptor/mocks/ir-historical.json @@ -0,0 +1,52 @@ +{ + "data": [ + { + "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 0000000000000000000000000000000000000000..03956ce7a95779a5011bb0b094807d906e90cdb5 --- /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 0000000000000000000000000000000000000000..ee2919dd1e4b8a88963943f70eb749fd3e0fd5ea --- /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 0000000000000000000000000000000000000000..22844a0597a9f35b6354e6d49fa4197c00449056 --- /dev/null +++ b/src/app/models/chart-protocol.ts @@ -0,0 +1,4 @@ +export interface ChartProtocol { + bytes: number; + packets: number; +} diff --git a/src/app/pipes/decimal.pipe.ts b/src/app/pipes/decimal.pipe.ts index 1d9a6e1a701c08b48bb6e60c15e014a713d7c9cd..8311eabce33825a94a7fae095311bd91c3beb30f 100644 --- a/src/app/pipes/decimal.pipe.ts +++ b/src/app/pipes/decimal.pipe.ts @@ -9,8 +9,8 @@ export class DecimalPipe implements PipeTransform { return ''; } - let num = Number(value).toFixed(decimalPlaces); - - return num.replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + return Number(value).toLocaleString('pl-PL', { + maximumFractionDigits: decimalPlaces, + }); } } diff --git a/src/app/service/chart.service.ts b/src/app/service/chart.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..861c9e0de847fd4bc7bdb26014324afc0e51828f --- /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, + }, + }, + }, + }; + } +} diff --git a/src/styles.scss b/src/styles.scss index e81a44670599e509ff814d887e2131c95f3351fe..ef564f1c87d46ecb4721cb344ec04a9530fe5658 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'); + } + } +}