Quellcode durchsuchen

Admin Panel WIP: Create Read Update works

benc1 vor 3 Jahren
Ursprung
Commit
925610b055

+ 5 - 2
ng/package.json

@@ -24,6 +24,7 @@
     "@fortawesome/free-brands-svg-icons": "^6.1.1",
     "@fortawesome/free-regular-svg-icons": "^6.1.1",
     "@fortawesome/free-solid-svg-icons": "^6.1.1",
+    "@ng-bootstrap/ng-bootstrap": "^13.1.1",
     "chart.js": "^3.6.0",
     "chartjs-adapter-date-fns": "^2.0.0",
     "date-fns": "^2.29.3",
@@ -36,6 +37,8 @@
     "@angular-devkit/build-angular": "^15.0.1",
     "@angular/cli": "~15.0.1",
     "@angular/compiler-cli": "^15.0.1",
+    "@angular/localize": "^15.0.1",
+    "@popperjs/core": "^2.11.6",
     "@types/jasmine": "~4.0.0",
     "jasmine-core": "~4.3.0",
     "karma": "~6.4.0",
@@ -43,6 +46,6 @@
     "karma-coverage": "~2.2.0",
     "karma-jasmine": "~5.1.0",
     "karma-jasmine-html-reporter": "~2.0.0",
-    "typescript": "^4.9.3"
+    "typescript": "~4.8.0"
   }
-}
+}

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

