Refactor gc and gi history page (#14728)

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Will Sun 2021-04-26 10:25:36 +08:00 committed by GitHub
parent 2ffa6580fa
commit 705cb5b55d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 323 additions and 489 deletions

View File

@ -3,10 +3,7 @@ import { RouterModule, Routes } from "@angular/router";
import { GcPageComponent } from "./gc-page.component"; import { GcPageComponent } from "./gc-page.component";
import { GcComponent } from "./gc/gc.component"; import { GcComponent } from "./gc/gc.component";
import { GcHistoryComponent } from "./gc/gc-history/gc-history.component"; import { GcHistoryComponent } from "./gc/gc-history/gc-history.component";
import { GcRepoService } from "./gc/gc.service";
import { SharedModule } from "../../../shared/shared.module"; import { SharedModule } from "../../../shared/shared.module";
import { GcApiDefaultRepository, GcApiRepository } from "./gc/gc.api.repository";
import { GcViewModelFactory } from "./gc/gc.viewmodel.factory";
const routes: Routes = [ const routes: Routes = [
{ {
@ -24,10 +21,5 @@ const routes: Routes = [
GcComponent, GcComponent,
GcHistoryComponent GcHistoryComponent
], ],
providers: [
GcRepoService,
{provide: GcApiRepository, useClass: GcApiDefaultRepository },
GcViewModelFactory
]
}) })
export class GcModule {} export class GcModule {}

View File

