Merge with master

This commit is contained in:
Tan Jiang 2018-03-26 10:37:17 +08:00
commit 381ecc3521
234 changed files with 30882 additions and 14015 deletions

View File

@ -77,7 +77,7 @@ script:
- sudo mkdir -p /etc/ui/ca/
- sudo mv ./tests/ca.crt /etc/ui/ca/
- sudo mkdir -p /harbor
- sudo mv ./VERSION /harbor/VERSION
- sudo mv ./VERSION /harbor/UIVERSION
- sudo service mysql stop
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0
- cat ./src/ui_ng/lib/npm-ut-test-results

View File

@ -85,9 +85,14 @@ BUILDBIN=false
MIGRATORFLAG=false
# version prepare
# for docker image tag
VERSIONTAG=dev
# for harbor package name
PKGVERSIONTAG=dev
# for harbor about dialog
UIVERSIONTAG=dev
VERSIONFILEPATH=$(CURDIR)
VERSIONFILENAME=VERSION
VERSIONFILENAME=UIVERSION
#versions
REGISTRYVERSION=v2.6.2
@ -205,13 +210,13 @@ DOCKERSAVE_PARA=$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
vmware/nginx-photon:$(NGINXVERSION) vmware/registry-photon:$(REGISTRYVERSION)-$(VERSIONTAG) \
vmware/photon:$(PHOTONVERSION)
PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(VERSIONTAG).tgz \
PACKAGE_OFFLINE_PARA=-zcvf harbor-offline-installer-$(PKGVERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tar.gz \
$(HARBORPKG)/prepare $(HARBORPKG)/NOTICE \
$(HARBORPKG)/LICENSE $(HARBORPKG)/install.sh \
$(HARBORPKG)/harbor.cfg $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
$(HARBORPKG)/ha
PACKAGE_ONLINE_PARA=-zcvf harbor-online-installer-$(VERSIONTAG).tgz \
PACKAGE_ONLINE_PARA=-zcvf harbor-online-installer-$(PKGVERSIONTAG).tgz \
$(HARBORPKG)/common/templates $(HARBORPKG)/prepare \
$(HARBORPKG)/LICENSE $(HARBORPKG)/NOTICE \
$(HARBORPKG)/install.sh $(HARBORPKG)/$(DOCKERCOMPOSEFILENAME) \
@ -236,7 +241,7 @@ ifeq ($(MIGRATORFLAG), true)
endif
version:
@printf $(VERSIONTAG) > $(VERSIONFILEPATH)/$(VERSIONFILENAME);
@printf $(UIVERSIONTAG) > $(VERSIONFILEPATH)/$(VERSIONFILENAME);
check_environment:
@$(MAKEPATH)/$(CHECKENVCMD)

View File

@ -1 +1 @@
dev
v1.5.0

1
contrib/helm/harbor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
charts/*

View File

@ -1,5 +1,5 @@
name: harbor
version: 0.1.0
version: 0.1.1
appVersion: 1.4.0
description: An Enterprise-class Docker Registry by VMware
keywords:

View File

@ -152,8 +152,8 @@ The following tables lists the configurable parameters of the Harbor chart and t
| `clair.enabled` | Enable clair? | `true` |
| `clair.image.repository` | Repository for clair image | `vmware/clair-photon` |
| `clair.image.tag` | Tag for clair image | `v2.0.1-v1.4.0`
| `clair.postgresPassword` | password for clair postgres | see values.yaml |
| `clair.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | `clair.pgResources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined |
| `clair.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined
| `postgresql` | Overrides for postgresql chart [values.yaml](https://github.com/kubernetes/charts/blob/f2938a46e3ae8e2512ede1142465004094c3c333/stable/postgresql/values.yaml) | see values.yaml
| | | |

View File

@ -0,0 +1,6 @@
dependencies:
- name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com
version: 0.9.1
digest: sha256:e89ecacdca0cc0414763a586832bf7ca3d57bd25ac8e1a08e41080b610eb5a7d
generated: 2018-03-09T15:34:27.167977722-06:00

View File

@ -0,0 +1,4 @@
dependencies:
- name: postgresql
version: 0.9.1
repository: https://kubernetes-charts.storage.googleapis.com

View File

@ -50,10 +50,10 @@ data:
ADMIRAL_URL: "NA"
RESET: "false"
WITH_CLAIR: "{{ .Values.clair.enabled }}"
CLAIR_DB_HOST: "{{ template "harbor.fullname" . }}-clair-pg"
CLAIR_DB_HOST: "{{ .Release.Name }}-postgresql"
CLAIR_DB_PORT: "5432"
CLAIR_DB: "postgres"
CLAIR_DB_USERNAME: "postgres"
CLAIR_DB: "{{ .Values.clair.postgresDatabase }}"
CLAIR_DB_USERNAME: "{{ .Values.clair.postgresUser }}"
CLAIR_DB_PASSWORD: "{{ .Values.clair.postgresPassword }}"
UAA_ENDPOINT: ""
UAA_CLIENTID: ""

View File

@ -2,7 +2,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ template "harbor.fullname" . }}
name: {{ template "harbor.fullname" . }}-clair
labels:
{{ include "harbor.labels" . | indent 4 }}
component: clair
@ -12,8 +12,7 @@ data:
database:
type: pgsql
options:
source: "postgresql://postgres:{{ .Values.clair.postgresPassword }}@{{ template "harbor.fullname" . }}-clair-pg:5432?sslmode=disable"
source: "postgresql://{{ .Values.clair.postgresUser }}:{{ .Values.clair.postgresPassword }}@{{ .Release.Name }}-postgresql:5432/{{ .Values.clair.postgresDatabase }}?sslmode=disable"
# Number of elements kept in the cache
# Values unlikely to change (e.g. namespaces) are cached in order to save prevent needless roundtrips to the database.
cachesize: 16384

View File

@ -34,7 +34,7 @@ spec:
volumes:
- name: clair-config
configMap:
name: "{{ template "harbor.fullname" . }}"
name: "{{ template "harbor.fullname" . }}-clair"
items:
- key: config.yaml
path: config.yaml

View File

@ -1,11 +0,0 @@
{{ if .Values.clair.enabled }}
apiVersion: v1
kind: Secret
metadata:
name: {{ template "harbor.fullname" . }}-clair-pg-config
labels:
{{ include "harbor.labels" . | indent 4 }}
type: Opaque
data:
POSTGRES_PASSWORD: {{ .Values.clair.postgresPassword | b64enc | quote }}
{{ end }}

View File

@ -1,72 +0,0 @@
{{ if .Values.clair.enabled }}
apiVersion: apps/v1beta2
kind: StatefulSet
metadata:
name: {{ template "harbor.fullname" . }}-clair-pg
labels:
{{ include "harbor.labels" . | indent 4 }}
component: clair-pg
spec:
serviceName: "{{ template "harbor.fullname" . }}-clair-pg"
selector:
matchLabels:
{{ include "harbor.matchLabels" . | indent 6 }}
component: clair-pg
template:
metadata:
name: {{ template "harbor.fullname" . }}-clair-pg
labels:
{{ include "harbor.labels" . | indent 8 }}
component: clair-pg
spec:
containers:
- name: postgres
image: {{ .Values.clair.pgImage.repository }}:{{ .Values.clair.pgImage.tag }}
imagePullPolicy: {{ .Values.clair.pgImage.pullPolicy }}
resources:
{{ toYaml .Values.clair.pgResources | indent 10 }}
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ template "harbor.fullname" . }}-clair-pg-config
key: POSTGRES_PASSWORD
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 100m
memory: 512Mi
volumeMounts:
- name: pgdata
mountPath: /var/lib/postgresql
ports:
- containerPort: 5432
name: postgres-port
protocol: TCP
{{- if not .Values.persistence.enabled }}
volumes:
- name: pgdata
emptyDir: {}
{{- end }}
{{- if .Values.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: pgdata
labels:
{{ include "harbor.labels" . | indent 8 }}
spec:
accessModes: [{{ .Values.clair.volumes.pgData.accessMode | quote }}]
{{- if .Values.clair.volumes.pgData.storageClass }}
{{- if (eq "-" .Values.clair.volumes.pgData.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: "{{ .Values.clair.volumes.pgData.storageClass }}"
{{- end }}
{{- end }}
resources:
requests:
storage: {{ .Values.clair.volumes.pgData.size | quote }}
{{- end -}}
{{- end -}}

View File

@ -1,14 +0,0 @@
{{ if .Values.clair.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ template "harbor.fullname" . }}-clair-pg
labels:
{{ include "harbor.labels" . | indent 4 }}
spec:
ports:
- port: 5432
selector:
{{ include "harbor.matchLabels" . | indent 4 }}
component: clair-pg
{{ end }}

View File

@ -61,7 +61,7 @@ spec:
volumeClaimTemplates:
- metadata:
name: "registry-data"
labels:
labels:
{{ include "harbor.labels" . | indent 8 }}
spec:
accessModes: [{{ .Values.registry.volumes.data.accessMode | quote }}]
@ -76,4 +76,4 @@ spec:
requests:
storage: {{ .Values.registry.volumes.data.size | quote }}
{{- end -}}
{{- end -}}
{{- end -}}

View File

@ -243,20 +243,18 @@ registry:
# memory: 256Mi
# cpu: 100m
## Clair support is not yet fully implemented in the Helm Charts
## Enabling it will just break things.
#
clair:
enabled: true
image:
repository: vmware/clair-photon
tag: v2.0.1-v1.4.0
pullPolicy: IfNotPresent
## The following needs to match the credentials
## in the `postgresql` configuration under the
## `postgresql` namespace below.
postgresPassword: not-a-secure-password
pgImage:
repository: postgres
tag: "9.6.4"
pullPolicy: IfNotPresent
postgresUser: clair
postgresDatabase: clair
# resources:
# requests:
# memory: 256Mi
@ -280,3 +278,13 @@ clair:
#
notary:
enabled: false
## Settings for postgresql dependency.
## see https://github.com/kubernetes/charts/tree/master/stable/postgresql
## for further configurables.
postgresql:
postgresUser: clair
postgresPassword: not-a-secure-password
postgresDatabase: clair
persistence:
enabled: false

View File

@ -44,25 +44,25 @@ You can compile the code by one of the three approaches:
* Get offcial Golang image from docker hub:
```sh
$ docker pull golang:1.7.3
$ docker pull golang:1.9.2
```
* Build, install and bring up Harbor without Notary:
```sh
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7
$ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0
```
* Build, install and bring up Harbor with Notary:
```sh
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7 NOTARYFLAG=true
$ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0 NOTARYFLAG=true
```
* Build, install and bring up Harbor with Clair:
```sh
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.7 CLAIRFLAG=true
$ make install GOBUILDIMAGE=golang:1.9.2 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.3.0 CLAIRFLAG=true
```
#### II. Compile code with your own Golang environment, then build Harbor
@ -118,6 +118,8 @@ REGISTRYSERVER | Remote registry server IP address
REGISTRYUSER | Remote registry server user name
REGISTRYPASSWORD | Remote registry server user password
REGISTRYPROJECTNAME| Project name on remote registry server
VERSIONTAG | Harbor images tag, default: dev
PKGVERSIONTAG | Harbor online and offline version tag, default:dev
* Predefined targets:

View File

@ -1,6 +1,7 @@
**IMPORTANT** This guide is deprecated and not updated any more. We strongly recommend using [Harbor Helm Chart](https://github.com/vmware/harbor/tree/master/contrib/helm/harbor) to deploy latest Harbor release on Kubernetes.
## Integration with Kubernetes
This Document decribes how to deploy Harbor on Kubernetes. It has been verified on **Kubernetes v1.6.5** and **Harbor v1.2.0**
This Document decribes how to deploy Harbor on Kubernetes. It has been verified on **Kubernetes v1.6.5** and **Harbor v1.2.0**
### Prerequisite

View File

@ -906,6 +906,11 @@ paths:
type: string
required: false
description: Repo name for filtering results.
- name: label_id
in: query
type: integer
required: false
description: The ID of label used to filter the result.
- name: page
in: query
type: integer
@ -1144,6 +1149,11 @@ paths:
type: string
required: true
description: Relevant repository name.
- name: label_ids
in: query
type: string
required: false
description: A list of comma separated label IDs.
tags:
- Products
responses:
@ -2464,7 +2474,7 @@ paths:
'403':
description: Only admin has this authority.
'415':
$ref: '#responses/UnsupportedMediaType'
$ref: '#/responses/UnsupportedMediaType'
'500':
description: Unexpected internal errors.
responses:
@ -3346,6 +3356,10 @@ definitions:
description: >-
This attribute restricts what users have the permission to create
project. It can be "everyone" or "adminonly".
read_only:
type: boolean
description: >-
'docker push' is prohibited by Harbor if you set it to true.
self_registration:
type: boolean
description: >-

View File

@ -56,3 +56,4 @@ UAA_VERIFY_CERT=$uaa_verify_cert
UI_URL=http://ui:8080
JOBSERVICE_URL=http://jobservice:8080
REGISTRY_STORAGE_PROVIDER_NAME=$storage_provider_name
READ_ONLY=false

View File

@ -12,7 +12,7 @@ COPY src/ui/static /go/bin/static
COPY src/favicon.ico /go/bin/favicon.ico
RUN mkdir /go/bin/harbor/
COPY VERSION /go/bin/harbor/VERSION
COPY VERSION /go/bin/harbor/UIVERSION
RUN chmod u+x /go/bin/harbor_ui

View File

@ -1,5 +1,7 @@
## Configuration file of Harbor
#This attribute is for migrator to detect the version of the .cfg file, DO NOT MODIFY!
_version = 1.5.0
#The IP address or hostname to access admin UI and registry service.
#DO NOT use localhost or 127.0.0.1, because Harbor needs to be accessed by external clients.
hostname = reg.mydomain.com

View File

@ -277,8 +277,9 @@ create table harbor_resource_label (
label_id int NOT NULL,
# the resource_id is the ID of project when the resource_type is p
# the resource_id is the ID of repository when the resource_type is r
# the resource_id is the name of image when the resource_type is i
resource_id varchar(256) NOT NULL,
resource_id int,
# the resource_name is the name of image when the resource_type is i
resource_name varchar(256),
# 'p' for project
# 'r' for repository
# 'i' for image
@ -286,7 +287,7 @@ create table harbor_resource_label (
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP,
PRIMARY KEY(id),
CONSTRAINT unique_label_resource UNIQUE (label_id,resource_id, resource_type)
CONSTRAINT unique_label_resource UNIQUE (label_id,resource_id, resource_name, resource_type)
);
CREATE TABLE IF NOT EXISTS `alembic_version` (

View File

@ -267,9 +267,12 @@ create table harbor_resource_label (
/*
the resource_id is the ID of project when the resource_type is p
the resource_id is the ID of repository when the resource_type is r
the resource_id is the name of image when the resource_type is i
*/
resource_id varchar(256) NOT NULL,
resource_id int,
/*
the resource_name is the name of image when the resource_type is i
*/
resource_name varchar(256),
/*
'p' for project
'r' for repository
@ -278,7 +281,7 @@ create table harbor_resource_label (
resource_type char(1) NOT NULL,
creation_time timestamp default CURRENT_TIMESTAMP,
update_time timestamp default CURRENT_TIMESTAMP,
UNIQUE (label_id,resource_id, resource_type)
UNIQUE (label_id,resource_id,resource_name,resource_type)
);
create table alembic_version (

View File

@ -7,10 +7,16 @@ if [ -d /etc/registry ]; then
fi
if [ -d /var/lib/registry ]; then
chown 10000:10000 -R /var/lib/registry
fi
fi
if [ -d /storage ]; then
chown 10000:10000 -R /storage
fi
if ! stat -c '%u:%g' /storage | grep -q '10000:10000' ; then
# 10000 is the id of harbor user/group.
# Usually NFS Server does not allow changing owner of the export directory,
# so need to skip this step and requires NFS Server admin to set its owner to 10000.
chown 10000:10000 -R /storage
fi
fi
case "$1" in
*.yaml|*.yml) set -- registry serve "$@" ;;
serve|garbage-collect|help|-*) set -- registry "$@" ;;

View File

@ -1,6 +1,6 @@
FROM vmware/photon:1.0
RUN tdnf distro-sync -y || echo \
RUN tdnf distro-sync -y \
&& tdnf erase vim -y \
&& tdnf install sudo -y >> /dev/null\
&& tdnf clean all \
@ -8,7 +8,7 @@ RUN tdnf distro-sync -y || echo \
&& mkdir /harbor/
HEALTHCHECK CMD curl -s -o /dev/null -w "%{http_code}" 127.0.0.1:8080/api/systeminfo|grep 200
COPY ./make/dev/ui/harbor_ui ./src/favicon.ico ./make/photon/ui/start.sh ./VERSION /harbor/
COPY ./make/dev/ui/harbor_ui ./src/favicon.ico ./make/photon/ui/start.sh ./UIVERSION /harbor/
COPY ./src/ui/views /harbor/views
COPY ./src/ui/static /harbor/static

16
src/Gopkg.lock generated
View File

@ -124,8 +124,8 @@
[[projects]]
name = "github.com/go-sql-driver/mysql"
packages = ["."]
revision = "a732e14c62dde3285440047bba97581bc472ae18"
version = "v1.2"
revision = "a0583e0143b1624142adab07e0e97fe106d99561"
version = "v1.3"
[[projects]]
name = "github.com/gocraft/work"
@ -152,25 +152,29 @@
[[projects]]
name = "github.com/gorilla/handlers"
packages = ["."]
revision = "13d73096a474cac93275c679c7b8a2dc17ddba82"
revision = "90663712d74cb411cbef281bc1e08c19d1a76145"
version = "v1.3.0"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "780415097119f6f61c55475fe59b66f3c3e9ea53"
revision = "7f08801859139f86dfafd1c296e2cba9a80d292e"
version = "v1.6.0"
[[projects]]
branch = "master"
name = "github.com/lib/pq"
packages = [
".",
"oid"
]
revision = "dd1fe2071026ce53f36a39112e645b4d4f5793a4"
revision = "b2004221932bd6b13167ef654c81cffac36f7537"
[[projects]]
name = "github.com/mattn/go-sqlite3"
packages = ["."]
revision = "3fb7a0e792edd47bf0cf1e919dfc14e2be412e15"
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
version = "v1.6.0"
[[projects]]
name = "github.com/miekg/pkcs11"

View File

@ -42,7 +42,11 @@ ignored = ["github.com/vmware/harbor/tests*"]
[[constraint]]
name = "github.com/go-sql-driver/mysql"
version = "=1.2.0"
version = "=1.3.0"
[[constraint]]
name = "github.com/mattn/go-sqlite3"
version = "=1.6.0"
[[constraint]]
name = "github.com/opencontainers/go-digest"
@ -54,4 +58,12 @@ ignored = ["github.com/vmware/harbor/tests*"]
[[constraint]]
name = "github.com/stretchr/testify"
version = "=1.2.0"
version = "=1.2.0"
[[constraint]]
name = "github.com/gorilla/handlers"
version = "=1.3.0"
[[constraint]]
name = "github.com/gorilla/mux"
version = "=1.6.0"

View File

@ -48,6 +48,7 @@ var (
common.EmailInsecure: true,
common.LDAPVerifyCert: true,
common.UAAVerifyCert: true,
common.ReadOnly: true,
}
mapKeys = map[string]bool{
common.ScanAllPolicy: true,

View File

@ -152,6 +152,10 @@ var (
common.UIURL: "UI_URL",
common.JobServiceURL: "JOBSERVICE_URL",
common.RegistryStorageProviderName: "REGISTRY_STORAGE_PROVIDER_NAME",
common.ReadOnly: &parser{
env: "READ_ONLY",
parse: parseStringToBool,
},
}
// configurations need read from environment variables

View File

@ -99,4 +99,5 @@ const (
RegistryStorageProviderName = "registry_storage_provider_name"
UserMember = "u"
GroupMember = "g"
ReadOnly = "read_only"
)

View File

@ -47,15 +47,6 @@ func GetRepositoryByName(name string) (*models.RepoRecord, error) {
return &r, err
}
// GetAllRepositories ...
func GetAllRepositories() ([]*models.RepoRecord, error) {
o := GetOrmer()
var repos []*models.RepoRecord
_, err := o.QueryTable("repository").Limit(-1).
OrderBy("Name").All(&repos)
return repos, err
}
// DeleteRepository ...
func DeleteRepository(name string) error {
o := GetOrmer()
@ -94,18 +85,6 @@ func RepositoryExists(name string) bool {
return o.QueryTable("repository").Filter("name", name).Exist()
}
// GetRepositoryByProjectName ...
func GetRepositoryByProjectName(name string) ([]*models.RepoRecord, error) {
sql := `select * from repository
where project_id = (
select project_id from project
where name = ?
)`
repos := []*models.RepoRecord{}
_, err := GetOrmer().Raw(sql, name).QueryRows(&repos)
return repos, err
}
//GetTopRepos returns the most popular repositories whose project ID is
// in projectIDs
func GetTopRepos(projectIDs []int64, n int) ([]*models.RepoRecord, error) {
@ -124,46 +103,78 @@ func GetTopRepos(projectIDs []int64, n int) ([]*models.RepoRecord, error) {
}
// GetTotalOfRepositories ...
func GetTotalOfRepositories(name string) (int64, error) {
qs := GetOrmer().QueryTable(&models.RepoRecord{})
if len(name) != 0 {
qs = qs.Filter("Name__contains", name)
func GetTotalOfRepositories(query ...*models.RepositoryQuery) (int64, error) {
sql, params := repositoryQueryConditions(query...)
sql = `select count(*) ` + sql
var total int64
if err := GetOrmer().Raw(sql, params).QueryRow(&total); err != nil {
return 0, err
}
return qs.Count()
return total, nil
}
// GetTotalOfRepositoriesByProject ...
func GetTotalOfRepositoriesByProject(projectIDs []int64, name string) (int64, error) {
if len(projectIDs) == 0 {
return 0, nil
}
qs := GetOrmer().QueryTable(&models.RepoRecord{}).
Filter("project_id__in", projectIDs)
if len(name) != 0 {
qs = qs.Filter("Name__contains", name)
}
return qs.Count()
}
// GetRepositoriesByProject ...
func GetRepositoriesByProject(projectID int64, name string,
limit, offset int64) ([]*models.RepoRecord, error) {
// GetRepositories ...
func GetRepositories(query ...*models.RepositoryQuery) ([]*models.RepoRecord, error) {
repositories := []*models.RepoRecord{}
qs := GetOrmer().QueryTable(&models.RepoRecord{}).
Filter("ProjectID", projectID)
if len(name) != 0 {
qs = qs.Filter("Name__contains", name)
sql, params := repositoryQueryConditions(query...)
sql = `select r.repository_id, r.name, r.project_id, r.description, r.pull_count,
r.star_count, r.creation_time, r.update_time ` + sql + `order by r.name `
if len(query) > 0 && query[0] != nil {
page, size := query[0].Page, query[0].Size
if size > 0 {
sql += `limit ? `
params = append(params, size)
if page > 0 {
sql += `offset ? `
params = append(params, size*(page-1))
}
}
}
if limit > 0 {
qs = qs.Limit(limit).Offset(offset)
}
_, err := qs.All(&repositories)
return repositories, err
if _, err := GetOrmer().Raw(sql, params).QueryRows(&repositories); err != nil {
return nil, err
}
return repositories, nil
}
func repositoryQueryConditions(query ...*models.RepositoryQuery) (string, []interface{}) {
params := []interface{}{}
sql := `from repository r `
if len(query) == 0 || query[0] == nil {
return sql, params
}
q := query[0]
if len(q.ProjectName) > 0 {
sql += `join project p on r.project_id = p.project_id `
}
if q.LabelID > 0 {
sql += `join harbor_resource_label rl on r.repository_id = rl.resource_id
and rl.resource_type = 'r' `
}
sql += `where 1=1 `
if len(q.Name) > 0 {
sql += `and r.name like ? `
params = append(params, "%"+Escape(q.Name)+"%")
}
if len(q.ProjectIDs) > 0 {
sql += fmt.Sprintf(`and r.project_id in ( %s ) `,
paramPlaceholder(len(q.ProjectIDs)))
params = append(params, q.ProjectIDs)
}
if len(q.ProjectName) > 0 {
sql += `and p.name = ? `
params = append(params, q.ProjectName)
}
if q.LabelID > 0 {
sql += `and rl.label_id = ? `
params = append(params, q.LabelID)
}
return sql, params
}

View File

@ -16,10 +16,11 @@ package dao
import (
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vmware/harbor/src/common"
"github.com/vmware/harbor/src/common/models"
)
@ -32,61 +33,93 @@ var (
}
)
func TestGetRepositoryByProjectName(t *testing.T) {
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
}
}()
func TestGetTotalOfRepositories(t *testing.T) {
total, err := GetTotalOfRepositories()
require.Nil(t, err)
repositories, err := GetRepositoryByProjectName(project)
if err != nil {
t.Fatalf("failed to get repositories of project %s: %v",
project, err)
}
err = addRepository(repository)
require.Nil(t, err)
defer deleteRepository(name)
if len(repositories) == 0 {
t.Fatal("unexpected length of repositories: 0, at least 1")
}
n, err := GetTotalOfRepositories()
require.Nil(t, err)
assert.Equal(t, total+1, n)
}
exist := false
for _, repo := range repositories {
if repo.Name == name {
exist = true
func TestGetRepositories(t *testing.T) {
// no query
repositories, err := GetRepositories()
require.Nil(t, err)
n := len(repositories)
err = addRepository(repository)
require.Nil(t, err)
defer deleteRepository(name)
repositories, err = GetRepositories()
require.Nil(t, err)
assert.Equal(t, n+1, len(repositories))
// query by name
repositories, err = GetRepositories(&models.RepositoryQuery{
Name: name,
})
require.Nil(t, err)
require.Equal(t, 1, len(repositories))
assert.Equal(t, name, repositories[0].Name)
// query by project name
repositories, err = GetRepositories(&models.RepositoryQuery{
ProjectName: project,
})
require.Nil(t, err)
found := false
for _, repository := range repositories {
if repository.Name == name {
found = true
break
}
}
if !exist {
t.Errorf("there is no repository whose name is %s", name)
}
}
assert.True(t, found)
func TestGetTotalOfRepositories(t *testing.T) {
total, err := GetTotalOfRepositories("")
if err != nil {
t.Fatalf("failed to get total of repositoreis: %v", err)
}
if err := addRepository(repository); err != nil {
t.Fatalf("failed to add repository %s: %v", name, err)
}
defer func() {
if err := deleteRepository(name); err != nil {
t.Fatalf("failed to delete repository %s: %v", name, err)
// query by project ID
repositories, err = GetRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{1},
})
require.Nil(t, err)
found = false
for _, repository := range repositories {
if repository.Name == name {
found = true
break
}
}()
n, err := GetTotalOfRepositories("")
if err != nil {
t.Fatalf("failed to get total of repositoreis: %v", err)
}
assert.True(t, found)
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
// query by label ID
labelID, err := AddLabel(&models.Label{
Name: "label_for_test",
})
require.Nil(t, err)
defer DeleteLabel(labelID)
r, err := GetRepositoryByName(name)
require.Nil(t, err)
rlID, err := AddResourceLabel(&models.ResourceLabel{
LabelID: labelID,
ResourceID: r.RepositoryID,
ResourceType: common.ResourceTypeRepository,
})
require.Nil(t, err)
defer DeleteResourceLabel(rlID)
repositories, err = GetRepositories(&models.RepositoryQuery{
LabelID: labelID,
})
require.Nil(t, err)
require.Equal(t, 1, len(repositories))
assert.Equal(t, name, repositories[0].Name)
}
func TestGetTopRepos(t *testing.T) {
@ -149,112 +182,6 @@ func TestGetTopRepos(t *testing.T) {
require.Equal(topRepos[0].Name, repository3.Name)
}
func TestGetTotalOfRepositoriesByProject(t *testing.T) {
var projectID int64 = 1
repoName := "library/total_count"
total, err := GetTotalOfRepositoriesByProject([]int64{projectID}, repoName)
if err != nil {
t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
return
}
if err := addRepository(&models.RepoRecord{
Name: repoName,
ProjectID: projectID,
}); err != nil {
t.Errorf("failed to add repository %s: %v", repoName, err)
return
}
defer func() {
if err := deleteRepository(repoName); err != nil {
t.Errorf("failed to delete repository %s: %v", name, err)
return
}
}()
n, err := GetTotalOfRepositoriesByProject([]int64{projectID}, repoName)
if err != nil {
t.Errorf("failed to get total of repositoreis of project %d: %v", projectID, err)
return
}
if n != total+1 {
t.Errorf("unexpected total: %d != %d", n, total+1)
}
}
func TestGetRepositoriesByProject(t *testing.T) {
var projectID int64 = 1
repoName := "library/repository"
if err := addRepository(&models.RepoRecord{
Name: repoName,
ProjectID: projectID,
}); err != nil {
t.Errorf("failed to add repository %s: %v", repoName, err)
return
}
defer func() {
if err := deleteRepository(repoName); err != nil {
t.Errorf("failed to delete repository %s: %v", name, err)
return
}
}()
repositories, err := GetRepositoriesByProject(projectID, repoName, 10, 0)
if err != nil {
t.Errorf("failed to get repositoreis of project %d: %v", projectID, err)
return
}
t.Log(repositories)
for _, repository := range repositories {
if repository.Name == repoName {
return
}
}
t.Errorf("repository %s not found", repoName)
}
func TestGetAllRepositories(t *testing.T) {
require := require.New(t)
var repos []*models.RepoRecord
repos, err := GetAllRepositories()
require.NoError(err)
allBefore := len(repos)
project1 := models.Project{
OwnerID: 1,
Name: "projectRepo",
}
var err2 error
project1.ProjectID, err2 = AddProject(project1)
require.NoError(err2)
for i := 0; i < 1200; i++ {
end := strconv.Itoa(i)
repoRecord := models.RepoRecord{
Name: "test" + end,
ProjectID: project1.ProjectID,
}
err := AddRepository(repoRecord)
require.NoError(err)
}
repos, err = GetAllRepositories()
require.NoError(err)
allAfter := len(repos)
require.Equal(allAfter, allBefore+1200)
err = clearRepositoryData()
require.NoError(err)
}
func addRepository(repository *models.RepoRecord) error {
return AddRepository(*repository)
}

View File

@ -29,14 +29,26 @@ func AddResourceLabel(rl *models.ResourceLabel) (int64, error) {
return GetOrmer().Insert(rl)
}
// GetResourceLabel specified by ID
func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel, error) {
// GetResourceLabel specified by resource ID or name
// Get the ResourceLabel by ResourceID if rIDOrName is int
// Get the ResourceLabel by ResourceName if rIDOrName is string
func GetResourceLabel(rType string, rIDOrName interface{}, labelID int64) (*models.ResourceLabel, error) {
rl := &models.ResourceLabel{
ResourceType: rType,
ResourceID: rID,
LabelID: labelID,
}
if err := GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID"); err != nil {
var err error
id, ok := rIDOrName.(int64)
if ok {
rl.ResourceID = id
err = GetOrmer().Read(rl, "ResourceType", "ResourceID", "LabelID")
} else {
rl.ResourceName = rIDOrName.(string)
err = GetOrmer().Read(rl, "ResourceType", "ResourceName", "LabelID")
}
if err != nil {
if err == orm.ErrNoRows {
return nil, nil
}
@ -47,13 +59,20 @@ func GetResourceLabel(rType, rID string, labelID int64) (*models.ResourceLabel,
}
// GetLabelsOfResource returns the label list of the resource
func GetLabelsOfResource(rType, rID string) ([]*models.Label, error) {
// Get the labels by ResourceID if rIDOrName is int, or get the labels by ResourceName
func GetLabelsOfResource(rType string, rIDOrName interface{}) ([]*models.Label, error) {
sql := `select l.id, l.name, l.description, l.color, l.scope, l.project_id, l.creation_time, l.update_time
from harbor_resource_label rl
join harbor_label l on rl.label_id=l.id
where rl.resource_type = ? and rl.resource_id = ?`
where rl.resource_type = ? and`
if _, ok := rIDOrName.(int64); ok {
sql += ` rl.resource_id = ?`
} else {
sql += ` rl.resource_name = ?`
}
labels := []*models.Label{}
_, err := GetOrmer().Raw(sql, rType, rID).QueryRows(&labels)
_, err := GetOrmer().Raw(sql, rType, rIDOrName).QueryRows(&labels)
return labels, err
}
@ -65,10 +84,39 @@ func DeleteResourceLabel(id int64) error {
return err
}
// DeleteLabelsOfResource removes all labels of resource specified by rType and rID
func DeleteLabelsOfResource(rType, rID string) error {
_, err := GetOrmer().QueryTable(&models.ResourceLabel{}).
Filter("ResourceType", rType).
Filter("ResourceID", rID).Delete()
// DeleteLabelsOfResource removes all labels of the resource
func DeleteLabelsOfResource(rType string, rIDOrName interface{}) error {
qs := GetOrmer().QueryTable(&models.ResourceLabel{}).
Filter("ResourceType", rType)
if _, ok := rIDOrName.(int64); ok {
qs = qs.Filter("ResourceID", rIDOrName)
} else {
qs = qs.Filter("ResourceName", rIDOrName)
}
_, err := qs.Delete()
return err
}
// ListResourceLabels lists ResourceLabel according to the query conditions
func ListResourceLabels(query ...*models.ResourceLabelQuery) ([]*models.ResourceLabel, error) {
qs := GetOrmer().QueryTable(&models.ResourceLabel{})
if len(query) > 0 {
q := query[0]
if q.LabelID > 0 {
qs = qs.Filter("LabelID", q.LabelID)
}
if len(q.ResourceType) > 0 {
qs = qs.Filter("ResourceType", q.ResourceType)
}
if q.ResourceID > 0 {
qs = qs.Filter("ResourceID", q.ResourceID)
}
if len(q.ResourceName) > 0 {
qs = qs.Filter("ResourceName", q.ResourceName)
}
}
rls := []*models.ResourceLabel{}
_, err := qs.All(&rls)
return rls, err
}

View File

@ -32,7 +32,7 @@ func TestMethodsOfResourceLabel(t *testing.T) {
require.Nil(t, err)
defer DeleteLabel(labelID)
resourceID := "1"
var resourceID int64 = 1
resourceType := common.ResourceTypeRepository
// add
@ -56,6 +56,16 @@ func TestMethodsOfResourceLabel(t *testing.T) {
require.Equal(t, 1, len(labels))
assert.Equal(t, id, r.ID)
// list
rls, err := ListResourceLabels(&models.ResourceLabelQuery{
LabelID: labelID,
ResourceType: resourceType,
ResourceID: resourceID,
})
require.Nil(t, err)
require.Equal(t, 1, len(rls))
assert.Equal(t, id, rls[0].ID)
// delete
err = DeleteResourceLabel(id)
require.Nil(t, err)

View File

@ -69,7 +69,8 @@ func (l *Label) Valid(v *validation.Validation) {
type ResourceLabel struct {
ID int64 `orm:"pk;auto;column(id)"`
LabelID int64 `orm:"column(label_id)"`
ResourceID string `orm:"column(resource_id)"`
ResourceID int64 `orm:"column(resource_id)"`
ResourceName string `orm:"column(resource_name)"`
ResourceType string `orm:"column(resource_type)"`
CreationTime time.Time `orm:"column(creation_time)"`
UpdateTime time.Time `orm:"column(update_time)"`
@ -79,3 +80,11 @@ type ResourceLabel struct {
func (r *ResourceLabel) TableName() string {
return "harbor_resource_label"
}
// ResourceLabelQuery : query parameters for the mapping relationships of resource and label
type ResourceLabelQuery struct {
LabelID int64
ResourceID int64
ResourceName string
ResourceType string
}

View File

@ -33,7 +33,7 @@ type Project struct {
OwnerName string `orm:"-" json:"owner_name"`
Togglable bool `orm:"-" json:"togglable"`
Role int `orm:"-" json:"current_user_role_id"`
RepoCount int `orm:"-" json:"repo_count"`
RepoCount int64 `orm:"-" json:"repo_count"`
Metadata map[string]string `orm:"-" json:"metadata"`
}

View File

@ -37,3 +37,12 @@ type RepoRecord struct {
func (rp *RepoRecord) TableName() string {
return RepoTable
}
// RepositoryQuery : query parameters for repository
type RepositoryQuery struct {
Name string
ProjectIDs []int64
ProjectName string
LabelID int64
Pagination
}

View File

@ -75,6 +75,7 @@ var adminServerDefaultConfig = map[string]interface{}{
common.UAAVerifyCert: false,
common.UIURL: "http://myui:8888/",
common.JobServiceURL: "http://myjob:8888/",
common.ReadOnly: false,
}
// NewAdminserver returns a mock admin server

View File

@ -2,6 +2,7 @@ package registry
import (
"github.com/vmware/harbor/src/common/dao"
common_models "github.com/vmware/harbor/src/common/models"
"github.com/vmware/harbor/src/common/utils/log"
"github.com/vmware/harbor/src/replication"
"github.com/vmware/harbor/src/replication/models"
@ -30,7 +31,9 @@ func (ha *HarborAdaptor) GetNamespace(name string) models.Namespace {
//GetRepositories is used to get all the repositories under the specified namespace
func (ha *HarborAdaptor) GetRepositories(namespace string) []models.Repository {
repos, err := dao.GetRepositoryByProjectName(namespace)
repos, err := dao.GetRepositories(&common_models.RepositoryQuery{
ProjectName: namespace,
})
if err != nil {
log.Errorf("failed to get repositories under namespace %s: %v", namespace, err)
return nil

View File

@ -60,6 +60,7 @@ var (
common.UAAClientSecret,
common.UAAEndpoint,
common.UAAVerifyCert,
common.ReadOnly,
}
stringKeys = []string{
@ -97,6 +98,7 @@ var (
common.SelfRegistration,
common.LDAPVerifyCert,
common.UAAVerifyCert,
common.ReadOnly,
}
passwordKeys = []string{

View File

@ -199,6 +199,8 @@ func (p *ProjectAPI) Get() {
}
}
p.populateProperties(p.project)
p.Data["json"] = p.project
p.ServeJSON()
}
@ -268,7 +270,9 @@ func (p *ProjectAPI) Deletable() {
}
func deletable(projectID int64) (*deletableResp, error) {
count, err := dao.GetTotalOfRepositoriesByProject([]int64{projectID}, "")
count, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{projectID},
})
if err != nil {
return nil, err
}
@ -372,25 +376,7 @@ func (p *ProjectAPI) List() {
}
for _, project := range result.Projects {
if p.SecurityCtx.IsAuthenticated() {
roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
if len(roles) != 0 {
project.Role = roles[0]
}
if project.Role == common.RoleProjectAdmin ||
p.SecurityCtx.IsSysAdmin() {
project.Togglable = true
}
}
repos, err := dao.GetRepositoryByProjectName(project.Name)
if err != nil {
log.Errorf("failed to get repositories of project %s: %v", project.Name, err)
p.CustomAbort(http.StatusInternalServerError, "")
}
project.RepoCount = len(repos)
p.populateProperties(project)
}
p.SetPaginationHeader(result.Total, page, size)
@ -398,6 +384,30 @@ func (p *ProjectAPI) List() {
p.ServeJSON()
}
func (p *ProjectAPI) populateProperties(project *models.Project) {
if p.SecurityCtx.IsAuthenticated() {
roles := p.SecurityCtx.GetProjectRoles(project.ProjectID)
if len(roles) != 0 {
project.Role = roles[0]
}
if project.Role == common.RoleProjectAdmin ||
p.SecurityCtx.IsSysAdmin() {
project.Togglable = true
}
}
total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{project.ProjectID},
})
if err != nil {
log.Errorf("failed to get total of repositories of project %d: %v", project.ProjectID, err)
p.CustomAbort(http.StatusInternalServerError, "")
}
project.RepoCount = total
}
// Put ...
func (p *ProjectAPI) Put() {
if !p.SecurityCtx.IsAuthenticated() {

View File

@ -96,6 +96,12 @@ func (ra *RepositoryAPI) Get() {
return
}
labelID, err := ra.GetInt64("label_id", 0)
if err != nil {
ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id")))
return
}
exist, err := ra.ProjectMgr.Exists(projectID)
if err != nil {
ra.ParseAndHandleError(fmt.Sprintf("failed to check the existence of project %d",
@ -117,33 +123,33 @@ func (ra *RepositoryAPI) Get() {
return
}
keyword := ra.GetString("q")
query := &models.RepositoryQuery{
ProjectIDs: []int64{projectID},
Name: ra.GetString("q"),
LabelID: labelID,
}
query.Page, query.Size = ra.GetPaginationParams()
total, err := dao.GetTotalOfRepositoriesByProject(
[]int64{projectID}, keyword)
total, err := dao.GetTotalOfRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get total of repositories of project %d: %v",
projectID, err))
return
}
page, pageSize := ra.GetPaginationParams()
repositories, err := getRepositories(projectID,
keyword, pageSize, pageSize*(page-1))
repositories, err := getRepositories(query)
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to get repository: %v", err))
return
}
ra.SetPaginationHeader(total, page, pageSize)
ra.SetPaginationHeader(total, query.Page, query.Size)
ra.Data["json"] = repositories
ra.ServeJSON()
}
func getRepositories(projectID int64, keyword string,
limit, offset int64) ([]*repoResp, error) {
repositories, err := dao.GetRepositoriesByProject(projectID, keyword, limit, offset)
func getRepositories(query *models.RepositoryQuery) ([]*repoResp, error) {
repositories, err := dao.GetRepositories(query)
if err != nil {
return nil, err
}
@ -171,8 +177,7 @@ func assembleRepos(repositories []*models.RepoRecord) ([]*repoResp, error) {
}
repo.TagsCount = int64(len(tags))
labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository,
strconv.FormatInt(repository.RepositoryID, 10))
labels, err := dao.GetLabelsOfResource(common.ResourceTypeRepository, repository.RepositoryID)
if err != nil {
log.Errorf("failed to get labels of repository %s: %v", repository.Name, err)
} else {
@ -385,6 +390,11 @@ func (ra *RepositoryAPI) GetTag() {
// GetTags returns tags of a repository
func (ra *RepositoryAPI) GetTags() {
repoName := ra.GetString(":splat")
labelID, err := ra.GetInt64("label_id", 0)
if err != nil {
ra.HandleBadRequest(fmt.Sprintf("invalid label_id: %s", ra.GetString("label_id")))
return
}
projectName, _ := utils.ParseRepository(repoName)
exist, err := ra.ProjectMgr.Exists(projectName)
@ -420,7 +430,31 @@ func (ra *RepositoryAPI) GetTags() {
return
}
ra.Data["json"] = assembleTags(client, repoName, tags, ra.SecurityCtx.GetUsername())
// filter tags by label ID
if labelID > 0 {
rls, err := dao.ListResourceLabels(&models.ResourceLabelQuery{
LabelID: labelID,
ResourceType: common.ResourceTypeImage,
})
if err != nil {
ra.HandleInternalServerError(fmt.Sprintf("failed to list resource labels: %v", err))
return
}
labeledTags := map[string]struct{}{}
for _, rl := range rls {
labeledTags[strings.Split(rl.ResourceName, ":")[1]] = struct{}{}
}
ts := []string{}
for _, tag := range tags {
if _, ok := labeledTags[tag]; ok {
ts = append(ts, tag)
}
}
tags = ts
}
ra.Data["json"] = assembleTags(client, repoName, tags,
ra.SecurityCtx.GetUsername())
ra.ServeJSON()
}
@ -443,6 +477,15 @@ func assembleTags(client *registry.Repository, repository string,
for _, t := range tags {
item := &tagResp{}
// labels
image := fmt.Sprintf("%s:%s", repository, t)
labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image)
if err != nil {
log.Errorf("failed to get labels of image %s: %v", image, err)
} else {
item.Labels = labels
}
// the detail information of tag
tagDetail, err := getTagDetail(client, t)
if err != nil {
@ -468,15 +511,6 @@ func assembleTags(client *registry.Repository, repository string,
}
}
// labels
image := fmt.Sprintf("%s:%s", repository, t)
labels, err := dao.GetLabelsOfResource(common.ResourceTypeImage, image)
if err != nil {
log.Errorf("failed to get labels of image %s: %v", image, err)
} else {
item.Labels = labels
}
result = append(result, item)
}

View File

@ -144,24 +144,20 @@ func (r *RepositoryLabelAPI) AddToImage() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeImage,
ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
ResourceName: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
}
r.addLabel(rl)
}
// RemoveFromImage removes the label from an image
func (r *RepositoryLabelAPI) RemoveFromImage() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeImage,
ResourceID: fmt.Sprintf("%s:%s", r.repository.Name, r.tag),
}
r.removeLabel(rl)
r.removeLabel(common.ResourceTypeImage,
fmt.Sprintf("%s:%s", r.repository.Name, r.tag), r.label.ID)
}
// GetOfRepository returns labels of a repository
func (r *RepositoryLabelAPI) GetOfRepository() {
r.getLabels(common.ResourceTypeRepository, strconv.FormatInt(r.repository.RepositoryID, 10))
r.getLabels(common.ResourceTypeRepository, r.repository.RepositoryID)
}
// AddToRepository adds the label to a repository
@ -169,26 +165,21 @@ func (r *RepositoryLabelAPI) AddToRepository() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeRepository,
ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
ResourceID: r.repository.RepositoryID,
}
r.addLabel(rl)
}
// RemoveFromRepository removes the label from a repository
func (r *RepositoryLabelAPI) RemoveFromRepository() {
rl := &models.ResourceLabel{
LabelID: r.label.ID,
ResourceType: common.ResourceTypeRepository,
ResourceID: strconv.FormatInt(r.repository.RepositoryID, 10),
}
r.removeLabel(rl)
r.removeLabel(common.ResourceTypeRepository, r.repository.RepositoryID, r.label.ID)
}
func (r *RepositoryLabelAPI) getLabels(rType, rID string) {
labels, err := dao.GetLabelsOfResource(rType, rID)
func (r *RepositoryLabelAPI) getLabels(rType string, rIDOrName interface{}) {
labels, err := dao.GetLabelsOfResource(rType, rIDOrName)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %s: %v",
rType, rID, err))
r.HandleInternalServerError(fmt.Sprintf("failed to get labels of resource %s %v: %v",
rType, rIDOrName, err))
return
}
r.Data["json"] = labels
@ -196,10 +187,16 @@ func (r *RepositoryLabelAPI) getLabels(rType, rID string) {
}
func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
var rIDOrName interface{}
if rl.ResourceID != 0 {
rIDOrName = rl.ResourceID
} else {
rIDOrName = rl.ResourceName
}
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rIDOrName, rl.LabelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v",
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %v: %v",
rl.LabelID, rl.ResourceType, rIDOrName, err))
return
}
@ -208,8 +205,8 @@ func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
return
}
if _, err := dao.AddResourceLabel(rl); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to add label %d to resource %s %s: %v",
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
r.HandleInternalServerError(fmt.Sprintf("failed to add label %d to resource %s %v: %v",
rl.LabelID, rl.ResourceType, rIDOrName, err))
return
}
@ -217,22 +214,22 @@ func (r *RepositoryLabelAPI) addLabel(rl *models.ResourceLabel) {
r.Redirect(http.StatusOK, strconv.FormatInt(rl.LabelID, 10))
}
func (r *RepositoryLabelAPI) removeLabel(rl *models.ResourceLabel) {
rlabel, err := dao.GetResourceLabel(rl.ResourceType, rl.ResourceID, rl.LabelID)
func (r *RepositoryLabelAPI) removeLabel(rType string, rIDOrName interface{}, labelID int64) {
rl, err := dao.GetResourceLabel(rType, rIDOrName, labelID)
if err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %s: %v",
rl.LabelID, rl.ResourceType, rl.ResourceID, err))
r.HandleInternalServerError(fmt.Sprintf("failed to check the existence of label %d for resource %s %v: %v",
labelID, rType, rIDOrName, err))
return
}
if rlabel == nil {
if rl == nil {
r.HandleNotFound(fmt.Sprintf("label %d of resource %s %s not found",
rl.LabelID, rl.ResourceType, rl.ResourceID))
labelID, rType, rIDOrName))
return
}
if err = dao.DeleteResourceLabel(rlabel.ID); err != nil {
if err = dao.DeleteResourceLabel(rl.ID); err != nil {
r.HandleInternalServerError(fmt.Sprintf("failed to delete resource label record %d: %v",
rlabel.ID, err))
rl.ID, err))
return
}
}

View File

@ -27,7 +27,7 @@ func TestGetRepos(t *testing.T) {
assert := assert.New(t)
apiTest := newHarborAPI()
projectID := "1"
keyword := "hello-world"
keyword := "library/hello-world"
fmt.Println("Testing Repos Get API")
//-------------------case 1 : response code = 200------------------------//

View File

@ -99,13 +99,15 @@ func (s *SearchAPI) Get() {
}
}
repos, err := dao.GetRepositoryByProjectName(p.Name)
total, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{p.ProjectID},
})
if err != nil {
log.Errorf("failed to get repositories of project %s: %v", p.Name, err)
log.Errorf("failed to get total of repositories of project %d: %v", p.ProjectID, err)
s.CustomAbort(http.StatusInternalServerError, "")
}
p.RepoCount = len(repos)
p.RepoCount = total
projectResult = append(projectResult, p)
}
@ -124,7 +126,7 @@ func (s *SearchAPI) Get() {
func filterRepositories(projects []*models.Project, keyword string) (
[]map[string]interface{}, error) {
repositories, err := dao.GetAllRepositories()
repositories, err := dao.GetRepositories()
if err != nil {
return nil, err
}

View File

@ -64,17 +64,22 @@ func (s *StatisticAPI) Get() {
}
statistic[PubPC] = (int64)(len(pubProjs))
ids := []int64{}
for _, p := range pubProjs {
ids = append(ids, p.ProjectID)
if len(pubProjs) == 0 {
statistic[PubRC] = 0
} else {
ids := []int64{}
for _, p := range pubProjs {
ids = append(ids, p.ProjectID)
}
n, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: ids,
})
if err != nil {
log.Errorf("failed to get total of public repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[PubRC] = n
}
n, err := dao.GetTotalOfRepositoriesByProject(ids, "")
if err != nil {
log.Errorf("failed to get total of public repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
}
statistic[PubRC] = n
if s.SecurityCtx.IsSysAdmin() {
result, err := s.ProjectMgr.List(nil)
@ -85,7 +90,7 @@ func (s *StatisticAPI) Get() {
statistic[TPC] = result.Total
statistic[PriPC] = result.Total - statistic[PubPC]
n, err := dao.GetTotalOfRepositories("")
n, err := dao.GetTotalOfRepositories()
if err != nil {
log.Errorf("failed to get total of repositories: %v", err)
s.CustomAbort(http.StatusInternalServerError, "")
@ -107,20 +112,25 @@ func (s *StatisticAPI) Get() {
}
statistic[PriPC] = result.Total
if result.Total == 0 {
statistic[PriRC] = 0
} else {
ids := []int64{}
for _, p := range result.Projects {
ids = append(ids, p.ProjectID)
}
ids := []int64{}
for _, p := range result.Projects {
ids = append(ids, p.ProjectID)
n, err := dao.GetTotalOfRepositories(&models.RepositoryQuery{
ProjectIDs: ids,
})
if err != nil {
s.HandleInternalServerError(fmt.Sprintf(
"failed to get total of repositories for user %s: %v",
s.username, err))
return
}
statistic[PriRC] = n
}
n, err = dao.GetTotalOfRepositoriesByProject(ids, "")
if err != nil {
s.HandleInternalServerError(fmt.Sprintf(
"failed to get total of repositories for user %s: %v",
s.username, err))
return
}
statistic[PriRC] = n
}
s.Data["json"] = statistic

View File

@ -37,7 +37,7 @@ type SystemInfoAPI struct {
}
const defaultRootCert = "/etc/ui/ca/ca.crt"
const harborVersionFile = "/harbor/VERSION"
const harborVersionFile = "/harbor/UIVERSION"
//SystemInfo models for system info.
type SystemInfo struct {

View File

@ -86,7 +86,7 @@ func SyncRegistry(pm promgr.ProjectManager) error {
}
var repoRecordsInDB []*models.RepoRecord
repoRecordsInDB, err = dao.GetAllRepositories()
repoRecordsInDB, err = dao.GetRepositories()
if err != nil {
log.Errorf("error occurred while getting all registories. %v", err)
return err

View File

@ -495,3 +495,13 @@ func UAASettings() (*models.UAASettings, error) {
}
return us, nil
}
// ReadOnly returns a bool to indicates if Harbor is in read only mode.
func ReadOnly() bool {
cfg, err := mg.Get()
if err != nil {
log.Errorf("Failed to get configuration, will return false as read only, error: %v", err)
return false
}
return cfg[common.ReadOnly].(bool)
}

View File

@ -146,6 +146,9 @@ func TestConfig(t *testing.T) {
if !WithAdmiral() {
t.Errorf("WithAdmiral should be true")
}
if ReadOnly() {
t.Errorf("ReadOnly should be false")
}
if AdmiralEndpoint() != "http://www.vmware.com" {
t.Errorf("Unexpected admiral endpoint: %s", AdmiralEndpoint())
}

76
src/ui/filter/readonly.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright (c) 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.
package filter
import (
"net/http"
"regexp"
"github.com/astaxie/beego/context"
"github.com/vmware/harbor/src/ui/config"
)
const (
repoURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)(?:[a-z0-9]+(?:[._-][a-z0-9]+)*)$`
tagURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)tags/([\w][\w.-]{0,127})$`
labelURL = `^/api/repositories/((?:[a-z0-9]+(?:[._-][a-z0-9]+)*/)+)tags/([\w][\w.-]{0,127})/labels/[0-9]+$`
)
//ReadonlyFilter filters the delete repo/tag request and returns 503.
func ReadonlyFilter(ctx *context.Context) {
filter(ctx.Request, ctx.ResponseWriter)
}
func filter(req *http.Request, resp http.ResponseWriter) {
if !config.ReadOnly() {
return
}
if req.Method != http.MethodDelete {
return
}
if matchRepoTagDelete(req) {
resp.WriteHeader(http.StatusServiceUnavailable)
}
}
// Only block repository and tag deletion
func matchRepoTagDelete(req *http.Request) bool {
if inWhiteList(req) {
return false
}
re := regexp.MustCompile(tagURL)
s := re.FindStringSubmatch(req.URL.Path)
if len(s) == 3 {
return true
}
re = regexp.MustCompile(repoURL)
s = re.FindStringSubmatch(req.URL.Path)
if len(s) == 2 {
return true
}
return false
}
func inWhiteList(req *http.Request) bool {
re := regexp.MustCompile(labelURL)
s := re.FindStringSubmatch(req.URL.Path)
if len(s) == 3 {
return true
}
return false
}

View File

@ -0,0 +1,83 @@
// Copyright (c) 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.
package filter
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/vmware/harbor/src/common"
utilstest "github.com/vmware/harbor/src/common/utils/test"
"github.com/vmware/harbor/src/ui/config"
)
func TestReadonlyFilter(t *testing.T) {
var defaultConfig = map[string]interface{}{
common.ExtEndpoint: "host01.com",
common.AUTHMode: "db_auth",
common.CfgExpiration: 5,
common.TokenExpiration: 30,
common.DatabaseType: "mysql",
common.MySQLHost: "127.0.0.1",
common.MySQLPort: 3306,
common.MySQLUsername: "root",
common.MySQLPassword: "root123",
common.MySQLDatabase: "registry",
common.SQLiteFile: "/tmp/registry.db",
common.ReadOnly: true,
}
adminServer, err := utilstest.NewAdminserver(defaultConfig)
if err != nil {
panic(err)
}
defer adminServer.Close()
if err := os.Setenv("ADMINSERVER_URL", adminServer.URL); err != nil {
panic(err)
}
if err := config.Init(); err != nil {
panic(err)
}
assert := assert.New(t)
req1, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/ubuntu", nil)
rec := httptest.NewRecorder()
filter(req1, rec)
assert.Equal(http.StatusServiceUnavailable, rec.Code)
req2, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world", nil)
rec = httptest.NewRecorder()
filter(req2, rec)
assert.Equal(http.StatusServiceUnavailable, rec.Code)
req3, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world/tags/14.04", nil)
rec = httptest.NewRecorder()
filter(req3, rec)
assert.Equal(http.StatusServiceUnavailable, rec.Code)
req4, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/hello-world/tags/latest", nil)
rec = httptest.NewRecorder()
filter(req4, rec)
assert.Equal(http.StatusServiceUnavailable, rec.Code)
req5, _ := http.NewRequest("DELETE", "http://127.0.0.1:5000/api/repositories/library/vmware/hello-world", nil)
rec = httptest.NewRecorder()
filter(req5, rec)
assert.Equal(http.StatusServiceUnavailable, rec.Code)
}

View File

@ -143,6 +143,7 @@ func main() {
filter.Init()
beego.InsertFilter("/*", beego.BeforeRouter, filter.SecurityFilter)
beego.InsertFilter("/*", beego.BeforeRouter, filter.ReadonlyFilter)
beego.InsertFilter("/api/*", beego.BeforeRouter, filter.MediaTypeFilter("application/json"))
initRouters()

View File

@ -154,6 +154,20 @@ func (uh urlHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
uh.next.ServeHTTP(rw, req)
}
type readonlyHandler struct {
next http.Handler
}
func (rh readonlyHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if config.ReadOnly() {
if req.Method == http.MethodDelete || req.Method == http.MethodPost || req.Method == http.MethodPatch {
http.Error(rw, "Upload/Delete is prohibited in read only mode.", http.StatusServiceUnavailable)
return
}
}
rh.next.ServeHTTP(rw, req)
}
type listReposHandler struct {
next http.Handler
}

View File

@ -41,7 +41,7 @@ func Init(urls ...string) error {
return err
}
Proxy = httputil.NewSingleHostReverseProxy(targetURL)
handlers = handlerChain{head: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}
handlers = handlerChain{head: readonlyHandler{next: urlHandler{next: listReposHandler{next: contentTrustHandler{next: vulnerableHandler{next: Proxy}}}}}}
return nil
}

View File

@ -33,7 +33,7 @@ import (
// ScanAllImages scans all images of Harbor by submiting jobs to jobservice, the whole process will move on if failed to submit any job of a single image.
func ScanAllImages() error {
repos, err := dao.GetAllRepositories()
repos, err := dao.GetRepositories()
if err != nil {
log.Errorf("Failed to list all repositories, error: %v", err)
return err
@ -46,7 +46,9 @@ func ScanAllImages() error {
// ScanImagesByProjectID scans all images under a projet, the whole process will move on if failed to submit any job of a single image.
func ScanImagesByProjectID(id int64) error {
repos, err := dao.GetRepositoriesByProject(id, "", 0, 0)
repos, err := dao.GetRepositories(&models.RepositoryQuery{
ProjectIDs: []int64{id},
})
if err != nil {
log.Errorf("Failed list repositories in project %d, error: %v", id, err)
return err

View File

@ -0,0 +1,23 @@
export const CREATE_EDIT_LABEL_STYLE: string = `
.form-group-label-override {
font-size: 14px;
font-weight: 400;
}
form{margin-bottom:-10px;padding-top:0; margin-top: 20px;width: 100%;background-color: #eee; border:1px solid #ccc;}
form .form-group{display:inline-flex;padding-left: 70px;}
form .form-group>label:first-child{width: auto;}
section{padding:.5rem 0;}
section> label{margin-left: 20px;}
.dropdown-menu{display:inline-block;width:166px; padding:6px;}
.dropdown-item{ display:inline-flex; margin:2px 4px;
display: inline-block;padding: 0px; width:30px;height:24px; text-align: center;line-height: 24px;}
.btnColor{
margin: 0 !important;
padding: 0;
width: 26px;
height:22px;
min-width: 26px;}
.dropdown-item{border:0px; color: white; font-size:12px;}
`;

View File

@ -0,0 +1,36 @@
export const CREATE_EDIT_LABEL_TEMPLATE: string = `
<div>
<form #labelForm="ngForm" [hidden]="!formShow">
<section>
<label>
<label for="name">{{'LABEL.LABEL_NAME' | translate}}</label>
<label aria-haspopup="true" role="tooltip" [class.invalid]="isLabelNameExist" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left">
<input type="text" id="name" name="name" required size="20" autocomplete="off" [(ngModel)]="labelModel.name" #name="ngModel" (keyup)="existValid(labelModel.name)">
<span class="tooltip-content">
{{'LABEL.NAME_ALREADY_EXIST' | translate }}
</span>
</label>
</label>
<label>
<label for="color">{{'LABEL.COLOR' | translate}}</label>
<clr-dropdown [clrCloseMenuOnItemClick]="false">
<button type="button" class="btn btn-outline btnColor btn-sm" clrDropdownTrigger>
<clr-icon shape="caret down" size="20" style='right:2px; width:24px; height:18px;'></clr-icon>
</button>
<clr-dropdown-menu *clrIfOpen>
<label type="button" class="dropdown-item" (click)="labelModel.color=i" *ngFor="let i of labelColor" [ngStyle]="{'background-color': i}">Aa</label>
</clr-dropdown-menu>
</clr-dropdown>
<input type="text" id="color" size="8" name="color" [(ngModel)]="labelModel.color" #color="ngModel">
</label>
<label>
<label for="description">{{'LABEL.DESCRIPTION' | translate}}</label>
<input type="text" id="description" name="description" size="30" [(ngModel)]="labelModel.description" #description="ngModel">
</label>
<label>
<button type="button" class="btn btn-sm btn-outline" (click)="onCancel()" [disabled]="inProgress">{{ 'BUTTON.CANCEL' | translate }}</button>
<button type="submit" class="btn btn-sm btn-primary" (click)="onSubmit()" [disabled]="!isValid">{{ 'BUTTON.OK' | translate }}</button>
</label>
</section>
</form>
</div>`;

View File

@ -0,0 +1,85 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { SharedModule } from '../shared/shared.module';
import { FilterComponent } from '../filter/filter.component';
import { InlineAlertComponent } from '../inline-alert/inline-alert.component';
import { ErrorHandler } from '../error-handler/error-handler';
import {Label} from '../service/interface';
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
import {CreateEditLabelComponent} from "./create-edit-label.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
describe('CreateEditLabelComponent (inline template)', () => {
let mockOneData: Label = {
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
}
let comp: CreateEditLabelComponent;
let fixture: ComponentFixture<CreateEditLabelComponent>;
let config: IServiceConfig = {
systemInfoEndpoint: '/api/label/testing'
};
let labelService: LabelService;
let spy: jasmine.Spy;
let spyOne: jasmine.Spy;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
NoopAnimationsModule
],
declarations: [
FilterComponent,
CreateEditLabelComponent,
InlineAlertComponent ],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: LabelService, useClass: LabelDefaultService }
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(CreateEditLabelComponent);
comp = fixture.componentInstance;
labelService = fixture.debugElement.injector.get(LabelService);
spy = spyOn(labelService, 'getLabels').and.returnValue(Promise.resolve(mockOneData));
spyOne = spyOn(labelService, 'createLabel').and.returnValue(Promise.resolve(mockOneData));
fixture.detectChanges();
comp.openModal();
fixture.detectChanges();
});
it('should be created', () => {
fixture.detectChanges();
expect(comp).toBeTruthy();
});
it('should get label and open modal', () => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(comp.labelModel.name).toEqual('');
});
});
});

View File

@ -0,0 +1,164 @@
// Copyright (c) 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.
import {
Component,
Output,
EventEmitter,
OnDestroy,
Input, OnInit, ViewChild
} from '@angular/core';
import {Label} from '../service/interface';
import { CREATE_EDIT_LABEL_STYLE } from './create-edit-label.component.css';
import { CREATE_EDIT_LABEL_TEMPLATE } from './create-edit-label.component.html';
import {toPromise, clone, compareValue} from '../utils';
import {Subject} from "rxjs/Subject";
import {LabelService} from "../service/label.service";
import {ErrorHandler} from "../error-handler/error-handler";
import {NgForm} from "@angular/forms";
@Component({
selector: 'hbr-create-edit-label',
template: CREATE_EDIT_LABEL_TEMPLATE,
styles: [CREATE_EDIT_LABEL_STYLE]
})
export class CreateEditLabelComponent implements OnInit, OnDestroy {
formShow: boolean;
inProgress: boolean;
copeLabelModel: Label;
labelModel: Label = this.initLabel();
labelId = 0;
nameChecker: Subject<string> = new Subject<string>();
checkOnGoing: boolean;
isLabelNameExist = false;
labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8'];
labelForm: NgForm;
@ViewChild('labelForm')
currentForm: NgForm;
@Input() projectId: number;
@Input() scope: string;
@Output() reload = new EventEmitter();
constructor(
private labelService: LabelService,
private errorHandler: ErrorHandler,
) { }
ngOnInit(): void {
this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((name: string) => {
this.checkOnGoing = true;
toPromise<Label[]>(this.labelService.getLabels(this.scope, this.projectId))
.then(targets => {
if (targets && targets.length) {
if (targets.find(m => m.name === name)) {
this.isLabelNameExist = true;
} else {
this.isLabelNameExist = false;
};
}else {
this.isLabelNameExist = false;
}
this.checkOnGoing = false;
}).catch(error => {
this.checkOnGoing = false;
this.errorHandler.error(error)
});
});
}
ngOnDestroy(): void {
this.nameChecker.unsubscribe();
}
initLabel(): Label {
return {
name: '',
description: '',
color: '',
scope: '',
project_id: 0
};
}
openModal(): void {
this.labelModel = this.initLabel();
this.formShow = true;
this.labelId = 0;
this.copeLabelModel = null;
}
editModel(labelId: number, label: Label[]): void {
this.labelModel = clone(label[0]);
this.formShow = true;
this.labelId = labelId;
this.copeLabelModel = clone(label[0]);
}
public get hasChanged(): boolean {
return !compareValue(this.copeLabelModel, this.labelModel);
}
public get isValid(): boolean {
return !(this.checkOnGoing || this.isLabelNameExist || !(this.currentForm && this.currentForm.valid) || !this.hasChanged || this.inProgress);
}
existValid(text: string): void {
if (text) {
this.nameChecker.next(text);
}
}
onSubmit(): void {
this.inProgress = true;
if (this.labelId <= 0) {
this.labelModel.scope = this.scope;
this.labelModel.project_id = this.projectId;
toPromise<Label>(this.labelService.createLabel(this.labelModel))
.then(res => {
this.inProgress = false;
this.reload.emit();
this.labelModel = this.initLabel();
}).catch(err => {
this.inProgress = false;
this.errorHandler.error(err)
});
} else {
toPromise<Label>(this.labelService.updateLabel(this.labelId, this.labelModel))
.then(res => {
this.inProgress = false;
this.reload.emit();
this.labelModel = this.initLabel();
}).catch(err => {
this.inProgress = false;
this.errorHandler.error(err)
});
}
}
onCancel(): void {
this.inProgress = false;
this.labelModel = this.initLabel();
this.formShow = false;
}
}

View File

@ -0,0 +1,6 @@
import { Type } from '@angular/core';
import {CreateEditLabelComponent} from "./create-edit-label.component";
export const CREATE_EDIT_LABEL_DIRECTIVES: Type<any>[] = [
CreateEditLabelComponent
];

View File

@ -47,6 +47,8 @@ import {
JobLogDefaultService,
ProjectService,
ProjectDefaultService,
LabelService,
LabelDefaultService
} from './service/index';
import {
ErrorHandler,
@ -58,6 +60,9 @@ import { TranslateModule } from '@ngx-translate/core';
import { TranslateServiceInitializer } from './i18n/index';
import { DEFAULT_LANG_COOKIE_KEY, DEFAULT_SUPPORTING_LANGS, DEFAULT_LANG } from './utils';
import { ChannelService } from './channel/index';
import {LABEL_DIRECTIVES} from "./label/index";
import {CREATE_EDIT_LABEL_DIRECTIVES} from "./create-edit-label/index";
import {LABEL_PIECE_DIRECTIVES} from "./label-piece/index";
/**
* Declare default service configuration; all the endpoints will be defined in
@ -81,7 +86,8 @@ export const DefaultServiceConfig: IServiceConfig = {
langMessageFileSuffixForHttpLoader: "-lang.json",
localI18nMessageVariableMap: {},
configurationEndpoint: "/api/configurations",
scanJobEndpoint: "/api/jobs/scan"
scanJobEndpoint: "/api/jobs/scan",
labelEndpoint: "/api/labels"
};
/**
@ -126,6 +132,9 @@ export interface HarborModuleConfig {
//Service implementation for project policy
projectPolicyService?: Provider,
//Service implementation for label
labelService?: Provider,
}
/**
@ -170,7 +179,10 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
PUSH_IMAGE_BUTTON_DIRECTIVES,
CONFIGURATION_DIRECTIVES,
JOB_LOG_VIEWER_DIRECTIVES,
PROJECT_POLICY_CONFIG_DIRECTIVES
PROJECT_POLICY_CONFIG_DIRECTIVES,
LABEL_DIRECTIVES,
CREATE_EDIT_LABEL_DIRECTIVES,
LABEL_PIECE_DIRECTIVES
],
exports: [
LOG_DIRECTIVES,
@ -192,7 +204,10 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co
CONFIGURATION_DIRECTIVES,
JOB_LOG_VIEWER_DIRECTIVES,
TranslateModule,
PROJECT_POLICY_CONFIG_DIRECTIVES
PROJECT_POLICY_CONFIG_DIRECTIVES,
LABEL_DIRECTIVES,
CREATE_EDIT_LABEL_DIRECTIVES,
LABEL_PIECE_DIRECTIVES
],
providers: []
})
@ -214,6 +229,7 @@ export class HarborLibraryModule {
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
// Do initializing
TranslateServiceInitializer,
{
@ -243,6 +259,7 @@ export class HarborLibraryModule {
config.configService || { provide: ConfigurationService, useClass: ConfigurationDefaultService },
config.jobLogService || { provide: JobLogService, useClass: JobLogDefaultService },
config.projectPolicyService || { provide: ProjectService, useClass: ProjectDefaultService },
config.labelService || {provide: LabelService, useClass: LabelDefaultService},
ChannelService
]
};

View File

@ -21,3 +21,5 @@ export * from './config/index';
export * from './job-log-viewer/index';
export * from './channel/index';
export * from './project-policy-config/index';
export * from './label/index';
export * from './create-edit-label';

View File

@ -0,0 +1,8 @@
import { Type } from "@angular/core";
import {LabelPieceComponent} from './label-piece.component';
/*export * from "./filter.component";*/
export const LABEL_PIECE_DIRECTIVES: Type<any>[] = [
LabelPieceComponent
];

View File

@ -0,0 +1,46 @@
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
// Copyright (c) 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.
import { Component, Input } from '@angular/core';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { LABEL_PIEICE_TEMPLATE, LABEL_PIEICE_STYLES } from './label-piece.template';
var LabelPieceComponent = (function () {
function LabelPieceComponent() {
}
LabelPieceComponent.prototype.ngOnInit = function () {
};
return LabelPieceComponent;
}());
__decorate([
Input(),
__metadata("design:type", Object)
], LabelPieceComponent.prototype, "label", void 0);
LabelPieceComponent = __decorate([
Component({
selector: 'hbr-label-piece',
styles: [LABEL_PIEICE_STYLES],
template: LABEL_PIEICE_TEMPLATE
})
], LabelPieceComponent);
export { LabelPieceComponent };
//# sourceMappingURL=label-piece.component.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["label-piece.component.ts"],"names":[],"mappings":";;;;;;;;;AAAA,uDAAC;AACD,EAAE;AACF,kEAAkE;AAClE,mEAAmE;AACnE,0CAA0C;AAC1C,EAAE;AACF,gDAAgD;AAChD,EAAE;AACF,sEAAsE;AACtE,oEAAoE;AACpE,2EAA2E;AAC3E,sEAAsE;AACtE,iCAAiC;AACjC,OAAO,EAAE,SAAA,EAAW,KAAA,EAAoC,MAAO,eAAA,CAAgB;AAI/E,OAAO,gCAAA,CAAiC;AACxC,OAAO,wCAAA,CAAyC;AAEhD,OAAO,EAAE,qBAAA,EAAuB,mBAAA,EAAoB,MAAO,wBAAA,CAAyB;AAUpF,IAAa,mBAAmB;IAAhC;IAaA,CAAC;IAFG,sCAAQ,GAAR;IACA,CAAC;IACL,0BAAC;AAAD,CAbA,AAaC,IAAA;AAJY;IAAR,KAAK,EAAE;;kDAAc;AATb,mBAAmB;IAN/B,SAAS,CAAC;QACP,QAAQ,EAAE,iBAAiB;QAC3B,MAAM,EAAE,CAAC,mBAAmB,CAAC;QAC7B,QAAQ,EAAE,qBAAqB;KAClC,CAAC;GAEW,mBAAmB,CAa/B;SAbY,mBAAmB","file":"label-piece.component.js","sourceRoot":""}

View File

@ -0,0 +1,36 @@
// Copyright (c) 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.
import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { LABEL_PIEICE_TEMPLATE, LABEL_PIEICE_STYLES } from './label-piece.template';
import {Label} from "../service/interface";
@Component({
selector: 'hbr-label-piece',
styles: [LABEL_PIEICE_STYLES],
template: LABEL_PIEICE_TEMPLATE
})
export class LabelPieceComponent implements OnInit {
@Input() label: Label;
ngOnInit(): void {
}
}

View File

@ -0,0 +1,8 @@
/**
* Define template resources for filter component
*/
/**
* Define template resources for filter component
*/ export var LABEL_PIEICE_TEMPLATE = "\n<label class=\"label\" [ngStyle]=\"{'background-color': label.color}\">\n <clr-icon *ngIf=\"label.scope=='p'\" shape=\"organization\"></clr-icon>\n <clr-icon *ngIf=\"label.scope=='g'\" shape=\"administrator\"></clr-icon>\n {{label.name}}\n</label>\n";
export var LABEL_PIEICE_STYLES = "\n .label{border: none; color:#222;}\n .label clr-icon{ margin-right: 3px;}\n";
//# sourceMappingURL=label-piece.template.js.map

View File

@ -0,0 +1 @@
{"version":3,"sources":["label-piece.template.ts"],"names":[],"mappings":"AAAA;;GAEG;AAFH,AAIA;;GAFG,CAEH,MAAM,CAAC,IAAM,qBAAqB,GAAW,uQAM5C,CAAC;AAEF,MAAM,CAAC,IAAM,mBAAmB,GAAW,mFAG1C,CAAC","file":"label-piece.template.js","sourceRoot":""}

View File

@ -0,0 +1,17 @@
/**
* Define template resources for filter component
*/
export const LABEL_PIEICE_TEMPLATE: string = `
<label class="label" [ngStyle]="{'background-color': label.color}">
<clr-icon *ngIf="label.scope=='p'" shape="organization"></clr-icon>
<clr-icon *ngIf="label.scope=='g'" shape="administrator"></clr-icon>
{{label.name}}
</label>
`;
export const LABEL_PIEICE_STYLES: string = `
.label{border: none; color:#222;padding-top:2px;}
.label clr-icon{ margin-right: 3px; display:block;}
.btn-group .dropdown-menu clr-icon{display:block;}
`;

View File

@ -0,0 +1,6 @@
import { Type } from '@angular/core';
import {LabelComponent} from "./label.component";
export const LABEL_DIRECTIVES: Type<any>[] = [
LabelComponent
];

View File

@ -0,0 +1,21 @@
export const LABEL_STYLE: string = `
.option-left {
padding-left: 16px;
margin-top: -6px;
}
.option-right {
padding-right: 16px;
}
.refresh-btn {
cursor: pointer;
}
.refresh-btn:hover {
color: #007CBB;
}
.rightPos{
position: absolute;
z-index: 100;
right: 35px;
margin-top: 4px;
height: 24px;}
`;

View File

@ -0,0 +1,43 @@
export const LABEL_TEMPLATE = `
<div>
<div class="row" style="position:relative;">
<div>
<div class="row flex-items-xs-between rightPos">
<div class="flex-items-xs-middle option-right">
<hbr-filter [withDivider]="true" filterPlaceholder='{{"LABEL.FILTER_LABEL_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></hbr-filter>
<span class="refresh-btn" (click)="refreshTargets()">
<clr-icon shape="refresh"></clr-icon>
</span>
</div>
</div>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 btnGroup">
<button type="button" class="btn btn-sm btn-secondary" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'LABEL.NEW_LABEL' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length == 1)" (click)="editLabel(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'LABEL.EDIT' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!selectedRow.length" (click)="deleteLabels(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'LABEL.DELETE' | translate}}</button>
<hbr-create-edit-label [scope]="scope" [projectId]="projectId" (reload)="reload()"></hbr-create-edit-label>
</div>
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-column [clrDgField]="'name'">{{'LABEL.LABEL' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'endpoint'">{{'LABEL.DESCRIPTION' | translate}}</clr-dg-column>
<clr-dg-column [clrDgField]="'insecure'">{{'LABEL.CREATION_TIME' | translate }}</clr-dg-column>
<clr-dg-placeholder>{{'DESTINATION.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let label of targets" [clrDgItem]='label'>
<clr-dg-cell>
<hbr-label-piece [label]="label"></hbr-label-piece>
</clr-dg-cell>
<clr-dg-cell>{{label.description}}</clr-dg-cell>
<clr-dg-cell>{{label.creation_time | date: 'short'}}</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'DESTINATION.OF' | translate}}</span>
{{pagination.totalItems}} {{'DESTINATION.ITEMS' | translate}}
<clr-dg-pagination #pagination [clrDgPageSize]="15"></clr-dg-pagination>
</clr-dg-footer>
</clr-datagrid>
</div>
</div>
<confirmation-dialog #confirmationDialog [batchInfors]="batchDelectionInfos" (confirmAction)="confirmDeletion($event)"></confirmation-dialog>
</div>
`;

View File

@ -0,0 +1,131 @@
import {Label} from "../service/interface";
import {LabelComponent} from "./label.component";
import {async, ComponentFixture, TestBed} from "@angular/core/testing";
import {LabelDefaultService, LabelService} from "../service/label.service";
import {SharedModule} from "../shared/shared.module";
import {NoopAnimationsModule} from "@angular/platform-browser/animations";
import {FilterComponent} from "../filter/filter.component";
import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component";
import {CreateEditLabelComponent} from "../create-edit-label/create-edit-label.component";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {InlineAlertComponent} from "../inline-alert/inline-alert.component";
import {ErrorHandler} from "../error-handler/error-handler";
import {IServiceConfig, SERVICE_CONFIG} from "../service.config";
describe('LabelComponent (inline template)', () => {
let mockData: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}
];
let mockOneData: Label = {
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
}
let comp: LabelComponent;
let fixture: ComponentFixture<LabelComponent>;
let labelService: LabelService;
let spy: jasmine.Spy;
let spyOneLabel: jasmine.Spy;
let config: IServiceConfig = {
systemInfoEndpoint: '/api/label/testing'
};
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
SharedModule,
NoopAnimationsModule
],
declarations: [
FilterComponent,
ConfirmationDialogComponent,
CreateEditLabelComponent,
LabelComponent,
LabelPieceComponent,
InlineAlertComponent
],
providers: [
ErrorHandler,
{ provide: SERVICE_CONFIG, useValue: config },
{provide: LabelService, useClass: LabelDefaultService}
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(LabelComponent);
comp = fixture.componentInstance;
labelService = fixture.debugElement.injector.get(LabelService);
spy = spyOn(labelService, 'getLabels').and.returnValues(Promise.resolve(mockData));
spyOneLabel = spyOn(labelService, 'getLabel').and.returnValues(Promise.resolve(mockOneData));
fixture.detectChanges();
});
it('should retrieve label data', () => {
fixture.detectChanges();
expect(spy.calls.any()).toBeTruthy();
});
it('should open create label modal', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
comp.editLabel([mockOneData]);
fixture.detectChanges();
expect(comp.targets[0].name).toEqual('label0-g');
})
}));
/*it('should open to edit existing label', async() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
let de: DebugElement = fixture.debugElement.query(del => del.classes['active']);
expect(de).toBeTruthy();
fixture.detectChanges();
click(de);
fixture.detectChanges();
let deInput: DebugElement = fixture.debugElement.query(By.css['input']);
expect(deInput).toBeTruthy();
let elInput: HTMLElement = deInput.nativeElement;
expect(elInput).toBeTruthy();
expect(elInput.textContent).toEqual('label1-g');
})
})*/
})

View File

@ -0,0 +1,175 @@
// Copyright (c) 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.
import {
Component, OnInit, OnDestroy, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef,
Input
} from '@angular/core';
import {LABEL_TEMPLATE} from "./label.component.html";
import {LABEL_STYLE} from "./label.component.css";
import {Label} from "../service/interface";
import {LabelDefaultService, LabelService} from "../service/label.service";
import {toPromise} from "../utils";
import {ErrorHandler} from "../error-handler/error-handler";
import {CreateEditLabelComponent} from "../create-edit-label/create-edit-label.component";
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {ConfirmationMessage} from "../confirmation-dialog/confirmation-message";
import {ConfirmationButtons, ConfirmationState, ConfirmationTargets} from "../shared/shared.const";
import {ConfirmationAcknowledgement} from "../confirmation-dialog/confirmation-state-message";
import {TranslateService} from "@ngx-translate/core";
import {ConfirmationDialogComponent} from "../confirmation-dialog/confirmation-dialog.component";
@Component({
selector: 'hbr-label',
template: LABEL_TEMPLATE,
styles: [LABEL_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LabelComponent implements OnInit {
timerHandler: any;
loading: boolean;
targets: Label[];
targetName: string;
selectedRow: Label[] = [];
batchDelectionInfos: BatchInfo[] = [];
@Input() scope: string;
@Input() projectId = 0;
@Input() hasProjectAdminRole: boolean;
@ViewChild(CreateEditLabelComponent)
createEditLabel: CreateEditLabelComponent;
@ViewChild('confirmationDialog')
confirmationDialogComponent: ConfirmationDialogComponent;
constructor(
private labelService: LabelService,
private errorHandler: ErrorHandler,
private translateService: TranslateService,
private ref: ChangeDetectorRef) {
}
ngOnInit(): void {
this.retrieve(this.scope);
}
retrieve(scope: string, name = '') {
this.loading = true;
this.selectedRow = [];
this.targetName = '';
toPromise<Label[]>(this.labelService.getLabels(scope, this.projectId, name))
.then(targets => {
this.targets = targets || [];
this.loading = false;
this.forceRefreshView(2000);
}).catch(error => {
this.errorHandler.error(error);
this.loading = false;
})
}
openModal(): void {
this.createEditLabel.openModal();
}
reload(): void {
this.retrieve(this.scope);
}
doSearchTargets(targetName: string) {
this.retrieve(this.scope, targetName);
}
refreshTargets() {
this.retrieve(this.scope);
}
selectedChange(): void {
// this.forceRefreshView(5000);
}
editLabel(label: Label[]): void {
this.createEditLabel.editModel(label[0].id, label);
}
deleteLabels(targets: Label[]): void {
if (targets && targets.length) {
let targetNames: string[] = [];
this.batchDelectionInfos = [];
targets.forEach(target => {
targetNames.push(target.name);
let initBatchMessage = new BatchInfo ();
initBatchMessage.name = target.name;
this.batchDelectionInfos.push(initBatchMessage);
});
let deletionMessage = new ConfirmationMessage(
'REPLICATION.DELETION_TITLE_TARGET',
'REPLICATION.DELETION_SUMMARY_TARGET',
targetNames.join(', ') || '',
targets,
ConfirmationTargets.TARGET,
ConfirmationButtons.DELETE_CANCEL);
this.confirmationDialogComponent.open(deletionMessage);
}
}
confirmDeletion(message: ConfirmationAcknowledgement) {
if (message &&
message.source === ConfirmationTargets.TARGET &&
message.state === ConfirmationState.CONFIRMED) {
let targetLists: Label[] = message.data;
if (targetLists && targetLists.length) {
let promiseLists: any[] = [];
targetLists.forEach(target => {
promiseLists.push(this.delOperate(target.id, target.name));
})
Promise.all(promiseLists).then((item) => {
this.selectedRow = [];
this.retrieve(this.scope);
});
}
}
}
delOperate(id: number, name: string) {
let findedList = this.batchDelectionInfos.find(data => data.name === name);
return toPromise<number>(this.labelService
.deleteLabel(id))
.then(
response => {
this.translateService.get('BATCH.DELETED_SUCCESS')
.subscribe(res => {
findedList = BathInfoChanges(findedList, res);
});
}).catch(
error => {
this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => {
findedList = BathInfoChanges(findedList, res, false, true);
});
});
}
// Forcely refresh the view
forceRefreshView(duration: number): void {
// Reset timer
if (this.timerHandler) {
clearInterval(this.timerHandler);
}
this.timerHandler = setInterval(() => this.ref.markForCheck(), 100);
setTimeout(() => {
if (this.timerHandler) {
clearInterval(this.timerHandler);
this.timerHandler = null;
}
}, duration);
}
}

View File

@ -1,7 +1,7 @@
export const LIST_REPLICATION_RULE_TEMPLATE: string = `
<div style="padding-bottom: 15px;">
<clr-datagrid [clrDgLoading]="loading" [(clrDgSingleSelected)]="selectedRow" [clDgRowSelection]="true">
<clr-dg-action-bar style="height:24px;">
<clr-dg-action-bar>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" (click)="openModal()"><clr-icon shape="plus" size="16"></clr-icon>&nbsp;{{'REPLICATION.NEW_REPLICATION_RULE' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="editRule(selectedRow)"><clr-icon shape="pencil" size="16"></clr-icon>&nbsp;{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="isSystemAdmin" [disabled]="!selectedRow" (click)="deleteRule(selectedRow)"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPLICATION.DELETE_POLICY' | translate}}</button>

View File

@ -22,8 +22,9 @@ import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
import { JobLogViewerComponent } from '../job-log-viewer/index';
import { click } from '../utils';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
describe('RepositoryComponentListview (inline template)', () => {
describe('RepositoryComponentListView (inline template)', () => {
let compRepo: RepositoryListviewComponent;
let fixtureRepo: ComponentFixture<RepositoryListviewComponent>;
@ -82,7 +83,8 @@ describe('RepositoryComponentListview (inline template)', () => {
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
"signature": null,
"labels": []
}
];
@ -101,6 +103,7 @@ describe('RepositoryComponentListview (inline template)', () => {
declarations: [
RepositoryListviewComponent,
TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent,
FilterComponent,
VULNERABILITY_DIRECTIVES,

View File

@ -9,7 +9,7 @@ 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 {Repository, RepositoryItem, Tag, SystemInfo, Label} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { TagService, TagDefaultService } from '../service/tag.service';
@ -20,6 +20,8 @@ import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
import { JobLogViewerComponent } from '../job-log-viewer/index';
import { click } from '../utils';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
describe('RepositoryComponentStackview (inline template)', () => {
@ -27,10 +29,12 @@ describe('RepositoryComponentStackview (inline template)', () => {
let fixtureRepo: ComponentFixture<RepositoryStackviewComponent>;
let repositoryService: RepositoryService;
let tagService: TagService;
let labelService: LabelService;
let systemInfoService: SystemInfoService;
let spyRepos: jasmine.Spy;
let spyTags: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spySystemInfo: jasmine.Spy;
let mockSystemInfo: SystemInfo = {
@ -81,7 +85,31 @@ describe('RepositoryComponentStackview (inline template)', () => {
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
"signature": null,
"labels": []
}
];
let mockLabels: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}
];
@ -99,6 +127,7 @@ describe('RepositoryComponentStackview (inline template)', () => {
declarations: [
RepositoryStackviewComponent,
TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent,
FilterComponent,
VULNERABILITY_DIRECTIVES,
@ -111,7 +140,8 @@ describe('RepositoryComponentStackview (inline template)', () => {
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: TagService, useClass: TagDefaultService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService }
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
{provide: LabelService, useClass: LabelDefaultService}
]
});
}));
@ -127,6 +157,11 @@ describe('RepositoryComponentStackview (inline template)', () => {
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
labelService = fixtureRepo.debugElement.injector.get(LabelService);
spyLabels = spyOn(labelService, 'getLabels').and.returnValues(Promise.resolve(mockLabels));
fixtureRepo.detectChanges();
});

View File

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

View File

@ -1,6 +1,6 @@
import { ComponentFixture, TestBed, async, } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import {Component, DebugElement} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { SharedModule } from '../shared/shared.module';
@ -16,12 +16,15 @@ import { JobLogViewerComponent } from '../job-log-viewer/index';
import { ErrorHandler } from '../error-handler/error-handler';
import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface';
import {Repository, RepositoryItem, Tag, SystemInfo, Label} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
import { TagService, TagDefaultService } from '../service/tag.service';
import { ChannelService } from '../channel/index';
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
class RouterStub {
navigateByUrl(url: string) { return url; }
@ -34,10 +37,13 @@ describe('RepositoryComponent (inline template)', () => {
let repositoryService: RepositoryService;
let systemInfoService: SystemInfoService;
let tagService: TagService;
let labelService: LabelService;
let spyRepos: jasmine.Spy;
let spyTags: jasmine.Spy;
let spySystemInfo: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let mockSystemInfo: SystemInfo = {
'with_notary': true,
@ -87,10 +93,53 @@ describe('RepositoryComponent (inline template)', () => {
'docker_version': '1.12.3',
'author': 'NGINX Docker Maintainers \"docker-maint@nginx.com\"',
'created': new Date('2016-11-08T22:41:15.912313785Z'),
'signature': null
'signature': null,
'labels': []
}
];
let mockLabels: Label[] = [{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 1,
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}]
let mockLabels1: Label[] = [{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 1,
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 1,
scope: "p",
update_time: "",
}]
let config: IServiceConfig = {
repositoryBaseEndpoint: '/api/repository/testing',
systemInfoEndpoint: '/api/systeminfo/testing',
@ -109,6 +158,7 @@ describe('RepositoryComponent (inline template)', () => {
ConfirmationDialogComponent,
FilterComponent,
TagComponent,
LabelPieceComponent,
VULNERABILITY_DIRECTIVES,
PUSH_IMAGE_BUTTON_DIRECTIVES,
INLINE_ALERT_DIRECTIVES,
@ -120,25 +170,31 @@ describe('RepositoryComponent (inline template)', () => {
{ provide: RepositoryService, useClass: RepositoryDefaultService },
{ provide: SystemInfoService, useClass: SystemInfoDefaultService },
{ provide: TagService, useClass: TagDefaultService },
{ provide: LabelService, useClass: LabelDefaultService},
{ provide: ChannelService},
]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(RepositoryComponent);
compRepo = fixture.componentInstance;
compRepo.projectId = 1;
compRepo.hasProjectAdminRole = true;
compRepo.repoName = 'library/nginx';
repositoryService = fixture.debugElement.injector.get(RepositoryService);
systemInfoService = fixture.debugElement.injector.get(SystemInfoService);
tagService = fixture.debugElement.injector.get(TagService);
labelService = fixture.debugElement.injector.get(LabelService);
spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo));
spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo));
spyTags = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTagData));
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
fixture.detectChanges();
});

View File

@ -51,6 +51,7 @@ export class RepositoryComponent implements OnInit {
@Input() repoName: string;
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Input() isGuest: boolean;
@Input() withNotary: boolean;
@Input() withClair: boolean;
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();

View File

@ -196,4 +196,16 @@ export interface IServiceConfig {
* @memberof IServiceConfig
*/
scanJobEndpoint?: string;
/**
* The base endpoint of the service used to handle the labels.
* labels related endpoints will be built based on this endpoint.
* E.g:
* If the base endpoint is '/api/labels',
* the label endpoint will be '/api/labels/:id'.
*
* @type {string}
* @memberOf IServiceConfig
*/
labelEndpoint?: string;
}

View File

@ -10,3 +10,4 @@ export * from './scanning.service';
export * from './configuration.service';
export * from './job-log.service';
export * from './project.service';
export * from './label.service';

View File

@ -60,6 +60,7 @@ export interface Tag extends Base {
created: Date;
signature?: string;
scan_overview?: VulnerabilitySummary;
labels: Label[];
}
/**
@ -267,3 +268,12 @@ export interface TagClickEvent {
repository_name: string;
tag_name: string;
}
export interface Label {
[key: string]: any | any[];
name: string;
description: string;
color: string;
scope: string;
project_id: number;
}

View File

@ -0,0 +1,125 @@
import {Observable} from "rxjs/Observable";
import {Label} from "./interface";
import {Inject, Injectable} from "@angular/core";
import {Http} from "@angular/http";
import {IServiceConfig, SERVICE_CONFIG} from "../service.config";
import {buildHttpRequestOptions, HTTP_JSON_OPTIONS} from "../utils";
import {RequestQueryParams} from "./RequestQueryParams";
export abstract class LabelService {
abstract getGLabels(name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]>;
abstract getPLabels(projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]>;
abstract getLabels(scope: string, projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]>;
abstract createLabel(label: Label): Observable<Label> | Promise<Label> | Label;
abstract getLabel(id: number): Observable<Label> | Promise<Label> | Label;
abstract updateLabel(id: number, param: Label): Observable<any> | Promise<any> | any;
abstract deleteLabel(id: number): Observable<any> | Promise<any> | any;
}
@Injectable()
export class LabelDefaultService extends LabelService {
_labelUrl: string;
constructor(
@Inject(SERVICE_CONFIG) config: IServiceConfig,
private http: Http
) {
super();
this._labelUrl = config.labelEndpoint ? config.labelEndpoint : "/api/labels";
}
getLabels(scope: string, projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]> {
if (!queryParams) {
queryParams = new RequestQueryParams();
}
if (scope) {
queryParams.set('scope', scope);
}
if (projectId) {
queryParams.set('project_id', '' + projectId);
}
if (name) {
queryParams.set('name', '' + name);
}
return this.http.get(this._labelUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
getGLabels(name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]> {
if (!queryParams) {
queryParams = new RequestQueryParams();
}
queryParams.set('scope', 'g');
if (name) {
queryParams.set('name', '' + name);
}
return this.http.get(this._labelUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
getPLabels(projectId: number, name?: string, queryParams?: RequestQueryParams): Observable<Label[]> | Promise<Label[]> {
if (!queryParams) {
queryParams = new RequestQueryParams();
}
queryParams.set('scope', 'p');
if (projectId) {
queryParams.set('project_id', '' + projectId);
}
if (name) {
queryParams.set('name', '' + name);
}
return this.http.get(this._labelUrl, buildHttpRequestOptions(queryParams)).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
createLabel(label: Label): Observable<any> | Promise<any> | any {
if (!label) {
return Promise.reject('Invalid label.');
}
return this.http.post(this._labelUrl, JSON.stringify(label), HTTP_JSON_OPTIONS).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
getLabel(id: number): Observable<Label> | Promise<Label> | Label {
if (!id || id <= 0) {
return Promise.reject('Bad request argument.');
}
let reqUrl = `${this._labelUrl}/${id}`
return this.http.get(reqUrl).toPromise()
.then(response => response.json())
.catch(error => Promise.reject(error));
}
updateLabel(id: number, label: Label): Observable<any> | Promise<any> | any {
if (!id || id <= 0) {
return Promise.reject('Bad request argument.');
}
if (!label) {
return Promise.reject('Invalid endpoint.');
}
let reqUrl = `${this._labelUrl}/${id}`
return this.http.put(reqUrl, JSON.stringify(label), HTTP_JSON_OPTIONS).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
deleteLabel(id: number): Observable<any> | Promise<any> | any {
if (!id || id <= 0) {
return Promise.reject('Bad request argument.');
}
let reqUrl = `${this._labelUrl}/${id}`
return this.http.delete(reqUrl).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
}

View File

@ -20,7 +20,8 @@ describe('TagService', () => {
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
"signature": null,
'labels': []
}
];

View File

@ -1,6 +1,6 @@
import { Observable } from 'rxjs/Observable';
import { RequestQueryParams } from './RequestQueryParams';
import { Tag } from './interface';
import {Label, Tag} from './interface';
import { Injectable, Inject } from "@angular/core";
import 'rxjs/add/observable/of';
import { Http } from '@angular/http';
@ -65,6 +65,9 @@ export abstract class TagService {
* @memberOf TagService
*/
abstract getTag(repositoryName: string, tag: string, queryParams?: RequestQueryParams): Observable<Tag> | Promise<Tag> | Tag;
abstract addLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any;
abstract deleteLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any;
}
/**
@ -77,13 +80,14 @@ export abstract class TagService {
@Injectable()
export class TagDefaultService extends TagService {
_baseUrl: string;
_labelUrl: string;
constructor(
private http: Http,
@Inject(SERVICE_CONFIG) private config: IServiceConfig
) {
super();
this._baseUrl = this.config.repositoryBaseEndpoint ? this.config.repositoryBaseEndpoint : '/api/repositories';
this._labelUrl = this.config.labelEndpoint? this.config.labelEndpoint : '/api/labels';
}
//Private methods
@ -136,4 +140,28 @@ export class TagDefaultService extends TagService {
.then(response => response.json() as Tag)
.catch(error => Promise.reject(error));
}
public addLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any {
if (!labelId || !tagName || !repoName) {
return Promise.reject('Invalid parameters.');
}
let _addLabelToImageUrl = `/api/repositories/${repoName}/tags/${tagName}/labels`;
return this.http.post(_addLabelToImageUrl, {id: labelId}, HTTP_JSON_OPTIONS).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
public deleteLabelToImages(repoName: string, tagName: string, labelId: number): Observable<any> | Promise<any> | any {
if (!labelId || !tagName || !repoName) {
return Promise.reject('Invalid parameters.');
}
let _addLabelToImageUrl = `/api/repositories/${repoName}/tags/${tagName}/labels/${labelId}`;
return this.http.delete(_addLabelToImageUrl).toPromise()
.then(response => response.status)
.catch(error => Promise.reject(error));
}
}

View File

@ -50,7 +50,8 @@ describe('TagDetailComponent (inline template)', () => {
"author": "steven",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null,
scan_overview: mockVulnerability
"scan_overview": mockVulnerability,
"labels": [],
};
let config: IServiceConfig = {

View File

@ -30,7 +30,8 @@ export class TagDetailComponent implements OnInit {
architecture: "--",
os: "--",
docker_version: "--",
digest: "--"
digest: "--",
labels: [],
};
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();

View File

@ -50,4 +50,20 @@ export const TAG_STYLE = `
right: 35px;
margin-top: 4px;
}
.btn-group .dropdown-menu clr-icon{display: block;}
.dropdown-menu .dropdown-item{position: relative;padding-left:.5rem; padding-right:.5rem;}
.dropdown-menu input{position: relative;margin-left:.5rem; margin-right:.5rem;}
.pull-left{display:inline-block;float:left;}
.pull-right{display:inline-block; float:right;}
.btn-link{display:inline-flex;width: 15px;min-width:15px; color:black; vertical-align: super; }
.trigger-item, .signpost-item{display: inline;}
.signpost-content-body .label{margin:.3rem;}
.labelDiv{position: absolute; left:34px;top:3px;}
.datagrid-action-bar{z-index:10;}
.trigger-item hbr-label-piece{display: flex !important;margin: 6px 0;}
:host >>> .signpost-content{min-width:4rem;}
:host >>> .signpost-content-body{padding:0 .4rem;}
:host >>> .signpost-content-header{display:none;}
.filterLabelPiece{position: absolute; bottom :0px;z-index:1;}
`;

View File

@ -15,8 +15,25 @@ export const TAG_TEMPLATE = `
<div class="row" style="position:relative;">
<div>
<div class="row flex-items-xs-right rightPos">
<div class='filterLabelPiece' [style.left.px]='filterLabelPieceWidth' ><hbr-label-piece [hidden]='!filterOneLabel' [label]="filterOneLabel"></hbr-label-piece></div>
<div class="flex-xs-middle">
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName"></hbr-filter>
<clr-dropdown>
<hbr-filter [withDivider]="true" filterPlaceholder="{{'TAG.FILTER_FOR_TAGS' | translate}}" (filter)="doSearchTagNames($event)" [currentValue]="lastFilteredTagName" clrDropdownTrigger></hbr-filter>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #labelNamePiece (keyup)="handleInputFilter(labelNamePiece.value)"></div>
<div [hidden]='imageFilterLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageFilterLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="dropdown-item" *ngFor='let label of imageFilterLabels' (click)="label.iconsShow = true; filterLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); label.iconsShow = false; unFilterLabel(label)"></clr-icon>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
</div>
</div>
@ -24,26 +41,46 @@ export const TAG_TEMPLATE = `
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded" [(clrDgSelected)]="selectedRow" (clrDgSelectedChange)="selectedChange()">
<clr-dg-action-bar>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(canScanNow(selectedRow) && selectedRow.length==1)" (click)="scanNow(selectedRow)"><clr-icon shape="shield-check" size="16"></clr-icon>&nbsp;{{'VULNERABILITY.SCAN_NOW' | translate}}</button>
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon>&nbsp;{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
<clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" >{{'REPOSITORY.ADD_LABELS' | translate}}</button>
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
<div style='display:grid'>
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
<div class="form-group"><input type="text" placeholder="Filter labels" #stickLabelNamePiece (keyup)="handleStickInputFilter(stickLabelNamePiece.value)"></div>
<div [hidden]='imageStickLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="label.iconsShow = true; selectLabel(label)">
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); label.iconsShow = false; unSelectLabel(label)"></clr-icon>
</button>
</div>
</div>
</clr-dropdown-menu>
</clr-dropdown>
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon>&nbsp;{{'REPOSITORY.DELETE' | translate}}</button>
</div>
</clr-dg-action-bar>
<clr-dg-column style="width: 160px;" [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="min-width: 120px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 100px; max-width:220px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" *ngIf="withClair">{{'REPOSITORY.VULNERABILITY' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
<clr-dg-column style="min-width: 130px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
<clr-dg-column style="width: 150px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
<clr-dg-column style="width: 140px;" [clrDgField]="'labels'">{{'REPOSITORY.LABELS' | translate}}</clr-dg-column>
<clr-dg-placeholder>{{'TAG.PLACEHOLDER' | translate }}</clr-dg-placeholder>
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
<clr-dg-cell class="truncated" style="width: 160px;" [ngSwitch]="withClair">
<clr-dg-cell class="truncated" style="width: 120px;" [ngSwitch]="withClair">
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)" title="{{t.name}}">{{t.name}}</a>
<span *ngSwitchDefault>{{t.name}}</span>
</clr-dg-cell>
<clr-dg-cell style="width: 90px;">{{sizeTransform(t.size)}}</clr-dg-cell>
<clr-dg-cell style="min-width: 120px; max-width:220px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<clr-dg-cell style="min-width: 100px; max-width:220px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">
<hbr-copy-input #copyInput (onCopyError)="onCpError($event)" iconMode="true" defaultValue="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}"></hbr-copy-input>
</clr-dg-cell>
<clr-dg-cell style="width: 140px;" *ngIf="withClair">
@ -58,8 +95,23 @@ export const TAG_TEMPLATE = `
</a>
</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: 150px;">{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell style="width: 140px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
<clr-dg-cell style="width: 140px;">
<hbr-label-piece *ngIf="t.labels?.length" [label]="t.labels[0]"></hbr-label-piece>
<div class="signpost-item" [hidden]="t.labels?.length<=1">
<div class="trigger-item">
<clr-signpost>
<button class="btn btn-link" clrSignpostTrigger>...</button>
<clr-signpost-content [clrPosition]="'left-top'" *clrIfOpen>
<div>
<hbr-label-piece *ngFor="let label of t.labels" [label]="label"></hbr-label-piece>
</div>
</clr-signpost-content>
</clr-signpost>
</div>
</div>
</clr-dg-cell>
</clr-dg-row>
<clr-dg-footer>
<span *ngIf="pagination.totalItems">{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}</span>

View File

@ -8,7 +8,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
import { TagComponent } from './tag.component';
import { ErrorHandler } from '../error-handler/error-handler';
import { Tag } from '../service/interface';
import {Label, Tag} from '../service/interface';
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
@ -19,6 +19,8 @@ import { ChannelService } from '../channel/index';
import { JobLogViewerComponent } from '../job-log-viewer/index';
import {CopyInputComponent} from "../push-image/copy-input.component";
import {LabelPieceComponent} from "../label-piece/label-piece.component";
import {LabelDefaultService, LabelService} from "../service/label.service";
describe('TagComponent (inline template)', () => {
@ -26,6 +28,8 @@ describe('TagComponent (inline template)', () => {
let fixture: ComponentFixture<TagComponent>;
let tagService: TagService;
let spy: jasmine.Spy;
let spyLabels: jasmine.Spy;
let spyLabels1: jasmine.Spy;
let mockTags: Tag[] = [
{
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
@ -36,7 +40,54 @@ describe('TagComponent (inline template)', () => {
"docker_version": "1.12.3",
"author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"",
"created": new Date("2016-11-08T22:41:15.912313785Z"),
"signature": null
"signature": null,
"labels": [],
}
];
let mockLabels: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 0,
scope: "g",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 0,
scope: "g",
update_time: "",
}
];
let mockLabels1: Label[] = [
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 1,
name: "label0-g",
project_id: 1,
scope: "p",
update_time: "",
},
{
color: "#9b0d54",
creation_time: "",
description: "",
id: 2,
name: "label1-g",
project_id: 1,
scope: "p",
update_time: "",
}
];
@ -51,6 +102,7 @@ describe('TagComponent (inline template)', () => {
],
declarations: [
TagComponent,
LabelPieceComponent,
ConfirmationDialogComponent,
VULNERABILITY_DIRECTIVES,
FILTER_DIRECTIVES,
@ -62,7 +114,8 @@ describe('TagComponent (inline template)', () => {
ChannelService,
{ provide: SERVICE_CONFIG, useValue: config },
{ provide: TagService, useClass: TagDefaultService },
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
{ provide: ScanningResultService, useClass: ScanningResultDefaultService },
{provide: LabelService, useClass: LabelDefaultService}
]
});
}));
@ -78,8 +131,25 @@ describe('TagComponent (inline template)', () => {
comp.registryUrl = 'http://registry.testing.com';
comp.withNotary = false;
let labelService: LabelService;
tagService = fixture.debugElement.injector.get(TagService);
spy = spyOn(tagService, 'getTags').and.returnValues(Promise.resolve(mockTags));
labelService = fixture.debugElement.injector.get(LabelService);
/*spyLabels = spyOn(labelService, 'getLabels').and.callFake(function (param) {
if (param === 'g') {
return Promise.resolve(mockLabels);
}else {
Promise.resolve(mockLabels1)
}
})*/
spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels));
spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1));
fixture.detectChanges();
});

View File

@ -20,7 +20,7 @@ import {
EventEmitter,
ChangeDetectionStrategy,
ChangeDetectorRef,
ElementRef
ElementRef, AfterContentInit, AfterViewInit
} from "@angular/core";
import { TagService, VulnerabilitySeverity, RequestQueryParams } from "../service/index";
@ -36,7 +36,7 @@ import { ConfirmationDialogComponent } from "../confirmation-dialog/confirmation
import { ConfirmationMessage } from "../confirmation-dialog/confirmation-message";
import { ConfirmationAcknowledgement } from "../confirmation-dialog/confirmation-state-message";
import { Tag, TagClickEvent } from "../service/interface";
import {Label, Tag, TagClickEvent} from "../service/interface";
import { TAG_TEMPLATE } from "./tag.component.html";
import { TAG_STYLE } from "./tag.component.css";
@ -48,7 +48,8 @@ import {
doFiltering,
doSorting,
VULNERABILITY_SCAN_STATUS,
DEFAULT_PAGE_SIZE
DEFAULT_PAGE_SIZE,
clone,
} from "../utils";
import { TranslateService } from "@ngx-translate/core";
@ -57,6 +58,8 @@ import { State, Comparator } from "clarity-angular";
import {CopyInputComponent} from "../push-image/copy-input.component";
import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message";
import {Observable} from "rxjs/Observable";
import {LabelService} from "../service/label.service";
import {Subject} from "rxjs/Subject";
@Component({
selector: "hbr-tag",
@ -64,7 +67,7 @@ import {Observable} from "rxjs/Observable";
styles: [TAG_STYLE],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TagComponent implements OnInit {
export class TagComponent implements OnInit, AfterViewInit {
signedCon: {[key: string]: any | string[]} = {};
@Input() projectId: number;
@ -73,6 +76,7 @@ export class TagComponent implements OnInit {
@Input() hasSignedIn: boolean;
@Input() hasProjectAdminRole: boolean;
@Input() isGuest: boolean;
@Input() registryUrl: string;
@Input() withNotary: boolean;
@Input() withClair: boolean;
@ -98,7 +102,27 @@ export class TagComponent implements OnInit {
copyFailed = false;
selectedRow: Tag[] = [];
@ViewChild("confirmationDialog")
imageLabels: {[key: string]: boolean | Label | any}[] = [];
imageStickLabels: {[key: string]: boolean | Label | any}[] = [];
imageFilterLabels: {[key: string]: boolean | Label | any}[] = [];
labelListOpen = false;
selectedTag: Tag[];
labelNameFilter: Subject<string> = new Subject<string> ();
stickLabelNameFilter: Subject<string> = new Subject<string> ();
filterOnGoing: boolean;
initFilter = {
name: '',
description: '',
color: '',
scope: '',
project_id: 0,
}
filterOneLabel: Label = this.initFilter;
@ViewChild('confirmationDialog')
confirmationDialog: ConfirmationDialogComponent;
@ViewChild("digestTarget") textInput: ElementRef;
@ -112,6 +136,7 @@ export class TagComponent implements OnInit {
constructor(
private errorHandler: ErrorHandler,
private tagService: TagService,
private labelService: LabelService,
private translateService: TranslateService,
private ref: ChangeDetectorRef,
private channel: ChannelService
@ -128,14 +153,57 @@ export class TagComponent implements OnInit {
}
this.retrieve();
this.lastFilteredTagName = "";
this.lastFilteredTagName = '';
this.labelNameFilter
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
if (name && name.length) {
this.filterOnGoing = true;
this.imageFilterLabels = [];
this.imageLabels.forEach(data => {
if (data.label.name.indexOf(name) !== -1) {
this.imageFilterLabels.push(data);
}
})
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 200);
}, 1000);
}
});
this.stickLabelNameFilter
.debounceTime(500)
.distinctUntilChanged()
.subscribe((name: string) => {
if (name && name.length) {
this.filterOnGoing = true;
this.imageFilterLabels = [];
this.imageLabels.forEach(data => {
if (data.label.name.indexOf(name) !== -1) {
this.imageFilterLabels.push(data);
}
})
setTimeout(() => {
setInterval(() => this.ref.markForCheck(), 200);
}, 1000);
}
});
}
selectedChange(): void {
let hnd = setInterval(() => this.ref.markForCheck(), 200);
setTimeout(() => clearInterval(hnd), 2000);
ngAfterViewInit() {
this.getAllLabels();
}
public get filterLabelPieceWidth() {
let len = this.lastFilteredTagName.length ? this.lastFilteredTagName.length * 6 + 60 : 115;
return len > 210 ? 210 : len;
}
doSearchTagNames(tagName: string) {
this.lastFilteredTagName = tagName;
this.currentPage = 1;
@ -191,7 +259,139 @@ export class TagComponent implements OnInit {
this.doSearchTagNames("");
}
getAllLabels(): void {
toPromise<Label[]>(this.labelService.getGLabels()).then((res: Label[]) => {
if (res.length) {
res.forEach(data => {
this.imageLabels.push({'iconsShow': false, 'label': data});
});
}
toPromise<Label[]>(this.labelService.getPLabels(this.projectId)).then((res1: Label[]) => {
if (res1.length) {
res1.forEach(data => {
this.imageLabels.push({'iconsShow': false, 'label': data});
});
}
this.imageFilterLabels = clone(this.imageLabels);
this.imageStickLabels = clone(this.imageLabels);
}).catch(error => {
this.errorHandler.error(error);
});
}).catch(error => {
this.errorHandler.error(error);
});
}
selectedChange(tag?: Tag[]): void {
if (tag && tag[0].labels && tag[0].labels.length) {
tag[0].labels.forEach((labelInfo: Label) => {
this.imageStickLabels.forEach(data => {
if (labelInfo.id === data['label'].id) {
data.iconsShow = true;
}
});
});
}
}
addLabels(tag: Tag[]): void {
this.labelListOpen = true;
this.selectedTag = tag;
this.selectedChange(tag);
}
selectLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && labelInfo.iconsShow) {
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
toPromise<any>(this.tagService.addLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
this.refresh();
}).catch(err => {
this.errorHandler.error(err);
});
}
}
unSelectLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && !labelInfo.iconsShow) {
let labelId = labelInfo.label.id;
this.selectedRow = this.selectedTag;
toPromise<any>(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => {
this.refresh();
}).catch(err => {
this.errorHandler.error(err);
});
}
}
filterLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && labelInfo.iconsShow) {
let labelName = labelInfo.label.name;
this.imageFilterLabels.filter(data => {
if (data.label.name !== labelName) {
data.iconsShow = false;
}
});
this.filterOneLabel = labelInfo.label;
// reload datagu
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;
if (this.lastFilteredTagName) {
st.filters = [{property: 'name', value: this.lastFilteredTagName}, {property: 'labels.name', value: labelName}];
}else {
st.filters = [{property: 'labels.name', value: labelName}];
}
this.clrLoad(st);
}
}
unFilterLabel(labelInfo: {[key: string]: any | string[]}): void {
if (labelInfo && !labelInfo.iconsShow) {
this.filterOneLabel = this.initFilter;
// reload datagu
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;
if (this.lastFilteredTagName) {
st.filters = [{property: 'name', value: this.lastFilteredTagName}];
}else {
st.filters = [];
}
this.clrLoad(st);
}
}
handleInputFilter($event: string) {
if ($event && $event.length) {
this.labelNameFilter.next($event);
}else {
this.imageFilterLabels = clone(this.imageLabels);
}
}
handleStickInputFilter($event: string) {
if ($event && $event.length) {
this.stickLabelNameFilter.next($event);
}else {
this.imageStickLabels = clone(this.imageLabels);
}
}
retrieve() {
this.tags = [];

View File

@ -179,7 +179,18 @@ export function doFiltering<T extends { [key: string]: any | any[] }>(items: T[]
property: string;
value: string;
}) => {
items = items.filter(item => regexpFilter(filter["value"], item[filter["property"]]));
items = items.filter(item => {
if (filter['property'].indexOf('.') !== -1) {
let arr = filter['property'].split('.');
if (Array.isArray(item[arr[0]]) && item[arr[0]].length) {
return item[arr[0]].some((data: any) => {
return regexpFilter(filter['value'], data[arr[1]]);
});
}
}else {
return regexpFilter(filter['value'], item[filter['property']]);
}
});
});
return items;

View File

@ -27,11 +27,11 @@
"@ngx-translate/http-loader": "0.0.3",
"@types/jquery": "^2.0.41",
"@webcomponents/custom-elements": "^1.0.0",
"clarity-angular": "^0.10.17",
"clarity-angular": "^0.10.27",
"clarity-icons": "^0.10.17",
"clarity-ui": "^0.10.17",
"clarity-ui": "^0.10.27",
"core-js": "^2.4.1",
"harbor-ui": "0.6.47",
"harbor-ui": "0.6.53",
"intl": "^1.2.5",
"mutationobserver-shim": "^0.3.2",
"ngx-cookie": "^1.0.0",

View File

@ -12,6 +12,9 @@
<li role="presentation" class="nav-item">
<button id="config-system" class="btn btn-link nav-link" aria-controls="system_settings" [class.active]='isCurrentTabLink("config-system")' type="button" (click)='tabLinkClick("config-system")'>{{'CONFIG.SYSTEM' | translate }}</button>
</li>
<li role="presentation" class="nav-item">
<button id="config-label" class="btn btn-link nav-link" aria-controls="system_label" [class.active]='isCurrentTabLink("config-label")' type="button" (click)='tabLinkClick("config-label")'>{{'CONFIG.LABEL' | translate }}</button>
</li>
<li role="presentation" class="nav-item" *ngIf="withClair">
<button id="config-vulnerability" class="btn btn-link nav-link" aria-controls="vulnerability" [class.active]='isCurrentTabLink("config-vulnerability")' type="button" (click)='tabLinkClick("config-vulnerability")'>{{'CONFIG.VULNERABILITY' | translate}}</button>
</li>
@ -25,12 +28,16 @@
<section id="system_settings" role="tabpanel" aria-labelledby="config-system" [hidden]='!isCurrentTabContent("system_settings")'>
<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>
</section>
<section id="system_label" role="tabpanel" aria-labelledby="config-label" [hidden]='!isCurrentTabContent("system_label")' style="padding-top: 16px;">
<hbr-label [scope]="'g'"></hbr-label>
<!--<system-settings [(systemSettings)]="allConfig" [hasAdminRole]="hasAdminRole" [hasCAFile]="hasCAFile"></system-settings>-->
</section>
<section id="vulnerability" *ngIf="withClair" role="tabpanel" aria-labelledby="config-vulnerability" [hidden]='!isCurrentTabContent("vulnerability")'>
<vulnerability-config [(vulnerabilityConfig)]="allConfig"></vulnerability-config>
</section>
<div>
<button type="button" class="btn btn-primary" (click)="save()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-primary" (click)="save()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.SAVE' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="cancel()" [hidden]="hideBtn" [disabled]="!isValid() || !hasChanges()">{{'BUTTON.CANCEL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testMailServer()" *ngIf="showTestServerBtn" [disabled]="!isMailConfigValid()">{{'BUTTON.TEST_MAIL' | translate}}</button>
<button type="button" class="btn btn-outline" (click)="testLDAPServer()" *ngIf="showLdapServerBtn" [disabled]="!isLDAPConfigValid()">{{'BUTTON.TEST_LDAP' | translate}}</button>
<span id="forTestingMail" class="spinner spinner-inline" [hidden]="hideMailTestingSpinner"></span>

View File

@ -38,7 +38,8 @@ const TabLinkContentMap = {
'config-replication': 'replication',
'config-email': 'email',
'config-system': 'system_settings',
'config-vulnerability': 'vulnerability'
'config-vulnerability': 'vulnerability',
'config-label': 'system_label'
};
@Component({
@ -200,6 +201,10 @@ export class ConfigurationComponent implements OnInit, OnDestroy {
this.allConfig.auth_mode.value === 'ldap_auth';
}
public get hideBtn(): boolean {
return this.currentTabId === 'config-label';
}
public get hideMailTestingSpinner(): boolean {
return !this.testingMailOnGoing || !this.showTestServerBtn;
}

View File

@ -51,6 +51,7 @@ import { MemberGuard } from './shared/route/member-guard-activate.service';
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
import { LeavingRepositoryRouteDeactivate } from './shared/route/leaving-repository-deactivate.service';
import {ProjectLabelComponent} from "./project/project-label/project-label.component";
const harborRoutes: Routes = [
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
@ -138,6 +139,9 @@ const harborRoutes: Routes = [
{
path: 'logs',
component: AuditLogComponent
},{
path: 'labels',
component: ProjectLabelComponent
},
{
path: 'configs',

View File

@ -10,12 +10,15 @@
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSProjectAdmin || isSystemAdmin">
<a class="nav-link" routerLink="labels" routerLinkActive="active">{{'PROJECT_DETAIL.LABELS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid && (isSystemAdmin || isMember)">
<a class="nav-link" routerLink="configs" routerLinkActive="active">{{'PROJECT_DETAIL.CONFIG' | translate}}</a>
</li>

Some files were not shown because too many files have changed in this diff Show More