Merge pull request #8392 from AllForNothing/tag-retention

add tag-retention tab to project detail


Ignore codacy at present.
This commit is contained in:
Steven Zou 2019-07-30 11:44:21 +08:00 committed by GitHub
commit dcc79ca2a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1482 additions and 21 deletions

View File

@ -104,8 +104,8 @@ describe('ProjectPolicyConfigComponent', () => {
ProjectPolicyConfigComponent,
ConfirmationDialogComponent,
ConfirmationDialogComponent,
],
providers: [
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: ProjectService, useClass: ProjectDefaultService },
@ -113,7 +113,7 @@ describe('ProjectPolicyConfigComponent', () => {
{ provide: UserPermissionService, useClass: UserPermissionDefaultService},
]
})
.compileComponents();
.compileComponents();
}));
beforeEach(() => {
@ -131,9 +131,9 @@ describe('ProjectPolicyConfigComponent', () => {
userPermissionService = fixture.debugElement.injector.get(UserPermissionService);
spyOn(userPermissionService, "getPermission")
.withArgs(component.projectId,
USERSTATICPERMISSION.CONFIGURATION.KEY, USERSTATICPERMISSION.CONFIGURATION.VALUE.UPDATE )
.and.returnValue(of(mockHasChangeConfigRole));
.withArgs(component.projectId,
USERSTATICPERMISSION.CONFIGURATION.KEY, USERSTATICPERMISSION.CONFIGURATION.VALUE.UPDATE )
.and.returnValue(of(mockHasChangeConfigRole));
fixture.detectChanges();
});

View File

@ -106,11 +106,12 @@ export class ProjectPolicyConfigComponent implements OnInit {
this.systemInfoService.getSystemInfo()
.subscribe(systemInfo => {
this.systemInfo = systemInfo;
setTimeout(() => {
this.dateSystemInput.nativeElement.parentNode.setAttribute("hidden", "hidden");
}, 100);
if (this.withClair) {
setTimeout(() => {
this.dateSystemInput.nativeElement.parentNode.setAttribute("hidden", "hidden");
}, 100);
}
} , error => this.errorHandler.error(error));
// retrive project level policy data
this.retrieve();
this.getPermission();

View File

@ -132,5 +132,17 @@ export const USERSTATICPERMISSION = {
"READ": "read",
}
},
"TAG_RETENTION": {
'KEY': "tag-retention",
'VALUE': {
"CREATE": "create",
"UPDATE": "update",
"DELETE": "delete",
"LIST": "list",
"READ": "read",
"PULL": "pull",
"PUSH": "push"
}
},
};

View File

@ -57,6 +57,8 @@ import { ListChartVersionsComponent } from './project/helm-chart/list-chart-vers
import { HelmChartDetailComponent } from './project/helm-chart/helm-chart-detail/chart-detail.component';
import { OidcOnboardComponent } from './oidc-onboard/oidc-onboard.component';
import { SummaryComponent } from './project/summary/summary.component';
import { TagRetentionComponent } from "./project/tag-retention/tag-retention.component";
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
@ -201,6 +203,10 @@ const harborRoutes: Routes = [
{
path: 'robot-account',
component: RobotAccountComponent
},
{
path: 'tag-retention',
component: TagRetentionComponent
}
]
},

View File

@ -25,6 +25,9 @@
<li class="nav-item" *ngIf="hasRobotListPermission">
<a class="nav-link" routerLink="robot-account" routerLinkActive="active">{{'PROJECT_DETAIL.ROBOT_ACCOUNTS' | translate}}</a>
</li>
<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="isSessionValid && (hasConfigurationListPermission)">
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
</li>

View File

@ -43,6 +43,7 @@ export class ProjectDetailComponent implements OnInit {
hasLogListPermission: boolean;
hasConfigurationListPermission: boolean;
hasRobotListPermission: boolean;
hasTagRetentionPermission: boolean;
constructor(
private route: ActivatedRoute,
private router: Router,
@ -83,10 +84,12 @@ export class ProjectDetailComponent implements OnInit {
USERSTATICPERMISSION.ROBOT.KEY, USERSTATICPERMISSION.ROBOT.VALUE.LIST));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.LABEL.KEY, USERSTATICPERMISSION.LABEL.VALUE.CREATE));
permissionsList.push(this.userPermissionService.getPermission(projectId,
USERSTATICPERMISSION.TAG_RETENTION.KEY, USERSTATICPERMISSION.TAG_RETENTION.VALUE.READ));
forkJoin(...permissionsList).subscribe(Rules => {
[this.hasProjectReadPermission, this.hasLogListPermission, this.hasConfigurationListPermission, this.hasMemberListPermission
, this.hasLabelListPermission, this.hasRepositoryListPermission, this.hasHelmChartsListPermission, this.hasRobotListPermission
, this.hasLabelCreatePermission] = Rules;
, this.hasLabelCreatePermission, this.hasTagRetentionPermission] = Rules;
}, error => this.errorHandler.error(error));
}

View File

