mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
commit
c33ed6f957
@ -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**
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
67
src/ui_ng/lib/src/gridview/grid-view.component.css.ts
Normal file
67
src/ui_ng/lib/src/gridview/grid-view.component.css.ts
Normal 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;
|
||||
}
|
||||
|
||||
`
|
18
src/ui_ng/lib/src/gridview/grid-view.component.html.ts
Normal file
18
src/ui_ng/lib/src/gridview/grid-view.component.html.ts
Normal 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>
|
||||
`
|
48
src/ui_ng/lib/src/gridview/grid-view.component.spec.ts
Normal file
48
src/ui_ng/lib/src/gridview/grid-view.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
246
src/ui_ng/lib/src/gridview/grid-view.component.ts
Normal file
246
src/ui_ng/lib/src/gridview/grid-view.component.ts
Normal 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;
|
||||
}
|
||||
}
|
8
src/ui_ng/lib/src/gridview/index.ts
Normal file
8
src/ui_ng/lib/src/gridview/index.ts
Normal 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
|
||||
];
|
@ -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';
|
||||
|
8
src/ui_ng/lib/src/repository-gridview/index.ts
Normal file
8
src/ui_ng/lib/src/repository-gridview/index.ts
Normal 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
|
||||
];
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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> {{'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>
|
||||
`;
|
@ -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');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
|
@ -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 { }
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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> {{'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> {{'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> {{'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}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||
|
@ -196,7 +196,9 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.getAllLabels();
|
||||
if (!this.withAdmiral) {
|
||||
this.getAllLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public get filterLabelPieceWidth() {
|
||||
|
@ -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",
|
||||
|
@ -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>
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
@ -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"]);
|
||||
}
|
||||
}
|
@ -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?"
|
||||
|
@ -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?"
|
||||
|
@ -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 ?"
|
||||
|
@ -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": "表单内容改变,确认是否取消?"
|
||||
|
Loading…
Reference in New Issue
Block a user