Add label module

This commit is contained in:
pfh 2018-03-23 09:58:06 +08:00
parent b4a4eb11c6
commit 71124d08dd
53 changed files with 1639 additions and 56 deletions

View File

@ -0,0 +1,23 @@
export const CREATE_EDIT_LABEL_STYLE: string = `
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
form{margin-bottom:-10px;padding-top:0; margin-top: 20px;width: 100%;background-color: #eee; border:1px solid #ccc;}
form .form-group{display:inline-flex;padding-left: 70px;}
form .form-group>label:first-child{width: auto;}
section{padding:.5rem 0;}
section> label{margin-left: 20px;}
.dropdown-menu{display:inline-block;width:166px; padding:6px;}
.dropdown-item{ display:inline-flex; margin:2px 4px;
display: inline-block;padding: 0px; width:30px;height:24px; text-align: center;line-height: 24px;}
.btnColor{
margin: 0 !important;
padding: 0;
width: 26px;
height:22px;
min-width: 26px;}
.dropdown-item{border:0px; color: white; font-size:12px;}
`;

View File

@ -0,0 +1,36 @@
export const CREATE_EDIT_LABEL_TEMPLATE: string = `
<div>
<form #labelForm="ngForm" [hidden]="!formShow">
<section>
<label>
<label for="name">{{'LABEL.LABEL_NAME' | translate}}</label>
<label aria-haspopup="true" role="tooltip" [class.invalid]="isLabelNameExist" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left">
<input type="text" id="name" name="name" required size="20" autocomplete="off" [(ngModel)]="labelModel.name" #name="ngModel" (keyup)="existValid(labelModel.name)">
<span class="tooltip-content">
{{'LABEL.NAME_ALREADY_EXIST' | translate }}
</span>
</label>
</label>
<label>
<label for="color">{{'LABEL.COLOR' | translate}}</label>
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button type="button" class="btn btn-outline btnColor btn-sm" clrDropdownTrigger>
<clr-icon shape="caret down" size="20" style='right:2px; width:24px; height:18px;'></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
<label type="button" class="dropdown-item" (click)="labelModel.color=i" *ngFor="let i of labelColor" [ngStyle]="{'background-color': i}">Aa</label>
</clr-dropdown-menu>
</clr-dropdown>
<input type="text" id="color" size="8" name="color" [(ngModel)]="labelModel.color" #color="ngModel">
</label>
<label>
<label for="description">{{'LABEL.DESCRIPTION' | translate}}</label>
<input type="text" id="description" name="description" size="30" [(ngModel)]="labelModel.description" #description="ngModel">
</label>
<label>
<button type="button" class="btn btn-sm btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-sm btn-primary" (click)="onSubmit()" [disabled]="!isValid">{{ 'BUTTON.OK' | translate }}</button>
</label>
</section>
</form>
</div>`;

View File

@ -0,0 +1,85 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { SharedModule } from '../shared/shared.module';
import { FilterComponent } from '../filter/filter.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { ErrorHandler } from '../error-handler/error-handler';
import {Label} from '../service/interface';
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
import {CreateEditLabelComponent} from "./create-edit-label.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
describe('CreateEditLabelComponent (inline template)', () => {
let mockOneData: Label = {
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
}
let comp: CreateEditLabelComponent;
let fixture: ComponentFixture<CreateEditLabelComponent>;
let config: IServiceConfig = {
systemInfoEndpoint: '/api/label/testing'
};
let labelService: LabelService;
let spy: jasmine.Spy;
let spyOne: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
NoopAnimationsModule
],
declarations: [
FilterComponent,
CreateEditLabelComponent,
InlineAlertComponent ],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: LabelService, useClass: LabelDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateEditLabelComponent);
comp = fixture.componentInstance;
labelService = fixture.debugElement.injector.get(LabelService);
spy = spyOn(labelService, 'getLabels').and.returnValue(Promise.resolve(mockOneData));
spyOne = spyOn(labelService, 'createLabel').and.returnValue(Promise.resolve(mockOneData));
fixture.detectChanges();
comp.openModal();
fixture.detectChanges();
});
it('should be created', () => {
fixture.detectChanges();
expect(comp).toBeTruthy();
});
it('should get label and open modal', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(comp.labelModel.name).toEqual('');
});
});
});

View File

@ -0,0 +1,164 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
Component,
Output,
EventEmitter,
OnDestroy,
Input, OnInit, ViewChild
} from '@angular/core';
import {Label} from '../service/interface';
import { CREATE_EDIT_LABEL_STYLE } from './create-edit-label.component.css';
import { CREATE_EDIT_LABEL_TEMPLATE } from './create-edit-label.component.html';
import {toPromise, clone, compareValue} from '../utils';
import {Subject} from "rxjs/Subject";
import {LabelService} from "../service/label.service";
import {ErrorHandler} from "../error-handler/error-handler";
import {NgForm} from "@angular/forms";
@Component({
selector: 'hbr-create-edit-label',
template: CREATE_EDIT_LABEL_TEMPLATE,
styles: [CREATE_EDIT_LABEL_STYLE]
})
export class CreateEditLabelComponent implements OnInit, OnDestroy {
formShow: boolean;
inProgress: boolean;
copeLabelModel: Label;
labelModel: Label = this.initLabel();
labelId = 0;
nameChecker: Subject<string> = new Subject<string>();
checkOnGoing: boolean;
isLabelNameExist = false;
labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8'];
labelForm: NgForm;
@ViewChild('labelForm')
currentForm: NgForm;
@Input() projectId: number;
@Input() scope: string;
@Output() reload = new EventEmitter();
constructor(
private labelService: LabelService,
private errorHandler: ErrorHandler,
) { }
ngOnInit(): void {
this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((name: string) => {
this.checkOnGoing = true;
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId))
.then(targets => {
if (targets && targets.length) {
if (targets.find(m => m.name === name)) {
this.isLabelNameExist = true;
} else {
this.isLabelNameExist = false;
};
}else {
this.isLabelNameExist = false;
}
this.checkOnGoing = false;
}).catch(error => {
this.checkOnGoing = false;
this.errorHandler.error(error)
});
});
}
ngOnDestroy(): void {
this.nameChecker.unsubscribe();
}
initLabel(): Label {
return {
name: '',
description: '',
color: '',
scope: '',
project_id: 0
};
}
openModal(): void {
this.labelModel = this.initLabel();
this.formShow = true;
this.labelId = 0;
this.copeLabelModel = null;
}
editModel(labelId: number, label: Label[]): void {
this.labelModel = clone(label[0]);
this.formShow = true;
this.labelId = labelId;
this.copeLabelModel = clone(label[0]);
}
public get hasChanged(): boolean {
return !compareValue(this.copeLabelModel, this.labelModel);
}
public get isValid(): boolean {
return !(this.checkOnGoing || this.isLabelNameExist || !(this.currentForm && this.currentForm.valid) || !this.hasChanged || this.inProgress);
}
existValid(text: string): void {
if (text) {
this.nameChecker.next(text);
}
}
onSubmit(): void {
this.inProgress = true;
if (this.labelId <= 0) {
this.labelModel.scope = this.scope;
this.labelModel.project_id = this.projectId;
toPromise<Label>(this.labelService.createLabel(this.labelModel))
.then(res => {
this.inProgress = false;
this.reload.emit();
this.labelModel = this.initLabel();
}).catch(err => {
this.inProgress = false;
this.errorHandler.error(err)
});
} else {
toPromise<Label>(this.labelService.updateLabel(this.labelId, this.labelModel))
.then(res => {
this.inProgress = false;
this.reload.emit();
this.labelModel = this.initLabel();
}).catch(err => {
this.inProgress = false;
this.errorHandler.error(err)
});
}
}
onCancel(): void {
this.inProgress = false;
this.labelModel = this.initLabel();
this.formShow = false;
}
}

View File

@ -0,0 +1,6 @@
import { Type } from '@angular/core';
import {CreateEditLabelComponent} from "./create-edit-label.component";
export const CREATE_EDIT_LABEL_DIRECTIVES: Type<any>[] = [
CreateEditLabelComponent
];

View File

