From: Tiago Melo Date: Thu, 4 Oct 2018 16:20:10 +0000 (+0100) Subject: mgr/dashboard: Add UI for NFS List and Detail X-Git-Tag: v14.1.0~114^2~1 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=063552446117b1ee0bdf45d1e4464fd282b1fed0;p=ceph-ci.git mgr/dashboard: Add UI for NFS List and Detail Signed-off-by: Tiago Melo --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index fcb99c2f0d0..ecd38e4330b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -17,6 +17,8 @@ import { MonitorComponent } from './ceph/cluster/monitor/monitor.component'; import { OsdListComponent } from './ceph/cluster/osd/osd-list/osd-list.component'; import { PrometheusListComponent } from './ceph/cluster/prometheus/prometheus-list/prometheus-list.component'; import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component'; +import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component'; +import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component'; import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component'; import { PoolFormComponent } from './ceph/pool/pool-form/pool-form.component'; import { PoolListComponent } from './ceph/pool/pool-list/pool-list.component'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts index 1887a7dd403..1ba6085070e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/ceph.module.ts @@ -6,6 +6,7 @@ import { BlockModule } from './block/block.module'; import { CephfsModule } from './cephfs/cephfs.module'; import { ClusterModule } from './cluster/cluster.module'; import { DashboardModule } from './dashboard/dashboard.module'; +import { NfsModule } from './nfs/nfs.module'; import { PerformanceCounterModule } from './performance-counter/performance-counter.module'; import { PoolModule } from './pool/pool.module'; import { RgwModule } from './rgw/rgw.module'; @@ -20,6 +21,7 @@ import { RgwModule } from './rgw/rgw.module'; BlockModule, PoolModule, CephfsModule, + NfsModule, SharedModule ], declarations: [] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html new file mode 100644 index 00000000000..1a1a74a8048 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.html @@ -0,0 +1,7 @@ + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts new file mode 100644 index 00000000000..d5a38eb7ceb --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.spec.ts @@ -0,0 +1,98 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import * as _ from 'lodash'; +import { TabsModule } from 'ngx-bootstrap/tabs'; + +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { SharedModule } from '../../../shared/shared.module'; +import { NfsDetailsComponent } from './nfs-details.component'; + +describe('NfsDetailsComponent', () => { + let component: NfsDetailsComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [NfsDetailsComponent], + imports: [SharedModule, TabsModule.forRoot(), HttpClientTestingModule], + providers: i18nProviders + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NfsDetailsComponent); + component = fixture.componentInstance; + + component.selection = new CdTableSelection(); + component.selection.selected = [ + { + export_id: 1, + path: '/qwe', + fsal: { name: 'CEPH', user_id: 'fs', fs_name: 1 }, + cluster_id: 'cluster1', + daemons: ['node1', 'node2'], + pseudo: '/qwe', + tag: 'asd', + access_type: 'RW', + squash: 'no_root_squash', + protocols: [3, 4], + transports: ['TCP', 'UDP'], + clients: [], + id: 'cluster1:1', + state: 'LOADING' + } + ]; + component.selection.update(); + + fixture.detectChanges(); + }); + + beforeEach(() => {}); + + it('should create', () => { + component.ngOnChanges(); + expect(component.data).toBeTruthy(); + }); + + it('should prepare data', () => { + component.ngOnChanges(); + expect(component.data).toEqual({ + 'Access Type': 'RW', + 'CephFS Filesystem': 1, + 'CephFS User': 'fs', + Cluster: 'cluster1', + Daemons: ['node1', 'node2'], + 'NFS Protocol': ['NFSv3', 'NFSv4'], + Path: '/qwe', + Pseudo: '/qwe', + 'Security Label': undefined, + Squash: 'no_root_squash', + 'Storage Backend': 'CephFS', + Transport: ['TCP', 'UDP'] + }); + }); + + it('should prepare data if RGW', () => { + const newData = _.assignIn(component.selection.first(), { + fsal: { + name: 'RGW', + rgw_user_id: 'rgw_user_id' + } + }); + component.selection.selected = [newData]; + component.selection.update(); + component.ngOnChanges(); + expect(component.data).toEqual({ + 'Access Type': 'RW', + Cluster: 'cluster1', + Daemons: ['node1', 'node2'], + 'NFS Protocol': ['NFSv3', 'NFSv4'], + 'Object Gateway User': 'rgw_user_id', + Path: '/qwe', + Pseudo: '/qwe', + Squash: 'no_root_squash', + 'Storage Backend': 'Object Gateway', + Transport: ['TCP', 'UDP'] + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts new file mode 100644 index 00000000000..c3ca8ba038d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-details/nfs-details.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; + +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; + +@Component({ + selector: 'cd-nfs-details', + templateUrl: './nfs-details.component.html', + styleUrls: ['./nfs-details.component.scss'] +}) +export class NfsDetailsComponent implements OnChanges { + @Input() + selection: CdTableSelection; + + selectedItem: any; + data: any; + + constructor(private i18n: I18n) {} + + ngOnChanges() { + if (this.selection.hasSelection) { + this.selectedItem = this.selection.first(); + this.data = {}; + this.data[this.i18n('Cluster')] = this.selectedItem.cluster_id; + this.data[this.i18n('Daemons')] = this.selectedItem.daemons; + this.data[this.i18n('NFS Protocol')] = this.selectedItem.protocols.map( + (protocol) => 'NFSv' + protocol + ); + this.data[this.i18n('Pseudo')] = this.selectedItem.pseudo; + this.data[this.i18n('Access Type')] = this.selectedItem.access_type; + this.data[this.i18n('Squash')] = this.selectedItem.squash; + this.data[this.i18n('Transport')] = this.selectedItem.transports; + this.data[this.i18n('Path')] = this.selectedItem.path; + + if (this.selectedItem.fsal.name === 'CEPH') { + this.data[this.i18n('Storage Backend')] = this.i18n('CephFS'); + this.data[this.i18n('CephFS User')] = this.selectedItem.fsal.user_id; + this.data[this.i18n('CephFS Filesystem')] = this.selectedItem.fsal.fs_name; + this.data[this.i18n('Security Label')] = this.selectedItem.fsal.sec_label_xattr; + } else { + this.data[this.i18n('Storage Backend')] = this.i18n('Object Gateway'); + this.data[this.i18n('Object Gateway User')] = this.selectedItem.fsal.rgw_user_id; + } + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html new file mode 100644 index 00000000000..d41cf65261d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.html @@ -0,0 +1,33 @@ +To apply any changes you have to restart the Ganisha services. + + +
+ + +
+ + + +
+ + + CephFS + Object Gateway + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts new file mode 100644 index 00000000000..056e6b45b05 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts @@ -0,0 +1,328 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastModule } from 'ng2-toastr'; +import { TabsModule } from 'ngx-bootstrap/tabs'; +import { BehaviorSubject, of } from 'rxjs'; + +import { + configureTestBed, + i18nProviders, + PermissionHelper +} from '../../../../testing/unit-test-helper'; +import { NfsService } from '../../../shared/api/nfs.service'; +import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component'; +import { ExecutingTask } from '../../../shared/models/executing-task'; +import { SummaryService } from '../../../shared/services/summary.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { NfsDetailsComponent } from '../nfs-details/nfs-details.component'; +import { NfsListComponent } from './nfs-list.component'; + +describe('NfsListComponent', () => { + let component: NfsListComponent; + let fixture: ComponentFixture; + let summaryService: SummaryService; + let nfsService: NfsService; + let httpTesting: HttpTestingController; + + const refresh = (data) => { + summaryService['summaryDataSource'].next(data); + }; + + configureTestBed( + { + declarations: [NfsListComponent, NfsDetailsComponent], + imports: [ + HttpClientTestingModule, + RouterTestingModule, + SharedModule, + ToastModule.forRoot(), + TabsModule.forRoot() + ], + providers: [TaskListService, i18nProviders] + }, + true + ); + + beforeEach(() => { + fixture = TestBed.createComponent(NfsListComponent); + component = fixture.componentInstance; + summaryService = TestBed.get(SummaryService); + nfsService = TestBed.get(NfsService); + httpTesting = TestBed.get(HttpTestingController); + + // this is needed because summaryService isn't being reset after each test. + summaryService['summaryDataSource'] = new BehaviorSubject(null); + summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('after ngOnInit', () => { + beforeEach(() => { + fixture.detectChanges(); + spyOn(nfsService, 'list').and.callThrough(); + httpTesting.expectOne('api/nfs-ganesha/daemon').flush([]); + httpTesting.expectOne('api/summary'); + }); + + afterEach(() => { + httpTesting.verify(); + }); + + it('should load exports on init', () => { + refresh({}); + httpTesting.expectOne('api/nfs-ganesha/export'); + expect(nfsService.list).toHaveBeenCalled(); + }); + + it('should not load images on init because no data', () => { + refresh(undefined); + expect(nfsService.list).not.toHaveBeenCalled(); + }); + + it('should call error function on init when summary service fails', () => { + spyOn(component.table, 'reset'); + summaryService['summaryDataSource'].error(undefined); + expect(component.table.reset).toHaveBeenCalled(); + }); + }); + + describe('handling of executing tasks', () => { + let exports: any[]; + + const addExport = (export_id) => { + const model = { + export_id: export_id, + path: 'path_' + export_id, + fsal: 'fsal_' + export_id, + cluster_id: 'cluster_' + export_id + }; + exports.push(model); + }; + + const addTask = (name: string, export_id: string) => { + const task = new ExecutingTask(); + task.name = name; + switch (task.name) { + case 'nfs/create': + task.metadata = { + path: 'path_' + export_id, + fsal: 'fsal_' + export_id, + cluster_id: 'cluster_' + export_id + }; + break; + default: + task.metadata = { + cluster_id: 'cluster_' + export_id, + export_id: export_id + }; + break; + } + summaryService.addRunningTask(task); + }; + + const expectExportTasks = (expo: any, executing: string) => { + expect(expo.cdExecuting).toEqual(executing); + }; + + beforeEach(() => { + exports = []; + addExport('a'); + addExport('b'); + addExport('c'); + component.exports = exports; + refresh({ executing_tasks: [], finished_tasks: [] }); + spyOn(nfsService, 'list').and.callFake(() => of(exports)); + fixture.detectChanges(); + + const req = httpTesting.expectOne('api/nfs-ganesha/daemon'); + req.flush([]); + }); + + it('should gets all exports without tasks', () => { + expect(component.exports.length).toBe(3); + expect(component.exports.every((expo) => !expo.cdExecuting)).toBeTruthy(); + }); + + it('should add a new export from a task', fakeAsync(() => { + addTask('nfs/create', 'd'); + tick(); + expect(component.exports.length).toBe(4); + expectExportTasks(component.exports[0], undefined); + expectExportTasks(component.exports[1], undefined); + expectExportTasks(component.exports[2], undefined); + expectExportTasks(component.exports[3], 'Creating'); + })); + + it('should show when an existing export is being modified', () => { + addTask('nfs/edit', 'a'); + addTask('nfs/delete', 'b'); + expect(component.exports.length).toBe(3); + expectExportTasks(component.exports[0], 'Updating'); + expectExportTasks(component.exports[1], 'Deleting'); + }); + }); + + describe('show action buttons and drop down actions depending on permissions', () => { + let tableActions: TableActionsComponent; + let scenario: { fn; empty; single }; + let permissionHelper: PermissionHelper; + + const getTableActionComponent = (): TableActionsComponent => { + fixture.detectChanges(); + return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance; + }; + + beforeEach(() => { + permissionHelper = new PermissionHelper(component.permission, () => + getTableActionComponent() + ); + scenario = { + fn: () => tableActions.getCurrentButton().name, + single: 'Edit', + empty: 'Add' + }; + }); + + describe('with all', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1); + }); + + it(`shows 'Edit' for single selection else 'Add' as main action`, () => + permissionHelper.testScenarios(scenario)); + + it('shows all actions', () => { + expect(tableActions.tableActions.length).toBe(3); + expect(tableActions.tableActions).toEqual(component.tableActions); + }); + }); + + describe('with read, create and update', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 0); + }); + + it(`shows 'Edit' for single selection else 'Add' as main action`, () => + permissionHelper.testScenarios(scenario)); + + it(`shows all actions except for 'Delete'`, () => { + expect(tableActions.tableActions.length).toBe(2); + component.tableActions.pop(); + expect(tableActions.tableActions).toEqual(component.tableActions); + }); + }); + + describe('with read, create and delete', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(1, 0, 1); + }); + + it(`shows 'Delete' for single selection else 'Add' as main action`, () => { + scenario.single = 'Delete'; + permissionHelper.testScenarios(scenario); + }); + + it(`shows 'Add', and 'Delete' action`, () => { + expect(tableActions.tableActions.length).toBe(2); + expect(tableActions.tableActions).toEqual([ + component.tableActions[0], + component.tableActions[2] + ]); + }); + }); + + describe('with read, edit and delete', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 1); + }); + + it(`shows always 'Edit' as main action`, () => { + scenario.empty = 'Edit'; + permissionHelper.testScenarios(scenario); + }); + + it(`shows 'Edit' and 'Delete' actions`, () => { + expect(tableActions.tableActions.length).toBe(2); + expect(tableActions.tableActions).toEqual([ + component.tableActions[1], + component.tableActions[2] + ]); + }); + }); + + describe('with read and create', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(1, 0, 0); + }); + + it(`always shows 'Add' as main action`, () => { + scenario.single = 'Add'; + permissionHelper.testScenarios(scenario); + }); + + it(`shows 'Add' action`, () => { + expect(tableActions.tableActions.length).toBe(1); + expect(tableActions.tableActions).toEqual([component.tableActions[0]]); + }); + }); + + describe('with read and edit', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(0, 1, 0); + }); + + it(`shows always 'Edit' as main action`, () => { + scenario.empty = 'Edit'; + permissionHelper.testScenarios(scenario); + }); + + it(`shows 'Edit' action`, () => { + expect(tableActions.tableActions.length).toBe(1); + expect(tableActions.tableActions).toEqual([component.tableActions[1]]); + }); + }); + + describe('with read and delete', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(0, 0, 1); + }); + + it(`shows always 'Delete' as main action`, () => { + scenario.single = 'Delete'; + scenario.empty = 'Delete'; + permissionHelper.testScenarios(scenario); + }); + + it(`shows 'Delete' action`, () => { + expect(tableActions.tableActions.length).toBe(1); + expect(tableActions.tableActions).toEqual([component.tableActions[2]]); + }); + }); + + describe('with only read', () => { + beforeEach(() => { + tableActions = permissionHelper.setPermissionsAndGetActions(0, 0, 0); + }); + + it('shows no main action', () => { + permissionHelper.testScenarios({ + fn: () => tableActions.getCurrentButton(), + single: undefined, + empty: undefined + }); + }); + + it('shows no actions', () => { + expect(tableActions.tableActions.length).toBe(0); + expect(tableActions.tableActions).toEqual([]); + }); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts new file mode 100644 index 00000000000..fb00a8ae3e9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts @@ -0,0 +1,213 @@ +import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import * as _ from 'lodash'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { Subscription } from 'rxjs'; + +import { NfsService } from '../../../shared/api/nfs.service'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { TableComponent } from '../../../shared/datatable/table/table.component'; +import { CellTemplate } from '../../../shared/enum/cell-template.enum'; +import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; +import { CdTableAction } from '../../../shared/models/cd-table-action'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; +import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; + +@Component({ + selector: 'cd-nfs-list', + templateUrl: './nfs-list.component.html', + styleUrls: ['./nfs-list.component.scss'], + providers: [TaskListService] +}) +export class NfsListComponent implements OnInit, OnDestroy { + @ViewChild('nfsState') + nfsState: TemplateRef; + @ViewChild('nfsFsal') + nfsFsal: TemplateRef; + + @ViewChild('table') + table: TableComponent; + + columns: CdTableColumn[]; + permission: Permission; + selection = new CdTableSelection(); + summaryDataSubscription: Subscription; + viewCacheStatus: any; + exports: any[]; + tableActions: CdTableAction[]; + isDefaultCluster = false; + + modalRef: BsModalRef; + + builders = { + 'nfs/create': (metadata) => { + return { + path: metadata['path'], + cluster_id: metadata['cluster_id'], + fsal: metadata['fsal'] + }; + } + }; + + constructor( + private authStorageService: AuthStorageService, + private i18n: I18n, + private modalService: BsModalService, + private nfsService: NfsService, + private taskListService: TaskListService, + private taskWrapper: TaskWrapperService + ) { + this.permission = this.authStorageService.getPermissions().nfs; + const getNfsUri = () => + this.selection.first() && + `${encodeURI(this.selection.first().cluster_id)}/${encodeURI( + this.selection.first().export_id + )}`; + + const addAction: CdTableAction = { + permission: 'create', + icon: 'fa-plus', + routerLink: () => '/nfs/add', + canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection, + name: this.i18n('Add') + }; + + const editAction: CdTableAction = { + permission: 'update', + icon: 'fa-pencil', + routerLink: () => `/nfs/edit/${getNfsUri()}`, + name: this.i18n('Edit') + }; + + const deleteAction: CdTableAction = { + permission: 'delete', + icon: 'fa-times', + click: () => this.deleteNfsModal(), + name: this.i18n('Delete') + }; + + this.tableActions = [addAction, editAction, deleteAction]; + } + + ngOnInit() { + this.columns = [ + { + name: this.i18n('Export'), + prop: 'path', + flexGrow: 2, + cellTransformation: CellTemplate.executing + }, + { + name: this.i18n('Cluster'), + prop: 'cluster_id', + flexGrow: 2 + }, + { + name: this.i18n('Daemons'), + prop: 'daemons', + flexGrow: 2 + }, + { + name: this.i18n('Storage Backend'), + prop: 'fsal', + flexGrow: 2, + cellTemplate: this.nfsFsal + }, + { + name: this.i18n('Access Type'), + prop: 'access_type', + flexGrow: 2 + } + ]; + + this.nfsService.daemon().subscribe( + (daemons: any) => { + const clusters = _(daemons) + .map((daemon) => daemon.cluster_id) + .uniq() + .value(); + + this.isDefaultCluster = clusters.length === 1 && clusters[0] === '_default_'; + this.columns[1].isHidden = this.isDefaultCluster; + if (this.table) { + this.table.updateColumns(); + } + + this.taskListService.init( + () => this.nfsService.list(), + (resp) => this.prepareResponse(resp), + (exports) => (this.exports = exports), + () => this.onFetchError(), + this.taskFilter, + this.itemFilter, + this.builders + ); + }, + (error) => { + this.onFetchError(); + } + ); + } + + ngOnDestroy() { + if (this.summaryDataSubscription) { + this.summaryDataSubscription.unsubscribe(); + } + } + + prepareResponse(resp: any): any[] { + let result = []; + resp.forEach((nfs) => { + nfs.id = `${nfs.cluster_id}:${nfs.export_id}`; + nfs.state = 'LOADING'; + result = result.concat(nfs); + }); + + return result; + } + + onFetchError() { + this.table.reset(); // Disable loading indicator. + this.viewCacheStatus = { status: ViewCacheStatus.ValueException }; + } + + itemFilter(entry, task) { + return ( + entry.cluster_id === task.metadata['cluster_id'] && + entry.export_id === task.metadata['export_id'] + ); + } + + taskFilter(task) { + return ['nfs/create', 'nfs/delete', 'nfs/edit'].includes(task.name); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + deleteNfsModal() { + const cluster_id = this.selection.first().cluster_id; + const export_id = this.selection.first().export_id; + + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + initialState: { + itemDescription: this.i18n('NFS'), + submitActionObservable: () => + this.taskWrapper.wrapTaskAroundCall({ + task: new FinishedTask('nfs/delete', { + cluster_id: cluster_id, + export_id: export_id + }), + call: this.nfsService.delete(cluster_id, export_id) + }) + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts new file mode 100644 index 00000000000..1a738c39ed0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts @@ -0,0 +1,26 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; + +import { TabsModule } from 'ngx-bootstrap/tabs'; +import { TypeaheadModule } from 'ngx-bootstrap/typeahead'; + +import { SharedModule } from '../../shared/shared.module'; +import { NfsDetailsComponent } from './nfs-details/nfs-details.component'; +import { NfsFormClientComponent } from './nfs-form-client/nfs-form-client.component'; +import { NfsFormComponent } from './nfs-form/nfs-form.component'; +import { NfsListComponent } from './nfs-list/nfs-list.component'; + +@NgModule({ + imports: [ + ReactiveFormsModule, + RouterModule, + SharedModule, + TabsModule.forRoot(), + CommonModule, + TypeaheadModule.forRoot() + ], + declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent] +}) +export class NfsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 52ac52a110d..60eeb1e6b14 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -157,10 +157,17 @@ class="dropdown-item" routerLink="/block/iscsi">iSCSI - + +
  • + NFS +
  • +