Add pagination for user groups (#15933)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
孙世军 2021-11-03 14:29:41 +08:00 committed by GitHub
parent d4affc2eba
commit b881f1b020
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 147 additions and 177 deletions

View File

@ -1,14 +1,11 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClarityModule } from '@clr/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { GroupService } from "../group.service";
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { MessageHandlerService } from "../../../../shared/services/message-handler.service";
import { SessionService } from "../../../../shared/services/session.service";
import { AppConfigService } from "../../../../services/app-config.service";
import { AddGroupModalComponent } from './add-group-modal.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { UsergroupService } from "../../../../../../ng-swagger-gen/services/usergroup.service";
import { SharedTestingModule } from "../../../../shared/shared.module";
describe('AddGroupModalComponent', () => {
let component: AddGroupModalComponent;
@ -36,20 +33,16 @@ describe('AddGroupModalComponent', () => {
TestBed.configureTestingModule({
declarations: [AddGroupModalComponent],
imports: [
ClarityModule,
FormsModule,
BrowserAnimationsModule,
TranslateModule.forRoot()
SharedTestingModule
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
providers: [
ChangeDetectorRef,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: SessionService, useValue: fakeSessionService },
{ provide: AppConfigService, useValue: fakeAppConfigService },
{ provide: GroupService, useValue: fakeGroupService },
{ provide: UsergroupService, useValue: fakeGroupService },
]
})
.compileComponents();

View File

@ -1,14 +1,12 @@
import { finalize } from 'rxjs/operators';
import { Subscription } from "rxjs";
import { Component, OnInit, EventEmitter, Output, ChangeDetectorRef, OnDestroy, ViewChild } from "@angular/core";
import { Component, OnInit, EventEmitter, Output, OnDestroy, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms";
import { GroupService } from "../group.service";
import { MessageHandlerService } from "../../../../shared/services/message-handler.service";
import { SessionService } from "../../../../shared/services/session.service";
import { UserGroup } from "../group";
import { AppConfigService } from "../../../../services/app-config.service";
import { GroupType } from "../../../../shared/entities/shared.const";
import { UserGroup } from 'ng-swagger-gen/models/user-group';
import { UsergroupService } from "../../../../../../ng-swagger-gen/services/usergroup.service";
@Component({
selector: "hbr-add-group-modal",
@ -21,9 +19,6 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
dnTooltip = 'TOOLTIP.ITEM_REQUIRED';
group: UserGroup;
formChangeSubscription: Subscription;
@ViewChild('groupForm', { static: true })
groupForm: NgForm;
@ -38,8 +33,7 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
private session: SessionService,
private msgHandler: MessageHandlerService,
private appConfigService: AppConfigService,
private groupService: GroupService,
private cdr: ChangeDetectorRef
private groupService: UsergroupService,
) { }
ngOnInit() {
@ -52,7 +46,9 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
if (this.appConfigService.isOidcMode()) {
this.isOidcMode = true;
}
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : this.isHttpAuthMode ? GroupType.HTTP_TYPE : GroupType.OIDC_TYPE);
this.group = {
group_type: this.isLdapMode ? GroupType.LDAP_TYPE : this.isHttpAuthMode ? GroupType.HTTP_TYPE : GroupType.OIDC_TYPE
};
}
@ -88,8 +84,9 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
createGroup() {
let groupCopy = Object.assign({}, this.group);
this.groupService
.createGroup(groupCopy).pipe(
this.groupService.createUserGroup({
usergroup: groupCopy
}).pipe(
finalize(() => this.close()))
.subscribe(
res => {
@ -103,7 +100,10 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
editGroup() {
let groupCopy = Object.assign({}, this.group);
this.groupService
.editGroup(groupCopy).pipe(
.updateUserGroup({
groupId: groupCopy.id,
usergroup: groupCopy
}).pipe(
finalize(() => this.close()))
.subscribe(
res => {
@ -115,7 +115,9 @@ export class AddGroupModalComponent implements OnInit, OnDestroy {
}
resetGroup() {
this.group = new UserGroup(this.isLdapMode ? GroupType.LDAP_TYPE : this.isHttpAuthMode ? GroupType.HTTP_TYPE : GroupType.OIDC_TYPE);
this.group = {
group_type: this.isLdapMode ? GroupType.LDAP_TYPE : this.isHttpAuthMode ? GroupType.HTTP_TYPE : GroupType.OIDC_TYPE
};
this.groupForm.reset();
}
}

View File

@ -1,4 +1,4 @@
<div class="row">
<div class="row relative">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2">{{'GROUP.GROUP' | translate}}</h2>
<div class="action-panel-pos rightPos">
@ -9,7 +9,7 @@
</span>
</div>
<div>
<clr-datagrid [(clrDgSelected)]="selectedGroups" [clrDgLoading]="loading">
<clr-datagrid (clrDgRefresh)="loadData($event)" [(clrDgSelected)]="selectedGroups" [clrDgLoading]="loading">
<clr-dg-action-bar >
<button type="button" class="btn btn-secondary" (click)="addGroup()" [disabled]="!canAddGroup">
<clr-icon shape="plus" size="15"></clr-icon>&nbsp;{{'GROUP.ADD' | translate}}</button>
@ -23,21 +23,22 @@
<clr-dg-column>{{'GROUP.TYPE' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="isLdapMode">{{'GROUP.DN' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let group of groups" [clrDgItem]="group">
<clr-dg-row *ngFor="let group of groups" [clrDgItem]="group">
<clr-dg-cell>{{group.group_name}}</clr-dg-cell>
<clr-dg-cell>{{groupToSring(group.group_type) | translate}}</clr-dg-cell>
<clr-dg-cell *ngIf="isLdapMode">{{group.ldap_group_dn}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="15">
<span *ngIf="groups?.length">
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount">
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span *ngIf="totalCount">
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'GROUP.OF' | translate}}
</span>
{{groups?.length}} {{'GROUP.ITEMS' | translate}}
{{totalCount}} {{'GROUP.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<hbr-add-group-modal (dataChange)="loadData()"></hbr-add-group-modal>
<hbr-add-group-modal (dataChange)="refresh()"></hbr-add-group-modal>
</div>
</div>

View File

@ -39,7 +39,9 @@
.rightPos {
position: absolute;
right: 20px;
margin-top: -7px;
height: 32px;
height:32px;
z-index: 100;
}
.relative {
position: relative;
}

View File

@ -1,16 +1,17 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { GroupComponent } from './group.component';
import { ClarityModule } from '@clr/angular';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { FormsModule } from '@angular/forms';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { SessionService } from "../../../shared/services/session.service";
import { GroupService } from "./group.service";
import { of } from "rxjs";
import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { AppConfigService } from '../../../services/app-config.service';
import { OperationService } from "../../../shared/components/operation/operation.service";
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { UsergroupService } from "../../../../../ng-swagger-gen/services/usergroup.service";
import { SharedTestingModule } from "../../../shared/shared.module";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { delay } from "rxjs/operators";
import { UserGroup } from "../../../../../ng-swagger-gen/models/user-group";
describe('GroupComponent', () => {
let component: GroupComponent;
@ -18,12 +19,16 @@ describe('GroupComponent', () => {
let fakeMessageHandlerService = null;
let fakeOperationService = null;
let fakeGroupService = {
getUserGroups: function () {
return of([{
group_name: ''
}, {
group_name: 'abc'
}]);
listUserGroupsResponse: function () {
const res: HttpResponse<Array<UserGroup>> = new HttpResponse<Array<UserGroup>>({
headers: new HttpHeaders({'x-total-count': '3'}),
body: [{
group_name: ''
}, {
group_name: 'abc'
}]
});
return of(res).pipe(delay(0));
}
};
let fakeConfirmationDialogService = {
@ -47,18 +52,15 @@ describe('GroupComponent', () => {
TestBed.configureTestingModule({
declarations: [GroupComponent],
imports: [
ClarityModule,
FormsModule,
TranslateModule.forRoot()
SharedTestingModule
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
providers: [
TranslateService,
{ provide: MessageHandlerService, useValue: fakeMessageHandlerService },
{ provide: OperationService, useValue: fakeOperationService },
{ provide: GroupService, useValue: fakeGroupService },
{ provide: UsergroupService, useValue: fakeGroupService },
{ provide: ConfirmationDialogService, useValue: fakeConfirmationDialogService },
{ provide: SessionService, useValue: fakeSessionService },
{ provide: AppConfigService, useValue: fakeAppConfigService }

View File

@ -1,11 +1,9 @@
import { of, Subscription, forkJoin } from "rxjs";
import { flatMap, catchError } from "rxjs/operators";
import { flatMap, catchError, finalize, debounceTime, switchMap, filter } from "rxjs/operators";
import { SessionService } from "../../../shared/services/session.service";
import { TranslateService } from "@ngx-translate/core";
import { Component, OnInit, ViewChild, OnDestroy } from "@angular/core";
import { AddGroupModalComponent } from "./add-group-modal/add-group-modal.component";
import { UserGroup } from "./group";
import { GroupService } from "./group.service";
import { MessageHandlerService } from "../../../shared/services/message-handler.service";
import { throwError as observableThrowError } from "rxjs";
import { AppConfigService } from '../../../services/app-config.service';
@ -20,6 +18,11 @@ import {
import { ConfirmationDialogService } from "../../global-confirmation-dialog/confirmation-dialog.service";
import { errorHandler } from "../../../shared/units/shared.utils";
import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmation-message";
import { ClrDatagridStateInterface } from "@clr/angular";
import { DEFAULT_PAGE_SIZE } from "../../../shared/units/utils";
import { UsergroupService } from "../../../../../ng-swagger-gen/services/usergroup.service";
import { UserGroup } from "../../../../../ng-swagger-gen/models/user-group";
import { FilterComponent } from "../../../shared/components/filter/filter.component";
@Component({
selector: "app-group",
@ -27,25 +30,26 @@ import { ConfirmationMessage } from "../../global-confirmation-dialog/confirmati
styleUrls: ["./group.component.scss"]
})
export class GroupComponent implements OnInit, OnDestroy {
searchTerm = "";
loading = true;
groups: UserGroup[] = [];
currentPage = 1;
totalCount = 0;
currentPage: number = 1;
totalCount: number = 0;
pageSize: number = DEFAULT_PAGE_SIZE;
selectedGroups: UserGroup[] = [];
currentTerm = "";
delSub: Subscription;
batchOps = 'idle';
batchInfos = new Map();
isLdapMode: boolean;
@ViewChild(AddGroupModalComponent) newGroupModal: AddGroupModalComponent;
searchSub: Subscription;
@ViewChild(FilterComponent, {static: true})
filterComponent: FilterComponent;
constructor(
private operationService: OperationService,
private translate: TranslateService,
private operateDialogService: ConfirmationDialogService,
private groupService: GroupService,
private groupService: UsergroupService,
private msgHandler: MessageHandlerService,
private session: SessionService,
private translateService: TranslateService,
@ -53,7 +57,6 @@ export class GroupComponent implements OnInit, OnDestroy {
) { }
ngOnInit() {
this.loadData();
if (this.appConfigService.isLdapMode()) {
this.isLdapMode = true;
}
@ -70,25 +73,87 @@ export class GroupComponent implements OnInit, OnDestroy {
}
}
);
if (!this.searchSub) {
this.searchSub = this.filterComponent.filterTerms.pipe(
filter(groupName => !!groupName),
debounceTime(500),
switchMap(groupName => {
this.currentPage = 1;
this.selectedGroups = [];
this.loading = true;
return this.groupService.searchUserGroupsResponse({
groupname: groupName,
pageSize: this.pageSize,
page: this.currentPage
})
.pipe(finalize(() => {
this.loading = false;
}));
})).subscribe(response => {
this.totalCount = Number.parseInt(
response.headers.get('x-total-count'), 10
);
this.groups = response.body as UserGroup[];
}, error => {
this.msgHandler.handleError(error);
});
}
}
ngOnDestroy(): void {
this.delSub.unsubscribe();
if (this.searchSub) {
this.searchSub.unsubscribe();
this.searchSub = null;
}
}
refresh(): void {
this.currentPage = 1;
this.selectedGroups = [];
this.currentTerm = '';
this.filterComponent.currentValue = '';
this.loadData();
}
loadData(): void {
loadData(state?: ClrDatagridStateInterface): void {
if (state && state.page) {
this.pageSize = state.page.size;
}
this.loading = true;
this.groupService.getUserGroups().subscribe(groups => {
this.groups = groups.filter(group => {
if (!group.group_name) { group.group_name = ''; }
return group.group_name.includes(this.searchTerm);
}
);
this.loading = false;
});
if (this.currentTerm) {
this.groupService.searchUserGroupsResponse({
groupname: encodeURIComponent(this.currentTerm),
page: this.currentPage,
pageSize: this.pageSize
})
.pipe(finalize(() => this.loading = false))
.subscribe(
response => {
this.totalCount = Number.parseInt(
response.headers.get('x-total-count'), 10
);
this.groups = response.body as UserGroup[];
},
err => {
this.msgHandler.error(err);
});
} else {
this.groupService.listUserGroupsResponse({
page: this.currentPage,
pageSize: this.pageSize
})
.pipe(finalize(() => this.loading = false))
.subscribe(
response => {
this.totalCount = Number.parseInt(
response.headers.get('x-total-count'), 10
);
this.groups = response.body as UserGroup[];
},
err => {
this.msgHandler.error(err);
});
}
}
addGroup(): void {
@ -130,7 +195,9 @@ export class GroupComponent implements OnInit, OnDestroy {
this.operationService.publishInfo(operMessage);
return this.groupService
.deleteGroup(group.id)
.deleteUserGroup({
groupId: group.id
})
.pipe(flatMap(response => {
return this.translate.get("BATCH.DELETED_SUCCESS").pipe(flatMap(res => {
operateChanges(operMessage, OperationState.success);
@ -169,8 +236,10 @@ export class GroupComponent implements OnInit, OnDestroy {
}
doFilter(groupName: string): void {
this.searchTerm = groupName;
this.loadData();
if (!groupName) {
this.currentTerm = groupName;
this.loadData();
}
}
get canAddGroup(): boolean {
return this.session.currentUser.has_admin_role;

View File

@ -1,18 +0,0 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { GroupService } from './group.service';
describe('GroupService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [GroupService],
imports: [
HttpClientTestingModule
]
});
});
it('should be created', inject([GroupService], (service: GroupService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -1,67 +0,0 @@
import {throwError as observableThrowError, Observable} from "rxjs";
import {catchError, map} from 'rxjs/operators';
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { UserGroup } from "./group";
import { CURRENT_BASE_HREF, HTTP_GET_OPTIONS, HTTP_JSON_OPTIONS } from "../../../shared/units/utils";
const userGroupEndpoint = CURRENT_BASE_HREF + "/usergroups";
const ldapGroupSearchEndpoint = CURRENT_BASE_HREF + "/ldap/groups/search?groupname=";
@Injectable({
providedIn: 'root',
})
export class GroupService {
constructor(private http: HttpClient) {}
private handleErrorObservable(error: Response | any) {
console.error(error.error || error);
return observableThrowError(error.error || error);
}
getUserGroups(): Observable<UserGroup[]> {
return this.http.get<UserGroup[]>(userGroupEndpoint, HTTP_GET_OPTIONS).pipe(
map(response => {
return response || [];
}),
catchError(error => {
return this.handleErrorObservable(error);
}), );
}
createGroup(group: UserGroup): Observable<any> {
return this.http
.post(userGroupEndpoint, group, HTTP_JSON_OPTIONS).pipe(
map(response => {
return response || [];
}),
catchError(this.handleErrorObservable), );
}
editGroup(group: UserGroup): Observable<any> {
return this.http
.put(`${userGroupEndpoint}/${group.id}`, group, HTTP_JSON_OPTIONS).pipe(
map(response => {
return response || [];
}),
catchError(this.handleErrorObservable), );
}
deleteGroup(group_id: number): Observable<any> {
return this.http
.delete(`${userGroupEndpoint}/${group_id}`).pipe(
map(response => {
return response || [];
}),
catchError(this.handleErrorObservable), );
}
searchGroup(group_name: string): Observable<UserGroup[]> {
return this.http
.get<UserGroup[]>(`${ldapGroupSearchEndpoint}${group_name}`, HTTP_GET_OPTIONS).pipe(
map(response => {
return response || [];
}),
catchError(this.handleErrorObservable), );
}
}

View File

@ -1,12 +0,0 @@
export class UserGroup {
id?: number;
group_name?: string;
group_type: number;
ldap_group_dn?: string;
constructor(groupType) {
{
this.group_type = groupType;
}
}
}

View File

@ -1,4 +1,4 @@
<div class="row">
<div class="row relative">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<h2 class="custom-h2">{{'SIDE_NAV.SYSTEM_MGMT.USER' | translate}}</h2>
<div class="action-panel-pos rightPos">

View File

@ -42,8 +42,6 @@
height:32px;
z-index: 100;
}
::ng-deep harbor-user{
>div{
position: relative;
}
.relative {
position: relative;
}