Add Label Marker to Helmchart version

1. Add Label marker component
2. Add Label apis
3. Add Label to Helmchart version

Signed-off-by: Qian Deng <dengq@vmware.com>
This commit is contained in:
Qian Deng 2018-10-17 17:12:10 +08:00
parent 5cf6c04ea6
commit 7ffa135432
16 changed files with 450 additions and 76 deletions

View File

@ -34,4 +34,8 @@
display: flex;
justify-content: center;
align-items: center;
}
@mixin dropdown-as-action-button {
margin: .25rem .5rem .25rem 0;
}

View File

@ -38,13 +38,29 @@
<clr-datagrid (clrDgRefresh)="refresh()" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRows">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary"
[disabled]="!(selectedRows.length===1)"
(click)="versionDownload()">
<clr-icon shape="download" size="16"></clr-icon>&nbsp;{{'HELM_CHART.DOWNLOAD' | translate}}</button>
[disabled]="!(selectedRows.length===1)"
(click)="versionDownload()">
<clr-icon shape="download" size="16"></clr-icon>&nbsp;{{'HELM_CHART.DOWNLOAD' | translate}}
</button>
<button type="button" class="btn btn-sm btn-secondary"
[disabled]="selectedRows.length<=0 || !hasProjectAdminRole"
(click)="openVersionDeleteModal(selectedRows)">
<clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'BUTTON.DELETE' | translate}}</button>
[disabled]="selectedRows.length<=0 || !hasProjectAdminRole"
(click)="openVersionDeleteModal(selectedRows)">
<clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'BUTTON.DELETE' | translate}}
</button>
<clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger
[disabled]="!(selectedRows.length==1)">
<clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<hbr-resource-label-marker
[labels]="projectLabels"
[projectName]="projectName"
[resource]="selectedRows[0]"
[resourceType]="resourceType">
</hbr-resource-label-marker>
</clr-dropdown-menu>
</clr-dropdown>
</clr-dg-action-bar>
<clr-dg-column>{{'HELM_CHART.VERSION' | translate}}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.STATUS' | translate }}</clr-dg-column>
@ -52,6 +68,7 @@
<clr-dg-column>{{'HELM_CHART.MAINTAINERS' | translate }}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.CREATED' | translate }}</clr-dg-column>
<clr-dg-column style="width: 140px;">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'HELM_CHART.NO_VERSION_PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let v of chartVersions" [clrDgItem]="v">
<clr-dg-cell>
@ -64,6 +81,21 @@
<clr-dg-cell>{{ v.engine }}</clr-dg-cell>
<clr-dg-cell>{{ getMaintainerString(v.maintainers) }}</clr-dg-cell>
<clr-dg-cell>{{ v.created | date}}</clr-dg-cell>
<clr-dg-cell style="width: 140px;">
<hbr-label-piece *ngIf="v.labels?.length" [label]="v.labels[0]"> </hbr-label-piece>
<div class="signpost-item" [hidden]="v.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 v.labels" [label]="label"></hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
</div>
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount">

View File

