From b849c636e223e8d20c361c47281e9249139dddd9 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Wed, 8 Jan 2025 22:17:16 +0100 Subject: [PATCH] mgr/dashboard: add smb share listing in UI Fixes: https://tracker.ceph.com/issues/69449 Signed-off-by: Pedro Gonzalez Gomez --- src/pybind/mgr/dashboard/controllers/smb.py | 2 +- .../smb-cluster-list.component.html | 28 +++--- .../smb-cluster-tabs.component.html | 15 ++++ .../smb-cluster-tabs.component.scss | 0 .../smb-cluster-tabs.component.spec.ts | 49 ++++++++++ .../smb-cluster-tabs.component.ts | 12 +++ .../smb-share-list.component.html | 11 +++ .../smb-share-list.component.scss | 0 .../smb-share-list.component.spec.ts | 24 +++++ .../smb-share-list.component.ts | 89 +++++++++++++++++++ .../frontend/src/app/ceph/smb/smb.model.ts | 28 +++++- .../frontend/src/app/ceph/smb/smb.module.ts | 14 ++- .../src/app/shared/api/smb.service.spec.ts | 8 +- .../src/app/shared/api/smb.service.ts | 6 +- src/pybind/mgr/dashboard/tests/test_smb.py | 67 +++++++------- 15 files changed, 303 insertions(+), 50 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/smb.py b/src/pybind/mgr/dashboard/controllers/smb.py index 100b196f9669b..5a3592a232f0e 100644 --- a/src/pybind/mgr/dashboard/controllers/smb.py +++ b/src/pybind/mgr/dashboard/controllers/smb.py @@ -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 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html index 8d3fa098ad551..73e7deb2fac83 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-list/smb-cluster-list.component.html @@ -1,23 +1,27 @@ -
- - -
-
+
+ + +
+ > + + +
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 index 0000000000000..b729c89a84705 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.html @@ -0,0 +1,15 @@ + + + + + + + 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 index 0000000000000..e69de29bb2d1d 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 index 0000000000000..d5d302bf0224b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.spec.ts @@ -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; + + 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 index 0000000000000..9ec56eccd7535 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-cluster-tabs/smb-cluster-tabs.component.ts @@ -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 index 0000000000000..54cc55a2b8d8c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.html @@ -0,0 +1,11 @@ + + + + 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 index 0000000000000..e69de29bb2d1d 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 index 0000000000000..933a874816491 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.spec.ts @@ -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; + + 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 index 0000000000000..466d8dc4318b2 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb-share-list/smb-share-list.component.ts @@ -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; + subject$ = new BehaviorSubject([]); + + 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([]); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts index a5e10490a7b94..87cc3f1288358 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.model.ts @@ -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'; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts index a3a9816e93e60..f96504de41225 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/smb/smb.module.ts @@ -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 { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts index e7dc64520f939..b458e2d7796e1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.spec.ts @@ -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'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts index b5e8007482bf3..1a175bf53dddc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/smb.service.ts @@ -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 { + return this.http.get(`${this.baseURL}/share?cluster_id=${clusterId}`); + } } diff --git a/src/pybind/mgr/dashboard/tests/test_smb.py b/src/pybind/mgr/dashboard/tests/test_smb.py index 9a577709d066d..c56a592e2fd25 100644 --- a/src/pybind/mgr/dashboard/tests/test_smb.py +++ b/src/pybind/mgr/dashboard/tests/test_smb.py @@ -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 = { -- 2.39.5