Merge pull request #5995 from cd1989/image-retag-in-portal

Add image retag function in portal
This commit is contained in:
Mia ZHOU 2018-10-24 10:15:08 +08:00 committed by GitHub
commit caa9a4173e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 488 additions and 70 deletions

View File

@ -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 },

View File

@ -0,0 +1,40 @@
<form [formGroup]="imageNameForm" class="clr-form clr-form-compact">
<div class="clr-form-control clr-row">
<label for="project-name" class="required clr-control-label clr-col-xs-12 clr-col-md-4">{{ 'PROJECT.NAME' | translate }}</label>
<div class="clr-control-container clr-col-xs-12 clr-col-md-8">
<div class="clr-input-wrapper" (mouseleave)="leaveProjectInput()">
<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)='blurProjectInput()' 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="select-box" [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-4">{{ '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 && (repoName.dirty || repoName.touched)'>
<input type="text" id="repo-name" class="clr-input" formControlName="repoName" required />
<span *ngIf="repoName.invalid && (repoName.dirty || repoName.touched)" class="tooltip-content">{{ 'TOOLTIP.NONEMPTY' | translate }}</span>
</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-4">{{ '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 && (tagName.dirty || tagName.touched)'>
<input type="text" id="tag-name" class="clr-input" formControlName="tagName" required />
<span *ngIf="repoName.invalid && (repoName.dirty || repoName.touched)" class="tooltip-content">{{ 'TOOLTIP.NONEMPTY' | translate }}</span>
</label>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,52 @@
.clr-form {
width:100%;
}
.select-box {
position: absolute;
width: 100%;
height: auto;
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);
ul {
li {
list-style: none;
padding: 3px 20px;
cursor: pointer;
&: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;
input {
width: 100%;
}
}
label.required {
&:after {
content: '*';
font-size: .58479532rem;
line-height: .5rem;
color: #c92100;
margin-left: .25rem;
}
}

View File

@ -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<ImageNameInputComponent>;
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();
}));
});

View File

@ -0,0 +1,103 @@
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",
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(200))
.pipe(distinctUntilChanged())
.subscribe((name: string) => {
this.noProjectInfo = "";
this.selectedProjectList = [];
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 = response.find((data: any) => data.name === name);
if (!exist) {
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
} else {
this.noProjectInfo = "";
}
} else {
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
}
}, (error: any) => {
this.errorHandler.error(error);
this.noProjectInfo = "REPLICATION.NO_PROJECT_INFO";
});
} else {
this.errorHandler.error("not Observable type");
}
});
}
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";
}
}
blurProjectInput(): void {
this.validateProjectName();
}
leaveProjectInput(): void {
this.selectedProjectList = [];
}
selectedProjectName(projectName: string) {
this.imageNameForm.controls["projectName"].setValue(projectName);
this.selectedProjectList = [];
this.noProjectInfo = "";
}
}

View 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];

View File

@ -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 }
]

View File

@ -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 }

View File

@ -1,14 +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 "./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";

View File

@ -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;
}

View 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";
import { catchError } from "rxjs/operators";
import { throwError as observableThrowError } from "rxjs/index";
/**
* 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>}
*
* @memberOf RetagService
*/
abstract retag(request: RetagRequest): Observable<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> {
return this.http
.post(`/api/repositories/${request.targetProject}/${request.targetRepo}/tags`,
{
"tag": request.targetTag,
"src_image": request.srcImage,
"override": request.override
},
HTTP_JSON_OPTIONS)
.pipe(catchError(error => observableThrowError(error)));
}
}

View File