@ -33,6 +33,12 @@
}
}
clr-dg-action-bar {
clr-dropdown{
@include dropdown-as-action-button;
}
}
.card-container {
margin-top: 21px;
.card-header {

View File

@ -1,3 +1,4 @@
import { Label } from './../../service/interface';
import {
Component,
Input,
@ -18,7 +19,8 @@ import {
SystemInfo,
SystemInfoService,
HelmChartVersion,
HelmChartMaintainer
HelmChartMaintainer,
LabelService
} from "./../../service/index";
import { ErrorHandler } from "./../../error-handler/error-handler";
import { toPromise, DEFAULT_PAGE_SIZE, downloadFile } from "../../utils";
@ -34,7 +36,8 @@ import {
ConfirmationButtons,
ConfirmationTargets,
ConfirmationState,
DefaultHelmIcon
DefaultHelmIcon,
ResourceType
} from "../../shared/shared.const";
@Component({
@ -45,6 +48,7 @@ import {
})
export class ChartVersionComponent implements OnInit {
signedCon: { [key: string]: any | string[] } = {};
@Input() projectId: number;
@Input() projectName: string;
@Input() chartName: string;
@Input() roleName: string;
@ -60,7 +64,9 @@ export class ChartVersionComponent implements OnInit {
versionsCopy: HelmChartVersion[] = [];
systemInfo: SystemInfo;
selectedRows: HelmChartVersion[] = [];
projectLabels: Label[] = [];
loading = true;
resourceType = ResourceType.CHART_VERSION;
isCardView: boolean;
cardHover = false;
@ -82,6 +88,7 @@ export class ChartVersionComponent implements OnInit {
private translateService: TranslateService,
private systemInfoService: SystemInfoService,
private helmChartService: HelmChartService,
private resrouceLabelService: LabelService,
private cdr: ChangeDetectorRef,
private operationService: OperationService,
) {}
@ -96,6 +103,7 @@ export class ChartVersionComponent implements OnInit {
.then(systemInfo => (this.systemInfo = systemInfo))
.catch(error => this.errorHandler.error(error));
this.refresh();
this.getProjectLabels();
this.lastFilteredVersionName = "";
}
@ -104,6 +112,15 @@ export class ChartVersionComponent implements OnInit {
this.refresh();
}
getProjectLabels() {
this.resrouceLabelService.getProjectLabels(this.projectId).subscribe(
(labels: Label[]) => {
this.projectLabels = labels;
console.log('chart version project labels', this.projectLabels);
}
);
}
refresh() {
this.loading = true;
this.helmChartService

View File

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

View File

@ -0,0 +1,22 @@
<div>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group filter-div">
<input #filterInput type="text" placeholder="Filter labels" [(ngModel)]="labelFilter">
</div>
<div class="label-items-container">
<div class="dropdown-item" *ngFor='let label of sortedLabels'>
<span *ngIf="!isMarkOngoing(label)" class="mark-label-span">
<clr-icon *ngIf="isMarked(label)" shape="check" ></clr-icon>
</span>
<span *ngIf="isMarkOngoing(label)" class="spinner spinner-sm">
Loading...
</span>
<span (click)="markLabel(label)">
<hbr-label-piece [label]="label" [labelWidth]="130"></hbr-label-piece>
</span>
<span class="unmark-label-span" (click)="unmarkLabel(label)">
<clr-icon *ngIf="isMarked(label)" shape="times-circle"></clr-icon>
</span>
</div>
</div>
</div>

View File

@ -0,0 +1,34 @@
@mixin icon-span {
width: 12px;
min-height: 12px;
clr-icon {
margin-bottom: 12px;
}
}
.filter-div {
margin-left: 9px;
}
.label-items-container {
max-height: 300px;
overflow-y: auto;
.dropdown-item {
padding-left: 12px;
padding-right: 12px;
.unmark-label-span {
@include icon-span();
float: right;
}
.mark-label-span {
@include icon-span();
float: left;
margin-right: 9px;
}
}
}

View File

@ -0,0 +1,158 @@
// Copyright (c) 2018 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, ChangeDetectorRef, ViewChild, ElementRef } from '@angular/core';
import { Observable, fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { finalize } from 'rxjs/operators';
import { RepositoryItem, HelmChartVersion } from './../../service/interface';
import {Label} from "../../service/interface";
import { ResourceType } from '../../shared/shared.const';
import { LabelService } from '../../service/label.service';
import { TranslateService } from '@ngx-translate/core';
import { ErrorHandler } from '../../error-handler/error-handler';
@Component({
selector: 'hbr-resource-label-marker',
templateUrl: './label-marker.component.html',
styleUrls: ['./label-marker.component.scss']
})
export class LabelMarkerComponent implements OnInit {
@Input() labels: Label[] = [];
@Input() projectName: string;
@Input() resource: RepositoryItem | HelmChartVersion;
@Input() resourceType: ResourceType;
labelFilter = '';
markedMap: Map<number, boolean> = new Map<number, boolean>();
markingMap: Map<number, boolean> = new Map<number, boolean>();
sortedLabels: Label[] = [];
loading = false;
@ViewChild('filterInput') filterInputRef: ElementRef;
ngOnInit(): void {
this.sortedLabels = this.labels;
this.refresh();
fromEvent(this.filterInputRef.nativeElement, 'keyup')
.pipe(debounceTime(500))
.subscribe(() => this.refresh());
}
constructor(
private labelService: LabelService,
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private cdr: ChangeDetectorRef) {}
refresh() {
this.loading = true;
if (this.resourceType === ResourceType.CHART_VERSION) {
this.labelService.getChartVersionLabels(
this.projectName,
this.resource.name,
(this.resource as HelmChartVersion).version)
.pipe(finalize(() => {
this.loading = false;
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
}))
.subscribe( chartVersionLabels => {
for (let label of chartVersionLabels) {
console.log('marked label', label);
this.markedMap.set(label.id, true);
}
this.sortedLabels = this.getSortedLabels();
});
}
}
markLabel(label: Label) {
if (this.markedMap.get(label.id) || this.isMarkOngoing(label)) {
return;
}
this.markingMap.set(label.id, true);
this.labelService.markChartLabel(
this.projectName,
this.resource.name,
(this.resource as HelmChartVersion).version,
label)
.pipe(finalize(() => {
this.markingMap.set(label.id, false);
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}))
.subscribe(
() => {
this.markedMap.set(label.id, true);
this.refresh();
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
},
err => this.errorHandler.error(err)
);
}
unmarkLabel(label: Label) {
if (!this.isMarked(label) || this.isMarkOngoing(label)) {
return;
}
this.markingMap.set(label.id, true);
this.labelService.unmarkChartLabel(
this.projectName,
this.resource.name,
(this.resource as HelmChartVersion).version,
label)
.pipe(finalize(() => {
this.markingMap.set(label.id, false);
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}))
.subscribe(
() => {
this.markedMap.set(label.id, false);
this.refresh();
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
},
err => this.errorHandler.error(err)
);
}
isMarked(label: Label) {
return this.markedMap.get(label.id) ? true : false;
}
isMarkOngoing(label: Label) {
return this.markingMap.get(label.id) ? true : false;
}
getSortedLabels(): Label[] {
return this.labels.filter( l => l.name.includes(this.labelFilter))
.sort((a, b) => {
if (this.isMarked(a) && !this.isMarked(b)) {
return -1;
} else if (!this.isMarked(a) && this.isMarked(b)) {
return 1;
} else {
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
}
});
}
}

View File

@ -146,12 +146,10 @@ export class HelmChartDefaultService extends HelmChartService {
return this.http
.get(`${this.config.helmChartEndpoint}/${projectName}/charts`, HTTP_GET_OPTIONS)
.pipe(map(response => {
return this.extractHelmItems(response);
}))
.pipe(catchError(error => {
return this.handleErrorObservable(error);
}));
.pipe(
map(response => this.extractHelmItems(response),
catchError(error => this.handleErrorObservable(error))
));
}
public deleteHelmChart(projectId: number | string, chartName: string): Observable<any> {
@ -172,10 +170,10 @@ export class HelmChartDefaultService extends HelmChartService {
chartName: string,
): Observable<HelmChartVersion[]> {
return this.http.get(`${this.config.helmChartEndpoint}/${projectName}/charts/${chartName}`, HTTP_GET_OPTIONS)
.pipe(map(response => {
return this.extractData(response);
}))
.pipe(catchError(this.handleErrorObservable));
.pipe(
map(response => this.extractData(response)),
catchError(this.handleErrorObservable)
);
}
public deleteChartVersion(projectName: string, chartName: string, version: string): any {

View File

@ -285,7 +285,6 @@ export interface Label {
scope: string;
project_id: number;
}
export interface CardItemEvent {
event_type: string;
item: any;
@ -322,9 +321,11 @@ export interface HelmChartVersion {
engine: string;
icon: string;
appVersion: string;
apiVersion: string;
urls: string[];
created: string;
digest: string;
labels: Label[];
deprecated?: boolean;
}
@ -334,6 +335,7 @@ export interface HelmChartDetail {
values: any;
files: HelmchartFile;
security: HelmChartSecurity;
labels: Label[];
}
export interface HelmChartMetaData {

View File

@ -1,10 +1,14 @@
import { Observable} from "rxjs";
import { Label } from "./interface";
import { Inject, Injectable } from "@angular/core";
import { Http } from "@angular/http";
import { Observable} from "rxjs";
import { map } from "rxjs/operators";
import { RequestQueryParams } from "./RequestQueryParams";
import { Label } from "./interface";
import { IServiceConfig, SERVICE_CONFIG } from "../service.config";
import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from "../utils";
import { RequestQueryParams } from "./RequestQueryParams";
import { extractJson } from "../shared/shared.utils";
export abstract class LabelService {
abstract getGLabels(
@ -18,6 +22,12 @@ export abstract class LabelService {
queryParams?: RequestQueryParams
): Observable<Label[]> | Promise<Label[]>;
abstract getProjectLabels(
projectId: number,
name?: string,
queryParams?: RequestQueryParams
): Observable<Label[]>;
abstract getLabels(
scope: string,
projectId?: number,
@ -37,46 +47,42 @@ export abstract class LabelService {
): Observable<any> | Promise<any> | any;
abstract deleteLabel(id: number): Observable<any> | Promise<any> | any;
abstract getChartVersionLabels(
projectName: string,
chartName: string,
version?: string,
): Observable<Label[]>;
abstract markChartLabel(
projectName: string,
chartName: string,
version: string,
label: Label,
): Observable<any>;
abstract unmarkChartLabel(
projectName: string,
chartName: string,
version: string,
label: Label,
): Observable<any>;
}
@Injectable()
export class LabelDefaultService extends LabelService {
_labelUrl: string;
labelUrl: string;
chartUrl: string;
constructor(
@Inject(SERVICE_CONFIG) config: IServiceConfig,
private http: Http
) {
super();
this._labelUrl = config.labelEndpoint
? config.labelEndpoint
: "/api/labels";
this.labelUrl = config.labelEndpoint ? config.labelEndpoint : "/api/labels";
this.chartUrl = config.helmChartEndpoint ? config.helmChartEndpoint : "/api/chartrepo";
}
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,
@ -91,7 +97,7 @@ export class LabelDefaultService extends LabelService {
queryParams.set("name", "" + name);
}
return this.http
.get(this._labelUrl, buildHttpRequestOptions(queryParams))
.get(this.labelUrl, buildHttpRequestOptions(queryParams))
.toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
@ -113,7 +119,51 @@ export class LabelDefaultService extends LabelService {
queryParams.set("name", "" + name);
}
return this.http
.get(this._labelUrl, buildHttpRequestOptions(queryParams))
.get(this.labelUrl, buildHttpRequestOptions(queryParams))
.toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
getProjectLabels(
projectId: number,
name?: string,
queryParams?: RequestQueryParams
): Observable<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))
.pipe(map( res => extractJson(res)));
}
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));
@ -124,7 +174,7 @@ export class LabelDefaultService extends LabelService {
return Promise.reject("Invalid label.");
}
return this.http
.post(this._labelUrl, JSON.stringify(label), HTTP_JSON_OPTIONS)
.post(this.labelUrl, JSON.stringify(label), HTTP_JSON_OPTIONS)
.toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
@ -134,9 +184,9 @@ export class LabelDefaultService extends LabelService {
if (!id || id <= 0) {
return Promise.reject("Bad request argument.");
}
let reqUrl = `${this._labelUrl}/${id}`;
let reqUrl = `${this.labelUrl}/${id}`;
return this.http
.get(reqUrl)
.get(reqUrl, HTTP_JSON_OPTIONS)
.toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
@ -149,22 +199,54 @@ export class LabelDefaultService extends LabelService {
if (!label) {
return Promise.reject("Invalid endpoint.");
}
let reqUrl = `${this._labelUrl}/${id}`;
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}`;
let reqUrl = `${this.labelUrl}/${id}`;
return this.http
.delete(reqUrl)
.toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
getChartVersionLabels(
projectName: string,
chartName: string,
version: string
): Observable<Label[]> {
return this.http.get(`${this.chartUrl}/${projectName}/charts/${chartName}/${version}/labels`)
.pipe(map(res => extractJson(res)));
}
markChartLabel(
projectName: string,
chartName: string,
version: string,
label: Label,
): Observable<any> {
return this.http.post(`${this.chartUrl}/${projectName}/charts/${chartName}/${version}/labels`,
JSON.stringify(label), HTTP_JSON_OPTIONS)
.pipe(map(res => extractJson(res)));
}
unmarkChartLabel(
projectName: string,
chartName: string,
version: string,
label: Label,
): Observable<any> {
return this.http.delete(`${this.chartUrl}/${projectName}/charts/${chartName}/${version}/labels/${label.id}`, HTTP_JSON_OPTIONS)
.pipe(map(res => extractJson(res)));
}
}

View File

@ -100,3 +100,8 @@ export enum Roles {
GUEST = 3,
OTHER = 0,
}
export enum ResourceType {
REPOSITORY = 1,
CHART_VERSION = 2,
}

View File

@ -17,6 +17,8 @@
**
* returns {string}
*/
import { Response } from "@angular/http";
export const errorHandler = function (error: any): string {
if (!error) {
return "UNKNOWN_ERROR";
@ -45,3 +47,10 @@ export const errorHandler = function (error: any): string {
}
}
};
export const extractJson = (res: Response) => {
if (res.text() === '') {
return [];
}
return (res.json() || []);
};

View File

@ -42,22 +42,22 @@
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1) || !isClairDBFullyReady" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown *ngIf="!withAdmiral">
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" ><clr-icon shape="plus" size="16"></clr-icon>{{'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" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' style="padding-left:10px;">{{'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' [hidden]='!label.show' (click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<clr-dropdown *ngIf="!withAdmiral">
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" ><clr-icon shape="plus" size="16"></clr-icon>{{'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" [(ngModel)]="stickName" (keyup)="handleStickInputFilter()"></div>
<div [hidden]='imageStickLabels.length' style="padding-left:10px;">{{'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' [hidden]='!label.show' (click)="stickLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="130"></hbr-label-piece></div>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>

View File

@ -1,3 +1,5 @@
@import "../mixin";
.option-right {
padding-right: 18px;
padding-bottom: 6px;
@ -132,7 +134,7 @@
}
.dropdown .dropdown-toggle.btn {
margin: .25rem .5rem .25rem 0;
@include dropdown-as-action-button;
}
.labelFilterPanel {

View File

@ -5,6 +5,7 @@
<a href="javascript:void(0)" (click)="gotoChartList()">{{ 'HELM_CHART.HELMCHARTS'| translate}}</a>
</div>
<hbr-helm-chart-version
[projectId]='projectId'
[projectName]='projectName'
[chartName]='chartName'
[roleName]='roleName'