Procházet zdrojové kódy

Angular: WIP - Navigation, Server Data Page

Christian Kahlau před 3 roky
rodič
revize
08eb7370f5

+ 5 - 0
ng/package.json

@@ -18,6 +18,11 @@
     "@angular/platform-browser": "^14.2.0",
     "@angular/platform-browser-dynamic": "^14.2.0",
     "@angular/router": "^14.2.0",
+    "@fortawesome/angular-fontawesome": "^0.11.1",
+    "@fortawesome/fontawesome-svg-core": "^6.1.1",
+    "@fortawesome/free-brands-svg-icons": "^6.1.1",
+    "@fortawesome/free-regular-svg-icons": "^6.1.1",
+    "@fortawesome/free-solid-svg-icons": "^6.1.1",
     "rxjs": "~7.5.0",
     "tslib": "^2.3.0",
     "zone.js": "~0.11.4"

+ 3 - 2
ng/proxy-conf.json

@@ -1,7 +1,8 @@
 [
   {
-    "context": ["/"],
+    "context": ["/api"],
     "target": "http://10.8.0.1:8880",
-    "secure": false
+    "secure": false,
+    "pathRewrite": { "^/api": "" }
   }
 ]

+ 6 - 2
ng/src/app/app-routing.module.ts

@@ -1,8 +1,12 @@
 import { NgModule } from '@angular/core';
 import { RouterModule, Routes } from '@angular/router';
-import { HomeComponent } from './pages/home/home.component';
+import { HomePageComponent } from './pages/home-page/home-page.component';
+import { ServerDataPageComponent } from './pages/server-data-page/server-data-page.component';
 
