diff --git a/contrib/helm/harbor/README.md b/contrib/helm/harbor/README.md index 52da97939..836437712 100644 --- a/contrib/helm/harbor/README.md +++ b/contrib/helm/harbor/README.md @@ -2,57 +2,40 @@ ## Introduction -This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes cluster. +This [Helm](https://github.com/kubernetes/helm) chart installs [Harbor](http://vmware.github.io/harbor/) in a Kubernetes cluster. Currently this chart supports Harbor v1.4.0 release. ## Prerequisites - Kubernetes cluster 1.8+ with Beta APIs enabled - Kubernetes Ingress Controller is enabled - kubectl CLI 1.8+ -- PV provisioner support in the underlying infrastructure +- Helm CLI 2.8.0+ ## Setup a Kubernetes cluster You can use any tools to setup a K8s cluster. In this guide, we use [minikube](https://github.com/kubernetes/minikube) to setup a K8s cluster as the dev/test env. - ```bash # Start minikube minikube start --vm-driver=none # Enable Ingress Controller minikube addons enable ingress ``` - ## Installing the Chart First install [Helm CLI](https://github.com/kubernetes/helm#install), then initialize Helm. ```bash -helm init --canary-image +helm init ``` - Download Harbor helm chart code. - ```bash git clone https://github.com/vmware/harbor -cd harbor/contrib/helm/harbor +cd contrib/helm/harbor ``` - -### Insecure Registry Mode - -If setting Harbor Registry as insecure-registries for docker, -you don't need to generate Root CA and SSL certificate for the Harbor ingress controller. - -Install the Harbor helm chart with a release name `my-release`: - +Download external dependent charts required by Harbor chart. ```bash -helm install . --debug --name my-release --set externalDomain=harbor.my.domain,insecureRegistry=true +helm dependency update ``` - -**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI. -You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor..xip.io`. - -Then add `"insecure-registries": ["harbor.my.domain"]` in the docker daemon config file and restart docker service. - ### Secure Registry Mode By default this chart will generate a root CA and SSL certificate for your Harbor. @@ -62,18 +45,33 @@ open values.yaml, set the value of 'externalDomain' to your Harbor FQDN, and set value of 'tlsCrt', 'tlsKey', 'caCrt'. The common name of the certificate must match your Harbor FQDN. Install the Harbor helm chart with a release name `my-release`: - ```bash helm install . --debug --name my-release --set externalDomain=harbor.my.domain ``` +**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI. +You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor..xip.io`. Follow the `NOTES` section in the command output to get Harbor admin password and **add Harbor root CA into docker trusted certificates**. The command deploys Harbor on the Kubernetes cluster in the default configuration. -The [configuration](#configuration) section lists the parameters that can be configured during installation. +The [configuration](#configuration) section lists the parameters that can be configured in values.yaml or via '--set' params during installation. > **Tip**: List all releases using `helm list` +### Insecure Registry Mode + +If setting Harbor Registry as insecure-registries for docker, +you don't need to generate Root CA and SSL certificate for the Harbor ingress controller. + +Install the Harbor helm chart with a release name `my-release`: +```bash +helm install . --debug --name my-release --set externalDomain=harbor.my.domain,insecureRegistry=true +``` +**Make sure** `harbor.my.domain` resolves to the K8s Ingress Controller IP on the machines where you run docker or access Harbor UI. +You can add `harbor.my.domain` and IP mapping in the DNS server, or in /etc/hosts, or use the FQDN `harbor..xip.io`. + +Then add `"insecure-registries": ["harbor.my.domain"]` in the docker daemon config file and restart docker service. + ## Uninstalling the Chart To uninstall/delete the `my-release` deployment: @@ -111,7 +109,17 @@ The following tables lists the configurable parameters of the Harbor chart and t | `adminserver.emailIdentity` | | "" | | `adminserver.key` | adminsever key | `not-a-secure-key` | | `adminserver.emailPwd` | password for email | `not-a-secure-password` | -| `adminserver.harborAdminPassword` | password for admin user | `Harbor12345` | +| `adminserver.adminPassword` | password for admin user | `Harbor12345` | +| `adminserver.authenticationMode` | authentication mode for Harbor ( `db_auth` for local database, `ldap_auth` for LDAP, etc...) [Docs](https://github.com/vmware/harbor/blob/master/docs/user_guide.md#user-account) | `db_auth` | +| `adminserver.selfRegistration` | Allows users to register by themselves, otherwise only administrators can add users | `on` | +| `adminserver.ldap.url` | LDAP server URL for `ldap_auth` authentication | `ldaps://ldapserver` | +| `adminserver.ldap.searchDN` | LDAP Search DN | `` | +| `adminserver.ldap.baseDN` | LDAP Base DN | `` | +| `adminserver.ldap.filter` | LDAP Filter | `(objectClass=person)` | +| `adminserver.ldap.uid` | LDAP UID | `uid` | +| `adminserver.ldap.scope` | LDAP Scope | `2` | +| `adminserver.ldap.timeout` | LDAP Timeout | `5` | +| `adminserver.ldap.verifyCert` | LDAP Verify HTTPS Certificate | `True` | | `adminserver.resources` | [resources](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to allocate for container | undefined | | `adminserver.volumes` | used to create PVCs if persistence is enabled (see instructions in values.yaml) | see values.yaml | | **Jobservice** | diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml index 1ff8f8b8b..90d62aff8 100644 --- a/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml +++ b/contrib/helm/harbor/templates/adminserver/adminserver-cm.yaml @@ -29,17 +29,16 @@ data: WITH_NOTARY: "{{ .Values.notary.enabled }}" LOG_LEVEL: "info" IMAGE_STORE_PATH: "/" # This is a temporary hack. - AUTH_MODE: "db_auth" - SELF_REGISTRATION: "on" - LDAP_URL: "ldaps://ldapserver" - LDAP_SEARCH_DN: "" - LDAP_BASE_DN: "" - LDAP_FILTER: "(objectClass=person)" - LDAP_UID: "uid" - LDAP_SCOPE: "2" - LDAP_TIMEOUT: "5" - LDAP_TIMEOUT: "5" - LDAP_VERIFY_CERT: "True" + AUTH_MODE: "{{ .Values.adminserver.authenticationMode }}" + SELF_REGISTRATION: "{{ .Values.adminserver.selfRegistration }}" + LDAP_URL: "{{ .Values.adminserver.ldap.url }}" + LDAP_SEARCH_DN: "{{ .Values.adminserver.ldap.searchDN }}" + LDAP_BASE_DN: "{{ .Values.adminserver.ldap.baseDN }}" + LDAP_FILTER: "{{ .Values.adminserver.ldap.filter }}" + LDAP_UID: "{{ .Values.adminserver.ldap.uid }}" + LDAP_SCOPE: "{{ .Values.adminserver.ldap.scope }}" + LDAP_TIMEOUT: "{{ .Values.adminserver.ldap.timeout }}" + LDAP_VERIFY_CERT: "{{ .Values.adminserver.ldap.verifyCert }}" DATABASE_TYPE: "mysql" PROJECT_CREATION_RESTRICTION: "everyone" VERIFY_REMOTE_CERT: "off" diff --git a/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml b/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml index 6014ca16f..fd2aff13f 100644 --- a/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml +++ b/contrib/helm/harbor/templates/adminserver/adminserver-secrets.yaml @@ -9,7 +9,7 @@ type: Opaque data: secretKey: {{ .Values.secretKey | b64enc | quote }} EMAIL_PWD: {{ .Values.adminserver.emailPwd | b64enc | quote }} - HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.harborAdminPassword | b64enc | quote }} + HARBOR_ADMIN_PASSWORD: {{ .Values.adminserver.adminPassword | b64enc | quote }} MYSQL_PWD: {{ .Values.mysql.pass | b64enc | quote }} JOBSERVICE_SECRET: {{ .Values.jobservice.secret | b64enc | quote }} UI_SECRET: {{ .Values.ui.secret | b64enc | quote }} diff --git a/contrib/helm/harbor/values.yaml b/contrib/helm/harbor/values.yaml index 737bf2b74..0e980e98e 100644 --- a/contrib/helm/harbor/values.yaml +++ b/contrib/helm/harbor/values.yaml @@ -65,7 +65,18 @@ adminserver: emailIdentity: "" emailInsecure: "False" emailPwd: not-a-secure-password - harborAdminPassword: Harbor12345 + adminPassword: Harbor12345 + authenticationMode: "db_auth" + selfRegistration: "on" + ldap: + url: "ldaps://ldapserver" + searchDN: "" + baseDN: "" + filter: "(objectClass=person)" + uid: "uid" + scope: "2" + timeout: "5" + verifyCert: "True" ## Persist data to a persistent volume volumes: config: diff --git a/make/common/templates/clair/clair_env b/make/common/templates/clair/clair_env new file mode 100644 index 000000000..62991299b --- /dev/null +++ b/make/common/templates/clair/clair_env @@ -0,0 +1,3 @@ +http_proxy=$http_proxy +https_proxy=$https_proxy +no_proxy=$no_proxy diff --git a/make/docker-compose.clair.tpl b/make/docker-compose.clair.tpl index e5e4d7bfe..4c652f115 100644 --- a/make/docker-compose.clair.tpl +++ b/make/docker-compose.clair.tpl @@ -47,6 +47,8 @@ services: options: syslog-address: "tcp://127.0.0.1:1514" tag: "clair" + env_file: + ./common/config/clair/clair_env networks: harbor-clair: external: false diff --git a/make/harbor.cfg b/make/harbor.cfg index a7c851b07..86e8743f0 100644 --- a/make/harbor.cfg +++ b/make/harbor.cfg @@ -36,6 +36,12 @@ log_rotate_count = 50 #are all valid. log_rotate_size = 200M +#Config http proxy for Clair, e.g. http://my.proxy.com:3128 +#Clair doesn't need to connect to harbor ui container via http proxy. +http_proxy = +https_proxy = +no_proxy = 127.0.0.1,localhost,ui + #NOTES: The properties between BEGIN INITIAL PROPERTIES and END INITIAL PROPERTIES #only take effect in the first boot, the subsequent changes of these properties #should be performed on web ui diff --git a/make/photon/db/registry.sql b/make/photon/db/registry.sql index 8a4d1b5c8..b256bc3a4 100644 --- a/make/photon/db/registry.sql +++ b/make/photon/db/registry.sql @@ -199,7 +199,8 @@ create table replication_job ( update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX policy (policy_id), - INDEX poid_uptime (policy_id, update_time) + INDEX poid_uptime (policy_id, update_time), + INDEX poid_status (policy_id, status) ); create table replication_immediate_trigger ( @@ -223,7 +224,11 @@ create table img_scan_job ( job_uuid varchar(64), creation_time timestamp default CURRENT_TIMESTAMP, update_time timestamp default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, - PRIMARY KEY (id) + PRIMARY KEY (id), + INDEX idx_status (status), + INDEX idx_digest (digest), + INDEX idx_uuid (job_uuid), + INDEX idx_repository_tag (repository,tag) ); create table img_scan_overview ( @@ -298,4 +303,4 @@ CREATE TABLE IF NOT EXISTS `alembic_version` ( `version_num` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -insert into alembic_version values ('1.4.0'); +insert into alembic_version values ('1.5.0'); diff --git a/make/photon/db/registry_sqlite.sql b/make/photon/db/registry_sqlite.sql index a6e5d8710..c6e4ed2e9 100644 --- a/make/photon/db/registry_sqlite.sql +++ b/make/photon/db/registry_sqlite.sql @@ -290,5 +290,5 @@ create table alembic_version ( version_num varchar(32) NOT NULL ); -insert into alembic_version values ('1.4.0'); +insert into alembic_version values ('1.5.0'); diff --git a/make/prepare b/make/prepare index 49e278077..6dd2742cd 100755 --- a/make/prepare +++ b/make/prepare @@ -566,6 +566,15 @@ if args.clair_mode: username = clair_db_username, host = clair_db_host, port = clair_db_port) + # config http proxy for Clair + http_proxy = rcp.get("configuration", "http_proxy").strip() + https_proxy = rcp.get("configuration", "https_proxy").strip() + no_proxy = rcp.get("configuration", "no_proxy").strip() + clair_env = os.path.join(clair_config_dir, "clair_env") + render(os.path.join(clair_temp_dir, "clair_env"), clair_env, + http_proxy = http_proxy, + https_proxy = https_proxy, + no_proxy = no_proxy) if args.ha_mode: prepare_ha(rcp, args) diff --git a/src/common/dao/resource_label.go b/src/common/dao/resource_label.go index 56b17916b..b80b3bb03 100644 --- a/src/common/dao/resource_label.go +++ b/src/common/dao/resource_label.go @@ -120,3 +120,9 @@ func ListResourceLabels(query ...*models.ResourceLabelQuery) ([]*models.Resource _, err := qs.All(&rls) return rls, err } + +// DeleteResourceLabelByLabel delete the mapping relationship by label ID +func DeleteResourceLabelByLabel(id int64) error { + _, err := GetOrmer().QueryTable(&models.ResourceLabel{}).Filter("LabelID", id).Delete() + return err +} diff --git a/src/common/dao/resource_label_test.go b/src/common/dao/resource_label_test.go index 572bec8b5..467f019a0 100644 --- a/src/common/dao/resource_label_test.go +++ b/src/common/dao/resource_label_test.go @@ -81,4 +81,15 @@ func TestMethodsOfResourceLabel(t *testing.T) { labels, err = GetLabelsOfResource(resourceType, resourceID) require.Nil(t, err) require.Equal(t, 0, len(labels)) + + // delete by label ID + id, err = AddResourceLabel(rl) + require.Nil(t, err) + err = DeleteResourceLabelByLabel(labelID) + require.Nil(t, err) + rls, err = ListResourceLabels(&models.ResourceLabelQuery{ + LabelID: labelID, + }) + require.Nil(t, err) + require.Equal(t, 0, len(rls)) } diff --git a/src/common/dao/version.go b/src/common/dao/version.go index 77c61d1de..b2981dc4b 100644 --- a/src/common/dao/version.go +++ b/src/common/dao/version.go @@ -20,7 +20,7 @@ import ( const ( // SchemaVersion is the version of database schema - SchemaVersion = "1.4.0" + SchemaVersion = "1.5.0" ) // GetSchemaVersion return the version of database schema diff --git a/src/ui/api/label.go b/src/ui/api/label.go index 3d7dfe978..db6144e57 100644 --- a/src/ui/api/label.go +++ b/src/ui/api/label.go @@ -256,6 +256,10 @@ func (l *LabelAPI) Put() { // Delete the label func (l *LabelAPI) Delete() { id := l.label.ID + if err := dao.DeleteResourceLabelByLabel(id); err != nil { + l.HandleInternalServerError(fmt.Sprintf("failed to delete resource label mappings of label %d: %v", id, err)) + return + } if err := dao.DeleteLabel(id); err != nil { l.HandleInternalServerError(fmt.Sprintf("failed to delete label %d: %v", id, err)) return diff --git a/src/ui/api/systeminfo.go b/src/ui/api/systeminfo.go index 699d2fef6..1a176d13a 100644 --- a/src/ui/api/systeminfo.go +++ b/src/ui/api/systeminfo.go @@ -99,6 +99,7 @@ type GeneralInfo struct { NextScanAll int64 `json:"next_scan_all"` ClairVulnStatus *models.ClairVulnerabilityStatus `json:"clair_vulnerability_status,omitempty"` RegistryStorageProviderName string `json:"registry_storage_provider_name"` + ReadOnly bool `json:"read_only"` } // validate for validating user if an admin. @@ -177,6 +178,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() { HasCARoot: caStatErr == nil, HarborVersion: harborVersion, RegistryStorageProviderName: cfg[common.RegistryStorageProviderName].(string), + ReadOnly: config.ReadOnly(), } if info.WithClair { info.ClairVulnStatus = getClairVulnStatus() diff --git a/src/ui/api/systeminfo_test.go b/src/ui/api/systeminfo_test.go index edcc8c157..6f2edafd4 100644 --- a/src/ui/api/systeminfo_test.go +++ b/src/ui/api/systeminfo_test.go @@ -64,6 +64,7 @@ func TestGetGeneralInfo(t *testing.T) { assert.Equal(false, g.WithNotary, "with notary should be false") assert.Equal(true, g.HasCARoot, "has ca root should be true") assert.NotEmpty(g.HarborVersion, "harbor version should not be empty") + assert.Equal(false, g.ReadOnly, "readonly should be false") } func TestGetCert(t *testing.T) { diff --git a/src/ui_ng/lib/README.md b/src/ui_ng/lib/README.md index 47cd19d79..36cf2cfbe 100644 --- a/src/ui_ng/lib/README.md +++ b/src/ui_ng/lib/README.md @@ -93,6 +93,12 @@ On specific project mode, without need projectId, but also need to provide proje * **Repository and Tag Management View** +The `hbr-repository-stackview` directive is deprecated. Using `hbr-repository-listview` and `hbr-repository` instead. You should define two routers one for render +`hbr-repository-listview` the other is for `hbr-repository`. `hbr-repository-listview` will output an event, you need catch this event and redirect to related +page contains `hbr-repository`. + +**hbr-repository-listview Directive** + **projectId** is used to specify which projects the repositories are from. **projectName** is used to generate the related commands for pushing images. @@ -101,18 +107,99 @@ On specific project mode, without need projectId, but also need to provide proje **hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property. -**tagClickEvent** is an @output event emitter for you to catch the tag click events. +**repoClickEvent** is an @output event emitter for you to catch the repository click events. + ``` - + ... -watchTagClickEvent(tag: Tag): void { - //Process tag +watchRepoClickEvent(repo: RepositoryItem): void { + //Process repo + ... +} +``` + + +**hbr-repository-gridview Directive** + +**projectId** is used to specify which projects the repositories are from. + +**projectName** is used to generate the related commands for pushing images. + +**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user. + +**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property. + +**withVIC** is integrated with VIC + +**repoClickEvent** is an @output event emitter for you to catch the repository click events. + +**repoProvisionEvent** is an @output event emitter for you to catch the deploy button click event. + +**addInfoEvent** is an @output event emitter for you to catch the add additional info button event. + + @Output() repoProvisionEvent = new EventEmitter(); + @Output() addInfoEvent = new EventEmitter(); + + +``` + + +... + + +watchRepoClickEvent(repo: RepositoryItem): void { + //Process repo ... } +watchRepoProvisionEvent(repo: RepositoryItem): void { + //Process repo + ... +} + +watchAddInfoEvent(repo: RepositoryItem): void { + //Process repo + ... +} +``` + + +**hbr-repository Directive** + +**projectId** is used to specify which projects the repositories are from. + +**repoName** is used to generate the related commands for pushing images. + +**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user. + +**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property. + +**withClair** is Clair installed + +**withNotary** is Notary installed + +**tagClickEvent** is an @output event emitter for you to catch the tag click events. + +**goBackClickEvent** is an @output event emitter for you to catch the go back events. + +``` + + +watchTagClickEvt(tagEvt: TagClickEvent): void { + ... +} + +watchGoBackEvt(projectId: string): void { + ... +} ``` * **Tag detail view** diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index 0383986ee..dac2b4339 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.45", + "version": "0.6.61", "description": "Harbor shared UI components based on Clarity and Angular4", "scripts": { "start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json", diff --git a/src/ui_ng/lib/pkg/package.json b/src/ui_ng/lib/pkg/package.json index 79ec52511..f5ecf6cf4 100644 --- a/src/ui_ng/lib/pkg/package.json +++ b/src/ui_ng/lib/pkg/package.json @@ -1,6 +1,6 @@ { "name": "harbor-ui", - "version": "0.6.45", + "version": "0.6.61", "description": "Harbor shared UI components based on Clarity and Angular4", "author": "VMware", "module": "index.js", diff --git a/src/ui_ng/lib/src/config/config.ts b/src/ui_ng/lib/src/config/config.ts index f3b80b8d7..624967756 100644 --- a/src/ui_ng/lib/src/config/config.ts +++ b/src/ui_ng/lib/src/config/config.ts @@ -81,6 +81,7 @@ export class Configuration { token_expiration: NumberValueItem; cfg_expiration: NumberValueItem; scan_all_policy: ComplexValueItem; + read_only: BoolValueItem; public constructor() { this.auth_mode = new StringValueItem("db_auth", true); @@ -116,5 +117,6 @@ export class Configuration { daily_time: 0 } }, true); + this.read_only = new BoolValueItem(false, true); } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts index 4653a70d1..8bf46d759 100644 --- a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts +++ b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.css.ts @@ -10,8 +10,8 @@ export const CREATE_EDIT_LABEL_STYLE: string = ` section{padding:.5rem 0;} section> label{margin-left: 20px;} - .dropdown-menu{display:inline-block;width:166px; padding:6px;} - .dropdown-item{ display:inline-flex; margin:2px 4px; + .dropdown-menu {display:inline-block;width:166px; padding:6px;} + .dropdown-menu .dropdown-item{ display:inline-flex; margin:2px 4px; display: inline-block;padding: 0px; width:30px;height:24px; text-align: center;line-height: 24px;} .btnColor{ margin: 0 !important; diff --git a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts index c1e838231..1c35656a4 100644 --- a/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts +++ b/src/ui_ng/lib/src/create-edit-label/create-edit-label.component.ts @@ -16,7 +16,7 @@ import { Output, EventEmitter, OnDestroy, - Input, OnInit, ViewChild + Input, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; @@ -27,16 +27,15 @@ import { CREATE_EDIT_LABEL_TEMPLATE } from './create-edit-label.component.html'; import {toPromise, clone, compareValue} from '../utils'; -import {Subject} from "rxjs/Subject"; - import {LabelService} from "../service/label.service"; import {ErrorHandler} from "../error-handler/error-handler"; import {NgForm} from "@angular/forms"; +import {Subject} from "rxjs/Subject"; @Component({ selector: 'hbr-create-edit-label', template: CREATE_EDIT_LABEL_TEMPLATE, - styles: [CREATE_EDIT_LABEL_STYLE] + styles: [CREATE_EDIT_LABEL_STYLE], }) export class CreateEditLabelComponent implements OnInit, OnDestroy { @@ -46,12 +45,13 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy { labelModel: Label = this.initLabel(); labelId = 0; - nameChecker: Subject = new Subject(); checkOnGoing: boolean; isLabelNameExist = false; labelColor = ['#00ab9a', '#9da3db', '#be90d6', '#9b0d54', '#f52f22', '#747474', '#0095d3', '#f38b00', ' #62a420', '#89cbdf', '#004a70', '#9460b8']; + nameChecker = new Subject(); + labelForm: NgForm; @ViewChild('labelForm') currentForm: NgForm; @@ -66,16 +66,12 @@ export class CreateEditLabelComponent implements OnInit, OnDestroy { ) { } ngOnInit(): void { - this.nameChecker.debounceTime(500).distinctUntilChanged().subscribe((name: string) => { + this.nameChecker.debounceTime(500).subscribe((name: string) => { this.checkOnGoing = true; - toPromise(this.labelService.getLabels(this.scope, this.projectId)) + toPromise(this.labelService.getLabels(this.scope, this.projectId, name)) .then(targets => { if (targets && targets.length) { - if (targets.find(m => m.name === name)) { - this.isLabelNameExist = true; - } else { - this.isLabelNameExist = false; - }; + this.isLabelNameExist = true; }else { this.isLabelNameExist = false; } diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.css.ts b/src/ui_ng/lib/src/gridview/grid-view.component.css.ts new file mode 100644 index 000000000..e97f73bf3 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.css.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved. +// This software is released under MIT license. +// The full license information can be found in LICENSE in the root directory of this project. + +// @import 'node_modules/admiral-ui-common/css/mixins'; + +export const GRIDVIEW_STYLE = ` +.grid-content { + position: relative; + top: 36px; + left: 0; + right: 0; + bottom: 0; + overflow: auto; + max-height: 65vh; +} + +.card-item { + display: block; + max-width: 400px; + min-width: 300px; + position: absolute; + margin-right: 40px; + transition: width 0.4s, transform 0.4s; +} + +.content-empty { + text-align: center; + display: block; + margin-top: 100px; +} + +.central-block-loading { + position: absolute; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + @include animation(fadein 0.4s); + text-align: center; + background-color: rgba(255, 255, 255, 0.5); +} +.central-block-loading-more { + position: relative; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + @include animation(fadein 0.4s); + text-align: center; + background-color: rgba(255, 255, 255, 0.5); +} +.vertical-helper { + display: inline-block; + height: 100%; + vertical-align: middle; +} + +.spinner { + width: 100px; + height: 100px; + vertical-align: middle; +} + +` \ No newline at end of file diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.html.ts b/src/ui_ng/lib/src/gridview/grid-view.component.html.ts new file mode 100644 index 000000000..fdc9937aa --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.html.ts @@ -0,0 +1,18 @@ +export const GRIDVIEW_TEMPLATE = ` +
+
+ + + + + + {{'REPOSITORY.NO_ITEMS' | translate}} + +
+
+ +
+
+
+` \ No newline at end of file diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.spec.ts b/src/ui_ng/lib/src/gridview/grid-view.component.spec.ts new file mode 100644 index 000000000..5c0c63445 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.spec.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GridViewComponent } from './grid-view.component'; +import { SharedModule } from '../shared/shared.module'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; + + +describe('GridViewComponent', () => { + let component: GridViewComponent; + let fixture: ComponentFixture; + + let config: IServiceConfig = { + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + ], + declarations: [ + GridViewComponent, + ], + providers: [{ + provide: SERVICE_CONFIG, useValue: config }] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(GridViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/ui_ng/lib/src/gridview/grid-view.component.ts b/src/ui_ng/lib/src/gridview/grid-view.component.ts new file mode 100644 index 000000000..d34f08f94 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/grid-view.component.ts @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +import { Component, Input, Output, SimpleChanges, ContentChild, ViewChild, ViewChildren, + TemplateRef, HostListener, ViewEncapsulation, EventEmitter, AfterViewInit } from '@angular/core'; +import { CancelablePromise } from '../shared/shared.utils'; +import { Router, ActivatedRoute, NavigationEnd } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; + +import { TranslateService } from '@ngx-translate/core'; +import { GRIDVIEW_TEMPLATE } from './grid-view.component.html'; +import { GRIDVIEW_STYLE } from './grid-view.component.css'; +import { ScrollPosition } from '../service/interface' + +@Component({ + selector: 'hbr-gridview', + template: GRIDVIEW_TEMPLATE, + styles: [GRIDVIEW_STYLE], + encapsulation: ViewEncapsulation.None +}) +/** + * Grid view general component. + */ +export class GridViewComponent implements AfterViewInit { + @Input() loading: boolean; + @Input() totalCount: number; + @Input() currentPage: number; + @Input() pageSize: number; + @Input() expectScrollPercent = 70; + @Input() withAdmiral: boolean; + @Input() + set items(value: any[]) { + let newCardStyles = value.map((d, index) => { + if (index < this.cardStyles.length) { + return this.cardStyles[index]; + } + return { + opacity: '0', + overflow: 'hidden' + }; + }); + this.cardStyles = newCardStyles; + this._items = value; + } + + @Output() loadNextPageEvent = new EventEmitter(); + + @ViewChildren('cardItem') cards: any; + @ViewChild('itemsHolder') itemsHolder: any; + @ContentChild(TemplateRef) gridItemTmpl: any; + + _items: any[] = []; + + cardStyles: any = []; + itemsHolderStyle: any = {}; + layoutTimeout: any; + + querySub: Subscription; + routerSub: Subscription; + + totalItemsCount: number; + loadedPages = 0; + nextPageLink: string; + hidePartialRows = false; + loadPagesTimeout: any; + + CurrentScrollPosition: ScrollPosition = { + sH: 0, + sT: 0, + cH: 0 + }; + + preScrollPosition: ScrollPosition = null; + + constructor(private translate: TranslateService) { } + + ngAfterViewInit() { + this.cards.changes.subscribe(() => { + this.throttleLayout(); + }); + this.throttleLayout(); + } + + get items() { + return this._items; + } + + @HostListener('scroll', ['$event']) + onScroll(event: any) { + + this.preScrollPosition = this.CurrentScrollPosition; + this.CurrentScrollPosition = { + sH: event.target.scrollHeight, + sT: event.target.scrollTop, + cH: event.target.clientHeight + } + if (!this.loading + && this.isScrollDown() + && this.isScrollExpectPercent() + && (this.currentPage * this.pageSize < this.totalCount)) { + this.loadNextPageEvent.emit(); + } + } + + isScrollDown(): boolean { + return this.preScrollPosition.sT < this.CurrentScrollPosition.sT; + } + + isScrollExpectPercent(): boolean { + return ((this.CurrentScrollPosition.sT + this.CurrentScrollPosition.cH) / this.CurrentScrollPosition.sH) > (this.expectScrollPercent / 100); + } + + @HostListener('window:resize') + onResize(event: any) { + this.throttleLayout(); + } + + throttleLayout() { + clearTimeout(this.layoutTimeout); + this.layoutTimeout = setTimeout(() => { + this.layout.call(this); + }, 40); + } + + get isFirstPage() { + return this.currentPage <= 1; + } + + layout() { + let el = this.itemsHolder.nativeElement; + + let width = el.offsetWidth; + let items = el.querySelectorAll('.card-item'); + let items_count = items.length; + if (items_count === 0) { + el.height = 0; + return; + } + + let itemsHeight = []; + for (let i = 0; i < items_count; i++) { + itemsHeight[i] = items[i].offsetHeight; + } + + let height = Math.max.apply(null, itemsHeight); + let itemsStyle: CSSStyleDeclaration = window.getComputedStyle(items[0]); + + let minWidthStyle: string = itemsStyle.minWidth; + let maxWidthStyle: string = itemsStyle.maxWidth; + + let minWidth = parseInt(minWidthStyle, 10); + let maxWidth = parseInt(maxWidthStyle, 10); + + let marginHeight: number = + parseInt(itemsStyle.marginTop, 10) + parseInt(itemsStyle.marginBottom, 10); + let marginWidth: number = + parseInt(itemsStyle.marginLeft, 10) + parseInt(itemsStyle.marginRight, 10); + + let columns = Math.floor(width / (minWidth + marginWidth)); + + let columnsToUse = Math.max(Math.min(columns, items_count), 1); + let rows = Math.floor(items_count / columnsToUse); + let itemWidth = Math.min(Math.floor(width / columnsToUse) - marginWidth, maxWidth); + let itemSpacing = columnsToUse === 1 || columns > items_count ? marginWidth : + (width - marginWidth - columnsToUse * itemWidth) / (columnsToUse - 1); + if (!this.withAdmiral) { + // Fixed spacing and margin on standalone mode + itemSpacing = marginWidth; + itemWidth = minWidth; + } + + let visible = items_count; + if (this.hidePartialRows && this.totalItemsCount && items_count !== this.totalItemsCount) { + visible = rows * columnsToUse; + } + + let count = 0; + for (let i = 0; i < visible; i++) { + let item = items[i]; + let itemStyle = window.getComputedStyle(item); + + let left = (i % columnsToUse) * (itemWidth + itemSpacing); + let top = Math.floor(count / columnsToUse) * (height + marginHeight); + + // trick to show nice apear animation, where the item is already positioned, + // but it will pop out + let oldTransform = itemStyle.transform; + if (!oldTransform || oldTransform === 'none') { + this.cardStyles[i] = { + transform: 'translate(' + left + 'px,' + top + 'px) scale(0)', + width: itemWidth + 'px', + transition: 'none', + overflow: 'hidden' + }; + this.throttleLayout(); + } else { + this.cardStyles[i] = { + transform: 'translate(' + left + 'px,' + top + 'px) scale(1)', + width: itemWidth + 'px', + transition: null, + overflow: 'hidden' + }; + this.throttleLayout(); + } + + if (!item.classList.contains('context-selected')) { + let itemHeight = itemsHeight[i]; + if (itemStyle.display === 'none' && itemHeight !== 0) { + this.cardStyles[i].display = null; + } + if (itemHeight !== 0) { + count++; + } + } + } + + for (let i = visible; i < items_count; i++) { + this.cardStyles[i] = { + display: 'none' + }; + } + this.itemsHolderStyle = { + height: Math.ceil(count / columnsToUse) * (height + marginHeight) + 'px' + }; + } + + onCardEnter(i: number) { + this.cardStyles[i].overflow = 'visible'; + } + + onCardLeave(i: number) { + this.cardStyles[i].overflow = 'hidden'; + } + + trackByFn(index: number, item: any) { + return index; + } +} diff --git a/src/ui_ng/lib/src/gridview/index.ts b/src/ui_ng/lib/src/gridview/index.ts new file mode 100644 index 000000000..3f5e40836 --- /dev/null +++ b/src/ui_ng/lib/src/gridview/index.ts @@ -0,0 +1,8 @@ +import { Type } from "@angular/core"; +import { GridViewComponent } from './grid-view.component'; + +export * from "./grid-view.component"; + +export const HBR_GRIDVIEW_DIRECTIVES: Type[] = [ + GridViewComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 17c87e7fa..8584dcbf5 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -25,6 +25,8 @@ import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index'; import { CONFIGURATION_DIRECTIVES } from './config/index'; import { JOB_LOG_VIEWER_DIRECTIVES } from './job-log-viewer/index'; import { PROJECT_POLICY_CONFIG_DIRECTIVES } from './project-policy-config/index'; +import { HBR_GRIDVIEW_DIRECTIVES } from './gridview/index'; +import { REPOSITORY_GRIDVIEW_DIRECTIVES } from './repository-gridview'; import { SystemInfoService, @@ -182,7 +184,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co PROJECT_POLICY_CONFIG_DIRECTIVES, LABEL_DIRECTIVES, CREATE_EDIT_LABEL_DIRECTIVES, - LABEL_PIECE_DIRECTIVES + LABEL_PIECE_DIRECTIVES, + HBR_GRIDVIEW_DIRECTIVES, + REPOSITORY_GRIDVIEW_DIRECTIVES, ], exports: [ LOG_DIRECTIVES, @@ -207,7 +211,9 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co PROJECT_POLICY_CONFIG_DIRECTIVES, LABEL_DIRECTIVES, CREATE_EDIT_LABEL_DIRECTIVES, - LABEL_PIECE_DIRECTIVES + LABEL_PIECE_DIRECTIVES, + HBR_GRIDVIEW_DIRECTIVES, + REPOSITORY_GRIDVIEW_DIRECTIVES, ], providers: [] }) diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 2e2d0e05f..501097b29 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -2,7 +2,7 @@ export * from './harbor-library.module'; export * from './service.config'; export * from './service/index'; export * from './error-handler/index'; -//export * from './utils'; +// export * from './utils'; export * from './log/index'; export * from './filter/index'; export * from './endpoint/index'; @@ -23,3 +23,5 @@ export * from './channel/index'; export * from './project-policy-config/index'; export * from './label/index'; export * from './create-edit-label'; +export * from './gridview/index'; +export * from './repository-gridview/index'; diff --git a/src/ui_ng/lib/src/repository-gridview/index.ts b/src/ui_ng/lib/src/repository-gridview/index.ts new file mode 100644 index 000000000..aa9cd65f2 --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/index.ts @@ -0,0 +1,8 @@ +import { Type } from "@angular/core"; +import { RepositoryGridviewComponent } from './repository-gridview.component'; + +export * from "./repository-gridview.component"; + +export const REPOSITORY_GRIDVIEW_DIRECTIVES: Type[] = [ + RepositoryGridviewComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.css.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.css.ts new file mode 100644 index 000000000..b0b8539e5 --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.css.ts @@ -0,0 +1,76 @@ +export const REPOSITORY_GRIDVIEW_STYLE = ` +.rightPos{ + position: absolute; + z-index: 100; + right: 35px; + margin-top: 4px; +} + +.toolbar { + overflow: hidden; +} + +.filter-divider { + display: inline-block; + height: 16px; + width: 2px; + background-color: #cccccc; + padding-top: 12px; + padding-bottom: 12px; + position: relative; + top: 9px; + margin-right: 6px; + margin-left: 6px; +} + +.card-block { + margin-top: 24px; + min-height: 100px; +} + +.form-group { + display: flex; +} + +.form-group > label { + width: 100px; +} + + +.card-media-block { + margin-top: 12px; + margin-bottom: 12px; +} + +.card-media-block > img { + height: 45px; + width: 45px; +} +.card-media-description { + height: 45px; +} +.card-media-description > p { + margin-top: 0px; +} + +.card-text { + height: 45px; + overflow: hidden; + margin-bottom: 18px; +} + +.card-block { + margin-top: 0px; +} + +.card-footer { + padding-top: 6px; + padding-bottom: 6px; +} + +.list-img > img { + height: 24px; + width: 24px; + margin-right: 12px; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.html.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.html.ts new file mode 100644 index 000000000..90c8820a7 --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.html.ts @@ -0,0 +1,92 @@ +export const REPOSITORY_GRIDVIEW_TEMPLATE = ` +
+
+
+
+
+ + + + + + + + + + +
+
+
+
+
+ + + + + {{'REPOSITORY.NAME' | translate}} + {{'REPOSITORY.TAGS_COUNT' | translate}} + {{'REPOSITORY.PULL_COUNT' | translate}} + {{'REPOSITORY.PLACEHOLDER' | translate }} + + {{r.name}} + {{r.tags_count}} + {{r.pull_count}} + + + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} + {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}} + + + +
+ + + +
+
+ +
+ + {{item.name}} + +

