Преглед на файлове

Server + ng: Implemented Dashboard & reinvented navigation to server data pages

Christian Kahlau преди 3 години
родител
ревизия
2342676b4d

+ 0 - 34
ng/src/app/app.component.html

@@ -1,40 +1,6 @@
 <app-header></app-header>
 
 <div class="container pt-5">
-  <ul class="nav flex-row">
-    <li class="flex-fill">
-      <div class="card h-100">
-        <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 [class.hidden]="currentUrl !== '/'" [icon]="fa.faAngleDown" class="ps-2"></fa-icon>
-        </div>
-        <div class="card-body p-1">- soon come -</div>
-      </div>
-    </li>
-    <li *ngFor="let server of serverConfigs" class="flex-fill">
-      <div class="card h-100">
-        <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 [class.hidden]="currentUrl !== '/srv/' + server.id" [icon]="fa.faAngleDown" class="ps-2"></fa-icon>
-        </div>
-        <div class="card-body p-1" aria-expanded="true">
-          <div *ngFor="let type of server.types" class="badge bg-primary me-1">
-            <div class="d-flex flex-column">
-              <div class="d-flex flex-row text-uppercase"><fa-icon [icon]="type.type | faType" class="pe-2 status-ok"></fa-icon>{{ type.type }}</div>
-              <ul *ngIf="type.subtypes" class="list-unstyled text-start subtypes-list">
-                <li *ngFor="let sub of type.subtypes" class="status-ok">
-                  {{ sub.type }}
-                </li>
-              </ul>
-            </div>
-          </div>
-        </div>
-      </div>
-    </li>
-  </ul>
-
   <div class="pt-3">
     <router-outlet></router-outlet>
   </div>

+ 0 - 38
ng/src/app/app.component.scss

@@ -1,38 +0,0 @@
-ul.nav {
-  .hidden {
-    visibility: hidden;
-  }
-
-  fa-icon {
-    &.status-ok {
-      color: var(--bs-light);
-    }
-    &.status-warn {
-      color: var(--bs-warning);
-    }
-    &.status-error {
-      color: var(--bs-danger);
-    }
-  }
-
-  .list-unstyled.subtypes-list {
-    padding-left: 0;
-
-    li::before {
-      content: '\25cf'; // ●
-      font-size: larger;
-    }
-
-    li.status-ok::before {
-      color: var(--bs-light);
-    }
-
-    li.status-warn::before {
-      color: var(--bs-warning);
-    }
-
-    li.status-error::before {
-      color: var(--bs-danger);
-    }
-  }
-}

+ 1 - 24
ng/src/app/app.component.ts

@@ -1,7 +1,4 @@
 import { Component, OnInit } from '@angular/core';
-import { ActivationEnd, Router } from '@angular/router';
-import { faAngleDown, faAngleRight, faChalkboard, faServer } from '@fortawesome/free-solid-svg-icons';
-import { filter, Subscription } from 'rxjs';
 
 import { ServerApiService } from './services/server-api.service';
 
