mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-20 23:57:42 +01:00
Add label module
This commit is contained in:
parent
b4a4eb11c6
commit
71124d08dd
@ -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;}
|
||||
`;
|
@ -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>`;
|
@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
6
src/ui_ng/lib/src/create-edit-label/index.ts
Normal file
6
src/ui_ng/lib/src/create-edit-label/index.ts
Normal 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
|
||||
];
|
@ -47,6 +47,8 @@ import {
|
||||
JobLogDefaultService,
|
||||
ProjectService,
|
||||
ProjectDefaultService,
|
||||
LabelService,
|
||||
LabelDefaultService
|
||||
} from './service/index';
|
||||
import {
|
||||
ErrorHandler,
|
||||
@ -58,6 +60,9 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateServiceInitializer } from './i18n/index';
|
||||
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils';
|
||||
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
|
||||
@ -81,7 +86,8 @@ export const DefaultServiceConfig: IServiceConfig = {
|
||||
langMessageFileSuffixForHttpLoader: "-lang.json",
|
||||
localI18nMessageVariableMap: {},
|
||||
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
|
||||
projectPolicyService?: Provider,
|
||||
|
||||
//Service implementation for label
|
||||
labelService?: Provider,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,7 +179,10 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||
CONFIGURATION_DIRECTIVES,
|
||||
JOB_LOG_VIEWER_DIRECTIVES,
|
||||
PROJECT_POLICY_CONFIG_DIRECTIVES
|
||||
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
||||
LABEL_DIRECTIVES,
|
||||
CREATE_EDIT_LABEL_DIRECTIVES,
|
||||
LABEL_PIECE_DIRECTIVES
|
||||
],
|
||||
exports: [
|
||||
LOG_DIRECTIVES,
|
||||
@ -192,7 +204,10 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||
CONFIGURATION_DIRECTIVES,
|
||||
JOB_LOG_VIEWER_DIRECTIVES,
|
||||
TranslateModule,
|
||||
PROJECT_POLICY_CONFIG_DIRECTIVES
|
||||
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
||||
LABEL_DIRECTIVES,
|
||||
CREATE_EDIT_LABEL_DIRECTIVES,
|
||||
LABEL_PIECE_DIRECTIVES
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
@ -214,6 +229,7 @@ export class HarborLibraryModule {
|
||||
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
|
||||
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
|
||||
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
|
||||
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
|
||||
// Do initializing
|
||||
TranslateServiceInitializer,
|
||||
{
|
||||
@ -243,6 +259,7 @@ export class HarborLibraryModule {
|
||||
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
|
||||
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
|
||||
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
|
||||
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
|
||||
ChannelService
|
||||
]
|
||||
};
|
||||
|
@ -21,3 +21,5 @@ export * from './config/index';
|
||||
export * from './job-log-viewer/index';
|
||||
export * from './channel/index';
|
||||
export * from './project-policy-config/index';
|
||||
export * from './label/index';
|
||||
export * from './create-edit-label';
|
||||
|
8
src/ui_ng/lib/src/label-piece/index.ts
Normal file
8
src/ui_ng/lib/src/label-piece/index.ts
Normal 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
|
||||
];
|
46
src/ui_ng/lib/src/label-piece/label-piece.component.js
Normal file
46
src/ui_ng/lib/src/label-piece/label-piece.component.js
Normal 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
|
@ -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":""}
|
36
src/ui_ng/lib/src/label-piece/label-piece.component.ts
Normal file
36
src/ui_ng/lib/src/label-piece/label-piece.component.ts
Normal 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 {
|
||||
}
|
||||
}
|
8
src/ui_ng/lib/src/label-piece/label-piece.template.js
Normal file
8
src/ui_ng/lib/src/label-piece/label-piece.template.js
Normal 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
|
@ -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":""}
|
17
src/ui_ng/lib/src/label-piece/label-piece.template.ts
Normal file
17
src/ui_ng/lib/src/label-piece/label-piece.template.ts
Normal 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;}
|
||||
`;
|
6
src/ui_ng/lib/src/label/index.ts
Normal file
6
src/ui_ng/lib/src/label/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Type } from '@angular/core';
|
||||
import {LabelComponent} from "./label.component";
|
||||
|
||||
export const LABEL_DIRECTIVES: Type<any>[] = [
|
||||
LabelComponent
|
||||
];
|
21
src/ui_ng/lib/src/label/label.component.css.ts
Normal file
21
src/ui_ng/lib/src/label/label.component.css.ts
Normal 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;}
|
||||
`;
|
43
src/ui_ng/lib/src/label/label.component.html.ts
Normal file
43
src/ui_ng/lib/src/label/label.component.html.ts
Normal 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> {{'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> {{'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> {{'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>
|
||||
`;
|
131
src/ui_ng/lib/src/label/label.component.spec.ts
Normal file
131
src/ui_ng/lib/src/label/label.component.spec.ts
Normal 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');
|
||||
|
||||
})
|
||||
})*/
|
||||
|
||||
})
|
175
src/ui_ng/lib/src/label/label.component.ts
Normal file
175
src/ui_ng/lib/src/label/label.component.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
|
||||
<div style="padding-bottom: 15px;">
|
||||
<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> {{'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> {{'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> {{'REPLICATION.DELETE_POLICY' | translate}}</button>
|
||||
|
@ -22,8 +22,9 @@ import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
|
||||
import { JobLogViewerComponent } from '../job-log-viewer/index';
|
||||
|
||||
import { click } from '../utils';
|
||||
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
||||
|
||||
describe('RepositoryComponentListview (inline template)', () => {
|
||||
describe('RepositoryComponentListView (inline template)', () => {
|
||||
|
||||
let compRepo: RepositoryListviewComponent;
|
||||
let fixtureRepo: ComponentFixture<RepositoryListviewComponent>;
|
||||
@ -82,7 +83,8 @@ describe('RepositoryComponentListview (inline template)', () => {
|
||||
"docker_version": "1.12.3",
|
||||
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
|
||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||
"signature": null
|
||||
"signature": null,
|
||||
"labels": []
|
||||
}
|
||||
];
|
||||
|
||||
@ -101,6 +103,7 @@ describe('RepositoryComponentListview (inline template)', () => {
|
||||
declarations: [
|
||||
RepositoryListviewComponent,
|
||||
TagComponent,
|
||||
LabelPieceComponent,
|
||||
ConfirmationDialogComponent,
|
||||
FilterComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
|
@ -9,7 +9,7 @@ import { TagComponent } from '../tag/tag.component';
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
|
||||
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 { RepositoryService, RepositoryDefaultService } from '../service/repository.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 { click } from '../utils';
|
||||
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
||||
import {LabelDefaultService, LabelService} from "../service/label.service";
|
||||
|
||||
describe('RepositoryComponentStackview (inline template)', () => {
|
||||
|
||||
@ -27,10 +29,12 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
let fixtureRepo: ComponentFixture<RepositoryStackviewComponent>;
|
||||
let repositoryService: RepositoryService;
|
||||
let tagService: TagService;
|
||||
let labelService: LabelService;
|
||||
let systemInfoService: SystemInfoService;
|
||||
|
||||
let spyRepos: jasmine.Spy;
|
||||
let spyTags: jasmine.Spy;
|
||||
let spyLabels: jasmine.Spy;
|
||||
let spySystemInfo: jasmine.Spy;
|
||||
|
||||
let mockSystemInfo: SystemInfo = {
|
||||
@ -81,7 +85,31 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
"docker_version": "1.12.3",
|
||||
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
|
||||
"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: [
|
||||
RepositoryStackviewComponent,
|
||||
TagComponent,
|
||||
LabelPieceComponent,
|
||||
ConfirmationDialogComponent,
|
||||
FilterComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
@ -111,7 +140,8 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
{ 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));
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -46,7 +46,7 @@ export const REPOSITORY_TEMPLATE = `
|
||||
</section>
|
||||
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
|
||||
<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>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import {Component, DebugElement} from '@angular/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
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 { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface';
|
||||
import {Repository, RepositoryItem, Tag, SystemInfo, Label} from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
import { ChannelService } from '../channel/index';
|
||||
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
||||
import {LabelDefaultService, LabelService} from "../service/label.service";
|
||||
|
||||
|
||||
class RouterStub {
|
||||
navigateByUrl(url: string) { return url; }
|
||||
@ -34,10 +37,13 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
let repositoryService: RepositoryService;
|
||||
let systemInfoService: SystemInfoService;
|
||||
let tagService: TagService;
|
||||
let labelService: LabelService;
|
||||
|
||||
let spyRepos: jasmine.Spy;
|
||||
let spyTags: jasmine.Spy;
|
||||
let spySystemInfo: jasmine.Spy;
|
||||
let spyLabels: jasmine.Spy;
|
||||
let spyLabels1: jasmine.Spy;
|
||||
|
||||
let mockSystemInfo: SystemInfo = {
|
||||
'with_notary': true,
|
||||
@ -87,10 +93,53 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
'docker_version': '1.12.3',
|
||||
'author': 'NGINX Docker Maintainers \"docker-maint@nginx.com\"',
|
||||
'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 = {
|
||||
repositoryBaseEndpoint: '/api/repository/testing',
|
||||
systemInfoEndpoint: '/api/systeminfo/testing',
|
||||
@ -109,6 +158,7 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
ConfirmationDialogComponent,
|
||||
FilterComponent,
|
||||
TagComponent,
|
||||
LabelPieceComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||
INLINE_ALERT_DIRECTIVES,
|
||||
@ -120,25 +170,31 @@ describe('RepositoryComponent (inline template)', () => {
|
||||
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
|
||||
{ provide: TagService, useClass: TagDefaultService },
|
||||
{ provide: LabelService, useClass: LabelDefaultService},
|
||||
{ provide: ChannelService},
|
||||
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RepositoryComponent);
|
||||
|
||||
compRepo = fixture.componentInstance;
|
||||
|
||||
compRepo.projectId = 1;
|
||||
compRepo.hasProjectAdminRole = true;
|
||||
compRepo.repoName = 'library/nginx';
|
||||
repositoryService = fixture.debugElement.injector.get(RepositoryService);
|
||||
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
|
||||
tagService = fixture.debugElement.injector.get(TagService);
|
||||
labelService = fixture.debugElement.injector.get(LabelService);
|
||||
|
||||
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -51,6 +51,7 @@ export class RepositoryComponent implements OnInit {
|
||||
@Input() repoName: string;
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Input() isGuest: boolean;
|
||||
@Input() withNotary: boolean;
|
||||
@Input() withClair: boolean;
|
||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||
|
@ -196,4 +196,16 @@ export interface IServiceConfig {
|
||||
* @memberof IServiceConfig
|
||||
*/
|
||||
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;
|
||||
}
|
@ -10,3 +10,4 @@ export * from './scanning.service';
|
||||
export * from './configuration.service';
|
||||
export * from './job-log.service';
|
||||
export * from './project.service';
|
||||
export * from './label.service';
|
||||
|
@ -60,6 +60,7 @@ export interface Tag extends Base {
|
||||
created: Date;
|
||||
signature?: string;
|
||||
scan_overview?: VulnerabilitySummary;
|
||||
labels: Label[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -267,3 +268,12 @@ export interface TagClickEvent {
|
||||
repository_name: string;
|
||||
tag_name: string;
|
||||
}
|
||||
|
||||
export interface Label {
|
||||
[key: string]: any | any[];
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
scope: string;
|
||||
project_id: number;
|
||||
}
|
||||
|
125
src/ui_ng/lib/src/service/label.service.ts
Normal file
125
src/ui_ng/lib/src/service/label.service.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -20,7 +20,8 @@ describe('TagService', () => {
|
||||
"docker_version": "1.12.3",
|
||||
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
|
||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||
"signature": null
|
||||
"signature": null,
|
||||
'labels': []
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { RequestQueryParams } from './RequestQueryParams';
|
||||
import { Tag } from './interface';
|
||||
import {Label, Tag} from './interface';
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import 'rxjs/add/observable/of';
|
||||
import { Http } from '@angular/http';
|
||||
@ -65,6 +65,9 @@ export abstract class TagService {
|
||||
* @memberOf TagService
|
||||
*/
|
||||
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()
|
||||
export class TagDefaultService extends TagService {
|
||||
_baseUrl: string;
|
||||
|
||||
_labelUrl: string;
|
||||
constructor(
|
||||
private http: Http,
|
||||
@Inject(SERVICE_CONFIG) private config: IServiceConfig
|
||||
) {
|
||||
super();
|
||||
this._baseUrl = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
|
||||
this._labelUrl = this.config.labelEndpoint? this.config.labelEndpoint : '/api/labels';
|
||||
}
|
||||
|
||||
//Private methods
|
||||
@ -136,4 +140,28 @@ export class TagDefaultService extends TagService {
|
||||
.then(response => response.json() as Tag)
|
||||
.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));
|
||||
}
|
||||
}
|
@ -50,7 +50,8 @@ describe('TagDetailComponent (inline template)', () => {
|
||||
"author": "steven",
|
||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||
"signature": null,
|
||||
scan_overview: mockVulnerability
|
||||
"scan_overview": mockVulnerability,
|
||||
"labels": [],
|
||||
};
|
||||
|
||||
let config: IServiceConfig = {
|
||||
|
@ -30,7 +30,8 @@ export class TagDetailComponent implements OnInit {
|
||||
architecture: "--",
|
||||
os: "--",
|
||||
docker_version: "--",
|
||||
digest: "--"
|
||||
digest: "--",
|
||||
labels: [],
|
||||
};
|
||||
|
||||
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
@ -50,4 +50,20 @@ export const TAG_STYLE = `
|
||||
right: 35px;
|
||||
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;}
|
||||
`;
|
@ -15,8 +15,25 @@ export const TAG_TEMPLATE = `
|
||||
<div class="row" style="position:relative;">
|
||||
<div>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,26 +41,46 @@ export const TAG_TEMPLATE = `
|
||||
<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-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> {{'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> {{'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> {{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</div>
|
||||
</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="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: 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="width: 150px;"[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: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | 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-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>
|
||||
<span *ngSwitchDefault>{{t.name}}</span>
|
||||
</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>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 140px;" *ngIf="withClair">
|
||||
@ -58,8 +95,23 @@ export const TAG_TEMPLATE = `
|
||||
</a>
|
||||
</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: 140px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</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-footer>
|
||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
|
||||
|
@ -8,7 +8,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { TagComponent } from './tag.component';
|
||||
|
||||
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 { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/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 {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)', () => {
|
||||
|
||||
@ -26,6 +28,8 @@ describe('TagComponent (inline template)', () => {
|
||||
let fixture: ComponentFixture<TagComponent>;
|
||||
let tagService: TagService;
|
||||
let spy: jasmine.Spy;
|
||||
let spyLabels: jasmine.Spy;
|
||||
let spyLabels1: jasmine.Spy;
|
||||
let mockTags: Tag[] = [
|
||||
{
|
||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||
@ -36,7 +40,54 @@ describe('TagComponent (inline template)', () => {
|
||||
"docker_version": "1.12.3",
|
||||
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
|
||||
"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: [
|
||||
TagComponent,
|
||||
LabelPieceComponent,
|
||||
ConfirmationDialogComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
FILTER_DIRECTIVES,
|
||||
@ -62,7 +114,8 @@ describe('TagComponent (inline template)', () => {
|
||||
ChannelService,
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ 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.withNotary = false;
|
||||
|
||||
|
||||
let labelService: LabelService;
|
||||
|
||||
|
||||
tagService = fixture.debugElement.injector.get(TagService);
|
||||
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();
|
||||
});
|
||||
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
EventEmitter,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
ElementRef
|
||||
ElementRef, AfterContentInit, AfterViewInit
|
||||
} from "@angular/core";
|
||||
|
||||
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 { 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_STYLE } from "./tag.component.css";
|
||||
@ -48,7 +48,8 @@ import {
|
||||
doFiltering,
|
||||
doSorting,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
DEFAULT_PAGE_SIZE
|
||||
DEFAULT_PAGE_SIZE,
|
||||
clone,
|
||||
} from "../utils";
|
||||
|
||||
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 {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
|
||||
import {Observable} from "rxjs/Observable";
|
||||
import {LabelService} from "../service/label.service";
|
||||
import {Subject} from "rxjs/Subject";
|
||||
|
||||
@Component({
|
||||
selector: "hbr-tag",
|
||||
@ -64,7 +67,7 @@ import {Observable} from "rxjs/Observable";
|
||||
styles: [TAG_STYLE],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TagComponent implements OnInit {
|
||||
export class TagComponent implements OnInit, AfterViewInit {
|
||||
|
||||
signedCon: {[key: string]: any | string[]} = {};
|
||||
@Input() projectId: number;
|
||||
@ -73,6 +76,7 @@ export class TagComponent implements OnInit {
|
||||
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Input() isGuest: boolean;
|
||||
@Input() registryUrl: string;
|
||||
@Input() withNotary: boolean;
|
||||
@Input() withClair: boolean;
|
||||
@ -98,7 +102,27 @@ export class TagComponent implements OnInit {
|
||||
copyFailed = false;
|
||||
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;
|
||||
|
||||
@ViewChild("digestTarget") textInput: ElementRef;
|
||||
@ -112,6 +136,7 @@ export class TagComponent implements OnInit {
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private tagService: TagService,
|
||||
private labelService: LabelService,
|
||||
private translateService: TranslateService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private channel: ChannelService
|
||||
@ -128,14 +153,57 @@ export class TagComponent implements OnInit {
|
||||
}
|
||||
|
||||
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 {
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 200);
|
||||
setTimeout(() => clearInterval(hnd), 2000);
|
||||
ngAfterViewInit() {
|
||||
this.getAllLabels();
|
||||
}
|
||||
|
||||
public get filterLabelPieceWidth() {
|
||||
let len = this.lastFilteredTagName.length ? this.lastFilteredTagName.length * 6 + 60 : 115;
|
||||
return len > 210 ? 210 : len;
|
||||
}
|
||||
|
||||
doSearchTagNames(tagName: string) {
|
||||
this.lastFilteredTagName = tagName;
|
||||
this.currentPage = 1;
|
||||
@ -191,7 +259,139 @@ export class TagComponent implements OnInit {
|
||||
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() {
|
||||
this.tags = [];
|
||||
|
@ -179,7 +179,18 @@ export function doFiltering<T extends { [key: string]: any | any[] }>(items: T[]
|
||||
property: 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;
|
||||
|
@ -27,11 +27,11 @@
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@types/jquery": "^2.0.41",
|
||||
"@webcomponents/custom-elements": "^1.0.0",
|
||||
"clarity-angular": "^0.10.17",
|
||||
"clarity-angular": "^0.10.27",
|
||||
"clarity-icons": "^0.10.17",
|
||||
"clarity-ui": "^0.10.17",
|
||||
"clarity-ui": "^0.10.27",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "0.6.47",
|
||||
"harbor-ui": "0.6.53",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
|
@ -12,6 +12,9 @@
|
||||
<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>
|
||||
</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">
|
||||
<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>
|
||||
@ -25,12 +28,16 @@
|
||||
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
|
||||
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
|
||||
</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")'>
|
||||
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
|
||||
</section>
|
||||
<div>
|
||||
<button type="button" class="btn btn-primary" (click)="save()" [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-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | 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)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
|
||||
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>
|
||||
|
@ -38,7 +38,8 @@ const TabLinkContentMap = {
|
||||
'config-replication': 'replication',
|
||||
'config-email': 'email',
|
||||
'config-system': 'system_settings',
|
||||
'config-vulnerability': 'vulnerability'
|
||||
'config-vulnerability': 'vulnerability',
|
||||
'config-label': 'system_label'
|
||||
};
|
||||
|
||||
@Component({
|
||||
@ -200,6 +201,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||
this.allConfig.auth_mode.value === 'ldap_auth';
|
||||
}
|
||||
|
||||
public get hideBtn(): boolean {
|
||||
return this.currentTabId === 'config-label';
|
||||
}
|
||||
|
||||
public get hideMailTestingSpinner(): boolean {
|
||||
return !this.testingMailOnGoing || !this.showTestServerBtn;
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ import { MemberGuard } from './shared/route/member-guard-activate.service';
|
||||
|
||||
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
|
||||
import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service';
|
||||
import {ProjectLabelComponent} from "./project/project-label/project-label.component";
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
|
||||
@ -138,6 +139,9 @@ const harborRoutes: Routes = [
|
||||
{
|
||||
path: 'logs',
|
||||
component: AuditLogComponent
|
||||
},{
|
||||
path: 'labels',
|
||||
component: ProjectLabelComponent
|
||||
},
|
||||
{
|
||||
path: 'configs',
|
||||
|
@ -10,12 +10,15 @@
|
||||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | 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="isSProjectAdmin || isSystemAdmin">
|
||||
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
||||
</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)">
|
||||
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
|
||||
</li>
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ import { MemberService } from './member/member.service';
|
||||
import { ProjectRoutingResolver } from './project-routing-resolver.service';
|
||||
|
||||
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
|
||||
import {ProjectLabelComponent} from "../project/project-label/project-label.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@ -48,7 +49,8 @@ import { TargetExistsValidatorDirective } from '../shared/target-exists-directiv
|
||||
ProjectDetailComponent,
|
||||
MemberComponent,
|
||||
AddMemberComponent,
|
||||
TargetExistsValidatorDirective
|
||||
TargetExistsValidatorDirective,
|
||||
ProjectLabelComponent
|
||||
],
|
||||
exports: [ProjectComponent, ListProjectComponent],
|
||||
providers: [ProjectRoutingResolver, ProjectService, MemberService]
|
||||
|
@ -1,3 +1,3 @@
|
||||
<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>
|
@ -30,6 +30,7 @@ export class TagRepositoryComponent implements OnInit {
|
||||
projectId: number;
|
||||
repoName: string;
|
||||
hasProjectAdminRole: boolean = false;
|
||||
isGuest: boolean;
|
||||
registryUrl: string;
|
||||
|
||||
@ViewChild(RepositoryComponent)
|
||||
@ -47,11 +48,12 @@ export class TagRepositoryComponent implements OnInit {
|
||||
if (!this.projectId) {
|
||||
this.projectId = this.route.snapshot.parent.params['id'];
|
||||
};
|
||||
// let resolverData = this.route.snapshot.parent.data;
|
||||
|
||||
let resolverData = this.route.snapshot.data;
|
||||
|
||||
if (resolverData) {
|
||||
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'];
|
||||
|
||||
|
@ -178,6 +178,7 @@
|
||||
"REPLICATION": "Replication",
|
||||
"USERS": "Members",
|
||||
"LOGS": "Logs",
|
||||
"LABELS": "labels",
|
||||
"PROJECTS": "Projects",
|
||||
"CONFIG": "Configuration"
|
||||
},
|
||||
@ -411,7 +412,10 @@
|
||||
"PLACEHOLDER": "We couldn't find any repositories!",
|
||||
"INFO": "Info",
|
||||
"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": {
|
||||
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
|
||||
@ -433,6 +437,7 @@
|
||||
"AUTH": "Authentication",
|
||||
"REPLICATION": "Replication",
|
||||
"EMAIL": "Email",
|
||||
"LABEL": "Label",
|
||||
"SYSTEM": "System Settings",
|
||||
"VULNERABILITY": "Vulnerability",
|
||||
"CONFIRM_TITLE": "Confirm to cancel",
|
||||
@ -612,6 +617,18 @@
|
||||
"COPY_ERROR": "Copy failed, please try to manually copy.",
|
||||
"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": {
|
||||
"MONDAY": "Monday",
|
||||
"TUESDAY": "Tuesday",
|
||||
|
@ -178,6 +178,7 @@
|
||||
"REPLICATION": "Replicación",
|
||||
"USERS": "Miembros",
|
||||
"LOGS": "Logs",
|
||||
"LABELS": "labels",
|
||||
"PROJECTS": "Proyectos",
|
||||
"CONFIG": "Configuración"
|
||||
},
|
||||
@ -411,7 +412,10 @@
|
||||
"PLACEHOLDER": "We couldn't find any repositories!",
|
||||
"INFO": "Información",
|
||||
"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": {
|
||||
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
|
||||
@ -433,6 +437,7 @@
|
||||
"AUTH": "Autentificación",
|
||||
"REPLICATION": "Replicación",
|
||||
"EMAIL": "Email",
|
||||
"LABEL": "Label",
|
||||
"SYSTEM": "Opciones del Sistema",
|
||||
"VULNERABILITY": "Vulnerability",
|
||||
"CONFIRM_TITLE": "Confirma cancelación",
|
||||
@ -612,6 +617,18 @@
|
||||
"COPY_ERROR": "Copy failed, please try to manually copy.",
|
||||
"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": {
|
||||
"MONDAY": "Monday",
|
||||
"TUESDAY": "Tuesday",
|
||||
|
@ -386,6 +386,7 @@
|
||||
"AUTH": "Identification",
|
||||
"REPLICATION": "Réplication",
|
||||
"EMAIL": "Email",
|
||||
"LABEL": "Label",
|
||||
"SYSTEM": "Réglages Système",
|
||||
"CONFIRM_TITLE": "Confirmer pour annuler",
|
||||
"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 !",
|
||||
"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.",
|
||||
"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.",
|
||||
|
@ -178,6 +178,7 @@
|
||||
"REPLICATION": "复制",
|
||||
"USERS": "成员",
|
||||
"LOGS": "日志",
|
||||
"LABELS": "标签",
|
||||
"PROJECTS": "项目",
|
||||
"CONFIG": "配置管理"
|
||||
},
|
||||
@ -411,7 +412,10 @@
|
||||
"PLACEHOLDER": "未发现任何镜像库!",
|
||||
"INFO": "描述信息",
|
||||
"NO_INFO": "此镜像仓库没有描述信息",
|
||||
"IMAGE": "镜像"
|
||||
"IMAGE": "镜像",
|
||||
"LABELS": "标签",
|
||||
"ADD_TO_IMAGE": "添加标签到此镜像",
|
||||
"ADD_LABELS": "添加标签"
|
||||
},
|
||||
"ALERT": {
|
||||
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
|
||||
@ -433,6 +437,7 @@
|
||||
"AUTH": "认证模式",
|
||||
"REPLICATION": "复制",
|
||||
"EMAIL": "邮箱",
|
||||
"LABEL": "标签",
|
||||
"SYSTEM": "系统设置",
|
||||
"VULNERABILITY": "漏洞",
|
||||
"CONFIRM_TITLE": "确认取消",
|
||||
@ -612,6 +617,18 @@
|
||||
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。",
|
||||
"FILTER_FOR_TAGS": "过滤项目"
|
||||
},
|
||||
"LABEL": {
|
||||
"LABEL": "标签",
|
||||
"DESCRIPTION": "描述",
|
||||
"CREATION_TIME": "创建时间",
|
||||
"NEW_LABEL": "新建标签",
|
||||
"EDIT": "编辑",
|
||||
"DELETE": "删除",
|
||||
"LABEL_NAME": "标签名字",
|
||||
"COLOR": "颜色",
|
||||
"FILTER_Label_PLACEHOLDER": "过滤标签",
|
||||
"NO_LABELS": "无标签"
|
||||
},
|
||||
"WEEKLY": {
|
||||
"MONDAY": "周一",
|
||||
"TUESDAY": "周二",
|
||||
|
Loading…
Reference in New Issue
Block a user