Enhancement: Harbor Helmchart UI Label

1. Add Filter to helmchart datagrid
2. Fix UI misc issues

Signed-off-by: Qian Deng <dengq@vmware.com>
This commit is contained in:
Qian Deng 2018-10-24 15:59:29 +08:00
parent 2eff6a794a
commit 7164856239
17 changed files with 179 additions and 35 deletions

View File

@ -49,7 +49,7 @@
</button>
<clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger
[disabled]="!(selectedRows.length==1)">
[disabled]="!(selectedRows.length===1)">
<clr-icon shape="plus" size="16"></clr-icon>{{'REPOSITORY.ADD_LABELS' | translate}}
</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
@ -69,9 +69,17 @@
<clr-dg-column>{{'HELM_CHART.MAINTAINERS' | translate }}</clr-dg-column>
<clr-dg-column>{{'HELM_CHART.CREATED' | translate }}</clr-dg-column>
<clr-dg-column>{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-column>
{{'REPOSITORY.LABELS' | translate}}
<clr-dg-filter [clrDgFilter]="labelFilter">
<hbr-chart-version-label-filter #labelFilter class="label-filter"
[labels]="projectLabels"
[resourceType]="resourceType">
</hbr-chart-version-label-filter>
</clr-dg-filter>
</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-row *clrDgItems="let v of chartVersions" [clrDgItem]="v">
<clr-dg-cell>
<span class="list-img">
<img [src]="v.icon ?v.icon:chartDefaultIcon" (error)="getDefaultIcon(v);" />
@ -83,8 +91,10 @@
<clr-dg-cell>{{ getMaintainerString(v.maintainers) }}</clr-dg-cell>
<clr-dg-cell>{{ v.created | date}}</clr-dg-cell>
<clr-dg-cell>
<hbr-label-piece *ngIf="v.labels?.length" [label]="v.labels[0]"> </hbr-label-piece>
<hbr-resource-label-signpost *ngIf="v.labels?.length>1" [labels]="v.labels"></hbr-resource-label-signpost>
<div>
<hbr-label-piece *ngIf="v.labels?.length" [label]="v.labels[0]" [labelWidth]="130"> </hbr-label-piece>
<hbr-resource-label-signpost *ngIf="v.labels?.length>1" [labels]="v.labels"></hbr-resource-label-signpost>
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>

View File

@ -34,13 +34,15 @@
}
clr-dg-action-bar {
clr-dropdown{
clr-dropdown {
@include dropdown-as-action-button;
}
}
hbr-resource-label-signpost {
display: inline-block;
max-height: 24px;
margin-left: 3px;
}
.card-container {

View File

@ -317,8 +317,8 @@ export class ChartVersionComponent implements OnInit {
.subscribe(labels => {
let versionIdx = this.chartVersions.findIndex(v => v.name === version.name);
this.chartVersions[versionIdx].labels = labels;
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
let hnd = setInterval(() => this.cdr.markForCheck(), 200);
setTimeout(() => clearInterval(hnd), 5000);
});
}
}

View File

@ -2,9 +2,11 @@ import { Type } from '@angular/core';
import { LabelComponent} from "./label.component";
import { LabelMarkerComponent } from './label-marker/label-marker.component';
import { LabelSignPostComponent } from './label-signpost/label-signpost.component';
import { LabelFilterComponent } from './label-filter/label-filter.component';
export const LABEL_DIRECTIVES: Type<any>[] = [
LabelComponent,
LabelMarkerComponent,
LabelSignPostComponent
LabelSignPostComponent,
LabelFilterComponent
];

View File

@ -0,0 +1,18 @@
<div>
<div class="form-group filter-div">
<input #filterInput type="text" placeholder='{{"LABEL.FILTER_LABEL_PLACEHOLDER" | translate }}' [(ngModel)]="labelFilter">
</div>
<div class="label-items-container">
<div class="dropdown-item" *ngFor='let label of filteredLabels'>
<span class="check-label-span">
<clr-icon *ngIf="isSelected(label)" shape="check"></clr-icon>
</span>
<span (click)="selectLabel(label)">
<hbr-label-piece [label]="label" [labelWidth]="90"></hbr-label-piece>
</span>
<span class="x-label-span" (click)="unselectLabel(label)">
<clr-icon *ngIf="isSelected(label)" shape="times-circle"></clr-icon>
</span>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,69 @@
import { OnInit, Input, EventEmitter, Component, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
import {ClrDatagridFilterInterface} from "@clr/angular";
import { fromEvent } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { Label, Tag } from './../../service/interface';
import { HelmChartVersion } from '../../service/interface';
import { ResourceType } from '../../shared/shared.const';
@Component({
selector: "hbr-chart-version-label-filter",
templateUrl: './label-filter.component.html',
styleUrls: ['./label-filter.component.scss']
})
export class LabelFilterComponent implements ClrDatagridFilterInterface<any>, OnInit {
@Input() labels: Label[] = [];
@Input() resourceType: ResourceType;
@ViewChild('filterInput') filterInputRef: ElementRef;
selectedLabels: Map<number, boolean> = new Map<number, boolean>();
changes: EventEmitter<any> = new EventEmitter<any>(false);
labelFilter = '';
ngOnInit(): void {
fromEvent(this.filterInputRef.nativeElement, 'keyup')
.pipe(debounceTime(500))
.subscribe(() => {
let hnd = setInterval(() => this.cdr.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
});
}
constructor(private cdr: ChangeDetectorRef) {}
get filteredLabels() {
return this.labels.filter(label => label.name.includes(this.labelFilter));
}
isActive(): boolean {
return this.selectedLabels.size > 0;
}
accepts(cv: any): boolean {
if (this.resourceType === ResourceType.CHART_VERSION) {
return (cv as HelmChartVersion).labels.some(label => this.selectedLabels.get(label.id));
} else if (this.resourceType === ResourceType.REPOSITORY_TAG) {
return (cv as Tag).labels.some(label => this.selectedLabels.get(label.id));
} else {
return true;
}
}
selectLabel(label: Label) {
this.selectedLabels.set(label.id, true);
this.changes.emit();
}
unselectLabel(label: Label) {
this.selectedLabels.delete(label.id);
this.changes.emit(true);
}
isSelected(label: Label) {
return this.selectedLabels.has(label.id);
}
}

View File

@ -9,6 +9,8 @@
.filter-div {
margin-left: 9px;
margin-right: 9px;
margin-bottom: 9px;
}
.label-items-container {

View File

@ -1,8 +1,6 @@
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>
...
</button>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div *ngFor="let label of labels">
<hbr-label-piece [label]="label" [labelWidth]="130"></hbr-label-piece>

View File

@ -4,6 +4,7 @@ clr-signpost {
margin: 0;
height: 24px;
vertical-align: super;
line-height: 6px;
}
clr-signpost-content {

View File

@ -104,4 +104,5 @@ export enum Roles {
export enum ResourceType {
REPOSITORY = 1,
CHART_VERSION = 2,
REPOSITORY_TAG = 3,
}

View File

@ -14,28 +14,34 @@
<div class="row" style="position:relative;">
<div>
<div class="row flex-items-xs-right rightPos">
<div id="filterArea">
<div class='filterLabelPiece' *ngIf="!withAdmiral" [hidden]="!openLabelFilterPiece" [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel" [labelWidth]="130"></hbr-label-piece></div>
<div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filterEvt)="doSearchTagNames($event)" (openFlag)="openFlagEvent($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
<div class="labelFilterPanel" *ngIf="!withAdmiral" [hidden]="!openLabelFilterPanel">
<a class="filterClose" (click)="closeFilter()">&times;</a>
<label class="filterLabelHeader">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" [(ngModel)]= "filterName" (keyup)="handleInputFilter()"></div>
<div [hidden]='imageFilterLabels.length' style="padding-left:10px;">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="labelBtn" *ngFor='let label of imageFilterLabels' [hidden]="!label.show" (click)="rightFilterLabel(label)">
<div id="filterArea">
<div class='filterLabelPiece' *ngIf="!withAdmiral" [hidden]="!openLabelFilterPiece" [style.left.px]='filterLabelPieceWidth'>
<hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel" [labelWidth]="130"></hbr-label-piece>
</div>
<div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filterEvt)="doSearchTagNames($event)"
(openFlag)="openFlagEvent($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
<div class="labelFilterPanel" *ngIf="!withAdmiral" [hidden]="!openLabelFilterPanel">
<a class="filterClose" (click)="closeFilter()">&times;</a>
<label class="filterLabelHeader">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" [(ngModel)]="filterName" (keyup)="handleInputFilter()"></div>
<div [hidden]='imageFilterLabels.length' style="padding-left:10px;">{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="labelBtn" *ngFor='let label of imageFilterLabels' [hidden]="!label.show"
(click)="rightFilterLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label" [labelWidth]="160"></hbr-label-piece></div>
</button>
<div class='labelDiv'>
<hbr-label-piece [label]="label.label" [labelWidth]="160"></hbr-label-piece>
</div>
</button>
</div>
</div>
</div>
</div>
<span class="refresh-btn" (click)="refresh()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow">

View File

@ -64,4 +64,9 @@
height: 40px;
width: 1px;
top: 10px;
}
#results {
overflow-y: auto;
max-height: 99%;
}

View File

@ -21,8 +21,7 @@ export class ListChartVersionRoComponent implements OnInit {
private projectService: ProjectService,
private router: Router) { }
ngOnInit() {
}
ngOnInit() {}
getStatusString(chart: HelmChartVersion) {
if (chart.deprecated) {

View File

@ -763,7 +763,7 @@
"DELETE": "Delete",
"LABEL_NAME": "Label Name",
"COLOR": "Color",
"FILTER_Label_PLACEHOLDER": "Filter Labels",
"FILTER_LABEL_PLACEHOLDER": "Filter Labels",
"NO_LABELS": "No labels",
"DELETION_TITLE_TARGET": "Confirm Label Deletion",
"DELETION_SUMMARY_TARGET": "Do you want to delete {{param}}?",

View File

@ -726,7 +726,7 @@
"DELETE": "Delete",
"LABEL_NAME": "Label Name",
"COLOR": "Color",
"FILTER_Label_PLACEHOLDER": "Filter Labels",
"FILTER_LABEL_PLACEHOLDER": "Filter Labels",
"NO_LABELS": "No labels",
"DELETION_TITLE_TARGET": "Confirm Label Deletion",
"DELETION_SUMMARY_TARGET": "Do you want to delete {{param}}?",

View File

@ -762,7 +762,7 @@
"DELETE": "删除",
"LABEL_NAME": "标签名字",
"COLOR": "颜色",
"FILTER_Label_PLACEHOLDER": "过滤标签",
"FILTER_LABEL_PLACEHOLDER": "过滤标签",
"NO_LABELS": "无标签",
"DELETION_TITLE_TARGET":"删除标签确认",
"DELETION_SUMMARY_TARGET": "确认删除标签 {{param}}?",