Merge pull request #7302 from pureshine/registry-update

Fix replication ui related issues
This commit is contained in:
Wenkai Yin 2019-04-09 20:55:44 +08:00 committed by GitHub
commit 2644c52926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 333 additions and 212 deletions

View File

@ -31,8 +31,6 @@ import { InlineAlertComponent } from "../inline-alert/inline-alert.component";
import { Endpoint } from "../service/interface"; import { Endpoint } from "../service/interface";
import { clone, compareValue, isEmptyObject } from "../utils"; import { clone, compareValue, isEmptyObject } from "../utils";
const FAKE_PASSWORD = "rjGcfuRu"; const FAKE_PASSWORD = "rjGcfuRu";
@Component({ @Component({
@ -72,14 +70,17 @@ export class CreateEditEndpointComponent
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private translateService: TranslateService, private translateService: TranslateService,
private ref: ChangeDetectorRef private ref: ChangeDetectorRef
) { } ) {}
ngOnInit(): void { ngOnInit(): void {
this.endpointService.getAdapters().subscribe(adapters => { this.endpointService.getAdapters().subscribe(
this.adapterList = adapters || []; adapters => {
}, error => { this.adapterList = adapters || [];
this.errorHandler.error(error); },
}); error => {
this.errorHandler.error(error);
}
);
} }
public get isValid(): boolean { public get isValid(): boolean {
@ -118,7 +119,7 @@ export class CreateEditEndpointComponent
insecure: false, insecure: false,
name: "", name: "",
type: "harbor", type: "harbor",
url: "", url: ""
}; };
} }
@ -167,8 +168,8 @@ export class CreateEditEndpointComponent
this.translateService this.translateService
.get("DESTINATION.TITLE_EDIT") .get("DESTINATION.TITLE_EDIT")
.subscribe(res => (this.modalTitle = res)); .subscribe(res => (this.modalTitle = res));
this.endpointService.getEndpoint(targetId) this.endpointService.getEndpoint(targetId).subscribe(
.subscribe(target => { target => {
this.target = target; this.target = target;
// Keep data cache // Keep data cache
this.initVal = clone(target); this.initVal = clone(target);
@ -179,7 +180,9 @@ export class CreateEditEndpointComponent
this.open(); this.open();
this.controlEnabled = true; this.controlEnabled = true;
this.forceRefreshView(2000); this.forceRefreshView(2000);
}, error => this.errorHandler.error(error)); },
error => this.errorHandler.error(error)
);
} else { } else {
this.endpointId = ""; this.endpointId = "";
this.translateService this.translateService
@ -213,18 +216,20 @@ export class CreateEditEndpointComponent
} }
this.testOngoing = true; this.testOngoing = true;
this.endpointService.pingEndpoint(payload) this.endpointService.pingEndpoint(payload).subscribe(
.subscribe(response => { response => {
this.inlineAlert.showInlineSuccess({ this.inlineAlert.showInlineSuccess({
message: "DESTINATION.TEST_CONNECTION_SUCCESS" message: "DESTINATION.TEST_CONNECTION_SUCCESS"
}); });
this.forceRefreshView(2000); this.forceRefreshView(2000);
this.testOngoing = false; this.testOngoing = false;
}, error => { },
error => {
this.inlineAlert.showInlineError("DESTINATION.TEST_CONNECTION_FAILURE"); this.inlineAlert.showInlineError("DESTINATION.TEST_CONNECTION_FAILURE");
this.forceRefreshView(2000); this.forceRefreshView(2000);
this.testOngoing = false; this.testOngoing = false;
}); }
);
} }
onSubmit() { onSubmit() {
@ -240,8 +245,8 @@ export class CreateEditEndpointComponent
return; // Avoid duplicated submitting return; // Avoid duplicated submitting
} }
this.onGoing = true; this.onGoing = true;
this.endpointService.createEndpoint(this.target) this.endpointService.createEndpoint(this.target).subscribe(
.subscribe(response => { response => {
this.translateService this.translateService
.get("DESTINATION.CREATED_SUCCESS") .get("DESTINATION.CREATED_SUCCESS")
.subscribe(res => this.errorHandler.info(res)); .subscribe(res => this.errorHandler.info(res));
@ -249,14 +254,16 @@ export class CreateEditEndpointComponent
this.onGoing = false; this.onGoing = false;
this.close(); this.close();
this.forceRefreshView(2000); this.forceRefreshView(2000);
}, error => { },
error => {
this.onGoing = false; this.onGoing = false;
let errorMessageKey = this.handleErrorMessageKey(error.status); let errorMessageKey = this.handleErrorMessageKey(error.status);
this.translateService.get(errorMessageKey).subscribe(res => { this.translateService.get(errorMessageKey).subscribe(res => {
this.inlineAlert.showInlineError(res); this.inlineAlert.showInlineError(res);
}); });
this.forceRefreshView(2000); this.forceRefreshView(2000);
}); }
);
} }
updateEndpoint() { updateEndpoint() {
@ -272,6 +279,7 @@ export class CreateEditEndpointComponent
if (isEmptyObject(changes)) { if (isEmptyObject(changes)) {
return; return;
} }
let changekeys: { [key: string]: any } = Object.keys(changes); let changekeys: { [key: string]: any } = Object.keys(changes);
changekeys.forEach((key: string) => { changekeys.forEach((key: string) => {
@ -283,8 +291,8 @@ export class CreateEditEndpointComponent
} }
this.onGoing = true; this.onGoing = true;
this.endpointService.updateEndpoint(this.target.id, payload) this.endpointService.updateEndpoint(this.target.id, payload).subscribe(
.subscribe(response => { response => {
this.translateService this.translateService
.get("DESTINATION.UPDATED_SUCCESS") .get("DESTINATION.UPDATED_SUCCESS")
.subscribe(res => this.errorHandler.info(res)); .subscribe(res => this.errorHandler.info(res));
@ -292,14 +300,16 @@ export class CreateEditEndpointComponent
this.close(); this.close();
this.onGoing = false; this.onGoing = false;
this.forceRefreshView(2000); this.forceRefreshView(2000);
}, error => { },
error => {
let errorMessageKey = this.handleErrorMessageKey(error.status); let errorMessageKey = this.handleErrorMessageKey(error.status);
this.translateService.get(errorMessageKey).subscribe(res => { this.translateService.get(errorMessageKey).subscribe(res => {
this.inlineAlert.showInlineError(res); this.inlineAlert.showInlineError(res);
}); });
this.onGoing = false; this.onGoing = false;
this.forceRefreshView(2000); this.forceRefreshView(2000);
}); }
);
} }
handleErrorMessageKey(status: number): string { handleErrorMessageKey(status: number): string {
@ -353,7 +363,6 @@ export class CreateEditEndpointComponent
if (!compareValue(this.formValues, data)) { if (!compareValue(this.formValues, data)) {
this.formValues = data; this.formValues = data;
this.inlineAlert.close();
} }
} }
} }
@ -368,20 +377,36 @@ export class CreateEditEndpointComponent
} }
for (let prop of Object.keys(this.target)) { for (let prop of Object.keys(this.target)) {
let field: any = this.initVal[prop]; let field: any = this.initVal[prop];
if (!compareValue(field, this.target[prop])) { if (typeof field !== "object") {
changes[prop] = this.target[prop]; if (!compareValue(field, this.target[prop])) {
// Number changes[prop] = this.target[prop];
if (typeof field === "number") { // Number
changes[prop] = +changes[prop]; if (typeof field === "number") {
} changes[prop] = +changes[prop];
}
// Trim string value // Trim string value
if (typeof field === "string") { if (typeof field === "string") {
changes[prop] = ("" + changes[prop]).trim(); changes[prop] = ("" + changes[prop]).trim();
}
}
} else {
for (let pro of Object.keys(field)) {
if (!compareValue(field[pro], this.target[prop][pro])) {
changes[pro] = this.target[prop][pro];
// Number
if (typeof field[pro] === "number") {
changes[pro] = +changes[pro];
}
// Trim string value
if (typeof field[pro] === "string") {
changes[pro] = ("" + changes[pro]).trim();
}
}
} }
} }
} }
return changes; return changes;
} }
} }

