From ea8fe8a2c5ffc336f5f683cce1ab10f4fbf20245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Wed, 10 Oct 2018 11:08:26 +0800 Subject: [PATCH 1/2] Add image retag function in portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- src/portal/lib/src/harbor-library.module.ts | 17 +++- .../image-name-input.component.html | 38 ++++++++ .../image-name-input.component.scss | 46 +++++++++ .../image-name-input.component.ts | 94 +++++++++++++++++++ src/portal/lib/src/image-name-input/index.ts | 4 + src/portal/lib/src/service/index.ts | 1 + src/portal/lib/src/service/interface.ts | 12 ++- src/portal/lib/src/service/retag.service.ts | 55 +++++++++++ src/portal/lib/src/tag/tag.component.html | 12 +++ src/portal/lib/src/tag/tag.component.ts | 40 ++++++-- src/portal/src/i18n/lang/en-us-lang.json | 4 +- src/portal/src/i18n/lang/es-es-lang.json | 4 +- src/portal/src/i18n/lang/fr-fr-lang.json | 4 +- src/portal/src/i18n/lang/zh-cn-lang.json | 4 +- 14 files changed, 319 insertions(+), 16 deletions(-) create mode 100644 src/portal/lib/src/image-name-input/image-name-input.component.html create mode 100644 src/portal/lib/src/image-name-input/image-name-input.component.scss create mode 100644 src/portal/lib/src/image-name-input/image-name-input.component.ts create mode 100644 src/portal/lib/src/image-name-input/index.ts create mode 100644 src/portal/lib/src/service/retag.service.ts 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", From 5063f57a1f2c289b8dfaa3b20635ae8a4f75e22a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=BE=B7?= Date: Thu, 11 Oct 2018 23:48:56 +0800 Subject: [PATCH 2/2] Fix validation in image name input form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 陈德 --- .../image-name-input.component.html | 20 +++--- .../image-name-input.component.scss | 52 +++++++------- .../image-name-input.component.spec.ts | 63 +++++++++++++++++ .../image-name-input.component.ts | 47 +++++++------ .../repository-gridview.component.spec.ts | 9 ++- .../repository/repository.component.spec.ts | 13 ++-- src/portal/lib/src/service/index.ts | 30 ++++---- src/portal/lib/src/service/retag.service.ts | 18 ++--- src/portal/lib/src/tag/tag.component.html | 8 +-- src/portal/lib/src/tag/tag.component.scss | 10 +++ src/portal/lib/src/tag/tag.component.spec.ts | 68 ++++++++++--------- src/portal/lib/src/tag/tag.component.ts | 18 +++-- src/portal/src/i18n/lang/en-us-lang.json | 6 +- src/portal/src/i18n/lang/es-es-lang.json | 3 +- src/portal/src/i18n/lang/fr-fr-lang.json | 3 +- src/portal/src/i18n/lang/zh-cn-lang.json | 5 +- 16 files changed, 244 insertions(+), 129 deletions(-) create mode 100644 src/portal/lib/src/image-name-input/image-name-input.component.spec.ts 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 index ee53800b4..f2dabcb15 100644 --- 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 @@ -1,13 +1,13 @@ -
+
- +
-
+
-
+
  • {{project?.name}}
