]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: update the upgrade layout by introducing new items 52395/head
authorNizamudeen A <nia@redhat.com>
Sun, 9 Jul 2023 13:36:02 +0000 (19:06 +0530)
committerNizamudeen A <nia@redhat.com>
Fri, 4 Aug 2023 17:35:28 +0000 (23:05 +0530)
Shows Cluster Health
Shows the count of mgr daemons
Shows a table with all the daemon versions
Display the cluster logs

Fixes: https://tracker.ceph.com/issues/62016
Signed-off-by: Nizamudeen A <nia@redhat.com>
34 files changed:
src/pybind/mgr/dashboard/controllers/daemon.py
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/logs/logs.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade-form/upgrade-start-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/daemon.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/tests/test_daemon.py

index eeea5a326255541fc08d071b51d15834265d836f..d5c288131b92e5263f92f49607341ee5d4684ec7 100644 (file)
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-from typing import Optional
+from typing import List, Optional
 
 from ..exceptions import DashboardException
 from ..security import Scope
@@ -31,3 +31,19 @@ class Daemon(RESTController):
         orch = OrchClient.instance()
         res = orch.daemons.action(action=action, daemon_name=daemon_name, image=container_image)
         return res
+
+    @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST])
+    @handle_orchestrator_error('daemon')
+    @RESTController.MethodMap(version=APIVersion.DEFAULT)
+    def list(self, daemon_types: Optional[List[str]] = None):
+        """List all daemons in the cluster. Also filter by the daemon types specified
+
+        :param daemon_types: List of daemon types to filter by.
+        :return: Returns list of daemons.
+        :rtype: list
+        """
+        orch = OrchClient.instance()
+        daemons = [d.to_dict() for d in orch.services.list_daemons()]
+        if daemon_types:
+            daemons = [d for d in daemons if d['daemon_type'] in daemon_types]
+        return daemons
index 98d14779ddf1f9755ca0b05641c064e96a244eea..d57fb9855a77a3714c908f8e82ed01189bdf05a7 100644 (file)
@@ -286,8 +286,19 @@ const routes: Routes = [
       },
       {
         path: 'upgrade',
+        canActivate: [ModuleStatusGuardService],
         component: UpgradeComponent,
-        data: { breadcrumbs: 'Cluster/Upgrade' }
+        data: {
+          moduleStatusGuardConfig: {
+            uiApiPath: 'orchestrator',
+            redirectTo: 'error',
+            backend: 'cephadm',
+            section: 'orch',
+            section_info: 'Orchestrator',
+            header: 'Orchestrator is not available'
+          },
+          breadcrumbs: 'Cluster/Upgrade'
+        }
       },
       {
         path: 'perf_counters/:type/:id',
index dbf2c28b687d144e8b1e013f9b0f7a7495a0e2f4..f92674e50743d724004522ecf0007f9710c6146f 100644 (file)
@@ -6,7 +6,7 @@
        class="nav-tabs"
        cdStatefulTab="logs"
        [cdStatefulTabDefault]="defaultTab"
-       [hidden]="hideNavLinks">
+       [hidden]="!showNavLinks">
     <ng-container ngbNavItem="cluster-logs">
       <a ngbNavLink
          i18n>Cluster Logs</a>
              *ngIf="clog">
           <div class="btn-group"
                role="group"
-               *ngIf="clog.length && !hideClusterLogs">
+               *ngIf="clog.length && showClusterLogs">
             <cd-download-button [objectItem]="clog"
                                 [textItem]="clogText"
                                 fileName="cluster_log"
-                                *ngIf="!hideDwnldCpyBtn">
+                                *ngIf="showDownloadCopyButton">
             </cd-download-button>
             <cd-copy-2-clipboard-button
                     [source]="clogText"
                     [byId]="false"
-                    *ngIf="!hideDwnldCpyBtn">
+                    *ngIf="showDownloadCopyButton">
             </cd-copy-2-clipboard-button>
           </div>
-          <div class="card-body">
+          <div class="card-body"
+               [ngClass]="{'overflow-auto': scrollable}">
             <p *ngFor="let line of clog">
               <span class="timestamp">{{ line.stamp | cdDate }}</span>
               <span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
          i18n>Audit Logs</a>
       <ng-template ngbNavContent>
         <div class="card bg-light mb-3"
-             *ngIf="audit_log && !hideAuditLogs">
+             *ngIf="audit_log && showAuditLogs">
           <div class="btn-group"
                role="group"
                *ngIf="audit_log.length">
             <cd-download-button [objectItem]="audit_log"
                                 [textItem]="auditLogText"
                                 fileName="audit_log"
-                                *ngIf="!hideDwnldCpyBtn">
+                                *ngIf="showDownloadCopyButton">
             </cd-download-button>
             <cd-copy-2-clipboard-button
                     [source]="auditLogText"
                     [byId]="false"
-                    *ngIf="!hideDwnldCpyBtn">
+                    *ngIf="showDownloadCopyButton">
             </cd-copy-2-clipboard-button>
           </div>
           <div class="card-body">
@@ -77,7 +78,7 @@
       <a ngbNavLink
          i18n>Daemon Logs</a>
       <ng-template ngbNavContent>
-        <ng-container *ngIf="!hideDaemonLogs && lokiServiceStatus$ | async as lokiServiceStatus ; else daemonLogsTpl ">
+        <ng-container *ngIf="showDaemonLogs && lokiServiceStatus$ | async as lokiServiceStatus ; else daemonLogsTpl ">
           <div *ngIf="promtailServiceStatus$ | async as promtailServiceStatus; else daemonLogsTpl">
             <cd-grafana i18n-title
                         title="Daemon logs"
@@ -97,7 +98,7 @@
 
 <ng-template #logFiltersTpl>
   <div class="row mb-3"
-       *ngIf="!hideFilterTools">
+       *ngIf="showFilterTools">
   <div class="col-lg-10 d-flex">
     <div class="col-sm-1 me-3">
       <label for="logs-priority"
index 54ab44250603c09cb7b73bfa70a8e97f0caf7a1b..56580e515193078d1f8e4c2f68f1dfd71718aeed 100644 (file)
@@ -52,3 +52,7 @@ p {
 ::ng-deep cd-logs ngb-timepicker input.ngb-tp-input {
   width: 3.5rem !important;
 }
+
+.card-body.overflow-auto {
+  height: 50vh;
+}
index acddc0194b21297825b931caf1939dca4a12e1df..4c381eab037e7fd725c0f82c9b7b30925c085e24 100644 (file)
@@ -16,19 +16,21 @@ import { Icons } from '~/app/shared/enum/icons.enum';
 })
 export class LogsComponent implements OnInit, OnDestroy {
   @Input()
-  hideClusterLogs = false;
+  showClusterLogs = true;
   @Input()
-  hideAuditLogs = false;
+  showAuditLogs = true;
   @Input()
-  hideDaemonLogs = false;
+  showDaemonLogs = true;
   @Input()
-  hideNavLinks = false;
+  showNavLinks = true;
   @Input()
-  hideFilterTools = false;
+  showFilterTools = true;
   @Input()
-  hideDwnldCpyBtn = false;
+  showDownloadCopyButton = true;
   @Input()
   defaultTab = '';
+  @Input()
+  scrollable = false;
 
   contentData: any;
   clog: Array<any>;
index cb68a3f1f515c5e2f5e0a73d8c2ce3ff1e33cd50..a6232ee082aad003d5ed5292ac267ec214ea26ff 100644 (file)
@@ -6,13 +6,14 @@ import { UpgradeService } from '~/app/shared/api/upgrade.service';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
 
 describe('UpgradeComponent', () => {
   let component: UpgradeComponent;
   let fixture: ComponentFixture<UpgradeComponent>;
 
   configureTestBed({
-    imports: [HttpClientTestingModule, SharedModule],
+    imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
     schemas: [NO_ERRORS_SCHEMA],
     declarations: [UpgradeComponent],
     providers: [UpgradeService]
index dd8f66950b3b079f15bf77986ea2c4ca55be926d..c91af1cdcce05b78c78dca80539616d1c2d402c2 100644 (file)
@@ -1,13 +1,49 @@
-<ng-container *ngIf="{upgradeInfo: upgradeInfo$ | async, error: upgradeInfoError$ } as upgrade">
-  <div class="row h-25 ms-1"
-       *ngIf="!upgrade.upgradeInfoError && upgrade.upgradeInfo as upgradeInfo; else checkUpgrade">
-    <ng-container *ngIf="healthData$ | async as healthData">
-      <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
-        <span class="bold">Current Version</span>
-        <span class="mt-1">{{ version }}</span>
+<div class="row h-25"
+     *cdScope="'configOpt'">
+  <ng-container *ngIf="healthData$ | async as healthData">
+    <cd-card class="col-sm-3 px-3 d-flex"
+             cardTitle="New Version"
+             i18n-cardTitle
+             aria-label="New Version"
+             i18n-aria-label
+             id="newVersionAvailable">
+      <div class="d-flex flex-column justify-content-center align-items-center"
+           *ngIf="info$ | async as info; else checkingForUpgradeStatus">
+        <ng-container *ngIf="info.versions.length > 0; else noUpgradesAvailable">
+          <div i18n-ngbTooltip
+               [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
+            <button class="btn btn-accent mt-2"
+                    id="upgrade"
+                    aria-label="Upgrade now"
+                    (click)="upgradeNow(info.versions[info.versions.length - 1])"
+                    [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
+                    i18n>Upgrade to {{ info.versions[info.versions.length - 1] }}</button>
+          </div>
+          <a class="mt-2 link-primary mb-2"
+             (click)="startUpgradeModal()"
+             i18n>Select another version...</a>
+        </ng-container>
+      </div>
+    </cd-card>
+
+    <cd-card class="col-sm-3 px-3 d-flex"
+             cardTitle="Current Version"
+             i18n-cardTitle
+             aria-label="Current Version"
+             i18n-aria-label
+             id="currentVersion">
+      <div class="d-flex flex-column justify-content-center align-items-center">
+        <h5>{{ version }}</h5>
       </div>
-      <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
-        <span class="bold">Cluster Status</span>
+    </cd-card>
+
+    <cd-card class="col-sm-3 px-3 d-flex"
+             cardTitle="Cluster Status"
+             i18n-cardTitle
+             aria-label="Cluster Status"
+             i18n-aria-label
+             id="clusterStatus">
+      <div class="d-flex flex-column justify-content-center align-items-center">
         <ng-template #healthChecks>
           <ul>
             <li *ngFor="let check of healthData.health.checks">
              [ngStyle]="healthData.health.status | healthColor"
              [ngbPopover]="healthChecks"
              popoverClass="info-card-popover-cluster-status">
-             {{ healthData.health.status | healthLabel | uppercase }}
+            {{ healthData.health.status | healthLabel | uppercase }}
           <i *ngIf="healthData.health?.status !== 'HEALTH_OK'"
              class="fa fa-exclamation-triangle"></i>
         </div>
       </div>
-      <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
-        <span class="bold">Upgrade Status</span>
-        <ng-container *ngIf="upgradeInfo.versions.length > 0; else noUpgradesAvailable">
-          <span class="mt-2"
-                i18n>
-          <i [ngClass]="[icons.up]"
-             class="text-info"></i>
-            Upgrade available</span>
-          <div i18n-ngbTooltip
-               [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
-            <button class="btn btn-accent mt-2"
-                    id="upgrade"
-                    aria-label="Upgrade now"
-                    [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
-                    (click)="startUpgradeModal()"
-                    i18n>Upgrade now</button>
-          </div>
-        </ng-container>
-      </div>
-      <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
-        <span class="bold">MGR Count</span>
-        <span class="mt-1">
+    </cd-card>
+
+    <cd-card class="col-sm-3 px-3 d-flex"
+             cardTitle="MGR Count"
+             i18n-cardTitle
+             aria-label="MGR Count"
+             i18n-aria-label
+             id="mgrCount">
+      <div class="d-flex flex-column justify-content-center align-items-center">
+        <h5>
           <i class="text-success"
              [ngClass]="[icons.success]"
              *ngIf="(healthData.mgr_map | mgrSummary).total > 1; else warningIcon">
           </i>
           {{ (healthData.mgr_map | mgrSummary).total }}
-        </span>
+        </h5>
       </div>
+    </cd-card>
 
-      <div class="d-flex mt-3 p-0">
-        <dl class="w-50"
-            *ngIf="fsid$ | async as fsid">
-          <dt class="bold mt-5"
-              i18n>Cluster FSID</dt>
-          <dd class="mt-2">{{ fsid }}</dd>
+    <div class="d-flex mt-3">
+      <dl class="w-50"
+          *ngIf="fsid$ | async as fsid">
+        <dt class="bold mt-5"
+            i18n>Cluster FSID</dt>
+        <dd class="mt-2">{{ fsid }}</dd>
+
+        <ng-container *ngIf="info$ | async as info; else loadingDetails">
           <dt class="bold mt-5"
               i18n>Release Image</dt>
-          <dd class="mt-2">{{ upgradeInfo.image }}</dd>
+          <dd class="mt-2">{{ info.image }}</dd>
           <dt class="bold mt-5"
               i18n>Registry</dt>
-          <dd class="mt-2">{{ upgradeInfo.registry }}</dd>
-        </dl>
+          <dd class="mt-2">{{ info.registry }}</dd>
+        </ng-container>
+      </dl>
+      <div class="w-50">
+        <ng-container *ngIf="daemons$ | async as daemons">
+          <legend class="cd-header"
+                  i18n>Daemon versions</legend>
+          <div>
+            <cd-table #daemonsTable
+                      [data]="daemons"
+                      selectionType="single"
+                      [columns]="columns"
+                      columnMode="flex"
+                      [limit]="5">
+            </cd-table>
+          </div>
+        </ng-container>
       </div>
-    </ng-container>
-  </div>
-</ng-container>
+    </div>
 
-<ng-template #checkUpgrade>
-  <div class="row h-75 justify-content-center align-items-center">
-    <h3 class="mt-1 bold text-center"
-        id="checking-for-upgrades"
-        i18n>Checking for upgrades
-      <i [ngClass]="[icons.spin, icons.spinner]"></i>
-    </h3>
-  </div>
-</ng-template>
+    <legend class="cd-header"
+            i18n>Cluster logs</legend>
+    <cd-logs [showAuditLogs]="false"
+             [showDaemonLogs]="false"
+             [showNavLinks]="false"
+             [showFilterTools]="false"
+             [showDownloadCopyButton]="false"
+             defaultTab="cluster-logs"
+             [scrollable]="true"></cd-logs>
+  </ng-container>
+</div>
 
 <ng-template #noUpgradesAvailable>
   <span class="mt-1"
      title="To upgrade, you need minimum 2 mgr daemons.">
   </i>
 </ng-template>
+
+<ng-template #checkingForUpgradeStatus>
+  <div class="d-flex flex-column justify-content-center align-items-center"
+       *ngIf="!errorMessage; else upgradeStatusError">
+    <button class="btn btn-accent mt-2 mb-4"
+            id="upgrade"
+            aria-label="Upgrade now"
+            [disabled]="true"
+            i18n>Checking for upgrades
+      <i [ngClass]="[icons.spin, icons.spinner]"></i>
+    </button>
+  </div>
+</ng-template>
+
+<ng-template #loadingDetails>
+  <div class="w-50"
+       *ngIf="!errorMessage; else upgradeInfoError">
+    <span class="text-info justify-content-center align-items-center"
+          i18n>Fetching registry informations
+      <i [ngClass]="[icons.spin, icons.spinner]"></i>
+    </span>
+  </div>
+</ng-template>
+
+<ng-template #upgradeStatusError>
+  <div class="d-flex flex-column justify-content-center align-items-center">
+    <span class="text-danger mt-2 mb-4"
+          id="upgrade-status-error"
+          i18n>
+      <i [ngClass]="[icons.danger]"></i>
+      {{ errorMessage }}
+    </span>
+  </div>
+</ng-template>
+
+<ng-template #upgradeInfoError>
+  <span class="text-danger justify-content-center align-items-center"
+        i18n>
+    <i [ngClass]="[icons.danger]"></i>
+    Failed to fetch registry informations
+  </span>
+</ng-template>
index 0250fe9b122db44cc202f86b4951ecd450f2b475..407a7da58e12de01a31d672ea0f831b94399dc9a 100644 (file)
@@ -7,9 +7,13 @@ import { BehaviorSubject, of } from 'rxjs';
 import { UpgradeService } from '~/app/shared/api/upgrade.service';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
 import { HealthService } from '~/app/shared/api/health.service';
 import { SharedModule } from '~/app/shared/shared.module';
+import { LogsComponent } from '../logs/logs.component';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ToastrModule } from 'ngx-toastr';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 
 export class SummaryServiceMock {
   summaryDataSource = new BehaviorSubject({
@@ -47,9 +51,9 @@ describe('UpgradeComponent', () => {
   };
 
   configureTestBed({
-    imports: [HttpClientTestingModule, SharedModule],
+    imports: [HttpClientTestingModule, SharedModule, NgbNavModule, ToastrModule.forRoot()],
+    declarations: [UpgradeComponent, LogsComponent],
     schemas: [NO_ERRORS_SCHEMA],
-    declarations: [UpgradeComponent],
     providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }]
   });
 
@@ -59,6 +63,15 @@ describe('UpgradeComponent', () => {
     upgradeInfoSpy = spyOn(TestBed.inject(UpgradeService), 'list').and.callFake(() => of(null));
     getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
     getHealthSpy.and.returnValue(of(healthPayload));
+    const upgradeInfoPayload = {
+      image: 'quay.io/ceph-test/ceph',
+      registry: 'quay.io',
+      versions: ['18.1.0', '18.1.1', '18.1.2']
+    };
+    upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+    spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() => ({
+      configOpt: { read: true }
+    }));
     fixture.detectChanges();
   });
 
@@ -67,16 +80,12 @@ describe('UpgradeComponent', () => {
   });
 
   it('should load the view once check for upgrade is done', () => {
-    const upgradeInfoPayload = {
-      image: 'quay.io/ceph-test/ceph',
-      registry: 'quay.io',
-      versions: ['18.1.0', '18.1.1', '18.1.2']
-    };
-    upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
     component.ngOnInit();
     fixture.detectChanges();
-    const firstCellSpan = fixture.debugElement.nativeElement.querySelector('span');
-    expect(firstCellSpan.textContent).toBe('Current Version');
+    const firstCellSpan = fixture.debugElement.nativeElement.querySelector(
+      'cd-card[cardTitle="New Version"] .card-title'
+    );
+    expect(firstCellSpan.textContent).toContain('New Version');
   });
 
   it('should show button to Upgrade if a new version is available', () => {
@@ -108,11 +117,15 @@ describe('UpgradeComponent', () => {
   });
 
   it('should show the loading screen while the api call is pending', () => {
-    const loading = fixture.debugElement.nativeElement.querySelector('h3');
-    expect(loading.textContent).toBe('Checking for upgrades ');
+    upgradeInfoSpy.and.returnValue(of(null));
+    component.ngOnInit();
+    fixture.detectChanges();
+    const loading = fixture.debugElement.nativeElement.querySelector('#newVersionAvailable');
+    expect(loading.textContent).toContain('Checking for upgrade');
   });
 
   it('should upgrade only when there are more than 1 mgr', () => {
+    // Only one mgr in payload
     const upgradeInfoPayload = {
       image: 'quay.io/ceph-test/ceph',
       registry: 'quay.io',
@@ -146,4 +159,13 @@ describe('UpgradeComponent', () => {
     fixture.detectChanges();
     expect(upgradeBtn.disabled).toBeFalsy();
   });
+
+  it('should show the error message when the upgrade fetch fails', () => {
+    upgradeInfoSpy.and.returnValue(of(null));
+    component.errorMessage = 'Failed to retrieve';
+    component.ngOnInit();
+    fixture.detectChanges();
+    const loading = fixture.debugElement.nativeElement.querySelector('#upgrade-status-error');
+    expect(loading.textContent).toContain('Failed to retrieve');
+  });
 });
index b779a213d6031b0f77030a4c179830faa336987e..a271a24202d72e479f37ebd8884b06c060ffe033 100644 (file)
@@ -1,13 +1,17 @@
 import { Component, OnInit } from '@angular/core';
 import { Observable, of } from 'rxjs';
-import { catchError, ignoreElements, tap } from 'rxjs/operators';
+import { catchError, publishReplay, refCount, tap } from 'rxjs/operators';
+import { DaemonService } from '~/app/shared/api/daemon.service';
 import { HealthService } from '~/app/shared/api/health.service';
 import { UpgradeService } from '~/app/shared/api/upgrade.service';
 import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { Daemon } from '~/app/shared/models/daemon.interface';
 import { Permission } from '~/app/shared/models/permissions';
 import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { NotificationService } from '~/app/shared/services/notification.service';
 import { SummaryService } from '~/app/shared/services/summary.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { UpgradeStartModalComponent } from './upgrade-form/upgrade-start-modal.component';
@@ -19,13 +23,16 @@ import { UpgradeStartModalComponent } from './upgrade-form/upgrade-start-modal.c
 })
 export class UpgradeComponent implements OnInit {
   version: string;
-  upgradeInfo$: Observable<UpgradeInfoInterface>;
-  upgradeInfoError$: Observable<any>;
+  info$: Observable<UpgradeInfoInterface>;
   permission: Permission;
   healthData$: Observable<any>;
+  daemons$: Observable<Daemon[]>;
   fsid$: Observable<any>;
   modalRef: NgbModalRef;
   upgradableVersions: string[];
+  errorMessage: string;
+
+  columns: CdTableColumn[] = [];
 
   icons = Icons;
 
@@ -33,33 +40,72 @@ export class UpgradeComponent implements OnInit {
     private modalService: ModalService,
     private summaryService: SummaryService,
     private upgradeService: UpgradeService,
-    private authStorageService: AuthStorageService,
-    private healthService: HealthService
-  ) {
-    this.permission = this.authStorageService.getPermissions().configOpt;
-  }
+    private healthService: HealthService,
+    private daemonService: DaemonService,
+    private notificationService: NotificationService
+  ) {}
 
   ngOnInit(): void {
+    this.columns = [
+      {
+        name: $localize`Daemon name`,
+        prop: 'daemon_name',
+        flexGrow: 1,
+        filterable: true
+      },
+      {
+        name: $localize`Version`,
+        prop: 'version',
+        flexGrow: 1,
+        filterable: true
+      }
+    ];
+
     this.summaryService.subscribe((summary) => {
       const version = summary.version.replace('ceph version ', '').split('-');
       this.version = version[0];
     });
-    this.upgradeInfo$ = this.upgradeService
-      .list()
-      .pipe(
-        tap((upgradeInfo: UpgradeInfoInterface) => (this.upgradableVersions = upgradeInfo.versions))
-      );
-    this.upgradeInfoError$ = this.upgradeInfo$?.pipe(
-      ignoreElements(),
-      catchError((error) => of(error))
+    this.info$ = this.upgradeService.list().pipe(
+      tap((upgradeInfo: UpgradeInfoInterface) => (this.upgradableVersions = upgradeInfo.versions)),
+      publishReplay(1),
+      refCount(),
+      catchError((err) => {
+        err.preventDefault();
+        this.errorMessage = $localize`Not retrieving upgrades`;
+        this.notificationService.show(
+          NotificationType.error,
+          this.errorMessage,
+          err.error.detail || err.error.message
+        );
+        return of(null);
+      })
     );
     this.healthData$ = this.healthService.getMinimalHealth();
+    this.daemons$ = this.daemonService.list(this.upgradeService.upgradableServiceTypes);
     this.fsid$ = this.healthService.getClusterFsid();
   }
 
   startUpgradeModal() {
     this.modalRef = this.modalService.show(UpgradeStartModalComponent, {
-      versions: this.upgradableVersions.sort()
+      versions: this.upgradableVersions
+    });
+  }
+
+  upgradeNow(version: string) {
+    this.upgradeService.start(version).subscribe({
+      error: (error) => {
+        this.notificationService.show(
+          NotificationType.error,
+          $localize`Failed to start the upgrade`,
+          error
+        );
+      },
+      complete: () => {
+        this.notificationService.show(
+          NotificationType.success,
+          $localize`Started upgrading the cluster`
+        );
+      }
     });
   }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.html
deleted file mode 100644 (file)
index 9b7bf03..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-<div class="d-flex pl-1 pb-2 pt-2">
-  <div class="ms-2 me-auto">
-    <a [routerLink]="link"
-       *ngIf="link && total > 0; else noLinkTitle"
-       [ngPlural]="total"
-       i18n>
-        {{ total }}
-      <ng-template ngPluralCase="=0">{{ title }}</ng-template>
-      <ng-template ngPluralCase="=1">{{ title }}</ng-template>
-      <ng-template ngPluralCase="other">{{ title }}s</ng-template>
-    </a>
-  </div>
-
-  <ng-container [ngSwitch]="summaryType">
-    <ng-container *ngSwitchCase="'iscsi'">
-      <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
-    </ng-container>
-    <ng-container *ngSwitchCase="'osd'">
-      <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
-    </ng-container>
-    <ng-container *ngSwitchCase="'simplified'">
-      <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
-    </ng-container>
-    <ng-container *ngSwitchDefault>
-      <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
-    </ng-container>
-  </ng-container>
-</div>
-
-<ng-template #defaultSummary>
-  <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
-    <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
-      {{ data.success }}
-    </span>
-    <span *ngIf="data.categoryPgAmount?.clean">
-      {{ data.categoryPgAmount?.clean }}
-    </span>
-    <i class="text-success"
-       [ngClass]="[icons.success]">
-    </i>
-  </span>
-  <span *ngIf="data.info"
-        class="ms-2">
-    <span *ngIf="data.info">
-      {{ data.info }}
-    </span>
-    <i class="text-info"
-       [ngClass]="[icons.danger]">
-    </i>
-  </span>
-  <span *ngIf="data.warn || data.categoryPgAmount?.warning"
-        class="ms-2">
-    <span *ngIf="data.warn">
-      {{ data.warn }}
-    </span>
-    <span *ngIf="data.categoryPgAmount?.warning">
-      {{ data.categoryPgAmount?.warning }}
-    </span>
-    <i class="text-warning"
-       [ngClass]="[icons.warning]">
-    </i>
-  </span>
-  <span *ngIf="data.error || data.categoryPgAmount?.unknown"
-        class="ms-2">
-    <span *ngIf="data.error">
-      {{ data.error }}
-    </span>
-    <span *ngIf="data.categoryPgAmount?.unknown">
-      {{ data.categoryPgAmount?.unknown }}
-    </span>
-    <i class="text-danger"
-       [ngClass]="[icons.danger]">
-    </i>
-  </span>
-  <span *ngIf="data.categoryPgAmount?.working"
-        class="ms-2">
-    <span *ngIf="data.categoryPgAmount?.working">
-      {{ data.categoryPgAmount?.working }}
-    </span>
-    <i class="text-warning"
-       [ngClass]="[icons.spinner, icons.spin]">
-    </i>
-  </span>
-</ng-template>
-
-<ng-template #osdSummary>
-  <span *ngIf="data.up === data.in">
-    {{ data.up }}
-    <i class="text-success"
-       [ngClass]="[icons.success]">
-    </i>
-  </span>
-  <span *ngIf="data.up !== data.in">
-    {{ data.up }}
-    <span class="fw-bold text-success">
-        up
-    </span>
-  </span>
-  <span *ngIf="data.in !== data.up"
-        class="ms-2">
-    {{ data.in }}
-    <span class="fw-bold text-success">
-        in
-    </span>
-  </span>
-  <span *ngIf="data.down"
-        class="ms-2">
-    {{ data.down }}
-    <span class="fw-bold text-danger me-2">
-        down
-    </span>
-  </span>
-  <span *ngIf="data.out"
-        class="ms-2">
-    {{ data.out }}
-    <span class="fw-bold text-danger me-2">
-        out
-    </span>
-  </span>
-  <span *ngIf="data.nearfull"
-        class="ms-2">
-        {{ data.nearfull }}
-    <span class="fw-bold text-warning me-2">
-      nearfull</span></span>
-  <span *ngIf="data.full"
-        class="ms-2">
-        {{ data.full }}
-    <span class="fw-bold text-danger">
-      full
-    </span>
-  </span>
-</ng-template>
-
-<ng-template #iscsiSummary>
-  <span>
-    {{ data.up }}
-    <i class="text-success"
-       *ngIf="data.up || data.up === 0"
-       [ngClass]="[icons.success]">
-    </i>
-  </span>
-  <span *ngIf="data.down"
-        class="ms-2">
-        {{ data.down }}
-    <i class="text-danger"
-       [ngClass]="[icons.danger]">
-    </i>
-  </span>
-</ng-template>
-
-<ng-template #simplifiedSummary>
-  <span>
-    {{ data }}
-    <i class="text-success"
-       [ngClass]="[icons.success]"></i>
-  </span>
-</ng-template>
-
-<ng-template #noLinkTitle>
-  <span *ngIf="total || total === 0"
-        [ngPlural]="total">
-    {{ total }}
-    <ng-template ngPluralCase="=0">{{ title }}</ng-template>
-    <ng-template ngPluralCase="=1">{{ title }}</ng-template>
-    <ng-template ngPluralCase="other">{{ title }}s</ng-template>
-  </span>
-</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.scss
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.spec.ts
deleted file mode 100644 (file)
index 8932e67..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { CardRowComponent } from './card-row.component';
-
-describe('CardRowComponent', () => {
-  let component: CardRowComponent;
-  let fixture: ComponentFixture<CardRowComponent>;
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      declarations: [CardRowComponent]
-    }).compileComponents();
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(CardRowComponent);
-    component = fixture.componentInstance;
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card-row/card-row.component.ts
deleted file mode 100644 (file)
index 90c9391..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Component, Input, OnChanges } from '@angular/core';
-import { Icons } from '~/app/shared/enum/icons.enum';
-
-@Component({
-  selector: 'cd-card-row',
-  templateUrl: './card-row.component.html',
-  styleUrls: ['./card-row.component.scss']
-})
-export class CardRowComponent implements OnChanges {
-  @Input()
-  title: string;
-
-  @Input()
-  link: string;
-
-  @Input()
-  data: any;
-
-  @Input()
-  summaryType = 'default';
-
-  icons = Icons;
-  total: number;
-
-  ngOnChanges(): void {
-    if (this.data.total || this.data.total === 0) {
-      this.total = this.data.total;
-    } else if (this.summaryType === 'iscsi') {
-      this.total = this.data.up + this.data.down || 0;
-    } else {
-      this.total = this.data;
-    }
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.html
deleted file mode 100644 (file)
index a2f5b9d..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<div class="card shadow-sm flex-fill">
-  <h4 class="card-title mt-4 ms-4 mb-0">
-    {{ cardTitle }}
-  </h4>
-  <div class="card-body ps-0 pe-0">
-    <ng-content></ng-content>
-  </div>
-</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.scss
deleted file mode 100644 (file)
index fdf19a0..0000000
+++ /dev/null
@@ -1,5 +0,0 @@
-.card-body {
-  display: flex;
-  flex-direction: column;
-  justify-content: space-evenly;
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.spec.ts
deleted file mode 100644 (file)
index 287e1cf..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { CardComponent } from './card.component';
-
-describe('CardComponent', () => {
-  let component: CardComponent;
-  let fixture: ComponentFixture<CardComponent>;
-
-  configureTestBed({
-    imports: [RouterTestingModule],
-    declarations: [CardComponent]
-  });
-
-  beforeEach(() => {
-    fixture = TestBed.createComponent(CardComponent);
-    component = fixture.componentInstance;
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-
-  it('Setting cards title makes title visible', () => {
-    const title = 'Card Title';
-    component.cardTitle = title;
-    fixture.detectChanges();
-    const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
-
-    expect(titleDiv.textContent).toContain(title);
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/card/card.component.ts
deleted file mode 100644 (file)
index 8e93cc8..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Component, Input } from '@angular/core';
-
-@Component({
-  selector: 'cd-card',
-  templateUrl: './card.component.html',
-  styleUrls: ['./card.component.scss']
-})
-export class CardComponent {
-  @Input()
-  cardTitle: string;
-}
index 9e1529c3e3794042468d698f030457532cf2c372..50db430906e273d4e358032fde85cd3c548b22dc 100644 (file)
@@ -9,12 +9,10 @@ import { SimplebarAngularModule } from 'simplebar-angular';
 
 import { SharedModule } from '~/app/shared/shared.module';
 import { CephSharedModule } from '../shared/ceph-shared.module';
-import { CardComponent } from './card/card.component';
 import { DashboardAreaChartComponent } from './dashboard-area-chart/dashboard-area-chart.component';
 import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component';
 import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component';
 import { DashboardV3Component } from './dashboard/dashboard-v3.component';
-import { CardRowComponent } from './card-row/card-row.component';
 import { PgSummaryPipe } from './pg-summary.pipe';
 
 @NgModule({
@@ -34,20 +32,12 @@ import { PgSummaryPipe } from './pg-summary.pipe';
 
   declarations: [
     DashboardV3Component,
-    CardComponent,
     DashboardPieComponent,
-    CardRowComponent,
     PgSummaryPipe,
     DashboardAreaChartComponent,
     DashboardTimeSelectorComponent
   ],
 
-  exports: [
-    DashboardV3Component,
-    CardComponent,
-    CardRowComponent,
-    DashboardAreaChartComponent,
-    DashboardTimeSelectorComponent
-  ]
+  exports: [DashboardV3Component, DashboardAreaChartComponent, DashboardTimeSelectorComponent]
 })
 export class DashboardV3Module {}
index f2f5d0bb50832cc94c7ab14e70e1c679d6e94642..c274a2f5406772c1eeb4688abaa1b6eaafc76aa2 100644 (file)
@@ -18,8 +18,6 @@ import { SummaryService } from '~/app/shared/services/summary.service';
 import { SharedModule } from '~/app/shared/shared.module';
 import { configureTestBed } from '~/testing/unit-test-helper';
 import { PgCategoryService } from '../../shared/pg-category.service';
-import { CardRowComponent } from '../card-row/card-row.component';
-import { CardComponent } from '../card/card.component';
 import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component';
 import { PgSummaryPipe } from '../pg-summary.pipe';
 import { DashboardV3Component } from './dashboard-v3.component';
@@ -138,13 +136,7 @@ describe('Dashbord Component', () => {
 
   configureTestBed({
     imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule],
-    declarations: [
-      DashboardV3Component,
-      CardComponent,
-      DashboardPieComponent,
-      CardRowComponent,
-      PgSummaryPipe
-    ],
+    declarations: [DashboardV3Component, DashboardPieComponent, PgSummaryPipe],
     schemas: [NO_ERRORS_SCHEMA],
     providers: [
       { provide: SummaryService, useClass: SummaryServiceMock },
index f887bcd8224f726a2cf6417526f240996c19e42b..8a2a158f0a3d50f02e263a6968362dfc6555c1c2 100644 (file)
@@ -5,8 +5,6 @@ import { of } from 'rxjs';
 import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
 import { RgwDaemon } from '../models/rgw-daemon';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { CardComponent } from '../../dashboard-v3/card/card.component';
-import { CardRowComponent } from '../../dashboard-v3/card-row/card-row.component';
 import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
 import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
 import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
@@ -14,6 +12,9 @@ import { RgwZoneService } from '~/app/shared/api/rgw-zone.service';
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
 import { HealthService } from '~/app/shared/api/health.service';
+import { CardRowComponent } from '~/app/shared/components/card-row/card-row.component';
+import { CardComponent } from '~/app/shared/components/card/card.component';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
 
 describe('RgwOverviewDashboardComponent', () => {
   let component: RgwOverviewDashboardComponent;
@@ -134,6 +135,7 @@ describe('RgwOverviewDashboardComponent', () => {
         CardRowComponent,
         DimlessBinaryPipe
       ],
+      schemas: [NO_ERRORS_SCHEMA],
       imports: [HttpClientTestingModule]
     }).compileComponents();
   });
index a66ed7edb1983d1a144eeab89d6dd4aea4825f8b..0912e693139f5d8687cb30a8485d55e98d2f7113 100644 (file)
@@ -1,7 +1,9 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
 
 import { cdEncode } from '~/app/shared/decorators/cd-encode';
+import { Daemon } from '../models/daemon.interface';
 
 @cdEncode
 @Injectable({
@@ -25,4 +27,10 @@ export class DaemonService {
       }
     );
   }
+
+  list(daemonTypes: string[]): Observable<Daemon[]> {
+    return this.http.get<Daemon[]>(this.url, {
+      params: { daemon_types: daemonTypes }
+    });
+  }
 }
index 6e713ae512299ecc8173be1e1539ebff9bf1622c..8421fc57f37c96c3737a72128bb6136fb513ff7a 100644 (file)
@@ -11,6 +11,19 @@ import { UpgradeInfoInterface } from '../models/upgrade.interface';
 export class UpgradeService extends ApiClient {
   baseURL = 'api/cluster/upgrade';
 
+  upgradableServiceTypes = [
+    'mgr',
+    'mon',
+    'crash',
+    'osd',
+    'mds',
+    'rgw',
+    'rbd-mirror',
+    'cephfs-mirror',
+    'iscsi',
+    'nfs'
+  ];
+
   constructor(private http: HttpClient, private summaryService: SummaryService) {
     super();
   }
@@ -38,7 +51,7 @@ export class UpgradeService extends ApiClient {
         cVersion[0] === tVersion[0] && (cVersion[1] < tVersion[1] || cVersion[2] < tVersion[2])
       );
     });
-    upgradeInfo.versions = upgradableVersions;
+    upgradeInfo.versions = upgradableVersions.sort();
     return upgradeInfo;
   }
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html
new file mode 100644 (file)
index 0000000..9b7bf03
--- /dev/null
@@ -0,0 +1,167 @@
+<div class="d-flex pl-1 pb-2 pt-2">
+  <div class="ms-2 me-auto">
+    <a [routerLink]="link"
+       *ngIf="link && total > 0; else noLinkTitle"
+       [ngPlural]="total"
+       i18n>
+        {{ total }}
+      <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+      <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+      <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+    </a>
+  </div>
+
+  <ng-container [ngSwitch]="summaryType">
+    <ng-container *ngSwitchCase="'iscsi'">
+      <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchCase="'osd'">
+      <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchCase="'simplified'">
+      <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
+    </ng-container>
+    <ng-container *ngSwitchDefault>
+      <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
+    </ng-container>
+  </ng-container>
+</div>
+
+<ng-template #defaultSummary>
+  <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
+    <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
+      {{ data.success }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.clean">
+      {{ data.categoryPgAmount?.clean }}
+    </span>
+    <i class="text-success"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.info"
+        class="ms-2">
+    <span *ngIf="data.info">
+      {{ data.info }}
+    </span>
+    <i class="text-info"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+  <span *ngIf="data.warn || data.categoryPgAmount?.warning"
+        class="ms-2">
+    <span *ngIf="data.warn">
+      {{ data.warn }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.warning">
+      {{ data.categoryPgAmount?.warning }}
+    </span>
+    <i class="text-warning"
+       [ngClass]="[icons.warning]">
+    </i>
+  </span>
+  <span *ngIf="data.error || data.categoryPgAmount?.unknown"
+        class="ms-2">
+    <span *ngIf="data.error">
+      {{ data.error }}
+    </span>
+    <span *ngIf="data.categoryPgAmount?.unknown">
+      {{ data.categoryPgAmount?.unknown }}
+    </span>
+    <i class="text-danger"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+  <span *ngIf="data.categoryPgAmount?.working"
+        class="ms-2">
+    <span *ngIf="data.categoryPgAmount?.working">
+      {{ data.categoryPgAmount?.working }}
+    </span>
+    <i class="text-warning"
+       [ngClass]="[icons.spinner, icons.spin]">
+    </i>
+  </span>
+</ng-template>
+
+<ng-template #osdSummary>
+  <span *ngIf="data.up === data.in">
+    {{ data.up }}
+    <i class="text-success"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.up !== data.in">
+    {{ data.up }}
+    <span class="fw-bold text-success">
+        up
+    </span>
+  </span>
+  <span *ngIf="data.in !== data.up"
+        class="ms-2">
+    {{ data.in }}
+    <span class="fw-bold text-success">
+        in
+    </span>
+  </span>
+  <span *ngIf="data.down"
+        class="ms-2">
+    {{ data.down }}
+    <span class="fw-bold text-danger me-2">
+        down
+    </span>
+  </span>
+  <span *ngIf="data.out"
+        class="ms-2">
+    {{ data.out }}
+    <span class="fw-bold text-danger me-2">
+        out
+    </span>
+  </span>
+  <span *ngIf="data.nearfull"
+        class="ms-2">
+        {{ data.nearfull }}
+    <span class="fw-bold text-warning me-2">
+      nearfull</span></span>
+  <span *ngIf="data.full"
+        class="ms-2">
+        {{ data.full }}
+    <span class="fw-bold text-danger">
+      full
+    </span>
+  </span>
+</ng-template>
+
+<ng-template #iscsiSummary>
+  <span>
+    {{ data.up }}
+    <i class="text-success"
+       *ngIf="data.up || data.up === 0"
+       [ngClass]="[icons.success]">
+    </i>
+  </span>
+  <span *ngIf="data.down"
+        class="ms-2">
+        {{ data.down }}
+    <i class="text-danger"
+       [ngClass]="[icons.danger]">
+    </i>
+  </span>
+</ng-template>
+
+<ng-template #simplifiedSummary>
+  <span>
+    {{ data }}
+    <i class="text-success"
+       [ngClass]="[icons.success]"></i>
+  </span>
+</ng-template>
+
+<ng-template #noLinkTitle>
+  <span *ngIf="total || total === 0"
+        [ngPlural]="total">
+    {{ total }}
+    <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+    <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+    <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+  </span>
+</ng-template>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.spec.ts
new file mode 100644 (file)
index 0000000..8932e67
--- /dev/null
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CardRowComponent } from './card-row.component';
+
+describe('CardRowComponent', () => {
+  let component: CardRowComponent;
+  let fixture: ComponentFixture<CardRowComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [CardRowComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CardRowComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts
new file mode 100644 (file)
index 0000000..90c9391
--- /dev/null
@@ -0,0 +1,34 @@
+import { Component, Input, OnChanges } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+  selector: 'cd-card-row',
+  templateUrl: './card-row.component.html',
+  styleUrls: ['./card-row.component.scss']
+})
+export class CardRowComponent implements OnChanges {
+  @Input()
+  title: string;
+
+  @Input()
+  link: string;
+
+  @Input()
+  data: any;
+
+  @Input()
+  summaryType = 'default';
+
+  icons = Icons;
+  total: number;
+
+  ngOnChanges(): void {
+    if (this.data.total || this.data.total === 0) {
+      this.total = this.data.total;
+    } else if (this.summaryType === 'iscsi') {
+      this.total = this.data.up + this.data.down || 0;
+    } else {
+      this.total = this.data;
+    }
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html
new file mode 100644 (file)
index 0000000..a2f5b9d
--- /dev/null
@@ -0,0 +1,8 @@
+<div class="card shadow-sm flex-fill">
+  <h4 class="card-title mt-4 ms-4 mb-0">
+    {{ cardTitle }}
+  </h4>
+  <div class="card-body ps-0 pe-0">
+    <ng-content></ng-content>
+  </div>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.scss
new file mode 100644 (file)
index 0000000..fdf19a0
--- /dev/null
@@ -0,0 +1,5 @@
+.card-body {
+  display: flex;
+  flex-direction: column;
+  justify-content: space-evenly;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.spec.ts
new file mode 100644 (file)
index 0000000..287e1cf
--- /dev/null
@@ -0,0 +1,33 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CardComponent } from './card.component';
+
+describe('CardComponent', () => {
+  let component: CardComponent;
+  let fixture: ComponentFixture<CardComponent>;
+
+  configureTestBed({
+    imports: [RouterTestingModule],
+    declarations: [CardComponent]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(CardComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('Setting cards title makes title visible', () => {
+    const title = 'Card Title';
+    component.cardTitle = title;
+    fixture.detectChanges();
+    const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
+
+    expect(titleDiv.textContent).toContain(title);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts
new file mode 100644 (file)
index 0000000..8e93cc8
--- /dev/null
@@ -0,0 +1,11 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+  selector: 'cd-card',
+  templateUrl: './card.component.html',
+  styleUrls: ['./card.component.scss']
+})
+export class CardComponent {
+  @Input()
+  cardTitle: string;
+}
index 5350e2bd50b00b0ddc5299ada572c2a00600a7be..17f418d1e148f707dfc011dcf27d5fdd4e02dec5 100644 (file)
@@ -49,6 +49,8 @@ import { SubmitButtonComponent } from './submit-button/submit-button.component';
 import { TelemetryNotificationComponent } from './telemetry-notification/telemetry-notification.component';
 import { UsageBarComponent } from './usage-bar/usage-bar.component';
 import { WizardComponent } from './wizard/wizard.component';
+import { CardComponent } from './card/card.component';
+import { CardRowComponent } from './card-row/card-row.component';
 
 @NgModule({
   imports: [
@@ -101,7 +103,9 @@ import { WizardComponent } from './wizard/wizard.component';
     WizardComponent,
     CustomLoginBannerComponent,
     CdLabelComponent,
-    ColorClassFromTextPipe
+    ColorClassFromTextPipe,
+    CardComponent,
+    CardRowComponent
   ],
   providers: [],
   exports: [
@@ -131,7 +135,9 @@ import { WizardComponent } from './wizard/wizard.component';
     MotdComponent,
     WizardComponent,
     CustomLoginBannerComponent,
-    CdLabelComponent
+    CdLabelComponent,
+    CardComponent,
+    CardRowComponent
   ]
 })
 export class ComponentsModule {}
index 1b5e6db946df8319bf6931cd68b008666d676c49..6e39f4bff138eabe8172f34be07f33192474ab50 100644 (file)
@@ -37,6 +37,7 @@ import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { CdUserConfig } from '~/app/shared/models/cd-user-config';
 import { TimerService } from '~/app/shared/services/timer.service';
 
+const TABLE_LIST_LIMIT = 10;
 @Component({
   selector: 'cd-table',
   templateUrl: './table.component.html',
@@ -104,7 +105,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   footer? = true;
   // Page size to show. Set to 0 to show unlimited number of rows.
   @Input()
-  limit? = 10;
+  limit? = TABLE_LIST_LIMIT;
   @Input()
   maxLimit? = 9999;
   // Has the row details?
@@ -343,7 +344,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
       this._loadUserConfig();
       this._initUserConfigAutoSave();
     }
-    if (!this.userConfig.limit) {
+    if (this.limit !== TABLE_LIST_LIMIT || !this.userConfig.limit) {
       this.userConfig.limit = this.limit;
     }
     if (!(this.userConfig.offset >= 0)) {
index 23e00468369b66b05b4571a72f80c4c8b3a5d64f..8afa6a4ba9e9390146ca7098f13224684a0ad935 100644 (file)
@@ -3183,6 +3183,37 @@ paths:
       - jwt: []
       tags:
       - CrushRule
+  /api/daemon:
+    get:
+      description: "List all daemons in the cluster. Also filter by the daemon types\
+        \ specified\n\n        :param daemon_types: List of daemon types to filter\
+        \ by.\n        :return: Returns list of daemons.\n        :rtype: list\n \
+        \       "
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: daemon_types
+        schema:
+          type: string
+      responses:
+        '200':
+          content:
+            application/vnd.ceph.api.v1.0+json:
+              type: object
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - Daemon
   /api/daemon/{daemon_name}:
     put:
       parameters:
index 2008c8630f51dc3eb44873b34aa3f01637bd8ab4..4ba23866d076a2fc961ea9a481e678f3d4854444 100644 (file)
@@ -39,3 +39,8 @@ class DaemonTest(ControllerTestCase):
                 'component': None
             })
             self.assertStatus(400)
+
+    def test_daemon_list(self):
+        with patch_orch(True):
+            self._get(f'{self.URL_DAEMON}')
+            self.assertStatus(200)