@ -47,6 +47,8 @@ import {
JobLogDefaultService, JobLogDefaultService,
ProjectService, ProjectService,
ProjectDefaultService, ProjectDefaultService,
LabelService,
LabelDefaultService
} from './service/index'; } from './service/index';
import { import {
ErrorHandler, ErrorHandler,
@ -58,6 +60,9 @@ import { TranslateModule } from '@ngx-translate/core';
import { TranslateServiceInitializer } from './i18n/index'; import { TranslateServiceInitializer } from './i18n/index';
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils'; import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils';
import { ChannelService } from './channel/index'; import { ChannelService } from './channel/index';
import {LABEL_DIRECTIVES} from "./label/index";
import {CREATE_EDIT_LABEL_DIRECTIVES} from "./create-edit-label/index";
import {LABEL_PIECE_DIRECTIVES} from "./label-piece/index";
/** /**
* Declare default service configuration; all the endpoints will be defined in * Declare default service configuration; all the endpoints will be defined in
@ -81,7 +86,8 @@ export const DefaultServiceConfig: IServiceConfig = {
langMessageFileSuffixForHttpLoader: "-lang.json", langMessageFileSuffixForHttpLoader: "-lang.json",
localI18nMessageVariableMap: {}, localI18nMessageVariableMap: {},
configurationEndpoint: "/api/configurations", configurationEndpoint: "/api/configurations",
scanJobEndpoint: "/api/jobs/scan" scanJobEndpoint: "/api/jobs/scan",
labelEndpoint: "/api/labels"
}; };
/** /**
@ -126,6 +132,9 @@ export interface HarborModuleConfig {
//Service implementation for project policy //Service implementation for project policy
projectPolicyService?: Provider, projectPolicyService?: Provider,
//Service implementation for label
labelService?: Provider,
} }
/** /**
@ -170,7 +179,10 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
PUSH_IMAGE_BUTTON_DIRECTIVES, PUSH_IMAGE_BUTTON_DIRECTIVES,
CONFIGURATION_DIRECTIVES, CONFIGURATION_DIRECTIVES,
JOB_LOG_VIEWER_DIRECTIVES, JOB_LOG_VIEWER_DIRECTIVES,
PROJECT_POLICY_CONFIG_DIRECTIVES PROJECT_POLICY_CONFIG_DIRECTIVES,
LABEL_DIRECTIVES,
CREATE_EDIT_LABEL_DIRECTIVES,
LABEL_PIECE_DIRECTIVES
], ],
exports: [ exports: [
LOG_DIRECTIVES, LOG_DIRECTIVES,
@ -192,7 +204,10 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
CONFIGURATION_DIRECTIVES, CONFIGURATION_DIRECTIVES,
JOB_LOG_VIEWER_DIRECTIVES, JOB_LOG_VIEWER_DIRECTIVES,
TranslateModule, TranslateModule,
PROJECT_POLICY_CONFIG_DIRECTIVES PROJECT_POLICY_CONFIG_DIRECTIVES,
LABEL_DIRECTIVES,
CREATE_EDIT_LABEL_DIRECTIVES,
LABEL_PIECE_DIRECTIVES
], ],
providers: [] providers: []
}) })
@ -214,6 +229,7 @@ export class HarborLibraryModule {
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService }, config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService }, config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
// Do initializing // Do initializing
TranslateServiceInitializer, TranslateServiceInitializer,
{ {
@ -243,6 +259,7 @@ export class HarborLibraryModule {
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService }, config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService }, config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService }, config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
ChannelService ChannelService
] ]
}; };

View File

@ -21,3 +21,5 @@ export * from './config/index';
export * from './job-log-viewer/index'; export * from './job-log-viewer/index';
export * from './channel/index'; export * from './channel/index';
export * from './project-policy-config/index'; export * from './project-policy-config/index';
export * from './label/index';
export * from './create-edit-label';

View File

@ -0,0 +1,8 @@
import { Type } from "@angular/core";
import {LabelPieceComponent} from './label-piece.component';
/*export * from "./filter.component";*/
export const LABEL_PIECE_DIRECTIVES: Type<any>[] = [
LabelPieceComponent
];

View File

@ -0,0 +1,46 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { LABEL_PIEICE_TEMPLATE, LABEL_PIEICE_STYLES } from './label-piece.template';
var LabelPieceComponent = (function () {
function LabelPieceComponent() {
}
LabelPieceComponent.prototype.ngOnInit = function () {
};
return LabelPieceComponent;
}());
__decorate([
Input(),
__metadata("design:type", Object)
], LabelPieceComponent.prototype, "label", void 0);
LabelPieceComponent = __decorate([
Component({
selector: 'hbr-label-piece',
styles: [LABEL_PIEICE_STYLES],
template: LABEL_PIEICE_TEMPLATE
})
], LabelPieceComponent);
export { LabelPieceComponent };
//# sourceMappingURL=label-piece.component.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["label-piece.component.ts"],"names":[],"mappings":";;;;;;;;;AAAA,uDAAC;AACD,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,gDAAgD;AAChD,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AACjC,OAAO,EAAE,SAAA,EAAW,KAAA,EAAoC,MAAO,eAAA,CAAgB;AAI/E,OAAO,gCAAA,CAAiC;AACxC,OAAO,wCAAA,CAAyC;AAEhD,OAAO,EAAE,qBAAA,EAAuB,mBAAA,EAAoB,MAAO,wBAAA,CAAyB;AAUpF,IAAa,mBAAmB;IAAhC;IAaA,CAAC;IAFG,sCAAQ,GAAR;IACA,CAAC;IACL,0BAAC;AAAD,CAbA,AAaC,IAAA;AAJY;IAAR,KAAK,EAAE;;kDAAc;AATb,mBAAmB;IAN/B,SAAS,CAAC;QACP,QAAQ,EAAE,iBAAiB;QAC3B,MAAM,EAAE,CAAC,mBAAmB,CAAC;QAC7B,QAAQ,EAAE,qBAAqB;KAClC,CAAC;GAEW,mBAAmB,CAa/B;SAbY,mBAAmB","file":"label-piece.component.js","sourceRoot":""}

View File

@ -0,0 +1,36 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { LABEL_PIEICE_TEMPLATE, LABEL_PIEICE_STYLES } from './label-piece.template';
import {Label} from "../service/interface";
@Component({
selector: 'hbr-label-piece',
styles: [LABEL_PIEICE_STYLES],
template: LABEL_PIEICE_TEMPLATE
})
export class LabelPieceComponent implements OnInit {
@Input() label: Label;
ngOnInit(): void {
}
}

View File

@ -0,0 +1,8 @@
/**
* Define template resources for filter component
*/
/**
* Define template resources for filter component
*/ export var LABEL_PIEICE_TEMPLATE = "\n<label class=\"label\" [ngStyle]=\"{'background-color': label.color}\">\n <clr-icon *ngIf=\"label.scope=='p'\" shape=\"organization\"></clr-icon>\n <clr-icon *ngIf=\"label.scope=='g'\" shape=\"administrator\"></clr-icon>\n {{label.name}}\n</label>\n";
export var LABEL_PIEICE_STYLES = "\n .label{border: none; color:#222;}\n .label clr-icon{ margin-right: 3px;}\n";
//# sourceMappingURL=label-piece.template.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["label-piece.template.ts"],"names":[],"mappings":"AAAA;;GAEG;AAFH,AAIA;;GAFG,CAEH,MAAM,CAAC,IAAM,qBAAqB,GAAW,uQAM5C,CAAC;AAEF,MAAM,CAAC,IAAM,mBAAmB,GAAW,mFAG1C,CAAC","file":"label-piece.template.js","sourceRoot":""}

View File

@ -0,0 +1,17 @@
/**
* Define template resources for filter component
*/
export const LABEL_PIEICE_TEMPLATE: string = `
<label class="label" [ngStyle]="{'background-color': label.color}">
<clr-icon *ngIf="label.scope=='p'" shape="organization"></clr-icon>
<clr-icon *ngIf="label.scope=='g'" shape="administrator"></clr-icon>
{{label.name}}
</label>
`;
export const LABEL_PIEICE_STYLES: string = `
.label{border: none; color:#222;padding-top:2px;}
.label clr-icon{ margin-right: 3px; display:block;}
.btn-group .dropdown-menu clr-icon{display:block;}
`;

View File

@ -0,0 +1,6 @@
import { Type } from '@angular/core';
import {LabelComponent} from "./label.component";
export const LABEL_DIRECTIVES: Type<any>[] = [
LabelComponent
];

View File

@ -0,0 +1,21 @@
export const LABEL_STYLE: string = `
.option-left {
padding-left: 16px;
margin-top: -6px;
}
.option-right {
padding-right: 16px;
}
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #007CBB;
}
.rightPos{
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
height: 24px;}
`;

View File

@ -0,0 +1,43 @@
export const LABEL_TEMPLATE = `
<div>
<div class="row" style="position:relative;">
<div>
<div class="row flex-items-xs-between rightPos">
<div class="flex-items-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"LABEL.FILTER_LABEL_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></hbr-filter>
<span class="refresh-btn" (click)="refreshTargets()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 btnGroup">
<button type="button" class="btn btn-sm btn-secondary" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'LABEL.NEW_LABEL' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length == 1)" (click)="editLabel(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'LABEL.EDIT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow.length" (click)="deleteLabels(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'LABEL.DELETE' | translate}}</button>
<hbr-create-edit-label [scope]="scope" [projectId]="projectId" (reload)="reload()"></hbr-create-edit-label>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-column [clrDgField]="'name'">{{'LABEL.LABEL' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'endpoint'">{{'LABEL.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'insecure'">{{'LABEL.CREATION_TIME' | translate }}</clr-dg-column>
<clr-dg-placeholder>{{'DESTINATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let label of targets" [clrDgItem]='label'>
<clr-dg-cell>
<hbr-label-piece [label]="label"></hbr-label-piece>
</clr-dg-cell>
<clr-dg-cell>{{label.description}}</clr-dg-cell>
<clr-dg-cell>{{label.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'DESTINATION.OF' | translate}}</span>
{{pagination.totalItems}} {{'DESTINATION.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<confirmation-dialog #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
</div>
`;

View File

@ -0,0 +1,131 @@
import {Label} from "../service/interface";
import {LabelComponent} from "./label.component";
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {LabelDefaultService, LabelService} from "../service/label.service";
import {SharedModule} from "../shared/shared.module";
import {NoopAnimationsModule} from "@angular/platform-browser/animations";
import {FilterComponent} from "../filter/filter.component";
import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component";
import {CreateEditLabelComponent} from "../create-edit-label/create-edit-label.component";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {InlineAlertComponent} from "../inline-alert/inline-alert.component";
import {ErrorHandler} from "../error-handler/error-handler";
import {IServiceConfig, SERVICE_CONFIG} from "../service.config";
describe('LabelComponent (inline template)', () => {
let mockData: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}
];
let mockOneData: Label = {
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
}
let comp: LabelComponent;
let fixture: ComponentFixture<LabelComponent>;
let labelService: LabelService;
let spy: jasmine.Spy;
let spyOneLabel: jasmine.Spy;
let config: IServiceConfig = {
systemInfoEndpoint: '/api/label/testing'
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
NoopAnimationsModule
],
declarations: [
FilterComponent,
ConfirmationDialogComponent,
CreateEditLabelComponent,
LabelComponent,
LabelPieceComponent,
InlineAlertComponent
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{provide: LabelService, useClass: LabelDefaultService}
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(LabelComponent);
comp = fixture.componentInstance;
labelService = fixture.debugElement.injector.get(LabelService);
spy = spyOn(labelService, 'getLabels').and.returnValues(Promise.resolve(mockData));
spyOneLabel = spyOn(labelService, 'getLabel').and.returnValues(Promise.resolve(mockOneData));
fixture.detectChanges();
});
it('should retrieve label data', () => {
fixture.detectChanges();
expect(spy.calls.any()).toBeTruthy();
});
it('should open create label modal', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
comp.editLabel([mockOneData]);
fixture.detectChanges();
expect(comp.targets[0].name).toEqual('label0-g');
})
}));
/*it('should open to edit existing label', async() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
let de: DebugElement = fixture.debugElement.query(del => del.classes['active']);
expect(de).toBeTruthy();
fixture.detectChanges();
click(de);
fixture.detectChanges();
let deInput: DebugElement = fixture.debugElement.query(By.css['input']);
expect(deInput).toBeTruthy();
let elInput: HTMLElement = deInput.nativeElement;
expect(elInput).toBeTruthy();
expect(elInput.textContent).toEqual('label1-g');
})
})*/
})