@ -40,6 +40,10 @@ import { HelmChartModule } from './helm-chart/helm-chart.module';
import { RobotAccountComponent } from './robot-account/robot-account.component';
import { AddRobotComponent } from './robot-account/add-robot/add-robot.component';
import { AddHttpAuthGroupComponent } from './member/add-http-auth-group/add-http-auth-group.component';
import { TagRetentionComponent } from "./tag-retention/tag-retention.component";
import { AddRuleComponent } from "./tag-retention/add-rule/add-rule.component";
import { TagRetentionService } from "./tag-retention/tag-retention.service";
@NgModule({
imports: [
@ -63,10 +67,12 @@ import { AddHttpAuthGroupComponent } from './member/add-http-auth-group/add-http
AddGroupComponent,
RobotAccountComponent,
AddRobotComponent,
AddHttpAuthGroupComponent
AddHttpAuthGroupComponent,
TagRetentionComponent,
AddRuleComponent,
],
exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, MemberService, RobotService]
providers: [ProjectRoutingResolver, MemberService, RobotService, TagRetentionService]
})
export class ProjectModule {

View File

@ -51,6 +51,7 @@ export class Project {
prevent_vul: string | boolean;
severity: string;
auto_scan: string | boolean;
retention_id: number;
};
constructor () {
this.metadata = <any>{};

View File

@ -0,0 +1,118 @@
<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">
<p class="color-97">{{'TAG_RETENTION.ADD_SUBTITLE' | translate}}</p>
<div class="height-72">
<div class="clr-row">
<div class="clr-col-4">
<div class="over-line"></div>
<label>{{'TAG_RETENTION.BY_WHAT' | translate}}</label>
</div>
<div class="clr-col-5">
<div class="over-line"></div>
<div class="clr-select-wrapper w-100">
<select [(ngModel)]="template" class="clr-select w-100">
<option *ngFor="let t of metadata?.templates"
value="{{t?.rule_template}}">{{getI18nKey(t?.display_text)|translate}}</option>
</select>
</div>
</div>
<div class="clr-col-3">
<div class="over-line">
<span *ngIf="template !=='always'">{{getI18nKey(unit)|translate}}</span>
</div>
<div class="w-100 disabled">
<input *ngIf="template !=='always'" [(ngModel)]="num" class="clr-input w-100">
</div>
</div>
</div>
</div>
<div class="height-72">
<div class="clr-row">
<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 [(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 [(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 [(ngModel)]="tagsSelect" class="clr-select w-100">
<option *ngFor="let d of metadata?.tag_selectors[1]?.decorations"
value="{{d}}">{{getI18nKey(d)|translate}}</option>
</select>
</div>
</div>
<div class="clr-col-5">
<div class="w-100">
<input required [(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 class="height-72">
<div class="clr-row">
<div class="clr-col-4">
<label>{{'TAG_RETENTION.LABELS' | translate}}</label>
</div>
<div class="clr-col-3">
<div class="clr-select-wrapper w-100">
<select [(ngModel)]="labelsSelect" 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 [(ngModel)]="labelsInput" 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_LABELS' | translate}}</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" (click)="cancel()">{{'BUTTON.CANCEL' | translate}}</button>
<button [disabled]="canNotAdd()" type="button" 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,13 @@
.color-97 {
color: #979797;
}
.over-line {
height: 15px;
line-height: 15px;
font-size: 10px;
}
.height-72 {
height: 72px;
}

View File

@ -0,0 +1,174 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.
import {
Component,
OnInit,
OnDestroy,
Output,
EventEmitter,
} from "@angular/core";
import { Rule, RuleMetadate } from "../retention";
import { compareValue } from "@harbor/ui";
import { TagRetentionService } from "../tag-retention.service";
@Component({
selector: "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<Rule>();
metadata: RuleMetadate = new RuleMetadate();
rule: Rule = new Rule();
isAdd: boolean = true;
editRuleOrigin: Rule;
constructor(private tagRetentionService: TagRetentionService) {
}
ngOnInit(): void {
}
ngOnDestroy(): void {
}
set template(template) {
this.rule.template = template;
}
get template() {
return this.rule.template;
}
get unit(): string {
let str = "";
this.metadata.templates.forEach(t => {
if (t.rule_template === this.rule.template) {
str = t.params[0].unit;
}
});
return str;
}
get num() {
return this.rule.params[this.template];
}
set num(num) {
this.rule.params[this.template] = num;
}
get repoSelect() {
return this.rule.scope_selectors.repository[0].decoration;
}
set repoSelect(repoSelect) {
this.rule.scope_selectors.repository[0].decoration = repoSelect;
}
set repositories(repositories) {
if (repositories.indexOf(",") !== -1) {
this.rule.scope_selectors.repository[0].pattern = "{" + repositories + "}";
} else {
this.rule.scope_selectors.repository[0].pattern = repositories;
}
}
get repositories() {
return this.rule.scope_selectors.repository[0].pattern.replace(/[{}]/g, "");
}
get tagsSelect() {
return this.rule.tag_selectors[0].decoration;
}
set tagsSelect(tagsSelect) {
this.rule.tag_selectors[0].decoration = tagsSelect;
}
set tagsInput(tagsInput) {
if (tagsInput.indexOf(",") !== -1) {
this.rule.tag_selectors[0].pattern = "{" + tagsInput + "}";
} else {
this.rule.tag_selectors[0].pattern = tagsInput;
}
}
get tagsInput() {
return this.rule.tag_selectors[0].pattern.replace(/[{}]/g, "");
}
get labelsSelect() {
return this.rule.tag_selectors[1].decoration;
}
set labelsSelect(labelsSelect) {
this.rule.tag_selectors[1].decoration = labelsSelect;
}
set labelsInput(labelsInput) {
this.rule.tag_selectors[1].pattern = labelsInput;
}
get labelsInput() {
return this.rule.tag_selectors[1].pattern;
}
canNotAdd(): boolean {
if (this.rule.template === 'always'
&& this.rule.scope_selectors.repository[0].pattern
&& this.rule.tag_selectors[0].pattern) {
return false;
}
if (this.isAdd) {
if (this.rule.template
&& this.rule.params[this.template]
&& this.rule.scope_selectors.repository[0].pattern
&& this.rule.tag_selectors[0].pattern) {
return false;
}
} else {
if (this.rule.template
&& this.rule.params[this.template]
&& this.rule.scope_selectors.repository[0].pattern
&& this.rule.tag_selectors[0].pattern && !compareValue(this.editRuleOrigin, this.rule)) {
return false;
}
}
return true;
}
open() {
this.addRuleOpened = true;
}
close() {
this.addRuleOpened = false;
}
cancel() {
this.close();
}
add() {
this.close();
this.clickAdd.emit(this.rule);
}
getI18nKey(str: string) {
return this.tagRetentionService.getI18nKey(str);
}
}

View File

@ -0,0 +1,133 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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 {
algorithm: string;
rules: Array<Rule>;
trigger: {
kind: string;
settings: {
cron: string;
}
};
scope: {
level: string,
ref: number;
};
cap: number;
constructor() {
this.rules = [];
this.algorithm = "or";
this.trigger = {
kind: "Schedule",
settings: {
cron: "@daily",
}
};
}
}
export class Rule {
isDisabled: boolean;
id: number;
priority: number;
action: string;
template: string;
params: object;
tag_selectors: Array<Selector>;
scope_selectors: {
repository: Array<Selector>;
};
constructor() {
this.action = "retain";
this.params = {};
this.scope_selectors = {
repository: [
{
kind: 'doublestar',
decoration: 'repoMatches',
pattern: '**'
}
]
};
this.tag_selectors = [
{
kind: 'doublestar',
decoration: 'matches',
pattern: '**'
},
{
kind: 'label',
decoration: "withLabels",
pattern: null
}
];
}
}
export class Selector {
kind: string;
decoration: string;
pattern: string;
}
export class Param {
type: string;
unit: string;
required: boolean;
}
export class Template {
rule_template: string;
display_text: string;
action: "retain";
params: Array<Param>;
}
export class SelectorRuleMetadate {
display_text: string;
kind: string;
decorations: Array<string>;
}
export class RuleMetadate {
templates: Array<Template>;
scope_selectors: Array<SelectorRuleMetadate>;
tag_selectors: Array<SelectorRuleMetadate>;
constructor() {
this.templates = [];
this.scope_selectors = [
{
display_text: null,
kind: null,
decorations: []
}
];
this.tag_selectors = [
{
display_text: null,
kind: null,
decorations: []
},
{
display_text: null,
kind: null,
decorations: []
}
];
}
}

View File

@ -0,0 +1,180 @@
<div class="clr-row pt-1 fw8">
<div class="clr-col-2">
<label class="fw9-l">{{'TAG_RETENTION.RETENTION_RULES' | translate}}</label><span
class="badge badge-3 clr-offset-1">{{retention?.rules?.length ? retention?.rules?.length : 0}}</span>
</div>
</div>
<div class="clr-row pt-1">
<div class="clr-col">
<ul *ngIf="retention?.rules?.length > 0" class="list-unstyled">
<li class="rule" *ngFor="let rule of retention?.rules;let i = index;">
<div class="clr-row">
<div class="clr-col rule-name">
<span>
<span>{{getI18nKey(rule?.action)|translate}}</span>
<span>{{getI18nKey(rule?.template)|translate:{number: rule?.params[rule?.template] } }}</span>
<span class="color-97">{{'TAG_RETENTION.WITH_CONDITION' | translate}}</span>
<span>{{'TAG_RETENTION.REPO' | translate}}</span>
<span>{{getI18nKey(rule?.scope_selectors?.repository[0]?.decoration)|translate}}</span>
<span>{{formatPattern(rule?.scope_selectors?.repository[0]?.pattern)}}</span>
<span class="color-97 space">{{'TAG_RETENTION.AND' | translate}}</span>
<span>{{'TAG_RETENTION.LOWER_TAGS' | translate}}</span>
<span>{{getI18nKey(rule?.tag_selectors[0]?.decoration)|translate}}</span>
<span>{{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 class="clr-col-1">
<clr-icon (click)="openEditor(i)" shape="ellipsis-vertical"
class="dropdown-toggle hand"></clr-icon>
<div class="dropdown" [ngClass]="{open:ruleIndex===i}">
<div class="dropdown-menu">
<button type="button" class="dropdown-item"
(click)="editRuleByIndex(i)">{{'TAG_RETENTION.EDIT' | translate}}</button>
<button type="button" class="dropdown-item"
(click)="deleteRule(i)">{{'TAG_RETENTION.DELETE' | translate}}</button>
</div>
</div>
</div>
</div>
</li>
</ul>
<div class="clr-row pt-1">
<div class="clr-col-5 color-97">
<div>
<span>{{'TAG_RETENTION.ADD_RULE_HELP_1' | translate}}</span>
</div>
<div>
<span>{{'TAG_RETENTION.ADD_RULE_HELP_2' | translate}}</span>
</div>
</div>
<div class="clr-col">
<button class="btn btn-link" (click)="openAddRule()">{{'TAG_RETENTION.ADD_RULE' | translate}}</button>
</div>
</div>
</div>
</div>
<div class="clr-row pt-1">
<div class="clr-col-2 pt-2"><label class="fw9-l">{{'TAG_RETENTION.RETENTION_RUNS' | translate}}</label></div>
<div class="clr-col-10">
<clr-dg-action-bar>
<button [disabled]="!(retention?.rules?.length > 0)" class="btn btn-outline"
(click)="isRetentionRunOpened=true">
<clr-icon shape="play"></clr-icon>
<span class="ml-5">{{'TAG_RETENTION.RUN_NOW' | translate}}</span></button>
<button [disabled]="!(retention?.rules?.length > 0)" class="btn btn-outline"
(click)="whatIfRun()">{{'TAG_RETENTION.WHAT_IF_RUN' | translate}}</button>
<button [disabled]="!(selectedItem && (selectedItem.status ==='InProgress' || selectedItem.status ==='Running'))"
class="btn btn-outline" (click)="abortRun()">{{'TAG_RETENTION.ABORT' | translate}}</button>
<button [disabled]="!retentionId" class="btn btn-outline"
(click)="refreshList()">
<clr-icon shape="refresh"></clr-icon>
</button>
</clr-dg-action-bar>
<clr-datagrid [clrDgLoading]="loadingExecutions" [(clrDgSingleSelected)]="selectedItem">
<clr-dg-column>
{{'TAG_RETENTION.SERIAL' | translate}}
<clr-dg-string-filter [clrDgStringFilter]="serialFilter"></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column>
{{'TAG_RETENTION.STATUS' | translate}}
<clr-dg-string-filter [clrDgStringFilter]="statusFilter"></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column>
{{'TAG_RETENTION.DRY_RUN' | translate}}
<clr-dg-string-filter [clrDgStringFilter]="dryRunFilter"></clr-dg-string-filter>
</clr-dg-column>
<clr-dg-column>
{{'TAG_RETENTION.START_TIME' | translate}}
</clr-dg-column>
<clr-dg-column>
{{'TAG_RETENTION.DURATION' | translate}}
</clr-dg-column>
<clr-dg-placeholder>
{{'TAG_RETENTION.NO_EXECUTION' | translate}}
</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let execution of executionList;let i = index;" [clrDgItem]="execution">
<clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">
<clr-icon shape="angle" [dir]="index===i?'down':'right'"></clr-icon>
<span class="ml-1">{{execution.id}}</span>
</clr-dg-cell>
<clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">{{execution.status}}</clr-dg-cell>
<clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.dry_run ? 'YES' : 'NO'}}</clr-dg-cell>
<clr-dg-cell class="hand"
(click)="openDetail(i,execution.id)">{{execution.start_time|date:'short'}}</clr-dg-cell>
<clr-dg-cell class="hand" (click)="openDetail(i,execution.id)">{{execution.duration}}</clr-dg-cell>
<clr-dg-row-detail *ngIf="index===i">
<clr-datagrid [clrDgLoading]="loadingHistories" class="w-100">
<clr-dg-column>{{'TAG_RETENTION.REPOSITORY' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG_RETENTION.STATUS' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG_RETENTION.START_TIME' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG_RETENTION.DURATION' | translate}}</clr-dg-column>
<clr-dg-column>{{'TAG_RETENTION.LOG' | translate}}</clr-dg-column>
<clr-dg-placeholder>
{{'TAG_RETENTION.NO_HISTORY' | translate}}
</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let task of historyList" [clrDgItem]="task">
<clr-dg-cell>{{task.repository}}</clr-dg-cell>
<clr-dg-cell>{{task.status}}</clr-dg-cell>
<clr-dg-cell>{{task.start_time|date:'short'}}</clr-dg-cell>
<clr-dg-cell>{{task.duration}}</clr-dg-cell>
<clr-dg-cell><span (click)="seeLog(task.execution_id,task.id)"
class="hand color-79b">{{'TAG_RETENTION.LOG' | translate}}</span>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="innerPagination.totalItems">{{innerPagination.firstItem + 1}}
-
{{innerPagination.lastItem + 1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span>
{{innerPagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
<clr-dg-pagination #innerPagination [clrDgPageSize]="5"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</clr-dg-row-detail>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}}
-
{{pagination.lastItem + 1 }} {{'ROBOT_ACCOUNT.OF' |
translate}} </span>
{{pagination.totalItems }} {{'ROBOT_ACCOUNT.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<add-rule #addRule (clickAdd)="clickAdd($event)"></add-rule>
<clr-modal [(clrModalOpen)]="isRetentionRunOpened"
[clrModalStaticBackdrop]="true" [clrModalClosable]="true">
<h3 class="modal-title">{{'TAG_RETENTION.RETENTION_RUN' | translate}}</h3>
<div class="modal-body">
<p class="color-97">
{{'TAG_RETENTION.RETENTION_RUN_EXPLAIN' | translate}}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline"
(click)="isRetentionRunOpened=false">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="runRetention()">{{'BUTTON.RUN' | translate}}</button>
</div>
</clr-modal>
<clr-modal [(clrModalOpen)]="isAbortedOpened"
[clrModalStaticBackdrop]="true" [clrModalClosable]="true">
<h3 class="modal-title">{{'TAG_RETENTION.RETENTION_RUN_ABORTED' | translate}}</h3>
<div class="modal-body">
<p class="color-97">{{'TAG_RETENTION.RETENTION_RUN_ABORTED_EXPLAIN' | translate}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary"
(click)="abortRetention()">{{'BUTTON.OK' | translate}}</button>
</div>
</clr-modal>
<div class="backdrop-transparent" (click)="ruleIndex = -1" *ngIf="ruleIndex !== -1"></div>

View File

@ -0,0 +1,46 @@
.color-97 {
color: #979797;
}
.rule {
height: 24px;
line-height: 24px;
}
.rule-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 0.6rem;
font-weight: 500;
}
.ml-5 {
margin-left: 5px;
}
.width-60 {
width: 60px;
}
.hand {
cursor: pointer;
}
.datagrid-container {
padding: 0;
}
.color-79b {
color: #0079b8;
}
.backdrop-transparent {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
opacity: 0;
z-index: 999;
}

View File

@ -0,0 +1,290 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AddRuleComponent } from "./add-rule/add-rule.component";
import { ClrDatagridStringFilterInterface } from "@clr/angular";
import { TagRetentionService } from "./tag-retention.service";
import { Retention, Rule } from "./retention";
import { Project } from "../project";
import { clone, ErrorHandler } from "@harbor/ui";
const MIN = 60000;
const SEC = 1000;
const MIN_STR = "min";
const SEC_STR = "sec";
@Component({
selector: 'tag-retention',
templateUrl: './tag-retention.component.html',
styleUrls: ['./tag-retention.component.scss']
})
export class TagRetentionComponent implements OnInit {
serialFilter: ClrDatagridStringFilterInterface<any> = {
accepts(item: any, search: string): boolean {
return item.id.toString().indexOf(search) !== -1;
}
};
statusFilter: ClrDatagridStringFilterInterface<any> = {
accepts(item: any, search: string): boolean {
return item.status.toLowerCase().indexOf(search.toLowerCase()) !== -1;
}
};
dryRunFilter: ClrDatagridStringFilterInterface<any> = {
accepts(item: any, search: string): boolean {
let str = item.dry_run ? 'YES' : 'NO';
return str.indexOf(search) !== -1;
}
};
projectId: number;
addRuleOpened: boolean = false;
isRetentionRunOpened: boolean = false;
isAbortedOpened: boolean = false;
selectedItem: any;
ruleIndex: number = -1;
index: number = -1;
retentionId: number;
retention: Retention = new Retention();
editIndex: number;
executionList = [];
historyList = [];
loadingExecutions: boolean = false;
loadingHistories: boolean = false;
dryRun: boolean = false;
@ViewChild('addRule') addRuleComponent: AddRuleComponent;
constructor(
private route: ActivatedRoute,
private tagRetentionService: TagRetentionService,
private errorHandler: ErrorHandler,
) {
}
ngOnInit() {
this.projectId = +this.route.snapshot.parent.params['id'];
this.retention.scope = {
level: "project",
ref: this.projectId
};
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
let project = <Project>resolverData["projectResolver"];
if (project.metadata && project.metadata.retention_id) {
this.retentionId = project.metadata.retention_id;
}
}
this.getRetention();
this.getMetadata();
this.refreshList();
}
getMetadata() {
this.tagRetentionService.getRetentionMetadata().subscribe(
response => {
this.addRuleComponent.metadata = response;
}, error => {
this.errorHandler.error(error);
});
}
getRetention() {
if (this.retentionId) {
this.tagRetentionService.getRetention(this.retentionId).subscribe(
response => {
this.retention = response;
}, error => {
this.errorHandler.error(error);
});
}
}
editRuleByIndex(index) {
this.editIndex = index;
this.addRuleComponent.rule = clone(this.retention.rules[index]);
this.addRuleComponent.editRuleOrigin = clone(this.retention.rules[index]);
this.addRuleComponent.open();
this.addRuleComponent.isAdd = false;
this.ruleIndex = -1;
}
deleteRule(index) {
let retention: Retention = clone(this.retention);
retention.rules.splice(index, 1);
this.ruleIndex = -1;
this.tagRetentionService.updateRetention(this.retentionId, retention).subscribe(
response => {
this.retention = retention;
}, error => {
this.errorHandler.error(error);
});
}
openAddRule() {
this.addRuleComponent.open();
this.addRuleComponent.isAdd = true;
this.addRuleComponent.rule = new Rule();
}
runRetention() {
this.isRetentionRunOpened = false;
this.tagRetentionService.runNowTrigger(this.retentionId).subscribe(
response => {
this.refreshList();
}, error => {
this.errorHandler.error(error);
});
}
whatIfRun() {
this.tagRetentionService.whatIfRunTrigger(this.retentionId).subscribe(
response => {
this.refreshList();
}, error => {
this.errorHandler.error(error);
});
}
refreshList() {
this.index = -1 ;
if (this.retentionId) {
this.loadingExecutions = true;
this.tagRetentionService.getRunNowList(this.retentionId).subscribe(
res => {
this.loadingExecutions = false;
this.executionList = res;
TagRetentionComponent.calculateDuration(this.executionList);
}, error => {
this.loadingExecutions = false;
this.errorHandler.error(error);
});
}
}
static calculateDuration(arr: Array<any>) {
if (arr && arr.length > 0) {
for (let i = 0; i < arr.length; i++) {
let duration = new Date(arr[i].end_time).getTime() - new Date(arr[i].start_time).getTime();
let min = Math.floor(duration / MIN);
let sec = Math.floor((duration % MIN) / SEC);
arr[i]['duration'] = "";
if ((min || sec) && duration > 0) {
if (min) {
arr[i]['duration'] += '' + min + MIN_STR;
}
if (sec) {
arr[i]['duration'] += '' + sec + SEC_STR;
}
} else if ( min === 0 && sec === 0 && duration > 0) {
arr[i]['duration'] = "0";
} else {
arr[i]['duration'] = "N/A";
}
}
}
}
abortRun() {
this.isAbortedOpened = true;
this.tagRetentionService.AbortRun(this.retentionId, this.selectedItem.id).subscribe(
res => {
this.refreshList();
}, error => {
this.errorHandler.error(error);
});
}
abortRetention() {
this.isAbortedOpened = false;
}
openEditor(index) {
if (this.ruleIndex !== index) {
this.ruleIndex = index;
} else {
this.ruleIndex = -1;
}
}
openDetail(index, executionId) {
if (this.index !== index) {
this.index = index;
this.historyList = [];
this.loadingHistories = true;
this.tagRetentionService.getExecutionHistory(this.retentionId, executionId).subscribe(
res => {
this.loadingHistories = false;
this.historyList = res;
TagRetentionComponent.calculateDuration(this.historyList);
}, error => {
this.loadingHistories = false;
this.errorHandler.error(error);
});
} else {
this.index = -1;
}
}
refreshAfterCreatRetention() {
this.tagRetentionService.getProjectInfo(this.projectId).subscribe(
response => {
this.retentionId = response.metadata.retention_id;
this.getRetention();
}, error => {
this.errorHandler.error(error);
});
}
clickAdd(rule) {
if (this.addRuleComponent.isAdd) {
let retention: Retention = clone(this.retention);
retention.rules.push(rule);
if (!this.retentionId) {
this.tagRetentionService.createRetention(retention).subscribe(
response => {
this.refreshAfterCreatRetention();
}, error => {
this.errorHandler.error(error);
});
} else {
this.tagRetentionService.updateRetention(this.retentionId, retention).subscribe(
response => {
this.retention = retention;
}, error => {
this.errorHandler.error(error);
});
}
} else {
let retention: Retention = clone(this.retention);
retention.rules[this.editIndex] = rule;
this.tagRetentionService.updateRetention(this.retentionId, retention).subscribe(
response => {
this.retention = retention;
}, error => {
this.errorHandler.error(error);
});
}
}
seeLog(executionId, taskId) {
this.tagRetentionService.seeLog(this.retentionId, executionId, taskId);
}
formatPattern(pattern: string): string {
return pattern.replace(/[{}]/g, "");
}
getI18nKey(str: string) {
return this.tagRetentionService.getI18nKey(str);
}
}

View File

@ -0,0 +1,114 @@
// Copyright Project Harbor Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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.
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Retention, RuleMetadate } from "./retention";
import { Observable, throwError as observableThrowError } from "rxjs";
import { map, catchError } from "rxjs/operators";
import { Project } from "../project";
@Injectable()
export class TagRetentionService {
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",
"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",
"repoMatches": "MAT",
"repoExcludes": "EXC",
"matches": "MAT",
"excludes": "EXC",
"withLabels": "WITH",
"withoutLabels": "WITHOUT",
"COUNT": "UNIT_COUNT",
"DAYS": "UNIT_DAY",
};
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)));
}
getRetention(retentionId): Observable<Retention> {
return this.http.get(`/api/retentions/${retentionId}`)
.pipe(map(response => response as Retention))
.pipe(catchError(error => observableThrowError(error)));
}
createRetention(retention: Retention) {
return this.http.post(`/api/retentions`, retention)
.pipe(catchError(error => observableThrowError(error)));
}
updateRetention(retentionId, retention: Retention) {
return this.http.put(`/api/retentions/${retentionId}`, retention)
.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)));
}
runNowTrigger(retentionId) {
return this.http.post(`/api/retentions/${retentionId}/executions`, {dry_run: false})
.pipe(catchError(error => observableThrowError(error)));
}
whatIfRunTrigger(retentionId) {
return this.http.post(`/api/retentions/${retentionId}/executions`, {dry_run: true})
.pipe(catchError(error => observableThrowError(error)));
}
AbortRun(retentionId, executionId) {
return this.http.patch(`/api/retentions/${retentionId}/executions/${executionId}`, {action: 'stop'})
.pipe(catchError(error => observableThrowError(error)));
}
getRunNowList(retentionId) {
return this.http.get(`/api/retentions/${retentionId}/executions`)
.pipe(map(response => response as Array<any>))
.pipe(catchError(error => observableThrowError(error)));
}
getExecutionHistory(retentionId, executionId) {
return this.http.get(`/api/retentions/${retentionId}/executions/${executionId}/tasks`)
.pipe(map(response => response as Array<any>))
.pipe(catchError(error => observableThrowError(error)));
}
seeLog(retentionId, executionId, taskId) {
window.open(`api/retentions/${retentionId}/executions/${executionId}/tasks/${taskId}`, '_blank');
}
}

View File

@ -47,9 +47,10 @@ export class UserService {
, catchError(error => this.handleError(error)));
}
getUsers(): Observable<User[]> {
return this.http.get(userMgmtEndpoint, HTTP_GET_OPTIONS)
.pipe(map(response => response as User[])
, catchError(error => this.handleError(error)));
return this.http.get(userMgmtEndpoint)
.pipe(map(((response: any) => {
return response as User[];
}), catchError(error => this.handleError(error))));
}
// Add new user

View File

@ -43,7 +43,9 @@
"ACTIONS": "Actions",
"BROWSE": "Browse",
"UPLOAD": "Upload",
"NO_FILE": "No file selected"
"NO_FILE": "No file selected",
"ADD": "ADD",
"RUN": "RUN"
},
"BATCH": {
"DELETED_SUCCESS": "Deleted successfully",
@ -1090,6 +1092,76 @@
"SYS_WHITELIST": "System whitelist",
"PRO_WHITELIST": "Project whitelist",
"ADD_SYSTEM": "ADD SYSTEM"
},
"TAG_RETENTION": {
"TAG_RETENTION": "Tag Retention Policy",
"RETENTION_RULES": "Retention rules",
"RULE_NAME_1": " the images from the last {{number}} days",
"RULE_NAME_2": " the most recent active {{number}} images",
"RULE_NAME_3": " the most recently pushed {{number}} images",
"RULE_NAME_4": " the most recently pulled {{number}} images",
"RULE_NAME_5": " always",
"ADD_RULE": "ADD RULE",
"ADD_RULE_HELP_1": "Click the ADD RULE button to add a rule.",
"ADD_RULE_HELP_2": "Tag retention polices run once a day.",
"RETENTION_RUNS": "Retention runs",
"RUN_NOW": "RUN NOW",
"WHAT_IF_RUN": "DRY RUN",
"ABORT": "ABORT",
"SERIAL": "ID",
"STATUS": "Status",
"DRY_RUN": "Dry Run",
"START_TIME": "Start Time",
"DURATION": "Duration",
"DETAILS": "Details",
"REPOSITORY": "Repository",
"EDIT": "Edit",
"DISABLE": "Disable",
"ENABLE": "Enable",
"DELETE": "Delete",
"ADD_TITLE": "Add Tag Retention Rule",
"ADD_SUBTITLE": "Specify a tag retention rule for this project. All tag retention rules are independently calculated and each rule can be applied to a selected list of repositories.",
"BY_WHAT": "By image count or number of days",
"RULE_TEMPLATE_1": "the images from the last # days",
"RULE_TEMPLATE_2": "the most recent active # images",
"RULE_TEMPLATE_3": "the most recently pushed # images",
"RULE_TEMPLATE_4": "the most recently pulled # images",
"RULE_TEMPLATE_5": "always",
"ACTION_RETAIN": "Retain",
"UNIT_DAY": "DAYS",
"UNIT_COUNT": "COUNT",
"NUMBER": "NUMBER",
"IN_REPOSITORIES": "Repositories",
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
"TAGS": "Tags",
"MATCHES_TAGS": "Matches tags",
"MATCHES_EXCEPT_TAGS": "Matches except tags",
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,**,or regex",
"LABELS": "Labels",
"MATCHES_LABELS": "Matches Labels",
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
"REP_LABELS": "Enter multiple comma separated labels",
"RETENTION_RUN": "Retention Run",
"RETENTION_RUN_EXPLAIN": "Executing the retention policy can have adverse effects to the images in this project and affected image tags will be deleted. Press CANCEL and use a DRY RUN to simulate the effect of this policy. Otherwise press RUN to proceed.",
"RETENTION_RUN_ABORTED": "Retention Run Aborted",
"RETENTION_RUN_ABORTED_EXPLAIN": "This retention run has been aborted. Images already deleted are irreversible. You can initiate another run to continue to delete images. In order to simulate a run, you can use the “DRY RUN”.",
"LOADING": "Loading...",
"NO_EXECUTION": "We couldn't find any executions!",
"NO_HISTORY": "We couldn't find any histories!",
"DELETION": "Deletions",
"EDIT_TITLE": "Edit Tag Retention Rule",
"LOG": "Log",
"EXCLUDES": "Excludes",
"MATCHES": "Matches",
"REPO": " repositories",
"EXC": " excluding ",
"MAT": " matching ",
"AND": " and",
"WITH": " with ",
"WITHOUT": " without ",
"LOWER_LABELS": " labels",
"WITH_CONDITION": " with",
"LOWER_TAGS": " tags"
}
}

View File

@ -43,7 +43,9 @@
"ACTIONS": "Actions",
"BROWSE": "Browse",
"UPLOAD": "Upload",
"NO_FILE": "No file selected"
"NO_FILE": "No file selected",
"ADD": "ADD",
"RUN": "RUN"
},
"BATCH": {
"DELETED_SUCCESS": "Deleted successfully",
@ -1088,6 +1090,76 @@
"SYS_WHITELIST": "System whitelist",
"PRO_WHITELIST": "Project whitelist",
"ADD_SYSTEM": "ADD SYSTEM"
},
"TAG_RETENTION": {
"TAG_RETENTION": "Tag Retention Policy",
"RETENTION_RULES": "Retention rules",
"RULE_NAME_1": " the images from the last {{number}} days",
"RULE_NAME_2": " the most recent active {{number}} images",
"RULE_NAME_3": " the most recently pushed {{number}} images",
"RULE_NAME_4": " the most recently pulled {{number}} images",
"RULE_NAME_5": " always",
"ADD_RULE": "ADD RULE",
"ADD_RULE_HELP_1": "Click the ADD RULE button to add a rule.",
"ADD_RULE_HELP_2": "Tag retention polices run once a day.",
"RETENTION_RUNS": "Retention runs",
"RUN_NOW": "RUN NOW",
"WHAT_IF_RUN": "DRY RUN",
"ABORT": "ABORT",
"SERIAL": "ID",
"STATUS": "Status",
"DRY_RUN": "Dry Run",
"START_TIME": "Start Time",
"DURATION": "Duration",
"DETAILS": "Details",
"REPOSITORY": "Repository",
"EDIT": "Edit",
"DISABLE": "Disable",
"ENABLE": "Enable",
"DELETE": "Delete",
"ADD_TITLE": "Add Tag Retention Rule",
"ADD_SUBTITLE": "Specify a tag retention rule for this project. All tag retention rules are independently calculated and each rule can be applied to a selected list of repositories.",
"BY_WHAT": "By image count or number of days",
"RULE_TEMPLATE_1": "the images from the last # days",
"RULE_TEMPLATE_2": "the most recent active # images",
"RULE_TEMPLATE_3": "the most recently pushed # images",
"RULE_TEMPLATE_4": "the most recently pulled # images",
"RULE_TEMPLATE_5": "always",
"ACTION_RETAIN": "Retain",
"UNIT_DAY": "DAYS",
"UNIT_COUNT": "COUNT",
"NUMBER": "NUMBER",
"IN_REPOSITORIES": "Repositories",
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
"TAGS": "Tags",
"MATCHES_TAGS": "Matches tags",
"MATCHES_EXCEPT_TAGS": "Matches except tags",
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,**,or regex",
"LABELS": "Labels",
"MATCHES_LABELS": "Matches Labels",
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
"REP_LABELS": "Enter multiple comma separated labels",
"RETENTION_RUN": "Retention Run",
"RETENTION_RUN_EXPLAIN": "Executing the retention policy can have adverse effects to the images in this project and affected image tags will be deleted. Press CANCEL and use a DRY RUN to simulate the effect of this policy. Otherwise press RUN to proceed.",
"RETENTION_RUN_ABORTED": "Retention Run Aborted",
"RETENTION_RUN_ABORTED_EXPLAIN": "This retention run has been aborted. Images already deleted are irreversible. You can initiate another run to continue to delete images. In order to simulate a run, you can use the “DRY RUN”.",
"LOADING": "Loading...",
"NO_EXECUTION": "We couldn't find any executions!",
"NO_HISTORY": "We couldn't find any histories!",
"DELETION": "Deletions",
"EDIT_TITLE": "Edit Tag Retention Rule",
"LOG": "Log",
"EXCLUDES": "Excludes",
"MATCHES": "Matches",
"REPO": " repositories",
"EXC": " excluding ",
"MAT": " matching ",
"AND": " and",
"WITH": " with ",
"WITHOUT": " without ",
"LOWER_LABELS": " labels",
"WITH_CONDITION": " with",
"LOWER_TAGS": " tags"
}
}

View File

@ -40,7 +40,9 @@
"ACTIONS": "Actions",
"BROWSE": "Browse",
"UPLOAD": "Upload",
"NO_FILE": "No file selected"
"NO_FILE": "No file selected",
"ADD": "ADD",
"RUN": "RUN"
},
"BATCH": {
"DELETED_SUCCESS": "Deleted successfully",
@ -1060,6 +1062,76 @@
"SYS_WHITELIST": "System whitelist",
"PRO_WHITELIST": "Project whitelist",
"ADD_SYSTEM": "ADD SYSTEM"
},
"TAG_RETENTION": {
"TAG_RETENTION": "Tag Retention Policy",
"RETENTION_RULES": "Retention rules",
"RULE_NAME_1": " the images from the last {{number}} days",
"RULE_NAME_2": " the most recent active {{number}} images",
"RULE_NAME_3": " the most recently pushed {{number}} images",
"RULE_NAME_4": " the most recently pulled {{number}} images",
"RULE_NAME_5": " always",
"ADD_RULE": "ADD RULE",
"ADD_RULE_HELP_1": "Click the ADD RULE button to add a rule.",
"ADD_RULE_HELP_2": "Tag retention polices run once a day.",
"RETENTION_RUNS": "Retention runs",
"RUN_NOW": "RUN NOW",
"WHAT_IF_RUN": "DRY RUN",
"ABORT": "ABORT",
"SERIAL": "ID",
"STATUS": "Status",
"DRY_RUN": "Dry Run",
"START_TIME": "Start Time",
"DURATION": "Duration",
"DETAILS": "Details",
"REPOSITORY": "Repository",
"EDIT": "Edit",
"DISABLE": "Disable",
"ENABLE": "Enable",
"DELETE": "Delete",
"ADD_TITLE": "Add Tag Retention Rule",
"ADD_SUBTITLE": "Specify a tag retention rule for this project. All tag retention rules are independently calculated and each rule can be applied to a selected list of repositories.",
"BY_WHAT": "By image count or number of days",
"RULE_TEMPLATE_1": "the images from the last # days",
"RULE_TEMPLATE_2": "the most recent active # images",
"RULE_TEMPLATE_3": "the most recently pushed # images",
"RULE_TEMPLATE_4": "the most recently pulled # images",
"RULE_TEMPLATE_5": "always",
"ACTION_RETAIN": "Retain",
"UNIT_DAY": "DAYS",
"UNIT_COUNT": "COUNT",
"NUMBER": "NUMBER",
"IN_REPOSITORIES": "Repositories",
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
"TAGS": "Tags",
"MATCHES_TAGS": "Matches tags",
"MATCHES_EXCEPT_TAGS": "Matches except tags",
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,**,or regex",
"LABELS": "Labels",
"MATCHES_LABELS": "Matches Labels",
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
"REP_LABELS": "Enter multiple comma separated labels",
"RETENTION_RUN": "Retention Run",
"RETENTION_RUN_EXPLAIN": "Executing the retention policy can have adverse effects to the images in this project and affected image tags will be deleted. Press CANCEL and use a DRY RUN to simulate the effect of this policy. Otherwise press RUN to proceed.",
"RETENTION_RUN_ABORTED": "Retention Run Aborted",
"RETENTION_RUN_ABORTED_EXPLAIN": "This retention run has been aborted. Images already deleted are irreversible. You can initiate another run to continue to delete images. In order to simulate a run, you can use the “DRY RUN”.",
"LOADING": "Loading...",
"NO_EXECUTION": "We couldn't find any executions!",
"NO_HISTORY": "We couldn't find any histories!",
"DELETION": "Deletions",
"EDIT_TITLE": "Edit Tag Retention Rule",
"LOG": "Log",
"EXCLUDES": "Excludes",
"MATCHES": "Matches",
"REPO": " repositories",
"EXC": " excluding ",
"MAT": " matching ",
"AND": " and",
"WITH": " with ",
"WITHOUT": " without ",
"LOWER_LABELS": " labels",
"WITH_CONDITION": " with",
"LOWER_TAGS": " tags"
}
}

