]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Updates to empty state component 68052/head
authorAfreen Misbah <afreen@ibm.com>
Tue, 5 May 2026 21:05:11 +0000 (02:35 +0530)
committerAfreen Misbah <afreen@ibm.com>
Tue, 12 May 2026 09:09:49 +0000 (14:39 +0530)
- added state for no storage in empty state component
- extended the icon component to take into account the scenario of button with icon
- fix unit tests

Signed-off-by: Afreen Misbah <afreen@ibm.com>
21 files changed:
PendingReleaseNotes
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts

index a5aa832074d4853b360900909337281facef3ea6..9e7f042e20c7c960911e1de203e7c3bd938121d2 100644 (file)
@@ -21,7 +21,7 @@
     it must be explicitly loaded in the configuration file or code (see https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md).
 * RGW: Fixed bucket notification events so the 'x_amz_request_id' in NotificationEvent now matches the 'x_amz_request_id' returned by the corresponding S3 operation.
 
-* DASHBOARD: Introduces a new landing page - "Overview". This revamps UX and adds more information in the landing page - overall cluster health, health checks, resilency panel (showing active/clean Pgs status), total and used raw capacity, alerts, capacity breakdown by object, file and block and performance charts - throughput, latency and IOPS. This renames teh landing page from "Dashboard" to "Overview"
+* DASHBOARD: Introduces a new landing page - "Overview". This revamps UX and adds more information in the landing page - overall cluster health, health checks, resilency panel (showing PG status, active/clean percent), total and used raw capacity, alerts, capacity breakdown by object, file and block and performance charts - throughput, latency and IOPS. This renames the landing page from "Dashboard" to "Overview"
 * DASHBOARD: Removed the older landing page which was deprecated in Quincy.
   Admins can no longer enable the older, deprecated landing page layout by
   adjusting FEATURE_TOGGLE_DASHBOARD.
index dddcdafc0152251956f032a7f5afeea977c49425..5c7687bb3a297e3ef5fdfc0b483e86a2d95359f0 100644 (file)
                 aria-label="Add Storage"
                 i18n>
           Add storage
-          <svg [cdsIcon]="icons.add"
-               [size]="icons.size20"
-               class="cds--btn__icon">
-          </svg>
+          <cd-icon
+            [cdsIcon]="icons.add"
+            [size]="icons.size20"
+            class="cds--btn__icon">
+          </cd-icon>
         </button>
         <button cdsButton="tertiary"
                 (click)="skipClusterCreation()"
                 aria-label="View cluster overview"
                 i18n>
           View cluster overview
-          <svg [cdsIcon]="icons.right"
-               [size]="icons.size20"
-               class="cds--btn__icon">
-          </svg>
+          <cd-icon
+            [cdsIcon]="icons.right"
+            [size]="icons.size20"
+            class="cds--btn__icon">
+          </cd-icon>
         </button>
       </div>
     </div>
