Merge pull request #4487 from ninjadq/add_grid_view

Add grid view
This commit is contained in:
Qian Deng 2018-03-29 20:13:04 +08:00 committed by GitHub
commit c33ed6f957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1362 additions and 70 deletions

View File

@ -93,6 +93,12 @@ On specific project mode, without need projectId, but also need to provide proje
* **Repository and Tag Management View**
The `hbr-repository-stackview` directive is deprecated. Using `hbr-repository-listview` and `hbr-repository` instead. You should define two routers one for render
`hbr-repository-listview` the other is for `hbr-repository`. `hbr-repository-listview` will output an event, you need catch this event and redirect to related
page contains `hbr-repository`.
**hbr-repository-listview Directive**
**projectId** is used to specify which projects the repositories are from.
**projectName** is used to generate the related commands for pushing images.
@ -101,18 +107,99 @@ On specific project mode, without need projectId, but also need to provide proje
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
**repoClickEvent** is an @output event emitter for you to catch the repository click events.
```
<hbr-repository-stackview [projectId]="..." [projectName]="" [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
<hbr-repository-listview [projectId]="" [projectName]="" [hasSignedIn]="" [hasProjectAdminRole]=""
(repoClickEvent)="watchRepoClickEvent($event)"></hbr-repository-listview>
...
watchTagClickEvent(tag: Tag): void {
//Process tag
watchRepoClickEvent(repo: RepositoryItem): void {
//Process repo
...
}
```
**hbr-repository-gridview Directive**
**projectId** is used to specify which projects the repositories are from.
**projectName** is used to generate the related commands for pushing images.
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
**withVIC** is integrated with VIC
**repoClickEvent** is an @output event emitter for you to catch the repository click events.
**repoProvisionEvent** is an @output event emitter for you to catch the deploy button click event.
**addInfoEvent** is an @output event emitter for you to catch the add additional info button event.
@Output() repoProvisionEvent = new EventEmitter<RepositoryItem>();
@Output() addInfoEvent = new EventEmitter<RepositoryItem>();
```
<hbr-repository-gridview [projectId]="" [projectName]="" [hasSignedIn]="" [hasProjectAdminRole]=""
(repoClickEvent)="watchRepoClickEvent($event)"
(repoProvisionEvent)="watchRepoProvisionEvent($event)"
(addInfoEvent)="watchAddInfoEvent($event)"></hbr-repository-gridview>
...
watchRepoClickEvent(repo: RepositoryItem): void {
//Process repo
...
}
watchRepoProvisionEvent(repo: RepositoryItem): void {
//Process repo
...
}
watchAddInfoEvent(repo: RepositoryItem): void {
//Process repo
...
}
```
**hbr-repository Directive**
**projectId** is used to specify which projects the repositories are from.
**repoName** is used to generate the related commands for pushing images.
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
**withClair** is Clair installed
**withNotary** is Notary installed
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
**goBackClickEvent** is an @output event emitter for you to catch the go back events.
```
<hbr-repository [projectId]="" [repoName]="" [hasSignedIn]="" [hasProjectAdminRole]="" [withClair]="" [withNotary]=""
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)" ></hbr-repository>
watchTagClickEvt(tagEvt: TagClickEvent): void {
...
}
watchGoBackEvt(projectId: string): void {
...
}
```
* **Tag detail view**

View File

