]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add alerts doc url to active alert list 70606/head
authorAashish Sharma <aashish@li-e9bf2ecc-2ad7-11b2-a85c-baf05c5182ab.ibm.com>
Tue, 28 Jul 2026 07:00:59 +0000 (12:30 +0530)
committerAashish Sharma <aashish@li-e9bf2ecc-2ad7-11b2-a85c-baf05c5182ab.ibm.com>
Tue, 28 Jul 2026 09:46:37 +0000 (15:16 +0530)
Add documentation link in Learn more column for every alert in the
active alerts list table

Fixes: https://tracker-origin.ceph.com/issues/78747
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/prometheus/active-alert-list/active-alert-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts

index 1ea3e54a374699086f9972318cf7c486a04fe295..1aa69e4172157dc22e3b3bce832ad5d79d08129b 100644 (file)
@@ -64,7 +64,6 @@
 
 <ng-template
   #externalLinkTpl
-  let-row="data.row"
   let-value="data.value"
 >
   <a
     Source
   </a>
 </ng-template>
+
+<ng-template
+  #docLinkTpl
+  let-value="data.value"
+>
+  @if (alertDocUrls[value]) {
+    <a
+      [href]="alertDocUrls[value]"
+      target="_blank"
+      i18n
+      ><cd-icon type="launch"></cd-icon> Docs</a
+    >
+  }
+</ng-template>
index 72fb3c883bbe3b0beb5a60abd8239f50b79d71a3..779db6b81236bc17d95d688014ac287ad71d2e6a 100644 (file)
@@ -100,4 +100,33 @@ describe('ActiveAlertListComponent', () => {
       }
     });
   });
+
+  describe('alertDocUrls', () => {
+    beforeEach(() => component.ngOnInit());
+
+    it('should be empty and hasDocUrls false by default (no upstream doc URL)', () => {
+      expect(component.hasDocUrls).toBe(false);
+      expect(component.alertDocUrls).toEqual({});
+    });
+
+    it('should remain empty when alerts arrive and hasDocUrls is false', () => {
+      component['prometheusAlertService'].alerts = [
+        { labels: { alertname: 'CephHealthError' } } as any
+      ];
+      component['prometheusAlertService']['totalSubject'].next(1);
+      expect(component.alertDocUrls).toEqual({});
+    });
+
+    it('should populate map when hasDocUrls is true and alerts arrive', () => {
+      spyOn(component['docService'], 'alertDocUrl').and.returnValue(
+        'https://example.com/docs#managing-alerts__cephhealtherror'
+      );
+      component.hasDocUrls = true;
+      component['prometheusAlertService'].alerts = [
+        { labels: { alertname: 'CephHealthError' } } as any
+      ];
+      component['prometheusAlertService']['totalSubject'].next(1);
+      expect(component.alertDocUrls['CephHealthError']).toContain('cephhealtherror');
+    });
+  });
 });
index c367ad9ba6a6f23ccc1257f825ffc2cde6069743..bea1f46d1d438fb561bab802a1e22b5b4ae359b1 100644 (file)
@@ -1,6 +1,9 @@
-import { Component, Inject, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Component, Inject, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 
+import { Subscription } from 'rxjs';
+import { filter, first } from 'rxjs/operators';
+
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
 import { Icons } from '~/app/shared/enum/icons.enum';
@@ -14,6 +17,7 @@ import { AlertState } from '~/app/shared/models/prometheus-alerts';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.service';
 import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { DocService } from '~/app/shared/services/doc.service';
 
 const BASE_URL = 'silences'; // as only silence actions can be used
 
@@ -30,9 +34,11 @@ const SeverityMap = {
   styleUrls: ['./active-alert-list.component.scss'],
   standalone: false
 })
-export class ActiveAlertListComponent extends PrometheusListHelper implements OnInit {
+export class ActiveAlertListComponent extends PrometheusListHelper implements OnInit, OnDestroy {
   @ViewChild('externalLinkTpl', { static: true })
   externalLinkTpl: TemplateRef<any>;
+  @ViewChild('docLinkTpl', { static: true })
+  docLinkTpl: TemplateRef<any>;
   @ViewChild(TableComponent) table: TableComponent;
   columns: CdTableColumn[];
   innerColumns: CdTableColumn[];
@@ -41,7 +47,11 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On
   selection = new CdTableSelection();
   icons = Icons;
   expandedInnerRow: any;
+  alertDocUrls: Record<string, string | null> = {};
+  hasDocUrls = false;
   multilineTextKeys = ['description', 'impact', 'fix'];
+  private cephRelease = '';
+  private alertsSub: Subscription;
 
   filters: CdTableColumn[] = [
     {
@@ -76,10 +86,21 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On
     public prometheusAlertService: PrometheusAlertService,
     private urlBuilder: URLBuilderService,
     private route: ActivatedRoute,
+    private docService: DocService,
     @Inject(PrometheusService) prometheusService: PrometheusService
   ) {
     super(prometheusService);
     this.permission = this.authStorageService.getPermissions().prometheus;
+    this.docService.releaseData$
+      .pipe(
+        filter((v) => !!v),
+        first()
+      )
+      .subscribe((release) => {
+        this.cephRelease = release;
+        this.hasDocUrls = !!this.docService.urlGenerator('managing-alerts', release);
+        this.alertDocUrls = {};
+      });
     this.tableActions = [
       {
         permission: 'create',
@@ -153,13 +174,33 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On
         flexGrow: 1
       },
       {
-        name: $localize`URL`,
+        name: $localize`Query`,
         prop: 'generatorURL',
         flexGrow: 1,
         sortable: false,
         cellTemplate: this.externalLinkTpl
-      }
+      },
+      ...(this.hasDocUrls
+        ? [
+            {
+              name: $localize`Learn more`,
+              prop: 'labels.alertname',
+              flexGrow: 1,
+              sortable: false,
+              cellTemplate: this.docLinkTpl
+            }
+          ]
+        : [])
     ];
+    this.alertsSub = this.prometheusAlertService.totalAlerts$.subscribe(() => {
+      if (!this.hasDocUrls) return;
+      this.alertDocUrls = Object.fromEntries(
+        this.prometheusAlertService.alerts.map((a) => [
+          a.labels.alertname,
+          this.docService.alertDocUrl(a.labels.alertname, this.cephRelease)
+        ])
+      );
+    });
     this.prometheusAlertService.getGroupedAlerts(true);
     this.route.queryParams.subscribe((params) => {
       const severity = params['severity'];
@@ -175,6 +216,10 @@ export class ActiveAlertListComponent extends PrometheusListHelper implements On
     });
   }
 
+  ngOnDestroy() {
+    this.alertsSub?.unsubscribe();
+  }
+
   setExpandedInnerRow(row: any) {
     this.expandedInnerRow = row;
   }
index e28f2f766b0a48edda6861af836f2177a68bc138..c8a4e920b0b9f011407131f592aaabf055541cd3 100644 (file)
@@ -52,6 +52,14 @@ export class DocService {
     return sections[section];
   }
 
+  alertDocUrl(alertName: string, releaseVersion = ''): string | null {
+    const baseUrl = this.urlGenerator('managing-alerts', releaseVersion);
+    if (!baseUrl || !alertName) {
+      return null;
+    }
+    return `${baseUrl}#${encodeURIComponent(`managing-alerts__${alertName.toLowerCase()}`)}`;
+  }
+
   subscribeOnce(
     section: string,
     next: (release: string) => void,