Merge branch 'master' into job_service

This commit is contained in:
Steven Zou 2018-03-29 23:25:20 +08:00
commit d1899c840d
77 changed files with 1780 additions and 193 deletions

View File

@ -2,57 +2,40 @@
## Introduction
This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes cluster.
This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes cluster. Currently this chart supports Harbor v1.4.0 release.
## Prerequisites
- Kubernetes cluster 1.8+ with Beta APIs enabled
- Kubernetes Ingress Controller is enabled
- kubectl CLI 1.8+
- PV provisioner support in the underlying infrastructure
- Helm CLI 2.8.0+
## Setup a Kubernetes cluster
You can use any tools to setup a K8s cluster.
In this guide, we use [minikube](https://github.com/kubernetes/minikube) to setup a K8s cluster as the dev/test env.
```bash
# Start minikube
minikube start --vm-driver=none
# Enable Ingress Controller
minikube addons enable ingress
```
## Installing the Chart
First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm.
```bash
helm init --canary-image
helm init
```
Download Harbor helm chart code.
```bash
git clone https://github.com/vmware/harbor
cd harbor/contrib/helm/harbor
cd contrib/helm/harbor
```
### Insecure Registry Mode
If setting Harbor Registry as insecure-registries for docker,
you don't need to generate Root CA and SSL certificate for the Harbor ingress controller.
Install the Harbor helm chart with a release name `my-release`:
Download external dependent charts required by Harbor chart.
```bash
helm install . --debug --name my-release --set externalDomain=harbor.my.domain,insecureRegistry=true
helm dependency update
```
**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI.
You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor.<IP>.xip.io`.
Then add `"insecure-registries": ["harbor.my.domain"]` in the docker daemon config file and restart docker service.
### Secure Registry Mode
By default this chart will generate a root CA and SSL certificate for your Harbor.
@ -62,18 +45,33 @@ open values.yaml, set the value of 'externalDomain' to your Harbor FQDN, and
set value of 'tlsCrt', 'tlsKey', 'caCrt'. The common name of the certificate must match your Harbor FQDN.
Install the Harbor helm chart with a release name `my-release`:
```bash
helm install . --debug --name my-release --set externalDomain=harbor.my.domain
```
**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI.
You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor.<IP>.xip.io`.
Follow the `NOTES` section in the command output to get Harbor admin password and **add Harbor root CA into docker trusted certificates**.
The command deploys Harbor on the Kubernetes cluster in the default configuration.
The [configuration](#configuration) section lists the parameters that can be configured during installation.
The [configuration](#configuration) section lists the parameters that can be configured in values.yaml or via '--set' params during installation.
> **Tip**: List all releases using `helm list`
### Insecure Registry Mode
If setting Harbor Registry as insecure-registries for docker,
you don't need to generate Root CA and SSL certificate for the Harbor ingress controller.
Install the Harbor helm chart with a release name `my-release`:
```bash
helm install . --debug --name my-release --set externalDomain=harbor.my.domain,insecureRegistry=true
```
**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI.
You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor.<IP>.xip.io`.
Then add `"insecure-registries": ["harbor.my.domain"]` in the docker daemon config file and restart docker service.
## Uninstalling the Chart
To uninstall/delete the `my-release` deployment:
@ -111,7 +109,17 @@ The following tables lists the configurable parameters of the Harbor chart and t
| `adminserver.emailIdentity` | | "" |
| `adminserver.key` | adminsever key | `not-a-secure-key` |
| `adminserver.emailPwd` | password for email | `not-a-secure-password` |
| `adminserver.harborAdminPassword` | password for admin user | `Harbor12345` |
| `adminserver.adminPassword` | password for admin user | `Harbor12345` |
| `adminserver.authenticationMode` | authentication mode for Harbor ( `db_auth` for local database, `ldap_auth` for LDAP, etc...) [Docs](https://github.com/vmware/harbor/blob/master/docs/user_guide.md#user-account) | `db_auth` |
| `adminserver.selfRegistration` | Allows users to register by themselves, otherwise only administrators can add users | `on` |
| `adminserver.ldap.url` | LDAP server URL for `ldap_auth` authentication | `ldaps://ldapserver` |
| `adminserver.ldap.searchDN` | LDAP Search DN | `` |
| `adminserver.ldap.baseDN` | LDAP Base DN | `` |
| `adminserver.ldap.filter` | LDAP Filter | `(objectClass=person)` |
| `adminserver.ldap.uid` | LDAP UID | `uid` |
| `adminserver.ldap.scope` | LDAP Scope | `2` |
| `adminserver.ldap.timeout` | LDAP Timeout | `5` |
| `adminserver.ldap.verifyCert` | LDAP Verify HTTPS Certificate | `True` |
| `adminserver.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| `adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
| **Jobservice** |

View File

@ -29,17 +29,16 @@ data:
WITH_NOTARY: "{{ .Values.notary.enabled }}"
LOG_LEVEL: "info"
IMAGE_STORE_PATH: "/" # This is a temporary hack.
AUTH_MODE: "db_auth"
SELF_REGISTRATION: "on"
LDAP_URL: "ldaps://ldapserver"
LDAP_SEARCH_DN: ""
LDAP_BASE_DN: ""
LDAP_FILTER: "(objectClass=person)"
LDAP_UID: "uid"
LDAP_SCOPE: "2"
LDAP_TIMEOUT: "5"
LDAP_TIMEOUT: "5"
LDAP_VERIFY_CERT: "True"
AUTH_MODE: "{{ .Values.adminserver.authenticationMode }}"
SELF_REGISTRATION: "{{ .Values.adminserver.selfRegistration }}"
LDAP_URL: "{{ .Values.adminserver.ldap.url }}"
LDAP_SEARCH_DN: "{{ .Values.adminserver.ldap.searchDN }}"
LDAP_BASE_DN: "{{ .Values.adminserver.ldap.baseDN }}"
LDAP_FILTER: "{{ .Values.adminserver.ldap.filter }}"
LDAP_UID: "{{ .Values.adminserver.ldap.uid }}"
LDAP_SCOPE: "{{ .Values.adminserver.ldap.scope }}"
LDAP_TIMEOUT: "{{ .Values.adminserver.ldap.timeout }}"
LDAP_VERIFY_CERT: "{{ .Values.adminserver.ldap.verifyCert }}"
DATABASE_TYPE: "mysql"
PROJECT_CREATION_RESTRICTION: "everyone"
VERIFY_REMOTE_CERT: "off"

View File

@ -9,7 +9,7 @@ type: Opaque
data:
secretKey: {{ .Values.secretKey | b64enc | quote }}
EMAIL_PWD: {{ .Values.adminserver.emailPwd | b64enc | quote }}
HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.harborAdminPassword | b64enc | quote }}
HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.adminPassword | b64enc | quote }}
MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }}
JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }}
UI_SECRET: {{ .Values.ui.secret | b64enc | quote }}