@ -1,6 +1,6 @@
{
"name": "harbor-ui",
"version": "0.6.45",
"version": "0.6.61",
"description": "Harbor shared UI components based on Clarity and Angular4",
"scripts": {
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",

View File

@ -1,6 +1,6 @@
{
"name": "harbor-ui",
"version": "0.6.45",
"version": "0.6.61",
"description": "Harbor shared UI components based on Clarity and Angular4",
"author": "VMware",
"module": "index.js",

View File

@ -0,0 +1,67 @@
// Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved.
// This software is released under MIT license.
// The full license information can be found in LICENSE in the root directory of this project.
// @import 'node_modules/admiral-ui-common/css/mixins';
export const GRIDVIEW_STYLE = `
.grid-content {
position: relative;
top: 36px;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
max-height: 65vh;
}
.card-item {
display: block;
max-width: 400px;
min-width: 300px;
position: absolute;
margin-right: 40px;
transition: width 0.4s, transform 0.4s;
}
.content-empty {
text-align: center;
display: block;
margin-top: 100px;
}
.central-block-loading {
position: absolute;
z-index: 10;
top: 0;
left: 0;
right: 0;
bottom: 0;
@include animation(fadein 0.4s);
text-align: center;
background-color: rgba(255, 255, 255, 0.5);
}
.central-block-loading-more {
position: relative;
z-index: 10;
top: 0;
left: 0;
right: 0;
bottom: 0;
@include animation(fadein 0.4s);
text-align: center;
background-color: rgba(255, 255, 255, 0.5);
}
.vertical-helper {
display: inline-block;
height: 100%;
vertical-align: middle;
}
.spinner {
width: 100px;
height: 100px;
vertical-align: middle;
}
`

View File

@ -0,0 +1,18 @@
export const GRIDVIEW_TEMPLATE = `
<div class="grid-content" (scroll)="onScroll($event)">
<div class="items" [ngStyle]="itemsHolderStyle" #itemsHolder >
<span *ngFor="let item of items;let i = index; trackBy:trackByFn" class='card-item' [ngStyle]="cardStyles[i]" #cardItem
(mouseenter)='onCardEnter(i)' (mouseleave)='onCardLeave(i)'>
<ng-template [ngTemplateOutlet]="gridItemTmpl" [ngOutletContext]="{item: item}">
</ng-template>
</span>
<span *ngIf="items.length === 0 && !loading" class="content-empty">
{{'REPOSITORY.NO_ITEMS' | translate}}
</span>
</div>
<div *ngIf="loading" [ngClass]="{'central-block-loading': isFirstPage, 'central-block-loading-more': !isFirstPage}">
<span class="vertical-helper"></span>
<div class="spinner"></div>
</div>
</div>
`

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) 2017 VMware, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product may include a number of subcomponents with separate copyright notices
* and license terms. Your use of these subcomponents is subject to the terms and
* conditions of the subcomponent's license, as noted in the LICENSE file.
*/
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GridViewComponent } from './grid-view.component';
import { SharedModule } from '../shared/shared.module';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
describe('GridViewComponent', () => {
let component: GridViewComponent;
let fixture: ComponentFixture<GridViewComponent>;
let config: IServiceConfig = {
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
],
declarations: [
GridViewComponent,
],
providers: [{
provide: SERVICE_CONFIG, useValue: config }]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GridViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,246 @@
/*
* Copyright (c) 2017 VMware, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product may include a number of subcomponents with separate copyright notices
* and license terms. Your use of these subcomponents is subject to the terms and
* conditions of the subcomponent's license, as noted in the LICENSE file.
*/
import { Component, Input, Output, SimpleChanges, ContentChild, ViewChild, ViewChildren,
TemplateRef, HostListener, ViewEncapsulation, EventEmitter, AfterViewInit } from '@angular/core';
import { CancelablePromise } from '../shared/shared.utils';
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { TranslateService } from '@ngx-translate/core';
import { GRIDVIEW_TEMPLATE } from './grid-view.component.html';
import { GRIDVIEW_STYLE } from './grid-view.component.css';
import { ScrollPosition } from '../service/interface'
@Component({
selector: 'hbr-gridview',
template: GRIDVIEW_TEMPLATE,
styles: [GRIDVIEW_STYLE],
encapsulation: ViewEncapsulation.None
})
/**
* Grid view general component.
*/
export class GridViewComponent implements AfterViewInit {
@Input() loading: boolean;
@Input() totalCount: number;
@Input() currentPage: number;
@Input() pageSize: number;
@Input() expectScrollPercent = 70;
@Input() withAdmiral: boolean;
@Input()
set items(value: any[]) {
let newCardStyles = value.map((d, index) => {
if (index < this.cardStyles.length) {
return this.cardStyles[index];
}
return {
opacity: '0',
overflow: 'hidden'
};
});
this.cardStyles = newCardStyles;
this._items = value;
}
@Output() loadNextPageEvent = new EventEmitter<any>();
@ViewChildren('cardItem') cards: any;
@ViewChild('itemsHolder') itemsHolder: any;
@ContentChild(TemplateRef) gridItemTmpl: any;
_items: any[] = [];
cardStyles: any = [];
itemsHolderStyle: any = {};
layoutTimeout: any;
querySub: Subscription;
routerSub: Subscription;
totalItemsCount: number;
loadedPages = 0;
nextPageLink: string;
hidePartialRows = false;
loadPagesTimeout: any;
CurrentScrollPosition: ScrollPosition = {
sH: 0,
sT: 0,
cH: 0
};
preScrollPosition: ScrollPosition = null;
constructor(private translate: TranslateService) { }
ngAfterViewInit() {
this.cards.changes.subscribe(() => {
this.throttleLayout();
});
this.throttleLayout();
}
get items() {
return this._items;
}
@HostListener('scroll', ['$event'])
onScroll(event: any) {
this.preScrollPosition = this.CurrentScrollPosition;
this.CurrentScrollPosition = {
sH: event.target.scrollHeight,
sT: event.target.scrollTop,
cH: event.target.clientHeight
}
if (!this.loading
&& this.isScrollDown()
&& this.isScrollExpectPercent()
&& (this.currentPage * this.pageSize < this.totalCount)) {
this.loadNextPageEvent.emit();
}
}
isScrollDown(): boolean {
return this.preScrollPosition.sT < this.CurrentScrollPosition.sT;
}
isScrollExpectPercent(): boolean {
return ((this.CurrentScrollPosition.sT + this.CurrentScrollPosition.cH) / this.CurrentScrollPosition.sH) > (this.expectScrollPercent / 100);
}
@HostListener('window:resize')
onResize(event: any) {
this.throttleLayout();
}
throttleLayout() {
clearTimeout(this.layoutTimeout);
this.layoutTimeout = setTimeout(() => {
this.layout.call(this);
}, 40);
}
get isFirstPage() {
return this.currentPage <= 1;
}
layout() {
let el = this.itemsHolder.nativeElement;
let width = el.offsetWidth;
let items = el.querySelectorAll('.card-item');
let items_count = items.length;
if (items_count === 0) {
el.height = 0;
return;
}
let itemsHeight = [];
for (let i = 0; i < items_count; i++) {
itemsHeight[i] = items[i].offsetHeight;
}
let height = Math.max.apply(null, itemsHeight);
let itemsStyle: CSSStyleDeclaration = window.getComputedStyle(items[0]);
let minWidthStyle: string = itemsStyle.minWidth;
let maxWidthStyle: string = itemsStyle.maxWidth;
let minWidth = parseInt(minWidthStyle, 10);
let maxWidth = parseInt(maxWidthStyle, 10);
let marginHeight: number =
parseInt(itemsStyle.marginTop, 10) + parseInt(itemsStyle.marginBottom, 10);
let marginWidth: number =
parseInt(itemsStyle.marginLeft, 10) + parseInt(itemsStyle.marginRight, 10);
let columns = Math.floor(width / (minWidth + marginWidth));
let columnsToUse = Math.max(Math.min(columns, items_count), 1);
let rows = Math.floor(items_count / columnsToUse);
let itemWidth = Math.min(Math.floor(width / columnsToUse) - marginWidth, maxWidth);
let itemSpacing = columnsToUse === 1 || columns > items_count ? marginWidth :
(width - marginWidth - columnsToUse * itemWidth) / (columnsToUse - 1);
if (!this.withAdmiral) {
// Fixed spacing and margin on standalone mode
itemSpacing = marginWidth;
itemWidth = minWidth;
}
let visible = items_count;
if (this.hidePartialRows && this.totalItemsCount && items_count !== this.totalItemsCount) {
visible = rows * columnsToUse;
}
let count = 0;
for (let i = 0; i < visible; i++) {
let item = items[i];
let itemStyle = window.getComputedStyle(item);
let left = (i % columnsToUse) * (itemWidth + itemSpacing);
let top = Math.floor(count / columnsToUse) * (height + marginHeight);
// trick to show nice apear animation, where the item is already positioned,
// but it will pop out
let oldTransform = itemStyle.transform;
if (!oldTransform || oldTransform === 'none') {
this.cardStyles[i] = {
transform: 'translate(' + left + 'px,' + top + 'px) scale(0)',
width: itemWidth + 'px',
transition: 'none',
overflow: 'hidden'
};
this.throttleLayout();
} else {
this.cardStyles[i] = {
transform: 'translate(' + left + 'px,' + top + 'px) scale(1)',
width: itemWidth + 'px',
transition: null,
overflow: 'hidden'
};
this.throttleLayout();
}
if (!item.classList.contains('context-selected')) {
let itemHeight = itemsHeight[i];
if (itemStyle.display === 'none' && itemHeight !== 0) {
this.cardStyles[i].display = null;
}
if (itemHeight !== 0) {
count++;
}
}
}
for (let i = visible; i < items_count; i++) {
this.cardStyles[i] = {
display: 'none'
};
}
this.itemsHolderStyle = {
height: Math.ceil(count / columnsToUse) * (height + marginHeight) + 'px'
};
}
onCardEnter(i: number) {
this.cardStyles[i].overflow = 'visible';
}
onCardLeave(i: number) {
this.cardStyles[i].overflow = 'hidden';
}
trackByFn(index: number, item: any) {
return index;
}
}

View File

@ -0,0 +1,8 @@
import { Type } from "@angular/core";
import { GridViewComponent } from './grid-view.component';
export * from "./grid-view.component";
export const HBR_GRIDVIEW_DIRECTIVES: Type<any>[] = [
GridViewComponent
];

View File

@ -25,6 +25,8 @@ import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index';
import { CONFIGURATION_DIRECTIVES } from './config/index';
import { JOB_LOG_VIEWER_DIRECTIVES } from './job-log-viewer/index';
import { PROJECT_POLICY_CONFIG_DIRECTIVES } from './project-policy-config/index';
import { HBR_GRIDVIEW_DIRECTIVES } from './gridview/index';
import { REPOSITORY_GRIDVIEW_DIRECTIVES } from './repository-gridview';
import {
SystemInfoService,
@ -182,7 +184,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
PROJECT_POLICY_CONFIG_DIRECTIVES,
LABEL_DIRECTIVES,
CREATE_EDIT_LABEL_DIRECTIVES,
LABEL_PIECE_DIRECTIVES
LABEL_PIECE_DIRECTIVES,
HBR_GRIDVIEW_DIRECTIVES,
REPOSITORY_GRIDVIEW_DIRECTIVES,
],
exports: [
LOG_DIRECTIVES,
@ -207,7 +211,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
PROJECT_POLICY_CONFIG_DIRECTIVES,
LABEL_DIRECTIVES,
CREATE_EDIT_LABEL_DIRECTIVES,
LABEL_PIECE_DIRECTIVES
LABEL_PIECE_DIRECTIVES,
HBR_GRIDVIEW_DIRECTIVES,
REPOSITORY_GRIDVIEW_DIRECTIVES,
],
providers: []
})

View File

@ -2,7 +2,7 @@ export * from './harbor-library.module';
export * from './service.config';
export * from './service/index';
export * from './error-handler/index';
//export * from './utils';
// export * from './utils';
export * from './log/index';
export * from './filter/index';
export * from './endpoint/index';
@ -23,3 +23,5 @@ export * from './channel/index';
export * from './project-policy-config/index';
export * from './label/index';
export * from './create-edit-label';
export * from './gridview/index';
export * from './repository-gridview/index';

View File

@ -0,0 +1,8 @@
import { Type } from "@angular/core";
import { RepositoryGridviewComponent } from './repository-gridview.component';
export * from "./repository-gridview.component";
export const REPOSITORY_GRIDVIEW_DIRECTIVES: Type<any>[] = [
RepositoryGridviewComponent
];

View File

@ -0,0 +1,76 @@
export const REPOSITORY_GRIDVIEW_STYLE = `
.rightPos{
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
}
.toolbar {
overflow: hidden;
}
.filter-divider {
display: inline-block;
height: 16px;
width: 2px;
background-color: #cccccc;
padding-top: 12px;
padding-bottom: 12px;
position: relative;
top: 9px;
margin-right: 6px;
margin-left: 6px;
}
.card-block {
margin-top: 24px;
min-height: 100px;
}
.form-group {
display: flex;
}
.form-group > label {
width: 100px;
}
.card-media-block {
margin-top: 12px;
margin-bottom: 12px;
}
.card-media-block > img {
height: 45px;
width: 45px;
}
.card-media-description {
height: 45px;
}
.card-media-description > p {
margin-top: 0px;
}
.card-text {
height: 45px;
overflow: hidden;
margin-bottom: 18px;
}
.card-block {
margin-top: 0px;
}
.card-footer {
padding-top: 6px;
padding-bottom: 6px;
}
.list-img > img {
height: 24px;
width: 24px;
margin-right: 12px;
}
`;

View File

@ -0,0 +1,92 @@
export const REPOSITORY_GRIDVIEW_TEMPLATE = `
<div>
<div class="row" style="position:relative;">
<div class="toolbar">
<div class="row flex-items-xs-right option-right rightPos">
<div class="flex-xs-middle">
<hbr-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)" [currentValue]="lastFilteredRepoName"></hbr-filter>
<span class="card-btn" (click)="showCard(true)" (mouseenter) ="mouseEnter('card') " (mouseleave) ="mouseLeave('card')">
<clr-icon [ngClass]="{'is-highlight': isCardView || isHovering('card') }" shape="view-cards"></clr-icon>
</span>
<span class="list-btn" (click)="showCard(false)" (mouseenter) ="mouseEnter('list') " (mouseleave) ="mouseLeave('list')">
<clr-icon [ngClass]="{'is-highlight': !isCardView || isHovering('list') }"shape="view-list"></clr-icon>
</span>
<span class="filter-divider"></span>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div>
</div>
</div>
</div>
<div *ngIf="!isCardView" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" (click)="deleteRepos(selectedRow)" [disabled]="!(selectedRow.length && hasProjectAdminRole)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</clr-dg-action-bar>
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]="r">
<clr-dg-cell><a href="javascript:void(0)" (click)="watchRepoClickEvt(r)"><span *ngIf="withAdmiral" class="list-img"><img [src]="getImgLink(r)"/></span>{{r.name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
<hbr-gridview *ngIf="isCardView" #gridView style="position:relative;" [items]="repositories" [loading]="loading" [pageSize]="pageSize"
[currentPage]="currentPage" [totalCount]="totalCount" [expectScrollPercent]="90" [withAdmiral]="withAdmiral" (loadNextPageEvent)="loadNextPage()">
<ng-template let-item="item">
<a class="card clickable" (click)="watchRepoClickEvt(item)">
<div class="card-header">
<div class="card-media-block">
<img *ngIf="withAdmiral" [src]="getImgLink(item)"/>
<div class="card-media-description">
<span class="card-media-title">
{{item.name}}
</span>
<p class="card-media-text">{{registryUrl}}</p>
</div>
</div>
</div>
<div class="card-block">
<div class="card-text">
{{getRepoDescrition(item)}}
</div>
<div class="form-group">
<label>{{'REPOSITORY.TAGS_COUNT' | translate}}</label>
<div>{{item.tags_count}}</div>
</div>
<div class="form-group">
<label>{{'REPOSITORY.TAGS_COUNT' | translate}}</label>
<div>{{item.pull_count}}</div>
</div>
</div>
<div class="card-footer">
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button *ngIf="withAdmiral" type="button" class="btn btn-link" (click)="provisionItemEvent($event, item)">{{'REPOSITORY.DEPLOY' | translate}}</button>
<button type="button" class="btn btn-link" (click)="$event.stopPropagation()" clrDropdownTrigger>
{{'REPOSITORY.ACTION' | translate}}
<clr-icon shape="caret down"></clr-icon>
</button>
<clr-dropdown-menu clrPosition="top-left" *clrIfOpen>
<button *ngIf="withAdmiral" type="button" class="btn btn-link" clrDropdownItem (click)="itemAddInfoEvent($event, item)">
{{'REPOSITORY.ADDITIONAL_INFO' | translate}}
</button>
<button type="button" class="btn btn-link" clrDropdownItem (click)="deleteItemEvent($event, item)">
{{'REPOSITORY.DELETE' | translate}}
</button>
</clr-dropdown-menu>
</clr-dropdown>
</div>
</a>
</ng-template>
</hbr-gridview>
<confirmation-dialog #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
</div>
`;

View File

@ -0,0 +1,175 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { SharedModule } from '../shared/shared.module';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { RepositoryGridviewComponent } from './repository-gridview.component';
import { TagComponent } from '../tag/tag.component';
import { FilterComponent } from '../filter/filter.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { TagService, TagDefaultService } from '../service/tag.service';
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
import { HBR_GRIDVIEW_DIRECTIVES } from '../gridview/index'
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index';
import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
import { JobLogViewerComponent } from '../job-log-viewer/index';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import { click } from '../utils';
describe('RepositoryComponentGridview (inline template)', () => {
let compRepo: RepositoryGridviewComponent;
let fixtureRepo: ComponentFixture<RepositoryGridviewComponent>;
let repositoryService: RepositoryService;
let tagService: TagService;
let systemInfoService: SystemInfoService;
let spyRepos: jasmine.Spy;
let spySystemInfo: jasmine.Spy;
let mockSystemInfo: SystemInfo = {
"with_notary": true,
"with_admiral": false,
"admiral_endpoint": "NA",
"auth_mode": "db_auth",
"registry_url": "10.112.122.56",
"project_creation_restriction": "everyone",
"self_registration": true,
"has_ca_root": false,
"harbor_version": "v1.1.1-rc1-160-g565110d"
};
let mockRepoData: RepositoryItem[] = [
{
"id": 1,
"name": "library/busybox",
"project_id": 1,
"description": "asdfsadf",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
},
{
"id": 2,
"name": "library/nginx",
"project_id": 1,
"description": "asdf",
"pull_count": 0,
"star_count": 0,
"tags_count": 1
}
];
let mockRepo: Repository = {
metadata: {xTotalCount: 2},
data: mockRepoData
};
let mockTagData: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
"name": "1.11.5",
"size": "2049",
"architecture": "amd64",
"os": "linux",
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null,
"labels": []
}
];
let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repository/testing',
systemInfoEndpoint: '/api/systeminfo/testing',
targetBaseEndpoint: '/api/tag/testing'
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
RouterTestingModule
],
declarations: [
RepositoryGridviewComponent,
TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent,
FilterComponent,
VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES,
INLINE_ALERT_DIRECTIVES,
HBR_GRIDVIEW_DIRECTIVES,
JobLogViewerComponent
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: TagService, useClass: TagDefaultService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
]
});
}));
beforeEach(() => {
fixtureRepo = TestBed.createComponent(RepositoryGridviewComponent);
compRepo = fixtureRepo.componentInstance;
compRepo.projectId = 1;
compRepo.hasProjectAdminRole = true;
repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService);
systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService);
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
fixtureRepo.detectChanges();
});
it('should create', () => {
expect(compRepo).toBeTruthy();
});
it('should load and render data', async(() => {
fixtureRepo.detectChanges();
fixtureRepo.whenStable().then(() => {
fixtureRepo.detectChanges();
let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell'));
expect(deRepo).toBeTruthy();
let elRepo: HTMLElement = deRepo.nativeElement;
expect(elRepo).toBeTruthy();
expect(elRepo.textContent).toEqual('library/busybox');
});
}));
it('should filter data by keyword', async(() => {
fixtureRepo.detectChanges();
fixtureRepo.whenStable().then(() => {
fixtureRepo.detectChanges();
compRepo.doSearchRepoNames('nginx');
fixtureRepo.detectChanges();
let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell'));
expect(de).toBeTruthy();
expect(de.length).toEqual(1);
let el: HTMLElement = de[0].nativeElement;
expect(el).toBeTruthy();
expect(el.textContent).toEqual('library/nginx');
});
}));
});

