From 9987d897056ed7ca0d4370989ba56f8bcf36e751 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Fri, 8 Sep 2017 19:15:50 +0800 Subject: [PATCH] enable server end pagination for project list --- .../repository-stackview.component.ts | 6 +- .../list-project/list-project.component.html | 19 +- .../list-project/list-project.component.ts | 296 ++++++++++++++---- .../src/app/project/project.component.html | 4 +- .../src/app/project/project.component.ts | 160 +--------- src/ui_ng/src/app/shared/shared.utils.ts | 128 ++++++++ 6 files changed, 389 insertions(+), 224 deletions(-) diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts index ac2dc7bfa..1ec92911d 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.ts @@ -68,9 +68,9 @@ export class RepositoryStackviewComponent implements OnChanges, OnInit { @ViewChild('confirmationDialog') confirmationDialog: ConfirmationDialogComponent; - pullCountComparator: Comparator = new CustomComparator('pull_count', 'number'); + pullCountComparator: Comparator = new CustomComparator('pull_count', 'number'); - tagsCountComparator: Comparator = new CustomComparator('tags_count', 'number'); + tagsCountComparator: Comparator = new CustomComparator('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) { diff --git a/src/ui_ng/src/app/project/list-project/list-project.component.html b/src/ui_ng/src/app/project/list-project/list-project.component.html index bc7c3c3ce..4c57c22f5 100644 --- a/src/ui_ng/src/app/project/list-project/list-project.component.html +++ b/src/ui_ng/src/app/project/list-project/list-project.component.html @@ -1,10 +1,10 @@ - - {{'PROJECT.NAME' | translate}} - {{'PROJECT.ACCESS_LEVEL' | translate}} - {{'PROJECT.ROLE' | translate}} - {{'PROJECT.REPO_COUNT'| translate}} - {{'PROJECT.CREATION_TIME' | translate}} - + + {{'PROJECT.NAME' | translate}} + {{'PROJECT.ACCESS_LEVEL' | translate}} + {{'PROJECT.ROLE' | translate}} + {{'PROJECT.REPO_COUNT'| translate}} + {{'PROJECT.CREATION_TIME' | translate}} + @@ -17,8 +17,7 @@ {{p.creation_time | date: 'short'}} - {{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} - {{pagination.totalItems }} {{'PROJECT.ITEMS' | translate}} - + {{pagination.firstItem + 1}} - {{pagination.lastItem +1 }} {{'PROJECT.OF' | translate}} {{pagination.totalItems }} {{'PROJECT.ITEMS' | translate}} + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/list-project/list-project.component.ts b/src/ui_ng/src/app/project/list-project/list-project.component.ts index ccb579d08..4e4cfd4d8 100644 --- a/src/ui_ng/src/app/project/list-project/list-project.component.ts +++ b/src/ui_ng/src/app/project/list-project/list-project.component.ts @@ -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 = new CustomComparator("repo_count", "number"); + timeComparator: Comparator = new CustomComparator("creation_time", "date"); + accessLevelComparator: Comparator = new CustomComparator("public", "number"); + roleComparator: Comparator = new CustomComparator("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(); - @Output() toggle = new EventEmitter(); - @Output() delete = new EventEmitter(); - - 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(this.projects, state); + this.projects = doSorting(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; + } } \ No newline at end of file diff --git a/src/ui_ng/src/app/project/project.component.html b/src/ui_ng/src/app/project/project.component.html index d55f95930..11bbfde3e 100644 --- a/src/ui_ng/src/app/project/project.component.html +++ b/src/ui_ng/src/app/project/project.component.html @@ -13,7 +13,7 @@
- @@ -25,6 +25,6 @@
- + \ No newline at end of file diff --git a/src/ui_ng/src/app/project/project.component.ts b/src/ui_ng/src/app/project/project.component.ts index f81017403..876e91bd6 100644 --- a/src/ui_ng/src/app/project/project.component.ts +++ b/src/ui_ng/src/app/project/project.component.ts @@ -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(); } } \ No newline at end of file diff --git a/src/ui_ng/src/app/shared/shared.utils.ts b/src/ui_ng/src/app/shared/shared.utils.ts index 5e4ca4ffe..6f04d8542 100644 --- a/src/ui_ng/src/app/shared/shared.utils.ts +++ b/src/ui_ng/src/app/shared/shared.utils.ts @@ -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 implements Comparator { + + 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(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(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; + }); } \ No newline at end of file