View File

@ -0,0 +1,175 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
Component, OnInit, OnDestroy, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef,
Input
} from '@angular/core';
import {LABEL_TEMPLATE} from "./label.component.html";
import {LABEL_STYLE} from "./label.component.css";
import {Label} from "../service/interface";
import {LabelDefaultService, LabelService} from "../service/label.service";
import {toPromise} from "../utils";
import {ErrorHandler} from "../error-handler/error-handler";
import {CreateEditLabelComponent} from "../create-edit-label/create-edit-label.component";
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {ConfirmationMessage} from "../confirmation-dialog/confirmation-message";
import {ConfirmationButtons, ConfirmationState, ConfirmationTargets} from "../shared/shared.const";
import {ConfirmationAcknowledgement} from "../confirmation-dialog/confirmation-state-message";
import {TranslateService} from "@ngx-translate/core";
import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component";
@Component({
selector: 'hbr-label',
template: LABEL_TEMPLATE,
styles: [LABEL_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LabelComponent implements OnInit {
timerHandler: any;
loading: boolean;
targets: Label[];
targetName: string;
selectedRow: Label[] = [];
batchDelectionInfos: BatchInfo[] = [];
@Input() scope: string;
@Input() projectId = 0;
@Input() hasProjectAdminRole: boolean;
@ViewChild(CreateEditLabelComponent)
createEditLabel: CreateEditLabelComponent;
@ViewChild('confirmationDialog')
confirmationDialogComponent: ConfirmationDialogComponent;
constructor(
private labelService: LabelService,
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private ref: ChangeDetectorRef) {
}
ngOnInit(): void {
this.retrieve(this.scope);
}
retrieve(scope: string, name = '') {
this.loading = true;
this.selectedRow = [];
this.targetName = '';
toPromise<Label[]>(this.labelService.getLabels(scope, this.projectId, name))
.then(targets => {
this.targets = targets || [];
this.loading = false;
this.forceRefreshView(2000);
}).catch(error => {
this.errorHandler.error(error);
this.loading = false;
})
}
openModal(): void {
this.createEditLabel.openModal();
}
reload(): void {
this.retrieve(this.scope);
}
doSearchTargets(targetName: string) {
this.retrieve(this.scope, targetName);
}
refreshTargets() {
this.retrieve(this.scope);
}
selectedChange(): void {
// this.forceRefreshView(5000);
}
editLabel(label: Label[]): void {
this.createEditLabel.editModel(label[0].id, label);
}
deleteLabels(targets: Label[]): void {
if (targets && targets.length) {
let targetNames: string[] = [];
this.batchDelectionInfos = [];
targets.forEach(target => {
targetNames.push(target.name);
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = target.name;
this.batchDelectionInfos.push(initBatchMessage);
});
let deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE_TARGET',
'REPLICATION.DELETION_SUMMARY_TARGET',
targetNames.join(', ') || '',
targets,
ConfirmationTargets.TARGET,
ConfirmationButtons.DELETE_CANCEL);
this.confirmationDialogComponent.open(deletionMessage);
}
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
let targetLists: Label[] = message.data;
if (targetLists && targetLists.length) {
let promiseLists: any[] = [];
targetLists.forEach(target => {
promiseLists.push(this.delOperate(target.id, target.name));
})
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.retrieve(this.scope);
});
}
}
}
delOperate(id: number, name: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === name);
return toPromise<number>(this.labelService
.deleteLabel(id))
.then(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS')
.subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(
error => {
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
});
}
// Forcely refresh the view
forceRefreshView(duration: number): void {
// Reset timer
if (this.timerHandler) {
clearInterval(this.timerHandler);
}
this.timerHandler = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => {
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = null;
}
}, duration);
}
}

View File

@ -1,7 +1,7 @@
export const LIST_REPLICATION_RULE_TEMPLATE: string = ` export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<div style="padding-bottom: 15px;"> <div style="padding-bottom: 15px;">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true"> <clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true">
<clr-dg-action-bar style="height:24px;"> <clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="editRule(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'REPLICATION.EDIT_POLICY' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="editRule(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button>

View File

@ -22,8 +22,9 @@ import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
import { JobLogViewerComponent } from '../job-log-viewer/index'; import { JobLogViewerComponent } from '../job-log-viewer/index';
import { click } from '../utils'; import { click } from '../utils';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
describe('RepositoryComponentListview (inline template)', () => { describe('RepositoryComponentListView (inline template)', () => {
let compRepo: RepositoryListviewComponent; let compRepo: RepositoryListviewComponent;
let fixtureRepo: ComponentFixture<RepositoryListviewComponent>; let fixtureRepo: ComponentFixture<RepositoryListviewComponent>;
@ -82,7 +83,8 @@ describe('RepositoryComponentListview (inline template)', () => {
"docker_version": "1.12.3", "docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"), "created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null "signature": null,
"labels": []
} }
]; ];
@ -101,6 +103,7 @@ describe('RepositoryComponentListview (inline template)', () => {
declarations: [ declarations: [
RepositoryListviewComponent, RepositoryListviewComponent,
TagComponent, TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent, ConfirmationDialogComponent,
FilterComponent, FilterComponent,
VULNERABILITY_DIRECTIVES, VULNERABILITY_DIRECTIVES,

View File

@ -9,7 +9,7 @@ import { TagComponent } from '../tag/tag.component';
import { FilterComponent } from '../filter/filter.component'; import { FilterComponent } from '../filter/filter.component';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface'; import {Repository, RepositoryItem, Tag, SystemInfo, Label} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { TagService, TagDefaultService } from '../service/tag.service'; import { TagService, TagDefaultService } from '../service/tag.service';
@ -20,6 +20,8 @@ import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
import { JobLogViewerComponent } from '../job-log-viewer/index'; import { JobLogViewerComponent } from '../job-log-viewer/index';
import { click } from '../utils'; import { click } from '../utils';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
describe('RepositoryComponentStackview (inline template)', () => { describe('RepositoryComponentStackview (inline template)', () => {
@ -27,10 +29,12 @@ describe('RepositoryComponentStackview (inline template)', () => {
let fixtureRepo: ComponentFixture<RepositoryStackviewComponent>; let fixtureRepo: ComponentFixture<RepositoryStackviewComponent>;
let repositoryService: RepositoryService; let repositoryService: RepositoryService;
let tagService: TagService; let tagService: TagService;
let labelService: LabelService;
let systemInfoService: SystemInfoService; let systemInfoService: SystemInfoService;
let spyRepos: jasmine.Spy; let spyRepos: jasmine.Spy;
let spyTags: jasmine.Spy; let spyTags: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spySystemInfo: jasmine.Spy; let spySystemInfo: jasmine.Spy;
let mockSystemInfo: SystemInfo = { let mockSystemInfo: SystemInfo = {
@ -81,7 +85,31 @@ describe('RepositoryComponentStackview (inline template)', () => {
"docker_version": "1.12.3", "docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"), "created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null "signature": null,
"labels": []
}
];
let mockLabels: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
} }
]; ];
@ -99,6 +127,7 @@ describe('RepositoryComponentStackview (inline template)', () => {
declarations: [ declarations: [
RepositoryStackviewComponent, RepositoryStackviewComponent,
TagComponent, TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent, ConfirmationDialogComponent,
FilterComponent, FilterComponent,
VULNERABILITY_DIRECTIVES, VULNERABILITY_DIRECTIVES,
@ -111,7 +140,8 @@ describe('RepositoryComponentStackview (inline template)', () => {
{ provide: SERVICE_CONFIG, useValue: config }, { provide: SERVICE_CONFIG, useValue: config },
{ provide: RepositoryService, useClass: RepositoryDefaultService }, { provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: TagService, useClass: TagDefaultService }, { provide: TagService, useClass: TagDefaultService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService } { provide: SystemInfoService, useClass: SystemInfoDefaultService },
{provide: LabelService, useClass: LabelDefaultService}
] ]
}); });
})); }));
@ -127,6 +157,11 @@ describe('RepositoryComponentStackview (inline template)', () => {
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo)); spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo)); spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
labelService = fixtureRepo.debugElement.injector.get(LabelService);
spyLabels = spyOn(labelService, 'getLabels').and.returnValues(Promise.resolve(mockLabels));
fixtureRepo.detectChanges(); fixtureRepo.detectChanges();
}); });

View File

@ -46,7 +46,7 @@ export const REPOSITORY_TEMPLATE = `
</section> </section>
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'> <section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
<div id=images-container> <div id=images-container>
<hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)" class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId"></hbr-tag> <hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)" class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [isGuest]="isGuest" [projectId]="projectId"></hbr-tag>
</div> </div>
</section> </section>
</div> </div>

