]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Include executing tasks in notification panel 66855/head
authorAashish Sharma <aashish@li-e9bf2ecc-2ad7-11b2-a85c-baf05c5182ab.ibm.com>
Fri, 9 Jan 2026 08:11:26 +0000 (13:41 +0530)
committerAashish Sharma <aashish@li-e9bf2ecc-2ad7-11b2-a85c-baf05c5182ab.ibm.com>
Thu, 15 Jan 2026 08:05:34 +0000 (13:35 +0530)
The new notification panel only includes the notifications currently and not the executing tasks. We need to include it in the panel as well

Fixes: https://tracker.ceph.com/issues/74364
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.scss
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/notification-panel/notification-area/notification-area.component.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss

index 7236662e7cc1642d222baac9eaa70daa4c97b86b..a9791bf2179fe2fdf1b3fde87775df9903a4aff1 100644 (file)
@@ -16,7 +16,8 @@ import {
   ToggleModule,
   ButtonModule,
   PlaceholderModule,
-  TagModule
+  TagModule,
+  ProgressBarModule
 } from 'carbon-components-angular';
 
 import { AppRoutingModule } from '~/app/app-routing.module';
@@ -72,7 +73,8 @@ import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
     ToggleModule,
     ButtonModule,
     PlaceholderModule,
-    TagModule
+    TagModule,
+    ProgressBarModule
   ],
   declarations: [
     AboutComponent,
index e9814713b45a2d640e5c5318258e0e9681ff20d0..ca261b8db3aca0fc9fcd8f0e2dec78723f3774df 100644 (file)
-<ng-template #notificationItemTemplate
-             let-notification="notification"
-             let-last="last">
+@if (executingTasks.length > 0) {
+<div
+  class="notification-section-heading"
+  i18n>
+  Running tasks
+</div>
+
+@for (task of executingTasks; track task.begin_time) {
+<div class="notification-wrapper">
+  <div class="notification-item task-item">
+    <cd-icon type="infoCircle"></cd-icon>
+
+    <div class="notification-content">
+      <div class="notification-title">
+        {{ task.description }}
+      </div>
+
+      <cds-progress-bar
+        [max]="100"
+        [value]="task.progress || 0"
+        status="active"
+        type="inline"
+        size="small">
+      </cds-progress-bar>
+
+      <div class="task-row">
+        <span class="notification-timestamp">
+          {{ task.begin_time | relativeDate }}
+        </span>
+
+        <span class="task-progress">
+          {{ task.progress || 0 }} %
+        </span>
+      </div>
+    </div>
+  </div>
+
+  <div class="notification-divider"></div>
+</div>
+}
+}
+
+<ng-template
+  #notificationItemTemplate
+  let-notification="notification"
+  let-last="last">
   <div class="notification-wrapper">
     <div class="notification-item">
-      <cd-icon id="notification-icon"
-               [type]="notificationIconMap[notification.type] || notificationIconMap['default']">
+      <cd-icon
+        id="notification-icon"
+        [type]="notificationIconMap[notification.type] || notificationIconMap['default']">
       </cd-icon>
+
       <div class="notification-content">
-        <div class="notification-timestamp">{{ notification.timestamp | relativeDate }}</div>
-        <div class="notification-title">{{ notification.title }}</div>
-        <div class="notification-message"
-             [innerHTML]="notification.message | sanitizeHtml"></div>
+        <div class="notification-timestamp">
+          {{ notification.timestamp | relativeDate }}
+        </div>
+        <div class="notification-title">
+          {{ notification.title }}
+        </div>
+        <div
+          class="notification-message"
+          [innerHTML]="notification.message | sanitizeHtml">
+        </div>
       </div>
-      <button cdsButton="ghost"
-              size="sm"
-              class="notification-close"
-              (click)="removeNotification(notification, $event)">
+
+      <button
+        cdsButton="ghost"
+        size="sm"
+        class="notification-close"
+        (click)="removeNotification(notification, $event)">
         <cd-icon type="destroy"></cd-icon>
       </button>
     </div>
-  @if (!last) {
+
+    @if (!last) {
     <div class="notification-divider"></div>
-  }
+    }
   </div>
 </ng-template>
 
 @if (todayNotifications.length > 0) {
-  <div class="notification-section-heading"
-       i18n>Today</div>
-  @for (notification of todayNotifications; track notification.timestamp; let last = $last) {
-    <ng-container *ngTemplateOutlet="notificationItemTemplate; context: { notification: notification, last: last }"></ng-container>
-  }
+<div
+  class="notification-section-heading"
+  i18n>
+  Today
+</div>
+
+@for (notification of todayNotifications; track notification.timestamp; let last = $last) {
+<ng-container
+  *ngTemplateOutlet="notificationItemTemplate;
+    context: { notification: notification, last: last }">
+</ng-container>
+}
 }
 
 @if (previousNotifications.length > 0) {
-  <div class="notification-section-heading"
-       i18n>Previous</div>
-  @for (notification of previousNotifications; track notification.timestamp; let last = $last) {
-    <ng-container *ngTemplateOutlet="notificationItemTemplate; context: { notification: notification, last: last }"></ng-container>
-  }
+<div
+  class="notification-section-heading"
+  i18n>
+  Previous
+</div>
+
+@for (notification of previousNotifications; track notification.timestamp; let last = $last) {
+<ng-container
+  *ngTemplateOutlet="notificationItemTemplate;
+    context: { notification: notification, last: last }">
+</ng-container>
+}
 }
 
 @if (todayNotifications.length === 0 && previousNotifications.length === 0) {
-  <div class="notification-empty">
-    <div i18n>No notifications</div>
-  </div>
+<div class="notification-empty">
+  <div i18n>No notifications</div>
+</div>
 }
index c7ddebbd465fa814b8d8288302d023f93dea011e..ec1d8ec7e468a607e7b8ed95e3f23d6c50988206 100644 (file)
@@ -1,3 +1,4 @@
+@use './src/styles/defaults' as *;
 @use '@carbon/styles/scss/theme' as *;
 @use '@carbon/styles/scss/spacing' as *;
 @use '@carbon/type';
   display: block;
 }
 