View File

@ -6,7 +6,8 @@
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasDeleteReplicationPermission" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="hasDeleteReplicationPermission" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasExecuteReplicationPermission" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon>&nbsp;{{'REPLICATION.REPLICATE' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="hasExecuteReplicationPermission" [disabled]="!selectedRow" (click)="replicateRule(selectedRow)"><clr-icon shape="export" size="16"></clr-icon>&nbsp;{{'REPLICATION.REPLICATE' | translate}}</button>
</clr-dg-action-bar> </clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'REPLICATION.NAME' | translate}}</clr-dg-column> <clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'status'" class="status-width">{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column class="min-width">{{'REPLICATION.SRC_NAMESPACE' | translate}}</clr-dg-column> <clr-dg-column class="min-width">{{'REPLICATION.SRC_NAMESPACE' | translate}}</clr-dg-column>
<clr-dg-column>{{'REPLICATION.REPLICATION_MODE' | translate}}</clr-dg-column> <clr-dg-column>{{'REPLICATION.REPLICATION_MODE' | translate}}</clr-dg-column>
<clr-dg-column class="min-width">{{'REPLICATION.DESTINATION_NAMESPACE' | translate}}</clr-dg-column> <clr-dg-column class="min-width">{{'REPLICATION.DESTINATION_NAMESPACE' | translate}}</clr-dg-column>
@ -15,6 +16,17 @@
<clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder> <clr-dg-placeholder>{{'REPLICATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let p of changedRules; let i=index" [clrDgItem]="p" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''"> <clr-dg-row *clrDgItems="let p of changedRules; let i=index" [clrDgItem]="p" [style.backgroundColor]="(projectScope && withReplicationJob && selectedId === p.id) ? '#eee' : ''">
<clr-dg-cell>{{p.name}}</clr-dg-cell> <clr-dg-cell>{{p.name}}</clr-dg-cell>
<clr-dg-cell class="status-width">
<div [ngSwitch]="p.enabled">
<clr-tooltip *ngSwitchCase="false" class="tooltip-lg">
<clr-icon clrTooltipTrigger shape="exclamation-triangle" class="is-warning text-alignment" size="22"></clr-icon>Disabled
<clr-tooltip-content clrPosition="top-right" clrSize="xs" *clrIfOpen>
<span>{{'REPLICATION.RULE_DISABLED' | translate}}</span>
</clr-tooltip-content>
</clr-tooltip>
<div *ngSwitchCase="true" ><clr-icon shape="success-standard" class="is-success text-alignment" size="18"></clr-icon> Enabled</div>
</div>
</clr-dg-cell>
<clr-dg-cell class="min-width"> <clr-dg-cell class="min-width">
{{p.src_registry ? p.src_registry.name : ''}} : {{p.src_namespaces?.length>0 ? p.src_namespaces[0]: ''}} {{p.src_registry ? p.src_registry.name : ''}} : {{p.src_namespaces?.length>0 ? p.src_namespaces[0]: ''}}
<clr-tooltip> <clr-tooltip>
@ -28,7 +40,7 @@
{{p.src_registry && p.src_registry.id > 0 ? 'pull-based' : 'push-based'}} {{p.src_registry && p.src_registry.id > 0 ? 'pull-based' : 'push-based'}}
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell class="min-width"> <clr-dg-cell class="min-width">
{{p.dest_registry ? p.dest_registry.name : ''}} : {{p.dest_namespace? p.dest_namespace: ''}} {{p.dest_registry ? p.dest_registry.name : ''}} : {{p.dest_namespace? p.dest_namespace: '-'}}
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{p.trigger ? p.trigger.type : ''}}</clr-dg-cell> <clr-dg-cell>{{p.trigger ? p.trigger.type : ''}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>

View File

@ -8,3 +8,6 @@
.min-width { .min-width {
width: 224px; width: 224px;
} }
.status-width {
width: 105px;
}

View File

@ -69,22 +69,21 @@
<h3 class="modal-title">Tasks</h3> <h3 class="modal-title">Tasks</h3>
<div class="row flex-items-xs-between flex-items-xs-bottom"> <div class="row flex-items-xs-between flex-items-xs-bottom">
<div class="action-select"> <div class="action-select">
<div class="select filterTag" [hidden]="!isOpenFilterTag"> <div class="select filter-tag" [hidden]="!isOpenFilterTag">
<select (change)="selectFilter($event)"> <select (change)="selectFilter($event)">
<option value="resourceType">{{'REPLICATION.RESOURCE_TYPE' |translate}}</option> <option value="resource_type">{{'REPLICATION.RESOURCE_TYPE' |translate}}</option>
<option value="resource">{{'REPLICATION.RESOURCE' | translate}}</option>
<option value="destination">{{'REPLICATION.DESTINATION' | translate}}</option>
<option value="status">{{'REPLICATION.STATUS' | translate}}</option> <option value="status">{{'REPLICATION.STATUS' | translate}}</option>
</select> </select>
</div> </div>
<hbr-filter [withDivider]="true" (openFlag)="openFilter($event)" <hbr-filter [withDivider]="true" (openFlag)="openFilter($event)"
filterPlaceholder='{{"REPLICATION.FILTER_PLACEHOLDER" | translate}}' (filterEvt)="doSearch($event)"></hbr-filter> filterPlaceholder='{{"REPLICATION.FILTER_PLACEHOLDER" | translate}}'
(filterEvt)="doSearch($event)" [currentValue]="searchTask"></hbr-filter>
<span class="refresh-btn"> <span class="refresh-btn">
<clr-icon shape="refresh" (click)="refreshTasks()"></clr-icon> <clr-icon shape="refresh" (click)="refreshTasks()"></clr-icon>
</span> </span>
</div> </div>
</div> </div>
<clr-datagrid [(clrDgSelected)]="selectedRow" [clrDgLoading]="loading"> <clr-datagrid (clrDgRefresh)="clrLoadTasks($event)" [(clrDgSelected)]="selectedRow" [clrDgLoading]="loading">
<clr-dg-column>{{'REPLICATION.TASK_ID'| translate}}</clr-dg-column> <clr-dg-column>{{'REPLICATION.TASK_ID'| translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'resource_type'">{{'REPLICATION.RESOURCE_TYPE' <clr-dg-column [clrDgField]="'resource_type'">{{'REPLICATION.RESOURCE_TYPE'
| translate}}</clr-dg-column> | translate}}</clr-dg-column>
@ -92,6 +91,8 @@
translate}}</clr-dg-column> translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'dst_resource'">{{'REPLICATION.DESTINATION' | <clr-dg-column [clrDgField]="'dst_resource'">{{'REPLICATION.DESTINATION' |
translate}}</clr-dg-column> translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'operation'">{{'REPLICATION.OPERATION' |
translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' | <clr-dg-column [clrDgField]="'status'">{{'REPLICATION.STATUS' |
translate}}</clr-dg-column> translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.CREATION_TIME' <clr-dg-column [clrDgSortBy]="startTimeComparator">{{'REPLICATION.CREATION_TIME'
@ -104,6 +105,7 @@
<clr-dg-cell>{{t.resource_type}}</clr-dg-cell> <clr-dg-cell>{{t.resource_type}}</clr-dg-cell>
<clr-dg-cell>{{t.src_resource}}</clr-dg-cell> <clr-dg-cell>{{t.src_resource}}</clr-dg-cell>
<clr-dg-cell>{{t.dst_resource}}</clr-dg-cell> <clr-dg-cell>{{t.dst_resource}}</clr-dg-cell>
<clr-dg-cell>{{t.operation}}</clr-dg-cell>
<clr-dg-cell>{{t.status}}</clr-dg-cell> <clr-dg-cell>{{t.status}}</clr-dg-cell>
<clr-dg-cell>{{t.start_time | date: 'short'}}</clr-dg-cell> <clr-dg-cell>{{t.start_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{t.end_time | date: 'short'}}</clr-dg-cell> <clr-dg-cell>{{t.end_time | date: 'short'}}</clr-dg-cell>
@ -123,7 +125,7 @@
{{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' | {{pagination.lastItem +1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span> translate}} </span>
{{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}} {{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination> <clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="currentPage" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </clr-datagrid>
</div> </div>

View File

@ -10,7 +10,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
>div:first-child { >div:first-child {
width: 210px; width: 250px;
} }
>div:nth-child(2) { >div:nth-child(2) {
width: 140px; width: 140px;
@ -72,7 +72,7 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
.filterTag { .filter-tag {
float: left; float: left;
margin-top: 8px; margin-top: 8px;
} }
@ -85,5 +85,8 @@
color: #007CBB; color: #007CBB;
} }
} }
clr-datagrid {
margin-top: 20px;
}
} }
} }