View File

@ -1,6 +1,6 @@
import { ComponentFixture, TestBed, async, } from '@angular/core/testing'; import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core'; import {Component, DebugElement} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
@ -16,12 +16,15 @@ import { JobLogViewerComponent } from '../job-log-viewer/index';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface'; import {Repository, RepositoryItem, Tag, SystemInfo, Label} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service'; import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
import { TagService, TagDefaultService } from '../service/tag.service'; import { TagService, TagDefaultService } from '../service/tag.service';
import { ChannelService } from '../channel/index'; import { ChannelService } from '../channel/index';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
class RouterStub { class RouterStub {
navigateByUrl(url: string) { return url; } navigateByUrl(url: string) { return url; }
@ -34,10 +37,13 @@ describe('RepositoryComponent (inline template)', () => {
let repositoryService: RepositoryService; let repositoryService: RepositoryService;
let systemInfoService: SystemInfoService; let systemInfoService: SystemInfoService;
let tagService: TagService; let tagService: TagService;
let labelService: LabelService;
let spyRepos: jasmine.Spy; let spyRepos: jasmine.Spy;
let spyTags: jasmine.Spy; let spyTags: jasmine.Spy;
let spySystemInfo: jasmine.Spy; let spySystemInfo: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let mockSystemInfo: SystemInfo = { let mockSystemInfo: SystemInfo = {
'with_notary': true, 'with_notary': true,
@ -87,10 +93,53 @@ describe('RepositoryComponent (inline template)', () => {
'docker_version': '1.12.3', 'docker_version': '1.12.3',
'author': 'NGINX Docker Maintainers \"docker-maint@nginx.com\"', 'author': 'NGINX Docker Maintainers \"docker-maint@nginx.com\"',
'created': new Date('2016-11-08T22:41:15.912313785Z'), 'created': new Date('2016-11-08T22:41:15.912313785Z'),
'signature': null 'signature': null,
'labels': []
} }
]; ];
let mockLabels: Label[] = [{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 1,
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}]
let mockLabels1: Label[] = [{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 1,
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 1,
scope: "p",
update_time: "",
}]
let config: IServiceConfig = { let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repository/testing', repositoryBaseEndpoint: '/api/repository/testing',
systemInfoEndpoint: '/api/systeminfo/testing', systemInfoEndpoint: '/api/systeminfo/testing',
@ -109,6 +158,7 @@ describe('RepositoryComponent (inline template)', () => {
ConfirmationDialogComponent, ConfirmationDialogComponent,
FilterComponent, FilterComponent,
TagComponent, TagComponent,
LabelPieceComponent,
VULNERABILITY_DIRECTIVES, VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES, PUSH_IMAGE_BUTTON_DIRECTIVES,
INLINE_ALERT_DIRECTIVES, INLINE_ALERT_DIRECTIVES,
@ -120,25 +170,31 @@ describe('RepositoryComponent (inline template)', () => {
{ provide: RepositoryService, useClass: RepositoryDefaultService }, { provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }, { provide: SystemInfoService, useClass: SystemInfoDefaultService },
{ provide: TagService, useClass: TagDefaultService }, { provide: TagService, useClass: TagDefaultService },
{ provide: LabelService, useClass: LabelDefaultService},
{ provide: ChannelService}, { provide: ChannelService},
] ]
}); });
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(RepositoryComponent); fixture = TestBed.createComponent(RepositoryComponent);
compRepo = fixture.componentInstance; compRepo = fixture.componentInstance;
compRepo.projectId = 1; compRepo.projectId = 1;
compRepo.hasProjectAdminRole = true; compRepo.hasProjectAdminRole = true;
compRepo.repoName = 'library/nginx'; compRepo.repoName = 'library/nginx';
repositoryService = fixture.debugElement.injector.get(RepositoryService); repositoryService = fixture.debugElement.injector.get(RepositoryService);
systemInfoService = fixture.debugElement.injector.get(SystemInfoService); systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
tagService = fixture.debugElement.injector.get(TagService); tagService = fixture.debugElement.injector.get(TagService);
labelService = fixture.debugElement.injector.get(LabelService);
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo)); spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo)); spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData)); spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData));
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -51,6 +51,7 @@ export class RepositoryComponent implements OnInit {
@Input() repoName: string; @Input() repoName: string;
@Input() hasSignedIn: boolean; @Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean; @Input() hasProjectAdminRole: boolean;
@Input() isGuest: boolean;
@Input() withNotary: boolean; @Input() withNotary: boolean;
@Input() withClair: boolean; @Input() withClair: boolean;
@Output() tagClickEvent = new EventEmitter<TagClickEvent>(); @Output() tagClickEvent = new EventEmitter<TagClickEvent>();

View File

@ -196,4 +196,16 @@ export interface IServiceConfig {
* @memberof IServiceConfig * @memberof IServiceConfig
*/ */
scanJobEndpoint?: string; scanJobEndpoint?: string;
/**
* The base endpoint of the service used to handle the labels.
* labels related endpoints will be built based on this endpoint.
* E.g:
* If the base endpoint is '/api/labels',
* the label endpoint will be '/api/labels/:id'.
*
* @type {string}
* @memberOf IServiceConfig
*/
labelEndpoint?: string;
} }

View File

@ -10,3 +10,4 @@ export * from './scanning.service';
export * from './configuration.service'; export * from './configuration.service';
export * from './job-log.service'; export * from './job-log.service';
export * from './project.service'; export * from './project.service';
export * from './label.service';

View File