@ -1,30 +1,35 @@
<h5 class="history-header" id="history-header">{{'GC.JOB_HISTORY' | translate}}</h5> <h5 class="history-header" id="history-header">{{'GC.JOB_HISTORY' | translate}}</h5>
<span class="refresh-btn" (click)="getJobs()"> <span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon> <clr-icon shape="refresh"></clr-icon>
</span> </span>
<clr-datagrid [clrDgLoading]="loading"> <clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="getJobs($event)">
<clr-dg-column>{{'GC.JOB_ID' | translate}}</clr-dg-column> <clr-dg-column [clrDgField]="'id'">{{'GC.JOB_ID' | translate}}</clr-dg-column>
<clr-dg-column>{{'GC.TRIGGER_TYPE' | translate}}</clr-dg-column> <clr-dg-column>{{'GC.TRIGGER_TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG_RETENTION.DRY_RUN' | translate}}</clr-dg-column> <clr-dg-column>{{'TAG_RETENTION.DRY_RUN' | translate}}</clr-dg-column>
<clr-dg-column>{{'STATUS' | translate}}</clr-dg-column> <clr-dg-column>{{'STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'CREATION_TIME' | translate}}</clr-dg-column> <clr-dg-column>{{'CREATION_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'UPDATE_TIME' | translate}}</clr-dg-column> <clr-dg-column [clrDgSortBy]="'update_time'">{{'UPDATE_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'LOGS' | translate}}</clr-dg-column> <clr-dg-column>{{'LOGS' | translate}}</clr-dg-column>
<clr-dg-row *clrDgItems="let job of jobs" [clrDgItem]='job'> <clr-dg-row *ngFor="let job of jobs" [clrDgItem]='job'>
<clr-dg-cell>{{job.id }}</clr-dg-cell> <clr-dg-cell>{{job.id }}</clr-dg-cell>
<clr-dg-cell>{{(job.type ? 'SCHEDULE.'+ job.type.toUpperCase() : '') | translate }}</clr-dg-cell> <clr-dg-cell>{{(job.schedule?.type ? 'SCHEDULE.' + job.schedule?.type.toUpperCase() : '') | translate }}</clr-dg-cell>
<clr-dg-cell>{{isDryRun(job?.parameters) | translate}}</clr-dg-cell> <clr-dg-cell>{{isDryRun(job?.job_parameters) | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.status.toUpperCase() | translate}}</clr-dg-cell> <clr-dg-cell>{{job.job_status.toUpperCase() | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.createTime | harborDatetime:'medium'}}</clr-dg-cell> <clr-dg-cell>{{job.creation_time | harborDatetime:'medium'}}</clr-dg-cell>
<clr-dg-cell>{{job.updateTime | harborDatetime:'medium'}}</clr-dg-cell> <clr-dg-cell>{{job.update_time | harborDatetime:'medium'}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<a *ngIf="job.status.toLowerCase() === 'success' || job.status.toLowerCase() === 'error'" target="_blank" [href]="getLogLink(job.id)"><clr-icon shape="list"></clr-icon></a> <a *ngIf="job.job_status.toLowerCase() === 'success' || job.job_status.toLowerCase() === 'error'" target="_blank"
</clr-dg-cell> [href]="getLogLink(job.id)">
</clr-dg-row> <clr-icon shape="list"></clr-icon>
<clr-dg-footer> </a>
<clr-dg-pagination [clrDgPageSize]="15"> </clr-dg-cell>
<clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size> </clr-dg-row>
{{'GC.LATEST_JOBS' | translate :{param: jobs.length} }} <clr-dg-footer>
</clr-dg-pagination> <clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [(clrDgPage)]="page" [clrDgTotalItems]="total">
</clr-dg-footer> <clr-dg-page-size [clrPageSizeOptions]="[15,25,50]">{{"PAGINATION.PAGE_SIZE" | translate}}</clr-dg-page-size>
<span *ngIf="total">{{pagination.firstItem + 1}}
- {{pagination.lastItem + 1}} {{'DESTINATION.OF' | translate}}</span>
{{total}} {{'DESTINATION.ITEMS' | translate}}
</clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid> </clr-datagrid>

View File

@ -1,100 +1,99 @@
import { import {
ComponentFixture, ComponentFixture,
ComponentFixtureAutoDetect, ComponentFixtureAutoDetect, fakeAsync,
TestBed, TestBed, tick,
waitForAsync
} from '@angular/core/testing'; } from '@angular/core/testing';
import { GcRepoService } from "../gc.service";
import { of } from 'rxjs'; import { of } from 'rxjs';
import { GcViewModelFactory } from "../gc.viewmodel.factory";
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ErrorHandler } from '../../../../../shared/units/error-handler';
import { GcHistoryComponent } from './gc-history.component'; import { GcHistoryComponent } from './gc-history.component';
import { GcJobData } from "../gcLog";
import { SharedTestingModule } from "../../../../../shared/shared.module"; import { SharedTestingModule } from "../../../../../shared/shared.module";
import { GCHistory } from "../../../../../../../ng-swagger-gen/models/gchistory";
import { HttpHeaders, HttpResponse } from "@angular/common/http";
import { Registry } from "../../../../../../../ng-swagger-gen/models/registry";
import { GcService } from "../../../../../../../ng-swagger-gen/services/gc.service";
import { CURRENT_BASE_HREF } from "../../../../../shared/units/utils";
import { delay } from "rxjs/operators";
describe('GcHistoryComponent', () => { describe('GcHistoryComponent', () => {
let component: GcHistoryComponent; let component: GcHistoryComponent;
let fixture: ComponentFixture<GcHistoryComponent>; let fixture: ComponentFixture<GcHistoryComponent>;
const mockJobs: GcJobData[] = [ const mockJobs: GCHistory[] = [
{ {
id: 1, id: 1,
job_name: 'test', job_name: 'test',
job_kind: 'manual', job_kind: 'manual',
schedule: null, schedule: null,
job_status: 'pending', job_status: 'pending',
job_parameters: '{"dry_run":true}', job_parameters: '{"dry_run":true}',
job_uuid: 'abc', creation_time: null,
creation_time: null, update_time: null,
update_time: null, },
delete: false {
}, id: 2,
{ job_name: 'test',
id: 2, job_kind: 'manual',
job_name: 'test', schedule: null,
job_kind: 'manual', job_status: 'finished',
schedule: null, job_parameters: '{"dry_run":true}',
job_status: 'finished', creation_time: null,
job_parameters: '{"dry_run":true}', update_time: null,
job_uuid: 'bcd', }
creation_time: null, ];
update_time: null, const fakedGcService = {
delete: false count: 0,
} getGCHistoryResponse() {
]; if (this.count === 0) {
let fakeGcRepoService = { this.count += 1;
count: 0, const response: HttpResponse<Array<Registry>> = new HttpResponse<Array<Registry>>({
getJobs() { headers: new HttpHeaders({'x-total-count': [mockJobs[0]].length.toString()}),
if (this.count === 0) { body: [mockJobs[0]]
this.count += 1;
return of([mockJobs[0]]);
} else {
this.count += 1;
return of([mockJobs[1]]);
}
},
getLogLink() {
return null;
}
};
const fakeGcViewModelFactory = new GcViewModelFactory();
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [GcHistoryComponent],
imports: [
SharedTestingModule,
TranslateModule.forRoot()
],
providers: [
ErrorHandler,
TranslateService,
GcViewModelFactory,
{ provide: GcRepoService, useValue: fakeGcRepoService },
{ provide: GcViewModelFactory, useValue: fakeGcViewModelFactory },
// open auto detect
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
}); });
return of(response).pipe(delay(0));
} else {
this.count += 1;
const response: HttpResponse<Array<Registry>> = new HttpResponse<Array<Registry>>({
headers: new HttpHeaders({'x-total-count': [mockJobs[1]].length.toString()}),
body: [mockJobs[1]]
});
return of(response).pipe(delay(0));
}
}
};
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [GcHistoryComponent],
imports: [
SharedTestingModule,
],
providers: [
{provide: GcService, useValue: fakedGcService},
// open auto detect
{provide: ComponentFixtureAutoDetect, useValue: true}
]
}); });
});
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(GcHistoryComponent); fixture = TestBed.createComponent(GcHistoryComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
});
afterEach(() => {
if (component && component.timerDelay) {
component.timerDelay.unsubscribe();
component.timerDelay = null;
}
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should retry getting jobs', fakeAsync(() => {
tick(10000);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(component.jobs[0].job_status).toEqual('finished');
}); });
afterEach(() => { }));
if (component && component.timerDelay) { it('should return right log link', () => {
component.timerDelay.unsubscribe(); expect(component.getLogLink('1')).toEqual(`${CURRENT_BASE_HREF}/system/gc/1/log`);
component.timerDelay = null; });
}
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should retry getting jobs', waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(component.jobs[1].status).toEqual('finished');
});
}));
}); });

