]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard : Add Certificate tab under service details 66962/head
authorAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 19 Jan 2026 08:47:54 +0000 (14:17 +0530)
committerAbhishek Desai <abhishek.desai1@ibm.com>
Mon, 2 Feb 2026 15:27:31 +0000 (20:57 +0530)
fixes : https://tracker.ceph.com/issues/74429
Signed-off-by: Abhishek Desai <abhishek.desai1@ibm.com>
15 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/cluster/services.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-details/service-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/cd-date.pipe.ts

index 72eeb37a2efbd60d96a0c70a3113edd695388a14..fa21762af4616fd27d3f1cb5340bc45d691fd229 100644 (file)
@@ -32,7 +32,7 @@ export class ServicesPageHelper extends PageHelper {
   clickServiceTab(serviceName: string, tabName: string) {
     this.getExpandCollapseElement(serviceName).click();
     cy.get('cd-service-details').within(() => {
-      this.getTab(tabName).click();
+      this.getCdsTab(tabName).click();
     });
   }
 
index 77d972de465ea8922aef46b588da6fa931055643..99f00bc0640264319a1f7f5577fff429bfdbd172 100644 (file)
@@ -95,6 +95,10 @@ export abstract class PageHelper {
     return cy.contains('.nav.nav-tabs a', tabName);
   }
 
+  getCdsTab(tabName: string) {
+    return cy.contains('cds-tab-headers button[role="tab"]', tabName);
+  }
+
   getTabText(index: number) {
     return this.getTabs().its(index).text();
   }
index b05b726e5f600d5092e612478d078c4dd7bbade8..2285de4d3d1b29f7c7221cc84be105170c1e02a8 100644 (file)
@@ -21,7 +21,8 @@ import {
   SelectModule,
   LayoutModule,
   NumberModule,
-  FileUploaderModule
+  FileUploaderModule,
+  TabsModule
 } from 'carbon-components-angular';
 import Analytics from '@carbon/icons/es/analytics/16';
 import CloseFilled from '@carbon/icons/es/close--filled/16';
@@ -77,6 +78,7 @@ import { SilenceListComponent } from './prometheus/silence-list/silence-list.com
 import { SilenceMatcherModalComponent } from './prometheus/silence-matcher-modal/silence-matcher-modal.component';
 import { PlacementPipe } from './services/placement.pipe';
 import { ServiceDaemonListComponent } from './services/service-daemon-list/service-daemon-list.component';
+import { ServiceCertificateDetailsComponent } from './services/service-cert-details/service-certificate-details.component';
 import { ServiceDetailsComponent } from './services/service-details/service-details.component';
 import { ServiceFormComponent } from './services/service-form/service-form.component';
 import { ServicesComponent } from './services/services.component';
@@ -124,6 +126,7 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/
     ToggletipModule,
     IconModule,
     TagModule,
+    TabsModule,
     TextLabelListComponent,
     SelectModule,
     LayoutModule,
@@ -161,6 +164,7 @@ import { TextLabelListComponent } from '~/app/shared/components/text-label-list/
     ActiveAlertListComponent,
     ServiceDetailsComponent,
     ServiceDaemonListComponent,
+    ServiceCertificateDetailsComponent,
     TelemetryComponent,
     PrometheusTabsComponent,
     ServiceFormComponent,
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.html
new file mode 100644 (file)
index 0000000..b04f2ed
--- /dev/null
@@ -0,0 +1,108 @@
+<cd-details-card>
+  <ng-container
+    class="details-body-content">
+    <div cdsStack="horizontal"
+         [gap]="6">
+      <div cdsStack="vertical"
+           [gap]="3">
+        <div cdsStack="vertical"
+             [gap]="1">
+          <span class="cds--type-label-01"
+                i18n>
+            Certificate name
+          </span>
+          <span class="cds--type-body-compact-01"
+                [title]="certificate?.cert_name || '-'">
+            {{ certificate?.cert_name || '-' }}
+          </span>
+        </div>
+        <div cdsStack="vertical"
+             [gap]="1">
+          <span class="cds--type-label-01"
+                i18n>
+            Valid until
+          </span>
+          <span class="cds--type-body-compact-01"
+                [title]="formatDate(certificate?.expiry_date) || '-'">
+            {{ formatDate(certificate?.expiry_date) || '-' }}
+          </span>
+        </div>
+      </div>
+
+      <div cdsStack="vertical"
+           [gap]="3">
+        <div cdsStack="horizontal"
+             [gap]="1">
+          <div cdsStack="vertical"
+               [gap]="1">
+            <span class="cds--type-label-01"
+                  i18n>
+              Status
+            </span>
+            <ng-container *ngTemplateOutlet="statusTemplate; context: { status: certificate?.status }"></ng-container>
+          </div>
+          <!-- icon -->
+          <cds-icon-button kind="ghost"
+                           size="sm"
+                           (click)="onEdit()"
+                           class="cds-mt-5">
+            <cd-icon type="edit">
+            </cd-icon>
+          </cds-icon-button>
+
+        </div>
+        <div cdsStack="vertical"
+             [gap]="1">
+          <span class="cds--type-label-01"
+                i18n>
+            Days remaining
+          </span>
+          <span class="cds--type-body-compact-01"
+                [title]="certificate?.days_to_expiration  ? certificate.days_to_expiration : '-'">
+            {{ certificate?.days_to_expiration ? certificate.days_to_expiration : '-' }}
+          </span>
+        </div>
+      </div>
+
+      <div cdsStack="vertical"
+           [gap]="3">
+        <div cdsStack="vertical"
+             [gap]="1">
+          <span class="cds--type-label-01"
+                i18n>
+            Issuer
+          </span>
+          <span class="cds--type-body-compact-01"
+                [title]="certificate?.issuer || certificate?.signed_by || '-'">
+            {{ certificate?.issuer || certificate?.signed_by || '-' }}
+          </span>
+        </div>
+        <div cdsStack="vertical"
+             [gap]="1">
+          <span class="cds--type-label-01"
+                i18n>
+            Common name
+          </span>
+          <span class="cds--type-body-compact-01"
+                [title]="certificate?.common_name || '-'">
+            {{ certificate?.common_name || '-' }}
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <ng-template #statusTemplate
+                 let-status="status">
+      <div cdsStack="horizontal"
+           [gap]="2"
+           class="status-row">
+        <cd-icon
+          [type]="statusIconMap[status] || statusIconMap['default']">
+        </cd-icon>
+        <span class="cds--type-body-compact-01">{{ formatCertificateStatus(certificate) }}</span>
+
+      </div>
+    </ng-template>
+
+  </ng-container>
+</cd-details-card>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.spec.ts
new file mode 100644 (file)
index 0000000..6aea2e3
--- /dev/null
@@ -0,0 +1,122 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { expect as jestExpect } from '@jest/globals';
+
+import { IconComponent } from '~/app/shared/components/icon/icon.component';
+import {
+  CephCertificateStatus,
+  CephServiceCertificate
+} from '~/app/shared/models/service.interface';
+import { ServiceCertificateDetailsComponent } from './service-certificate-details.component';
+
+describe('ServiceCertificateDetailsComponent', () => {
+  let component: ServiceCertificateDetailsComponent;
+  let fixture: ComponentFixture<ServiceCertificateDetailsComponent>;
+  const baseCert: CephServiceCertificate = {
+    cert_name: 'cert',
+    scope: 'SERVICE',
+    requires_certificate: true,
+    status: CephCertificateStatus.valid,
+    days_to_expiration: 0,
+    signed_by: 'user',
+    has_certificate: true,
+    certificate_source: 'user',
+    expiry_date: '2025-01-01',
+    issuer: 'issuer',
+    common_name: 'cn'
+  };
+  const makeCert = (override: Partial<CephServiceCertificate>): CephServiceCertificate => ({
+    ...baseCert,
+    ...override
+  });
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ServiceCertificateDetailsComponent, IconComponent],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(ServiceCertificateDetailsComponent);
+    component = fixture.componentInstance;
+  });
+
+  it('should create', () => {
+    fixture.detectChanges();
+    jestExpect(component).toBeTruthy();
+  });
+
+  it('should format certificate status with expiry date', () => {
+    const cert = makeCert({ status: CephCertificateStatus.valid, expiry_date: '2025-01-01' });
+
+    jestExpect(component.formatCertificateStatus(cert)).toBe('Valid - 01 Jan 2025');
+  });
+
+  it('should return dash when certificate not required', () => {
+    const cert = makeCert({ requires_certificate: false, has_certificate: false });
+    jestExpect(component.formatCertificateStatus(cert)).toBe('-');
+  });
+
+  it('should emit editService with service identifiers', () => {
+    component.serviceName = 'svc-name';
+    component.serviceType = 'svc-type';
+    fixture.detectChanges();
+
+    const emitSpy = jest.spyOn(component.editService, 'emit');
+    const button = fixture.debugElement.query(By.css('cds-icon-button'));
+
+    button.triggerEventHandler('click', {});
+
+    jestExpect(emitSpy).toHaveBeenCalledWith({ serviceName: 'svc-name', serviceType: 'svc-type' });
+  });
+
+  it('should show success icon and text for valid status', () => {
+    component.certificate = makeCert({
+      status: CephCertificateStatus.valid,
+      expiry_date: '2025-01-01'
+    });
+    fixture.detectChanges();
+
+    const statusIcon = fixture.debugElement.query(By.css('.status-row cd-icon'));
+    const statusText = fixture.debugElement
+      .query(By.css('.status-row span'))
+      .nativeElement.textContent.trim();
+
+    jestExpect(statusIcon.componentInstance.type).toBe('success');
+    jestExpect(statusText).toBe('Valid - 01 Jan 2025');
+  });
+
+  it('should fall back to warning icon for invalid status', () => {
+    component.certificate = makeCert({ status: 'invalid_status', expiry_date: '2025-01-01' });
+    fixture.detectChanges();
+
+    const statusIcon = fixture.debugElement.query(By.css('.status-row cd-icon'));
+    jestExpect(statusIcon.componentInstance.type).toBe('warning');
+  });
+
+  it('should use warning icon for warning status', () => {
+    component.certificate = makeCert({
+      status: CephCertificateStatus.expiringSoon,
+      expiry_date: '2025-01-01'
+    });
+    fixture.detectChanges();
+
+    const statusIcon = fixture.debugElement.query(By.css('.status-row cd-icon'));
+    const statusText = fixture.debugElement
+      .query(By.css('.status-row span'))
+      .nativeElement.textContent.trim();
+
+    jestExpect(statusIcon.componentInstance.type).toBe('warning');
+    jestExpect(statusText).toBe('Expiring soon - 01 Jan 2025');
+  });
+
+  it('should display dash when service has no certificate', () => {
+    component.certificate = makeCert({ requires_certificate: false, has_certificate: false });
+    fixture.detectChanges();
+
+    const statusText = fixture.debugElement
+      .query(By.css('.status-row span'))
+      .nativeElement.textContent.trim();
+    jestExpect(statusText).toBe('-');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-cert-details/service-certificate-details.component.ts
new file mode 100644 (file)
index 0000000..05605a7
--- /dev/null
@@ -0,0 +1,63 @@
+import {
+  CephCertificateStatus,
+  CephServiceCertificate
+} from '~/app/shared/models/service.interface';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { ICON_TYPE } from '~/app/shared/enum/icons.enum';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
+
+@Component({
+  selector: 'cd-service-certificate-details',
+  templateUrl: './service-certificate-details.component.html',
+  styleUrls: ['./service-certificate-details.component.scss'],
+  providers: [CdDatePipe],
+  standalone: false
+})
+export class ServiceCertificateDetailsComponent {
+  @Input() certificate: CephServiceCertificate;
+  @Input() serviceName?: string;
+  @Input() serviceType?: string;
+
+  @Output() editService = new EventEmitter<{ serviceName?: string; serviceType?: string }>();
+
+  readonly statusIconMap: Record<string, keyof typeof ICON_TYPE> = {
+    valid: 'success',
+    expiring: 'warning',
+    expiring_soon: 'warning',
+    expired: 'danger',
+    default: 'warning'
+  };
+
+  constructor(private cdDatePipe: CdDatePipe) {}
+
+  formatCertificateStatus(cert: CephServiceCertificate): string {
+    if (!cert || !cert.requires_certificate || !cert.status) {
+      return '-';
+    }
+
+    const formattedDate = this.formatDate(cert.expiry_date);
+    switch (cert.status) {
+      case CephCertificateStatus.valid:
+        return formattedDate ? `Valid - ${formattedDate}` : 'Valid';
+      case CephCertificateStatus.expiringSoon:
+        return formattedDate ? `Expiring soon - ${formattedDate}` : 'Expiring soon';
+      case CephCertificateStatus.expired:
+        return formattedDate ? `Expired - ${formattedDate}` : 'Expired';
+      case CephCertificateStatus.notConfigured:
+        return 'Not configured';
+      default:
+        return formattedDate ? `${cert.status} - ${formattedDate}` : cert.status;
+    }
+  }
+
+  formatDate(dateValue: string | Date | null | undefined): string | null {
+    if (!dateValue) {
+      return null;
+    }
+    return this.cdDatePipe.transform(dateValue, 'DD MMM y');
+  }
+
+  onEdit(): void {
+    this.editService.emit({ serviceName: this.serviceName, serviceType: this.serviceType });
+  }
+}
index dc252a42cdac33e752a370d0094ec8386f4c413a..6e21591674593a7ef3c3328236113fd23ab3c278 100644 (file)
@@ -5,34 +5,22 @@
 </div>
 
 <ng-template #serviceDetailsTpl>
-  <ng-container>
-    <nav ngbNav
-         #nav="ngbNav"
-         class="nav-tabs"
-         cdStatefulTab="service-details">
-      <ng-container ngbNavItem="details">
-        <a ngbNavLink
-           i18n>Daemons</a>
-        <ng-template ngbNavContent>
-          <ng-container *ngTemplateOutlet="serviceDaemonDetailsTpl"></ng-container>
-        </ng-template>
-      </ng-container>
-      <ng-container ngbNavItem="service_events">
-        <a ngbNavLink
-           i18n>Service Events</a>
-        <ng-template ngbNavContent>
-          <cd-table *ngIf="hasOrchestrator"
-                    #serviceTable
-                    [data]="services"
-                    [columns]="serviceColumns"
-                    columnMode="flex"
-                    (fetchData)="getServices($event)">
-          </cd-table>
-        </ng-template>
-      </ng-container>
-    </nav>
-    <div [ngbNavOutlet]="nav"></div>
-  </ng-container>
+@switch (mode) {
+  @case ('daemons') {
+  <ng-container *ngTemplateOutlet="serviceDaemonDetailsTpl"></ng-container>
+  }
+  @case ('events') {
+  @if (hasOrchestrator) {
+  <cd-table
+    #serviceTable
+    [data]="services"
+    [columns]="serviceColumns"
+    columnMode="flex"
+    (fetchData)="getServices($event)">
+  </cd-table>
+  }
+  }
+}
 </ng-template>
 
 <ng-template #statusTpl
index 672c3a046575c46d9b3442f0515ea85e4e9347a1..d63d4b804a519758e43185c3b7c1e9030cb4c99a 100644 (file)
@@ -7,6 +7,8 @@ import {
   OnChanges,
   OnDestroy,
   OnInit,
+  Output,
+  EventEmitter,
   QueryList,
   TemplateRef,
   ViewChild,
@@ -68,6 +70,9 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI
   @Input()
   flag?: string;
 
+  @Input()
+  mode: 'daemons' | 'events' = 'daemons';
+
   total = 100;
 
   warningThreshold = 0.8;
@@ -84,6 +89,9 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI
   selection = new CdTableSelection();
   permissions: Permissions;
 
+  @Output()
+  editService = new EventEmitter<{ serviceName?: string; serviceType?: string }>();
+
   hasOrchestrator = false;
   showDocPanel = false;
 
index 704f0f98e0e92649a927049b83fbd219eaec0a8c..104e7e21fc0da9d083d4f86fd51718fca4db1694 100644 (file)
@@ -1,4 +1,31 @@
-<ng-container *ngIf="selection">
-  <cd-service-daemon-list [serviceName]="selection['service_name']">
-  </cd-service-daemon-list>
-</ng-container>
+@if (service) {
+  <cds-tabs [cacheActive]="false">
+    <cds-tab heading="Daemons"
+             i18n-heading>
+      <cd-service-daemon-list
+        [serviceName]="service?.service_name"
+        mode="daemons">
+      </cd-service-daemon-list>
+    </cds-tab>
+
+    @if (hasCertificate) {
+      <cds-tab heading="Certificate"
+               i18n-heading>
+        <cd-service-certificate-details
+          [certificate]="certificate"
+          [serviceName]="service?.service_name"
+          [serviceType]="service?.service_type"
+          (editService)="onEditService($event)">
+        </cd-service-certificate-details>
+      </cds-tab>
+    }
+
+    <cds-tab heading="Service Events"
+             i18n-heading>
+      <cd-service-daemon-list
+        [serviceName]="service?.service_name"
+        mode="events">
+      </cd-service-daemon-list>
+    </cds-tab>
+  </cds-tabs>
+}
index eca7c7dc60bd586f7d0f82cd55c05b13f6e63839..49f8c2c51a4b679a3205e888c432eaddb0b2ef62 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, Input } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { Permissions } from '~/app/shared/models/permissions';
@@ -15,4 +15,24 @@ export class ServiceDetailsComponent {
 
   @Input()
   selection: CdTableSelection;
+
+  @Output()
+  editService = new EventEmitter<{ serviceName?: string; serviceType?: string }>();
+
+  get service() {
+    return this.selection as any;
+  }
+
+  get certificate() {
+    return this.service?.certificate;
+  }
+
+  get hasCertificate() {
+    const cert = this.certificate;
+    return cert?.has_certificate;
+  }
+
+  onEditService(payload: { serviceName?: string; serviceType?: string }) {
+    this.editService.emit(payload);
+  }
 }
index 07380d6a169c5d40302747ceef0e9795a853fc19..b864fb163ebe8da9562642fa048163983476c655 100644 (file)
@@ -20,7 +20,8 @@
     </cd-table-actions>
     <cd-service-details *cdTableDetail
                         [permissions]="permissions"
-                        [selection]="expandedRow">
+                        [selection]="expandedRow"
+                        (editService)="openModal(true, $event)">
     </cd-service-details>
   </cd-table>
 </ng-container>
index 782650c8eafc4952b11d1807c6e789429fedd6a2..0aa51e340abc14d4d5bbe49d1f5bad222e1e144e 100644 (file)
@@ -1,4 +1,3 @@
-import { DatePipe } from '@angular/common';
 import { Component, Input, OnChanges, OnInit, TemplateRef, ViewChild } from '@angular/core';
 import { Router } from '@angular/router';
 
@@ -24,6 +23,7 @@ import {
   CephServiceCertificate,
   CephServiceSpec
 } from '~/app/shared/models/service.interface';
+import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
 import { RelativeDatePipe } from '~/app/shared/pipes/relative-date.pipe';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
@@ -40,7 +40,10 @@ const BASE_URL = 'services';
   selector: 'cd-services',
   templateUrl: './services.component.html',
   styleUrls: ['./services.component.scss'],
-  providers: [DatePipe, { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }],
+  providers: [
+    CdDatePipe,
+    { provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }
+  ],
   standalone: false
 })
 export class ServicesComponent extends ListWithDetails implements OnChanges, OnInit {
@@ -94,7 +97,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     private router: Router,
     private settingsService: SettingsService,
     private cdsModalService: ModalCdsService,
-    private datePipe: DatePipe
+    private cdDatePipe: CdDatePipe
   ) {
     super();
     this.permissions = this.authStorageService.getPermissions();
@@ -124,18 +127,20 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     ];
   }
 
