From bf317d0b2673db13ff4d27a1b40a31335ec964a5 Mon Sep 17 00:00:00 2001 From: Shijun Sun <30999793+AllForNothing@users.noreply.github.com> Date: Tue, 7 Jun 2022 15:18:36 +0800 Subject: [PATCH] Add clearances ui (#16941) Add audit log purge and forwarding ui Signed-off-by: AllForNothing --- src/portal/src/app/base/base.module.ts | 8 +- .../harbor-shell/harbor-shell.component.html | 7 +- .../history/purge-history.component.html | 64 ++++ .../history/purge-history.component.scss} | 0 .../history/purge-history.component.spec.ts | 106 ++++++ .../history/purge-history.component.ts | 137 ++++++++ .../set-job/set-job.component.html | 211 ++++++++++++ .../set-job/set-job.component.scss | 36 ++ .../set-job/set-job.component.spec.ts | 72 ++++ .../set-job/set-job.component.ts | 307 ++++++++++++++++++ .../clearing-job/clearing-job-interfact.ts | 22 ++ .../clearing-job/clearing-job.component.html | 21 ++ .../clearing-job/clearing-job.component.scss | 0 .../clearing-job.component.spec.ts} | 19 +- .../clearing-job/clearing-job.component.ts | 11 + .../clearing-job/clearing-job.module.ts | 41 +++ .../gc/gc-history/gc-history.component.html | 4 +- .../gc/gc-history/gc-history.component.scss | 13 + .../gc-history/gc-history.component.spec.ts | 10 +- .../gc/gc-history/gc-history.component.ts | 21 +- .../clearing-job/gc-page/gc/gc.component.html | 97 ++++++ .../gc-page/gc/gc.component.scss | 5 +- .../gc-page/gc/gc.component.spec.ts | 29 +- .../gc-page/gc/gc.component.ts | 106 ++++-- .../config/config.component.html | 10 + .../left-side-nav/config/config.module.ts | 6 + .../left-side-nav/config/config.service.ts | 8 +- .../app/base/left-side-nav/config/config.ts | 4 + .../config/security/security.component.html | 156 +++++++++ .../config/security/security.component.scss | 95 ++++++ .../security/security.component.spec.ts | 75 +++++ .../config/security/security.component.ts | 221 +++++++++++++ .../system/system-settings.component.html | 209 ++++-------- .../system/system-settings.component.scss | 12 +- .../system/system-settings.component.spec.ts | 77 ++--- .../system/system-settings.component.ts | 273 +++------------- .../gc-page/gc-page.component.html | 33 -- .../gc-page/gc-page.component.scss | 6 - .../gc-page/gc-page.component.ts | 23 -- .../base/left-side-nav/gc-page/gc.module.ts | 18 - .../gc-page/gc/gc.component.html | 52 --- .../cron-schedule.component.html | 6 +- .../cron-schedule.component.scss | 2 - .../cron-schedule/cron-schedule.component.ts | 3 +- src/portal/src/i18n/lang/de-de-lang.json | 23 ++ src/portal/src/i18n/lang/en-us-lang.json | 23 ++ src/portal/src/i18n/lang/es-es-lang.json | 23 ++ src/portal/src/i18n/lang/fr-fr-lang.json | 23 ++ src/portal/src/i18n/lang/pt-br-lang.json | 23 ++ src/portal/src/i18n/lang/tr-tr-lang.json | 23 ++ src/portal/src/i18n/lang/zh-cn-lang.json | 23 ++ src/portal/src/i18n/lang/zh-tw-lang.json | 23 ++ 52 files changed, 2178 insertions(+), 642 deletions(-) create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.html rename src/portal/src/app/base/left-side-nav/{gc-page/gc/gc-history/gc-history.component.scss => clearing-job/audit-log-purge/history/purge-history.component.scss} (100%) create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.spec.ts create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.ts create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.html create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.scss create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.spec.ts create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.ts create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/clearing-job-interfact.ts create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.html create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.scss rename src/portal/src/app/base/left-side-nav/{gc-page/gc-page.component.spec.ts => clearing-job/clearing-job.component.spec.ts} (52%) create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.ts create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.module.ts rename src/portal/src/app/base/left-side-nav/{ => clearing-job}/gc-page/gc/gc-history/gc-history.component.html (94%) create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.scss rename src/portal/src/app/base/left-side-nav/{ => clearing-job}/gc-page/gc/gc-history/gc-history.component.spec.ts (89%) rename src/portal/src/app/base/left-side-nav/{ => clearing-job}/gc-page/gc/gc-history/gc-history.component.ts (88%) create mode 100644 src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html rename src/portal/src/app/base/left-side-nav/{ => clearing-job}/gc-page/gc/gc.component.scss (82%) rename src/portal/src/app/base/left-side-nav/{ => clearing-job}/gc-page/gc/gc.component.spec.ts (71%) rename src/portal/src/app/base/left-side-nav/{ => clearing-job}/gc-page/gc/gc.component.ts (65%) create mode 100644 src/portal/src/app/base/left-side-nav/config/security/security.component.html create mode 100644 src/portal/src/app/base/left-side-nav/config/security/security.component.scss create mode 100644 src/portal/src/app/base/left-side-nav/config/security/security.component.spec.ts create mode 100644 src/portal/src/app/base/left-side-nav/config/security/security.component.ts delete mode 100644 src/portal/src/app/base/left-side-nav/gc-page/gc-page.component.html delete mode 100644 src/portal/src/app/base/left-side-nav/gc-page/gc-page.component.scss delete mode 100644 src/portal/src/app/base/left-side-nav/gc-page/gc-page.component.ts delete mode 100644 src/portal/src/app/base/left-side-nav/gc-page/gc.module.ts delete mode 100644 src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.html diff --git a/src/portal/src/app/base/base.module.ts b/src/portal/src/app/base/base.module.ts index 1b5ed210b..5af49487e 100644 --- a/src/portal/src/app/base/base.module.ts +++ b/src/portal/src/app/base/base.module.ts @@ -116,12 +116,12 @@ const routes: Routes = [ ).then(m => m.ProjectQuotasModule), }, { - path: 'gc', + path: 'clearing-job', canActivate: [SystemAdminGuard], loadChildren: () => - import('./left-side-nav/gc-page/gc.module').then( - m => m.GcModule - ), + import( + './left-side-nav/clearing-job/clearing-job.module' + ).then(m => m.ClearingJobModule), }, { path: 'configs', diff --git a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html index cf2d489a0..4d09e181a 100644 --- a/src/portal/src/app/base/harbor-shell/harbor-shell.component.html +++ b/src/portal/src/app/base/harbor-shell/harbor-shell.component.html @@ -160,15 +160,12 @@ - {{ - 'SIDE_NAV.SYSTEM_MGMT.GARBAGE_COLLECTION' - | translate - }} + {{ 'CLEARANCES.CLEARANCES' | translate }} + {{ 'CLEARANCES.PURGE_HISTORY' | translate }} + + + + + + {{ + 'GC.JOB_ID' | translate + }} + {{ 'GC.TRIGGER_TYPE' | translate }} + {{ 'TAG_RETENTION.DRY_RUN' | translate }} + {{ 'STATUS' | translate }} + {{ 'CREATION_TIME' | translate }} + {{ + 'UPDATE_TIME' | translate + }} + {{ 'LOGS' | translate }} + + {{ job.id }} + {{ + (job.schedule?.type + ? 'SCHEDULE.' + job.schedule?.type.toUpperCase() + : '' + ) | translate + }} + {{ + isDryRun(job?.job_parameters) | translate + }} + {{ + job.job_status.toUpperCase() | translate + }} + {{ + job.creation_time | harborDatetime: 'medium' + }} + {{ + job.update_time | harborDatetime: 'medium' + }} + + + + + + + + + {{ + 'PAGINATION.PAGE_SIZE' | translate + }} + {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} + {{ 'DESTINATION.OF' | translate }} + {{ total }} {{ 'DESTINATION.ITEMS' | translate }} + + + diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.scss b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.scss similarity index 100% rename from src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.scss rename to src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.scss diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.spec.ts b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.spec.ts new file mode 100644 index 000000000..2d9925a04 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.spec.ts @@ -0,0 +1,106 @@ +import { + ComponentFixture, + ComponentFixtureAutoDetect, + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; +import { of } from 'rxjs'; +import { SharedTestingModule } from '../../../../../shared/shared.module'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { CURRENT_BASE_HREF } from '../../../../../shared/units/utils'; +import { delay } from 'rxjs/operators'; +import { PurgeHistoryComponent } from './purge-history.component'; +import { ExecHistory } from '../../../../../../../ng-swagger-gen/models/exec-history'; +import { PurgeService } from 'ng-swagger-gen/services/purge.service'; + +describe('GcHistoryComponent', () => { + let component: PurgeHistoryComponent; + let fixture: ComponentFixture; + const mockJobs: ExecHistory[] = [ + { + id: 1, + job_name: 'test', + job_kind: 'manual', + schedule: null, + job_status: 'pending', + job_parameters: '{"dry_run":true}', + creation_time: null, + update_time: null, + }, + { + id: 2, + job_name: 'test', + job_kind: 'manual', + schedule: null, + job_status: 'finished', + job_parameters: '{"dry_run":true}', + creation_time: null, + update_time: null, + }, + ]; + const fakedPurgeService = { + count: 0, + getPurgeHistoryResponse() { + if (this.count === 0) { + this.count += 1; + const response: HttpResponse> = + new HttpResponse>({ + headers: new HttpHeaders({ + 'x-total-count': [mockJobs[0]].length.toString(), + }), + body: [mockJobs[0]], + }); + return of(response).pipe(delay(0)); + } else { + this.count += 1; + const response: HttpResponse> = + new HttpResponse>({ + headers: new HttpHeaders({ + 'x-total-count': [mockJobs[1]].length.toString(), + }), + body: [mockJobs[1]], + }); + return of(response).pipe(delay(0)); + } + }, + }; + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [PurgeHistoryComponent], + imports: [SharedTestingModule], + providers: [ + { provide: PurgeService, useValue: fakedPurgeService }, + // open auto detect + { provide: ComponentFixtureAutoDetect, useValue: true }, + ], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PurgeHistoryComponent); + component = fixture.componentInstance; + 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'); + }); + })); + it('should return right log link', () => { + expect(component.getLogLink('1')).toEqual( + `${CURRENT_BASE_HREF}/system/purgeaudit/1/log` + ); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.ts b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.ts new file mode 100644 index 000000000..a625851ba --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/history/purge-history.component.ts @@ -0,0 +1,137 @@ +import { Component, OnDestroy } from '@angular/core'; +import { ClrDatagridStateInterface } from '@clr/angular'; +import { GCHistory } from 'ng-swagger-gen/models/gchistory'; +import { finalize, Subscription, timer } from 'rxjs'; +import { REFRESH_TIME_DIFFERENCE } from 'src/app/shared/entities/shared.const'; +import { ErrorHandler } from 'src/app/shared/units/error-handler/error-handler'; +import { + CURRENT_BASE_HREF, + getPageSizeFromLocalStorage, + getSortingString, + PageSizeMapKeys, + setPageSizeToLocalStorage, +} from 'src/app/shared/units/utils'; +import { PurgeService } from '../../../../../../../ng-swagger-gen/services/purge.service'; +import { JOB_STATUS, NO, YES } from '../../clearing-job-interfact'; + +@Component({ + selector: 'app-purge-history', + templateUrl: './purge-history.component.html', + styleUrls: ['./purge-history.component.scss'], +}) +export class PurgeHistoryComponent implements OnDestroy { + jobs: Array = []; + loading: boolean = true; + timerDelay: Subscription; + pageSize: number = getPageSizeFromLocalStorage( + PageSizeMapKeys.GC_HISTORY_COMPONENT, + 5 + ); + page: number = 1; + total: number = 0; + state: ClrDatagridStateInterface; + constructor( + private purgeService: PurgeService, + private errorHandler: ErrorHandler + ) {} + refresh() { + this.page = 1; + this.total = 0; + this.getJobs(true); + } + + getJobs(withLoading: boolean, state?: ClrDatagridStateInterface) { + if (state) { + this.state = state; + } + if (state && state.page) { + this.pageSize = state.page.size; + setPageSizeToLocalStorage( + PageSizeMapKeys.GC_HISTORY_COMPONENT, + this.pageSize + ); + } + 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); + } + if (withLoading) { + this.loading = true; + } + this.purgeService + .getPurgeHistoryResponse({ + page: this.page, + pageSize: this.pageSize, + q: q, + sort: sort, + }) + .pipe(finalize(() => (this.loading = false))) + .subscribe( + res => { + // Get total count + if (res.headers) { + const xHeader: string = + res.headers.get('X-Total-Count'); + if (xHeader) { + this.total = parseInt(xHeader, 0); + } + 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(false, this.state); + } else { + this.timerDelay.unsubscribe(); + this.timerDelay = null; + } + }); + } + }, + error => { + this.errorHandler.error(error); + this.loading = false; + } + ); + } + + isDryRun(param: string): string { + if (param) { + const paramObj: any = JSON.parse(param); + if (paramObj && paramObj.dry_run) { + return YES; + } + } + return NO; + } + + ngOnDestroy() { + if (this.timerDelay) { + this.timerDelay.unsubscribe(); + this.timerDelay = null; + } + } + + getLogLink(id): string { + return `${CURRENT_BASE_HREF}/system/purgeaudit/${id}/log`; + } +} diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.html b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.html new file mode 100644 index 000000000..a4a68be68 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.html @@ -0,0 +1,211 @@ +
+ +
+
+
+
+ {{ 'WEBHOOK.STATUS' | translate }} +
+
+
+
+ {{ + 'CLEARANCES.LAST_COMPLETED' | translate + }} + + + + {{ + 'SCHEDULE.NONE' | translate + }} + {{ lastCompletedTime | harborDatetime + }}({{ + 'TAG_RETENTION.DRY_RUN' | translate + }}) + + +
+
+ {{ + 'CLEARANCES.NEXT_SCHEDULED_TIME' | translate + }} + {{ + nextScheduledTime | harborDatetime + }} +
+
+
+
+
+ +
+ +
+
+ {{ 'CLEARANCES.KEEP_IN' | translate }} + + + + {{ + 'CLEARANCES.KEEP_IN_TOOLTIP' | translate + }} + + +
+
+
+ + + + {{ 'CLEARANCES.KEEP_IN_ERROR' | translate }} + +
+
+ +
+
+
+
+
+ {{ 'CLEARANCES.INCLUDED_OPERATIONS' | translate + }} + + + {{ + 'CLEARANCES.INCLUDED_OPERATION_TOOLTIP' | translate + }} + + +
+
+ + +
+
+ + {{ + 'CLEARANCES.INCLUDED_OPERATION_ERROR' | translate + }} +
+
+
+
+
+
+ +
+
+ +
+
+ +
diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.scss b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.scss new file mode 100644 index 000000000..2b06400d2 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.scss @@ -0,0 +1,36 @@ +.cron-selection { + display: flex; + align-items: center; +} + +.gc-start-btn { + width:150px; + margin-top: 35px; +} + +.flex-200 { + flex: 0 0 200px; + max-width: 200px; +} + +.mt-05 { + margin-top: 0.5rem; +} + +.font-weight-400 { + font-weight: 400; +} +.center { + justify-content: center; + align-items: center; +} +.flex { + display: flex; +} +.mt-08 { + margin-top: 0.8rem; +} +.unit-select { + padding-top: 1px; + margin-left: 2rem; +} diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.spec.ts b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.spec.ts new file mode 100644 index 000000000..bfb880944 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.spec.ts @@ -0,0 +1,72 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule'; +import { CronTooltipComponent } from '../../../../../shared/components/cron-schedule'; +import { of } from 'rxjs'; +import { SharedTestingModule } from '../../../../../shared/shared.module'; +import { SetJobComponent } from './set-job.component'; +import { PurgeService } from 'ng-swagger-gen/services/purge.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('GcComponent', () => { + let component: SetJobComponent; + let fixture: ComponentFixture; + let purgeService: PurgeService; + let mockSchedule = []; + const fakedErrorHandler = { + error(error) { + return error; + }, + info(info) { + return info; + }, + }; + let spySchedule: jasmine.Spy; + let spyGcNow: jasmine.Spy; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [SharedTestingModule], + declarations: [ + SetJobComponent, + CronScheduleComponent, + CronTooltipComponent, + ], + providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SetJobComponent); + component = fixture.componentInstance; + + purgeService = fixture.debugElement.injector.get(PurgeService); + spySchedule = spyOn(purgeService, 'getPurgeSchedule').and.returnValues( + of(mockSchedule as any) + ); + spyGcNow = spyOn(purgeService, 'createPurgeSchedule').and.returnValues( + of(null) + ); + fixture.detectChanges(); + }); + it('should create', () => { + expect(component).toBeTruthy(); + }); + it('should get schedule and job', () => { + expect(spySchedule.calls.count()).toEqual(1); + }); + it('should trigger gcNow', () => { + const ele: HTMLButtonElement = + fixture.nativeElement.querySelector('#gc-now'); + ele.click(); + fixture.detectChanges(); + 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); + }); +}); diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.ts b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.ts new file mode 100644 index 000000000..8bb2c7efb --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/audit-log-purge/set-job/set-job.component.ts @@ -0,0 +1,307 @@ +import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule'; +import { OriginCron } from '../../../../../shared/services'; +import { finalize } from 'rxjs/operators'; +import { ScheduleType } from '../../../../../shared/entities/shared.const'; +import { GcComponent } from '../../gc-page/gc/gc.component'; +import { PurgeService } from '../../../../../../../ng-swagger-gen/services/purge.service'; +import { ExecHistory } from '../../../../../../../ng-swagger-gen/models/exec-history'; +import { + JOB_STATUS, + REFRESH_STATUS_TIME_DIFFERENCE, + RETENTION_OPERATIONS, + RETENTION_OPERATIONS_I18N_MAP, + RetentionTimeUnit, +} from '../../clearing-job-interfact'; +import { clone } from '../../../../../shared/units/utils'; +import { PurgeHistoryComponent } from '../history/purge-history.component'; + +const ONE_MINUTE: number = 60000; +const ONE_DAY: number = 24; + +@Component({ + selector: 'app-set-job', + templateUrl: './set-job.component.html', + styleUrls: ['./set-job.component.scss'], +}) +export class SetJobComponent implements OnInit, OnDestroy { + originCron: OriginCron; + disableGC: boolean = false; + getLabelCurrent = 'CLEARANCES.SCHEDULE_TO_PURGE'; + loadingGcStatus = false; + @ViewChild(CronScheduleComponent) + CronScheduleComponent: CronScheduleComponent; + dryRunOnGoing: boolean = false; + lastCompletedTime: string; + loadingLastCompletedTime: boolean = false; + isDryRun: boolean = false; + nextScheduledTime: string; + statusTimeout: any; + + retentionTime: number; + retentionUnit: string = RetentionTimeUnit.DAYS; + operations: string[] = clone(RETENTION_OPERATIONS); + selectedOperations: string[] = clone(RETENTION_OPERATIONS); + @ViewChild(PurgeHistoryComponent) + purgeHistoryComponent: PurgeHistoryComponent; + constructor( + private purgeService: PurgeService, + private errorHandler: ErrorHandler + ) {} + + ngOnInit() { + this.getCurrentSchedule(true); + this.getStatus(); + } + ngOnDestroy() { + if (this.statusTimeout) { + clearTimeout(this.statusTimeout); + this.statusTimeout = null; + } + } + // get the latest non-dry-run execution to get the status + getStatus() { + this.loadingLastCompletedTime = true; + this.purgeService + .getPurgeHistory({ + page: 1, + pageSize: 1, + sort: '-update_time', + }) + .subscribe(res => { + if (res?.length) { + this.isDryRun = JSON.parse(res[0]?.job_parameters).dry_run; + this.lastCompletedTime = res[0]?.update_time; + if ( + res[0]?.job_status === JOB_STATUS.RUNNING || + res[0]?.job_status === JOB_STATUS.PENDING + ) { + this.statusTimeout = setTimeout(() => { + this.getStatus(); + }, REFRESH_STATUS_TIME_DIFFERENCE); + } else { + this.loadingLastCompletedTime = false; + } + } + }); + } + getCurrentSchedule(withLoading: boolean) { + if (withLoading) { + this.loadingGcStatus = true; + } + this.purgeService + .getPurgeSchedule() + .pipe( + finalize(() => { + this.loadingGcStatus = false; + }) + ) + .subscribe({ + next: schedule => { + this.initSchedule(schedule); + }, + error: error => { + this.errorHandler.error(error); + }, + }); + } + + initSchedule(purgeHistory: ExecHistory) { + if ((purgeHistory?.schedule as any)?.next_scheduled_time) { + this.nextScheduledTime = ( + purgeHistory.schedule as any + )?.next_scheduled_time; + } + if (purgeHistory && purgeHistory.schedule) { + this.originCron = { + type: purgeHistory.schedule.type, + cron: purgeHistory.schedule.cron, + }; + if (purgeHistory && purgeHistory.job_parameters) { + const obj = JSON.parse(purgeHistory.job_parameters); + if (obj?.include_operations) { + this.selectedOperations = + obj?.include_operations?.split(','); + } else { + this.selectedOperations = []; + } + if ( + obj?.audit_retention_hour > ONE_DAY && + obj?.audit_retention_hour % ONE_DAY === 0 + ) { + this.retentionTime = obj?.audit_retention_hour / ONE_DAY; + this.retentionUnit = RetentionTimeUnit.DAYS; + } else { + this.retentionTime = obj?.audit_retention_hour; + this.retentionUnit = RetentionTimeUnit.HOURS; + } + } else { + this.retentionTime = null; + this.selectedOperations = clone(RETENTION_OPERATIONS); + this.retentionUnit = RetentionTimeUnit.DAYS; + } + } else { + this.originCron = { + type: ScheduleType.NONE, + cron: '', + }; + } + } + + gcNow(): void { + this.disableGC = true; + setTimeout(() => { + this.enableGc(); + }, ONE_MINUTE); + const retentionTime: number = + this.retentionUnit === RetentionTimeUnit.DAYS + ? this.retentionTime * 24 + : this.retentionTime; + this.purgeService + .createPurgeSchedule({ + schedule: { + parameters: { + audit_retention_hour: +retentionTime, + include_operations: this.selectedOperations.join(','), + dry_run: false, + }, + schedule: { + type: ScheduleType.MANUAL, + }, + }, + }) + .subscribe({ + next: response => { + this.errorHandler.info('CLEARANCES.PURGE_NOW_SUCCESS'); + this.refresh(); + }, + error: error => { + this.errorHandler.error(error); + }, + }); + } + + dryRun() { + this.dryRunOnGoing = true; + const retentionTime: number = + this.retentionUnit === RetentionTimeUnit.DAYS + ? this.retentionTime * 24 + : this.retentionTime; + this.purgeService + .createPurgeSchedule({ + schedule: { + parameters: { + audit_retention_hour: +retentionTime, + include_operations: this.selectedOperations.join(','), + dry_run: true, + }, + schedule: { + type: ScheduleType.MANUAL, + }, + }, + }) + .pipe(finalize(() => (this.dryRunOnGoing = false))) + .subscribe({ + next: response => { + this.errorHandler.info('GC.DRY_RUN_SUCCESS'); + this.refresh(); + }, + error: error => { + this.errorHandler.error(error); + }, + }); + } + + private enableGc() { + this.disableGC = false; + } + + saveGcSchedule(cron: string) { + const retentionTime: number = + this.retentionUnit === RetentionTimeUnit.DAYS + ? this.retentionTime * 24 + : this.retentionTime; + if (this.originCron && this.originCron.type === ScheduleType.NONE) { + // no schedule, then create + this.purgeService + .createPurgeSchedule({ + schedule: { + parameters: { + audit_retention_hour: +retentionTime, + include_operations: + this.selectedOperations.join(','), + dry_run: false, + }, + schedule: { + type: GcComponent.getScheduleType(cron), + cron: cron, + }, + }, + }) + .subscribe({ + next: response => { + this.errorHandler.info( + 'CLEARANCES.PURGE_SCHEDULE_RESET' + ); + this.CronScheduleComponent.resetSchedule(); + this.getCurrentSchedule(false); // refresh schedule + }, + error: error => { + this.errorHandler.error(error); + }, + }); + } else { + this.purgeService + .updatePurgeSchedule({ + schedule: { + parameters: { + audit_retention_hour: +retentionTime, + include_operations: + this.selectedOperations.join(','), + dry_run: false, + }, + schedule: { + type: GcComponent.getScheduleType(cron), + cron: cron, + }, + }, + }) + .subscribe({ + next: response => { + this.errorHandler.info( + 'CLEARANCES.PURGE_SCHEDULE_RESET' + ); + this.CronScheduleComponent.resetSchedule(); + this.getCurrentSchedule(false); // refresh schedule + }, + error: error => { + this.errorHandler.error(error); + }, + }); + } + } + hasOperation(operation: string): boolean { + return this.selectedOperations?.indexOf(operation) !== -1; + } + operationsToText(operation: string): string { + if (RETENTION_OPERATIONS_I18N_MAP[operation]) { + return RETENTION_OPERATIONS_I18N_MAP[operation]; + } + return operation; + } + setOperation(operation: string) { + if (this.selectedOperations.indexOf(operation) === -1) { + this.selectedOperations.push(operation); + } else { + this.selectedOperations.splice( + this.selectedOperations.findIndex(item => item === operation), + 1 + ); + } + } + refresh() { + this.getStatus(); + this.purgeHistoryComponent?.refresh(); + } +} diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job-interfact.ts b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job-interfact.ts new file mode 100644 index 000000000..262e4bb66 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job-interfact.ts @@ -0,0 +1,22 @@ +export enum RetentionTimeUnit { + HOURS = 'hours', + DAYS = 'days', +} + +export const RETENTION_OPERATIONS = ['create', 'delete', 'pull']; + +export const RETENTION_OPERATIONS_I18N_MAP = { + pull: 'AUDIT_LOG.PULL', + create: 'AUDIT_LOG.CREATE', + delete: 'AUDIT_LOG.DELETE', +}; + +export const JOB_STATUS = { + PENDING: 'Pending', + RUNNING: 'Running', +}; + +export const YES: string = 'TAG_RETENTION.YES'; +export const NO: string = 'TAG_RETENTION.NO'; + +export const REFRESH_STATUS_TIME_DIFFERENCE: number = 5000; diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.html b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.html new file mode 100644 index 000000000..aa8b022c3 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.html @@ -0,0 +1,21 @@ +