+.task-row {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-top: var(--cds-spacing-02);
+}
+
+.task-progress {
+  @include type.type-style('label-01');
+
+  color: var(--cds-text-secondary);
+  text-align: right;
+}
+
 .notification-timestamp {
   @include type.type-style('label-01');
 
   overflow-y: auto;
   background-color: $layer-01;
 }
+
+:host ::ng-deep .infoCircle-icon {
+  fill: $primary !important;
+}
index 095524df5af9d55c7f4852cb8cb923d35ce102af..a7ef567b89fe37a75249e5a3641b77946c17bb52 100644 (file)
@@ -9,6 +9,7 @@ import { CdNotification, CdNotificationConfig } from '../../../../shared/models/
 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
 import { SharedModule } from '../../../../shared/shared.module';
 import { configureTestBed } from '~/testing/unit-test-helper';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
 
 describe('NotificationAreaComponent', () => {
   let component: NotificationAreaComponent;
@@ -20,6 +21,10 @@ describe('NotificationAreaComponent', () => {
   const yesterday = new Date(today);
   yesterday.setDate(yesterday.getDate() - 1);
 
+  configureTestBed({
+    imports: [HttpClientTestingModule]
+  });
+
   const createNotification = (
     type: NotificationType,
     title: string,
@@ -90,7 +95,7 @@ describe('NotificationAreaComponent', () => {
   });
 
   it('should unsubscribe from notification service on destroy', () => {
-    const subSpy = spyOn(component['sub'], 'unsubscribe');
+    const subSpy = spyOn(component['subs'], 'unsubscribe');
     component.ngOnDestroy();
     expect(subSpy).toHaveBeenCalled();
   });
index 7ea97ea5898d83ad08704edfa0cf80ed081acb2d..7cf0e76dd9b8268556a612429ae58d53e3e6e363 100644 (file)
@@ -1,8 +1,16 @@
-import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
 import { Subscription } from 'rxjs';
 import { NotificationService } from '../../../../shared/services/notification.service';
 import { CdNotification } from '../../../../shared/models/cd-notification';
 import { NotificationType } from '../../../../shared/enum/notification-type.enum';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { Mutex } from 'async-mutex';
+import _ from 'lodash';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import moment from 'moment';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { TaskMessageService } from '~/app/shared/services/task-message.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
 
 @Component({
   selector: 'cd-notification-area',
@@ -13,7 +21,11 @@ import { NotificationType } from '../../../../shared/enum/notification-type.enum
 export class NotificationAreaComponent implements OnInit, OnDestroy {
   todayNotifications: CdNotification[] = [];
   previousNotifications: CdNotification[] = [];
-  private sub: Subscription;
+  private subs = new Subscription();
+  last_task = '';
+  mutex = new Mutex();
+  icons = Icons;
+  executingTasks: ExecutingTask[] = [];
 
   readonly notificationIconMap = {
     [NotificationType.success]: 'success',
@@ -23,32 +35,73 @@ export class NotificationAreaComponent implements OnInit, OnDestroy {
     default: 'infoCircle'
   } as const;
 
-  constructor(private notificationService: NotificationService) {}
+  constructor(
+    private notificationService: NotificationService,
+    private summaryService: SummaryService,
+    private cdRef: ChangeDetectorRef,
+    private taskMessageService: TaskMessageService
+  ) {}
 
   ngOnInit(): void {
-    this.sub = this.notificationService.data$.subscribe((notifications: CdNotification[]) => {
-      const today: Date = new Date();
-      this.todayNotifications = [];
-      this.previousNotifications = [];
-      notifications.forEach((n: CdNotification) => {
-        const notifDate = new Date(n.timestamp);
-        if (
-          notifDate.getDate() === today.getDate() &&
-          notifDate.getMonth() === today.getMonth() &&
-          notifDate.getFullYear() === today.getFullYear()
-        ) {
-          this.todayNotifications.push(n);
-        } else {
-          this.previousNotifications.push(n);
-        }
-      });
-    });
+    this.subs.add(
+      this.notificationService.data$.subscribe((notifications: CdNotification[]) => {
+        const today: Date = new Date();
+        this.todayNotifications = [];
+        this.previousNotifications = [];
+        notifications.forEach((n: CdNotification) => {
+          const notifDate = new Date(n.timestamp);
+          if (
+            notifDate.getDate() === today.getDate() &&
+            notifDate.getMonth() === today.getMonth() &&
+            notifDate.getFullYear() === today.getFullYear()
+          ) {
+            this.todayNotifications.push(n);
+          } else {
+            this.previousNotifications.push(n);
+          }
+        });
+      })
+    );
+
+    this.subs.add(
+      this.summaryService.subscribe((summary) => {
+        this._handleTasks(summary.executing_tasks);
+
+        this.mutex.acquire().then((release) => {
+          _.filter(
+            summary.finished_tasks,
+            (task: FinishedTask) => !this.last_task || moment(task.end_time).isAfter(this.last_task)
+          ).forEach((task) => {
+            const config = this.notificationService.finishedTaskToNotification(task, task.success);
+            const notification = new CdNotification(config);
+            notification.timestamp = task.end_time;
+            notification.duration = task.duration;
+
+            if (!this.last_task || moment(task.end_time).isAfter(this.last_task)) {
+              this.last_task = task.end_time;
+              window.localStorage.setItem('last_task', this.last_task);
+            }
+
+            this.notificationService.save(notification);
+          });
+
+          this.cdRef.detectChanges();
+
+          release();
+        });
+      })
+    );
   }
 
-  ngOnDestroy(): void {
-    if (this.sub) {
-      this.sub.unsubscribe();
+  _handleTasks(executingTasks: ExecutingTask[]) {
+    for (const executingTask of executingTasks) {
+      executingTask.description = this.taskMessageService.getRunningTitle(executingTask);
     }
+    this.executingTasks = executingTasks;
+  }
+
+  ngOnDestroy(): void {
+    this.subs.unsubscribe();
   }
 
   removeNotification(notification: CdNotification, event: MouseEvent) {
index a3ea809897c1a09331a61da5fbda4676c41d7d46..902be1b14810d8e6b7ab792eae4d6222e46d7d1c 100644 (file)
@@ -211,3 +211,11 @@ input:-webkit-autofill:active {
   -webkit-text-fill-color: inherit;
   transition: background-color 5000s ease-in-out 0s;
 }
+
+.cds--progress-bar__track {
+  background-color: colors.$gray-30;
+}
+
+.cds--progress-bar__bar {
+  background-color: var(--cds-primary);
+}