View File

@ -65,7 +65,18 @@ adminserver:
emailIdentity: ""
emailInsecure: "False"
emailPwd: not-a-secure-password
harborAdminPassword: Harbor12345
adminPassword: Harbor12345
authenticationMode: "db_auth"
selfRegistration: "on"
ldap:
url: "ldaps://ldapserver"
searchDN: ""
baseDN: ""
filter: "(objectClass=person)"
uid: "uid"
scope: "2"
timeout: "5"
verifyCert: "True"
## Persist data to a persistent volume
volumes:
config:

View File

@ -0,0 +1,3 @@
http_proxy=$http_proxy
https_proxy=$https_proxy
no_proxy=$no_proxy

View File

@ -47,6 +47,8 @@ services:
options:
syslog-address: "tcp://127.0.0.1:1514"
tag: "clair"
env_file:
./common/config/clair/clair_env
networks:
harbor-clair:
external: false

View File

@ -36,6 +36,12 @@ log_rotate_count = 50
#are all valid.
log_rotate_size = 200M
#Config http proxy for Clair, e.g. http://my.proxy.com:3128
#Clair doesn't need to connect to harbor ui container via http proxy.
http_proxy =
https_proxy =
no_proxy = 127.0.0.1,localhost,ui
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
#only take effect in the first boot, the subsequent changes of these properties
#should be performed on web ui

View File