View File

@ -0,0 +1,404 @@
import { Component, Input, Output, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import {Observable} from "rxjs/Observable";
import { TranslateService } from '@ngx-translate/core';
import { Comparator, State } from 'clarity-angular';
import { REPOSITORY_GRIDVIEW_TEMPLATE } from './repository-gridview.component.html';
import { REPOSITORY_GRIDVIEW_STYLE } from './repository-gridview.component.css';
import { Repository, SystemInfo, SystemInfoService, RepositoryService, RequestQueryParams, RepositoryItem, TagService } from '../service/index';
import { ErrorHandler } from '../error-handler/error-handler';
import { toPromise, CustomComparator , DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting} from '../utils';
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
import { Tag, CardItemEvent } from '../service/interface';
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import { GridViewComponent } from '../gridview/grid-view.component'
@Component({
selector: 'hbr-repository-gridview',
template: REPOSITORY_GRIDVIEW_TEMPLATE,
styles: [REPOSITORY_GRIDVIEW_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RepositoryGridviewComponent implements OnChanges, OnInit {
signedCon: {[key: string]: any | string[]} = {};
@Input() projectId: number;
@Input() projectName = 'unknown';
@Input() urlPrefix: string;
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Output() repoClickEvent = new EventEmitter<RepositoryItem>();
@Output() repoProvisionEvent = new EventEmitter<RepositoryItem>();
@Output() addInfoEvent = new EventEmitter<RepositoryItem>();
lastFilteredRepoName: string;
repositories: RepositoryItem[] = [];
repositoriesCopy: RepositoryItem[] = [];
systemInfo: SystemInfo;
selectedRow: RepositoryItem[] = [];
loading = true;
isCardView: boolean;
cardHover = false;
listHover = false;
batchDelectionInfos: BatchInfo[] = [];
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
pageSize: number = DEFAULT_PAGE_SIZE;
currentPage = 1;
totalCount = 0;
currentState: State;
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
@ViewChild('gridView')
gridView: GridViewComponent;
constructor(
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private repositoryService: RepositoryService,
private systemInfoService: SystemInfoService,
private tagService: TagService,
private ref: ChangeDetectorRef,
private router: Router) { }
public get registryUrl(): string {
return this.systemInfo ? this.systemInfo.registry_url : '';
}
public get withAdmiral(): boolean {
return this.systemInfo ? this.systemInfo.with_admiral : false;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['projectId'] && changes['projectId'].currentValue) {
this.refresh();
}
}
ngOnInit(): void {
if (this.withAdmiral) {
this.isCardView = true;
} else {
this.isCardView = false;
}
// Get system info for tag views
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
.then(systemInfo => this.systemInfo = systemInfo)
.catch(error => this.errorHandler.error(error));
this.lastFilteredRepoName = '';
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.REPOSITORY &&
message.state === ConfirmationState.CONFIRMED) {
let promiseLists: any[] = [];
let repoNames: string[] = message.data.split(',');
repoNames.forEach(repoName => {
promiseLists.push(this.delOperate(repoName));
});
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.refresh();
let st: State = this.getStateAfterDeletion();
if (!st) {
this.refresh();
} else {
this.clrLoad(st);
}
});
}
}
delOperate(repoName: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === repoName);
if (this.signedCon[repoName].length !== 0) {
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).subscribe(res => {
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
});
} else {
return toPromise<number>(this.repositoryService
.deleteRepository(repoName))
.then(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS').subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(error => {
if (error.status === "412") {
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => {
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
});
return;
}
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
});
}
}
doSearchRepoNames(repoName: string) {
this.lastFilteredRepoName = repoName;
this.currentPage = 1;
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = 0;
st.page.to = this.pageSize - 1;
this.clrLoad(st);
}
saveSignatures(event: {[key: string]: string[]}): void {
Object.assign(this.signedCon, event);
}
deleteRepos(repoLists: RepositoryItem[]) {
if (repoLists && repoLists.length) {
let repoNames: string[] = [];
this.batchDelectionInfos = [];
let repArr: any[] = [];
repoLists.forEach(repo => {
repoNames.push(repo.name);
let initBatchMessage = new BatchInfo();
initBatchMessage.name = repo.name;
this.batchDelectionInfos.push(initBatchMessage);
if (!this.signedCon[repo.name]) {
repArr.push(this.getTagInfo(repo.name));
}
});
Promise.all(repArr).then(() => {
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', '', repoNames.join(','), 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL);
});
}
}
getTagInfo(repoName: string): Promise<void> {
this.signedCon[repoName] = [];
return toPromise<Tag[]>(this.tagService
.getTags(repoName))
.then(items => {
items.forEach((t: Tag) => {
if (t.signature !== null) {
this.signedCon[repoName].push(t.name);
}
});
})
.catch(error => this.errorHandler.error(error));
}
signedDataSet(repoName: string): void {
let signature = '';
if (this.signedCon[repoName].length === 0) {
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL);
return;
}
signature = this.signedCon[repoName].join(',');
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO_SIGNED', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO_SIGNED', ConfirmationButtons.CLOSE);
}
confirmationDialogSet(summaryTitle: string, signature: string, repoName: string, summaryKey: string, button: ConfirmationButtons): void {
this.translateService.get(summaryKey,
{
repoName: repoName,
signedImages: signature,
})
.subscribe((res: string) => {
summaryKey = res;
let message = new ConfirmationMessage(
summaryTitle,
summaryKey,
repoName,
repoName,
ConfirmationTargets.REPOSITORY,
button);
this.confirmationDialog.open(message);
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
});
}
provisionItemEvent(evt: any, repo: RepositoryItem): void {
evt.stopPropagation();
this.repoProvisionEvent.emit(repo);
}
deleteItemEvent(evt: any, item: RepositoryItem): void {
evt.stopPropagation();
this.deleteRepos([item]);
}
itemAddInfoEvent(evt: any, repo: RepositoryItem): void {
evt.stopPropagation();
this.addInfoEvent.emit(repo);
}
selectedChange(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 2000);
}
refresh() {
this.doSearchRepoNames('');
}
loadNextPage() {
if (this.currentPage * this.pageSize >= this.totalCount) {
return
}
this.currentPage = this.currentPage + 1;
// Pagination
let params: RequestQueryParams = new RequestQueryParams();
params.set("page", '' + this.currentPage);
params.set("page_size", '' + this.pageSize);
this.loading = true;
toPromise<Repository>(this.repositoryService.getRepositories(
this.projectId,
this.lastFilteredRepoName,
params))
.then((repo: Repository) => {
this.totalCount = repo.metadata.xTotalCount;
this.repositoriesCopy = repo.data;
this.signedCon = {};
// Do filtering and sorting
this.repositoriesCopy = doFiltering<RepositoryItem>(this.repositoriesCopy, this.currentState);
this.repositoriesCopy = doSorting<RepositoryItem>(this.repositoriesCopy, this.currentState);
this.repositories = this.repositories.concat(this.repositoriesCopy);
this.loading = false;
})
.catch(error => {
this.loading = false;
this.errorHandler.error(error);
});
}
clrLoad(state: State): void {
this.selectedRow = [];
// Keep it for future filtering and sorting
this.currentState = state;
let pageNumber: number = calculatePage(state);
if (pageNumber <= 0) { pageNumber = 1; }
// Pagination
let params: RequestQueryParams = new RequestQueryParams();
params.set("page", '' + pageNumber);
params.set("page_size", '' + this.pageSize);
this.loading = true;
toPromise<Repository>(this.repositoryService.getRepositories(
this.projectId,
this.lastFilteredRepoName,
params))
.then((repo: Repository) => {
this.totalCount = repo.metadata.xTotalCount;
this.repositories = repo.data;
this.signedCon = {};
// Do filtering and sorting
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
this.repositories = doSorting<RepositoryItem>(this.repositories, state);
this.loading = false;
})
.catch(error => {
this.loading = false;
this.errorHandler.error(error);
});
// Force refresh view
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}
getStateAfterDeletion(): State {
let total: number = this.totalCount - 1;
if (total <= 0) { return null; }
let totalPages: number = Math.ceil(total / this.pageSize);
let targetPageNumber: number = this.currentPage;
if (this.currentPage > totalPages) {
targetPageNumber = totalPages; // Should == currentPage -1
}
let st: State = this.currentState;
if (!st) {
st = { page: {} };
}
st.page.size = this.pageSize;
st.page.from = (targetPageNumber - 1) * this.pageSize;
st.page.to = targetPageNumber * this.pageSize - 1;
return st;
}
watchRepoClickEvt(repo: RepositoryItem) {
this.repoClickEvent.emit(repo);
}
getImgLink(repo: RepositoryItem): string {
return '/container-image-icons?container-image=' + repo.name
}
getRepoDescrition(repo: RepositoryItem): string {
if (repo && repo.description) {
return repo.description;
}
return "No description for this repo. You can add it to this repository."
}
showCard(cardView: boolean) {
if (this.isCardView === cardView) {
return
}
this.isCardView = cardView;
this.refresh();
}
mouseEnter(itemName: string) {
if (itemName === 'card') {
this.cardHover = true;
} else {
this.listHover = true;
}
}
mouseLeave(itemName: string) {
if (itemName === 'card') {
this.cardHover = false;
} else {
this.listHover = false;
}
}
isHovering(itemName: string) {
if (itemName === 'card') {
return this.cardHover;
} else {
return this.listHover;
}
}
}

