]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: rbd pagination poc
authorPere Diaz Bou <pdiazbou@redhat.com>
Mon, 13 Jun 2022 15:15:45 +0000 (17:15 +0200)
committerPere Diaz Bou <pdiazbou@redhat.com>
Tue, 12 Jul 2022 17:09:02 +0000 (19:09 +0200)
Signed-off-by: Pere Diaz Bou <pdiazbou@redhat.com>
14 files changed:
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.model.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-user-config.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/services/rbd.py

index 3f362774b02073f65d48808b37ba960fc093facd..99c48e21b888e7cea24216cda63ee69043761fd7 100644 (file)
@@ -4,6 +4,7 @@
 
 import logging
 import math
+import cherrypy
 from datetime import datetime
 from functools import partial
 
@@ -78,31 +79,37 @@ class Rbd(RESTController):
     ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten",
                               "journaling"}
 
-    def _rbd_list(self, pool_name=None):
+    def _rbd_list(self, pool_name=None, offset=0, limit=5):
         if pool_name:
             pools = [pool_name]
         else:
             pools = [p['pool_name'] for p in CephService.get_pool_list('rbd')]
 
         result = []
-        for pool in pools:
-            # pylint: disable=unbalanced-tuple-unpacking
-            status, value = RbdService.rbd_pool_list(pool)
-            for i, image in enumerate(value):
-                value[i]['configuration'] = RbdConfiguration(
-                    pool, image['namespace'], image['name']).list()
-            result.append({'status': status, 'value': value, 'pool_name': pool})
-        return result
+        images, num_total_images = RbdService.rbd_pool_list(pools, offset=offset, limit=limit)
+        cherrypy.response.headers['X-Total-Count'] = num_total_images
+        pool_result = {}
+        for i, image in enumerate(images):
+            pool = image['pool']
+            if pool not in pool_result:
+                pool_result[pool] = {'status': 1, 'value': [], 'pool_name': image['pool']}
+            pool_result[pool]['value'].append(image)
+                
+            images[i]['configuration'] = RbdConfiguration(
+                pool, image['namespace'], image['name']).list()
+        return list(pool_result.values())
 
     @handle_rbd_error()
     @handle_rados_error('pool')
     @EndpointDoc("Display Rbd Images",
                  parameters={
                      'pool_name': (str, 'Pool Name'),
+                     'limit': (int, 'limit'),
+                     'offset': (int, 'offset'),
                  },
                  responses={200: RBD_SCHEMA})
-    def list(self, pool_name=None):
-        return self._rbd_list(pool_name)
+    def list(self, pool_name=None, offset: int = 0, limit: int = 5):
+        return self._rbd_list(pool_name, offset=offset, limit=limit)
 
     @handle_rbd_error()
     @handle_rados_error('pool')
@@ -527,7 +534,7 @@ class RbdNamespace(RESTController):
     def delete(self, pool_name, namespace):
         with mgr.rados.open_ioctx(pool_name) as ioctx:
             # pylint: disable=unbalanced-tuple-unpacking