+ {{ 'CLEARANCES.CLEARANCES' | translate }} +

+ + diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.scss b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc-page.component.spec.ts b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.spec.ts similarity index 52% rename from src/portal/src/app/base/left-side-nav/gc-page/gc-page.component.spec.ts rename to src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.spec.ts index 975953b0c..ab8421257 100644 --- a/src/portal/src/app/base/left-side-nav/gc-page/gc-page.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.spec.ts @@ -1,30 +1,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { SessionService } from '../../../shared/services/session.service'; -import { GcPageComponent } from './gc-page.component'; +import { ClearingJobComponent } from './clearing-job.component'; import { SharedTestingModule } from '../../../shared/shared.module'; describe('GcPageComponent', () => { - let component: GcPageComponent; - let fixture: ComponentFixture; - let fakeSessionService = { - getCurrentUser: function () { - return { has_admin_role: true }; - }, - }; + let component: ClearingJobComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [GcPageComponent], + declarations: [ClearingJobComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [SharedTestingModule], - providers: [ - { provide: SessionService, useValue: fakeSessionService }, - ], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(GcPageComponent); + fixture = TestBed.createComponent(ClearingJobComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.ts b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.ts new file mode 100644 index 000000000..8254d9884 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-clearing-job', + templateUrl: './clearing-job.component.html', + styleUrls: ['./clearing-job.component.scss'], +}) +export class ClearingJobComponent { + inProgress: boolean = true; + constructor() {} +} diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.module.ts b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.module.ts new file mode 100644 index 000000000..ea54fc313 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/clearing-job.module.ts @@ -0,0 +1,41 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { GcComponent } from './gc-page/gc/gc.component'; +import { GcHistoryComponent } from './gc-page/gc/gc-history/gc-history.component'; +import { SharedModule } from '../../../shared/shared.module'; +import { SetJobComponent } from './audit-log-purge/set-job/set-job.component'; +import { ClearingJobComponent } from './clearing-job.component'; +import { PurgeHistoryComponent } from './audit-log-purge/history/purge-history.component'; + +const routes: Routes = [ + { + path: '', + component: ClearingJobComponent, + children: [ + { + path: 'gc', + component: GcComponent, + }, + { + path: 'audit-log-purge', + component: SetJobComponent, + }, + { + path: '', + redirectTo: 'gc', + pathMatch: 'full', + }, + ], + }, +]; +@NgModule({ + imports: [SharedModule, RouterModule.forChild(routes)], + declarations: [ + GcComponent, + GcHistoryComponent, + ClearingJobComponent, + SetJobComponent, + PurgeHistoryComponent, + ], +}) +export class ClearingJobModule {} diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.html b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.html similarity index 94% rename from src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.html rename to src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.html index 234a9132c..8cc2bf738 100644 --- a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.html +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.html @@ -1,4 +1,4 @@ -
+
{{ 'GC.JOB_HISTORY' | translate }}
@@ -51,7 +51,7 @@ [clrDgPageSize]="pageSize" [(clrDgPage)]="page" [clrDgTotalItems]="total"> - {{ + {{ 'PAGINATION.PAGE_SIZE' | translate }} { diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.ts b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.ts similarity index 88% rename from src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.ts rename to src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.ts index b344dff80..f5a9ff777 100644 --- a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc-history/gc-history.component.ts +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc-history/gc-history.component.ts @@ -1,25 +1,19 @@ import { Component, OnDestroy } from '@angular/core'; -import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { ErrorHandler } from '../../../../../../shared/units/error-handler'; import { Subscription, timer } from 'rxjs'; -import { REFRESH_TIME_DIFFERENCE } from '../../../../../shared/entities/shared.const'; -import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service'; +import { REFRESH_TIME_DIFFERENCE } from '../../../../../../shared/entities/shared.const'; +import { GcService } from '../../../../../../../../ng-swagger-gen/services/gc.service'; import { CURRENT_BASE_HREF, getPageSizeFromLocalStorage, getSortingString, PageSizeMapKeys, setPageSizeToLocalStorage, -} from '../../../../../shared/units/utils'; +} 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 = { - PENDING: 'Pending', - RUNNING: 'Running', -}; -const YES: string = 'TAG_RETENTION.YES'; -const NO: string = 'TAG_RETENTION.NO'; +import { GCHistory } from '../../../../../../../../ng-swagger-gen/models/gchistory'; +import { JOB_STATUS, NO, YES } from '../../../clearing-job-interfact'; @Component({ selector: 'gc-history', @@ -31,7 +25,8 @@ export class GcHistoryComponent implements OnDestroy { loading: boolean = true; timerDelay: Subscription; pageSize: number = getPageSizeFromLocalStorage( - PageSizeMapKeys.GC_HISTORY_COMPONENT + PageSizeMapKeys.GC_HISTORY_COMPONENT, + 5 ); page: number = 1; total: number = 0; diff --git a/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html new file mode 100644 index 000000000..80ab32fb3 --- /dev/null +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.html @@ -0,0 +1,97 @@ +
+ +
+
+
+
+ {{ 'WEBHOOK.STATUS' | translate }} +
+
+
+
+ {{ + 'CLEARANCES.LAST_COMPLETED' | translate + }} + + + + {{ + 'SCHEDULE.NONE' | translate + }} + {{ lastCompletedTime | harborDatetime + }}({{ + 'TAG_RETENTION.DRY_RUN' | translate + }}) + + +
+
+ {{ + 'CLEARANCES.NEXT_SCHEDULED_TIME' | translate + }} + {{ + nextScheduledTime | harborDatetime + }} +
+
+
+
+
+ +
+
+
+
+ {{ 'GC.EXPLAIN' | translate }} +
+
+
+
+
+ + + + + + +
+
+
+
+ +
+
+ +
+
+ +
diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.scss b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.scss similarity index 82% rename from src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.scss rename to src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.scss index 74840d900..b718950ac 100644 --- a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.scss +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.scss @@ -1,5 +1,4 @@ .cron-selection { - margin-top: 1rem; display: flex; align-items: center; } @@ -26,3 +25,7 @@ font-weight: 100; font-size: 10px; } +.center { + justify-content: center; + align-items: center; +} diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.spec.ts b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.spec.ts similarity index 71% rename from src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.spec.ts rename to src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.spec.ts index 509a3c9f5..526d4d9a3 100644 --- a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.spec.ts @@ -1,12 +1,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { GcComponent } from './gc.component'; -import { ErrorHandler } from '../../../../shared/units/error-handler'; -import { CronScheduleComponent } from '../../../../shared/components/cron-schedule'; -import { CronTooltipComponent } from '../../../../shared/components/cron-schedule'; +import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule'; +import { CronTooltipComponent } from '../../../../../shared/components/cron-schedule'; import { of } from 'rxjs'; -import { SharedTestingModule } from '../../../../shared/shared.module'; -import { GcService } from '../../../../../../ng-swagger-gen/services/gc.service'; -import { ScheduleType } from '../../../../shared/entities/shared.const'; +import { SharedTestingModule } from '../../../../../shared/shared.module'; +import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service'; +import { ScheduleType } from '../../../../../shared/entities/shared.const'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('GcComponent', () => { let component: GcComponent; @@ -23,6 +24,7 @@ describe('GcComponent', () => { }; let spySchedule: jasmine.Spy; let spyGcNow: jasmine.Spy; + let spyStatus: jasmine.Spy; beforeEach(() => { TestBed.configureTestingModule({ imports: [SharedTestingModule], @@ -32,6 +34,7 @@ describe('GcComponent', () => { CronTooltipComponent, ], providers: [{ provide: ErrorHandler, useValue: fakedErrorHandler }], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); }); @@ -46,6 +49,20 @@ describe('GcComponent', () => { spyGcNow = spyOn(gcRepoService, 'createGCSchedule').and.returnValues( of(null) ); + spyStatus = spyOn(gcRepoService, 'getGCHistory').and.returnValues( + of([ + { + id: 1, + job_name: 'test', + job_kind: 'manual', + schedule: null, + job_status: 'finished', + job_parameters: '{"dry_run":true}', + creation_time: null, + update_time: null, + }, + ]) + ); fixture.detectChanges(); }); it('should create', () => { diff --git a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.ts b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.ts similarity index 65% rename from src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.ts rename to src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.ts index 2130fb3e3..087a1f20c 100644 --- a/src/portal/src/app/base/left-side-nav/gc-page/gc/gc.component.ts +++ b/src/portal/src/app/base/left-side-nav/clearing-job/gc-page/gc/gc.component.ts @@ -1,17 +1,16 @@ -import { - Component, - Output, - EventEmitter, - ViewChild, - OnInit, -} from '@angular/core'; -import { ErrorHandler } from '../../../../shared/units/error-handler'; -import { CronScheduleComponent } from '../../../../shared/components/cron-schedule'; -import { OriginCron } from '../../../../shared/services'; +import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core'; +import { ErrorHandler } from '../../../../../shared/units/error-handler'; +import { CronScheduleComponent } from '../../../../../shared/components/cron-schedule'; +import { OriginCron } from '../../../../../shared/services'; 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'; +import { GcService } from '../../../../../../../ng-swagger-gen/services/gc.service'; +import { GCHistory } from '../../../../../../../ng-swagger-gen/models/gchistory'; +import { ScheduleType } from '../../../../../shared/entities/shared.const'; +import { GcHistoryComponent } from './gc-history/gc-history.component'; +import { + JOB_STATUS, + REFRESH_STATUS_TIME_DIFFERENCE, +} from '../../clearing-job-interfact'; const ONE_MINUTE = 60000; @@ -20,16 +19,22 @@ const ONE_MINUTE = 60000; templateUrl: './gc.component.html', styleUrls: ['./gc.component.scss'], }) -export class GcComponent implements OnInit { +export class GcComponent implements OnInit, OnDestroy { originCron: OriginCron; disableGC: boolean = false; getLabelCurrent = 'GC.CURRENT_SCHEDULE'; - @Output() loadingGcStatus = new EventEmitter(); + loadingGcStatus = false; @ViewChild(CronScheduleComponent) CronScheduleComponent: CronScheduleComponent; shouldDeleteUntagged: boolean; dryRunOnGoing: boolean = false; + lastCompletedTime: string; + loadingLastCompletedTime: boolean = false; + isDryRun: boolean = false; + nextScheduledTime: string; + statusTimeout: any; + @ViewChild(GcHistoryComponent) gcHistoryComponent: GcHistoryComponent; constructor( private gcService: GcService, private errorHandler: ErrorHandler @@ -37,15 +42,47 @@ export class GcComponent implements OnInit { ngOnInit() { this.getCurrentSchedule(); + this.getStatus(); + } + ngOnDestroy() { + if (this.statusTimeout) { + clearTimeout(this.statusTimeout); + this.statusTimeout = null; + } + } + // get the latest non-dry-run execution to get the status + getStatus() { + this.loadingLastCompletedTime = true; + this.gcService + .getGCHistory({ + page: 1, + pageSize: 1, + sort: '-update_time', + }) + .subscribe(res => { + if (res?.length) { + this.isDryRun = JSON.parse(res[0]?.job_parameters).dry_run; + this.lastCompletedTime = res[0]?.update_time; + if ( + res[0]?.job_status === JOB_STATUS.RUNNING || + res[0]?.job_status === JOB_STATUS.PENDING + ) { + this.statusTimeout = setTimeout(() => { + this.getStatus(); + }, REFRESH_STATUS_TIME_DIFFERENCE); + } else { + this.loadingLastCompletedTime = false; + } + } + }); } - getCurrentSchedule() { - this.loadingGcStatus.emit(true); + this.loadingGcStatus = true; this.gcService .getGCSchedule() .pipe( finalize(() => { - this.loadingGcStatus.emit(false); + this.loadingGcStatus = false; }) ) .subscribe( @@ -59,6 +96,11 @@ export class GcComponent implements OnInit { } private initSchedule(gcHistory: GCHistory) { + if ((gcHistory?.schedule as any)?.next_scheduled_time) { + this.nextScheduledTime = ( + gcHistory.schedule as any + )?.next_scheduled_time; + } if (gcHistory && gcHistory.schedule) { this.originCron = { type: gcHistory.schedule.type, @@ -97,14 +139,15 @@ export class GcComponent implements OnInit { }, }, }) - .subscribe( - response => { + .subscribe({ + next: response => { this.errorHandler.info('GC.MSG_SUCCESS'); + this.refresh(); }, - error => { + error: error => { this.errorHandler.error(error); - } - ); + }, + }); } dryRun() { @@ -122,14 +165,15 @@ export class GcComponent implements OnInit { }, }) .pipe(finalize(() => (this.dryRunOnGoing = false))) - .subscribe( - response => { + .subscribe({ + next: response => { this.errorHandler.info('GC.DRY_RUN_SUCCESS'); + this.refresh(); }, - error => { + error: error => { this.errorHandler.error(error); - } - ); + }, + }); } private enableGc() { @@ -137,7 +181,7 @@ export class GcComponent implements OnInit { } saveGcSchedule(cron: string) { - if (this.originCron && this.originCron.type !== ScheduleType.NONE) { + if (this.originCron && this.originCron.type === ScheduleType.NONE) { // no schedule, then create this.gcService .createGCSchedule({ @@ -206,4 +250,8 @@ export class GcComponent implements OnInit { } return ScheduleType.NONE; } + refresh() { + this.getStatus(); + this.gcHistoryComponent?.refresh(); + } } diff --git a/src/portal/src/app/base/left-side-nav/config/config.component.html b/src/portal/src/app/base/left-side-nav/config/config.component.html index dcca9f4df..cf310fd21 100644 --- a/src/portal/src/app/base/left-side-nav/config/config.component.html +++ b/src/portal/src/app/base/left-side-nav/config/config.component.html @@ -27,6 +27,16 @@ {{ 'CONFIG.EMAIL' | translate }} +