Quellcode durchsuchen

implemented multi-instance confirm modals; admin-panel: enabled deletion of service checks

Christian Kahlau vor 2 Jahren
Ursprung
Commit
c644542f66

+ 2 - 0
ng/package.json

@@ -31,6 +31,7 @@
     "ng2-charts": "^4.0.1",
     "rxjs": "~7.5.0",
     "tslib": "^2.3.0",
+    "uuid": "^9.0.0",
     "zone.js": "~0.11.4"
   },
   "devDependencies": {
@@ -40,6 +41,7 @@
     "@angular/localize": "^15.0.1",
     "@popperjs/core": "^2.11.6",
     "@types/jasmine": "~4.0.0",
+    "@types/uuid": "^9.0.1",
     "jasmine-core": "~4.3.0",
     "karma": "~6.4.0",
     "karma-chrome-launcher": "~3.1.0",

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

@@ -5,3 +5,7 @@
     <router-outlet></router-outlet>
   </div>
 </div>
+
+<ng-container>
+  <app-confirm-modal *ngFor="let modal of cmpService.confirmModals" (ref)="modal.ref($event)"></app-confirm-modal>
+</ng-container>

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

@@ -1,5 +1,6 @@
 import { Component, OnInit } from '@angular/core';
 
+import { ComponentService } from './services/component.service';
 import { ServerApiService } from './services/server-api.service';
 
 @Component({
@@ -8,7 +9,7 @@ import { ServerApiService } from './services/server-api.service';
   styleUrls: ['./app.component.scss']
 })
 export class AppComponent implements OnInit {
-  constructor(private apiService: ServerApiService) {}
+  constructor(private apiService: ServerApiService, public cmpService: ComponentService) {}
 
   async ngOnInit() {
     try {

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

@@ -3,11 +3,12 @@ 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 { NgbAccordionModule, NgbModalModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
 import { NgChartsModule } from 'ng2-charts';
 
 import { AppRoutingModule } from './app-routing.module';
 import { AppComponent } from './app.component';
+import { ConfirmModalComponent } from './components/confirm-modal/confirm-modal.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';
@@ -36,6 +37,7 @@ import { StatusColorPipe } from './pipes/status-color.pipe';
     AdminServiceChecksPageComponent,
     AppComponent,
     BytePipe,
+    ConfirmModalComponent,
     FaByTypePipe,
     HeaderComponent,
     HomePageComponent,
@@ -61,6 +63,7 @@ import { StatusColorPipe } from './pipes/status-color.pipe';
     FormsModule,
     HttpClientModule,
     NgbAccordionModule,
+    NgbModalModule,
     NgbNavModule,
     NgChartsModule,
     ReactiveFormsModule

+ 13 - 0
ng/src/app/components/confirm-modal/confirm-modal.component.html

@@ -0,0 +1,13 @@
+<ng-template #content let-modal>
+  <div class="modal-header">
+    <h4 class="modal-title">{{ strings.title }}</h4>
+    <button type="button" class="btn-close" aria-label="Close" (click)="modal.dismiss(0)"></button>
+  </div>
+  <div class="modal-body">
+    <p>{{ strings.message }}</p>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-outline-danger" (click)="modal.close(false)">{{ strings.no }}</button>
+    <button type="button" class="btn btn-outline-primary" (click)="modal.close(true)">{{ strings.yes }}</button>
+  </div>
+</ng-template>

+ 0 - 0
ng/src/app/components/confirm-modal/confirm-modal.component.scss


+ 23 - 0
ng/src/app/components/confirm-modal/confirm-modal.component.spec.ts

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

+ 63 - 0
ng/src/app/components/confirm-modal/confirm-modal.component.ts

@@ -0,0 +1,63 @@
+import { Component, EventEmitter, Output, TemplateRef, ViewChild } from '@angular/core';
+
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+
+export type ConfirmModalOptions = {
+  buttonTitles?: {
+    yes?: string;
+    no?: string;
+  };
+  modalContent?: {
+    title?: string;
+    message?: string;
+  };
+};
+
+@Component({
+  selector: 'app-confirm-modal',
+  templateUrl: './confirm-modal.component.html',
+  styleUrls: ['./confirm-modal.component.scss']
+})
+export class ConfirmModalComponent {
+  @ViewChild('content') content!: TemplateRef<HTMLElement>;
+
+  @Output() ref: EventEmitter<ConfirmModalComponent> = new EventEmitter();
+
+  private defaults = {
+    yes: 'Yes',
+    no: 'No',
+    title: 'Please confirm:',
+    message: '- This could be your message -'
+  };
+
+  public strings = {
+    ...this.defaults
+  };
+
+  constructor(private modalService: NgbModal) {
+    const hdl = setInterval(() => {
+      if (this.ref.observed) {
+        clearInterval(hdl);
+        this.ref.next(this);
+      }
+    }, 100);
+  }
+
+  async open(options?: ConfirmModalOptions): Promise<boolean> {
+    try {
+      this.strings = {
+        ...this.defaults,
+        ...options?.buttonTitles,
+        ...options?.modalContent
+      };
+
+      return await this.modalService.open(this.content).result;
+    } catch (error) {
+      if (typeof error === 'number') {
+        // error is one of "ModalDismissReasons"-Enum
+        return false;
+      }
+      throw error; // something unexpected
+    }
+  }
+}

+ 12 - 1
ng/src/app/pages/admin-panel/admin-service-checks-page/admin-service-checks-page.component.html

@@ -26,7 +26,18 @@
                 <button class="accordion-button" ngbPanelToggle [class.collapsed]="!opened" [ngClass]="{ 'bg-primary text-white': opened }">
                   <p class="flex-fill m-0">{{ serviceCheck.title }}</p>
 
-                  <fa-icon class="me-1" (click)="saveServiceCheck($event)" *ngIf="opened" [icon]="fa.save"></fa-icon>
+                  <fa-icon
+                    class="btn btn-sm btn-outline-light me-4"
+                    title="Save configuration"
+                    (click)="saveServiceCheck($event)"
+                    *ngIf="opened"
+                    [icon]="fa.save"></fa-icon>
+                  <fa-icon
+                    class="btn btn-sm btn-outline-danger me-4"
+                    title="Delete this Service Check"
+                    (click)="removeServiceCheck($event, serviceCheck.id)"
+                    *ngIf="opened"
+                    [icon]="fa.trash"></fa-icon>
                 </button>
               </ng-template>
               <ng-template ngbPanelContent>

+ 40 - 3
ng/src/app/pages/admin-panel/admin-service-checks-page/admin-service-checks-page.component.ts

@@ -1,9 +1,10 @@
 import { Component, ViewChild } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
-import { faAngleRight, faPlus, faSave, faServer } from '@fortawesome/free-solid-svg-icons';
+import { faAngleRight, faPlus, faSave, faServer, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
 import { NgbAccordion, NgbPanelChangeEvent } from '@ng-bootstrap/ng-bootstrap';
 
 import { ServiceCheckFormComponent } from 'src/app/components/service-check-form/service-check-form.component';
+import { ComponentService } from 'src/app/services/component.service';
 import { ServiceApiService } from 'src/app/services/service-api.service';
 import { ServerApiService } from 'src/app/services/server-api.service';
 
@@ -21,12 +22,18 @@ export class AdminServiceChecksPageComponent {
   @ViewChild('acc') accordeonRef!: NgbAccordion;
   public serverConfigs?: ServerConfig[];
   public serviceChecks: HttpCheckConfig[] = [];
-  public fa = { save: faSave, server: faServer, angleRight: faAngleRight, plus: faPlus };
+  public fa = { save: faSave, server: faServer, angleRight: faAngleRight, plus: faPlus, trash: faTrashAlt };
 
   public activeId = 0;
   public params?: { serverID?: number; checkID?: number; activeIds?: string[] };
 
-  constructor(apiService: ServerApiService, route: ActivatedRoute, private router: Router, private serviceApi: ServiceApiService) {
+  constructor(
+    apiService: ServerApiService,
+    private cmpService: ComponentService,
+    route: ActivatedRoute,
+    private router: Router,
+    private serviceApi: ServiceApiService
+  ) {
     apiService.serverConfigs$.subscribe(data => {
       this.serverConfigs = data;
       this.syncInit();
@@ -129,4 +136,34 @@ export class AdminServiceChecksPageComponent {
     }
     this.onAccordeonChange({ nextState: true, panelId, preventDefault: () => {} });
   }
+
+  async removeServiceCheck(event: Event, checkId: number) {
+    event.stopPropagation();
+    try {
+      if (!this.params?.serverID) return;
+
+      if (checkId === -1) {
+        const decision = await this.cmpService.openConfirmModal({
+          modalContent: { message: `Really delete unsaved service check?` }
+        });
+        if (decision) this.serviceChecks.shift();
+      } else {
+        const decision = await this.cmpService.openConfirmModal({
+          modalContent: { message: `Really delete service check #${checkId}?` }
+        });
+
+        if (!decision) return;
+
+        await this.serviceApi.deleteServiceCheck(this.params.serverID, checkId);
+
+        const idx = this.serviceChecks.findIndex(c => c.id === checkId);
+        if (idx >= 0) this.serviceChecks.splice(idx, 1);
+      }
+
+      this.params.activeIds = [];
+      this.onAccordeonChange({ nextState: false, panelId: '', preventDefault: () => {} });
+    } catch (error) {
+      console.error(error);
+    }
+  }
 }

+ 16 - 0
ng/src/app/services/component.service.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { ComponentService } from './component.service';
+
+describe('ComponentService', () => {
+  let service: ComponentService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(ComponentService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});

+ 37 - 0
ng/src/app/services/component.service.ts

@@ -0,0 +1,37 @@
+import { Injectable } from '@angular/core';
+import { v4 as uuid } from 'uuid';
+
+import { ConfirmModalComponent, ConfirmModalOptions } from 'src/app/components/confirm-modal/confirm-modal.component';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ComponentService {
+  public confirmModals: { ref: (cmp: ConfirmModalComponent) => void; hdl: string }[] = [];
+
+  constructor() {}
+
+  async openConfirmModal(options?: ConfirmModalOptions) {
+    return new Promise<boolean>((resolve, reject) => {
+      try {
+        const modalModel = {
+          hdl: uuid(),
+          ref: async (cmp: ConfirmModalComponent) => {
+            try {
+              const result = await cmp.open(options);
+              resolve(result);
+            } catch (error) {
+              reject(error);
+            }
+
+            const idx = this.confirmModals.findIndex(c => c.hdl === modalModel.hdl);
+            if (idx >= 0) this.confirmModals.splice(idx, 1);
+          }
+        };
+        this.confirmModals.push(modalModel);
+      } catch (error) {
+        reject(error);
+      }
+    });
+  }
+}

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

@@ -24,6 +24,10 @@ export class ServiceApiService {
     return firstValueFrom(this.http.put<HttpCheckConfig>(`${environment.apiBaseUrl}services/${serverId}`, checkConfig));
   }
 
+  public deleteServiceCheck(serverID: number, serviceID: number): Promise<boolean> {
+    return firstValueFrom(this.http.delete<any>(`${environment.apiBaseUrl}services/${serverID}/${serviceID}`).pipe(map(() => true)));
+  }
+
   public queryServiceData(serverID: number, serviceID: number, start: Date, end: Date) {
     return firstValueFrom(
       this.http