View File

@ -4,8 +4,9 @@ import { ReplicationService } from "../../service/replication.service";
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { finalize } from "rxjs/operators"; import { finalize } from "rxjs/operators";
import { ErrorHandler } from "../../error-handler/error-handler"; import { ErrorHandler } from "../../error-handler/error-handler";
import { ReplicationJob, ReplicationTasks, Comparator, ReplicationJobItem } from "../../service/interface"; import { ReplicationJob, ReplicationTasks, Comparator, ReplicationJobItem, State } from "../../service/interface";
import { CustomComparator } from "../../utils"; import { CustomComparator, DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting } from "../../utils";
import { RequestQueryParams } from "../../service/RequestQueryParams";
@Component({ @Component({
selector: 'replication-tasks', selector: 'replication-tasks',
templateUrl: './replication-tasks.component.html', templateUrl: './replication-tasks.component.html',
@ -14,10 +15,16 @@ import { CustomComparator } from "../../utils";
export class ReplicationTasksComponent implements OnInit { export class ReplicationTasksComponent implements OnInit {
isOpenFilterTag: boolean; isOpenFilterTag: boolean;
selectedRow: []; selectedRow: [];
loading = false; currentPage: number = 1;
currentPagePvt: number = 0;
totalCount: number = 0;
pageSize: number = DEFAULT_PAGE_SIZE;
currentState: State;
loading = true;
searchTask: string; searchTask: string;
defaultFilter = "resourceType"; defaultFilter = "resource_type";
tasks: ReplicationTasks[] = []; tasks: ReplicationTasks;
taskItem: ReplicationTasks[] = [];
tasksCopy: ReplicationTasks[] = []; tasksCopy: ReplicationTasks[] = [];
stopOnGoing: boolean; stopOnGoing: boolean;
executions: ReplicationJobItem[]; executions: ReplicationJobItem[];
@ -37,7 +44,6 @@ export class ReplicationTasksComponent implements OnInit {
) { } ) { }
ngOnInit(): void { ngOnInit(): void {
this.clrLoadTasks();
this.searchTask = ''; this.searchTask = '';
this.getExecutionDetail(); this.getExecutionDetail();
} }
@ -97,34 +103,49 @@ export class ReplicationTasksComponent implements OnInit {
return this.replicationService.getJobBaseUrl() + "/executions/" + this.executionId + "/tasks/" + taskId + "/log"; return this.replicationService.getJobBaseUrl() + "/executions/" + this.executionId + "/tasks/" + taskId + "/log";
} }
clrLoadTasks(): void { clrLoadTasks(state: State): void {
this.loading = true;
this.replicationService.getReplicationTasks(this.executionId) if (!state || !state.page) {
.pipe(finalize(() => (this.loading = false))) return;
.subscribe(tasks => { }
if (this.defaultFilter === 'resourceType') { // Keep it for future filter
this.tasks = tasks.filter(x => this.currentState = state;
x.resource_type.includes(this.searchTask)
); let pageNumber: number = calculatePage(state);
} else if (this.defaultFilter === 'resource') { if (pageNumber !== this.currentPagePvt) {
this.tasks = tasks.filter(x => // load data
x.src_resource.includes(this.searchTask) let params: RequestQueryParams = new RequestQueryParams();
); params.set("page", '' + pageNumber);
} else if (this.defaultFilter === 'destination') { params.set("page_size", '' + this.pageSize);
this.tasks = tasks.filter(x => if (this.searchTask && this.searchTask !== "") {
x.dst_resource.includes(this.searchTask) params.set(this.defaultFilter, this.searchTask);
);
} else {
this.tasks = tasks.filter(x =>
x.status.includes(this.searchTask)
);
} }
this.tasksCopy = tasks.map(x => Object.assign({}, x)); this.loading = true;
this.replicationService.getReplicationTasks(this.executionId, params)
.pipe(finalize(() => (this.loading = false)))
.subscribe(res => {
this.totalCount = res.length;
this.tasks = res; // Keep the data
this.taskItem = this.tasks.filter(tasks => tasks.resource_type !== "");
this.taskItem = doFiltering<ReplicationTasks>(this.taskItem, state);
this.taskItem = doSorting<ReplicationTasks>(this.taskItem, state);
this.currentPagePvt = pageNumber;
}, },
error => { error => {
this.errorHandler.error(error); this.errorHandler.error(error);
}); });
} else {
this.taskItem = this.tasks.filter(tasks => tasks.resource_type !== "");
// Do customized filter
this.taskItem = doFiltering<ReplicationTasks>(this.taskItem, state);
// Do customized sorting
this.taskItem = doSorting<ReplicationTasks>(this.taskItem, state);
}
} }
onBack(): void { onBack(): void {
this.router.navigate(["harbor", "replications"]); this.router.navigate(["harbor", "replications"]);
@ -138,15 +159,41 @@ export class ReplicationTasksComponent implements OnInit {
// refresh icon // refresh icon
refreshTasks(): void { refreshTasks(): void {
this.searchTask = ''; this.searchTask = '';
this.clrLoadTasks(); this.loading = true;
this.replicationService.getReplicationTasks(this.executionId)
.subscribe(res => {
this.tasks = res;
this.loading = false;
},
error => {
this.loading = false;
this.errorHandler.error(error);
});
} }
doSearch(value: string): void { public doSearch(value: string): void {
if (!value) { if (!value) {
return; return;
} }
this.searchTask = value.trim(); this.searchTask = value.trim();
this.clrLoadTasks(); this.loading = true;
this.currentPage = 1;
if (this.currentPagePvt === 1) {
// Force reloading
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.currentPagePvt = 0;
this.clrLoadTasks(st);
}
} }
openFilter(isOpen: boolean): void { openFilter(isOpen: boolean): void {

View File

@ -24,19 +24,21 @@
<div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div *ngIf="withReplicationJob" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="row flex-items-xs-between jobsRow"> <div class="row flex-items-xs-between jobsRow">
<h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5> <h5 class="flex-items-xs-bottom option-left-down">{{'REPLICATION.REPLICATION_EXECUTIONS' | translate}}</h5>
<div class="flex-items-xs-bottom option-right-down"> <div class="row flex-items-xs-between flex-items-xs-bottom">
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{"REPLICATION.ADVANCED" | translate}}</button> <div class="execution-select">
</div> <div class="select filter-tag" [hidden]="!isOpenFilterTag">
</div> <select (change)="doFilterJob($event)">
<div class="row flex-items-xs-right row-right" [hidden]="currentJobSearchOption === 0"> <option value="trigger">{{'REPLICATION.REPLICATION_TRIGGER' |translate}}</option>
<div class="select select-status"> <option value="status">{{'REPLICATION.STATUS' | translate}}</option>
<select (change)="doFilterJobStatus($event)"> </select>
<option *ngFor="let j of jobStatus" value="{{j.key}}" [selected]="currentJobStatus.key === j.key">{{j.description | translate}}</option> </div>
</select> <hbr-filter [withDivider]="true" (openFlag)="openFilter($event)"
</div> filterPlaceholder='{{"REPLICATION.FILTER_EXECUTIONS_PLACEHOLDER" | translate}}'
<div class="flex-items-xs-middle"> (filterEvt)="doSearchExecutions($event)" [currentValue]="currentTerm"></hbr-filter>
<hbr-datetime [dateInput]="search.startTime" (search)="doJobSearchByStartTime($event)"></hbr-datetime> <span class="refresh-btn">
<hbr-datetime [dateInput]="search.endTime" [oneDayOffset]="true" (search)="doJobSearchByEndTime($event)"></hbr-datetime> <clr-icon shape="refresh" (click)="refreshJobs()"></clr-icon>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -61,11 +63,19 @@
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{j.trigger}}</clr-dg-cell> <clr-dg-cell>{{j.trigger}}</clr-dg-cell>
<clr-dg-cell>{{j.start_time | date: 'short'}}</clr-dg-cell> <clr-dg-cell>{{j.start_time | date: 'short'}}</clr-dg-cell>
<clr-dg-cell>{{'3mins'}}</clr-dg-cell> <clr-dg-cell>{{getDuration(j)}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
{{'90%'}} {{(j.succeed > 0 ? j.succeed / j.total : 0) | percent }}
</clr-dg-cell>
<clr-dg-cell>
{{j.status}}
<clr-tooltip>
<clr-icon *ngIf="j.status_text" clrTooltipTrigger shape="info-circle" size="20"></clr-icon>
<clr-tooltip-content [clrPosition]="'left'" clrSize="md" *clrIfOpen>
<span>{{j.status_text}}</span>
</clr-tooltip-content>
</clr-tooltip>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{j.status}}</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
<span *ngIf="showPaginationIndex">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPLICATION.OF' | translate}}</span> <span *ngIf="showPaginationIndex">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPLICATION.OF' | translate}}</span>

View File

@ -13,6 +13,25 @@
padding-right: 16px; padding-right: 16px;
} }
.execution-select {
padding-right: 18px;
height: 24px;
display: flex;
justify-content: flex-end;
padding-top: 25px;
.filter-tag {
float: left;
margin-top: 8px;
}
.refresh-btn {
cursor: pointer;
margin-top: 7px;
&:hover {
color: #007CBB;
}
}
}
.rightPos{ .rightPos{
position: absolute; position: absolute;
right: 35px; right: 35px;
@ -49,3 +68,9 @@
} }
} }
} }
clr-datagrid {
::ng-deep .datagrid-table {
position: static;
}
}

