mirror of
https://github.com/goharbor/harbor
synced 2025-04-12 17:46:12 +00:00
Merge branch 'master' into job_service
This commit is contained in:
commit
d1899c840d
|
@ -2,57 +2,40 @@
|
||||||
|
|
||||||
## Introduction
|
## 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
|
## Prerequisites
|
||||||
|
|
||||||
- Kubernetes cluster 1.8+ with Beta APIs enabled
|
- Kubernetes cluster 1.8+ with Beta APIs enabled
|
||||||
- Kubernetes Ingress Controller is enabled
|
- Kubernetes Ingress Controller is enabled
|
||||||
- kubectl CLI 1.8+
|
- kubectl CLI 1.8+
|
||||||
- PV provisioner support in the underlying infrastructure
|
- Helm CLI 2.8.0+
|
||||||
|
|
||||||
## Setup a Kubernetes cluster
|
## Setup a Kubernetes cluster
|
||||||
|
|
||||||
You can use any tools to setup a K8s 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.
|
In this guide, we use [minikube](https://github.com/kubernetes/minikube) to setup a K8s cluster as the dev/test env.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start minikube
|
# Start minikube
|
||||||
minikube start --vm-driver=none
|
minikube start --vm-driver=none
|
||||||
# Enable Ingress Controller
|
# Enable Ingress Controller
|
||||||
minikube addons enable ingress
|
minikube addons enable ingress
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing the Chart
|
## Installing the Chart
|
||||||
|
|
||||||
First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm.
|
First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm.
|
||||||
```bash
|
```bash
|
||||||
helm init --canary-image
|
helm init
|
||||||
```
|
```
|
||||||
|
|
||||||
Download Harbor helm chart code.
|
Download Harbor helm chart code.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/vmware/harbor
|
git clone https://github.com/vmware/harbor
|
||||||
cd harbor/contrib/helm/harbor
|
cd contrib/helm/harbor
|
||||||
```
|
```
|
||||||
|
Download external dependent charts required by Harbor chart.
|
||||||
### 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
|
```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
|
### Secure Registry Mode
|
||||||
|
|
||||||
By default this chart will generate a root CA and SSL certificate for your Harbor.
|
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.
|
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`:
|
Install the Harbor helm chart with a release name `my-release`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
helm install . --debug --name my-release --set externalDomain=harbor.my.domain
|
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**.
|
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 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`
|
> **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
|
## Uninstalling the Chart
|
||||||
|
|
||||||
To uninstall/delete the `my-release` deployment:
|
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.emailIdentity` | | "" |
|
||||||
| `adminserver.key` | adminsever key | `not-a-secure-key` |
|
| `adminserver.key` | adminsever key | `not-a-secure-key` |
|
||||||
| `adminserver.emailPwd` | password for email | `not-a-secure-password` |
|
| `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.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 |
|
| `adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml |
|
||||||
| **Jobservice** |
|
| **Jobservice** |
|
||||||
|
|
|
@ -29,17 +29,16 @@ data:
|
||||||
WITH_NOTARY: "{{ .Values.notary.enabled }}"
|
WITH_NOTARY: "{{ .Values.notary.enabled }}"
|
||||||
LOG_LEVEL: "info"
|
LOG_LEVEL: "info"
|
||||||
IMAGE_STORE_PATH: "/" # This is a temporary hack.
|
IMAGE_STORE_PATH: "/" # This is a temporary hack.
|
||||||
AUTH_MODE: "db_auth"
|
AUTH_MODE: "{{ .Values.adminserver.authenticationMode }}"
|
||||||
SELF_REGISTRATION: "on"
|
SELF_REGISTRATION: "{{ .Values.adminserver.selfRegistration }}"
|
||||||
LDAP_URL: "ldaps://ldapserver"
|
LDAP_URL: "{{ .Values.adminserver.ldap.url }}"
|
||||||
LDAP_SEARCH_DN: ""
|
LDAP_SEARCH_DN: "{{ .Values.adminserver.ldap.searchDN }}"
|
||||||
LDAP_BASE_DN: ""
|
LDAP_BASE_DN: "{{ .Values.adminserver.ldap.baseDN }}"
|
||||||
LDAP_FILTER: "(objectClass=person)"
|
LDAP_FILTER: "{{ .Values.adminserver.ldap.filter }}"
|
||||||
LDAP_UID: "uid"
|
LDAP_UID: "{{ .Values.adminserver.ldap.uid }}"
|
||||||
LDAP_SCOPE: "2"
|
LDAP_SCOPE: "{{ .Values.adminserver.ldap.scope }}"
|
||||||
LDAP_TIMEOUT: "5"
|
LDAP_TIMEOUT: "{{ .Values.adminserver.ldap.timeout }}"
|
||||||
LDAP_TIMEOUT: "5"
|
LDAP_VERIFY_CERT: "{{ .Values.adminserver.ldap.verifyCert }}"
|
||||||
LDAP_VERIFY_CERT: "True"
|
|
||||||
DATABASE_TYPE: "mysql"
|
DATABASE_TYPE: "mysql"
|
||||||
PROJECT_CREATION_RESTRICTION: "everyone"
|
PROJECT_CREATION_RESTRICTION: "everyone"
|
||||||
VERIFY_REMOTE_CERT: "off"
|
VERIFY_REMOTE_CERT: "off"
|
||||||
|
|
|
@ -9,7 +9,7 @@ type: Opaque
|
||||||
data:
|
data:
|
||||||
secretKey: {{ .Values.secretKey | b64enc | quote }}
|
secretKey: {{ .Values.secretKey | b64enc | quote }}
|
||||||
EMAIL_PWD: {{ .Values.adminserver.emailPwd | 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 }}
|
MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }}
|
||||||
JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }}
|
JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }}
|
||||||
UI_SECRET: {{ .Values.ui.secret | b64enc | quote }}
|
UI_SECRET: {{ .Values.ui.secret | b64enc | quote }}
|
||||||
|
|
|
@ -65,7 +65,18 @@ adminserver:
|
||||||
emailIdentity: ""
|
emailIdentity: ""
|
||||||
emailInsecure: "False"
|
emailInsecure: "False"
|
||||||
emailPwd: not-a-secure-password
|
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
|
## Persist data to a persistent volume
|
||||||
volumes:
|
volumes:
|
||||||
config:
|
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:
|
options:
|
||||||
syslog-address: "tcp://127.0.0.1:1514"
|
syslog-address: "tcp://127.0.0.1:1514"
|
||||||
tag: "clair"
|
tag: "clair"
|
||||||
|
env_file:
|
||||||
|
./common/config/clair/clair_env
|
||||||
networks:
|
networks:
|
||||||
harbor-clair:
|
harbor-clair:
|
||||||
external: false
|
external: false
|
||||||
|
|
|
@ -36,6 +36,12 @@ log_rotate_count = 50
|
||||||
#are all valid.
|
#are all valid.
|
||||||
log_rotate_size = 200M
|
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
|
#NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES
|
||||||
#only take effect in the first boot, the subsequent changes of these properties
|
#only take effect in the first boot, the subsequent changes of these properties
|
||||||
#should be performed on web ui
|
#should be performed on web ui
|
||||||
|
|
|
@ -199,7 +199,8 @@ create table replication_job (
|
||||||
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
INDEX policy (policy_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 (
|
create table replication_immediate_trigger (
|
||||||
|
@ -223,7 +224,11 @@ create table img_scan_job (
|
||||||
job_uuid varchar(64),
|
job_uuid varchar(64),
|
||||||
creation_time timestamp default CURRENT_TIMESTAMP,
|
creation_time timestamp default CURRENT_TIMESTAMP,
|
||||||
update_time timestamp default CURRENT_TIMESTAMP on update 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 (
|
create table img_scan_overview (
|
||||||
|
@ -298,4 +303,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` (
|
||||||
`version_num` varchar(32) NOT NULL
|
`version_num` varchar(32) NOT NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
) 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
|
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,
|
username = clair_db_username,
|
||||||
host = clair_db_host,
|
host = clair_db_host,
|
||||||
port = clair_db_port)
|
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:
|
if args.ha_mode:
|
||||||
prepare_ha(rcp, args)
|
prepare_ha(rcp, args)
|
||||||
|
|
|
@ -120,3 +120,9 @@ func ListResourceLabels(query ...*models.ResourceLabelQuery) ([]*models.Resource
|
||||||
_, err := qs.All(&rls)
|
_, err := qs.All(&rls)
|
||||||
return rls, err
|
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)
|
labels, err = GetLabelsOfResource(resourceType, resourceID)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
require.Equal(t, 0, len(labels))
|
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 (
|
const (
|
||||||
// SchemaVersion is the version of database schema
|
// SchemaVersion is the version of database schema
|
||||||
SchemaVersion = "1.4.0"
|
SchemaVersion = "1.5.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetSchemaVersion return the version of database schema
|
// GetSchemaVersion return the version of database schema
|
||||||
|
|
|
@ -256,6 +256,10 @@ func (l *LabelAPI) Put() {
|
||||||
// Delete the label
|
// Delete the label
|
||||||
func (l *LabelAPI) Delete() {
|
func (l *LabelAPI) Delete() {
|
||||||
id := l.label.ID
|
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 {
|
if err := dao.DeleteLabel(id); err != nil {
|
||||||
l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err))
|
l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err))
|
||||||
return
|
return
|
||||||
|
|
|
@ -99,6 +99,7 @@ type GeneralInfo struct {
|
||||||
NextScanAll int64 `json:"next_scan_all"`
|
NextScanAll int64 `json:"next_scan_all"`
|
||||||
ClairVulnStatus *models.ClairVulnerabilityStatus `json:"clair_vulnerability_status,omitempty"`
|
ClairVulnStatus *models.ClairVulnerabilityStatus `json:"clair_vulnerability_status,omitempty"`
|
||||||
RegistryStorageProviderName string `json:"registry_storage_provider_name"`
|
RegistryStorageProviderName string `json:"registry_storage_provider_name"`
|
||||||
|
ReadOnly bool `json:"read_only"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate for validating user if an admin.
|
// validate for validating user if an admin.
|
||||||
|
@ -177,6 +178,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
|
||||||
HasCARoot: caStatErr == nil,
|
HasCARoot: caStatErr == nil,
|
||||||
HarborVersion: harborVersion,
|
HarborVersion: harborVersion,
|
||||||
RegistryStorageProviderName: cfg[common.RegistryStorageProviderName].(string),
|
RegistryStorageProviderName: cfg[common.RegistryStorageProviderName].(string),
|
||||||
|
ReadOnly: config.ReadOnly(),
|
||||||
}
|
}
|
||||||
if info.WithClair {
|
if info.WithClair {
|
||||||
info.ClairVulnStatus = getClairVulnStatus()
|
info.ClairVulnStatus = getClairVulnStatus()
|
||||||
|
|
|
@ -64,6 +64,7 @@ func TestGetGeneralInfo(t *testing.T) {
|
||||||
assert.Equal(false, g.WithNotary, "with notary should be false")
|
assert.Equal(false, g.WithNotary, "with notary should be false")
|
||||||
assert.Equal(true, g.HasCARoot, "has ca root should be true")
|
assert.Equal(true, g.HasCARoot, "has ca root should be true")
|
||||||
assert.NotEmpty(g.HarborVersion, "harbor version should not be empty")
|
assert.NotEmpty(g.HarborVersion, "harbor version should not be empty")
|
||||||
|
assert.Equal(false, g.ReadOnly, "readonly should be false")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCert(t *testing.T) {
|
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**
|
* **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.
|
**projectId** is used to specify which projects the repositories are from.
|
||||||
|
|
||||||
**projectName** is used to generate the related commands for pushing images.
|
**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.
|
**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 {
|
watchRepoClickEvent(repo: RepositoryItem): void {
|
||||||
//Process tag
|
//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**
|
* **Tag detail view**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "harbor-ui",
|
"name": "harbor-ui",
|
||||||
"version": "0.6.45",
|
"version": "0.6.61",
|
||||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "harbor-ui",
|
"name": "harbor-ui",
|
||||||
"version": "0.6.45",
|
"version": "0.6.61",
|
||||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||||
"author": "VMware",
|
"author": "VMware",
|
||||||
"module": "index.js",
|
"module": "index.js",
|
||||||
|
|
|
@ -81,6 +81,7 @@ export class Configuration {
|
||||||
token_expiration: NumberValueItem;
|
token_expiration: NumberValueItem;
|
||||||
cfg_expiration: NumberValueItem;
|
cfg_expiration: NumberValueItem;
|
||||||
scan_all_policy: ComplexValueItem;
|
scan_all_policy: ComplexValueItem;
|
||||||
|
read_only: BoolValueItem;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
this.auth_mode = new StringValueItem("db_auth", true);
|
this.auth_mode = new StringValueItem("db_auth", true);
|
||||||
|
@ -116,5 +117,6 @@ export class Configuration {
|
||||||
daily_time: 0
|
daily_time: 0
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
this.read_only = new BoolValueItem(false, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,8 +10,8 @@ export const CREATE_EDIT_LABEL_STYLE: string = `
|
||||||
section{padding:.5rem 0;}
|
section{padding:.5rem 0;}
|
||||||
section> label{margin-left: 20px;}
|
section> label{margin-left: 20px;}
|
||||||
|
|
||||||
.dropdown-menu{display:inline-block;width:166px; padding:6px;}
|
.dropdown-menu {display:inline-block;width:166px; padding:6px;}
|
||||||
.dropdown-item{ display:inline-flex; margin:2px 4px;
|
.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;}
|
display: inline-block;padding: 0px; width:30px;height:24px; text-align: center;line-height: 24px;}
|
||||||
.btnColor{
|
.btnColor{
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
Output,
|
Output,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
Input, OnInit, ViewChild
|
Input, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef
|
||||||
} from '@angular/core';
|
} 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 {toPromise, clone, compareValue} from '../utils';
|
||||||
|
|
||||||
import {Subject} from "rxjs/Subject";
|
|
||||||
|
|
||||||
import {LabelService} from "../service/label.service";
|
import {LabelService} from "../service/label.service";
|
||||||
import {ErrorHandler} from "../error-handler/error-handler";
|
import {ErrorHandler} from "../error-handler/error-handler";
|
||||||
import {NgForm} from "@angular/forms";
|
import {NgForm} from "@angular/forms";
|
||||||
|
import {Subject} from "rxjs/Subject";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-create-edit-label',
|
selector: 'hbr-create-edit-label',
|
||||||
template: CREATE_EDIT_LABEL_TEMPLATE,
|
template: CREATE_EDIT_LABEL_TEMPLATE,
|
||||||
styles: [CREATE_EDIT_LABEL_STYLE]
|
styles: [CREATE_EDIT_LABEL_STYLE],
|
||||||
})
|
})
|
||||||
|
|
||||||
export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
||||||
|
@ -46,12 +45,13 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
||||||
labelModel: Label = this.initLabel();
|
labelModel: Label = this.initLabel();
|
||||||
labelId = 0;
|
labelId = 0;
|
||||||
|
|
||||||
nameChecker: Subject<string> = new Subject<string>();
|
|
||||||
checkOnGoing: boolean;
|
checkOnGoing: boolean;
|
||||||
isLabelNameExist = false;
|
isLabelNameExist = false;
|
||||||
|
|
||||||
labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8'];
|
labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8'];
|
||||||
|
|
||||||
|
nameChecker = new Subject<string>();
|
||||||
|
|
||||||
labelForm: NgForm;
|
labelForm: NgForm;
|
||||||
@ViewChild('labelForm')
|
@ViewChild('labelForm')
|
||||||
currentForm: NgForm;
|
currentForm: NgForm;
|
||||||
|
@ -66,16 +66,12 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((name: string) => {
|
this.nameChecker.debounceTime(500).subscribe((name: string) => {
|
||||||
this.checkOnGoing = true;
|
this.checkOnGoing = true;
|
||||||
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId))
|
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId, name))
|
||||||
.then(targets => {
|
.then(targets => {
|
||||||
if (targets && targets.length) {
|
if (targets && targets.length) {
|
||||||
if (targets.find(m => m.name === name)) {
|
this.isLabelNameExist = true;
|
||||||
this.isLabelNameExist = true;
|
|
||||||
} else {
|
|
||||||
this.isLabelNameExist = false;
|
|
||||||
};
|
|
||||||
}else {
|
}else {
|
||||||
this.isLabelNameExist = false;
|
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 { CONFIGURATION_DIRECTIVES } from './config/index';
|
||||||
import { JOB_LOG_VIEWER_DIRECTIVES } from './job-log-viewer/index';
|
import { JOB_LOG_VIEWER_DIRECTIVES } from './job-log-viewer/index';
|
||||||
import { PROJECT_POLICY_CONFIG_DIRECTIVES } from './project-policy-config/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 {
|
import {
|
||||||
SystemInfoService,
|
SystemInfoService,
|
||||||
|
@ -182,7 +184,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||||
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
||||||
LABEL_DIRECTIVES,
|
LABEL_DIRECTIVES,
|
||||||
CREATE_EDIT_LABEL_DIRECTIVES,
|
CREATE_EDIT_LABEL_DIRECTIVES,
|
||||||
LABEL_PIECE_DIRECTIVES
|
LABEL_PIECE_DIRECTIVES,
|
||||||
|
HBR_GRIDVIEW_DIRECTIVES,
|
||||||
|
REPOSITORY_GRIDVIEW_DIRECTIVES,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
LOG_DIRECTIVES,
|
LOG_DIRECTIVES,
|
||||||
|
@ -207,7 +211,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
|
||||||
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
PROJECT_POLICY_CONFIG_DIRECTIVES,
|
||||||
LABEL_DIRECTIVES,
|
LABEL_DIRECTIVES,
|
||||||
CREATE_EDIT_LABEL_DIRECTIVES,
|
CREATE_EDIT_LABEL_DIRECTIVES,
|
||||||
LABEL_PIECE_DIRECTIVES
|
LABEL_PIECE_DIRECTIVES,
|
||||||
|
HBR_GRIDVIEW_DIRECTIVES,
|
||||||
|
REPOSITORY_GRIDVIEW_DIRECTIVES,
|
||||||
],
|
],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@ export * from './harbor-library.module';
|
||||||
export * from './service.config';
|
export * from './service.config';
|
||||||
export * from './service/index';
|
export * from './service/index';
|
||||||
export * from './error-handler/index';
|
export * from './error-handler/index';
|
||||||
//export * from './utils';
|
// export * from './utils';
|
||||||
export * from './log/index';
|
export * from './log/index';
|
||||||
export * from './filter/index';
|
export * from './filter/index';
|
||||||
export * from './endpoint/index';
|
export * from './endpoint/index';
|
||||||
|
@ -23,3 +23,5 @@ export * from './channel/index';
|
||||||
export * from './project-policy-config/index';
|
export * from './project-policy-config/index';
|
||||||
export * from './label/index';
|
export * from './label/index';
|
||||||
export * from './create-edit-label';
|
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-column [clrDgSortBy]="pullCountComparator">{{'REPOSITORY.PULL_COUNT' | translate}}</clr-dg-column>
|
||||||
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
<clr-dg-placeholder>{{'REPOSITORY.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
<clr-dg-row *ngFor="let r of repositories" [clrDgItem]="r">
|
<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.tags_count}}</clr-dg-cell>
|
||||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||||
</clr-dg-row>
|
</clr-dg-row>
|
||||||
|
|
|
@ -34,7 +34,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||||
import { Subscription } from 'rxjs/Subscription';
|
import { Subscription } from 'rxjs/Subscription';
|
||||||
import { Tag, TagClickEvent } from '../service/interface';
|
import { Tag } from '../service/interface';
|
||||||
|
|
||||||
import { State } from "clarity-angular";
|
import { State } from "clarity-angular";
|
||||||
import {
|
import {
|
||||||
|
@ -60,14 +60,14 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||||
|
|
||||||
@Input() hasSignedIn: boolean;
|
@Input() hasSignedIn: boolean;
|
||||||
@Input() hasProjectAdminRole: boolean;
|
@Input() hasProjectAdminRole: boolean;
|
||||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
@Output() repoClickEvent = new EventEmitter<RepositoryItem>();
|
||||||
|
|
||||||
lastFilteredRepoName: string;
|
lastFilteredRepoName: string;
|
||||||
repositories: RepositoryItem[];
|
repositories: RepositoryItem[];
|
||||||
systemInfo: SystemInfo;
|
systemInfo: SystemInfo;
|
||||||
selectedRow: RepositoryItem[] = [];
|
selectedRow: RepositoryItem[] = [];
|
||||||
|
|
||||||
loading: boolean = true;
|
loading = true;
|
||||||
|
|
||||||
@ViewChild('confirmationDialog')
|
@ViewChild('confirmationDialog')
|
||||||
confirmationDialog: ConfirmationDialogComponent;
|
confirmationDialog: ConfirmationDialogComponent;
|
||||||
|
@ -279,19 +279,15 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||||
this.doSearchRepoNames('');
|
this.doSearchRepoNames('');
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
|
|
||||||
this.tagClickEvent.emit(tagClickEvt);
|
|
||||||
}
|
|
||||||
|
|
||||||
clrLoad(state: State): void {
|
clrLoad(state: State): void {
|
||||||
this.selectedRow = [];
|
this.selectedRow = [];
|
||||||
//Keep it for future filtering and sorting
|
// Keep it for future filtering and sorting
|
||||||
this.currentState = state;
|
this.currentState = state;
|
||||||
|
|
||||||
let pageNumber: number = calculatePage(state);
|
let pageNumber: number = calculatePage(state);
|
||||||
if (pageNumber <= 0) { pageNumber = 1; }
|
if (pageNumber <= 0) { pageNumber = 1; }
|
||||||
|
|
||||||
//Pagination
|
// Pagination
|
||||||
let params: RequestQueryParams = new RequestQueryParams();
|
let params: RequestQueryParams = new RequestQueryParams();
|
||||||
params.set("page", '' + pageNumber);
|
params.set("page", '' + pageNumber);
|
||||||
params.set("page_size", '' + this.pageSize);
|
params.set("page_size", '' + this.pageSize);
|
||||||
|
@ -307,7 +303,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||||
this.repositories = repo.data;
|
this.repositories = repo.data;
|
||||||
|
|
||||||
this.signedCon = {};
|
this.signedCon = {};
|
||||||
//Do filtering and sorting
|
// Do filtering and sorting
|
||||||
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
|
this.repositories = doFiltering<RepositoryItem>(this.repositories, state);
|
||||||
this.repositories = doSorting<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);
|
this.errorHandler.error(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Force refresh view
|
// Force refresh view
|
||||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||||
setTimeout(() => clearInterval(hnd), 5000);
|
setTimeout(() => clearInterval(hnd), 5000);
|
||||||
}
|
}
|
||||||
|
@ -331,7 +327,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||||
let targetPageNumber: number = this.currentPage;
|
let targetPageNumber: number = this.currentPage;
|
||||||
|
|
||||||
if (this.currentPage > totalPages) {
|
if (this.currentPage > totalPages) {
|
||||||
targetPageNumber = totalPages;//Should == currentPage -1
|
targetPageNumber = totalPages; // Should == currentPage -1
|
||||||
}
|
}
|
||||||
|
|
||||||
let st: State = this.currentState;
|
let st: State = this.currentState;
|
||||||
|
@ -344,8 +340,8 @@ export class RepositoryListviewComponent implements OnChanges, OnInit {
|
||||||
|
|
||||||
return st;
|
return st;
|
||||||
}
|
}
|
||||||
public gotoLink(projectId: number, repoName: string): void {
|
|
||||||
let linkUrl = [this.router.url, repoName];
|
watchRepoClickEvt(repo: RepositoryItem) {
|
||||||
this.router.navigate(linkUrl);
|
this.repoClickEvent.emit(repo);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -46,7 +46,7 @@ export const REPOSITORY_TEMPLATE = `
|
||||||
</section>
|
</section>
|
||||||
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
|
<section id="image" role="tabpanel" aria-labelledby="repo-image" [hidden]='!isCurrentTabContent("image")'>
|
||||||
<div id=images-container>
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,6 +54,7 @@ export class RepositoryComponent implements OnInit {
|
||||||
@Input() isGuest: boolean;
|
@Input() isGuest: boolean;
|
||||||
@Input() withNotary: boolean;
|
@Input() withNotary: boolean;
|
||||||
@Input() withClair: boolean;
|
@Input() withClair: boolean;
|
||||||
|
@Input() withAdmiral: boolean;
|
||||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||||
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
||||||
|
|
||||||
|
|
|
@ -250,7 +250,7 @@ export interface VulnerabilitySummary {
|
||||||
job_id?: number;
|
job_id?: number;
|
||||||
severity: VulnerabilitySeverity;
|
severity: VulnerabilitySeverity;
|
||||||
components: VulnerabilityComponents;
|
components: VulnerabilityComponents;
|
||||||
update_time: Date; //Use as complete timestamp
|
update_time: Date; // Use as complete timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VulnerabilityComponents {
|
export interface VulnerabilityComponents {
|
||||||
|
@ -277,3 +277,15 @@ export interface Label {
|
||||||
scope: string;
|
scope: string;
|
||||||
project_id: number;
|
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()
|
return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise()
|
||||||
.then(response => response)
|
.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,
|
provide: MissingTranslationHandler,
|
||||||
useClass: MyMissingTranslationHandler
|
useClass: MyMissingTranslationHandler
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -65,9 +65,8 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) {
|
||||||
CookieModule,
|
CookieModule,
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
ClarityModule,
|
ClarityModule,
|
||||||
TranslateModule
|
TranslateModule,
|
||||||
],
|
],
|
||||||
providers: [CookieService]
|
providers: [CookieService]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class SharedModule { }
|
export class SharedModule { }
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const errorHandler = function (error: any): string {
|
||||||
return "UNKNOWN_ERROR";
|
return "UNKNOWN_ERROR";
|
||||||
}
|
}
|
||||||
if (!(error.statusCode || error.status)) {
|
if (!(error.statusCode || error.status)) {
|
||||||
//treat as string message
|
// treat as string message
|
||||||
return '' + error;
|
return '' + error;
|
||||||
} else {
|
} else {
|
||||||
switch (error.statusCode || error.status) {
|
switch (error.statusCode || error.status) {
|
||||||
|
@ -47,3 +47,27 @@ export const errorHandler = function (error: any): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = `
|
export const TAG_DETAIL_STYLES: string = `
|
||||||
.overview-section {
|
.overview-section {
|
||||||
background-color: white;
|
|
||||||
padding-bottom: 36px;
|
padding-bottom: 36px;
|
||||||
border-bottom: 1px solid #cccccc;
|
border-bottom: 1px solid #cccccc;
|
||||||
}
|
}
|
||||||
|
@ -78,27 +77,37 @@ export const TAG_DETAIL_STYLES: string = `
|
||||||
padding-left: 24px;
|
padding-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vulnerabilities-info .third-column {
|
.third-column {
|
||||||
margin-left: 36px;
|
margin-left: 36px;
|
||||||
}
|
}
|
||||||
|
.vulnerability{
|
||||||
|
margin-left: 50px;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-bottom: 20px;}
|
||||||
|
|
||||||
.vulnerabilities-info .second-column,
|
.vulnerabilities-info .second-column {
|
||||||
.vulnerabilities-info .fourth-column {
|
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fourth-column{
|
||||||
|
float: left;
|
||||||
|
margin-left:20px;}
|
||||||
|
|
||||||
.vulnerabilities-info .second-row {
|
.vulnerabilities-info .second-row {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-title {
|
.detail-title {
|
||||||
font-weight: 500;
|
float:left;
|
||||||
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-detail-label {
|
.image-detail-label {
|
||||||
text-align: right;
|
margin-right: 10px;
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-detail-value {
|
.image-detail-value {
|
||||||
|
|
|
@ -7,26 +7,22 @@ export const TAG_DETAIL_HTML: string = `
|
||||||
</div>
|
</div>
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
<div class="tag-name">
|
<div class="tag-name">
|
||||||
<h1>{{tagDetails.name}}</h1>
|
<h1>{{repositoryId}}:{{tagDetails.name}}</h1>
|
||||||
</div>
|
|
||||||
<div class="tag-timestamp">
|
|
||||||
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-block">
|
<div class="summary-block">
|
||||||
<div class="image-summary">
|
<div class="image-summary">
|
||||||
<div class="detail-title">
|
|
||||||
{{'TAG.IMAGE_DETAILS' | translate }}
|
|
||||||
</div>
|
|
||||||
<div class="flex-block">
|
<div class="flex-block">
|
||||||
<div class="image-detail-label">
|
<div class="image-detail-label">
|
||||||
|
<div>{{'TAG.AUTHOR' | translate }}</div>
|
||||||
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
||||||
<div>{{'TAG.OS' | translate }}</div>
|
<div>{{'TAG.OS' | translate }}</div>
|
||||||
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
|
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
|
||||||
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="image-detail-value">
|
<div class="image-detail-value">
|
||||||
|
<div>{{author | translate}}</div>
|
||||||
<div>{{tagDetails.architecture}}</div>
|
<div>{{tagDetails.architecture}}</div>
|
||||||
<div>{{tagDetails.os}}</div>
|
<div>{{tagDetails.os}}</div>
|
||||||
<div>{{tagDetails.docker_version}}</div>
|
<div>{{tagDetails.docker_version}}</div>
|
||||||
|
@ -35,8 +31,8 @@ export const TAG_DETAIL_HTML: string = `
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="detail-title">
|
<div class="vulnerability">
|
||||||
{{'TAG.IMAGE_VULNERABILITIES' | translate }}
|
<hbr-vulnerability-bar [repoName]="repositoryId" [tagId]="tagDetails.name" [summary]="tagDetails.scan_overview"></hbr-vulnerability-bar>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-block vulnerabilities-info">
|
<div class="flex-block vulnerabilities-info">
|
||||||
<div>
|
<div>
|
||||||
|
@ -46,12 +42,6 @@ export const TAG_DETAIL_HTML: string = `
|
||||||
<div class="second-row">
|
<div class="second-row">
|
||||||
<clr-icon shape="exclamation-triangle" size="24" class="tip-icon-medium"></clr-icon>
|
<clr-icon shape="exclamation-triangle" size="24" class="tip-icon-medium"></clr-icon>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<clr-icon shape="play" size="20" class="tip-icon-low rotate-90"></clr-icon>
|
<clr-icon shape="play" size="20" class="tip-icon-low rotate-90"></clr-icon>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,11 +49,20 @@ export const TAG_DETAIL_HTML: string = `
|
||||||
<clr-icon shape="help" size="18" style="margin-left: 2px;"></clr-icon>
|
<clr-icon shape="help" size="18" style="margin-left: 2px;"></clr-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fourth-column">
|
<div class="second-column">
|
||||||
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }}</div>
|
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}</div>
|
||||||
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | 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>
|
||||||
|
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -10,6 +10,11 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||||
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||||
import { FilterComponent } from '../filter/index';
|
import { FilterComponent } from '../filter/index';
|
||||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
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)', () => {
|
describe('TagDetailComponent (inline template)', () => {
|
||||||
|
|
||||||
|
@ -66,10 +71,16 @@ describe('TagDetailComponent (inline template)', () => {
|
||||||
declarations: [
|
declarations: [
|
||||||
TagDetailComponent,
|
TagDetailComponent,
|
||||||
ResultGridComponent,
|
ResultGridComponent,
|
||||||
|
VULNERABILITY_DIRECTIVES,
|
||||||
|
LabelPieceComponent,
|
||||||
|
JobLogViewerComponent,
|
||||||
FilterComponent
|
FilterComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
ErrorHandler,
|
ErrorHandler,
|
||||||
|
ChannelService,
|
||||||
|
JobLogDefaultService,
|
||||||
|
{provide: JobLogService, useClass: JobLogDefaultService},
|
||||||
{ provide: SERVICE_CONFIG, useValue: config },
|
{ provide: SERVICE_CONFIG, useValue: config },
|
||||||
{ provide: TagService, useClass: TagDefaultService },
|
{ provide: TagService, useClass: TagDefaultService },
|
||||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||||
|
@ -119,7 +130,7 @@ describe('TagDetailComponent (inline template)', () => {
|
||||||
|
|
||||||
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
||||||
expect(el).toBeTruthy();
|
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();
|
expect(el).toBeTruthy();
|
||||||
let el2: HTMLElement = el.querySelector('div');
|
let el2: HTMLElement = el.querySelector('div');
|
||||||
expect(el2).toBeTruthy();
|
expect(el2).toBeTruthy();
|
||||||
expect(el2.textContent).toEqual("amd64");
|
expect(el2.textContent).toEqual("steven");
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -147,7 +158,7 @@ describe('TagDetailComponent (inline template)', () => {
|
||||||
expect(el).toBeTruthy();
|
expect(el).toBeTruthy();
|
||||||
let el2: HTMLElement = el.querySelector('div');
|
let el2: HTMLElement = el.querySelector('div');
|
||||||
expect(el2).toBeTruthy();
|
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 { TagService, Tag, VulnerabilitySeverity } from '../service/index';
|
||||||
import { toPromise } from '../utils';
|
import { toPromise } from '../utils';
|
||||||
import { ErrorHandler } from '../error-handler/index';
|
import { ErrorHandler } from '../error-handler/index';
|
||||||
|
import {Label} from "../service/interface";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'hbr-tag-detail',
|
selector: 'hbr-tag-detail',
|
||||||
|
@ -19,9 +20,11 @@ export class TagDetailComponent implements OnInit {
|
||||||
_mediumCount: number = 0;
|
_mediumCount: number = 0;
|
||||||
_lowCount: number = 0;
|
_lowCount: number = 0;
|
||||||
_unknownCount: number = 0;
|
_unknownCount: number = 0;
|
||||||
|
labels: Label;
|
||||||
|
|
||||||
@Input() tagId: string;
|
@Input() tagId: string;
|
||||||
@Input() repositoryId: string;
|
@Input() repositoryId: string;
|
||||||
|
@Input() withAdmiral: boolean;
|
||||||
tagDetails: Tag = {
|
tagDetails: Tag = {
|
||||||
name: "--",
|
name: "--",
|
||||||
size: "--",
|
size: "--",
|
||||||
|
@ -74,7 +77,7 @@ export class TagDetailComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
onBack(): void {
|
onBack(): void {
|
||||||
this.backEvt.emit(this.tagId);
|
this.backEvt.emit(this.repositoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPackageText(count: number): string {
|
getPackageText(count: number): string {
|
||||||
|
|
|
@ -66,4 +66,11 @@ export const TAG_STYLE = `
|
||||||
:host >>> .signpost-content-body{padding:0 .4rem;}
|
:host >>> .signpost-content-body{padding:0 .4rem;}
|
||||||
:host >>> .signpost-content-header{display:none;}
|
:host >>> .signpost-content-header{display:none;}
|
||||||
.filterLabelPiece{position: absolute; bottom :0px;z-index:1;}
|
.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="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='filterLabelPiece' [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel"></hbr-label-piece></div>
|
||||||
<div class="flex-xs-middle">
|
<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>
|
<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>
|
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||||
<div style='display:grid'>
|
<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 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'>{{'LABEL.NO_LABELS' | translate }}</div>
|
||||||
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
|
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
|
||||||
|
@ -41,28 +42,26 @@ export const TAG_TEMPLATE = `
|
||||||
<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-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
|
||||||
<clr-dg-action-bar>
|
<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]="!(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>
|
<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>
|
<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>
|
<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>
|
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||||
<div style='display:grid'>
|
<div style='display:grid'>
|
||||||
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
|
<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 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'>{{'LABEL.NO_LABELS' | translate }}</div>
|
||||||
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
|
<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)">
|
<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>
|
<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>
|
<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>
|
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); unSelectLabel(label); label.iconsShow = false"></clr-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</clr-dropdown-menu>
|
</clr-dropdown-menu>
|
||||||
</clr-dropdown>
|
</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>
|
<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-action-bar>
|
||||||
<clr-dg-column style="width: 120px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
<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>
|
<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="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: 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: 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-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||||
<clr-dg-cell class="truncated" style="width: 120px;" [ngSwitch]="withClair">
|
<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 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: 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: 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>
|
<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="signpost-item" [hidden]="t.labels?.length<=1">
|
||||||
<div class="trigger-item">
|
<div class="trigger-item">
|
||||||
|
|
|
@ -140,13 +140,6 @@ describe('TagComponent (inline template)', () => {
|
||||||
|
|
||||||
labelService = fixture.debugElement.injector.get(LabelService);
|
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));
|
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
|
||||||
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
|
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||||
@Input() registryUrl: string;
|
@Input() registryUrl: string;
|
||||||
@Input() withNotary: boolean;
|
@Input() withNotary: boolean;
|
||||||
@Input() withClair: boolean;
|
@Input() withClair: boolean;
|
||||||
|
@Input() withAdmiral: boolean;
|
||||||
@Output() refreshRepo = new EventEmitter<boolean>();
|
@Output() refreshRepo = new EventEmitter<boolean>();
|
||||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||||
@Output() signatureOutput = new EventEmitter<any>();
|
@Output() signatureOutput = new EventEmitter<any>();
|
||||||
|
@ -180,11 +180,11 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||||
.subscribe((name: string) => {
|
.subscribe((name: string) => {
|
||||||
if (name && name.length) {
|
if (name && name.length) {
|
||||||
this.filterOnGoing = true;
|
this.filterOnGoing = true;
|
||||||
this.imageFilterLabels = [];
|
this.imageStickLabels = [];
|
||||||
|
|
||||||
this.imageLabels.forEach(data => {
|
this.imageLabels.forEach(data => {
|
||||||
if (data.label.name.indexOf(name) !== -1) {
|
if (data.label.name.indexOf(name) !== -1) {
|
||||||
this.imageFilterLabels.push(data);
|
this.imageStickLabels.push(data);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -196,7 +196,9 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.getAllLabels();
|
if (!this.withAdmiral) {
|
||||||
|
this.getAllLabels();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get filterLabelPieceWidth() {
|
public get filterLabelPieceWidth() {
|
||||||
|
@ -302,7 +304,7 @@ export class TagComponent implements OnInit, AfterViewInit {
|
||||||
this.selectedChange(tag);
|
this.selectedChange(tag);
|
||||||
}
|
}
|
||||||
selectLabel(labelInfo: {[key: string]: any | string[]}): void {
|
selectLabel(labelInfo: {[key: string]: any | string[]}): void {
|
||||||
if (labelInfo && labelInfo.iconsShow) {
|
if (labelInfo && !labelInfo.iconsShow) {
|
||||||
let labelId = labelInfo.label.id;
|
let labelId = labelInfo.label.id;
|
||||||
this.selectedRow = this.selectedTag;
|
this.selectedRow = this.selectedTag;
|
||||||
toPromise<any>(this.tagService.addLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
|
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 {
|
unSelectLabel(labelInfo: {[key: string]: any | string[]}): void {
|
||||||
if (labelInfo && !labelInfo.iconsShow) {
|
if (labelInfo && labelInfo.iconsShow) {
|
||||||
let labelId = labelInfo.label.id;
|
let labelId = labelInfo.label.id;
|
||||||
this.selectedRow = this.selectedTag;
|
this.selectedRow = this.selectedTag;
|
||||||
toPromise<any>(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
|
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)) {
|
} else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) {
|
||||||
return (size / Math.pow(1024, 2)).toFixed(2) + "MB";
|
return (size / Math.pow(1024, 2)).toFixed(2) + "MB";
|
||||||
} else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) {
|
} 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 {
|
} else {
|
||||||
return size + "B";
|
return size + "B";
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
"clarity-icons": "^0.10.17",
|
"clarity-icons": "^0.10.17",
|
||||||
"clarity-ui": "^0.10.27",
|
"clarity-ui": "^0.10.27",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"harbor-ui": "0.6.53",
|
"harbor-ui": "0.6.61",
|
||||||
"intl": "^1.2.5",
|
"intl": "^1.2.5",
|
||||||
"mutationobserver-shim": "^0.3.2",
|
"mutationobserver-shim": "^0.3.2",
|
||||||
"ngx-cookie": "^1.0.0",
|
"ngx-cookie": "^1.0.0",
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<li role="presentation" class="nav-item">
|
<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>
|
<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>
|
||||||
<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>
|
<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>
|
||||||
<li role="presentation" class="nav-item" *ngIf="withClair">
|
<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")'>
|
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
|
||||||
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
|
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
|
||||||
</section>
|
</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>
|
<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")'>
|
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
|
||||||
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
|
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -81,6 +81,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
|
||||||
return this.appConfigService.getConfig().with_clair;
|
return this.appConfigService.getConfig().with_clair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get withAdmiral(): boolean {
|
||||||
|
return this.appConfigService.getConfig().with_admiral;
|
||||||
|
}
|
||||||
|
|
||||||
isCurrentTabLink(tabId: string): boolean {
|
isCurrentTabLink(tabId: string): boolean {
|
||||||
return this.currentTabId === tabId;
|
return this.currentTabId === tabId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,14 @@ const harborRoutes: Routes = [
|
||||||
projectResolver: ProjectRoutingResolver
|
projectResolver: ProjectRoutingResolver
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'projects/:id/repositories/:repo/tags/:tag',
|
||||||
|
component: TagDetailPageComponent,
|
||||||
|
canActivate: [MemberGuard],
|
||||||
|
resolve: {
|
||||||
|
projectResolver: ProjectRoutingResolver
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'projects/:id',
|
path: 'projects/:id',
|
||||||
component: ProjectDetailComponent,
|
component: ProjectDetailComponent,
|
||||||
|
@ -124,10 +132,6 @@ const harborRoutes: Routes = [
|
||||||
path: 'repositories/:repo/tags',
|
path: 'repositories/:repo/tags',
|
||||||
component: TagRepositoryComponent,
|
component: TagRepositoryComponent,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'repositories/:repo/tags/:tag',
|
|
||||||
component: TagDetailPageComponent
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'replications',
|
path: 'replications',
|
||||||
component: ReplicationPageComponent,
|
component: ReplicationPageComponent,
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
|
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
|
||||||
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
||||||
</li>
|
</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>
|
<a class="nav-link" routerLink="labels" routerLinkActive="active">{{'PROJECT_DETAIL.LABELS' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { SessionService } from '../../shared/session.service';
|
||||||
import { ProjectService } from '../../project/project.service';
|
import { ProjectService } from '../../project/project.service';
|
||||||
|
|
||||||
import { RoleMapping } from '../../shared/shared.const';
|
import { RoleMapping } from '../../shared/shared.const';
|
||||||
|
import {AppConfigService} from "../../app-config.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'project-detail',
|
selector: 'project-detail',
|
||||||
|
@ -38,6 +39,7 @@ export class ProjectDetailComponent {
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private sessionService: SessionService,
|
private sessionService: SessionService,
|
||||||
|
private appConfigService: AppConfigService,
|
||||||
private projectService: ProjectService) {
|
private projectService: ProjectService) {
|
||||||
|
|
||||||
this.hasSignedIn = this.sessionService.getCurrentUser() !== null;
|
this.hasSignedIn = this.sessionService.getCurrentUser() !== null;
|
||||||
|
@ -61,6 +63,10 @@ export class ProjectDetailComponent {
|
||||||
return this.sessionService.getCurrentUser() != null;
|
return this.sessionService.getCurrentUser() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get withAdmiral(): boolean {
|
||||||
|
return this.appConfigService.getConfig().with_admiral;
|
||||||
|
}
|
||||||
|
|
||||||
backToProject(): void {
|
backToProject(): void {
|
||||||
if (window.sessionStorage) {
|
if (window.sessionStorage) {
|
||||||
window.sessionStorage.setItem('fromDetails', 'true');
|
window.sessionStorage.setItem('fromDetails', 'true');
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
<div style="margin-top: 24px;">
|
<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>
|
</div>
|
|
@ -17,7 +17,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { Project } from '../project/project';
|
import { Project } from '../project/project';
|
||||||
import { SessionService } from '../shared/session.service';
|
import { SessionService } from '../shared/session.service';
|
||||||
|
|
||||||
import { TagClickEvent } from 'harbor-ui';
|
import { TagClickEvent, RepositoryItem } from 'harbor-ui';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'repository',
|
selector: 'repository',
|
||||||
|
@ -47,8 +47,8 @@ export class RepositoryPageComponent implements OnInit {
|
||||||
this.hasSignedIn = this.session.getCurrentUser() !== null;
|
this.hasSignedIn = this.session.getCurrentUser() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTagClickEvent(tagEvt: TagClickEvent): void {
|
watchRepoClickEvent(repoEvt: RepositoryItem): void {
|
||||||
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name];
|
let linkUrl = ['harbor', 'projects', repoEvt.project_id, 'repositories', repoEvt.name];
|
||||||
this.router.navigate(linkUrl);
|
this.router.navigate(linkUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<div style="margin-top: 24px;">
|
<div>
|
||||||
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [repositoryId]="repositoryId"></hbr-tag-detail>
|
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="tagId" [withAdmiral]="withAdmiral" [repositoryId]="repositoryId"></hbr-tag-detail>
|
||||||
</div>
|
</div>
|
|
@ -13,6 +13,7 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import {AppConfigService} from "../../app-config.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'repository',
|
selector: 'repository',
|
||||||
|
@ -25,6 +26,7 @@ export class TagDetailPageComponent implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
|
private appConfigService: AppConfigService,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
@ -32,10 +34,14 @@ export class TagDetailPageComponent implements OnInit {
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.repositoryId = this.route.snapshot.params["repo"];
|
this.repositoryId = this.route.snapshot.params["repo"];
|
||||||
this.tagId = this.route.snapshot.params["tag"];
|
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 {
|
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>
|
<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>
|
</div>
|
|
@ -68,6 +68,10 @@ export class TagRepositoryComponent implements OnInit {
|
||||||
return this.appConfigService.getConfig().with_clair;
|
return this.appConfigService.getConfig().with_clair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get withAdmiral(): boolean {
|
||||||
|
return this.appConfigService.getConfig().with_admiral;
|
||||||
|
}
|
||||||
|
|
||||||
get hasSignedIn(): boolean {
|
get hasSignedIn(): boolean {
|
||||||
return this.session.getCurrentUser() !== null;
|
return this.session.getCurrentUser() !== null;
|
||||||
}
|
}
|
||||||
|
@ -75,12 +79,13 @@ export class TagRepositoryComponent implements OnInit {
|
||||||
hasChanges(): boolean {
|
hasChanges(): boolean {
|
||||||
return this.repositoryComponent.hasChanges();
|
return this.repositoryComponent.hasChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
watchTagClickEvt(tagEvt: TagClickEvent): void {
|
watchTagClickEvt(tagEvt: TagClickEvent): void {
|
||||||
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
|
let linkUrl = ['harbor', 'projects', tagEvt.project_id, 'repositories', tagEvt.repository_name, 'tags', tagEvt.tag_name];
|
||||||
this.router.navigate(linkUrl);
|
this.router.navigate(linkUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
goBack(tag: string): void {
|
watchGoBackEvt(projectId: string): void {
|
||||||
this.router.navigate(["harbor", "projects", this.projectId, "repositories"]);
|
this.router.navigate(["harbor", "projects", projectId, "repositories"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -404,6 +404,7 @@
|
||||||
"REPOSITORIES": "Repositories",
|
"REPOSITORIES": "Repositories",
|
||||||
"OF": "of",
|
"OF": "of",
|
||||||
"ITEMS": "items",
|
"ITEMS": "items",
|
||||||
|
"NO_ITEMS": "NO ITEMS",
|
||||||
"POP_REPOS": "Popular Repositories",
|
"POP_REPOS": "Popular Repositories",
|
||||||
"DELETED_REPO_SUCCESS": "Deleted repositories successfully.",
|
"DELETED_REPO_SUCCESS": "Deleted repositories successfully.",
|
||||||
"DELETED_TAG_SUCCESS": "Deleted tags successfully.",
|
"DELETED_TAG_SUCCESS": "Deleted tags successfully.",
|
||||||
|
@ -415,7 +416,11 @@
|
||||||
"IMAGE": "Images",
|
"IMAGE": "Images",
|
||||||
"LABELS": ":labels",
|
"LABELS": ":labels",
|
||||||
"ADD_TO_IMAGE": "Add labels to this image",
|
"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": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
|
"FORM_CHANGE_CONFIRMATION": "Some changes are not saved yet. Do you want to cancel?"
|
||||||
|
@ -438,6 +443,8 @@
|
||||||
"REPLICATION": "Replication",
|
"REPLICATION": "Replication",
|
||||||
"EMAIL": "Email",
|
"EMAIL": "Email",
|
||||||
"LABEL": "Label",
|
"LABEL": "Label",
|
||||||
|
"REPOSITORY": "Repository",
|
||||||
|
"REPO_READ_ONLY": "Repository Read Only",
|
||||||
"SYSTEM": "System Settings",
|
"SYSTEM": "System Settings",
|
||||||
"VULNERABILITY": "Vulnerability",
|
"VULNERABILITY": "Vulnerability",
|
||||||
"CONFIRM_TITLE": "Confirm to cancel",
|
"CONFIRM_TITLE": "Confirm to cancel",
|
||||||
|
|
|
@ -404,6 +404,7 @@
|
||||||
"REPOSITORIES": "Repositorios",
|
"REPOSITORIES": "Repositorios",
|
||||||
"OF": "of",
|
"OF": "of",
|
||||||
"ITEMS": "elementos",
|
"ITEMS": "elementos",
|
||||||
|
"NO_ITEMS": "NO ITEMS",
|
||||||
"POP_REPOS": "Repositorios Populares",
|
"POP_REPOS": "Repositorios Populares",
|
||||||
"DELETED_REPO_SUCCESS": "Repositorio eliminado satisfactoriamente.",
|
"DELETED_REPO_SUCCESS": "Repositorio eliminado satisfactoriamente.",
|
||||||
"DELETED_TAG_SUCCESS": "Etiqueta eliminada satisfactoriamente.",
|
"DELETED_TAG_SUCCESS": "Etiqueta eliminada satisfactoriamente.",
|
||||||
|
@ -413,9 +414,13 @@
|
||||||
"INFO": "Información",
|
"INFO": "Información",
|
||||||
"NO_INFO": "Sin información de descripción para este repositorio",
|
"NO_INFO": "Sin información de descripción para este repositorio",
|
||||||
"IMAGE": "Imágenes",
|
"IMAGE": "Imágenes",
|
||||||
"LABELS": ":labels",
|
"LABELS": "Labels",
|
||||||
"ADD_TO_IMAGE": "Add labels to this image",
|
"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": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
|
"FORM_CHANGE_CONFIRMATION": "Algunos cambios no se han guardado aún. ¿Quiere cancelar?"
|
||||||
|
@ -438,6 +443,8 @@
|
||||||
"REPLICATION": "Replicación",
|
"REPLICATION": "Replicación",
|
||||||
"EMAIL": "Email",
|
"EMAIL": "Email",
|
||||||
"LABEL": "Label",
|
"LABEL": "Label",
|
||||||
|
"REPOSITORY": "Repository",
|
||||||
|
"REPO_READ_ONLY": "Repository Read Only",
|
||||||
"SYSTEM": "Opciones del Sistema",
|
"SYSTEM": "Opciones del Sistema",
|
||||||
"VULNERABILITY": "Vulnerability",
|
"VULNERABILITY": "Vulnerability",
|
||||||
"CONFIRM_TITLE": "Confirma cancelación",
|
"CONFIRM_TITLE": "Confirma cancelación",
|
||||||
|
@ -613,9 +620,12 @@
|
||||||
"OS": "OS",
|
"OS": "OS",
|
||||||
"SCAN_COMPLETION_TIME": "Scan Completed",
|
"SCAN_COMPLETION_TIME": "Scan Completed",
|
||||||
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
"IMAGE_VULNERABILITIES": "Image Vulnerabilities",
|
||||||
|
"LEVEL_VULNERABILITIES": "Level Vulnerabilities",
|
||||||
"PLACEHOLDER": "We couldn't find any tags!",
|
"PLACEHOLDER": "We couldn't find any tags!",
|
||||||
"COPY_ERROR": "Copy failed, please try to manually copy.",
|
"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": "Label",
|
"LABEL": "Label",
|
||||||
|
|
|
@ -364,7 +364,11 @@
|
||||||
"DELETED_TAG_SUCCESS": "Tag supprimé avec succés.",
|
"DELETED_TAG_SUCCESS": "Tag supprimé avec succés.",
|
||||||
"COPY": "Copier",
|
"COPY": "Copier",
|
||||||
"NOTARY_IS_UNDETERMINED": "Ne peut pas déterminer la signature de ce tag.",
|
"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": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"
|
"FORM_CHANGE_CONFIRMATION": "Certaines modifications ne sont pas encore enregistrées. Voulez-vous annuler ?"
|
||||||
|
|
|
@ -404,6 +404,7 @@
|
||||||
"REPOSITORIES": "镜像仓库",
|
"REPOSITORIES": "镜像仓库",
|
||||||
"OF": "共计",
|
"OF": "共计",
|
||||||
"ITEMS": "条记录",
|
"ITEMS": "条记录",
|
||||||
|
"NO_ITEMS": "没有记录",
|
||||||
"POP_REPOS": "受欢迎的镜像仓库",
|
"POP_REPOS": "受欢迎的镜像仓库",
|
||||||
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。",
|
"DELETED_REPO_SUCCESS": "成功删除镜像仓库。",
|
||||||
"DELETED_TAG_SUCCESS": "成功删除镜像标签。",
|
"DELETED_TAG_SUCCESS": "成功删除镜像标签。",
|
||||||
|
@ -415,7 +416,11 @@
|
||||||
"IMAGE": "镜像",
|
"IMAGE": "镜像",
|
||||||
"LABELS": "标签",
|
"LABELS": "标签",
|
||||||
"ADD_TO_IMAGE": "添加标签到此镜像",
|
"ADD_TO_IMAGE": "添加标签到此镜像",
|
||||||
"ADD_LABELS": "添加标签"
|
"ADD_LABELS": "添加标签",
|
||||||
|
"FILTER_BY_LABEL": "过滤标签",
|
||||||
|
"ACTION": "操作",
|
||||||
|
"DEPLOY": "部署",
|
||||||
|
"ADDITIONAL_INFO": "添加信息"
|
||||||
},
|
},
|
||||||
"ALERT": {
|
"ALERT": {
|
||||||
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
|
"FORM_CHANGE_CONFIRMATION": "表单内容改变,确认是否取消?"
|
||||||
|
@ -438,6 +443,8 @@
|
||||||
"REPLICATION": "复制",
|
"REPLICATION": "复制",
|
||||||
"EMAIL": "邮箱",
|
"EMAIL": "邮箱",
|
||||||
"LABEL": "标签",
|
"LABEL": "标签",
|
||||||
|
"REPOSITORY": "仓库",
|
||||||
|
"REPO_READ_ONLY": "仓库只读",
|
||||||
"SYSTEM": "系统设置",
|
"SYSTEM": "系统设置",
|
||||||
"VULNERABILITY": "漏洞",
|
"VULNERABILITY": "漏洞",
|
||||||
"CONFIRM_TITLE": "确认取消",
|
"CONFIRM_TITLE": "确认取消",
|
||||||
|
@ -613,9 +620,12 @@
|
||||||
"OS": "操作系统",
|
"OS": "操作系统",
|
||||||
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
"SCAN_COMPLETION_TIME": "扫描完成时间",
|
||||||
"IMAGE_VULNERABILITIES": "镜像缺陷",
|
"IMAGE_VULNERABILITIES": "镜像缺陷",
|
||||||
|
"LEVEL_VULNERABILITIES": "缺陷等级",
|
||||||
"PLACEHOLDER": "未发现任何标签!",
|
"PLACEHOLDER": "未发现任何标签!",
|
||||||
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。",
|
"COPY_ERROR": "拷贝失败,请尝试手动拷贝。",
|
||||||
"FILTER_FOR_TAGS": "过滤项目"
|
"FILTER_FOR_TAGS": "过滤项目",
|
||||||
|
"AUTHOR": "作者",
|
||||||
|
"LABELS": "标签"
|
||||||
},
|
},
|
||||||
"LABEL": {
|
"LABEL": {
|
||||||
"LABEL": "标签",
|
"LABEL": "标签",
|
||||||
|
|
|
@ -72,3 +72,60 @@ uid: mike02
|
||||||
uidnumber: 5001
|
uidnumber: 5001
|
||||||
userpassword: {MD5}wb68DeX0CyENafzUADNn9A==
|
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
|
||||||
|
|
|
@ -43,3 +43,7 @@ User Email Should Exist
|
||||||
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
|
Sign In Harbor ${HARBOR_URL} %{HARBOR_ADMIN} %{HARBOR_PASSWORD}
|
||||||
Switch to User Tag
|
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']
|
click element //vulnerability-config//select/option[@value='none']
|
||||||
sleep 1
|
sleep 1
|
||||||
click element ${config_save_button_xpath}
|
click element ${config_save_button_xpath}
|
||||||
|
|
||||||
Set Scan All To Daily
|
Set Scan All To Daily
|
||||||
click element //vulnerability-config//select
|
click element //vulnerability-config//select
|
||||||
click element //vulnerability-config//select/option[@value='daily']
|
click element //vulnerability-config//select/option[@value='daily']
|
||||||
sleep 1
|
sleep 1
|
||||||
click element ${config_save_button_xpath}
|
click element ${config_save_button_xpath}
|
||||||
|
|
||||||
Click Scan Now
|
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
|
Goto Project Config
|
||||||
Click Element //project-detail//ul/li[contains(.,'Configuration')]
|
Click Element //project-detail//ul/li[contains(.,'Configuration')]
|
||||||
|
Sleep 2
|
||||||
|
|
||||||
Click Project Public
|
Click Project Public
|
||||||
Mouse Down //hbr-project-policy-config//input[@name='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.
|
#It's the log of project.
|
||||||
Go To Project Log
|
Go To Project Log
|
||||||
Click Element xpath=//project-detail//ul/li[3]
|
Click Element xpath=${project_log_xpath}
|
||||||
Sleep 2
|
Sleep 2
|
||||||
|
|
||||||
Switch To Member
|
Switch To Member
|
||||||
Click Element xpath=//project-detail//li[2]
|
Click Element xpath=${project_member_xpath}
|
||||||
Sleep 1
|
Sleep 1
|
||||||
|
|
||||||
Switch To Log
|
Switch To Log
|
||||||
|
|
|
@ -23,3 +23,5 @@ ${project_save_css} html body.no-scrolling harbor-app harbor-shell clr-main-con
|
||||||
${log_xpath} //clr-main-container/div/clr-vertical-nav/div/a[contains(.,'Logs')]
|
${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')]
|
${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/Configuration_Elements.robot
|
||||||
Resource Harbor-Pages/ToolKit.robot
|
Resource Harbor-Pages/ToolKit.robot
|
||||||
Resource Harbor-Pages/Vulnerability.robot
|
Resource Harbor-Pages/Vulnerability.robot
|
||||||
|
Resource Harbor-Pages/LDAP-Mode.robot
|
||||||
Resource Docker-Util.robot
|
Resource Docker-Util.robot
|
||||||
Resource Admiral-Util.robot
|
Resource Admiral-Util.robot
|
||||||
Resource OVA-Util.robot
|
Resource OVA-Util.robot
|
||||||
|
|
|
@ -37,6 +37,56 @@ Test Case - Ldap Sign in and out
|
||||||
Sign In Harbor ${HARBOR_URL} mike zhu88jie
|
Sign In Harbor ${HARBOR_URL} mike zhu88jie
|
||||||
Close Browser
|
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
|
Test Case - Ldap User Create Project
|
||||||
Init Chrome Driver
|
Init Chrome Driver
|
||||||
${d}= Get Current Date result_format=%m%s
|
${d}= Get Current Date result_format=%m%s
|
||||||
|
|
|
@ -35,6 +35,18 @@ Test Case - Vulnerability Data Not Ready
|
||||||
Go To Vulnerability Config
|
Go To Vulnerability Config
|
||||||
Vulnerability Not Ready Config Hint
|
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
|
Test Case - Create An New User
|
||||||
Init Chrome Driver
|
Init Chrome Driver
|
||||||
${d}= Get Current Date result_format=%m%s
|
${d}= Get Current Date result_format=%m%s
|
||||||
|
|
|
@ -73,3 +73,5 @@ Changelog for harbor database schema
|
||||||
- create table `user_group`
|
- 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.
|
- 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 `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