-  openModal(edit = false) {
+  openModal(edit = false, payload?: { serviceName?: string; serviceType?: string } | string) {
+    const serviceNameFromPayload = typeof payload === 'string' ? payload : payload?.serviceName;
+    const serviceTypeFromPayload = typeof payload === 'string' ? undefined : payload?.serviceType;
+
+    const targetServiceName = serviceNameFromPayload ?? this.selection.first()?.service_name;
+    const targetServiceType = serviceTypeFromPayload ?? this.selection.first()?.service_type;
+
     if (this.routedModal) {
       edit
         ? this.router.navigate([
             BASE_URL,
             {
               outlets: {
-                modal: [
-                  URLVerbs.EDIT,
-                  this.selection.first().service_type,
-                  this.selection.first().service_name
-                ]
+                modal: [URLVerbs.EDIT, targetServiceType, targetServiceName]
               }
             }
           ])
@@ -144,8 +149,8 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
       let initialState = {};
       edit
         ? (initialState = {
-            serviceName: this.selection.first()?.service_name,
-            serviceType: this.selection?.first()?.service_type,
+            serviceName: targetServiceName,
+            serviceType: targetServiceType,
             hiddenServices: this.hiddenServices,
             editing: edit
           })
@@ -321,7 +326,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI
     }
 
     const formattedDate = cert.expiry_date
-      ? this.datePipe.transform(cert.expiry_date, 'dd MMM y')
+      ? this.cdDatePipe.transform(cert.expiry_date, 'DD MMM y')
       : null;
 
     switch (cert.status) {
index 6ee238e76fea955d91d47f0f1c54c4c2aea34a36..303590a814a742c675f3ec42002e00dd114a11e9 100644 (file)
@@ -26,8 +26,9 @@ export interface CephServiceCertificate {
   has_certificate: boolean;
   certificate_source: string;
   expiry_date: string;
+  issuer: string;
+  common_name: string;
 }
-
 // This will become handy when creating arbitrary services
 export interface CephServiceSpec {
   service_name: string;
index 0ff248ebcc2348e51143acbf7a0c6d3ef4c550c7..2fdc1c3063b657f2b9a51c630707455b19711b59 100644 (file)
@@ -7,24 +7,21 @@ import moment from 'moment';
   standalone: false
 })
 export class CdDatePipe implements PipeTransform {
+  private static readonly DEFAULT_FORMAT = 'D/M/YY hh:mm A';
+
   constructor() {}
 
-  transform(value: any): any {
+  transform(value: any, format: string = CdDatePipe.DEFAULT_FORMAT): any {
     if (value === null || value === '') {
       return '';
     }
     let date: string;
     const offset = moment().utcOffset();
     if (_.isNumber(value)) {
-      date = moment
-        .parseZone(moment.unix(value))
-        .utc()
-        .utcOffset(offset)
-        .local()
-        .format('D/M/YY hh:mm A');
+      date = moment.parseZone(moment.unix(value)).utc().utcOffset(offset).local().format(format);
     } else {
       value = value?.replace?.('Z', '');
-      date = moment.parseZone(value).utc().utcOffset(offset).local().format('D/M/YY hh:mm A');
+      date = moment.parseZone(value).utc().utcOffset(offset).local().format(format);
     }
     return date;
   }