From 29c74e21d76f436906f2fcadb0f76312e63cbe81 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Sat, 15 Feb 2025 21:18:55 +0100 Subject: [PATCH 01/13] FE-3 Add Configuration model --- src/app/api/configuration.api.ts | 28 ++++++++++++++++++++++++++-- src/app/models/.gitkeep | 0 src/app/models/configuration.ts | 21 +++++++++++++++++++++ src/app/models/frame-range.ts | 10 ++++++++++ src/app/models/protocol.ts | 8 ++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) delete mode 100644 src/app/models/.gitkeep create mode 100644 src/app/models/configuration.ts create mode 100644 src/app/models/frame-range.ts create mode 100644 src/app/models/protocol.ts diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts index 691912a..1c7a9ce 100644 --- a/src/app/api/configuration.api.ts +++ b/src/app/api/configuration.api.ts @@ -1,8 +1,32 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map, Observable } from 'rxjs'; +import { + Configuration, + configurationToDto, + dtoToConfiguration, +} from '../models/configuration'; export const CONFIGURATION_API_URL = '/api/v1/configuration'; @Injectable({ providedIn: 'root', }) -export class ConfigurationApi {} +export class ConfigurationApi { + private readonly httpClient = inject(HttpClient); + + fetch(): Observable<Configuration> { + return this.httpClient + .get<Configuration>(CONFIGURATION_API_URL) + .pipe(map(dto => dtoToConfiguration(dto))); + } + + save(configuration: Configuration): Observable<Configuration> { + return this.httpClient + .put<Configuration>( + CONFIGURATION_API_URL, + configurationToDto(configuration) + ) + .pipe(map(dto => dtoToConfiguration(dto))); + } +} diff --git a/src/app/models/.gitkeep b/src/app/models/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/models/configuration.ts b/src/app/models/configuration.ts new file mode 100644 index 0000000..c908851 --- /dev/null +++ b/src/app/models/configuration.ts @@ -0,0 +1,21 @@ +import { FrameRange } from './frame-range'; +import { Protocol } from './protocol'; + +export type Configuration = ConfigurationDto; + +export interface ConfigurationDto { + mac_source: string[]; + mac_destination: string[]; + frame_range: FrameRange[]; + protocol: Protocol[]; +} + +export function dtoToConfiguration(dto: ConfigurationDto): Configuration { + return dto; +} + +export function configurationToDto( + configuration: Configuration +): ConfigurationDto { + return configuration; +} diff --git a/src/app/models/frame-range.ts b/src/app/models/frame-range.ts new file mode 100644 index 0000000..bbcb1e4 --- /dev/null +++ b/src/app/models/frame-range.ts @@ -0,0 +1,10 @@ +const FrameRange: { [key: string]: [number, number] } = { + UNDER_63: [0, 63], + FROM_64_TO_127: [64, 127], + FROM_128_TO_255: [128, 255], + FROM_256_TO_511: [256, 511], + FROM_512_TO_1023: [512, 1023], + FROM_1024_TO_1518: [1024, 1518], + ABOVE_1518: [1519, Infinity], +}; +export type FrameRange = (typeof FrameRange)[keyof typeof FrameRange]; diff --git a/src/app/models/protocol.ts b/src/app/models/protocol.ts new file mode 100644 index 0000000..bbc040f --- /dev/null +++ b/src/app/models/protocol.ts @@ -0,0 +1,8 @@ +export enum Protocol { + IPv4 = 'IPv4', + IPv6 = 'IPv6', + ARP = 'ARP', + ICMP = 'ICMP', + TCP = 'TCP', + UDP = 'UDP', +} -- GitLab From 90ea171ae6665c1b4a65e91e577c2f0eb5d348ff Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Sat, 15 Feb 2025 22:49:16 +0100 Subject: [PATCH 02/13] FE-3 Add components for configuration page --- src/app/api/configuration.api.ts | 10 ++-- src/app/app.config.ts | 5 +- .../components/button/button.component.html | 5 ++ .../components/button/button.component.scss | 19 ++++++++ .../button/button.component.spec.ts | 23 +++++++++ src/app/components/button/button.component.ts | 13 +++++ .../configuration-form.builder.ts | 0 .../configuration-form.component.html | 6 +++ .../configuration-form.component.scss | 0 .../configuration-form.component.spec.ts | 23 +++++++++ .../configuration-form.component.ts | 10 ++++ .../configuration-template.component.html | 22 +++++++++ .../configuration-template.component.scss | 29 ++++++++++++ .../configuration-template.component.spec.ts | 23 +++++++++ .../configuration-template.component.ts | 10 ++++ .../configuration.component.html | 47 +++++++++++++++++-- .../configuration.component.scss | 8 ++++ .../configuration/configuration.component.ts | 28 +++++++++-- .../components/header/header.component.scss | 2 +- .../page-wrapper/page-wrapper.component.html | 5 +- .../page-wrapper/page-wrapper.component.scss | 17 ++++--- src/app/components/pill/pill.component.html | 6 +++ src/app/components/pill/pill.component.scss | 11 +++++ .../components/pill/pill.component.spec.ts | 23 +++++++++ src/app/components/pill/pill.component.ts | 12 +++++ src/app/models/configuration.ts | 19 ++++++-- src/app/models/frame-range.ts | 10 ---- src/app/models/protocol.ts | 8 ---- src/vars.scss | 1 + 29 files changed, 353 insertions(+), 42 deletions(-) create mode 100644 src/app/components/button/button.component.html create mode 100644 src/app/components/button/button.component.scss create mode 100644 src/app/components/button/button.component.spec.ts create mode 100644 src/app/components/button/button.component.ts create mode 100644 src/app/components/configuration-form/configuration-form.builder.ts create mode 100644 src/app/components/configuration-form/configuration-form.component.html create mode 100644 src/app/components/configuration-form/configuration-form.component.scss create mode 100644 src/app/components/configuration-form/configuration-form.component.spec.ts create mode 100644 src/app/components/configuration-form/configuration-form.component.ts create mode 100644 src/app/components/configuration-template/configuration-template.component.html create mode 100644 src/app/components/configuration-template/configuration-template.component.scss create mode 100644 src/app/components/configuration-template/configuration-template.component.spec.ts create mode 100644 src/app/components/configuration-template/configuration-template.component.ts create mode 100644 src/app/components/pill/pill.component.html create mode 100644 src/app/components/pill/pill.component.scss create mode 100644 src/app/components/pill/pill.component.spec.ts create mode 100644 src/app/components/pill/pill.component.ts delete mode 100644 src/app/models/frame-range.ts delete mode 100644 src/app/models/protocol.ts diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts index 1c7a9ce..f26de7a 100644 --- a/src/app/api/configuration.api.ts +++ b/src/app/api/configuration.api.ts @@ -1,8 +1,9 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { map, Observable } from 'rxjs'; +import { delay, map, Observable, of } from 'rxjs'; import { Configuration, + configurationMock, configurationToDto, dtoToConfiguration, } from '../models/configuration'; @@ -16,9 +17,10 @@ export class ConfigurationApi { private readonly httpClient = inject(HttpClient); fetch(): Observable<Configuration> { - return this.httpClient - .get<Configuration>(CONFIGURATION_API_URL) - .pipe(map(dto => dtoToConfiguration(dto))); + // return this.httpClient + // .get<Configuration>(CONFIGURATION_API_URL) + // .pipe(map(dto => dtoToConfiguration(dto))); + return of(configurationMock).pipe(delay(500)); } save(configuration: Configuration): Observable<Configuration> { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 35ca21e..2835fc6 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -4,12 +4,15 @@ import { provideAnimationsAsync } from '@angular/platform-browser/animations/asy import { providePrimeNG } from 'primeng/config'; import { routes } from './app.routes'; +import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), - providePrimeNG(), provideAnimationsAsync(), + provideHttpClient(), + providePrimeNG(), + provideAnimationsAsync(), ], }; diff --git a/src/app/components/button/button.component.html b/src/app/components/button/button.component.html new file mode 100644 index 0000000..5661607 --- /dev/null +++ b/src/app/components/button/button.component.html @@ -0,0 +1,5 @@ +<button + [ngClass]="{ 'button-secondary': secondary() !== null }" + mat-flat-button> + <ng-content /> +</button> diff --git a/src/app/components/button/button.component.scss b/src/app/components/button/button.component.scss new file mode 100644 index 0000000..40311a0 --- /dev/null +++ b/src/app/components/button/button.component.scss @@ -0,0 +1,19 @@ +@use '../../../vars'; +@use 'sass:map'; + +:host { + button { + text-transform: uppercase; + font-size: map.get(vars.$text, 'md'); + padding: map.get(vars.$spacing, 'xl') map.get(vars.$spacing, 'xxxl'); + } + + * { + background-color: vars.$textPrimary; + } + + .button-secondary { + color: vars.$textPrimary; + background-color: map.get(vars.$grey, 20); + } +} diff --git a/src/app/components/button/button.component.spec.ts b/src/app/components/button/button.component.spec.ts new file mode 100644 index 0000000..15e6373 --- /dev/null +++ b/src/app/components/button/button.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ButtonComponent } from './button.component'; + +describe('ButtonComponent', () => { + let component: ButtonComponent; + let fixture: ComponentFixture<ButtonComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ButtonComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/button/button.component.ts b/src/app/components/button/button.component.ts new file mode 100644 index 0000000..cc2d400 --- /dev/null +++ b/src/app/components/button/button.component.ts @@ -0,0 +1,13 @@ +import { Component, input } from '@angular/core'; +import { MatButton } from '@angular/material/button'; +import { NgClass } from '@angular/common'; + +@Component({ + selector: 'app-button', + imports: [MatButton, NgClass], + templateUrl: './button.component.html', + styleUrl: './button.component.scss', +}) +export class ButtonComponent { + secondary = input<string | null>(null); +} diff --git a/src/app/components/configuration-form/configuration-form.builder.ts b/src/app/components/configuration-form/configuration-form.builder.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/configuration-form/configuration-form.component.html b/src/app/components/configuration-form/configuration-form.component.html new file mode 100644 index 0000000..cf46ab7 --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.component.html @@ -0,0 +1,6 @@ +<app-configuration-template> + <div mac-source>editable Mac source</div> + <div mac-destination>editable Mac destination</div> + <div frames>editable Frames</div> + <div protocols>editable Protocols</div> +</app-configuration-template> diff --git a/src/app/components/configuration-form/configuration-form.component.scss b/src/app/components/configuration-form/configuration-form.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/configuration-form/configuration-form.component.spec.ts b/src/app/components/configuration-form/configuration-form.component.spec.ts new file mode 100644 index 0000000..ea53535 --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfigurationFormComponent } from './configuration-form.component'; + +describe('ConfigurationFormComponent', () => { + let component: ConfigurationFormComponent; + let fixture: ComponentFixture<ConfigurationFormComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfigurationFormComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConfigurationFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts new file mode 100644 index 0000000..3151838 --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; + +@Component({ + selector: 'app-configuration-form', + imports: [ConfigurationTemplateComponent], + templateUrl: './configuration-form.component.html', + styleUrl: './configuration-form.component.scss', +}) +export class ConfigurationFormComponent {} diff --git a/src/app/components/configuration-template/configuration-template.component.html b/src/app/components/configuration-template/configuration-template.component.html new file mode 100644 index 0000000..d706265 --- /dev/null +++ b/src/app/components/configuration-template/configuration-template.component.html @@ -0,0 +1,22 @@ +<div class="configuration-section"> + <div class="configuration-section__title">MAC address</div> + <div class="configuration-section__card"> + <ng-content select="[mac-source]" /> + <mat-divider /> + <ng-content select="[mac-destination]" /> + </div> +</div> + +<div class="configuration-section"> + <div class="configuration-section__title">Frame sizes</div> + <div class="configuration-section__card"> + <ng-content select="[frames]" /> + </div> +</div> + +<div class="configuration-section"> + <div class="configuration-section__title">Protocols</div> + <div class="configuration-section__card"> + <ng-content select="[protocols]" /> + </div> +</div> diff --git a/src/app/components/configuration-template/configuration-template.component.scss b/src/app/components/configuration-template/configuration-template.component.scss new file mode 100644 index 0000000..fcdfe78 --- /dev/null +++ b/src/app/components/configuration-template/configuration-template.component.scss @@ -0,0 +1,29 @@ +@use '../../../vars'; +@use 'sass:map'; + +:host { + display: flex; + flex-direction: column; + gap: map.get(vars.$spacing, 'lg'); + + .configuration-section { + display: flex; + flex-direction: column; + gap: map.get(vars.$spacing, 'xs'); + + &__title { + font-weight: bold; + color: map.get(vars.$grey, 100); + font-size: map.get(vars.$text, 'lg'); + } + + &__card { + display: flex; + flex-direction: column; + gap: map.get(vars.$spacing, 'md'); + padding: map.get(vars.$spacing, 'lg'); + border-radius: map.get(vars.$radius, 'sm'); + background-color: map.get(vars.$grey, 10); + } + } +} diff --git a/src/app/components/configuration-template/configuration-template.component.spec.ts b/src/app/components/configuration-template/configuration-template.component.spec.ts new file mode 100644 index 0000000..71a11af --- /dev/null +++ b/src/app/components/configuration-template/configuration-template.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfigurationTemplateComponent } from './configuration-template.component'; + +describe('ConfigurationTemplateComponent', () => { + let component: ConfigurationTemplateComponent; + let fixture: ComponentFixture<ConfigurationTemplateComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConfigurationTemplateComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConfigurationTemplateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/configuration-template/configuration-template.component.ts b/src/app/components/configuration-template/configuration-template.component.ts new file mode 100644 index 0000000..d6f45a2 --- /dev/null +++ b/src/app/components/configuration-template/configuration-template.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { MatDivider } from '@angular/material/divider'; + +@Component({ + selector: 'app-configuration-template', + imports: [MatDivider], + templateUrl: './configuration-template.component.html', + styleUrl: './configuration-template.component.scss', +}) +export class ConfigurationTemplateComponent {} diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index 8434b8c..8948d28 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -1,6 +1,47 @@ <app-page-wrapper> - <div header>Analyzer configuration</div> + <div title>Analyzer configuration</div> - Configuration content goes here - <p-skeleton height="10rem" width="100%" /> + <div actions class="actions"> + @if (isReadonly()) { + <app-button (click)="isReadonly.set(false)">Edit</app-button> + } @else { + <app-button secondary (click)="isReadonly.set(true)">Cancel</app-button> + <app-button>Save</app-button> + } + </div> + + @if (configurationRes.isLoading()) { + Loading... + } @else { + @if (isReadonly()) { + @let configuration = configurationRes.value()!; + + <app-configuration-template> + <div mac-source> + Source: + @for (mac of configuration?.mac_source; track $index) { + <app-pill>{{ mac }}</app-pill> + } + </div> + <div mac-destination> + Destination: + @for (mac of configuration?.mac_destination; track $index) { + <app-pill>{{ mac }}</app-pill> + } + </div> + <div frames> + @for (frame of configuration?.frame_range; track $index) { + <app-pill>{{ frame }}</app-pill> + } + </div> + <div protocols> + @for (protocol of configuration?.protocol; track $index) { + <app-pill>{{ protocol }}</app-pill> + } + </div> + </app-configuration-template> + } @else { + <app-configuration-form /> + } + } </app-page-wrapper> diff --git a/src/app/components/configuration/configuration.component.scss b/src/app/components/configuration/configuration.component.scss index 2995308..6361cd3 100644 --- a/src/app/components/configuration/configuration.component.scss +++ b/src/app/components/configuration/configuration.component.scss @@ -1,4 +1,12 @@ +@use '../../../vars'; +@use 'sass:map'; + :host { height: 100%; width: 100%; + + .actions { + display: flex; + gap: map.get(vars.$spacing, 'md'); + } } diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index 6cde6f1..e4185ff 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -1,11 +1,33 @@ -import { Component } from '@angular/core'; +import { Component, inject, resource, signal } from '@angular/core'; import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component'; +import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; +import { ConfigurationFormComponent } from '../configuration-form/configuration-form.component'; +import { ButtonComponent } from '../button/button.component'; +import { ConfigurationApi } from '../../api/configuration.api'; +import { firstValueFrom } from 'rxjs'; +import { PillComponent } from '../pill/pill.component'; import { Skeleton } from 'primeng/skeleton'; @Component({ selector: 'app-configuration', - imports: [PageWrapperComponent, Skeleton], + imports: [ + PageWrapperComponent, + ConfigurationTemplateComponent, + ConfigurationFormComponent, + ButtonComponent, + PillComponent, + Skeleton, + ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', }) -export class ConfigurationComponent {} +export class ConfigurationComponent { + configurationApi = inject(ConfigurationApi); + + isReadonly = signal<boolean>(true); + configurationRes = resource({ + loader: async () => { + return await firstValueFrom(this.configurationApi.fetch()); + }, + }); +} diff --git a/src/app/components/header/header.component.scss b/src/app/components/header/header.component.scss index c5a236d..194ccfa 100644 --- a/src/app/components/header/header.component.scss +++ b/src/app/components/header/header.component.scss @@ -45,7 +45,7 @@ } } - & > * { + * { color: vars.$textPrimary; } } diff --git a/src/app/components/page-wrapper/page-wrapper.component.html b/src/app/components/page-wrapper/page-wrapper.component.html index 60cb6e2..fae4904 100644 --- a/src/app/components/page-wrapper/page-wrapper.component.html +++ b/src/app/components/page-wrapper/page-wrapper.component.html @@ -1,5 +1,8 @@ <div class="page-header"> - <ng-content select="[header]"></ng-content> + <div class="page-header__title"> + <ng-content select="[title]"></ng-content> + </div> + <ng-content select="[actions]"></ng-content> </div> <mat-divider /> <div class="page-body"> diff --git a/src/app/components/page-wrapper/page-wrapper.component.scss b/src/app/components/page-wrapper/page-wrapper.component.scss index 1a83647..ab5c237 100644 --- a/src/app/components/page-wrapper/page-wrapper.component.scss +++ b/src/app/components/page-wrapper/page-wrapper.component.scss @@ -5,21 +5,26 @@ display: block; min-height: calc(100% - 2 * map.get(vars.$spacing, 'lg')); - margin: map.get(vars.$spacing, 'lg'); + margin: map.get(vars.$spacing, 'xl'); background-color: map.get(vars.$grey, 0); border-radius: map.get(vars.$radius, 'xs'); .page { &-header { - padding: map.get(vars.$spacing, 'lg') map.get(vars.$spacing, 'lg') + display: flex; + justify-content: space-between; + align-items: center; + padding: map.get(vars.$spacing, 'lg') map.get(vars.$spacing, 'xxl') map.get(vars.$spacing, 'xs'); - font-family: Monoton, cursive; - font-size: map.get(vars.$text, 'xxl'); + + &__title { + font-family: Monoton, cursive; + font-size: map.get(vars.$text, 'xxl'); + } } &-body { - padding: map.get(vars.$spacing, 'xl') map.get(vars.$spacing, 'lg') - map.get(vars.$spacing, 'lg'); + padding: map.get(vars.$spacing, 'xxl'); } } } diff --git a/src/app/components/pill/pill.component.html b/src/app/components/pill/pill.component.html new file mode 100644 index 0000000..b4b2f25 --- /dev/null +++ b/src/app/components/pill/pill.component.html @@ -0,0 +1,6 @@ +<div class="pill"> + <ng-content /> + @if (removable()) { + <mat-icon>remove</mat-icon> + } +</div> diff --git a/src/app/components/pill/pill.component.scss b/src/app/components/pill/pill.component.scss new file mode 100644 index 0000000..8bc0840 --- /dev/null +++ b/src/app/components/pill/pill.component.scss @@ -0,0 +1,11 @@ +@use '../../../vars'; +@use 'sass:map'; + +.pill { + display: flex; + flex-direction: row; + gap: map.get(vars.$spacing, 'sm'); + border-radius: map.get(vars.$radius, 'md'); + border: 1px solid map.get(vars.$grey, 200); + background-color: map.get(vars.$grey, 0); +} diff --git a/src/app/components/pill/pill.component.spec.ts b/src/app/components/pill/pill.component.spec.ts new file mode 100644 index 0000000..5eb1cd0 --- /dev/null +++ b/src/app/components/pill/pill.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PillComponent } from './pill.component'; + +describe('PillComponent', () => { + let component: PillComponent; + let fixture: ComponentFixture<PillComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PillComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PillComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/pill/pill.component.ts b/src/app/components/pill/pill.component.ts new file mode 100644 index 0000000..ccc77c2 --- /dev/null +++ b/src/app/components/pill/pill.component.ts @@ -0,0 +1,12 @@ +import { Component, input } from '@angular/core'; +import { MatIcon } from '@angular/material/icon'; + +@Component({ + selector: 'app-pill', + imports: [MatIcon], + templateUrl: './pill.component.html', + styleUrl: './pill.component.scss', +}) +export class PillComponent { + removable = input<string | null>(null); +} diff --git a/src/app/models/configuration.ts b/src/app/models/configuration.ts index c908851..0a1fe56 100644 --- a/src/app/models/configuration.ts +++ b/src/app/models/configuration.ts @@ -1,13 +1,10 @@ -import { FrameRange } from './frame-range'; -import { Protocol } from './protocol'; - export type Configuration = ConfigurationDto; export interface ConfigurationDto { mac_source: string[]; mac_destination: string[]; - frame_range: FrameRange[]; - protocol: Protocol[]; + frame_range: [number, number][]; + protocol: string[]; } export function dtoToConfiguration(dto: ConfigurationDto): Configuration { @@ -19,3 +16,15 @@ export function configurationToDto( ): ConfigurationDto { return configuration; } + +// @todo: delete +export const configurationMock: Configuration = { + mac_source: ['01:23:45:67:89:ab'], + mac_destination: ['fe:dc:ba:98:76:54'], + frame_range: [ + [0, 63], + [128, 255], + [1024, 1518], + ], + protocol: ['IPv4', 'IP6', 'UDP'], +}; diff --git a/src/app/models/frame-range.ts b/src/app/models/frame-range.ts deleted file mode 100644 index bbcb1e4..0000000 --- a/src/app/models/frame-range.ts +++ /dev/null @@ -1,10 +0,0 @@ -const FrameRange: { [key: string]: [number, number] } = { - UNDER_63: [0, 63], - FROM_64_TO_127: [64, 127], - FROM_128_TO_255: [128, 255], - FROM_256_TO_511: [256, 511], - FROM_512_TO_1023: [512, 1023], - FROM_1024_TO_1518: [1024, 1518], - ABOVE_1518: [1519, Infinity], -}; -export type FrameRange = (typeof FrameRange)[keyof typeof FrameRange]; diff --git a/src/app/models/protocol.ts b/src/app/models/protocol.ts deleted file mode 100644 index bbc040f..0000000 --- a/src/app/models/protocol.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum Protocol { - IPv4 = 'IPv4', - IPv6 = 'IPv6', - ARP = 'ARP', - ICMP = 'ICMP', - TCP = 'TCP', - UDP = 'UDP', -} diff --git a/src/vars.scss b/src/vars.scss index 4cdf01f..9a4c50c 100644 --- a/src/vars.scss +++ b/src/vars.scss @@ -9,6 +9,7 @@ $grey: ( 20: #f3eaea, 30: #dddddd, 50: #c5c5c5, + 100: #686868, 200: #181818, 500: #000000, ); -- GitLab From 00943da5ab7791daf234fe9cb4a158e431098410 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Tue, 18 Feb 2025 14:56:42 +0100 Subject: [PATCH 03/13] FE-3 Minor refactor --- .../configuration-form.component.ts | 7 +++++-- .../configuration/configuration.component.html | 7 ++++--- .../configuration/configuration.component.ts | 1 + .../components/dashboard/dashboard.component.html | 3 ++- src/app/components/sidenav/sidenav.component.ts | 13 +++++++++++-- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts index 3151838..e062fc9 100644 --- a/src/app/components/configuration-form/configuration-form.component.ts +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -1,5 +1,6 @@ -import { Component } from '@angular/core'; +import { Component, input } from '@angular/core'; import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; +import { Configuration } from '../../models/configuration'; @Component({ selector: 'app-configuration-form', @@ -7,4 +8,6 @@ import { ConfigurationTemplateComponent } from '../configuration-template/config templateUrl: './configuration-form.component.html', styleUrl: './configuration-form.component.scss', }) -export class ConfigurationFormComponent {} +export class ConfigurationFormComponent { + configuration = input.required<Configuration>(); +} diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index 8948d28..6169cc8 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -12,10 +12,11 @@ @if (configurationRes.isLoading()) { Loading... + <!-- @todo: ??? it's empty for some reason --> + <p-skeleton height="10rem" /> } @else { + @let configuration = configurationRes.value()!; @if (isReadonly()) { - @let configuration = configurationRes.value()!; - <app-configuration-template> <div mac-source> Source: @@ -41,7 +42,7 @@ </div> </app-configuration-template> } @else { - <app-configuration-form /> + <app-configuration-form [configuration]="configuration" /> } } </app-page-wrapper> diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index e4185ff..9c9815b 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -7,6 +7,7 @@ import { ConfigurationApi } from '../../api/configuration.api'; import { firstValueFrom } from 'rxjs'; import { PillComponent } from '../pill/pill.component'; import { Skeleton } from 'primeng/skeleton'; +import { Configuration } from '../../models/configuration'; @Component({ selector: 'app-configuration', diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index cb0f201..d8bafe6 100644 --- a/src/app/components/dashboard/dashboard.component.html +++ b/src/app/components/dashboard/dashboard.component.html @@ -1,5 +1,6 @@ <app-page-wrapper> - <div header>Monitoring and Analysis</div> + <div title>Monitoring and Analysis</div> + <div actions>actions</div> Dashboard content goes here <p-skeleton height="10rem" width="100%" /> diff --git a/src/app/components/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts index 2fd9c08..a630fe2 100644 --- a/src/app/components/sidenav/sidenav.component.ts +++ b/src/app/components/sidenav/sidenav.component.ts @@ -1,5 +1,11 @@ -import { afterNextRender, Component, inject, signal } from '@angular/core'; -import { MatSidenavModule } from '@angular/material/sidenav'; +import { + afterNextRender, + Component, + inject, + signal, + viewChild, +} from '@angular/core'; +import { MatDrawer, MatSidenavModule } from '@angular/material/sidenav'; import { MatButtonModule } from '@angular/material/button'; import { NavigationEnd, Router, RouterLink } from '@angular/router'; import { Button } from 'primeng/button'; @@ -23,6 +29,8 @@ export class SidenavComponent { router = inject(Router); currentPage = signal<string>(''); + drawer = viewChild<MatDrawer>('drawer'); + constructor() { afterNextRender(() => { this.router.events.subscribe(event => { @@ -30,6 +38,7 @@ export class SidenavComponent { this.currentPage.set(event.url); } }); + this.drawer()?.open(); }); } } -- GitLab From b1e7b645aadb577d46ac15daf5a6d7c6af1decc8 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Wed, 19 Feb 2025 21:15:15 +0100 Subject: [PATCH 04/13] FE-3 Add skeletons and pill styles --- package-lock.json | 13 ++++++ package.json | 1 + .../configuration.component.html | 43 +++++++++++++------ .../configuration.component.scss | 26 +++++++++++ .../configuration/configuration.component.ts | 7 +-- src/app/components/pill/pill.component.scss | 8 +++- src/app/models/configuration.ts | 11 ++++- 7 files changed, 89 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 347cf05..e00ab1f 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", + "ngx-skeleton-loader": "^10.0.0", "primeicons": "^7.0.0", "primeng": "^19.0.6", "rxjs": "~7.8.0", @@ -9746,6 +9747,18 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/ngx-skeleton-loader": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-10.0.0.tgz", + "integrity": "sha512-TYrWLrdRtzoZoPzurNDUJdAbdyplqgyDztCefEi+clHl5MSumwG4NrGxZC1OVxz7RitomhnF7wTM8T/j+tdwXw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=18.0.0", + "@angular/core": ">=18.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", diff --git a/package.json b/package.json index 19bc81a..aa94cd0 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", + "ngx-skeleton-loader": "^10.0.0", "primeicons": "^7.0.0", "primeng": "^19.0.6", "rxjs": "~7.8.0", diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index 6169cc8..68cbe45 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -11,28 +11,43 @@ </div> @if (configurationRes.isLoading()) { - Loading... - <!-- @todo: ??? it's empty for some reason --> - <p-skeleton height="10rem" /> + <app-configuration-template> + <div mac-source> + <ngx-skeleton-loader count="5" /> + </div> + <div mac-destination> + <ngx-skeleton-loader count="5" /> + </div> + <div frames> + <ngx-skeleton-loader count="5" /> + </div> + <div protocols> + <ngx-skeleton-loader count="5" /> + </div> + </app-configuration-template> } @else { @let configuration = configurationRes.value()!; @if (isReadonly()) { <app-configuration-template> - <div mac-source> - Source: - @for (mac of configuration?.mac_source; track $index) { - <app-pill>{{ mac }}</app-pill> - } + <div mac-source class="configuration__mac"> + <div class="configuration__mac-title">Source:</div> + <div class="configuration__mac-content"> + @for (mac of configuration?.mac_source; track $index) { + <app-pill>{{ mac }}</app-pill> + } + </div> </div> - <div mac-destination> - Destination: - @for (mac of configuration?.mac_destination; track $index) { - <app-pill>{{ mac }}</app-pill> - } + <div mac-destination class="configuration__mac"> + <div class="configuration__mac-title">Destination:</div> + <div class="configuration__mac-content"> + @for (mac of configuration?.mac_destination; track $index) { + <app-pill>{{ mac }}</app-pill> + } + </div> </div> <div frames> @for (frame of configuration?.frame_range; track $index) { - <app-pill>{{ frame }}</app-pill> + <app-pill>{{ frame[0] }} — {{ frame[1] }}</app-pill> } </div> <div protocols> diff --git a/src/app/components/configuration/configuration.component.scss b/src/app/components/configuration/configuration.component.scss index 6361cd3..ea6eaa7 100644 --- a/src/app/components/configuration/configuration.component.scss +++ b/src/app/components/configuration/configuration.component.scss @@ -9,4 +9,30 @@ display: flex; gap: map.get(vars.$spacing, 'md'); } + + ngx-skeleton-loader { + display: flex; + flex-direction: row; + gap: map.get(vars.$spacing, 'md'); + + ::ng-deep * { + height: 50px; + } + } + + .configuration { + &__mac { + display: flex; + justify-content: space-between; + + &-title { + color: map.get(vars.$grey, 100); + font-size: map.get(vars.$text, 'lg'); + } + + &-content { + width: calc(100% - 200px); + } + } + } } diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index 9c9815b..f6a69f8 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -6,8 +6,8 @@ import { ButtonComponent } from '../button/button.component'; import { ConfigurationApi } from '../../api/configuration.api'; import { firstValueFrom } from 'rxjs'; import { PillComponent } from '../pill/pill.component'; -import { Skeleton } from 'primeng/skeleton'; -import { Configuration } from '../../models/configuration'; +import { SkeletonModule } from 'primeng/skeleton'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ selector: 'app-configuration', @@ -17,7 +17,8 @@ import { Configuration } from '../../models/configuration'; ConfigurationFormComponent, ButtonComponent, PillComponent, - Skeleton, + SkeletonModule, + NgxSkeletonLoaderModule, ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', diff --git a/src/app/components/pill/pill.component.scss b/src/app/components/pill/pill.component.scss index 8bc0840..63fb4f4 100644 --- a/src/app/components/pill/pill.component.scss +++ b/src/app/components/pill/pill.component.scss @@ -2,10 +2,14 @@ @use 'sass:map'; .pill { - display: flex; + display: inline-flex; flex-direction: row; + justify-content: center; gap: map.get(vars.$spacing, 'sm'); border-radius: map.get(vars.$radius, 'md'); - border: 1px solid map.get(vars.$grey, 200); + border: 1px solid map.get(vars.$grey, 50); background-color: map.get(vars.$grey, 0); + color: map.get(vars.$grey, 100); + padding: map.get(vars.$spacing, 'xs') map.get(vars.$spacing, 'lg'); + margin: map.get(vars.$spacing, 'xs'); } diff --git a/src/app/models/configuration.ts b/src/app/models/configuration.ts index 0a1fe56..e1b1473 100644 --- a/src/app/models/configuration.ts +++ b/src/app/models/configuration.ts @@ -19,7 +19,16 @@ export function configurationToDto( // @todo: delete export const configurationMock: Configuration = { - mac_source: ['01:23:45:67:89:ab'], + mac_source: [ + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + '01:23:45:67:89:ab', + ], mac_destination: ['fe:dc:ba:98:76:54'], frame_range: [ [0, 63], -- GitLab From e77dfa761b80a080dd9663d1fb2510460a729cd2 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Wed, 19 Feb 2025 21:18:10 +0100 Subject: [PATCH 05/13] FE-3 Remove unused specs --- src/app/app.component.spec.ts | 29 ------------------- .../button/button.component.spec.ts | 23 --------------- .../configuration-form.component.spec.ts | 23 --------------- .../configuration-template.component.spec.ts | 23 --------------- .../configuration.component.spec.ts | 23 --------------- .../dashboard/dashboard.component.spec.ts | 23 --------------- .../header/header.component.spec.ts | 23 --------------- .../not-found/not-found.component.spec.ts | 23 --------------- .../page-wrapper.component.spec.ts | 23 --------------- .../components/pill/pill.component.spec.ts | 23 --------------- .../sidenav/sidenav.component.spec.ts | 23 --------------- 11 files changed, 259 deletions(-) delete mode 100644 src/app/app.component.spec.ts delete mode 100644 src/app/components/button/button.component.spec.ts delete mode 100644 src/app/components/configuration-form/configuration-form.component.spec.ts delete mode 100644 src/app/components/configuration-template/configuration-template.component.spec.ts delete mode 100644 src/app/components/configuration/configuration.component.spec.ts delete mode 100644 src/app/components/dashboard/dashboard.component.spec.ts delete mode 100644 src/app/components/header/header.component.spec.ts delete mode 100644 src/app/components/not-found/not-found.component.spec.ts delete mode 100644 src/app/components/page-wrapper/page-wrapper.component.spec.ts delete mode 100644 src/app/components/pill/pill.component.spec.ts delete mode 100644 src/app/components/sidenav/sidenav.component.spec.ts diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts deleted file mode 100644 index 20a9c43..0000000 --- a/src/app/app.component.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; - -describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AppComponent], - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app).toBeTruthy(); - }); - - it(`should have the 'miars-frontend' title`, () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.componentInstance; - expect(app.title).toEqual('miars-frontend'); - }); - - it('should render title', () => { - const fixture = TestBed.createComponent(AppComponent); - fixture.detectChanges(); - const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, miars-frontend'); - }); -}); diff --git a/src/app/components/button/button.component.spec.ts b/src/app/components/button/button.component.spec.ts deleted file mode 100644 index 15e6373..0000000 --- a/src/app/components/button/button.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ButtonComponent } from './button.component'; - -describe('ButtonComponent', () => { - let component: ButtonComponent; - let fixture: ComponentFixture<ButtonComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ButtonComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(ButtonComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/configuration-form/configuration-form.component.spec.ts b/src/app/components/configuration-form/configuration-form.component.spec.ts deleted file mode 100644 index ea53535..0000000 --- a/src/app/components/configuration-form/configuration-form.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConfigurationFormComponent } from './configuration-form.component'; - -describe('ConfigurationFormComponent', () => { - let component: ConfigurationFormComponent; - let fixture: ComponentFixture<ConfigurationFormComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ConfigurationFormComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(ConfigurationFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/configuration-template/configuration-template.component.spec.ts b/src/app/components/configuration-template/configuration-template.component.spec.ts deleted file mode 100644 index 71a11af..0000000 --- a/src/app/components/configuration-template/configuration-template.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConfigurationTemplateComponent } from './configuration-template.component'; - -describe('ConfigurationTemplateComponent', () => { - let component: ConfigurationTemplateComponent; - let fixture: ComponentFixture<ConfigurationTemplateComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ConfigurationTemplateComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(ConfigurationTemplateComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/configuration/configuration.component.spec.ts b/src/app/components/configuration/configuration.component.spec.ts deleted file mode 100644 index a719df8..0000000 --- a/src/app/components/configuration/configuration.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConfigurationComponent } from './configuration.component'; - -describe('ConfigurationComponent', () => { - let component: ConfigurationComponent; - let fixture: ComponentFixture<ConfigurationComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ConfigurationComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(ConfigurationComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/dashboard/dashboard.component.spec.ts b/src/app/components/dashboard/dashboard.component.spec.ts deleted file mode 100644 index 30e39a2..0000000 --- a/src/app/components/dashboard/dashboard.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DashboardComponent } from './dashboard.component'; - -describe('DashboardComponent', () => { - let component: DashboardComponent; - let fixture: ComponentFixture<DashboardComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DashboardComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(DashboardComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/header/header.component.spec.ts b/src/app/components/header/header.component.spec.ts deleted file mode 100644 index 204ed6e..0000000 --- a/src/app/components/header/header.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HeaderComponent } from './header.component'; - -describe('HeaderComponent', () => { - let component: HeaderComponent; - let fixture: ComponentFixture<HeaderComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [HeaderComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(HeaderComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/not-found/not-found.component.spec.ts b/src/app/components/not-found/not-found.component.spec.ts deleted file mode 100644 index 5b65d9e..0000000 --- a/src/app/components/not-found/not-found.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { NotFoundComponent } from './not-found.component'; - -describe('NotFoundComponent', () => { - let component: NotFoundComponent; - let fixture: ComponentFixture<NotFoundComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [NotFoundComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(NotFoundComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/page-wrapper/page-wrapper.component.spec.ts b/src/app/components/page-wrapper/page-wrapper.component.spec.ts deleted file mode 100644 index 73525cc..0000000 --- a/src/app/components/page-wrapper/page-wrapper.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PageWrapperComponent } from './page-wrapper.component'; - -describe('PageWrapperComponent', () => { - let component: PageWrapperComponent; - let fixture: ComponentFixture<PageWrapperComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PageWrapperComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(PageWrapperComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/pill/pill.component.spec.ts b/src/app/components/pill/pill.component.spec.ts deleted file mode 100644 index 5eb1cd0..0000000 --- a/src/app/components/pill/pill.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PillComponent } from './pill.component'; - -describe('PillComponent', () => { - let component: PillComponent; - let fixture: ComponentFixture<PillComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PillComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(PillComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/components/sidenav/sidenav.component.spec.ts b/src/app/components/sidenav/sidenav.component.spec.ts deleted file mode 100644 index 25f799c..0000000 --- a/src/app/components/sidenav/sidenav.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SidenavComponent } from './sidenav.component'; - -describe('SidenavComponent', () => { - let component: SidenavComponent; - let fixture: ComponentFixture<SidenavComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [SidenavComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(SidenavComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); -- GitLab From 374e11000c821a6cc1d84df18391735977bb93e6 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Wed, 19 Feb 2025 22:43:15 +0100 Subject: [PATCH 06/13] FE-3 Improve api mocking; Add configuration profile selection --- src/app/api/configuration.api.ts | 31 ++++++++----- src/app/app.config.ts | 5 ++- .../configuration-template.component.html | 3 ++ .../configuration-template.component.scss | 34 ++++++++------ .../configuration.component.html | 29 +++++++++--- .../configuration.component.scss | 6 +++ .../configuration/configuration.component.ts | 44 +++++++++++++++++-- src/app/interceptor/interceptor.ts | 18 ++++++++ src/app/interceptor/mock-interceptor.ts | 17 +++++++ .../interceptor/mocks/configuration-1.json | 24 ++++++++++ .../interceptor/mocks/configuration-2.json | 11 +++++ src/app/interceptor/mocks/configurations.json | 35 +++++++++++++++ src/app/models/api-response.ts | 3 ++ src/app/models/configuration.ts | 24 ++-------- 14 files changed, 225 insertions(+), 59 deletions(-) create mode 100644 src/app/interceptor/interceptor.ts create mode 100644 src/app/interceptor/mock-interceptor.ts create mode 100644 src/app/interceptor/mocks/configuration-1.json create mode 100644 src/app/interceptor/mocks/configuration-2.json create mode 100644 src/app/interceptor/mocks/configurations.json create mode 100644 src/app/models/api-response.ts diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts index f26de7a..7ed09c6 100644 --- a/src/app/api/configuration.api.ts +++ b/src/app/api/configuration.api.ts @@ -1,12 +1,13 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { delay, map, Observable, of } from 'rxjs'; +import { map, Observable, tap } from 'rxjs'; import { Configuration, - configurationMock, + ConfigurationDto, configurationToDto, dtoToConfiguration, } from '../models/configuration'; +import { ApiResponse } from '../models/api-response'; export const CONFIGURATION_API_URL = '/api/v1/configuration'; @@ -16,19 +17,25 @@ export const CONFIGURATION_API_URL = '/api/v1/configuration'; export class ConfigurationApi { private readonly httpClient = inject(HttpClient); - fetch(): Observable<Configuration> { - // return this.httpClient - // .get<Configuration>(CONFIGURATION_API_URL) - // .pipe(map(dto => dtoToConfiguration(dto))); - return of(configurationMock).pipe(delay(500)); + fetch(): Observable<Configuration[]> { + return this.httpClient + .get<ApiResponse<ConfigurationDto[]>>(CONFIGURATION_API_URL) + .pipe(map(dto => dto.data.map(dtoToConfiguration))); + } + + find(configName: string): Observable<Configuration> { + return this.httpClient + .get< + ApiResponse<ConfigurationDto> + >(`${CONFIGURATION_API_URL}/${configName}`) + .pipe(map(dto => dtoToConfiguration(dto.data))); } save(configuration: Configuration): Observable<Configuration> { return this.httpClient - .put<Configuration>( - CONFIGURATION_API_URL, - configurationToDto(configuration) - ) - .pipe(map(dto => dtoToConfiguration(dto))); + .put< + ApiResponse<ConfigurationDto> + >(CONFIGURATION_API_URL, configurationToDto(configuration)) + .pipe(map(dto => dtoToConfiguration(dto.data))); } } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 2835fc6..40e49cb 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -3,15 +3,16 @@ import { provideRouter } from '@angular/router'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; import { providePrimeNG } from 'primeng/config'; +import { MockInterceptor } from './interceptor/mock-interceptor'; import { routes } from './app.routes'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), - provideHttpClient(), + provideHttpClient(withInterceptors([MockInterceptor])), providePrimeNG(), provideAnimationsAsync(), ], diff --git a/src/app/components/configuration-template/configuration-template.component.html b/src/app/components/configuration-template/configuration-template.component.html index d706265..98213f6 100644 --- a/src/app/components/configuration-template/configuration-template.component.html +++ b/src/app/components/configuration-template/configuration-template.component.html @@ -1,3 +1,6 @@ +<div class="configuration-header"> + <ng-content /> +</div> <div class="configuration-section"> <div class="configuration-section__title">MAC address</div> <div class="configuration-section__card"> diff --git a/src/app/components/configuration-template/configuration-template.component.scss b/src/app/components/configuration-template/configuration-template.component.scss index fcdfe78..b8091e6 100644 --- a/src/app/components/configuration-template/configuration-template.component.scss +++ b/src/app/components/configuration-template/configuration-template.component.scss @@ -6,24 +6,30 @@ flex-direction: column; gap: map.get(vars.$spacing, 'lg'); - .configuration-section { - display: flex; - flex-direction: column; - gap: map.get(vars.$spacing, 'xs'); - - &__title { - font-weight: bold; - color: map.get(vars.$grey, 100); - font-size: map.get(vars.$text, 'lg'); + .configuration { + &-header { + margin-bottom: map.get(vars.$spacing, 'xl'); } - &__card { + &-section { display: flex; flex-direction: column; - gap: map.get(vars.$spacing, 'md'); - padding: map.get(vars.$spacing, 'lg'); - border-radius: map.get(vars.$radius, 'sm'); - background-color: map.get(vars.$grey, 10); + gap: map.get(vars.$spacing, 'xs'); + + &__title { + font-weight: bold; + color: map.get(vars.$grey, 100); + font-size: map.get(vars.$text, 'lg'); + } + + &__card { + display: flex; + flex-direction: column; + gap: map.get(vars.$spacing, 'md'); + padding: map.get(vars.$spacing, 'lg'); + border-radius: map.get(vars.$radius, 'sm'); + background-color: map.get(vars.$grey, 10); + } } } } diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index 68cbe45..c448fa4 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -10,7 +10,7 @@ } </div> - @if (configurationRes.isLoading()) { + @if (configuration.isLoading()) { <app-configuration-template> <div mac-source> <ngx-skeleton-loader count="5" /> @@ -26,38 +26,53 @@ </div> </app-configuration-template> } @else { - @let configuration = configurationRes.value()!; @if (isReadonly()) { <app-configuration-template> + <p-select + [disabled]="configurationOptions.isLoading()" + [options]="configurationOptions.value()!" + [optionValue]="chosenConfiguration()?.name" + (onChange)="onChangeConfiguration($event)" + optionLabel="name" + placeholder="Configuration" /> + <div mac-source class="configuration__mac"> <div class="configuration__mac-title">Source:</div> <div class="configuration__mac-content"> - @for (mac of configuration?.mac_source; track $index) { + @for (mac of configuration.value()?.mac_source; track $index) { <app-pill>{{ mac }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> } </div> </div> <div mac-destination class="configuration__mac"> <div class="configuration__mac-title">Destination:</div> <div class="configuration__mac-content"> - @for (mac of configuration?.mac_destination; track $index) { + @for (mac of configuration.value()?.mac_destination; track $index) { <app-pill>{{ mac }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> } </div> </div> <div frames> - @for (frame of configuration?.frame_range; track $index) { + @for (frame of configuration.value()?.frame_range; track $index) { <app-pill>{{ frame[0] }} — {{ frame[1] }}</app-pill> + } @empty { + <app-pill>All sizes</app-pill> } </div> <div protocols> - @for (protocol of configuration?.protocol; track $index) { + @for (protocol of configuration.value()?.protocol; track $index) { <app-pill>{{ protocol }}</app-pill> + } @empty { + <app-pill>None</app-pill> } </div> </app-configuration-template> } @else { - <app-configuration-form [configuration]="configuration" /> + <app-configuration-form [configuration]="configuration.value()!" /> } } </app-page-wrapper> diff --git a/src/app/components/configuration/configuration.component.scss b/src/app/components/configuration/configuration.component.scss index ea6eaa7..40b8efd 100644 --- a/src/app/components/configuration/configuration.component.scss +++ b/src/app/components/configuration/configuration.component.scss @@ -10,6 +10,12 @@ gap: map.get(vars.$spacing, 'md'); } + p-select { + border-radius: map.get(vars.$radius, 'xs'); + background-color: map.get(vars.$grey, 30); + padding: map.get(vars.$spacing, 'md') map.get(vars.$spacing, 'xl'); + } + ngx-skeleton-loader { display: flex; flex-direction: row; diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index f6a69f8..cb9c3d4 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -4,10 +4,13 @@ import { ConfigurationTemplateComponent } from '../configuration-template/config import { ConfigurationFormComponent } from '../configuration-form/configuration-form.component'; import { ButtonComponent } from '../button/button.component'; import { ConfigurationApi } from '../../api/configuration.api'; -import { firstValueFrom } from 'rxjs'; +import { firstValueFrom, map, tap } from 'rxjs'; import { PillComponent } from '../pill/pill.component'; import { SkeletonModule } from 'primeng/skeleton'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { Select, SelectChangeEvent } from 'primeng/select'; +import { FormsModule } from '@angular/forms'; +import { Configuration } from '../../models/configuration'; @Component({ selector: 'app-configuration', @@ -19,6 +22,8 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; PillComponent, SkeletonModule, NgxSkeletonLoaderModule, + Select, + FormsModule, ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', @@ -27,9 +32,42 @@ export class ConfigurationComponent { configurationApi = inject(ConfigurationApi); isReadonly = signal<boolean>(true); - configurationRes = resource({ + + configurationOptions = resource({ loader: async () => { - return await firstValueFrom(this.configurationApi.fetch()); + return await firstValueFrom( + this.configurationApi.fetch().pipe( + tap(configurations => { + const appliedConfiguration = configurations.find(c => c.is_applied); + if (appliedConfiguration) { + this.chosenConfiguration.set(appliedConfiguration); + } + }), + map(configurations => + configurations.map(configuration => { + return { + id: configuration.id, + name: + configuration.name + (configuration.is_applied ? '*' : ''), + }; + }) + ) + ) + ); + }, + }); + + chosenConfiguration = signal<Partial<Configuration> | undefined>(undefined); + + configuration = resource({ + request: () => this.chosenConfiguration(), + loader: async ({ request }) => { + return await firstValueFrom(this.configurationApi.find(request?.id!)); }, }); + + onChangeConfiguration(event: SelectChangeEvent) { + // @todo: fix + this.chosenConfiguration.set(event.value); + } } diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts new file mode 100644 index 0000000..2dfdedf --- /dev/null +++ b/src/app/interceptor/interceptor.ts @@ -0,0 +1,18 @@ +import * as configurationList from './mocks/configurations.json'; +import * as configuration1 from './mocks/configuration-1.json'; +import * as configuration2 from './mocks/configuration-2.json'; + +export const urls = [ + { + url: '/api/v1/configuration/1abc', + json: configuration1, + }, + { + url: '/api/v1/configuration/2def', + json: configuration2, + }, + { + url: '/api/v1/configuration', + json: configurationList, + }, +]; diff --git a/src/app/interceptor/mock-interceptor.ts b/src/app/interceptor/mock-interceptor.ts new file mode 100644 index 0000000..d1ae561 --- /dev/null +++ b/src/app/interceptor/mock-interceptor.ts @@ -0,0 +1,17 @@ +import { HttpInterceptorFn, HttpResponse } from '@angular/common/http'; +import { delay, of } from 'rxjs'; +import { urls } from './interceptor'; + +export const MockInterceptor: HttpInterceptorFn = (req, next) => { + const { url, method } = req; + + for (const element of urls) { + 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 next(req); +}; diff --git a/src/app/interceptor/mocks/configuration-1.json b/src/app/interceptor/mocks/configuration-1.json new file mode 100644 index 0000000..d7e04dc --- /dev/null +++ b/src/app/interceptor/mocks/configuration-1.json @@ -0,0 +1,24 @@ +{ + "data": { + "id": "1abc", + "name": "Custom", + "is_applied": false, + "mac_source": [ + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab" + ], + "mac_destination": ["fe:dc:ba:98:76:54"], + "frame_range": [ + [0, 63], + [128, 255], + [1024, 1518] + ], + "protocol": ["IPv4", "IP6", "UDP"] + } +} diff --git a/src/app/interceptor/mocks/configuration-2.json b/src/app/interceptor/mocks/configuration-2.json new file mode 100644 index 0000000..572d9a8 --- /dev/null +++ b/src/app/interceptor/mocks/configuration-2.json @@ -0,0 +1,11 @@ +{ + "data": { + "id": "2def", + "name": "Everything", + "is_applied": true, + "mac_source": [], + "mac_destination": [], + "frame_range": [], + "protocol": [] + } +} diff --git a/src/app/interceptor/mocks/configurations.json b/src/app/interceptor/mocks/configurations.json new file mode 100644 index 0000000..de16092 --- /dev/null +++ b/src/app/interceptor/mocks/configurations.json @@ -0,0 +1,35 @@ +{ + "data": [ + { + "id": "1abc", + "name": "Custom", + "is_applied": false, + "mac_source": [ + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab", + "01:23:45:67:89:ab" + ], + "mac_destination": ["fe:dc:ba:98:76:54"], + "frame_range": [ + [0, 63], + [128, 255], + [1024, 1518] + ], + "protocol": ["IPv4", "IP6", "UDP"] + }, + { + "id": "2def", + "name": "Everything", + "is_applied": true, + "mac_source": [], + "mac_destination": [], + "frame_range": [], + "protocol": [] + } + ] +} diff --git a/src/app/models/api-response.ts b/src/app/models/api-response.ts new file mode 100644 index 0000000..3db5cbb --- /dev/null +++ b/src/app/models/api-response.ts @@ -0,0 +1,3 @@ +export interface ApiResponse<T> { + data: T; +} diff --git a/src/app/models/configuration.ts b/src/app/models/configuration.ts index e1b1473..e7a7d93 100644 --- a/src/app/models/configuration.ts +++ b/src/app/models/configuration.ts @@ -1,6 +1,9 @@ export type Configuration = ConfigurationDto; export interface ConfigurationDto { + id: string; + name: string; + is_applied: boolean; mac_source: string[]; mac_destination: string[]; frame_range: [number, number][]; @@ -16,24 +19,3 @@ export function configurationToDto( ): ConfigurationDto { return configuration; } - -// @todo: delete -export const configurationMock: Configuration = { - mac_source: [ - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - '01:23:45:67:89:ab', - ], - mac_destination: ['fe:dc:ba:98:76:54'], - frame_range: [ - [0, 63], - [128, 255], - [1024, 1518], - ], - protocol: ['IPv4', 'IP6', 'UDP'], -}; -- GitLab From c1118c978df1cf413ea93e28a9101369ebad5b02 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 19:47:26 +0100 Subject: [PATCH 07/13] FE-3 Fix configuration selection --- .../configuration-template.component.scss | 4 -- .../configuration.component.html | 41 ++++++++++++------- .../configuration.component.scss | 6 +++ .../configuration/configuration.component.ts | 22 ++++++---- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/src/app/components/configuration-template/configuration-template.component.scss b/src/app/components/configuration-template/configuration-template.component.scss index b8091e6..8566472 100644 --- a/src/app/components/configuration-template/configuration-template.component.scss +++ b/src/app/components/configuration-template/configuration-template.component.scss @@ -7,10 +7,6 @@ gap: map.get(vars.$spacing, 'lg'); .configuration { - &-header { - margin-bottom: map.get(vars.$spacing, 'xl'); - } - &-section { display: flex; flex-direction: column; diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index c448fa4..7d2e677 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -10,32 +10,45 @@ } </div> + <ng-template #configurationSelect> + <mat-form-field> + <mat-label>Configuration</mat-label> + <mat-select + [value]="chosenConfiguration()" + (valueChange)="onChangeConfiguration($event)"> + @for (config of configurationOptions.value()!; track $index) { + <mat-option [value]="config.id">{{ config.name }}</mat-option> + } + </mat-select> + </mat-form-field> + </ng-template> + @if (configuration.isLoading()) { <app-configuration-template> - <div mac-source> - <ngx-skeleton-loader count="5" /> + <ng-container *ngTemplateOutlet="configurationSelect" /> + <div mac-source class="configuration__mac"> + <div class="configuration__mac-title">Source:</div> + <div class="configuration__mac-content"> + <ngx-skeleton-loader count="7" /> + </div> </div> - <div mac-destination> - <ngx-skeleton-loader count="5" /> + <div mac-destination class="configuration__mac"> + <div class="configuration__mac-title">Destination:</div> + <div class="configuration__mac-content"> + <ngx-skeleton-loader count="7" /> + </div> </div> <div frames> - <ngx-skeleton-loader count="5" /> + <ngx-skeleton-loader count="10" /> </div> <div protocols> - <ngx-skeleton-loader count="5" /> + <ngx-skeleton-loader count="10" /> </div> </app-configuration-template> } @else { @if (isReadonly()) { <app-configuration-template> - <p-select - [disabled]="configurationOptions.isLoading()" - [options]="configurationOptions.value()!" - [optionValue]="chosenConfiguration()?.name" - (onChange)="onChangeConfiguration($event)" - optionLabel="name" - placeholder="Configuration" /> - + <ng-container *ngTemplateOutlet="configurationSelect" /> <div mac-source class="configuration__mac"> <div class="configuration__mac-title">Source:</div> <div class="configuration__mac-content"> diff --git a/src/app/components/configuration/configuration.component.scss b/src/app/components/configuration/configuration.component.scss index 40b8efd..6854a46 100644 --- a/src/app/components/configuration/configuration.component.scss +++ b/src/app/components/configuration/configuration.component.scss @@ -41,4 +41,10 @@ } } } + + // Configuration select + ::ng-deep .mdc-text-field--filled:not(.mdc-text-field--disabled) { + background-color: map.get(vars.$grey, 10); + border-radius: map.get(vars.$radius, 'xs'); + } } diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index cb9c3d4..498d1e6 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -8,9 +8,10 @@ import { firstValueFrom, map, tap } from 'rxjs'; import { PillComponent } from '../pill/pill.component'; import { SkeletonModule } from 'primeng/skeleton'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { Select, SelectChangeEvent } from 'primeng/select'; import { FormsModule } from '@angular/forms'; -import { Configuration } from '../../models/configuration'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatOption, MatSelect } from '@angular/material/select'; +import { NgTemplateOutlet } from '@angular/common'; @Component({ selector: 'app-configuration', @@ -22,8 +23,12 @@ import { Configuration } from '../../models/configuration'; PillComponent, SkeletonModule, NgxSkeletonLoaderModule, - Select, FormsModule, + MatFormField, + MatSelect, + MatOption, + MatLabel, + NgTemplateOutlet, ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', @@ -40,7 +45,7 @@ export class ConfigurationComponent { tap(configurations => { const appliedConfiguration = configurations.find(c => c.is_applied); if (appliedConfiguration) { - this.chosenConfiguration.set(appliedConfiguration); + this.chosenConfiguration.set(appliedConfiguration.id); } }), map(configurations => @@ -57,17 +62,16 @@ export class ConfigurationComponent { }, }); - chosenConfiguration = signal<Partial<Configuration> | undefined>(undefined); + chosenConfiguration = signal<string | undefined>(undefined); configuration = resource({ request: () => this.chosenConfiguration(), loader: async ({ request }) => { - return await firstValueFrom(this.configurationApi.find(request?.id!)); + return await firstValueFrom(this.configurationApi.find(request)); }, }); - onChangeConfiguration(event: SelectChangeEvent) { - // @todo: fix - this.chosenConfiguration.set(event.value); + onChangeConfiguration(configId: string) { + this.chosenConfiguration.set(configId); } } -- GitLab From d633aed4bf0c1e1960461d892f5d75653a9e78ac Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 21:07:11 +0100 Subject: [PATCH 08/13] FE-3 Implement Configuration form --- .../configuration-form.builder.ts | 77 +++++++++++++++ .../configuration-form.component.html | 97 +++++++++++++++++- .../configuration-form.component.scss | 20 ++++ .../configuration-form.component.ts | 98 ++++++++++++++++++- .../configuration.component.html | 4 +- src/app/components/pill/pill.component.html | 2 +- src/app/components/pill/pill.component.scss | 2 + src/app/components/pill/pill.component.ts | 5 +- .../interceptor/mocks/configuration-1.json | 20 ++-- .../interceptor/mocks/configuration-2.json | 4 +- src/app/interceptor/mocks/configurations.json | 24 ++--- src/app/models/configuration.ts | 4 +- 12 files changed, 318 insertions(+), 39 deletions(-) diff --git a/src/app/components/configuration-form/configuration-form.builder.ts b/src/app/components/configuration-form/configuration-form.builder.ts index e69de29..56a4fcf 100644 --- a/src/app/components/configuration-form/configuration-form.builder.ts +++ b/src/app/components/configuration-form/configuration-form.builder.ts @@ -0,0 +1,77 @@ +import { + FormBuilder, + FormControl, + FormGroup, + Validators, +} from '@angular/forms'; +import { inject, Injectable } from '@angular/core'; +import { Configuration } from '../../models/configuration'; + +export const SizeRanges: [number, number][] = [ + [0, 63], + [64, 127], + [128, 255], + [256, 511], + [512, 1023], + [1024, 1518], + [1519, Infinity], +]; + +export const Protocols: string[] = [ + 'ARP', + 'IPv4', + 'IPv6', + 'ICMP', + 'TCP', + 'UDP', +]; + +export type ConfigurationForm = FormGroup<{ + name: FormControl<string>; + source_mac: FormControl<string[] | undefined>; + dest_mac: FormControl<string[] | undefined>; + frame_ranges: FormControl<[number, number][] | undefined>; + protocols: FormControl<string[] | undefined>; +}>; + +@Injectable({ + providedIn: 'root', +}) +export class ConfigurationFormBuilder { + formBuilder = inject(FormBuilder); + + create(configuration?: Configuration): ConfigurationForm { + const fb = this.formBuilder.nonNullable; + + return fb.group({ + name: fb.control<string>(configuration?.name || '', { + validators: [Validators.required], + }), + source_mac: fb.control<string[] | undefined>( + configuration?.mac_source || undefined + ), + dest_mac: fb.control<string[] | undefined>( + configuration?.mac_destination || undefined + ), + frame_ranges: fb.control<[number, number][] | undefined>( + configuration?.frame_ranges || undefined + ), + protocols: fb.control<string[] | undefined>( + configuration?.protocols || undefined + ), + }); + } + + toValue(form: ConfigurationForm): Configuration { + const data = form.value; + return <Configuration>{ + id: '', + name: data.name, + is_applied: false, + mac_source: data.source_mac || [], + mac_destination: data.dest_mac || [], + frame_ranges: data.frame_ranges || [], + protocols: data.protocols || [], + }; + } +} diff --git a/src/app/components/configuration-form/configuration-form.component.html b/src/app/components/configuration-form/configuration-form.component.html index cf46ab7..e003e8b 100644 --- a/src/app/components/configuration-form/configuration-form.component.html +++ b/src/app/components/configuration-form/configuration-form.component.html @@ -1,6 +1,95 @@ <app-configuration-template> - <div mac-source>editable Mac source</div> - <div mac-destination>editable Mac destination</div> - <div frames>editable Frames</div> - <div protocols>editable Protocols</div> + <mat-form-field> + <mat-label>Configuration name</mat-label> + <input matInput [formControl]="form.controls.name" /> + </mat-form-field> + + <div mac-source class="configuration__mac"> + <div class="configuration__mac-title"> + <mat-form-field> + <mat-label>Source</mat-label> + <input + matInput + #sourceInput + (keydown.enter)="addSourceMac(sourceInput.value)" /> + </mat-form-field> + </div> + <div class="configuration__mac-content"> + @for (mac of form.controls.source_mac.value; track $index) { + <app-pill [removable]="true" (click)="removeSourceMac($index)">{{ + mac + }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> + } + </div> + </div> + <div mac-destination class="configuration__mac"> + <div class="configuration__mac-title"> + <mat-form-field> + <mat-label>Destination</mat-label> + <input + matInput + #destInput + (keydown.enter)="addDestMac(destInput.value)" /> + </mat-form-field> + </div> + <div class="configuration__mac-content"> + @for (mac of form.controls.dest_mac.value; track $index) { + <app-pill [removable]="true" (click)="removeDestMac($index)">{{ + mac + }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> + } + </div> + </div> + <div frames> + @if (sizeRangeOptions().length) { + <mat-form-field> + <mat-label>Add size range</mat-label> + <mat-select + (selectionChange)=" + addFrameRange($event.value); selectRange.value = undefined + " + #selectRange> + @for (range of sizeRangeOptions(); track $index) { + <mat-option [value]="range" + >{{ range[0] }} — {{ range[1] }}</mat-option + > + } + </mat-select> + </mat-form-field> + } + @for (range of form.controls.frame_ranges.value; track $index) { + <app-pill [removable]="true" (click)="removeFrameRange($index)" + >{{ range[0] }} — {{ range[1] }}</app-pill + > + } @empty { + <app-pill>All sizes</app-pill> + } + </div> + <div protocols> + @if (protocolOptions().length) { + <mat-form-field> + <mat-label>Add protocol</mat-label> + <mat-select + (selectionChange)=" + addProtocol($event.value); selectProtocol.value = undefined + " + #selectProtocol> + @for (protocol of protocolOptions(); track $index) { + <mat-option [value]="protocol">{{ protocol }}</mat-option> + } + </mat-select> + </mat-form-field> + } + @for (protocol of form.controls.protocols.value; track $index) { + <app-pill [removable]="true" (click)="removeProtocol($index)">{{ + protocol + }}</app-pill> + } @empty { + <app-pill>None</app-pill> + } + </div> </app-configuration-template> diff --git a/src/app/components/configuration-form/configuration-form.component.scss b/src/app/components/configuration-form/configuration-form.component.scss index e69de29..e6d4508 100644 --- a/src/app/components/configuration-form/configuration-form.component.scss +++ b/src/app/components/configuration-form/configuration-form.component.scss @@ -0,0 +1,20 @@ +@use '../../../vars'; +@use 'sass:map'; + +:host { + .configuration { + &__mac { + display: flex; + justify-content: space-between; + + &-title { + color: map.get(vars.$grey, 100); + font-size: map.get(vars.$text, 'lg'); + } + + &-content { + width: calc(100% - 200px); + } + } + } +} diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts index e062fc9..ba9814a 100644 --- a/src/app/components/configuration-form/configuration-form.component.ts +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -1,13 +1,103 @@ -import { Component, input } from '@angular/core'; +import { + Component, + computed, + inject, + input, + OnInit, + signal, +} from '@angular/core'; import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; import { Configuration } from '../../models/configuration'; +import { + ConfigurationForm, + ConfigurationFormBuilder, + SizeRanges, + Protocols, +} from './configuration-form.builder'; +import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { MatOption, MatSelect } from '@angular/material/select'; +import { ReactiveFormsModule } from '@angular/forms'; +import { PillComponent } from '../pill/pill.component'; @Component({ selector: 'app-configuration-form', - imports: [ConfigurationTemplateComponent], + imports: [ + ConfigurationTemplateComponent, + MatFormField, + MatInput, + MatLabel, + MatSelect, + MatOption, + ReactiveFormsModule, + PillComponent, + ], templateUrl: './configuration-form.component.html', styleUrl: './configuration-form.component.scss', }) -export class ConfigurationFormComponent { - configuration = input.required<Configuration>(); +export class ConfigurationFormComponent implements OnInit { + configurationFormBuilder = inject(ConfigurationFormBuilder); + configuration = input<Configuration>(); + form!: ConfigurationForm; + + sizeRangeOptions = signal<[number, number][]>(SizeRanges); + protocolOptions = signal<string[]>(Protocols); + + ngOnInit() { + this.form = this.configurationFormBuilder.create(this.configuration()); + } + + addSourceMac(sourceMac: string) { + this.form.controls.source_mac.value?.push(sourceMac); + } + + removeSourceMac(index: number) { + this.form.controls.source_mac.value?.splice(index, 1); + } + + addDestMac(destMac: string) { + this.form.controls.dest_mac.value?.push(destMac); + } + + removeDestMac(index: number) { + this.form.controls.dest_mac.value?.splice(index, 1); + } + + addFrameRange(frameRange: [number, number]) { + this.form.controls.frame_ranges.value?.push(frameRange); + this.updateSizeRangeOptions(); + } + + removeFrameRange(index: number) { + this.form.controls.frame_ranges.value?.splice(index, 1); + this.updateSizeRangeOptions(); + } + + addProtocol(protocol: string) { + this.form.controls.protocols.value?.push(protocol); + this.updateProtocolOptions(); + } + + removeProtocol(index: number) { + this.form.controls.protocols.value?.splice(index, 1); + this.updateProtocolOptions(); + } + + updateSizeRangeOptions() { + const chosenRanges = this.form.controls.frame_ranges.value; + if (!chosenRanges) return; + const sizeRanges = SizeRanges.filter( + range => !chosenRanges.includes(range) + ); + this.sizeRangeOptions.set(sizeRanges); + } + + updateProtocolOptions() { + const chosenProtocols = this.form.controls.protocols.value; + if (!chosenProtocols) return; + const protocols = Protocols.filter( + protocol => !chosenProtocols.includes(protocol) + ); + this.protocolOptions.set(protocols); + } } diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index 7d2e677..db621dc 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -70,14 +70,14 @@ </div> </div> <div frames> - @for (frame of configuration.value()?.frame_range; track $index) { + @for (frame of configuration.value()?.frame_ranges; track $index) { <app-pill>{{ frame[0] }} — {{ frame[1] }}</app-pill> } @empty { <app-pill>All sizes</app-pill> } </div> <div protocols> - @for (protocol of configuration.value()?.protocol; track $index) { + @for (protocol of configuration.value()?.protocols; track $index) { <app-pill>{{ protocol }}</app-pill> } @empty { <app-pill>None</app-pill> diff --git a/src/app/components/pill/pill.component.html b/src/app/components/pill/pill.component.html index b4b2f25..0548d45 100644 --- a/src/app/components/pill/pill.component.html +++ b/src/app/components/pill/pill.component.html @@ -1,6 +1,6 @@ <div class="pill"> <ng-content /> @if (removable()) { - <mat-icon>remove</mat-icon> + <mat-icon (click)="clickRemove.emit()">close</mat-icon> } </div> diff --git a/src/app/components/pill/pill.component.scss b/src/app/components/pill/pill.component.scss index 63fb4f4..ac096b3 100644 --- a/src/app/components/pill/pill.component.scss +++ b/src/app/components/pill/pill.component.scss @@ -5,6 +5,7 @@ display: inline-flex; flex-direction: row; justify-content: center; + align-items: center; gap: map.get(vars.$spacing, 'sm'); border-radius: map.get(vars.$radius, 'md'); border: 1px solid map.get(vars.$grey, 50); @@ -12,4 +13,5 @@ color: map.get(vars.$grey, 100); padding: map.get(vars.$spacing, 'xs') map.get(vars.$spacing, 'lg'); margin: map.get(vars.$spacing, 'xs'); + line-height: map.get(vars.$text, 'xl'); } diff --git a/src/app/components/pill/pill.component.ts b/src/app/components/pill/pill.component.ts index ccc77c2..f43c8ac 100644 --- a/src/app/components/pill/pill.component.ts +++ b/src/app/components/pill/pill.component.ts @@ -1,4 +1,4 @@ -import { Component, input } from '@angular/core'; +import { Component, input, output } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; @Component({ @@ -8,5 +8,6 @@ import { MatIcon } from '@angular/material/icon'; styleUrl: './pill.component.scss', }) export class PillComponent { - removable = input<string | null>(null); + removable = input<boolean | null>(null); + clickRemove = output<void>(); } diff --git a/src/app/interceptor/mocks/configuration-1.json b/src/app/interceptor/mocks/configuration-1.json index d7e04dc..6e97131 100644 --- a/src/app/interceptor/mocks/configuration-1.json +++ b/src/app/interceptor/mocks/configuration-1.json @@ -4,21 +4,21 @@ "name": "Custom", "is_applied": false, "mac_source": [ - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab" + "01:23:45:67:89:00", + "01:23:45:67:89:11", + "01:23:45:67:89:22", + "01:23:45:67:89:33", + "01:23:45:67:89:44", + "01:23:45:67:89:55", + "01:23:45:67:89:66", + "01:23:45:67:89:77" ], "mac_destination": ["fe:dc:ba:98:76:54"], - "frame_range": [ + "frame_ranges": [ [0, 63], [128, 255], [1024, 1518] ], - "protocol": ["IPv4", "IP6", "UDP"] + "protocols": ["IPv4", "IP6", "UDP"] } } diff --git a/src/app/interceptor/mocks/configuration-2.json b/src/app/interceptor/mocks/configuration-2.json index 572d9a8..d18ca94 100644 --- a/src/app/interceptor/mocks/configuration-2.json +++ b/src/app/interceptor/mocks/configuration-2.json @@ -5,7 +5,7 @@ "is_applied": true, "mac_source": [], "mac_destination": [], - "frame_range": [], - "protocol": [] + "frame_ranges": [], + "protocols": [] } } diff --git a/src/app/interceptor/mocks/configurations.json b/src/app/interceptor/mocks/configurations.json index de16092..b1ed282 100644 --- a/src/app/interceptor/mocks/configurations.json +++ b/src/app/interceptor/mocks/configurations.json @@ -5,22 +5,22 @@ "name": "Custom", "is_applied": false, "mac_source": [ - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab", - "01:23:45:67:89:ab" + "01:23:45:67:89:00", + "01:23:45:67:89:11", + "01:23:45:67:89:22", + "01:23:45:67:89:33", + "01:23:45:67:89:44", + "01:23:45:67:89:55", + "01:23:45:67:89:66", + "01:23:45:67:89:77" ], "mac_destination": ["fe:dc:ba:98:76:54"], - "frame_range": [ + "frame_ranges": [ [0, 63], [128, 255], [1024, 1518] ], - "protocol": ["IPv4", "IP6", "UDP"] + "protocols": ["IPv4", "IP6", "UDP"] }, { "id": "2def", @@ -28,8 +28,8 @@ "is_applied": true, "mac_source": [], "mac_destination": [], - "frame_range": [], - "protocol": [] + "frame_ranges": [], + "protocols": [] } ] } diff --git a/src/app/models/configuration.ts b/src/app/models/configuration.ts index e7a7d93..8424ba4 100644 --- a/src/app/models/configuration.ts +++ b/src/app/models/configuration.ts @@ -6,8 +6,8 @@ export interface ConfigurationDto { is_applied: boolean; mac_source: string[]; mac_destination: string[]; - frame_range: [number, number][]; - protocol: string[]; + frame_ranges: [number, number][]; + protocols: string[]; } export function dtoToConfiguration(dto: ConfigurationDto): Configuration { -- GitLab From 489152ca5adb45c25806dd2f392b4f0692740208 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 21:23:27 +0100 Subject: [PATCH 09/13] FE-3 MAC validation --- .../configuration-form.builder.ts | 14 ++++++++++++++ .../configuration-form.component.html | 10 ++++++++++ .../configuration-form.component.ts | 17 ++++++++++++++--- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/app/components/configuration-form/configuration-form.builder.ts b/src/app/components/configuration-form/configuration-form.builder.ts index 56a4fcf..884fac8 100644 --- a/src/app/components/configuration-form/configuration-form.builder.ts +++ b/src/app/components/configuration-form/configuration-form.builder.ts @@ -32,6 +32,9 @@ export type ConfigurationForm = FormGroup<{ dest_mac: FormControl<string[] | undefined>; frame_ranges: FormControl<[number, number][] | undefined>; protocols: FormControl<string[] | undefined>; + + source_mac_control: FormControl<string>; + dest_mac_control: FormControl<string>; }>; @Injectable({ @@ -59,6 +62,17 @@ export class ConfigurationFormBuilder { protocols: fb.control<string[] | undefined>( configuration?.protocols || undefined ), + + source_mac_control: fb.control<string>('', { + validators: [ + Validators.pattern(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/), + ], + }), + dest_mac_control: fb.control<string>('', { + validators: [ + Validators.pattern(/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/), + ], + }), }); } diff --git a/src/app/components/configuration-form/configuration-form.component.html b/src/app/components/configuration-form/configuration-form.component.html index e003e8b..d33b846 100644 --- a/src/app/components/configuration-form/configuration-form.component.html +++ b/src/app/components/configuration-form/configuration-form.component.html @@ -9,9 +9,14 @@ <mat-form-field> <mat-label>Source</mat-label> <input + [formControl]="form.controls.source_mac_control" matInput #sourceInput (keydown.enter)="addSourceMac(sourceInput.value)" /> + <mat-hint>Press Enter to add</mat-hint> + @if (form.controls.source_mac_control.hasError('pattern')) { + <mat-error>Mac address is incorrect!</mat-error> + } </mat-form-field> </div> <div class="configuration__mac-content"> @@ -29,9 +34,14 @@ <mat-form-field> <mat-label>Destination</mat-label> <input + [formControl]="form.controls.dest_mac_control" matInput #destInput (keydown.enter)="addDestMac(destInput.value)" /> + <mat-hint>Press Enter to add</mat-hint> + @if (form.controls.dest_mac_control.hasError('pattern')) { + <mat-error>Mac address is incorrect!</mat-error> + } </mat-form-field> </div> <div class="configuration__mac-content"> diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts index ba9814a..814a06b 100644 --- a/src/app/components/configuration-form/configuration-form.component.ts +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -14,7 +14,12 @@ import { SizeRanges, Protocols, } from './configuration-form.builder'; -import { MatFormField, MatLabel } from '@angular/material/form-field'; +import { + MatError, + MatFormField, + MatHint, + MatLabel, +} from '@angular/material/form-field'; import { MatInput } from '@angular/material/input'; import { MatOption, MatSelect } from '@angular/material/select'; import { ReactiveFormsModule } from '@angular/forms'; @@ -31,6 +36,8 @@ import { PillComponent } from '../pill/pill.component'; MatOption, ReactiveFormsModule, PillComponent, + MatError, + MatHint, ], templateUrl: './configuration-form.component.html', styleUrl: './configuration-form.component.scss', @@ -48,7 +55,9 @@ export class ConfigurationFormComponent implements OnInit { } addSourceMac(sourceMac: string) { - this.form.controls.source_mac.value?.push(sourceMac); + if (!this.form.controls.source_mac_control.errors) { + this.form.controls.source_mac.value?.push(sourceMac); + } } removeSourceMac(index: number) { @@ -56,7 +65,9 @@ export class ConfigurationFormComponent implements OnInit { } addDestMac(destMac: string) { - this.form.controls.dest_mac.value?.push(destMac); + if (!this.form.controls.dest_mac_control.errors) { + this.form.controls.dest_mac.value?.push(destMac); + } } removeDestMac(index: number) { -- GitLab From 1f19a843c8ea612912e0a6a76b6a93895a8b86ed Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 21:35:49 +0100 Subject: [PATCH 10/13] FE-3 Fix form in creation mode --- src/app/app.component.scss | 2 +- .../configuration-form.builder.ts | 24 ++--- .../configuration.component.html | 99 +++++++++++-------- .../configuration/configuration.component.ts | 10 +- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/app/app.component.scss b/src/app/app.component.scss index c0b0c96..3112c39 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -3,7 +3,7 @@ .content { display: flex; - height: calc(100vh - vars.$headerHeight); + min-height: calc(100vh - vars.$headerHeight); background-color: map.get(vars.$grey, 30); margin-top: vars.$headerHeight; } diff --git a/src/app/components/configuration-form/configuration-form.builder.ts b/src/app/components/configuration-form/configuration-form.builder.ts index 884fac8..6f5d13b 100644 --- a/src/app/components/configuration-form/configuration-form.builder.ts +++ b/src/app/components/configuration-form/configuration-form.builder.ts @@ -28,10 +28,10 @@ export const Protocols: string[] = [ export type ConfigurationForm = FormGroup<{ name: FormControl<string>; - source_mac: FormControl<string[] | undefined>; - dest_mac: FormControl<string[] | undefined>; - frame_ranges: FormControl<[number, number][] | undefined>; - protocols: FormControl<string[] | undefined>; + source_mac: FormControl<string[]>; + dest_mac: FormControl<string[]>; + frame_ranges: FormControl<[number, number][]>; + protocols: FormControl<string[]>; source_mac_control: FormControl<string>; dest_mac_control: FormControl<string>; @@ -50,18 +50,12 @@ export class ConfigurationFormBuilder { name: fb.control<string>(configuration?.name || '', { validators: [Validators.required], }), - source_mac: fb.control<string[] | undefined>( - configuration?.mac_source || undefined - ), - dest_mac: fb.control<string[] | undefined>( - configuration?.mac_destination || undefined - ), - frame_ranges: fb.control<[number, number][] | undefined>( - configuration?.frame_ranges || undefined - ), - protocols: fb.control<string[] | undefined>( - configuration?.protocols || undefined + source_mac: fb.control<string[]>(configuration?.mac_source || []), + dest_mac: fb.control<string[]>(configuration?.mac_destination || []), + frame_ranges: fb.control<[number, number][]>( + configuration?.frame_ranges || [] ), + protocols: fb.control<string[]>(configuration?.protocols || []), source_mac_control: fb.control<string>('', { validators: [ diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index db621dc..9ed7e00 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -2,11 +2,18 @@ <div title>Analyzer configuration</div> <div actions class="actions"> - @if (isReadonly()) { - <app-button (click)="isReadonly.set(false)">Edit</app-button> - } @else { - <app-button secondary (click)="isReadonly.set(true)">Cancel</app-button> - <app-button>Save</app-button> + @switch (pageMode()) { + @case (ConfigurationPageMode.READ) { + <app-button (click)="pageMode.set(ConfigurationPageMode.CREATE)" + >Create</app-button + > + } + @default { + <app-button secondary (click)="pageMode.set(ConfigurationPageMode.READ)" + >Cancel</app-button + > + <app-button>Save</app-button> + } } </div> @@ -46,46 +53,60 @@ </div> </app-configuration-template> } @else { - @if (isReadonly()) { - <app-configuration-template> - <ng-container *ngTemplateOutlet="configurationSelect" /> - <div mac-source class="configuration__mac"> - <div class="configuration__mac-title">Source:</div> - <div class="configuration__mac-content"> - @for (mac of configuration.value()?.mac_source; track $index) { - <app-pill>{{ mac }}</app-pill> + @switch (pageMode()) { + @case (ConfigurationPageMode.CREATE) { + <app-configuration-form /> + } + @case (ConfigurationPageMode.EDIT) { + <app-configuration-form [configuration]="configuration.value()!" /> + } + @default { + <app-configuration-template> + <div style="display: flex; justify-content: space-between"> + <ng-container *ngTemplateOutlet="configurationSelect" /> + <app-button (click)="pageMode.set(ConfigurationPageMode.EDIT)"> + Edit + </app-button> + </div> + <div mac-source class="configuration__mac"> + <div class="configuration__mac-title">Source:</div> + <div class="configuration__mac-content"> + @for (mac of configuration.value()?.mac_source; track $index) { + <app-pill>{{ mac }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> + } + </div> + </div> + <div mac-destination class="configuration__mac"> + <div class="configuration__mac-title">Destination:</div> + <div class="configuration__mac-content"> + @for ( + mac of configuration.value()?.mac_destination; + track $index + ) { + <app-pill>{{ mac }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> + } + </div> + </div> + <div frames> + @for (frame of configuration.value()?.frame_ranges; track $index) { + <app-pill>{{ frame[0] }} — {{ frame[1] }}</app-pill> } @empty { - <app-pill>All addresses</app-pill> + <app-pill>All sizes</app-pill> } </div> - </div> - <div mac-destination class="configuration__mac"> - <div class="configuration__mac-title">Destination:</div> - <div class="configuration__mac-content"> - @for (mac of configuration.value()?.mac_destination; track $index) { - <app-pill>{{ mac }}</app-pill> + <div protocols> + @for (protocol of configuration.value()?.protocols; track $index) { + <app-pill>{{ protocol }}</app-pill> } @empty { - <app-pill>All addresses</app-pill> + <app-pill>None</app-pill> } </div> - </div> - <div frames> - @for (frame of configuration.value()?.frame_ranges; track $index) { - <app-pill>{{ frame[0] }} — {{ frame[1] }}</app-pill> - } @empty { - <app-pill>All sizes</app-pill> - } - </div> - <div protocols> - @for (protocol of configuration.value()?.protocols; track $index) { - <app-pill>{{ protocol }}</app-pill> - } @empty { - <app-pill>None</app-pill> - } - </div> - </app-configuration-template> - } @else { - <app-configuration-form [configuration]="configuration.value()!" /> + </app-configuration-template> + } } } </app-page-wrapper> diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index 498d1e6..a3ee4ef 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -13,6 +13,12 @@ import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { NgTemplateOutlet } from '@angular/common'; +enum ConfigurationPageMode { + READ, + EDIT, + CREATE, +} + @Component({ selector: 'app-configuration', imports: [ @@ -36,7 +42,7 @@ import { NgTemplateOutlet } from '@angular/common'; export class ConfigurationComponent { configurationApi = inject(ConfigurationApi); - isReadonly = signal<boolean>(true); + pageMode = signal<ConfigurationPageMode>(ConfigurationPageMode.READ); configurationOptions = resource({ loader: async () => { @@ -74,4 +80,6 @@ export class ConfigurationComponent { onChangeConfiguration(configId: string) { this.chosenConfiguration.set(configId); } + + protected readonly ConfigurationPageMode = ConfigurationPageMode; } -- GitLab From 893f8e52a28144258c1e73cad1a30697c896b982 Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 21:49:38 +0100 Subject: [PATCH 11/13] FE-3 Add form submission events --- .../configuration-form.builder.ts | 4 +++- .../configuration-form.component.html | 17 +++++++++++++---- .../configuration-form.component.scss | 11 +++++++++++ .../configuration-form.component.ts | 17 +++++++++++++++-- .../configuration/configuration.component.html | 15 +++++++-------- .../configuration/configuration.component.ts | 7 +++++++ 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/app/components/configuration-form/configuration-form.builder.ts b/src/app/components/configuration-form/configuration-form.builder.ts index 6f5d13b..b785bf7 100644 --- a/src/app/components/configuration-form/configuration-form.builder.ts +++ b/src/app/components/configuration-form/configuration-form.builder.ts @@ -27,6 +27,7 @@ export const Protocols: string[] = [ ]; export type ConfigurationForm = FormGroup<{ + id: FormControl<string | undefined>; name: FormControl<string>; source_mac: FormControl<string[]>; dest_mac: FormControl<string[]>; @@ -47,6 +48,7 @@ export class ConfigurationFormBuilder { const fb = this.formBuilder.nonNullable; return fb.group({ + id: fb.control<string | undefined>(configuration?.id), name: fb.control<string>(configuration?.name || '', { validators: [Validators.required], }), @@ -73,7 +75,7 @@ export class ConfigurationFormBuilder { toValue(form: ConfigurationForm): Configuration { const data = form.value; return <Configuration>{ - id: '', + id: data.id, name: data.name, is_applied: false, mac_source: data.source_mac || [], diff --git a/src/app/components/configuration-form/configuration-form.component.html b/src/app/components/configuration-form/configuration-form.component.html index d33b846..4a1e25b 100644 --- a/src/app/components/configuration-form/configuration-form.component.html +++ b/src/app/components/configuration-form/configuration-form.component.html @@ -1,8 +1,17 @@ <app-configuration-template> - <mat-form-field> - <mat-label>Configuration name</mat-label> - <input matInput [formControl]="form.controls.name" /> - </mat-form-field> + <div class="configuration-form__header"> + <mat-form-field> + <mat-label>Configuration name</mat-label> + <input matInput [formControl]="form.controls.name" /> + @if (form.controls.name.hasError('required')) { + <mat-error>Name is required!</mat-error> + } + </mat-form-field> + <div class="configuration-form__header-actions"> + <app-button secondary (click)="cancel.emit()">Cancel</app-button> + <app-button (click)="onSubmit()">Save</app-button> + </div> + </div> <div mac-source class="configuration__mac"> <div class="configuration__mac-title"> diff --git a/src/app/components/configuration-form/configuration-form.component.scss b/src/app/components/configuration-form/configuration-form.component.scss index e6d4508..5e6b0cf 100644 --- a/src/app/components/configuration-form/configuration-form.component.scss +++ b/src/app/components/configuration-form/configuration-form.component.scss @@ -3,6 +3,17 @@ :host { .configuration { + &-form { + &__header { + display: flex; + justify-content: space-between; + + &-actions { + display: flex; + gap: map.get(vars.$spacing, 'md'); + } + } + } &__mac { display: flex; justify-content: space-between; diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts index 814a06b..0857f29 100644 --- a/src/app/components/configuration-form/configuration-form.component.ts +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -1,9 +1,9 @@ import { Component, - computed, inject, input, OnInit, + output, signal, } from '@angular/core'; import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; @@ -11,8 +11,8 @@ import { Configuration } from '../../models/configuration'; import { ConfigurationForm, ConfigurationFormBuilder, - SizeRanges, Protocols, + SizeRanges, } from './configuration-form.builder'; import { MatError, @@ -24,6 +24,7 @@ import { MatInput } from '@angular/material/input'; import { MatOption, MatSelect } from '@angular/material/select'; import { ReactiveFormsModule } from '@angular/forms'; import { PillComponent } from '../pill/pill.component'; +import { ButtonComponent } from '../button/button.component'; @Component({ selector: 'app-configuration-form', @@ -38,6 +39,7 @@ import { PillComponent } from '../pill/pill.component'; PillComponent, MatError, MatHint, + ButtonComponent, ], templateUrl: './configuration-form.component.html', styleUrl: './configuration-form.component.scss', @@ -47,6 +49,9 @@ export class ConfigurationFormComponent implements OnInit { configuration = input<Configuration>(); form!: ConfigurationForm; + submit = output<Configuration>(); + cancel = output<void>(); + sizeRangeOptions = signal<[number, number][]>(SizeRanges); protocolOptions = signal<string[]>(Protocols); @@ -54,6 +59,14 @@ export class ConfigurationFormComponent implements OnInit { this.form = this.configurationFormBuilder.create(this.configuration()); } + onSubmit() { + if (this.form.valid) { + this.submit.emit(this.configurationFormBuilder.toValue(this.form)); + } else { + this.form.markAsDirty(); + } + } + addSourceMac(sourceMac: string) { if (!this.form.controls.source_mac_control.errors) { this.form.controls.source_mac.value?.push(sourceMac); diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index 9ed7e00..bfa13b0 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -8,12 +8,6 @@ >Create</app-button > } - @default { - <app-button secondary (click)="pageMode.set(ConfigurationPageMode.READ)" - >Cancel</app-button - > - <app-button>Save</app-button> - } } </div> @@ -55,10 +49,15 @@ } @else { @switch (pageMode()) { @case (ConfigurationPageMode.CREATE) { - <app-configuration-form /> + <app-configuration-form + (cancel)="pageMode.set(ConfigurationPageMode.READ)" + (submit)="onSubmitConfiguration($event)" /> } @case (ConfigurationPageMode.EDIT) { - <app-configuration-form [configuration]="configuration.value()!" /> + <app-configuration-form + [configuration]="configuration.value()!" + (cancel)="pageMode.set(ConfigurationPageMode.READ)" + (submit)="onSubmitConfiguration($event)" /> } @default { <app-configuration-template> diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index a3ee4ef..5b7f754 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -12,6 +12,7 @@ import { FormsModule } from '@angular/forms'; import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { NgTemplateOutlet } from '@angular/common'; +import { Configuration } from '../../models/configuration'; enum ConfigurationPageMode { READ, @@ -81,5 +82,11 @@ export class ConfigurationComponent { this.chosenConfiguration.set(configId); } + onSubmitConfiguration(configuration: Configuration) { + console.log('Submitting configuration', configuration); + this.configurationApi.save(configuration); + this.pageMode.set(ConfigurationPageMode.READ); + } + protected readonly ConfigurationPageMode = ConfigurationPageMode; } -- GitLab From d18da82f325713257fb30020d19f1a8846e3dbda Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 22:28:57 +0100 Subject: [PATCH 12/13] FE-3 Implement Apply and Delete events --- src/app/api/configuration.api.ts | 14 +++ .../configuration.component.html | 51 ++++++--- .../configuration.component.scss | 14 +++ .../configuration/configuration.component.ts | 104 ++++++++++++------ 4 files changed, 135 insertions(+), 48 deletions(-) diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts index 7ed09c6..064fd85 100644 --- a/src/app/api/configuration.api.ts +++ b/src/app/api/configuration.api.ts @@ -38,4 +38,18 @@ export class ConfigurationApi { >(CONFIGURATION_API_URL, configurationToDto(configuration)) .pipe(map(dto => dtoToConfiguration(dto.data))); } + + apply(configurationId: string): Observable<Configuration> { + return this.httpClient + .put< + ApiResponse<ConfigurationDto> + >(`${CONFIGURATION_API_URL}/${configurationId}/apply`, {}) + .pipe(map(dto => dtoToConfiguration(dto.data))); + } + + delete(configurationId: string): Observable<void> { + return this.httpClient.delete<void>( + `${CONFIGURATION_API_URL}/${configurationId}` + ); + } } diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html index bfa13b0..6c6e9ab 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -5,7 +5,7 @@ @switch (pageMode()) { @case (ConfigurationPageMode.READ) { <app-button (click)="pageMode.set(ConfigurationPageMode.CREATE)" - >Create</app-button + >New profile</app-button > } } @@ -17,16 +17,21 @@ <mat-select [value]="chosenConfiguration()" (valueChange)="onChangeConfiguration($event)"> - @for (config of configurationOptions.value()!; track $index) { + @for (config of configurationOptions(); track $index) { <mat-option [value]="config.id">{{ config.name }}</mat-option> } </mat-select> </mat-form-field> </ng-template> - @if (configuration.isLoading()) { + @if (isLoading()) { <app-configuration-template> - <ng-container *ngTemplateOutlet="configurationSelect" /> + <div class="configuration__header"> + <ng-container *ngTemplateOutlet="configurationSelect" /> + <div class="configuration__header-actions"> + <ngx-skeleton-loader count="3" /> + </div> + </div> <div mac-source class="configuration__mac"> <div class="configuration__mac-title">Source:</div> <div class="configuration__mac-content"> @@ -55,22 +60,39 @@ } @case (ConfigurationPageMode.EDIT) { <app-configuration-form - [configuration]="configuration.value()!" + [configuration]="configuration()!" (cancel)="pageMode.set(ConfigurationPageMode.READ)" (submit)="onSubmitConfiguration($event)" /> } @default { <app-configuration-template> - <div style="display: flex; justify-content: space-between"> + <div class="configuration__header"> <ng-container *ngTemplateOutlet="configurationSelect" /> - <app-button (click)="pageMode.set(ConfigurationPageMode.EDIT)"> - Edit - </app-button> + + <div class="configuration__header-actions"> + <button mat-fab extended (click)="onDeleteConfiguration()"> + <mat-icon>delete</mat-icon> + Delete + </button> + @if (!configuration()?.is_applied) { + <button mat-fab extended (click)="onApplyConfiguration()"> + <mat-icon>save</mat-icon> + Apply + </button> + } + <button + mat-fab + extended + (click)="pageMode.set(ConfigurationPageMode.EDIT)"> + <mat-icon>edit_square</mat-icon> + Edit + </button> + </div> </div> <div mac-source class="configuration__mac"> <div class="configuration__mac-title">Source:</div> <div class="configuration__mac-content"> - @for (mac of configuration.value()?.mac_source; track $index) { + @for (mac of configuration()?.mac_source; track $index) { <app-pill>{{ mac }}</app-pill> } @empty { <app-pill>All addresses</app-pill> @@ -80,10 +102,7 @@ <div mac-destination class="configuration__mac"> <div class="configuration__mac-title">Destination:</div> <div class="configuration__mac-content"> - @for ( - mac of configuration.value()?.mac_destination; - track $index - ) { + @for (mac of configuration()?.mac_destination; track $index) { <app-pill>{{ mac }}</app-pill> } @empty { <app-pill>All addresses</app-pill> @@ -91,14 +110,14 @@ </div> </div> <div frames> - @for (frame of configuration.value()?.frame_ranges; track $index) { + @for (frame of configuration()?.frame_ranges; track $index) { <app-pill>{{ frame[0] }} — {{ frame[1] }}</app-pill> } @empty { <app-pill>All sizes</app-pill> } </div> <div protocols> - @for (protocol of configuration.value()?.protocols; track $index) { + @for (protocol of configuration()?.protocols; track $index) { <app-pill>{{ protocol }}</app-pill> } @empty { <app-pill>None</app-pill> diff --git a/src/app/components/configuration/configuration.component.scss b/src/app/components/configuration/configuration.component.scss index 6854a46..efe7e85 100644 --- a/src/app/components/configuration/configuration.component.scss +++ b/src/app/components/configuration/configuration.component.scss @@ -27,6 +27,20 @@ } .configuration { + &__header { + display: flex; + justify-content: space-between; + + &-actions { + display: flex; + gap: map.get(vars.$spacing, 'xs'); + + & > button { + box-shadow: none; + } + } + } + &__mac { display: flex; justify-content: space-between; diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index 5b7f754..11789ed 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -1,10 +1,16 @@ -import { Component, inject, resource, signal } from '@angular/core'; +import { + afterNextRender, + Component, + effect, + inject, + signal, +} from '@angular/core'; import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component'; import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; import { ConfigurationFormComponent } from '../configuration-form/configuration-form.component'; import { ButtonComponent } from '../button/button.component'; import { ConfigurationApi } from '../../api/configuration.api'; -import { firstValueFrom, map, tap } from 'rxjs'; +import { map, Observable, tap } from 'rxjs'; import { PillComponent } from '../pill/pill.component'; import { SkeletonModule } from 'primeng/skeleton'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -13,6 +19,8 @@ import { MatFormField, MatLabel } from '@angular/material/form-field'; import { MatOption, MatSelect } from '@angular/material/select'; import { NgTemplateOutlet } from '@angular/common'; import { Configuration } from '../../models/configuration'; +import { MatFabButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; enum ConfigurationPageMode { READ, @@ -36,6 +44,8 @@ enum ConfigurationPageMode { MatOption, MatLabel, NgTemplateOutlet, + MatIcon, + MatFabButton, ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', @@ -44,39 +54,53 @@ export class ConfigurationComponent { configurationApi = inject(ConfigurationApi); pageMode = signal<ConfigurationPageMode>(ConfigurationPageMode.READ); + configurationOptions = signal<Partial<Configuration>[]>([]); + chosenConfiguration = signal<string | undefined>(undefined); + isLoading = signal<boolean>(true); + configuration = signal<Configuration | undefined>(undefined); - configurationOptions = resource({ - loader: async () => { - return await firstValueFrom( - this.configurationApi.fetch().pipe( - tap(configurations => { - const appliedConfiguration = configurations.find(c => c.is_applied); - if (appliedConfiguration) { - this.chosenConfiguration.set(appliedConfiguration.id); - } - }), - map(configurations => - configurations.map(configuration => { - return { - id: configuration.id, - name: - configuration.name + (configuration.is_applied ? '*' : ''), - }; - }) - ) - ) - ); - }, - }); + constructor() { + effect(() => { + this.isLoading.set(true); + const chosenConfiguration = this.chosenConfiguration(); + if (chosenConfiguration) { + this.configurationApi.find(chosenConfiguration).subscribe(config => { + this.isLoading.set(false); + this.configuration.set(config); + }); + } + }); - chosenConfiguration = signal<string | undefined>(undefined); + afterNextRender(() => { + this.fetchConfigurationOptions().subscribe(configurations => { + const appliedConfiguration = configurations.find(c => c.is_applied); + if (appliedConfiguration) { + this.chosenConfiguration.set(appliedConfiguration.id); + } + this.configurationOptions.set(configurations); + }); + }); + } + + 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); + }); + }); + } - configuration = resource({ - request: () => this.chosenConfiguration(), - loader: async ({ request }) => { - return await firstValueFrom(this.configurationApi.find(request)); - }, - }); + onApplyConfiguration() { + this.configurationApi.apply(this.chosenConfiguration()!).subscribe(() => { + this.fetchConfigurationOptions().subscribe(configurations => + this.configurationOptions.set(configurations) + ); + }); + } onChangeConfiguration(configId: string) { this.chosenConfiguration.set(configId); @@ -88,5 +112,21 @@ export class ConfigurationComponent { this.pageMode.set(ConfigurationPageMode.READ); } + private fetchConfigurationOptions(): Observable<Partial<Configuration>[]> { + this.isLoading.set(true); + return this.configurationApi.fetch().pipe( + map(configurations => + configurations.map(configuration => { + return { + id: configuration.id, + is_applied: configuration.is_applied, + name: configuration.name + (configuration.is_applied ? '*' : ''), + }; + }) + ), + tap(() => this.isLoading.set(false)) + ); + } + protected readonly ConfigurationPageMode = ConfigurationPageMode; } -- GitLab From 4de8ac4ed8394cbfdeba5b2dcb8142572dd16beb Mon Sep 17 00:00:00 2001 From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com> Date: Thu, 20 Feb 2025 22:38:35 +0100 Subject: [PATCH 13/13] FE-3 Update select options in Edit mode --- .../configuration-form.component.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/components/configuration-form/configuration-form.component.ts b/src/app/components/configuration-form/configuration-form.component.ts index 0857f29..0996c77 100644 --- a/src/app/components/configuration-form/configuration-form.component.ts +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -57,6 +57,8 @@ export class ConfigurationFormComponent implements OnInit { ngOnInit() { this.form = this.configurationFormBuilder.create(this.configuration()); + this.updateSizeRangeOptions(); + this.updateProtocolOptions(); } onSubmit() { @@ -108,17 +110,16 @@ export class ConfigurationFormComponent implements OnInit { } updateSizeRangeOptions() { - const chosenRanges = this.form.controls.frame_ranges.value; - if (!chosenRanges) return; + const chosenRanges = this.form.controls.frame_ranges.value ?? []; const sizeRanges = SizeRanges.filter( - range => !chosenRanges.includes(range) + range => + !chosenRanges.some(cr => cr[0] === range[0] && cr[1] === range[1]) ); this.sizeRangeOptions.set(sizeRanges); } updateProtocolOptions() { - const chosenProtocols = this.form.controls.protocols.value; - if (!chosenProtocols) return; + const chosenProtocols = this.form.controls.protocols.value ?? []; const protocols = Protocols.filter( protocol => !chosenProtocols.includes(protocol) ); -- GitLab