-const routes: Routes = [{ path: '', pathMatch: 'full', component: HomeComponent }];
+const routes: Routes = [
+  { path: '', pathMatch: 'full', component: HomePageComponent },
+  { path: 'srv/:id', component: ServerDataPageComponent }
+];
 
 @NgModule({
   imports: [RouterModule.forRoot(routes)],

+ 38 - 2
ng/src/app/app.component.html

@@ -1,5 +1,41 @@
 <app-header></app-header>
 
-<div class="container pt-5">
-  <router-outlet></router-outlet>
+<div class="container pt-5 d-flex flex-row">
+  <ul class="nav flex-column">
+    <li>
+      <div class="card" style="min-width: 200px">
+        <div class="card-header btn btn-toolbar bg-primary text-light" routerLink="/">
+          <fa-icon [icon]="fa.faChalkboard" class="pe-2"></fa-icon>
+          <span class="flex-fill text-start">Dashboard</span>
+          <fa-icon [icon]="fa.faAngleRight"></fa-icon>
+        </div>
+      </div>
+    </li>
+    <li *ngFor="let server of serverConfigs">
+      <div class="card">
+        <div class="card-header btn btn-toolbar bg-primary text-light" [routerLink]="'/srv/' + server.id">
+          <fa-icon [icon]="fa.faServer" class="pe-2"></fa-icon>
+          <span class="flex-fill text-start">{{ server.title }}</span>
+          <fa-icon [icon]="fa.faAngleRight" class="caret-rotate"></fa-icon>
+        </div>
+        <div class="card-body" aria-expanded="true">
+          <h5 class="border-bottom pb-2">Server metrics:</h5>
+          <ul class="nav flex-column">
+            <li *ngFor="let type of server.types" class="nav-item list-unstyled">
+              <fa-icon [icon]="type.type | faType" class="pe-2"></fa-icon>{{ type.type }}
+              <ul *ngIf="type.subtypes?.length" class="nav flex-column ps-4">
+                <li *ngFor="let sub of type.subtypes" class="nav-item list-unstyled">
+                  <fa-icon [icon]="type.type | faType" class="pe-2"></fa-icon>{{ sub.type }}
+                </li>
+              </ul>
+            </li>
+          </ul>
+        </div>
+      </div>
+    </li>
+  </ul>
+
+  <div class="flex-fill ps-3">
+    <router-outlet></router-outlet>
+  </div>
 </div>

+ 27 - 3
ng/src/app/app.component.ts

@@ -1,10 +1,34 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { ServerApiService } from './services/server-api.service';
+
+import { faAngleDown, faAngleRight, faChalkboard, faServer } from '@fortawesome/free-solid-svg-icons';
 
 @Component({
   selector: 'app-root',
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.scss']
 })
-export class AppComponent {
-  title = 'ng';
+export class AppComponent implements OnInit {
+  private subscriptions: Subscription[] = [];
+
+  public fa = { faAngleDown, faAngleRight, faChalkboard, faServer };
+  public serverConfigs: ServerConfig[] = [];
+
+  constructor(private apiService: ServerApiService) {
+    this.subscriptions.push(this.apiService.serverConfigs$.subscribe({ next: this.onServerConfigs.bind(this) }));
+  }
+
+  async ngOnInit() {
+    try {
+      const servers = await this.apiService.getAllServerConfigs();
+      servers.forEach(server => this.apiService.getServerDataTypes(server.id));
+    } catch (err) {
+      console.error(err);
+    }
+  }
+
+  private onServerConfigs(data: ServerConfig[]) {
+    this.serverConfigs = data;
+  }
 }

+ 6 - 3
ng/src/app/app.module.ts

@@ -5,11 +5,14 @@ import { BrowserModule } from '@angular/platform-browser';
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
 import { HeaderComponent } from './components/header/header.component';
-import { HomeComponent } from './pages/home/home.component';
+import { HomePageComponent } from './pages/home-page/home-page.component';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { FaByTypePipe } from './pipes/fa-by-type.pipe';
+import { ServerDataPageComponent } from './pages/server-data-page/server-data-page.component';
 
 @NgModule({
-  declarations: [AppComponent, HeaderComponent, HomeComponent],
-  imports: [AppRoutingModule, BrowserModule, HttpClientModule],
+  declarations: [AppComponent, HeaderComponent, HomePageComponent, FaByTypePipe, ServerDataPageComponent],
+  imports: [AppRoutingModule, BrowserModule, FontAwesomeModule, HttpClientModule],
   providers: [],
   bootstrap: [AppComponent]
 })

+ 18 - 0
ng/src/app/lib/indexed-subject.class.ts

@@ -0,0 +1,18 @@
+import { PartialObserver, ReplaySubject } from 'rxjs';
+
+export class IndexedReplaySubject<K, T> {
+  private index: Map<K, ReplaySubject<T>> = new Map();
+
+  get(key: K): ReplaySubject<T> {
+    let sub = this.index.get(key);
+    if (!sub) {
+      sub = new ReplaySubject(1);
+      this.index.set(key, sub);
+    }
+    return sub;
+  }
+
+  next(key: K, value: T) {
+    this.get(key).next(value);
+  }
+}

+ 0 - 0
ng/src/app/pages/home/home.component.html → ng/src/app/pages/home-page/home-page.component.html


+ 0 - 0
ng/src/app/pages/home/home.component.scss → ng/src/app/pages/home-page/home-page.component.scss


+ 6 - 7
ng/src/app/pages/home/home.component.spec.ts → ng/src/app/pages/home-page/home-page.component.spec.ts

@@ -1,18 +1,17 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { HomeComponent } from './home.component';
+import { HomePageComponent } from './home-page.component';
 
 describe('HomeComponent', () => {
-  let component: HomeComponent;
-  let fixture: ComponentFixture<HomeComponent>;
+  let component: HomePageComponent;
+  let fixture: ComponentFixture<HomePageComponent>;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      declarations: [ HomeComponent ]
-    })
-    .compileComponents();
+      declarations: [HomePageComponent]
+    }).compileComponents();
 
-    fixture = TestBed.createComponent(HomeComponent);
+    fixture = TestBed.createComponent(HomePageComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });

+ 4 - 4
ng/src/app/pages/home/home.component.ts → ng/src/app/pages/home-page/home-page.component.ts

@@ -1,13 +1,13 @@
 import { Component, OnInit } from '@angular/core';
 
-import { ServerApiService } from './../../services/server-api.service';
+import { ServerApiService } from '../../services/server-api.service';
 
 @Component({
   selector: 'app-home',
-  templateUrl: './home.component.html',
-  styleUrls: ['./home.component.scss']
+  templateUrl: './home-page.component.html',
+  styleUrls: ['./home-page.component.scss']
 })