@ -60,6 +60,7 @@ export interface Tag extends Base {
created: Date; created: Date;
signature?: string; signature?: string;
scan_overview?: VulnerabilitySummary; scan_overview?: VulnerabilitySummary;
labels: Label[];
} }
/** /**
@ -267,3 +268,12 @@ export interface TagClickEvent {
repository_name: string; repository_name: string;
tag_name: string; tag_name: string;
} }
export interface Label {
[key: string]: any | any[];
name: string;
description: string;
color: string;
scope: string;
project_id: number;
}

View File

@ -0,0 +1,125 @@
import {Observable} from "rxjs/Observable";
import {Label} from "./interface";
import {Inject, Injectable} from "@angular/core";
import {Http} from "@angular/http";
import {IServiceConfig, SERVICE_CONFIG} from "../service.config";
import {buildHttpRequestOptions, HTTP_JSON_OPTIONS} from "../utils";
import {RequestQueryParams} from "./RequestQueryParams";
export abstract class LabelService {
abstract getGLabels(name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]>;
abstract getPLabels(projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]>;
abstract getLabels(scope: string, projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]>;
abstract createLabel(label: Label): Observable<Label> | Promise<Label> | Label;
abstract getLabel(id: number): Observable<Label> | Promise<Label> | Label;
abstract updateLabel(id: number, param: Label): Observable<any> | Promise<any> | any;
abstract deleteLabel(id: number): Observable<any> | Promise<any> | any;
}
@Injectable()
export class LabelDefaultService extends LabelService {
_labelUrl: string;
constructor(
@Inject(SERVICE_CONFIG) config: IServiceConfig,
private http: Http
) {
super();
this._labelUrl = config.labelEndpoint ? config.labelEndpoint : "/api/labels";
}
getLabels(scope: string, projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]> {
if (!queryParams) {
queryParams = new RequestQueryParams();
}
if (scope) {
queryParams.set('scope', scope);
}
if (projectId) {
queryParams.set('project_id', '' + projectId);
}
if (name) {
queryParams.set('name', '' + name);
}
return this.http.get(this._labelUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
getGLabels(name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]> {
if (!queryParams) {
queryParams = new RequestQueryParams();
}
queryParams.set('scope', 'g');
if (name) {
queryParams.set('name', '' + name);
}
return this.http.get(this._labelUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
getPLabels(projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]> {
if (!queryParams) {
queryParams = new RequestQueryParams();
}
queryParams.set('scope', 'p');
if (projectId) {
queryParams.set('project_id', '' + projectId);
}
if (name) {
queryParams.set('name', '' + name);
}
return this.http.get(this._labelUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
createLabel(label: Label): Observable<any> | Promise<any> | any {
if (!label) {
return Promise.reject('Invalid label.');
}
return this.http.post(this._labelUrl, JSON.stringify(label), HTTP_JSON_OPTIONS).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
getLabel(id: number): Observable<Label> | Promise<Label> | Label {
if (!id || id <= 0) {
return Promise.reject('Bad request argument.');
}
let reqUrl = `${this._labelUrl}/${id}`
return this.http.get(reqUrl).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
updateLabel(id: number, label: Label): Observable<any> | Promise<any> | any {
if (!id || id <= 0) {
return Promise.reject('Bad request argument.');
}
if (!label) {
return Promise.reject('Invalid endpoint.');
}
let reqUrl = `${this._labelUrl}/${id}`
return this.http.put(reqUrl, JSON.stringify(label), HTTP_JSON_OPTIONS).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
deleteLabel(id: number): Observable<any> | Promise<any> | any {
if (!id || id <= 0) {
return Promise.reject('Bad request argument.');
}
let reqUrl = `${this._labelUrl}/${id}`
return this.http.delete(reqUrl).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
}

View File

@ -20,7 +20,8 @@ describe('TagService', () => {
"docker_version": "1.12.3", "docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"), "created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null "signature": null,
'labels': []
} }
]; ];

View File

@ -1,6 +1,6 @@
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RequestQueryParams } from './RequestQueryParams'; import { RequestQueryParams } from './RequestQueryParams';
import { Tag } from './interface'; import {Label, Tag} from './interface';
import { Injectable, Inject } from "@angular/core"; import { Injectable, Inject } from "@angular/core";
import 'rxjs/add/observable/of'; import 'rxjs/add/observable/of';
import { Http } from '@angular/http'; import { Http } from '@angular/http';
@ -65,6 +65,9 @@ export abstract class TagService {
* @memberOf TagService * @memberOf TagService
*/ */
abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag; abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag;
abstract addLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any;
abstract deleteLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any;
} }
/** /**
@ -77,13 +80,14 @@ export abstract class TagService {
@Injectable() @Injectable()
export class TagDefaultService extends TagService { export class TagDefaultService extends TagService {
_baseUrl: string; _baseUrl: string;
_labelUrl: string;
constructor( constructor(
private http: Http, private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig @Inject(SERVICE_CONFIG) private config: IServiceConfig
) { ) {
super(); super();
this._baseUrl = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories'; this._baseUrl = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
this._labelUrl = this.config.labelEndpoint? this.config.labelEndpoint : '/api/labels';
} }
//Private methods //Private methods
@ -136,4 +140,28 @@ export class TagDefaultService extends TagService {
.then(response => response.json() as Tag) .then(response => response.json() as Tag)
.catch(error => Promise.reject(error)); .catch(error => Promise.reject(error));
} }
public addLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any {
if (!labelId || !tagName || !repoName) {
return Promise.reject('Invalid parameters.');
}
let _addLabelToImageUrl = `/api/repositories/${repoName}/tags/${tagName}/labels`;
return this.http.post(_addLabelToImageUrl, {id: labelId}, HTTP_JSON_OPTIONS).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
public deleteLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any {
if (!labelId || !tagName || !repoName) {
return Promise.reject('Invalid parameters.');
}
let _addLabelToImageUrl = `/api/repositories/${repoName}/tags/${tagName}/labels/${labelId}`;
return this.http.delete(_addLabelToImageUrl).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
} }

View File

@ -50,7 +50,8 @@ describe('TagDetailComponent (inline template)', () => {
"author": "steven", "author": "steven",
"created": new Date("2016-11-08T22:41:15.912313785Z"), "created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null, "signature": null,
scan_overview: mockVulnerability "scan_overview": mockVulnerability,
"labels": [],
}; };
let config: IServiceConfig = { let config: IServiceConfig = {

View File

@ -30,7 +30,8 @@ export class TagDetailComponent implements OnInit {
architecture: "--", architecture: "--",
os: "--", os: "--",
docker_version: "--", docker_version: "--",
digest: "--" digest: "--",
labels: [],
}; };
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>(); @Output() backEvt: EventEmitter<any> = new EventEmitter<any>();

View File

@ -50,4 +50,20 @@ export const TAG_STYLE = `
right: 35px; right: 35px;
margin-top: 4px; margin-top: 4px;
} }
.btn-group .dropdown-menu clr-icon{display: block;}
.dropdown-menu .dropdown-item{position: relative;padding-left:.5rem; padding-right:.5rem;}
.dropdown-menu input{position: relative;margin-left:.5rem; margin-right:.5rem;}
.pull-left{display:inline-block;float:left;}
.pull-right{display:inline-block; float:right;}
.btn-link{display:inline-flex;width: 15px;min-width:15px; color:black; vertical-align: super; }
.trigger-item, .signpost-item{display: inline;}
.signpost-content-body .label{margin:.3rem;}
.labelDiv{position: absolute; left:34px;top:3px;}
.datagrid-action-bar{z-index:10;}
.trigger-item hbr-label-piece{display: flex !important;margin: 6px 0;}
:host >>> .signpost-content{min-width:4rem;}
:host >>> .signpost-content-body{padding:0 .4rem;}
:host >>> .signpost-content-header{display:none;}
.filterLabelPiece{position: absolute; bottom :0px;z-index:1;}
`; `;

View File

@ -15,8 +15,25 @@ export const TAG_TEMPLATE = `
<div class="row" style="position:relative;"> <div class="row" style="position:relative;">
<div> <div>
<div class="row flex-items-xs-right rightPos"> <div class="row flex-items-xs-right rightPos">
<div class='filterLabelPiece' [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel"></hbr-label-piece></div>
<div class="flex-xs-middle"> <div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName"></hbr-filter> <clr-dropdown>
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName" clrDropdownTrigger></hbr-filter>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #labelNamePiece (keyup)="handleInputFilter(labelNamePiece.value)"></div>
<div [hidden]='imageFilterLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="dropdown-item" *ngFor='let label of imageFilterLabels' (click)="label.iconsShow = true; filterLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); label.iconsShow = false; unFilterLabel(label)"></clr-icon>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span> <span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div> </div>
</div> </div>
@ -24,26 +41,46 @@ export const TAG_TEMPLATE = `
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()"> <clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar> <clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button> <button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" >{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #stickLabelNamePiece (keyup)="handleStickInputFilter(stickLabelNamePiece.value)"></div>
<div [hidden]='imageStickLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="label.iconsShow = true; selectLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); label.iconsShow = false; unSelectLabel(label)"></clr-icon>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<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> <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>
</div>
</clr-dg-action-bar> </clr-dg-action-bar>
<clr-dg-column style="width: 160px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column> <clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column> <clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 120px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column> <clr-dg-column style="min-width: 100px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" *ngIf="withClair">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column> <clr-dg-column style="width: 140px;" *ngIf="withClair">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column> <clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 130px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column> <clr-dg-column style="min-width: 130px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 150px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column> <clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column> <clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" [clrDgField]="'labels'">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder> <clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'> <clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated" style="width: 160px;" [ngSwitch]="withClair"> <clr-dg-cell class="truncated" style="width: 120px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a> <a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngSwitchDefault>{{t.name}}</span> <span *ngSwitchDefault>{{t.name}}</span>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell style="width: 90px;">{{sizeTransform(t.size)}}</clr-dg-cell> <clr-dg-cell style="width: 90px;">{{sizeTransform(t.size)}}</clr-dg-cell>
<clr-dg-cell style="min-width: 120px; max-width:220px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"> <clr-dg-cell style="min-width: 100px; max-width:220px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input> <hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell style="width: 140px;" *ngIf="withClair"> <clr-dg-cell style="width: 140px;" *ngIf="withClair">
@ -58,8 +95,23 @@ export const TAG_TEMPLATE = `
</a> </a>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell class="truncated" style="min-width: 130px;" title="{{t.author}}">{{t.author}}</clr-dg-cell> <clr-dg-cell class="truncated" style="min-width: 130px;" title="{{t.author}}">{{t.author}}</clr-dg-cell>
<clr-dg-cell style="width: 150px;">{{t.created | date: 'short'}}</clr-dg-cell> <clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell style="width: 140px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell> <clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell style="width: 140px;">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]"></hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div>
<hbr-label-piece *ngFor="let label of t.labels" [label]="label"></hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
</div>
</div>
</clr-dg-cell>
</clr-dg-row> </clr-dg-row>
<clr-dg-footer> <clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span> <span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>

View File

@ -8,7 +8,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { TagComponent } from './tag.component'; import { TagComponent } from './tag.component';
import { ErrorHandler } from '../error-handler/error-handler'; import { ErrorHandler } from '../error-handler/error-handler';
import { Tag } from '../service/interface'; import {Label, Tag} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index'; import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index'; import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
@ -19,6 +19,8 @@ import { ChannelService } from '../channel/index';
import { JobLogViewerComponent } from '../job-log-viewer/index'; import { JobLogViewerComponent } from '../job-log-viewer/index';
import {CopyInputComponent} from "../push-image/copy-input.component"; import {CopyInputComponent} from "../push-image/copy-input.component";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
describe('TagComponent (inline template)', () => { describe('TagComponent (inline template)', () => {
@ -26,6 +28,8 @@ describe('TagComponent (inline template)', () => {
let fixture: ComponentFixture<TagComponent>; let fixture: ComponentFixture<TagComponent>;
let tagService: TagService; let tagService: TagService;
let spy: jasmine.Spy; let spy: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let mockTags: Tag[] = [ let mockTags: Tag[] = [
{ {
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
@ -36,7 +40,54 @@ describe('TagComponent (inline template)', () => {
"docker_version": "1.12.3", "docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"), "created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null "signature": null,
"labels": [],
}
];
let mockLabels: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}
];
let mockLabels1: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 1,
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 1,
scope: "p",
update_time: "",
} }
]; ];
@ -51,6 +102,7 @@ describe('TagComponent (inline template)', () => {
], ],
declarations: [ declarations: [
TagComponent, TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent, ConfirmationDialogComponent,
VULNERABILITY_DIRECTIVES, VULNERABILITY_DIRECTIVES,
FILTER_DIRECTIVES, FILTER_DIRECTIVES,
@ -62,7 +114,8 @@ describe('TagComponent (inline template)', () => {
ChannelService, ChannelService,
{ provide: SERVICE_CONFIG, useValue: config }, { provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService }, { provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService } { provide: ScanningResultService, useClass: ScanningResultDefaultService },
{provide: LabelService, useClass: LabelDefaultService}
] ]
}); });
})); }));
@ -78,8 +131,25 @@ describe('TagComponent (inline template)', () => {
comp.registryUrl = 'http://registry.testing.com'; comp.registryUrl = 'http://registry.testing.com';
comp.withNotary = false; comp.withNotary = false;
let labelService: LabelService;
tagService = fixture.debugElement.injector.get(TagService); 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, 'getLabels').and.callFake(function (param) {
if (param === 'g') {
return Promise.resolve(mockLabels);
}else {
Promise.resolve(mockLabels1)
}
})*/
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -20,7 +20,7 @@ import {
EventEmitter, EventEmitter,
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
ElementRef ElementRef, AfterContentInit, AfterViewInit
} from "@angular/core"; } from "@angular/core";
import { TagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index"; import { TagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index";
@ -36,7 +36,7 @@ import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message"; import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message"; import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
import { Tag, TagClickEvent } from "../service/interface"; import {Label, Tag, TagClickEvent} from "../service/interface";
import { TAG_TEMPLATE } from "./tag.component.html"; import { TAG_TEMPLATE } from "./tag.component.html";
import { TAG_STYLE } from "./tag.component.css"; import { TAG_STYLE } from "./tag.component.css";
@ -48,7 +48,8 @@ import {
doFiltering, doFiltering,
doSorting, doSorting,
VULNERABILITY_SCAN_STATUS, VULNERABILITY_SCAN_STATUS,
DEFAULT_PAGE_SIZE DEFAULT_PAGE_SIZE,
clone,
} from "../utils"; } from "../utils";
import { TranslateService } from "@ngx-translate/core"; import { TranslateService } from "@ngx-translate/core";
@ -57,6 +58,8 @@ import { State, Comparator } from "clarity-angular";
import {CopyInputComponent} from "../push-image/copy-input.component"; import {CopyInputComponent} from "../push-image/copy-input.component";
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message"; import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable"; import {Observable} from "rxjs/Observable";
import {LabelService} from "../service/label.service";
import {Subject} from "rxjs/Subject";
@Component({ @Component({
selector: "hbr-tag", selector: "hbr-tag",
@ -64,7 +67,7 @@ import {Observable} from "rxjs/Observable";
styles: [TAG_STYLE], styles: [TAG_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class TagComponent implements OnInit { export class TagComponent implements OnInit, AfterViewInit {
signedCon: {[key: string]: any | string[]} = {}; signedCon: {[key: string]: any | string[]} = {};
@Input() projectId: number; @Input() projectId: number;
@ -73,6 +76,7 @@ export class TagComponent implements OnInit {
@Input() hasSignedIn: boolean; @Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean; @Input() hasProjectAdminRole: boolean;
@Input() isGuest: boolean;
@Input() registryUrl: string; @Input() registryUrl: string;
@Input() withNotary: boolean; @Input() withNotary: boolean;
@Input() withClair: boolean; @Input() withClair: boolean;
@ -98,7 +102,27 @@ export class TagComponent implements OnInit {
copyFailed = false; copyFailed = false;
selectedRow: Tag[] = []; selectedRow: Tag[] = [];
@ViewChild("confirmationDialog") imageLabels: {[key: string]: boolean | Label | any}[] = [];
imageStickLabels: {[key: string]: boolean | Label | any}[] = [];
imageFilterLabels: {[key: string]: boolean | Label | any}[] = [];
labelListOpen = false;
selectedTag: Tag[];
labelNameFilter: Subject<string> = new Subject<string> ();
stickLabelNameFilter: Subject<string> = new Subject<string> ();
filterOnGoing: boolean;
initFilter = {
name: '',
description: '',
color: '',
scope: '',
project_id: 0,
}
filterOneLabel: Label = this.initFilter;
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent; confirmationDialog: ConfirmationDialogComponent;
@ViewChild("digestTarget") textInput: ElementRef; @ViewChild("digestTarget") textInput: ElementRef;
@ -112,6 +136,7 @@ export class TagComponent implements OnInit {
constructor( constructor(
private errorHandler: ErrorHandler, private errorHandler: ErrorHandler,
private tagService: TagService, private tagService: TagService,
private labelService: LabelService,
private translateService: TranslateService, private translateService: TranslateService,
private ref: ChangeDetectorRef, private ref: ChangeDetectorRef,
private channel: ChannelService private channel: ChannelService
@ -128,14 +153,57 @@ export class TagComponent implements OnInit {
} }
this.retrieve(); this.retrieve();
this.lastFilteredTagName = ""; this.lastFilteredTagName = '';
this.labelNameFilter
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
if (name && name.length) {
this.filterOnGoing = true;
this.imageFilterLabels = [];
this.imageLabels.forEach(data => {
if (data.label.name.indexOf(name) !== -1) {
this.imageFilterLabels.push(data);
}
})
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 200);
}, 1000);
}
});
this.stickLabelNameFilter
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
if (name && name.length) {
this.filterOnGoing = true;
this.imageFilterLabels = [];
this.imageLabels.forEach(data => {
if (data.label.name.indexOf(name) !== -1) {
this.imageFilterLabels.push(data);
}
})
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 200);
}, 1000);
}
});
} }
selectedChange(): void { ngAfterViewInit() {
let hnd = setInterval(() => this.ref.markForCheck(), 200); this.getAllLabels();
setTimeout(() => clearInterval(hnd), 2000);
} }
public get filterLabelPieceWidth() {
let len = this.lastFilteredTagName.length ? this.lastFilteredTagName.length * 6 + 60 : 115;
return len > 210 ? 210 : len;
}
doSearchTagNames(tagName: string) { doSearchTagNames(tagName: string) {
this.lastFilteredTagName = tagName; this.lastFilteredTagName = tagName;
this.currentPage = 1; this.currentPage = 1;
@ -191,7 +259,139 @@ export class TagComponent implements OnInit {
this.doSearchTagNames(""); this.doSearchTagNames("");
} }
getAllLabels(): void {
toPromise<Label[]>(this.labelService.getGLabels()).then((res: Label[]) => {
if (res.length) {
res.forEach(data => {
this.imageLabels.push({'iconsShow': false, 'label': data});
});
}
toPromise<Label[]>(this.labelService.getPLabels(this.projectId)).then((res1: Label[]) => {
if (res1.length) {
res1.forEach(data => {
this.imageLabels.push({'iconsShow': false, 'label': data});
});
}
this.imageFilterLabels = clone(this.imageLabels);
this.imageStickLabels = clone(this.imageLabels);
}).catch(error => {
this.errorHandler.error(error);
});
}).catch(error => {
this.errorHandler.error(error);
});
}
selectedChange(tag?: Tag[]): void {
if (tag && tag[0].labels && tag[0].labels.length) {
tag[0].labels.forEach((labelInfo: Label) => {
this.imageStickLabels.forEach(data => {
if (labelInfo.id === data['label'].id) {
data.iconsShow = true;
}
});
});
}
}
addLabels(tag: Tag[]): void {
this.labelListOpen = true;
this.selectedTag = tag;
this.selectedChange(tag);
}
selectLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && labelInfo.iconsShow) {
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
toPromise<any>(this.tagService.addLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
this.refresh();
}).catch(err => {
this.errorHandler.error(err);
});
}
}
unSelectLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && !labelInfo.iconsShow) {
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
toPromise<any>(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
this.refresh();
}).catch(err => {
this.errorHandler.error(err);
});
}
}
filterLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && labelInfo.iconsShow) {
let labelName = labelInfo.label.name;
this.imageFilterLabels.filter(data => {
if (data.label.name !== labelName) {
data.iconsShow = false;
}
});
this.filterOneLabel = labelInfo.label;
// reload datagu
this.currentPage = 1;
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = 0;
st.page.to = this.pageSize - 1;
if (this.lastFilteredTagName) {
st.filters = [{property: 'name', value: this.lastFilteredTagName}, {property: 'labels.name', value: labelName}];
}else {
st.filters = [{property: 'labels.name', value: labelName}];
}
this.clrLoad(st);
}
}
unFilterLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && !labelInfo.iconsShow) {
this.filterOneLabel = this.initFilter;
// reload datagu
this.currentPage = 1;
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = 0;
st.page.to = this.pageSize - 1;
if (this.lastFilteredTagName) {
st.filters = [{property: 'name', value: this.lastFilteredTagName}];
}else {
st.filters = [];
}
this.clrLoad(st);
}
}
handleInputFilter($event: string) {
if ($event && $event.length) {
this.labelNameFilter.next($event);
}else {
this.imageFilterLabels = clone(this.imageLabels);
}
}
handleStickInputFilter($event: string) {
if ($event && $event.length) {
this.stickLabelNameFilter.next($event);
}else {
this.imageStickLabels = clone(this.imageLabels);
}
}
retrieve() { retrieve() {
this.tags = []; this.tags = [];

View File

@ -179,7 +179,18 @@ export function doFiltering<T extends { [key: string]: any | any[] }>(items: T[]
property: string; property: string;
value: string; value: string;
}) => { }) => {
items = items.filter(item => regexpFilter(filter["value"], item[filter["property"]])); items = items.filter(item => {
if (filter['property'].indexOf('.') !== -1) {
let arr = filter['property'].split('.');
if (Array.isArray(item[arr[0]]) && item[arr[0]].length) {
return item[arr[0]].some((data: any) => {
return regexpFilter(filter['value'], data[arr[1]]);
});
}
}else {
return regexpFilter(filter['value'], item[filter['property']]);
}
});
}); });
return items; return items;

