mirror of
https://github.com/goharbor/harbor.git
synced 2025-03-02 10:41:59 +01:00
Merge branch 'master' into job_service
This commit is contained in:
commit
d1899c840d
@ -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** |
|
||||
|
@ -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"
|
||||
|
@ -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 }}
|
||||
|
@ -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:
|
||||
|
3
make/common/templates/clair/clair_env
Normal file
3
make/common/templates/clair/clair_env
Normal file
@ -0,0 +1,3 @@
|
||||
http_proxy=$http_proxy
|
||||
https_proxy=$https_proxy
|
||||
no_proxy=$no_proxy
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -93,6 +93,12 @@ On specific project mode, without need projectId, but also need to provide proje
|
||||
|
||||
* **Repository and Tag Management View**
|
||||
|
||||
The `hbr-repository-stackview` directive is deprecated. Using `hbr-repository-listview` and `hbr-repository` instead. You should define two routers one for render
|
||||
`hbr-repository-listview` the other is for `hbr-repository`. `hbr-repository-listview` will output an event, you need catch this event and redirect to related
|
||||
page contains `hbr-repository`.
|
||||
|
||||
**hbr-repository-listview Directive**
|
||||
|
||||
**projectId** is used to specify which projects the repositories are from.
|
||||
|
||||
**projectName** is used to generate the related commands for pushing images.
|
||||
@ -101,18 +107,99 @@ On specific project mode, without need projectId, but also need to provide proje
|
||||
|
||||
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
|
||||
|
||||
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
|
||||
**repoClickEvent** is an @output event emitter for you to catch the repository click events.
|
||||
|
||||
|
||||
```
|
||||
<hbr-repository-stackview [projectId]="..." [projectName]="" [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
|
||||
<hbr-repository-listview [projectId]="" [projectName]="" [hasSignedIn]="" [hasProjectAdminRole]=""
|
||||
(repoClickEvent)="watchRepoClickEvent($event)"></hbr-repository-listview>
|
||||
|
||||
...
|
||||
|
||||
watchTagClickEvent(tag: Tag): void {
|
||||
//Process tag
|
||||
watchRepoClickEvent(repo: RepositoryItem): void {
|
||||
//Process repo
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**hbr-repository-gridview Directive**
|
||||
|
||||
**projectId** is used to specify which projects the repositories are from.
|
||||
|
||||
**projectName** is used to generate the related commands for pushing images.
|
||||
|
||||
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
|
||||
|
||||
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
|
||||
|
||||
**withVIC** is integrated with VIC
|
||||
|
||||
**repoClickEvent** is an @output event emitter for you to catch the repository click events.
|
||||
|
||||
**repoProvisionEvent** is an @output event emitter for you to catch the deploy button click event.
|
||||
|
||||
**addInfoEvent** is an @output event emitter for you to catch the add additional info button event.
|
||||
|
||||
@Output() repoProvisionEvent = new EventEmitter<RepositoryItem>();
|
||||
@Output() addInfoEvent = new EventEmitter<RepositoryItem>();
|
||||
|
||||
|
||||
```
|
||||
<hbr-repository-gridview [projectId]="" [projectName]="" [hasSignedIn]="" [hasProjectAdminRole]=""
|
||||
(repoClickEvent)="watchRepoClickEvent($event)"
|
||||
(repoProvisionEvent)="watchRepoProvisionEvent($event)"
|
||||
(addInfoEvent)="watchAddInfoEvent($event)"></hbr-repository-gridview>
|
||||
|
||||
...
|
||||
|
||||
|
||||
watchRepoClickEvent(repo: RepositoryItem): void {
|
||||
//Process repo
|
||||
...
|
||||
}
|
||||
|
||||
watchRepoProvisionEvent(repo: RepositoryItem): void {
|
||||
//Process repo
|
||||
...
|
||||
}
|
||||
|
||||
watchAddInfoEvent(repo: RepositoryItem): void {
|
||||
//Process repo
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**hbr-repository Directive**
|
||||
|
||||
**projectId** is used to specify which projects the repositories are from.
|
||||
|
||||
**repoName** is used to generate the related commands for pushing images.
|
||||
|
||||
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
|
||||
|
||||
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
|
||||
|
||||
**withClair** is Clair installed
|
||||
|
||||
**withNotary** is Notary installed
|
||||
|
||||
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
|
||||
|
||||
**goBackClickEvent** is an @output event emitter for you to catch the go back events.
|
||||
|
||||
```
|
||||
<hbr-repository [projectId]="" [repoName]="" [hasSignedIn]="" [hasProjectAdminRole]="" [withClair]="" [withNotary]=""
|
||||
(tagClickEvent)="watchTagClickEvt($event)" (backEvt)="watchGoBackEvt($event)" ></hbr-repository>
|
||||
|
||||
watchTagClickEvt(tagEvt: TagClickEvent): void {
|
||||
...
|
||||
}
|
||||
|
||||
watchGoBackEvt(projectId: string): void {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* **Tag detail view**
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "harbor-ui",
|
||||
"version": "0.6.45",
|
||||
"version": "0.6.61",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"scripts": {
|
||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "harbor-ui",
|
||||
"version": "0.6.45",
|
||||
"version": "0.6.61",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"author": "VMware",
|
||||
"module": "index.js",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
67
src/ui_ng/lib/src/gridview/grid-view.component.css.ts
Normal file
67
src/ui_ng/lib/src/gridview/grid-view.component.css.ts
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved.
|
||||
// This software is released under MIT license.
|
||||
// The full license information can be found in LICENSE in the root directory of this project.
|
||||
|
||||
// @import 'node_modules/admiral-ui-common/css/mixins';
|
||||
|
||||
export const GRIDVIEW_STYLE = `
|
||||
.grid-content {
|
||||
position: relative;
|
||||
top: 36px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: auto;
|
||||
max-height: 65vh;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
display: block;
|
||||
max-width: 400px;
|
||||
min-width: 300px;
|
||||
position: absolute;
|
||||
margin-right: 40px;
|
||||
transition: width 0.4s, transform 0.4s;
|
||||
}
|
||||
|
||||
.content-empty {
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.central-block-loading {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@include animation(fadein 0.4s);
|
||||
text-align: center;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.central-block-loading-more {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
@include animation(fadein 0.4s);
|
||||
text-align: center;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.vertical-helper {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
`
|
18
src/ui_ng/lib/src/gridview/grid-view.component.html.ts
Normal file
18
src/ui_ng/lib/src/gridview/grid-view.component.html.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export const GRIDVIEW_TEMPLATE = `
|
||||
<div class="grid-content" (scroll)="onScroll($event)">
|
||||
<div class="items" [ngStyle]="itemsHolderStyle" #itemsHolder >
|
||||
<span *ngFor="let item of items;let i = index; trackBy:trackByFn" class='card-item' [ngStyle]="cardStyles[i]" #cardItem
|
||||
(mouseenter)='onCardEnter(i)' (mouseleave)='onCardLeave(i)'>
|
||||
<ng-template [ngTemplateOutlet]="gridItemTmpl" [ngOutletContext]="{item: item}">
|
||||
</ng-template>
|
||||
</span>
|
||||
<span *ngIf="items.length === 0 && !loading" class="content-empty">
|
||||
{{'REPOSITORY.NO_ITEMS' | translate}}
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="loading" [ngClass]="{'central-block-loading': isFirstPage, 'central-block-loading-more': !isFirstPage}">
|
||||
<span class="vertical-helper"></span>
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
48
src/ui_ng/lib/src/gridview/grid-view.component.spec.ts
Normal file
48
src/ui_ng/lib/src/gridview/grid-view.component.spec.ts
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
*
|
||||
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this product except in compliance with the License.
|
||||
*
|
||||
* This product may include a number of subcomponents with separate copyright notices
|
||||
* and license terms. Your use of these subcomponents is subject to the terms and
|
||||
* conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
*/
|
||||
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GridViewComponent } from './grid-view.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
|
||||
|
||||
describe('GridViewComponent', () => {
|
||||
let component: GridViewComponent;
|
||||
let fixture: ComponentFixture<GridViewComponent>;
|
||||
|
||||
let config: IServiceConfig = {
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
],
|
||||
declarations: [
|
||||
GridViewComponent,
|
||||
],
|
||||
providers: [{
|
||||
provide: SERVICE_CONFIG, useValue: config }]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GridViewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
246
src/ui_ng/lib/src/gridview/grid-view.component.ts
Normal file
246
src/ui_ng/lib/src/gridview/grid-view.component.ts
Normal file
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
*
|
||||
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
|
||||
* You may not use this product except in compliance with the License.
|
||||
*
|
||||
* This product may include a number of subcomponents with separate copyright notices
|
||||
* and license terms. Your use of these subcomponents is subject to the terms and
|
||||
* conditions of the subcomponent's license, as noted in the LICENSE file.
|
||||
*/
|
||||
|
||||
import { Component, Input, Output, SimpleChanges, ContentChild, ViewChild, ViewChildren,
|
||||
TemplateRef, HostListener, ViewEncapsulation, EventEmitter, AfterViewInit } from '@angular/core';
|
||||
import { CancelablePromise } from '../shared/shared.utils';
|
||||
import { Router, ActivatedRoute, NavigationEnd } from '@angular/router';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { GRIDVIEW_TEMPLATE } from './grid-view.component.html';
|
||||
import { GRIDVIEW_STYLE } from './grid-view.component.css';
|
||||
import { ScrollPosition } from '../service/interface'
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-gridview',
|
||||
template: GRIDVIEW_TEMPLATE,
|
||||
styles: [GRIDVIEW_STYLE],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
/**
|
||||
* Grid view general component.
|
||||
*/
|
||||
export class GridViewComponent implements AfterViewInit {
|
||||
@Input() loading: boolean;
|
||||
@Input() totalCount: number;
|
||||
@Input() currentPage: number;
|
||||
@Input() pageSize: number;
|
||||
@Input() expectScrollPercent = 70;
|
||||
@Input() withAdmiral: boolean;
|
||||
@Input()
|
||||
set items(value: any[]) {
|
||||
let newCardStyles = value.map((d, index) => {
|
||||
if (index < this.cardStyles.length) {
|
||||
return this.cardStyles[index];
|
||||
}
|
||||
return {
|
||||
opacity: '0',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
});
|
||||
this.cardStyles = newCardStyles;
|
||||
this._items = value;
|
||||
}
|
||||
|
||||
@Output() loadNextPageEvent = new EventEmitter<any>();
|
||||
|
||||
@ViewChildren('cardItem') cards: any;
|
||||
@ViewChild('itemsHolder') itemsHolder: any;
|
||||
@ContentChild(TemplateRef) gridItemTmpl: any;
|
||||
|
||||
_items: any[] = [];
|
||||
|
||||
cardStyles: any = [];
|
||||
itemsHolderStyle: any = {};
|
||||
layoutTimeout: any;
|
||||
|
||||
querySub: Subscription;
|
||||
routerSub: Subscription;
|
||||
|
||||
totalItemsCount: number;
|
||||
loadedPages = 0;
|
||||
nextPageLink: string;
|
||||
hidePartialRows = false;
|
||||
loadPagesTimeout: any;
|
||||
|
||||
CurrentScrollPosition: ScrollPosition = {
|
||||
sH: 0,
|
||||
sT: 0,
|
||||
cH: 0
|
||||
};
|
||||
|
||||
preScrollPosition: ScrollPosition = null;
|
||||
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.cards.changes.subscribe(() => {
|
||||
this.throttleLayout();
|
||||
});
|
||||
this.throttleLayout();
|
||||
}
|
||||
|
||||
get items() {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
@HostListener('scroll', ['$event'])
|
||||
onScroll(event: any) {
|
||||
|
||||
this.preScrollPosition = this.CurrentScrollPosition;
|
||||
this.CurrentScrollPosition = {
|
||||
sH: event.target.scrollHeight,
|
||||
sT: event.target.scrollTop,
|
||||
cH: event.target.clientHeight
|
||||
}
|
||||
if (!this.loading
|
||||
&& this.isScrollDown()
|
||||
&& this.isScrollExpectPercent()
|
||||
&& (this.currentPage * this.pageSize < this.totalCount)) {
|
||||
this.loadNextPageEvent.emit();
|
||||
}
|
||||
}
|
||||
|
||||
isScrollDown(): boolean {
|
||||
return this.preScrollPosition.sT < this.CurrentScrollPosition.sT;
|
||||
}
|
||||
|
||||
isScrollExpectPercent(): boolean {
|
||||
return ((this.CurrentScrollPosition.sT + this.CurrentScrollPosition.cH) / this.CurrentScrollPosition.sH) > (this.expectScrollPercent / 100);
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize(event: any) {
|
||||
this.throttleLayout();
|
||||
}
|
||||
|
||||
throttleLayout() {
|
||||
clearTimeout(this.layoutTimeout);
|
||||
this.layoutTimeout = setTimeout(() => {
|
||||
this.layout.call(this);
|
||||
}, 40);
|
||||
}
|
||||
|
||||
get isFirstPage() {
|
||||
return this.currentPage <= 1;
|
||||
}
|
||||
|
||||
layout() {
|
||||
let el = this.itemsHolder.nativeElement;
|
||||
|
||||
let width = el.offsetWidth;
|
||||
let items = el.querySelectorAll('.card-item');
|
||||
let items_count = items.length;
|
||||
if (items_count === 0) {
|
||||
el.height = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
let itemsHeight = [];
|
||||
for (let i = 0; i < items_count; i++) {
|
||||
itemsHeight[i] = items[i].offsetHeight;
|
||||
}
|
||||
|
||||
let height = Math.max.apply(null, itemsHeight);
|
||||
let itemsStyle: CSSStyleDeclaration = window.getComputedStyle(items[0]);
|
||||
|
||||
let minWidthStyle: string = itemsStyle.minWidth;
|
||||
let maxWidthStyle: string = itemsStyle.maxWidth;
|
||||
|
||||
let minWidth = parseInt(minWidthStyle, 10);
|
||||
let maxWidth = parseInt(maxWidthStyle, 10);
|
||||
|
||||
let marginHeight: number =
|
||||
parseInt(itemsStyle.marginTop, 10) + parseInt(itemsStyle.marginBottom, 10);
|
||||
let marginWidth: number =
|
||||
parseInt(itemsStyle.marginLeft, 10) + parseInt(itemsStyle.marginRight, 10);
|
||||
|
||||
let columns = Math.floor(width / (minWidth + marginWidth));
|
||||
|
||||
let columnsToUse = Math.max(Math.min(columns, items_count), 1);
|
||||
let rows = Math.floor(items_count / columnsToUse);
|
||||
let itemWidth = Math.min(Math.floor(width / columnsToUse) - marginWidth, maxWidth);
|
||||
let itemSpacing = columnsToUse === 1 || columns > items_count ? marginWidth :
|
||||
(width - marginWidth - columnsToUse * itemWidth) / (columnsToUse - 1);
|
||||
if (!this.withAdmiral) {
|
||||
// Fixed spacing and margin on standalone mode
|
||||
itemSpacing = marginWidth;
|
||||
itemWidth = minWidth;
|
||||
}
|
||||
|
||||
let visible = items_count;
|
||||
if (this.hidePartialRows && this.totalItemsCount && items_count !== this.totalItemsCount) {
|
||||
visible = rows * columnsToUse;
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (let i = 0; i < visible; i++) {
|
||||
let item = items[i];
|
||||
let itemStyle = window.getComputedStyle(item);
|
||||
|
||||
let left = (i % columnsToUse) * (itemWidth + itemSpacing);
|
||||
let top = Math.floor(count / columnsToUse) * (height + marginHeight);
|
||||
|
||||
// trick to show nice apear animation, where the item is already positioned,
|
||||
// but it will pop out
|
||||
let oldTransform = itemStyle.transform;
|
||||
if (!oldTransform || oldTransform === 'none') {
|
||||
this.cardStyles[i] = {
|
||||
transform: 'translate(' + left + 'px,' + top + 'px) scale(0)',
|
||||
width: itemWidth + 'px',
|
||||
transition: 'none',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
this.throttleLayout();
|
||||
} else {
|
||||
this.cardStyles[i] = {
|
||||
transform: 'translate(' + left + 'px,' + top + 'px) scale(1)',
|
||||
width: itemWidth + 'px',
|
||||
transition: null,
|
||||
overflow: 'hidden'
|
||||
};
|
||||
this.throttleLayout();
|
||||
}
|
||||
|
||||
if (!item.classList.contains('context-selected')) {
|
||||
let itemHeight = itemsHeight[i];
|
||||
if (itemStyle.display === 'none' && itemHeight !== 0) {
|
||||
this.cardStyles[i].display = null;
|
||||
}
|
||||
if (itemHeight !== 0) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = visible; i < items_count; i++) {
|
||||
this.cardStyles[i] = {
|
||||
display: 'none'
|
||||
};
|
||||
}
|
||||
this.itemsHolderStyle = {
|
||||
height: Math.ceil(count / columnsToUse) * (height + marginHeight) + 'px'
|
||||
};
|
||||
}
|
||||
|
||||
onCardEnter(i: number) {
|
||||
this.cardStyles[i].overflow = 'visible';
|
||||
}
|
||||
|
||||
onCardLeave(i: number) {
|
||||
this.cardStyles[i].overflow = 'hidden';
|
||||
}
|
||||
|
||||
trackByFn(index: number, item: any) {
|
||||
return index;
|
||||
}
|
||||
}
|
8
src/ui_ng/lib/src/gridview/index.ts
Normal file
8
src/ui_ng/lib/src/gridview/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Type } from "@angular/core";
|
||||
import { GridViewComponent } from './grid-view.component';
|
||||
|
||||
export * from "./grid-view.component";
|
||||
|
||||
export const HBR_GRIDVIEW_DIRECTIVES: Type<any>[] = [
|
||||
GridViewComponent
|
||||
];
|
@ -25,6 +25,8 @@ import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index';
|
||||
import { CONFIGURATION_DIRECTIVES } from './config/index';
|
||||
import { JOB_LOG_VIEWER_DIRECTIVES } from './job-log-viewer/index';
|
||||
import { PROJECT_POLICY_CONFIG_DIRECTIVES } from './project-policy-config/index';
|
||||
import { HBR_GRIDVIEW_DIRECTIVES } from './gridview/index';
|
||||
import { REPOSITORY_GRIDVIEW_DIRECTIVES } from './repository-gridview';
|
||||
|
||||
import {
|
||||
SystemInfoService,
|
||||
@ -182,7 +184,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
||||
LABEL_DIRECTIVES,
|
||||
CREATE_EDIT_LABEL_DIRECTIVES,
|
||||
LABEL_PIECE_DIRECTIVES
|
||||
LABEL_PIECE_DIRECTIVES,
|
||||
HBR_GRIDVIEW_DIRECTIVES,
|
||||
REPOSITORY_GRIDVIEW_DIRECTIVES,
|
||||
],
|
||||
exports: [
|
||||
LOG_DIRECTIVES,
|
||||
@ -207,7 +211,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
||||
LABEL_DIRECTIVES,
|
||||
CREATE_EDIT_LABEL_DIRECTIVES,
|
||||
LABEL_PIECE_DIRECTIVES
|
||||
LABEL_PIECE_DIRECTIVES,
|
||||
HBR_GRIDVIEW_DIRECTIVES,
|
||||
REPOSITORY_GRIDVIEW_DIRECTIVES,
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
|
@ -2,7 +2,7 @@ export * from './harbor-library.module';
|
||||
export * from './service.config';
|
||||
export * from './service/index';
|
||||
export * from './error-handler/index';
|
||||
//export * from './utils';
|
||||
// export * from './utils';
|
||||
export * from './log/index';
|
||||
export * from './filter/index';
|
||||
export * from './endpoint/index';
|
||||
@ -23,3 +23,5 @@ export * from './channel/index';
|
||||
export * from './project-policy-config/index';
|
||||
export * from './label/index';
|
||||
export * from './create-edit-label';
|
||||
export * from './gridview/index';
|
||||
export * from './repository-gridview/index';
|
||||
|
8
src/ui_ng/lib/src/repository-gridview/index.ts
Normal file
8
src/ui_ng/lib/src/repository-gridview/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Type } from "@angular/core";
|
||||
import { RepositoryGridviewComponent } from './repository-gridview.component';
|
||||
|
||||
export * from "./repository-gridview.component";
|
||||
|
||||
export const REPOSITORY_GRIDVIEW_DIRECTIVES: Type<any>[] = [
|
||||
RepositoryGridviewComponent
|
||||
];
|
@ -0,0 +1,76 @@
|
||||
export const REPOSITORY_GRIDVIEW_STYLE = `
|
||||
.rightPos{
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
right: 35px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.filter-divider {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 2px;
|
||||
background-color: #cccccc;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 12px;
|
||||
position: relative;
|
||||
top: 9px;
|
||||
margin-right: 6px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.card-block {
|
||||
margin-top: 24px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.form-group > label {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
|
||||
.card-media-block {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-media-block > img {
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
}
|
||||
.card-media-description {
|
||||
height: 45px;
|
||||
}
|
||||
.card-media-description > p {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.card-text {
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.card-block {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.list-img > img {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
`;
|
@ -0,0 +1,92 @@
|
||||
export const REPOSITORY_GRIDVIEW_TEMPLATE = `
|
||||
<div>
|
||||
<div class="row" style="position:relative;">
|
||||
<div class="toolbar">
|
||||
<div class="row flex-items-xs-right option-right rightPos">
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)" [currentValue]="lastFilteredRepoName"></hbr-filter>
|
||||
<span class="card-btn" (click)="showCard(true)" (mouseenter) ="mouseEnter('card') " (mouseleave) ="mouseLeave('card')">
|
||||
<clr-icon [ngClass]="{'is-highlight': isCardView || isHovering('card') }" shape="view-cards"></clr-icon>
|
||||
</span>
|
||||
<span class="list-btn" (click)="showCard(false)" (mouseenter) ="mouseEnter('list') " (mouseleave) ="mouseLeave('list')">
|
||||
<clr-icon [ngClass]="{'is-highlight': !isCardView || isHovering('list') }"shape="view-list"></clr-icon>
|
||||
</span>
|
||||
<span class="filter-divider"></span>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!isCardView" class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid (clrDgRefresh)="clrLoad($event)" [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
|
||||
<clr-dg-action-bar>
|
||||
<button type="button" class="btn btn-sm btn-secondary" (click)="deleteRepos(selectedRow)" [disabled]="!(selectedRow.length && hasProjectAdminRole)"><clr-icon shape="times" size="16"></clr-icon> {{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="tagsCountComparator">{{'REPOSITORY.TAGS_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]="r">
|
||||
<clr-dg-cell><a href="javascript:void(0)" (click)="watchRepoClickEvt(r)"><span *ngIf="withAdmiral" class="list-img"><img [src]="getImgLink(r)"/></span>{{r.name}}</a></clr-dg-cell>
|
||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>
|
||||
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
<hbr-gridview *ngIf="isCardView" #gridView style="position:relative;" [items]="repositories" [loading]="loading" [pageSize]="pageSize"
|
||||
[currentPage]="currentPage" [totalCount]="totalCount" [expectScrollPercent]="90" [withAdmiral]="withAdmiral" (loadNextPageEvent)="loadNextPage()">
|
||||
<ng-template let-item="item">
|
||||
<a class="card clickable" (click)="watchRepoClickEvt(item)">
|
||||
<div class="card-header">
|
||||
<div class="card-media-block">
|
||||
<img *ngIf="withAdmiral" [src]="getImgLink(item)"/>
|
||||
<div class="card-media-description">
|
||||
<span class="card-media-title">
|
||||
{{item.name}}
|
||||
</span>
|
||||
<p class="card-media-text">{{registryUrl}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<div class="card-text">
|
||||
{{getRepoDescrition(item)}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{'REPOSITORY.TAGS_COUNT' | translate}}</label>
|
||||
<div>{{item.tags_count}}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{'REPOSITORY.TAGS_COUNT' | translate}}</label>
|
||||
<div>{{item.pull_count}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<clr-dropdown [clrCloseMenuOnItemClick]="false">
|
||||
<button *ngIf="withAdmiral" type="button" class="btn btn-link" (click)="provisionItemEvent($event, item)">{{'REPOSITORY.DEPLOY' | translate}}</button>
|
||||
<button type="button" class="btn btn-link" (click)="$event.stopPropagation()" clrDropdownTrigger>
|
||||
{{'REPOSITORY.ACTION' | translate}}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<clr-dropdown-menu clrPosition="top-left" *clrIfOpen>
|
||||
<button *ngIf="withAdmiral" type="button" class="btn btn-link" clrDropdownItem (click)="itemAddInfoEvent($event, item)">
|
||||
{{'REPOSITORY.ADDITIONAL_INFO' | translate}}
|
||||
</button>
|
||||
<button type="button" class="btn btn-link" clrDropdownItem (click)="deleteItemEvent($event, item)">
|
||||
{{'REPOSITORY.DELETE' | translate}}
|
||||
</button>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
</div>
|
||||
</a>
|
||||
</ng-template>
|
||||
</hbr-gridview>
|
||||
<confirmation-dialog #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,175 @@
|
||||
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { RepositoryGridviewComponent } from './repository-gridview.component';
|
||||
import { TagComponent } from '../tag/tag.component';
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||
import { HBR_GRIDVIEW_DIRECTIVES } from '../gridview/index'
|
||||
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index';
|
||||
import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
|
||||
import { JobLogViewerComponent } from '../job-log-viewer/index';
|
||||
import {LabelPieceComponent} from "../label-piece/label-piece.component";
|
||||
|
||||
import { click } from '../utils';
|
||||
|
||||
describe('RepositoryComponentGridview (inline template)', () => {
|
||||
|
||||
let compRepo: RepositoryGridviewComponent;
|
||||
let fixtureRepo: ComponentFixture<RepositoryGridviewComponent>;
|
||||
let repositoryService: RepositoryService;
|
||||
let tagService: TagService;
|
||||
let systemInfoService: SystemInfoService;
|
||||
|
||||
let spyRepos: jasmine.Spy;
|
||||
let spySystemInfo: jasmine.Spy;
|
||||
|
||||
let mockSystemInfo: SystemInfo = {
|
||||
"with_notary": true,
|
||||
"with_admiral": false,
|
||||
"admiral_endpoint": "NA",
|
||||
"auth_mode": "db_auth",
|
||||
"registry_url": "10.112.122.56",
|
||||
"project_creation_restriction": "everyone",
|
||||
"self_registration": true,
|
||||
"has_ca_root": false,
|
||||
"harbor_version": "v1.1.1-rc1-160-g565110d"
|
||||
};
|
||||
|
||||
let mockRepoData: RepositoryItem[] = [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "library/busybox",
|
||||
"project_id": 1,
|
||||
"description": "asdfsadf",
|
||||
"pull_count": 0,
|
||||
"star_count": 0,
|
||||
"tags_count": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "library/nginx",
|
||||
"project_id": 1,
|
||||
"description": "asdf",
|
||||
"pull_count": 0,
|
||||
"star_count": 0,
|
||||
"tags_count": 1
|
||||
}
|
||||
];
|
||||
|
||||
let mockRepo: Repository = {
|
||||
metadata: {xTotalCount: 2},
|
||||
data: mockRepoData
|
||||
};
|
||||
|
||||
let mockTagData: Tag[] = [
|
||||
{
|
||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||
"name": "1.11.5",
|
||||
"size": "2049",
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"docker_version": "1.12.3",
|
||||
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
|
||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||
"signature": null,
|
||||
"labels": []
|
||||
}
|
||||
];
|
||||
|
||||
let config: IServiceConfig = {
|
||||
repositoryBaseEndpoint: '/api/repository/testing',
|
||||
systemInfoEndpoint: '/api/systeminfo/testing',
|
||||
targetBaseEndpoint: '/api/tag/testing'
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule,
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
RepositoryGridviewComponent,
|
||||
TagComponent,
|
||||
LabelPieceComponent,
|
||||
ConfirmationDialogComponent,
|
||||
FilterComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||
INLINE_ALERT_DIRECTIVES,
|
||||
HBR_GRIDVIEW_DIRECTIVES,
|
||||
JobLogViewerComponent
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: RepositoryService, useClass: RepositoryDefaultService },
|
||||
{ provide: TagService, useClass: TagDefaultService },
|
||||
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixtureRepo = TestBed.createComponent(RepositoryGridviewComponent);
|
||||
compRepo = fixtureRepo.componentInstance;
|
||||
compRepo.projectId = 1;
|
||||
compRepo.hasProjectAdminRole = true;
|
||||
|
||||
repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService);
|
||||
systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService);
|
||||
|
||||
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
|
||||
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
|
||||
fixtureRepo.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(compRepo).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should load and render data', async(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
fixtureRepo.whenStable().then(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell'));
|
||||
expect(deRepo).toBeTruthy();
|
||||
let elRepo: HTMLElement = deRepo.nativeElement;
|
||||
expect(elRepo).toBeTruthy();
|
||||
expect(elRepo.textContent).toEqual('library/busybox');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should filter data by keyword', async(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
fixtureRepo.whenStable().then(() => {
|
||||
fixtureRepo.detectChanges();
|
||||
|
||||
compRepo.doSearchRepoNames('nginx');
|
||||
fixtureRepo.detectChanges();
|
||||
let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell'));
|
||||
expect(de).toBeTruthy();
|
||||
expect(de.length).toEqual(1);
|
||||
let el: HTMLElement = de[0].nativeElement;
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent).toEqual('library/nginx');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
@ -0,0 +1,404 @@
|
||||
import { Component, Input, Output, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import {Observable} from "rxjs/Observable";
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Comparator, State } from 'clarity-angular';
|
||||
|
||||
import { REPOSITORY_GRIDVIEW_TEMPLATE } from './repository-gridview.component.html';
|
||||
import { REPOSITORY_GRIDVIEW_STYLE } from './repository-gridview.component.css';
|
||||
import { Repository, SystemInfo, SystemInfoService, RepositoryService, RequestQueryParams, RepositoryItem, TagService } from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { toPromise, CustomComparator , DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting} from '../utils';
|
||||
import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const';
|
||||
import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component';
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
import { Tag, CardItemEvent } from '../service/interface';
|
||||
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
|
||||
import { GridViewComponent } from '../gridview/grid-view.component'
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-repository-gridview',
|
||||
template: REPOSITORY_GRIDVIEW_TEMPLATE,
|
||||
styles: [REPOSITORY_GRIDVIEW_STYLE],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class RepositoryGridviewComponent implements OnChanges, OnInit {
|
||||
signedCon: {[key: string]: any | string[]} = {};
|
||||
@Input() projectId: number;
|
||||
@Input() projectName = 'unknown';
|
||||
@Input() urlPrefix: string;
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Output() repoClickEvent = new EventEmitter<RepositoryItem>();
|
||||
@Output() repoProvisionEvent = new EventEmitter<RepositoryItem>();
|
||||
@Output() addInfoEvent = new EventEmitter<RepositoryItem>();
|
||||
|
||||
lastFilteredRepoName: string;
|
||||
repositories: RepositoryItem[] = [];
|
||||
repositoriesCopy: RepositoryItem[] = [];
|
||||
systemInfo: SystemInfo;
|
||||
selectedRow: RepositoryItem[] = [];
|
||||
loading = true;
|
||||
|
||||
isCardView: boolean;
|
||||
cardHover = false;
|
||||
listHover = false;
|
||||
|
||||
batchDelectionInfos: BatchInfo[] = [];
|
||||
pullCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('pull_count', 'number');
|
||||
tagsCountComparator: Comparator<RepositoryItem> = new CustomComparator<RepositoryItem>('tags_count', 'number');
|
||||
|
||||
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||
currentPage = 1;
|
||||
totalCount = 0;
|
||||
currentState: State;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
|
||||
@ViewChild('gridView')
|
||||
gridView: GridViewComponent;
|
||||
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private translateService: TranslateService,
|
||||
private repositoryService: RepositoryService,
|
||||
private systemInfoService: SystemInfoService,
|
||||
private tagService: TagService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private router: Router) { }
|
||||
|
||||
public get registryUrl(): string {
|
||||
return this.systemInfo ? this.systemInfo.registry_url : '';
|
||||
}
|
||||
|
||||
public get withAdmiral(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_admiral : false;
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['projectId'] && changes['projectId'].currentValue) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.withAdmiral) {
|
||||
this.isCardView = true;
|
||||
} else {
|
||||
this.isCardView = false;
|
||||
}
|
||||
// Get system info for tag views
|
||||
toPromise<SystemInfo>(this.systemInfoService.getSystemInfo())
|
||||
.then(systemInfo => this.systemInfo = systemInfo)
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
|
||||
this.lastFilteredRepoName = '';
|
||||
}
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.REPOSITORY &&
|
||||
message.state === ConfirmationState.CONFIRMED) {
|
||||
|
||||
let promiseLists: any[] = [];
|
||||
let repoNames: string[] = message.data.split(',');
|
||||
|
||||
repoNames.forEach(repoName => {
|
||||
promiseLists.push(this.delOperate(repoName));
|
||||
});
|
||||
|
||||
Promise.all(promiseLists).then((item) => {
|
||||
this.selectedRow = [];
|
||||
this.refresh();
|
||||
let st: State = this.getStateAfterDeletion();
|
||||
if (!st) {
|
||||
this.refresh();
|
||||
} else {
|
||||
this.clrLoad(st);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
delOperate(repoName: string) {
|
||||
let findedList = this.batchDelectionInfos.find(data => data.name === repoName);
|
||||
if (this.signedCon[repoName].length !== 0) {
|
||||
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
|
||||
this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
|
||||
});
|
||||
} else {
|
||||
return toPromise<number>(this.repositoryService
|
||||
.deleteRepository(repoName))
|
||||
.then(
|
||||
response => {
|
||||
this.translateService.get('BATCH.DELETED_SUCCESS').subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res);
|
||||
});
|
||||
}).catch(error => {
|
||||
if (error.status === "412") {
|
||||
Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'),
|
||||
this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res[0], false, true, res[1]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
|
||||
findedList = BathInfoChanges(findedList, res, false, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
doSearchRepoNames(repoName: string) {
|
||||
this.lastFilteredRepoName = repoName;
|
||||
this.currentPage = 1;
|
||||
let st: State = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
this.clrLoad(st);
|
||||
}
|
||||
|
||||
saveSignatures(event: {[key: string]: string[]}): void {
|
||||
Object.assign(this.signedCon, event);
|
||||
}
|
||||
|
||||
deleteRepos(repoLists: RepositoryItem[]) {
|
||||
if (repoLists && repoLists.length) {
|
||||
let repoNames: string[] = [];
|
||||
this.batchDelectionInfos = [];
|
||||
let repArr: any[] = [];
|
||||
|
||||
repoLists.forEach(repo => {
|
||||
repoNames.push(repo.name);
|
||||
let initBatchMessage = new BatchInfo();
|
||||
initBatchMessage.name = repo.name;
|
||||
this.batchDelectionInfos.push(initBatchMessage);
|
||||
|
||||
if (!this.signedCon[repo.name]) {
|
||||
repArr.push(this.getTagInfo(repo.name));
|
||||
}
|
||||
});
|
||||
|
||||
Promise.all(repArr).then(() => {
|
||||
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', '', repoNames.join(','), 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getTagInfo(repoName: string): Promise<void> {
|
||||
this.signedCon[repoName] = [];
|
||||
return toPromise<Tag[]>(this.tagService
|
||||
.getTags(repoName))
|
||||
.then(items => {
|
||||
items.forEach((t: Tag) => {
|
||||
if (t.signature !== null) {
|
||||
this.signedCon[repoName].push(t.name);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
signedDataSet(repoName: string): void {
|
||||
let signature = '';
|
||||
if (this.signedCon[repoName].length === 0) {
|
||||
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL);
|
||||
return;
|
||||
}
|
||||
signature = this.signedCon[repoName].join(',');
|
||||
this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO_SIGNED', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO_SIGNED', ConfirmationButtons.CLOSE);
|
||||
}
|
||||
|
||||
confirmationDialogSet(summaryTitle: string, signature: string, repoName: string, summaryKey: string, button: ConfirmationButtons): void {
|
||||
this.translateService.get(summaryKey,
|
||||
{
|
||||
repoName: repoName,
|
||||
signedImages: signature,
|
||||
})
|
||||
.subscribe((res: string) => {
|
||||
summaryKey = res;
|
||||
let message = new ConfirmationMessage(
|
||||
summaryTitle,
|
||||
summaryKey,
|
||||
repoName,
|
||||
repoName,
|
||||
ConfirmationTargets.REPOSITORY,
|
||||
button);
|
||||
this.confirmationDialog.open(message);
|
||||
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 5000);
|
||||
});
|
||||
}
|
||||
|
||||
provisionItemEvent(evt: any, repo: RepositoryItem): void {
|
||||
evt.stopPropagation();
|
||||
this.repoProvisionEvent.emit(repo);
|
||||
}
|
||||
deleteItemEvent(evt: any, item: RepositoryItem): void {
|
||||
evt.stopPropagation();
|
||||
this.deleteRepos([item]);
|
||||
}
|
||||
itemAddInfoEvent(evt: any, repo: RepositoryItem): void {
|
||||
evt.stopPropagation();
|
||||
this.addInfoEvent.emit(repo);
|
||||
}
|
||||
selectedChange(): void {
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 2000);
|
||||
}
|
||||
refresh() {
|
||||
this.doSearchRepoNames('');
|
||||
}
|
||||
|
||||
loadNextPage() {
|
||||
if (this.currentPage * this.pageSize >= this.totalCount) {
|
||||
return
|
||||
}
|
||||
this.currentPage = this.currentPage + 1;
|
||||
|
||||
// Pagination
|
||||
let params: RequestQueryParams = new RequestQueryParams();
|
||||
params.set("page", '' + this.currentPage);
|
||||
params.set("page_size", '' + this.pageSize);
|
||||
|
||||
this.loading = true;
|
||||
|
||||
toPromise<Repository>(this.repositoryService.getRepositories(
|
||||
this.projectId,
|
||||
this.lastFilteredRepoName,
|
||||
params))
|
||||
.then((repo: Repository) => {
|
||||
this.totalCount = repo.metadata.xTotalCount;
|
||||
this.repositoriesCopy = repo.data;
|
||||
this.signedCon = {};
|
||||
// Do filtering and sorting
|
||||
this.repositoriesCopy = doFiltering<RepositoryItem>(this.repositoriesCopy, this.currentState);
|
||||
this.repositoriesCopy = doSorting<RepositoryItem>(this.repositoriesCopy, this.currentState);
|
||||
this.repositories = this.repositories.concat(this.repositoriesCopy);
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
clrLoad(state: State): void {
|
||||
this.selectedRow = [];
|
||||
// Keep it for future filtering and sorting
|
||||
this.currentState = state;
|
||||
|
||||
let pageNumber: number = calculatePage(state);
|
||||
if (pageNumber <= 0) { pageNumber = 1; }
|
||||
|
||||
// Pagination
|
||||
let params: RequestQueryParams = new RequestQueryParams();
|
||||
params.set("page", '' + pageNumber);
|
||||
params.set("page_size", '' + this.pageSize);
|
||||
|
||||
this.loading = true;
|
||||
|
||||
toPromise<Repository>(this.repositoryService.getRepositories(
|
||||
this.projectId,
|
||||
this.lastFilteredRepoName,
|
||||
params))
|
||||
.then((repo: Repository) => {
|
||||
this.totalCount = repo.metadata.xTotalCount;
|
||||
this.repositories = repo.data;
|
||||
|
||||
this.signedCon = {};
|
||||
// Do filtering and sorting
|
||||
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
|
||||
this.repositories = doSorting<RepositoryItem>(this.repositories, state);
|
||||
|
||||
this.loading = false;
|
||||
})
|
||||
.catch(error => {
|
||||
this.loading = false;
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
|
||||
// Force refresh view
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 5000);
|
||||
}
|
||||
|
||||
getStateAfterDeletion(): State {
|
||||
let total: number = this.totalCount - 1;
|
||||
if (total <= 0) { return null; }
|
||||
|
||||
let totalPages: number = Math.ceil(total / this.pageSize);
|
||||
let targetPageNumber: number = this.currentPage;
|
||||
|
||||
if (this.currentPage > totalPages) {
|
||||
targetPageNumber = totalPages; // Should == currentPage -1
|
||||
}
|
||||
|
||||
let st: State = this.currentState;
|
||||
if (!st) {
|
||||
st = { page: {} };
|
||||
}
|
||||
st.page.size = this.pageSize;
|
||||
st.page.from = (targetPageNumber - 1) * this.pageSize;
|
||||
st.page.to = targetPageNumber * this.pageSize - 1;
|
||||
|
||||
return st;
|
||||
}
|
||||
|
||||
watchRepoClickEvt(repo: RepositoryItem) {
|
||||
this.repoClickEvent.emit(repo);
|
||||
}
|
||||
|
||||
getImgLink(repo: RepositoryItem): string {
|
||||
return '/container-image-icons?container-image=' + repo.name
|
||||
}
|
||||
|
||||
getRepoDescrition(repo: RepositoryItem): string {
|
||||
if (repo && repo.description) {
|
||||
return repo.description;
|
||||
}
|
||||
return "No description for this repo. You can add it to this repository."
|
||||
}
|
||||
showCard(cardView: boolean) {
|
||||
if (this.isCardView === cardView) {
|
||||
return
|
||||
}
|
||||
this.isCardView = cardView;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
mouseEnter(itemName: string) {
|
||||
if (itemName === 'card') {
|
||||
this.cardHover = true;
|
||||
} else {
|
||||
this.listHover = true;
|
||||
}
|
||||
}
|
||||
|
||||
mouseLeave(itemName: string) {
|
||||
if (itemName === 'card') {
|
||||
this.cardHover = false;
|
||||
} else {
|
||||
this.listHover = false;
|
||||
}
|
||||
}
|
||||
|
||||
isHovering(itemName: string) {
|
||||
if (itemName === 'card') {
|
||||
return this.cardHover;
|
||||
} else {
|
||||
return this.listHover;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ export const REPOSITORY_LISTVIEW_TEMPLATE = `
|
||||
<clr-dg-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]="r">
|
||||
<clr-dg-cell><a href="javascript:void(0)" (click)="gotoLink(projectId || r.project_id, r.name || r.repository_name)">{{r.name}}</a></clr-dg-cell>
|
||||
<clr-dg-cell><a href="javascript:void(0)" (click)="watchRepoClickEvt(r)">{{r.name}}</a></clr-dg-cell>
|
||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
|
@ -34,7 +34,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Tag, TagClickEvent } from '../service/interface';
|
||||
import { Tag } from '../service/interface';
|
||||
|
||||
import { State } from "clarity-angular";
|
||||
import {
|
||||
@ -60,14 +60,14 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||
@Output() repoClickEvent = new EventEmitter<RepositoryItem>();
|
||||
|
||||
lastFilteredRepoName: string;
|
||||
repositories: RepositoryItem[];
|
||||
systemInfo: SystemInfo;
|
||||
selectedRow: RepositoryItem[] = [];
|
||||
|
||||
loading: boolean = true;
|
||||
loading = true;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
@ -279,19 +279,15 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||
this.doSearchRepoNames('');
|
||||
}
|
||||
|
||||
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
|
||||
this.tagClickEvent.emit(tagClickEvt);
|
||||
}
|
||||
|
||||
clrLoad(state: State): void {
|
||||
this.selectedRow = [];
|
||||
//Keep it for future filtering and sorting
|
||||
// Keep it for future filtering and sorting
|
||||
this.currentState = state;
|
||||
|
||||
let pageNumber: number = calculatePage(state);
|
||||
if (pageNumber <= 0) { pageNumber = 1; }
|
||||
|
||||
//Pagination
|
||||
// Pagination
|
||||
let params: RequestQueryParams = new RequestQueryParams();
|
||||
params.set("page", '' + pageNumber);
|
||||
params.set("page_size", '' + this.pageSize);
|
||||
@ -307,7 +303,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||
this.repositories = repo.data;
|
||||
|
||||
this.signedCon = {};
|
||||
//Do filtering and sorting
|
||||
// Do filtering and sorting
|
||||
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
|
||||
this.repositories = doSorting<RepositoryItem>(this.repositories, state);
|
||||
|
||||
@ -318,7 +314,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
|
||||
//Force refresh view
|
||||
// Force refresh view
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 5000);
|
||||
}
|
||||
@ -331,7 +327,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||
let targetPageNumber: number = this.currentPage;
|
||||
|
||||
if (this.currentPage > totalPages) {
|
||||
targetPageNumber = totalPages;//Should == currentPage -1
|
||||
targetPageNumber = totalPages; // Should == currentPage -1
|
||||
}
|
||||
|
||||
let st: State = this.currentState;
|
||||
@ -344,8 +340,8 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||
|
||||
return st;
|
||||
}
|
||||
public gotoLink(projectId: number, repoName: string): void {
|
||||
let linkUrl = [this.router.url, repoName];
|
||||
this.router.navigate(linkUrl);
|
||||
|
||||
watchRepoClickEvt(repo: RepositoryItem) {
|
||||
this.repoClickEvent.emit(repo);
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>();
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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); });
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
||||
provide: MissingTranslationHandler,
|
||||
useClass: MyMissingTranslationHandler
|
||||
}
|
||||
})
|
||||
}),
|
||||
],
|
||||
exports: [
|
||||
CommonModule,
|
||||
@ -65,9 +65,8 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
||||
CookieModule,
|
||||
ClipboardModule,
|
||||
ClarityModule,
|
||||
TranslateModule
|
||||
TranslateModule,
|
||||
],
|
||||
providers: [CookieService]
|
||||
})
|
||||
|
||||
export class SharedModule { }
|
||||
|
@ -15,7 +15,7 @@ import { NgForm } from '@angular/forms';
|
||||
import { httpStatusCode, AlertType } from './shared.const';
|
||||
/**
|
||||
* To handle the error message body
|
||||
*
|
||||
*
|
||||
* @export
|
||||
* @returns {string}
|
||||
*/
|
||||
@ -24,7 +24,7 @@ export const errorHandler = function (error: any): string {
|
||||
return "UNKNOWN_ERROR";
|
||||
}
|
||||
if (!(error.statusCode || error.status)) {
|
||||
//treat as string message
|
||||
// treat as string message
|
||||
return '' + error;
|
||||
} else {
|
||||
switch (error.statusCode || error.status) {
|
||||
@ -46,4 +46,28 @@ export const errorHandler = function (error: any): string {
|
||||
return "UNKNOWN_ERROR";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> {
|
||||
|
||||
constructor(promise: Promise<T>) {
|
||||
this.wrappedPromise = new Promise((resolve, reject) => {
|
||||
promise.then((val) =>
|
||||
this.isCanceled ? reject({isCanceled: true}) : resolve(val)
|
||||
);
|
||||
promise.catch((error) =>
|
||||
this.isCanceled ? reject({isCanceled: true}) : reject(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private wrappedPromise: Promise<T>;
|
||||
private isCanceled: boolean;
|
||||
getPromise(): Promise<T> {
|
||||
return this.wrappedPromise;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.isCanceled = true;
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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> {{'VULNERABILITY.SCAN_NOW' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon> {{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<clr-dropdown>
|
||||
<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> {{'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}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -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');
|
||||
|
@ -1,3 +1,5 @@
|
||||
<div style="margin-top: 24px;">
|
||||
<hbr-repository-listview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-listview>
|
||||
<hbr-repository-gridview [projectId]="projectId" [projectName]="projectName" [hasSignedIn]="hasSignedIn"
|
||||
[hasProjectAdminRole]="hasProjectAdminRole"
|
||||
(repoClickEvent)="watchRepoClickEvent($event)"></hbr-repository-gridview>
|
||||
</div>
|
@ -17,7 +17,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Project } from '../project/project';
|
||||
import { SessionService } from '../shared/session.service';
|
||||
|
||||
import { TagClickEvent } from 'harbor-ui';
|
||||
import { TagClickEvent, RepositoryItem } from 'harbor-ui';
|
||||
|
||||
@Component({
|
||||
selector: 'repository',
|
||||
@ -47,8 +47,8 @@ export class RepositoryPageComponent implements OnInit {
|
||||
this.hasSignedIn = this.session.getCurrentUser() !== null;
|
||||
}
|
||||
|
||||
watchTagClickEvent(tagEvt: TagClickEvent): void {
|
||||
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name];
|
||||
watchRepoClickEvent(repoEvt: RepositoryItem): void {
|
||||
let linkUrl = ['harbor', 'projects', repoEvt.project_id, 'repositories', repoEvt.name];
|
||||
this.router.navigate(linkUrl);
|
||||
}
|
||||
};
|
||||
|
@ -1,3 +1,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>
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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"]);
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -364,7 +364,11 @@
|
||||
"DELETED_TAG_SUCCESS": "Tag supprimé avec succés.",
|
||||
"COPY": "Copier",
|
||||
"NOTARY_IS_UNDETERMINED": "Ne peut pas déterminer la signature de ce tag.",
|
||||
"PLACEHOLDER": "Nous ne trouvons aucun dépôt !"
|
||||
"PLACEHOLDER": "Nous ne trouvons aucun dépôt !",
|
||||
"IMAGE": "Images",
|
||||
"ACTION": "ACTION",
|
||||
"DEPLOY": "DEPLOY",
|
||||
"ADDITIONAL_INFO": "Add Additional Info"
|
||||
},
|
||||
"ALERT": {
|
||||
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"
|
||||
|
@ -404,6 +404,7 @@
|
||||
"REPOSITORIES": "镜像仓库",
|
||||
"OF": "共计",
|
||||
"ITEMS": "条记录",
|
||||
"NO_ITEMS": "没有记录",
|
||||
"POP_REPOS": "受欢迎的镜像仓库",
|
||||
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。",
|
||||
"DELETED_TAG_SUCCESS": "成功删除镜像标签。",
|
||||
@ -415,7 +416,11 @@
|
||||
"IMAGE": "镜像",
|
||||
"LABELS": "标签",
|
||||
"ADD_TO_IMAGE": "添加标签到此镜像",
|
||||
"ADD_LABELS": "添加标签"
|
||||
"ADD_LABELS": "添加标签",
|
||||
"FILTER_BY_LABEL": "过滤标签",
|
||||
"ACTION": "操作",
|
||||
"DEPLOY": "部署",
|
||||
"ADDITIONAL_INFO": "添加信息"
|
||||
},
|
||||
"ALERT": {
|
||||
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
|
||||
@ -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": "标签",
|
||||
|
@ -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
|
||||
|
@ -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='']
|
||||
|
@ -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
|
29
tests/resources/Harbor-Pages/LDAP-Mode.robot
Normal file
29
tests/resources/Harbor-Pages/LDAP-Mode.robot
Normal 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
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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')]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user