Implement gc UI

Signed-off-by: Meina Zhou <meinaz@vmware.com>
This commit is contained in:
Meina Zhou 2018-09-12 13:08:28 +08:00
parent 002e5a2b70
commit aefb97dec8
22 changed files with 637 additions and 14 deletions

View File

@ -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';

View File

@ -11,7 +11,7 @@
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": true,
"types": [],
"typeRoots": ["node_modules/@types"],
"lib": [
"dom",
"es2015"

View File

@ -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",

View File

@ -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 { 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();

View File

@ -18,6 +18,9 @@
<li role="presentation" class="nav-item" *ngIf="withClair">
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="hasAdminRole">
<button id="config-gc" class="btn btn-link nav-link" aria-controls="gc" [class.active]='isCurrentTabLink("config-gc")' type="button" (click)='tabLinkClick("config-gc")'>{{'CONFIG.GC' | translate}}</button>
</li>
</ul>
<section id="authentication" role="tabpanel" aria-labelledby="config-auth" [hidden]='!isCurrentTabContent("authentication")'>
<config-auth [allConfig]="allConfig"></config-auth>
@ -34,6 +37,9 @@
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
</section>
<section id="gc" *ngIf="hasAdminRole" role="tabpanel" aria-labelledby="config-gc" [hidden]='!isCurrentTabContent("gc")'>
<gc-config></gc-config>
</section>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>

View File

@ -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 {

View File

@ -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 { }

View File

@ -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<any> {
return this.http.post("/api/system/gc/schedule", param)
.pipe(catchError(err => Observable.throw(err)));
}
public putSchedule(param): Observable<any> {
return this.http.put("/api/system/gc/schedule", param)
.pipe(catchError(err => Observable.throw(err)));
}
public getSchedule(): Observable<any> {
return this.http.get("/api/system/gc/schedule")
.pipe(catchError(err => Observable.throw(err)))
.pipe(map(response => response.json()));
}
public getLog(id): Observable<any> {
return this.http.get("/api/system/gc/" + id + "/log")
.pipe(catchError(err => Observable.throw(err)));
}
public getStatus(id): Observable<any> {
return this.http.get("/api/system/gc/" + id)
.pipe(catchError(err => Observable.throw(err)))
.pipe(map(response => response.json()));
}
public getJobs(): Observable<any> {
return this.http.get("/api/system/gc")
.pipe(catchError(err => Observable.throw(err)))
.pipe(map(response => response.json()));
}
}

View File

@ -0,0 +1,50 @@
<div class="normal-wrapper flex-layout" *ngIf="!isEditMode">
<span>{{'GC.CURRENT_SCHEDULE' | translate}}</span>
<span>{{(originScheduleType ? 'SCHEDULE.'+ originScheduleType.toUpperCase(): "") | translate}}</span>
<span [hidden]="originScheduleType!==SCHEDULE_TYPE.WEEKLY">{{'GC.ON' | translate}} {{originWeekDay.text | translate}}</span>
<span [hidden]="originScheduleType===SCHEDULE_TYPE.NONE">{{'GC.AT' | translate}} {{originOffTime.text}}</span>
<button class="btn btn-outline" (click)="editSchedule()">{{'BUTTON.EDIT' | translate}}</button>
</div>
<div class="setting-wrapper flex-layout" *ngIf="isEditMode">
<label for="gcPolicy">{{'CONFIG.GC' | translate}}</label>
<div class="select">
<select id="gcPolicy" name="gcPolicy" [(ngModel)]="scheduleType">
<option [value]="SCHEDULE_TYPE.NONE">{{'SCHEDULE.NONE' | translate}}</option>
<option [value]="SCHEDULE_TYPE.DAILY">{{'SCHEDULE.DAILY' | translate}}</option>
<option [value]="SCHEDULE_TYPE.WEEKLY">{{'SCHEDULE.WEEKLY' | translate}}</option>
</select>
</div>
<section [hidden]="scheduleType!== SCHEDULE_TYPE.WEEKLY" class="select day-selector-wrapper">
<span>{{'GC.ON' | translate}}</span>
<select id="daySelector" name="daySelector" [(ngModel)]="weekDay">
<option *ngFor="let d of weekDays" [ngValue]="d">{{d.text | translate}}</option>
</select>
</section>
<section [hidden]="scheduleType===SCHEDULE_TYPE.NONE">
<span>{{'GC.AT' | translate}}</span>
<input type="time" name="dailyTimePicker" required [(ngModel)]="dailyTime" />
</section>
<button class="btn btn-primary btn-sm" (click)="scheduleGc()">{{'BUTTON.SAVE' | translate}}</button>
<button class="btn btn-primary btn-sm" (click)="isEditMode = false" >{{'BUTTON.CANCEL' | translate}}</button>
</div>
<button class="btn btn-success btn-sm gc-start-btn" (click)="gcNow()">{{'GC.GC_NOW' | translate}}</button>
<div class="job-header">{{'GC.JOB_LIST' | translate}}</div>
<clr-datagrid>
<clr-dg-column>{{'GC.JOB_ID' | translate}}</clr-dg-column>
<clr-dg-column>{{'GC.TRIGGER_TYPE' | translate}}</clr-dg-column>
<clr-dg-column>{{'STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'END_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'DETAILS' | translate}}</clr-dg-column>
<clr-dg-row *ngFor="let job of jobs" [clrDgItem]='job'>
<clr-dg-cell>{{job.id }}</clr-dg-cell>
<clr-dg-cell>{{'SCHEDULE.'+ job.type.toUpperCase() | translate }}</clr-dg-cell>
<clr-dg-cell>{{job.status.toUpperCase() | translate}}</clr-dg-cell>
<clr-dg-cell>{{job.createTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>{{job.updateTime | date:'medium'}}</clr-dg-cell>
<clr-dg-cell>
<a target="_blank" href="/api/system/gc/{{job.id}}/log">{{'GC.LOG_DETAIL' | translate}}</a>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>{{'GC.LATEST_JOBS' | translate :{param: jobs.length} }}</clr-dg-footer>
</clr-datagrid>

View File

@ -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;
}

View File

@ -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<GcComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ GcComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GcComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -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<GcJobViewModel> = [];
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);
});
});
}
}
}

View File

@ -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"
};

View File

@ -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 <any> {
let param = {
"schedule": {
"type": "Manual"
}
};
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(type, offTime, weekday ?): Observable <any> {
let param = {
"schedule": {
"type": type,
"offtime": offTime,
}
};
if (weekday) {
param.schedule["weekday"] = weekday;
}
return this.gcApiRepository.postSchedule(param);
}
public putScheduleGc(type, offTime, weekday ?): Observable <any> {
let param = {
"schedule": {
"type": type,
"offtime": offTime,
}
};
if (weekday) {
param.schedule["weekday"] = weekday;
}
return this.gcApiRepository.putSchedule(param);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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."
}
}

View File

@ -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."
}
}

View File

@ -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."
}
}

View File

@ -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":"您的垃圾回收请求提交过于频繁,请稍候重试"
}
}