enable server end pagination for project list

This commit is contained in:
Steven Zou 2017-09-08 19:15:50 +08:00
parent d21795cf53
commit 9987d89705
6 changed files with 389 additions and 224 deletions

View File

@ -68,9 +68,9 @@ export class RepositoryStackviewComponent implements OnChanges, OnInit {
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
pullCountComparator: Comparator<Repository> = new CustomComparator<Repository>('pull_count', 'number');
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
tagsCountComparator: Comparator<Repository> = new CustomComparator<Repository>('tags_count', 'number');
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage: number = 1;
@ -277,7 +277,7 @@ export class RepositoryStackviewComponent implements OnChanges, OnInit {
let total: number = this.totalCount - 1;
if (total <= 0) { return null; }
let totalPages: number = Math.floor(total / this.pageSize);
let totalPages: number = Math.ceil(total / this.pageSize);
let targetPageNumber: number = this.currentPage;
if (this.currentPage > totalPages) {

View File

@ -1,10 +1,10 @@
<clr-datagrid (clrDgRefresh)="refresh($event)" [clrDgLoading]="loading">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="showRoleInfo">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let p of projects">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading">
<clr-dg-column [clrDgField]="'name'">{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="accessLevelComparator">{{'PROJECT.ACCESS_LEVEL' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="showRoleInfo" [clrDgSortBy]="roleComparator">{{'PROJECT.ROLE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="repoCountComparator">{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="timeComparator">{{'PROJECT.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let p of projects">
<clr-dg-action-overflow [hidden]="!(p.current_user_role_id === 1 || isSystemAdmin)">
<button class="action-item" (click)="newReplicationRule(p)" [hidden]="!isSystemAdmin">{{'PROJECT.REPLICATION_RULE' | translate}}</button>
<button class="action-item" (click)="toggleProject(p)">{{'PROJECT.MAKE' | translate}} {{(p.public === 0 ? 'PROJECT.PUBLIC' : 'PROJECT.PRIVATE') | translate}} </button>
@ -17,8 +17,7 @@
<clr-dg-cell>{{p.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span>
{{pagination.totalItems }} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} </span> {{pagination.totalItems }} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>

View File

@ -11,7 +11,14 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, EventEmitter, Output, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import {
Component,
Output,
Input,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy
} from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';
import { Project } from '../project';
import { ProjectService } from '../project.service';
@ -19,76 +26,241 @@ import { ProjectService } from '../project.service';
import { SessionService } from '../../shared/session.service';
import { SearchTriggerService } from '../../base/global-search/search-trigger.service';
import { ProjectTypes, RoleInfo } from '../../shared/shared.const';
import { CustomComparator, doFiltering, doSorting, calculatePage } from '../../shared/shared.utils';
import { State } from 'clarity-angular';
import { Comparator, State } from 'clarity-angular';
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
import { StatisticHandler } from '../../shared/statictics/statistic-handler.service';
import { Subscription } from 'rxjs/Subscription';
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
@Component({
selector: 'list-project',
templateUrl: 'list-project.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
selector: 'list-project',
templateUrl: 'list-project.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListProjectComponent {
_filterType: string = ProjectTypes[0];
export class ListProjectComponent implements OnDestroy {
loading: boolean = true;
projects: Project[] = [];
filteredType: number = 0;//All projects
searchKeyword: string = "";
@Input() loading: boolean = true;
@Input() projects: Project[];
@Input()
get filteredType(): string {
return this._filterType;
}
set filteredType(value: string) {
if (value && value.trim() !== "") {
this._filterType = value;
roleInfo = RoleInfo;
repoCountComparator: Comparator<Project> = new CustomComparator<Project>("repo_count", "number");
timeComparator: Comparator<Project> = new CustomComparator<Project>("creation_time", "date");
accessLevelComparator: Comparator<Project> = new CustomComparator<Project>("public", "number");
roleComparator: Comparator<Project> = new CustomComparator<Project>("current_user_role_id", "number");
currentPage: number = 1;
totalCount: number = 0;
pageSize: number = 15;
currentState: State;
subscription: Subscription;
constructor(
private session: SessionService,
private router: Router,
private searchTrigger: SearchTriggerService,
private proService: ProjectService,
private msgHandler: MessageHandlerService,
private statisticHandler: StatisticHandler,
private deletionDialogService: ConfirmationDialogService,
private ref: ChangeDetectorRef) {
this.subscription = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT) {
let projectId = message.data;
this.proService
.deleteProject(projectId)
.subscribe(
response => {
this.msgHandler.showSuccess('PROJECT.DELETED_SUCCESS');
let st: State = this.getStateAfterDeletion();
if (!st) {
this.refresh();
} else {
this.clrLoad(st);
this.statisticHandler.refresh();
}
},
error => {
if (error && error.status === 412) {
this.msgHandler.showError('PROJECT.FAILED_TO_DELETE_PROJECT', '');
} else {
this.msgHandler.handleError(error);
}
}
);
let hnd = setInterval(() => ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
}
});
let hnd = setInterval(() => ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}
}
@Output() paginate = new EventEmitter<State>();
@Output() toggle = new EventEmitter<Project>();
@Output() delete = new EventEmitter<Project>();
roleInfo = RoleInfo;
constructor(
private session: SessionService,
private router: Router,
private searchTrigger: SearchTriggerService,
private ref: ChangeDetectorRef) {
let hnd = setInterval(() => ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 1000);
}
get showRoleInfo(): boolean {
return this.filteredType !== ProjectTypes[2];
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role > 0;
}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'projects', proId, 'repositories'];
this.router.navigate(linkUrl);
}
refresh(state: State) {
this.paginate.emit(state);
}
newReplicationRule(p: Project) {
if (p) {
this.router.navigateByUrl(`/harbor/projects/${p.project_id}/replications?is_create=true`);
get showRoleInfo(): boolean {
return this.filteredType !== 2;
}
}
toggleProject(p: Project) {
this.toggle.emit(p);
}
public get isSystemAdmin(): boolean {
let account = this.session.getCurrentUser();
return account != null && account.has_admin_role > 0;
}
deleteProject(p: Project) {
this.delete.emit(p);
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
goToLink(proId: number): void {
this.searchTrigger.closeSearch(true);
let linkUrl = ['harbor', 'projects', proId, 'repositories'];
this.router.navigate(linkUrl);
}
clrLoad(state: State) {
//Keep state for future filtering and sorting
this.currentState = state;
let pageNumber: number = calculatePage(state);
if (pageNumber <= 0) { pageNumber = 1; }
this.loading = true;
let passInFilteredType: number = undefined;
if (this.filteredType > 0) {
passInFilteredType = this.filteredType - 1;
}
this.proService.listProjects(this.searchKeyword, passInFilteredType, pageNumber, this.pageSize).toPromise()
.then(response => {
//Get total count
if (response.headers) {
let xHeader: string = response.headers.get("X-Total-Count");
if (xHeader) {
this.totalCount = parseInt(xHeader, 0);
}
}
this.projects = response.json() as Project[];
//Do customising filtering and sorting
this.projects = doFiltering<Project>(this.projects, state);
this.projects = doSorting<Project>(this.projects, state);
this.loading = false;
})
.catch(error => {
this.loading = false;
this.msgHandler.handleError(error);
});
//Force refresh view
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}
newReplicationRule(p: Project) {
if (p) {
this.router.navigateByUrl(`/harbor/projects/${p.project_id}/replications?is_create=true`);
}
}
toggleProject(p: Project) {
if (p) {
p.public === 0 ? p.public = 1 : p.public = 0;
this.proService
.toggleProjectPublic(p.project_id, p.public)
.subscribe(
response => {
this.msgHandler.showSuccess('PROJECT.TOGGLED_SUCCESS');
let pp: Project = this.projects.find((item: Project) => item.project_id === p.project_id);
if (pp) {
pp.public = p.public;
this.statisticHandler.refresh();
}
},
error => this.msgHandler.handleError(error)
);
//Force refresh view
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
}
}
deleteProject(p: Project) {
let deletionMessage = new ConfirmationMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
p.name,
p.project_id,
ConfirmationTargets.PROJECT,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
}
refresh(): void {
this.currentPage = 1;
this.filteredType = 0;
this.searchKeyword = "";
this.reload();
this.statisticHandler.refresh();
}
doFilterProject(filter: number): void {
this.currentPage = 1;
this.filteredType = filter;
this.reload();
}
doSearchProject(proName: string): void {
this.currentPage = 1;
this.searchKeyword = proName;
this.reload();
}
reload(): void {
let st: State = this.currentState;
if (!st) {
st = {
page: {}
};
}
st.page.from = 0;
st.page.to = this.pageSize - 1;
st.page.size = this.pageSize;
this.clrLoad(st);
}
getStateAfterDeletion(): State {
let total: number = this.totalCount - 1;
if (total <= 0) { return null; }
let totalPages: number = Math.ceil(total / this.pageSize);
let targetPageNumber: number = this.currentPage;
if (this.currentPage > totalPages) {
targetPageNumber = totalPages;//Should == currentPage -1
}
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = (targetPageNumber - 1) * this.pageSize;
st.page.to = targetPageNumber * this.pageSize - 1;
return st;
}
}

View File

@ -13,7 +13,7 @@
</div>
<div class="option-right">
<div class="select" style="float: left; left:-6px; top:8px;">
<select (change)="doFilterProjects($event)" [(ngModel)]="selecteType">
<select (change)="doFilterProjects()" [(ngModel)]="selecteType">
<option value="0" [selected]="currentFilteredType === 0">{{projectTypes[0] | translate}}</option>
<option value="1">{{projectTypes[1] | translate}}</option>
<option value="2">{{projectTypes[2] | translate}}</option>
@ -25,6 +25,6 @@
</span>
</div>
</div>
<list-project [projects]="changedProjects" [filteredType]="projectTypes[currentFilteredType]" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)" [loading]="loading"></list-project>
<list-project></list-project>
</div>
</div>

View File

@ -11,44 +11,21 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {Component, OnInit, ViewChild, OnDestroy, ChangeDetectorRef, ChangeDetectionStrategy} from '@angular/core';
import { Component, OnInit, ViewChild, OnDestroy} from '@angular/core';
import { Router } from '@angular/router';
import { Project } from './project';
import { ProjectService } from './project.service';
import { CreateProjectComponent } from './create-project/create-project.component';
import { ListProjectComponent } from './list-project/list-project.component';
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
import { Message } from '../global-message/message';
import { Response } from '@angular/http';
import { ConfirmationDialogService } from '../shared/confirmation-dialog/confirmation-dialog.service';
import { ConfirmationMessage } from '../shared/confirmation-dialog/confirmation-message';
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../shared/shared.const';
import { Subscription } from 'rxjs/Subscription';
import { State } from 'clarity-angular';
import { AppConfigService } from '../app-config.service';
import { SessionService } from '../shared/session.service';
import { ProjectTypes } from '../shared/shared.const';
import { StatisticHandler } from '../shared/statictics/statistic-handler.service';
@Component({
selector: 'project',
templateUrl: 'project.component.html',
styleUrls: ['./project.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
styleUrls: ['./project.component.css']
})
export class ProjectComponent implements OnInit, OnDestroy {
changedProjects: Project[];
export class ProjectComponent implements OnInit {
projectTypes = ProjectTypes;
@ViewChild(CreateProjectComponent)
@ -60,65 +37,30 @@ export class ProjectComponent implements OnInit, OnDestroy {
currentFilteredType: number = 0;//all projects
projectName: string = "";
subscription: Subscription;
loading: boolean = true;
get selecteType (): number {
get selecteType(): number {
return this.currentFilteredType;
}
set selecteType(_project: number) {
this.currentFilteredType = _project;
if (window.sessionStorage) {
window.sessionStorage['projectTypeValue'] = _project;
}
}
constructor(
private projectService: ProjectService,
private messageHandlerService: MessageHandlerService,
private appConfigService: AppConfigService,
private sessionService: SessionService,
private deletionDialogService: ConfirmationDialogService,
private statisticHandler: StatisticHandler,
private ref: ChangeDetectorRef) {
this.subscription = deletionDialogService.confirmationConfirm$.subscribe(message => {
if (message &&
message.state === ConfirmationState.CONFIRMED &&
message.source === ConfirmationTargets.PROJECT) {
let projectId = message.data;
this.projectService
.deleteProject(projectId)
.subscribe(
response => {
this.messageHandlerService.showSuccess('PROJECT.DELETED_SUCCESS');
this.retrieve();
this.statisticHandler.refresh();
},
error => {
if (error && error.status === 412) {
this.messageHandlerService.showError('PROJECT.FAILED_TO_DELETE_PROJECT', '');
} else {
this.messageHandlerService.handleError(error);
}
}
);
}
});
private sessionService: SessionService) {
}
ngOnInit(): void {
if (window.sessionStorage && window.sessionStorage['projectTypeValue'] && window.sessionStorage['fromDetails']) {
if (window.sessionStorage && window.sessionStorage['projectTypeValue'] && window.sessionStorage['fromDetails']) {
this.currentFilteredType = +window.sessionStorage['projectTypeValue'];
window.sessionStorage.removeItem('fromDetails');
}
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
get projectCreationRestriction(): boolean {
let account = this.sessionService.getCurrentUser();
if (account) {
@ -132,105 +74,29 @@ export class ProjectComponent implements OnInit, OnDestroy {
return false;
}
retrieve(state?: State): void {
this.projectName = "";
if (this.currentFilteredType !== 0) {
this.getProjects('', this.currentFilteredType - 1);
return;
}
this.getProjects();
}
getProjects(name?: string, isPublic?: number, page?: number, pageSize?: number): void {
this.loading = true;
this.projectService
.listProjects(name, isPublic, page, pageSize)
.subscribe(
response => {
this.changedProjects = response.json();
this.loading = false;
},
error => {
this.loading = false;
this.messageHandlerService.handleError(error);
}
);
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
setTimeout(()=>clearInterval(hnd), 2000);
}
openModal(): void {
this.creationProject.newProject();
}
createProject(created: boolean) {
if (created) {
this.retrieve();
this.statisticHandler.refresh();
this.refresh();
}
}
doSearchProjects(projectName: string): void {
this.projectName = projectName;
if (projectName === "") {
if (this.currentFilteredType === 0) {
this.getProjects();
} else {
this.getProjects(projectName, this.currentFilteredType - 1);
}
} else {
this.getProjects(projectName);
}
this.listProject.doSearchProject(this.projectName);
}
doFilterProjects($event: any): void {
if ($event && $event.target && $event.target["value"]) {
this.projectName = "";
this.currentFilteredType = +$event.target["value"];
if (this.currentFilteredType === 0) {
this.getProjects();
} else {
this.getProjects("", this.currentFilteredType - 1);
}
}
}
toggleProject(p: Project) {
if (p) {
p.public === 0 ? p.public = 1 : p.public = 0;
this.projectService
.toggleProjectPublic(p.project_id, p.public)
.subscribe(
response => {
this.messageHandlerService.showSuccess('PROJECT.TOGGLED_SUCCESS');
this.statisticHandler.refresh();
if (this.currentFilteredType === 0) {
this.getProjects();
} else {
this.getProjects("", this.currentFilteredType - 1);
}
},
error => this.messageHandlerService.handleError(error)
);
}
}
deleteProject(p: Project) {
let deletionMessage = new ConfirmationMessage(
'PROJECT.DELETION_TITLE',
'PROJECT.DELETION_SUMMARY',
p.name,
p.project_id,
ConfirmationTargets.PROJECT,
ConfirmationButtons.DELETE_CANCEL
);
this.deletionDialogService.openComfirmDialog(deletionMessage);
doFilterProjects(): void {
this.listProject.doFilterProject(this.selecteType);
}
refresh(): void {
this.currentFilteredType = 0;
this.retrieve();
this.statisticHandler.refresh();
this.projectName = "";
this.listProject.refresh();
}
}

View File

@ -14,6 +14,8 @@
import { NgForm } from '@angular/forms';
import { httpStatusCode, AlertType } from './shared.const';
import { MessageService } from '../global-message/message.service';
import { Comparator, State } from 'clarity-angular';
/**
* To handle the error message body
*
@ -106,4 +108,130 @@ export const maintainUrlQueryParmas = function (uri: string, key: string, value:
return uri + separator + key + "=" + value + hash;
}
}
}
//Copy from ui library utils.ts
/**
* Calculate page number by state
*/
export function calculatePage(state: State): number {
if (!state || !state.page) {
return 1;
}
return Math.ceil((state.page.to + 1) / state.page.size);
}
/**
* Comparator for fields with specific type.
*
*/
export class CustomComparator<T> implements Comparator<T> {
fieldName: string;
type: string;
constructor(fieldName: string, type: string) {
this.fieldName = fieldName;
this.type = type;
}
compare(a: { [key: string]: any | any[] }, b: { [key: string]: any | any[] }) {
let comp = 0;
if (a && b) {
let fieldA = a[this.fieldName];
let fieldB = b[this.fieldName];
switch (this.type) {
case "number":
comp = fieldB - fieldA;
break;
case "date":
comp = new Date(fieldB).getTime() - new Date(fieldA).getTime();
break;
}
}
return comp;
}
}
/**
* Filter columns via RegExp
*
* @export
* @param {State} state
* @returns {void}
*/
export function doFiltering<T extends { [key: string]: any | any[] }>(items: T[], state: State): T[] {
if (!items || items.length === 0) {
return items;
}
if (!state || !state.filters || state.filters.length === 0) {
return items;
}
state.filters.forEach((filter: {
property: string;
value: string;
}) => {
items = items.filter(item => regexpFilter(filter["value"], item[filter["property"]]));
});
return items;
}
/**
* Match items via RegExp
*
* @export
* @param {string} terms
* @param {*} testedValue
* @returns {boolean}
*/
export function regexpFilter(terms: string, testedValue: any): boolean {
let reg = new RegExp('.*' + terms + '.*', 'i');
return reg.test(testedValue);
}
/**
* Sorting the data by column
*
* @export
* @template T
* @param {T[]} items
* @param {State} state
* @returns {T[]}
*/
export function doSorting<T extends { [key: string]: any | any[] }>(items: T[], state: State): T[] {
if (!items || items.length === 0) {
return items;
}
if (!state || !state.sort) {
return items;
}
return items.sort((a: T, b: T) => {
let comp: number = 0;
if (typeof state.sort.by !== "string") {
comp = state.sort.by.compare(a, b);
} else {
let propA = a[state.sort.by.toString()], propB = b[state.sort.by.toString()];
if (typeof propA === "string") {
comp = propA.localeCompare(propB);
} else {
if (propA > propB) {
comp = 1;
} else if (propA < propB) {
comp = -1;
}
}
}
if (state.sort.reverse) {
comp = -comp;
}
return comp;
});
}