diff --git a/src/portal/lib/src/harbor-library.module.ts b/src/portal/lib/src/harbor-library.module.ts
index ab0c08485..031aeacd4 100644
--- a/src/portal/lib/src/harbor-library.module.ts
+++ b/src/portal/lib/src/harbor-library.module.ts
@@ -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 },
diff --git a/src/portal/lib/src/image-name-input/image-name-input.component.html b/src/portal/lib/src/image-name-input/image-name-input.component.html
new file mode 100644
index 000000000..ee53800b4
--- /dev/null
+++ b/src/portal/lib/src/image-name-input/image-name-input.component.html
@@ -0,0 +1,38 @@
+
\ No newline at end of file
diff --git a/src/portal/lib/src/image-name-input/image-name-input.component.scss b/src/portal/lib/src/image-name-input/image-name-input.component.scss
new file mode 100644
index 000000000..8e4bc5e99
--- /dev/null
+++ b/src/portal/lib/src/image-name-input/image-name-input.component.scss
@@ -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;
+}
\ No newline at end of file
diff --git a/src/portal/lib/src/image-name-input/image-name-input.component.ts b/src/portal/lib/src/image-name-input/image-name-input.component.ts
new file mode 100644
index 000000000..21d30a3ce
--- /dev/null
+++ b/src/portal/lib/src/image-name-input/image-name-input.component.ts
@@ -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 = new Subject();
+ 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(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 = "";
+ }
+}
\ No newline at end of file
diff --git a/src/portal/lib/src/image-name-input/index.ts b/src/portal/lib/src/image-name-input/index.ts
new file mode 100644
index 000000000..1c644f54b
--- /dev/null
+++ b/src/portal/lib/src/image-name-input/index.ts
@@ -0,0 +1,4 @@
+import { Type } from "@angular/core";
+import { ImageNameInputComponent } from "./image-name-input.component";
+
+export const IMAGE_NAME_INPUT_DIRECTIVES: Type[] = [ImageNameInputComponent];
diff --git a/src/portal/lib/src/service/index.ts b/src/portal/lib/src/service/index.ts
index e437247da..a11f5d17f 100644
--- a/src/portal/lib/src/service/index.ts
+++ b/src/portal/lib/src/service/index.ts
@@ -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';
diff --git a/src/portal/lib/src/service/interface.ts b/src/portal/lib/src/service/interface.ts
index 2aa87af68..02834bc71 100644
--- a/src/portal/lib/src/service/interface.ts
+++ b/src/portal/lib/src/service/interface.ts
@@ -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;
}
diff --git a/src/portal/lib/src/service/retag.service.ts b/src/portal/lib/src/service/retag.service.ts
new file mode 100644
index 000000000..1498662a4
--- /dev/null
+++ b/src/portal/lib/src/service/retag.service.ts
@@ -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 | Promise | any)}
+ *
+ * @memberOf RetagService
+ */
+ abstract retag(request: RetagRequest): Observable | Promise | 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 | Promise | 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));
+ };
+}
\ No newline at end of file
diff --git a/src/portal/lib/src/tag/tag.component.html b/src/portal/lib/src/tag/tag.component.html
index 00521cca6..0d9c4be79 100644
--- a/src/portal/lib/src/tag/tag.component.html
+++ b/src/portal/lib/src/tag/tag.component.html
@@ -11,6 +11,17 @@
+
+ {{ 'REPOSITORY.RETAG' | translate }}
+
+
+
@@ -58,6 +69,7 @@
+
{{'REPOSITORY.TAG' | translate}}
diff --git a/src/portal/lib/src/tag/tag.component.ts b/src/portal/lib/src/tag/tag.component.ts
index 3b4073799..ad080297d 100644
--- a/src/portal/lib/src/tag/tag.component.ts
+++ b/src/portal/lib/src/tag/tag.component.ts
@@ -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
= new CustomComparator("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(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[] = [];
diff --git a/src/portal/src/i18n/lang/en-us-lang.json b/src/portal/src/i18n/lang/en-us-lang.json
index 3fd523103..7b03e3b9d 100644
--- a/src/portal/src/i18n/lang/en-us-lang.json
+++ b/src/portal/src/i18n/lang/en-us-lang.json
@@ -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",
diff --git a/src/portal/src/i18n/lang/es-es-lang.json b/src/portal/src/i18n/lang/es-es-lang.json
index 1d3796d4c..923f0de99 100644
--- a/src/portal/src/i18n/lang/es-es-lang.json
+++ b/src/portal/src/i18n/lang/es-es-lang.json
@@ -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",
diff --git a/src/portal/src/i18n/lang/fr-fr-lang.json b/src/portal/src/i18n/lang/fr-fr-lang.json
index 3fc53b93d..d96668966 100644
--- a/src/portal/src/i18n/lang/fr-fr-lang.json
+++ b/src/portal/src/i18n/lang/fr-fr-lang.json
@@ -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",
diff --git a/src/portal/src/i18n/lang/zh-cn-lang.json b/src/portal/src/i18n/lang/zh-cn-lang.json
index 81cd91a57..c3aeef3ce 100644
--- a/src/portal/src/i18n/lang/zh-cn-lang.json
+++ b/src/portal/src/i18n/lang/zh-cn-lang.json
@@ -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",