@@ -16,21 +16,23 @@
- +
-
- +
-
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 index 8e4bc5e99..5738c4dbd 100644 --- 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 @@ -1,27 +1,31 @@ -.selectBox { +.clr-form { + width:100%; +} + +.select-box { 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; -} + 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; + &:hover { + color: #262626; + background-image: linear-gradient(180deg, #f5f5f5 0, #e8e8e8); + background-repeat: repeat-x; + } + } + } } .clr-input-wrapper { @@ -31,16 +35,18 @@ .wrap-label { display: block; + + input { + width: 100%; + } } -.wrap-label input { - width: 100%; -} - -label.required:after { - content: '*'; - font-size: .58479532rem; - line-height: .5rem; - color: #c92100; - margin-left: .25rem; +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.spec.ts b/src/portal/lib/src/image-name-input/image-name-input.component.spec.ts new file mode 100644 index 000000000..7541c6751 --- /dev/null +++ b/src/portal/lib/src/image-name-input/image-name-input.component.spec.ts @@ -0,0 +1,63 @@ +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; + +import { SharedModule } from "../shared/shared.module"; +import { ImageNameInputComponent } from "../image-name-input/image-name-input.component"; + +import { ErrorHandler } from "../error-handler/error-handler"; +import { ProjectDefaultService, ProjectService } from "../service/index"; +import { ChannelService } from "../channel/index"; +import { Project } from "../project-policy-config/project"; +import { IServiceConfig, SERVICE_CONFIG } from "../service.config"; + +describe("ImageNameInputComponent (inline template)", () => { + let comp: ImageNameInputComponent; + let fixture: ComponentFixture; + let spy: jasmine.Spy; + + let mockProjects: Project[] = [ + { + "project_id": 1, + "name": "project_01", + "creation_time": "", + }, + { + "project_id": 2, + "name": "project_02", + "creation_time": "", + } + ]; + + let config: IServiceConfig = { + projectBaseEndpoint: "/api/projects/testing" + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [ + ImageNameInputComponent + ], + providers: [ + ErrorHandler, + ChannelService, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: ProjectService, useClass: ProjectDefaultService } + ] + }); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ImageNameInputComponent); + comp = fixture.componentInstance; + + let projectService: ProjectService; + projectService = fixture.debugElement.injector.get(ProjectService); + spy = spyOn(projectService, "listProjects").and.returnValues(Promise.resolve(mockProjects)); + }); + + it("should load data", async(() => { + expect(spy.calls.any).toBeTruthy(); + })); +}); 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 index 21d30a3ce..d05ad4c85 100644 --- 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 @@ -1,11 +1,10 @@ -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"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Project } from "../project-policy-config/project"; +import { Observable, Subject } from "rxjs/index"; +import { debounceTime, distinctUntilChanged } from "rxjs/operators"; +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", @@ -31,18 +30,18 @@ export class ImageNameInputComponent implements OnInit, OnDestroy { } ngOnInit(): void { this.proNameChecker - .pipe(debounceTime(500)) + .pipe(debounceTime(200)) .pipe(distinctUntilChanged()) - .subscribe((resp: string) => { - let name = this.imageNameForm.controls["projectName"].value; + .subscribe((name: string) => { this.noProjectInfo = ""; this.selectedProjectList = []; - toPromise(this.proService.listProjects(name, undefined)) - .then((res: any) => { - if (res) { - this.selectedProjectList = res.slice(0, 10); + const prolist: any = this.proService.listProjects(name, undefined); + if (prolist.subscribe) { + prolist.subscribe(response => { + if (response) { + this.selectedProjectList = response.slice(0, 10); // if input project name exist in the project list - let exist = res.find((data: any) => data.name === name); + let exist = response.find((data: any) => data.name === name); if (!exist) { this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO"; } else { @@ -51,11 +50,13 @@ export class ImageNameInputComponent implements OnInit, OnDestroy { } else { this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO"; } - }) - .catch((error: any) => { + }, (error: any) => { this.errorHandler.error(error); this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO"; }); + } else { + this.errorHandler.error("not Observable type"); + } }); } @@ -86,9 +87,17 @@ export class ImageNameInputComponent implements OnInit, OnDestroy { } } + blurProjectInput(): void { + this.validateProjectName(); + } + + leaveProjectInput(): void { + this.selectedProjectList = []; + } + 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/repository-gridview/repository-gridview.component.spec.ts b/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts index 9b02e9fb8..93f406242 100644 --- a/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts +++ b/src/portal/lib/src/repository-gridview/repository-gridview.component.spec.ts @@ -6,6 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '../shared/shared.module'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ImageNameInputComponent } from "../image-name-input/image-name-input.component"; import { RepositoryGridviewComponent } from './repository-gridview.component'; import { TagComponent } from '../tag/tag.component'; import { FilterComponent } from '../filter/filter.component'; @@ -21,8 +22,9 @@ import { HBR_GRIDVIEW_DIRECTIVES } from '../gridview/index'; import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index'; import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index'; import { JobLogViewerComponent } from '../job-log-viewer/index'; -import {LabelPieceComponent} from "../label-piece/label-piece.component"; -import {OperationService} from "../operation/operation.service"; +import { LabelPieceComponent } from "../label-piece/label-piece.component"; +import { OperationService } from "../operation/operation.service"; +import {ProjectDefaultService, ProjectService, RetagDefaultService, RetagService} from "../service"; describe('RepositoryComponentGridview (inline template)', () => { @@ -104,6 +106,7 @@ describe('RepositoryComponentGridview (inline template)', () => { TagComponent, LabelPieceComponent, ConfirmationDialogComponent, + ImageNameInputComponent, FilterComponent, VULNERABILITY_DIRECTIVES, PUSH_IMAGE_BUTTON_DIRECTIVES, @@ -116,6 +119,8 @@ describe('RepositoryComponentGridview (inline template)', () => { { provide: SERVICE_CONFIG, useValue: config }, { provide: RepositoryService, useClass: RepositoryDefaultService }, { provide: TagService, useClass: TagDefaultService }, + { provide: ProjectService, useClass: ProjectDefaultService }, + { provide: RetagService, useClass: RetagDefaultService }, { provide: SystemInfoService, useClass: SystemInfoDefaultService }, { provide: OperationService } ] diff --git a/src/portal/lib/src/repository/repository.component.spec.ts b/src/portal/lib/src/repository/repository.component.spec.ts index ce425938e..c224eba68 100644 --- a/src/portal/lib/src/repository/repository.component.spec.ts +++ b/src/portal/lib/src/repository/repository.component.spec.ts @@ -5,6 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '../shared/shared.module'; import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ImageNameInputComponent } from "../image-name-input/image-name-input.component"; import { RepositoryComponent } from './repository.component'; import { RepositoryGridviewComponent } from '../repository-gridview/repository-gridview.component'; import { GridViewComponent } from '../gridview/grid-view.component'; @@ -17,15 +18,16 @@ import { JobLogViewerComponent } from '../job-log-viewer/index'; import { ErrorHandler } from '../error-handler/error-handler'; -import {Repository, RepositoryItem, Tag, SystemInfo, Label} from '../service/interface'; +import { Repository, RepositoryItem, Tag, SystemInfo, Label } from '../service/interface'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service'; import { TagService, TagDefaultService } from '../service/tag.service'; import { ChannelService } from '../channel/index'; -import {LabelPieceComponent} from "../label-piece/label-piece.component"; -import {LabelDefaultService, LabelService} from "../service/label.service"; -import {OperationService} from "../operation/operation.service"; +import { LabelPieceComponent } from "../label-piece/label-piece.component"; +import { LabelDefaultService, LabelService } from "../service/label.service"; +import { OperationService } from "../operation/operation.service"; +import { ProjectDefaultService, ProjectService, RetagDefaultService, RetagService } from "../service"; class RouterStub { @@ -159,6 +161,7 @@ describe('RepositoryComponent (inline template)', () => { GridViewComponent, RepositoryGridviewComponent, ConfirmationDialogComponent, + ImageNameInputComponent, FilterComponent, TagComponent, LabelPieceComponent, @@ -173,6 +176,8 @@ describe('RepositoryComponent (inline template)', () => { { provide: RepositoryService, useClass: RepositoryDefaultService }, { provide: SystemInfoService, useClass: SystemInfoDefaultService }, { provide: TagService, useClass: TagDefaultService }, + { provide: ProjectService, useClass: ProjectDefaultService }, + { provide: RetagService, useClass: RetagDefaultService }, { provide: LabelService, useClass: LabelDefaultService}, { provide: ChannelService}, { provide: OperationService } diff --git a/src/portal/lib/src/service/index.ts b/src/portal/lib/src/service/index.ts index a11f5d17f..afc38321a 100644 --- a/src/portal/lib/src/service/index.ts +++ b/src/portal/lib/src/service/index.ts @@ -1,15 +1,15 @@ -export * from './interface'; -export * from './system-info.service'; -export * from './access-log.service'; -export * from './endpoint.service'; -export * from './replication.service'; -export * from './repository.service'; -export * from './tag.service'; -export * from './RequestQueryParams'; -export * from './scanning.service'; -export * from './configuration.service'; -export * from './job-log.service'; -export * from './project.service'; -export * from './label.service'; -export * from './helm-chart.service'; -export * from './retag.service'; +export * from "./interface"; +export * from "./system-info.service"; +export * from "./access-log.service"; +export * from "./endpoint.service"; +export * from "./replication.service"; +export * from "./repository.service"; +export * from "./tag.service"; +export * from "./RequestQueryParams"; +export * from "./scanning.service"; +export * from "./configuration.service"; +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/retag.service.ts b/src/portal/lib/src/service/retag.service.ts index 1498662a4..d6c92003d 100644 --- a/src/portal/lib/src/service/retag.service.ts +++ b/src/portal/lib/src/service/retag.service.ts @@ -1,8 +1,10 @@ import { Observable } from "rxjs"; import { Http } from "@angular/http"; -import { Injectable } from '@angular/core'; +import { Injectable } from "@angular/core"; import { RetagRequest } from "./interface"; import { HTTP_JSON_OPTIONS } from "../utils"; +import { catchError } from "rxjs/operators"; +import { throwError as observableThrowError } from "rxjs/index"; /** * Define the service methods to perform images retag. @@ -17,11 +19,11 @@ export abstract class RetagService { * * @abstract * param {RetagRequest} request - * returns {(Observable | Promise | any)} + * returns {Observable} * * @memberOf RetagService */ - abstract retag(request: RetagRequest): Observable | Promise | any; + abstract retag(request: RetagRequest): Observable; } /** @@ -39,7 +41,7 @@ export class RetagDefaultService extends RetagService { super(); } - retag(request: RetagRequest): Observable | Promise | any { + retag(request: RetagRequest): Observable { return this.http .post(`/api/repositories/${request.targetProject}/${request.targetRepo}/tags`, { @@ -48,8 +50,6 @@ export class RetagDefaultService extends RetagService { "override": request.override }, HTTP_JSON_OPTIONS) - .toPromise() - .then(response => response.status) - .catch(error => Promise.reject(error)); - }; -} \ No newline at end of file + .pipe(catchError(error => observableThrowError(error))); + } +} diff --git a/src/portal/lib/src/tag/tag.component.html b/src/portal/lib/src/tag/tag.component.html index 0d9c4be79..a102c800b 100644 --- a/src/portal/lib/src/tag/tag.component.html +++ b/src/portal/lib/src/tag/tag.component.html @@ -13,13 +13,13 @@ -