View File

@ -27,11 +27,11 @@
"@ngx-translate/http-loader": "0.0.3", "@ngx-translate/http-loader": "0.0.3",
"@types/jquery": "^2.0.41", "@types/jquery": "^2.0.41",
"@webcomponents/custom-elements": "^1.0.0", "@webcomponents/custom-elements": "^1.0.0",
"clarity-angular": "^0.10.17", "clarity-angular": "^0.10.27",
"clarity-icons": "^0.10.17", "clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.17", "clarity-ui": "^0.10.27",
"core-js": "^2.4.1", "core-js": "^2.4.1",
"harbor-ui": "0.6.47", "harbor-ui": "0.6.53",
"intl": "^1.2.5", "intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2", "mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0", "ngx-cookie": "^1.0.0",

View File

@ -12,6 +12,9 @@
<li role="presentation" class="nav-item"> <li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button> <button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li> </li>
<li role="presentation" class="nav-item">
<button id="config-label" class="btn btn-link nav-link" aria-controls="system_label" [class.active]='isCurrentTabLink("config-label")' type="button" (click)='tabLinkClick("config-label")'>{{'CONFIG.LABEL' | translate }}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="withClair"> <li role="presentation" class="nav-item" *ngIf="withClair">
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button> <button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
</li> </li>
@ -25,12 +28,16 @@
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'> <section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings> <system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
</section> </section>
<section id="system_label" role="tabpanel" aria-labelledby="config-label" [hidden]='!isCurrentTabContent("system_label")' style="padding-top: 16px;">
<hbr-label [scope]="'g'"></hbr-label>
<!--<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>-->
</section>
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'> <section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config> <vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
</section> </section>
<div> <div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button> <button type="button" class="btn btn-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button> <button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button> <button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button> <button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span> <span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>

