Commit 74cbfcb9 authored by Deng, Qian's avatar Deng, Qian
Browse files

Add cardview for harbor

parent 557c68a5
......@@ -116,11 +116,58 @@ page contains `hbr-repository`.
...
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
...
}
```
......
{
"name": "harbor-ui",
"version": "0.6.52",
"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",
......
{
"name": "harbor-ui",
"version": "0.6.52",
"version": "0.6.61",
"description": "Harbor shared UI components based on Clarity and Angular4",
"author": "VMware",
"module": "index.js",
......
// 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;
}
`
\ No newline at end of file
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>
`
\ No newline at end of file
/*
* 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();
});
});
/*
* 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;
}
}
import { Type } from "@angular/core";
import { GridViewComponent } from './grid-view.component';
export * from "./grid-view.component";
export const HBR_GRIDVIEW_DIRECTIVES: Type<any>[] = [
GridViewComponent
];
\ No newline at end of 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: []
})
......
......@@ -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';
import { Type } from "@angular/core";
import { RepositoryGridviewComponent } from './repository-gridview.component';
export * from "./repository-gridview.component";
export const REPOSITORY_GRIDVIEW_DIRECTIVES: Type<any>[] = [
RepositoryGridviewComponent
];
\ No newline at end of file
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;
}