-export class HomeComponent implements OnInit {
+export class HomePageComponent implements OnInit {
   public servers?: Array<
     Server & { types?: Array<ServerDataTypesConfig & { data?: ServerData[]; subtypes?: Array<ServerDataTypesConfig & { data?: ServerData[] }> }> }
   >;

+ 13 - 0
ng/src/app/pages/server-data-page/server-data-page.component.html

@@ -0,0 +1,13 @@
+<p>server-data-page works!</p>
+<p *ngIf="server">{{ server.title }}</p>
+
+<div *ngIf="types?.length">
+  <div *ngFor="let type of types">
+    <div *ngIf="type.subtypes?.length; else single">
+      <div *ngFor="let sub of type.subtypes">{{ type.type }}:{{ sub.type }} = {{ sub.data?.length || 0 }} Entries</div>
+    </div>
+    <ng-template #single>
+      <div>{{ type.type }} = {{ type.data?.length || 0 }} Entries</div>
+    </ng-template>
+  </div>
+</div>

+ 0 - 0
ng/src/app/pages/server-data-page/server-data-page.component.scss


+ 23 - 0
ng/src/app/pages/server-data-page/server-data-page.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ServerDataPageComponent } from './server-data-page.component';
+
+describe('ServerDataPageComponent', () => {
+  let component: ServerDataPageComponent;
+  let fixture: ComponentFixture<ServerDataPageComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ ServerDataPageComponent ]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(ServerDataPageComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 81 - 0
ng/src/app/pages/server-data-page/server-data-page.component.ts

@@ -0,0 +1,81 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { ActivationEnd, Router } from '@angular/router';
+import { filter, map, Subscription } from 'rxjs';
+
+import { ServerApiService } from 'src/app/services/server-api.service';
+
+@Component({
+  selector: 'app-server-data-page',
+  templateUrl: './server-data-page.component.html',
+  styleUrls: ['./server-data-page.component.scss']
+})
+export class ServerDataPageComponent implements OnInit, OnDestroy {
+  private subscriptions: Subscription[] = [];
+
+  public server?: ServerConfig;
+  public types?: ServerDataTypeWithData[];
+
+  constructor(private apiService: ServerApiService, router: Router) {
+    router.events.subscribe({
+      next: event => {
+        if (event instanceof ActivationEnd) {
+          this.clearSubscriptions();
+
+          const serverID = Number(event.snapshot.params['id']);
+          this.subscriptions.push(
+            apiService.serverConfigs$.pipe(map(servers => servers.find(s => s.id === serverID))).subscribe(this.onServerConfig.bind(this)),
+            apiService.serverDataTypes$
+              .get(serverID)
+              .pipe(filter(types => !!types))
+              .subscribe(this.onServerDataTypes.bind(this))
+          );
+        }
+      }
+    });
+  }
+
+  ngOnInit(): void {}
+
+  onServerConfig(server?: ServerConfig) {
+    if (server) {
+      this.server = server;
+    } else {
+      this.server = undefined;
+    }
+  }
+
+  onServerDataTypes(types: ServerDataTypeWithData[]) {
+    if (this.server) {
+      this.types = types.map(type => {
+        if (!type.subtypes) this.updateData(this.server as ServerConfig, type);
+        else {
+          type.subtypes.forEach(sub => {
+            this.updateData(this.server as ServerConfig, type, sub);
+          });
+        }
+        return type;
+      });
+    }
+  }
+
+  async updateData(server: ServerConfig, type: ServerDataTypeWithData, subType?: ServerDataTypeWithData) {
+    try {
+      const end = new Date();
+      const start = new Date(end.getTime() - 1000 * 60 * 60 * 4);
+      const data = await this.apiService.queryServerData(server.id, `${type.type}${subType ? `:${subType.type}` : ''}`, start, end);
+      if (subType) subType.data = data;
+      else type.data = data;
+    } catch (err) {
+      console.error(err);
+    }
+  }
+
+  ngOnDestroy(): void {
+    this.clearSubscriptions();
+  }
+
+  clearSubscriptions() {
+    this.subscriptions.forEach(s => s.unsubscribe());
+    this.subscriptions = [];
+  }
+}

+ 8 - 0
ng/src/app/pipes/fa-by-type.pipe.spec.ts

@@ -0,0 +1,8 @@
+import { FaByTypePipe } from './fa-by-type.pipe';
+
+describe('FaByTypePipe', () => {
+  it('create an instance', () => {
+    const pipe = new FaByTypePipe();
+    expect(pipe).toBeTruthy();
+  });
+});

+ 17 - 0
ng/src/app/pipes/fa-by-type.pipe.ts

@@ -0,0 +1,17 @@
+import { IconDefinition } from '@fortawesome/fontawesome-common-types';
+
+import { Pipe, PipeTransform } from '@angular/core';
+import { faQuestionCircle } from '@fortawesome/free-regular-svg-icons';
+import { faHardDrive as hdd, faMemory as ram, faMicrochip as cpu } from '@fortawesome/free-solid-svg-icons';
+
+@Pipe({
+  name: 'faType'
+})
+export class FaByTypePipe implements PipeTransform {
+  private map: { [key: string]: IconDefinition } = { ram, cpu, hdd };
+  transform(value: string | undefined | null): IconDefinition {
+    if (!value) return faQuestionCircle;
+
+    return this.map[value];
+  }
+}

+ 25 - 6
ng/src/app/services/server-api.service.ts

@@ -1,6 +1,10 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
-import { firstValueFrom, map } from 'rxjs';
+import { firstValueFrom, map, ReplaySubject } from 'rxjs';
+import { environment } from 'src/environments/environment';
+import { IndexedReplaySubject } from '../lib/indexed-subject.class';
+
+type ServerDataTypesSubject = ReplaySubject<ServerDataTypeWithData[]>;
 
 @Injectable({
   providedIn: 'root'
@@ -8,18 +12,33 @@ import { firstValueFrom, map } from 'rxjs';
 export class ServerApiService {
   constructor(private http: HttpClient) {}
 
-  public getAllServerConfigs() {
-    return firstValueFrom(this.http.get<Server[]>('/server'));
+  private servers: ServerConfig[] = [];
+
+  public serverConfigs$ = new ReplaySubject<ServerConfig[]>(1);
+  public serverDataTypes$ = new IndexedReplaySubject<number, ServerDataTypeWithData[]>();
+
+  public async getAllServerConfigs() {
+    this.servers = await firstValueFrom(this.http.get<Server[]>(environment.apiBaseUrl + 'server'));
+    this.serverConfigs$.next(this.servers);
+    return this.servers;
   }
 
-  public getServerDataTypes(serverID: number) {
-    return firstValueFrom(this.http.get<ServerDataTypesConfig[]>(`/server/${serverID}/data/types`));
+  public async getServerDataTypes(serverID: number) {
+    const types = await firstValueFrom(this.http.get<ServerDataTypesConfig[]>(`${environment.apiBaseUrl}server/${serverID}/data/types`));
+    const server = this.servers.find(s => s.id === serverID);
+    if (server) server.types = types;
+
+    this.serverDataTypes$.get(serverID).next(types);
+
+    return types;
   }
 
   public queryServerData(serverID: number, type: string, start: Date, end: Date) {
     return firstValueFrom(
       this.http
-        .get<QueryResponse<ServerData>>(`/server/${serverID}/data`, { params: { type, start: start.toString(), end: end.toString() } })
+        .get<QueryResponse<ServerData>>(`${environment.apiBaseUrl}server/${serverID}/data`, {
+          params: { type, start: start.toString(), end: end.toString() }
+        })
         .pipe(map(resp => resp.data.map(data => ({ ...data, time: new Date(data.time) }))))
     );
   }

+ 8 - 0
ng/src/app/types/server-config.d.ts

@@ -0,0 +1,8 @@
+type ServerConfig = Server & {
+  types?: Array<ServerDataTypeWithData>;
+};
+
+type ServerDataTypeWithData = ServerDataTypesConfig & {
+  data?: ServerData[];
+  subtypes?: Array<ServerDataTypesConfig & { data?: ServerData[] }>;
+};

+ 2 - 1
ng/src/environments/environment.prod.ts

@@ -1,3 +1,4 @@
 export const environment = {
-  production: true
+  production: true,
+  apiBaseUrl: '/'
 };

+ 2 - 1
ng/src/environments/environment.ts

@@ -3,7 +3,8 @@
 // The list of file replacements can be found in `angular.json`.
 
 export const environment = {
-  production: false
+  production: false,
+  apiBaseUrl: '/api/'
 };
 
 /*