Fix filter bug for replication tasks page

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
AllForNothing 2020-12-21 14:02:20 +08:00
parent 9bc6f3cee4
commit b749ba4e54
5 changed files with 185 additions and 52 deletions

View File

@ -1,7 +1,7 @@
import { TestBed, inject } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ReplicationService } from "../../../lib/services";
import { ReplicationTasksRoutingResolverService } from "./replication-tasks-routing-resolver.service";
import { ReplicationService } from "../../../../ng-swagger-gen/services";
describe('ReplicationTasksRoutingResolverService', () => {
beforeEach(() => {

View File

@ -15,25 +15,26 @@ import { Injectable } from '@angular/core';
import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { map, catchError } from "rxjs/operators";
import { ReplicationJob, ReplicationService } from "../../../lib/services";
import { ReplicationService } from "../../../../ng-swagger-gen/services";
import { ReplicationExecution } from "../../../../ng-swagger-gen/models/replication-execution";
@Injectable({
providedIn: 'root'
})
export class ReplicationTasksRoutingResolverService implements Resolve<ReplicationJob> {
export class ReplicationTasksRoutingResolverService implements Resolve<ReplicationExecution> {
constructor(
private replicationService: ReplicationService,
private router: Router) { }
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ReplicationJob> | any {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ReplicationExecution> | any {
// Support both parameters and query parameters
let executionId = route.params['id'];
if (!executionId) {
executionId = route.queryParams['project_id'];
}
return this.replicationService.getExecutionById(executionId)
.pipe(map((res: ReplicationJob) => {
return this.replicationService.getReplicationExecution(+executionId)
.pipe(map((res: ReplicationExecution) => {
if (!res) {
this.router.navigate(['/harbor', 'projects']);
}

View File

@ -14,15 +14,15 @@
<h2 class="custom-h2 h2-style">{{executionId}}</h2>
</div>
<div>
<div class="status-progress" *ngIf="executions && executions['status'] === 'InProgress'">
<div class="status-progress" *ngIf="execution && execution['status'] === 'InProgress'">
<span class="spinner spinner-inline"></span>
<span>{{'REPLICATION.IN_PROGRESS'| translate}}</span>
</div>
<div class="status-success" *ngIf="executions && executions['status'] === 'Succeed'">
<div class="status-success" *ngIf="execution && execution['status'] === 'Succeed'">
<clr-icon size="18" shape="success-standard" class="color-green"></clr-icon>
<span>{{'REPLICATION.SUCCESS'| translate}}</span>
</div>
<div class="status-failed" *ngIf="executions && executions['status'] === 'Failed'">
<div class="status-failed" *ngIf="execution && execution['status'] === 'Failed'">
<clr-icon size="18" shape="error-standard" class="color-red"></clr-icon>
<span>{{'REPLICATION.FAILURE'| translate}}</span>
</div>
@ -75,12 +75,12 @@
</section>
<div class="tasks-detail">
<h3 class="modal-title">Tasks</h3>
<h3 class="modal-title">{{'P2P_PROVIDER.TASKS' | translate}}</h3>
<div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="action-select">
<div class="select filter-tag" [hidden]="!isOpenFilterTag">
<select (change)="selectFilter($event)">
<option value="resource_type">{{'REPLICATION.RESOURCE_TYPE' |translate}}</option>
<option value="resourceType">{{'REPLICATION.RESOURCE_TYPE' |translate}}</option>
<option value="status">{{'REPLICATION.STATUS' | translate}}</option>
</select>
</div>
@ -93,15 +93,16 @@
</div>
</div>
<clr-datagrid (clrDgRefresh)="clrLoadTasks($event)" [clrDgLoading]="loading">
<clr-dg-column [clrDgSortBy]="'id'">{{'REPLICATION.TASK_ID'| translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'resource_type'" class="resource-width">{{'REPLICATION.RESOURCE_TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.TASK_ID'| translate}}</clr-dg-column>
<clr-dg-column class="resource-width">{{'REPLICATION.RESOURCE_TYPE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'src_resource'">{{'REPLICATION.SOURCE' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'dst_resource'">{{'REPLICATION.DESTINATION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="endTimeComparator">{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'P2P_PROVIDER.TASKS_PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let t of tasks">
<clr-dg-cell>{{t.id}}</clr-dg-cell>
<clr-dg-cell class="resource-width">{{t.resource_type}}</clr-dg-cell>

View File

@ -0,0 +1,120 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { ReplicationExecution } from "../../../../../ng-swagger-gen/models/replication-execution";
import { ReplicationTasksComponent } from "./replication-tasks.component";
import { ActivatedRoute } from "@angular/router";
import { ReplicationService } from "../../../../../ng-swagger-gen/services";
import { ErrorHandler } from "../../../utils/error-handler";
import { RouterTestingModule } from "@angular/router/testing";
import { TranslateModule } from "@ngx-translate/core";
import { of, Subscription } from "rxjs";
import { delay } from "rxjs/operators";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { ClarityModule, ClrDatagridStateInterface } from "@clr/angular";
import { ReplicationTask } from "../../../../../ng-swagger-gen/models/replication-task";
describe('ReplicationTasksComponent', () => {
const mockJob: ReplicationExecution = {
id: 1,
status: "Failed",
policy_id: 1,
trigger: "Manual",
total: 0,
failed: 1,
succeed: 0,
in_progress: 0,
stopped: 0
};
const mockTask: ReplicationTask = {
"dst_resource": "library/lightstreamer [1 item(s) in total]",
"end_time": "2020-12-21T05:56:04.000Z",
"execution_id": 15,
"id": 30,
"job_id": "8f45cd0c512ba3d8f23ee3fa",
"operation": "copy",
"resource_type": "image",
"src_resource": "library/lightstreamer [1 item(s) in total]",
"start_time": "2020-12-21T05:56:03.000Z",
"status": "Failed"
};
let fixture: ComponentFixture<ReplicationTasksComponent>;
let comp: ReplicationTasksComponent;
const fakedErrorHandler = {
error() {
}
};
const fakedActivatedRoute = {
snapshot: {
params: {
id: 1
},
data: {
replicationTasksRoutingResolver: mockJob
}
}
};
const fakedReplicationService = {
listReplicationTasksResponse() {
return of(new HttpResponse({
body: [mockTask],
headers: new HttpHeaders({
"x-total-count": "1"
})
})).pipe(delay(0));
},
getReplicationExecution() {
return of(mockJob).pipe(delay(0));
}
};
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA,
NO_ERRORS_SCHEMA],
imports: [
NoopAnimationsModule,
RouterTestingModule,
ClarityModule,
TranslateModule.forRoot()
],
declarations: [
ReplicationTasksComponent,
],
providers: [
{provide: ErrorHandler, useValue: fakedErrorHandler},
{provide: ReplicationService, useValue: fakedReplicationService},
{provide: ActivatedRoute, useValue: fakedActivatedRoute}
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(ReplicationTasksComponent);
comp = fixture.componentInstance;
comp.timerDelay = new Subscription();
comp.getExecutionDetail();
fixture.detectChanges();
});
afterEach(() => {
if (comp.timerDelay) {
comp.timerDelay.unsubscribe();
comp.timerDelay = null;
}
});
it('should be created', () => {
expect(comp).toBeTruthy();
});
it('job status should be failed', async () => {
fixture.detectChanges();
await fixture.whenStable();
const span: HTMLSpanElement = fixture.nativeElement.querySelector('.status-failed>span');
expect(span.innerText).toEqual('REPLICATION.FAILURE');
});
it('should render task list', async () => {
fixture.autoDetectChanges();
await fixture.whenStable();
const row = fixture.nativeElement.querySelectorAll('clr-dg-row');
expect(row.length).toEqual(1);
});
});

View File

@ -1,20 +1,23 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ReplicationService } from "../../../services/replication.service";
import { TranslateService } from '@ngx-translate/core';
import { finalize } from "rxjs/operators";
import { Subscription, timer } from "rxjs";
import { ErrorHandler } from "../../../utils/error-handler/error-handler";
import { ReplicationJob, ReplicationTasks, Comparator, ReplicationJobItem, State } from "../../../services/interface";
import { CustomComparator, DEFAULT_PAGE_SIZE } from "../../../utils/utils";
import { RequestQueryParams } from "../../../services/RequestQueryParams";
import { ErrorHandler } from "../../../utils/error-handler";
import { ClrDatagridComparatorInterface, ReplicationJob, ReplicationTasks } from "../../../services";
import { CURRENT_BASE_HREF, CustomComparator, DEFAULT_PAGE_SIZE, doFiltering, doSorting } from "../../../utils/utils";
import { REFRESH_TIME_DIFFERENCE } from '../../../entities/shared.const';
import { ClrDatagridStateInterface } from '@clr/angular';
import { ReplicationExecution } from "../../../../../ng-swagger-gen/models/replication-execution";
import { ReplicationService } from "../../../../../ng-swagger-gen/services";
import ListReplicationTasksParams = ReplicationService.ListReplicationTasksParams;
import { ReplicationTask } from "../../../../../ng-swagger-gen/models/replication-task";
const executionStatus = 'InProgress';
const STATUS_MAP = {
"Succeed": "Succeeded"
};
const SUCCEED: string = 'Succeed';
@Component({
selector: 'replication-tasks',
templateUrl: './replication-tasks.component.html',
@ -28,18 +31,18 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
totalCount: number;
loading = true;
searchTask: string;
defaultFilter = "resource_type";
tasks: ReplicationTasks[];
defaultFilter = "resourceType";
tasks: ReplicationTask[];
taskItem: ReplicationTasks[] = [];
tasksCopy: ReplicationTasks[] = [];
stopOnGoing: boolean;
executions: ReplicationJobItem[];
execution: ReplicationExecution;
timerDelay: Subscription;
executionId: string;
startTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
startTimeComparator: ClrDatagridComparatorInterface<ReplicationTask> = new CustomComparator<
ReplicationJob
>("start_time", "date");
endTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
endTimeComparator: ClrDatagridComparatorInterface<ReplicationTask> = new CustomComparator<
ReplicationJob
>("end_time", "date");
@ -56,18 +59,17 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
this.executionId = this.route.snapshot.params['id'];
const resolverData = this.route.snapshot.data;
if (resolverData) {
const replicationJob = <ReplicationJob>(resolverData["replicationTasksRoutingResolver"]);
this.executions = replicationJob.data;
this.execution = <ReplicationExecution>(resolverData["replicationTasksRoutingResolver"]);
this.clrLoadPage();
}
}
getExecutionDetail(): void {
this.inProgress = true;
if (this.executionId) {
this.replicationService.getExecutionById(this.executionId)
this.replicationService.getReplicationExecution(+this.executionId)
.pipe(finalize(() => (this.inProgress = false)))
.subscribe(res => {
this.executions = res.data;
this.execution = res;
this.clrLoadPage();
},
error => {
@ -80,12 +82,12 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
if (!this.timerDelay) {
this.timerDelay = timer(REFRESH_TIME_DIFFERENCE, REFRESH_TIME_DIFFERENCE).subscribe(() => {
let count: number = 0;
if (this.executions['status'] === executionStatus) {
if (this.execution['status'] === executionStatus) {
count++;
}
if (count > 0) {
this.getExecutionDetail();
let state: State = {
let state: ClrDatagridStateInterface = {
page: {}
};
this.clrLoadTasks(state);
@ -98,36 +100,36 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
}
public get trigger(): string {
return this.executions && this.executions['trigger']
? this.executions['trigger']
return this.execution && this.execution['trigger']
? this.execution['trigger']
: "";
}
public get startTime(): Date {
return this.executions && this.executions['start_time']
? this.executions['start_time']
public get startTime(): string {
return this.execution && this.execution['start_time']
? this.execution['start_time']
: null;
}
public get successNum(): string {
return this.executions && this.executions['succeed'];
public get successNum(): number {
return this.execution && this.execution['succeed'];
}
public get failedNum(): string {
return this.executions && this.executions['failed'];
public get failedNum(): number {
return this.execution && this.execution['failed'];
}
public get progressNum(): string {
return this.executions && this.executions['in_progress'];
public get progressNum(): number {
return this.execution && this.execution['in_progress'];
}
public get stoppedNum(): string {
return this.executions && this.executions['stopped'];
public get stoppedNum(): number {
return this.execution && this.execution['stopped'];
}
stopJob() {
this.stopOnGoing = true;
this.replicationService.stopJobs(this.executionId)
this.replicationService.stopReplication(+this.executionId)
.subscribe(response => {
this.stopOnGoing = false;
this.getExecutionDetail();
@ -141,7 +143,7 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
}
viewLog(taskId: number | string): string {
return this.replicationService.getJobBaseUrl() + "/executions/" + this.executionId + "/tasks/" + taskId + "/log";
return CURRENT_BASE_HREF + "/replication" + "/executions/" + this.executionId + "/tasks/" + taskId + "/log";
}
ngOnDestroy() {
@ -157,14 +159,20 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
if (state && state.page && state.page.size) {
this.pageSize = state.page.size;
}
let params: RequestQueryParams = new RequestQueryParams();
params = params.set('page_size', this.pageSize + '').set('page', this.currentPage + '');
const param: ListReplicationTasksParams = {
id: +this.executionId,
page: this.currentPage,
pageSize: this.pageSize,
};
if (this.searchTask && this.searchTask !== "") {
params = params.set(this.defaultFilter, this.searchTask);
if (this.searchTask === STATUS_MAP.Succeed && this.defaultFilter === 'status') {// convert 'Succeeded' to 'Succeed'
param[this.defaultFilter] = SUCCEED;
} else {
param[this.defaultFilter] = this.searchTask;
}
}
this.loading = true;
this.replicationService.getReplicationTasks(this.executionId, params)
this.replicationService.listReplicationTasksResponse(param)
.pipe(finalize(() => {
this.loading = false;
}))
@ -176,6 +184,9 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
}
}
this.tasks = res.body; // Keep the data
// Do customising filtering and sorting
this.tasks = doFiltering<ReplicationTask>(this.tasks, state);
this.tasks = doSorting<ReplicationTask>(this.tasks, state);
},
error => {
this.errorHandler.error(error);
@ -193,7 +204,7 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
// refresh icon
refreshTasks(): void {
this.currentPage = 1;
let state: State = {
let state: ClrDatagridStateInterface = {
page: {}
};
this.clrLoadTasks(state);
@ -202,7 +213,7 @@ export class ReplicationTasksComponent implements OnInit, OnDestroy {
public doSearch(value: string): void {
this.currentPage = 1;
this.searchTask = value.trim();
let state: State = {
let state: ClrDatagridStateInterface = {
page: {}
};
this.clrLoadTasks(state);