mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-26 12:15:20 +01:00
Refactor artifact-list component (#17577)
Signed-off-by: AllForNothing <sshijun@vmware.com> Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
parent
b6c978c7f7
commit
3d8959be49
@ -5,21 +5,11 @@ import {
|
||||
UserPermissionService,
|
||||
USERSTATICPERMISSION,
|
||||
} from '../../../../../shared/services';
|
||||
import { LabelState } from './artifact-list/artifact-list-tab/artifact-list-tab.component';
|
||||
import { forkJoin, Observable } from 'rxjs';
|
||||
import { LabelService } from 'ng-swagger-gen/services/label.service';
|
||||
import { Label } from 'ng-swagger-gen/models/label';
|
||||
import { ErrorHandler } from '../../../../../shared/units/error-handler';
|
||||
import { clone } from '../../../../../shared/units/utils';
|
||||
|
||||
const PAGE_SIZE: number = 100;
|
||||
|
||||
@Injectable()
|
||||
export class ArtifactListPageService {
|
||||
private _scanBtnState: ClrLoadingState;
|
||||
private _allLabels: LabelState[] = [];
|
||||
imageStickLabels: LabelState[] = [];
|
||||
imageFilterLabels: LabelState[] = [];
|
||||
private _hasEnabledScanner: boolean = false;
|
||||
private _hasAddLabelImagePermission: boolean = false;
|
||||
private _hasRetagImagePermission: boolean = false;
|
||||
@ -28,14 +18,10 @@ export class ArtifactListPageService {
|
||||
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
private labelService: LabelService,
|
||||
private userPermissionService: UserPermissionService,
|
||||
private errorHandlerService: ErrorHandler
|
||||
) {}
|
||||
resetClonedLabels() {
|
||||
this.imageStickLabels = clone(this._allLabels);
|
||||
this.imageFilterLabels = clone(this._allLabels);
|
||||
}
|
||||
|
||||
getScanBtnState(): ClrLoadingState {
|
||||
return this._scanBtnState;
|
||||
}
|
||||
@ -88,115 +74,6 @@ export class ArtifactListPageService {
|
||||
);
|
||||
}
|
||||
|
||||
private _getAllLabels(projectId: number): void {
|
||||
// get all project labels
|
||||
this._allLabels = []; // reset
|
||||
this.labelService
|
||||
.ListLabelsResponse({
|
||||
pageSize: PAGE_SIZE,
|
||||
page: 1,
|
||||
scope: 'p',
|
||||
projectId: projectId,
|
||||
})
|
||||
.subscribe(res => {
|
||||
if (res.headers) {
|
||||
const xHeader: string = res.headers.get('X-Total-Count');
|
||||
const totalCount = parseInt(xHeader, 0);
|
||||
let arr = res.body || [];
|
||||
if (totalCount <= PAGE_SIZE) {
|
||||
// already gotten all project labels
|
||||
if (arr && arr.length) {
|
||||
arr.forEach(data => {
|
||||
this._allLabels.push({
|
||||
iconsShow: false,
|
||||
label: data,
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
this.resetClonedLabels();
|
||||
}
|
||||
} else {
|
||||
// get all the project labels in specified times
|
||||
const times: number = Math.ceil(totalCount / PAGE_SIZE);
|
||||
const observableList: Observable<Label[]>[] = [];
|
||||
for (let i = 2; i <= times; i++) {
|
||||
observableList.push(
|
||||
this.labelService.ListLabels({
|
||||
page: i,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: 'p',
|
||||
projectId: projectId,
|
||||
})
|
||||
);
|
||||
}
|
||||
this._handleLabelRes(observableList, arr);
|
||||
}
|
||||
}
|
||||
});
|
||||
// get all global labels
|
||||
this.labelService
|
||||
.ListLabelsResponse({
|
||||
pageSize: PAGE_SIZE,
|
||||
page: 1,
|
||||
scope: 'g',
|
||||
})
|
||||
.subscribe(res => {
|
||||
if (res.headers) {
|
||||
const xHeader: string = res.headers.get('X-Total-Count');
|
||||
const totalCount = parseInt(xHeader, 0);
|
||||
let arr = res.body || [];
|
||||
if (totalCount <= PAGE_SIZE) {
|
||||
// already gotten all global labels
|
||||
if (arr && arr.length) {
|
||||
arr.forEach(data => {
|
||||
this._allLabels.push({
|
||||
iconsShow: false,
|
||||
label: data,
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
this.resetClonedLabels();
|
||||
}
|
||||
} else {
|
||||
// get all the global labels in specified times
|
||||
const times: number = Math.ceil(totalCount / PAGE_SIZE);
|
||||
const observableList: Observable<Label[]>[] = [];
|
||||
for (let i = 2; i <= times; i++) {
|
||||
observableList.push(
|
||||
this.labelService.ListLabels({
|
||||
page: i,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: 'g',
|
||||
})
|
||||
);
|
||||
}
|
||||
this._handleLabelRes(observableList, arr);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _handleLabelRes(
|
||||
observableList: Observable<Label[]>[],
|
||||
arr: Label[]
|
||||
) {
|
||||
forkJoin(observableList).subscribe(response => {
|
||||
if (response && response.length) {
|
||||
response.forEach(item => {
|
||||
arr = arr.concat(item);
|
||||
});
|
||||
arr.forEach(data => {
|
||||
this._allLabels.push({
|
||||
iconsShow: false,
|
||||
label: data,
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
this.resetClonedLabels();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _getPermissionRule(projectId: number): void {
|
||||
const permissions = [
|
||||
{
|
||||
@ -226,10 +103,6 @@ export class ArtifactListPageService {
|
||||
this._hasRetagImagePermission = results[1];
|
||||
this._hasDeleteImagePermission = results[2];
|
||||
this._hasScanImagePermission = results[3];
|
||||
// only has label permission
|
||||
if (this._hasAddLabelImagePermission) {
|
||||
this._getAllLabels(projectId);
|
||||
}
|
||||
},
|
||||
error => this.errorHandlerService.error(error)
|
||||
);
|
||||
|
@ -0,0 +1,92 @@
|
||||
<div #filterArea id="filterArea" class="clr-row">
|
||||
<clr-icon
|
||||
id="{{ searchId }}"
|
||||
*ngIf="!opened"
|
||||
shape="search"
|
||||
size="20"
|
||||
class="search-btn"
|
||||
(click)="opened = true; dropdownOpened = true"></clr-icon>
|
||||
|
||||
<ng-container *ngIf="opened">
|
||||
<div class="clr-control-container m-r-10px">
|
||||
<div class="clr-select-wrapper">
|
||||
<select
|
||||
id="{{ typeSelectId }}"
|
||||
class="clr-select"
|
||||
[(ngModel)]="filterByType"
|
||||
(change)="selectFilterType()">
|
||||
<option
|
||||
*ngFor="let filter of multipleFilter"
|
||||
value="{{ filter.filterBy }}">
|
||||
{{ filter.filterByShowText | translate }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown" [class.open]="dropdownOpened">
|
||||
<div
|
||||
class="dropdown-toggle border-bottom-color"
|
||||
(click)="dropdownOpened = !dropdownOpened">
|
||||
<clr-icon
|
||||
class="search-dropdown-toggle"
|
||||
shape="search"
|
||||
size="20"
|
||||
(click)="opened = false"></clr-icon>
|
||||
|
||||
<div class="clr-control-container" *ngIf="!selectedValue">
|
||||
<div class="clr-input-wrapper">
|
||||
<input
|
||||
readonly
|
||||
placeholder="{{
|
||||
'ARTIFACT.FILTER_FOR_ARTIFACTS' | translate
|
||||
}}"
|
||||
class="clr-input" />
|
||||
</div>
|
||||
</div>
|
||||
<span *ngIf="selectedValue">
|
||||
<span *ngIf="filterByType === 'labels'"
|
||||
><hbr-label-piece
|
||||
[label]="selectedValue"></hbr-label-piece
|
||||
></span>
|
||||
<span *ngIf="filterByType !== 'labels'">{{
|
||||
selectedValue
|
||||
}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dropdown-menu">
|
||||
<ng-container
|
||||
*ngIf="filterByType === multipleFilter[0].filterBy">
|
||||
<div
|
||||
(click)="selectValue(item)"
|
||||
class="dropdown-item"
|
||||
*ngFor="let item of multipleFilter[0].listItem">
|
||||
{{ item.showItem | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="filterByType === multipleFilter[1].filterBy">
|
||||
<div
|
||||
(click)="selectValue(item)"
|
||||
class="dropdown-item"
|
||||
*ngFor="let item of multipleFilter[1].listItem">
|
||||
{{ item.showItem | translate }}
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="filterByType === multipleFilter[2].filterBy">
|
||||
<label class="clr-control-label">{{
|
||||
'REPOSITORY.FILTER_ARTIFACT_BY_LABEL' | translate
|
||||
}}</label>
|
||||
<app-label-selector
|
||||
(clickLabel)="selectValue($event)"
|
||||
[scope]="'p'"
|
||||
[projectId]="projectId"
|
||||
[width]="200"
|
||||
[ownedLabels]="getSelectLabel()">
|
||||
</app-label-selector>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<span class="filter-divider" *ngIf="withDivider"></span>
|
||||
</div>
|
@ -0,0 +1,37 @@
|
||||
.search-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
height: 1.3rem;
|
||||
width: 200px;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
border-bottom: 0.05rem solid;
|
||||
}
|
||||
|
||||
.clr-control-label {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-divider {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
width: 1px;
|
||||
margin-right: 6px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.clr-row {
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.m-r-10px {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.search-dropdown-toggle {
|
||||
margin-right: 5px;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ArtifactFilterComponent } from './artifact-filter.component';
|
||||
import { SharedTestingModule } from '../../../../../../../../shared/shared.module';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('ArtifactFilterComponent', () => {
|
||||
let component: ArtifactFilterComponent;
|
||||
let fixture: ComponentFixture<ArtifactFilterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [ArtifactFilterComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ArtifactFilterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Expanding should work', async () => {
|
||||
await fixture.whenStable();
|
||||
const searchIcon = fixture.nativeElement.querySelector(
|
||||
`#${component.searchId}`
|
||||
);
|
||||
searchIcon.click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
let selector;
|
||||
selector = fixture.nativeElement.querySelector(
|
||||
`#${component.typeSelectId}`
|
||||
);
|
||||
expect(selector).toBeTruthy();
|
||||
const searchIconClose = fixture.nativeElement.querySelector(
|
||||
`.search-dropdown-toggle`
|
||||
);
|
||||
searchIconClose.click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
selector = fixture.nativeElement.querySelector(
|
||||
`#${component.typeSelectId}`
|
||||
);
|
||||
expect(!!selector).toBeFalse();
|
||||
});
|
||||
});
|
@ -0,0 +1,90 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
Renderer2,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { ArtifactFilterEvent, multipleFilter } from '../../../../artifact';
|
||||
import { Label } from '../../../../../../../../../../ng-swagger-gen/models/label';
|
||||
|
||||
@Component({
|
||||
selector: 'app-artifact-filter',
|
||||
templateUrl: './artifact-filter.component.html',
|
||||
styleUrls: ['./artifact-filter.component.scss'],
|
||||
})
|
||||
export class ArtifactFilterComponent {
|
||||
@Input()
|
||||
withDivider: boolean = false;
|
||||
@ViewChild('filterArea')
|
||||
filterArea: ElementRef;
|
||||
@Input()
|
||||
projectId: number;
|
||||
opened: boolean = false;
|
||||
multipleFilter = multipleFilter;
|
||||
filterByType: string = multipleFilter[0].filterBy;
|
||||
dropdownOpened: boolean = true;
|
||||
selectedValue: string | Label;
|
||||
@Output()
|
||||
filterEvent = new EventEmitter<ArtifactFilterEvent>();
|
||||
readonly searchId: string = 'search-btn';
|
||||
readonly typeSelectId: string = 'type-select';
|
||||
constructor(private renderer: Renderer2) {
|
||||
// click outside, then close dropdown
|
||||
this.renderer.listen('window', 'click', (e: Event) => {
|
||||
if (
|
||||
!(
|
||||
(e.target as any).id === this.searchId ||
|
||||
(e.target as any).id === this.typeSelectId ||
|
||||
this.filterArea.nativeElement.contains(e.target)
|
||||
)
|
||||
) {
|
||||
this.dropdownOpened = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
selectFilterType() {
|
||||
this.selectedValue = null;
|
||||
this.dropdownOpened = true;
|
||||
if (this.filterByType === this.multipleFilter[2].filterBy) {
|
||||
this.filterEvent.emit({ type: this.filterByType, isLabel: true });
|
||||
} else {
|
||||
this.filterEvent.emit({ type: this.filterByType, isLabel: false });
|
||||
}
|
||||
}
|
||||
|
||||
selectValue(value: any) {
|
||||
if (this.filterByType === this.multipleFilter[2].filterBy) {
|
||||
// for labels
|
||||
if (value.isAdd) {
|
||||
this.selectedValue = value.label;
|
||||
} else {
|
||||
this.selectedValue = null;
|
||||
}
|
||||
this.filterEvent.emit({
|
||||
type: this.filterByType,
|
||||
isLabel: true,
|
||||
label: this.selectedValue as Label,
|
||||
});
|
||||
} else {
|
||||
this.selectedValue = value?.filterText;
|
||||
this.filterEvent.emit({
|
||||
type: this.filterByType,
|
||||
isLabel: false,
|
||||
stringValue: this.selectedValue as string,
|
||||
});
|
||||
}
|
||||
}
|
||||
getSelectLabel(): Label[] {
|
||||
if (
|
||||
this.filterByType === this.multipleFilter[2].filterBy &&
|
||||
this.selectedValue
|
||||
) {
|
||||
return [this.selectedValue as Label];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@
|
||||
<clr-datagrid
|
||||
[clrDgLoading]="loading"
|
||||
(clrDgRefresh)="clrDgRefresh($event)"
|
||||
class="datagrid-top"
|
||||
[(clrDgSelected)]="selectedRow">
|
||||
<clr-dg-action-bar class="action-bar">
|
||||
<div>
|
||||
@ -55,6 +54,7 @@
|
||||
clrPosition="bottom-left"
|
||||
*clrIfOpen>
|
||||
<div
|
||||
id="artifact-list-copy-digest"
|
||||
class="action-dropdown-item no-border"
|
||||
aria-label="copy digest"
|
||||
clrDropdownItem
|
||||
@ -66,6 +66,7 @@
|
||||
</div>
|
||||
<clr-dropdown>
|
||||
<button
|
||||
id="artifact-list-add-labels"
|
||||
class="action-dropdown-item"
|
||||
clrDropdownTrigger
|
||||
[disabled]="
|
||||
@ -73,68 +74,29 @@
|
||||
!hasAddLabelImagePermission ||
|
||||
depth ||
|
||||
inprogress
|
||||
"
|
||||
(click)="addLabels()">
|
||||
">
|
||||
{{ 'REPOSITORY.ADD_LABELS' | translate }}
|
||||
</button>
|
||||
<clr-dropdown-menu
|
||||
[hidden]="!selectedRow.length">
|
||||
<div class="filter-grid">
|
||||
<div class="filter-grid" clrDropdownItem>
|
||||
<label class="dropdown-header">{{
|
||||
'REPOSITORY.ADD_LABEL_TO_IMAGE'
|
||||
| translate
|
||||
}}</label>
|
||||
<div
|
||||
class="form-group filter-label-input">
|
||||
<input
|
||||
clrInput
|
||||
type="text"
|
||||
placeholder="Filter labels"
|
||||
[(ngModel)]="stickName"
|
||||
(keyup)="
|
||||
handleStickInputFilter()
|
||||
" />
|
||||
</div>
|
||||
<div
|
||||
[hidden]="
|
||||
artifactListPageService
|
||||
?.imageStickLabels.length
|
||||
<app-label-selector
|
||||
[width]="200"
|
||||
[ownedLabels]="
|
||||
selectedRow[0]?.labels
|
||||
"
|
||||
class="no-labels">
|
||||
{{ 'LABEL.NO_LABELS' | translate }}
|
||||
</div>
|
||||
<div
|
||||
[hidden]="
|
||||
!artifactListPageService
|
||||
?.imageStickLabels.length
|
||||
"
|
||||
class="has-label">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
clrDropdownItem
|
||||
*ngFor="
|
||||
let label of artifactListPageService?.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>
|
||||
(clickLabel)="stickLabel($event)"
|
||||
[projectId]="projectId"
|
||||
[scope]="'p'"></app-label-selector>
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<div
|
||||
id="artifact-list-copy"
|
||||
class="action-dropdown-item"
|
||||
aria-label="retag"
|
||||
[clrDisabled]="
|
||||
@ -162,161 +124,11 @@
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right rightPos">
|
||||
<div id="filterArea" *ngIf="!depth">
|
||||
<div
|
||||
class="filterLabelPiece"
|
||||
*ngIf="
|
||||
openLabelFilterPiece &&
|
||||
filterByType === 'labels'
|
||||
"
|
||||
[style.left.px]="110">
|
||||
<hbr-label-piece
|
||||
*ngIf="showlabel"
|
||||
[hidden]="!filterOneLabel"
|
||||
[label]="filterOneLabel"
|
||||
[labelWidth]="130"></hbr-label-piece>
|
||||
</div>
|
||||
<div class="flex-xs-middle">
|
||||
<div class="execution-select">
|
||||
<div
|
||||
class="select filter-tag"
|
||||
[hidden]="!openSelectFilterPiece">
|
||||
<clr-select-container>
|
||||
<select
|
||||
clrSelect
|
||||
[(ngModel)]="filterByType"
|
||||
(change)="selectFilterType()">
|
||||
<option
|
||||
*ngFor="
|
||||
let filter of mutipleFilter
|
||||
"
|
||||
value="{{ filter.filterBy }}">
|
||||
{{
|
||||
filter.filterByShowText
|
||||
| translate
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
</clr-select-container>
|
||||
</div>
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-filter
|
||||
[withDivider]="true"
|
||||
[readonly]="isFilterReadonly"
|
||||
filterPlaceholder="{{
|
||||
getFilterPlaceholder() | translate
|
||||
}}"
|
||||
(filterEvt)="
|
||||
doSearchArtifactByFilter($event)
|
||||
"
|
||||
(openFlag)="openFlagEvent($event)"
|
||||
[currentValue]="lastFilteredTagName">
|
||||
</hbr-filter>
|
||||
<div
|
||||
[hidden]="!openSelectFilterPiece"
|
||||
class="label-filter-panel list-filter">
|
||||
<div
|
||||
*ngFor="
|
||||
let filter of mutipleFilter
|
||||
">
|
||||
<ul
|
||||
class="list-unstyled"
|
||||
*ngIf="
|
||||
filterByType ===
|
||||
filter.filterBy
|
||||
">
|
||||
<li
|
||||
class="cursor-pointer"
|
||||
(click)="
|
||||
selectFilter(
|
||||
item.showItem,
|
||||
item.filterText
|
||||
)
|
||||
"
|
||||
*ngFor="
|
||||
let item of filter.listItem
|
||||
">
|
||||
{{
|
||||
item.showItem
|
||||
| translate
|
||||
}}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="label-filter-panel"
|
||||
[hidden]="
|
||||
!(
|
||||
openLabelFilterPanel &&
|
||||
filterByType === 'labels'
|
||||
)
|
||||
">
|
||||
<a
|
||||
class="filterClose"
|
||||
(click)="closeFilter()"
|
||||
>×</a
|
||||
>
|
||||
<label
|
||||
class="filterLabelHeader filter-dark"
|
||||
>{{
|
||||
'REPOSITORY.FILTER_ARTIFACT_BY_LABEL'
|
||||
| translate
|
||||
}}</label
|
||||
>
|
||||
<div class="form-group mb-05">
|
||||
<input
|
||||
clrInput
|
||||
type="text"
|
||||
placeholder="Filter labels"
|
||||
[(ngModel)]="filterName"
|
||||
(keyup)="handleInputFilter()" />
|
||||
</div>
|
||||
<div
|
||||
[hidden]="
|
||||
artifactListPageService
|
||||
?.imageFilterLabels.length
|
||||
"
|
||||
class="no-labels">
|
||||
{{ 'LABEL.NO_LABELS' | translate }}
|
||||
</div>
|
||||
<div
|
||||
[hidden]="
|
||||
!artifactListPageService
|
||||
?.imageFilterLabels.length
|
||||
"
|
||||
class="has-label">
|
||||
<button
|
||||
type="button"
|
||||
class="labelBtn"
|
||||
*ngFor="
|
||||
let label of artifactListPageService?.imageFilterLabels
|
||||
"
|
||||
[hidden]="!label.show"
|
||||
(click)="
|
||||
rightFilterLabel(label)
|
||||
">
|
||||
<clr-icon
|
||||
shape="check"
|
||||
class="pull-left"
|
||||
[hidden]="
|
||||
!label.iconsShow
|
||||
"></clr-icon>
|
||||
<div class="labelDiv top-3-px">
|
||||
<hbr-label-piece
|
||||
[label]="label.label"
|
||||
[labelWidth]="
|
||||
160
|
||||
"></hbr-label-piece>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-pos">
|
||||
<app-artifact-filter
|
||||
[withDivider]="true"
|
||||
(filterEvent)="filterEvent($event)"
|
||||
[projectId]="projectId"></app-artifact-filter>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</span>
|
||||
@ -649,27 +461,23 @@
|
||||
<div
|
||||
class="signpost-item"
|
||||
[hidden]="artifact.labels?.length <= 1">
|
||||
<div class="trigger-item">
|
||||
<clr-signpost>
|
||||
<button
|
||||
class="btn btn-link"
|
||||
clrSignpostTrigger>
|
||||
...
|
||||
</button>
|
||||
<clr-signpost-content
|
||||
[clrPosition]="'top-middle'"
|
||||
*clrIfOpen>
|
||||
<div>
|
||||
<hbr-label-piece
|
||||
*ngFor="
|
||||
let label of artifact.labels
|
||||
"
|
||||
[label]="label">
|
||||
</hbr-label-piece>
|
||||
</div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
</div>
|
||||
<clr-signpost>
|
||||
<button class="btn btn-link" clrSignpostTrigger>
|
||||
...
|
||||
</button>
|
||||
<clr-signpost-content
|
||||
[clrPosition]="'top-middle'"
|
||||
*clrIfOpen>
|
||||
<div
|
||||
*ngFor="let label of artifact.labels"
|
||||
class="margin-5px">
|
||||
<hbr-label-piece
|
||||
[labelWidth]="130"
|
||||
[label]="label">
|
||||
</hbr-label-piece>
|
||||
</div>
|
||||
</clr-signpost-content>
|
||||
</clr-signpost>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dg-cell>
|
||||
|
@ -1,52 +1,4 @@
|
||||
@mixin text-overflow
|
||||
{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap:break-word;
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
|
||||
@mixin text-overflow-param($width) {
|
||||
width: $width;
|
||||
@include text-overflow;
|
||||
}
|
||||
|
||||
@mixin grid-right-top-pos{
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 35px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@mixin absolute-center($width:108px) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: $width !important;
|
||||
height: $width !important;
|
||||
}
|
||||
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@mixin dropdown-as-action-button {
|
||||
margin: .25rem .5rem .25rem 0;
|
||||
}
|
||||
|
||||
.option-right {
|
||||
padding-right: 18px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
margin-top: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -54,24 +6,11 @@
|
||||
color: #007cbb;
|
||||
}
|
||||
|
||||
.sub-header-title {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.embeded-datagrid {
|
||||
width: 98%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.hidden-tag {
|
||||
display: block;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
:host::ng-deep .datagrid-placeholder {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.truncated {
|
||||
width: 100px;
|
||||
line-height: 20px;
|
||||
@ -82,196 +21,16 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.copy-failed {
|
||||
color: red;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .datagrid clr-dg-column {
|
||||
min-width: 80px;
|
||||
}
|
||||
/* stylelint-disable */
|
||||
.rightPos {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 35px;
|
||||
.right-pos {
|
||||
margin-right: 35px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn-group .dropdown-menu clr-icon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dropdown-menu .dropdown-item {
|
||||
position: relative;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
line-height: 1.3rem;
|
||||
height: 1.3rem;
|
||||
}
|
||||
|
||||
.dropdown-menu input {
|
||||
position: relative;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pull-left {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.pull-right {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
display: inline-flex;
|
||||
min-width: 15px;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
||||
.trigger-item,
|
||||
.signpost-item {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.signpost-content-body .label {
|
||||
margin: 0.3rem;
|
||||
}
|
||||
|
||||
.labelDiv {
|
||||
position: absolute;
|
||||
left: 34px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.trigger-item hbr-label-piece {
|
||||
display: flex !important;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
:host::ng-deep .signpost-content {
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
:host::ng-deep .signpost-content-body {
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
:host::ng-deep .signpost-content-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.filterLabelPiece {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown .dropdown-toggle.btn {
|
||||
@include dropdown-as-action-button;
|
||||
}
|
||||
|
||||
.label-filter-panel {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
padding: .5rem 0;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 1px 0.125rem hsl(0deg 0% 45% / 25%);
|
||||
min-width: 5rem;
|
||||
max-width: 15rem;
|
||||
border-radius: 0.125rem;
|
||||
|
||||
.form-group input {
|
||||
position: relative;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.no-labels {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.has-label {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.labelBtn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-size: 0.58333rem;
|
||||
letter-spacing: normal;
|
||||
font-weight: 400;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #565656;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.labelBtn:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.filterLabelHeader {
|
||||
font-size: 0.5rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: normal;
|
||||
padding: 0 0.5rem;
|
||||
line-height: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filterClose {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.retag-modal-body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
hbr-image-name-input {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.datagrid-top {
|
||||
.flex-max-width {
|
||||
max-width: 220px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.icon-cell {
|
||||
max-width: 1.5rem;
|
||||
min-width: 1.5rem;
|
||||
|
||||
clr-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color-green {
|
||||
color: #1d5100;
|
||||
}
|
||||
@ -284,12 +43,6 @@ hbr-image-name-input {
|
||||
color: #565656;
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
::ng-deep .datagrid-table {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -301,17 +54,6 @@ clr-datagrid {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.immutable {
|
||||
padding-right: 94px;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.white-normal {
|
||||
white-space: normal;
|
||||
}
|
||||
@ -322,15 +64,6 @@ clr-datagrid {
|
||||
flex-shrink:0;
|
||||
}
|
||||
|
||||
.max-width-38 {
|
||||
max-width: 38px !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
clr-datagrid {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.artifact-icon {
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
@ -345,10 +78,6 @@ clr-datagrid {
|
||||
}
|
||||
}
|
||||
|
||||
.w-rem-4 {
|
||||
min-width: 4rem !important;
|
||||
}
|
||||
|
||||
.tag-header-color {
|
||||
color: #fff;
|
||||
}
|
||||
@ -393,20 +122,6 @@ clr-datagrid {
|
||||
}
|
||||
}
|
||||
|
||||
.filter-label-input {
|
||||
::ng-deep {
|
||||
.clr-input-wrapper {
|
||||
max-height: 2rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.scan-btn{
|
||||
margin-top: -.3rem;
|
||||
}
|
||||
|
||||
.eslip {
|
||||
margin-left: -3px;
|
||||
}
|
||||
@ -426,16 +141,6 @@ clr-datagrid {
|
||||
}
|
||||
}
|
||||
|
||||
.list-filter {
|
||||
width:7rem;
|
||||
margin-left: 0.8rem;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.no-border:focus {
|
||||
outline: none;
|
||||
}
|
||||
@ -484,8 +189,36 @@ clr-datagrid {
|
||||
.width-p-75 {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.signpost-item {
|
||||
margin-left: -24px;
|
||||
height: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.margin-5px {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.flex-max-width {
|
||||
max-width: 220px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.filter-grid {
|
||||
cursor: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.filter-grid:hover {
|
||||
color: unset;
|
||||
background-color: unset;
|
||||
|
||||
}
|
||||
|
@ -3,21 +3,15 @@ import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ArtifactListTabComponent } from './artifact-list-tab.component';
|
||||
import { of } from 'rxjs';
|
||||
import { delay } from 'rxjs/operators';
|
||||
import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
ArtifactDefaultService,
|
||||
ArtifactService,
|
||||
} from '../../../artifact.service';
|
||||
import {
|
||||
Label,
|
||||
ProjectDefaultService,
|
||||
ProjectService,
|
||||
ScanningResultDefaultService,
|
||||
ScanningResultService,
|
||||
UserPermissionDefaultService,
|
||||
UserPermissionService,
|
||||
USERSTATICPERMISSION,
|
||||
} from '../../../../../../../shared/services';
|
||||
import { ArtifactFront as Artifact } from '../../../artifact';
|
||||
import { ErrorHandler } from '../../../../../../../shared/units/error-handler';
|
||||
@ -25,30 +19,25 @@ import { OperationService } from '../../../../../../../shared/components/operati
|
||||
import { ArtifactService as NewArtifactService } from '../../../../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
import { Tag } from '../../../../../../../../../ng-swagger-gen/models/tag';
|
||||
import { SharedTestingModule } from '../../../../../../../shared/shared.module';
|
||||
import { LabelService } from '../../../../../../../../../ng-swagger-gen/services/label.service';
|
||||
import { Registry } from '../../../../../../../../../ng-swagger-gen/models/registry';
|
||||
import { AppConfigService } from '../../../../../../../services/app-config.service';
|
||||
import { ArtifactListPageService } from '../../artifact-list-page.service';
|
||||
import { ClrLoadingState } from '@clr/angular';
|
||||
import { Accessory } from 'ng-swagger-gen/models/accessory';
|
||||
import { ArtifactModule } from '../../../artifact.module';
|
||||
|
||||
describe('ArtifactListTabComponent (inline template)', () => {
|
||||
let comp: ArtifactListTabComponent;
|
||||
let fixture: ComponentFixture<ArtifactListTabComponent>;
|
||||
let userPermissionService: UserPermissionService;
|
||||
let spyLabels: jasmine.Spy;
|
||||
let spyLabels1: jasmine.Spy;
|
||||
let spyScanner: jasmine.Spy;
|
||||
const scannerMock = {
|
||||
disabled: false,
|
||||
name: 'Trivy',
|
||||
};
|
||||
const mockActivatedRoute = {
|
||||
snapshot: {
|
||||
params: {
|
||||
id: 1,
|
||||
repo: 'test',
|
||||
digest: 'ABC',
|
||||
parent: {
|
||||
parent: {
|
||||
id: 1,
|
||||
repo: 'test',
|
||||
digest: 'ABC',
|
||||
},
|
||||
},
|
||||
},
|
||||
data: {
|
||||
projectResolver: {
|
||||
@ -182,77 +171,9 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
pull_time: '0001-01-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
let filtereName = '';
|
||||
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: '',
|
||||
},
|
||||
];
|
||||
let mockHasAddLabelImagePermission: boolean = true;
|
||||
let mockHasRetagImagePermission: boolean = true;
|
||||
let mockHasDeleteImagePermission: boolean = true;
|
||||
let mockHasScanImagePermission: boolean = true;
|
||||
const mockErrorHandler = {
|
||||
error: () => {},
|
||||
};
|
||||
const permissions = [
|
||||
{
|
||||
resource: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.KEY,
|
||||
action: USERSTATICPERMISSION.REPOSITORY_ARTIFACT_LABEL.VALUE.CREATE,
|
||||
},
|
||||
{
|
||||
resource: USERSTATICPERMISSION.REPOSITORY.KEY,
|
||||
action: USERSTATICPERMISSION.REPOSITORY.VALUE.PULL,
|
||||
},
|
||||
{
|
||||
resource: USERSTATICPERMISSION.ARTIFACT.KEY,
|
||||
action: USERSTATICPERMISSION.ARTIFACT.VALUE.DELETE,
|
||||
},
|
||||
{
|
||||
resource: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.KEY,
|
||||
action: USERSTATICPERMISSION.REPOSITORY_TAG_SCAN_JOB.VALUE.CREATE,
|
||||
},
|
||||
];
|
||||
const mockRouter = {
|
||||
events: {
|
||||
subscribe: () => {
|
||||
@ -285,15 +206,10 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
return of(null).pipe(delay(0));
|
||||
},
|
||||
listArtifactsResponse: () => {
|
||||
if (filtereName === 'sha256:3e33e3e3') {
|
||||
return of({
|
||||
body: [mockArtifacts[1]],
|
||||
});
|
||||
} else {
|
||||
return of({
|
||||
body: mockArtifacts,
|
||||
}).pipe(delay(0));
|
||||
}
|
||||
return of({
|
||||
headers: new HttpHeaders({ 'x-total-count': '2' }),
|
||||
body: mockArtifacts,
|
||||
}).pipe(delay(0));
|
||||
},
|
||||
deleteArtifact: () => of(null),
|
||||
getIconsFromBackEnd() {
|
||||
@ -317,9 +233,6 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
};
|
||||
|
||||
const mockedArtifactListPageService = {
|
||||
imageStickLabels: [],
|
||||
imageFilterLabels: [],
|
||||
resetClonedLabels() {},
|
||||
getScanBtnState(): ClrLoadingState {
|
||||
return ClrLoadingState.DEFAULT;
|
||||
},
|
||||
@ -342,7 +255,7 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
};
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
imports: [SharedTestingModule, ArtifactModule],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
declarations: [ArtifactListTabComponent],
|
||||
providers: [
|
||||
@ -354,15 +267,10 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
{ provide: AppConfigService, useValue: mockedAppConfigService },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
{ provide: ArtifactService, useValue: mockNewArtifactService },
|
||||
{ provide: ProjectService, useClass: ProjectDefaultService },
|
||||
{
|
||||
provide: ScanningResultService,
|
||||
useClass: ScanningResultDefaultService,
|
||||
},
|
||||
{
|
||||
provide: UserPermissionService,
|
||||
useClass: UserPermissionDefaultService,
|
||||
},
|
||||
{ provide: ErrorHandler, useValue: mockErrorHandler },
|
||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
||||
{ provide: OperationService, useValue: mockOperationService },
|
||||
@ -374,43 +282,12 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
fixture = TestBed.createComponent(ArtifactListTabComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.projectId = 1;
|
||||
comp.repoName = 'library/nginx';
|
||||
comp.registryUrl = 'http://registry.testing.com';
|
||||
let labelService: LabelService;
|
||||
userPermissionService = fixture.debugElement.injector.get(
|
||||
UserPermissionService
|
||||
);
|
||||
let http: HttpClient;
|
||||
http = fixture.debugElement.injector.get(HttpClient);
|
||||
spyScanner = spyOn(http, 'get').and.returnValue(of(scannerMock));
|
||||
spyOn(userPermissionService, 'hasProjectPermissions')
|
||||
.withArgs(comp.projectId, permissions)
|
||||
.and.returnValue(
|
||||
of([
|
||||
mockHasAddLabelImagePermission,
|
||||
mockHasRetagImagePermission,
|
||||
mockHasDeleteImagePermission,
|
||||
mockHasScanImagePermission,
|
||||
])
|
||||
);
|
||||
|
||||
labelService = fixture.debugElement.injector.get(LabelService);
|
||||
const response: HttpResponse<Array<Registry>> = new HttpResponse<
|
||||
Array<Registry>
|
||||
>({
|
||||
headers: new HttpHeaders({ 'x-total-count': [].length.toString() }),
|
||||
body: mockLabels,
|
||||
});
|
||||
spyLabels = spyOn(labelService, 'ListLabelsResponse').and.returnValues(
|
||||
of(response).pipe(delay(0))
|
||||
);
|
||||
spyLabels1 = spyOn(labelService, 'ListLabels')
|
||||
.withArgs({ projectId: comp.projectId })
|
||||
.and.returnValues(of(mockLabels1).pipe(delay(0)));
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
comp.loading = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@ -426,33 +303,63 @@ describe('ArtifactListTabComponent (inline template)', () => {
|
||||
expect(el.textContent).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('sha256:4875cda3');
|
||||
});
|
||||
it('should filter data by keyword', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
filtereName = 'sha256:3e33e3e3';
|
||||
comp.doSearchArtifactByFilter('sha256:3e33e3e3');
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
const el: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('.digest');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('sha256:3e33e3e3');
|
||||
});
|
||||
it('should delete artifact', async () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
it('should open copy digest modal', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[0]];
|
||||
filtereName = 'sha256:3e33e3e3';
|
||||
comp.confirmDeletion({ source: 9, state: 1, data: comp.selectedRow });
|
||||
await stepOpenAction(fixture, comp);
|
||||
fixture.nativeElement
|
||||
.querySelector('#artifact-list-copy-digest')
|
||||
.click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(fixture.nativeElement.querySelector('textarea')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should open add labels modal', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
fixture.nativeElement
|
||||
.querySelector('#artifact-list-add-labels')
|
||||
.click();
|
||||
fixture.detectChanges();
|
||||
const el: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('.digest');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('sha256:3e33e3e3');
|
||||
await fixture.whenStable();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('app-label-selector')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should open copy artifact modal', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
fixture.nativeElement.querySelector('#artifact-list-copy').click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('hbr-image-name-input')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should open delete modal', async () => {
|
||||
await fixture.whenStable();
|
||||
comp.selectedRow = [mockArtifacts[1]];
|
||||
await stepOpenAction(fixture, comp);
|
||||
fixture.nativeElement.querySelector('#artifact-list-delete').click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.confirmation-title')
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
async function stepOpenAction(fixture, comp) {
|
||||
comp.projectId = 1;
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.nativeElement.querySelector('#artifact-list-action').click();
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
}
|
||||
|
@ -11,21 +11,9 @@
|
||||
// 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,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { forkJoin, Observable, of, Subject, Subscription } from 'rxjs';
|
||||
import {
|
||||
catchError,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
finalize,
|
||||
map,
|
||||
} from 'rxjs/operators';
|
||||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
|
||||
import { forkJoin, Observable, of, Subscription } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
ClrDatagridComparatorInterface,
|
||||
@ -48,7 +36,6 @@ import {
|
||||
setPageSizeToLocalStorage,
|
||||
VULNERABILITY_SCAN_STATUS,
|
||||
} from '../../../../../../../shared/units/utils';
|
||||
import { ImageNameInputComponent } from '../../../../../../../shared/components/image-name-input/image-name-input.component';
|
||||
import { ErrorHandler } from '../../../../../../../shared/units/error-handler';
|
||||
import { ArtifactService } from '../../../artifact.service';
|
||||
import { OperationService } from '../../../../../../../shared/components/operation/operation.service';
|
||||
@ -65,12 +52,12 @@ import {
|
||||
import {
|
||||
AccessoryType,
|
||||
artifactDefault,
|
||||
ArtifactFilterEvent,
|
||||
ArtifactFront as Artifact,
|
||||
ArtifactFront,
|
||||
ArtifactType,
|
||||
getPullCommandByDigest,
|
||||
getPullCommandByTag,
|
||||
mutipleFilter,
|
||||
} from '../../../artifact';
|
||||
import { Project } from '../../../../../project';
|
||||
import { ArtifactService as NewArtifactService } from '../../../../../../../../../ng-swagger-gen/services/artifact.service';
|
||||
@ -98,12 +85,6 @@ import { Tag } from '../../../../../../../../../ng-swagger-gen/models/tag';
|
||||
import { CopyArtifactComponent } from './copy-artifact/copy-artifact.component';
|
||||
import { CopyDigestComponent } from './copy-digest/copy-digest.component';
|
||||
|
||||
export interface LabelState {
|
||||
iconsShow: boolean;
|
||||
label: Label;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const AVAILABLE_TIME = '0001-01-01T00:00:00.000Z';
|
||||
|
||||
const CHECKING: string = 'checking';
|
||||
@ -122,12 +103,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
registryUrl: string;
|
||||
artifactList: ArtifactFront[] = [];
|
||||
availableTime = AVAILABLE_TIME;
|
||||
lastFilteredTagName: string;
|
||||
inprogress: boolean;
|
||||
openLabelFilterPanel: boolean;
|
||||
openLabelFilterPiece: boolean;
|
||||
showlabel: boolean;
|
||||
|
||||
pullComparator: Comparator<Artifact> = new CustomComparator<Artifact>(
|
||||
'pull_time',
|
||||
'date'
|
||||
@ -139,21 +115,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
|
||||
loading = true;
|
||||
selectedRow: Artifact[] = [];
|
||||
labelListOpen = false;
|
||||
selectedTag: Artifact[];
|
||||
labelNameFilter: Subject<string> = new Subject<string>();
|
||||
stickLabelNameFilter: Subject<string> = new Subject<string>();
|
||||
filterOnGoing: boolean;
|
||||
stickName = '';
|
||||
filterName = '';
|
||||
initFilter = {
|
||||
name: '',
|
||||
description: '',
|
||||
color: '',
|
||||
scope: '',
|
||||
project_id: 0,
|
||||
};
|
||||
filterOneLabel: Label = this.initFilter;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
@ -193,11 +154,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
scanStoppedArtifactLength: number = 0;
|
||||
artifactDigest: string;
|
||||
depth: string;
|
||||
labelNameFilterSub: Subscription;
|
||||
stickLabelNameFilterSub: Subscription;
|
||||
mutipleFilter = clone(mutipleFilter);
|
||||
filterByType: string = this.mutipleFilter[0].filterBy;
|
||||
openSelectFilterPiece = false;
|
||||
// could Pagination filter
|
||||
filters: string[];
|
||||
scanFinishedArtifactLength: number = 0;
|
||||
@ -214,7 +170,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
private appConfigService: AppConfigService,
|
||||
public artifactListPageService: ArtifactListPageService
|
||||
private artifactListPageService: ArtifactListPageService
|
||||
) {}
|
||||
initRouterData() {
|
||||
this.projectId =
|
||||
@ -237,10 +193,8 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
const arr: string[] = this.depth.split('-');
|
||||
this.artifactDigest = this.depth.split('-')[arr.length - 1];
|
||||
}
|
||||
this.lastFilteredTagName = '';
|
||||
}
|
||||
ngOnInit() {
|
||||
this.artifactListPageService.resetClonedLabels();
|
||||
this.registryUrl = this.appConfigService.getConfig().registry_url;
|
||||
this.initRouterData();
|
||||
if (!this.updateArtifactSub) {
|
||||
@ -257,86 +211,17 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!this.labelNameFilterSub) {
|
||||
this.labelNameFilterSub = this.labelNameFilter
|
||||
.pipe(debounceTime(500))
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((name: string) => {
|
||||
if (this.filterName.length) {
|
||||
this.filterOnGoing = true;
|
||||
this.artifactListPageService.imageFilterLabels.forEach(
|
||||
data => {
|
||||
data.show =
|
||||
data.label.name.indexOf(this.filterName) !==
|
||||
-1;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!this.stickLabelNameFilterSub) {
|
||||
this.stickLabelNameFilterSub = this.stickLabelNameFilter
|
||||
.pipe(debounceTime(500))
|
||||
.pipe(distinctUntilChanged())
|
||||
.subscribe((name: string) => {
|
||||
if (this.stickName.length) {
|
||||
this.filterOnGoing = true;
|
||||
this.artifactListPageService.imageStickLabels.forEach(
|
||||
data => {
|
||||
data.show =
|
||||
data.label.name.indexOf(this.stickName) !==
|
||||
-1;
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this.labelNameFilterSub) {
|
||||
this.labelNameFilterSub.unsubscribe();
|
||||
this.labelNameFilterSub = null;
|
||||
}
|
||||
if (this.stickLabelNameFilterSub) {
|
||||
this.stickLabelNameFilterSub.unsubscribe();
|
||||
this.stickLabelNameFilterSub = null;
|
||||
}
|
||||
if (this.updateArtifactSub) {
|
||||
this.updateArtifactSub.unsubscribe();
|
||||
this.updateArtifactSub = null;
|
||||
}
|
||||
}
|
||||
public get filterLabelPieceWidth() {
|
||||
let len = this.lastFilteredTagName.length
|
||||
? this.lastFilteredTagName.length * 6 + 60
|
||||
: 115;
|
||||
return len > 210 ? 210 : len;
|
||||
}
|
||||
|
||||
get withNotary(): boolean {
|
||||
return this.appConfigService.getConfig()?.with_notary;
|
||||
}
|
||||
|
||||
doSearchArtifactByFilter(filterWords) {
|
||||
this.lastFilteredTagName = filterWords;
|
||||
this.currentPage = 1;
|
||||
|
||||
let st: ClrDatagridStateInterface = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
this.filters = [];
|
||||
if (this.lastFilteredTagName) {
|
||||
this.filters.push(
|
||||
`${this.filterByType}=~${this.lastFilteredTagName}`
|
||||
);
|
||||
}
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
clrDgRefresh(state: ClrDatagridStateInterface) {
|
||||
setTimeout(() => {
|
||||
//add setTimeout to avoid ng check error
|
||||
@ -538,39 +423,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
return pullCommand;
|
||||
}
|
||||
labelSelectedChange(artifact?: Artifact[]): void {
|
||||
this.artifactListPageService.imageStickLabels.forEach(data => {
|
||||
data.iconsShow = false;
|
||||
data.show = true;
|
||||
});
|
||||
if (artifact && artifact[0].labels && artifact[0].labels.length) {
|
||||
artifact[0].labels.forEach((labelInfo: Label) => {
|
||||
let findedLabel =
|
||||
this.artifactListPageService.imageStickLabels.find(
|
||||
data => labelInfo.id === data['label'].id
|
||||
);
|
||||
if (findedLabel) {
|
||||
this.artifactListPageService.imageStickLabels.splice(
|
||||
this.artifactListPageService.imageStickLabels.indexOf(
|
||||
findedLabel
|
||||
),
|
||||
1
|
||||
);
|
||||
this.artifactListPageService.imageStickLabels.unshift(
|
||||
findedLabel
|
||||
);
|
||||
findedLabel.iconsShow = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addLabels(): void {
|
||||
this.labelListOpen = true;
|
||||
this.selectedTag = this.selectedRow;
|
||||
this.stickName = '';
|
||||
this.labelSelectedChange(this.selectedRow);
|
||||
}
|
||||
|
||||
canAddLabel(): boolean {
|
||||
if (this.selectedRow && this.selectedRow.length === 1) {
|
||||
@ -590,250 +442,60 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
return false;
|
||||
}
|
||||
|
||||
stickLabel(labelInfo: LabelState): void {
|
||||
if (labelInfo && !labelInfo.iconsShow) {
|
||||
this.selectLabel(labelInfo);
|
||||
}
|
||||
if (labelInfo && labelInfo.iconsShow) {
|
||||
this.unSelectLabel(labelInfo);
|
||||
stickLabel(labelEvent: { label: Label; isAdd: boolean }): void {
|
||||
if (labelEvent.isAdd) {
|
||||
this.addLabel(labelEvent?.label);
|
||||
} else {
|
||||
this.removeLabel(labelEvent?.label);
|
||||
}
|
||||
}
|
||||
|
||||
selectLabel(labelInfo: LabelState): void {
|
||||
addLabel(label: Label) {
|
||||
if (!this.inprogress) {
|
||||
// add label to multiple artifact
|
||||
const ObservableArr: Array<Observable<null>> = [];
|
||||
this.selectedRow.forEach(item => {
|
||||
const params: NewArtifactService.AddLabelParams = {
|
||||
projectName: this.projectName,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
reference: item.digest,
|
||||
label: labelInfo.label,
|
||||
};
|
||||
ObservableArr.push(this.newArtifactService.addLabel(params));
|
||||
});
|
||||
const params: NewArtifactService.AddLabelParams = {
|
||||
projectName: this.projectName,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
reference: this.selectedRow[0].digest,
|
||||
label: label,
|
||||
};
|
||||
this.inprogress = true;
|
||||
forkJoin(ObservableArr)
|
||||
this.newArtifactService
|
||||
.addLabel(params)
|
||||
.pipe(finalize(() => (this.inprogress = false)))
|
||||
.subscribe(
|
||||
res => {
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.refresh();
|
||||
// set the selected label in front
|
||||
this.artifactListPageService.imageStickLabels.splice(
|
||||
this.artifactListPageService.imageStickLabels.indexOf(
|
||||
labelInfo
|
||||
),
|
||||
1
|
||||
);
|
||||
this.artifactListPageService.imageStickLabels.some(
|
||||
(data, i) => {
|
||||
if (!data.iconsShow) {
|
||||
this.artifactListPageService.imageStickLabels.splice(
|
||||
i,
|
||||
0,
|
||||
labelInfo
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
);
|
||||
// when is the last one
|
||||
if (
|
||||
this.artifactListPageService.imageStickLabels.every(
|
||||
data => data.iconsShow === true
|
||||
)
|
||||
) {
|
||||
this.artifactListPageService.imageStickLabels.push(
|
||||
labelInfo
|
||||
);
|
||||
}
|
||||
labelInfo.iconsShow = true;
|
||||
},
|
||||
err => {
|
||||
error: err => {
|
||||
this.refresh();
|
||||
this.errorHandlerService.error(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
unSelectLabel(labelInfo: LabelState): void {
|
||||
removeLabel(label: Label) {
|
||||
if (!this.inprogress) {
|
||||
this.inprogress = true;
|
||||
let labelId = labelInfo.label.id;
|
||||
this.selectedRow = this.selectedTag;
|
||||
let params: NewArtifactService.RemoveLabelParams = {
|
||||
projectName: this.projectName,
|
||||
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||
reference: this.selectedRow[0].digest,
|
||||
labelId: labelId,
|
||||
labelId: label.id,
|
||||
};
|
||||
this.newArtifactService.removeLabel(params).subscribe(
|
||||
res => {
|
||||
this.refresh();
|
||||
|
||||
// insert the unselected label to groups with the same icons
|
||||
this.sortOperation(
|
||||
this.artifactListPageService.imageStickLabels,
|
||||
labelInfo
|
||||
);
|
||||
labelInfo.iconsShow = false;
|
||||
this.inprogress = false;
|
||||
},
|
||||
err => {
|
||||
this.inprogress = false;
|
||||
this.errorHandlerService.error(err);
|
||||
}
|
||||
);
|
||||
this.inprogress = true;
|
||||
this.newArtifactService
|
||||
.removeLabel(params)
|
||||
.pipe(finalize(() => (this.inprogress = false)))
|
||||
.subscribe({
|
||||
next: res => {
|
||||
this.refresh();
|
||||
},
|
||||
error: err => {
|
||||
this.refresh();
|
||||
this.errorHandlerService.error(err);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rightFilterLabel(labelInfo: LabelState): void {
|
||||
if (labelInfo) {
|
||||
if (!labelInfo.iconsShow) {
|
||||
this.filterLabel(labelInfo);
|
||||
this.showlabel = true;
|
||||
} else {
|
||||
this.unFilterLabel(labelInfo);
|
||||
this.showlabel = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filterLabel(labelInfo: LabelState): void {
|
||||
let labelId = labelInfo.label.id;
|
||||
this.artifactListPageService.imageFilterLabels.filter(data => {
|
||||
data.iconsShow = data.label.id === labelId;
|
||||
});
|
||||
this.filterOneLabel = labelInfo.label;
|
||||
|
||||
// reload data
|
||||
this.currentPage = 1;
|
||||
let st: ClrDatagridStateInterface = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
|
||||
this.filters = [`${this.filterByType}=(${labelId})`];
|
||||
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
unFilterLabel(labelInfo: LabelState): void {
|
||||
this.filterOneLabel = this.initFilter;
|
||||
labelInfo.iconsShow = false;
|
||||
// reload data
|
||||
this.currentPage = 1;
|
||||
let st: ClrDatagridStateInterface = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
|
||||
this.filters = [];
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
closeFilter(): void {
|
||||
this.openLabelFilterPanel = false;
|
||||
}
|
||||
|
||||
reSortImageFilterLabels() {
|
||||
if (
|
||||
this.artifactListPageService.imageFilterLabels &&
|
||||
this.artifactListPageService.imageFilterLabels.length
|
||||
) {
|
||||
for (
|
||||
let i = 0;
|
||||
i < this.artifactListPageService.imageFilterLabels.length;
|
||||
i++
|
||||
) {
|
||||
if (
|
||||
this.artifactListPageService.imageFilterLabels[i].iconsShow
|
||||
) {
|
||||
const arr: LabelState[] =
|
||||
this.artifactListPageService.imageFilterLabels.splice(
|
||||
i,
|
||||
1
|
||||
);
|
||||
this.artifactListPageService.imageFilterLabels.unshift(
|
||||
...arr
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getFilterPlaceholder(): string {
|
||||
return this.showlabel ? '' : 'ARTIFACT.FILTER_FOR_ARTIFACTS';
|
||||
}
|
||||
|
||||
openFlagEvent(isOpen: boolean): void {
|
||||
if (isOpen) {
|
||||
this.openLabelFilterPanel = true;
|
||||
// every time when filer panel opens, resort imageFilterLabels labels
|
||||
this.reSortImageFilterLabels();
|
||||
this.openLabelFilterPiece = true;
|
||||
this.openSelectFilterPiece = true;
|
||||
this.filterName = '';
|
||||
// redisplay all labels
|
||||
this.artifactListPageService.imageFilterLabels.forEach(data => {
|
||||
data.show = data.label.name.indexOf(this.filterName) !== -1;
|
||||
});
|
||||
} else {
|
||||
this.openLabelFilterPanel = false;
|
||||
this.openLabelFilterPiece = false;
|
||||
this.openSelectFilterPiece = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleInputFilter() {
|
||||
if (this.filterName.length) {
|
||||
this.labelNameFilter.next(this.filterName);
|
||||
} else {
|
||||
this.artifactListPageService.imageFilterLabels.every(
|
||||
data => (data.show = true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleStickInputFilter() {
|
||||
if (this.stickName.length) {
|
||||
this.stickLabelNameFilter.next(this.stickName);
|
||||
} else {
|
||||
this.artifactListPageService.imageStickLabels.every(
|
||||
data => (data.show = true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// insert the unselected label to groups with the same icons
|
||||
sortOperation(labelList: LabelState[], labelInfo: LabelState): void {
|
||||
labelList.some((data, i) => {
|
||||
if (!data.iconsShow) {
|
||||
if (data.label.scope === labelInfo.label.scope) {
|
||||
labelList.splice(i, 0, labelInfo);
|
||||
labelList.splice(labelList.indexOf(labelInfo, 0), 1);
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
data.label.scope !== labelInfo.label.scope &&
|
||||
i === labelList.length - 1
|
||||
) {
|
||||
labelList.push(labelInfo);
|
||||
labelList.splice(labelList.indexOf(labelInfo), 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sizeTransform(tagSize: string): string {
|
||||
return formatSize(tagSize);
|
||||
}
|
||||
@ -1011,7 +673,6 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
return of(error);
|
||||
})
|
||||
);
|
||||
// }
|
||||
}
|
||||
|
||||
showDigestId() {
|
||||
@ -1147,56 +808,19 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
selectFilterType() {
|
||||
this.lastFilteredTagName = '';
|
||||
if (this.filterByType === 'labels') {
|
||||
this.openLabelFilterPanel = true;
|
||||
// every time when filer panel opens, resort imageFilterLabels labels
|
||||
this.reSortImageFilterLabels();
|
||||
this.openLabelFilterPiece = true;
|
||||
filterEvent(e: ArtifactFilterEvent) {
|
||||
this.filters = [];
|
||||
if (e?.isLabel) {
|
||||
if (e?.label?.name) {
|
||||
this.filters.push(`${e.type}=(${e?.label?.id})`);
|
||||
}
|
||||
} else {
|
||||
this.openLabelFilterPiece = false;
|
||||
this.filterOneLabel = this.initFilter;
|
||||
this.showlabel = false;
|
||||
this.artifactListPageService.imageFilterLabels.forEach(data => {
|
||||
data.iconsShow = false;
|
||||
});
|
||||
if (e?.stringValue) {
|
||||
this.filters.push(`${e.type}=${e?.stringValue}`);
|
||||
}
|
||||
}
|
||||
this.currentPage = 1;
|
||||
let st: ClrDatagridStateInterface = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
this.filters = [];
|
||||
this.clrLoad(st);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
selectFilter(showItem: string, filterItem: string) {
|
||||
this.lastFilteredTagName = filterItem;
|
||||
this.currentPage = 1;
|
||||
|
||||
let st: ClrDatagridStateInterface = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
this.filters = [];
|
||||
if (filterItem) {
|
||||
this.filters.push(`${this.filterByType}=${filterItem}`);
|
||||
}
|
||||
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
get isFilterReadonly() {
|
||||
return this.filterByType === 'labels' ? 'readonly' : null;
|
||||
}
|
||||
|
||||
// when finished, remove it from selectedRow
|
||||
scanFinished(artifact: Artifact) {
|
||||
if (this.selectedRow && this.selectedRow.length) {
|
||||
|
@ -23,6 +23,7 @@ import { SubAccessoriesComponent } from './artifact-list-page/artifact-list/arti
|
||||
import { ArtifactListPageService } from './artifact-list-page/artifact-list-page.service';
|
||||
import { CopyArtifactComponent } from './artifact-list-page/artifact-list/artifact-list-tab/copy-artifact/copy-artifact.component';
|
||||
import { CopyDigestComponent } from './artifact-list-page/artifact-list/artifact-list-tab/copy-digest/copy-digest.component';
|
||||
import { ArtifactFilterComponent } from './artifact-list-page/artifact-list/artifact-list-tab/artifact-filter/artifact-filter.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -86,6 +87,7 @@ const routes: Routes = [
|
||||
SubAccessoriesComponent,
|
||||
CopyArtifactComponent,
|
||||
CopyDigestComponent,
|
||||
ArtifactFilterComponent,
|
||||
],
|
||||
imports: [RouterModule.forChild(routes), SharedModule],
|
||||
providers: [
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Accessory } from 'ng-swagger-gen/models/accessory';
|
||||
import { Artifact } from '../../../../../../ng-swagger-gen/models/artifact';
|
||||
import { Platform } from '../../../../../../ng-swagger-gen/models/platform';
|
||||
import { Label } from '../../../../../../ng-swagger-gen/models/label';
|
||||
|
||||
export interface ArtifactFront extends Artifact {
|
||||
platform?: Platform;
|
||||
@ -18,7 +19,11 @@ export interface AccessoryFront extends Accessory {
|
||||
scan_overview?: any;
|
||||
}
|
||||
|
||||
export const mutipleFilter = [
|
||||
export const multipleFilter: Array<{
|
||||
filterBy: string;
|
||||
filterByShowText: string;
|
||||
listItem: any[];
|
||||
}> = [
|
||||
{
|
||||
filterBy: 'type',
|
||||
filterByShowText: 'Type',
|
||||
@ -120,3 +125,10 @@ export function getPullCommandByTag(
|
||||
}
|
||||
return pullCommand;
|
||||
}
|
||||
|
||||
export interface ArtifactFilterEvent {
|
||||
type?: string;
|
||||
stringValue?: string;
|
||||
isLabel?: boolean;
|
||||
label?: Label;
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
<div [style.width.px]="width">
|
||||
<div class="clr-input-wrapper filter-label-input">
|
||||
<input
|
||||
class="clr-input"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="{{ 'LABEL.FILTER_LABEL_PLACEHOLDER' | translate }}"
|
||||
[(ngModel)]="searchValue"
|
||||
(keyup)="search()" />
|
||||
</div>
|
||||
<div class="no-labels" [hidden]="!loading">
|
||||
<span class="spinner spinner-inline"></span>
|
||||
</div>
|
||||
<div [hidden]="loading">
|
||||
<div
|
||||
[hidden]="candidateLabels.length"
|
||||
class="no-labels"
|
||||
(click)="goToLabelPage()">
|
||||
{{ 'LABEL.NO_LABELS' | translate }}
|
||||
</div>
|
||||
<div [hidden]="!candidateLabels.length" class="has-label">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown-item"
|
||||
*ngFor="let label of candidateLabels"
|
||||
(click)="selectLabel(label)">
|
||||
<clr-icon
|
||||
shape="check"
|
||||
class="check-icon"
|
||||
[style.visibility]="isSelect(label) ? 'visible' : 'hidden'">
|
||||
</clr-icon>
|
||||
<hbr-label-piece [label]="label" [labelWidth]="130">
|
||||
</hbr-label-piece>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,33 @@
|
||||
.has-label {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.filter-label-input {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.no-labels {
|
||||
height: 4rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.clr-form-control {
|
||||
margin-top: 0 !important;
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { LabelSelectorComponent } from './label-selector.component';
|
||||
import { SharedTestingModule } from '../../shared.module';
|
||||
import { LabelService } from '../../../../../ng-swagger-gen/services/label.service';
|
||||
import { Label } from '../../../../../ng-swagger-gen/models/label';
|
||||
import { of } from 'rxjs';
|
||||
import { delay, finalize } from 'rxjs/operators';
|
||||
|
||||
describe('LabelSelectorComponent', () => {
|
||||
let component: LabelSelectorComponent;
|
||||
let fixture: ComponentFixture<LabelSelectorComponent>;
|
||||
const mockedLabels: Label[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'good',
|
||||
scope: 'p',
|
||||
project_id: 1,
|
||||
color: '#ccc',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'bad',
|
||||
scope: 'p',
|
||||
project_id: 1,
|
||||
color: '#ccc',
|
||||
},
|
||||
];
|
||||
let spy: jasmine.Spy;
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [SharedTestingModule],
|
||||
declarations: [LabelSelectorComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LabelSelectorComponent);
|
||||
component = fixture.componentInstance;
|
||||
spy = spyOn(TestBed.inject(LabelService), 'ListLabels').and.returnValue(
|
||||
of(mockedLabels).pipe(delay(0))
|
||||
);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
component.loading = false;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render candidates', async () => {
|
||||
await fixture.whenStable();
|
||||
const rows = fixture.nativeElement.querySelectorAll('hbr-label-piece');
|
||||
expect(rows.length).toEqual(2);
|
||||
});
|
||||
|
||||
it('owned labels should be checked', async () => {
|
||||
await fixture.whenStable();
|
||||
component.ownedLabels = [mockedLabels[0]];
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
const checkIcon = fixture.nativeElement.querySelector('.check-icon');
|
||||
expect(checkIcon.style.visibility).toEqual('visible');
|
||||
});
|
||||
});
|
@ -0,0 +1,213 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
} from '@angular/core';
|
||||
import { Label } from '../../../../../ng-swagger-gen/models/label';
|
||||
import { forkJoin, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { LabelService } from '../../../../../ng-swagger-gen/services/label.service';
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
finalize,
|
||||
map,
|
||||
switchMap,
|
||||
} from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
const GLOBAL: string = 'g';
|
||||
const PROJECT: string = 'p';
|
||||
const PAGE_SIZE: number = 50;
|
||||
|
||||
@Component({
|
||||
selector: 'app-label-selector',
|
||||
templateUrl: './label-selector.component.html',
|
||||
styleUrls: ['./label-selector.component.scss'],
|
||||
})
|
||||
export class LabelSelectorComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input()
|
||||
ownedLabels: Label[] = [];
|
||||
@Input()
|
||||
width: number = 180; // unit px
|
||||
@Input()
|
||||
scope: string = GLOBAL; // 'g' for global and 'p' for project, default 'g'
|
||||
@Input()
|
||||
projectId: number; // if scope = 'p', projectId is required
|
||||
candidateLabels: Label[] = [];
|
||||
searchValue: string;
|
||||
loading: boolean = false;
|
||||
@Output()
|
||||
clickLabel = new EventEmitter<{
|
||||
label: Label;
|
||||
isAdd: boolean;
|
||||
}>();
|
||||
private _searchSubject = new Subject<string>();
|
||||
private _subSearch: Subscription;
|
||||
constructor(private labelService: LabelService, private router: Router) {
|
||||
if (!this._subSearch) {
|
||||
this._subSearch = this._searchSubject
|
||||
.pipe(
|
||||
debounceTime(500),
|
||||
distinctUntilChanged(),
|
||||
filter(labelName => {
|
||||
if (!labelName) {
|
||||
this.initCandidateLabel();
|
||||
}
|
||||
return !!labelName;
|
||||
}),
|
||||
switchMap(labelName => {
|
||||
return this.getLabelObservable(labelName);
|
||||
})
|
||||
)
|
||||
.subscribe(res => {
|
||||
this.candidateLabels = res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.checkProjectId();
|
||||
this.initCandidateLabel();
|
||||
}
|
||||
|
||||
initCandidateLabel() {
|
||||
// Place the owned label at the top of the array then remove duplicates
|
||||
const Obs: Observable<Label[]>[] = [];
|
||||
if (this.ownedLabels?.length) {
|
||||
const projectLabelIds: number[] = [];
|
||||
const globalLabelIds: number[] = [];
|
||||
this.ownedLabels?.forEach(item => {
|
||||
if (item.scope === PROJECT) {
|
||||
projectLabelIds.push(item.id);
|
||||
}
|
||||
if (item.scope === GLOBAL) {
|
||||
globalLabelIds.push(item.id);
|
||||
}
|
||||
});
|
||||
if (projectLabelIds?.length) {
|
||||
Obs.push(
|
||||
this.labelService.ListLabels({
|
||||
page: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: PROJECT,
|
||||
projectId: this.projectId,
|
||||
q: encodeURIComponent(
|
||||
`id={${projectLabelIds.join(' ')}}`
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (globalLabelIds?.length) {
|
||||
Obs.push(
|
||||
this.labelService.ListLabels({
|
||||
page: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: GLOBAL,
|
||||
q: encodeURIComponent(
|
||||
`id={${globalLabelIds.join(' ')}}`
|
||||
),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Obs.push(this.getLabelObservable(''));
|
||||
forkJoin(Obs)
|
||||
.pipe(
|
||||
map(result => [].concat.apply([], result)),
|
||||
map((result: Label[]) => {
|
||||
return result.filter(
|
||||
(v, i, a) => a.findIndex(v2 => v2.id === v.id) === i
|
||||
);
|
||||
})
|
||||
)
|
||||
.subscribe(res => {
|
||||
this.candidateLabels = res;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
this.checkProjectId();
|
||||
}
|
||||
ngOnDestroy() {
|
||||
if (this._subSearch) {
|
||||
this._subSearch.unsubscribe();
|
||||
this._subSearch = null;
|
||||
}
|
||||
}
|
||||
|
||||
checkProjectId() {
|
||||
if (this.scope === PROJECT && !this.projectId) {
|
||||
throw new Error('Attribute [projectId] is required');
|
||||
}
|
||||
}
|
||||
|
||||
search() {
|
||||
this._searchSubject.next(this.searchValue);
|
||||
}
|
||||
|
||||
selectLabel(label: Label) {
|
||||
this.clickLabel.emit({ label: label, isAdd: !this.isSelect(label) });
|
||||
}
|
||||
isSelect(label: Label): boolean {
|
||||
if (this.ownedLabels?.length) {
|
||||
return this.ownedLabels.some(item => {
|
||||
return item.id === label.id;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
goToLabelPage() {
|
||||
if (this.scope === PROJECT) {
|
||||
this.router.navigate([
|
||||
'harbor',
|
||||
'projects',
|
||||
this.projectId,
|
||||
'labels',
|
||||
]);
|
||||
} else {
|
||||
this.router.navigate(['harbor', 'labels']);
|
||||
}
|
||||
}
|
||||
|
||||
getLabelObservable(labelName: string): Observable<Label[]> {
|
||||
this.loading = true;
|
||||
if (this.scope === PROJECT) {
|
||||
return forkJoin([
|
||||
this.labelService.ListLabels({
|
||||
page: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: PROJECT,
|
||||
projectId: this.projectId,
|
||||
q: labelName
|
||||
? encodeURIComponent(`name=~${labelName}`)
|
||||
: null,
|
||||
}),
|
||||
this.labelService.ListLabels({
|
||||
page: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: GLOBAL,
|
||||
q: labelName
|
||||
? encodeURIComponent(`name=~${labelName}`)
|
||||
: null,
|
||||
}),
|
||||
]).pipe(
|
||||
map(result => [].concat.apply([], result)),
|
||||
finalize(() => (this.loading = false))
|
||||
);
|
||||
}
|
||||
return this.labelService
|
||||
.ListLabels({
|
||||
page: 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
scope: GLOBAL,
|
||||
q: labelName ? encodeURIComponent(`name=~${labelName}`) : null,
|
||||
})
|
||||
.pipe(finalize(() => (this.loading = false)));
|
||||
}
|
||||
}
|
@ -6,11 +6,13 @@
|
||||
border: labelColor?.color === '#FFFFFF' ? '1px solid #A1A1A1' : 'none'
|
||||
}"
|
||||
[style.max-width.px]="labelWidth">
|
||||
<clr-icon
|
||||
*ngIf="hasIcon && label.scope === 'p'"
|
||||
shape="organization"></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="hasIcon && label.scope === 'g'"
|
||||
shape="administrator"></clr-icon>
|
||||
{{ label.name }}
|
||||
<span>
|
||||
<clr-icon
|
||||
*ngIf="hasIcon && label.scope === 'p'"
|
||||
shape="organization"></clr-icon>
|
||||
<clr-icon
|
||||
*ngIf="hasIcon && label.scope === 'g'"
|
||||
shape="administrator"></clr-icon>
|
||||
</span>
|
||||
<span class="label-name">{{ label.name }}</span>
|
||||
</label>
|
||||
|
@ -1,19 +1,16 @@
|
||||
.label {
|
||||
border: none;
|
||||
color: #222;
|
||||
display: inline-block;
|
||||
justify-content: flex-start;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: .875rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.label clr-icon {
|
||||
margin-right: 3px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.btn-group .dropdown-menu clr-icon {
|
||||
display: block;
|
||||
}
|
||||
.label-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
@ -90,6 +90,7 @@ import locale_pt from '@angular/common/locales/pt-PT';
|
||||
import locale_tr from '@angular/common/locales/tr';
|
||||
import locale_de from '@angular/common/locales/de';
|
||||
import { SupportedLanguage } from './entities/shared.const';
|
||||
import { LabelSelectorComponent } from './components/label-selector/label-selector.component';
|
||||
|
||||
const localesForSupportedLangs: Record<SupportedLanguage, unknown[]> = {
|
||||
'en-us': locale_en,
|
||||
@ -171,6 +172,7 @@ ClarityIcons.add({
|
||||
ImageNameInputComponent,
|
||||
HarborDatetimePipe,
|
||||
RemainingTimeComponent,
|
||||
LabelSelectorComponent,
|
||||
],
|
||||
exports: [
|
||||
TranslateModule,
|
||||
@ -210,6 +212,7 @@ ClarityIcons.add({
|
||||
ImageNameInputComponent,
|
||||
HarborDatetimePipe,
|
||||
RemainingTimeComponent,
|
||||
LabelSelectorComponent,
|
||||
],
|
||||
providers: [
|
||||
{ provide: EndpointService, useClass: EndpointDefaultService },
|
||||
|
@ -338,3 +338,12 @@ hbr-create-edit-rule {
|
||||
border-bottom-color: $normal-border-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
app-artifact-filter {
|
||||
.border-bottom-color {
|
||||
border-bottom-color: $normal-border-color !important;
|
||||
}
|
||||
.search-dropdown-toggle {
|
||||
color: $normal-border-color !important;;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user