View File

@ -1,64 +1,103 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { GcRepoService } from "../gc.service";
import { GcJobViewModel } from "../gcLog";
import { GcViewModelFactory } from "../gc.viewmodel.factory";
import { ErrorHandler } from "../../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../../shared/units/error-handler";
import { Subscription, timer } from "rxjs"; import { Subscription, timer } from "rxjs";
import { REFRESH_TIME_DIFFERENCE } from '../../../../../shared/entities/shared.const'; import { REFRESH_TIME_DIFFERENCE } from '../../../../../shared/entities/shared.const';
import { GcService } from "../../../../../../../ng-swagger-gen/services/gc.service";
import { CURRENT_BASE_HREF, DEFAULT_PAGE_SIZE, getSortingString } from "../../../../../shared/units/utils";
import { ClrDatagridStateInterface } from "@clr/angular";
import { finalize } from "rxjs/operators";
import { GCHistory } from "../../../../../../../ng-swagger-gen/models/gchistory";
const JOB_STATUS = { const JOB_STATUS = {
PENDING: "pending", PENDING: "pending",
RUNNING: "running" RUNNING: "running"
}; };
const YES: string = 'TAG_RETENTION.YES'; const YES: string = 'TAG_RETENTION.YES';
const NO: string = 'TAG_RETENTION.NO'; const NO: string = 'TAG_RETENTION.NO';
@Component({ @Component({
selector: 'gc-history', selector: 'gc-history',
templateUrl: './gc-history.component.html', templateUrl: './gc-history.component.html',
styleUrls: ['./gc-history.component.scss'] styleUrls: ['./gc-history.component.scss']
}) })
export class GcHistoryComponent implements OnInit, OnDestroy { export class GcHistoryComponent implements OnInit, OnDestroy {
jobs: Array<GcJobViewModel> = []; jobs: Array<GCHistory> = [];
loading: boolean; loading: boolean = true;
timerDelay: Subscription; timerDelay: Subscription;
pageSize: number = DEFAULT_PAGE_SIZE;
page: number = 1;
total: number = 0;
state: ClrDatagridStateInterface;
constructor( constructor(
private gcRepoService: GcRepoService, private gcService: GcService,
private gcViewModelFactory: GcViewModelFactory,
private errorHandler: ErrorHandler private errorHandler: ErrorHandler
) {} ) {
}
ngOnInit() { ngOnInit() {
}
refresh() {
this.page = 1;
this.total = 0;
this.getJobs(); this.getJobs();
} }
getJobs() { getJobs(state?: ClrDatagridStateInterface) {
if (state) {
this.state = state;
}
if (state && state.page) {
this.pageSize = state.page.size;
}
let q: string;
if (state && state.filters && state.filters.length) {
q = encodeURIComponent(`${state.filters[0].property}=~${state.filters[0].value}`);
}
let sort: string;
if (state && state.sort && state.sort.by) {
sort = getSortingString(state);
}
this.loading = true; this.loading = true;
this.gcRepoService.getJobs().subscribe(jobs => { this.gcService.getGCHistoryResponse({
this.jobs = this.gcViewModelFactory.createJobViewModel(jobs); page: this.page,
this.loading = false; pageSize: this.pageSize,
// to avoid some jobs not finished. q: q,
if (!this.timerDelay) { sort: sort
this.timerDelay = timer(REFRESH_TIME_DIFFERENCE, REFRESH_TIME_DIFFERENCE).subscribe(() => { }).pipe(finalize(() => this.loading = false))
let count: number = 0; .subscribe(res => {
this.jobs.forEach(job => { // Get total count
if ( if (res.headers) {
job['status'] === JOB_STATUS.PENDING || const xHeader: string = res.headers.get("X-Total-Count");
job['status'] === JOB_STATUS.RUNNING if (xHeader) {
) { this.total = parseInt(xHeader, 0);
count++; }
this.jobs = res.body;
}
// to avoid some jobs not finished.
if (!this.timerDelay) {
this.timerDelay = timer(REFRESH_TIME_DIFFERENCE, REFRESH_TIME_DIFFERENCE).subscribe(() => {
let count: number = 0;
this.jobs.forEach(job => {
if (
job.job_status === JOB_STATUS.PENDING ||
job.job_status === JOB_STATUS.RUNNING
) {
count++;
}
});
if (count > 0) {
this.getJobs(this.state);
} else {
this.timerDelay.unsubscribe();
this.timerDelay = null;
} }
}); });
if (count > 0) { }
this.getJobs(); }, error => {
} else {
this.timerDelay.unsubscribe();
this.timerDelay = null;
}
});
}
}, error => {
this.errorHandler.error(error); this.errorHandler.error(error);
this.loading = false; this.loading = false;
}); });
} }
isDryRun(param: string): string { isDryRun(param: string): string {
@ -78,7 +117,7 @@ export class GcHistoryComponent implements OnInit, OnDestroy {
} }
getLogLink(id): string { getLogLink(id): string {
return this.gcRepoService.getLogLink(id); return `${CURRENT_BASE_HREF}/system/gc/${id}/log`;
} }
} }

View File

@ -1,65 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { throwError as observableThrowError, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { CURRENT_BASE_HREF } from "../../../../shared/units/utils";
export abstract class GcApiRepository {
abstract postSchedule(param): Observable<any>;
abstract putSchedule(param): Observable<any>;
abstract getSchedule(): Observable<any>;
abstract getLog(id): Observable<any>;
abstract getStatus(id): Observable<any>;
abstract getJobs(): Observable<any>;
abstract getLogLink(id): string;
}
@Injectable()
export class GcApiDefaultRepository extends GcApiRepository {
constructor(
private http: HttpClient
) {
super();
}
public postSchedule(param): Observable<any> {
return this.http.post(`${CURRENT_BASE_HREF}/system/gc/schedule`, param)
.pipe(catchError(error => observableThrowError(error)));
}
public putSchedule(param): Observable<any> {
return this.http.put(`${CURRENT_BASE_HREF}/system/gc/schedule`, param)
.pipe(catchError(error => observableThrowError(error)));
}
public getSchedule(): Observable<any> {
return this.http.get(`${CURRENT_BASE_HREF}/system/gc/schedule`)
.pipe(catchError(error => observableThrowError(error)));
}
public getLog(id): Observable<any> {
return this.http.get(`${CURRENT_BASE_HREF}/system/gc/${id}/log`)
.pipe(catchError(error => observableThrowError(error)));
}
public getStatus(id): Observable<any> {
return this.http.get(`${CURRENT_BASE_HREF}/system/gc/${id}`)
.pipe(catchError(error => observableThrowError(error)));
}
public getJobs(): Observable<any> {
return this.http.get(`${CURRENT_BASE_HREF}/system/gc`)
.pipe(catchError(error => observableThrowError(error)));
}
public getLogLink(id) {
return `${CURRENT_BASE_HREF}/system/gc/${id}/log`;
}
}

View File

@ -1,6 +1,6 @@
<div class="cron-selection"> <div class="cron-selection">
<cron-selection [labelCurrent]="getLabelCurrent" #CronScheduleComponent [labelEdit]='getLabelCurrent' <cron-selection [labelCurrent]="getLabelCurrent" #CronScheduleComponent [labelEdit]='getLabelCurrent'
[originCron]='originCron' (inputvalue)="scheduleGc($event)"></cron-selection> [originCron]='originCron' (inputvalue)="saveGcSchedule($event)"></cron-selection>
</div> </div>
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-2 flex-200"></div> <div class="clr-col-2 flex-200"></div>
@ -22,11 +22,11 @@
</div> </div>
<div class="clr-row"> <div class="clr-row">
<div class="clr-col-2 flex-200"> <div class="clr-col-2 flex-200">
<button class="btn btn-primary gc-start-btn" (click)="gcNow()" <button id="gc-now" class="btn btn-primary gc-start-btn" (click)="gcNow()"
[disabled]="disableGC">{{'GC.GC_NOW' | translate}}</button> [disabled]="disableGC">{{'GC.GC_NOW' | translate}}</button>
</div> </div>
<div class="clr-col"> <div class="clr-col">
<button class="btn btn-outline gc-start-btn" (click)="dryRun()" <button id="gc-dry-run" class="btn btn-outline gc-start-btn" (click)="dryRun()"
[disabled]="dryRunOnGoing">{{'TAG_RETENTION.WHAT_IF_RUN' | translate}}</button> [disabled]="dryRunOnGoing">{{'TAG_RETENTION.WHAT_IF_RUN' | translate}}</button>
</div> </div>
</div> </div>

View File

@ -1,35 +1,18 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { GcComponent } from './gc.component'; import { GcComponent } from './gc.component';
import { GcApiRepository, GcApiDefaultRepository} from './gc.api.repository';
import { GcRepoService } from './gc.service';
import { ErrorHandler } from '../../../../shared/units/error-handler'; import { ErrorHandler } from '../../../../shared/units/error-handler';
import { GcViewModelFactory } from './gc.viewmodel.factory';
import { CronScheduleComponent } from '../../../../shared/components/cron-schedule'; import { CronScheduleComponent } from '../../../../shared/components/cron-schedule';
import { CronTooltipComponent } from "../../../../shared/components/cron-schedule"; import { CronTooltipComponent } from "../../../../shared/components/cron-schedule";
import { of } from 'rxjs'; import { of } from 'rxjs';
import { GcJobData } from './gcLog';
import { CURRENT_BASE_HREF } from "../../../../shared/units/utils";
import { SharedTestingModule } from "../../../../shared/shared.module"; import { SharedTestingModule } from "../../../../shared/shared.module";
import { GcService } from "../../../../../../ng-swagger-gen/services/gc.service";
import { ScheduleType } from "../../../../shared/entities/shared.const";
describe('GcComponent', () => { describe('GcComponent', () => {
let component: GcComponent; let component: GcComponent;
let fixture: ComponentFixture<GcComponent>; let fixture: ComponentFixture<GcComponent>;
let gcRepoService: GcRepoService; let gcRepoService: GcService;
let mockSchedule = []; let mockSchedule = [];
let mockJobs: GcJobData[] = [
{
id: 22222,
schedule: null,
job_status: 'string',
job_parameters: '{"dry_run":true}',
creation_time: new Date().toDateString(),
update_time: new Date().toDateString(),
job_name: 'string',
job_kind: 'string',
job_uuid: 'string',
delete: false
}
];
const fakedErrorHandler = { const fakedErrorHandler = {
error(error) { error(error) {
return error; return error;
@ -39,7 +22,6 @@ describe('GcComponent', () => {
} }
}; };
let spySchedule: jasmine.Spy; let spySchedule: jasmine.Spy;
let spyJobs: jasmine.Spy;
let spyGcNow: jasmine.Spy; let spyGcNow: jasmine.Spy;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@ -48,10 +30,7 @@ describe('GcComponent', () => {
], ],
declarations: [ GcComponent, CronScheduleComponent, CronTooltipComponent], declarations: [ GcComponent, CronScheduleComponent, CronTooltipComponent],
providers: [ providers: [
{ provide: GcApiRepository, useClass: GcApiDefaultRepository },
{ provide: ErrorHandler, useValue: fakedErrorHandler }, { provide: ErrorHandler, useValue: fakedErrorHandler },
GcRepoService,
GcViewModelFactory
] ]
}) })
.compileComponents(); .compileComponents();
@ -61,10 +40,9 @@ describe('GcComponent', () => {
fixture = TestBed.createComponent(GcComponent); fixture = TestBed.createComponent(GcComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
gcRepoService = fixture.debugElement.injector.get(GcRepoService); gcRepoService = fixture.debugElement.injector.get(GcService);
spySchedule = spyOn(gcRepoService, "getSchedule").and.returnValues(of(mockSchedule)); spySchedule = spyOn(gcRepoService, "getGCSchedule").and.returnValues(of(mockSchedule));
spyJobs = spyOn(gcRepoService, "getJobs").and.returnValues(of(mockJobs)); spyGcNow = spyOn(gcRepoService, "createGCSchedule").and.returnValues(of(true));
spyGcNow = spyOn(gcRepoService, "manualGc").and.returnValues(of(true));
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it('should create', () => {
@ -72,12 +50,25 @@ describe('GcComponent', () => {
}); });
it('should get schedule and job', () => { it('should get schedule and job', () => {
expect(spySchedule.calls.count()).toEqual(1); expect(spySchedule.calls.count()).toEqual(1);
expect(spyJobs.calls.count()).toEqual(1);
}); });
it('should trigger gcNow', () => { it('should trigger gcNow', () => {
const ele: HTMLButtonElement = fixture.nativeElement.querySelector('.gc-start-btn'); const ele: HTMLButtonElement = fixture.nativeElement.querySelector('#gc-now');
ele.click(); ele.click();
fixture.detectChanges(); fixture.detectChanges();
expect(spyGcNow.calls.count()).toEqual(1); expect(spyGcNow.calls.count()).toEqual(1);
}); });
it('should trigger dry run', () => {
const ele: HTMLButtonElement = fixture.nativeElement.querySelector('#gc-dry-run');
ele.click();
fixture.detectChanges();
expect(spyGcNow.calls.count()).toEqual(1);
});
it('getScheduleType function should work', () => {
expect(GcComponent.getScheduleType).toBeTruthy();
expect(GcComponent.getScheduleType(null)).toEqual(ScheduleType.NONE);
expect(GcComponent.getScheduleType('0 0 0 0 0 0')).toEqual(ScheduleType.CUSTOM);
expect(GcComponent.getScheduleType('0 0 * * * *')).toEqual(ScheduleType.HOURLY);
expect(GcComponent.getScheduleType('0 0 0 * * *')).toEqual(ScheduleType.DAILY);
expect(GcComponent.getScheduleType('0 0 0 * * 0')).toEqual(ScheduleType.WEEKLY);
});
}); });

View File

@ -1,32 +1,26 @@
import { import {
Component, Component,
Input,
Output, Output,
EventEmitter, EventEmitter,
ViewChild, ViewChild,
OnInit OnInit
} from "@angular/core"; } from "@angular/core";
import { TranslateService } from "@ngx-translate/core";
import { GcJobViewModel } from "./gcLog";
import { GcViewModelFactory } from "./gc.viewmodel.factory";
import { GcRepoService } from "./gc.service";
import {
SCHEDULE_TYPE_NONE,
ONE_MINITUE,
THREE_SECONDS, GCSchedule
} from "./gc.const";
import { ErrorHandler } from "../../../../shared/units/error-handler"; import { ErrorHandler } from "../../../../shared/units/error-handler";
import { CronScheduleComponent } from "../../../../shared/components/cron-schedule/cron-schedule.component"; import { CronScheduleComponent } from "../../../../shared/components/cron-schedule";
import { OriginCron } from '../../../../shared/services/interface'; import { OriginCron } from '../../../../shared/services';
import { finalize } from "rxjs/operators"; import { finalize } from "rxjs/operators";
import { GcService } from "../../../../../../ng-swagger-gen/services/gc.service";
import { GCHistory } from "../../../../../../ng-swagger-gen/models/gchistory";
import { ScheduleType } from "../../../../shared/entities/shared.const";
const ONE_MINUTE = 60000;
@Component({ @Component({
selector: "gc-config", selector: "gc-config",
templateUrl: "./gc.component.html", templateUrl: "./gc.component.html",
styleUrls: ["./gc.component.scss"] styleUrls: ["./gc.component.scss"]
}) })
export class GcComponent implements OnInit { export class GcComponent implements OnInit {
jobs: Array<GcJobViewModel> = [];
schedule: GCSchedule = {};
originCron: OriginCron; originCron: OriginCron;
disableGC: boolean = false; disableGC: boolean = false;
getLabelCurrent = 'GC.CURRENT_SCHEDULE'; getLabelCurrent = 'GC.CURRENT_SCHEDULE';
@ -35,67 +29,66 @@ export class GcComponent implements OnInit {
CronScheduleComponent: CronScheduleComponent; CronScheduleComponent: CronScheduleComponent;
shouldDeleteUntagged: boolean; shouldDeleteUntagged: boolean;
dryRunOnGoing: boolean = false; dryRunOnGoing: boolean = false;
constructor( constructor(
private gcRepoService: GcRepoService, private gcService: GcService,
private gcViewModelFactory: GcViewModelFactory,
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private translate: TranslateService
) { ) {
translate.setDefaultLang("en-us");
} }
ngOnInit() { ngOnInit() {
this.getCurrentSchedule(); this.getCurrentSchedule();
this.getJobs();
} }
getCurrentSchedule() { getCurrentSchedule() {
this.loadingGcStatus.emit(true); this.loadingGcStatus.emit(true);
this.gcRepoService.getSchedule() this.gcService.getGCSchedule()
.pipe(finalize(() => { .pipe(finalize(() => {
this.loadingGcStatus.emit(false); this.loadingGcStatus.emit(false);
})) }))
.subscribe(schedule => { .subscribe(schedule => {
this.initSchedule(schedule); this.initSchedule(schedule);
}, error => { }, error => {
this.errorHandler.error(error); this.errorHandler.error(error);
}); });
} }
public initSchedule(schedule: GCSchedule) { private initSchedule(gcHistory: GCHistory) {
if (schedule && schedule.schedule !== null) { if (gcHistory && gcHistory.schedule) {
this.schedule = schedule; this.originCron = {
this.originCron = this.schedule.schedule; type: gcHistory.schedule.type,
cron: gcHistory.schedule.cron
};
} else { } else {
this.originCron = { this.originCron = {
type: SCHEDULE_TYPE_NONE, type: ScheduleType.NONE,
cron: '' cron: ''
}; };
} }
if (schedule && schedule.job_parameters) { if (gcHistory && gcHistory.job_parameters) {
this.shouldDeleteUntagged = JSON.parse(schedule.job_parameters).delete_untagged; this.shouldDeleteUntagged = JSON.parse(gcHistory.job_parameters).delete_untagged;
} else { } else {
this.shouldDeleteUntagged = false; this.shouldDeleteUntagged = false;
} }
} }
getJobs() {
this.gcRepoService.getJobs().subscribe(jobs => {
this.jobs = this.gcViewModelFactory.createJobViewModel(jobs);
});
}
gcNow(): void { gcNow(): void {
this.disableGC = true; this.disableGC = true;
setTimeout(() => { setTimeout(() => {
this.enableGc(); this.enableGc();
}, ONE_MINITUE); }, ONE_MINUTE);
this.gcRepoService.manualGc(this.shouldDeleteUntagged, false).subscribe( this.gcService.createGCSchedule({
parameters: {
delete_untagged: this.shouldDeleteUntagged,
dry_run: false
},
schedule: {
type: ScheduleType.MANUAL
}
}).subscribe(
response => { response => {
this.translate.get("GC.MSG_SUCCESS").subscribe((res: string) => { this.errorHandler.info("GC.MSG_SUCCESS");
this.errorHandler.info(res);
});
}, },
error => { error => {
this.errorHandler.error(error); this.errorHandler.error(error);
@ -105,62 +98,66 @@ export class GcComponent implements OnInit {
dryRun() { dryRun() {
this.dryRunOnGoing = true; this.dryRunOnGoing = true;
this.gcRepoService.manualGc(this.shouldDeleteUntagged, true) this.gcService.createGCSchedule({
parameters: {
delete_untagged: this.shouldDeleteUntagged,
dry_run: true
},
schedule: {
type: ScheduleType.MANUAL
}
})
.pipe(finalize(() => this.dryRunOnGoing = false)) .pipe(finalize(() => this.dryRunOnGoing = false))
.subscribe( .subscribe(
response => { response => {
this.translate.get("GC.DRY_RUN_SUCCESS").subscribe((res: string) => { this.errorHandler.info("GC.DRY_RUN_SUCCESS");
this.errorHandler.info(res); },
}); error => {
}, this.errorHandler.error(error);
error => { }
this.errorHandler.error(error); );
}
);
} }
private enableGc() { private enableGc() {
this.disableGC = false; this.disableGC = false;
} }
private resetSchedule(cron) { saveGcSchedule(cron: string) {
this.schedule = { if (this.originCron && this.originCron.type !== ScheduleType.NONE) {// no schedule, then create
schedule: { this.gcService.createGCSchedule({
type: this.CronScheduleComponent.scheduleType, parameters: {
cron: cron delete_untagged: this.shouldDeleteUntagged,
} dry_run: false
}; },
if (!cron) { schedule: {
this.shouldDeleteUntagged = false; type: GcComponent.getScheduleType(cron),
} cron: cron
this.getJobs(); }
} }).subscribe(
scheduleGc(cron: string) {
let schedule = this.schedule;
if (schedule && schedule.schedule && schedule.schedule.type !== SCHEDULE_TYPE_NONE) {
this.gcRepoService.putScheduleGc(this.shouldDeleteUntagged, this.CronScheduleComponent.scheduleType, cron).subscribe(
response => { response => {
this.translate this.errorHandler.info("GC.MSG_SCHEDULE_RESET");
.get("GC.MSG_SCHEDULE_RESET") this.CronScheduleComponent.resetSchedule();
.subscribe((res) => { this.getCurrentSchedule(); // refresh schedule
this.errorHandler.info(res);
this.CronScheduleComponent.resetSchedule();
});
this.resetSchedule(cron);
}, },
error => { error => {
this.errorHandler.error(error); this.errorHandler.error(error);
} }
); );
} else { } else {
this.gcRepoService.postScheduleGc(this.shouldDeleteUntagged, this.CronScheduleComponent.scheduleType, cron).subscribe( this.gcService.updateGCSchedule({
parameters: {
delete_untagged: this.shouldDeleteUntagged,
dry_run: false
},
schedule: {
type: GcComponent.getScheduleType(cron),
cron: cron
}
}).subscribe(
response => { response => {
this.translate.get("GC.MSG_SCHEDULE_SET").subscribe((res) => { this.errorHandler.info("GC.MSG_SCHEDULE_RESET");
this.errorHandler.info(res); this.CronScheduleComponent.resetSchedule();
this.CronScheduleComponent.resetSchedule(); this.getCurrentSchedule(); // refresh schedule
});
this.resetSchedule(cron);
}, },
error => { error => {
this.errorHandler.error(error); this.errorHandler.error(error);
@ -168,4 +165,20 @@ export class GcComponent implements OnInit {
); );
} }
} }
static getScheduleType(cron: string): 'Hourly' | 'Daily' | 'Weekly' | 'Custom' | 'Manual' | 'None' {
if (cron) {
if (cron === '0 0 * * * *') {
return ScheduleType.HOURLY;
}
if (cron === '0 0 0 * * *') {
return ScheduleType.DAILY;
}
if (cron === '0 0 0 * * 0') {
return ScheduleType.WEEKLY;
}
return ScheduleType.CUSTOM;
}
return ScheduleType.NONE;
}
} }

View File

@ -1,25 +0,0 @@
import { OriginCron } from "../../../../shared/services";
export const SCHEDULE_TYPE_NONE = "None";
export const ONE_MINITUE = 60000;
export const THREE_SECONDS = 3000;
export interface GCSchedule {
schedule?: OriginCron;
parameters?: {[key: string]: any};
id?: number;
job_name?: string;
job_kind?: string;
job_parameters?: string;
job_status?: string;
deleted?: boolean;
creation_time?: Date;
update_time?: Date;
}

View File

@ -1,72 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable, Subscription, Subject, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { GcApiRepository } from './gc.api.repository';
import { ErrorHandler } from '../../../../shared/units/error-handler';
import { GcJobData } from './gcLog';
@Injectable()
export class GcRepoService {
constructor(
private gcApiRepository: GcApiRepository,
) {}
public manualGc(shouldDeleteUntagged: boolean, isDryRun: boolean): Observable<any> {
const param = {
"schedule": {
"type": "Manual"
},
parameters: {
delete_untagged: shouldDeleteUntagged,
dry_run: isDryRun
}
};
return this.gcApiRepository.postSchedule(param);
}
public getJobs(): Observable <GcJobData []> {
return this.gcApiRepository.getJobs();
}
public getLog(id): Observable <any> {
return this.gcApiRepository.getLog(id);
}
public getSchedule(): Observable <any> {
return this.gcApiRepository.getSchedule();
}
public postScheduleGc(shouldDeleteUntagged: boolean, type, cron): Observable <any> {
let param = {
"schedule": {
"type": type,
"cron": cron,
},
parameters: {
delete_untagged: shouldDeleteUntagged
}
};
return this.gcApiRepository.postSchedule(param);
}
public putScheduleGc(shouldDeleteUntagged, type, cron): Observable <any> {
let param = {
"schedule": {
"type": type,
"cron": cron,
},
parameters: {
delete_untagged: shouldDeleteUntagged
}
};
return this.gcApiRepository.putSchedule(param);
}
public getLogLink(id): string {
return this.gcApiRepository.getLogLink(id);
}
}

View File

@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import { GcJobData, GcJobViewModel } from './gcLog';
@Injectable()
export class GcViewModelFactory {
public createJobViewModel(jobs: GcJobData[]): GcJobViewModel[] {
let gcViewModels: GcJobViewModel[] = [];
for (let job of jobs) {
let createTime = new Date(job.creation_time);
let updateTime = new Date(job.update_time);
gcViewModels.push({
id: job.id,
type: job.schedule ? job.schedule.type : null,
status: job.job_status,
parameters: job.job_parameters,
createTime: createTime,
updateTime: updateTime,
details: null
});
}
return gcViewModels;
}
}

View File

@ -1,28 +0,0 @@
export class GcJobData {
id: number;
job_name: string;
job_kind: string;
schedule: Schedule;
job_status: string;
job_parameters: string;
job_uuid: string;
creation_time: string;
update_time: string;
delete: boolean;
}
export class Schedule {
type: string;
cron: string;
}
export class GcJobViewModel {
id: number;
type: string;
status: string;
parameters: string;
createTime: Date;
updateTime: Date;
details: string;
}

View File

@ -242,3 +242,12 @@ export enum ResourceType {
} }
export const CARD_VIEW_LOCALSTORAGE_KEY = 'card-view'; export const CARD_VIEW_LOCALSTORAGE_KEY = 'card-view';
export enum ScheduleType {
NONE = "None",
DAILY = "Daily",
WEEKLY = "Weekly",
HOURLY = "Hourly",
CUSTOM = "Custom",
MANUAL = 'Manual'
}

View File

@ -47,4 +47,4 @@ export abstract class ErrorHandler {
abstract log(log: any): void; abstract log(log: any): void;
abstract handleErrorPopupUnauthorized(error: any): void; abstract handleErrorPopupUnauthorized(error: any): void;
} }