Add immutable tag in project detail

Signed-off-by: Yogi_Wang <yawang@vmware.com>
This commit is contained in:
Yang Wang (c) 2019-10-08 12:18:56 +08:00 committed by Yogi_Wang
parent 5c5e475da4
commit bfe19711db
22 changed files with 1435 additions and 152 deletions

3
.gitignore vendored
View File

@ -33,7 +33,7 @@ src/portal/typings/
src/portal/src/**/*.js
src/portal/src/**/*.js.map
src/portal/lib/coverage
**/npm*.log
**/*ngsummary.json
@ -42,3 +42,4 @@ src/portal/src/**/*.js.map
**/dist
**/.bin
src/core/conf/app.conf

View File

@ -61,6 +61,7 @@ import { OidcOnboardComponent } from './oidc-onboard/oidc-onboard.component';
import { LicenseComponent } from './license/license.component';
import { SummaryComponent } from './project/summary/summary.component';
import { TagRetentionComponent } from './project/tag-retention/tag-retention.component';
import { ImmutableTagComponent } from './project/immutable-tag/immutable-tag.component';
import { USERSTATICPERMISSION } from '@harbor/ui';
import { ScannerComponent } from "./project/scanner/scanner.component";
@ -277,6 +278,16 @@ const harborRoutes: Routes = [
},
component: TagRetentionComponent
},
{
path: 'immutable-tag',
data: {
permissionParam: {
resource: USERSTATICPERMISSION.TAG_RETENTION.KEY,
action: USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ
}
},
component: ImmutableTagComponent
},
{
path: 'webhook',
data: {

View File

@ -0,0 +1,67 @@
<clr-modal [(clrModalOpen)]="addRuleOpened" [clrModalStaticBackdrop]="true" [clrModalClosable]="true" [clrModalSize]="'lg'">
<h3 class="modal-title" *ngIf="isAdd">{{'TAG_RETENTION.ADD_TITLE' | translate}}</h3>
<h3 class="modal-title" *ngIf="!isAdd">{{'TAG_RETENTION.EDIT_TITLE' | translate}}</h3>
<div class="modal-body no-scrolling">
<inline-alert class="modal-title"></inline-alert>
<p class="color-97">{{'TAG_RETENTION.ADD_SUBTITLE' | translate}}</p>
<div class="height-72">
<div class="clr-row mt-1">
<div class="clr-col-4">
<span>{{'TAG_RETENTION.IN_REPOSITORIES' | translate}}</span>
</div>
<div class="clr-col-3">
<div class="clr-select-wrapper w-100">
<select id="scope_selectors" [(ngModel)]="repoSelect" class="clr-select w-100">
<option *ngFor="let d of metadata?.scope_selectors[0]?.decorations"
value="{{d}}">{{getI18nKey(d)|translate}}</option>
</select>
</div>
</div>
<div class="clr-col-5">
<div class="w-100">
<input required id="scope-input" [(ngModel)]="repositories" class="clr-input w-100">
</div>
</div>
</div>
<div class="clr-row">
<div class="clr-col-4"></div>
<div class="clr-col-8">
<span>{{'TAG_RETENTION.REP_SEPARATOR' | translate}}</span>
</div>
</div>
</div>
<div class="height-72">
<div class="clr-row">
<div class="clr-col-4">
<label>{{'TAG_RETENTION.TAGS' | translate}}</label>
</div>
<div class="clr-col-3">
<div class="clr-select-wrapper w-100">
<select id="tag_selectors" [(ngModel)]="tagsSelect" class="clr-select w-100">
<option *ngFor="let d of metadata?.tag_selectors[0]?.decorations"
value="{{d}}">{{getI18nKey(d)|translate}}</option>
</select>
</div>
</div>
<div class="clr-col-5">
<div class="w-100">
<input required id="tag-input" [(ngModel)]="tagsInput" class="clr-input w-100">
</div>
</div>
</div>
<div class="clr-row">
<div class="clr-col-4"></div>
<div class="clr-col-8">
<span>{{'TAG_RETENTION.TAG_SEPARATOR' | translate}}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" id="close-btn" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button [disabled]="canNotAdd()" type="button" id="add-edit-btn" class="btn btn-primary" (click)="add()">
<span *ngIf="isAdd">{{'BUTTON.ADD' | translate}}</span>
<span *ngIf="!isAdd">{{'BUTTON.SAVE' | translate}}</span>
</button>
</div>
</clr-modal>

View File

@ -0,0 +1,7 @@
.color-97 {
color: #979797;
}
.height-72 {
height: 72px;
}

View File

@ -0,0 +1,115 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms';
import { AddRuleComponent } from './add-rule.component';
import { CUSTOM_ELEMENTS_SCHEMA, EventEmitter } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { ImmutableTagService } from '../immutable-tag.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
import { ImmutableRetentionRule } from "../../tag-retention/retention";
import { compareValue } from "@harbor/ui";
describe('AddRuleComponent', () => {
let component: AddRuleComponent;
let fixture: ComponentFixture<AddRuleComponent>;
let mockRule = {
"id": 1,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "**"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [AddRuleComponent, InlineAlertComponent],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
ClarityModule,
FormsModule,
NoopAnimationsModule,
HttpClientTestingModule,
TranslateModule.forRoot()
],
providers: [
ImmutableTagService
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AddRuleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
component.addRuleOpened = true;
component.repoSelect = mockRule.scope_selectors.repository[0].decoration;
component.repositories = mockRule.scope_selectors.repository[0].pattern.replace(/[{}]/g, "");
component.tagsSelect = mockRule.tag_selectors[0].decoration;
component.tagsInput = mockRule.tag_selectors[0].pattern.replace(/[{}]/g, "");
component.clickAdd = new EventEmitter<ImmutableRetentionRule>();
component.rules = [];
component.isAdd = true;
component.open();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should rightly display default repositories and tag", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRep: HTMLInputElement = fixture.nativeElement.querySelector("#scope-input");
expect(elRep).toBeTruthy();
expect(elRep.value.trim()).toEqual("**");
let elTag: HTMLInputElement = fixture.nativeElement.querySelector("#tag-input");
expect(elTag).toBeTruthy();
expect(elTag.value.trim()).toEqual("**");
});
}));
it("should rightly close", async(() => {
fixture.detectChanges();
let elRep: HTMLButtonElement = fixture.nativeElement.querySelector("#close-btn");
elRep.dispatchEvent(new Event('click'));
elRep.click();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(component.addRuleOpened).toEqual(false);
});
}));
it("should be validating repeat rule ", async(() => {
fixture.detectChanges();
component.rules = [mockRule];
const elRep: HTMLButtonElement = fixture.nativeElement.querySelector("#add-edit-btn");
elRep.dispatchEvent(new Event('click'));
elRep.click();
fixture.whenStable().then(() => {
fixture.detectChanges();
const elRep1: HTMLSpanElement = fixture.nativeElement.querySelector(".alert-text");
expect(elRep1).toBeTruthy();
});
}));
});

View File

