diff --git a/src/portal/lib/src/config/registry-config.component.ts b/src/portal/lib/src/config/registry-config.component.ts index fcf6ed468..f7f8acd1b 100644 --- a/src/portal/lib/src/config/registry-config.component.ts +++ b/src/portal/lib/src/config/registry-config.component.ts @@ -13,7 +13,7 @@ import { clone } from '../utils'; import { ErrorHandler } from '../error-handler/index'; -import { SystemSettingsComponent, VulnerabilityConfigComponent } from './index'; +import { SystemSettingsComponent, VulnerabilityConfigComponent} from './index'; import { Configuration } from './config'; @Component({ diff --git a/src/portal/lib/src/config/system/system-settings.component.ts b/src/portal/lib/src/config/system/system-settings.component.ts index 29524fa09..4ec95e194 100644 --- a/src/portal/lib/src/config/system/system-settings.component.ts +++ b/src/portal/lib/src/config/system/system-settings.component.ts @@ -1,6 +1,5 @@ import { Component, Input, Output, EventEmitter, ViewChild, Inject } from '@angular/core'; import { NgForm } from '@angular/forms'; - import { Configuration } from '../config'; import { SERVICE_CONFIG, IServiceConfig } from '../../service.config'; diff --git a/src/portal/lib/tsconfig.lib.json b/src/portal/lib/tsconfig.lib.json index edaab721f..07f7260fc 100644 --- a/src/portal/lib/tsconfig.lib.json +++ b/src/portal/lib/tsconfig.lib.json @@ -11,7 +11,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, - "types": [], + "typeRoots": ["node_modules/@types"], "lib": [ "dom", "es2015" diff --git a/src/portal/package.json b/src/portal/package.json index bda59bcd5..7f2a88ea5 100644 --- a/src/portal/package.json +++ b/src/portal/package.json @@ -4,7 +4,7 @@ "description": "Harbor UI with Clarity", "angular-cli": {}, "scripts": { - "start": "ng serve --aot --ssl true --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json", + "start": "ng serve --ssl true --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json", "lint": "tslint \"src/**/*.ts\"", "lint:lib": "tslint \"lib/**/*.ts\" -e \"lib/dist/**/*\" ", "test": "ng test harbor-portal", diff --git a/src/portal/src/app/app.module.ts b/src/portal/src/app/app.module.ts index 7d7b0023b..1601dbb46 100644 --- a/src/portal/src/app/app.module.ts +++ b/src/portal/src/app/app.module.ts @@ -20,12 +20,21 @@ import { HarborRoutingModule } from './harbor-routing.module'; import { SharedModule } from './shared/shared.module'; import { AccountModule } from './account/account.module'; import { ConfigurationModule } from './config/config.module'; +import { registerLocaleData } from '@angular/common'; import { TranslateService } from "@ngx-translate/core"; import { AppConfigService } from './app-config.service'; -import {SkinableConfig} from "./skinable-config.service"; +import { SkinableConfig } from "./skinable-config.service"; import { ProjectConfigComponent } from './project/project-config/project-config.component'; +import zh from '@angular/common/locales/zh-Hans'; +import es from '@angular/common/locales/es'; +import localeFr from '@angular/common/locales/fr'; +registerLocaleData(zh, 'zh-cn'); +registerLocaleData(es, 'es-es'); +registerLocaleData(localeFr, 'fr-fr'); + + export function initConfig(configService: AppConfigService, skinableService: SkinableConfig) { return () => { skinableService.getCustomFile(); diff --git a/src/portal/src/app/config/config.component.html b/src/portal/src/app/config/config.component.html index 6bb025986..9d31b3738 100644 --- a/src/portal/src/app/config/config.component.html +++ b/src/portal/src/app/config/config.component.html @@ -18,6 +18,9 @@ +
@@ -34,6 +37,9 @@
+
+ +
diff --git a/src/portal/src/app/config/config.component.ts b/src/portal/src/app/config/config.component.ts index 533970227..5d04d8e35 100644 --- a/src/portal/src/app/config/config.component.ts +++ b/src/portal/src/app/config/config.component.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { Subscription } from "rxjs"; -import { Configuration, StringValueItem, SystemSettingsComponent, VulnerabilityConfigComponent } from '@harbor/ui'; +import { Configuration, StringValueItem, SystemSettingsComponent, VulnerabilityConfigComponent} from '@harbor/ui'; import { ConfirmationTargets, ConfirmationState } from '../shared/shared.const'; import { SessionService } from '../shared/session.service'; @@ -24,6 +24,7 @@ import { MessageHandlerService } from '../shared/message-handler/message-handler import { AppConfigService } from '../app-config.service'; import { ConfigurationAuthComponent } from './auth/config-auth.component'; import { ConfigurationEmailComponent } from './email/config-email.component'; +import { GcComponent} from './gc/gc.component'; import { ConfigurationService } from './config.service'; @@ -34,6 +35,7 @@ const TabLinkContentMap = { 'config-email': 'email', 'config-system': 'system_settings', 'config-vulnerability': 'vulnerability', + 'config-gc': 'gc', 'config-label': 'system_label', }; @@ -53,6 +55,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { @ViewChild(SystemSettingsComponent) systemSettingsConfig: SystemSettingsComponent; @ViewChild(VulnerabilityConfigComponent) vulnerabilityConfig: VulnerabilityConfigComponent; + @ViewChild(GcComponent) gcConfig: GcComponent; @ViewChild(ConfigurationEmailComponent) mailConfig: ConfigurationEmailComponent; @ViewChild(ConfigurationAuthComponent) authConfig: ConfigurationAuthComponent; @@ -201,7 +204,7 @@ export class ConfigurationComponent implements OnInit, OnDestroy { } public get hideBtn(): boolean { - return this.currentTabId === 'config-label'; + return this.currentTabId === 'config-label' || this.currentTabId === 'config-gc'; } public get hideMailTestingSpinner(): boolean { diff --git a/src/portal/src/app/config/config.module.ts b/src/portal/src/app/config/config.module.ts index 4a74ee4f7..1d1531251 100644 --- a/src/portal/src/app/config/config.module.ts +++ b/src/portal/src/app/config/config.module.ts @@ -19,6 +19,12 @@ import { ConfigurationComponent } from './config.component'; import { ConfigurationService } from './config.service'; import { ConfigurationAuthComponent } from './auth/config-auth.component'; import { ConfigurationEmailComponent } from './email/config-email.component'; +import { GcComponent } from './gc/gc.component'; +import { GcRepoService } from './gc/gc.service'; +import { GcApiRepository } from './gc/gc.api.repository'; +import { GcViewModelFactory } from './gc/gc.viewmodel.factory'; +import { GcUtility } from './gc/gc.utility'; + @NgModule({ imports: [ @@ -29,8 +35,9 @@ import { ConfigurationEmailComponent } from './email/config-email.component'; ConfigurationComponent, ConfigurationAuthComponent, ConfigurationEmailComponent, + GcComponent ], exports: [ConfigurationComponent], - providers: [ConfigurationService] + providers: [ConfigurationService, GcRepoService, GcApiRepository, GcViewModelFactory, GcUtility ] }) export class ConfigurationModule { } diff --git a/src/portal/src/app/config/gc/gc.api.repository.ts b/src/portal/src/app/config/gc/gc.api.repository.ts new file mode 100644 index 000000000..04de55d38 --- /dev/null +++ b/src/portal/src/app/config/gc/gc.api.repository.ts @@ -0,0 +1,49 @@ + +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; + + +@Injectable() +export class GcApiRepository { + + constructor( + private http: Http, + ) { + } + + public postSchedule(param): Observable { + return this.http.post("/api/system/gc/schedule", param) + .pipe(catchError(err => Observable.throw(err))); + } + + public putSchedule(param): Observable { + return this.http.put("/api/system/gc/schedule", param) + .pipe(catchError(err => Observable.throw(err))); + } + + public getSchedule(): Observable { + return this.http.get("/api/system/gc/schedule") + .pipe(catchError(err => Observable.throw(err))) + .pipe(map(response => response.json())); + } + + public getLog(id): Observable { + return this.http.get("/api/system/gc/" + id + "/log") + .pipe(catchError(err => Observable.throw(err))); + } + + public getStatus(id): Observable { + return this.http.get("/api/system/gc/" + id) + .pipe(catchError(err => Observable.throw(err))) + .pipe(map(response => response.json())); + } + + public getJobs(): Observable { + return this.http.get("/api/system/gc") + .pipe(catchError(err => Observable.throw(err))) + .pipe(map(response => response.json())); + } + +} diff --git a/src/portal/src/app/config/gc/gc.component.html b/src/portal/src/app/config/gc/gc.component.html new file mode 100644 index 000000000..3b228aa05 --- /dev/null +++ b/src/portal/src/app/config/gc/gc.component.html @@ -0,0 +1,50 @@ +
+ {{'GC.CURRENT_SCHEDULE' | translate}} + {{(originScheduleType ? 'SCHEDULE.'+ originScheduleType.toUpperCase(): "") | translate}} + {{'GC.ON' | translate}} {{originWeekDay.text | translate}} + {{'GC.AT' | translate}} {{originOffTime.text}} + +
+
+ +
+ +
+
+ {{'GC.ON' | translate}} + +
+
+ {{'GC.AT' | translate}} + +
+ + +
+ +
{{'GC.JOB_LIST' | translate}}
+ + {{'GC.JOB_ID' | translate}} + {{'GC.TRIGGER_TYPE' | translate}} + {{'STATUS' | translate}} + {{'START_TIME' | translate}} + {{'END_TIME' | translate}} + {{'DETAILS' | translate}} + + {{job.id }} + {{'SCHEDULE.'+ job.type.toUpperCase() | translate }} + {{job.status.toUpperCase() | translate}} + {{job.createTime | date:'medium'}} + {{job.updateTime | date:'medium'}} + + {{'GC.LOG_DETAIL' | translate}} + + + {{'GC.LATEST_JOBS' | translate :{param: jobs.length} }} + \ No newline at end of file diff --git a/src/portal/src/app/config/gc/gc.component.scss b/src/portal/src/app/config/gc/gc.component.scss new file mode 100644 index 000000000..b1170b9f5 --- /dev/null +++ b/src/portal/src/app/config/gc/gc.component.scss @@ -0,0 +1,20 @@ +.flex-layout { + display: flex; + align-items: center; + margin:20px 0; + > * { + margin-right:35px; + } +} + +.gc-start-btn { + width:150px; +} + +.job-header { + margin:20px 0 -10px 0; +} + +.day-selector-wrapper { + display: flex; +} \ No newline at end of file diff --git a/src/portal/src/app/config/gc/gc.component.spec.ts b/src/portal/src/app/config/gc/gc.component.spec.ts new file mode 100644 index 000000000..a018496cd --- /dev/null +++ b/src/portal/src/app/config/gc/gc.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GcComponent } from './gc.component'; + +describe('GcComponent', () => { + let component: GcComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ GcComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GcComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/config/gc/gc.component.ts b/src/portal/src/app/config/gc/gc.component.ts new file mode 100644 index 000000000..378377aee --- /dev/null +++ b/src/portal/src/app/config/gc/gc.component.ts @@ -0,0 +1,134 @@ +import { Component, Input, Output, EventEmitter, ViewChild, OnInit } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { GcJobViewModel, WeekDay } from "./gcLog"; +import { GcViewModelFactory } from "./gc.viewmodel.factory"; +import { GcRepoService } from "./gc.service"; +import { WEEKDAYS, SCHEDULE_TYPE } from './gc.const'; +import { GcUtility } from './gc.utility'; +import { ErrorHandler } from '@harbor/ui'; +@Component({ + selector: 'gc-config', + templateUrl: './gc.component.html', + styleUrls: ['./gc.component.scss'] +}) +export class GcComponent implements OnInit { + jobs: Array = []; + schedule: any; + originScheduleType: string; + originOffTime: any = { value: null, text: "" }; + originWeekDay: any = { value: null, text: "" }; + scheduleType: string; + isEditMode: boolean = false; + weekDays = WEEKDAYS; + SCHEDULE_TYPE = SCHEDULE_TYPE; + weekDay: WeekDay = WEEKDAYS[0]; + dailyTime: string; + + constructor(private gcRepoService: GcRepoService, + private gcViewModelFactory: GcViewModelFactory, + private gcUtility: GcUtility, + private errorHandler: ErrorHandler, + private translate: TranslateService) { + translate.setDefaultLang('en-us'); + } + + + ngOnInit() { + this.getCurrentSchedule(); + this.getJobs(); + } + + getCurrentSchedule() { + this.gcRepoService.getSchedule().subscribe(schedule => { + this.initSchedule(schedule); + }); + } + + private initSchedule(schedule: any) { + if (schedule && schedule.length > 0) { + this.schedule = schedule[0]; + const cron = this.schedule.schedule; + this.originScheduleType = cron.type; + this.originWeekDay = this.weekDays[cron.weekday]; + let dailyTime = this.gcUtility.getDailyTime(cron.offtime); + this.originOffTime = { value: cron.offtime, text: dailyTime }; + } else { + this.originScheduleType = SCHEDULE_TYPE.NONE; + } + } + + editSchedule() { + this.isEditMode = true; + this.scheduleType = this.originScheduleType; + if (this.originWeekDay.value) { + this.weekDay = this.originWeekDay; + } else { + this.weekDay = this.weekDays[0]; + } + + if (this.originOffTime.value) { + this.dailyTime = this.originOffTime.text; + } else { + this.dailyTime = "00:00"; + } + } + + getJobs() { + this.gcRepoService.getJobs().subscribe(jobs => { + this.jobs = this.gcViewModelFactory.createJobViewModel(jobs); + }); + } + + gcNow(): void { + this.gcRepoService.manualGc().subscribe(response => { + this.translate.get('GC.MSG_SUCCESS').subscribe((res: string) => { + this.errorHandler.info(res); + }); + this.getJobs(); + }, error => { + this.errorHandler.error(error); + }); + } + + scheduleGc(): void { + let offTime = this.gcUtility.getOffTime(this.dailyTime); + if (this.schedule) { + this.gcRepoService.putScheduleGc(this.scheduleType, offTime, this.weekDay.value).subscribe(response => { + this.translate.get('GC.MSG_SCHEDULE_RESET').subscribe((res: string) => { + this.errorHandler.info(res); + }); + this.originScheduleType = this.scheduleType; + this.originWeekDay = this.weekDay; + this.originOffTime = { value: offTime, text: this.dailyTime }; + this.isEditMode = false; + this.getJobs(); + }, error => { + this.translate.get('GC.MSG_ERROR').subscribe((res: string) => { + this.errorHandler.info(res); + }); + }); + } else { + this.gcRepoService.postScheduleGc(this.scheduleType, offTime, this.weekDay.value).subscribe(response => { + this.translate.get('GC.MSG_SCHEDULE_SET').subscribe((res: string) => { + this.errorHandler.info(res); + }); + this.schedule = { + schedule: { + type: this.scheduleType, + offTime: offTime, + weekDay: this.weekDay.value + } + }; + this.originScheduleType = this.scheduleType; + this.originWeekDay = this.weekDay; + this.originOffTime = { value: offTime, text: this.dailyTime }; + this.isEditMode = false; + this.getJobs(); + }, error => { + this.translate.get('GC.MSG_ERROR').subscribe((res: string) => { + this.errorHandler.info(res); + }); + }); + } + } +} diff --git a/src/portal/src/app/config/gc/gc.const.ts b/src/portal/src/app/config/gc/gc.const.ts new file mode 100644 index 000000000..1c30a6d4c --- /dev/null +++ b/src/portal/src/app/config/gc/gc.const.ts @@ -0,0 +1,20 @@ +export const WEEKDAYS = [ + {value: 0, text: "WEEKLY.MONDAY"}, + {value: 1, text: "WEEKLY.TUESDAY"}, + {value: 2, text: "WEEKLY.WEDNESDAY"}, + {value: 3, text: "WEEKLY.THURSDAY"}, + {value: 4, text: "WEEKLY.FRIDAY"}, + {value: 5, text: "WEEKLY.SATURDAY"}, + {value: 6, text: "WEEKLY.SUNDAY"} +]; + +export const SCHEDULE_TYPE = { + NONE: "None", + DAILY: "Daily", + WEEKLY: "Weekly" +}; + + + + + diff --git a/src/portal/src/app/config/gc/gc.service.ts b/src/portal/src/app/config/gc/gc.service.ts new file mode 100644 index 000000000..05611f8a1 --- /dev/null +++ b/src/portal/src/app/config/gc/gc.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; +import { Observable, Subscription, Subject, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { GcApiRepository } from './gc.api.repository'; +import { ErrorHandler } from '@harbor/ui'; +import { GcJobData } from './gcLog'; + +@Injectable() +export class GcRepoService { + + constructor(private http: Http, + private gcApiRepository: GcApiRepository, + private errorHandler: ErrorHandler) { + } + + public manualGc(): Observable { + let param = { + "schedule": { + "type": "Manual" + } + }; + return this.gcApiRepository.postSchedule(param); + } + + public getJobs(): Observable { + return this.gcApiRepository.getJobs(); + } + + public getLog(id): Observable { + return this.gcApiRepository.getLog(id); + } + + public getSchedule(): Observable { + return this.gcApiRepository.getSchedule(); + } + + public postScheduleGc(type, offTime, weekday ?): Observable { + let param = { + "schedule": { + "type": type, + "offtime": offTime, + } + }; + if (weekday) { + param.schedule["weekday"] = weekday; + } + return this.gcApiRepository.postSchedule(param); + } + + public putScheduleGc(type, offTime, weekday ?): Observable { + let param = { + "schedule": { + "type": type, + "offtime": offTime, + } + }; + if (weekday) { + param.schedule["weekday"] = weekday; + } + return this.gcApiRepository.putSchedule(param); + } +} diff --git a/src/portal/src/app/config/gc/gc.utility.ts b/src/portal/src/app/config/gc/gc.utility.ts new file mode 100644 index 000000000..6c06e55ea --- /dev/null +++ b/src/portal/src/app/config/gc/gc.utility.ts @@ -0,0 +1,62 @@ + +import { Injectable } from '@angular/core'; + +const ONE_HOUR_SECONDS: number = 3600; +const ONE_DAY_SECONDS: number = 24 * ONE_HOUR_SECONDS; + +@Injectable() +export class GcUtility { + private _localTime: Date = new Date(); + public getOffTime(v: string) { + let values: string[] = v.split(":"); + if (!values || values.length !== 2) { + return; + } + let hours: number = +values[0]; + let minutes: number = +values[1]; + // Convert to UTC time + let timezoneOffset: number = this._localTime.getTimezoneOffset(); + let utcTimes: number = hours * ONE_HOUR_SECONDS + minutes * 60; + utcTimes += timezoneOffset * 60; + if (utcTimes < 0) { + utcTimes += ONE_DAY_SECONDS; + } + if (utcTimes >= ONE_DAY_SECONDS) { + utcTimes -= ONE_DAY_SECONDS; + } + return utcTimes; + } + + public getDailyTime(v: number ) { + let timeOffset: number = 0; // seconds + timeOffset = + v; + // Convert to current time + let timezoneOffset: number = this._localTime.getTimezoneOffset(); + // Local time + timeOffset = timeOffset - timezoneOffset * 60; + if (timeOffset < 0) { + timeOffset = timeOffset + ONE_DAY_SECONDS; + } + + if (timeOffset >= ONE_DAY_SECONDS) { + timeOffset -= ONE_DAY_SECONDS; + } + + // To time string + let hours: number = Math.floor(timeOffset / ONE_HOUR_SECONDS); + let minutes: number = Math.floor((timeOffset - hours * ONE_HOUR_SECONDS) / 60); + + let timeStr: string = "" + hours; + if (hours < 10) { + timeStr = "0" + timeStr; + } + if (minutes < 10) { + timeStr += ":0"; + } else { + timeStr += ":"; + } + timeStr += minutes; + + return timeStr; + } +} diff --git a/src/portal/src/app/config/gc/gc.viewmodel.factory.ts b/src/portal/src/app/config/gc/gc.viewmodel.factory.ts new file mode 100644 index 000000000..99b54e1d2 --- /dev/null +++ b/src/portal/src/app/config/gc/gc.viewmodel.factory.ts @@ -0,0 +1,23 @@ +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.type, + status: job.job_status, + createTime: createTime, + updateTime: updateTime, + details: null + }); + } + return gcViewModels; + } +} diff --git a/src/portal/src/app/config/gc/gcLog.ts b/src/portal/src/app/config/gc/gcLog.ts new file mode 100644 index 000000000..8de1d4137 --- /dev/null +++ b/src/portal/src/app/config/gc/gcLog.ts @@ -0,0 +1,37 @@ +export class GcJobData { + id: number; + job_name: string; + job_kind: string; + schedule: Schedule; + job_status: string; + job_uuid: string; + creation_time: string; + update_time: string; + delete: boolean; +} + +export class Schedule { + type: string; + weekday: number; + offtime: number; +} +export class GcJobViewModel { + id: number; + type: string; + status: string; + createTime: Date; + updateTime: Date; + details: string; +} + +export class WeekDay { + value: number; + text: string; +} + +export class GcScheduleViewModel { + type: string; + weekDay: string; + dailyTime: string; +} + diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 741e6d16c..f3f2f562a 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -555,6 +555,7 @@ "REPO_READ_ONLY": "Repository Read Only", "SYSTEM": "System Settings", "VULNERABILITY": "Vulnerability", + "GC": "Garbage Collection", "CONFIRM_TITLE": "Confirm to cancel", "CONFIRM_SUMMARY": "Some changes have not been saved. Do you want to discard them?", "SAVE_SUCCESS": "Configuration has been successfully saved.", @@ -810,5 +811,33 @@ "PRECONDITION_FAILED": "We are unable to perform your action because of a precondition failure.", "SERVER_ERROR": "We are unable to perform your action because internal server errors have occurred.", "INCONRRECT_OLD_PWD": "The old password is incorrect.", - "UNKNOWN": "n/a" + "UNKNOWN": "n/a", + "STATUS":"Status", + "START_TIME": "Start Time", + "END_TIME": "End Time", + "DETAILS":"Details", + "PENDING":"Pending", + "FINISHED":"Finished", + "SCHEDULE": { + "NONE": "None", + "DAILY": "Daily", + "WEEKLY": "Weekly", + "MANUAL": "Manual" + }, + "GC": { + "CURRENT_SCHEDULE": "Current Schedule", + "ON": "on", + "AT": "at", + "GC_NOW": "GC NOW", + "JOB_LIST":"GC Jobs List", + "JOB_ID":"Job ID", + "TRIGGER_TYPE": "Trigger Type", + "LATEST_JOBS": "Latest {{param}} Jobs", + "LOG_DETAIL":"Log Details", + "MSG_SUCCESS":"Garbage Collection Successful", + "MSG_SCHEDULE_SET":"Garbage Collection schedule has been set", + "MSG_SCHEDULE_RESET":"Garbage Collection schedule has been reset", + "MSG_ERROR":"Can not do Garbase Collection too often,please try again later." + } + } diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index e3db3d1c7..f5ab6a557 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -554,6 +554,7 @@ "REPO_READ_ONLY": "Repository Read Only", "SYSTEM": "Opciones del Sistema", "VULNERABILITY": "Vulnerability", + "GC": "Garbage Collection", "CONFIRM_TITLE": "Confirma cancelación", "CONFIRM_SUMMARY": "Algunos cambios no han sido guardados aún. ¿Quiere descartarlos?", "SAVE_SUCCESS": "La configuración ha sido guardada satisfactoriamente.", @@ -595,7 +596,9 @@ "ROOT_CERT_DOWNLOAD": "Download the root certificate of registry.", "SCANNING_POLICY": "Set image scanning policy based on different requirements. 'None': No active policy; 'Daily At': Triggering scanning at the specified time everyday.", "VERIFY_CERT": "Verify Cert from LDAP Server", - "READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. " + "READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. ", + "GC_POLICY": "" + }, "LDAP": { "URL": "LDAP URL", @@ -805,5 +808,32 @@ "PRECONDITION_FAILED": "No hemos podido llevar a cabo la acción debido a un error de precondición.", "SERVER_ERROR": "No hemos podido llevar a cabo la acción debido a un error interno.", "INCONRRECT_OLD_PWD": "La contraseña antigua no es correcta.", - "UNKNOWN": "n/a" + "UNKNOWN": "n/a", + "STATUS":"Status", + "START_TIME": "Start Time", + "END_TIME": "End Time", + "DETAILS":"Details", + "PENDING":"Pending", + "FINISHED":"Finished", + "SCHEDULE": { + "NONE": "None", + "DAILY": "Daily", + "WEEKLY": "Weekly", + "MANUAL": "Manual" + }, + "GC": { + "CURRENT_SCHEDULE": "Current Schedule", + "ON": "on", + "AT": "at", + "GC_NOW": "GC NOW", + "JOB_LIST":"GC Jobs List", + "JOB_ID":"Job ID", + "TRIGGER_TYPE": "Trigger Type", + "LATEST_JOBS": "Latest {{param}} Jobs", + "LOG_DETAIL":"Log Details", + "MSG_SUCCESS":"Garbage Collection Successful", + "MSG_SCHEDULE_SET":"Garbage Collection schedule has been set", + "MSG_SCHEDULE_RESET":"Garbage Collection schedule has been reset", + "MSG_ERROR":"Can not do Garbase Collection too often,please try again later." + } } \ No newline at end of file diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index e8d200ef4..513499ee6 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -553,6 +553,7 @@ "PRO_CREATION_ADMIN": "Les Administrateurs Seulement", "ROOT_CERT": "Enregistrer le Certificat Racine", "ROOT_CERT_LINK": "Télécharger", + "GC": "Garbage Collection", "TOOLTIP": { "SELF_REGISTRATION_ENABLE": "Activer l'inscription.", "SELF_REGISTRATION_DISABLE": "Désactiver l'inscription.", @@ -566,7 +567,8 @@ "PRO_CREATION_RESTRICTION": "L'indicateur pour définir quels utilisateurs ont le droit de créer des projets. Par défaut, tout le monde peut créer un projet. Définissez sur 'Administrateur Seulement' pour que seul un administrateur puisse créer un projet.", "ROOT_CERT_DOWNLOAD": "Téléchargez le certificat racine du dépôt.", "SCANNING_POLICY": "Définissez la politique d'analyse des images en fonction des différentes exigences. 'Aucune' : pas de politique active; 'Tousles jours à' : déclenchement du balayage à l'heure spécifiée tous les jours.", - "READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. " + "READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. ", + "GC_POLICY": "" }, "LDAP": { "URL": "URL LDAP", @@ -769,5 +771,32 @@ "PRECONDITION_FAILED": "Nous ne pouvons pas exécuter votre action en raison d'un échec de conditions préalables.", "SERVER_ERROR": "Nous ne sommes pas en mesure d'exécuter votre action parce que des erreurs internes de serveur se sont produites.", "INCONRRECT_OLD_PWD": "L'ancien mot de passe est incorrect.", - "UNKNOWN": "n. d." + "UNKNOWN": "n. d.", + "STATUS":"Status", + "START_TIME": "Start Time", + "END_TIME": "End Time", + "DETAILS":"Details", + "PENDING":"Pending", + "FINISHED":"Finished", + "SCHEDULE": { + "NONE": "None", + "DAILY": "Daily", + "WEEKLY": "Weekly", + "MANUAL": "Manual" + }, + "GC": { + "CURRENT_SCHEDULE": "Current Schedule", + "ON": "on", + "AT": "at", + "GC_NOW": "GC NOW", + "JOB_LIST":"GC Jobs List", + "JOB_ID":"Job ID", + "TRIGGER_TYPE": "Trigger Type", + "LATEST_JOBS": "Latest {{param}} Jobs", + "LOG_DETAIL":"Log Details", + "MSG_SUCCESS":"Garbage Collection Successful", + "MSG_SCHEDULE_SET":"Garbage Collection schedule has been set", + "MSG_SCHEDULE_RESET":"Garbage Collection schedule has been reset", + "MSG_ERROR":"Can not do Garbase Collection too often,please try again later." + } } diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index e71bf129b..6d9d96686 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -554,6 +554,7 @@ "REPO_READ_ONLY": "仓库只读", "SYSTEM": "系统设置", "VULNERABILITY": "漏洞", + "GC": "垃圾清理", "CONFIRM_TITLE": "确认取消", "CONFIRM_SUMMARY": "配置项有改动, 确定取消?", "SAVE_SUCCESS": "变更的配置项成功保存。", @@ -806,5 +807,32 @@ "PRECONDITION_FAILED": "验证前置条件失败, 无法执行操作。", "SERVER_ERROR": "服务器出现内部错误,请求无法完成。", "INCONRRECT_OLD_PWD": "旧密码不正确。", - "UNKNOWN": "未知" + "UNKNOWN": "未知", + "STATUS":"状态", + "START_TIME": "开始时间", + "END_TIME": "结束时间", + "DETAILS":"详情", + "PENDING":"未开始", + "FINISHED":"已完成", + "SCHEDULE": { + "NONE": "无", + "DAILY": "每天", + "WEEKLY": "每周", + "MANUAL": "手动" + }, + "GC": { + "CURRENT_SCHEDULE": "当前定时任务", + "ON": " ", + "AT": " ", + "GC_NOW": "立即清理垃圾", + "JOB_LIST":"任务列表", + "JOB_ID":"任务ID", + "TRIGGER_TYPE": "触发类型", + "LATEST_JOBS": "最新的 {{param}} 个任务", + "LOG_DETAIL":"日志详情", + "MSG_SUCCESS":"垃圾回收成功", + "MSG_SCHEDULE_SET":"垃圾回收定时任务设置成功", + "MSG_SCHEDULE_RESET":"垃圾回收定时任务已被重置", + "MSG_ERROR":"您的垃圾回收请求提交过于频繁,请稍候重试" + } } \ No newline at end of file