View File

@ -38,7 +38,8 @@ const TabLinkContentMap = {
'config-replication': 'replication', 'config-replication': 'replication',
'config-email': 'email', 'config-email': 'email',
'config-system': 'system_settings', 'config-system': 'system_settings',
'config-vulnerability': 'vulnerability' 'config-vulnerability': 'vulnerability',
'config-label': 'system_label'
}; };
@Component({ @Component({
@ -200,6 +201,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
this.allConfig.auth_mode.value === 'ldap_auth'; this.allConfig.auth_mode.value === 'ldap_auth';
} }
public get hideBtn(): boolean {
return this.currentTabId === 'config-label';
}
public get hideMailTestingSpinner(): boolean { public get hideMailTestingSpinner(): boolean {
return !this.testingMailOnGoing || !this.showTestServerBtn; return !this.testingMailOnGoing || !this.showTestServerBtn;
} }

View File

@ -51,6 +51,7 @@ import { MemberGuard } from './shared/route/member-guard-activate.service';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component'; import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service'; import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service';
import {ProjectLabelComponent} from "./project/project-label/project-label.component";
const harborRoutes: Routes = [ const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' }, { path: '', redirectTo: 'harbor', pathMatch: 'full' },
@ -138,6 +139,9 @@ const harborRoutes: Routes = [
{ {
path: 'logs', path: 'logs',
component: AuditLogComponent component: AuditLogComponent
},{
path: 'labels',
component: ProjectLabelComponent
}, },
{ {
path: 'configs', path: 'configs',

View File

@ -10,12 +10,15 @@
<li class="nav-item" *ngIf="isSystemAdmin || isMember"> <li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a> <a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin"> <li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a> <a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="labels" routerLinkActive="active">{{'PROJECT_DETAIL.LABELS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && (isSystemAdmin || isMember)"> <li class="nav-item" *ngIf="isSessionValid && (isSystemAdmin || isMember)">
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a> <a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
</li> </li>

View File

@ -0,0 +1,5 @@
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="top: 8px;">
<hbr-label [projectId]="projectId" [scope]="'p'" [hasProjectAdminRole]="hasProjectAdminRole"></hbr-label>
</div>
</div>

View File

@ -0,0 +1,49 @@
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { SessionService } from '../../shared/session.service';
import { SessionUser } from '../../shared/session-user';
import { Project } from '../project';
@Component({
selector: 'app-project-config',
templateUrl: './project-label.component.html',
styleUrls: ['./project-label.component.css']
})
export class ProjectLabelComponent implements OnInit {
projectId: number;
projectName: string;
currentUser: SessionUser;
hasSignedIn: boolean;
hasProjectAdminRole: boolean;
constructor(
private route: ActivatedRoute,
private router: Router,
private session: SessionService) {}
ngOnInit() {
this.projectId = +this.route.snapshot.parent.params['id'];
this.currentUser = this.session.getCurrentUser();
this.hasSignedIn = this.session.getCurrentUser() !== null;
let resolverData = this.route.snapshot.parent.data;
if (resolverData) {
let pro: Project = <Project>resolverData['projectResolver'];
this.hasProjectAdminRole = pro.has_project_admin_role;
this.projectName = pro.name;
}
}
}

View File

@ -32,6 +32,7 @@ import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service'; import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive'; import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
import {ProjectLabelComponent} from "../project/project-label/project-label.component";
@NgModule({ @NgModule({
imports: [ imports: [
@ -48,7 +49,8 @@ import { TargetExistsValidatorDirective } from '../shared/target-exists-directiv
ProjectDetailComponent, ProjectDetailComponent,
MemberComponent, MemberComponent,
AddMemberComponent, AddMemberComponent,
TargetExistsValidatorDirective TargetExistsValidatorDirective,
ProjectLabelComponent
], ],
exports: [ProjectComponent, ListProjectComponent], exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService] providers: [ProjectRoutingResolver, ProjectService, MemberService]

View File

@ -1,3 +1,3 @@
<div> <div>
<hbr-repository (tagClickEvent)="watchTagClickEvt($event)" (backEvt)="goBack($event)" [repoName]="repoName" [withClair]="withClair" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId"></hbr-repository> <hbr-repository (tagClickEvent)="watchTagClickEvt($event)" (backEvt)="goBack($event)" [repoName]="repoName" [withClair]="withClair" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [isGuest]="isGuest" [projectId]="projectId"></hbr-repository>
</div> </div>

View File

@ -30,6 +30,7 @@ export class TagRepositoryComponent implements OnInit {
projectId: number; projectId: number;
repoName: string; repoName: string;
hasProjectAdminRole: boolean = false; hasProjectAdminRole: boolean = false;
isGuest: boolean;
registryUrl: string; registryUrl: string;
@ViewChild(RepositoryComponent) @ViewChild(RepositoryComponent)
@ -47,11 +48,12 @@ export class TagRepositoryComponent implements OnInit {
if (!this.projectId) { if (!this.projectId) {
this.projectId = this.route.snapshot.parent.params['id']; this.projectId = this.route.snapshot.parent.params['id'];
}; };
// let resolverData = this.route.snapshot.parent.data;
let resolverData = this.route.snapshot.data; let resolverData = this.route.snapshot.data;
if (resolverData) { if (resolverData) {
this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role; this.hasProjectAdminRole = (<Project>resolverData['projectResolver']).has_project_admin_role;
this.isGuest = (<Project>resolverData['projectResolver']).current_user_role_id === 3;
} }
this.repoName = this.route.snapshot.params['repo']; this.repoName = this.route.snapshot.params['repo'];

View File

@ -178,6 +178,7 @@
"REPLICATION": "Replication", "REPLICATION": "Replication",
"USERS": "Members", "USERS": "Members",
"LOGS": "Logs", "LOGS": "Logs",
"LABELS": "labels",
"PROJECTS": "Projects", "PROJECTS": "Projects",
"CONFIG": "Configuration" "CONFIG": "Configuration"
}, },
@ -411,7 +412,10 @@
"PLACEHOLDER": "We couldn't find any repositories!", "PLACEHOLDER": "We couldn't find any repositories!",
"INFO": "Info", "INFO": "Info",
"NO_INFO": "No description info for this repository", "NO_INFO": "No description info for this repository",
"IMAGE": "Images" "IMAGE": "Images",
"LABELS": ":labels",
"ADD_TO_IMAGE": "Add labels to this image",
"ADD_LABELS": "Add labels"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?" "FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
@ -433,6 +437,7 @@
"AUTH": "Authentication", "AUTH": "Authentication",
"REPLICATION": "Replication", "REPLICATION": "Replication",
"EMAIL": "Email", "EMAIL": "Email",
"LABEL": "Label",
"SYSTEM": "System Settings", "SYSTEM": "System Settings",
"VULNERABILITY": "Vulnerability", "VULNERABILITY": "Vulnerability",
"CONFIRM_TITLE": "Confirm to cancel", "CONFIRM_TITLE": "Confirm to cancel",
@ -612,6 +617,18 @@
"COPY_ERROR": "Copy failed, please try to manually copy.", "COPY_ERROR": "Copy failed, please try to manually copy.",
"FILTER_FOR_TAGS": "Filter Tags" "FILTER_FOR_TAGS": "Filter Tags"
}, },
"LABEL": {
"LABEL": "Label",
"DESCRIPTION": "Description",
"CREATION_TIME": "Creation Time",
"NEW_LABEL": "New Label",
"EDIT": "Edit",
"DELETE": "Delete",
"LABEL_NAME": "Label Name",
"COLOR": "Color",
"FILTER_LABEL_PLACEHOLDER": "Filter Labels",
"NO_LABELS": "No labels"
},
"WEEKLY": { "WEEKLY": {
"MONDAY": "Monday", "MONDAY": "Monday",
"TUESDAY": "Tuesday", "TUESDAY": "Tuesday",

View File

@ -178,6 +178,7 @@
"REPLICATION": "Replicación", "REPLICATION": "Replicación",
"USERS": "Miembros", "USERS": "Miembros",
"LOGS": "Logs", "LOGS": "Logs",
"LABELS": "labels",
"PROJECTS": "Proyectos", "PROJECTS": "Proyectos",
"CONFIG": "Configuración" "CONFIG": "Configuración"
}, },
@ -411,7 +412,10 @@
"PLACEHOLDER": "We couldn't find any repositories!", "PLACEHOLDER": "We couldn't find any repositories!",
"INFO": "Información", "INFO": "Información",
"NO_INFO": "Sin información de descripción para este repositorio", "NO_INFO": "Sin información de descripción para este repositorio",
"IMAGE": "Imágenes" "IMAGE": "Imágenes",
"LABELS": ":labels",
"ADD_TO_IMAGE": "Add labels to this image",
"ADD_LABELS": "Add labels"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?" "FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
@ -433,6 +437,7 @@
"AUTH": "Autentificación", "AUTH": "Autentificación",
"REPLICATION": "Replicación", "REPLICATION": "Replicación",
"EMAIL": "Email", "EMAIL": "Email",
"LABEL": "Label",
"SYSTEM": "Opciones del Sistema", "SYSTEM": "Opciones del Sistema",
"VULNERABILITY": "Vulnerability", "VULNERABILITY": "Vulnerability",
"CONFIRM_TITLE": "Confirma cancelación", "CONFIRM_TITLE": "Confirma cancelación",
@ -612,6 +617,18 @@
"COPY_ERROR": "Copy failed, please try to manually copy.", "COPY_ERROR": "Copy failed, please try to manually copy.",
"FILTER_FOR_TAGS": "Etiquetas de filtro" "FILTER_FOR_TAGS": "Etiquetas de filtro"
}, },
"LABEL": {
"LABEL": "Label",
"DESCRIPTION": "Description",
"CREATION_TIME": "Creation Time",
"NEW_LABEL": "New Label",
"EDIT": "Edit",
"DELETE": "Delete",
"LABEL_NAME": "Label Name",
"COLOR": "Color",
"FILTER_Label_PLACEHOLDER": "Filter Labels",
"NO_LABELS": "No labels"
},
"WEEKLY": { "WEEKLY": {
"MONDAY": "Monday", "MONDAY": "Monday",
"TUESDAY": "Tuesday", "TUESDAY": "Tuesday",

View File

@ -386,6 +386,7 @@
"AUTH": "Identification", "AUTH": "Identification",
"REPLICATION": "Réplication", "REPLICATION": "Réplication",
"EMAIL": "Email", "EMAIL": "Email",
"LABEL": "Label",
"SYSTEM": "Réglages Système", "SYSTEM": "Réglages Système",
"CONFIRM_TITLE": "Confirmer pour annuler", "CONFIRM_TITLE": "Confirmer pour annuler",
"CONFIRM_SUMMARY": "Certaines modifications n'ont pas été sauvegardées. Voulez-vous les défaire ?", "CONFIRM_SUMMARY": "Certaines modifications n'ont pas été sauvegardées. Voulez-vous les défaire ?",
@ -554,6 +555,11 @@
"PLACEHOLDER": "Nous ne trouvons aucun tag !", "PLACEHOLDER": "Nous ne trouvons aucun tag !",
"COPY_ERROR": "Copie échouée, veuillez essayer de copier manuellement." "COPY_ERROR": "Copie échouée, veuillez essayer de copier manuellement."
}, },
"LABEL": {
"LABEL": "Label",
"DESCRIPTION": "Description",
"CREATION_TIME": "Creation Time"
},
"UNKNOWN_ERROR": "Des erreurs inconnues sont survenues. Veuillez réessayer plus tard.", "UNKNOWN_ERROR": "Des erreurs inconnues sont survenues. Veuillez réessayer plus tard.",
"UNAUTHORIZED_ERROR": "Votre session est invalide ou a expiré. Vous devez vous connecter pour continuer votre action.", "UNAUTHORIZED_ERROR": "Votre session est invalide ou a expiré. Vous devez vous connecter pour continuer votre action.",
"FORBIDDEN_ERROR": "Vous n'avez pas les privilèges appropriés pour effectuer l'action.", "FORBIDDEN_ERROR": "Vous n'avez pas les privilèges appropriés pour effectuer l'action.",

View File

@ -178,6 +178,7 @@
"REPLICATION": "复制", "REPLICATION": "复制",
"USERS": "成员", "USERS": "成员",
"LOGS": "日志", "LOGS": "日志",
"LABELS": "标签",
"PROJECTS": "项目", "PROJECTS": "项目",
"CONFIG": "配置管理" "CONFIG": "配置管理"
}, },
@ -411,7 +412,10 @@
"PLACEHOLDER": "未发现任何镜像库!", "PLACEHOLDER": "未发现任何镜像库!",
"INFO": "描述信息", "INFO": "描述信息",
"NO_INFO": "此镜像仓库没有描述信息", "NO_INFO": "此镜像仓库没有描述信息",
"IMAGE": "镜像" "IMAGE": "镜像",
"LABELS": "标签",
"ADD_TO_IMAGE": "添加标签到此镜像",
"ADD_LABELS": "添加标签"
}, },
"ALERT": { "ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?" "FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
@ -433,6 +437,7 @@
"AUTH": "认证模式", "AUTH": "认证模式",
"REPLICATION": "复制", "REPLICATION": "复制",
"EMAIL": "邮箱", "EMAIL": "邮箱",
"LABEL": "标签",
"SYSTEM": "系统设置", "SYSTEM": "系统设置",
"VULNERABILITY": "漏洞", "VULNERABILITY": "漏洞",
"CONFIRM_TITLE": "确认取消", "CONFIRM_TITLE": "确认取消",
@ -612,6 +617,18 @@
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。", "COPY_ERROR": "拷贝失败,请尝试手动拷贝。",
"FILTER_FOR_TAGS": "过滤项目" "FILTER_FOR_TAGS": "过滤项目"
}, },
"LABEL": {
"LABEL": "标签",
"DESCRIPTION": "描述",
"CREATION_TIME": "创建时间",
"NEW_LABEL": "新建标签",
"EDIT": "编辑",
"DELETE": "删除",
"LABEL_NAME": "标签名字",
"COLOR": "颜色",
"FILTER_Label_PLACEHOLDER": "过滤标签",
"NO_LABELS": "无标签"
},
"WEEKLY": { "WEEKLY": {
"MONDAY": "周一", "MONDAY": "周一",
"TUESDAY": "周二", "TUESDAY": "周二",