]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add smb share listing in UI 61270/head
authorPedro Gonzalez Gomez <pegonzal@redhat.com>
Wed, 8 Jan 2025 21:17:16 +0000 (22:17 +0100)
committerPedro Gonzalez Gomez <pegonzal@redhat.com>
Fri, 7 Feb 2025 11:43:51 +0000 (12:43 +0100)
Fixes: https://tracker.ceph.com/issues/69449
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
15 files changed:
src/pybind/mgr/dashboard/controllers/smb.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts
src/pybind/mgr/dashboard/tests/test_smb.py

index 100b196f9669bae4734fd22e105fe9d6716f665b..5a3592a232f0e3fa1c7e05fbbad535a28bfa0758 100644 (file)
@@ -201,7 +201,7 @@ class SMBShare(RESTController):
             'smb',
             'show',
             [f'{self._resource}.{cluster_id}' if cluster_id else self._resource])
-        return res['resources'] if 'resources' in res else res
+        return res['resources'] if 'resources' in res else [res]
 
     @raise_on_failure
     @DeletePermission
index 8d3fa098ad551d1e637b9c0db2aeaffe32d04384..73e7deb2fac8398bf60fb12639fa964e6f1fdde6 100644 (file)
@@ -1,23 +1,27 @@
 <ng-container *ngIf="smbClusters$ | async as smbClusters">
   <cd-table
-    #table
     [data]="smbClusters"
     columnMode="flex"
     [columns]="columns"
-    identifier="id"
-    forceIdentifier="true"
     selectionType="single"
-    [hasDetails]="false"
+    [hasDetails]="true"
     (setExpandedRow)="setExpandedRow($event)"
     (fetchData)="loadSMBCluster($event)"
     (updateSelection)="updateSelection($event)"
   >
-  <div class="table-actions">
-    <cd-table-actions class="btn-group"
-                      [permission]="permission"
-                      [selection]="selection"
-                      [tableActions]="tableActions">
-    </cd-table-actions>
-  </div>
-</cd-table>
+    <div class="table-actions">
+      <cd-table-actions
+        class="btn-group"
+        [permission]="permission"
+        [selection]="selection"
+        [tableActions]="tableActions"
+      >
+      </cd-table-actions>
+    </div>
+  >
+    <cd-smb-cluster-tabs
+      *cdTableDetail
+      [selection]="expandedRow">
+    </cd-smb-cluster-tabs>
+  </cd-table>
 </ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.html