View File

@ -20,7 +20,7 @@ export const REPOSITORY_LISTVIEW_TEMPLATE = `
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]="r">
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name}}</a></clr-dg-cell>
<clr-dg-cell><a href="javascript:void(0)" (click)="watchRepoClickEvt(r)">{{r.name}}</a></clr-dg-cell>
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
</clr-dg-row>

View File

@ -34,7 +34,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
import { Subscription } from 'rxjs/Subscription';
import { Tag, TagClickEvent } from '../service/interface';
import { Tag } from '../service/interface';
import { State } from "clarity-angular";
import {
@ -60,14 +60,14 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
@Output() repoClickEvent = new EventEmitter<RepositoryItem>();
lastFilteredRepoName: string;
repositories: RepositoryItem[];
systemInfo: SystemInfo;
selectedRow: RepositoryItem[] = [];
loading: boolean = true;
loading = true;
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
@ -279,19 +279,15 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
this.doSearchRepoNames('');
}
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
this.tagClickEvent.emit(tagClickEvt);
}
clrLoad(state: State): void {
this.selectedRow = [];
//Keep it for future filtering and sorting
// Keep it for future filtering and sorting
this.currentState = state;
let pageNumber: number = calculatePage(state);
if (pageNumber <= 0) { pageNumber = 1; }
//Pagination
// Pagination
let params: RequestQueryParams = new RequestQueryParams();
params.set("page", '' + pageNumber);
params.set("page_size", '' + this.pageSize);
@ -307,7 +303,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
this.repositories = repo.data;
this.signedCon = {};
//Do filtering and sorting
// Do filtering and sorting
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
this.repositories = doSorting<RepositoryItem>(this.repositories, state);
@ -318,7 +314,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
this.errorHandler.error(error);
});
//Force refresh view
// Force refresh view
let hnd = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => clearInterval(hnd), 5000);
}
@ -331,7 +327,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
let targetPageNumber: number = this.currentPage;
if (this.currentPage > totalPages) {
targetPageNumber = totalPages;//Should == currentPage -1
targetPageNumber = totalPages; // Should == currentPage -1
}
let st: State = this.currentState;
@ -344,8 +340,8 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
return st;
}
public gotoLink(projectId: number, repoName: string): void {
let linkUrl = [this.router.url, repoName];
this.router.navigate(linkUrl);
watchRepoClickEvt(repo: RepositoryItem) {
this.repoClickEvent.emit(repo);
}
}

View File

@ -250,7 +250,7 @@ export interface VulnerabilitySummary {
job_id?: number;
severity: VulnerabilitySeverity;
components: VulnerabilityComponents;
update_time: Date; //Use as complete timestamp
update_time: Date; // Use as complete timestamp
}
export interface VulnerabilityComponents {
@ -277,3 +277,15 @@ export interface Label {
scope: string;
project_id: number;
}
export interface CardItemEvent {
event_type: string;
item: any;
additional_info?: any;
}
export interface ScrollPosition {
sH: number;
sT: number;
cH: number;
};

View File

@ -55,7 +55,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler
}
})
}),
],
exports: [
CommonModule,
@ -65,9 +65,8 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
CookieModule,
ClipboardModule,
ClarityModule,
TranslateModule
TranslateModule,
],
providers: [CookieService]
})
export class SharedModule { }

View File

@ -15,7 +15,7 @@ import { NgForm } from '@angular/forms';
import { httpStatusCode, AlertType } from './shared.const';
/**
* To handle the error message body
*
*
* @export
* @returns {string}
*/
@ -24,7 +24,7 @@ export const errorHandler = function (error: any): string {
return "UNKNOWN_ERROR";
}
if (!(error.statusCode || error.status)) {
//treat as string message
// treat as string message
return '' + error;
} else {
switch (error.statusCode || error.status) {
@ -46,4 +46,28 @@ export const errorHandler = function (error: any): string {
return "UNKNOWN_ERROR";
}
}
}
export class CancelablePromise<T> {
constructor(promise: Promise<T>) {
this.wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
this.isCanceled ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
this.isCanceled ? reject({isCanceled: true}) : reject(error)
);
});
}
private wrappedPromise: Promise<T>;
private isCanceled: boolean;
getPromise(): Promise<T> {
return this.wrappedPromise;
}
cancel() {
this.isCanceled = true;
}
}

View File

@ -58,7 +58,7 @@ export const TAG_DETAIL_HTML: string = `
</div>
</div>
<div *ngIf="tagDetails.labels.length && !withAdmiral">
<div *ngIf="!withAdmiral && tagDetails?.labels?.length" >
<div class="third-column detail-title">{{'TAG.LABELS' | translate }}</div>
<div class="fourth-column">
<div *ngFor="let label of tagDetails.labels" style="margin-bottom: 2px;"><hbr-label-piece [label]="label"></hbr-label-piece></div>

