From 45c04e51ad86e8398e736b9c971b026ee32bbab7 Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Sun, 30 Mar 2025 13:48:01 +0200
Subject: [PATCH 1/2] FE-10 Integrate configuration page with API

---
 docker-compose.yml                            |  2 +-
 src/app/api/configuration.api.ts              | 20 ++++---
 .../configuration.component.html              | 36 +++++++------
 .../configuration/configuration.component.ts  | 52 +++++++++++++++----
 src/app/interceptor/interceptor.ts            | 15 ------
 .../interceptor/mocks/configuration-1.json    | 24 ---------
 .../interceptor/mocks/configuration-2.json    | 11 ----
 src/app/interceptor/mocks/configurations.json | 35 -------------
 src/app/models/configuration.ts               | 26 ++++++++--
 src/proxy.conf.json                           | 10 ++++
 10 files changed, 106 insertions(+), 125 deletions(-)
 delete mode 100644 src/app/interceptor/mocks/configuration-1.json
 delete mode 100644 src/app/interceptor/mocks/configuration-2.json
 delete mode 100644 src/app/interceptor/mocks/configurations.json
 create mode 100644 src/proxy.conf.json

diff --git a/docker-compose.yml b/docker-compose.yml
index 0dd09cd..05d5a6a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,7 +6,7 @@ services:
       - node_modules:/app/node_modules
     ports:
       - "4200:4200"
-    command: ["npm", "run", "start", "--", "--host", "0.0.0.0", "--poll=2000"]
+    command: ["npm", "run", "start", "--", "--host", "0.0.0.0", "--poll=2000", "--proxy-config", "proxy.conf.json"]
 
 volumes:
   node_modules:
diff --git a/src/app/api/configuration.api.ts b/src/app/api/configuration.api.ts
index 45e9943..e5c08bc 100644
--- a/src/app/api/configuration.api.ts
+++ b/src/app/api/configuration.api.ts
@@ -1,6 +1,6 @@
 import { inject, Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
-import { map, mergeMap, Observable, tap } from 'rxjs';
+import { map, mergeMap, Observable } from 'rxjs';
 import {
   Configuration,
   ConfigurationDto,
@@ -9,7 +9,7 @@ import {
 } from '../models/configuration';
 import { ApiResponse } from '../models/api-response';
 
-export const CONFIGURATION_API_URL = '/api/configuration';
+export const CONFIGURATION_API_URL = '/api/configurations';
 
 @Injectable({
   providedIn: 'root',
@@ -47,11 +47,19 @@ export class ConfigurationApi {
       );
   }
 
-  find(configName: string): Observable<Configuration> {
+  find(configId: string): Observable<Configuration> {
     return this.httpClient
       .get<
         ApiResponse<ConfigurationDto>
-      >(`${CONFIGURATION_API_URL}/${configName}`)
+      >(`${CONFIGURATION_API_URL}/${configId}`)
+      .pipe(map(dto => dtoToConfiguration(dto.data)));
+  }
+
+  add(configuration: Configuration): Observable<Configuration> {
+    return this.httpClient
+      .post<
+        ApiResponse<ConfigurationDto>
+      >(CONFIGURATION_API_URL, configurationToDto(configuration))
       .pipe(map(dto => dtoToConfiguration(dto.data)));
   }
 
@@ -59,13 +67,13 @@ export class ConfigurationApi {
     return this.httpClient
       .put<
         ApiResponse<ConfigurationDto>
-      >(CONFIGURATION_API_URL, configurationToDto(configuration))
+      >(`${CONFIGURATION_API_URL}/${configuration.id}`, configurationToDto(configuration))
       .pipe(map(dto => dtoToConfiguration(dto.data)));
   }
 
   apply(configurationId: string): Observable<Configuration> {
     return this.httpClient
-      .put<
+      .post<
         ApiResponse<ConfigurationDto>
       >(`${CONFIGURATION_API_URL}/${configurationId}/apply`, {})
       .pipe(map(dto => dtoToConfiguration(dto.data)));
diff --git a/src/app/components/configuration/configuration.component.html b/src/app/components/configuration/configuration.component.html
index 6c6e9ab..ef7e79f 100644
--- a/src/app/components/configuration/configuration.component.html
+++ b/src/app/components/configuration/configuration.component.html
@@ -15,7 +15,7 @@
     <mat-form-field>
       <mat-label>Configuration</mat-label>
       <mat-select
-        [value]="chosenConfiguration()"
+        [value]="chosenConfigurationId()"
         (valueChange)="onChangeConfiguration($event)">
         @for (config of configurationOptions(); track $index) {
           <mat-option [value]="config.id">{{ config.name }}</mat-option>
@@ -56,7 +56,7 @@
       @case (ConfigurationPageMode.CREATE) {
         <app-configuration-form
           (cancel)="pageMode.set(ConfigurationPageMode.READ)"
-          (submit)="onSubmitConfiguration($event)" />
+          (submit)="onAddConfiguration($event)" />
       }
       @case (ConfigurationPageMode.EDIT) {
         <app-configuration-form
@@ -70,23 +70,25 @@
             <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
+              @if (configuration()?.id) {
+                @if (!configuration()?.is_applied) {
+                  <button mat-fab extended (click)="onDeleteConfiguration()">
+                    <mat-icon>delete</mat-icon>
+                    Delete
+                  </button>
+                  <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>
               }
-              <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">
diff --git a/src/app/components/configuration/configuration.component.ts b/src/app/components/configuration/configuration.component.ts
index 910833b..b6dc83b 100644
--- a/src/app/components/configuration/configuration.component.ts
+++ b/src/app/components/configuration/configuration.component.ts
@@ -55,16 +55,16 @@ export class ConfigurationComponent implements OnInit {
 
   pageMode = signal<ConfigurationPageMode>(ConfigurationPageMode.READ);
   configurationOptions = signal<Partial<Configuration>[]>([]);
-  chosenConfiguration = signal<string | undefined>(undefined);
+  chosenConfigurationId = 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 => {
+      const chosenConfigurationId = this.chosenConfigurationId();
+      if (chosenConfigurationId) {
+        this.configurationApi.find(chosenConfigurationId).subscribe(config => {
           this.isLoading.set(false);
           this.configuration.set(config);
         });
@@ -77,13 +77,24 @@ export class ConfigurationComponent implements OnInit {
   }
 
   onDeleteConfiguration() {
-    this.configurationApi.delete(this.chosenConfiguration()!).subscribe(() => {
+    const configId = this.chosenConfigurationId();
+    if (!configId) return;
+
+    this.isLoading.set(true);
+    this.configurationApi.delete(configId).subscribe(() => {
       this.updateConfigurationOptions();
+      this.isLoading.set(false);
+
+      // no applied config -> set default view
+      this.configuration.set(undefined);
     });
   }
 
   onApplyConfiguration() {
-    this.configurationApi.apply(this.chosenConfiguration()!).subscribe(() => {
+    const configId = this.chosenConfigurationId();
+    if (!configId) return;
+
+    this.configurationApi.apply(configId).subscribe(() => {
       this.fetchConfigurationOptions().subscribe(configurations =>
         this.configurationOptions.set(configurations)
       );
@@ -91,13 +102,32 @@ export class ConfigurationComponent implements OnInit {
   }
 
   onChangeConfiguration(configId: string) {
-    this.chosenConfiguration.set(configId);
+    this.chosenConfigurationId.set(configId);
+  }
+
+  onAddConfiguration(configuration: Configuration) {
+    this.isLoading.set(true);
+
+    this.configurationApi.add(configuration).subscribe(config => {
+      this.configuration.set(config);
+      this.configurationOptions.update(configs => [...configs, config]);
+      this.chosenConfigurationId.set(config.id);
+
+      this.isLoading.set(false);
+      this.pageMode.set(ConfigurationPageMode.READ);
+    });
   }
 
   onSubmitConfiguration(configuration: Configuration) {
-    console.log('Submitting configuration', configuration);
-    this.configurationApi.save(configuration);
-    this.pageMode.set(ConfigurationPageMode.READ);
+    this.isLoading.set(true);
+
+    this.configurationApi.save(configuration).subscribe(config => {
+      this.configuration.set(config);
+      this.updateConfigurationOptions();
+
+      this.isLoading.set(false);
+      this.pageMode.set(ConfigurationPageMode.READ);
+    });
   }
 
   private fetchConfigurationOptions(): Observable<Partial<Configuration>[]> {
@@ -120,7 +150,7 @@ export class ConfigurationComponent implements OnInit {
     this.fetchConfigurationOptions().subscribe(configurations => {
       const appliedConfiguration = configurations.find(c => c.is_applied);
       if (appliedConfiguration) {
-        this.chosenConfiguration.set(appliedConfiguration.id);
+        this.chosenConfigurationId.set(appliedConfiguration.id);
       }
       this.configurationOptions.set(configurations);
     });
diff --git a/src/app/interceptor/interceptor.ts b/src/app/interceptor/interceptor.ts
index 6cab5d5..e7b0470 100644
--- a/src/app/interceptor/interceptor.ts
+++ b/src/app/interceptor/interceptor.ts
@@ -1,6 +1,3 @@
-import * as configurationList from './mocks/configurations.json';
-import * as configuration1 from './mocks/configuration-1.json';
-import * as configuration2 from './mocks/configuration-2.json';
 import * as framesCurrent from './mocks/frames-current.json';
 import * as framesHistorical from './mocks/frames-historical.json';
 import * as irCurrent from './mocks/ir-current.json';
@@ -11,18 +8,6 @@ import * as dashboardStats from './mocks/dashboard-statistics.json';
 import * as dashboardStats2 from './mocks/dashboard-statistics-2.json';
 
 export const urls = [
-  {
-    url: '/api/configuration/1abc',
-    json: () => configuration1,
-  },
-  {
-    url: '/api/configuration/2def',
-    json: () => configuration2,
-  },
-  {
-    url: '/api/configuration',
-    json: () => configurationList,
-  },
   {
     url: '/api/statistics/frames/current',
     json: () => framesCurrent,
diff --git a/src/app/interceptor/mocks/configuration-1.json b/src/app/interceptor/mocks/configuration-1.json
deleted file mode 100644
index 93c1d9b..0000000
--- a/src/app/interceptor/mocks/configuration-1.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
-  "data": {
-    "id": "1abc",
-    "name": "Custom",
-    "is_applied": true,
-    "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", "IPv6", "UDP"]
-  }
-}
diff --git a/src/app/interceptor/mocks/configuration-2.json b/src/app/interceptor/mocks/configuration-2.json
deleted file mode 100644
index f047b8b..0000000
--- a/src/app/interceptor/mocks/configuration-2.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "data": {
-    "id": "2def",
-    "name": "Everything",
-    "is_applied": false,
-    "mac_source": [],
-    "mac_destination": [],
-    "frame_ranges": [],
-    "protocols": []
-  }
-}
diff --git a/src/app/interceptor/mocks/configurations.json b/src/app/interceptor/mocks/configurations.json
deleted file mode 100644
index e7de125..0000000
--- a/src/app/interceptor/mocks/configurations.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
-  "data": [
-    {
-      "id": "1abc",
-      "name": "Custom",
-      "is_applied": true,
-      "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", "IPv6", "UDP"]
-    },
-    {
-      "id": "2def",
-      "name": "Everything",
-      "is_applied": false,
-      "mac_source": [],
-      "mac_destination": [],
-      "frame_ranges": [],
-      "protocols": []
-    }
-  ]
-}
diff --git a/src/app/models/configuration.ts b/src/app/models/configuration.ts
index 8424ba4..e3b631a 100644
--- a/src/app/models/configuration.ts
+++ b/src/app/models/configuration.ts
@@ -1,6 +1,4 @@
-export type Configuration = ConfigurationDto;
-
-export interface ConfigurationDto {
+export interface Configuration {
   id: string;
   name: string;
   is_applied: boolean;
@@ -10,12 +8,30 @@ export interface ConfigurationDto {
   protocols: string[];
 }
 
+export interface ConfigurationDto {
+  id: string;
+  name: string;
+  is_applied: boolean;
+  source_mac: string[];
+  destination_mac: string[];
+  frame_ranges: [number, number][];
+  protocols: string[];
+}
+
 export function dtoToConfiguration(dto: ConfigurationDto): Configuration {
-  return dto;
+  return {
+    ...dto,
+    mac_source: dto.source_mac,
+    mac_destination: dto.destination_mac,
+  };
 }
 
 export function configurationToDto(
   configuration: Configuration
 ): ConfigurationDto {
-  return configuration;
+  return {
+    ...configuration,
+    source_mac: configuration.mac_source,
+    destination_mac: configuration.mac_destination,
+  };
 }
diff --git a/src/proxy.conf.json b/src/proxy.conf.json
new file mode 100644
index 0000000..8c73ea9
--- /dev/null
+++ b/src/proxy.conf.json
@@ -0,0 +1,10 @@
+{
+  "/api": {
+    "target": "http://localhost:5000",
+    "secure": false,
+    "changeOrigin": true,
+    "pathRewrite": {
+      "^/api": ""
+    }
+  }
+}
-- 
GitLab


From 455eed1e4c02cc6d9072414206c488da04889a14 Mon Sep 17 00:00:00 2001
From: Ruslan Rabadanov <ruslanrabadanov2101@gmail.com>
Date: Sun, 30 Mar 2025 14:08:51 +0200
Subject: [PATCH 2/2] FE-10 Fix communication with API through docker container

---
 docker-compose.yml | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index 05d5a6a..6263ed3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,12 +1,11 @@
 services:
   miars-ng:
     build: .
+    network_mode: "host"
     volumes:
       - .:/app
       - node_modules:/app/node_modules
-    ports:
-      - "4200:4200"
-    command: ["npm", "run", "start", "--", "--host", "0.0.0.0", "--poll=2000", "--proxy-config", "proxy.conf.json"]
+    command: ["npm", "run", "start", "--", "--host", "0.0.0.0", "--poll=2000", "--proxy-config", "src/proxy.conf.json"]
 
 volumes:
   node_modules:
-- 
GitLab