From bfe19711db76bb7fdc1a2b37dbeb0dbdc75981a2 Mon Sep 17 00:00:00 2001 From: "Yang Wang (c)" Date: Tue, 8 Oct 2019 12:18:56 +0800 Subject: [PATCH] Add immutable tag in project detail Signed-off-by: Yogi_Wang --- .gitignore | 3 +- src/portal/src/app/harbor-routing.module.ts | 11 + .../add-rule/add-rule.component.html | 67 +++ .../add-rule/add-rule.component.scss | 7 + .../add-rule/add-rule.component.spec.ts | 115 +++++ .../add-rule/add-rule.component.ts | 177 ++++++++ .../immutable-tag.component.html | 63 +++ .../immutable-tag.component.scss | 0 .../immutable-tag.component.spec.ts | 399 ++++++++++++++++++ .../immutable-tag/immutable-tag.component.ts | 169 ++++++++ .../immutable-tag/immutable-tag.module.ts | 25 ++ .../immutable-tag.service.spec.ts | 127 ++++++ .../immutable-tag/immutable-tag.service.ts | 87 ++++ .../project-detail.component.html | 3 + src/portal/src/app/project/project.module.ts | 4 +- .../app/project/tag-retention/retention.ts | 48 ++- src/portal/src/i18n/lang/en-us-lang.json | 41 +- src/portal/src/i18n/lang/es-es-lang.json | 41 +- src/portal/src/i18n/lang/fr-fr-lang.json | 41 +- src/portal/src/i18n/lang/pt-br-lang.json | 77 ++-- src/portal/src/i18n/lang/tr-tr-lang.json | 41 +- src/portal/src/i18n/lang/zh-cn-lang.json | 41 +- 22 files changed, 1435 insertions(+), 152 deletions(-) create mode 100644 src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.html create mode 100644 src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.scss create mode 100644 src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.spec.ts create mode 100644 src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.ts create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.component.html create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.component.scss create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.component.spec.ts create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.component.ts create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.module.ts create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.service.spec.ts create mode 100644 src/portal/src/app/project/immutable-tag/immutable-tag.service.ts diff --git a/.gitignore b/.gitignore index be445166e..01dad7da8 100644 --- a/.gitignore +++ b/.gitignore @@ -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 + diff --git a/src/portal/src/app/harbor-routing.module.ts b/src/portal/src/app/harbor-routing.module.ts index 6ec95562a..96d7084b7 100644 --- a/src/portal/src/app/harbor-routing.module.ts +++ b/src/portal/src/app/harbor-routing.module.ts @@ -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: { diff --git a/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.html b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.html new file mode 100644 index 000000000..32caf0b85 --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.html @@ -0,0 +1,67 @@ + + + + + + \ No newline at end of file diff --git a/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.scss b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.scss new file mode 100644 index 000000000..8ec3a0df3 --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.scss @@ -0,0 +1,7 @@ +.color-97 { + color: #979797; +} + +.height-72 { + height: 72px; +} \ No newline at end of file diff --git a/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.spec.ts b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.spec.ts new file mode 100644 index 000000000..4db265e9c --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.spec.ts @@ -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; + 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(); + 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(); + }); + })); +}); diff --git a/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.ts b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.ts new file mode 100644 index 000000000..623342312 --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/add-rule/add-rule.component.ts @@ -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(); + @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); + } +} + diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.component.html b/src/portal/src/app/project/immutable-tag/immutable-tag.component.html new file mode 100644 index 000000000..22b2c4b92 --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/immutable-tag.component.html @@ -0,0 +1,63 @@ +
+
+ {{rules?.length ? rules?.length : 0}}/15 + Loading... +
+
+
+
+
    +
  • +
    +
    + +
    +
    + + + + + + {{'TAG_RETENTION.IN_REPOSITORIES' | translate}} + {{getI18nKey(rule?.scope_selectors?.repository[0]?.decoration)|translate}} + {{formatPattern(rule?.scope_selectors?.repository[0]?.pattern)}} + , + {{'TAG_RETENTION.LOWER_TAGS' | translate}} + {{getI18nKey(rule?.tag_selectors[0]?.decoration)|translate}} + {{formatPattern(rule?.tag_selectors[0]?.pattern)}} + + {{'TAG_RETENTION.AND' | translate}} + {{'TAG_RETENTION.LOWER_LABELS' | translate}} + {{getI18nKey(rule?.tag_selectors[1]?.decoration)|translate}} + {{rule?.tag_selectors[1]?.pattern}} + + +
    +
    +
  • +
+
+
+
+ +
+
+ {{'TAG_RETENTION.ADD_RULE_HELP_1' | translate}} +
+
+
+
+ + +
\ No newline at end of file diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.component.scss b/src/portal/src/app/project/immutable-tag/immutable-tag.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.component.spec.ts b/src/portal/src/app/project/immutable-tag/immutable-tag.component.spec.ts new file mode 100644 index 000000000..4e433c5e0 --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/immutable-tag.component.spec.ts @@ -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; + let fixtureAddrule: ComponentFixture; + 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; + }); + + })); + +}); diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.component.ts b/src/portal/src/app/project/immutable-tag/immutable-tag.component.ts new file mode 100644 index 000000000..b5ef4e0bf --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/immutable-tag.component.ts @@ -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); + } +} + diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.module.ts b/src/portal/src/app/project/immutable-tag/immutable-tag.module.ts new file mode 100644 index 000000000..995f71cbf --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/immutable-tag.module.ts @@ -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 { } diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.service.spec.ts b/src/portal/src/app/project/immutable-tag/immutable-tag.service.spec.ts new file mode 100644 index 000000000..100b5046a --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/immutable-tag.service.spec.ts @@ -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); + }); + } + ) + ); +}); diff --git a/src/portal/src/app/project/immutable-tag/immutable-tag.service.ts b/src/portal/src/app/project/immutable-tag/immutable-tag.service.ts new file mode 100644 index 000000000..de28390c4 --- /dev/null +++ b/src/portal/src/app/project/immutable-tag/immutable-tag.service.ts @@ -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 { + return this.http.get(`/api/retentions/metadatas`) + .pipe(map(response => response as RuleMetadate)) + .pipe(catchError(error => observableThrowError(error))); + } + + getRules(projectId): Observable { + 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))); + } +} + + diff --git a/src/portal/src/app/project/project-detail/project-detail.component.html b/src/portal/src/app/project/project-detail/project-detail.component.html index 4f8f55f80..15a6dc0a5 100644 --- a/src/portal/src/app/project/project-detail/project-detail.component.html +++ b/src/portal/src/app/project/project-detail/project-detail.component.html @@ -28,6 +28,9 @@ + diff --git a/src/portal/src/app/project/project.module.ts b/src/portal/src/app/project/project.module.ts index 0d4ddccbd..b20a8ceec 100644 --- a/src/portal/src/app/project/project.module.ts +++ b/src/portal/src/app/project/project.module.ts @@ -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, diff --git a/src/portal/src/app/project/tag-retention/retention.ts b/src/portal/src/app/project/tag-retention/retention.ts index 8d30f5879..3c30052e9 100644 --- a/src/portal/src/app/project/tag-retention/retention.ts +++ b/src/portal/src/app/project/tag-retention/retention.ts @@ -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; 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; scope_selectors: { repository: Array; @@ -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; diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json index 347c91c05..88e2035ee 100644 --- a/src/portal/src/i18n/lang/en-us-lang.json +++ b/src/portal/src/i18n/lang/en-us-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json index d21c621d6..a8492e61a 100644 --- a/src/portal/src/i18n/lang/es-es-lang.json +++ b/src/portal/src/i18n/lang/es-es-lang.json @@ -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": { diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json index a76d9dabd..ec9a492b8 100644 --- a/src/portal/src/i18n/lang/fr-fr-lang.json +++ b/src/portal/src/i18n/lang/fr-fr-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/pt-br-lang.json b/src/portal/src/i18n/lang/pt-br-lang.json index 4d211d9db..002c39964 100644 --- a/src/portal/src/i18n/lang/pt-br-lang.json +++ b/src/portal/src/i18n/lang/pt-br-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/tr-tr-lang.json b/src/portal/src/i18n/lang/tr-tr-lang.json index 28b8b9222..b56259501 100644 --- a/src/portal/src/i18n/lang/tr-tr-lang.json +++ b/src/portal/src/i18n/lang/tr-tr-lang.json @@ -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", diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json index e82e3ed55..279cc7565 100644 --- a/src/portal/src/i18n/lang/zh-cn-lang.json +++ b/src/portal/src/i18n/lang/zh-cn-lang.json @@ -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",