index 8feca50aa1736dfbddd3b39f22ae4ec46403acef..644353a63a3203403541f9d2c98c27db7a99be7e 100644 (file)
@@ -86,7 +86,6 @@ export class OverviewHealthCardComponent {
   private readonly authStorageService = inject(AuthStorageService);
 
   @Input({ required: true }) vm!: HealthCardVM;
-  @Input() emptyStateText: string | null = '';
   @Output() viewIncidents = new EventEmitter<void>();
   @Output() viewPGStates = new EventEmitter<void>();
   @Output() activeSectionChange = new EventEmitter<HealthCardTabSection | null>();
index 62dc9f1829ff51c5947a87dc42351ac08dfde47a..5cb5d2b06a2a6e547e73f5efccc2da3d3e5e1def 100644 (file)
@@ -8,6 +8,8 @@
 
 @let storageCard = (storageCardVm$ | async);
 @let health = (healthCardVm$ | async);
+@let storageEmptyState = (storageEmptyState$ | async);
+@let prometheusEmptyState = (prometheusEmptyState$ | async);
 <main cdsGrid
       [fullWidth]="true"
       [narrow]="true"
@@ -43,8 +45,8 @@
         [breakdownData]="storageCard?.breakdownData ?? []"
         [isBreakdownLoaded]="storageCard?.isBreakdownLoaded ?? false"
         [threshold]="storageCard?.threshold"
-        [storageEmptyState]="storageEmptyState$ | async"
-        [prometheusEmptyState]="prometheusEmptyState$ | async">
+        [storageEmptyState]="storageEmptyState"
+        [prometheusEmptyState]="prometheusEmptyState">
       </cd-overview-storage-card>
     </div>
   </div>
@@ -52,7 +54,9 @@
     <div cdsCol
          class="overview-pr-0"
          [columnNumbers]="{ lg: 16 }">
-      <cd-performance-card [emptyStateText]="prometheusEmptyState$ | async"></cd-performance-card>
+      <cd-performance-card
+        [prometheusEmptyState]="prometheusEmptyState"
+        [storageEmptyState]="storageEmptyState"></cd-performance-card>
     </div>
   </div>
 </main>
index 42a2f2a5b68344e7971e872b4610bbfb736e9fd5..4f8b582a6855a369c6ae8c489ad7130dbbd427e0 100644 (file)
@@ -104,21 +104,12 @@ export class OverviewComponent {
   );
 
   readonly storageEmptyState$ = this.hasNoOSDs$.pipe(startWith(false)).pipe(
-    map((hasNoOSDs) => {
-      if (hasNoOSDs) {
-        return $localize`You can view capacity usage and related metrics here once you add storage.`;
-      }
-      return '';
-    }),
+    map((hasNoOSDs) => hasNoOSDs),
     shareReplay({ bufferSize: 1, refCount: true })
   );
 
   readonly prometheusEmptyState$ = this.isPromethuesConfigured$.pipe(
-    map((isPromethuesConfigured) =>
-      isPromethuesConfigured
-        ? ''
-        : $localize`You must have Prometheus configured to access this capability.`
-    ),
+    map((isPromethuesConfigured) => !isPromethuesConfigured),
     shareReplay({ bufferSize: 1, refCount: true })
   );
 
index a6b97997307f1cb38ade0c59c0f1d14fa37017b3..f3505da166badcea3186c778fbab504c42bfc8da 100644 (file)
   </div>
   }
   @else {
-    <cd-empty-state [emptyStateText]="storageEmptyState"></cd-empty-state>
+    <cd-empty-state
+      text="You can view capacity usage and related metrics here once you add storage."
+      title="Storage is not configured yet"
+      i18n-title
+      i18n-text>
+      <button
+        cdsButton="primary"
+        i18n
+        size="sm"
+        [routerLink]="['/add-storage']"
+        >
+        Add storage
+        <cd-icon
+          type="add"
+          class="cds--btn__icon"></cd-icon>
+      </button>
+    </cd-empty-state>
   }
   <!-- CAPACITY BREAKDOWN CHART -->
   @if (!prometheusEmptyState && !storageEmptyState) {
index 6c2582ef1ebdf4fe422ae846ee9851620717d99c..2bbac7a549cc98c8a2526e7ebf07cd32a7fb23f9 100644 (file)
@@ -11,7 +11,8 @@ import {
   TooltipModule,
   SkeletonModule,
   LayoutModule,
-  TagModule
+  TagModule,
+  ButtonModule
 } from 'carbon-components-angular';
 import { ProductiveCardComponent } from '~/app/shared/components/productive-card/productive-card.component';
 import { MeterChartComponent, MeterChartOptions } from '@carbon/charts-angular';
@@ -20,6 +21,7 @@ import { AreaChartComponent } from '~/app/shared/components/area-chart/area-char
 import { ComponentsModule } from '~/app/shared/components/components.module';
 import { BreakdownChartData, CapacityThreshold, TrendPoint } from '~/app/shared/models/overview';
 import { EmptyStateComponent } from '~/app/shared/components/empty-state/empty-state.component';
+import { RouterModule } from '@angular/router';
 
 const CHART_HEIGHT = '45px';
 
@@ -35,7 +37,9 @@ const CHART_HEIGHT = '45px';
     AreaChartComponent,
     ComponentsModule,
     TagModule,
-    EmptyStateComponent
+    EmptyStateComponent,
+    ButtonModule,
+    RouterModule
   ],
   standalone: true,
   templateUrl: './overview-storage-card.component.html',
