mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-22 23:51:27 +01:00
Add image retag function in portal
Signed-off-by: 陈德 <chende@caicloud.io>
This commit is contained in:
parent
04e9190870
commit
ea8fe8a2c5
@ -29,6 +29,8 @@ import { LABEL_DIRECTIVES } from "./label/index";
|
||||
import { CREATE_EDIT_LABEL_DIRECTIVES } from "./create-edit-label/index";
|
||||
import { LABEL_PIECE_DIRECTIVES } from "./label-piece/index";
|
||||
import { HELMCHART_DIRECTIVE } from "./helm-chart/index";
|
||||
import { IMAGE_NAME_INPUT_DIRECTIVES } from "./image-name-input/index";
|
||||
|
||||
import {
|
||||
SystemInfoService,
|
||||
SystemInfoDefaultService,
|
||||
@ -53,7 +55,9 @@ import {
|
||||
LabelService,
|
||||
LabelDefaultService,
|
||||
HelmChartService,
|
||||
HelmChartDefaultService
|
||||
HelmChartDefaultService,
|
||||
RetagService,
|
||||
RetagDefaultService
|
||||
} from './service/index';
|
||||
import {
|
||||
ErrorHandler,
|
||||
@ -128,6 +132,9 @@ export interface HarborModuleConfig {
|
||||
// Service implementation for tag
|
||||
tagService?: Provider;
|
||||
|
||||
// Service implementation for retag
|
||||
retagService?: Provider;
|
||||
|
||||
// Service implementation for vulnerability scanning
|
||||
scanningService?: Provider;
|
||||
|
||||
@ -192,7 +199,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||
HBR_GRIDVIEW_DIRECTIVES,
|
||||
REPOSITORY_GRIDVIEW_DIRECTIVES,
|
||||
OPERATION_DIRECTIVES,
|
||||
HELMCHART_DIRECTIVE
|
||||
HELMCHART_DIRECTIVE,
|
||||
IMAGE_NAME_INPUT_DIRECTIVES
|
||||
],
|
||||
exports: [
|
||||
LOG_DIRECTIVES,
|
||||
@ -219,7 +227,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||
HBR_GRIDVIEW_DIRECTIVES,
|
||||
REPOSITORY_GRIDVIEW_DIRECTIVES,
|
||||
OPERATION_DIRECTIVES,
|
||||
HELMCHART_DIRECTIVE
|
||||
HELMCHART_DIRECTIVE,
|
||||
IMAGE_NAME_INPUT_DIRECTIVES
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
@ -237,6 +246,7 @@ export class HarborLibraryModule {
|
||||
config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService },
|
||||
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
config.tagService || { provide: TagService, useClass: TagDefaultService },
|
||||
config.retagService || { provide: RetagService, useClass: RetagDefaultService },
|
||||
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
||||
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
|
||||
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
|
||||
@ -269,6 +279,7 @@ export class HarborLibraryModule {
|
||||
config.replicationService || { provide: ReplicationService, useClass: ReplicationDefaultService },
|
||||
config.repositoryService || { provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
config.tagService || { provide: TagService, useClass: TagDefaultService },
|
||||
config.retagService || { provide: RetagService, useClass: RetagDefaultService },
|
||||
config.scanningService || { provide: ScanningResultService, useClass: ScanningResultDefaultService },
|
||||
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
|
||||
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
|
||||
|
@ -0,0 +1,38 @@
|
||||
<form [formGroup]="imageNameForm" class="clr-form clr-form-compact" style="width:100%;">
|
||||
<div class="clr-form-control clr-row">
|
||||
<label for="project-name" class="required clr-control-label clr-col-xs-12 clr-col-md-3">{{ 'PROJECT.NAME' | translate }}</label>
|
||||
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
|
||||
<div class="clr-input-wrapper">
|
||||
<label aria-haspopup="true" role="tooltip" class="wrap-label tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='noProjectInfo'>
|
||||
<input type="text" id="project-name" (keyup)='validateProjectName()' (blur)='validateProjectName()' class="clr-input" formControlName="projectName" required minlength="2" pattern="^[a-z0-9]+(?:[._-][a-z0-9]+)*$" />
|
||||
<span class="tooltip-content">{{noProjectInfo | translate}}</span>
|
||||
</label>
|
||||
<div class="selectBox inputWidth" [style.display]="selectedProjectList.length ? 'block' : 'none'">
|
||||
<ul>
|
||||
<li *ngFor="let project of selectedProjectList" (click)="selectedProjectName(project?.name)">{{project?.name}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control clr-row">
|
||||
<label for="repo-name" class="required clr-control-label clr-col-xs-12 clr-col-md-3">{{ 'REPOSITORY.REPO_NAME' | translate }}</label>
|
||||
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
|
||||
<div class="clr-input-wrapper">
|
||||
<label aria-haspopup="true" role="tooltip" class="wrap-label tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='repoName.invalid'>
|
||||
<input type="text" id="repo-name" class="clr-input" formControlName="repoName" required />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-form-control clr-row">
|
||||
<label for="tag-name" class="required clr-control-label clr-col-xs-12 clr-col-md-3">{{ 'REPOSITORY.TAG' | translate }}</label>
|
||||
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
|
||||
<div class="clr-input-wrapper">
|
||||
<label aria-haspopup="true" role="tooltip" class="wrap-label tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='tagName.invalid'>
|
||||
<input type="text" id="tag-name" class="clr-input" formControlName="tagName" required />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -0,0 +1,46 @@
|
||||
.selectBox {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
border: 1px solid #ccc;
|
||||
background-color: white;
|
||||
border: 1px solid rgba(0, 0, 0, .15);
|
||||
border-right-width: 2px;
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, .2);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.selectBox ul li {
|
||||
list-style: none;
|
||||
padding: 3px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.selectBox ul li:hover {
|
||||
color: #262626;
|
||||
background-image: linear-gradient(180deg, #f5f5f5 0, #e8e8e8);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.clr-input-wrapper {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wrap-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wrap-label input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label.required:after {
|
||||
content: '*';
|
||||
font-size: .58479532rem;
|
||||
line-height: .5rem;
|
||||
color: #c92100;
|
||||
margin-left: .25rem;
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
import {Component, OnDestroy, OnInit} from "@angular/core";
|
||||
import {Project} from "../project-policy-config/project";
|
||||
import {Subject} from "rxjs/index";
|
||||
import {debounceTime, distinctUntilChanged} from "rxjs/operators";
|
||||
import {toPromise} from "../utils";
|
||||
import {ProjectService} from "../service/project.service";
|
||||
import {AbstractControl, FormBuilder, FormGroup, Validators} from "@angular/forms";
|
||||
import {ErrorHandler} from "../error-handler/error-handler";
|
||||
|
||||
@Component({
|
||||
selector: "hbr-image-name-input",
|
||||
templateUrl: "./image-name-input.component.html",
|
||||
styleUrls: ["./image-name-input.component.scss"]
|
||||
})
|
||||
export class ImageNameInputComponent implements OnInit, OnDestroy {
|
||||
noProjectInfo = "";
|
||||
selectedProjectList: Project[] = [];
|
||||
proNameChecker: Subject<string> = new Subject<string>();
|
||||
imageNameForm: FormGroup;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private errorHandler: ErrorHandler,
|
||||
private proService: ProjectService,
|
||||
) {
|
||||
this.imageNameForm = this.fb.group({
|
||||
projectName: ["", Validators.required],
|
||||
repoName: ["", Validators.required],
|
||||
tagName: ["", Validators.required],
|
||||
});
|
||||
}
|
||||
ngOnInit(): void {
|
||||
this.proNameChecker
|
||||
.pipe(debounceTime(500))
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((resp: string) => {
|
||||
let name = this.imageNameForm.controls["projectName"].value;
|
||||
this.noProjectInfo = "";
|
||||
this.selectedProjectList = [];
|
||||
toPromise<Project[]>(this.proService.listProjects(name, undefined))
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
this.selectedProjectList = res.slice(0, 10);
|
||||
// if input project name exist in the project list
|
||||
let exist = res.find((data: any) => data.name === name);
|
||||
if (!exist) {
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
} else {
|
||||
this.noProjectInfo = "";
|
||||
}
|
||||
} else {
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
this.errorHandler.error(error);
|
||||
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
get projectName(): AbstractControl {
|
||||
return this.imageNameForm.get("projectName");
|
||||
}
|
||||
|
||||
get repoName(): AbstractControl {
|
||||
return this.imageNameForm.get("repoName");
|
||||
}
|
||||
|
||||
get tagName(): AbstractControl {
|
||||
return this.imageNameForm.get("tagName");
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.proNameChecker) {
|
||||
this.proNameChecker.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
validateProjectName(): void {
|
||||
let cont = this.imageNameForm.controls["projectName"];
|
||||
if (cont && cont.valid) {
|
||||
this.proNameChecker.next(cont.value);
|
||||
} else {
|
||||
this.noProjectInfo = "PROJECT.NAME_TOOLTIP";
|
||||
}
|
||||
}
|
||||
|
||||
selectedProjectName(projectName: string) {
|
||||
this.imageNameForm.controls["projectName"].setValue(projectName);
|
||||
this.selectedProjectList = [];
|
||||
this.noProjectInfo = "";
|
||||
}
|
||||
}
|
4
src/portal/lib/src/image-name-input/index.ts
Normal file
4
src/portal/lib/src/image-name-input/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Type } from "@angular/core";
|
||||
import { ImageNameInputComponent } from "./image-name-input.component";
|
||||
|
||||
export const IMAGE_NAME_INPUT_DIRECTIVES: Type<any>[] = [ImageNameInputComponent];
|
@ -12,3 +12,4 @@ export * from './job-log.service';
|
||||
export * from './project.service';
|
||||
export * from './label.service';
|
||||
export * from './helm-chart.service';
|
||||
export * from './retag.service';
|
||||
|
@ -390,6 +390,14 @@ export interface HelmChartSignature {
|
||||
* interface Manifest
|
||||
*/
|
||||
export interface Manifest {
|
||||
manifset: Object;
|
||||
config: string;
|
||||
manifset: Object;
|
||||
config: string;
|
||||
}
|
||||
|
||||
export interface RetagRequest {
|
||||
targetProject: string;
|
||||
targetRepo: string;
|
||||
targetTag: string;
|
||||
srcImage: string;
|
||||
override: boolean;
|
||||
}
|
||||
|
55
src/portal/lib/src/service/retag.service.ts
Normal file
55
src/portal/lib/src/service/retag.service.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { Http } from "@angular/http";
|
||||
import { Injectable } from '@angular/core';
|
||||
import { RetagRequest } from "./interface";
|
||||
import { HTTP_JSON_OPTIONS } from "../utils";
|
||||
|
||||
/**
|
||||
* Define the service methods to perform images retag.
|
||||
*
|
||||
**
|
||||
* @abstract
|
||||
* class RetagService
|
||||
*/
|
||||
export abstract class RetagService {
|
||||
/**
|
||||
* Retag an image.
|
||||
*
|
||||
* @abstract
|
||||
* param {RetagRequest} request
|
||||
* returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf RetagService
|
||||
*/
|
||||
abstract retag(request: RetagRequest): Observable<any> | Promise<any> | any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement default service for retag.
|
||||
*
|
||||
**
|
||||
* class RetagDefaultService
|
||||
* extends {RetagService}
|
||||
*/
|
||||
@Injectable()
|
||||
export class RetagDefaultService extends RetagService {
|
||||
constructor(
|
||||
private http: Http
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
retag(request: RetagRequest): Observable<any> | Promise<any> | any {
|
||||
return this.http
|
||||
.post(`/api/repositories/${request.targetProject}/${request.targetRepo}/tags`,
|
||||
{
|
||||
"tag": request.targetTag,
|
||||
"src_image": request.srcImage,
|
||||
"override": request.override
|
||||
},
|
||||
HTTP_JSON_OPTIONS)
|
||||
.toPromise()
|
||||
.then(response => response.status)
|
||||
.catch(error => Promise.reject(error));
|
||||
};
|
||||
}
|
@ -11,6 +11,17 @@
|
||||
<button type="button" class="btn btn-primary" [ngxClipboard]="digestTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)">{{'BUTTON.COPY' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
<clr-modal class="hidden-tag" [(clrModalOpen)]="retagDialogOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="retagDialogClosable">
|
||||
<h3 class="modal-title">{{ 'REPOSITORY.RETAG' | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<div class="row col-md-12">
|
||||
<hbr-image-name-input #imageNameInput style="width:100%;"></hbr-image-name-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" [disabled]="imageNameInput.projectName.invalid||imageNameInput.repoName.invalid||imageNameInput.tagName.invalid" class="btn btn-primary" (click)="onRetag()">{{'BUTTON.CONFIRM' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
<div class="row" style="position:relative;">
|
||||
<div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
@ -58,6 +69,7 @@
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="retag(selectedRow)"><clr-icon shape="copy" size="16"></clr-icon> {{'REPOSITORY.RETAG' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon> {{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||
|
@ -26,7 +26,7 @@ import { debounceTime , distinctUntilChanged} from 'rxjs/operators';
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { State, Comparator } from "@clr/angular";
|
||||
|
||||
import { TagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index";
|
||||
import { TagService, RetagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index";
|
||||
import { ErrorHandler } from "../error-handler/error-handler";
|
||||
import { ChannelService } from "../channel/index";
|
||||
import {
|
||||
@ -39,7 +39,7 @@ import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation
|
||||
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
|
||||
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
|
||||
|
||||
import {Label, Tag, TagClickEvent} from "../service/interface";
|
||||
import { Label, Tag, TagClickEvent, RetagRequest } from "../service/interface";
|
||||
|
||||
import {
|
||||
toPromise,
|
||||
@ -52,10 +52,11 @@ import {
|
||||
clone,
|
||||
} from "../utils";
|
||||
|
||||
import {CopyInputComponent} from "../push-image/copy-input.component";
|
||||
import {LabelService} from "../service/label.service";
|
||||
import {operateChanges, OperateInfo, OperationState} from "../operation/operate";
|
||||
import {OperationService} from "../operation/operation.service";
|
||||
import { CopyInputComponent } from "../push-image/copy-input.component";
|
||||
import { LabelService } from "../service/label.service";
|
||||
import { operateChanges, OperateInfo, OperationState } from "../operation/operate";
|
||||
import { OperationService } from "../operation/operation.service";
|
||||
import { ImageNameInputComponent } from "../image-name-input/image-name-input.component";
|
||||
|
||||
export interface LabelState {
|
||||
iconsShow: boolean;
|
||||
@ -90,14 +91,17 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
tags: Tag[];
|
||||
|
||||
showTagManifestOpened: boolean;
|
||||
retagDialogOpened: boolean;
|
||||
manifestInfoTitle: string;
|
||||
digestId: string;
|
||||
staticBackdrop = true;
|
||||
closable = false;
|
||||
retagDialogClosable = true;
|
||||
lastFilteredTagName: string;
|
||||
inprogress: boolean;
|
||||
openLabelFilterPanel: boolean;
|
||||
openLabelFilterPiece: boolean;
|
||||
retagSrcImage: string;
|
||||
|
||||
createdComparator: Comparator<Tag> = new CustomComparator<Tag>("created", "date");
|
||||
|
||||
@ -125,10 +129,12 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
};
|
||||
filterOneLabel: Label = this.initFilter;
|
||||
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
|
||||
@ViewChild('imageNameInput')
|
||||
imageNameInput: ImageNameInputComponent;
|
||||
|
||||
@ViewChild("digestTarget") textInput: ElementRef;
|
||||
@ViewChild("copyInput") copyInput: CopyInputComponent;
|
||||
|
||||
@ -140,6 +146,7 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private tagService: TagService,
|
||||
private retagService: RetagService,
|
||||
private labelService: LabelService,
|
||||
private translateService: TranslateService,
|
||||
private ref: ChangeDetectorRef,
|
||||
@ -566,6 +573,25 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
retag(tags: Tag[]) {
|
||||
this.retagDialogOpened = true;
|
||||
this.retagSrcImage = this.repoName + ":" + tags[0].digest;
|
||||
}
|
||||
|
||||
onRetag() {
|
||||
this.retagDialogOpened = false;
|
||||
toPromise<any>(this.retagService.retag({
|
||||
targetProject: this.imageNameInput.projectName.value,
|
||||
targetRepo: this.imageNameInput.repoName.value,
|
||||
targetTag: this.imageNameInput.tagName.value,
|
||||
srcImage: this.retagSrcImage,
|
||||
override: true
|
||||
})).then(rsp => {
|
||||
}).catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
deleteTags(tags: Tag[]) {
|
||||
if (tags && tags.length) {
|
||||
let tagNames: string[] = [];
|
||||
|
@ -472,9 +472,11 @@
|
||||
"ADD_TO_IMAGE": "Add labels to this image",
|
||||
"FILTER_BY_LABEL": "Filter images by label",
|
||||
"ADD_LABELS": "Add labels",
|
||||
"RETAG": "Retag",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
"ADDITIONAL_INFO": "Add Additional Info"
|
||||
"ADDITIONAL_INFO": "Add Additional Info",
|
||||
"TARGET_PROJECT": "Target Project"
|
||||
},
|
||||
"HELM_CHART": {
|
||||
"HELMCHARTS": "Charts",
|
||||
|
@ -471,9 +471,11 @@
|
||||
"ADD_TO_IMAGE": "Add labels to this image",
|
||||
"FILTER_BY_LABEL": "Filter images by label",
|
||||
"ADD_LABELS": "Add labels",
|
||||
"RETAG": "Retag",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
"ADDITIONAL_INFO": "Add Additional Info"
|
||||
"ADDITIONAL_INFO": "Add Additional Info",
|
||||
"TARGET_PROJECT": "Target Project"
|
||||
},
|
||||
"HELM_CHART": {
|
||||
"HELMCHARTS": "Charts",
|
||||
|
@ -449,9 +449,11 @@
|
||||
"ADD_TO_IMAGE": "Add labels to this image",
|
||||
"FILTER_BY_LABEL": "Filter images by label",
|
||||
"ADD_LABELS": "Add labels",
|
||||
"RETAG": "Retag",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
"ADDITIONAL_INFO": "Add Additional Info"
|
||||
"ADDITIONAL_INFO": "Add Additional Info",
|
||||
"TARGET_PROJECT": "Projet Cible"
|
||||
},
|
||||
"HELM_CHART": {
|
||||
"HELMCHARTS": "Charts",
|
||||
|
@ -470,10 +470,12 @@
|
||||
"LABELS": "标签",
|
||||
"ADD_TO_IMAGE": "添加标签到此镜像",
|
||||
"ADD_LABELS": "添加标签",
|
||||
"RETAG": "复制镜像",
|
||||
"FILTER_BY_LABEL": "过滤标签",
|
||||
"ACTION": "操作",
|
||||
"DEPLOY": "部署",
|
||||
"ADDITIONAL_INFO": "添加信息"
|
||||
"ADDITIONAL_INFO": "添加信息",
|
||||
"TARGET_PROJECT": "目标项目"
|
||||
},
|
||||
"HELM_CHART": {
|
||||
"HELMCHARTS": "Charts",
|
||||
|
Loading…
Reference in New Issue
Block a user