mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-22 18:25:56 +01:00
Merge with master
This commit is contained in:
commit
381ecc3521
@ -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
|
||||
|
13
Makefile
13
Makefile
@ -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)
|
||||
|
1
contrib/helm/harbor/.gitignore
vendored
Normal file
1
contrib/helm/harbor/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
charts/*
|
@ -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:
|
||||
|
@ -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
|
||||
| | | |
|
||||
|
||||
|
||||
|
6
contrib/helm/harbor/requirements.lock
Normal file
6
contrib/helm/harbor/requirements.lock
Normal 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
|
4
contrib/helm/harbor/requirements.yaml
Normal file
4
contrib/helm/harbor/requirements.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 0.9.1
|
||||
repository: https://kubernetes-charts.storage.googleapis.com
|
@ -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: ""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }}
|
@ -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 -}}
|
@ -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 }}
|
@ -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 -}}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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: >-
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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` (
|
||||
|
@ -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 (
|
||||
|
@ -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 "$@" ;;
|
||||
|
@ -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
16
src/Gopkg.lock
generated
@ -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"
|
||||
|
@ -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"
|
@ -48,6 +48,7 @@ var (
|
||||
common.EmailInsecure: true,
|
||||
common.LDAPVerifyCert: true,
|
||||
common.UAAVerifyCert: true,
|
||||
common.ReadOnly: true,
|
||||
}
|
||||
mapKeys = map[string]bool{
|
||||
common.ScanAllPolicy: true,
|
||||
|
@ -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
|
||||
|
@ -99,4 +99,5 @@ const (
|
||||
RegistryStorageProviderName = "registry_storage_provider_name"
|
||||
UserMember = "u"
|
||||
GroupMember = "g"
|
||||
ReadOnly = "read_only"
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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{
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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------------------------//
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
76
src/ui/filter/readonly.go
Normal 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
|
||||
}
|
83
src/ui/filter/readonly_test.go
Normal file
83
src/ui/filter/readonly_test.go
Normal 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)
|
||||
|
||||
}
|
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;}
|
||||
`;
|
@ -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>`;
|
@ -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('');
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
6
src/ui_ng/lib/src/create-edit-label/index.ts
Normal file
6
src/ui_ng/lib/src/create-edit-label/index.ts
Normal 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
|
||||
];
|
@ -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
|
||||
]
|
||||
};
|
||||
|
@ -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';
|
||||
|
8
src/ui_ng/lib/src/label-piece/index.ts
Normal file
8
src/ui_ng/lib/src/label-piece/index.ts
Normal 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
|
||||
];
|
46
src/ui_ng/lib/src/label-piece/label-piece.component.js
Normal file
46
src/ui_ng/lib/src/label-piece/label-piece.component.js
Normal 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
|
@ -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":""}
|
36
src/ui_ng/lib/src/label-piece/label-piece.component.ts
Normal file
36
src/ui_ng/lib/src/label-piece/label-piece.component.ts
Normal 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 {
|
||||
}
|
||||
}
|
8
src/ui_ng/lib/src/label-piece/label-piece.template.js
Normal file
8
src/ui_ng/lib/src/label-piece/label-piece.template.js
Normal 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
|
@ -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":""}
|
17
src/ui_ng/lib/src/label-piece/label-piece.template.ts
Normal file
17
src/ui_ng/lib/src/label-piece/label-piece.template.ts
Normal 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;}
|
||||
`;
|
6
src/ui_ng/lib/src/label/index.ts
Normal file
6
src/ui_ng/lib/src/label/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Type } from '@angular/core';
|
||||
import {LabelComponent} from "./label.component";
|
||||
|
||||
export const LABEL_DIRECTIVES: Type<any>[] = [
|
||||
LabelComponent
|
||||
];
|
21
src/ui_ng/lib/src/label/label.component.css.ts
Normal file
21
src/ui_ng/lib/src/label/label.component.css.ts
Normal 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;}
|
||||
`;
|
43
src/ui_ng/lib/src/label/label.component.html.ts
Normal file
43
src/ui_ng/lib/src/label/label.component.html.ts
Normal 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> {{'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> {{'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> {{'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>
|
||||
`;
|
131
src/ui_ng/lib/src/label/label.component.spec.ts
Normal file
131
src/ui_ng/lib/src/label/label.component.spec.ts
Normal 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');
|
||||
|
||||
})
|
||||
})*/
|
||||
|
||||
})
|
175
src/ui_ng/lib/src/label/label.component.ts
Normal file
175
src/ui_ng/lib/src/label/label.component.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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> {{'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> {{'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> {{'REPLICATION.DELETE_POLICY' | translate}}</button>
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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>();
|
||||
|
@ -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;
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
|
125
src/ui_ng/lib/src/service/label.service.ts
Normal file
125
src/ui_ng/lib/src/service/label.service.ts
Normal 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));
|
||||
}
|
||||
}
|
@ -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': []
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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 = {
|
||||
|
@ -30,7 +30,8 @@ export class TagDetailComponent implements OnInit {
|
||||
architecture: "--",
|
||||
os: "--",
|
||||
docker_version: "--",
|
||||
digest: "--"
|
||||
digest: "--",
|
||||
labels: [],
|
||||
};
|
||||
|
||||
@Output() backEvt: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
@ -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;}
|
||||
`;
|
@ -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> {{'VULNERABILITY.SCAN_NOW' | translate}}</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" [disabled]="!(selectedRow.length==1)" (click)="showDigestId(selectedRow)" ><clr-icon shape="copy" size="16"></clr-icon> {{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<clr-dropdown>
|
||||
<button type="button" class="btn btn-sm btn-secondary" clrDropdownTrigger [disabled]="!(selectedRow.length==1) || isGuest" (click)="addLabels(selectedRow)" >{{'REPOSITORY.ADD_LABELS' | translate}}</button>
|
||||
<clr-dropdown-menu clrPosition="bottom-left" *clrIfOpen>
|
||||
<div style='display:grid'>
|
||||
<label class="dropdown-header">{{'REPOSITORY.ADD_TO_IMAGE' | translate}}</label>
|
||||
<div class="form-group"><input type="text" placeholder="Filter labels" #stickLabelNamePiece (keyup)="handleStickInputFilter(stickLabelNamePiece.value)"></div>
|
||||
<div [hidden]='imageStickLabels.length'>{{'LABEL.NO_LABELS' | translate }}</div>
|
||||
<div [hidden]='!imageStickLabels.length' style='max-height:300px;overflow-y: auto;'>
|
||||
<button type="button" class="dropdown-item" *ngFor='let label of imageStickLabels' (click)="label.iconsShow = true; selectLabel(label)">
|
||||
<clr-icon shape="check" class='pull-left' [hidden]='!label.iconsShow'></clr-icon>
|
||||
<div class='labelDiv'><hbr-label-piece [label]="label.label"></hbr-label-piece></div>
|
||||
<clr-icon shape="times-circle" class='pull-right' [hidden]='!label.iconsShow' (click)="$event.stopPropagation(); label.iconsShow = false; unSelectLabel(label)"></clr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</clr-dropdown-menu>
|
||||
</clr-dropdown>
|
||||
<button type="button" class="btn btn-sm btn-secondary" *ngIf="hasProjectAdminRole" (click)="deleteTags(selectedRow)" [disabled]="!selectedRow.length"><clr-icon shape="times" size="16"></clr-icon> {{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</div>
|
||||
</clr-dg-action-bar>
|
||||
<clr-dg-column style="width: 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>
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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 = [];
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user