From: Afreen Misbah Date: Tue, 5 May 2026 21:05:11 +0000 (+0530) Subject: mgr/dashboard: Updates to empty state component X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F68052%2Fhead;p=ceph.git 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 --- diff --git a/PendingReleaseNotes b/PendingReleaseNotes index a5aa832074d4..9e7f042e20c7 100644 --- a/PendingReleaseNotes +++ b/PendingReleaseNotes @@ -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. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html index dddcdafc0152..5c7687bb3a29 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/create-cluster/create-cluster.component.html @@ -143,20 +143,22 @@ aria-label="Add Storage" i18n> Add storage - - + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts index 8feca50aa173..644353a63a32 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts @@ -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(); @Output() viewPGStates = new EventEmitter(); @Output() activeSectionChange = new EventEmitter(); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html index 62dc9f1829ff..5cb5d2b06a2a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html @@ -8,6 +8,8 @@ @let storageCard = (storageCardVm$ | async); @let health = (healthCardVm$ | async); +@let storageEmptyState = (storageEmptyState$ | async); +@let prometheusEmptyState = (prometheusEmptyState$ | async);
+ [storageEmptyState]="storageEmptyState" + [prometheusEmptyState]="prometheusEmptyState"> @@ -52,7 +54,9 @@
- +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts index 42a2f2a5b683..4f8b582a6855 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts @@ -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 }) ); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html index a6b97997307f..f3505da166ba 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.html @@ -57,7 +57,23 @@ } @else { - + + + } @if (!prometheusEmptyState && !storageEmptyState) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts index 6c2582ef1ebd..2bbac7a549cc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/storage-card/overview-storage-card.component.ts @@ -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) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts index d8267c791cf0..767d83881a55 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.spec.ts @@ -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] }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts index ae5e5db06548..2ac9169a50f0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts @@ -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; // 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 = {}; updatedChrtData = new Subject(); - 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( - source$: Observable, - fallback$: Observable = EMPTY - ): Observable { - return this.isPrometheusModuleEnabled().pipe( - switchMap((enabled) => (enabled ? source$ : fallback$)), - catchError(() => fallback$) - ); - } - - isPrometheusModuleEnabled(): Observable { - return this.mgrModuleService.list().pipe( - map((modules) => - modules.some((module) => module.name === PROMETHEUS_MODULE && module.enabled) - ), - catchError(() => of(false)) - ); - } - isPrometheusUsable(): Observable { return this.isSettingConfigured(this.settingsKey.prometheus).pipe( map((isConfigured) => isConfigured), @@ -103,10 +81,8 @@ export class PrometheusService { } isAlertmanagerUsable(): Observable { - 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)) ); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.html index 8a10a70a908e..c2ca9930cc13 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.html @@ -1,8 +1,15 @@ -
- - - {{ emptyStateText }} - +
+ + @if(title) { +

{{title}}

+ } +

+ {{ text }} +

+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.scss index 0d8ceda8d69c..b9eac6266085 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.scss @@ -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 + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.ts index eb96ed55f5aa..f0fab59ee86d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/empty-state/empty-state.component.ts @@ -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; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.html index afc9f4b077e2..b393d255e1db 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.html @@ -1,4 +1,4 @@ + [ngClass]="!useDefault ? [type + '-icon', customClass] : []"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts index d82dba62a294..b0f15dca4def 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.ts @@ -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; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html index 52b115abe968..166247ecc228 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.html @@ -7,7 +7,7 @@

Performance

- @if(!emptyStateText) { + @if(!prometheusEmptyState && !storageEmptyState) {
} - @if(emptyStateText) { - - } @else { + @if(storageEmptyState) { + + } + @else if(prometheusEmptyState) { + + } + @else {
{ let component: PerformanceCardComponent; let fixture: ComponentFixture; - 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); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts index a7add9f87af8..e89536446c0c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/performance-card/performance-card.component.ts @@ -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(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(); @@ -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); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html index c0dd093ae768..f9e9cae8e53c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.html @@ -1,28 +1,14 @@ - @if(!emptyStateText) {
- }
- @if(emptyStateText) { -
- - - {{ emptyStateText }} - -
- } - @else { - }
- @if(!!footerTemplate && !emptyStateText) { + @if(!!footerTemplate) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss index 589e17f59cdd..49596dbb1e85 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.scss @@ -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; - } - } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts index 0b6e2a082dfc..24509b030c93 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/productive-card/productive-card.component.ts @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index 7479cb9be8cc..ecf4715a5518 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -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;