@@ -1,22 +1,46 @@
 import { HttpClientModule } from '@angular/common/http';
 import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { BrowserModule } from '@angular/platform-browser';
 import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
 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 { 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 { ServiceCheckFormComponent } from './components/service-check-form/service-check-form.component';
 
 @NgModule({
-  declarations: [AppComponent, BytePipe, FaByTypePipe, HeaderComponent, HomePageComponent, ServerDataChartComponent, ServerDataPageComponent, AdminPanelComponent],
-  imports: [AppRoutingModule, BrowserModule, FontAwesomeModule, HttpClientModule, NgChartsModule],
+  declarations: [
+    AppComponent,
+    BytePipe,
+    FaByTypePipe,
+    HeaderComponent,
+    HomePageComponent,
+    ServerDataChartComponent,
+    ServerDataPageComponent,
+    AdminPanelComponent,
+    ServiceCheckFormComponent
+  ],
+  imports: [
+    AppRoutingModule,
+    BrowserModule,
+    FontAwesomeModule,
+    HttpClientModule,
+    NgbAccordionModule,
+    NgChartsModule,
+    NgbNavModule,
+    FormsModule,
+    ReactiveFormsModule
+  ],
   providers: [{ provide: BytePipe, multi: false }],
   bootstrap: [AppComponent]
 })

+ 24 - 13
ng/src/app/components/admin/admin-panel/admin-panel.component.html

@@ -1,14 +1,25 @@
-<p>admin-panel works!</p>
-
-<div class="accordion" id="accordionExample">
-  <div *ngFor="let serverConfig of serverConfigs" class="accordion-item">
-    <h2 class="accordion-header" id="headingOne">
-      <button class="accordion-button" type="button" data-bs-toggle="collapse" [attr.data-bs-target]="'#server_' + serverConfig.id">
-        {{ serverConfig.title }}
-      </button>
-    </h2>
-    <div id="server_{{ serverConfig.id }}" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#accordionExample">
-      <div class="accordion-body"></div>
-    </div>
-  </div>
+<div class="d-flex">
+  <ul ngbNav #nav="ngbNav" class="nav-pills" orientation="vertical">
+    <li *ngFor="let serverConfig of serverConfigs" ngbNavItem>
+      <a ngbNavLink (click)="fetchServiceChecks(serverConfig.id)">{{ serverConfig.title }}</a>
+      <ng-template ngbNavContent>
+        <button class="btn" (click)="addServiceCheck(serverConfig.id)">Add</button>
+        <ngb-accordion #acc="ngbAccordion">
+          <ngb-panel *ngFor="let serviceCheck of serviceChecks; let i = index">
+            <ng-template ngbPanelHeader let-opened="opened">
+              <div class="accordion-button custom-header justify-content-between" [class.collapsed]="!opened">
+                <p class="m-0">{{ serviceCheck.title }}</p>
+                <button (click)="saveServiceCheck(i)" *ngIf="opened"><fa-icon [icon]="fa.save"></fa-icon></button>
+                <button ngbPanelToggle class="accordion-button"></button>
+              </div>
+            </ng-template>
+            <ng-template ngbPanelContent>
+              <app-service-check-form [serviceCheck]="serviceCheck"></app-service-check-form>
+            </ng-template>
+          </ngb-panel>
+        </ngb-accordion>
+      </ng-template>
+    </li>
+  </ul>
+  <div [ngbNavOutlet]="nav" class="flex-fill"></div>
 </div>

+ 22 - 1
ng/src/app/components/admin/admin-panel/admin-panel.component.ts

@@ -1,6 +1,8 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
+import { faSave } from '@fortawesome/free-solid-svg-icons';
 import { AdminPanelService } from '../../../services/admin-panel.service';
 import { ServerApiService } from '../../../services/server-api.service';
+import { ServiceCheckFormComponent } from '../../service-check-form/service-check-form.component';
 
 @Component({
   selector: 'app-admin-panel',
@@ -8,11 +10,30 @@ import { ServerApiService } from '../../../services/server-api.service';
   styleUrls: ['./admin-panel.component.scss']
 })
 export class AdminPanelComponent implements OnInit {
+  @ViewChildren(ServiceCheckFormComponent) formChilds!: QueryList<ServiceCheckFormComponent>;
   public serverConfigs: ServerConfig[] = [];
+  public serviceChecks: HttpCheckConfig[] = [];
+  public fa = { save: faSave };
 
   constructor(private adminService: AdminPanelService, apiService: ServerApiService) {
     apiService.serverConfigs$.subscribe(data => (this.serverConfigs = data));
   }
 
   ngOnInit(): void {}
+
+  async fetchServiceChecks(serverId: number) {
+    try {
+      this.serviceChecks = await this.adminService.loadServiceChecks(serverId);
+    } catch (error: any) {
+      console.error(error);
+    }
+  }
+
+  saveServiceCheck(index: number) {
+    this.formChilds.get(index)?.save();
+  }
+
+  addServiceCheck(serverId: number) {
+    this.serviceChecks.unshift({ serverId, checks: [] as string[] } as HttpCheckConfig);
+  }
 }

+ 49 - 0
ng/src/app/components/service-check-form/service-check-form.component.html

@@ -0,0 +1,49 @@
+<div [formGroup]="serviceCheckForm" class="row form-row-service-check position-relative">
+  <div class="col-7">
+    <div class="row mb-2">
+      <label [for]="'Title_' + serviceCheck.id" class="col-sm-3 col-form-label">Title</label>
+      <div class="col-sm-9">
+        <input type="text" formControlName="title" class="form-control" [id]="'Title_' + serviceCheck.id" />
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label class="col-sm-3 form-check-label" [for]="'Active_' + serviceCheck.id"> Active </label>
+      <div class="col-sm-9">
+        <input class="form-check-input" formControlName="active" type="checkbox" value="" [id]="'Active_' + serviceCheck.id" />
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label [for]="'Interval_' + serviceCheck.id" class="col-sm-3 col-form-label">Interval (secs)</label>
+      <div class="col-sm-9">
+        <input type="number" step="10" formControlName="interval" class="form-control" [id]="'Interval_' + serviceCheck.id" />
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label [for]="'Timeout_' + serviceCheck.id" class="col-sm-3 col-form-label">Timeout (ms)</label>
+      <div class="col-sm-9">
+        <input type="number" step="100" formControlName="timeout" class="form-control" [id]="'Timeout_' + serviceCheck.id" />
+      </div>
+    </div>
+    <div class="row mb-2">
+      <label [for]="'Url_' + serviceCheck.id" class="col-sm-3 col-form-label">Url</label>
+      <div class="col-sm-9">
+        <input type="text" formControlName="url" class="form-control" [id]="'Url_' + serviceCheck.id" />
+      </div>
+    </div>
+  </div>
+  <div class="col-5 h-100 overflow-auto">
+    <ul formArrayName="checks" class="list-group">
+      <li class="list-group-item">
+        <div class="btn w-100" title="Add new search pattern" (click)="addPattern()"><fa-icon [icon]="fa.plus"></fa-icon></div>
+      </li>
+      <li *ngFor="let checkEntry of checks.controls; let i = index" class="list-group-item">
+        <div class="flex-row d-flex">
+          <input #checkInput type="text" class="flex-grow-1 flex-shrink-1 form-control-plaintext" [formControlName]="i" />
+          <div class="btn flex-grow-0 flex-shrink-0" title="Remove search pattern" (click)="removePattern(i)">
+            <fa-icon [icon]="fa.delete"></fa-icon>
+          </div>
+        </div>
+      </li>
+    </ul>
+  </div>
+</div>

+ 4 - 0
ng/src/app/components/service-check-form/service-check-form.component.scss

@@ -0,0 +1,4 @@
+.form-row-service-check {
+  max-height: 220px;
+  height: 220px;
+}

+ 23 - 0
ng/src/app/components/service-check-form/service-check-form.component.spec.ts

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

+ 73 - 0
ng/src/app/components/service-check-form/service-check-form.component.ts

@@ -0,0 +1,73 @@
+import { Component, ElementRef, Input, OnInit, QueryList, ViewChildren } from '@angular/core';
+import { FormControl, FormGroup, FormArray, FormBuilder } from '@angular/forms';
+import { faMinusSquare, faPlusSquare } from '@fortawesome/free-solid-svg-icons';
+import { AdminPanelService } from '../../services/admin-panel.service';
+import { deepCopy } from '../../../../../common/util/object-utils';
+
+@Component({
+  selector: 'app-service-check-form',
+  templateUrl: './service-check-form.component.html',
+  styleUrls: ['./service-check-form.component.scss']
+})
+export class ServiceCheckFormComponent implements OnInit {
+  @ViewChildren('checkInput')
+  set checkRows(childs: QueryList<ElementRef<HTMLInputElement>>) {
+    if (childs?.length && childs.get(0)?.nativeElement.value === '') {
+      //new added children via addPattern without value gets focus
+      childs.get(0)?.nativeElement.focus();
+    }
+  }
+
+  @Input()
+  serviceCheck!: HttpCheckConfig;
+  public fa = { plus: faPlusSquare, delete: faMinusSquare };
+
+  serviceCheckForm: FormGroup = this.formBuilder.group({
+    id: -1, //constant for defaultValues
+    title: '', //constant for defaultValues
+    active: undefined, //constant for defaultValues
+    url: '', //constant for defaultValues
+    interval: 10, //constant for defaultValues
+    timeout: 10, //constant for defaultValuess
+    checks: this.formBuilder.array([])
+  });
+
+  constructor(private formBuilder: FormBuilder, private checkService: AdminPanelService) {}
+
+  get checks(): FormArray {
+    return this.serviceCheckForm.controls['checks'] as FormArray;
+  }
+
+  ngOnInit(): void {
+    this.patchForm(this.serviceCheck);
+  }
+
+  private patchForm(serviceCheck: HttpCheckConfig) {
+    this.checks.controls = [];
+    serviceCheck.checks.reverse().forEach(e => {
+      this.checks.controls.push(new FormControl(e));
+    });
+    this.serviceCheckForm.patchValue(serviceCheck);
+  }
+
+  addPattern() {
+    this.checks.controls.unshift(new FormControl(''));
+    //focus set via viewChildren setter, here is too early
+  }
+
+  removePattern(index: number) {
+    this.checks.controls.splice(index, 1);
+  }
+
+  async save() {
+    try {
+      this.serviceCheckForm.updateValueAndValidity();
+      const copy = deepCopy(this.serviceCheckForm.value as HttpCheckConfig);
+      copy.checks = this.checks.controls.map(e => e.value).reverse();
+      const savedCheck = await this.checkService.saveServiceCheck(this.serviceCheck.serverId as number, copy);
+      this.patchForm(savedCheck);
+    } catch (error: any) {
+      console.error(error);
+    }
+  }
+}

+ 7 - 2
ng/src/app/services/admin-panel.service.ts

@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
 import { ServiceConfig } from '../../../../common/interfaces/service-config.interface';
 import { environment } from '../../environments/environment';
 import { HttpClient } from '@angular/common/http';
+import { firstValueFrom } from 'rxjs';
 
 @Injectable({
   providedIn: 'root'
@@ -9,7 +10,11 @@ import { HttpClient } from '@angular/common/http';
 export class AdminPanelService {
   constructor(private http: HttpClient) {}
 
-  loadServer() {
-    this.http.get<ServiceConfig[]>(`${environment.apiBaseUrl}server`);
+  public loadServiceChecks(serverId: number): Promise<HttpCheckConfig[]> {
+    return firstValueFrom(this.http.get<HttpCheckConfig[]>(`${environment.apiBaseUrl}services/${serverId}`));
+  }
+
+  public saveServiceCheck(serverId: number, checkConfig: HttpCheckConfig): Promise<HttpCheckConfig> {
+    return firstValueFrom(this.http.put<HttpCheckConfig>(`${environment.apiBaseUrl}services/${serverId}`, checkConfig));
   }
 }

+ 1 - 1
ng/tsconfig.app.json

@@ -6,5 +6,5 @@
     "types": []
   },
   "files": ["src/main.ts", "src/polyfills.ts"],
-  "include": ["src/**/*.d.ts", "..common/**/*.d.ts"]
+  "include": ["src/**/*.d.ts", "../common/**/*.d.ts"]
 }