]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Fix NFS routing 56658/head
authorAfreen <afreen23.git@gmail.com>
Wed, 3 Apr 2024 02:15:32 +0000 (07:45 +0530)
committerAfreen Misbah <afreen23.git@gmail.com>
Thu, 20 Jun 2024 13:38:36 +0000 (19:08 +0530)
Fixes https://tracker.ceph.com/issues/65310

The NFS tab in object and File nav uses same route due to which both
gets activated when one of them is clicked.
Hence, this PR separates the routing for Object and File nav.
Object-> NFS: /rgw/nfs
File-> NFS: /cephfs/nfs

Both routes use same NFS List and Form component but under different
routes as mentioned above.

Changes summary
- updated route for File from "/fs" to "/cephfs/<any_other_sub_route>"
  to support both fs and nfs tabs. Since using `/fs` and `/fs/nfs` will
activate both paths and it will be an undesirable user experience.
- `getFsalRouteFromPath` helper function to set the storage backend from
  route.
- removed `stoarge-backend` field from nfs form as now route decides teh
  storage backend
- breadcrumbs redirect to respective navs
- updated e2e tests
- updated unit tests
- changes list page of object-> nfs page to say Bucket instead of Path

Signed-off-by: Afreen <afreen23.git@gmail.com>
19 files changed:
src/pybind/mgr/dashboard/frontend/cypress/e2e/common/urls.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/10-nfs-exports.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/orchestrator/workflow/nfs/nfs-export.po.ts
src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts
src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/models/nfs.fsal.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-list/nfs-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/utils.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/api/nfs.service.ts

index 6f7316f98f59e9a1c8a44c7d98ed88d40b7bb787..970fc8ff509cf17ed89dd3ced4b5a866e82ed937 100644 (file)
@@ -42,7 +42,7 @@ export class UrlsCollection extends PageHelper {
     'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' },
 
     // CephFS
-    cephfs: { url: '#/cephfs', id: 'cd-cephfs-list' },
-    'create cephfs': { url: '#/cephfs/create', id: 'cd-cephfs-form' }
+    cephfs: { url: '#/cephfs/fs', id: 'cd-cephfs-list' },
+    'create cephfs': { url: '#/cephfs/fs/create', id: 'cd-cephfs-form' }
   };
 }
