diff --git a/package-lock.json b/package-lock.json index 347cf0590622cf40ad377963bc60d3021a6e6da6..e00ab1ff2f806704e71a481d74e1ae4d11852c03 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 19bc81ac924236d214f85509e39d04f61c735a4a..aa94cd0823438fb7def27972152e47ab9500b9ac 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/api/configuration.api.ts b/src/app/api/configuration.api.ts index 691912a047e8efae3d3d49be524a18cbdd1b7f82..064fd8503d330dfd8f8e5a438860fbe2ba3e064c 100644 --- a/src/app/api/configuration.api.ts +++ b/src/app/api/configuration.api.ts @@ -1,8 +1,55 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { map, Observable, tap } from 'rxjs'; +import { + Configuration, + ConfigurationDto, + configurationToDto, + dtoToConfiguration, +} from '../models/configuration'; +import { ApiResponse } from '../models/api-response'; 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<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< + ApiResponse<ConfigurationDto> + >(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/app.component.scss b/src/app/app.component.scss index c0b0c96217007634009491d119e61ef4421eb996..3112c3999012f8165db3d9682b94ec754415a45a 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/app.component.spec.ts b/src/app/app.component.spec.ts deleted file mode 100644 index 20a9c435dacb235ade1fe2ad42de43f2d5647ddd..0000000000000000000000000000000000000000 --- 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/app.config.ts b/src/app/app.config.ts index 35ca21e32aebc0e2cdd09ae2a6a3a0c3f7bea681..40e49cbd7de244f64cdea9137b81fb9edfb9e497 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -3,13 +3,17 @@ 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, withInterceptors } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync(), - providePrimeNG(), provideAnimationsAsync(), + provideHttpClient(withInterceptors([MockInterceptor])), + 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 0000000000000000000000000000000000000000..566160711f0f5c60abde951cf4ac0b7c759a5ebc --- /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 0000000000000000000000000000000000000000..40311a029f887efb3cf789fa1e716fddf4c70428 --- /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.ts b/src/app/components/button/button.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc2d400904cb38c0b53075e7e8d254c8c2b20f6b --- /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 0000000000000000000000000000000000000000..b785bf79cdb43c33990cfcdfa0b306bea5394dfb --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.builder.ts @@ -0,0 +1,87 @@ +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<{ + id: FormControl<string | undefined>; + name: FormControl<string>; + 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>; +}>; + +@Injectable({ + providedIn: 'root', +}) +export class ConfigurationFormBuilder { + formBuilder = inject(FormBuilder); + + create(configuration?: Configuration): ConfigurationForm { + const fb = this.formBuilder.nonNullable; + + return fb.group({ + id: fb.control<string | undefined>(configuration?.id), + name: fb.control<string>(configuration?.name || '', { + validators: [Validators.required], + }), + 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: [ + 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})$/), + ], + }), + }); + } + + toValue(form: ConfigurationForm): Configuration { + const data = form.value; + return <Configuration>{ + id: data.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 new file mode 100644 index 0000000000000000000000000000000000000000..4a1e25ba495884c5565a46a7d2d05f4c875f1a1c --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.component.html @@ -0,0 +1,114 @@ +<app-configuration-template> + <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"> + <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"> + @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 + [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"> + @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 new file mode 100644 index 0000000000000000000000000000000000000000..5e6b0cfaabdeb4ef4feea82e9427c9ccabebd075 --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.component.scss @@ -0,0 +1,31 @@ +@use '../../../vars'; +@use 'sass:map'; + +: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; + + &-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 new file mode 100644 index 0000000000000000000000000000000000000000..0996c777d0ac25d824cd26344fc76e5d9722e5e8 --- /dev/null +++ b/src/app/components/configuration-form/configuration-form.component.ts @@ -0,0 +1,128 @@ +import { + Component, + inject, + input, + OnInit, + output, + signal, +} from '@angular/core'; +import { ConfigurationTemplateComponent } from '../configuration-template/configuration-template.component'; +import { Configuration } from '../../models/configuration'; +import { + ConfigurationForm, + ConfigurationFormBuilder, + Protocols, + SizeRanges, +} from './configuration-form.builder'; +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'; +import { PillComponent } from '../pill/pill.component'; +import { ButtonComponent } from '../button/button.component'; + +@Component({ + selector: 'app-configuration-form', + imports: [ + ConfigurationTemplateComponent, + MatFormField, + MatInput, + MatLabel, + MatSelect, + MatOption, + ReactiveFormsModule, + PillComponent, + MatError, + MatHint, + ButtonComponent, + ], + templateUrl: './configuration-form.component.html', + styleUrl: './configuration-form.component.scss', +}) +export class ConfigurationFormComponent implements OnInit { + configurationFormBuilder = inject(ConfigurationFormBuilder); + configuration = input<Configuration>(); + form!: ConfigurationForm; + + submit = output<Configuration>(); + cancel = output<void>(); + + sizeRangeOptions = signal<[number, number][]>(SizeRanges); + protocolOptions = signal<string[]>(Protocols); + + ngOnInit() { + this.form = this.configurationFormBuilder.create(this.configuration()); + this.updateSizeRangeOptions(); + this.updateProtocolOptions(); + } + + 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); + } + } + + removeSourceMac(index: number) { + this.form.controls.source_mac.value?.splice(index, 1); + } + + addDestMac(destMac: string) { + if (!this.form.controls.dest_mac_control.errors) { + 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 ?? []; + const sizeRanges = SizeRanges.filter( + range => + !chosenRanges.some(cr => cr[0] === range[0] && cr[1] === range[1]) + ); + this.sizeRangeOptions.set(sizeRanges); + } + + updateProtocolOptions() { + const chosenProtocols = this.form.controls.protocols.value ?? []; + const protocols = Protocols.filter( + protocol => !chosenProtocols.includes(protocol) + ); + this.protocolOptions.set(protocols); + } +} 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 0000000000000000000000000000000000000000..98213f63c0c5a229ff79d1dbdaafa6dac5fb35cf --- /dev/null +++ b/src/app/components/configuration-template/configuration-template.component.html @@ -0,0 +1,25 @@ +<div class="configuration-header"> + <ng-content /> +</div> +<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 0000000000000000000000000000000000000000..8566472fecdaa1a4313fe03cf1a77f5fb642a631 --- /dev/null +++ b/src/app/components/configuration-template/configuration-template.component.scss @@ -0,0 +1,31 @@ +@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.ts b/src/app/components/configuration-template/configuration-template.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..d6f45a2ae79cb75f4cc59ddaea46e445c3975ff5 --- /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 8434b8c17cf0b0eb651f46dd6fd926415b7c24b9..6c6e9ab6e887126be45eac4dd694739630150ba8 100644 --- a/src/app/components/configuration/configuration.component.html +++ b/src/app/components/configuration/configuration.component.html @@ -1,6 +1,130 @@ <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"> + @switch (pageMode()) { + @case (ConfigurationPageMode.READ) { + <app-button (click)="pageMode.set(ConfigurationPageMode.CREATE)" + >New profile</app-button + > + } + } + </div> + + <ng-template #configurationSelect> + <mat-form-field> + <mat-label>Configuration</mat-label> + <mat-select + [value]="chosenConfiguration()" + (valueChange)="onChangeConfiguration($event)"> + @for (config of configurationOptions(); track $index) { + <mat-option [value]="config.id">{{ config.name }}</mat-option> + } + </mat-select> + </mat-form-field> + </ng-template> + + @if (isLoading()) { + <app-configuration-template> + <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"> + <ngx-skeleton-loader count="7" /> + </div> + </div> + <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="10" /> + </div> + <div protocols> + <ngx-skeleton-loader count="10" /> + </div> + </app-configuration-template> + } @else { + @switch (pageMode()) { + @case (ConfigurationPageMode.CREATE) { + <app-configuration-form + (cancel)="pageMode.set(ConfigurationPageMode.READ)" + (submit)="onSubmitConfiguration($event)" /> + } + @case (ConfigurationPageMode.EDIT) { + <app-configuration-form + [configuration]="configuration()!" + (cancel)="pageMode.set(ConfigurationPageMode.READ)" + (submit)="onSubmitConfiguration($event)" /> + } + @default { + <app-configuration-template> + <div class="configuration__header"> + <ng-container *ngTemplateOutlet="configurationSelect" /> + + <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()?.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) { + <app-pill>{{ mac }}</app-pill> + } @empty { + <app-pill>All addresses</app-pill> + } + </div> + </div> + <div frames> + @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()?.protocols; track $index) { + <app-pill>{{ protocol }}</app-pill> + } @empty { + <app-pill>None</app-pill> + } + </div> + </app-configuration-template> + } + } + } </app-page-wrapper> diff --git a/src/app/components/configuration/configuration.component.scss b/src/app/components/configuration/configuration.component.scss index 2995308e841ca96f9de6435ee45baad2167aa6e6..efe7e855928bbd6baf4cb9c9a24f72f527e26c09 100644 --- a/src/app/components/configuration/configuration.component.scss +++ b/src/app/components/configuration/configuration.component.scss @@ -1,4 +1,64 @@ +@use '../../../vars'; +@use 'sass:map'; + :host { height: 100%; width: 100%; + + .actions { + display: flex; + 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; + gap: map.get(vars.$spacing, 'md'); + + ::ng-deep * { + height: 50px; + } + } + + .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; + + &-title { + color: map.get(vars.$grey, 100); + font-size: map.get(vars.$text, 'lg'); + } + + &-content { + width: calc(100% - 200px); + } + } + } + + // 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.spec.ts b/src/app/components/configuration/configuration.component.spec.ts deleted file mode 100644 index a719df815f26937c9c5088f6b488339846c5277a..0000000000000000000000000000000000000000 --- 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/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts index 6cde6f1c642627c033bb2948d159e3975cde5268..11789edec83a31d97f37799fe86ffc9ff419990a 100644 --- a/src/app/components/configuration/configuration.component.ts +++ b/src/app/components/configuration/configuration.component.ts @@ -1,11 +1,132 @@ -import { Component } from '@angular/core'; +import { + afterNextRender, + Component, + effect, + inject, + signal, +} from '@angular/core'; import { PageWrapperComponent } from '../page-wrapper/page-wrapper.component'; -import { Skeleton } from 'primeng/skeleton'; +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 { map, Observable, tap } from 'rxjs'; +import { PillComponent } from '../pill/pill.component'; +import { SkeletonModule } from 'primeng/skeleton'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +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'; +import { MatFabButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; + +enum ConfigurationPageMode { + READ, + EDIT, + CREATE, +} @Component({ selector: 'app-configuration', - imports: [PageWrapperComponent, Skeleton], + imports: [ + PageWrapperComponent, + ConfigurationTemplateComponent, + ConfigurationFormComponent, + ButtonComponent, + PillComponent, + SkeletonModule, + NgxSkeletonLoaderModule, + FormsModule, + MatFormField, + MatSelect, + MatOption, + MatLabel, + NgTemplateOutlet, + MatIcon, + MatFabButton, + ], templateUrl: './configuration.component.html', styleUrl: './configuration.component.scss', }) -export class ConfigurationComponent {} +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); + + 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); + }); + } + }); + + 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); + }); + }); + } + + onApplyConfiguration() { + this.configurationApi.apply(this.chosenConfiguration()!).subscribe(() => { + this.fetchConfigurationOptions().subscribe(configurations => + this.configurationOptions.set(configurations) + ); + }); + } + + onChangeConfiguration(configId: string) { + this.chosenConfiguration.set(configId); + } + + onSubmitConfiguration(configuration: Configuration) { + console.log('Submitting configuration', configuration); + this.configurationApi.save(configuration); + 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; +} diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index cb0f2015d2c2aaeed5adaaed87a58cd8936d3ac8..d8bafe617dbe0d21b31c96bb7e223559926dbdee 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/dashboard/dashboard.component.spec.ts b/src/app/components/dashboard/dashboard.component.spec.ts deleted file mode 100644 index 30e39a2a4ce50c5ba05a66e706c22216e8278472..0000000000000000000000000000000000000000 --- 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.scss b/src/app/components/header/header.component.scss index c5a236d8118f89f8ce05b4308c1ef74320704183..194ccfaaff95f29d52a4a4f751d863e6ea45a759 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/header/header.component.spec.ts b/src/app/components/header/header.component.spec.ts deleted file mode 100644 index 204ed6e4b7fd0c0aa28ef4fd67a8097ac22272cd..0000000000000000000000000000000000000000 --- 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 5b65d9eadd6d55f6868f3a788f6606dad682ab28..0000000000000000000000000000000000000000 --- 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.html b/src/app/components/page-wrapper/page-wrapper.component.html index 60cb6e288d65ee39b7e4e70d39666827f3ec1ca7..fae4904e34fac12ee085461926655beb2b7366aa 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 1a8364713177fdbf9d329e860999f5b009ca4a94..ab5c237d1ca95efeca1f74759bf130e2079d4f03 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/page-wrapper/page-wrapper.component.spec.ts b/src/app/components/page-wrapper/page-wrapper.component.spec.ts deleted file mode 100644 index 73525cc5b2ec289ef51c9f3d96b9ac8995d23f3b..0000000000000000000000000000000000000000 --- 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.html b/src/app/components/pill/pill.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0548d4578c1da8e55ffe8bfe375fd681e0befc5c --- /dev/null +++ b/src/app/components/pill/pill.component.html @@ -0,0 +1,6 @@ +<div class="pill"> + <ng-content /> + @if (removable()) { + <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 new file mode 100644 index 0000000000000000000000000000000000000000..ac096b3fa2e8ce3c40f9075d432610c825631338 --- /dev/null +++ b/src/app/components/pill/pill.component.scss @@ -0,0 +1,17 @@ +@use '../../../vars'; +@use 'sass:map'; + +.pill { + 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); + 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'); + 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 new file mode 100644 index 0000000000000000000000000000000000000000..f43c8ace3b9566369df724bce387c1871662b6be --- /dev/null +++ b/src/app/components/pill/pill.component.ts @@ -0,0 +1,13 @@ +import { Component, input, output } 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<boolean | null>(null); + clickRemove = output<void>(); +} 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 25f799c1d0518fa961fcfb038eb94618951ca913..0000000000000000000000000000000000000000 --- 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(); - }); -}); diff --git a/src/app/components/sidenav/sidenav.component.ts b/src/app/components/sidenav/sidenav.component.ts index 2fd9c08614d71573be1f94baf716b6d68402d51b..a630fe2959e7f2106957b7664ff0dc3063d3151d 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(); }); } } diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts new file mode 100644 index 0000000000000000000000000000000000000000..2dfdedf6c4fbf82c4cc97a526eb7533b346baef7 --- /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 0000000000000000000000000000000000000000..d1ae561c1d7b35f3c2ae8fb39c14f7865087fc90 --- /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 0000000000000000000000000000000000000000..6e971313aa0358f2068b1a5eebec1e90d8cb8ba7 --- /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: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_ranges": [ + [0, 63], + [128, 255], + [1024, 1518] + ], + "protocols": ["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 0000000000000000000000000000000000000000..d18ca9470f7c6dc9b350038ae2edee502e66b3f7 --- /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_ranges": [], + "protocols": [] + } +} diff --git a/src/app/interceptor/mocks/configurations.json b/src/app/interceptor/mocks/configurations.json new file mode 100644 index 0000000000000000000000000000000000000000..b1ed282448df746db14186a2c1aae8cfe1f9a6dc --- /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: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_ranges": [ + [0, 63], + [128, 255], + [1024, 1518] + ], + "protocols": ["IPv4", "IP6", "UDP"] + }, + { + "id": "2def", + "name": "Everything", + "is_applied": true, + "mac_source": [], + "mac_destination": [], + "frame_ranges": [], + "protocols": [] + } + ] +} diff --git a/src/app/models/.gitkeep b/src/app/models/.gitkeep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/app/models/api-response.ts b/src/app/models/api-response.ts new file mode 100644 index 0000000000000000000000000000000000000000..3db5cbb43eef40a5bca74d47231a8d4c7f73676c --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..8424ba4169009534cb22bf058708fd0f8a4a592e --- /dev/null +++ b/src/app/models/configuration.ts @@ -0,0 +1,21 @@ +export type Configuration = ConfigurationDto; + +export interface ConfigurationDto { + id: string; + name: string; + is_applied: boolean; + mac_source: string[]; + mac_destination: string[]; + frame_ranges: [number, number][]; + protocols: string[]; +} + +export function dtoToConfiguration(dto: ConfigurationDto): Configuration { + return dto; +} + +export function configurationToDto( + configuration: Configuration +): ConfigurationDto { + return configuration; +} diff --git a/src/vars.scss b/src/vars.scss index 4cdf01f51db35fe3ea0c2cba5cdaf3c28c598b9a..9a4c50ca8dd75a3fe532f2efb03700e666a1613f 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, );