View File

@ -262,7 +262,6 @@ describe('Replication Component (inline template)', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
comp.doFilterJobStatus('finished');
let el: HTMLElement = deJobs.nativeElement; let el: HTMLElement = deJobs.nativeElement;
fixture.detectChanges(); fixture.detectChanges();
expect(el).toBeTruthy(); expect(el).toBeTruthy();
@ -274,8 +273,6 @@ describe('Replication Component (inline template)', () => {
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { fixture.whenStable().then(() => {
fixture.detectChanges(); fixture.detectChanges();
comp.doJobSearchByStartTime('2017-05-01');
comp.doJobSearchByEndTime('2015-05-25');
let el: HTMLElement = deJobs.nativeElement; let el: HTMLElement = deJobs.nativeElement;
fixture.detectChanges(); fixture.detectChanges();
expect(el).toBeTruthy(); expect(el).toBeTruthy();

View File

@ -53,9 +53,16 @@ import {
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message"; import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component"; import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation-dialog.component";
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message"; import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
import { operateChanges, OperationState, OperateInfo } from "../operation/operate"; import {
operateChanges,
OperationState,
OperateInfo
} from "../operation/operate";
import { OperationService } from "../operation/operation.service"; import { OperationService } from "../operation/operation.service";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
const ONE_HOUR_SECONDS: number = 3600;
const ONE_MINUTE_SECONDS: number = 60;
const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS;
const ruleStatus: { [key: string]: any } = [ const ruleStatus: { [key: string]: any } = [
{ key: "all", description: "REPLICATION.ALL_STATUS" }, { key: "all", description: "REPLICATION.ALL_STATUS" },
@ -63,26 +70,11 @@ const ruleStatus: { [key: string]: any } = [
{ key: "0", description: "REPLICATION.DISABLED" } { key: "0", description: "REPLICATION.DISABLED" }
]; ];
const jobStatus: { [key: string]: any } = [
{ key: "all", description: "REPLICATION.ALL" },
{ key: "pending", description: "REPLICATION.PENDING" },
{ key: "running", description: "REPLICATION.RUNNING" },
{ key: "error", description: "REPLICATION.ERROR" },
{ key: "retrying", description: "REPLICATION.RETRYING" },
{ key: "stopped", description: "REPLICATION.STOPPED" },
{ key: "finished", description: "REPLICATION.FINISHED" },
{ key: "canceled", description: "REPLICATION.CANCELED" }
];
export class SearchOption { export class SearchOption {
ruleId: number | string; ruleId: number | string;
ruleName: string = ""; ruleName: string = "";
repoName: string = ""; trigger: string = "";
status: string = ""; status: string = "";
startTime: string = "";
startTimestamp: string = "";
endTime: string = "";
endTimestamp: string = "";
page: number = 1; page: number = 1;
pageSize: number = DEFAULT_PAGE_SIZE; pageSize: number = DEFAULT_PAGE_SIZE;
} }
@ -109,12 +101,12 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@Output() goToRegistry = new EventEmitter<any>(); @Output() goToRegistry = new EventEmitter<any>();
search: SearchOption = new SearchOption(); search: SearchOption = new SearchOption();
isOpenFilterTag: boolean;
ruleStatus = ruleStatus; ruleStatus = ruleStatus;
currentRuleStatus: { key: string; description: string }; currentRuleStatus: { key: string; description: string };
jobStatus = jobStatus; currentTerm: string;
currentJobStatus: { key: string; description: string }; defaultFilter = "trigger";
changedRules: ReplicationRule[]; changedRules: ReplicationRule[];
@ -125,7 +117,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
hiddenJobList = true; hiddenJobList = true;
jobs: ReplicationJobItem[]; jobs: ReplicationJobItem[];
currentJobSearchOption: number;
@ViewChild(ListReplicationRuleComponent) @ViewChild(ListReplicationRuleComponent)
listReplicationRule: ListReplicationRuleComponent; listReplicationRule: ListReplicationRuleComponent;
@ -133,7 +124,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
@ViewChild(CreateEditRuleComponent) @ViewChild(CreateEditRuleComponent)
createEditPolicyComponent: CreateEditRuleComponent; createEditPolicyComponent: CreateEditRuleComponent;
@ViewChild("replicationConfirmDialog") @ViewChild("replicationConfirmDialog")
replicationConfirmDialog: ConfirmationDialogComponent; replicationConfirmDialog: ConfirmationDialogComponent;
@ -142,10 +132,10 @@ export class ReplicationComponent implements OnInit, OnDestroy {
creationTimeComparator: Comparator<ReplicationJob> = new CustomComparator< creationTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
ReplicationJob ReplicationJob
>("start_time", "date"); >("start_time", "date");
updateTimeComparator: Comparator<ReplicationJob> = new CustomComparator< updateTimeComparator: Comparator<ReplicationJob> = new CustomComparator<
ReplicationJob ReplicationJob
>("end_time", "date"); >("end_time", "date");
// Server driven pagination // Server driven pagination
currentPage: number = 1; currentPage: number = 1;
@ -160,7 +150,8 @@ export class ReplicationComponent implements OnInit, OnDestroy {
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private replicationService: ReplicationService, private replicationService: ReplicationService,
private operationService: OperationService, private operationService: OperationService,
private translateService: TranslateService) { } private translateService: TranslateService
) {}
public get showPaginationIndex(): boolean { public get showPaginationIndex(): boolean {
return this.totalCount > 0; return this.totalCount > 0;
@ -168,8 +159,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.currentRuleStatus = this.ruleStatus[0]; this.currentRuleStatus = this.ruleStatus[0];
this.currentJobStatus = this.jobStatus[0];
this.currentJobSearchOption = 0;
} }
ngOnDestroy() { ngOnDestroy() {
@ -215,43 +204,23 @@ export class ReplicationComponent implements OnInit, OnDestroy {
// Pagination // Pagination
params.set("page", "" + pageNumber); params.set("page", "" + pageNumber);
params.set("page_size", "" + this.pageSize); params.set("page_size", "" + this.pageSize);
// Search by status
if (this.search.status.trim()) { if (this.currentTerm && this.currentTerm !== "") {
params.set("status", this.search.status); params.set(this.defaultFilter, this.currentTerm);
}
// Search by repository
if (this.search.repoName.trim()) {
params.set("repository", this.search.repoName);
}
// Search by timestamps
if (this.search.startTimestamp.trim()) {
params.set("start_time", this.search.startTimestamp);
}
if (this.search.endTimestamp.trim()) {
params.set("end_time", this.search.endTimestamp);
} }
this.jobsLoading = true; this.jobsLoading = true;
// Do filtering and sorting this.replicationService.getExecutions(this.search.ruleId, params).subscribe(
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state); response => {
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);
this.jobsLoading = false;
this.replicationService.getExecutions(this.search.ruleId, params)
.subscribe(response => {
this.totalCount = response.metadata.xTotalCount; this.totalCount = response.metadata.xTotalCount;
this.jobs = response.data; this.jobs = response.data;
if (!this.timerDelay) { if (!this.timerDelay) {
this.timerDelay = timer(10000, 10000).subscribe(() => { this.timerDelay = timer(10000, 10000).subscribe(() => {
let count: number = 0; let count: number = 0;
this.jobs.forEach(job => { this.jobs.forEach(job => {
if ( if (
job.status === "pending" || job.status === "InProgress"
job.status === "running" ||
job.status === "retrying"
) { ) {
count++; count++;
} }
@ -264,18 +233,30 @@ export class ReplicationComponent implements OnInit, OnDestroy {
} }
}); });
} }
// Do filtering and sorting // Do filtering and sorting
this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state); this.jobs = doFiltering<ReplicationJobItem>(this.jobs, state);
this.jobs = doSorting<ReplicationJobItem>(this.jobs, state); this.jobs = doSorting<ReplicationJobItem>(this.jobs, state);
this.jobsLoading = false; this.jobsLoading = false;
}, error => { },
error => {
this.jobsLoading = false; this.jobsLoading = false;
this.errorHandler.error(error); this.errorHandler.error(error);
}); }
);
}
public doSearchExecutions(terms: string): void {
if (!terms) {
return;
}
this.currentTerm = terms.trim();
// Trigger data loading and start from first page
this.jobsLoading = true;
this.currentPage = 1;
this.jobsLoading = true;
// Force reloading
this.loadFirstPage();
} }
loadFirstPage(): void { loadFirstPage(): void {
let st: State = this.currentState; let st: State = this.currentState;
if (!st) { if (!st) {
@ -294,10 +275,6 @@ export class ReplicationComponent implements OnInit, OnDestroy {
if (rule && rule.id) { if (rule && rule.id) {
this.hiddenJobList = false; this.hiddenJobList = false;
this.search.ruleId = rule.id || ""; this.search.ruleId = rule.id || "";
this.search.repoName = "";
this.search.status = "";
this.currentJobSearchOption = 0;
this.currentJobStatus = { key: "all", description: "REPLICATION.ALL" };
this.loadFirstPage(); this.loadFirstPage();
} }
} }
@ -325,7 +302,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
let rule: ReplicationRule = message.data; let rule: ReplicationRule = message.data;
if (rule) { if (rule) {
forkJoin(this.replicationOperate(rule)).subscribe((item) => { forkJoin(this.replicationOperate(rule)).subscribe(item => {
this.selectOneRule(rule); this.selectOneRule(rule);
}); });
} }
@ -335,30 +312,39 @@ export class ReplicationComponent implements OnInit, OnDestroy {
replicationOperate(rule: ReplicationRule): Observable<any> { replicationOperate(rule: ReplicationRule): Observable<any> {
// init operation info // init operation info
let operMessage = new OperateInfo(); let operMessage = new OperateInfo();
operMessage.name = 'OPERATION.REPLICATION'; operMessage.name = "OPERATION.REPLICATION";
operMessage.data.id = rule.id; operMessage.data.id = rule.id;
operMessage.state = OperationState.progressing; operMessage.state = OperationState.progressing;
operMessage.data.name = rule.name; operMessage.data.name = rule.name;
this.operationService.publishInfo(operMessage); this.operationService.publishInfo(operMessage);
return this.replicationService.replicateRule(+rule.id) return this.replicationService.replicateRule(+rule.id).pipe(
.pipe(map(response => { map(response => {
this.translateService.get('BATCH.REPLICATE_SUCCESS') this.translateService
.subscribe(res => operateChanges(operMessage, OperationState.success)); .get("BATCH.REPLICATE_SUCCESS")
}) .subscribe(res =>
, catchError(error => { operateChanges(operMessage, OperationState.success)
if (error && error.status === 412) { );
return forkJoin(this.translateService.get('BATCH.REPLICATE_FAILURE'), }),
this.translateService.get('REPLICATION.REPLICATE_SUMMARY_FAILURE')) catchError(error => {
.pipe(map(function (res) { if (error && error.status === 412) {
operateChanges(operMessage, OperationState.failure, res[1]); return forkJoin(
})); this.translateService.get("BATCH.REPLICATE_FAILURE"),
} else { this.translateService.get("REPLICATION.REPLICATE_SUMMARY_FAILURE")
return this.translateService.get('BATCH.REPLICATE_FAILURE').pipe(map(res => { ).pipe(
map(function(res) {
operateChanges(operMessage, OperationState.failure, res[1]);
})
);
} else {
return this.translateService.get("BATCH.REPLICATE_FAILURE").pipe(
map(res => {
operateChanges(operMessage, OperationState.failure, res); operateChanges(operMessage, OperationState.failure, res);
})); })
} );
})); }
})
);
} }
customRedirect(rule: ReplicationRule) { customRedirect(rule: ReplicationRule) {
@ -370,21 +356,17 @@ export class ReplicationComponent implements OnInit, OnDestroy {
this.listReplicationRule.retrieveRules(ruleName); this.listReplicationRule.retrieveRules(ruleName);
} }
doFilterJobStatus($event: any) { doFilterJob($event: any): void {
if ($event && $event.target && $event.target["value"]) { this.defaultFilter = $event["target"].value;
let status = $event.target["value"]; this.doSearchJobs(this.currentTerm);
this.currentJobStatus = this.jobStatus.find((r: any) => r.key === status);
if (this.currentJobStatus.key === "all") {
status = "";
}
this.search.status = status;
this.doSearchJobs(this.search.repoName);
}
} }
doSearchJobs(repoName: string) { doSearchJobs(terms: string) {
this.search.repoName = repoName; if (!terms) {
return;
}
this.currentTerm = terms.trim();
this.currentPage = 1;
this.loadFirstPage(); this.loadFirstPage();
} }
@ -434,7 +416,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
this.selectedRow = []; this.selectedRow = [];
}) })
) )
.subscribe(() => { }); .subscribe(() => {});
} }
} }
@ -468,14 +450,7 @@ export class ReplicationComponent implements OnInit, OnDestroy {
} }
refreshJobs() { refreshJobs() {
this.currentJobStatus = this.jobStatus[0]; this.currentTerm = "";
this.search.startTime = " ";
this.search.endTime = " ";
this.search.repoName = "";
this.search.startTimestamp = "";
this.search.endTimestamp = "";
this.search.status = "";
this.currentPage = 1; this.currentPage = 1;
let st: State = { let st: State = {
@ -488,19 +463,36 @@ export class ReplicationComponent implements OnInit, OnDestroy {
this.clrLoadJobs(st); this.clrLoadJobs(st);
} }
toggleSearchJobOptionalName(option: number) { openFilter(isOpen: boolean): void {
option === 1 if (isOpen) {
? (this.currentJobSearchOption = 0) this.isOpenFilterTag = true;
: (this.currentJobSearchOption = 1); } else {
this.isOpenFilterTag = false;
}
} }
getDuration(j: ReplicationJobItem) {
if (!j) {
return;
}
if (j.status === "Failed") {
return "-";
}
let start_time = new Date(j.start_time).getTime();
let end_time = new Date(j.end_time).getTime();
let timesDiff = end_time - start_time;
let timesDiffSeconds = timesDiff / 1000;
let minutes = Math.floor(((timesDiffSeconds % ONE_DAY_SECONDS) % ONE_HOUR_SECONDS) / ONE_MINUTE_SECONDS);
let seconds = Math.floor(timesDiffSeconds % ONE_MINUTE_SECONDS);
if (minutes > 0) {
return minutes + "m" + seconds + "s";
}
doJobSearchByStartTime(fromTimestamp: string) { if (seconds > 0) {
this.search.startTimestamp = fromTimestamp; return seconds + "s";
this.loadFirstPage(); }
}
doJobSearchByEndTime(toTimestamp: string) { if (seconds <= 0 && timesDiff > 0) {
this.search.endTimestamp = toTimestamp; return timesDiff + 'ms';
this.loadFirstPage(); }
} }
} }

View File

@ -175,6 +175,7 @@ export interface ReplicationJobItem extends Base {
*/ */
export interface ReplicationTasks extends Base { export interface ReplicationTasks extends Base {
[key: string]: any | any[]; [key: string]: any | any[];
operation: string;
id: number; id: number;
execution_id: number; execution_id: number;
resource_type: string; resource_type: string;

View File

@ -67,7 +67,8 @@ export abstract class ReplicationService {
* @memberOf ReplicationService * @memberOf ReplicationService
*/ */
abstract getReplicationTasks( abstract getReplicationTasks(
executionId: number | string executionId: number | string,
queryParams?: RequestQueryParams
): Observable<ReplicationTasks>; ): Observable<ReplicationTasks>;
/** /**
* Create new replication rule. * Create new replication rule.
@ -287,14 +288,16 @@ export class ReplicationDefaultService extends ReplicationService {
} }
public getReplicationTasks( public getReplicationTasks(
executionId: number | string executionId: number | string,
queryParams?: RequestQueryParams
): Observable<ReplicationTasks> { ): Observable<ReplicationTasks> {
if (!executionId) { if (!executionId) {
return observableThrowError("Bad argument"); return observableThrowError("Bad argument");
} }
let url: string = `${this._replicateUrl}/executions/${executionId}/tasks`; let url: string = `${this._replicateUrl}/executions/${executionId}/tasks`;
return this.http return this.http
.get(url, HTTP_GET_OPTIONS) .get(url,
queryParams ? buildHttpRequestOptions(queryParams) : HTTP_GET_OPTIONS)
.pipe(map(response => response.json() as ReplicationTasks) .pipe(map(response => response.json() as ReplicationTasks)
, catchError(error => observableThrowError(error))); , catchError(error => observableThrowError(error)));
} }

View File

@ -340,6 +340,7 @@
"OF": "of" "OF": "of"
}, },
"REPLICATION": { "REPLICATION": {
"OPERATION": "Operation",
"CURRENT": "current", "CURRENT": "current",
"FILTER_PLACEHOLDER": "Filter Tasks", "FILTER_PLACEHOLDER": "Filter Tasks",
"STOP_TITLE": "Confirm Stop Executions", "STOP_TITLE": "Confirm Stop Executions",