From 72fb6dc68fc4d91ce7db74d2619627bdae2355ac Mon Sep 17 00:00:00 2001 From: Pere Diaz Bou Date: Thu, 8 Sep 2022 20:49:42 +0200 Subject: [PATCH] mgr/dashboard: paginate services Signed-off-by: Pere Diaz Bou --- .../mgr/dashboard/controllers/service.py | 10 +- .../service-daemon-list.component.spec.ts | 16 +- .../service-daemon-list.component.ts | 21 +- .../service-form.component.spec.ts | 7 +- .../service-form/service-form.component.ts | 191 +++++++++--------- .../cluster/services/services.component.html | 2 + .../services/services.component.spec.ts | 9 +- .../cluster/services/services.component.ts | 5 +- .../app/shared/api/ceph-service.service.ts | 25 ++- .../src/app/shared/api/paginate.model.ts | 17 ++ .../models/cd-table-fetch-data-context.ts | 7 + .../mgr/dashboard/services/_paginate.py | 63 ++++++ .../mgr/dashboard/services/orchestrator.py | 17 +- 13 files changed, 269 insertions(+), 121 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts create mode 100644 src/pybind/mgr/dashboard/services/_paginate.py diff --git a/src/pybind/mgr/dashboard/controllers/service.py b/src/pybind/mgr/dashboard/controllers/service.py index afe684302b1..b75f4173614 100644 --- a/src/pybind/mgr/dashboard/controllers/service.py +++ b/src/pybind/mgr/dashboard/controllers/service.py @@ -8,6 +8,7 @@ from ..services.exception import handle_custom_error, handle_orchestrator_error from ..services.orchestrator import OrchClient, OrchFeature from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \ ReadPermission, RESTController, Task, UpdatePermission +from ._version import APIVersion from .orchestrator import raise_if_no_orchestrator @@ -29,9 +30,14 @@ class Service(RESTController): return ServiceSpec.KNOWN_SERVICE_TYPES @raise_if_no_orchestrator([OrchFeature.SERVICE_LIST]) - def list(self, service_name: Optional[str] = None) -> List[dict]: + @RESTController.MethodMap(version=APIVersion(2, 0)) # type: ignore + def list(self, service_name: Optional[str] = None, offset: int = 0, limit: int = 5, + search: str = '', sort: str = '+service_name') -> List[dict]: orch = OrchClient.instance() - return [service.to_dict() for service in orch.services.list(service_name=service_name)] + services, count = orch.services.list(service_name=service_name, offset=int(offset), + limit=int(limit), search=search, sort=sort) + cherrypy.response.headers['X-Total-Count'] = count + return services @raise_if_no_orchestrator([OrchFeature.SERVICE_LIST]) def get(self, service_name: str) -> List[dict]: diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts index 31739a7c298..7ece91e76e5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.spec.ts @@ -10,6 +10,7 @@ import { CephModule } from '~/app/ceph/ceph.module'; import { CoreModule } from '~/app/core/core.module'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { HostService } from '~/app/shared/api/host.service'; +import { PaginateObservable } from '~/app/shared/api/paginate.model'; import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; @@ -117,6 +118,8 @@ describe('ServiceDaemonListComponent', () => { } ]; + const context = new CdTableFetchDataContext(() => undefined); + const getDaemonsByHostname = (hostname?: string) => { return hostname ? _.filter(daemons, { hostname: hostname }) : daemons; }; @@ -147,7 +150,12 @@ describe('ServiceDaemonListComponent', () => { spyOn(cephServiceService, 'getDaemons').and.callFake(() => of(getDaemonsByServiceName(component.serviceName)) ); - spyOn(cephServiceService, 'list').and.returnValue(of(services)); + + const paginate_obs = new PaginateObservable(of(services)); + spyOn(cephServiceService, 'list').and.returnValue(paginate_obs); + context.pageInfo.offset = 0; + context.pageInfo.limit = -1; + fixture.detectChanges(); }); @@ -157,18 +165,18 @@ describe('ServiceDaemonListComponent', () => { it('should list daemons by host', () => { component.hostname = 'mon0'; - component.getDaemons(new CdTableFetchDataContext(() => undefined)); + component.getDaemons(context); expect(component.daemons.length).toBe(1); }); it('should list daemons by service', () => { component.serviceName = 'osd'; - component.getDaemons(new CdTableFetchDataContext(() => undefined)); + component.getDaemons(context); expect(component.daemons.length).toBe(3); }); it('should list services', () => { - component.getServices(new CdTableFetchDataContext(() => undefined)); + component.getServices(context); expect(component.services.length).toBe(2); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts index d1c2f9cc3f1..4b9602eaa2b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts @@ -1,3 +1,4 @@ +import { HttpParams } from '@angular/common/http'; import { AfterViewInit, Component, @@ -281,15 +282,17 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI }); } getServices(context: CdTableFetchDataContext) { - this.serviceSub = this.cephServiceService.list(this.serviceName).subscribe( - (services: CephServiceSpec[]) => { - this.services = services; - }, - () => { - this.services = []; - context.error(); - } - ); + this.serviceSub = this.cephServiceService + .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName) + .observable.subscribe( + (services: CephServiceSpec[]) => { + this.services = services; + }, + () => { + this.services = []; + context.error(); + } + ); } trackByFn(_index: any, item: any) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts index b0574c6efb0..e86a86fd65e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts @@ -7,8 +7,10 @@ import { RouterTestingModule } from '@angular/router/testing'; import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; import { ToastrModule } from 'ngx-toastr'; +import { of } from 'rxjs'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; +import { PaginateObservable } from '~/app/shared/api/paginate.model'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed, FormHelper } from '~/testing/unit-test-helper'; @@ -533,6 +535,8 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA== describe('should test service mds', () => { beforeEach(() => { formHelper.setValue('service_type', 'mds'); + const paginate_obs = new PaginateObservable(of({})); + spyOn(cephServiceService, 'list').and.returnValue(paginate_obs); }); it('should test mds valid service id', () => { @@ -558,7 +562,8 @@ x4Ea7kGVgx9kWh5XjWz9wjZvY49UKIT5ppIAWPMbLl3UpfckiuNhTA== }); it('should check whether edit field is correctly loaded', () => { - const cephServiceSpy = spyOn(cephServiceService, 'list').and.callThrough(); + const paginate_obs = new PaginateObservable(of({})); + const cephServiceSpy = spyOn(cephServiceService, 'list').and.returnValue(paginate_obs); component.ngOnInit(); expect(cephServiceSpy).toBeCalledTimes(2); expect(component.action).toBe('Edit'); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts index a809aa3cbc9..3b5cc0c7186 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts @@ -1,3 +1,4 @@ +import { HttpParams } from '@angular/common/http'; import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; @@ -350,12 +351,14 @@ export class ServiceFormComponent extends CdForm implements OnInit { }); } - this.cephServiceService.list().subscribe((services: CephServiceSpec[]) => { - this.serviceList = services; - this.services = services.filter((service: any) => - this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type) - ); - }); + this.cephServiceService + .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } })) + .observable.subscribe((services: CephServiceSpec[]) => { + this.serviceList = services; + this.services = services.filter((service: any) => + this.INGRESS_SUPPORTED_SERVICE_TYPES.includes(service.service_type) + ); + }); this.cephServiceService.getKnownTypes().subscribe((resp: Array) => { // Remove service types: @@ -385,96 +388,100 @@ export class ServiceFormComponent extends CdForm implements OnInit { if (this.editing) { this.action = this.actionLabels.EDIT; this.disableForEditing(this.serviceType); - this.cephServiceService.list(this.serviceName).subscribe((response: CephServiceSpec[]) => { - const formKeys = ['service_type', 'service_id', 'unmanaged']; - formKeys.forEach((keys) => { - this.serviceForm.get(keys).setValue(response[0][keys]); - }); - if (!response[0]['unmanaged']) { - const placementKey = Object.keys(response[0]['placement'])[0]; - let placementValue: string; - ['hosts', 'label'].indexOf(placementKey) >= 0 - ? (placementValue = placementKey) - : (placementValue = 'hosts'); - this.serviceForm.get('placement').setValue(placementValue); - this.serviceForm.get('count').setValue(response[0]['placement']['count']); - if (response[0]?.placement[placementValue]) { - this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]); - } - } - switch (this.serviceType) { - case 'iscsi': - const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port']; - specKeys.forEach((key) => { - this.serviceForm.get(key).setValue(response[0].spec[key]); - }); - this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure); - if (response[0].spec?.api_secure) { - this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert); - this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key); + this.cephServiceService + .list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }), this.serviceName) + .observable.subscribe((response: CephServiceSpec[]) => { + const formKeys = ['service_type', 'service_id', 'unmanaged']; + formKeys.forEach((keys) => { + this.serviceForm.get(keys).setValue(response[0][keys]); + }); + if (!response[0]['unmanaged']) { + const placementKey = Object.keys(response[0]['placement'])[0]; + let placementValue: string; + ['hosts', 'label'].indexOf(placementKey) >= 0 + ? (placementValue = placementKey) + : (placementValue = 'hosts'); + this.serviceForm.get('placement').setValue(placementValue); + this.serviceForm.get('count').setValue(response[0]['placement']['count']); + if (response[0]?.placement[placementValue]) { + this.serviceForm.get(placementValue).setValue(response[0]?.placement[placementValue]); } - break; - case 'rgw': - this.serviceForm.get('rgw_frontend_port').setValue(response[0].spec?.rgw_frontend_port); - this.serviceForm.get('ssl').setValue(response[0].spec?.ssl); - if (response[0].spec?.ssl) { + } + switch (this.serviceType) { + case 'iscsi': + const specKeys = ['pool', 'api_password', 'api_user', 'trusted_ip_list', 'api_port']; + specKeys.forEach((key) => { + this.serviceForm.get(key).setValue(response[0].spec[key]); + }); + this.serviceForm.get('ssl').setValue(response[0].spec?.api_secure); + if (response[0].spec?.api_secure) { + this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert); + this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key); + } + break; + case 'rgw': this.serviceForm - .get('ssl_cert') - .setValue(response[0].spec?.rgw_frontend_ssl_certificate); - } - break; - case 'ingress': - const ingressSpecKeys = [ - 'backend_service', - 'virtual_ip', - 'frontend_port', - 'monitor_port', - 'virtual_interface_networks', - 'ssl' - ]; - ingressSpecKeys.forEach((key) => { - this.serviceForm.get(key).setValue(response[0].spec[key]); - }); - if (response[0].spec?.ssl) { - this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert); - this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key); - } - break; - case 'snmp-gateway': - const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination']; - snmpCommonSpecKeys.forEach((key) => { - this.serviceForm.get(key).setValue(response[0].spec[key]); - }); - if (this.serviceForm.getValue('snmp_version') === 'V3') { - const snmpV3SpecKeys = [ - 'engine_id', - 'auth_protocol', - 'privacy_protocol', - 'snmp_v3_auth_username', - 'snmp_v3_auth_password', - 'snmp_v3_priv_password' + .get('rgw_frontend_port') + .setValue(response[0].spec?.rgw_frontend_port); + this.serviceForm.get('ssl').setValue(response[0].spec?.ssl); + if (response[0].spec?.ssl) { + this.serviceForm + .get('ssl_cert') + .setValue(response[0].spec?.rgw_frontend_ssl_certificate); + } + break; + case 'ingress': + const ingressSpecKeys = [ + 'backend_service', + 'virtual_ip', + 'frontend_port', + 'monitor_port', + 'virtual_interface_networks', + 'ssl' ]; - snmpV3SpecKeys.forEach((key) => { - if (key !== null) { - if ( - key === 'snmp_v3_auth_username' || - key === 'snmp_v3_auth_password' || - key === 'snmp_v3_priv_password' - ) { - this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]); - } else { - this.serviceForm.get(key).setValue(response[0].spec[key]); - } - } + ingressSpecKeys.forEach((key) => { + this.serviceForm.get(key).setValue(response[0].spec[key]); }); - } else { - this.serviceForm - .get('snmp_community') - .setValue(response[0].spec['credentials']['snmp_community']); - } - break; - } - }); + if (response[0].spec?.ssl) { + this.serviceForm.get('ssl_cert').setValue(response[0].spec?.ssl_cert); + this.serviceForm.get('ssl_key').setValue(response[0].spec?.ssl_key); + } + break; + case 'snmp-gateway': + const snmpCommonSpecKeys = ['snmp_version', 'snmp_destination']; + snmpCommonSpecKeys.forEach((key) => { + this.serviceForm.get(key).setValue(response[0].spec[key]); + }); + if (this.serviceForm.getValue('snmp_version') === 'V3') { + const snmpV3SpecKeys = [ + 'engine_id', + 'auth_protocol', + 'privacy_protocol', + 'snmp_v3_auth_username', + 'snmp_v3_auth_password', + 'snmp_v3_priv_password' + ]; + snmpV3SpecKeys.forEach((key) => { + if (key !== null) { + if ( + key === 'snmp_v3_auth_username' || + key === 'snmp_v3_auth_password' || + key === 'snmp_v3_priv_password' + ) { + this.serviceForm.get(key).setValue(response[0].spec['credentials'][key]); + } else { + this.serviceForm.get(key).setValue(response[0].spec[key]); + } + } + }); + } else { + this.serviceForm + .get('snmp_community') + .setValue(response[0].spec['credentials']['snmp_community']); + } + break; + } + }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html index 36ab431fc96..fbcc8583cf1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.html @@ -9,6 +9,8 @@ [autoReload]="5000" (fetchData)="getServices($event)" [hasDetails]="hasDetails" + [serverSide]="true" + [count]="count" (setExpandedRow)="setExpandedRow($event)" (updateSelection)="updateSelection($event)"> { component = fixture.componentInstance; const orchService = TestBed.inject(OrchestratorService); const cephServiceService = TestBed.inject(CephServiceService); - spyOn(orchService, 'status').and.returnValue(of({ available: true })); + const paginate_obs = new PaginateObservable(of({ available: true })); + spyOn(orchService, 'status').and.returnValue(paginate_obs); spyOn(cephServiceService, 'list').and.returnValue(of(services)); fixture.detectChanges(); }); @@ -87,7 +89,10 @@ describe('ServicesComponent', () => { }); it('should return all services', () => { - component.getServices(new CdTableFetchDataContext(() => undefined)); + const context = new CdTableFetchDataContext(() => undefined) + context.pageInfo.offset = 0; + context.pageInfo.limit = -1 + component.getServices(context); expect(component.services.length).toBe(2); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index 318a54a6ee4..55638de24de 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -54,6 +54,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI permissions: Permissions; tableActions: CdTableAction[]; showDocPanel = false; + count = 0; bsModalRef: NgbModalRef; orchStatus: OrchestratorStatus; @@ -213,9 +214,11 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI return; } this.isLoadingServices = true; - this.cephServiceService.list().subscribe( + const pagination_obs = this.cephServiceService.list(context.toParams()); + pagination_obs.observable.subscribe( (services: CephServiceSpec[]) => { this.services = services; + this.count = pagination_obs.count; this.services = this.services.filter((col: any) => { return !this.hiddenServices.includes(col.service_name); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts index c62dfea7c01..4c7e4cab370 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/ceph-service.service.ts @@ -3,22 +3,33 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; +import { ApiClient } from '~/app/shared/api/api-client'; import { Daemon } from '../models/daemon.interface'; import { CephServiceSpec } from '../models/service.interface'; +import { PaginateObservable } from './paginate.model'; @Injectable({ providedIn: 'root' }) -export class CephServiceService { +export class CephServiceService extends ApiClient { private url = 'api/service'; - constructor(private http: HttpClient) {} + constructor(private http: HttpClient) { + super(); + } - list(serviceName?: string): Observable { - const options = serviceName - ? { params: new HttpParams().set('service_name', serviceName) } - : {}; - return this.http.get(this.url, options); + list(httpParams: HttpParams, serviceName?: string): PaginateObservable { + const options = { + headers: { Accept: this.getVersionHeaderValue(2, 0) }, + params: httpParams + }; + options['observe'] = 'response'; + if (serviceName) { + options.params = options.params.append('service_name', serviceName); + } + return new PaginateObservable( + this.http.get(this.url, options) + ); } getDaemons(serviceName?: string): Observable { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts new file mode 100644 index 00000000000..7c1c391fa30 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/paginate.model.ts @@ -0,0 +1,17 @@ +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export class PaginateObservable { + observable: Observable; + count: number; + + subscribe: any; + constructor(obs: Observable) { + this.observable = obs.pipe( + map((response: any) => { + this.count = Number(response.headers?.get('X-Total-Count')); + return response['body']; + }) + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts index 7937d82e6f3..834a89967a9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-table-fetch-data-context.ts @@ -23,15 +23,22 @@ export class CdTableFetchDataContext { } toParams(): HttpParams { + if (this.pageInfo.offset == NaN) { + this.pageInfo.offset = 0; + } + if (this.pageInfo.limit === null) { this.pageInfo.limit = 0; } + if (!this.search) { this.search = ''; } + if (!this.sort || this.sort.length < 2) { this.sort = '+name'; } + return new HttpParams({ fromObject: { offset: String(this.pageInfo.offset * this.pageInfo.limit), diff --git a/src/pybind/mgr/dashboard/services/_paginate.py b/src/pybind/mgr/dashboard/services/_paginate.py new file mode 100644 index 00000000000..3acfee09cc5 --- /dev/null +++ b/src/pybind/mgr/dashboard/services/_paginate.py @@ -0,0 +1,63 @@ +from typing import Any, Dict, List + +from ..exceptions import DashboardException + + +class ListPaginator: + def __init__(self, offset: int, limit: int, sort: str, search: str, + input_list: List[Any], default_sort: str, + searchable_params: List[str] = [], sortable_params: List[str] = []): + self.offset = offset + if limit < -1: + raise DashboardException(msg=f'Wrong limit value {limit}', code=400) + self.limit = limit + self.sort = sort + self.search = search + self.input_list = input_list + self.default_sort = default_sort + self.searchable_params = searchable_params + self.sortable_params = sortable_params + + def get_count(self): + return len(self.input_list) + + def find_value(self, item: Dict[str, Any], key: str): + keys = key.split('.') + value = item + for key in keys: + if key in value: + value = value[key] + else: + return '' + return value + + def list(self): + end = self.offset + self.limit + # '-1' is a special number to refer to all items in list + if self.limit == -1: + end = len(self.input_list) + + desc = self.sort[0] == '-' + sort_by = self.sort[1:] + + # trim down by search + trimmed_list = [] + if self.search: + for item in self.input_list: + for searchable_param in self.searchable_params: + value = self.find_value(item, searchable_param) + if isinstance(value, str): + if self.search in str(value): + trimmed_list.append(item) + + else: + trimmed_list = self.input_list + + if sort_by not in self.sortable_params: + sort_by = self.default_sort + + def sort(item): + return self.find_value(item, sort_by) + + for item in sorted(trimmed_list, key=sort, reverse=desc)[self.offset:end]: + yield item diff --git a/src/pybind/mgr/dashboard/services/orchestrator.py b/src/pybind/mgr/dashboard/services/orchestrator.py index 6829108e098..abcbf93bee1 100644 --- a/src/pybind/mgr/dashboard/services/orchestrator.py +++ b/src/pybind/mgr/dashboard/services/orchestrator.py @@ -10,6 +10,7 @@ from orchestrator import DaemonDescription, DeviceLightLoc, HostSpec, \ ServiceDescription, raise_if_exception from .. import mgr +from ._paginate import ListPaginator logger = logging.getLogger('orchestrator') @@ -95,11 +96,21 @@ class InventoryManager(ResourceManager): class ServiceManager(ResourceManager): - @wait_api_result def list(self, service_type: Optional[str] = None, - service_name: Optional[str] = None) -> List[ServiceDescription]: - return self.api.describe_service(service_type, service_name) + service_name: Optional[str] = None, + offset: int = 0, limit: int = -1, + sort: str = '+service_name', search: str = '') -> List[ServiceDescription]: + services = self.api.describe_service(service_type, service_name) + services = [service.to_dict() for service in services.result] + paginator = ListPaginator(offset, limit, sort, search, + input_list=services, + searchable_params=['service_name', 'status.running', + 'status.last_refreshed', 'status.size'], + sortable_params=['service_name', 'status.running', + 'status.last_refreshed', 'status.size'], + default_sort='service_name') + return list(paginator.list()), paginator.get_count() @wait_api_result def get(self, service_name: str) -> ServiceDescription: -- 2.39.5