-            _, images = RbdService.rbd_pool_list(pool_name, namespace)
+            images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace)
             if images:
                 raise DashboardException(
                     msg='Namespace contains images which must be deleted first',
@@ -541,7 +548,7 @@ class RbdNamespace(RESTController):
             namespaces = self.rbd_inst.namespace_list(ioctx)
             for namespace in namespaces:
                 # pylint: disable=unbalanced-tuple-unpacking
-                _, images = RbdService.rbd_pool_list(pool_name, namespace)
+                images, _ = RbdService.rbd_pool_list([pool_name], namespace=namespace)
                 result.append({
                     'namespace': namespace,
                     'num_images': len(images) if images else 0
index d84647e91dd62e0083046f2938ce5e1fc807296b..8b97634f139722cbaeece9b3ffd563ba0a36ade5 100644 (file)
@@ -88,7 +88,7 @@ export class IscsiTargetFormComponent extends CdForm implements OnInit {
   ngOnInit() {
     const promises: any[] = [
       this.iscsiService.listTargets(),
-      this.rbdService.list(),
+                               this.rbdService.list({'offset': 0, 'limit': 5}),
       this.iscsiService.portals(),
       this.iscsiService.settings(),
       this.iscsiService.version()
index 0f68f2df252cfe7c20462028a8cf095a55e14ea2..7946a7191cead7dc99d402f53f627985a448a80f 100644 (file)
@@ -6,12 +6,14 @@
           [columns]="columns"
           identifier="unique_id"
           [searchableObjects]="true"
+                                       [serverSide]="true"
+                                       [count]="count"
           forceIdentifier="true"
           selectionType="single"
           [hasDetails]="true"
           [status]="tableStatus"
-          [autoReload]="-1"
-          (fetchData)="taskListService.fetch()"
+          [autoReload]="5000"
+          (fetchData)="getRbdImages($event)"
           (setExpandedRow)="setExpandedRow($event)"
           (updateSelection)="updateSelection($event)">
   <cd-table-actions class="table-actions"
index 86819762164fcee1968f37989f24dcda0aa05abf..e432f389e5c72357f9113bc5b2c90a6ac609995b 100644 (file)
@@ -4,6 +4,8 @@ import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 import _ from 'lodash';
 import { Observable, Subscriber } from 'rxjs';
 
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableServerSideService } from '~/app/shared/services/cd-table-server-side.service';
 import { RbdService } from '~/app/shared/api/rbd.service';
 import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
 import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
@@ -73,6 +75,8 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
   tableStatus = new TableStatusViewCache();
   selection = new CdTableSelection();
   icons = Icons;
+       count: number = 0;
+       private tableContext: CdTableFetchDataContext = null;
 
   modalRef: NgbModalRef;
 
@@ -121,7 +125,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     public taskListService: TaskListService,
     private urlBuilder: URLBuilderService,
     public actionLabels: ActionLabelsI18n
-  ) {
+       ) {
     super();
     this.permission = this.authStorageService.getPermissions().rbdImage;
     const getImageUri = () =>
@@ -344,15 +348,18 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
       ].includes(task.name);
     };
 
-    this.taskListService.init(
-      () => this.rbdService.list(),
-      (resp) => this.prepareResponse(resp),
-      (images) => (this.images = images),
-      () => this.onFetchError(),
-      taskFilter,
-      itemFilter,
-      this.builders
-    );
+               console.log(taskFilter);
+               console.log(itemFilter);
+               console.log(this.builders);
+    // this.taskListService.init(
+               //      (context: CdTableFetchDataContext) => this.rbdService.list(context.toParams()),
+               //      (resp) => this.prepareResponse(resp),
+    //   (images) => (this.images = images),
+    //   () => this.onFetchError(),
+    //   taskFilter,
+    //   itemFilter,
+    //   this.builders
+    // );
   }
 
   onFetchError() {
@@ -360,10 +367,22 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     this.tableStatus = new TableStatusViewCache(ViewCacheStatus.ValueException);
   }
 
-  prepareResponse(resp: any[]): any[] {
-    let images: any[] = [];
+       getRbdImages(context: CdTableFetchDataContext = null){
+               if(context !== null) {
+                       this.tableContext = context;
+               }
+               console.log(context);
+               this.rbdService.list(this.tableContext.toParams()).
+                       subscribe((resp: any) => {
+                               console.log(resp);
+                               this.prepareResponse(resp);
+                       });
+       }
+       prepareResponse(resp: any[]): any[] {
+               let images: any[] = [];
     const viewCacheStatusMap = {};
 
+               console.log(resp);
     resp.forEach((pool) => {
       if (_.isUndefined(viewCacheStatusMap[pool.status])) {
         viewCacheStatusMap[pool.status] = [];
@@ -404,6 +423,13 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
       }
     });
 
+               console.group('reponse');
+               console.log(resp);
+               console.log(resp[0].headers);
+               this.count = CdTableServerSideService.getCount(resp[0]);
+               console.log(this.count);
+               this.images = images;
+               console.groupEnd();
     return images;
   }
 
@@ -626,3 +652,30 @@ export class RbdListComponent extends ListWithDetails implements OnInit {
     return false;
   }
 }
+
+
+/*
+       for pool in pools
+         for namespace in namespaces
+                 refs = get_image_refs
+                       for ref in refs:
+                          get_data(ref)
+        
+@ttl_cache(5)
+def get_refs();
+  joint_refs = []
+       for pool in pools
+         for namespace in namespaces
+                 refs = get_image_refs
+                       for ref in refs:
+                         joint_refs.append(ref)
+       return joint_refs
+
+sort(joint_refs)
+               for ref in joint_refs[offset:offset+limit]:
+get_data(ref)
+
+
+
+         
+*/
index af646d8ac61d3cf8bc725192139de50543f348ad..37eb0ee3c773792d6a3ba988a4d96334179cd2e3 100644 (file)
@@ -4,6 +4,7 @@ export interface RbdPool {
   pool_name: string;
   status: number;
   value: RbdImage[];
+       headers: any;
 }
 
 export interface RbdImage {
index 5482f093122869cd38e672062a3d7f2cc2470a24..dbf396b1f5915f4bbf89fe5196a7dbc7500521e2 100644 (file)
@@ -36,27 +36,30 @@ export class RbdService {
     });
   }
 
-  get(imageSpec: ImageSpec) {
-    return this.http.get(`api/block/image/${imageSpec.toStringEncoded()}`);
+       get(imageSpec: ImageSpec) {
+               return this.http.get(`api/block/image/${imageSpec.toStringEncoded()}`);
   }
 
-  list() {
-    return this.http.get<RbdPool[]>('api/block/image').pipe(
-      map((pools) =>
-        pools.map((pool) => {
-          pool.value.map((image) => {
+  list(params: any) {
+               return this.http.get<RbdPool[]>('api/block/image', {params: params, observe: 'response'}).pipe(
+                       map((response: any) => {
+                               console.log(response);
+        return response['body'].map((pool: any) => {
+          pool.value.map((image: any) => {
             if (!image.configuration) {
               return image;
             }
-            image.configuration.map((option) =>
+            image.configuration.map((option: any) =>
               Object.assign(option, this.rbdConfigurationService.getOptionByName(option.name))
             );
             return image;
           });
+                                       pool['headers'] = response.headers;
           return pool;
         })
-      )
-    );
+                       }
+                                )
+               );
   }
 
   copy(imageSpec: ImageSpec, rbd: any) {
index b38477c813733430dc8733df46ffb4899455c500..1ba4db6e2c8a895e41009a6c510a0ad906097f4f 100644 (file)
                  [rowClass]="getRowClass()"
                  [headerHeight]="header ? 'auto' : 0"
                  [footerHeight]="footer ? 'auto' : 0"
+                                                                [count]="count"
                  [limit]="userConfig.limit > 0 ? userConfig.limit : undefined"
+                 [offset]="userConfig.offset >= 0 ? userConfig.offset : 0"
+                 (page)="changePage($event)"
                  [loadingIndicator]="loadingIndicator"
                  [rowIdentity]="rowIdentity()"
                  [rowHeight]="'auto'">
index ddeda9ac44311ee9e13c01a5a26682a792b66f6b..096cbb35df1d7766cc1399ced000c4167af7acd4 100644 (file)
@@ -35,6 +35,7 @@ import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data
 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
 import { CdUserConfig } from '~/app/shared/models/cd-user-config';
 import { TimerService } from '~/app/shared/services/timer.service';
+import { PageInfo } from '../../models/cd-table-paging';
 
 @Component({
   selector: 'cd-table',
@@ -112,7 +113,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
    * prevent triggering fetchData when initializing the table.
    */
   @Input()
-  autoReload: any = 5000;
+  autoReload: number = 5000;
 
   // Which row property is unique for a row. If the identifier is not specified in any
   // column, then the property name of the first column is used. Defaults to 'id'.
@@ -151,6 +152,17 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   @Input()
   status = new TableStatus();
 
+  // Support server-side pagination/sorting/etc.
+  @Input()
+  serverSide: boolean = false;
+
+  /*
+  Only required when serverSide is enabled.
+  It should be provided by the server via "X-Total-Count" HTTP Header
+  */
+  @Input()
+  count: number = 0;
+
   /**
    * Should be a function to update the input data if undefined nothing will be triggered
    *
@@ -161,7 +173,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
    * The function is triggered through one table and all tables will update
    */
   @Output()
-  fetchData = new EventEmitter();
+  fetchData = new EventEmitter<CdTableFetchDataContext>();
 
   /**
    * This should be defined if you need access to the selection object.
@@ -327,6 +339,9 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     if (!this.userConfig.limit) {
       this.userConfig.limit = this.limit;
     }
+    if (!(this.userConfig.offset >= 0)) {
+      this.userConfig.offset = this.table.offset;
+    }
     if (!this.userConfig.sorts) {
       this.userConfig.sorts = this.sorts;
     }
@@ -605,11 +620,14 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
   }
 
   setLimit(e: any) {
-    const value = parseInt(e.target.value, 10);
+    const value = Number(e.target.value);
     if (value > 0) {
       this.userConfig.limit = value;
     }
-  }
+               if (this.serverSide) {
+                       this.reloadData();
+               }
+       }
 
   reloadData() {
     if (!this.updating) {
@@ -627,6 +645,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
         // to the correct state.
         this.useData();
       });
+      context.pageInfo.offset = this.userConfig.offset;
+      context.pageInfo.limit = this.userConfig.limit;
       this.fetchData.emit(context);
       this.updating = true;
     }
@@ -637,6 +657,13 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O
     this.reloadData();
   }
 
+ changePage(pageInfo: PageInfo) {
+    this.userConfig.offset = pageInfo.offset;
+    this.userConfig.limit = pageInfo.limit;
+    if (this.serverSide) {
+      this.reloadData();
+    }
+  }
   rowIdentity() {
     return (row: any) => {
       const id = row[this.identifier];
index b7c6e672d7d71adaf42d291d679f5c20c1001efa..dc7a46d1ff9276e5522666c0a351907fe965d052 100644 (file)
@@ -1,3 +1,6 @@
+import { HttpParams } from "@angular/common/http";
+import { PageInfo } from "./cd-table-paging";
+
 export class CdTableFetchDataContext {
   errorConfig = {
     resetData: true, // Force data table to show no data
@@ -10,8 +13,22 @@ export class CdTableFetchDataContext {
    * reset the data table to the correct state.
    */
   error: Function;
+  pageInfo: PageInfo = new PageInfo;
 
   constructor(error: () => void) {
     this.error = error;
   }
+
+       toParams(): HttpParams {
+               if (this.pageInfo.limit === null) {
+                       this.pageInfo.limit = 0;
+               }
+               return new HttpParams({
+                       fromObject: {
+                               offset: String(this.pageInfo.offset * this.pageInfo.limit),
+                               limit: String(this.pageInfo.limit)
+                       }
+               });
+
+       }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-paging.ts
new file mode 100644 (file)
index 0000000..c0d5842
--- /dev/null
@@ -0,0 +1,20 @@
+export const PAGE_LIMIT = 10;
+
+export class PageInfo {
+  // Total number of rows in a table
+  count: number;
+
+  // Current page (current row = offset x limit or pageSize)
+  offset: number = 0;
+
+  // Max. number of rows fetched from the server
+  limit: number = PAGE_LIMIT;
+
+  /*
+  pageSize and limit can be decoupled if hybrid server-side and client-side
+  are used. A use-case would be to reduce the amount of queries: that is,
+  the pageSize (client-side paging) might be 10, but the back-end queries
+  could have a limit of 100. That would avoid triggering requests
+  */
+  pageSize: number = PAGE_LIMIT;
+}
index 2e1a9e1970a47b1d6d774b76bd5782a535c3a277..3bb426b8dc1f72201f70b7dd522b5ebc180c5894 100644 (file)
@@ -4,6 +4,7 @@ import { CdTableColumn } from './cd-table-column';
 
 export interface CdUserConfig {
   limit?: number;
+  offset?: number;
   sorts?: SortPropDir[];
   columns?: CdTableColumn[];
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.spec.ts
new file mode 100644 (file)
index 0000000..dbe7bb4
--- /dev/null
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { CdTableServerSideService } from './cd-table-server-side.service';
+
+describe('CdTableServerSideService', () => {
+  let service: CdTableServerSideService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(CdTableServerSideService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/cd-table-server-side.service.ts
new file mode 100644 (file)
index 0000000..8e68a25
--- /dev/null
@@ -0,0 +1,14 @@
+import { HttpResponse } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class CdTableServerSideService {
+
+  constructor() { }
+
+  static getCount(resp: HttpResponse<any>): number {
+    return Number(resp.headers.get('X-Total-Count'))
+  }
+}
index 59b413dea8949ec8b5223a68c6f62ac4377a8f8d..eec5a6277cc035ad5a66b60db2eef6636e2f7df8 100644 (file)
@@ -408,34 +408,58 @@ class RbdService(object):
                                 errno=errno.ENOENT)
 
     @classmethod
-    @ViewCache()
-    def rbd_pool_list(cls, pool_name, namespace=None):
+    def _rbd_pool_image_refs(cls, pool_names: List[str], namespace=None):
+        joint_refs = []
         rbd_inst = rbd.RBD()
-        with mgr.rados.open_ioctx(pool_name) as ioctx:
-            result = []
-            if namespace:
-                namespaces = [namespace]
-            else:
-                namespaces = rbd_inst.namespace_list(ioctx)
-                # images without namespace
-                namespaces.append('')
-            for current_namespace in namespaces:
-                ioctx.set_namespace(current_namespace)
-                image_refs = cls._rbd_image_refs(ioctx)
-                for image_ref in image_refs:
+        for pool in pool_names:
+            with mgr.rados.open_ioctx(pool) as ioctx:
+                result = []
+                if namespace:
+                    namespaces = [namespace]
+                else:
+                    namespaces = rbd_inst.namespace_list(ioctx)
+                    # images without namespace
+                    namespaces.append('')
+                for current_namespace in namespaces:
+                    ioctx.set_namespace(current_namespace)
+                    image_refs = cls._rbd_image_refs(ioctx)
+                    for image in image_refs:
+                        image['namespace'] = current_namespace
+                        image['pool'] = pool
+                        joint_refs.append(image)
+        return joint_refs
+
+    @classmethod
+    def rbd_pool_list(cls, pool_names: List[str], namespace=None, offset=0, limit=0):
+        offset = int(offset)
+        limit = int(limit)
+        if limit < 0:
+            return []
+
+        refs = cls._rbd_pool_image_refs(pool_names, namespace)
+        image_refs = []
+        # transform to list so that we can count
+        for i in refs:
+            image_refs.append(i)
+            
+        result = []
+        for image_ref in sorted(image_refs, key=lambda v: v['name'])[offset:offset+limit]:
+            with mgr.rados.open_ioctx(image_ref['pool']) as ioctx:
+                ioctx.set_namespace(image_ref['namespace'])
+                try:
+                    stat = cls._rbd_image_stat(
+                        ioctx, image_ref['pool'], image_ref['namespace'], image_ref['name'])
+                except rbd.ImageNotFound:
+                    # Check if the RBD has been deleted partially. This happens for example if
+                    # the deletion process of the RBD has been started and was interrupted.
                     try:
-                        stat = cls._rbd_image_stat(
-                            ioctx, pool_name, current_namespace, image_ref['name'])
+                        stat = cls._rbd_image_stat_removing(
+                            ioctx, image_ref['pool'], image_ref['namespace'], image_ref['id'])
                     except rbd.ImageNotFound:
-                        # Check if the RBD has been deleted partially. This happens for example if
-                        # the deletion process of the RBD has been started and was interrupted.
-                        try:
-                            stat = cls._rbd_image_stat_removing(
-                                ioctx, pool_name, current_namespace, image_ref['id'])
-                        except rbd.ImageNotFound:
-                            continue
-                    result.append(stat)
-            return result
+                        continue
+                stat['pool'] = image_ref['pool']
+                result.append(stat)
+        return result, len(image_refs)
 
     @classmethod
     def get_image(cls, image_spec):