@ -0,0 +1,177 @@
import {
Component,
OnInit,
OnDestroy,
Output,
EventEmitter, ViewChild, Input,
} from "@angular/core";
import { ImmutableRetentionRule, RuleMetadate } from "../../tag-retention/retention";
import { compareValue } from "@harbor/ui";
import { ImmutableTagService } from "../immutable-tag.service";
import { InlineAlertComponent } from "../../../shared/inline-alert/inline-alert.component";
const EXISTING_RULE = "TAG_RETENTION.EXISTING_RULE";
const INVALID_RULE = "TAG_RETENTION.INVALID_RULE";
@Component({
selector: 'app-add-rule',
templateUrl: './add-rule.component.html',
styleUrls: ['./add-rule.component.scss']
})
export class AddRuleComponent implements OnInit, OnDestroy {
addRuleOpened: boolean = false;
@Output() clickAdd = new EventEmitter<ImmutableRetentionRule>();
@Input() rules: ImmutableRetentionRule[];
@Input() projectId: number;
metadata: RuleMetadate = new RuleMetadate();
rule: ImmutableRetentionRule = new ImmutableRetentionRule(this.projectId);
isAdd: boolean = true;
editRuleOrigin: ImmutableRetentionRule;
onGoing: boolean = false;
@ViewChild(InlineAlertComponent, { static: false }) inlineAlert: InlineAlertComponent;
constructor(private immutableTagService: ImmutableTagService) {
}
ngOnInit(): void {
}
ngOnDestroy(): void {
}
get repoSelect() {
if (this.rule && this.rule.scope_selectors && this.rule.scope_selectors.repository[0]) {
return this.rule.scope_selectors.repository[0].decoration;
}
return "";
}
set repoSelect(repoSelect) {
if (this.rule && this.rule.scope_selectors && this.rule.scope_selectors.repository[0]) {
this.rule.scope_selectors.repository[0].decoration = repoSelect;
}
}
set repositories(repositories) {
if (this.rule && this.rule.scope_selectors && this.rule.scope_selectors.repository
&& this.rule.scope_selectors.repository[0] && this.rule.scope_selectors.repository[0].pattern) {
if (repositories.indexOf(",") !== -1) {
this.rule.scope_selectors.repository[0].pattern = "{" + repositories + "}";
} else {
this.rule.scope_selectors.repository[0].pattern = repositories;
}
}
}
get repositories() {
if (this.rule && this.rule.scope_selectors && this.rule.scope_selectors.repository
&& this.rule.scope_selectors.repository[0] && this.rule.scope_selectors.repository[0].pattern) {
return this.rule.scope_selectors.repository[0].pattern.replace(/[{}]/g, "");
}
return "";
}
get tagsSelect() {
if (this.rule && this.rule.tag_selectors && this.rule.tag_selectors[0]) {
return this.rule.tag_selectors[0].decoration;
}
return "";
}
set tagsSelect(tagsSelect) {
if (this.rule && this.rule.tag_selectors && this.rule.tag_selectors[0]) {
this.rule.tag_selectors[0].decoration = tagsSelect;
}
}
set tagsInput(tagsInput) {
if (this.rule && this.rule.tag_selectors && this.rule.tag_selectors[0] && this.rule.tag_selectors[0].pattern) {
if (tagsInput.indexOf(",") !== -1) {
this.rule.tag_selectors[0].pattern = "{" + tagsInput + "}";
} else {
this.rule.tag_selectors[0].pattern = tagsInput;
}
}
}
get tagsInput() {
if (this.rule && this.rule.tag_selectors && this.rule.tag_selectors[0] && this.rule.tag_selectors[0].pattern) {
return this.rule.tag_selectors[0].pattern.replace(/[{}]/g, "");
}
return "";
}
canNotAdd(): boolean {
if (this.onGoing) {
return true;
}
if (!this.isAdd && compareValue(this.editRuleOrigin, this.rule)) {
return true;
}
return !(
this.rule && this.rule.scope_selectors && this.rule.scope_selectors.repository
&& this.rule.scope_selectors.repository[0] && this.rule.scope_selectors.repository[0].pattern
&& this.rule.scope_selectors.repository[0].pattern.replace(/[{}]/g, "")
&& this.rule.tag_selectors && this.rule.tag_selectors[0] && this.rule.tag_selectors[0].pattern
&& this.rule.tag_selectors[0].pattern.replace(/[{}]/g, ""));
}
open() {
this.addRuleOpened = true;
this.inlineAlert.alertClose = true;
this.onGoing = false;
}
close() {
this.addRuleOpened = false;
}
cancel() {
this.close();
}
add() {
// remove whitespaces
this.rule.scope_selectors.repository[0].pattern = this.rule.scope_selectors.repository[0].pattern.replace(/\s+/g, "");
this.rule.tag_selectors[0].pattern = this.rule.tag_selectors[0].pattern.replace(/\s+/g, "");
if (this.rule.scope_selectors.repository[0].decoration !== "repoMatches"
&& this.rule.scope_selectors.repository[0].pattern.indexOf("**") !== -1) {
this.inlineAlert.showInlineError(INVALID_RULE);
return;
}
if (this.isExistingRule()) {
this.inlineAlert.showInlineError(EXISTING_RULE);
return;
}
this.clickAdd.emit(this.rule);
}
isExistingRule(): boolean {
if (this.rules && this.rules.length > 0) {
for (let i = 0; i < this.rules.length; i++) {
if (this.isSameRule(this.rules[i])) {
return true;
}
}
}
return false;
}
isSameRule(rule: ImmutableRetentionRule): boolean {
if (this.rule.scope_selectors.repository[0].decoration !== rule.scope_selectors.repository[0].decoration) {
return false;
}
if (this.rule.scope_selectors.repository[0].pattern !== rule.scope_selectors.repository[0].pattern) {
return false;
}
if (this.rule.tag_selectors[0].decoration !== rule.tag_selectors[0].decoration) {
return false;
}
return this.rule.tag_selectors[0].pattern === rule.tag_selectors[0].pattern;
}
getI18nKey(str: string) {
return this.immutableTagService.getI18nKey(str);
}
}

View File