@ -199,7 +199,8 @@ create table replication_job (
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX policy (policy_id),
INDEX poid_uptime (policy_id, update_time)
INDEX poid_uptime (policy_id, update_time),
INDEX poid_status (policy_id, status)
);
create table replication_immediate_trigger (
@ -223,7 +224,11 @@ create table img_scan_job (
job_uuid varchar(64),
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY (id)
PRIMARY KEY (id),
INDEX idx_status (status),
INDEX idx_digest (digest),
INDEX idx_uuid (job_uuid),
INDEX idx_repository_tag (repository,tag)
);
create table img_scan_overview (
@ -298,4 +303,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` (
`version_num` varchar(32) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into alembic_version values ('1.4.0');
insert into alembic_version values ('1.5.0');

View File

@ -290,5 +290,5 @@ create table alembic_version (
version_num varchar(32) NOT NULL
);
insert into alembic_version values ('1.4.0');
insert into alembic_version values ('1.5.0');

View File

@ -566,6 +566,15 @@ if args.clair_mode:
username = clair_db_username,
host = clair_db_host,
port = clair_db_port)
# config http proxy for Clair
http_proxy = rcp.get("configuration", "http_proxy").strip()
https_proxy = rcp.get("configuration", "https_proxy").strip()
no_proxy = rcp.get("configuration", "no_proxy").strip()
clair_env = os.path.join(clair_config_dir, "clair_env")
render(os.path.join(clair_temp_dir, "clair_env"), clair_env,
http_proxy = http_proxy,
https_proxy = https_proxy,
no_proxy = no_proxy)
if args.ha_mode:
prepare_ha(rcp, args)

View File

@ -120,3 +120,9 @@ func ListResourceLabels(query ...*models.ResourceLabelQuery) ([]*models.Resource
_, err := qs.All(&rls)
return rls, err
}
// DeleteResourceLabelByLabel delete the mapping relationship by label ID
func DeleteResourceLabelByLabel(id int64) error {
_, err := GetOrmer().QueryTable(&models.ResourceLabel{}).Filter("LabelID", id).Delete()
return err
}

View File

@ -81,4 +81,15 @@ func TestMethodsOfResourceLabel(t *testing.T) {
labels, err = GetLabelsOfResource(resourceType, resourceID)
require.Nil(t, err)
require.Equal(t, 0, len(labels))
// delete by label ID
id, err = AddResourceLabel(rl)
require.Nil(t, err)
err = DeleteResourceLabelByLabel(labelID)
require.Nil(t, err)
rls, err = ListResourceLabels(&models.ResourceLabelQuery{
LabelID: labelID,
})
require.Nil(t, err)
require.Equal(t, 0, len(rls))
}

View File

@ -20,7 +20,7 @@ import (
const (
// SchemaVersion is the version of database schema
SchemaVersion = "1.4.0"
SchemaVersion = "1.5.0"
)
// GetSchemaVersion return the version of database schema

View File

@ -256,6 +256,10 @@ func (l *LabelAPI) Put() {
// Delete the label
func (l *LabelAPI) Delete() {
id := l.label.ID
if err := dao.DeleteResourceLabelByLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete resource label mappings of label %d: %v", id, err))
return
}
if err := dao.DeleteLabel(id); err != nil {
l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err))
return

View File

@ -99,6 +99,7 @@ type GeneralInfo struct {
NextScanAll int64 `json:"next_scan_all"`
ClairVulnStatus *models.ClairVulnerabilityStatus `json:"clair_vulnerability_status,omitempty"`
RegistryStorageProviderName string `json:"registry_storage_provider_name"`
ReadOnly bool `json:"read_only"`
}
// validate for validating user if an admin.
@ -177,6 +178,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
HasCARoot: caStatErr == nil,
HarborVersion: harborVersion,
RegistryStorageProviderName: cfg[common.RegistryStorageProviderName].(string),
ReadOnly: config.ReadOnly(),
}
if info.WithClair {
info.ClairVulnStatus = getClairVulnStatus()

View File

@ -64,6 +64,7 @@ func TestGetGeneralInfo(t *testing.T) {
assert.Equal(false, g.WithNotary, "with notary should be false")
assert.Equal(true, g.HasCARoot, "has ca root should be true")
assert.NotEmpty(g.HarborVersion, "harbor version should not be empty")
assert.Equal(false, g.ReadOnly, "readonly should be false")
}
func TestGetCert(t *testing.T) {

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

@ -81,6 +81,7 @@ export class Configuration {
token_expiration: NumberValueItem;
cfg_expiration: NumberValueItem;
scan_all_policy: ComplexValueItem;
read_only: BoolValueItem;
public constructor() {
this.auth_mode = new StringValueItem("db_auth", true);
@ -116,5 +117,6 @@ export class Configuration {
daily_time: 0
}
}, true);
this.read_only = new BoolValueItem(false, true);
}
}

View File

@ -10,8 +10,8 @@ export const CREATE_EDIT_LABEL_STYLE: string = `
section{padding:.5rem 0;}
section> label{margin-left: 20px;}
.dropdown-menu{display:inline-block;width:166px; padding:6px;}
.dropdown-item{ display:inline-flex; margin:2px 4px;
.dropdown-menu {display:inline-block;width:166px; padding:6px;}
.dropdown-menu .dropdown-item{ display:inline-flex; margin:2px 4px;
display: inline-block;padding: 0px; width:30px;height:24px; text-align: center;line-height: 24px;}
.btnColor{
margin: 0 !important;

View File

@ -16,7 +16,7 @@ import {
Output,
EventEmitter,
OnDestroy,
Input, OnInit, ViewChild
Input, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef
} from '@angular/core';
@ -27,16 +27,15 @@ import { CREATE_EDIT_LABEL_TEMPLATE } from './create-edit-label.component.html';
import {toPromise, clone, compareValue} from '../utils';
import {Subject} from "rxjs/Subject";
import {LabelService} from "../service/label.service";
import {ErrorHandler} from "../error-handler/error-handler";
import {NgForm} from "@angular/forms";
import {Subject} from "rxjs/Subject";
@Component({
selector: 'hbr-create-edit-label',
template: CREATE_EDIT_LABEL_TEMPLATE,
styles: [CREATE_EDIT_LABEL_STYLE]
styles: [CREATE_EDIT_LABEL_STYLE],
})
export class CreateEditLabelComponent implements OnInit, OnDestroy {
@ -46,12 +45,13 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
labelModel: Label = this.initLabel();
labelId = 0;
nameChecker: Subject<string> = new Subject<string>();
checkOnGoing: boolean;
isLabelNameExist = false;
labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8'];
nameChecker = new Subject<string>();
labelForm: NgForm;
@ViewChild('labelForm')
currentForm: NgForm;
@ -66,16 +66,12 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
) { }
ngOnInit(): void {
this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((name: string) => {
this.nameChecker.debounceTime(500).subscribe((name: string) => {
this.checkOnGoing = true;
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId))
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId, name))
.then(targets => {
if (targets && targets.length) {
if (targets.find(m => m.name === name)) {
this.isLabelNameExist = true;
} else {
this.isLabelNameExist = false;
};
this.isLabelNameExist = true;
}else {
this.isLabelNameExist = false;
}

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

@ -46,7 +46,7 @@ export const REPOSITORY_TEMPLATE = `
</section>
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
<div id=images-container>
<hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)" class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [isGuest]="isGuest" [projectId]="projectId"></hbr-tag>
<hbr-tag ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" (signatureOutput)="saveSignatures($event)" class="sub-grid-custom" [repoName]="repoName" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [withAdmiral]="withAdmiral" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [isGuest]="isGuest" [projectId]="projectId"></hbr-tag>
</div>
</section>
</div>

View File

@ -54,6 +54,7 @@ export class RepositoryComponent implements OnInit {
@Input() isGuest: boolean;
@Input() withNotary: boolean;
@Input() withClair: boolean;
@Input() withAdmiral: boolean;
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();

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

@ -140,6 +140,6 @@ export class RepositoryDefaultService extends RepositoryService {
return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise()
.then(response => response)
.catch(error => { Promise.reject(error); });
.catch(error => {return Promise.reject(error); });
}
}

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

@ -1,6 +1,5 @@
export const TAG_DETAIL_STYLES: string = `
.overview-section {
background-color: white;
padding-bottom: 36px;
border-bottom: 1px solid #cccccc;
}
@ -78,27 +77,37 @@ export const TAG_DETAIL_STYLES: string = `
padding-left: 24px;
}
.vulnerabilities-info .third-column {
.third-column {
margin-left: 36px;
}
.vulnerability{
margin-left: 50px;
margin-top: -12px;
margin-bottom: 20px;}
.vulnerabilities-info .second-column,
.vulnerabilities-info .fourth-column {
.vulnerabilities-info .second-column {
text-align: left;
margin-left: 6px;
}
.fourth-column{
float: left;
margin-left:20px;}
.vulnerabilities-info .second-row {
margin-top: 6px;
}
.detail-title {
font-weight: 500;
float:left;
font-weight: 600;
font-size: 14px;
}
.image-detail-label {
text-align: right;
margin-right: 10px;
text-align: left;
font-weight: 600;
}
.image-detail-value {

View File

@ -7,26 +7,22 @@ export const TAG_DETAIL_HTML: string = `
</div>
<div class="title-block">
<div class="tag-name">
<h1>{{tagDetails.name}}</h1>
</div>
<div class="tag-timestamp">
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
<h1>{{repositoryId}}:{{tagDetails.name}}</h1>
</div>
</div>
</div>
<div class="summary-block">
<div class="image-summary">
<div class="detail-title">
{{'TAG.IMAGE_DETAILS' | translate }}
</div>
<div class="flex-block">
<div class="image-detail-label">
<div>{{'TAG.AUTHOR' | translate }}</div>
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
<div>{{'TAG.OS' | translate }}</div>
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
</div>
<div class="image-detail-value">
<div>{{author | translate}}</div>
<div>{{tagDetails.architecture}}</div>
<div>{{tagDetails.os}}</div>
<div>{{tagDetails.docker_version}}</div>
@ -35,8 +31,8 @@ export const TAG_DETAIL_HTML: string = `
</div>
</div>
<div>
<div class="detail-title">
{{'TAG.IMAGE_VULNERABILITIES' | translate }}
<div class="vulnerability">
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="tagDetails.scan_overview"></hbr-vulnerability-bar>
</div>
<div class="flex-block vulnerabilities-info">
<div>
@ -46,12 +42,6 @@ export const TAG_DETAIL_HTML: string = `
<div class="second-row">
<clr-icon shape="exclamation-triangle" size="24" class="tip-icon-medium"></clr-icon>
</div>
</div>
<div class="second-column">
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }}</div>
<div class="second-row">{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }}</div>
</div>
<div class="third-column">
<div>
<clr-icon shape="play" size="20" class="tip-icon-low rotate-90"></clr-icon>
</div>
@ -59,11 +49,20 @@ export const TAG_DETAIL_HTML: string = `
<clr-icon shape="help" size="18" style="margin-left: 2px;"></clr-icon>
</div>
</div>
<div class="fourth-column">
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }}</div>
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }}</div>
<div class="second-column">
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}</div>
<div class="second-row">{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}</div>
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}</div>
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}</div>
</div>
</div>
</div>
<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>
</div>
</div>
</div>
</section>

View File

@ -10,6 +10,11 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { FilterComponent } from '../filter/index';
import { VULNERABILITY_SCAN_STATUS } from '../utils';
import {VULNERABILITY_DIRECTIVES} from "../vulnerability-scanning/index";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {JobLogViewerComponent} from "../job-log-viewer/job-log-viewer.component";
import {ChannelService} from "../channel/channel.service";
import {JobLogService, JobLogDefaultService} from "../service/job-log.service";
describe('TagDetailComponent (inline template)', () => {
@ -66,10 +71,16 @@ describe('TagDetailComponent (inline template)', () => {
declarations: [
TagDetailComponent,
ResultGridComponent,
VULNERABILITY_DIRECTIVES,
LabelPieceComponent,
JobLogViewerComponent,
FilterComponent
],
providers: [
ErrorHandler,
ChannelService,
JobLogDefaultService,
{provide: JobLogService, useClass: JobLogDefaultService},
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
@ -119,7 +130,7 @@ describe('TagDetailComponent (inline template)', () => {
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
expect(el).toBeTruthy();
expect(el.textContent.trim()).toEqual('nginx');
expect(el.textContent.trim()).toEqual('mock_repo:nginx');
});
}));
@ -133,7 +144,7 @@ describe('TagDetailComponent (inline template)', () => {
expect(el).toBeTruthy();
let el2: HTMLElement = el.querySelector('div');
expect(el2).toBeTruthy();
expect(el2.textContent).toEqual("amd64");
expect(el2.textContent).toEqual("steven");
});
}));
@ -147,7 +158,7 @@ describe('TagDetailComponent (inline template)', () => {
expect(el).toBeTruthy();
let el2: HTMLElement = el.querySelector('div');
expect(el2).toBeTruthy();
expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGH");
expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGHTAG.LEVEL_VULNERABILITIES");
});
}));

View File

@ -6,6 +6,7 @@ import { TAG_DETAIL_HTML } from './tag-detail.component.html';
import { TagService, Tag, VulnerabilitySeverity } from '../service/index';
import { toPromise } from '../utils';
import { ErrorHandler } from '../error-handler/index';
import {Label} from "../service/interface";
@Component({
selector: 'hbr-tag-detail',
@ -19,9 +20,11 @@ export class TagDetailComponent implements OnInit {
_mediumCount: number = 0;
_lowCount: number = 0;
_unknownCount: number = 0;
labels: Label;
@Input() tagId: string;
@Input() repositoryId: string;
@Input() withAdmiral: boolean;
tagDetails: Tag = {
name: "--",
size: "--",
@ -74,7 +77,7 @@ export class TagDetailComponent implements OnInit {
}
onBack(): void {
this.backEvt.emit(this.tagId);
this.backEvt.emit(this.repositoryId);
}
getPackageText(count: number): string {

View File

@ -66,4 +66,11 @@ export const TAG_STYLE = `
:host >>> .signpost-content-body{padding:0 .4rem;}
:host >>> .signpost-content-header{display:none;}
.filterLabelPiece{position: absolute; bottom :0px;z-index:1;}
.dropdown .dropdown-toggle.btn {
padding-right: 1rem;
border-left-width: 0;
border-right-width: 0;
border-radius: 0;
margin-top: -2px;
}
`;

View File

@ -17,11 +17,12 @@ 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">
<clr-dropdown>
<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'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<label class="dropdown-header">{{'REPOSITORY.FILTER_BY_LABEL' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #labelNamePiece (keyup)="handleInputFilter(labelNamePiece.value)"></div>
<div [hidden]='imageFilterLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
@ -38,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>
<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)="label.iconsShow = true; selectLabel(label)">
<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(); label.iconsShow = false; unSelectLabel(label)"></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>
@ -72,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">
@ -97,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">
@ -113,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

@ -140,13 +140,6 @@ describe('TagComponent (inline template)', () => {
labelService = fixture.debugElement.injector.get(LabelService);
/*spyLabels = spyOn(labelService, 'getLabels').and.callFake(function (param) {
if (param === 'g') {
return Promise.resolve(mockLabels);
}else {
Promise.resolve(mockLabels1)
}
})*/
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));

View File

@ -80,7 +80,7 @@ export class TagComponent implements OnInit, AfterViewInit {
@Input() registryUrl: string;
@Input() withNotary: boolean;
@Input() withClair: boolean;
@Input() withAdmiral: boolean;
@Output() refreshRepo = new EventEmitter<boolean>();
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
@Output() signatureOutput = new EventEmitter<any>();
@ -180,11 +180,11 @@ export class TagComponent implements OnInit, AfterViewInit {
.subscribe((name: string) => {
if (name && name.length) {
this.filterOnGoing = true;
this.imageFilterLabels = [];
this.imageStickLabels = [];
this.imageLabels.forEach(data => {
if (data.label.name.indexOf(name) !== -1) {
this.imageFilterLabels.push(data);
this.imageStickLabels.push(data);
}
})
setTimeout(() => {
@ -196,7 +196,9 @@ export class TagComponent implements OnInit, AfterViewInit {
}
ngAfterViewInit() {
this.getAllLabels();
if (!this.withAdmiral) {
this.getAllLabels();
}
}
public get filterLabelPieceWidth() {
@ -302,7 +304,7 @@ export class TagComponent implements OnInit, AfterViewInit {
this.selectedChange(tag);
}
selectLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && labelInfo.iconsShow) {
if (labelInfo && !labelInfo.iconsShow) {
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
toPromise<any>(this.tagService.addLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
@ -314,7 +316,7 @@ export class TagComponent implements OnInit, AfterViewInit {
}
unSelectLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && !labelInfo.iconsShow) {
if (labelInfo && labelInfo.iconsShow) {
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
toPromise<any>(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
@ -442,7 +444,7 @@ export class TagComponent implements OnInit, AfterViewInit {
} else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) {
return (size / Math.pow(1024, 2)).toFixed(2) + "MB";
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
return (size / Math.pow(1024, 3)).toFixed(2) + "MB";
return (size / Math.pow(1024, 3)).toFixed(2) + "GB";
} else {
return size + "B";
}

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.53",
"harbor-ui": "0.6.61",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -12,7 +12,7 @@
<li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<li role="presentation" class="nav-item" *ngIf="!withAdmiral">
<button id="config-label" class="btn btn-link nav-link" aria-controls="system_label" [class.active]='isCurrentTabLink("config-label")' type="button" (click)='tabLinkClick("config-label")'>{{'CONFIG.LABEL' | translate }}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="withClair">
@ -28,10 +28,9 @@
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
</section>
<section id="system_label" role="tabpanel" aria-labelledby="config-label" [hidden]='!isCurrentTabContent("system_label")' style="padding-top: 16px;">
<section id="system_label" role="tabpanel" aria-labelledby="config-label" *ngIf="!withAdmiral" [hidden]='!isCurrentTabContent("system_label")' style="padding-top: 16px;">
<hbr-label [scope]="'g'"></hbr-label>
<!--<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>-->
</section>
</section>
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
</section>

View File

@ -81,6 +81,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
return this.appConfigService.getConfig().with_clair;
}
public get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
isCurrentTabLink(tabId: string): boolean {
return this.currentTabId === tabId;
}

View File

@ -108,6 +108,14 @@ const harborRoutes: Routes = [
projectResolver: ProjectRoutingResolver
}
},
{
path: 'projects/:id/repositories/:repo/tags/:tag',
component: TagDetailPageComponent,
canActivate: [MemberGuard],
resolve: {
projectResolver: ProjectRoutingResolver
},
},
{
path: 'projects/:id',
component: ProjectDetailComponent,
@ -124,10 +132,6 @@ const harborRoutes: Routes = [
path: 'repositories/:repo/tags',
component: TagRepositoryComponent,
},
{
path: 'repositories/:repo/tags/:tag',
component: TagDetailPageComponent
},
{
path: 'replications',
component: ReplicationPageComponent,

View File

@ -13,7 +13,7 @@
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<li class="nav-item" *ngIf="(isSProjectAdmin || isSystemAdmin) && !withAdmiral">
<a class="nav-link" routerLink="labels" routerLinkActive="active">{{'PROJECT_DETAIL.LABELS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">

View File

@ -20,6 +20,7 @@ import { SessionService } from '../../shared/session.service';
import { ProjectService } from '../../project/project.service';
import { RoleMapping } from '../../shared/shared.const';
import {AppConfigService} from "../../app-config.service";
@Component({
selector: 'project-detail',
@ -38,6 +39,7 @@ export class ProjectDetailComponent {
private route: ActivatedRoute,
private router: Router,
private sessionService: SessionService,
private appConfigService: AppConfigService,
private projectService: ProjectService) {
this.hasSignedIn = this.sessionService.getCurrentUser() !== null;
@ -61,6 +63,10 @@ export class ProjectDetailComponent {
return this.sessionService.getCurrentUser() != null;
}
public get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
backToProject(): void {
if (window.sessionStorage) {
window.sessionStorage.setItem('fromDetails', 'true');

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,3 @@
<div style="margin-top: 24px;">
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [repositoryId]="repositoryId"></hbr-tag-detail>
<div>
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [withAdmiral]="withAdmiral" [repositoryId]="repositoryId"></hbr-tag-detail>
</div>

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import {AppConfigService} from "../../app-config.service";
@Component({
selector: 'repository',
@ -25,6 +26,7 @@ export class TagDetailPageComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private appConfigService: AppConfigService,
private router: Router
) {
}
@ -32,10 +34,14 @@ export class TagDetailPageComponent implements OnInit {
ngOnInit(): void {
this.repositoryId = this.route.snapshot.params["repo"];
this.tagId = this.route.snapshot.params["tag"];
this.projectId = this.route.snapshot.parent.params["id"];
this.projectId = this.route.snapshot.params["id"];
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
goBack(tag: string): void {
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
this.router.navigate(["harbor", "projects", this.projectId, "repositories", tag]);
}
}

View File

@ -1,3 +1,6 @@
<div>
<hbr-repository (tagClickEvent)="watchTagClickEvt($event)" (backEvt)="goBack($event)" [repoName]="repoName" [withClair]="withClair" [withNotary]="withNotary" [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

@ -68,6 +68,10 @@ export class TagRepositoryComponent implements OnInit {
return this.appConfigService.getConfig().with_clair;
}
get withAdmiral(): boolean {
return this.appConfigService.getConfig().with_admiral;
}
get hasSignedIn(): boolean {
return this.session.getCurrentUser() !== null;
}
@ -75,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.",
@ -415,7 +416,11 @@
"IMAGE": "Images",
"LABELS": ":labels",
"ADD_TO_IMAGE": "Add labels to this image",
"ADD_LABELS": "Add labels"
"FILTER_BY_LABEL": "Filter projects by label",
"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?"
@ -438,6 +443,8 @@
"REPLICATION": "Replication",
"EMAIL": "Email",
"LABEL": "Label",
"REPOSITORY": "Repository",
"REPO_READ_ONLY": "Repository Read Only",
"SYSTEM": "System Settings",
"VULNERABILITY": "Vulnerability",
"CONFIRM_TITLE": "Confirm 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.",
@ -413,9 +414,13 @@
"INFO": "Información",
"NO_INFO": "Sin información de descripción para este repositorio",
"IMAGE": "Imágenes",
"LABELS": ":labels",
"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?"
@ -438,6 +443,8 @@
"REPLICATION": "Replicación",
"EMAIL": "Email",
"LABEL": "Label",
"REPOSITORY": "Repository",
"REPO_READ_ONLY": "Repository Read Only",
"SYSTEM": "Opciones del Sistema",
"VULNERABILITY": "Vulnerability",
"CONFIRM_TITLE": "Confirma cancelación",
@ -613,9 +620,12 @@
"OS": "OS",
"SCAN_COMPLETION_TIME": "Scan Completed",
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
"LEVEL_VULNERABILITIES": "Level Vulnerabilities",
"PLACEHOLDER": "We couldn't find any tags!",
"COPY_ERROR": "Copy failed, please try to manually copy.",
"FILTER_FOR_TAGS": "Etiquetas de filtro"
"FILTER_FOR_TAGS": "Etiquetas de filtro",
"AUTHOR": "Author",
"LABELS": "LABELS"
},
"LABEL": {
"LABEL": "Label",

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": "表单内容改变,确认是否取消?"
@ -438,6 +443,8 @@
"REPLICATION": "复制",
"EMAIL": "邮箱",
"LABEL": "标签",
"REPOSITORY": "仓库",
"REPO_READ_ONLY": "仓库只读",
"SYSTEM": "系统设置",
"VULNERABILITY": "漏洞",
"CONFIRM_TITLE": "确认取消",
@ -613,9 +620,12 @@
"OS": "操作系统",
"SCAN_COMPLETION_TIME": "扫描完成时间",
"IMAGE_VULNERABILITIES": "镜像缺陷",
"LEVEL_VULNERABILITIES": "缺陷等级",
"PLACEHOLDER": "未发现任何标签!",
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。",
"FILTER_FOR_TAGS": "过滤项目"
"FILTER_FOR_TAGS": "过滤项目",
"AUTHOR": "作者",
"LABELS": "标签"
},
"LABEL": {
"LABEL": "标签",

View File

@ -71,4 +71,61 @@ sn: Mike02
uid: mike02
uidnumber: 5001
userpassword: {MD5}wb68DeX0CyENafzUADNn9A==
memberof: cn=harbor_users,ou=groups,dc=example,dc=com
memberof: cn=harbor_users,ou=groups,dc=example,dc=com
dn: cn=mike03,ou=people,dc=example,dc=com
cn: mike03
gidnumber: 10000
givenname: mike03
homedirectory: /home/mike03
loginshell: /bin/bash
mail: mike03@example.com
objectclass: top
objectclass: posixAccount
objectclass: shadowAccount
objectclass: inetOrgPerson
objectclass: organizationalPerson
objectclass: person
sn: Mike03
uid: mike03
uidnumber: 5002
userpassword: {MD5}wb68DeX0CyENafzUADNn9A==
memberof: cn=harbor_users,ou=groups,dc=example,dc=com
dn: cn=mike04,ou=people,dc=example,dc=com
cn: mike04
gidnumber: 10000
givenname: mike04
homedirectory: /home/mike04
loginshell: /bin/bash
mail: mike04@example.com
objectclass: top
objectclass: posixAccount
objectclass: shadowAccount
objectclass: inetOrgPerson
objectclass: organizationalPerson
objectclass: person
sn: Mike04
uid: mike04
uidnumber: 5003
userpassword: {MD5}wb68DeX0CyENafzUADNn9A==
memberof: cn=harbor_users,ou=groups,dc=example,dc=com
dn: cn=mike05,ou=people,dc=example,dc=com
cn: mike05
gidnumber: 10000
givenname: mike05
homedirectory: /home/mike05
loginshell: /bin/bash
mail: mike05@example.com
objectclass: top
objectclass: posixAccount
objectclass: shadowAccount
objectclass: inetOrgPerson
objectclass: organizationalPerson
objectclass: person
sn: Mike05
uid: mike05
uidnumber: 5004
userpassword: {MD5}wb68DeX0CyENafzUADNn9A==
memberof: cn=harbor_users,ou=groups,dc=example,dc=com

View File

@ -42,4 +42,8 @@ User Email Should Exist
[Arguments] ${email}
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
Switch to User Tag
Page Should Contain Element xpath=//clr-dg-cell[contains(., '${email}')]
Page Should Contain Element xpath=//clr-dg-cell[contains(., '${email}')]
Add User Button Should Be Disabled
Sleep 1
Page Should Contain Element //button[contains(.,'New') and @disabled='']

View File

@ -211,10 +211,22 @@ Set Scan All To None
click element //vulnerability-config//select/option[@value='none']
sleep 1
click element ${config_save_button_xpath}
Set Scan All To Daily
click element //vulnerability-config//select
click element //vulnerability-config//select/option[@value='daily']
sleep 1
click element ${config_save_button_xpath}
Click Scan Now
click element //vulnerability-config//button[contains(.,'SCAN')]
click element //vulnerability-config//button[contains(.,'SCAN')]
Enable Read Only
${rc} ${output}= Run And Return Rc And Output curl -u admin:Harbor12345 -s --insecure -H "Content-Type: application/json" -X PUT -d '{"read_only":true}' "https://${ip}/api/configurations"
Log To Console ${output}
Should Be Equal As Integers ${rc} 0
Disable Read Only
${rc} ${output}= Run And Return Rc And Output curl -u admin:Harbor12345 -s --insecure -H "Content-Type: application/json" -X PUT -d '{"read_only":false}' "https://${ip}/api/configurations"
Log To Console ${output}
Should Be Equal As Integers ${rc} 0

View File

@ -0,0 +1,29 @@
# Copyright 2016-2017 VMware, Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License
*** Settings ***
Documentation This resource provides any keywords related to the Harbor private registry appliance
Resource ../../resources/Util.robot
*** Variables ***
${HARBOR_VERSION} v1.1.1
*** Keywords ***
Ldap User Should Not See Change Password
Click Element //clr-header//clr-dropdown[2]//button
Sleep 1
Page Should Not Contain Password

View File

@ -9,6 +9,7 @@ ${HARBOR_VERSION} V1.1.1
Goto Project Config
Click Element //project-detail//ul/li[contains(.,'Configuration')]
Sleep 2
Click Project Public
Mouse Down //hbr-project-policy-config//input[@name='public']

View File

@ -44,11 +44,11 @@ Create An New Project With New User
#It's the log of project.
Go To Project Log
Click Element xpath=//project-detail//ul/li[3]
Click Element xpath=${project_log_xpath}
Sleep 2
Switch To Member
Click Element xpath=//project-detail//li[2]
Click Element xpath=${project_member_xpath}
Sleep 1
Switch To Log

View File

@ -22,4 +22,6 @@ ${project_public_xpath} //input[@name='public']/..//label
${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-container.main-container div.content-container div.content-area.content-area-override project div.row div.col-lg-12.col-md-12.col-sm-12.col-xs-12 div.row.flex-items-xs-between div.option-left create-project clr-modal div.modal div.modal-dialog div.modal-content div.modal-footer button.btn.btn-primary
${log_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Logs')]
${projects_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Projects')]
${project_replication_xpath} //project-detail//a[contains(.,'Replication')]
${project_replication_xpath} //project-detail//a[contains(.,'Replication')]
${project_log_xpath} //project-detail//li[contains(.,'Logs')]
${project_member_xpath} //project-detail//li[contains(.,'Members')]

View File

@ -44,6 +44,7 @@ Resource Harbor-Pages/Configuration.robot
Resource Harbor-Pages/Configuration_Elements.robot
Resource Harbor-Pages/ToolKit.robot
Resource Harbor-Pages/Vulnerability.robot
Resource Harbor-Pages/LDAP-Mode.robot
Resource Docker-Util.robot
Resource Admiral-Util.robot
Resource OVA-Util.robot

View File

@ -37,6 +37,56 @@ Test Case - Ldap Sign in and out
Sign In Harbor ${HARBOR_URL} mike zhu88jie
Close Browser
Test Case - System Admin On-board New Member
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Switch To User Tag
Page Should Not Contain mike02
Back To Projects
Create An New Project project${d}
Go Into Project project${d}
Switch To Member
Add Guest Member To Project mike02
Page Should Contain mike02
Close Browser
Test Case - LDAP User On-borad New Member
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
Sign In Harbor ${HARBOR_URL} mike03 zhu88jie
Switch To User Tag
Page Should Not Contain mike04
Back To Projects
Create An New Project project${d}
Go Into Project project${d}
Switch To Member
Add Guest Member To Project mike04
Page Should Contain mike04
Close Browser
Test Case - Home Page Differences With DB Mode
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Logout Harbor
Sleep 2
Page Should Not Contain Sign up
Page Should Not Contain Forgot password
Close Browser
Test Case - New User Button Is Unusable
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} ${HARBOR_ADMIN} ${HARBOR_PASSWORD}
Switch To User Tag
Add User Button Should Be Disabled
Close Browser
Test Case - Change Password Is Invisible
Init Chrome Driver
Sign In Harbor ${HARBOR_URL} mike05 zhu88jie
Ldap User Should Not See Change Password
Close Browser
Test Case - Ldap User Create Project
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
@ -58,4 +108,4 @@ Test Case - Ldap User Push An Image
Close Browser
Test Case - Ldap User Can Not login
Docker Login Fail ${ip} test 123456
Docker Login Fail ${ip} test 123456

View File

@ -35,6 +35,18 @@ Test Case - Vulnerability Data Not Ready
Go To Vulnerability Config
Vulnerability Not Ready Config Hint
Test Case - Read Only Mode
Init Chrome Driver
${d}= Get Current Date result_format=%m%s
Create An New Project With New User url=${HARBOR_URL} username=tester${d} email=tester${d}@vmware.com realname=tester${d} newPassword=Test1@34 comment=harbor projectname=project${d} public=true
Enable Read Only
Cannot Push image ${ip} tester${d} Test1@34 project${d} busybox:latest
Disable Read Only
Push image ${ip} tester${d} Test1@34 project${d} busybox:latest
Close Browser
Test Case - Create An New User
Init Chrome Driver
${d}= Get Current Date result_format=%m%s

View File

@ -73,3 +73,5 @@ Changelog for harbor database schema
- create table `user_group`
- modify table `project_member` use `id` as PK and add column `entity_type` to indicate if the member is user or group.
- add `job_uuid` column to `replication_job` and `img_scan_job`
- add index `poid_status` in table replication_job
- add index `idx_status`, `idx_status`, `idx_digest`, `idx_repository_tag` in table img_scan_job