View File

@ -43,7 +43,9 @@
"ACTIONS": "Ações",
"BROWSE": "Navegar",
"UPLOAD": "Upload",
"NO_FILE": "Nenhum arquivo selecionado"
"NO_FILE": "Nenhum arquivo selecionado",
"ADD": "ADD",
"RUN": "RUN"
},
"BATCH": {
"DELETED_SUCCESS": "Removido com sucesso",
@ -1085,6 +1087,76 @@
"SYS_WHITELIST": "System whitelist",
"PRO_WHITELIST": "Project whitelist",
"ADD_SYSTEM": "ADD SYSTEM"
},
"TAG_RETENTION": {
"TAG_RETENTION": "Tag Retention Policy",
"RETENTION_RULES": "Retention rules",
"RULE_NAME_1": " the images from the last {{number}} days",
"RULE_NAME_2": " the most recent active {{number}} images",
"RULE_NAME_3": " the most recently pushed {{number}} images",
"RULE_NAME_4": " the most recently pulled {{number}} images",
"RULE_NAME_5": " always",
"ADD_RULE": "ADD RULE",
"ADD_RULE_HELP_1": "Click the ADD RULE button to add a rule.",
"ADD_RULE_HELP_2": "Tag retention polices run once a day.",
"RETENTION_RUNS": "Retention runs",
"RUN_NOW": "RUN NOW",
"WHAT_IF_RUN": "DRY RUN",
"ABORT": "ABORT",
"SERIAL": "ID",
"STATUS": "Status",
"DRY_RUN": "Dry Run",
"START_TIME": "Start Time",
"DURATION": "Duration",
"DETAILS": "Details",
"REPOSITORY": "Repository",
"EDIT": "Edit",
"DISABLE": "Disable",
"ENABLE": "Enable",
"DELETE": "Delete",
"ADD_TITLE": "Add Tag Retention Rule",
"ADD_SUBTITLE": "Specify a tag retention rule for this project. All tag retention rules are independently calculated and each rule can be applied to a selected list of repositories.",
"BY_WHAT": "By image count or number of days",
"RULE_TEMPLATE_1": "the images from the last # days",
"RULE_TEMPLATE_2": "the most recent active # images",
"RULE_TEMPLATE_3": "the most recently pushed # images",
"RULE_TEMPLATE_4": "the most recently pulled # images",
"RULE_TEMPLATE_5": "always",
"ACTION_RETAIN": "Retain",
"UNIT_DAY": "DAYS",
"UNIT_COUNT": "COUNT",
"NUMBER": "NUMBER",
"IN_REPOSITORIES": "Repositories",
"REP_SEPARATOR": "Enter multiple comma separated repos,repo*,or **",
"TAGS": "Tags",
"MATCHES_TAGS": "Matches tags",
"MATCHES_EXCEPT_TAGS": "Matches except tags",
"TAG_SEPARATOR": "Enter multiple comma separated tags,tag*,**,or regex",
"LABELS": "Labels",
"MATCHES_LABELS": "Matches Labels",
"MATCHES_EXCEPT_LABELS": "Matches except Labels",
"REP_LABELS": "Enter multiple comma separated labels",
"RETENTION_RUN": "Retention Run",
"RETENTION_RUN_EXPLAIN": "Executing the retention policy can have adverse effects to the images in this project and affected image tags will be deleted. Press CANCEL and use a DRY RUN to simulate the effect of this policy. Otherwise press RUN to proceed.",
"RETENTION_RUN_ABORTED": "Retention Run Aborted",
"RETENTION_RUN_ABORTED_EXPLAIN": "This retention run has been aborted. Images already deleted are irreversible. You can initiate another run to continue to delete images. In order to simulate a run, you can use the “DRY RUN”.",
"LOADING": "Loading...",
"NO_EXECUTION": "We couldn't find any executions!",
"NO_HISTORY": "We couldn't find any histories!",
"DELETION": "Deletions",
"EDIT_TITLE": "Edit Tag Retention Rule",
"LOG": "Log",
"EXCLUDES": "Excludes",
"MATCHES": "Matches",
"REPO": " repositories",
"EXC": " excluding ",
"MAT": " matching ",
"AND": " and",
"WITH": " with ",
"WITHOUT": " without ",
"LOWER_LABELS": " labels",
"WITH_CONDITION": " with",
"LOWER_TAGS": " tags"
}

