mirror of
https://github.com/ceph/ceph
synced 2026-08-02 07:03:18 +00:00
mgr/dashboard: Updates to empty state component
- 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>
This commit is contained in:
parent
c259c6f3fc
commit
2eb19f35e3
@ -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.
|
||||
|
||||
@ -143,20 +143,22 @@
|
||||
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>
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 })
|
||||
);
|
||||
|
||||
|
||||
@ -57,7 +57,23 @@
|
||||
</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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]
|
||||
});
|
||||
|
||||
|
||||
@ -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))
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<svg [cdsIcon]="icon"
|
||||
[size]="size"
|
||||
[ngClass]="!useDefault ? [type + '-icon', class] : []">
|
||||
[ngClass]="!useDefault ? [type + '-icon', customClass] : []">
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 112 B After Width: | Height: | Size: 118 B |
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
@ -18,9 +18,18 @@
|
||||
</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"
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -40,21 +40,4 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user