@ -0,0 +1,63 @@
<div class="clr-row pt-1 fw8">
<div class="clr-col">
<label class="label-left font-size-54">{{'TAG_RETENTION.RETENTION_RULES' | translate}}</label><span class="badge badge-3 ml-5">{{rules?.length ? rules?.length : 0}}/15</span>
<span *ngIf="loadingRule" class="spinner spinner-inline ml-2">Loading...</span>
</div>
</div>
<div class="clr-row pt-1">
<div class="clr-col">
<ul *ngIf="rules?.length > 0" class="list-unstyled">
<li class="rule" *ngFor="let rule of rules;let i = index;">
<div class="clr-row">
<div class="clr-col-2 flex-150">
<div class="dropdown" [ngClass]="{open:ruleIndex===i}">
<button (click)="openEditor(i)" id="{{'action'+i}}" class="dropdown-toggle btn btn-link btn-sm">
{{'TAG_RETENTION.ACTION' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<div class="dropdown-menu">
<button *ngIf="!rule?.disabled" type="button" id="{{'disable-btn'+i}}" class="dropdown-item" (click)="toggleDisable(rule,true)">{{'TAG_RETENTION.DISABLE' | translate}}</button>
<button *ngIf="rule?.disabled" type="button" class="dropdown-item" (click)="toggleDisable(rule,false)">{{'TAG_RETENTION.ENABLE' | translate}}</button>
<button type="button" id="{{'edit-btn'+i}}" class="dropdown-item" (click)="editRuleByIndex(i)">{{'TAG_RETENTION.EDIT' | translate}}</button>
<button type="button" class="dropdown-item" id="{{'delete-btn'+i}}" (click)="deleteRule(rule.id)">{{'TAG_RETENTION.DELETE' | translate}}</button>
</div>
</div>
</div>
<div class="clr-col">
<span>
<clr-icon *ngIf="!rule?.disabled" class="color-green" shape="success-standard"></clr-icon>
<clr-icon id="{{'disable-icon'+i}}" *ngIf="rule?.disabled" class="color-red" shape="error-standard"></clr-icon>
</span>
<span class="rule-name ml-5">
<span>{{'TAG_RETENTION.IN_REPOSITORIES' | translate}}</span>
<span>{{getI18nKey(rule?.scope_selectors?.repository[0]?.decoration)|translate}}</span>
<span>{{formatPattern(rule?.scope_selectors?.repository[0]?.pattern)}}</span>
<span>,</span>
<span>{{'TAG_RETENTION.LOWER_TAGS' | translate}}</span>
<span>{{getI18nKey(rule?.tag_selectors[0]?.decoration)|translate}}</span>
<span id="{{'tag-selectors-patten'+i}}">{{formatPattern(rule?.tag_selectors[0]?.pattern)}}</span>
<ng-container *ngIf="rule?.tag_selectors[1]?.pattern && rule?.tag_selectors[1]?.pattern">
<span class="color-97">{{'TAG_RETENTION.AND' | translate}}</span>
<span>{{'TAG_RETENTION.LOWER_LABELS' | translate}}</span>
<span>{{getI18nKey(rule?.tag_selectors[1]?.decoration)|translate}}</span>
<span>{{rule?.tag_selectors[1]?.pattern}}</span>
</ng-container>
</span>
</div>
</div>
</li>
</ul>
<div class="v-center clr-row" [ngClass]="{'pt-1':rules?.length > 0}">
<div class="clr-col-2 flex-150"></div>
<div class="clr-col-2">
<button [disabled]="rules?.length >= 15" id="add-rule" class="btn btn-primary btn-sm" (click)="openAddRule()">{{'TAG_RETENTION.ADD_RULE' | translate}}</button>
</div>
<div class="clr-col-8 color-97 font-size-54">
{{'TAG_RETENTION.ADD_RULE_HELP_1' | translate}}
</div>
</div>
</div>
</div>
<app-add-rule #addRule [rules]="rules" [projectId]="projectId" (clickAdd)="clickAdd($event)"></app-add-rule>
<div class="backdrop-transparent" (click)="ruleIndex = -1" *ngIf="ruleIndex !== -1"></div>

View File

@ -0,0 +1,399 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { InlineAlertComponent } from "../../shared/inline-alert/inline-alert.component";
import { ImmutableTagComponent } from './immutable-tag.component';
import { ClarityModule } from '@clr/angular';
import { FormsModule } from '@angular/forms';
import { AddRuleComponent } from './add-rule/add-rule.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { ImmutableTagService } from './immutable-tag.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { ActivatedRoute } from '@angular/router';
import { of, throwError } from 'rxjs';
import { ErrorHandler, DefaultErrorHandler, clone } from '@harbor/ui';
describe('ImmutableTagComponent', () => {
let component: ImmutableTagComponent;
let addRuleComponent: AddRuleComponent;
let immutableTagService: ImmutableTagService;
let errorHandler: ErrorHandler;
let fixture: ComponentFixture<ImmutableTagComponent>;
let fixtureAddrule: ComponentFixture<AddRuleComponent>;
let mockMetadata = {
"templates": [
{
"rule_template": "latestPushedK",
"display_text": "the most recently pushed # images",
"action": "retain",
"params": [
{
"type": "int",
"unit": "COUNT",
"required": true
}
]
},
{
"rule_template": "latestPulledN",
"display_text": "the most recently pulled # images",
"action": "retain",
"params": [
{
"type": "int",
"unit": "COUNT",
"required": true
}
]
},
{
"rule_template": "nDaysSinceLastPush",
"display_text": "pushed within the last # days",
"action": "retain",
"params": [
{
"type": "int",
"unit": "DAYS",
"required": true
}
]
},
{
"rule_template": "nDaysSinceLastPull",
"display_text": "pulled within the last # days",
"action": "retain",
"params": [
{
"type": "int",
"unit": "DAYS",
"required": true
}
]
},
{
"rule_template": "always",
"display_text": "always",
"action": "retain",
"params": []
}
],
"scope_selectors": [
{
"display_text": "Repositories",
"kind": "doublestar",
"decorations": [
"repoMatches",
"repoExcludes"
]
}
],
"tag_selectors": [
{
"display_text": "Tags",
"kind": "doublestar",
"decorations": [
"matches",
"excludes"
]
}
]
};
let mockRules =
[
{
"id": 1,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "**"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
}, {
"id": 2,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "44"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
},
{
"id": 3,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "555"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
},
{
"id": 4,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "fff**"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**ggg"
}
]
}
}
];
let cloneRule = clone(mockRules[0]);
cloneRule.tag_selectors[0].pattern = 'rep';
let cloneRuleNoId = clone(mockRules[0]);
cloneRuleNoId.id = null;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ImmutableTagComponent, AddRuleComponent, InlineAlertComponent],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
],
imports: [
NoopAnimationsModule,
ClarityModule,
FormsModule,
HttpClientTestingModule,
TranslateModule.forRoot()
],
providers: [
ImmutableTagService,
{
provide: ActivatedRoute, useValue: {
paramMap: of({ get: (key) => 'value' }),
snapshot: {
parent: {
params: { id: 1 }
},
data: 1
}
}
},
{ provide: ErrorHandler, useClass: DefaultErrorHandler }
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ImmutableTagComponent);
fixtureAddrule = TestBed.createComponent(AddRuleComponent);
component = fixture.componentInstance;
addRuleComponent = fixtureAddrule.componentInstance;
addRuleComponent.open = () => {
return null;
};
component.projectId = 1;
component.addRuleComponent = TestBed.createComponent(AddRuleComponent).componentInstance;
component.addRuleComponent = TestBed.createComponent(AddRuleComponent).componentInstance;
component.addRuleComponent.open = () => {
return null;
};
component.addRuleComponent.inlineAlert = TestBed.createComponent(InlineAlertComponent).componentInstance;
immutableTagService = fixture.debugElement.injector.get(ImmutableTagService);
errorHandler = fixture.debugElement.injector.get(ErrorHandler);
spyOn(immutableTagService, "getRetentionMetadata")
.and.returnValue(of(mockMetadata, throwError('error')));
spyOn(immutableTagService, "getRules")
.withArgs(component.projectId)
.and.returnValue(of(mockRules))
.withArgs(0)
.and.returnValue(throwError('error'));
spyOn(immutableTagService, "updateRule")
.withArgs(component.projectId, mockRules[0])
.and.returnValue(of(null))
.withArgs(component.projectId, cloneRule)
.and.returnValue(of(null));
spyOn(immutableTagService, "deleteRule")
.withArgs(component.projectId, mockRules[3].id)
.and.returnValue(of(null));
spyOn(immutableTagService, "createRule")
.withArgs(component.projectId, cloneRuleNoId)
.and.returnValue(of(null))
.withArgs(0, cloneRuleNoId)
.and.returnValue(throwError({error: { message: 'error'}}));
spyOn(immutableTagService, "getProjectInfo")
.withArgs(component.projectId)
.and.returnValue(of(null));
spyOn(errorHandler, "error")
.and.returnValue(null);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it("should show some rules in page", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRep: HTMLLIElement[] = fixture.nativeElement.querySelectorAll(".rule");
expect(elRep).toBeTruthy();
expect(elRep.length).toEqual(4);
});
}));
it("should show error in list rule", async(() => {
fixture.detectChanges();
component.projectId = 0;
component.getRules();
fixture.whenStable().then(() => {
fixture.detectChanges();
component.projectId = 1;
});
}));
it("should toggle disable and enable", async(() => {
fixture.detectChanges();
let elRep: HTMLButtonElement = fixture.nativeElement.querySelector("#action0");
elRep.dispatchEvent(new Event('click'));
elRep.click();
let elRepDisable: HTMLButtonElement = fixture.nativeElement.querySelector("#disable-btn0");
expect(elRepDisable).toBeTruthy();
elRepDisable.dispatchEvent(new Event('click'));
elRepDisable.click();
mockRules[0].disabled = true;
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRepDisableIcon: HTMLButtonElement = fixture.nativeElement.querySelector("#disable-icon0");
expect(elRepDisableIcon).toBeTruthy();
});
}));
it("should be deleted", async(() => {
fixture.detectChanges();
let elRep: HTMLButtonElement = fixture.nativeElement.querySelector("#action0");
elRep.dispatchEvent(new Event('click'));
elRep.click();
let elRepDisable: HTMLButtonElement = fixture.nativeElement.querySelector("#delete-btn3");
expect(elRepDisable).toBeTruthy();
elRepDisable.dispatchEvent(new Event('click'));
elRepDisable.click();
let rule = mockRules.pop();
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRepRule: HTMLLIElement[] = fixture.nativeElement.querySelectorAll(".rule");
expect(elRepRule.length).toEqual(3);
mockRules.push(rule);
});
}));
it("should be add rule", async(() => {
fixture.detectChanges();
component.clickAdd(cloneRuleNoId);
mockRules.push(cloneRuleNoId);
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRepRule: HTMLLIElement[] = fixture.nativeElement.querySelectorAll(".rule");
expect(elRepRule.length).toEqual(5);
mockRules.pop();
});
}));
it("should be add rule error", async(() => {
fixture.detectChanges();
component.projectId = 0;
component.clickAdd(cloneRuleNoId);
// mockRules.push(cloneRuleNoId);
fixture.whenStable().then(() => {
fixture.detectChanges();
component.projectId = 1;
let elRepRule: HTMLLIElement[] = fixture.nativeElement.querySelectorAll(".rule");
expect(elRepRule.length).toEqual(4);
// mockRules.pop();
});
}));
it("should be edit rule ", async(() => {
fixture.detectChanges();
component.clickAdd(cloneRule);
mockRules[0].tag_selectors[0].pattern = 'rep';
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRepRule: HTMLLIElement = fixture.nativeElement.querySelector("#tag-selectors-patten0");
expect(elRepRule.textContent).toEqual('rep');
mockRules[0].tag_selectors[0].pattern = '**';
});
}));
it("should be edit rule with no add", async(() => {
fixture.detectChanges();
component.addRuleComponent.isAdd = false;
component.clickAdd(cloneRule);
mockRules[0].tag_selectors[0].pattern = 'rep';
fixture.whenStable().then(() => {
fixture.detectChanges();
let elRepRule: HTMLLIElement = fixture.nativeElement.querySelector("#tag-selectors-patten0");
expect(elRepRule.textContent).toEqual('rep');
mockRules[0].tag_selectors[0].pattern = '**';
component.addRuleComponent.isAdd = true;
});
}));
});

View File

@ -0,0 +1,169 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AddRuleComponent } from "./add-rule/add-rule.component";
import { ImmutableTagService } from "./immutable-tag.service";
import { ImmutableRetentionRule } from "../tag-retention/retention";
import { clone, ErrorHandler } from "@harbor/ui";
import { finalize } from "rxjs/operators";
@Component({
selector: 'app-immutable-tag',
templateUrl: './immutable-tag.component.html',
styleUrls: ['./immutable-tag.component.scss']
})
export class ImmutableTagComponent implements OnInit {
projectId: number;
selectedItem: any = null;
ruleIndex: number = -1;
index: number = -1;
rules: ImmutableRetentionRule[] = [];
editIndex: number;
loadingRule: boolean = false;
@ViewChild('addRule', { static: false }) addRuleComponent: AddRuleComponent;
constructor(
private route: ActivatedRoute,
private immutableTagService: ImmutableTagService,
public errorHandler: ErrorHandler,
) {
}
ngOnInit() {
this.projectId = +this.route.snapshot.parent.params['id'];
this.getRules();
this.getMetadata();
}
getMetadata() {
this.immutableTagService.getRetentionMetadata().subscribe(
response => {
this.addRuleComponent.metadata = response;
}, error => {
this.errorHandler.error(error);
});
}
getRules() {
this.immutableTagService.getRules(this.projectId).subscribe(
response => {
this.rules = response as ImmutableRetentionRule[];
this.loadingRule = false;
}, error => {
this.errorHandler.error(error);
this.loadingRule = false;
});
}
editRuleByIndex(index) {
this.editIndex = index;
this.addRuleComponent.rule = clone(this.rules[index]);
this.addRuleComponent.editRuleOrigin = clone(this.rules[index]);
this.addRuleComponent.open();
this.addRuleComponent.isAdd = false;
this.ruleIndex = -1;
}
toggleDisable(rule, isActionDisable) {
rule.disabled = isActionDisable;
this.ruleIndex = -1;
this.loadingRule = true;
this.immutableTagService.updateRule(this.projectId, rule).subscribe(
response => {
this.getRules();
}, error => {
this.loadingRule = false;
this.errorHandler.error(error);
});
}
deleteRule(ruleId) {
// // if rules is empty, clear schedule.
this.ruleIndex = -1;
this.loadingRule = true;
this.immutableTagService.deleteRule(this.projectId, ruleId).subscribe(
response => {
this.getRules();
}, error => {
this.loadingRule = false;
this.errorHandler.error(error);
});
}
openAddRule() {
this.addRuleComponent.open();
this.addRuleComponent.isAdd = true;
this.addRuleComponent.rule = new ImmutableRetentionRule(this.projectId);
}
openEditor(index) {
if (this.ruleIndex !== index) {
this.ruleIndex = index;
} else {
this.ruleIndex = -1;
}
}
refreshAfterCreatRetention() {
this.immutableTagService.getProjectInfo(this.projectId).subscribe(
response => {
this.getRules();
}, error => {
this.loadingRule = false;
this.errorHandler.error(error);
});
}
clickAdd(rule) {
this.loadingRule = true;
this.addRuleComponent.onGoing = true;
if (this.addRuleComponent.isAdd) {
if (!rule.id) {
this.immutableTagService.createRule(this.projectId, rule)
.pipe(finalize(() => this.addRuleComponent.onGoing = false)).subscribe(
response => {
this.refreshAfterCreatRetention();
this.addRuleComponent.close();
}, error => {
if (error && error.error && error.error.message) {
error = this.immutableTagService.getI18nKey(error.error.message);
}
this.addRuleComponent.inlineAlert.showInlineError(error);
this.loadingRule = false;
});
} else {
this.immutableTagService.updateRule(this.projectId, rule)
.pipe(finalize(() => this.addRuleComponent.onGoing = false)).subscribe(
response => {
this.getRules();
this.addRuleComponent.close();
}, error => {
this.loadingRule = false;
if (error && error.error && error.error.message) {
error = this.immutableTagService.getI18nKey(error.error.message);
}
this.addRuleComponent.inlineAlert.showInlineError(error);
});
}
} else {
this.immutableTagService.updateRule(this.projectId, rule)
.pipe(finalize(() => this.addRuleComponent.onGoing = false)).subscribe(
response => {
this.getRules();
this.addRuleComponent.close();
}, error => {
if (error && error.error && error.error.message) {
error = this.immutableTagService.getI18nKey(error.error.message);
}
this.addRuleComponent.inlineAlert.showInlineError(error);
this.loadingRule = false;
});
}
}
formatPattern(pattern: string): string {
return pattern.replace(/[{}]/g, "");
}
getI18nKey(str: string) {
return this.immutableTagService.getI18nKey(str);
}
}