{{registryUrl}}

+
+
+
+
+
+ {{getRepoDescrition(item)}} +
+
+ +
{{item.tags_count}}
+
+
+ +
{{item.pull_count}}
+
+
+ +
+
+
+ +
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.spec.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.spec.ts new file mode 100644 index 000000000..438754bfb --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.spec.ts @@ -0,0 +1,175 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { RouterTestingModule } from '@angular/router/testing'; + +import { SharedModule } from '../shared/shared.module'; +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { RepositoryGridviewComponent } from './repository-gridview.component'; +import { TagComponent } from '../tag/tag.component'; +import { FilterComponent } from '../filter/filter.component'; + +import { ErrorHandler } from '../error-handler/error-handler'; +import { Repository, RepositoryItem, Tag, SystemInfo } from '../service/interface'; +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { RepositoryService, RepositoryDefaultService } from '../service/repository.service'; +import { TagService, TagDefaultService } from '../service/tag.service'; +import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service'; +import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index'; +import { HBR_GRIDVIEW_DIRECTIVES } from '../gridview/index' +import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index'; +import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index'; +import { JobLogViewerComponent } from '../job-log-viewer/index'; +import {LabelPieceComponent} from "../label-piece/label-piece.component"; + +import { click } from '../utils'; + +describe('RepositoryComponentGridview (inline template)', () => { + + let compRepo: RepositoryGridviewComponent; + let fixtureRepo: ComponentFixture; + let repositoryService: RepositoryService; + let tagService: TagService; + let systemInfoService: SystemInfoService; + + let spyRepos: jasmine.Spy; + let spySystemInfo: jasmine.Spy; + + let mockSystemInfo: SystemInfo = { + "with_notary": true, + "with_admiral": false, + "admiral_endpoint": "NA", + "auth_mode": "db_auth", + "registry_url": "10.112.122.56", + "project_creation_restriction": "everyone", + "self_registration": true, + "has_ca_root": false, + "harbor_version": "v1.1.1-rc1-160-g565110d" + }; + + let mockRepoData: RepositoryItem[] = [ + { + "id": 1, + "name": "library/busybox", + "project_id": 1, + "description": "asdfsadf", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + }, + { + "id": 2, + "name": "library/nginx", + "project_id": 1, + "description": "asdf", + "pull_count": 0, + "star_count": 0, + "tags_count": 1 + } + ]; + + let mockRepo: Repository = { + metadata: {xTotalCount: 2}, + data: mockRepoData + }; + + let mockTagData: Tag[] = [ + { + "digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55", + "name": "1.11.5", + "size": "2049", + "architecture": "amd64", + "os": "linux", + "docker_version": "1.12.3", + "author": "NGINX Docker Maintainers \"docker-maint@nginx.com\"", + "created": new Date("2016-11-08T22:41:15.912313785Z"), + "signature": null, + "labels": [] + } + ]; + + let config: IServiceConfig = { + repositoryBaseEndpoint: '/api/repository/testing', + systemInfoEndpoint: '/api/systeminfo/testing', + targetBaseEndpoint: '/api/tag/testing' + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule, + RouterTestingModule + ], + declarations: [ + RepositoryGridviewComponent, + TagComponent, + LabelPieceComponent, + ConfirmationDialogComponent, + FilterComponent, + VULNERABILITY_DIRECTIVES, + PUSH_IMAGE_BUTTON_DIRECTIVES, + INLINE_ALERT_DIRECTIVES, + HBR_GRIDVIEW_DIRECTIVES, + JobLogViewerComponent + ], + providers: [ + ErrorHandler, + { provide: SERVICE_CONFIG, useValue: config }, + { provide: RepositoryService, useClass: RepositoryDefaultService }, + { provide: TagService, useClass: TagDefaultService }, + { provide: SystemInfoService, useClass: SystemInfoDefaultService } + ] + }); + })); + + beforeEach(() => { + fixtureRepo = TestBed.createComponent(RepositoryGridviewComponent); + compRepo = fixtureRepo.componentInstance; + compRepo.projectId = 1; + compRepo.hasProjectAdminRole = true; + + repositoryService = fixtureRepo.debugElement.injector.get(RepositoryService); + systemInfoService = fixtureRepo.debugElement.injector.get(SystemInfoService); + + spyRepos = spyOn(repositoryService, 'getRepositories').and.returnValues(Promise.resolve(mockRepo)); + spySystemInfo = spyOn(systemInfoService, 'getSystemInfo').and.returnValues(Promise.resolve(mockSystemInfo)); + fixtureRepo.detectChanges(); + }); + + it('should create', () => { + expect(compRepo).toBeTruthy(); + }); + + it('should load and render data', async(() => { + fixtureRepo.detectChanges(); + + fixtureRepo.whenStable().then(() => { + fixtureRepo.detectChanges(); + + let deRepo: DebugElement = fixtureRepo.debugElement.query(By.css('datagrid-cell')); + expect(deRepo).toBeTruthy(); + let elRepo: HTMLElement = deRepo.nativeElement; + expect(elRepo).toBeTruthy(); + expect(elRepo.textContent).toEqual('library/busybox'); + }); + })); + + it('should filter data by keyword', async(() => { + fixtureRepo.detectChanges(); + + fixtureRepo.whenStable().then(() => { + fixtureRepo.detectChanges(); + + compRepo.doSearchRepoNames('nginx'); + fixtureRepo.detectChanges(); + let de: DebugElement[] = fixtureRepo.debugElement.queryAll(By.css('datagrid-cell')); + expect(de).toBeTruthy(); + expect(de.length).toEqual(1); + let el: HTMLElement = de[0].nativeElement; + expect(el).toBeTruthy(); + expect(el.textContent).toEqual('library/nginx'); + }); + })); + +}); diff --git a/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.ts b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.ts new file mode 100644 index 000000000..5215eef0f --- /dev/null +++ b/src/ui_ng/lib/src/repository-gridview/repository-gridview.component.ts @@ -0,0 +1,404 @@ +import { Component, Input, Output, OnInit, ViewChild, ChangeDetectionStrategy, ChangeDetectorRef, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subscription } from 'rxjs/Subscription'; +import {Observable} from "rxjs/Observable"; +import { TranslateService } from '@ngx-translate/core'; +import { Comparator, State } from 'clarity-angular'; + +import { REPOSITORY_GRIDVIEW_TEMPLATE } from './repository-gridview.component.html'; +import { REPOSITORY_GRIDVIEW_STYLE } from './repository-gridview.component.css'; +import { Repository, SystemInfo, SystemInfoService, RepositoryService, RequestQueryParams, RepositoryItem, TagService } from '../service/index'; +import { ErrorHandler } from '../error-handler/error-handler'; +import { toPromise, CustomComparator , DEFAULT_PAGE_SIZE, calculatePage, doFiltering, doSorting} from '../utils'; +import { ConfirmationState, ConfirmationTargets, ConfirmationButtons } from '../shared/shared.const'; +import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation-dialog.component'; +import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; +import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; +import { Tag, CardItemEvent } from '../service/interface'; +import {BatchInfo, BathInfoChanges} from "../confirmation-dialog/confirmation-batch-message"; +import { GridViewComponent } from '../gridview/grid-view.component' + + +@Component({ + selector: 'hbr-repository-gridview', + template: REPOSITORY_GRIDVIEW_TEMPLATE, + styles: [REPOSITORY_GRIDVIEW_STYLE], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class RepositoryGridviewComponent implements OnChanges, OnInit { + signedCon: {[key: string]: any | string[]} = {}; + @Input() projectId: number; + @Input() projectName = 'unknown'; + @Input() urlPrefix: string; + @Input() hasSignedIn: boolean; + @Input() hasProjectAdminRole: boolean; + @Output() repoClickEvent = new EventEmitter(); + @Output() repoProvisionEvent = new EventEmitter(); + @Output() addInfoEvent = new EventEmitter(); + + lastFilteredRepoName: string; + repositories: RepositoryItem[] = []; + repositoriesCopy: RepositoryItem[] = []; + systemInfo: SystemInfo; + selectedRow: RepositoryItem[] = []; + loading = true; + + isCardView: boolean; + cardHover = false; + listHover = false; + + batchDelectionInfos: BatchInfo[] = []; + pullCountComparator: Comparator = new CustomComparator('pull_count', 'number'); + tagsCountComparator: Comparator = new CustomComparator('tags_count', 'number'); + + pageSize: number = DEFAULT_PAGE_SIZE; + currentPage = 1; + totalCount = 0; + currentState: State; + + @ViewChild('confirmationDialog') + confirmationDialog: ConfirmationDialogComponent; + + @ViewChild('gridView') + gridView: GridViewComponent; + + + constructor( + private errorHandler: ErrorHandler, + private translateService: TranslateService, + private repositoryService: RepositoryService, + private systemInfoService: SystemInfoService, + private tagService: TagService, + private ref: ChangeDetectorRef, + private router: Router) { } + + public get registryUrl(): string { + return this.systemInfo ? this.systemInfo.registry_url : ''; + } + + public get withAdmiral(): boolean { + return this.systemInfo ? this.systemInfo.with_admiral : false; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['projectId'] && changes['projectId'].currentValue) { + this.refresh(); + } + } + + ngOnInit(): void { + if (this.withAdmiral) { + this.isCardView = true; + } else { + this.isCardView = false; + } + // Get system info for tag views + toPromise(this.systemInfoService.getSystemInfo()) + .then(systemInfo => this.systemInfo = systemInfo) + .catch(error => this.errorHandler.error(error)); + + this.lastFilteredRepoName = ''; + } + + confirmDeletion(message: ConfirmationAcknowledgement) { + if (message && + message.source === ConfirmationTargets.REPOSITORY && + message.state === ConfirmationState.CONFIRMED) { + + let promiseLists: any[] = []; + let repoNames: string[] = message.data.split(','); + + repoNames.forEach(repoName => { + promiseLists.push(this.delOperate(repoName)); + }); + + Promise.all(promiseLists).then((item) => { + this.selectedRow = []; + this.refresh(); + let st: State = this.getStateAfterDeletion(); + if (!st) { + this.refresh(); + } else { + this.clrLoad(st); + } + }); + } + } + + delOperate(repoName: string) { + let findedList = this.batchDelectionInfos.find(data => data.name === repoName); + if (this.signedCon[repoName].length !== 0) { + Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('REPOSITORY.DELETION_TITLE_REPO_SIGNED')).subscribe(res => { + findedList = BathInfoChanges(findedList, res[0], false, true, res[1]); + }); + } else { + return toPromise(this.repositoryService + .deleteRepository(repoName)) + .then( + response => { + this.translateService.get('BATCH.DELETED_SUCCESS').subscribe(res => { + findedList = BathInfoChanges(findedList, res); + }); + }).catch(error => { + if (error.status === "412") { + Observable.forkJoin(this.translateService.get('BATCH.DELETED_FAILURE'), + this.translateService.get('REPOSITORY.TAGS_SIGNED')).subscribe(res => { + findedList = BathInfoChanges(findedList, res[0], false, true, res[1]); + }); + return; + } + this.translateService.get('BATCH.DELETED_FAILURE').subscribe(res => { + findedList = BathInfoChanges(findedList, res, false, true); + }); + }); + } + } + + doSearchRepoNames(repoName: string) { + this.lastFilteredRepoName = repoName; + this.currentPage = 1; + let st: State = this.currentState; + if (!st) { + st = { page: {} }; + } + st.page.size = this.pageSize; + st.page.from = 0; + st.page.to = this.pageSize - 1; + this.clrLoad(st); + } + + saveSignatures(event: {[key: string]: string[]}): void { + Object.assign(this.signedCon, event); + } + + deleteRepos(repoLists: RepositoryItem[]) { + if (repoLists && repoLists.length) { + let repoNames: string[] = []; + this.batchDelectionInfos = []; + let repArr: any[] = []; + + repoLists.forEach(repo => { + repoNames.push(repo.name); + let initBatchMessage = new BatchInfo(); + initBatchMessage.name = repo.name; + this.batchDelectionInfos.push(initBatchMessage); + + if (!this.signedCon[repo.name]) { + repArr.push(this.getTagInfo(repo.name)); + } + }); + + Promise.all(repArr).then(() => { + this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', '', repoNames.join(','), 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL); + }); + } + } + + getTagInfo(repoName: string): Promise { + this.signedCon[repoName] = []; + return toPromise(this.tagService + .getTags(repoName)) + .then(items => { + items.forEach((t: Tag) => { + if (t.signature !== null) { + this.signedCon[repoName].push(t.name); + } + }); + }) + .catch(error => this.errorHandler.error(error)); + } + + signedDataSet(repoName: string): void { + let signature = ''; + if (this.signedCon[repoName].length === 0) { + this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO', ConfirmationButtons.DELETE_CANCEL); + return; + } + signature = this.signedCon[repoName].join(','); + this.confirmationDialogSet('REPOSITORY.DELETION_TITLE_REPO_SIGNED', signature, repoName, 'REPOSITORY.DELETION_SUMMARY_REPO_SIGNED', ConfirmationButtons.CLOSE); + } + + confirmationDialogSet(summaryTitle: string, signature: string, repoName: string, summaryKey: string, button: ConfirmationButtons): void { + this.translateService.get(summaryKey, + { + repoName: repoName, + signedImages: signature, + }) + .subscribe((res: string) => { + summaryKey = res; + let message = new ConfirmationMessage( + summaryTitle, + summaryKey, + repoName, + repoName, + ConfirmationTargets.REPOSITORY, + button); + this.confirmationDialog.open(message); + + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 5000); + }); + } + + provisionItemEvent(evt: any, repo: RepositoryItem): void { + evt.stopPropagation(); + this.repoProvisionEvent.emit(repo); + } + deleteItemEvent(evt: any, item: RepositoryItem): void { + evt.stopPropagation(); + this.deleteRepos([item]); + } + itemAddInfoEvent(evt: any, repo: RepositoryItem): void { + evt.stopPropagation(); + this.addInfoEvent.emit(repo); + } + selectedChange(): void { + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 2000); + } + refresh() { + this.doSearchRepoNames(''); + } + + loadNextPage() { + if (this.currentPage * this.pageSize >= this.totalCount) { + return + } + this.currentPage = this.currentPage + 1; + + // Pagination + let params: RequestQueryParams = new RequestQueryParams(); + params.set("page", '' + this.currentPage); + params.set("page_size", '' + this.pageSize); + + this.loading = true; + + toPromise(this.repositoryService.getRepositories( + this.projectId, + this.lastFilteredRepoName, + params)) + .then((repo: Repository) => { + this.totalCount = repo.metadata.xTotalCount; + this.repositoriesCopy = repo.data; + this.signedCon = {}; + // Do filtering and sorting + this.repositoriesCopy = doFiltering(this.repositoriesCopy, this.currentState); + this.repositoriesCopy = doSorting(this.repositoriesCopy, this.currentState); + this.repositories = this.repositories.concat(this.repositoriesCopy); + this.loading = false; + }) + .catch(error => { + this.loading = false; + this.errorHandler.error(error); + }); + } + + clrLoad(state: State): void { + this.selectedRow = []; + // Keep it for future filtering and sorting + this.currentState = state; + + let pageNumber: number = calculatePage(state); + if (pageNumber <= 0) { pageNumber = 1; } + + // Pagination + let params: RequestQueryParams = new RequestQueryParams(); + params.set("page", '' + pageNumber); + params.set("page_size", '' + this.pageSize); + + this.loading = true; + + toPromise(this.repositoryService.getRepositories( + this.projectId, + this.lastFilteredRepoName, + params)) + .then((repo: Repository) => { + this.totalCount = repo.metadata.xTotalCount; + this.repositories = repo.data; + + this.signedCon = {}; + // Do filtering and sorting + this.repositories = doFiltering(this.repositories, state); + this.repositories = doSorting(this.repositories, state); + + this.loading = false; + }) + .catch(error => { + this.loading = false; + this.errorHandler.error(error); + }); + + // Force refresh view + let hnd = setInterval(() => this.ref.markForCheck(), 100); + setTimeout(() => clearInterval(hnd), 5000); + } + + getStateAfterDeletion(): State { + let total: number = this.totalCount - 1; + if (total <= 0) { return null; } + + let totalPages: number = Math.ceil(total / this.pageSize); + let targetPageNumber: number = this.currentPage; + + if (this.currentPage > totalPages) { + targetPageNumber = totalPages; // Should == currentPage -1 + } + + let st: State = this.currentState; + if (!st) { + st = { page: {} }; + } + st.page.size = this.pageSize; + st.page.from = (targetPageNumber - 1) * this.pageSize; + st.page.to = targetPageNumber * this.pageSize - 1; + + return st; + } + + watchRepoClickEvt(repo: RepositoryItem) { + this.repoClickEvent.emit(repo); + } + + getImgLink(repo: RepositoryItem): string { + return '/container-image-icons?container-image=' + repo.name + } + + getRepoDescrition(repo: RepositoryItem): string { + if (repo && repo.description) { + return repo.description; + } + return "No description for this repo. You can add it to this repository." + } + showCard(cardView: boolean) { + if (this.isCardView === cardView) { + return + } + this.isCardView = cardView; + this.refresh(); + } + + mouseEnter(itemName: string) { + if (itemName === 'card') { + this.cardHover = true; + } else { + this.listHover = true; + } + } + + mouseLeave(itemName: string) { + if (itemName === 'card') { + this.cardHover = false; + } else { + this.listHover = false; + } + } + + isHovering(itemName: string) { + if (itemName === 'card') { + return this.cardHover; + } else { + return this.listHover; + } + } +} \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository-listview/repository-listview.component.html.ts b/src/ui_ng/lib/src/repository-listview/repository-listview.component.html.ts index 41bce261f..2d7cf8b9e 100644 --- a/src/ui_ng/lib/src/repository-listview/repository-listview.component.html.ts +++ b/src/ui_ng/lib/src/repository-listview/repository-listview.component.html.ts @@ -20,7 +20,7 @@ export const REPOSITORY_LISTVIEW_TEMPLATE = ` {{'REPOSITORY.PULL_COUNT' | translate}} {{'REPOSITORY.PLACEHOLDER' | translate }} - {{r.name}} + {{r.name}} {{r.tags_count}} {{r.pull_count}} diff --git a/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts b/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts index 62aae2601..4886db198 100644 --- a/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts +++ b/src/ui_ng/lib/src/repository-listview/repository-listview.component.ts @@ -34,7 +34,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message'; import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message'; import { Subscription } from 'rxjs/Subscription'; -import { Tag, TagClickEvent } from '../service/interface'; +import { Tag } from '../service/interface'; import { State } from "clarity-angular"; import { @@ -60,14 +60,14 @@ export class RepositoryListviewComponent implements OnChanges, OnInit { @Input() hasSignedIn: boolean; @Input() hasProjectAdminRole: boolean; - @Output() tagClickEvent = new EventEmitter(); + @Output() repoClickEvent = new EventEmitter(); lastFilteredRepoName: string; repositories: RepositoryItem[]; systemInfo: SystemInfo; selectedRow: RepositoryItem[] = []; - loading: boolean = true; + loading = true; @ViewChild('confirmationDialog') confirmationDialog: ConfirmationDialogComponent; @@ -279,19 +279,15 @@ export class RepositoryListviewComponent implements OnChanges, OnInit { this.doSearchRepoNames(''); } - watchTagClickEvt(tagClickEvt: TagClickEvent): void { - this.tagClickEvent.emit(tagClickEvt); - } - clrLoad(state: State): void { this.selectedRow = []; - //Keep it for future filtering and sorting + // Keep it for future filtering and sorting this.currentState = state; let pageNumber: number = calculatePage(state); if (pageNumber <= 0) { pageNumber = 1; } - //Pagination + // Pagination let params: RequestQueryParams = new RequestQueryParams(); params.set("page", '' + pageNumber); params.set("page_size", '' + this.pageSize); @@ -307,7 +303,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit { this.repositories = repo.data; this.signedCon = {}; - //Do filtering and sorting + // Do filtering and sorting this.repositories = doFiltering(this.repositories, state); this.repositories = doSorting(this.repositories, state); @@ -318,7 +314,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit { this.errorHandler.error(error); }); - //Force refresh view + // Force refresh view let hnd = setInterval(() => this.ref.markForCheck(), 100); setTimeout(() => clearInterval(hnd), 5000); } @@ -331,7 +327,7 @@ export class RepositoryListviewComponent implements OnChanges, OnInit { let targetPageNumber: number = this.currentPage; if (this.currentPage > totalPages) { - targetPageNumber = totalPages;//Should == currentPage -1 + targetPageNumber = totalPages; // Should == currentPage -1 } let st: State = this.currentState; @@ -344,8 +340,8 @@ export class RepositoryListviewComponent implements OnChanges, OnInit { return st; } - public gotoLink(projectId: number, repoName: string): void { - let linkUrl = [this.router.url, repoName]; - this.router.navigate(linkUrl); + + watchRepoClickEvt(repo: RepositoryItem) { + this.repoClickEvent.emit(repo); } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/repository/repository.component.html.ts b/src/ui_ng/lib/src/repository/repository.component.html.ts index 50c5dec87..e22ad9ae0 100644 --- a/src/ui_ng/lib/src/repository/repository.component.html.ts +++ b/src/ui_ng/lib/src/repository/repository.component.html.ts @@ -46,7 +46,7 @@ export const REPOSITORY_TEMPLATE = `
- +
diff --git a/src/ui_ng/lib/src/repository/repository.component.ts b/src/ui_ng/lib/src/repository/repository.component.ts index cfe1c24dd..82c9e7096 100644 --- a/src/ui_ng/lib/src/repository/repository.component.ts +++ b/src/ui_ng/lib/src/repository/repository.component.ts @@ -54,6 +54,7 @@ export class RepositoryComponent implements OnInit { @Input() isGuest: boolean; @Input() withNotary: boolean; @Input() withClair: boolean; + @Input() withAdmiral: boolean; @Output() tagClickEvent = new EventEmitter(); @Output() backEvt: EventEmitter = new EventEmitter(); diff --git a/src/ui_ng/lib/src/service/interface.ts b/src/ui_ng/lib/src/service/interface.ts index dc5f4e7a9..6838e3a5a 100644 --- a/src/ui_ng/lib/src/service/interface.ts +++ b/src/ui_ng/lib/src/service/interface.ts @@ -250,7 +250,7 @@ export interface VulnerabilitySummary { job_id?: number; severity: VulnerabilitySeverity; components: VulnerabilityComponents; - update_time: Date; //Use as complete timestamp + update_time: Date; // Use as complete timestamp } export interface VulnerabilityComponents { @@ -277,3 +277,15 @@ export interface Label { scope: string; project_id: number; } + +export interface CardItemEvent { + event_type: string; + item: any; + additional_info?: any; +} + +export interface ScrollPosition { + sH: number; + sT: number; + cH: number; +}; diff --git a/src/ui_ng/lib/src/service/repository.service.ts b/src/ui_ng/lib/src/service/repository.service.ts index 546955271..4e1db7fa0 100644 --- a/src/ui_ng/lib/src/service/repository.service.ts +++ b/src/ui_ng/lib/src/service/repository.service.ts @@ -140,6 +140,6 @@ export class RepositoryDefaultService extends RepositoryService { return this.http.delete(url, HTTP_JSON_OPTIONS).toPromise() .then(response => response) - .catch(error => { Promise.reject(error); }); + .catch(error => {return Promise.reject(error); }); } } diff --git a/src/ui_ng/lib/src/shared/shared.module.ts b/src/ui_ng/lib/src/shared/shared.module.ts index 63d85fa16..aea8eef57 100644 --- a/src/ui_ng/lib/src/shared/shared.module.ts +++ b/src/ui_ng/lib/src/shared/shared.module.ts @@ -55,7 +55,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { provide: MissingTranslationHandler, useClass: MyMissingTranslationHandler } - }) + }), ], exports: [ CommonModule, @@ -65,9 +65,8 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { CookieModule, ClipboardModule, ClarityModule, - TranslateModule + TranslateModule, ], providers: [CookieService] }) - export class SharedModule { } diff --git a/src/ui_ng/lib/src/shared/shared.utils.ts b/src/ui_ng/lib/src/shared/shared.utils.ts index fe0550ebe..c9096db9a 100644 --- a/src/ui_ng/lib/src/shared/shared.utils.ts +++ b/src/ui_ng/lib/src/shared/shared.utils.ts @@ -15,7 +15,7 @@ import { NgForm } from '@angular/forms'; import { httpStatusCode, AlertType } from './shared.const'; /** * To handle the error message body - * + * * @export * @returns {string} */ @@ -24,7 +24,7 @@ export const errorHandler = function (error: any): string { return "UNKNOWN_ERROR"; } if (!(error.statusCode || error.status)) { - //treat as string message + // treat as string message return '' + error; } else { switch (error.statusCode || error.status) { @@ -46,4 +46,28 @@ export const errorHandler = function (error: any): string { return "UNKNOWN_ERROR"; } } +} + +export class CancelablePromise { + + constructor(promise: Promise) { + this.wrappedPromise = new Promise((resolve, reject) => { + promise.then((val) => + this.isCanceled ? reject({isCanceled: true}) : resolve(val) + ); + promise.catch((error) => + this.isCanceled ? reject({isCanceled: true}) : reject(error) + ); + }); + } + + private wrappedPromise: Promise; + private isCanceled: boolean; + getPromise(): Promise { + return this.wrappedPromise; + } + + cancel() { + this.isCanceled = true; + } } \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.css.ts b/src/ui_ng/lib/src/tag/tag-detail.component.css.ts index 2bfb781d8..f17860614 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.css.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.css.ts @@ -1,6 +1,5 @@ export const TAG_DETAIL_STYLES: string = ` .overview-section { - background-color: white; padding-bottom: 36px; border-bottom: 1px solid #cccccc; } @@ -78,27 +77,37 @@ export const TAG_DETAIL_STYLES: string = ` padding-left: 24px; } -.vulnerabilities-info .third-column { + .third-column { margin-left: 36px; } +.vulnerability{ +margin-left: 50px; + margin-top: -12px; + margin-bottom: 20px;} -.vulnerabilities-info .second-column, -.vulnerabilities-info .fourth-column { +.vulnerabilities-info .second-column { text-align: left; margin-left: 6px; } +.fourth-column{ +float: left; +margin-left:20px;} + .vulnerabilities-info .second-row { margin-top: 6px; } .detail-title { - font-weight: 500; + float:left; + font-weight: 600; font-size: 14px; } .image-detail-label { - text-align: right; + margin-right: 10px; + text-align: left; + font-weight: 600; } .image-detail-value { diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts index 0b92947bd..af9394f5e 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.html.ts @@ -7,26 +7,22 @@ export const TAG_DETAIL_HTML: string = `
-

{{tagDetails.name}}

-
-
- {{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}} +

{{repositoryId}}:{{tagDetails.name}}

-
- {{'TAG.IMAGE_DETAILS' | translate }} -
+
{{'TAG.AUTHOR' | translate }}
{{'TAG.ARCHITECTURE' | translate }}
{{'TAG.OS' | translate }}
{{'TAG.DOCKER_VERSION' | translate }}
{{'TAG.SCAN_COMPLETION_TIME' | translate }}
+
{{author | translate}}
{{tagDetails.architecture}}
{{tagDetails.os}}
{{tagDetails.docker_version}}
@@ -35,8 +31,8 @@ export const TAG_DETAIL_HTML: string = `
-
- {{'TAG.IMAGE_VULNERABILITIES' | translate }} +
+
@@ -46,12 +42,6 @@ export const TAG_DETAIL_HTML: string = `
-
-
-
{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }}
-
{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }}
-
-
@@ -59,11 +49,20 @@ export const TAG_DETAIL_HTML: string = `
-
-
{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }}
-
{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }}
+
+
{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}
+
{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}
+
{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}
+
{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }}{{'TAG.LEVEL_VULNERABILITIES' | translate }}
+ +
+
+
{{'TAG.LABELS' | translate }}
+
+
+
diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts index 876297038..20da427dd 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.spec.ts @@ -10,6 +10,11 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index'; import { FilterComponent } from '../filter/index'; import { VULNERABILITY_SCAN_STATUS } from '../utils'; +import {VULNERABILITY_DIRECTIVES} from "../vulnerability-scanning/index"; +import {LabelPieceComponent} from "../label-piece/label-piece.component"; +import {JobLogViewerComponent} from "../job-log-viewer/job-log-viewer.component"; +import {ChannelService} from "../channel/channel.service"; +import {JobLogService, JobLogDefaultService} from "../service/job-log.service"; describe('TagDetailComponent (inline template)', () => { @@ -66,10 +71,16 @@ describe('TagDetailComponent (inline template)', () => { declarations: [ TagDetailComponent, ResultGridComponent, + VULNERABILITY_DIRECTIVES, + LabelPieceComponent, + JobLogViewerComponent, FilterComponent ], providers: [ ErrorHandler, + ChannelService, + JobLogDefaultService, + {provide: JobLogService, useClass: JobLogDefaultService}, { provide: SERVICE_CONFIG, useValue: config }, { provide: TagService, useClass: TagDefaultService }, { provide: ScanningResultService, useClass: ScanningResultDefaultService } @@ -119,7 +130,7 @@ describe('TagDetailComponent (inline template)', () => { let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name'); expect(el).toBeTruthy(); - expect(el.textContent.trim()).toEqual('nginx'); + expect(el.textContent.trim()).toEqual('mock_repo:nginx'); }); })); @@ -133,7 +144,7 @@ describe('TagDetailComponent (inline template)', () => { expect(el).toBeTruthy(); let el2: HTMLElement = el.querySelector('div'); expect(el2).toBeTruthy(); - expect(el2.textContent).toEqual("amd64"); + expect(el2.textContent).toEqual("steven"); }); })); @@ -147,7 +158,7 @@ describe('TagDetailComponent (inline template)', () => { expect(el).toBeTruthy(); let el2: HTMLElement = el.querySelector('div'); expect(el2).toBeTruthy(); - expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGH"); + expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.SEVERITY.HIGHTAG.LEVEL_VULNERABILITIES"); }); })); diff --git a/src/ui_ng/lib/src/tag/tag-detail.component.ts b/src/ui_ng/lib/src/tag/tag-detail.component.ts index 617b04f48..9a1be6b3a 100644 --- a/src/ui_ng/lib/src/tag/tag-detail.component.ts +++ b/src/ui_ng/lib/src/tag/tag-detail.component.ts @@ -6,6 +6,7 @@ import { TAG_DETAIL_HTML } from './tag-detail.component.html'; import { TagService, Tag, VulnerabilitySeverity } from '../service/index'; import { toPromise } from '../utils'; import { ErrorHandler } from '../error-handler/index'; +import {Label} from "../service/interface"; @Component({ selector: 'hbr-tag-detail', @@ -19,9 +20,11 @@ export class TagDetailComponent implements OnInit { _mediumCount: number = 0; _lowCount: number = 0; _unknownCount: number = 0; + labels: Label; @Input() tagId: string; @Input() repositoryId: string; + @Input() withAdmiral: boolean; tagDetails: Tag = { name: "--", size: "--", @@ -74,7 +77,7 @@ export class TagDetailComponent implements OnInit { } onBack(): void { - this.backEvt.emit(this.tagId); + this.backEvt.emit(this.repositoryId); } getPackageText(count: number): string { diff --git a/src/ui_ng/lib/src/tag/tag.component.css.ts b/src/ui_ng/lib/src/tag/tag.component.css.ts index 34442217e..36f5e96e6 100644 --- a/src/ui_ng/lib/src/tag/tag.component.css.ts +++ b/src/ui_ng/lib/src/tag/tag.component.css.ts @@ -66,4 +66,11 @@ export const TAG_STYLE = ` :host >>> .signpost-content-body{padding:0 .4rem;} :host >>> .signpost-content-header{display:none;} .filterLabelPiece{position: absolute; bottom :0px;z-index:1;} +.dropdown .dropdown-toggle.btn { + padding-right: 1rem; + border-left-width: 0; + border-right-width: 0; + border-radius: 0; + margin-top: -2px; +} `; \ No newline at end of file diff --git a/src/ui_ng/lib/src/tag/tag.component.html.ts b/src/ui_ng/lib/src/tag/tag.component.html.ts index b578b6de7..36d3e5fac 100644 --- a/src/ui_ng/lib/src/tag/tag.component.html.ts +++ b/src/ui_ng/lib/src/tag/tag.component.html.ts @@ -17,11 +17,12 @@ export const TAG_TEMPLATE = `
- + +
- +
{{'LABEL.NO_LABELS' | translate }}
@@ -38,31 +39,29 @@ export const TAG_TEMPLATE = `
-
+
-
- - - -
- -
-
{{'LABEL.NO_LABELS' | translate }}
-
- -
-
-
-
+ + + +
+ +
+
{{'LABEL.NO_LABELS' | translate }}
+
+ +
+
+
+
-
{{'REPOSITORY.TAG' | translate}} {{'REPOSITORY.SIZE' | translate}} @@ -72,7 +71,7 @@ export const TAG_TEMPLATE = ` {{'REPOSITORY.AUTHOR' | translate}} {{'REPOSITORY.CREATED' | translate}} {{'REPOSITORY.DOCKER_VERSION' | translate}} - {{'REPOSITORY.LABELS' | translate}} + {{'REPOSITORY.LABELS' | translate}} {{'TAG.PLACEHOLDER' | translate }} @@ -97,7 +96,7 @@ export const TAG_TEMPLATE = ` {{t.author}} {{t.created | date: 'short'}} {{t.docker_version}} - +
@@ -113,7 +112,7 @@ export const TAG_TEMPLATE = `
- + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}} {{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}     diff --git a/src/ui_ng/lib/src/tag/tag.component.spec.ts b/src/ui_ng/lib/src/tag/tag.component.spec.ts index ec8291ab6..983498796 100644 --- a/src/ui_ng/lib/src/tag/tag.component.spec.ts +++ b/src/ui_ng/lib/src/tag/tag.component.spec.ts @@ -140,13 +140,6 @@ describe('TagComponent (inline template)', () => { labelService = fixture.debugElement.injector.get(LabelService); - /*spyLabels = spyOn(labelService, 'getLabels').and.callFake(function (param) { - if (param === 'g') { - return Promise.resolve(mockLabels); - }else { - Promise.resolve(mockLabels1) - } - })*/ spyLabels = spyOn(labelService, 'getGLabels').and.returnValues(Promise.resolve(mockLabels)); spyLabels1 = spyOn(labelService, 'getPLabels').and.returnValues(Promise.resolve(mockLabels1)); diff --git a/src/ui_ng/lib/src/tag/tag.component.ts b/src/ui_ng/lib/src/tag/tag.component.ts index c75438967..ee0137069 100644 --- a/src/ui_ng/lib/src/tag/tag.component.ts +++ b/src/ui_ng/lib/src/tag/tag.component.ts @@ -80,7 +80,7 @@ export class TagComponent implements OnInit, AfterViewInit { @Input() registryUrl: string; @Input() withNotary: boolean; @Input() withClair: boolean; - + @Input() withAdmiral: boolean; @Output() refreshRepo = new EventEmitter(); @Output() tagClickEvent = new EventEmitter(); @Output() signatureOutput = new EventEmitter(); @@ -180,11 +180,11 @@ export class TagComponent implements OnInit, AfterViewInit { .subscribe((name: string) => { if (name && name.length) { this.filterOnGoing = true; - this.imageFilterLabels = []; + this.imageStickLabels = []; this.imageLabels.forEach(data => { if (data.label.name.indexOf(name) !== -1) { - this.imageFilterLabels.push(data); + this.imageStickLabels.push(data); } }) setTimeout(() => { @@ -196,7 +196,9 @@ export class TagComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - this.getAllLabels(); + if (!this.withAdmiral) { + this.getAllLabels(); + } } public get filterLabelPieceWidth() { @@ -302,7 +304,7 @@ export class TagComponent implements OnInit, AfterViewInit { this.selectedChange(tag); } selectLabel(labelInfo: {[key: string]: any | string[]}): void { - if (labelInfo && labelInfo.iconsShow) { + if (labelInfo && !labelInfo.iconsShow) { let labelId = labelInfo.label.id; this.selectedRow = this.selectedTag; toPromise(this.tagService.addLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => { @@ -314,7 +316,7 @@ export class TagComponent implements OnInit, AfterViewInit { } unSelectLabel(labelInfo: {[key: string]: any | string[]}): void { - if (labelInfo && !labelInfo.iconsShow) { + if (labelInfo && labelInfo.iconsShow) { let labelId = labelInfo.label.id; this.selectedRow = this.selectedTag; toPromise(this.tagService.deleteLabelToImages(this.repoName, this.selectedRow[0].name, labelId)).then(res => { @@ -442,7 +444,7 @@ export class TagComponent implements OnInit, AfterViewInit { } else if (Math.pow(1024, 2) <= size && size < Math.pow(1024, 3)) { return (size / Math.pow(1024, 2)).toFixed(2) + "MB"; } else if (Math.pow(1024, 3) <= size && size < Math.pow(1024, 4)) { - return (size / Math.pow(1024, 3)).toFixed(2) + "MB"; + return (size / Math.pow(1024, 3)).toFixed(2) + "GB"; } else { return size + "B"; } diff --git a/src/ui_ng/package.json b/src/ui_ng/package.json index 5f5b8cf95..7940d6aa6 100644 --- a/src/ui_ng/package.json +++ b/src/ui_ng/package.json @@ -31,7 +31,7 @@ "clarity-icons": "^0.10.17", "clarity-ui": "^0.10.27", "core-js": "^2.4.1", - "harbor-ui": "0.6.53", + "harbor-ui": "0.6.61", "intl": "^1.2.5", "mutationobserver-shim": "^0.3.2", "ngx-cookie": "^1.0.0", diff --git a/src/ui_ng/src/app/config/config.component.html b/src/ui_ng/src/app/config/config.component.html index fee0a3a17..6f00dce28 100644 --- a/src/ui_ng/src/app/config/config.component.html +++ b/src/ui_ng/src/app/config/config.component.html @@ -12,7 +12,7 @@ - -