@ -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 retag-modal-body">
<div class="row col-md-12">
<hbr-image-name-input #imageNameInput></hbr-image-name-input>
</div>
</div>
<div class="modal-footer">
<button type="button" [disabled]="imageNameInput.projectName.invalid||imageNameInput.repoName.invalid||imageNameInput.tagName.invalid||imageNameInput.noProjectInfo!=''" 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>&nbsp;{{'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>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>

View File

@ -198,4 +198,14 @@
top: 5px;
cursor: pointer;
font-size: 20px;
}
.retag-modal-body {
overflow-y: hidden;
min-height: 172px;
padding-top: 16px;
}
hbr-image-name-input {
width: 100%;
}

View File

@ -1,26 +1,29 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { ComponentFixture, TestBed, async } from "@angular/core/testing";
import { DebugElement } from "@angular/core";
import { SharedModule } from '../shared/shared.module';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { TagComponent } from './tag.component';
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 { TagComponent } from "./tag.component";
import { ErrorHandler } from '../error-handler/error-handler';
import {Label, Tag} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { FILTER_DIRECTIVES } from '../filter/index';
import { ErrorHandler } from "../error-handler/error-handler";
import { Label, Tag } from "../service/interface";
import { SERVICE_CONFIG, IServiceConfig } from "../service.config";
import {
TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService,
RetagService, RetagDefaultService, ProjectService, ProjectDefaultService
} from "../service/index";
import { VULNERABILITY_DIRECTIVES } from "../vulnerability-scanning/index";
import { FILTER_DIRECTIVES } from "../filter/index";
import { ChannelService } from "../channel/index";
import { ChannelService } from '../channel/index';
import { JobLogViewerComponent } from "../job-log-viewer/index";
import { CopyInputComponent } from "../push-image/copy-input.component";
import { LabelPieceComponent } from "../label-piece/label-piece.component";
import { LabelDefaultService, LabelService } from "../service/label.service";
import { OperationService } from "../operation/operation.service";
import { JobLogViewerComponent } from '../job-log-viewer/index';
import {CopyInputComponent} from "../push-image/copy-input.component";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
import {OperationService} from "../operation/operation.service";
describe('TagComponent (inline template)', () => {
describe("TagComponent (inline template)", () => {
let comp: TagComponent;
let fixture: ComponentFixture<TagComponent>;
@ -90,7 +93,7 @@ describe('TagComponent (inline template)', () => {
];
let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repositories/testing'
repositoryBaseEndpoint: "/api/repositories/testing"
};
beforeEach(async(() => {
@ -102,18 +105,21 @@ describe('TagComponent (inline template)', () => {
TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent,
ImageNameInputComponent,
VULNERABILITY_DIRECTIVES,
FILTER_DIRECTIVES,
JobLogViewerComponent,
CopyInputComponent
CopyInputComponent
],
providers: [
ErrorHandler,
ChannelService,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ProjectService, useClass: ProjectDefaultService },
{ provide: RetagService, useClass: RetagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
{provide: LabelService, useClass: LabelDefaultService},
{ provide: LabelService, useClass: LabelDefaultService },
{ provide: OperationService }
]
});
@ -124,10 +130,10 @@ describe('TagComponent (inline template)', () => {
comp = fixture.componentInstance;
comp.projectId = 1;
comp.repoName = 'library/nginx';
comp.repoName = "library/nginx";
comp.hasProjectAdminRole = true;
comp.hasSignedIn = true;
comp.registryUrl = 'http://registry.testing.com';
comp.registryUrl = "http://registry.testing.com";
comp.withNotary = false;
@ -135,31 +141,31 @@ describe('TagComponent (inline template)', () => {
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTags));
spy = spyOn(tagService, "getTags").and.returnValues(Promise.resolve(mockTags));
labelService = fixture.debugElement.injector.get(LabelService);
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
spyLabels = spyOn(labelService, "getGLabels").and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, "getPLabels").and.returnValues(Promise.resolve(mockLabels1));
fixture.detectChanges();
});
it('should load data', async(() => {
it("should load data", async(() => {
expect(spy.calls.any).toBeTruthy();
}));
// fail after upgrade to angular 6.
xit('should load and render data', async(() => {
xit("should load and render data", async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
let de: DebugElement = fixture.debugElement.query(del => del.classes["datagrid-cell"]);
fixture.detectChanges();
expect(de).toBeTruthy();
let el: HTMLElement = de.nativeElement;
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('1.11.5');
expect(el.textContent.trim()).toEqual("1.11.5");
});
}));

View File

@ -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')
@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,29 @@ export class TagComponent implements OnInit, AfterViewInit {
}
}
retag(tags: Tag[]) {
if (tags && tags.length) {
this.retagDialogOpened = true;
this.retagSrcImage = this.repoName + ":" + tags[0].digest;
} else {
this.errorHandler.error("One tag should be selected before retag.");
}
}
onRetag() {
this.retagDialogOpened = false;
this.retagService.retag({
targetProject: this.imageNameInput.projectName.value,
targetRepo: this.imageNameInput.repoName.value,
targetTag: this.imageNameInput.tagName.value,
srcImage: this.retagSrcImage,
override: true
}).subscribe(response => {
}, error => {
this.errorHandler.error(error);
});
}
deleteTags(tags: Tag[]) {
if (tags && tags.length) {
let tagNames: string[] = [];

View File

@ -67,7 +67,9 @@
"EMAIL_EXISTING": "Email address already exists.",
"USER_EXISTING": "Username is already in use.",
"RULE_USER_EXISTING": "Name is already in use.",
"EMPTY": "Name is required"
"EMPTY": "Name is required",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode."
},
"PLACEHOLDER": {
"CURRENT_PWD": "Enter current password",
@ -472,9 +474,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",
"REPO_NAME": "Repository"
},
"HELM_CHART": {
"HELMCHARTS": "Charts",

View File

@ -68,6 +68,7 @@
"USER_EXISTING": "Ese nombre de usuario ya existe.",
"RULE_USER_EXISTING": "Name is already in use.",
"EMPTY": "Name is required",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode."
},
"PLACEHOLDER": {
@ -471,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",
"REPO_NAME": "Repository"
},
"HELM_CHART": {
"HELMCHARTS": "Charts",

View File

@ -55,6 +55,7 @@
"PORT_REQUIRED": "Le champ est obligatoire et doit être un numéro de port valide.",
"EMAIL_EXISTING": "L'adresse e-mail existe déjà.",
"USER_EXISTING": "Le nom d'utilisateur est déjà utilisé.",
"NONEMPTY": "Can't be empty",
"REPO_TOOLTIP": "Users can not do any operations to the images in this mode."
},
"PLACEHOLDER": {
@ -449,9 +450,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",
"REPO_NAME": "Repository"
},
"HELM_CHART": {
"HELMCHARTS": "Charts",

View File

@ -67,7 +67,8 @@
"EMAIL_EXISTING": "邮件地址已经存在。",
"USER_EXISTING": "用户名已经存在。",
"RULE_USER_EXISTING": "名称已经存在。",
"EMPTY": "名称为必填项"
"EMPTY": "名称为必填项",
"NONEMPTY": "不能为空"
},
"PLACEHOLDER": {
"CURRENT_PWD": "输入当前密码",
@ -470,10 +471,12 @@
"LABELS": "标签",
"ADD_TO_IMAGE": "添加标签到此镜像",
"ADD_LABELS": "添加标签",
"RETAG": "复制镜像",
"FILTER_BY_LABEL": "过滤标签",
"ACTION": "操作",
"DEPLOY": "部署",
"ADDITIONAL_INFO": "添加信息"
"ADDITIONAL_INFO": "添加信息",
"REPO_NAME": "镜像仓库"
},
"HELM_CHART": {
"HELMCHARTS": "Charts",