@@ -47,8 +51,8 @@ export class OverviewStorageCardComponent {
   private readonly formatterService = inject(FormatterService);
   private readonly cdr = inject(ChangeDetectorRef);
 
-  @Input() storageEmptyState: string | null = '';
-  @Input() prometheusEmptyState: string | null = '';
+  @Input() storageEmptyState: boolean = false;
+  @Input() prometheusEmptyState: boolean = false;
 
   @Input()
   set totalCapacity(value: number) {
index d8267c791cf04a1ed42cb6d00a46d2eba7881279..767d83881a558f890ee4cb5589909c04b7e2d270 100644 (file)
@@ -6,23 +6,13 @@ import { AlertmanagerNotification } from '../models/prometheus-alerts';
 import { PrometheusService } from './prometheus.service';
 import { SettingsService } from './settings.service';
 import moment from 'moment';
-import { of } from 'rxjs';
-import { MgrModuleService } from './mgr-module.service';
 
 describe('PrometheusService', () => {
   let service: PrometheusService;
   let httpTesting: HttpTestingController;
 
-  const mockMgrModuleService = {
-    list: jest.fn(() => of([])) // no modules enabled
-  };
-
   configureTestBed({
-    providers: [
-      PrometheusService,
-      SettingsService,
-      { provide: MgrModuleService, useValue: mockMgrModuleService }
-    ],
+    providers: [PrometheusService, SettingsService],
     imports: [HttpClientTestingModule]
   });
 
index ae5e5db06548f7586d47fb8c36845b78ca40fb15..2ac9169a50f0e0c96a09cbff3c958c59ab26e548 100644 (file)
@@ -1,7 +1,7 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
-import { EMPTY, Observable, Subject, Subscription, forkJoin, of, timer } from 'rxjs';
+import { Observable, Subject, Subscription, forkJoin, of, timer } from 'rxjs';
 import { catchError, map, switchMap } from 'rxjs/operators';
 
 import { AlertmanagerSilence } from '../models/alertmanager-silence';
@@ -12,7 +12,6 @@ import {
   PrometheusRuleGroup
 } from '../models/prometheus-alerts';
 import moment from 'moment';
-import { MgrModuleService } from './mgr-module.service';
 
 export type PromethuesGaugeMetricResult = {
   metric: Record<string, string>; // metric metadata
@@ -24,8 +23,6 @@ export type PromqlGuageMetric = {
   result: PromethuesGaugeMetricResult[];
 };
 
-const PROMETHEUS_MODULE = 'prometheus';
-
 @Injectable({
   providedIn: 'root'
 })
@@ -45,7 +42,7 @@ export class PrometheusService {
   private settings: Record<string, string | undefined> = {};
   updatedChrtData = new Subject<any>();
 
-  constructor(private http: HttpClient, private mgrModuleService: MgrModuleService) {}
+  constructor(private http: HttpClient) {}
 
   unsubscribe() {
     if (this.timerGetPrometheusDataSub) {
@@ -71,25 +68,6 @@ export class PrometheusService {
     this.disableSetting(this.settingsKey.prometheus);
   }
 
-  withPrometheusEnabled<T>(
-    source$: Observable<T>,
-    fallback$: Observable<T> = EMPTY
-  ): Observable<T> {
-    return this.isPrometheusModuleEnabled().pipe(
-      switchMap((enabled) => (enabled ? source$ : fallback$)),
-      catchError(() => fallback$)
-    );
-  }
-
-  isPrometheusModuleEnabled(): Observable<boolean> {
-    return this.mgrModuleService.list().pipe(
-      map((modules) =>
-        modules.some((module) => module.name === PROMETHEUS_MODULE && module.enabled)
-      ),
-      catchError(() => of(false))
-    );
-  }
-
   isPrometheusUsable(): Observable<boolean> {
     return this.isSettingConfigured(this.settingsKey.prometheus).pipe(
       map((isConfigured) => isConfigured),
@@ -103,10 +81,8 @@ export class PrometheusService {
   }
 
   isAlertmanagerUsable(): Observable<boolean> {
-    return this.isPrometheusModuleEnabled().pipe(
-      switchMap((enabled) =>
-        enabled ? this.isSettingConfigured(this.settingsKey.alertmanager) : of(false)
-      ),
+    return this.isSettingConfigured(this.settingsKey.alertmanager).pipe(
+      map((isConfigured) => isConfigured),
       catchError(() => of(false))
     );
   }
index 8a10a70a908e3b19f944be51650b179640baea1e..c2ca9930cc13184d0401f3b6ce42cc28a0dfbd1f 100644 (file)
@@ -1,8 +1,15 @@
-<div class="empty-state">
-  <img src="assets/locked.png"
-       [alt]="emptyStateText"/>
-  <span class="cds--type-label-01"
-        i18n>
-    {{ emptyStateText }}
-  </span>
+<div
+  class="empty-state"
+  [class.empty-state-title]="title">
+  <img
+    src="{{ imgSrc }}"
+    [alt]="text"/>
+  @if(title) {
+  <p class="cds--type-body-compact-02 cds-mb-0">{{title}}</p>
+  }
+  <p class="cds--type-label-01 empty-state-text"
+     i18n>
+    {{ text }}
+  </p>
+  <ng-content></ng-content>
 </div>
index 0d8ceda8d69c0bf541cfde598140a1186d4939ec..b9eac62660854901fea29d0d7a4d426de9ff369c 100644 (file)
@@ -5,17 +5,21 @@
   flex-direction: column;
   justify-content: flex-end;
   gap: var(--cds-spacing-05);
-  margin-top: 283px;
   padding: var(--cds-spacing-05) var(--cds-spacing-05) var(--cds-spacing-07) var(--cds-spacing-05);
   width: 264px;
   margin-left: var(--cds-spacing-05);
+  margin-top: 267px; // 283px -16px;
 
   img {
     width: 80px !important;
     height: 80px !important;
   }
 
-  span {
+  &-text {
     color: var(--cds-text-secondary);
   }
+
+  &-title {
+    margin-top: 140px; // 156px - 16px
+  }
 }
index eb96ed55f5aaae0bd1379557669f21bea202d552..f0fab59ee86d9854d3c5ad270440fe1b40585037 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, Input } from '@angular/core';
+import { EMPTY_STATE_IMAGE } from '../../enum/icons.enum';
 
 @Component({
   selector: 'cd-empty-state',
@@ -7,6 +8,7 @@ import { Component, Input } from '@angular/core';
   styleUrl: './empty-state.component.scss'
 })
 export class EmptyStateComponent {
-  /* Optional: Custom empty state text, when empty state is displyed*/
-  @Input() emptyStateText: string | null = '';
+  @Input() text: string | null = '';
+  @Input() title: string | null = '';
+  @Input() imgSrc: string = EMPTY_STATE_IMAGE.default;
 }
index afc9f4b077e2ce84f786d38d88b5ba2208fe6663..b393d255e1dbfbcaae80e1c2ea55d60cb3af08b4 100644 (file)
@@ -1,4 +1,4 @@
 <svg  [cdsIcon]="icon"
       [size]="size"
-      [ngClass]="!useDefault ? [type + '-icon', class] : []">
+      [ngClass]="!useDefault ? [type + '-icon', customClass] : []">
 </svg>
index d82dba62a29426d1b8ec45ceff790ff8ec1d4aa7..b0f15dca4defdc03a03e858a739d2f87a87f9920 100644 (file)
@@ -18,7 +18,7 @@ import { ICON_TYPE, Icons, IconSize } from '../../enum/icons.enum';
 export class IconComponent implements OnInit, OnChanges {
   @Input() type!: keyof typeof ICON_TYPE;
   @Input() size: IconSize = IconSize.size16;
-  @Input() class: string = '';
+  @Input() customClass: string = '';
   // No CSS class will be applied.
   @Input() useDefault: boolean = false;
 
index 52b115abe968ceb7be6c2121079df31d63225f2d..166247ecc22883b5f7562998613b1cefaf32fb8a 100644 (file)
@@ -7,7 +7,7 @@
   <ng-template #header>
     <h2 class="cds--type-heading-compact-02"
         i18n>Performance</h2>
-     @if(!emptyStateText) {
+     @if(!prometheusEmptyState && !storageEmptyState) {
     <div cdsStack="horizontal"
          gap="2">
       <cd-time-picker
     </div>
     }
   </ng-template>
-  @if(emptyStateText) {
-    <cd-empty-state [emptyStateText]="emptyStateText"></cd-empty-state>
-  } @else {
+  @if(storageEmptyState) {
+    <cd-empty-state
+      text="You must have storage configured to access this capability."
+      i18n-text></cd-empty-state>
+  }
+  @else if(prometheusEmptyState) {
+    <cd-empty-state
+      text="You must have Prometheus configured to access this capability."
+      i18n-text
+      [imgSrc]="emptyState.locked"></cd-empty-state>
+  }
+  @else {
   <div cdsGrid
        [narrow]="true"
        [condensed]="false"
index 9490910a432e8f70c72d10b801bce5c5b5feaadb..81a89bc51e815e2d3e7bdcab8b3fd81aa02ef3a3 100644 (file)
@@ -1,7 +1,7 @@
 import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
 import { PerformanceCardComponent } from './performance-card.component';
 import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { EMPTY, of } from 'rxjs';
+import { of } from 'rxjs';
 import { PrometheusService } from '../../api/prometheus.service';
 import { PerformanceCardService } from '../../api/performance-card.service';
 import { PerformanceData } from '../../models/performance-data';
@@ -13,7 +13,6 @@ import { Permissions } from '../../models/permissions';
 describe('PerformanceCardComponent', () => {
   let component: PerformanceCardComponent;
   let fixture: ComponentFixture<PerformanceCardComponent>;
-  let prometheusService: PrometheusService;
 
   const mockChartData: PerformanceData = {
     iops: [{ timestamp: new Date(), values: { 'Read IOPS': 100, 'Write IOPS': 50 } }],
@@ -25,8 +24,7 @@ describe('PerformanceCardComponent', () => {
 
   beforeEach(async () => {
     const prometheusServiceMock = {
-      lastHourDateObject: { start: 1000, end: 2000, step: 14 },
-      withPrometheusEnabled: jest.fn((source$) => source$)
+      lastHourDateObject: { start: 1000, end: 2000, step: 14 }
     };
 
     const performanceCardServiceMock = {
@@ -71,7 +69,6 @@ describe('PerformanceCardComponent', () => {
 
     fixture = TestBed.createComponent(PerformanceCardComponent);
     component = fixture.componentInstance;
-    prometheusService = TestBed.inject(PrometheusService);
   });
 
   it('should create', () => {
@@ -93,17 +90,18 @@ describe('PerformanceCardComponent', () => {
     expect(component.chartDataSignal()).toEqual(mockChartData);
   }));
 
-  it('should set emptyStateText when prometheus is enabled', fakeAsync(() => {
+  it('should not load chart data when no storage', fakeAsync(() => {
+    component.storageEmptyState = true;
     const time = { start: 1000, end: 2000, step: 14 };
     component.loadCharts(time);
 
     tick();
-    expect(component.emptyStateText).toBe('');
+
+    expect(component.chartDataSignal()).toBeNull();
   }));
 
   it('should not load chart data when prometheus is disabled', fakeAsync(() => {
-    (prometheusService.withPrometheusEnabled as jest.Mock).mockReturnValue(EMPTY);
-
+    component.prometheusEmptyState = true;
     const time = { start: 1000, end: 2000, step: 14 };
     component.loadCharts(time);
 
index a7add9f87af8606e7189c830fba0fb99f27a6b37..e89536446c0c74f3a37047c70b3bf8ff30d102d7 100644 (file)
@@ -8,7 +8,7 @@ import {
   computed,
   Input
 } from '@angular/core';
-import { Icons, IconSize } from '~/app/shared/enum/icons.enum';
+import { EMPTY_STATE_IMAGE, Icons, IconSize } from '~/app/shared/enum/icons.enum';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
 import {
   METRIC_UNIT_MAP,
@@ -18,7 +18,7 @@ import {
 } from '~/app/shared/models/performance-data';
 import { PerformanceCardService } from '../../api/performance-card.service';
 import { DropdownModule, GridModule, LayoutModule, ListItem } from 'carbon-components-angular';
-import { of, Subject, Subscription } from 'rxjs';
+import { Subject, Subscription } from 'rxjs';
 import { takeUntil } from 'rxjs/operators';
 import { ProductiveCardComponent } from '../productive-card/productive-card.component';
 import { CommonModule } from '@angular/common';
@@ -44,7 +44,8 @@ import { EmptyStateComponent } from '../empty-state/empty-state.component';
   encapsulation: ViewEncapsulation.None
 })
 export class PerformanceCardComponent implements OnInit, OnDestroy {
-  @Input() emptyStateText: string | null = '';
+  @Input() prometheusEmptyState: boolean = false;
+  @Input() storageEmptyState: boolean = false;
 
   chartDataSignal = signal<PerformanceData | null>(null);
   chartDataLengthSignal = computed(() => {
@@ -55,6 +56,7 @@ export class PerformanceCardComponent implements OnInit, OnDestroy {
   metricUnitMap = METRIC_UNIT_MAP;
   icons = Icons;
   iconSize = IconSize;
+  emptyState = EMPTY_STATE_IMAGE;
 
   private destroy$ = new Subject<void>();
 
@@ -94,8 +96,13 @@ export class PerformanceCardComponent implements OnInit, OnDestroy {
 
     this.chartSub?.unsubscribe();
 
-    this.chartSub = this.prometheusService
-      .withPrometheusEnabled(this.performanceCardService.getChartData(time))
+    if (this.storageEmptyState || this.prometheusEmptyState) {
+      this.chartDataSignal.set(null);
+      return;
+    }
+
+    this.chartSub = this.performanceCardService
+      .getChartData(time)
       .pipe(takeUntil(this.destroy$))
       .subscribe((data) => {
         this.chartDataSignal.set(data);
index c0dd093ae768a7c88b5ec761234a56cf2f11350b..f9e9cae8e53cc651144eff4069fe3cd457411c23 100644 (file)
@@ -1,28 +1,14 @@
 <cds-tile class="productive-card"
           [ngClass]="{'productive-card--shadow': applyShadow}"
           [cdsLayer]="0">
-  @if(!emptyStateText) {
   <div class="productive-card-header">
     <ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
   </div>
-  }
   <section class="productive-card-section cds--type-body-compact-01"
            [ngClass]="{'productive-card-section--footer': footerTemplate}">
-    @if(emptyStateText) {
-    <div class="productive-card-empty-state">
-      <img src="assets/locked.png"
-           [alt]="emptyStateText"/>
-      <span class="cds--type-label-01"
-            i18n>
-        {{ emptyStateText }}
-      </span>
-    </div>
-    }
-    @else {
     <ng-content></ng-content>
-    }
   </section>
-  @if(!!footerTemplate && !emptyStateText) {
+  @if(!!footerTemplate) {
   <footer class="productive-card-footer">
     <ng-container *ngTemplateOutlet="footerTemplate"></ng-container>
   </footer>
index 589e17f59cddcdbe400239b7c7ad9c8da0052390..49596dbb1e8587c9cd68b2afc1fe4cc2569d2c0c 100644 (file)
       radial-gradient(120% 60% at 50% 100%, rgba(colors.$magenta-60, 0.11) 0%, transparent 70%);
     box-shadow: var(--cds-ai-drop-shadow), inset 0 0 0 1px var(--cds-ai-inner-shadow);
   }
-
-  &-empty-state {
-    display: flex;
-    flex-direction: column;
-    justify-content: flex-end;
-    gap: var(--cds-spacing-05);
-    height: 350px;
-
-    p {
-      font-size: 12px !important;
-    }
-
-    img {
-      width: 100px !important;
-      height: 100px !important;
-    }
-  }
 }
index 0b6e2a082dfcbb72d8ded48c18b148c7d1edf0ff..24509b030c9396160d6d530a3005749ef0b20bae 100644 (file)
@@ -24,9 +24,6 @@ export class ProductiveCardComponent {
   /* Optional: Applies a tinted-colored background to card */
   @Input() applyShadow: boolean = false;
 
-  /* Optional: Custom empty state text, when empty state is displyed*/
-  @Input() emptyStateText: string | null = '';
-
   /* Optional: Header action template, appears alongwith title in top-right corner */
   @ContentChild('header', {
     read: TemplateRef
index 7479cb9be8cc1eacedb5009a52ca51ea3496b956..ecf4715a55185478da2e22efe8704d18bf0681d3 100644 (file)
@@ -175,5 +175,6 @@ export const ICON_TYPE = {
 
 export const EMPTY_STATE_IMAGE = {
   default: 'assets/empty-state.png',
-  search: 'assets/empty-state-search.png'
+  search: 'assets/empty-state-search.png',
+  locked: 'assets/locked.png'
 } as const;