View File

@ -17,8 +17,8 @@ export const TAG_TEMPLATE = `
<div class="row flex-items-xs-right rightPos">
<div class='filterLabelPiece' [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel"></hbr-label-piece></div>
<div class="flex-xs-middle">
<hbr-filter *ngIf="withAdmiral" [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
<clr-dropdown *ngIf="!withAdmiral">
<hbr-filter *ngIf="withAdmiral" [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
<clr-dropdown *ngIf="!withAdmiral">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName" clrDropdownTrigger></hbr-filter>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
@ -39,31 +39,29 @@ export const TAG_TEMPLATE = `
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(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)" >{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #stickLabelNamePiece (keyup)="handleStickInputFilter(stickLabelNamePiece.value)"></div>
<div [hidden]='imageStickLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="selectLabel(label); label.iconsShow = true">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); unSelectLabel(label); label.iconsShow = false"></clr-icon>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<clr-dropdown *ngIf="!withAdmiral" class="btn btn-sm btn-secondary">
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" >{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #stickLabelNamePiece (keyup)="handleStickInputFilter(stickLabelNamePiece.value)"></div>
<div [hidden]='imageStickLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="selectLabel(label); label.iconsShow = true">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); unSelectLabel(label); label.iconsShow = false"></clr-icon>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
<clr-dg-column style="width: 90px;" [clrDgField]="'size'">{{'REPOSITORY.SIZE' | translate}}</clr-dg-column>
@ -73,7 +71,7 @@ export const TAG_TEMPLATE = `
<clr-dg-column style="min-width: 130px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" [clrDgField]="'labels'">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-column *ngIf="!withAdmiral" style="width: 140px;" [clrDgField]="'labels'">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated" style="width: 120px;" [ngSwitch]="withClair">
@ -98,7 +96,7 @@ export const TAG_TEMPLATE = `
<clr-dg-cell class="truncated" style="min-width: 130px;" title="{{t.author}}">{{t.author}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell style="width: 140px;">
<clr-dg-cell *ngIf="!withAdmiral" style="width: 140px;">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]"></hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
<div class="trigger-item">
@ -114,7 +112,7 @@ export const TAG_TEMPLATE = `
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}&nbsp;&nbsp;&nbsp;&nbsp;
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>

View File

@ -196,7 +196,9 @@ export class TagComponent implements OnInit, AfterViewInit {
}
ngAfterViewInit() {
this.getAllLabels();
if (!this.withAdmiral) {
this.getAllLabels();
}
}
public get filterLabelPieceWidth() {

View File

@ -31,7 +31,7 @@
"clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.27",
"core-js": "^2.4.1",
"harbor-ui": "0.6.58",
"harbor-ui": "0.6.61",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -1,3 +1,5 @@
<div style="margin-top: 24px;">
<hbr-repository-listview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-listview>
<hbr-repository-gridview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn"
[hasProjectAdminRole]="hasProjectAdminRole"
(repoClickEvent)="watchRepoClickEvent($event)"></hbr-repository-gridview>
</div>

View File

@ -17,7 +17,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Project } from '../project/project';
import { SessionService } from '../shared/session.service';
import { TagClickEvent } from 'harbor-ui';
import { TagClickEvent, RepositoryItem } from 'harbor-ui';
@Component({
selector: 'repository',
@ -47,8 +47,8 @@ export class RepositoryPageComponent implements OnInit {
this.hasSignedIn = this.session.getCurrentUser() !== null;
}
watchTagClickEvent(tagEvt: TagClickEvent): void {
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name];
watchRepoClickEvent(repoEvt: RepositoryItem): void {
let linkUrl = ['harbor', 'projects', repoEvt.project_id, 'repositories', repoEvt.name];
this.router.navigate(linkUrl);
}
};

View File

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

View File

@ -79,12 +79,13 @@ export class TagRepositoryComponent implements OnInit {
hasChanges(): boolean {
return this.repositoryComponent.hasChanges();
}
watchTagClickEvt(tagEvt: TagClickEvent): void {
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
this.router.navigate(linkUrl);
}
goBack(tag: string): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
watchGoBackEvt(projectId: string): void {
this.router.navigate(["harbor", "projects", projectId, "repositories"]);
}
}

View File

@ -404,6 +404,7 @@
"REPOSITORIES": "Repositories",
"OF": "of",
"ITEMS": "items",
"NO_ITEMS": "NO ITEMS",
"POP_REPOS": "Popular Repositories",
"DELETED_REPO_SUCCESS": "Deleted repositories successfully.",
"DELETED_TAG_SUCCESS": "Deleted tags successfully.",
@ -416,7 +417,10 @@
"LABELS": ":labels",
"ADD_TO_IMAGE": "Add labels to this image",
"FILTER_BY_LABEL": "Filter projects by label",
"ADD_LABELS": "Add labels"
"ADD_LABELS": "Add labels",
"ACTION": "ACTION",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"

View File

@ -404,6 +404,7 @@
"REPOSITORIES": "Repositorios",
"OF": "of",
"ITEMS": "elementos",
"NO_ITEMS": "NO ITEMS",
"POP_REPOS": "Repositorios Populares",
"DELETED_REPO_SUCCESS": "Repositorio eliminado satisfactoriamente.",
"DELETED_TAG_SUCCESS": "Etiqueta eliminada satisfactoriamente.",
@ -415,7 +416,11 @@
"IMAGE": "Imágenes",
"LABELS": "Labels",
"ADD_TO_IMAGE": "Add labels to this image",
"ADD_LABELS": "Add labels"
"ADD_LABELS": "Add labels",
"FILTER_BY_LABEL": "Filter projects by label",
"ACTION": "ACTION",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"

View File

@ -364,7 +364,11 @@
"DELETED_TAG_SUCCESS": "Tag supprimé avec succés.",
"COPY": "Copier",
"NOTARY_IS_UNDETERMINED": "Ne peut pas déterminer la signature de ce tag.",
"PLACEHOLDER": "Nous ne trouvons aucun dépôt !"
"PLACEHOLDER": "Nous ne trouvons aucun dépôt !",
"IMAGE": "Images",
"ACTION": "ACTION",
"DEPLOY": "DEPLOY",
"ADDITIONAL_INFO": "Add Additional Info"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"

View File

@ -404,6 +404,7 @@
"REPOSITORIES": "镜像仓库",
"OF": "共计",
"ITEMS": "条记录",
"NO_ITEMS": "没有记录",
"POP_REPOS": "受欢迎的镜像仓库",
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。",
"DELETED_TAG_SUCCESS": "成功删除镜像标签。",
@ -415,7 +416,11 @@
"IMAGE": "镜像",
"LABELS": "标签",
"ADD_TO_IMAGE": "添加标签到此镜像",
"ADD_LABELS": "添加标签"
"ADD_LABELS": "添加标签",
"FILTER_BY_LABEL": "过滤标签",
"ACTION": "操作",
"DEPLOY": "部署",
"ADDITIONAL_INFO": "添加信息"
},
"ALERT": {
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"