@@ -11,23 +8,7 @@ import { ServerApiService } from './services/server-api.service';
   styleUrls: ['./app.component.scss']
 })
 export class AppComponent implements OnInit {
-  private subscriptions: Subscription[] = [];
-
-  public fa = { faAngleDown, faAngleRight, faChalkboard, faServer };
-  public serverConfigs: ServerConfig[] = [];
-
-  public currentUrl: string = '/';
-
-  constructor(private apiService: ServerApiService, router: Router) {
-    this.subscriptions.push(
-      this.apiService.serverConfigs$.subscribe({ next: this.onServerConfigs.bind(this) }),
-      router.events.pipe(filter(e => e instanceof ActivationEnd)).subscribe({
-        next: e => {
-          this.currentUrl = '/' + (e as ActivationEnd).snapshot.url.map(seg => seg.path).join('/');
-        }
-      })
-    );
-  }
+  constructor(private apiService: ServerApiService) {}
 
   async ngOnInit() {
     try {
@@ -37,8 +18,4 @@ export class AppComponent implements OnInit {
       console.error(err);
     }
   }
-
-  private onServerConfigs(data: ServerConfig[]) {
-    this.serverConfigs = data;
-  }
 }

+ 16 - 4
ng/src/app/app.module.ts

@@ -6,16 +6,28 @@ import { NgChartsModule } from 'ng2-charts';
 
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
+import { AdminPanelComponent } from './components/admin/admin-panel/admin-panel.component';
 import { HeaderComponent } from './components/header/header.component';
+import { ServerDataChartComponent } from './components/server-data-chart/server-data-chart.component';
+import { ServerMetricsWidgetComponent } from './components/server-metrics-widget/server-metrics-widget.component';
 import { HomePageComponent } from './pages/home-page/home-page.component';
-import { FaByTypePipe } from './pipes/fa-by-type.pipe';
 import { ServerDataPageComponent } from './pages/server-data-page/server-data-page.component';
+
 import { BytePipe } from './pipes/byte.pipe';
-import { ServerDataChartComponent } from './components/server-data-chart/server-data-chart.component';
-import { AdminPanelComponent } from './components/admin/admin-panel/admin-panel.component';
+import { FaByTypePipe } from './pipes/fa-by-type.pipe';
 
 @NgModule({
-  declarations: [AppComponent, BytePipe, FaByTypePipe, HeaderComponent, HomePageComponent, ServerDataChartComponent, ServerDataPageComponent, AdminPanelComponent],
+  declarations: [
+    AdminPanelComponent,
+    AppComponent,
+    BytePipe,
+    FaByTypePipe,
+    HeaderComponent,
+    HomePageComponent,
+    ServerDataChartComponent,
+    ServerDataPageComponent,
+    ServerMetricsWidgetComponent
+  ],
   imports: [AppRoutingModule, BrowserModule, FontAwesomeModule, HttpClientModule, NgChartsModule],
   providers: [{ provide: BytePipe, multi: false }],
   bootstrap: [AppComponent]

+ 25 - 0
ng/src/app/components/server-metrics-widget/server-metrics-widget.component.html

@@ -0,0 +1,25 @@
+<div
+  class="d-grid"
+  style="grid-template-columns: max-content auto"
+  [style.grid-template-rows]="'repeat(' + dataTypes.length + ')'"
+  [routerLink]="'/srv/' + server.id">
+  <ng-container *ngFor="let type of dataTypes; index as i">
+    <div class="d-inline text-uppercase flex-grow-0 pe-2" style="grid-column: 1 / span 1" [style.grid-row]="i + 1 + ' / span 1'">
+      <fa-icon [icon]="type.type | faType" class="pe-2 status-ok"></fa-icon>{{ type.type }}
+    </div>
+    <div class="pt-1 pb-1 h-100" style="grid-column: 2 / span 1" [style.grid-row]="i + 1 + ' / span 1'">
+      <div *ngIf="type.data; else loading" class="progress position-relative">
+        <div *ngIf="type.data.avg" class="progress-bar bg-avg" role="progressbar" [style.width]="type.data.avg + '%'"></div>
+        <div *ngIf="type.data.peak" class="progress-bar bg-peak" role="progressbar" [style.width]="type.data.peak - (type.data.avg || 0) + '%'"></div>
+        <span *ngIf="type.subtype" class="position-absolute end-0 pe-2">:{{ type.subtype }}</span>
+      </div>
+      <ng-template #loading>
+        <div class="progress">
+          <div class="progress-bar bg-progress progress-bar-striped progress-bar-animated text-primary" role="progressbar" style="width: 100%">
+            Loading data
+          </div>
+        </div>
+      </ng-template>
+    </div>
+  </ng-container>
+</div>

+ 0 - 0
ng/src/app/components/server-metrics-widget/server-metrics-widget.component.scss


+ 23 - 0
ng/src/app/components/server-metrics-widget/server-metrics-widget.component.spec.ts

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

+ 55 - 0
ng/src/app/components/server-metrics-widget/server-metrics-widget.component.ts

@@ -0,0 +1,55 @@
+import { Component, Input } from '@angular/core';
+import { faHardDrive } from '@fortawesome/free-solid-svg-icons';
+import { Subscription } from 'rxjs';
+
+import { ServerApiService } from 'src/app/services/server-api.service';
+
+type FlatServerDataTypes = {
+  type: string;
+  subtype?: string;
+  data?: ReducedValuesPerc;
+};
+
+@Component({
+  selector: 'app-server-metrics-widget',
+  templateUrl: './server-metrics-widget.component.html',
+  styleUrls: ['./server-metrics-widget.component.scss']
+})
+export class ServerMetricsWidgetComponent {
+  @Input() set server(server: ServerConfig) {
+    this._server = server;
+
+    if (!this._typeSubscription) {
+      this._typeSubscription = this.apiService.serverDataTypes$.get(server.id).subscribe({ next: this.onServerDataTypes.bind(this) });
+    }
+  }
+  public get server() {
+    return this._server;
+  }
+
+  public get dataTypes() {
+    return this._dataTypes;
+  }
+
+  public fa = { faHardDrive };
+
+  private _server!: ServerConfig;
+  private _dataTypes: FlatServerDataTypes[] = [];
+  private _typeSubscription?: Subscription;
+
+  constructor(private apiService: ServerApiService) {}
+
+  private async onServerDataTypes(types: ServerDataTypesConfig[]) {
+    this._dataTypes = types.reduce((res, type) => {
+      res.push(...(type.subtypes?.map(sub => ({ type: type.type, subtype: sub.type })) ?? [{ type: type.type }]));
+      return res;
+    }, [] as FlatServerDataTypes[]);
+
+    const end = new Date();
+    const start = new Date(end.getTime() - 1000 * 60 * 60 * 24);
+    for (const type of this._dataTypes) {
+      // Fetch 24h stats of this dataType
+      type.data = await this.apiService.queryServerStats(this.server.id, type.type + (type.subtype ? `:${type.subtype}` : ''), start, end);
+    }
+  }
+}

+ 36 - 1
ng/src/app/pages/home-page/home-page.component.html

@@ -1 +1,36 @@
-<p>Dashboard works!</p>
+<div
+  class="d-grid"
+  [style.grid-template-columns]="'250px repeat(' + (grid.columns - 1) + ', 1fr)'"
+  [style.grid-template-rows]="'repeat(' + grid.rows + ', max-content)'">
+  <div class="card-header btn btn-toolbar bg-light text-primary" routerLink="/" style="grid-column: 1 / span 1; grid-row: 1 / span 1">
+    <fa-icon [icon]="fa.faChalkboard" class="pe-2"></fa-icon>
+    <span class="flex-fill text-start">Dashboard</span>
+    <fa-icon [icon]="fa.faAngleRight" class="ps-2"></fa-icon>
+  </div>
+  <div class="card-header bg-primary text-light" style="grid-column: 2 / span 1; grid-row: 1 / span 1">
+    <fa-icon [icon]="fa.faServer" class="pe-2"></fa-icon>
+    <span class="flex-fill">Server Metrics (24h)</span>
+  </div>
+  <div class="card-header bg-primary text-light" style="grid-column: 3 / span 1; grid-row: 1 / span 1">
+    <fa-icon [icon]="fa.faServer" class="pe-2"></fa-icon>
+    <span class="flex-fill">Service Status</span>
+  </div>
+
+  <ng-container *ngFor="let server of serverConfigs; index as i">
+    <div class="card-header bg-primary text-light" style="grid-column: 1 / span 1" [style.grid-row]="i + 2 + ' / span 1'">
+      <fa-icon [icon]="fa.faServer" class="pe-2"></fa-icon>
+      <span class="flex-fill text-start">{{ server.title }}</span>
+    </div>
+
+    <app-server-metrics-widget
+      class="d-block p-1 border-end border-bottom text-primary pointer link-panel-highlight"
+      style="grid-column: 2 / span 1"
+      [style.grid-row]="i + 2 + ' / span 1'"
+      [server]="server">
+    </app-server-metrics-widget>
+
+    <div class="p-1 border-end border-bottom" style="grid-column: 3 / span 1" [style.grid-row]="i + 2 + ' / span 1'">
+      ... service checks soon come...
+    </div>
+  </ng-container>
+</div>

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

@@ -0,0 +1,3 @@
+.link-panel-highlight:hover {
+  background-color: #f6f7f9;
+}

+ 17 - 1
ng/src/app/pages/home-page/home-page.component.ts

@@ -1,4 +1,5 @@
 import { Component, OnInit } from '@angular/core';
+import { faAngleDown, faAngleRight, faChalkboard, faServer } from '@fortawesome/free-solid-svg-icons';
 
 import { ServerApiService } from '../../services/server-api.service';
 
@@ -8,7 +9,22 @@ import { ServerApiService } from '../../services/server-api.service';
   styleUrls: ['./home-page.component.scss']
 })
 export class HomePageComponent implements OnInit {
-  constructor(private serverApi: ServerApiService) {}
+  public fa = { faAngleDown, faAngleRight, faChalkboard, faServer };
+  public serverConfigs: ServerConfig[] = [];
+
+  public grid = {
+    columns: 3,
+    rows: 1
+  };
+
+  constructor(private apiService: ServerApiService) {
+    this.apiService.serverConfigs$.subscribe({ next: this.onServerConfigs.bind(this) });
+  }
 
   async ngOnInit(): Promise<void> {}
+
+  private onServerConfigs(data: ServerConfig[]) {
+    this.serverConfigs = data;
+    this.grid.rows = this.serverConfigs.length + 1;
+  }
 }

+ 10 - 0
ng/src/app/services/server-api.service.ts

@@ -47,4 +47,14 @@ export class ServerApiService {
         .pipe(map(resp => resp.data.map(data => ({ ...data, time: new Date(data.time) }))))
     );
   }
+
+  public queryServerStats(serverID: number, type: string, start: Date, end: Date) {
+    return firstValueFrom(
+      this.http
+        .get<QueryResponse<ReducedValuesPerc>>(`${environment.apiBaseUrl}server/${serverID}/stats`, {
+          params: { type, start: start.toString(), end: end.toString() }
+        })
+        .pipe(map(resp => resp.data))
+    );
+  }
 }

+ 28 - 0
ng/src/styles.scss

@@ -3,3 +3,31 @@
 .pointer {
   cursor: pointer;
 }
+
+.bg-avg {
+  background-color: #ffe69c !important;
+}
+
+.bg-peak {
+  background-color: #cce7ff !important;
+}
+
+.bg-max {
+  background-color: #e3a3a9 !important;
+}
+
+.bg-progress {
+  background-color: #e9ecef !important;
+}
+
+.text-avg {
+  color: #cc9a06 !important;
+}
+
+.text-peak {
+  color: #005299 !important;
+}
+
+.text-max {
+  color: #941320 !important;
+}