'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
<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>
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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;
+}
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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([]);
+ }
+}
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[];
};
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';
+}
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,
ModalModule,
NumberModule,
PlaceholderModule,
- SelectModule
+ SelectModule,
+ TabsModule
} from 'carbon-components-angular';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
DataTableModule,
GridModule,
SelectModule,
+ TabsModule,
InputModule,
CheckboxModule,
SelectModule,
IconModule
],
exports: [SmbClusterListComponent, SmbClusterFormComponent],
- declarations: [SmbClusterListComponent, SmbClusterFormComponent, SmbDomainSettingModalComponent],
+ declarations: [
+ SmbClusterListComponent,
+ SmbClusterFormComponent,
+ SmbDomainSettingModalComponent,
+ SmbClusterTabsComponent,
+ SmbShareListComponent
+ ],
providers: [provideCharts(withDefaultRegisterables())]
})
export class SmbModule {
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');
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');
+ });
});
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'
observe: 'response'
});
}
+
+ listShares(clusterId: string): Observable<SMBShare[]> {
+ return this.http.get<SMBShare[]>(`${this.baseURL}/share?cluster_id=${clusterId}`);
+ }
}
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):
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 = {