index fdd96d7e975ea7da5446da84b88a43429a398096..ff2e7581bb6a155b7403111fb5e572f06f663740 100644 (file)
@@ -19,11 +19,11 @@ describe('nfsExport page', () => {
 
   beforeEach(() => {
     cy.login();
-    nfsExport.navigateTo();
   });
 
   describe('breadcrumb test', () => {
     it('should open and show breadcrumb', () => {
+      nfsExport.navigateTo('rgw_index');
       nfsExport.expectBreadcrumbText('NFS');
     });
   });
@@ -43,23 +43,24 @@ describe('nfsExport page', () => {
       buckets.navigateTo('create');
       buckets.create(bucketName, 'dashboard');
 
-      nfsExport.navigateTo();
+      nfsExport.navigateTo('rgw_index');
       nfsExport.existTableCell(rgwPseudo, false);
-      nfsExport.navigateTo('create');
+      nfsExport.navigateTo('rgw_create');
       nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
       nfsExport.existTableCell(rgwPseudo);
     });
 
     // @TODO: uncomment this when a CephFS volume can be created through Dashboard.
     // it('should create a nfs-export with CephFS backend', () => {
-    //   nfsExport.navigateTo();
+    //   nfsExport.navigateTo('cephfs_index');
     //   nfsExport.existTableCell(fsPseudo, false);
-    //   nfsExport.navigateTo('create');
+    //   nfsExport.navigateTo('cephfs_create');
     //   nfsExport.create(backends[0], squash, client, fsPseudo);
     //   nfsExport.existTableCell(fsPseudo);
     // });
 
     it('should show Clients', () => {
+      nfsExport.navigateTo('rgw_index');
       nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
       cy.get('cd-nfs-details').within(() => {
         nfsExport.getTableCount('total').should('be.gte', 0);
@@ -67,12 +68,13 @@ describe('nfsExport page', () => {
     });
 
     it('should edit an export', () => {
-      nfsExport.editExport(rgwPseudo, editPseudo);
+      nfsExport.editExport(rgwPseudo, editPseudo, 'rgw_index');
 
       nfsExport.existTableCell(editPseudo);
     });
 
     it('should delete exports and bucket', () => {
+      nfsExport.navigateTo('rgw_index');
       nfsExport.delete(editPseudo);
 
       buckets.navigateTo();
index c700ef0581dd761c89754e1a2c5a660a13c2081b..3639eb9a8ab0860e6c59917bc781b03af5a86b0d 100644 (file)
@@ -3,21 +3,18 @@ import { PageHelper } from '../../../page-helper.po';
 /* tslint:enable*/
 
 const pages = {
-  index: { url: '#/nfs', id: 'cd-nfs-list' },
-  create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+  cephfs_index: { url: '#cephfs/nfs', id: 'cd-nfs-list' },
+  cephfs_create: { url: '#cephfs/nfs/create', id: 'cd-nfs-form' },
+  rgw_index: { url: '#rgw/nfs', id: 'cd-nfs-list' },
+  rgw_create: { url: '#rgw/nfs/create', id: 'cd-nfs-form' }
 };
 
 export class NFSPageHelper extends PageHelper {
   pages = pages;
-
-  @PageHelper.restrictTo(pages.create.url)
   create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
     this.selectOption('cluster_id', 'testnfs');
-    // select a storage backend
-    this.selectOption('name', backend);
     if (backend === 'CephFS') {
       this.selectOption('fs_name', 'myfs');
-
       cy.get('#security_label').click({ force: true });
     } else {
       cy.get('input[data-testid=rgw_path]').type(rgwPath);
@@ -38,8 +35,8 @@ export class NFSPageHelper extends PageHelper {
     cy.get('cd-submit-button').click();
   }
 
-  editExport(pseudo: string, editPseudo: string) {
-    this.navigateEdit(pseudo);
+  editExport(pseudo: string, editPseudo: string, url: string) {
+    this.navigateEdit(pseudo, true, true, url);
 
     cy.get('input[name=pseudo]').clear().type(editPseudo);
 
index 2a16ff7e1418ea22cb608256c585731aa6c36f86..49144b25fbfc9347e961ff9f657e2637bf0a147f 100644 (file)
@@ -52,9 +52,9 @@ export abstract class PageHelper {
   /**
    * Navigates to the edit page
    */
-  navigateEdit(name: string, select = true, breadcrumb = true) {
+  navigateEdit(name: string, select = true, breadcrumb = true, navigateTo: string = null) {
     if (select) {
-      this.navigateTo();
+      this.navigateTo(navigateTo);
       this.getFirstTableCell(name).click();
     }
     cy.contains('Creating...').should('not.exist');
index 6744e9cf23b35d045f6b87fd431ecad23af79d2a..8b09a5fc27d6b3bff529e4fc18d9ae35719d41d5 100644 (file)
@@ -370,18 +370,48 @@ const routes: Routes = [
       {
         path: 'cephfs',
         canActivate: [FeatureTogglesGuardService],
-        data: { breadcrumbs: 'File/File Systems' },
         children: [
-          { path: '', component: CephfsListComponent },
           {
-            path: URLVerbs.CREATE,
+            path: 'fs',
+            component: CephfsListComponent,
+            data: { breadcrumbs: 'File/File Systems' }
+          },
+          {
+            path: `fs/${URLVerbs.CREATE}`,
             component: CephfsVolumeFormComponent,
             data: { breadcrumbs: ActionLabels.CREATE }
           },
           {
-            path: `${URLVerbs.EDIT}/:id`,
+            path: `fs/${URLVerbs.EDIT}/:id`,
             component: CephfsVolumeFormComponent,
             data: { breadcrumbs: ActionLabels.EDIT }
+          },
+          {
+            path: 'nfs',
+            canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+            data: {
+              moduleStatusGuardConfig: {
+                uiApiPath: 'nfs-ganesha',
+                redirectTo: 'error',
+                section: 'nfs-ganesha',
+                section_info: 'NFS GANESHA',
+                header: 'NFS-Ganesha is not configured'
+              },
+              breadcrumbs: 'File/NFS'
+            },
+            children: [
+              { path: '', component: NfsListComponent },
+              {
+                path: URLVerbs.CREATE,
+                component: NfsFormComponent,
+                data: { breadcrumbs: ActionLabels.CREATE }
+              },
+              {
+                path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+                component: NfsFormComponent,
+                data: { breadcrumbs: ActionLabels.EDIT }
+              }
+            ]
           }
         ]
       },
@@ -421,34 +451,6 @@ const routes: Routes = [
             data: { breadcrumbs: ActionLabels.EDIT }
           }
         ]
-      },
-      // NFS
-      {
-        path: 'nfs',
-        canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
-        data: {
-          moduleStatusGuardConfig: {
-            uiApiPath: 'nfs-ganesha',
-            redirectTo: 'error',
-            section: 'nfs-ganesha',
-            section_info: 'NFS GANESHA',
-            header: 'NFS-Ganesha is not configured'
-          },
-          breadcrumbs: 'NFS'
-        },
-        children: [
-          { path: '', component: NfsListComponent },
-          {
-            path: URLVerbs.CREATE,
-            component: NfsFormComponent,
-            data: { breadcrumbs: ActionLabels.CREATE }
-          },
-          {
-            path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
-            component: NfsFormComponent,
-            data: { breadcrumbs: ActionLabels.EDIT }
-          }
-        ]
       }
     ]
   },
index dbbe522fa0a19f6b48549c90924c43d63a889874..0506c4c77341fbe8fc0aa8f978ace62c3da1928f 100644 (file)
@@ -68,7 +68,7 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
     private route: ActivatedRoute
   ) {
     super();
-    this.editing = this.router.url.startsWith(`/cephfs/${URLVerbs.EDIT}`);
+    this.editing = this.router.url.startsWith(`/cephfs/fs/${URLVerbs.EDIT}`);
     this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
     this.resource = $localize`File System`;
     this.hosts = {
@@ -176,7 +176,7 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
             this.form.setErrors({ cdSubmitButton: true });
           },
           complete: () => {
-            this.router.navigate([BASE_URL]);
+            this.router.navigate([`${BASE_URL}/fs`]);
           }
         });
     } else {
@@ -210,7 +210,7 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
             self.form.setErrors({ cdSubmitButton: true });
           },
           complete: () => {
-            this.router.navigate([BASE_URL]);
+            this.router.navigate([`${BASE_URL}/fs`]);
           }
         });
     }
index 2957401d86aae18a966b7f5d23bf0f93454cf708..748eeee0ee4c4dffccbe4ce1d38868856b315984 100644 (file)
@@ -27,7 +27,7 @@ import { map, switchMap } from 'rxjs/operators';
 import { HealthService } from '~/app/shared/api/health.service';
 import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component';
 
-const BASE_URL = 'cephfs';
+const BASE_URL = 'cephfs/fs';
 
 @Component({
   selector: 'cd-cephfs-list',
index f204ac6d8b6b5f8caa32cd9d95a7aacf7984f0ce..cbdc44f3ca8c2c8fd67dbacb5ac6b05cca71b209 100644 (file)
@@ -1,5 +1,9 @@
+export enum SUPPORTED_FSAL {
+  CEPH = 'CEPH',
+  RGW = 'RGW'
+}
 export interface NfsFSAbstractionLayer {
-  value: string;
+  value: SUPPORTED_FSAL;
   descr: string;
   disabled: boolean;
 }
index f54361a5f7d11af851bbdef938e705ad0106761e..ae427886f0e2c33625e1ce2cdaf9f4ba1c3d31fa 100644 (file)
@@ -15,9 +15,6 @@
                  for="cluster_id">
             <span class="required"
                   i18n>Cluster</span>
-            <cd-helper>
-              <p i18n>This is the ID of an NFS Service.</p>
-            </cd-helper>
           </label>
           <div class="cd-col-form-input">
             <select class="form-select"
@@ -36,6 +33,9 @@
               <option *ngFor="let cluster of allClusters"
                       [value]="cluster.cluster_id">{{ cluster.cluster_id }}</option>
             </select>
+            <cd-help-text>
+              <p i18n>This is the ID of an NFS Service.</p>
+            </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('cluster_id', formDir, 'required') || allClusters?.length === 0"
                   i18n>This field is required.
 
         <!-- FSAL -->
         <div formGroupName="fsal">
-          <!-- Name -->
-          <div class="form-group row">
-            <label class="cd-col-form-label required"
-                   for="name"
-                   i18n>Storage Backend</label>
-            <div class="cd-col-form-input">
-              <select class="form-select"
-                      formControlName="name"
-                      name="name"
-                      id="name"
-                      (change)="fsalChangeHandler()">
-                <option *ngIf="allFsals === null"
-                        value=""
-                        i18n>Loading...</option>
-                <option *ngIf="allFsals !== null && allFsals.length === 0"
-                        value=""
-                        i18n>-- No data pools available --</option>
-                <option *ngIf="allFsals !== null && allFsals.length > 0"
-                        value=""
-                        i18n>-- Select the storage backend --</option>
-                <option *ngFor="let fsal of allFsals"
-                        [value]="fsal.value"
-                        [disabled]="fsal.disabled">{{ fsal.descr }}</option>
-              </select>
-              <span class="invalid-feedback"
-                    *ngIf="nfsForm.showError('name', formDir, 'required')"
-                    i18n>This field is required.</span>
-              <span class="invalid-feedback"
-                    *ngIf="fsalAvailabilityError"
-                    i18n>{{ fsalAvailabilityError }}</span>
-            </div>
-          </div>
-
           <!-- CephFS Volume -->
           <div class="form-group row"
-               *ngIf="nfsForm.getValue('name') === 'CEPH'">
+               *ngIf="storageBackend === 'CEPH'">
             <label class="cd-col-form-label required"
                    for="fs_name"
                    i18n>Volume</label>
 
         <!-- Security Label -->
         <div class="form-group row"
-             *ngIf="nfsForm.getValue('name') === 'CEPH'">
+             *ngIf="storageBackend === 'CEPH'">
           <label class="cd-col-form-label"
                  [ngClass]="{'required': nfsForm.getValue('security_label')}"
                  for="security_label"
 
         <!-- Path -->
         <div class="form-group row"
-             *ngIf="nfsForm.getValue('name') === 'CEPH'">
+             *ngIf="storageBackend === 'CEPH'">
           <label class="cd-col-form-label"
                  for="path">
             <span class="required"
                   i18n>CephFS Path</span>
-            <cd-helper>
-              <p i18n>A path in a CephFS file system.</p>
-            </cd-helper>
           </label>
           <div class="cd-col-form-input">
             <input type="text"
                    [ngbTypeahead]="pathDataSource"
                    (selectItem)="pathChangeHandler()"
                    (blur)="pathChangeHandler()">
+            <cd-help-text>
+            <p i18n>A path in a CephFS file system.</p>
+          </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('path', formDir, 'required')"
                   i18n>This field is required.</span>
 
         <!-- Bucket -->
         <div class="form-group row"
-             *ngIf="nfsForm.getValue('name') === 'RGW'">
+             *ngIf="storageBackend === 'RGW'">
           <label class="cd-col-form-label"
                  for="path">
             <span class="required"
                  for="pseudo">
             <span class="required"
                   i18n>Pseudo</span>
-            <cd-helper>
-              <p i18n>The position that this <strong>NFS v4</strong> export occupies
-                in the <strong>Pseudo FS</strong> (it must be unique).</p>
-              <p i18n>By using different Pseudo options, the same Path may be exported multiple times.</p>
-            </cd-helper>
           </label>
           <div class="cd-col-form-input">
             <input type="text"
                    id="pseudo"
                    formControlName="pseudo"
                    minlength="2">
+            <cd-help-text>
+              <span i18n>The position this export occupies in the Pseudo FS. It must be unique.</span><br/>
+              <span i18n>By using different Pseudo options, the same Path may be exported multiple times.</span>
+            </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('pseudo', formDir, 'required')"
                   i18n>This field is required.</span>
               {{ getAccessTypeHelp(nfsForm.getValue('access_type')) }}
             </span>
             <span class="form-text text-warning"
-                  *ngIf="nfsForm.getValue('access_type') === 'RW' && nfsForm.getValue('name') === 'RGW'"
+                  *ngIf="nfsForm.getValue('access_type') === 'RW' && storageBackend === 'RGW'"
                   i18n>The Object Gateway NFS backend has a number of
               limitations which will seriously affect applications writing to
               the share. Please consult the <cd-doc section="rgw-nfs"></cd-doc>
           <label class="cd-col-form-label"
                  for="squash">
             <span i18n>Squash</span>
-            <ng-container *ngTemplateOutlet="squashHelper"></ng-container>
           </label>
           <div class="cd-col-form-input">
             <select class="form-select"
                       [value]="squash">{{ squash }}</option>
 
             </select>
+            <cd-help-text>
+              <span *ngIf="nfsForm.getValue('squash') === 'root_squash'"
+                    i18n>Maps the root user on the NFS client to an anonymous user/group with limited privileges. This prevents a root client user from having total control over the NFS export.</span>
+
+              <span *ngIf="nfsForm.getValue('squash') === 'root_id_squash'"
+                    i18n>Maps the root user on the NFS client to an anonymous user/group with limited privileges, preventing root access but retaining non-root group privileges.</span>
+
+              <span *ngIf="nfsForm.getValue('squash') === 'all_squash'"
+                    i18n>Maps all users on the NFS client to an anonymous user/group with limited privileges, ensuring that no user has special privileges on the NFS export.</span>
+
+              <span *ngIf="nfsForm.getValue('squash') === 'no_root_squash'"
+                    i18n>Allows the root user on the NFS client to retain full root privileges on the NFS server, which may pose security risks.</span>
+
+            </cd-help-text>
             <span class="invalid-feedback"
                   *ngIf="nfsForm.showError('squash', formDir,'required')"
                   i18n>This field is required.</span>
         <cd-nfs-form-client [form]="nfsForm"
                             [clients]="clients"
                             #nfsClients>
-          <ng-template #squashHelper>
-            <cd-helper>
-              <ul class="squash-helper">
-                <li>
-                  <span class="squash-helper-item-value">no_root_squash: </span>
-                  <span i18n>No user id squashing is performed.</span>
-                </li>
-                <li>
-                  <span class="squash-helper-item-value">root_id_squash: </span>
-                  <span i18n>uid 0 and gid 0 are squashed to the Anonymous_Uid and Anonymous_Gid gid 0 in alt_groups lists is also squashed.</span>
-                </li>
-                <li>
-                  <span class="squash-helper-item-value">root_squash: </span>
-                  <span i18n>uid 0 and gid of any value are squashed to the Anonymous_Uid and Anonymous_Gid alt_groups lists is discarded.</span>
-                </li>
-                <li>
-                  <span class="squash-helper-item-value">all_squash: </span>
-                  <span i18n>All users are squashed.</span>
-                </li>
-              </ul>
-            </cd-helper>
-          </ng-template>
         </cd-nfs-form-client>
 
+        <!-- Errors -->
+        <cd-alert-panel type="error"
+                        *ngIf="!!storageBackendError">
+          {{storageBackendError}}
+        </cd-alert-panel>
       </div>
-
       <div class="card-footer">
         <cd-form-button-panel (submitActionEvent)="submitAction()"
                               [form]="nfsForm"
+                              [disabled]="!!storageBackendError"
                               [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
                               wrappingClass="text-right"></cd-form-button-panel>
       </div>
index 4d892a120fc64f735d4ba921574256f677c9f8c6..cebcc8877a217ba752a01478da5bfe4d296ed08c 100644 (file)
@@ -1,11 +1,3 @@
 .cd-mb {
   margin-bottom: 10px;
 }
-
-.squash-helper {
-  padding-left: 1rem;
-}
-
-.squash-helper-item-value {
-  font-weight: bold;
-}
index 65267a1579164ac565b023b9db0483c2daf8de6a..e6fc6e8ddff30accad1bc90e40f3397c62707bc3 100644 (file)
@@ -1,7 +1,7 @@
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 import { ReactiveFormsModule } from '@angular/forms';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
@@ -21,6 +21,7 @@ describe('NfsFormComponent', () => {
   let fixture: ComponentFixture<NfsFormComponent>;
   let httpTesting: HttpTestingController;
   let activatedRoute: ActivatedRouteStub;
+  let router: Router;
 
   configureTestBed(
     {
@@ -45,9 +46,8 @@ describe('NfsFormComponent', () => {
 
   const matchSquash = (backendSquashValue: string, uiSquashValue: string) => {
     component.ngOnInit();
-    httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
-    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.expectOne('api/nfs-ganesha/export/mynfs/1').flush({
       fsal: {
         name: 'RGW'
@@ -69,12 +69,16 @@ describe('NfsFormComponent', () => {
     component = fixture.componentInstance;
     httpTesting = TestBed.inject(HttpTestingController);
     activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+    router = TestBed.inject(Router);
+
+    Object.defineProperty(router, 'url', {
+      get: jasmine.createSpy('url').and.returnValue('/cephfs/nfs')
+    });
     RgwHelper.selectDaemon();
     fixture.detectChanges();
 
-    httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
-    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+    httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
     httpTesting.verify();
   });
 
@@ -82,15 +86,6 @@ describe('NfsFormComponent', () => {
     expect(component).toBeTruthy();
   });
 
-  it('should process all data', () => {
-    expect(component.allFsals).toEqual([
-      { descr: 'CephFS', value: 'CEPH', disabled: false },
-      { descr: 'Object Gateway', value: 'RGW', disabled: false }
-    ]);
-    expect(component.allFsNames).toEqual([{ id: 1, name: 'a' }]);
-    expect(component.allClusters).toEqual([{ cluster_id: 'mynfs' }]);
-  });
-
   it('should create the form', () => {
     expect(component.nfsForm.value).toEqual({
       access_type: 'RW',
index 0543a9eb7abbdb3d4ce81b1f06e99ad7cb1c66ec..98ef5c85914207b4f676fdd2fd005ce8787cddc9 100644 (file)
@@ -1,4 +1,4 @@
-import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
 import {
   AbstractControl,
   AsyncValidatorFn,
@@ -12,7 +12,7 @@ import _ from 'lodash';
 import { forkJoin, Observable, of } from 'rxjs';
 import { catchError, debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
 
-import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
 import { Directory, NfsService } from '~/app/shared/api/nfs.service';
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
@@ -28,6 +28,7 @@ import { CdHttpErrorResponse } from '~/app/shared/services/api-interceptor.servi
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
+import { getFsalFromRoute, getPathfromFsal } from '../utils';
 
 @Component({
   selector: 'cd-nfs-form',
@@ -50,9 +51,10 @@ export class NfsFormComponent extends CdForm implements OnInit {
   allClusters: { cluster_id: string }[] = null;
   icons = Icons;
 
-  allFsals: any[] = [];
   allFsNames: any[] = null;
-  fsalAvailabilityError: string = null;
+
+  storageBackend: SUPPORTED_FSAL;
+  storageBackendError: string = null;
 
   defaultAccessType = { RGW: 'RO' };
   nfsAccessType: any[] = [];
@@ -87,25 +89,27 @@ export class NfsFormComponent extends CdForm implements OnInit {
     private rgwSiteService: RgwSiteService,
     private formBuilder: CdFormBuilder,
     private taskWrapper: TaskWrapperService,
-    private cdRef: ChangeDetectorRef,
     public actionLabels: ActionLabelsI18n
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().pool;
     this.resource = $localize`NFS export`;
+    this.storageBackend = getFsalFromRoute(this.router.url);
   }
 
   ngOnInit() {
     this.nfsAccessType = this.nfsService.nfsAccessType;
     this.nfsSquash = Object.keys(this.nfsService.nfsSquash);
     this.createForm();
-    const promises: Observable<any>[] = [
-      this.nfsService.listClusters(),
-      this.nfsService.fsals(),
-      this.nfsService.filesystems()
-    ];
+    const promises: Observable<any>[] = [this.nfsService.listClusters()];
 
-    if (this.router.url.startsWith('/nfs/edit')) {
+    if (this.storageBackend === 'RGW') {
+      promises.push(this.rgwSiteService.get('realms'));
+    } else {
+      promises.push(this.nfsService.filesystems());
+    }
+
+    if (this.router.url.startsWith(`/${getPathfromFsal(this.storageBackend)}/nfs/edit`)) {
       this.isEdit = true;
     }
 
@@ -115,7 +119,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
         this.cluster_id = decodeURIComponent(params.cluster_id);
         this.export_id = decodeURIComponent(params.export_id);
         promises.push(this.nfsService.get(this.cluster_id, this.export_id));
-
         this.getData(promises);
       });
       this.nfsForm.get('cluster_id').disable();
@@ -129,11 +132,9 @@ export class NfsFormComponent extends CdForm implements OnInit {
     forkJoin(promises).subscribe((data: any[]) => {
       this.resolveClusters(data[0]);
       this.resolveFsals(data[1]);
-      this.resolveFilesystems(data[2]);
-      if (data[3]) {
-        this.resolveModel(data[3]);
+      if (data[2]) {
+        this.resolveModel(data[2]);
       }
-
       this.loadingReady();
     });
   }
@@ -144,7 +145,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
         validators: [Validators.required]
       }),
       fsal: new CdFormGroup({
-        name: new UntypedFormControl('', {
+        name: new UntypedFormControl(this.storageBackend, {
           validators: [Validators.required]
         }),
         fs_name: new UntypedFormControl('', {
@@ -155,7 +156,9 @@ export class NfsFormComponent extends CdForm implements OnInit {
           ]
         })
       }),
-      path: new UntypedFormControl('/'),
+      path: new UntypedFormControl('/', {
+        validators: [Validators.required]
+      }),
       protocolNfsv3: new UntypedFormControl(true, {
         validators: [
           CdValidators.requiredIf({ protocolNfsv4: false }, (value: boolean) => {
@@ -247,20 +250,16 @@ export class NfsFormComponent extends CdForm implements OnInit {
   }
 
   resolveFsals(res: string[]) {
-    res.forEach((fsal) => {
-      const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => {
-        return fsal === currentFsalItem.value;
-      });
-
-      if (_.isObjectLike(fsalItem)) {
-        this.allFsals.push(fsalItem);
-      }
-    });
-    if (!this.isEdit && this.allFsals.length > 0) {
+    if (this.storageBackend === 'RGW') {
+      this.setPathValidation();
+      this.resolveRealms(res);
+    } else {
+      this.resolveFilesystems(res);
+    }
+    if (!this.isEdit && this.storageBackend === SUPPORTED_FSAL.RGW) {
       this.nfsForm.patchValue({
-        fsal: {
-          name: this.allFsals[0].value
-        }
+        path: '',
+        access_type: this.defaultAccessType[SUPPORTED_FSAL.RGW]
       });
     }
   }
@@ -276,58 +275,26 @@ export class NfsFormComponent extends CdForm implements OnInit {
     }
   }
 
-  fsalChangeHandler() {
-    this.setPathValidation();
-    const fsalValue = this.nfsForm.getValue('name');
-    const checkAvailability =
-      fsalValue === 'RGW'
-        ? this.rgwSiteService.get('realms').pipe(
-            mergeMap((realms: string[]) =>
-              realms.length === 0
-                ? of(true)
-                : this.rgwSiteService.isDefaultRealm().pipe(
-                    mergeMap((isDefaultRealm) => {
-                      if (!isDefaultRealm) {
-                        throw new Error('Selected realm is not the default.');
-                      }
-                      return of(true);
-                    })
-                  )
-            )
-          )
-        : this.nfsService.filesystems();
-
-    checkAvailability.subscribe({
-      next: () => {
-        this.setFsalAvailability(fsalValue, true);
-        if (!this.isEdit) {
-          this.nfsForm.patchValue({
-            path: fsalValue === 'RGW' ? '' : '/',
-            pseudo: this.generatePseudo(),
-            access_type: this.updateAccessType()
-          });
-        }
-
-        this.cdRef.detectChanges();
-      },
-      error: (error) => {
-        this.setFsalAvailability(fsalValue, false, error);
-        this.nfsForm.get('name').setValue('');
-      }
-    });
-  }
-
-  private setFsalAvailability(fsalValue: string, available: boolean, errorMessage: string = '') {
-    this.allFsals = this.allFsals.map((fsalItem: NfsFSAbstractionLayer) => {
-      if (fsalItem.value === fsalValue) {
-        fsalItem.disabled = !available;
-
-        this.fsalAvailabilityError = fsalItem.disabled
-          ? $localize`${fsalItem.descr} backend is not available. ${errorMessage}`
-          : null;
-      }
-      return fsalItem;
-    });
+  resolveRealms(realms: string[]) {
+    if (realms.length !== 0) {
+      this.rgwSiteService
+        .isDefaultRealm()
+        .pipe(
+          mergeMap((isDefaultRealm) => {
+            if (!isDefaultRealm) {
+              throw new Error('Selected realm is not the default.');
+            }
+            return of(true);
+          })
+        )
+        .subscribe({
+          error: (error) => {
+            const fsalDescr = this.nfsService.nfsFsal.find((f) => f.value === this.storageBackend)
+              .descr;
+            this.storageBackendError = $localize`${fsalDescr} backend is not available. ${error}`;
+          }
+        });
+    }
   }
 
   accessTypeChangeHandler() {
@@ -338,8 +305,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
   setPathValidation() {
     const path = this.nfsForm.get('path');
-    path.setValidators([Validators.required]);
-    if (this.nfsForm.getValue('name') === 'RGW') {
+    if (this.storageBackend === SUPPORTED_FSAL.RGW) {
       path.setAsyncValidators([CdValidators.bucketExistence(true, this.rgwBucketService)]);
     } else {
       path.setAsyncValidators([this.pathExistence(true)]);
@@ -410,7 +376,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
     let newPseudo = this.nfsForm.getValue('pseudo');
     if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
       newPseudo = undefined;
-      if (this.nfsForm.getValue('fsal') === 'CEPH') {
+      if (this.storageBackend === 'CEPH') {
         newPseudo = '/cephfs';
         if (_.isString(this.nfsForm.getValue('path'))) {
           newPseudo += this.nfsForm.getValue('path');
@@ -420,17 +386,6 @@ export class NfsFormComponent extends CdForm implements OnInit {
     return newPseudo;
   }
 
-  private updateAccessType() {
-    const name = this.nfsForm.getValue('name');
-    let accessType = this.defaultAccessType[name];
-
-    if (!accessType) {
-      accessType = 'RW';
-    }
-
-    return accessType;
-  }
-
   submitAction() {
     let action: Observable<any>;
     const requestModel = this.buildRequest();
@@ -457,7 +412,7 @@ export class NfsFormComponent extends CdForm implements OnInit {
 
     action.subscribe({
       error: (errorResponse: CdHttpErrorResponse) => this.setFormErrors(errorResponse),
-      complete: () => this.router.navigate(['/nfs'])
+      complete: () => this.router.navigate([`/${getPathfromFsal(this.storageBackend)}/nfs`])
     });
   }
 
index 5e43cdd658cb45d71a598751fc454bceecb61fea..1e82919f4029b25e4bb337b8a6c964903e3f5163 100644 (file)
@@ -17,6 +17,7 @@ import { SharedModule } from '~/app/shared/shared.module';
 import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
 import { NfsDetailsComponent } from '../nfs-details/nfs-details.component';
 import { NfsListComponent } from './nfs-list.component';
+import { SUPPORTED_FSAL } from '../models/nfs.fsal';
 
 describe('NfsListComponent', () => {
   let component: NfsListComponent;
@@ -45,6 +46,7 @@ describe('NfsListComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(NfsListComponent);
     component = fixture.componentInstance;
+    component.fsal = SUPPORTED_FSAL.CEPH;
     summaryService = TestBed.inject(SummaryService);
     nfsService = TestBed.inject(NfsService);
     httpTesting = TestBed.inject(HttpTestingController);
@@ -89,7 +91,9 @@ describe('NfsListComponent', () => {
       const model = {
         export_id: export_id,
         path: 'path_' + export_id,
-        fsal: 'fsal_' + export_id,
+        fsal: {
+          name: 'CEPH'
+        },
         cluster_id: 'cluster_' + export_id
       };
       exports.push(model);
@@ -102,7 +106,9 @@ describe('NfsListComponent', () => {
         case 'nfs/create':
           task.metadata = {
             path: 'path_' + export_id,
-            fsal: 'fsal_' + export_id,
+            fsal: {
+              name: 'CEPH'
+            },
             cluster_id: 'cluster_' + export_id
           };
           break;
index d5d0c2639300c322820a59b3c539c97942828aa2..8be95c6febe7991098e36305279f2be6e77e427c 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
 
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
@@ -22,6 +23,8 @@ import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { TaskListService } from '~/app/shared/services/task-list.service';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { getFsalFromRoute, getPathfromFsal } from '../utils';
+import { SUPPORTED_FSAL } from '../models/nfs.fsal';
 
 @Component({
   selector: 'cd-nfs-list',
@@ -46,6 +49,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
   exports: any[];
   tableActions: CdTableAction[];
   isDefaultCluster = false;
+  fsal: SUPPORTED_FSAL;
 
   modalRef: NgbModalRef;
 
@@ -65,10 +69,13 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
     private nfsService: NfsService,
     private taskListService: TaskListService,
     private taskWrapper: TaskWrapperService,
+    private router: Router,
     public actionLabels: ActionLabelsI18n
   ) {
     super();
     this.permission = this.authStorageService.getPermissions().nfs;
+    this.fsal = getFsalFromRoute(this.router.url);
+    const prefix = getPathfromFsal(this.fsal);
     const getNfsUri = () =>
       this.selection.first() &&
       `${encodeURI(this.selection.first().cluster_id)}/${encodeURI(
@@ -78,7 +85,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
     const createAction: CdTableAction = {
       permission: 'create',
       icon: Icons.add,
-      routerLink: () => '/nfs/create',
+      routerLink: () => `/${prefix}/nfs/create`,
       canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
       name: this.actionLabels.CREATE
     };
@@ -86,7 +93,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
     const editAction: CdTableAction = {
       permission: 'update',
       icon: Icons.edit,
-      routerLink: () => `/nfs/edit/${getNfsUri()}`,
+      routerLink: () => `/${prefix}/nfs/edit/${getNfsUri()}`,
       name: this.actionLabels.EDIT
     };
 
@@ -103,7 +110,7 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
   ngOnInit() {
     this.columns = [
       {
-        name: $localize`Path`,
+        name: this.fsal === SUPPORTED_FSAL.CEPH ? $localize`Path` : $localize`Bucket`,
         prop: 'path',
         flexGrow: 2,
         cellTransformation: CellTemplate.executing
@@ -150,12 +157,12 @@ export class NfsListComponent extends ListWithDetails implements OnInit, OnDestr
 
   prepareResponse(resp: any): any[] {
     let result: any[] = [];
-    resp.forEach((nfs: any) => {
+    const filteredresp = resp.filter((nfs: any) => nfs.fsal?.name === this.fsal);
+    filteredresp.forEach((nfs: any) => {
       nfs.id = `${nfs.cluster_id}:${nfs.export_id}`;
       nfs.state = 'LOADING';
       result = result.concat(nfs);
     });
-
     return result;
   }
 
index 4205eb63b26e40f91f68b729a4a181eff9d43124..afd52472c54da7a307bbd884749bf37dd0a7f6c3 100644 (file)
@@ -21,6 +21,7 @@ import { NfsListComponent } from './nfs-list/nfs-list.component';
     NgbTypeaheadModule,
     NgbTooltipModule
   ],
+  exports: [NfsListComponent, NfsFormComponent, NfsDetailsComponent],
   declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
 })
 export class NfsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/utils.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/utils.ts
new file mode 100644 (file)
index 0000000..2cdd7bb
--- /dev/null
@@ -0,0 +1,7 @@
+import { SUPPORTED_FSAL } from './models/nfs.fsal';
+
+export const getFsalFromRoute = (url: string): SUPPORTED_FSAL =>
+  url.startsWith('/rgw/nfs') ? SUPPORTED_FSAL.RGW : SUPPORTED_FSAL.CEPH;
+
+export const getPathfromFsal = (fsal: SUPPORTED_FSAL): string =>
+  fsal === SUPPORTED_FSAL.CEPH ? 'cephfs' : 'rgw';
index 248a59292d7257eea117354f99e7adf961a28388..d893bd688273c1b145c58ef72540d59d67f5fd4c 100644 (file)
@@ -8,6 +8,8 @@ import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
 import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
 import { CRUDTableComponent } from '~/app/shared/datatable/crud-table/crud-table.component';
+import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
+import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
 
 import { SharedModule } from '~/app/shared/shared.module';
 import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
@@ -45,6 +47,8 @@ import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone/rgw-sync-pr
 import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info/rgw-sync-metadata-info.component';
 import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-info.component';
 import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.component';
+import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component';
+import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component';
 
 @NgModule({
   imports: [
@@ -194,6 +198,33 @@ const routes: Routes = [
     path: 'multisite',
     data: { breadcrumbs: 'Multi-site' },
     children: [{ path: '', component: RgwMultisiteDetailsComponent }]
+  },
+  {
+    path: 'nfs',
+    canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+    data: {
+      moduleStatusGuardConfig: {
+        uiApiPath: 'nfs-ganesha',
+        redirectTo: 'error',
+        section: 'nfs-ganesha',
+        section_info: 'NFS GANESHA',
+        header: 'NFS-Ganesha is not configured'
+      },
+      breadcrumbs: 'NFS'
+    },
+    children: [
+      { path: '', component: NfsListComponent },
+      {
+        path: URLVerbs.CREATE,
+        component: NfsFormComponent,
+        data: { breadcrumbs: ActionLabels.CREATE }
+      },
+      {
+        path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+        component: NfsFormComponent,
+        data: { breadcrumbs: ActionLabels.EDIT }
+      }
+    ]
   }
 ];
 
index a4fb9ab24166ee0ce0f8b10002805b287a0c5a26..085733c20dbcb800a05ed469fc6dd1aae2819eb2 100644 (file)
 <div class="cd-navbar-main">
-<!-- ************************ -->
-<!-- NOTIFICATIONS     -->
-<!-- ************************ -->
-<cd-notifications-sidebar></cd-notifications-sidebar>
-<!-- ************************ -->
-<!-- HEADER                   -->
-<!-- ************************ -->
-<cds-header name="Ceph Dashboard"
-            class="cd-navbar-top"
-            [brand]="brandTemplate">
-  <cds-hamburger [active]="showMenuSidebar"
-                 data-testid="main-menu-toggler"
-                 (selected)="showMenuSidebar = !showMenuSidebar"></cds-hamburger>
-  <cds-header-global>
-    <cds-header-navigation>
-      <cd-language-selector class="d-flex"></cd-language-selector>
-    </cds-header-navigation>
-    <div class="cds--btn cds--btn--icon-only cds--header__action">
-      <cd-notifications (click)="toggleRightSidebar()"></cd-notifications>
+  <!-- ************************ -->
+  <!-- NOTIFICATIONS     -->
+  <!-- ************************ -->
+  <cd-notifications-sidebar></cd-notifications-sidebar>
+  <!-- ************************ -->
+  <!-- HEADER                   -->
+  <!-- ************************ -->
+  <cds-header name="Ceph Dashboard"
+              class="cd-navbar-top"
+              [brand]="brandTemplate">
+    <cds-hamburger [active]="showMenuSidebar"
+                   data-testid="main-menu-toggler"
+                   (selected)="showMenuSidebar = !showMenuSidebar"></cds-hamburger>
+    <cds-header-global>
+      <cds-header-navigation>
+        <cd-language-selector class="d-flex"></cd-language-selector>
+      </cds-header-navigation>
+      <div class="cds--btn cds--btn--icon-only cds--header__action">
+        <cd-notifications (click)="toggleRightSidebar()"></cd-notifications>
+      </div>
+      <div class="cds--btn cds--btn--icon-only cds--header__action">
+        <cd-dashboard-help></cd-dashboard-help>
+      </div>
+      <div class="cds--btn cds--btn--icon-only cds--header__action">
+        <cd-administration></cd-administration>
+      </div>
+      <div class="cds--btn cds--btn--icon-only cds--header__action">
+        <cd-identity></cd-identity>
+      </div>
+    </cds-header-global>
+  </cds-header>
+  <!-- ***************************** -->
+  <!-- LOGO BRAND TEMPLATE  -->
+  <!-- ***************************** -->
+  <ng-template #brandTemplate>
+    <a class="cds--header__name navbar-brand ms-3"
+       routerLink="/dashboard">
+      <img src="assets/Ceph_Ceph_Logo_with_text_white.svg"
+           alt="Ceph" />
+    </a>
+  </ng-template>
+  <!-- **************************************** -->
+  <!-- WRAPPER AROUND SIDENAV AND MAIN CONTAINT -->
+  <!-- **************************************** -->
+  <div class="wrapper">
+    <!-- Content -->
+    <nav id="sidebar"
+         [ngClass]="{'active': !showMenuSidebar}">
+      <ng-container *ngTemplateOutlet="cd_menu"></ng-container>
+    </nav>
+    <!-- Page Content -->
+    <div id="content"
+         [ngClass]="{'active': !showMenuSidebar}">
+      <ng-content></ng-content>
     </div>
-    <div class="cds--btn cds--btn--icon-only cds--header__action">
-      <cd-dashboard-help></cd-dashboard-help>
-    </div>
-    <div class="cds--btn cds--btn--icon-only cds--header__action">
-      <cd-administration></cd-administration>
-    </div>
-    <div class="cds--btn cds--btn--icon-only cds--header__action">
-      <cd-identity></cd-identity>
-    </div>
-  </cds-header-global>
-</cds-header>
-<!-- ***************************** -->
-<!-- LOGO BRAND TEMPLATE  -->
-<!-- ***************************** -->
-<ng-template #brandTemplate>
-  <a class="cds--header__name navbar-brand ms-3"
-     routerLink="/dashboard">
-    <img src="assets/Ceph_Ceph_Logo_with_text_white.svg"
-         alt="Ceph" />
-  </a>
-</ng-template>
-<!-- **************************************** -->
-<!-- WRAPPER AROUND SIDENAV AND MAIN CONTAINT -->
-<!-- **************************************** -->
-<div class="wrapper">
-  <!-- Content -->
-  <nav id="sidebar"
-       [ngClass]="{'active': !showMenuSidebar}">
-    <ng-container *ngTemplateOutlet="cd_menu"></ng-container>
-  </nav>
-  <!-- Page Content -->
-  <div id="content"
-       [ngClass]="{'active': !showMenuSidebar}">
-    <ng-content></ng-content>
   </div>
-</div>
-<!-- ************************ -->
-<!-- SIDENAV                  -->
-<!-- ************************ -->
-<ng-template #cd_menu>
-  <ng-container *ngIf="enabledFeature$ | async as enabledFeature">
-  <div cdsTheme="theme">
-    <cds-sidenav [expanded]="showMenuSidebar"
-                 class="mt-5">
-      <!-- Dashboard -->
-      <cds-sidenav-item route="/dashboard"
-                        [useRouter]="true"
-                        title="Dashboard"
-                        i18n-title
-                        class="nav-item tc_menuitem_dashboard">
-        <svg cdsIcon="template"
-             icon
-             size="20"></svg>
-        <span i18n>
-          Dashboard</span>
-      </cds-sidenav-item>
-      <!-- Multi-cluster Dashboard -->
-      <cds-sidenav-menu title="Multi-Cluster"
-                        i18n-title>
-        <svg cdsIcon="edge-cluster"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/multi-cluster/overview"
-                          title="Overview"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_multiCluster_overview"><span i18n>Overview</span></cds-sidenav-item>
-        <cds-sidenav-item route="/multi-cluster/manage-clusters"
-                          title="Manager Cluster"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_multiCluster_manage_clusters"><span i18n>Manager Cluster</span></cds-sidenav-item>
-      </cds-sidenav-menu>
-      <!-- Cluster -->
-      <cds-sidenav-menu title="Cluster"
-                        i18n-title
-                        *ngIf="permissions.hosts.read || permissions.monitor.read || permissions.osd.read || permissions.pool.read"
-                        class="tc_menuitem_cluster">
-        <svg cdsIcon="web-services--cluster"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/pool"
-                          [useRouter]="true"
-                          title="Pools"
-                          i18n-title
-                          *ngIf="permissions.pool.read"
-                          class="tc_submenuitem tc_submenuitem_cluster_pool"><span i18n>Pools</span></cds-sidenav-item>
-        <cds-sidenav-item route="/hosts"
-                          [useRouter]="true"
-                          title="Hosts"
-                          i18n-title
-                          *ngIf="permissions.hosts.read"
-                          class="tc_submenuitem tc_submenuitem_cluster_hosts"><span i18n>Hosts</span></cds-sidenav-item>
-        <cds-sidenav-item route="/osd"
-                          [useRouter]="true"
-                          title="OSDs"
-                          i18n-title
-                          *ngIf="permissions.osd.read"
-                          class="tc_submenuitem tc_submenuitem_cluster_osds"><span i18n>OSDs</span></cds-sidenav-item>
-        <cds-sidenav-item route="/inventory"
+  <!-- ************************ -->
+  <!-- SIDENAV                  -->
+  <!-- ************************ -->
+  <ng-template #cd_menu>
+    <ng-container *ngIf="enabledFeature$ | async as enabledFeature">
+    <div cdsTheme="theme">
+      <cds-sidenav [expanded]="showMenuSidebar"
+                   class="mt-5">
+        <!-- Dashboard -->
+        <cds-sidenav-item route="/dashboard"
                           [useRouter]="true"
-                          title="Physical Disks"
+                          title="Dashboard"
                           i18n-title
-                          *ngIf="permissions.hosts.read"
-                          class="tc_submenuitem tc_submenuitem_cluster_inventory"><span i18n>Physical Disks</span></cds-sidenav-item>
-        <cds-sidenav-item route="/crush-map"
-                          [useRouter]="true"
-                          title="CRUSH Map"
-                          i18n-title
-                          *ngIf="permissions.osd.read"
-                          class="tc_submenuitem tc_submenuitem_cluster_crush"><span i18n>CRUSH Map</span></cds-sidenav-item>
-        <cds-sidenav-item route="/monitor"
-                          [useRouter]="true"
-                          title="Monitors"
-                          i18n-title
-                          *ngIf="permissions.monitor.read"
-                          class="tc_submenuitem tc_submenuitem_cluster_monitor"><span i18n>Monitors</span></cds-sidenav-item>
-      </cds-sidenav-menu>
-      <!-- Block Storage -->
-      <cds-sidenav-menu title="Block"
-                        i18n-title
-                        *ngIf="(permissions.rbdImage.read || permissions.rbdMirroring.read|| permissions.iscsi.read) && (enabledFeature.rbd || enabledFeature.mirroring || enabledFeature.iscsi)"
-                        class="tc_menuitem_block">
-        <svg cdsIcon="datastore"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/block/rbd"
-                          [useRouter]="true"
-                          title="Images"
-                          i18n-title
-                          *ngIf="permissions.rbdImage.read && enabledFeature.rbd"
-                          class="tc_submenuitem tc_submenuitem_block_images"><span i18n>Images</span></cds-sidenav-item>
-        <cds-sidenav-item route="/block/mirroring"
-                          [useRouter]="true"
-                          title="Mirroring"
-                          i18n-title
-                          *ngIf="permissions.rbdMirroring.read && enabledFeature.mirroring"
-                          class="tc_submenuitem tc_submenuitem_block_mirroring">
-          <span i18n>Mirroring
-            <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
-                   class="badge badge-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
-            <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
-                   class="badge badge-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
-          </span>
-        </cds-sidenav-item>
-        <cds-sidenav-item route="/block/iscsi"
-                          [useRouter]="true"
-                          title="iSCSI"
-                          i18n-title
-                          *ngIf="permissions.iscsi.read && enabledFeature.iscsi"
-                          class="tc_submenuitem tc_submenuitem_block_iscsi"><span i18n>iSCSI</span></cds-sidenav-item>
-      </cds-sidenav-menu>
-      <!-- Object Storage -->
-      <cds-sidenav-menu title="Object"
-                        i18n-title
-                        *ngIf="permissions.rgw.read && enabledFeature.rgw"
-                        class="nav-item tc_menuitem_rgw">
-        <svg cdsIcon="object-storage"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/rgw/overview"
-                          title="Overview"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_rgw_overview"><span i18n>Overview</span></cds-sidenav-item>
-        <cds-sidenav-item route="/rgw/bucket"
-                          title="Buckets"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_rgw_buckets"><span i18n>Buckets</span></cds-sidenav-item>
-        <cds-sidenav-item route="/rgw/user"
-                          title="Users"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_rgw_users"><span i18n>Users</span></cds-sidenav-item>
-        <cds-sidenav-item route="/rgw/multisite"
-                          title="Multi-site"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_rgw_buckets"><span i18n>Multi-site</span></cds-sidenav-item>
-        <cds-sidenav-item route="/rgw/daemon"
-                          title="Gateways"
-                          i18n-title
-                          [useRouter]="true"
-                          class="tc_submenuitem tc_submenuitem_rgw_daemons"><span i18n>Gateways</span></cds-sidenav-item>
-        <cds-sidenav-item route="/nfs"
-                          [useRouter]="true"
-                          title="NFS"
-                          i18n-title
-                          *ngIf="permissions.nfs.read && enabledFeature.nfs"
-                          class="tc_submenuitem tc_submenuitem_rgw_nfs"><span i18n>NFS</span></cds-sidenav-item>
-      </cds-sidenav-menu>
-      <!-- Filesystem -->
-      <cds-sidenav-menu title="File"
-                        i18n-title
-                        *ngIf="permissions.nfs.read && enabledFeature.nfs || permissions.cephfs.read && enabledFeature.cephfs"
-                        class="tc_menuitem_file">
-        <svg cdsIcon="file-storage"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/cephfs"
-                          [useRouter]="true"
-                          title="File Systems"
-                          i18n-title
-                          *ngIf="permissions.cephfs.read && enabledFeature.cephfs"
-                          class="tc_submenuitem tc_submenuitem_file_cephfs"><span i18n>File Systems</span></cds-sidenav-item>
-        <cds-sidenav-item route="/nfs"
-                          [useRouter]="true"
-                          title="NFS"
-                          i18n-title
-                          *ngIf="permissions.nfs.read && enabledFeature.nfs"
-                          class="tc_submenuitem tc_submenuitem_file_nfs"><span i18n>NFS</span></cds-sidenav-item>
-      </cds-sidenav-menu>
-      <!-- Observability -->
-      <cds-sidenav-menu title="Observability"
-                        i18n-title
-                        *ngIf="permissions.log.read || permissions.prometheus.read"
-                        class="tc_menuitem_observe">
-        <svg cdsIcon="observed--hail"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/logs"
-                          [useRouter]="true"
-                          title="Logs"
-                          i18n-title
-                          *ngIf="permissions.log.read"
-                          class="tc_submenuitem tc_submenuitem_observe_log"><span i18n>Logs</span></cds-sidenav-item>
-        <cds-sidenav-item route="/monitoring"
-                          [useRouter]="true"
-                          title="Alerts"
-                          i18n-title
-                          *ngIf="permissions.prometheus.read"
-                          class="tc_submenuitem tc_submenuitem_observe_monitoring">
+                          class="nav-item tc_menuitem_dashboard">
+          <svg cdsIcon="template"
+               icon
+               size="20"></svg>
           <span i18n>
-            <ng-container>Alerts</ng-container>
-            <small *ngIf="prometheusAlertService.activeCriticalAlerts > 0"
-                   class="badge badge-danger ms-1">{{ prometheusAlertService.activeCriticalAlerts }}</small>
-            <small *ngIf="prometheusAlertService.activeWarningAlerts > 0"
-                   class="badge badge-warning ms-1">{{ prometheusAlertService.activeWarningAlerts }}</small>
-          </span>
+            Dashboard</span>
         </cds-sidenav-item>
-      </cds-sidenav-menu>
-      <!-- Administration -->
-      <cds-sidenav-menu title="Administration"
-                        i18n-title
-                        *ngIf="permissions.configOpt.read || permissions.hosts.read"
-                        class="tc_menuitem_admin">
-        <svg cdsIcon="network--admin-control"
-             icon
-             size="20"></svg>
-        <cds-sidenav-item route="/services/"
-                          [useRouter]="true"
-                          title="Services"
+        <!-- Multi-cluster Dashboard -->
+        <cds-sidenav-menu title="Multi-Cluster"
+                          i18n-title>
+          <svg cdsIcon="edge-cluster"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/multi-cluster/overview"
+                            title="Overview"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_multiCluster_overview"><span i18n>Overview</span></cds-sidenav-item>
+          <cds-sidenav-item route="/multi-cluster/manage-clusters"
+                            title="Manager Cluster"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_multiCluster_manage_clusters"><span i18n>Manager Cluster</span></cds-sidenav-item>
+        </cds-sidenav-menu>
+        <!-- Cluster -->
+        <cds-sidenav-menu title="Cluster"
                           i18n-title
-                          *ngIf="permissions.hosts.read"
-                          class="tc_submenuitem tc_submenuitem_admin_services"><span i18n>Services</span></cds-sidenav-item>
-        <cds-sidenav-item route="/upgrade"
-                          [useRouter]="true"
-                          title="Upgrade"
+                          *ngIf="permissions.hosts.read || permissions.monitor.read || permissions.osd.read || permissions.pool.read"
+                          class="tc_menuitem_cluster">
+          <svg cdsIcon="web-services--cluster"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/pool"
+                            [useRouter]="true"
+                            title="Pools"
+                            i18n-title
+                            *ngIf="permissions.pool.read"
+                            class="tc_submenuitem tc_submenuitem_cluster_pool"><span i18n>Pools</span></cds-sidenav-item>
+          <cds-sidenav-item route="/hosts"
+                            [useRouter]="true"
+                            title="Hosts"
+                            i18n-title
+                            *ngIf="permissions.hosts.read"
+                            class="tc_submenuitem tc_submenuitem_cluster_hosts"><span i18n>Hosts</span></cds-sidenav-item>
+          <cds-sidenav-item route="/osd"
+                            [useRouter]="true"
+                            title="OSDs"
+                            i18n-title
+                            *ngIf="permissions.osd.read"
+                            class="tc_submenuitem tc_submenuitem_cluster_osds"><span i18n>OSDs</span></cds-sidenav-item>
+          <cds-sidenav-item route="/inventory"
+                            [useRouter]="true"
+                            title="Physical Disks"
+                            i18n-title
+                            *ngIf="permissions.hosts.read"
+                            class="tc_submenuitem tc_submenuitem_cluster_inventory"><span i18n>Physical Disks</span></cds-sidenav-item>
+          <cds-sidenav-item route="/crush-map"
+                            [useRouter]="true"
+                            title="CRUSH Map"
+                            i18n-title
+                            *ngIf="permissions.osd.read"
+                            class="tc_submenuitem tc_submenuitem_cluster_crush"><span i18n>CRUSH Map</span></cds-sidenav-item>
+          <cds-sidenav-item route="/monitor"
+                            [useRouter]="true"
+                            title="Monitors"
+                            i18n-title
+                            *ngIf="permissions.monitor.read"
+                            class="tc_submenuitem tc_submenuitem_cluster_monitor"><span i18n>Monitors</span></cds-sidenav-item>
+        </cds-sidenav-menu>
+        <!-- Block Storage -->
+        <cds-sidenav-menu title="Block"
                           i18n-title
-                          *ngIf="permissions.configOpt.read"
-                          class="tc_submenuitem tc_submenuitem_admin_upgrade"><span i18n>Upgrade</span></cds-sidenav-item>
-        <cds-sidenav-item route="/ceph-users"
-                          [useRouter]="true"
-                          title="Ceph Users"
+                          *ngIf="(permissions.rbdImage.read || permissions.rbdMirroring.read|| permissions.iscsi.read) && (enabledFeature.rbd || enabledFeature.mirroring || enabledFeature.iscsi)"
+                          class="tc_menuitem_block">
+          <svg cdsIcon="datastore"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/block/rbd"
+                            [useRouter]="true"
+                            title="Images"
+                            i18n-title
+                            *ngIf="permissions.rbdImage.read && enabledFeature.rbd"
+                            class="tc_submenuitem tc_submenuitem_block_images"><span i18n>Images</span></cds-sidenav-item>
+          <cds-sidenav-item route="/block/mirroring"
+                            [useRouter]="true"
+                            title="Mirroring"
+                            i18n-title
+                            *ngIf="permissions.rbdMirroring.read && enabledFeature.mirroring"
+                            class="tc_submenuitem tc_submenuitem_block_mirroring">
+            <span i18n>Mirroring
+              <small *ngIf="summaryData?.rbd_mirroring?.warnings !== 0"
+                     class="badge badge-warning">{{ summaryData?.rbd_mirroring?.warnings }}</small>
+              <small *ngIf="summaryData?.rbd_mirroring?.errors !== 0"
+                     class="badge badge-danger">{{ summaryData?.rbd_mirroring?.errors }}</small>
+            </span>
+          </cds-sidenav-item>
+          <cds-sidenav-item route="/block/iscsi"
+                            [useRouter]="true"
+                            title="iSCSI"
+                            i18n-title
+                            *ngIf="permissions.iscsi.read && enabledFeature.iscsi"
+                            class="tc_submenuitem tc_submenuitem_block_iscsi"><span i18n>iSCSI</span></cds-sidenav-item>
+        </cds-sidenav-menu>
+        <!-- Object Storage -->
+        <cds-sidenav-menu title="Object"
                           i18n-title
-                          *ngIf="permissions.configOpt.read"
-                          class="tc_submenuitem tc_submenuitem_admin_users"><span i18n>Ceph Users</span></cds-sidenav-item>
-        <cds-sidenav-item route="/mgr-modules"
-                          [useRouter]="true"
-                          title="Manager Modules"
+                          *ngIf="permissions.rgw.read && enabledFeature.rgw"
+                          class="nav-item tc_menuitem_rgw">
+          <svg cdsIcon="object-storage"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/rgw/overview"
+                            title="Overview"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_rgw_overview"><span i18n>Overview</span></cds-sidenav-item>
+          <cds-sidenav-item route="/rgw/bucket"
+                            title="Buckets"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_rgw_buckets"><span i18n>Buckets</span></cds-sidenav-item>
+          <cds-sidenav-item route="/rgw/user"
+                            title="Users"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_rgw_users"><span i18n>Users</span></cds-sidenav-item>
+          <cds-sidenav-item route="/rgw/multisite"
+                            title="Multi-site"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_rgw_buckets"><span i18n>Multi-site</span></cds-sidenav-item>
+          <cds-sidenav-item route="/rgw/daemon"
+                            title="Gateways"
+                            i18n-title
+                            [useRouter]="true"
+                            class="tc_submenuitem tc_submenuitem_rgw_daemons"><span i18n>Gateways</span></cds-sidenav-item>
+          <cds-sidenav-item route="/nfs"
+                            [useRouter]="true"
+                            title="NFS"
+                            i18n-title
+                            *ngIf="permissions.nfs.read && enabledFeature.nfs"
+                            class="tc_submenuitem tc_submenuitem_rgw_nfs"><span i18n>NFS</span></cds-sidenav-item>
+        </cds-sidenav-menu>
+        <!-- Filesystem -->
+        <cds-sidenav-menu title="File"
                           i18n-title
-                          *ngIf="permissions.configOpt.read"
-                          class="tc_submenuitem tc_submenuitem_admin_modules"><span i18n>Manager Modules</span></cds-sidenav-item>
-        <cds-sidenav-item route="/configuration"
-                          [useRouter]="true"
-                          title="Configuration"
+                          *ngIf="permissions.nfs.read && enabledFeature.nfs || permissions.cephfs.read && enabledFeature.cephfs"
+                          class="tc_menuitem_file">
+          <svg cdsIcon="file-storage"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/cephfs"
+                            [useRouter]="true"
+                            title="File Systems"
+                            i18n-title
+                            *ngIf="permissions.cephfs.read && enabledFeature.cephfs"
+                            class="tc_submenuitem tc_submenuitem_file_cephfs"><span i18n>File Systems</span></cds-sidenav-item>
+          <cds-sidenav-item route="/nfs"
+                            [useRouter]="true"
+                            title="NFS"
+                            i18n-title
+                            *ngIf="permissions.nfs.read && enabledFeature.nfs"
+                            class="tc_submenuitem tc_submenuitem_file_nfs"><span i18n>NFS</span></cds-sidenav-item>
+        </cds-sidenav-menu>
+        <!-- Observability -->
+        <cds-sidenav-menu title="Observability"
+                          i18n-title
+                          *ngIf="permissions.log.read || permissions.prometheus.read"
+                          class="tc_menuitem_observe">
+          <svg cdsIcon="observed--hail"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/logs"
+                            [useRouter]="true"
+                            title="Logs"
+                            i18n-title
+                            *ngIf="permissions.log.read"
+                            class="tc_submenuitem tc_submenuitem_observe_log"><span i18n>Logs</span></cds-sidenav-item>
+          <cds-sidenav-item route="/monitoring"
+                            [useRouter]="true"
+                            title="Alerts"
+                            i18n-title
+                            *ngIf="permissions.prometheus.read"
+                            class="tc_submenuitem tc_submenuitem_observe_monitoring">
+            <span i18n>
+              <ng-container>Alerts</ng-container>
+              <small *ngIf="prometheusAlertService.activeCriticalAlerts > 0"
+                     class="badge badge-danger ms-1">{{ prometheusAlertService.activeCriticalAlerts }}</small>
+              <small *ngIf="prometheusAlertService.activeWarningAlerts > 0"
+                     class="badge badge-warning ms-1">{{ prometheusAlertService.activeWarningAlerts }}</small>
+            </span>
+          </cds-sidenav-item>
+        </cds-sidenav-menu>
+        <!-- Administration -->
+        <cds-sidenav-menu title="Administration"
                           i18n-title
-                          *ngIf="permissions.configOpt.read"
-                          class="tc_submenuitem tc_submenuitem_admin_configuration"><span i18n>Configuration</span></cds-sidenav-item>
-      </cds-sidenav-menu>
-    </cds-sidenav>
+                          *ngIf="permissions.configOpt.read || permissions.hosts.read"
+                          class="tc_menuitem_admin">
+          <svg cdsIcon="network--admin-control"
+               icon
+               size="20"></svg>
+          <cds-sidenav-item route="/services/"
+                            [useRouter]="true"
+                            title="Services"
+                            i18n-title
+                            *ngIf="permissions.hosts.read"
+                            class="tc_submenuitem tc_submenuitem_admin_services"><span i18n>Services</span></cds-sidenav-item>
+          <cds-sidenav-item route="/upgrade"
+                            [useRouter]="true"
+                            title="Upgrade"
+                            i18n-title
+                            *ngIf="permissions.configOpt.read"
+                            class="tc_submenuitem tc_submenuitem_admin_upgrade"><span i18n>Upgrade</span></cds-sidenav-item>
+          <cds-sidenav-item route="/ceph-users"
+                            [useRouter]="true"
+                            title="Ceph Users"
+                            i18n-title
+                            *ngIf="permissions.configOpt.read"
+                            class="tc_submenuitem tc_submenuitem_admin_users"><span i18n>Ceph Users</span></cds-sidenav-item>
+          <cds-sidenav-item route="/mgr-modules"
+                            [useRouter]="true"
+                            title="Manager Modules"
+                            i18n-title
+                            *ngIf="permissions.configOpt.read"
+                            class="tc_submenuitem tc_submenuitem_admin_modules"><span i18n>Manager Modules</span></cds-sidenav-item>
+          <cds-sidenav-item route="/configuration"
+                            [useRouter]="true"
+                            title="Configuration"
+                            i18n-title
+                            *ngIf="permissions.configOpt.read"
+                            class="tc_submenuitem tc_submenuitem_admin_configuration"><span i18n>Configuration</span></cds-sidenav-item>
+        </cds-sidenav-menu>
+      </cds-sidenav>
+    </div>
+    </ng-container>
+  </ng-template>
   </div>
-  </ng-container>
-</ng-template>
-</div>
index 9b4e4a0a288d06927e53b7e7e9027e37f5e22814..1fcce26e50a4ea9ab21f3024774c7a740394bc00 100644 (file)
@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
 
 import { Observable, throwError } from 'rxjs';
 
-import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { NfsFSAbstractionLayer, SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
 import { ApiClient } from '~/app/shared/api/api-client';
 
 export interface Directory {
@@ -34,12 +34,12 @@ export class NfsService extends ApiClient {
 
   nfsFsal: NfsFSAbstractionLayer[] = [
     {
-      value: 'CEPH',
+      value: SUPPORTED_FSAL.CEPH,
       descr: $localize`CephFS`,
       disabled: false
     },
     {
-      value: 'RGW',
+      value: SUPPORTED_FSAL.RGW,
       descr: $localize`Object Gateway`,
       disabled: false
     }