View File

@ -0,0 +1,25 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SharedModule } from '../../shared/shared.module';
import { ImmutableTagComponent } from './immutable-tag.component';
import { ImmutableTagService } from './immutable-tag.service';
import { TranslateModule } from '@ngx-translate/core';
import { AddRuleComponent } from './add-rule/add-rule.component';
@NgModule({
declarations: [ImmutableTagComponent, AddRuleComponent],
imports: [
CommonModule,
SharedModule,
TranslateModule
],
exports: [
],
providers: [
ImmutableTagService
]
})
export class ImmutableTagModule { }

View File

@ -0,0 +1,127 @@
import { ImmutableTagService } from './immutable-tag.service';
import { TestBed, inject } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('ImmutableTagService', () => {
beforeEach(() => TestBed.configureTestingModule({
providers: [ImmutableTagService],
imports: [
HttpClientTestingModule
]
}));
it('should be created', () => {
const service: ImmutableTagService = TestBed.get(ImmutableTagService);
expect(service).toBeTruthy();
});
it('should get rules',
inject(
[HttpTestingController, ImmutableTagService],
(httpMock: HttpTestingController, immutableTagService: ImmutableTagService) => {
const mockRules =
[
{
"id": 1,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "**"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
}, {
"id": 2,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "44"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
},
{
"id": 3,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "555"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**"
}
]
}
},
{
"id": 4,
"project_id": 1,
"disabled": false,
"priority": 0,
"action": "immutable",
"template": "immutable_template",
"tag_selectors": [
{
"kind": "doublestar",
"decoration": "matches",
"pattern": "fff**"
}
],
"scope_selectors": {
"repository": [
{
"kind": "doublestar",
"decoration": "repoMatches",
"pattern": "**ggg"
}
]
}
}
];
immutableTagService.getRules(1).subscribe((res) => {
expect(res).toEqual(mockRules);
});
}
)
);
});

