]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Cephfs Mirroring - Filesystem Selection 66821/head
authorDnyaneshwari Talwekar <dtalwekar@li-4c4c4544-0038-3510-8056-b5c04f473234.ibm.com>
Wed, 7 Jan 2026 10:46:55 +0000 (16:16 +0530)
committerDnyaneshwari Talwekar <dtalwekar@li-4c4c4544-0038-3510-8056-b5c04f473234.ibm.com>
Wed, 11 Feb 2026 11:22:11 +0000 (16:52 +0530)
Fixes: https://tracker.ceph.com/issues/74280
Signed-off-by: Dnyaneshwari Talwekar <dtalweka@redhat.com>
12 files changed:
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard-step.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs.model.ts
src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_spacings.scss

diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.html
new file mode 100644 (file)
index 0000000..0a2a647
--- /dev/null
@@ -0,0 +1,78 @@
+<div cds-mb="lg">
+  <div class="cds--type-heading-03"
+       i18n>Select filesystem</div>
+</div>
+<div class="cds-mt-5">
+  <cd-alert-panel type="info">
+    <div [cdsStack]="'vertical'"
+         [gap]="2">
+      <div class="cds--type-heading-compact-01"
+           i18n>Selection requirements</div>
+      <ul class="cds--type-body-compact-01 requirements-list">
+        <li i18n>Only one filesystem can be selected as the mirroring target</li>
+        <li i18n>The selected filesystem must have an active MDS (Metadata Server)</li>
+        <li i18n>Ensure sufficient storage capacity for incoming mirrored snapshots</li>
+      </ul>
+    </div>
+  </cd-alert-panel>
+</div>
+
+<div class="cds-mt-5">
+  <cd-table [data]="(filesystems$ | async) ?? []"
+            [columns]="columns"
+            columnMode="flex"
+            selectionType="singleRadio"
+            headerTitle="Select target filesystem"
+            headerDescription="Choose the filesystem that will receive mirrored data from the local cluster."
+            (updateSelection)="updateSelection($event)">
+    <ng-template #mdsStatus
+                 let-value="data.value"
+                 let-row="data.row">
+      <div [cdsStack]="'horizontal'"
+           gap="2">
+        @if (value === 'Active') {
+        <cd-icon type="success"></cd-icon>
+        }
+        @if (value === 'Warning') {
+        <cd-icon type="warning"></cd-icon>
+        }
+        @if (value === 'Inactive') {
+        <cd-icon type="error"></cd-icon>
+        }
+        <span class="cds--type-body-compact-01 cds-pt-2px">{{ mdsStatusLabels[value] }}</span>
+      </div>
+    </ng-template>
+
+    <ng-template #mirroringStatus
+                 let-value="data.value"
+                 let-row="data.row">
+      <div [cdsStack]="'horizontal'"
+           gap="2">
+        @if (value === 'Enabled') {
+        <cd-icon type="success"></cd-icon>
+        }
+        @if (value === 'Disabled') {
+        <cd-icon type="warning"></cd-icon>
+        }
+        <span class="cds--type-body-compact-01 cds-pt-2px">{{ mirroringStatusLabels[value] }}</span>
+      </div>
+    </ng-template>
+
+    <ng-template let-row="row"
+                 let-col="col">
+      <ng-container [ngSwitch]="col.prop">
+        <ng-container *ngSwitchCase="'pools'">
+          @for (pool of row.pools; track $index; let last = $last) {
+          <cd-badge>{{ pool }}</cd-badge>
+          @if (!last) {
+          <span>, </span>
+          }
+          }
+        </ng-container>
+        <ng-container *ngSwitchDefault>
+          {{ row[col.prop] }}
+        </ng-container>
+      </ng-container>
+    </ng-template>
+  </cd-table>
+</div>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.scss
new file mode 100644 (file)
index 0000000..46d4398
--- /dev/null
@@ -0,0 +1,7 @@
+@use '@carbon/layout';
+
+.requirements-list {
+  list-style-type: disc;
+  padding-left: var(--cds-spacing-05);
+  margin-left: layout.$spacing-02;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.spec.ts
new file mode 100644 (file)
index 0000000..47f8e05
--- /dev/null
@@ -0,0 +1,147 @@
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { of, throwError } from 'rxjs';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector.component';
+
+const createDetail = (
+  id: number,
+  name: string,
+  pools: Array<{ pool: string; used: number }>,
+  enabled = true,
+  peers: Record<string, unknown> = { peer: {} }
+) => ({
+  cephfs: {
+    id,
+    name,
+    pools,
+    flags: { enabled },
+    mirror_info: { peers }
+  }
+});
+
+describe('CephfsFilesystemSelectorComponent', () => {
+  let component: CephfsFilesystemSelectorComponent;
+  let fixture: ComponentFixture<CephfsFilesystemSelectorComponent>;
+  let cephfsServiceMock: jest.Mocked<Pick<CephfsService, 'list' | 'getCephfs'>>;
+
+  beforeEach(async () => {
+    cephfsServiceMock = {
+      list: jest.fn(),
+      getCephfs: jest.fn()
+    };
+
+    cephfsServiceMock.list.mockReturnValue(of([]));
+    cephfsServiceMock.getCephfs.mockReturnValue(of(null));
+
+    await TestBed.configureTestingModule({
+      declarations: [CephfsFilesystemSelectorComponent],
+      providers: [{ provide: CephfsService, useValue: cephfsServiceMock }, DimlessBinaryPipe],
+      schemas: [NO_ERRORS_SCHEMA]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(CephfsFilesystemSelectorComponent);
+    component = fixture.componentInstance;
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should configure columns on init', () => {
+    fixture.detectChanges();
+
+    expect(component.columns.map((c) => c.prop)).toEqual([
+      'name',
+      'used',
+      'pools',
+      'mdsStatus',
+      'mirroringStatus'
+    ]);
+  });
+
+  it('should populate filesystems from service data', fakeAsync(() => {
+    cephfsServiceMock.list.mockReturnValue(of([{ id: 1 }]));
+    cephfsServiceMock.getCephfs.mockReturnValue(
+      of(
+        createDetail(1, 'fs1', [
+          { pool: 'data', used: 100 },
+          { pool: 'meta', used: 50 }
+        ])
+      )
+    );
+
+    fixture.detectChanges();
+
+    let filesystems: any[] = [];
+    component.filesystems$.subscribe((rows) => {
+      filesystems = rows;
+    });
+    tick();
+
+    expect(filesystems).toEqual([
+      {
+        id: 1,
+        name: 'fs1',
+        pools: ['data', 'meta'],
+        used: '150',
+        mdsStatus: 'Inactive',
+        mirroringStatus: 'Disabled'
+      }
+    ]);
+  }));
+
+  it('should set mirroring status to Disabled when list response has no mirror info', fakeAsync(() => {
+    cephfsServiceMock.list.mockReturnValue(of([{ id: 2 }]));
+    cephfsServiceMock.getCephfs.mockReturnValue(of(createDetail(2, 'fs2', [])));
+
+    fixture.detectChanges();
+
+    let filesystems: any[] = [];
+    component.filesystems$.subscribe((rows) => {
+      filesystems = rows;
+    });
+    tick();
+
+    expect(filesystems[0].mirroringStatus).toBe('Disabled');
+  }));
+
+  it('should produce empty filesystems when list is empty', fakeAsync(() => {
+    cephfsServiceMock.list.mockReturnValue(of([]));
+
+    fixture.detectChanges();
+
+    let filesystems: any[] = [];
+    component.filesystems$.subscribe((rows) => {
+      filesystems = rows;
+    });
+    tick();
+
+    expect(filesystems).toEqual([]);
+    expect(cephfsServiceMock.getCephfs).not.toHaveBeenCalled();
+  }));
+
+  it('should skip null details when getCephfs errors', fakeAsync(() => {
+    cephfsServiceMock.list.mockReturnValue(of([{ id: 3 }]));
+    cephfsServiceMock.getCephfs.mockReturnValue(throwError(() => new Error('boom')));
+
+    fixture.detectChanges();
+
+    let filesystems: any[] = [];
+    component.filesystems$.subscribe((rows) => {
+      filesystems = rows;
+    });
+    tick();
+
+    expect(filesystems).toEqual([]);
+  }));
+
+  it('should update selection reference', () => {
+    const selection = new CdTableSelection([{ id: 1 }]);
+    component.updateSelection(selection);
+    expect(component.selection).toBe(selection);
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts
new file mode 100644 (file)
index 0000000..56d3bbb
--- /dev/null
@@ -0,0 +1,115 @@
+import { Component, OnInit, TemplateRef, ViewChild, inject } from '@angular/core';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { catchError, map, switchMap } from 'rxjs/operators';
+import { forkJoin, of, Observable } from 'rxjs';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import {
+  CephfsDetail,
+  FilesystemRow,
+  MdsStatus,
+  MirroringStatus,
+  MIRRORING_STATUS,
+  mdsStateToStatus
+} from '~/app/shared/models/cephfs.model';
+
+@Component({
+  selector: 'cd-cephfs-filesystem-selector',
+  templateUrl: './cephfs-filesystem-selector.component.html',
+  standalone: false,
+  styleUrls: ['./cephfs-filesystem-selector.component.scss']
+})
+export class CephfsFilesystemSelectorComponent implements OnInit {
+  @ViewChild('mdsStatus', { static: true })
+  mdsStatus: TemplateRef<any>;
+  @ViewChild('mirroringStatus', { static: true })
+  mirroringStatus: TemplateRef<any>;
+  columns: CdTableColumn[] = [];
+  filesystems$: Observable<FilesystemRow[]> = of([]);
+  selection = new CdTableSelection();
+  icons = Icons;
+  mdsStatusLabels: Record<MdsStatus, string> = {
+    Active: $localize`Active`,
+    Warning: $localize`Warning`,
+    Inactive: $localize`Inactive`
+  };
+  mirroringStatusLabels: Record<MirroringStatus, string> = {
+    Enabled: $localize`Enabled`,
+    Disabled: $localize`Disabled`
+  };
+
+  private cephfsService = inject(CephfsService);
+  private dimlessBinaryPipe = inject(DimlessBinaryPipe);
+
+  ngOnInit(): void {
+    this.columns = [
+      { name: $localize`Filesystem name`, prop: 'name', flexGrow: 2 },
+      { name: $localize`Usage`, prop: 'used', flexGrow: 1, pipe: this.dimlessBinaryPipe },
+      {
+        prop: $localize`pools`,
+        name: 'Pools used',
+        cellTransformation: CellTemplate.tag,
+        customTemplateConfig: {
+          class: 'tag-background-primary'
+        },
+        flexGrow: 1.3
+      },
+      { name: $localize`Status`, prop: 'mdsStatus', flexGrow: 0.8, cellTemplate: this.mdsStatus },
+
+      {
+        name: $localize`Mirroring status`,
+        prop: 'mirroringStatus',
+        flexGrow: 0.8,
+        cellTemplate: this.mirroringStatus
+      }
+    ];
+
+    this.filesystems$ = this.cephfsService.list().pipe(
+      switchMap((listResponse: Array<CephfsDetail>) => {
+        if (!listResponse?.length) {
+          return of([]);
+        }
+        const detailRequests = listResponse.map(
+          (fs): Observable<CephfsDetail | null> =>
+            this.cephfsService.getCephfs(fs.id).pipe(catchError(() => of(null)))
+        );
+        return forkJoin(detailRequests).pipe(
+          map((details: Array<CephfsDetail | null>) =>
+            details
+              .map((detail, index) => {
+                if (!detail?.cephfs) {
+                  return null;
+                }
+                const listItem = listResponse[index];
+                const pools = detail.cephfs.pools || [];
+                const poolNames = pools.map((p) => p.pool);
+                const totalUsed = pools.reduce((sum, p) => sum + p.used, 0);
+                const mdsInfo = listItem?.mdsmap?.info ?? {};
+                const firstMdsGid = Object.keys(mdsInfo)[0];
+                const mdsState = firstMdsGid ? mdsInfo[firstMdsGid]?.state : undefined;
+                return {
+                  id: detail.cephfs.id,
+                  name: detail.cephfs.name,
+                  pools: poolNames,
+                  used: `${totalUsed}`,
+                  mdsStatus: mdsStateToStatus(mdsState),
+                  mirroringStatus: listItem?.mirror_info
+                    ? MIRRORING_STATUS.Enabled
+                    : MIRRORING_STATUS.Disabled
+                } as FilesystemRow;
+              })
+              .filter((row): row is FilesystemRow => row !== null)
+          )
+        );
+      })
+    );
+  }
+
+  updateSelection(selection: CdTableSelection) {
+    this.selection = selection;
+  }
+}
index e8db5f9cf48cdf38ca1004e8e7b1e7ba9db8ee6b..2b712d810ba73db9044312d769fabe22d6d24311 100644 (file)
@@ -1,10 +1,10 @@
-export enum StepTitles {
-  ChooseMirrorPeerRole = 'Choose mirror peer role',
-  SelectFilesystem = 'Select filesystem',
-  CreateOrSelectEntity = 'Create or select entity',
-  GenerateBootstrapToken = 'Generate bootstrap token',
-  Review = 'Review'
-}
+export const StepTitles = {
+  ChooseMirrorPeerRole: $localize`Choose mirror peer role`,
+  SelectFilesystem: $localize`Select filesystem`,
+  CreateOrSelectEntity: $localize`Create or select entity`,
+  GenerateBootstrapToken: $localize`Generate bootstrap token`,
+  Review: $localize`Review`
+} as const;
 
 export const STEP_TITLES_MIRRORING_CONFIGURED = [
   StepTitles.ChooseMirrorPeerRole,
index 5ac776029b2240b29d54761c33ed320ad2d06674..029bcc3f294047ecffd025b4482160bd386fba10 100644 (file)
@@ -67,8 +67,7 @@
         type="info"
         spacingClass="mb-3 mt-3"
         dismissible="true"
-        (dismissed)="showMessage = false"
-        class="mirroring-alert">
+        (dismissed)="showMessage = false">
         <div>
           <div class="cds--type-heading-compact-01 cds-mb-2"
                i18n>About Remote Peer Setup</div>
@@ -94,7 +93,8 @@
 
   <!-- Step 1 -->
   <cd-tearsheet-step>
-    <div>Test 1</div>
+    <cd-cephfs-filesystem-selector>
+    </cd-cephfs-filesystem-selector>
   </cd-tearsheet-step>
 
   <!-- Step 2 -->
index 548cb4c9821d5edefeb074177ce955b554139bd6..47ec4f9b355294e0d0122534e8de8ada9c6dd322 100644 (file)
@@ -58,6 +58,7 @@ import Close from '@carbon/icons/es/close/32';
 import Trash from '@carbon/icons/es/trash-can/32';
 import { CephfsMirroringListComponent } from './cephfs-mirroring-list/cephfs-mirroring-list.component';
 import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs-mirroring-wizard.component';
+import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/cephfs-filesystem-selector.component';
 
 @NgModule({
   imports: [
@@ -112,7 +113,8 @@ import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs
     CephfsMountDetailsComponent,
     CephfsAuthModalComponent,
     CephfsMirroringListComponent,
-    CephfsMirroringWizardComponent
+    CephfsMirroringWizardComponent,
+    CephfsFilesystemSelectorComponent
   ],
   providers: [provideCharts(withDefaultRegisterables())]
 })
index 81b63d35d417381a2b60869b621cf8e7fbffc996..0065b624746bbb53cd6c165e13b59eeb62d49dc3 100644 (file)
             [expandable]="model.isRowExpandable(i)"
             [expanded]="model.isRowExpanded(i)"
             [showSelectionColumn]="showSelectionColumn"
+            [enableSingleSelect]="enableSingleSelect"
             [skeleton]="loadingIndicator"
             (selectRow)="onSelect(i)"
             (deselectRow)="onDeselect(i)"
 
 <ng-template #checkIconTpl
              let-value="data.value">
-@if (value | boolean) {
+  @if (value | boolean) {
   <cd-icon type="check"></cd-icon>
 }
 </ng-template>
index 30edd6a6ec4f1f95923fa63a1ad96996c6f2edad..2ce7a5182d4374e3d180f8149eb52b75bfafc3e3 100644 (file)
@@ -169,7 +169,7 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
   // Allows other components to specify which type of selection they want,
   // e.g. 'single' or 'multi'.
   @Input()
-  selectionType: string = undefined;
+  selectionType: 'single' | 'multiClick' | 'singleRadio' = undefined;
   // By default selected item details will be updated on table refresh, if data has changed
   @Input()
   updateSelectionOnRefresh: 'always' | 'never' | 'onChange' = 'onChange';
@@ -313,11 +313,11 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
   }
 
   get showSelectionColumn() {
-    return this.selectionType === 'multiClick';
+    return this.selectionType === 'multiClick' || this.selectionType === 'singleRadio';
   }
 
   get enableSingleSelect() {
-    return this.selectionType === 'single';
+    return this.selectionType === 'single' || this.selectionType === 'singleRadio';
   }
 
   get headerTitle(): string | TemplateRef<any> {
@@ -1123,7 +1123,7 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
   }
 
   onSelect(selectedRowIndex: number) {
-    if (this.selectionType === 'single') {
+    if (this.selectionType === 'single' || this.selectionType === 'singleRadio') {
       this.model.selectAll(false);
       this.selection.selected = [_.get(this.model.data?.[selectedRowIndex], [0, 'selected'])];
       this.model.selectRow(selectedRowIndex, true);
@@ -1146,7 +1146,7 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
 
   onDeselect(deselectedRowIndex: number) {
     this.model.selectRow(deselectedRowIndex, false);
-    if (this.selectionType === 'single') {
+    if (this.selectionType === 'single' || this.selectionType === 'singleRadio') {
       return;
     }
     this._toggleSelection(deselectedRowIndex, false);
index 955b423b55add6c5f50819a60fe5667565cc2e60..e6db422200091c0845da2d38fecf48705c2510d6 100644 (file)
@@ -52,4 +52,81 @@ export interface MirroringRow {
   id?: string;
 }
 
+export type CephfsPool = {
+  pool: string;
+  used: number;
+};
+
+export type CephfsDetail = {
+  id: number;
+  mdsmap: {
+    info: Record<string, any>;
+    fs_name: string;
+    enabled: boolean;
+    [key: string]: any;
+  };
+  mirror_info?: {
+    peers?: Record<string, string>;
+  };
+  cephfs: {
+    id: number;
+    name: string;
+    pools: CephfsPool[];
+    flags?: {
+      enabled?: boolean;
+    };
+    mirror_info?: {
+      peers?: Record<string, unknown>;
+    };
+  };
+};
+
+export type FilesystemRow = {
+  id: number;
+  name: string;
+  pools: string[];
+  used: string;
+  mdsStatus: MdsStatus;
+  mirroringStatus: MirroringStatus;
+};
+
+export type MdsStatus = 'Active' | 'Warning' | 'Inactive';
+
+export type MirroringStatus = 'Enabled' | 'Disabled';
+
+export const MDS_STATE = {
+  UP_ACTIVE: 'up:active',
+  UP_STARTING: 'up:starting',
+  UP_REJOIN: 'up:rejoin',
+  DOWN_FAILED: 'down:failed',
+  DOWN_STOPPED: 'down:stopped',
+  DOWN_CRASHED: 'down:crashed',
+  UNKNOWN: 'unknown'
+} as const;
+
+export const MDS_STATUS: Record<MdsStatus, MdsStatus> = {
+  Active: 'Active',
+  Warning: 'Warning',
+  Inactive: 'Inactive'
+} as const;
+
+export const MIRRORING_STATUS: Record<MirroringStatus, MirroringStatus> = {
+  Enabled: 'Enabled',
+  Disabled: 'Disabled'
+} as const;
+
+const MDS_STATE_TO_STATUS: Record<string, MdsStatus> = {
+  [MDS_STATE.UP_ACTIVE]: MDS_STATUS.Active,
+  [MDS_STATE.UP_STARTING]: MDS_STATUS.Warning,
+  [MDS_STATE.UP_REJOIN]: MDS_STATUS.Warning,
+  [MDS_STATE.DOWN_FAILED]: MDS_STATUS.Inactive,
+  [MDS_STATE.DOWN_STOPPED]: MDS_STATUS.Inactive,
+  [MDS_STATE.DOWN_CRASHED]: MDS_STATUS.Inactive
+};
+
+export function mdsStateToStatus(state: string | undefined): MdsStatus {
+  const status = state ? MDS_STATE_TO_STATUS[state] : undefined;
+  return status ?? MDS_STATUS.Inactive;
+}
+
 export type DaemonResponse = Daemon[];
index b4eb98629060b7ceb45283993d1cbdcbe0170522..dd87e614c70f3ab9872d5330b835632ee6258e97 100644 (file)
@@ -4,6 +4,10 @@
   padding: 0;
 }
 
+.cds-pt-2px {
+  padding-top: 2px;
+}
+
 .cds-pt-3 {
   padding-top: layout.$spacing-03;
 }