]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Display cluster status in favicon 36000/head
authorTiago Melo <tmelo@suse.com>
Thu, 9 Jul 2020 17:36:59 +0000 (17:36 +0000)
committerTiago Melo <tmelo@suse.com>
Thu, 16 Jul 2020 10:06:44 +0000 (10:06 +0000)
Fixes: https://tracker.ceph.com/issues/44542
Signed-off-by: Tiago Melo <tmelo@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/color.enum.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/health-color.pipe.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/index.html

index 5ad4535474a795a0550e6a5d28b6c8143f676ea4..5305d2abae47d9433e244aa673eaa843376ef191 100644 (file)
@@ -3,13 +3,15 @@ import { Router } from '@angular/router';
 
 import { Subscription } from 'rxjs';
 
+import { FaviconService } from '../../../shared/services/favicon.service';
 import { SummaryService } from '../../../shared/services/summary.service';
 import { TaskManagerService } from '../../../shared/services/task-manager.service';
 
 @Component({
   selector: 'cd-workbench-layout',
   templateUrl: './workbench-layout.component.html',
-  styleUrls: ['./workbench-layout.component.scss']
+  styleUrls: ['./workbench-layout.component.scss'],
+  providers: [FaviconService]
 })
 export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
   private subs = new Subscription();
@@ -17,12 +19,14 @@ export class WorkbenchLayoutComponent implements OnInit, OnDestroy {
   constructor(
     private router: Router,
     private summaryService: SummaryService,
-    private taskManagerService: TaskManagerService
+    private taskManagerService: TaskManagerService,
+    private faviconService: FaviconService
   ) {}
 
   ngOnInit() {
     this.subs.add(this.summaryService.startPolling());
     this.subs.add(this.taskManagerService.init(this.summaryService));
+    this.faviconService.init();
   }
 
   ngOnDestroy() {
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/color.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/color.enum.ts
new file mode 100644 (file)
index 0000000..0378827
--- /dev/null
@@ -0,0 +1,6 @@
+export enum Color {
+  // HEALTH
+  HEALTH_ERR = '#ff0000',
+  HEALTH_WARN = '#ffa500',
+  HEALTH_OK = '#00bb00'
+}
index 4b95022668c34d536cb69fe04b2492f8aafa7e6a..7da99de4b297b90301c2c12f1a01775577cd0c5b 100644 (file)
@@ -1,18 +1,12 @@
 import { Pipe, PipeTransform } from '@angular/core';
 
+import { Color } from '../enum/color.enum';
+
 @Pipe({
   name: 'healthColor'
 })
 export class HealthColorPipe implements PipeTransform {
   transform(value: any): any {
-    if (value === 'HEALTH_OK') {
-      return { color: '#00bb00' };
-    } else if (value === 'HEALTH_WARN') {
-      return { color: '#ffa500' };
-    } else if (value === 'HEALTH_ERR') {
-      return { color: '#ff0000' };
-    } else {
-      return null;
-    }
+    return Color[value] ? { color: Color[value] } : null;
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.spec.ts
new file mode 100644 (file)
index 0000000..7c7184e
--- /dev/null
@@ -0,0 +1,22 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '../../../testing/unit-test-helper';
+import { FaviconService } from './favicon.service';
+
+describe('FaviconService', () => {
+  let service: FaviconService;
+
+  configureTestBed({
+    imports: [HttpClientTestingModule],
+    providers: [FaviconService]
+  });
+
+  beforeEach(() => {
+    service = TestBed.inject(FaviconService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/favicon.service.ts
new file mode 100644 (file)
index 0000000..3ed43f6
--- /dev/null
@@ -0,0 +1,74 @@
+import { DOCUMENT } from '@angular/common';
+import { Inject, Injectable, OnDestroy } from '@angular/core';
+
+import { Subscription } from 'rxjs';
+
+import { Color } from '../enum/color.enum';
+import { SummaryService } from './summary.service';
+
+@Injectable()
+export class FaviconService implements OnDestroy {
+  sub: Subscription;
+  oldStatus: string;
+  url: string;
+
+  constructor(
+    @Inject(DOCUMENT) private document: HTMLDocument,
+    private summaryService: SummaryService
+  ) {}
+
+  init() {
+    this.url = this.document.getElementById('cdFavicon')?.getAttribute('href');
+
+    this.sub = this.summaryService.subscribe((summary) => {
+      this.changeIcon(summary.health_status);
+    });
+  }
+
+  changeIcon(status?: string) {
+    if (status === this.oldStatus) {
+      return;
+    }
+
+    this.oldStatus = status;
+
+    const favicon = this.document.getElementById('cdFavicon');
+    const faviconSize = 16;
+    const radius = faviconSize / 4;
+
+    const canvas = this.document.createElement('canvas');
+    canvas.width = faviconSize;
+    canvas.height = faviconSize;
+
+    const context = canvas.getContext('2d');
+    const img = this.document.createElement('img');
+    img.src = this.url;
+
+    img.onload = () => {
+      // Draw Original Favicon as Background
+      context.drawImage(img, 0, 0, faviconSize, faviconSize);
+
+      // Cut notification circle area
+      context.save();
+      context.globalCompositeOperation = 'destination-out';
+      context.beginPath();
+      context.arc(canvas.width - radius, radius, radius + 1, 0, 2 * Math.PI);
+      context.fill();
+      context.restore();
+
+      // Draw Notification Circle
+      context.beginPath();
+      context.arc(canvas.width - radius, radius, radius, 0, 2 * Math.PI);
+      context.fillStyle = Color[status] || 'transparent';
+      context.fill();
+
+      // Replace favicon
+      favicon.setAttribute('href', canvas.toDataURL('image/png'));
+    };
+  }
+
+  ngOnDestroy() {
+    this.changeIcon();
+    this.sub?.unsubscribe();
+  }
+}
index f314e2c53a8f5e0fa991df7576f5f7fb69283105..bfd6d3ecd83731e1dd316c07d008740c1ca125f9 100644 (file)
@@ -9,7 +9,7 @@
   </script>
 
   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-  <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <link rel="icon" type="image/x-icon" id="cdFavicon" href="favicon.ico">
 </head>
 <body>
   <noscript>