new file mode 100644 (file)
index 0000000..b729c89
--- /dev/null
@@ -0,0 +1,15 @@
+<ng-container *ngIf="selection">
+  <cds-tabs
+    type="contained"
+    followFocus="true"
+    isNavigation="true"
+    cacheActive="true">
+  <cds-tab
+    heading="Shares"
+    i18n-heading>
+    <cd-smb-share-list
+      [clusterId]="selection.cluster_id"
+    ></cd-smb-share-list>
+  </cds-tab>
+  </cds-tabs>
+</ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts
new file mode 100644 (file)
index 0000000..d5d302b
--- /dev/null
@@ -0,0 +1,49 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmbClusterTabsComponent } from './smb-cluster-tabs.component';
+import { RESOURCE_TYPE, SMBCluster } from '../smb.model';
+import { By } from '@angular/platform-browser';
+
+describe('SmbClusterTabsComponent', () => {
+  let component: SmbClusterTabsComponent;
+  let fixture: ComponentFixture<SmbClusterTabsComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [SmbClusterTabsComponent]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SmbClusterTabsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should not render anything if selection is falsy', () => {
+    component.selection = null;
+    fixture.detectChanges();
+
+    const tabsElement = fixture.debugElement.query(By.css('cds-tabs'));
+    expect(tabsElement).toBeNull();
+  });
+
+  const selectedSmbCluster = (clusterId: string) => {
+    const smbCluster: SMBCluster = {
+      resource_type: RESOURCE_TYPE,
+      cluster_id: clusterId,
+      auth_mode: 'user'
+    };
+    return smbCluster;
+  };
+
+  it('should render cds-tabs if selection is truthy', () => {
+    component.selection = selectedSmbCluster('fooBar');
+    fixture.detectChanges();
+
+    const tabsElement = fixture.debugElement.query(By.css('cds-tabs'));
+    expect(tabsElement).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.ts
new file mode 100644 (file)
index 0000000..9ec56ec
--- /dev/null
@@ -0,0 +1,12 @@
+import { Component, Input } from '@angular/core';
+import { SMBCluster } from '../smb.model';
+
+@Component({
+  selector: 'cd-smb-cluster-tabs',
+  templateUrl: './smb-cluster-tabs.component.html',
+  styleUrls: ['./smb-cluster-tabs.component.scss']
+})
+export class SmbClusterTabsComponent {
+  @Input()
+  selection: SMBCluster;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html
new file mode 100644 (file)
index 0000000..54cc55a
--- /dev/null
@@ -0,0 +1,11 @@
+<ng-container *ngIf="smbShares$ | async as smbShares">
+  <cd-table
+    [data]="smbShares"
+    columnMode="flex"
+    [columns]="columns"
+    selectionType="single"
+    [hasDetails]="false"
+    (fetchData)="loadSMBShares()"
+  >
+  </cd-table>
+  </ng-container>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.spec.ts
new file mode 100644 (file)
index 0000000..933a874
--- /dev/null
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SmbShareListComponent } from './smb-share-list.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+describe('SmbShareListComponent', () => {
+  let component: SmbShareListComponent;
+  let fixture: ComponentFixture<SmbShareListComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [HttpClientTestingModule],
+      declarations: [SmbShareListComponent]
+    }).compileComponents();
+
+    fixture = TestBed.createComponent(SmbShareListComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts
new file mode 100644 (file)
index 0000000..466d8dc
--- /dev/null
@@ -0,0 +1,89 @@
+import { Component, Input, OnInit, ViewChild } from '@angular/core';
+import { Observable, BehaviorSubject, of } from 'rxjs';
+import { TableComponent } from '~/app/shared/datatable/table/table.component';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { Permission } from '~/app/shared/models/permissions';
+import { SMBShare } from '../smb.model';
+import { switchMap, catchError } from 'rxjs/operators';
+import { SmbService } from '~/app/shared/api/smb.service';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+
+@Component({
+  selector: 'cd-smb-share-list',
+  templateUrl: './smb-share-list.component.html',
+  styleUrls: ['./smb-share-list.component.scss']
+})
+export class SmbShareListComponent implements OnInit {
+  @Input()
+  clusterId: string;
+  @ViewChild('table', { static: true })
+  table: TableComponent;
+  columns: CdTableColumn[];
+  permission: Permission;
+  context: CdTableFetchDataContext;
+
+  smbShares$: Observable<SMBShare[]>;
+  subject$ = new BehaviorSubject<SMBShare[]>([]);
+
+  constructor(private authStorageService: AuthStorageService, private smbService: SmbService) {
+    this.permission = this.authStorageService.getPermissions().smb;
+  }
+
+  ngOnInit() {
+    this.columns = [
+      {
+        name: $localize`ID`,
+        prop: 'share_id',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Name`,
+        prop: 'name',
+        flexGrow: 2
+      },
+      {
+        name: $localize`File System`,
+        prop: 'cephfs.volume',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Path`,
+        prop: 'cephfs.path',
+        cellTransformation: CellTemplate.path,
+        flexGrow: 2
+      },
+      {
+        name: $localize`Subvolume group`,
+        prop: 'cephfs.subvolumegroup',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Subvolume`,
+        prop: 'cephfs.subvolume',
+        flexGrow: 2
+      },
+      {
+        name: $localize`Provider`,
+        prop: 'cephfs.provider',
+        flexGrow: 2
+      }
+    ];
+
+    this.smbShares$ = this.subject$.pipe(
+      switchMap(() =>
+        this.smbService.listShares(this.clusterId).pipe(
+          catchError(() => {
+            this.context.error();
+            return of(null);
+          })
+        )
+      )
+    );
+  }
+
+  loadSMBShares() {
+    this.subject$.next([]);
+  }
+}
index a5e10490a7b9426a6a32709ccaa8655a0fdb0e32..87cc3f1288358abd444b1c0c99f7eb0b3c718fb4 100644 (file)
@@ -3,7 +3,7 @@ import { CephServicePlacement } from '~/app/shared/models/service.interface';
 export interface SMBCluster {
   resource_type: string;
   cluster_id: string;
-  auth_mode: typeof AUTHMODE;
+  auth_mode: typeof AUTHMODE[keyof typeof AUTHMODE];
   domain_settings?: DomainSettings;
   user_group_settings?: JoinSource[];
   custom_dns?: string[];
@@ -53,3 +53,29 @@ export const PLACEMENT = {
 };
 
 export const RESOURCE_TYPE = 'ceph.smb.cluster';
+
+export interface SMBShare {
+  cluster_id: string;
+  share_id: string;
+  intent: string;
+  cephfs: SMBCephfs;
+  name?: string;
+  readonly?: boolean;
+  browseable?: boolean;
+  restrict_access?: boolean;
+  login_control?: SMBShareLoginControl;
+}
+
+interface SMBCephfs {
+  volume: string;
+  path: string;
+  subvolumegroup?: string;
+  subvolume?: string;
+  provider?: string;
+}
+
+interface SMBShareLoginControl {
+  name: string;
+  access: 'read' | 'read-write' | 'none' | 'admin';
+  category?: 'user' | 'group';
+}
index a3a9816e93e6012d0030f45368ef3395d48096e3..f96504de41225dc875349c7fad9f69a12e6114eb 100644 (file)
@@ -5,6 +5,8 @@ import { AppRoutingModule } from '~/app/app-routing.module';
 import { provideCharts, withDefaultRegisterables, BaseChartDirective } from 'ng2-charts';
 import { DataTableModule } from '~/app/shared/datatable/datatable.module';
 import { SmbDomainSettingModalComponent } from './smb-domain-setting-modal/smb-domain-setting-modal.component';
+import { SmbClusterTabsComponent } from './smb-cluster-tabs/smb-cluster-tabs.component';
+import { SmbShareListComponent } from './smb-share-list/smb-share-list.component';
 import {
   ButtonModule,
   CheckboxModule,
@@ -18,7 +20,8 @@ import {
   ModalModule,
   NumberModule,
   PlaceholderModule,
-  SelectModule
+  SelectModule,
+  TabsModule
 } from 'carbon-components-angular';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { CommonModule } from '@angular/common';
@@ -40,6 +43,7 @@ import { NgModule } from '@angular/core';
     DataTableModule,
     GridModule,
     SelectModule,
+    TabsModule,
     InputModule,
     CheckboxModule,
     SelectModule,
@@ -53,7 +57,13 @@ import { NgModule } from '@angular/core';
     IconModule
   ],
   exports: [SmbClusterListComponent, SmbClusterFormComponent],
-  declarations: [SmbClusterListComponent, SmbClusterFormComponent, SmbDomainSettingModalComponent],
+  declarations: [
+    SmbClusterListComponent,
+    SmbClusterFormComponent,
+    SmbDomainSettingModalComponent,
+    SmbClusterTabsComponent,
+    SmbShareListComponent
+  ],
   providers: [provideCharts(withDefaultRegisterables())]
 })
 export class SmbModule {
index e7dc64520f939391e9afe2b604aaec86f41fb0dc..b458e2d7796e12d1d6a91aff370ba5d2f24358db 100644 (file)
@@ -23,7 +23,7 @@ describe('SmbService', () => {
     expect(service).toBeTruthy();
   });
 
-  it('should call list', () => {
+  it('should call list clusters', () => {
     service.listClusters().subscribe();
     const req = httpTesting.expectOne('api/smb/cluster');
     expect(req.request.method).toBe('GET');
@@ -40,4 +40,10 @@ describe('SmbService', () => {
     const req = httpTesting.expectOne('api/smb/cluster/cluster_1');
     expect(req.request.method).toBe('DELETE');
   });
+
+  it('should call list shares for a given cluster', () => {
+    service.listShares('tango').subscribe();
+    const req = httpTesting.expectOne('api/smb/share?cluster_id=tango');
+    expect(req.request.method).toBe('GET');
+  });
 });
index b5e8007482bf3f0bab6b3f070557471ada9b8d48..1a175bf53dddc68aee8d7c53be7f600fc583818c 100644 (file)
@@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 import { Observable, Subject } from 'rxjs';
 
-import { DomainSettings, SMBCluster } from '~/app/ceph/smb/smb.model';
+import { DomainSettings, SMBCluster, SMBShare } from '~/app/ceph/smb/smb.model';
 
 @Injectable({
   providedIn: 'root'
@@ -31,4 +31,8 @@ export class SmbService {
       observe: 'response'
     });
   }
+
+  listShares(clusterId: string): Observable<SMBShare[]> {
+    return this.http.get<SMBShare[]>(`${this.baseURL}/share?cluster_id=${clusterId}`);
+  }
 }
index 9a577709d066da33c0ccd749c960f7c22e41a238..c56a592e2fd25f734517897b0b7e13d550769aab 100644 (file)
@@ -146,35 +146,38 @@ class SMBClusterTest(ControllerTestCase):
 class SMBShareTest(ControllerTestCase):
     _endpoint = '/api/smb/share'
 
-    _shares = [{
-        "resource_type": "ceph.smb.share",
-        "cluster_id": "clusterUserTest",
-        "share_id": "share1",
-                    "intent": "present",
-                    "name": "share1name",
-                    "readonly": "false",
-                    "browseable": "true",
-                    "cephfs": {
-                        "volume": "fs1",
-                        "path": "/",
-                        "provider": "samba-vfs"
-                    }
-    },
-        {
-        "resource_type": "ceph.smb.share",
-        "cluster_id": "clusterADTest",
-        "share_id": "share2",
-                    "intent": "present",
-                    "name": "share2name",
-                    "readonly": "false",
-                    "browseable": "true",
-                    "cephfs": {
-                        "volume": "fs2",
-                        "path": "/",
-                        "provider": "samba-vfs"
-                    }
+    _shares = {
+        "resources": [
+            {
+                "resource_type": "ceph.smb.share",
+                "cluster_id": "clusterUserTest",
+                "share_id": "share1",
+                "intent": "present",
+                "name": "share1name",
+                "readonly": "false",
+                "browseable": "true",
+                "cephfs": {
+                    "volume": "fs1",
+                    "path": "/",
+                    "provider": "samba-vfs",
+                },
+            },
+            {
+                "resource_type": "ceph.smb.share",
+                "cluster_id": "clusterADTest",
+                "share_id": "share2",
+                "intent": "present",
+                "name": "share2name",
+                "readonly": "false",
+                "browseable": "true",
+                "cephfs": {
+                    "volume": "fs2",
+                    "path": "/",
+                    "provider": "samba-vfs",
+                },
+            },
+        ]
     }
-    ]
 
     @classmethod
     def setup_server(cls):
@@ -185,14 +188,14 @@ class SMBShareTest(ControllerTestCase):
 
         self._get(self._endpoint)
         self.assertStatus(200)
-        self.assertJsonBody(self._shares)
+        self.assertJsonBody(self._shares['resources'])
 
-    def test_list_from_cluster(self):
-        mgr.remote = Mock(return_value=self._shares[0])
+    def test_list_one_share(self):
+        mgr.remote = Mock(return_value=self._shares['resources'][0])
 
         self._get(self._endpoint)
         self.assertStatus(200)
-        self.assertJsonBody(self._shares[0])
+        self.assertJsonBody([self._shares['resources'][0]])
 
     def test_delete(self):
         _res = {