View File

@ -43,7 +43,9 @@
"ACTIONS": "操作",
"BROWSE": "选择文件",
"UPLOAD": "上传",
"NO_FILE": "未选择文件"
"NO_FILE": "未选择文件",
"ADD": "添加",
"RUN": "执行"
},
"BATCH": {
"DELETED_SUCCESS": "删除成功",
@ -1086,6 +1088,76 @@
"SYS_WHITELIST": "启用系统白名单",
"PRO_WHITELIST": "启用项目白名单",
"ADD_SYSTEM": "添加系统白名单"
},
"TAG_RETENTION": {
"TAG_RETENTION": "Tag保留策略",
"RETENTION_RULES": "保留规则",
"RULE_NAME_1": "最近{{number}}天的镜像",
"RULE_NAME_2": "最近活跃的{{number}}个镜像",
"RULE_NAME_3": "最近推送的{{number}}个镜像",
"RULE_NAME_4": "最近拉取的{{number}}个镜像",
"RULE_NAME_5": "全部镜像",
"ADD_RULE": "添加规则",
"ADD_RULE_HELP_1": "点击添加按钮可添加规则",
"ADD_RULE_HELP_2": "Tag保留策略每天运行一次.",
"RETENTION_RUNS": "运行保留策略",
"RUN_NOW": "立即运行",
"WHAT_IF_RUN": "模拟运行",
"ABORT": "中止",
"SERIAL": "ID",
"STATUS": "状态",
"DRY_RUN": "模拟运行",
"START_TIME": "开始时间",
"DURATION": "持续时间",
"DETAILS": "详情",
"REPOSITORY": "仓库",
"EDIT": "编辑",
"DISABLE": "禁用",
"ENABLE": "启用",
"DELETE": "删除",
"ADD_TITLE": "添加Tag保留规则",
"ADD_SUBTITLE": "为当前项目指定tag保留规则。所有tag保留规则独立计算并且适用于所有符合条件的仓库。",
"BY_WHAT": "以镜像或天数为条件",
"RULE_TEMPLATE_1": "最近#天的镜像",
"RULE_TEMPLATE_2": "最近活跃的#个镜像",
"RULE_TEMPLATE_3": "最近推送的#个镜像",
"RULE_TEMPLATE_4": "最近拉取的#个镜像",
"RULE_TEMPLATE_5": "全部",
"ACTION_RETAIN": "保留",
"UNIT_DAY": "天数",
"UNIT_COUNT": "个数",
"NUMBER": "数量",
"IN_REPOSITORIES": "仓库",
"REP_SEPARATOR": "使用逗号分隔repos,repo*和**",
"TAGS": "Tags",
"MATCHES_TAGS": "匹配tags",
"MATCHES_EXCEPT_TAGS": "反匹配tags",
"TAG_SEPARATOR": "使用逗号分割tags,tag*,**,or regex",
"LABELS": "标签",
"MATCHES_LABELS": "匹配标签",
"MATCHES_EXCEPT_LABELS": "反匹配标签",
"REP_LABELS": "使用逗号分割标签",
"RETENTION_RUN": "运行保留策略",
"RETENTION_RUN_EXPLAIN": "执行保留策略将对该项目中的镜像产生反向影响受影响的镜像tags将会被删除。您可选择取消或者使用模拟运行或者点击运行以继续。",
"RETENTION_RUN_ABORTED": "中止运行保留策略",
"RETENTION_RUN_ABORTED_EXPLAIN": "已中止运行保留策略,已删除的镜像不可恢复。您可执行另一个运行命令以便继续删除镜像。如需模拟运行,请点击模拟运行按钮。",
"LOADING": "载入中...",
"NO_EXECUTION": "暂无记录!",
"NO_HISTORY": "暂无记录!",
"DELETION": "删除记录",
"EDIT_TITLE": "编辑Tag保留规则",
"LOG": "日志",
"EXCLUDES": "排除",
"MATCHES": "匹配",
"REPO": "仓库",
"EXC": "反匹配",
"MAT": "匹配",
"AND": "且",
"WITH": "有",
"WITHOUT": "没有",
"LOWER_LABELS": "标签",
"WITH_CONDITION": "基于条件",
"LOWER_TAGS": "tags"
}
}