View File

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { ImmutableRetentionRule, RuleMetadate } from "../tag-retention/retention";
import { Observable, throwError as observableThrowError } from "rxjs";
import { map, catchError } from "rxjs/operators";
import { Project } from "../project";
import { HTTP_JSON_OPTIONS } from "@harbor/ui";
@Injectable()
export class ImmutableTagService {
private I18nMap: object = {
"retain": "ACTION_RETAIN",
"lastXDays": "RULE_NAME_1",
"latestActiveK": "RULE_NAME_2",
"latestPushedK": "RULE_NAME_3",
"latestPulledN": "RULE_NAME_4",
"always": "RULE_NAME_5",
"nDaysSinceLastPull": "RULE_NAME_6",
"nDaysSinceLastPush": "RULE_NAME_7",
"the images from the last # days": "RULE_TEMPLATE_1",
"the most recent active # images": "RULE_TEMPLATE_2",
"the most recently pushed # images": "RULE_TEMPLATE_3",
"the most recently pulled # images": "RULE_TEMPLATE_4",
"pulled within the last # days": "RULE_TEMPLATE_6",
"pushed within the last # days": "RULE_TEMPLATE_7",
"repoMatches": "MAT",
"repoExcludes": "EXC",
"matches": "MAT",
"excludes": "EXC",
"withLabels": "WITH",
"withoutLabels": "WITHOUT",
"COUNT": "UNIT_COUNT",
"DAYS": "UNIT_DAY",
"none": "NONE",
"nothing": "NONE",
"Parameters nDaysSinceLastPull is too large": "DAYS_LARGE",
"Parameters nDaysSinceLastPush is too large": "DAYS_LARGE",
"Parameters latestPushedK is too large": "COUNT_LARGE",
"Parameters latestPulledN is too large": "COUNT_LARGE"
};
constructor(
private http: HttpClient,
) {
}
getI18nKey(str: string): string {
if (this.I18nMap[str.trim()]) {
return "TAG_RETENTION." + this.I18nMap[str.trim()];
}
return str;
}
getRetentionMetadata(): Observable<RuleMetadate> {
return this.http.get(`/api/retentions/metadatas`)
.pipe(map(response => response as RuleMetadate))
.pipe(catchError(error => observableThrowError(error)));
}
getRules(projectId): Observable<ImmutableRetentionRule[]> {
return this.http.get(`/api/projects/${projectId}/immutabletagrules`)
.pipe(map(response => response as ImmutableRetentionRule[]))
.pipe(catchError(error => observableThrowError(error)));
}
createRule(projectId: number, retention: ImmutableRetentionRule) {
return this.http.post(`/api/projects/${projectId}/immutabletagrules`, retention)
.pipe(catchError(error => observableThrowError(error)));
}
updateRule(projectId, immutabletagrule: ImmutableRetentionRule) {
return this.http.put(`/api/projects/${projectId}/immutabletagrules/${immutabletagrule.id}`, immutabletagrule)
.pipe(catchError(error => observableThrowError(error)));
}
deleteRule(projectId, ruleId) {
return this.http.delete(`/api/projects/${projectId}/immutabletagrules/${ruleId}`, HTTP_JSON_OPTIONS)
.pipe(catchError(error => observableThrowError(error)));
}
getProjectInfo(projectId) {
return this.http.get(`/api/projects/${projectId}`)
.pipe(map(response => response as Project))
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -28,6 +28,9 @@
<li class="nav-item" *ngIf="hasTagRetentionPermission">
<a class="nav-link" routerLink="tag-retention" routerLinkActive="active">{{'TAG_RETENTION.TAG_RETENTION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="hasTagRetentionPermission">
<a class="nav-link" routerLink="immutable-tag" routerLinkActive="active">{{'PROJECT_DETAIL.IMMUTABLE_TAG' | translate}}</a>
</li>
<li class="nav-item" *ngIf="hasWebhookListPermission">
<a class="nav-link" routerLink="webhook" routerLinkActive="active">{{'PROJECT_DETAIL.WEBHOOKS' | translate}}</a>
</li>

View File

@ -18,6 +18,7 @@ import { SharedModule } from '../shared/shared.module';
import { RepositoryModule } from '../repository/repository.module';
import { ReplicationModule } from '../replication/replication.module';
import { SummaryModule } from './summary/summary.module';
import { ImmutableTagModule } from './immutable-tag/immutable-tag.module';
import { LogModule } from '../log/log.module';
import { ProjectComponent } from './project.component';
@ -58,7 +59,8 @@ import { ConfigScannerService } from "../config/scanner/config-scanner.service";
LogModule,
RouterModule,
HelmChartModule,
SummaryModule
SummaryModule,
ImmutableTagModule
],
declarations: [
ProjectComponent,

View File

@ -11,8 +11,19 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export class Retention {
export class BaseRetention {
algorithm: string;
scope: {
level: string,
ref: number;
};
cap: number;
constructor() {
this.algorithm = "or";
}
}
export class Retention extends BaseRetention {
rules: Array<Rule>;
trigger: {
kind: string;
@ -21,15 +32,9 @@ export class Retention {
cron: string;
}
};
scope: {
level: string,
ref: number;
};
cap: number;
constructor() {
super();
this.rules = [];
this.algorithm = "or";
this.trigger = {
kind: "Schedule",
references: {},
@ -40,13 +45,12 @@ export class Retention {
}
}
export class Rule {
export class BaseRule {
disabled: boolean;
template: string;
id: number;
priority: number;
action: string;
template: string;
params: object;
tag_selectors: Array<Selector>;
scope_selectors: {
repository: Array<Selector>;
@ -55,7 +59,6 @@ export class Rule {
constructor() {
this.disabled = false;
this.action = "retain";
this.params = {};
this.scope_selectors = {
repository: [
{
@ -75,6 +78,27 @@ export class Rule {
}
}
export class ImmutableRetentionRule extends BaseRule {
project_id: number;
constructor(project_id) {
super();
this.project_id = project_id;
this.priority = 0;
this.action = 'immutable';
this.template = 'immutable_template';
}
}
// rule for tag-retention
export class Rule extends BaseRule {
params: object;
constructor() {
super();
this.params = {};
}
}
export class Selector {
kind: string;
decoration: string;

View File

@ -247,7 +247,8 @@
"CONFIG": "Configuration",
"HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts",
"WEBHOOKS": "Webhooks"
"WEBHOOKS": "Webhooks",
"IMMUTABLE_TAG": "Immutable Tag"
},
"PROJECT_CONFIG": {
"REGISTRY": "Project registry",
@ -318,10 +319,10 @@
"TOKEN": "Token",
"NEW_ROBOT_ACCOUNT": "NEW ROBOT ACCOUNT",
"ENABLED_STATE": "Enabled state",
"NUMBER_REQUIRED":"Field is required and should be an integer other than 0.",
"NUMBER_REQUIRED": "Field is required and should be an integer other than 0.",
"DESCRIPTION": "Description",
"EXPIRATION": "Expiration",
"TOKEN_EXPIRATION":"Robot Token Expiration (Days)",
"TOKEN_EXPIRATION": "Robot Token Expiration (Days)",
"ACTION": "Action",
"EDIT": "Edit",
"ITEMS": "items",
@ -342,8 +343,8 @@
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE" : "export to file"
"PULL_IS_MUST": "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE": "export to file"
},
"WEBHOOK": {
"EDIT_BUTTON": "EDIT",
@ -451,7 +452,7 @@
"BOTH": "both",
"STOP_SUCCESS": "Stop Execution {{param}} Successful",
"STOP_SUMMARY": "Do you want to stop the executions {{param}}?",
"TASK_ID":"Task ID",
"TASK_ID": "Task ID",
"RESOURCE_TYPE": "Resource Type",
"SOURCE": "Source",
"DESTINATION": "Destination",
@ -484,7 +485,7 @@
"TESTING_CONNECTION": "Testing Connection...",
"TEST_CONNECTION_SUCCESS": "Connection tested successfully.",
"TEST_CONNECTION_FAILURE": "Failed to ping endpoint.",
"ID":"ID",
"ID": "ID",
"NAME": "Name",
"NAME_IS_REQUIRED": "Name is required.",
"DESCRIPTION": "Description",
@ -493,7 +494,7 @@
"REPLICATION_MODE": "Replication Mode",
"SRC_REGISTRY": "Source registry",
"DESTINATION_NAMESPACE": "Destination registry:Namespace",
"LAST_REPLICATION":"Last Replication",
"LAST_REPLICATION": "Last Replication",
"DESTINATION_NAME_IS_REQUIRED": "Endpoint name is required.",
"NEW_DESTINATION": "New Endpoint",
"DESTINATION_URL": "Endpoint URL",
@ -549,7 +550,7 @@
"SOURCE_RESOURCE_FILTER": "Source resource filter",
"SCHEDULED": "Scheduled",
"MANUAL": "Manual",
"EVENT_BASED":"Event Based",
"EVENT_BASED": "Event Based",
"DAILY": "Daily",
"WEEKLY": "Weekly",
"SETTING": "Options",
@ -568,14 +569,14 @@
"ACKNOWLEDGE": "Acknowledge",
"RULE_DISABLED": "This rule has been disabled because a label used in its filter has been deleted. \n Edit the rule and update its filter to enable it.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace",
"SOURCE_REGISTRY": "Source registry",
"SOURCE_NAMESPACES": "Source namespaces",
"DEST_REGISTRY": "Destination registry",
"DEST_NAMESPACE": "Destination namespace",
"NAMESPACE_TOOLTIP": "Namespace name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"TAG":"Tag",
"LABEL":"Label",
"RESOURCE":"Resource"
"TAG": "Tag",
"LABEL": "Label",
"RESOURCE": "Resource"
},
"DESTINATION": {
"NEW_ENDPOINT": "New Endpoint",
@ -585,7 +586,7 @@
"NAME_IS_REQUIRED": "Endpoint name is required.",
"URL": "Endpoint URL",
"URL_IS_REQUIRED": "Endpoint URL is required.",
"AUTHENTICATION":"Authentication",
"AUTHENTICATION": "Authentication",
"ACCESS_ID": "Access ID",
"ACCESS_SECRET": "Access Secret",
"STATUS": "Status",
@ -816,9 +817,9 @@
"READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. ",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode.",
"WEBHOOK_TOOLTIP": "Enable webhooks to receive callbacks at your designated endpoints when certain actions such as image or chart being pushed, pulled, deleted, scanned are performed",
"HOURLY_CRON":"Run once an hour, beginning of hour. Equivalent to 0 0 * * * *.",
"WEEKLY_CRON":"Run once a week, midnight between Sat/Sun. Equivalent to 0 0 0 * * 0.",
"DAILY_CRON":"Run once a day, midnight. Equivalent to 0 0 0 * * *."
"HOURLY_CRON": "Run once an hour, beginning of hour. Equivalent to 0 0 * * * *.",
"WEEKLY_CRON": "Run once a week, midnight between Sat/Sun. Equivalent to 0 0 0 * * 0.",
"DAILY_CRON": "Run once a day, midnight. Equivalent to 0 0 0 * * *."
},
"LDAP": {
"URL": "LDAP URL",

View File

@ -248,7 +248,8 @@
"CONFIG": "Configuración",
"HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts",
"WEBHOOKS": "Webhooks"
"WEBHOOKS": "Webhooks",
"IMMUTABLE_TAG": "Immutable Tag"
},
"PROJECT_CONFIG": {
"REGISTRY": "Registro de proyectos",
@ -320,8 +321,8 @@
"NEW_ROBOT_ACCOUNT": "NEW ROBOT ACCOUNT",
"ENABLED_STATE": "Enabled state",
"EXPIRATION": "Expiration",
"NUMBER_REQUIRED":"Field is required and should be an integer other than 0.",
"TOKEN_EXPIRATION":"Robot Token Expiration (Days)",
"NUMBER_REQUIRED": "Field is required and should be an integer other than 0.",
"TOKEN_EXPIRATION": "Robot Token Expiration (Days)",
"DESCRIPTION": "Description",
"ACTION": "Action",
"EDIT": "Edit",
@ -343,8 +344,8 @@
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirm removal of robot accounts",
"DELETION_SUMMARY": "Do you want to delete robot accounts {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE" : "export to file"
"PULL_IS_MUST": "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE": "export to file"
},
"WEBHOOK": {
"EDIT_BUTTON": "EDIT",
@ -451,7 +452,7 @@
"PLEASE_SELECT": "select an option",
"STOP_SUCCESS": "Stop Execution {{param}} Successful",
"STOP_SUMMARY": "De que desea detener las ejecuciones {{param}}?",
"TASK_ID":"Task ID",
"TASK_ID": "Task ID",
"RESOURCE_TYPE": "Resource Type",
"SOURCE": "Source",
"DESTINATION": "Destination",
@ -462,7 +463,7 @@
"FAILURE": "FAILURE",
"IN_PROGRESS": "IN PROGRESS",
"STOP_EXECUTIONS": "Stop Execution",
"ID":"ID",
"ID": "ID",
"REPLICATION_RULE": "Reglas de Replicación",
"NEW_REPLICATION_RULE": "Nueva Regla de Replicación",
"ENDPOINTS": "Endpoints",
@ -495,7 +496,7 @@
"REPLICATION_MODE": "Replication Mode",
"SRC_REGISTRY": "Source registry",
"DESTINATION_NAMESPACE": "Destination registry:Namespace",
"LAST_REPLICATION":"Last Replication",
"LAST_REPLICATION": "Last Replication",
"DESTINATION_NAME_IS_REQUIRED": "El nombre del endpoint es obligatorio.",
"NEW_DESTINATION": "Nuevo Endpoint",
"DESTINATION_URL": "URL del Endpoint",
@ -551,7 +552,7 @@
"SOURCE_RESOURCE_FILTER": "Source resource filter",
"SCHEDULED": "Scheduled",
"MANUAL": "Manual",
"EVENT_BASED":"Event Based",
"EVENT_BASED": "Event Based",
"DAILY": "Daily",
"WEEKLY": "Weekly",
"SETTING": "Options",
@ -569,14 +570,14 @@
"ACKNOWLEDGE": "Acknowledge",
"RULE_DISABLED": "This rule has been disabled because a label used in its filter has been deleted. \n Edit the rule and update its filter to enable it.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace",
"SOURCE_REGISTRY": "Source registry",
"SOURCE_NAMESPACES": "Source namespaces",
"DEST_REGISTRY": "Destination registry",
"DEST_NAMESPACE": "Destination namespace",
"NAMESPACE_TOOLTIP": "Namespace name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"TAG":"Tag",
"LABEL":"Label",
"RESOURCE":"Resource"
"TAG": "Tag",
"LABEL": "Label",
"RESOURCE": "Resource"
},
"DESTINATION": {
"NEW_ENDPOINT": "Nuevo Endpoint",
@ -586,7 +587,7 @@
"NAME_IS_REQUIRED": "El nombre del endpoint es obligatorio.",
"URL": "URL del Endpoint",
"URL_IS_REQUIRED": "La URL del endpoint es obligatoria.",
"AUTHENTICATION":"Autenticación",
"AUTHENTICATION": "Autenticación",
"ACCESS_ID": "ID de acceso",
"ACCESS_SECRET": "Secreto de acceso",
"STATUS": "Estado",
@ -815,9 +816,9 @@
"READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. ",
"GC_POLICY": "",
"WEBHOOK_TOOLTIP": "Enable webhooks to receive callbacks at your designated endpoints when certain actions such as image or chart being pushed, pulled, deleted, scanned are performed",
"HOURLY_CRON":"Run once an hour, beginning of hour. Equivalente a 0 0 * * * *.",
"WEEKLY_CRON":"Run once a week, midnight between Sat/Sun. Equivalente a 0 0 0 * * 0.",
"DAILY_CRON":"Run once a day, midnight. Equivalente a 0 0 0 * * *."
"HOURLY_CRON": "Run once an hour, beginning of hour. Equivalente a 0 0 * * * *.",
"WEEKLY_CRON": "Run once a week, midnight between Sat/Sun. Equivalente a 0 0 0 * * 0.",
"DAILY_CRON": "Run once a day, midnight. Equivalente a 0 0 0 * * *."
},
"LDAP": {

View File

@ -241,7 +241,8 @@
"CONFIG": "Configuration",
"HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts",
"WEBHOOKS": "Webhooks"
"WEBHOOKS": "Webhooks",
"IMMUTABLE_TAG": "Immutable Tag"
},
"PROJECT_CONFIG": {
"REGISTRY": "Dépôt du Projet",
@ -311,8 +312,8 @@
"NEW_ROBOT_ACCOUNT": "nouveau robot compte ",
"ENABLED_STATE": "état d 'activation",
"EXPIRATION": "Expiration",
"NUMBER_REQUIRED":"Field is required and should be an integer other than 0.",
"TOKEN_EXPIRATION":"Robot Token Expiration (Days)",
"NUMBER_REQUIRED": "Field is required and should be an integer other than 0.",
"TOKEN_EXPIRATION": "Robot Token Expiration (Days)",
"DESCRIPTION": "Description",
"ACTION": "Action",
"EDIT": "Edit",
@ -335,8 +336,8 @@
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "confirmer l'enlèvement des comptes du robot ",
"DELETION_SUMMARY": "Voulez-vous supprimer la règle {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE" : "export to file"
"PULL_IS_MUST": "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE": "export to file"
},
"WEBHOOK": {
"EDIT_BUTTON": "EDIT",
@ -444,7 +445,7 @@
"PLEASE_SELECT": "select an option",
"STOP_SUCCESS": "Stop Execution {{param}} Successful",
"STOP_SUMMARY": "Voulez-vous arrêter les exécutions {{param}}?",
"TASK_ID":"Task ID",
"TASK_ID": "Task ID",
"RESOURCE_TYPE": "Resource Type",
"SOURCE": "Source",
"DESTINATION": "Destination",
@ -455,7 +456,7 @@
"FAILURE": "FAILURE",
"IN_PROGRESS": "IN PROGRESS",
"STOP_EXECUTIONS": "Stop Execution",
"ID":"ID",
"ID": "ID",
"REPLICATION_RULE": "Règle de Réplication",
"NEW_REPLICATION_RULE": "Nouvelle Règle de Réplication",
"ENDPOINTS": "Points finaux",
@ -485,7 +486,7 @@
"REPLICATION_MODE": "Replication Mode",
"SRC_REGISTRY": "Source registry",
"DESTINATION_NAMESPACE": "Destination registry:Namespace",
"LAST_REPLICATION":"Last Replication",
"LAST_REPLICATION": "Last Replication",
"DESTINATION_NAME_IS_REQUIRED": "Le nom du Point Final est obligatoire.",
"NEW_DESTINATION": "Nouveau Point Final",
"DESTINATION_URL": "URL du Point Final",
@ -540,7 +541,7 @@
"SOURCE_RESOURCE_FILTER": "Source resource filter",
"SCHEDULED": "Scheduled",
"MANUAL": "Manual",
"EVENT_BASED":"Event Based",
"EVENT_BASED": "Event Based",
"DAILY": "Daily",
"WEEKLY": "Weekly",
"SETTING": "Options",
@ -559,14 +560,14 @@
"ACKNOWLEDGE": "Acknowledge",
"RULE_DISABLED": "This rule has been disabled because a label used in its filter has been deleted. \n Edit the rule and update its filter to enable it.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace",
"SOURCE_REGISTRY": "Source registry",
"SOURCE_NAMESPACES": "Source namespaces",
"DEST_REGISTRY": "Destination registry",
"DEST_NAMESPACE": "Destination namespace",
"NAMESPACE_TOOLTIP": "Namespace name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"TAG":"Tag",
"LABEL":"Label",
"RESOURCE":"Resource"
"TAG": "Tag",
"LABEL": "Label",
"RESOURCE": "Resource"
},
"DESTINATION": {
"NEW_ENDPOINT": "Nouveau Point Final",
@ -576,7 +577,7 @@
"NAME_IS_REQUIRED": "Le nom du Point final est obligatoire.",
"URL": "URL du Point Final",
"URL_IS_REQUIRED": "L'URL du Point Final est obligatoire.",
"AUTHENTICATION":"Authentification",
"AUTHENTICATION": "Authentification",
"ACCESS_ID": "ID d'accès",
"ACCESS_SECRET": "Secret d'accès",
"STATUS": "Statut",
@ -796,9 +797,9 @@
"READONLY_TOOLTIP": "In read-only mode, you can not delete repositories or tags or push images. ",
"GC_POLICY": "",
"WEBHOOK_TOOLTIP": "Enable webhooks to receive callbacks at your designated endpoints when certain actions such as image or chart being pushed, pulled, deleted, scanned are performed",
"HOURLY_CRON":"Run once an hour, beginning of hour. Équivalent à 0 0 * * * *.",
"WEEKLY_CRON":"Run once a week, midnight between Sat/Sun. Équivalent à 0 0 0 * * 0.",
"DAILY_CRON":"Run once a day, midnight. Équivalent à 0 0 0 * * *."
"HOURLY_CRON": "Run once an hour, beginning of hour. Équivalent à 0 0 * * * *.",
"WEEKLY_CRON": "Run once a week, midnight between Sat/Sun. Équivalent à 0 0 0 * * 0.",
"DAILY_CRON": "Run once a day, midnight. Équivalent à 0 0 0 * * *."
},
"LDAP": {
"URL": "URL LDAP",

View File

@ -245,7 +245,8 @@
"CONFIG": "Configuração",
"HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "Robot Accounts",
"WEBHOOKS": "Webhooks"
"WEBHOOKS": "Webhooks",
"IMMUTABLE_TAG": "Immutable Tag"
},
"PROJECT_CONFIG": {
"REGISTRY": "Registro do Projeto",
@ -317,8 +318,8 @@
"NEW_ROBOT_ACCOUNT": "Novo robô conta",
"ENABLED_STATE": "Enabled state",
"EXPIRATION": "Expiration",
"NUMBER_REQUIRED":"Field is required and should be an integer other than 0.",
"TOKEN_EXPIRATION":"Robot Token Expiration (Days)",
"NUMBER_REQUIRED": "Field is required and should be an integer other than 0.",
"TOKEN_EXPIRATION": "Robot Token Expiration (Days)",
"DESCRIPTION": "Descrição",
"ACTION": "AÇÃO",
"EDIT": "Editar",
@ -340,8 +341,8 @@
"COPY_SUCCESS": "Copy token successfully of '{{param}}'",
"DELETION_TITLE": "Confirmar a remoção do robô Contas",
"DELETION_SUMMARY": "Você quer remover a regra {{param}}?",
"PULL_IS_MUST" : "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE" : "export to file"
"PULL_IS_MUST": "Pull permission is checked by default and can not be modified.",
"EXPORT_TO_FILE": "export to file"
},
"GROUP": {
"GROUP": "Grupo",
@ -449,7 +450,7 @@
"PLEASE_SELECT": "select an option",
"STOP_SUCCESS": "Stop Execution {{param}} Successful",
"STOP_SUMMARY": "Você quer parar as execuções? {{param}}?",
"TASK_ID":"Task ID",
"TASK_ID": "Task ID",
"RESOURCE_TYPE": "Resource Type",
"SOURCE": "Source",
"DESTINATION": "Destination",
@ -460,7 +461,7 @@
"FAILURE": "FAILURE",
"IN_PROGRESS": "IN PROGRESS",
"STOP_EXECUTIONS": "Stop Execution",
"ID":"ID",
"ID": "ID",
"REPLICATION_RULE": "Regra de replicação",
"NEW_REPLICATION_RULE": "Nova regra de replicação",
"ENDPOINTS": "Endpoints",
@ -493,7 +494,7 @@
"REPLICATION_MODE": "Replication Mode",
"SRC_REGISTRY": "Source registry",
"DESTINATION_NAMESPACE": "Destination registry:Namespace",
"LAST_REPLICATION":"Last Replication",
"LAST_REPLICATION": "Last Replication",
"DESTINATION_NAME_IS_REQUIRED": "Nome do Endpoint é obrigatório.",
"NEW_DESTINATION": "Novo Endpoint",
"DESTINATION_URL": "URL do Endpoint",
@ -549,33 +550,33 @@
"SOURCE_RESOURCE_FILTER": "Source resource filter",
"SCHEDULED": "Agendado",
"MANUAL": "Manual",
"EVENT_BASED":"Event Based",
"EVENT_BASED": "Event Based",
"DAILY": "Diário",
"WEEKLY": "Semanal",
"SETTING":"Opções",
"TRIGGER":"Condições de disparo",
"TARGETS":"Destino",
"SETTING": "Opções",
"TRIGGER": "Condições de disparo",
"TARGETS": "Destino",
"MODE": "Modo",
"TRIGGER_MODE": "Modo de disparo",
"SOURCE_PROJECT": "Projeto de origem",
"REPLICATE": "Replicar",
"DELETE_REMOTE_IMAGES":"Remover resources remotas quando removido localmente",
"DELETE_REMOTE_IMAGES": "Remover resources remotas quando removido localmente",
"DELETE_ENABLED": "Enabled this policy",
"REPLICATE_IMMEDIATE":"Replicar imagens existentes imediatamente",
"REPLICATE_IMMEDIATE": "Replicar imagens existentes imediatamente",
"NEW": "Novo",
"NAME_TOOLTIP": "nome da regra de replicação deve conter ao menos 2 caracteres sendo caracteres minusculos, números e ._- e devem iniciar com letras e números.",
"DESTINATION_NAME_TOOLTIP": "Destination name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"ACKNOWLEDGE": "Reconhecer",
"RULE_DISABLED": "Essa regra foi desabilitada pois uma label usada no seu filtro foi removida. \n Edite a regra e atualize seu filtro para habilitá-la.",
"REPLI_MODE": "Replication mode",
"SOURCE_REGISTRY":"Source registry",
"SOURCE_NAMESPACES":"Source namespaces",
"DEST_REGISTRY":"Destination registry",
"DEST_NAMESPACE":"Destination namespace",
"SOURCE_REGISTRY": "Source registry",
"SOURCE_NAMESPACES": "Source namespaces",
"DEST_REGISTRY": "Destination registry",
"DEST_NAMESPACE": "Destination namespace",
"NAMESPACE_TOOLTIP": "Namespace name should be at least 2 characters long with lower case characters, numbers and ._- and must be start with characters or numbers.",
"TAG":"Tag",
"LABEL":"Label",
"RESOURCE":"Resource"
"TAG": "Tag",
"LABEL": "Label",
"RESOURCE": "Resource"
},
"DESTINATION": {
"NEW_ENDPOINT": "Novo Endpoint",
@ -585,7 +586,7 @@
"NAME_IS_REQUIRED": "Nome do Endpoint é obrigatório.",
"URL": "URL do Endpoint",
"URL_IS_REQUIRED": "URL do Endpoint URL é obrigatória.",
"AUTHENTICATION":"Autenticação",
"AUTHENTICATION": "Autenticação",
"ACCESS_ID": "ID de acesso",
"ACCESS_SECRET": "Secreto de acceso",
"STATUS": "Segredo de acesso",
@ -810,9 +811,9 @@
"READONLY_TOOLTIP": "Em modo somente leitura, você não pode remover repositórios ou tags ou enviar imagens. ",
"REPO_TOOLTIP": "Usuários não podem efetuar qualquer operação nas imagens nesse modo.",
"WEBHOOK_TOOLTIP": "Enable webhooks to receive callbacks at your designated endpoints when certain actions such as image or chart being pushed, pulled, deleted, scanned are performed",
"HOURLY_CRON":"Run once an hour, beginning of hour. Equivalente a 0 0 * * * *.",
"WEEKLY_CRON":"Run once a week, midnight between Sat/Sun. Equivalente a 0 0 0 * * 0.",
"DAILY_CRON":"Run once a day, midnight. Equivalente a 0 0 0 * * *."
"HOURLY_CRON": "Run once an hour, beginning of hour. Equivalente a 0 0 * * * *.",
"WEEKLY_CRON": "Run once a week, midnight between Sat/Sun. Equivalente a 0 0 0 * * 0.",
"DAILY_CRON": "Run once a day, midnight. Equivalente a 0 0 0 * * *."
},
"LDAP": {
"URL": "URL LDAP",
@ -1074,15 +1075,15 @@
"SERVER_ERROR": "Não foi possível executar suas ações pois ocorreram erros internos.",
"INCONRRECT_OLD_PWD": "A senha antiga está incorreta.",
"UNKNOWN": "n/a",
"STATUS":"Status",
"STATUS": "Status",
"START_TIME": "Início",
"END_TIME": "Finalização",
"DETAILS":"Detalhes",
"PENDING":"Pendente",
"FINISHED":"Finalizado",
"STOPPED":"Parado",
"RUNNING":"Executando",
"ERROR":"Erro",
"DETAILS": "Detalhes",
"PENDING": "Pendente",
"FINISHED": "Finalizado",
"STOPPED": "Parado",
"RUNNING": "Executando",
"ERROR": "Erro",
"SCHEDULE": {
"NONE": "Nenhum",
"DAILY": "Diário",
@ -1102,14 +1103,14 @@
"AT": "em",
"GC_NOW": "GC AGORA",
"JOB_HISTORY": "GC History",
"JOB_LIST":"Lista de tarefas de Limpeza",
"JOB_ID":"ID DA TAREFA",
"JOB_LIST": "Lista de tarefas de Limpeza",
"JOB_ID": "ID DA TAREFA",
"TRIGGER_TYPE": "TIPO DE DISPARO",
"LATEST_JOBS": "Ultimas tarefas {{param}}",
"LOG_DETAIL":"Detalhes de Log",
"MSG_SUCCESS":"Garbage Collection efetuado com sucesso",
"MSG_SCHEDULE_SET":"Agendamento de Garbage Collection efetuado",
"MSG_SCHEDULE_RESET":"Agendamento de Garbage Collection foi redefinido"
"LOG_DETAIL": "Detalhes de Log",
"MSG_SUCCESS": "Garbage Collection efetuado com sucesso",
"MSG_SCHEDULE_SET": "Agendamento de Garbage Collection efetuado",
"MSG_SCHEDULE_RESET": "Agendamento de Garbage Collection foi redefinido"
},
"RETAG": {
"MSG_SUCCESS": "Retag successfully",

View File

@ -247,7 +247,8 @@
"CONFIG": "Ayarlar",
"HELMCHART": "Helm Tabloları",
"ROBOT_ACCOUNTS": "Robot Hesapları",
"WEBHOOKS": "Ağ Kancaları"
"WEBHOOKS": "Ağ Kancaları",
"IMMUTABLE_TAG": "Immutable Tag"
},
"PROJECT_CONFIG": {
"REGISTRY": "Proje kaydı",
@ -318,10 +319,10 @@
"TOKEN": "Token",
"NEW_ROBOT_ACCOUNT": "YENİ ROBOT HESABI",
"ENABLED_STATE": "Etkin durum",
"NUMBER_REQUIRED":"Alan zorunludur ve 0 dışında bir tam sayı olmalıdır.",
"NUMBER_REQUIRED": "Alan zorunludur ve 0 dışında bir tam sayı olmalıdır.",
"DESCRIPTION": "Açıklama",
"EXPIRATION": "Süre Sonu",
"TOKEN_EXPIRATION":"Robot Token Sona Ermesi (Günler)",
"TOKEN_EXPIRATION": "Robot Token Sona Ermesi (Günler)",
"ACTION": "Eylem",
"EDIT": "Düzenle",
"ITEMS": "parça",
@ -342,8 +343,8 @@
"COPY_SUCCESS": "Token başarıyla kopyala '{{param}}'",
"DELETION_TITLE": "Robot hesaplarının kaldırılmasını onaylayın",
"DELETION_SUMMARY": "Robot hesaplarını silmek istiyor musunuz {{param}}?",
"PULL_IS_MUST" : "Çekme izni varsayılan olarak kontrol edilir ve değiştirilemez.",
"EXPORT_TO_FILE" : "dosyayı dışarı aktar"
"PULL_IS_MUST": "Çekme izni varsayılan olarak kontrol edilir ve değiştirilemez.",
"EXPORT_TO_FILE": "dosyayı dışarı aktar"
},
"WEBHOOK": {
"EDIT_BUTTON": "DÜZENLE",
@ -450,7 +451,7 @@
"BOTH": "her ikisi de",
"STOP_SUCCESS": "Yürütmeyi Durdur {{param}} Başarılı",
"STOP_SUMMARY": "Yürütmeyi durdurmayı iptal istiyor musunuz? {{param}}?",
"TASK_ID":"Görev ID",
"TASK_ID": "Görev ID",
"RESOURCE_TYPE": "Kaynak tipi",
"SOURCE": "Kaynak",
"DESTINATION": "Hedef",
@ -483,7 +484,7 @@
"TESTING_CONNECTION": "Bağlantı Testi...",
"TEST_CONNECTION_SUCCESS": "Bağlantı başarıyla test edildi.",
"TEST_CONNECTION_FAILURE": "Ping atılamadı",
"ID":"ID",
"ID": "ID",
"NAME": "İsim",
"NAME_IS_REQUIRED": "İsim gerekli.",
"DESCRIPTION": "Açıklama",
@ -492,7 +493,7 @@
"REPLICATION_MODE": "Çoğaltma modu",
"SRC_REGISTRY": "Kaynak kaydı",
"DESTINATION_NAMESPACE": "Hedef kayıt defteri: Ad alanı",
"LAST_REPLICATION":"Son Çoğaltma",
"LAST_REPLICATION": "Son Çoğaltma",
"DESTINATION_NAME_IS_REQUIRED": "Uç noktası adı gerekli.",
"NEW_DESTINATION": "Yeni Uç Noktası",
"DESTINATION_URL": "Uç Noktası URL",
@ -548,7 +549,7 @@
"SOURCE_RESOURCE_FILTER": "Kaynak vasıtası filtresi",
"SCHEDULED": "Belirlenmiş",
"MANUAL": "Manuel",
"EVENT_BASED":"Etkinlik Tabanı",
"EVENT_BASED": "Etkinlik Tabanı",
"DAILY": "Günlük",
"WEEKLY": "Haftalık",
"SETTING": "Ayarlar",
@ -567,14 +568,14 @@
"ACKNOWLEDGE": "Onaylamak",
"RULE_DISABLED": "Bu kural, filtresinde kullanılan bir etiket silindiğinden dolayı devre dışı bırakıldı. \n Kuralı düzenleyin ve etkinleştirmek için filtresini güncelleyin.",
"REPLI_MODE": "Çoğaltma modu",
"SOURCE_REGISTRY":"Kaynak kaydı",
"SOURCE_NAMESPACES":"Kaynak ad alanları",
"DEST_REGISTRY":"Hedef kaydı",
"DEST_NAMESPACE":"Hedef ad alanı",
"SOURCE_REGISTRY": "Kaynak kaydı",
"SOURCE_NAMESPACES": "Kaynak ad alanları",
"DEST_REGISTRY": "Hedef kaydı",
"DEST_NAMESPACE": "Hedef ad alanı",
"NAMESPACE_TOOLTIP": "İsim alanı ismi en az 2 karakter uzunluğunda, küçük harfli karakterler, sayılar ve ._- ile başlamalı ve karakter veya sayılarla başlamalıdır. Ad alanı adı en az 2 karakter uzunluğunda, küçük harf, rakam ve ._- ile başlamalı ve karakter veya rakamlarla başlamalıdır.",
"TAG":"Etiketlemek",
"LABEL":"Etiket",
"RESOURCE":"Kaynak"
"TAG": "Etiketlemek",
"LABEL": "Etiket",
"RESOURCE": "Kaynak"
},
"DESTINATION": {
"NEW_ENDPOINT": "Yeni Uç Nokta",
@ -584,7 +585,7 @@
"NAME_IS_REQUIRED": "Uç noktası adı gerekli.",
"URL": "Uç noktası adı gerekli.",
"URL_IS_REQUIRED": "Uç noktası URL gerekli.",
"AUTHENTICATION":"Doğrulama",
"AUTHENTICATION": "Doğrulama",
"ACCESS_ID": "Erişim kimliği",
"ACCESS_SECRET": "Erişim Şifresi",
"STATUS": "Durum",
@ -815,9 +816,9 @@
"READONLY_TOOLTIP": "Salt okunur modda, depoları veya imajları silemez veya görüntüleri yükleyemezsiniz.",
"REPO_TOOLTIP": "Kullanıcılar bu moddaki imajlarda hiçbir işlem yapamazlar.",
"WEBHOOK_TOOLTIP": "Görüntü veya grafik olarak basılan, çekilmiş, silinen, taranan gibi bazı eylemler gerçekleştirildiğinde ağ kancalarının belirtilen bitiş noktalarınızdan geri aramalar almasını sağlayın",
"HOURLY_CRON":"Saat başı bir saat, saat başı çalıştırın. Eşittir 0 0 * * * *.",
"WEEKLY_CRON":"Haftada bir kez, gece yarısı Cmt / Paz arasında koşun. Eşittir 0 0 0 * * 0.",
"DAILY_CRON":"Günde bir kez, gece yarısı çalıştırın. Eşittir0 0 0 * * *."
"HOURLY_CRON": "Saat başı bir saat, saat başı çalıştırın. Eşittir 0 0 * * * *.",
"WEEKLY_CRON": "Haftada bir kez, gece yarısı Cmt / Paz arasında koşun. Eşittir 0 0 0 * * 0.",
"DAILY_CRON": "Günde bir kez, gece yarısı çalıştırın. Eşittir0 0 0 * * *."
},
"LDAP": {
"URL": "LDAP URL",

View File

@ -246,7 +246,8 @@
"CONFIG": "配置管理",
"HELMCHART": "Helm Charts",
"ROBOT_ACCOUNTS": "机器人账户",
"WEBHOOKS": "Webhooks"
"WEBHOOKS": "Webhooks",
"IMMUTABLE_TAG": "不变的Tag"
},
"PROJECT_CONFIG": {
"REGISTRY": "项目仓库",
@ -318,8 +319,8 @@
"NEW_ROBOT_ACCOUNT": "添加机器人账户",
"ENABLED_STATE": "启用状态",
"EXPIRATION": "过期时间",
"NUMBER_REQUIRED":"此项为必填项且为不为0的整数。",
"TOKEN_EXPIRATION":"机器人账户令牌过期时间(天)",
"NUMBER_REQUIRED": "此项为必填项且为不为0的整数。",
"TOKEN_EXPIRATION": "机器人账户令牌过期时间(天)",
"DESCRIPTION": "描述",
"ACTION": "操作",
"EDIT": "编辑",
@ -341,8 +342,8 @@
"COPY_SUCCESS": "成功复制 '{{param}}' 的令牌",
"DELETION_TITLE": "删除账户确认",
"DELETION_SUMMARY": "你确认删除机器人账户 {{param}}?",
"PULL_IS_MUST" : "拉取权限默认选中且不可修改。",
"EXPORT_TO_FILE" : "导出到文件中"
"PULL_IS_MUST": "拉取权限默认选中且不可修改。",
"EXPORT_TO_FILE": "导出到文件中"
},
"WEBHOOK": {
"EDIT_BUTTON": "编辑",
@ -450,7 +451,7 @@
"PLEASE_SELECT": "请选择",
"STOP_SUCCESS": "停止任务 {{param}} 成功",
"STOP_SUMMARY": "确认停止任务{{param}}?",
"TASK_ID":"任务ID",
"TASK_ID": "任务ID",
"RESOURCE_TYPE": "源类型",
"SOURCE": "源",
"DESTINATION": "目标",
@ -461,7 +462,7 @@
"FAILURE": "失败",
"IN_PROGRESS": "进行中",
"STOP_EXECUTIONS": "停止任务",
"ID":"ID",
"ID": "ID",
"REPLICATION_RULE": "同步规则",
"NEW_REPLICATION_RULE": "新建规则",
"ENDPOINTS": "目标",
@ -494,7 +495,7 @@
"REPLICATION_MODE": "同步模式",
"SRC_REGISTRY": "源仓库",
"DESTINATION_NAMESPACE": "目标仓库:命名空间",
"LAST_REPLICATION":"最后一次同步",
"LAST_REPLICATION": "最后一次同步",
"DESTINATION_NAME_IS_REQUIRED": "目标名称为必填项。",
"NEW_DESTINATION": "创建目标",
"DESTINATION_URL": "目标URL",
@ -550,7 +551,7 @@
"SOURCE_RESOURCE_FILTER": "源资源过滤器",
"SCHEDULED": "定时",
"MANUAL": "手动",
"EVENT_BASED":"事件驱动",
"EVENT_BASED": "事件驱动",
"DAILY": "每天",
"WEEKLY": "每周",
"SETTING": "设置",
@ -569,14 +570,14 @@
"ACKNOWLEDGE": "确认",
"RULE_DISABLED": "这个规则因为过滤选项中的标签被删除已经不能用了,更新过滤项以便重新启用规则。",
"REPLI_MODE": "同步模式",
"SOURCE_REGISTRY":"源Registry",
"SOURCE_NAMESPACES":"源Namespace",
"DEST_REGISTRY":"目的Registry",
"DEST_NAMESPACE":"目的Namespace",
"SOURCE_REGISTRY": "源Registry",
"SOURCE_NAMESPACES": "源Namespace",
"DEST_REGISTRY": "目的Registry",
"DEST_NAMESPACE": "目的Namespace",
"NAMESPACE_TOOLTIP": "Namespace名称由小写字符、数字和._-组成且至少2个字符并以字符或者数字开头。",
"TAG":"Tag",
"LABEL":"标签",
"RESOURCE":"资源"
"TAG": "Tag",
"LABEL": "标签",
"RESOURCE": "资源"
},
"DESTINATION": {
"NEW_ENDPOINT": "新建目标",
@ -586,7 +587,7 @@
"NAME_IS_REQUIRED": "目标名为必填项。",
"URL": "目标URL",
"URL_IS_REQUIRED": "目标URL为必填项。",
"AUTHENTICATION":"认证",
"AUTHENTICATION": "认证",
"ACCESS_ID": "访问ID",
"ACCESS_SECRET": "访问密码",
"STATUS": "状态",
@ -816,9 +817,9 @@
"READONLY_TOOLTIP": "选中,表示正在维护状态,不可删除仓库及标签,也不可以推送镜像。",
"REPO_TOOLTIP": "用户在此模式下无法对镜像执行任何操作。",
"WEBHOOK_TOOLTIP": "当执行推送,拉动,删除,扫描镜像或 chart 等特定操作时,启用 webhooks 以在指定端点接收回调",
"HOURLY_CRON":"每小时运行一次。相当于 0 0 * * * *",
"WEEKLY_CRON":"每周一次,周六/周日午夜之间开始。相当于 0 0 * * * *",
"DAILY_CRON":"每天午夜运行一次。相当于 0 0 * * * *"
"HOURLY_CRON": "每小时运行一次。相当于 0 0 * * * *",
"WEEKLY_CRON": "每周一次,周六/周日午夜之间开始。相当于 0 0 * * * *",
"DAILY_CRON": "每天午夜运行一次。相当于 0 0 * * * *"
},
"LDAP": {
"URL": "LDAP URL",