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 @@ +
+
+ +
+
+ +
+
    +
  • {{project?.name}}
  • +
+
+
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
\ 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 @@ + + + + +
@@ -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",