Merge remote-tracking branch 'upstream/dev' into 170321_url

Conflicts:
	src/ui/router.go
This commit is contained in:
Wenkai Yin 2017-03-23 11:19:34 +08:00
commit 9e12274309
47 changed files with 459 additions and 434 deletions

5
.gitignore vendored
View File

@ -10,7 +10,10 @@ src/common/dao/dao.test
*.pyc *.pyc
jobservice/test jobservice/test
src/ui/static/dist/ src/ui/static/*.html
src/ui/static/*.bundle.js
src/ui/static/*.bundle.js.map
src/ui/static/harbor-log.*.png
src/ui_ng/coverage/ src/ui_ng/coverage/
src/ui_ng/dist/ src/ui_ng/dist/

View File

@ -76,6 +76,8 @@ before_script:
script: script:
- sudo mkdir -p /harbor_storage/ca_download - sudo mkdir -p /harbor_storage/ca_download
- sudo mv ./tests/ca.crt /harbor_storage/ca_download - sudo mv ./tests/ca.crt /harbor_storage/ca_download
- sudo mkdir -p /harbor
- sudo mv ./VERSION /harbor/VERSION
- sudo service mysql stop - sudo service mysql stop
- sudo ./tests/testprepare.sh - sudo ./tests/testprepare.sh
- docker-compose -f ./make/docker-compose.test.yml up -d - docker-compose -f ./make/docker-compose.test.yml up -d
@ -98,6 +100,7 @@ script:
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true - sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=danieljt/harbor-clarity-base:0.8.4 NOTARYFLAG=true
- docker ps - docker ps
- ./tests/notarytest.sh
- go run tests/startuptest.go https://localhost/ - go run tests/startuptest.go https://localhost/
- go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD} - go run tests/userlogintest.go -name ${HARBOR_ADMIN} -passwd ${HARBOR_ADMIN_PASSWD}

View File

@ -82,9 +82,9 @@ NOTARYFLAG=false
REGISTRYVERSION=2.6.0 REGISTRYVERSION=2.6.0
NGINXVERSION=1.11.5 NGINXVERSION=1.11.5
PHOTONVERSION=1.0 PHOTONVERSION=1.0
NOTARYVERSION=server-0.5.0-fix NOTARYVERSION=server-0.5.0
NOTARYSIGNERVERSION=signer-0.5.0 NOTARYSIGNERVERSION=signer-0.5.0
MARIADBVERSION=10.1.10 MARIADBVERSION=mariadb-10.1.10
HTTPPROXY= HTTPPROXY=
#clarity parameters #clarity parameters
@ -164,8 +164,8 @@ DOCKERCOMPOSEFILENAME=docker-compose.yml
DOCKERCOMPOSENOTARYFILENAME=docker-compose.notary.yml DOCKERCOMPOSENOTARYFILENAME=docker-compose.notary.yml
# version prepare # version prepare
VERSIONFILEPATH=$(SRCPATH)/ui/views/sections VERSIONFILEPATH=$(CURDIR)
VERSIONFILENAME=header-content.htm VERSIONFILENAME=VERSION
GITCMD=$(shell which git) GITCMD=$(shell which git)
GITTAG=$(GITCMD) describe --tags GITTAG=$(GITCMD) describe --tags
ifeq ($(DEVFLAG), true) ifeq ($(DEVFLAG), true)
@ -189,9 +189,7 @@ REGISTRYUSER=user
REGISTRYPASSWORD=default REGISTRYPASSWORD=default
version: version:
@if [ "$(DEVFLAG)" = "false" ] ; then \ @printf $(VERSIONTAG) > $(VERSIONFILEPATH)/$(VERSIONFILENAME);
$(SEDCMD) -i 's/version=\"{{.Version}}\"/version=\"$(VERSIONTAG)\"/' -i $(VERSIONFILEPATH)/$(VERSIONFILENAME) ; \
fi
check_environment: check_environment:
@$(MAKEPATH)/$(CHECKENVCMD) @$(MAKEPATH)/$(CHECKENVCMD)
@ -304,10 +302,10 @@ package_offline: compile build modify_composefile
@$(DOCKERPULL) registry:$(REGISTRYVERSION) @$(DOCKERPULL) registry:$(REGISTRYVERSION)
@$(DOCKERPULL) nginx:$(NGINXVERSION) @$(DOCKERPULL) nginx:$(NGINXVERSION)
@if [ "$(NOTARYFLAG)" = "true" ] ; then \ @if [ "$(NOTARYFLAG)" = "true" ] ; then \
echo "pulling notary and mariadb..."; \ echo "pulling notary and harbor-notary-db..."; \
$(DOCKERPULL) jiangd/notary:$(NOTARYVERSION); \ $(DOCKERPULL) vmware/notary-photon:$(NOTARYVERSION); \
$(DOCKERPULL) notary:$(NOTARYSIGNERVERSION); \ $(DOCKERPULL) vmware/notary-photon:$(NOTARYSIGNERVERSION); \
$(DOCKERPULL) mariadb:$(MARIADBVERSION); \ $(DOCKERPULL) vmware/harbor-notary-db:$(MARIADBVERSION); \
fi fi
@echo "saving harbor docker image" @echo "saving harbor docker image"
@ -319,7 +317,7 @@ package_offline: compile build modify_composefile
$(DOCKERIMAGENAME_DB):$(VERSIONTAG) \ $(DOCKERIMAGENAME_DB):$(VERSIONTAG) \
$(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \ $(DOCKERIMAGENAME_JOBSERVICE):$(VERSIONTAG) \
nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) \ nginx:$(NGINXVERSION) registry:$(REGISTRYVERSION) photon:$(PHOTONVERSION) \
jiangd/notary:$(NOTARYVERSION) notary:$(NOTARYSIGNERVERSION) mariadb:$(MARIADBVERSION); \ vmware/notary-photon:$(NOTARYVERSION) vmware/notary-photon:$(NOTARYSIGNERVERSION) vmware/harbor-notary-db:$(MARIADBVERSION); \
else \ else \
$(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \ $(DOCKERSAVE) -o $(HARBORPKG)/$(DOCKERIMGFILE).$(VERSIONTAG).tgz \
$(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \ $(DOCKERIMAGENAME_ADMINSERVER):$(VERSIONTAG) \
@ -420,7 +418,7 @@ cleandockercomposefile:
cleanversiontag: cleanversiontag:
@echo "cleaning version TAG" @echo "cleaning version TAG"
@$(SEDCMD) -i 's/version=\"$(VERSIONTAG)\"/version=\"{{.Version}}\"/' -i $(VERSIONFILEPATH)/$(VERSIONFILENAME) @rm -rf $(VERSIONFILEPATH)/$(VERSIONFILENAME)
cleanpackage: cleanpackage:
@echo "cleaning harbor install package" @echo "cleaning harbor install package"

1
VERSION Normal file
View File

@ -0,0 +1 @@
dev

View File

@ -2052,6 +2052,9 @@ definitions:
has_ca_root: has_ca_root:
type: boolean type: boolean
description: Indicate whether there is a ca root cert file ready for download in the file system. description: Indicate whether there is a ca root cert file ready for download in the file system.
harbor_version:
type: string
description: The build version of Harbor.
SystemInfo: SystemInfo:
type: object type: object
properties: properties:

View File

@ -1,7 +1,7 @@
CREATE DATABASE IF NOT EXISTS `notaryserver`; CREATE DATABASE IF NOT EXISTS `notaryserver`;
CREATE USER "server"@"%" IDENTIFIED BY ""; CREATE USER "server"@"notary-server.%" IDENTIFIED BY "";
GRANT GRANT
ALL PRIVILEGES ON `notaryserver`.* ALL PRIVILEGES ON `notaryserver`.*
TO "server"@"%"; TO "server"@"notary-server.%"

View File

@ -1,7 +1,7 @@
CREATE DATABASE IF NOT EXISTS `notarysigner`; CREATE DATABASE IF NOT EXISTS `notarysigner`;
CREATE USER "signer"@"%" IDENTIFIED BY ""; CREATE USER "signer"@"notary-signer.%" IDENTIFIED BY "";
GRANT GRANT
ALL PRIVILEGES ON `notarysigner`.* ALL PRIVILEGES ON `notarysigner`.*
TO "signer"@"%"; TO "signer"@"notary-signer.%";

View File

@ -1,210 +0,0 @@
-- MySQL dump 10.16 Distrib 10.1.10-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database:
-- ------------------------------------------------------
-- Server version 10.1.10-MariaDB-1~jessie
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
--
-- Current Database: `notaryserver`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `notaryserver` /*!40100 DEFAULT CHARACTER SET latin1 */;
USE `notaryserver`;
--
-- Table structure for table `change_category`
--
DROP TABLE IF EXISTS `change_category`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `change_category` (
`category` varchar(20) NOT NULL,
PRIMARY KEY (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `change_category`
--
LOCK TABLES `change_category` WRITE;
/*!40000 ALTER TABLE `change_category` DISABLE KEYS */;
INSERT INTO `change_category` VALUES ('deletion'),('update');
/*!40000 ALTER TABLE `change_category` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `changefeed`
--
DROP TABLE IF EXISTS `changefeed`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `changefeed` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gun` varchar(255) NOT NULL,
`version` int(11) NOT NULL,
`sha256` char(64) DEFAULT NULL,
`category` varchar(20) NOT NULL DEFAULT 'update',
PRIMARY KEY (`id`),
KEY `category` (`category`),
KEY `idx_changefeed_gun` (`gun`),
CONSTRAINT `changefeed_ibfk_1` FOREIGN KEY (`category`) REFERENCES `change_category` (`category`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `changefeed`
--
LOCK TABLES `changefeed` WRITE;
/*!40000 ALTER TABLE `changefeed` DISABLE KEYS */;
/*!40000 ALTER TABLE `changefeed` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `schema_migrations`
--
DROP TABLE IF EXISTS `schema_migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `schema_migrations` (
`version` int(11) NOT NULL,
PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `schema_migrations`
--
LOCK TABLES `schema_migrations` WRITE;
/*!40000 ALTER TABLE `schema_migrations` DISABLE KEYS */;
INSERT INTO `schema_migrations` VALUES (1),(2),(3),(4),(5);
/*!40000 ALTER TABLE `schema_migrations` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `tuf_files`
--
DROP TABLE IF EXISTS `tuf_files`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `tuf_files` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`gun` varchar(255) NOT NULL,
`role` varchar(255) NOT NULL,
`version` int(11) NOT NULL,
`data` longblob NOT NULL,
`sha256` char(64) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `gun` (`gun`,`role`,`version`),
KEY `sha256` (`sha256`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `tuf_files`
--
LOCK TABLES `tuf_files` WRITE;
/*!40000 ALTER TABLE `tuf_files` DISABLE KEYS */;
/*!40000 ALTER TABLE `tuf_files` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Current Database: `notarysigner`
--
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `notarysigner` /*!40100 DEFAULT CHARACTER SET latin1 */;
USE `notarysigner`;
--
-- Table structure for table `private_keys`
--
DROP TABLE IF EXISTS `private_keys`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `private_keys` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
`deleted_at` timestamp NULL DEFAULT NULL,
`key_id` varchar(255) NOT NULL,
`encryption_alg` varchar(255) NOT NULL,
`keywrap_alg` varchar(255) NOT NULL,
`algorithm` varchar(50) NOT NULL,
`passphrase_alias` varchar(50) NOT NULL,
`public` blob NOT NULL,
`private` blob NOT NULL,
`gun` varchar(255) NOT NULL,
`role` varchar(255) NOT NULL,
`last_used` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `key_id` (`key_id`),
UNIQUE KEY `key_id_2` (`key_id`,`algorithm`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `private_keys`
--
LOCK TABLES `private_keys` WRITE;
/*!40000 ALTER TABLE `private_keys` DISABLE KEYS */;
/*!40000 ALTER TABLE `private_keys` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `schema_migrations`
--
DROP TABLE IF EXISTS `schema_migrations`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `schema_migrations` (
`version` int(11) NOT NULL,
PRIMARY KEY (`version`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `schema_migrations`
--
LOCK TABLES `schema_migrations` WRITE;
/*!40000 ALTER TABLE `schema_migrations` DISABLE KEYS */;
INSERT INTO `schema_migrations` VALUES (1),(2);
/*!40000 ALTER TABLE `schema_migrations` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2017-02-14 6:32:48

View File

@ -7,7 +7,7 @@ services:
networks: networks:
- harbor-notary - harbor-notary
notary-server: notary-server:
image: jiangd/notary:server-0.5.0-fix image: vmware/notary-photon:server-0.5.0
container_name: notary-server container_name: notary-server
networks: networks:
- notary-mdb - notary-mdb
@ -16,7 +16,7 @@ services:
volumes: volumes:
- ./common/config/notary:/config - ./common/config/notary:/config
entrypoint: /usr/bin/env sh entrypoint: /usr/bin/env sh
command: -c "notary-server -config=/config/server-config.json -logf=logfmt" command: -c "/migrations/migrate.sh && notary-server -config=/config/server-config.json -logf=logfmt"
depends_on: depends_on:
- notary-db - notary-db
- notary-signer - notary-signer
@ -26,7 +26,7 @@ services:
syslog-address: "tcp://127.0.0.1:1514" syslog-address: "tcp://127.0.0.1:1514"
tag: "notary-server" tag: "notary-server"
notary-signer: notary-signer:
image: notary:signer-0.5.0 image: vmware/notary-photon:signer-0.5.0
container_name: notary-signer container_name: notary-signer
networks: networks:
notary-mdb: notary-mdb:
@ -38,7 +38,7 @@ services:
env_file: env_file:
- ./common/config/notary/signer_env - ./common/config/notary/signer_env
entrypoint: /usr/bin/env sh entrypoint: /usr/bin/env sh
command: -c "notary-signer -config=/config/signer-config.json -logf=logfmt" command: -c "/migrations/migrate.sh && notary-signer -config=/config/signer-config.json -logf=logfmt"
depends_on: depends_on:
- notary-db - notary-db
logging: logging:
@ -47,7 +47,7 @@ services:
syslog-address: "tcp://127.0.0.1:1514" syslog-address: "tcp://127.0.0.1:1514"
tag: "notary-signer" tag: "notary-signer"
notary-db: notary-db:
image: mariadb:10.1.10 image: vmware/harbor-notary-db:mariadb-10.1.10
container_name: notary-db container_name: notary-db
networks: networks:
notary-mdb: notary-mdb:
@ -56,8 +56,6 @@ services:
volumes: volumes:
- ./common/config/notary/mysql-initdb.d:/docker-entrypoint-initdb.d - ./common/config/notary/mysql-initdb.d:/docker-entrypoint-initdb.d
- /data/notary-db:/var/lib/mysql - /data/notary-db:/var/lib/mysql
ports:
- "3306:3306"
environment: environment:
- TERM=dumb - TERM=dumb
- MYSQL_ALLOW_EMPTY_PASSWORD="true" - MYSQL_ALLOW_EMPTY_PASSWORD="true"

View File

@ -7,6 +7,7 @@ COPY ./make/dev/ui/harbor_ui /harbor/
COPY ./src/ui/views /harbor/views COPY ./src/ui/views /harbor/views
COPY ./src/ui/static /harbor/static COPY ./src/ui/static /harbor/static
COPY ./src/favicon.ico /harbor/favicon.ico COPY ./src/favicon.ico /harbor/favicon.ico
COPY ./VERSION /harbor/VERSION
RUN chmod u+x /harbor/harbor_ui RUN chmod u+x /harbor/harbor_ui

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"io/ioutil"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@ -20,6 +21,7 @@ type SystemInfoAPI struct {
} }
const defaultRootCert = "/harbor_storage/ca_download/ca.crt" const defaultRootCert = "/harbor_storage/ca_download/ca.crt"
const harborVersionFile = "/harbor/VERSION"
//SystemInfo models for system info. //SystemInfo models for system info.
type SystemInfo struct { type SystemInfo struct {
@ -42,6 +44,7 @@ type GeneralInfo struct {
ProjectCreationRestrict string `json:"project_creation_restriction"` ProjectCreationRestrict string `json:"project_creation_restriction"`
SelfRegistration bool `json:"self_registration"` SelfRegistration bool `json:"self_registration"`
HasCARoot bool `json:"has_ca_root"` HasCARoot bool `json:"has_ca_root"`
HarborVersion string `json:"harbor_version"`
} }
// validate for validating user if an admin. // validate for validating user if an admin.
@ -113,6 +116,7 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
registryURL = l[0] registryURL = l[0]
} }
_, caStatErr := os.Stat(defaultRootCert) _, caStatErr := os.Stat(defaultRootCert)
harborVersion := sia.getVersion()
info := GeneralInfo{ info := GeneralInfo{
AdmiralEndpoint: cfg[common.AdmiralEndpoint].(string), AdmiralEndpoint: cfg[common.AdmiralEndpoint].(string),
WithAdmiral: config.WithAdmiral(), WithAdmiral: config.WithAdmiral(),
@ -122,7 +126,18 @@ func (sia *SystemInfoAPI) GetGeneralInfo() {
SelfRegistration: cfg[common.SelfRegistration].(bool), SelfRegistration: cfg[common.SelfRegistration].(bool),
RegistryURL: registryURL, RegistryURL: registryURL,
HasCARoot: caStatErr == nil, HasCARoot: caStatErr == nil,
HarborVersion: harborVersion,
} }
sia.Data["json"] = info sia.Data["json"] = info
sia.ServeJSON() sia.ServeJSON()
} }
// GetVersion gets harbor version.
func (sia *SystemInfoAPI) getVersion() string {
version, err := ioutil.ReadFile(harborVersionFile)
if err != nil {
log.Errorf("Error occured getting harbor version: %v", err)
return ""
}
return string(version[:])
}

View File

@ -3,8 +3,9 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/stretchr/testify/assert"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestGetVolumeInfo(t *testing.T) { func TestGetVolumeInfo(t *testing.T) {
@ -49,6 +50,7 @@ func TestGetGeneralInfo(t *testing.T) {
assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err)) assert.Nil(err, fmt.Sprintf("Unexpected Error: %v", err))
assert.Equal(false, g.WithNotary, "with notary should be false") assert.Equal(false, g.WithNotary, "with notary should be false")
assert.Equal(true, g.HasCARoot, "has ca root should be true") assert.Equal(true, g.HasCARoot, "has ca root should be true")
assert.NotEmpty(g.HarborVersion, "harbor version should not be empty")
} }
func TestGetCert(t *testing.T) { func TestGetCert(t *testing.T) {

View File

@ -62,7 +62,7 @@ func initRouters() {
//API: //API:
beego.Router("/api/search", &api.SearchAPI{}) beego.Router("/api/search", &api.SearchAPI{})
beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{}) beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &api.ProjectMemberAPI{})
beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post") beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head")
beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{}) beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{})
beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic")
beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog") beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog")

View File

@ -7,11 +7,11 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label> <label for="oldPassword" class="required form-group-label-override">{{'CHANGE_PWD.CURRENT_PWD' | translate}}</label>
<label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)"> <label for="oldPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]="oldPassInput.invalid && (oldPassInput.dirty || oldPassInput.touched)">
<input type="password" id="oldPassword" placeholder='{{"PLACEHOLDER.CURRENT_PWD" | translate}}' <input type="password" id="oldPassword"
required required
name="oldPassword" name="oldPassword"
[(ngModel)]="oldPwd" [(ngModel)]="oldPwd"
#oldPassInput="ngModel" size="30"> #oldPassInput="ngModel" size="42">
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.CURRENT_PWD' | translate}} {{'TOOLTIP.CURRENT_PWD' | translate}}
</span> </span>
@ -19,27 +19,32 @@
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label> <label for="newPassword" class="required form-group-label-override">{{'CHANGE_PWD.NEW_PWD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="newPassInput.invalid && (newPassInput.dirty || newPassInput.touched)"> <label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("newPassword")'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}' <input type="password" id="newPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="newPassword" name="newPassword"
[(ngModel)]="newPwd" [(ngModel)]="newPwd"
#newPassInput="ngModel" size="30"> #newPassInput="ngModel" size="42"
(input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}} {{'TOOLTIP.PASSWORD' | translate}}
</span> </span>
</label> </label>
<label class="sub-label-for-input">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label> <label for="reNewPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]="(reNewPassInput.invalid && (reNewPassInput.dirty || reNewPassInput.touched)) || (!newPassInput.invalid && reNewPassInput.value != newPassInput.value)"> <label for="reNewPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-top-left" [class.invalid]='!getValidationState("reNewPassword")'>
<input type="password" id="reNewPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}' <input type="password" id="reNewPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="reNewPassword" name="reNewPassword"
[(ngModel)]="reNewPwd" [(ngModel)]="reNewPwd"
#reNewPassInput="ngModel" size="30"> #reNewPassInput="ngModel" size="42"
(input)='handleValidation("reNewPassword", false)'
(focusout)='handleValidation("reNewPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.CONFIRM_PWD' | translate}} {{'TOOLTIP.CONFIRM_PWD' | translate}}
</span> </span>

View File

@ -23,6 +23,10 @@ export class PasswordSettingComponent implements AfterViewChecked {
private formValueChanged: boolean = false; private formValueChanged: boolean = false;
private onCalling: boolean = false; private onCalling: boolean = false;
private validationStateMap: any = {
"newPassword": true,
"reNewPassword": true
};
pwdFormRef: NgForm; pwdFormRef: NgForm;
@ViewChild("changepwdForm") pwdForm: NgForm; @ViewChild("changepwdForm") pwdForm: NgForm;
@ -52,6 +56,29 @@ export class PasswordSettingComponent implements AfterViewChecked {
return this.onCalling; return this.onCalling;
} }
private getValidationState(key: string): boolean {
return this.validationStateMap[key];
}
private handleValidation(key: string, flag: boolean): void {
if (flag) {
//Checking
let cont = this.pwdForm.controls[key];
if (cont) {
this.validationStateMap[key] = cont.valid;
if(key === "reNewPassword" && cont.valid){
let compareCont = this.pwdForm.controls["newPassword"];
if(compareCont){
this.validationStateMap[key]= cont.value === compareCont.value;
}
}
}
} else {
//Reset
this.validationStateMap[key] = true;
}
}
ngAfterViewChecked() { ngAfterViewChecked() {
if (this.pwdFormRef != this.pwdForm) { if (this.pwdFormRef != this.pwdForm) {
this.pwdFormRef = this.pwdForm; this.pwdFormRef = this.pwdForm;

View File

@ -7,7 +7,6 @@ import { CookieService } from 'angular2-cookie/core';
import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const'; import { CookieKeyOfAdmiral, HarborQueryParamKey } from './shared/shared.const';
import { maintainUrlQueryParmas } from './shared/shared.utils'; import { maintainUrlQueryParmas } from './shared/shared.utils';
export const systemInfoEndpoint = "/api/systeminfo"; export const systemInfoEndpoint = "/api/systeminfo";
/** /**
* Declare service to handle the bootstrap options * Declare service to handle the bootstrap options
@ -50,7 +49,6 @@ export class AppConfigService {
//Catch the error //Catch the error
console.error("Failed to load bootstrap options with error: ", error); console.error("Failed to load bootstrap options with error: ", error);
}); });
} }
public getConfig(): AppConfig { public getConfig(): AppConfig {

View File

@ -1,7 +1,5 @@
import { modalEvents } from './modal-events.const'
//Define a object to store the modal event //Define a object to store the modal event
export class ModalEvent { export class ModalEvent {
modalName: modalEvents; modalName: string;
modalFlag: boolean; //true for open, false for close modalFlag: boolean; //true for open, false for close
} }

View File

@ -7,7 +7,7 @@
</div> </div>
<div class="header-nav"> <div class="header-nav">
<a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a> <a href="{{admiralLink}}" class="nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Management</span></a>
<a href="javascript:void(0)" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a> <a href="javascript:void(0)" (click)="registryAction()" routerLink="/harbor" class="active nav-link" *ngIf="isIntegrationMode"><span class="nav-text">Registry</span></a>
</div> </div>
<global-search></global-search> <global-search></global-search>
<div class="header-actions"> <div class="header-actions">

View File

@ -9,9 +9,11 @@ import { SessionUser } from '../../shared/session-user';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { CookieService } from 'angular2-cookie/core'; import { CookieService } from 'angular2-cookie/core';
import { supportedLangs, enLang, languageNames, CommonRoutes } from '../../shared/shared.const'; import { supportedLangs, enLang, languageNames, CommonRoutes, AlertType } from '../../shared/shared.const';
import { errorHandler } from '../../shared/shared.utils';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
import { SearchTriggerService } from '../global-search/search-trigger.service';
import { MessageService } from '../../global-message/message.service';
@Component({ @Component({
selector: 'navigator', selector: 'navigator',
@ -31,7 +33,9 @@ export class NavigatorComponent implements OnInit {
private router: Router, private router: Router,
private translate: TranslateService, private translate: TranslateService,
private cookie: CookieService, private cookie: CookieService,
private appConfigService: AppConfigService) { } private appConfigService: AppConfigService,
private msgService: MessageService,
private searchTrigger: SearchTriggerService) { }
ngOnInit(): void { ngOnInit(): void {
this.selectedLang = this.translate.currentLang; this.selectedLang = this.translate.currentLang;
@ -98,8 +102,10 @@ export class NavigatorComponent implements OnInit {
this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]); this.router.navigate([CommonRoutes.EMBEDDED_SIGN_IN]);
}) })
.catch(error => { .catch(error => {
console.error("Log out with error: ", error); this.msgService.announceMessage(error.status | 500, errorHandler(error), AlertType.WARNING);
}); });
//Confirm search result panel is close
this.searchTrigger.closeSearch(false);
} }
//Switch languages //Switch languages
@ -124,5 +130,12 @@ export class NavigatorComponent implements OnInit {
//Naviagte to signin page //Naviagte to signin page
this.router.navigate([CommonRoutes.HARBOR_ROOT]); this.router.navigate([CommonRoutes.HARBOR_ROOT]);
} }
//Confirm search result panel is close
this.searchTrigger.closeSearch(false);
}
registryAction(): void {
this.searchTrigger.closeSearch(false);
} }
} }

View File

@ -5,4 +5,12 @@
.form-group-label-override { .form-group-label-override {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
}
.sub-label-for-input {
position: absolute;
top: 26px;
font-size: 10px;
font-weight: 400;
line-height: 12px;
} }

View File

@ -1,23 +1,26 @@
<clr-modal [(clrModalOpen)]="createProjectOpened"> <clr-modal [(clrModalOpen)]="createProjectOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3> <h3 class="modal-title">{{'PROJECT.NEW_PROJECT' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #projectForm="ngForm"> <form #projectForm="ngForm">
<section class="form-block"> <section class="form-block">
<div class="form-group"> <div class="form-group">
<label for="create_project_name" class="col-md-4">{{'PROJECT.NAME' | translate}}</label> <label for="create_project_name" class="col-md-4 form-group-label-override">{{'PROJECT.NAME' | translate}}</label>
<label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="create_project_name" aria-haspopup="true" role="tooltip" [class.invalid]="projectName.invalid && (projectName.dirty || projectName.touched)" [class.valid]="projectName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel"> <input type="text" id="create_project_name" [(ngModel)]="project.name" name="name" size="20" required minlength="2" #projectName="ngModel" targetExists="PROJECT_NAME">
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)"> <span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.required && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_IS_REQUIRED' | translate}} {{'PROJECT.NAME_IS_REQUIRED' | translate}}
</span> </span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)"> <span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.minlength && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_MINIMUM_LENGTH' | translate}} {{'PROJECT.NAME_MINIMUM_LENGTH' | translate}}
</span> </span>
<span class="tooltip-content" *ngIf="projectName.errors && projectName.errors.targetExists && (projectName.dirty || projectName.touched)">
{{'PROJECT.NAME_ALREADY_EXISTS' | translate}}
</span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label> <label class="col-md-4 form-group-label-override">{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</label>
<div class="checkbox-inline"> <div class="checkbox-inline">
<input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public"> <input type="checkbox" id="create_project_public" [(ngModel)]="project.public" name="public">
<label for="create_project_public"></label> <label for="create_project_public"></label>

View File

@ -1,5 +1,4 @@
import { Component, EventEmitter, Output, ViewChild, AfterViewChecked } from '@angular/core'; import { Component, EventEmitter, Output, ViewChild, AfterViewChecked, HostBinding } from '@angular/core';
import { Response } from '@angular/http'; import { Response } from '@angular/http';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
@ -14,6 +13,7 @@ import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.com
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'create-project', selector: 'create-project',
templateUrl: 'create-project.component.html', templateUrl: 'create-project.component.html',
@ -27,12 +27,15 @@ export class CreateProjectComponent implements AfterViewChecked {
currentForm: NgForm; currentForm: NgForm;
project: Project = new Project(); project: Project = new Project();
initVal: Project = new Project();
createProjectOpened: boolean; createProjectOpened: boolean;
hasChanged: boolean; hasChanged: boolean;
staticBackdrop: boolean = true;
closable: boolean = false;
@Output() create = new EventEmitter<boolean>(); @Output() create = new EventEmitter<boolean>();
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
private inlineAlert: InlineAlertComponent; private inlineAlert: InlineAlertComponent;
@ -75,7 +78,9 @@ export class CreateProjectComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.createProjectOpened = false; this.createProjectOpened = false;
this.projectForm.reset();
} }
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@ -83,17 +88,14 @@ export class CreateProjectComponent implements AfterViewChecked {
if(this.projectForm) { if(this.projectForm) {
this.projectForm.valueChanges.subscribe(data=>{ this.projectForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
@ -109,7 +111,7 @@ export class CreateProjectComponent implements AfterViewChecked {
confirmCancel(event: boolean): void { confirmCancel(event: boolean): void {
this.createProjectOpened = false; this.createProjectOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.projectForm.reset();
} }
} }

View File

@ -1,4 +1,4 @@
<clr-datagrid (clrDgRefresh)="refresh($event)" > <clr-datagrid (clrDgRefresh)="refresh($event)">
<clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.NAME' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.PUBLIC_OR_PRIVATE' | translate}}</clr-dg-column>
<clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column> <clr-dg-column>{{'PROJECT.REPO_COUNT'| translate}}</clr-dg-column>
@ -19,5 +19,5 @@
<clr-dg-footer> <clr-dg-footer>
{{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}} {{totalRecordCount || (projects ? projects.length : 0)}} {{'PROJECT.ITEMS' | translate}}
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination> <clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
</clr-dg-footer> </clr-dg-footer>
</clr-datagrid> </clr-datagrid>

View File

@ -1,27 +1,23 @@
<clr-modal [(clrModalOpen)]="addMemberOpened"> <clr-modal [(clrModalOpen)]="addMemberOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3> <h3 class="modal-title">{{'MEMBER.NEW_MEMBER' | translate}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #memberForm="ngForm"> <form #memberForm="ngForm">
<section class="form-block"> <section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group"> <div class="form-group">
<label for="member_name" class="col-md-4">{{'MEMBER.NAME' | translate}}</label> <label for="member_name" class="col-md-4 form-group-label-override">{{'MEMBER.NAME' | translate}}</label>
<label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="memberName.invalid && (memberName.dirty || memberName.touched)" [class.valid]="memberName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="member_name" aria-haspopup="true" role="tooltip" [class.invalid]="memberName.invalid && (memberName.dirty || memberName.touched)" [class.valid]="memberName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" #memberName="ngModel" required> <input type="text" id="member_name" [(ngModel)]="member.username" name="name" size="20" #memberName="ngModel" required targetExists="MEMBER_NAME" [projectId]="projectId">
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.required && (memberName.dirty || memberName.touched)"> <span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.required && (memberName.dirty || memberName.touched)">
Username is required. {{ 'MEMBER.USERNAME_IS_REQUIRED' | translate }}
</span>
<span class="tooltip-content" *ngIf="memberName.errors && memberName.errors.targetExists && (memberName.dirty || memberName.touched)">
{{ 'MEMBER.USERNAME_ALREADY_EXISTS' | translate }}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'MEMBER.ROLE' | translate}}</label> <label class="col-md-4 form-group-label-override">{{'MEMBER.ROLE' | translate}}</label>
<div class="radio"> <div class="radio">
<input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id"> <input type="radio" name="roleRadios" id="checkrads_project_admin" value="1" [(ngModel)]="member.role_id">
<label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label> <label for="checkrads_project_admin">{{'MEMBER.PROJECT_ADMIN' | translate}}</label>

View File

@ -20,10 +20,15 @@ import { Member } from '../member';
export class AddMemberComponent implements AfterViewChecked { export class AddMemberComponent implements AfterViewChecked {
member: Member = new Member(); member: Member = new Member();
initVal: Member = new Member();
addMemberOpened: boolean; addMemberOpened: boolean;
memberForm: NgForm; memberForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('memberForm') @ViewChild('memberForm')
currentForm: NgForm; currentForm: NgForm;
@ -40,6 +45,7 @@ export class AddMemberComponent implements AfterViewChecked {
private translateService: TranslateService) {} private translateService: TranslateService) {}
onSubmit(): void { onSubmit(): void {
if(!this.member.username || this.member.username.length === 0) { return; }
console.log('Adding member:' + JSON.stringify(this.member)); console.log('Adding member:' + JSON.stringify(this.member));
this.memberService this.memberService
.addMember(this.projectId, this.member.username, +this.member.role_id) .addMember(this.projectId, this.member.username, +this.member.role_id)
@ -76,6 +82,7 @@ export class AddMemberComponent implements AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.addMemberOpened = false; this.addMemberOpened = false;
this.memberForm.reset();
} }
} }
@ -83,21 +90,15 @@ export class AddMemberComponent implements AfterViewChecked {
this.memberForm = this.currentForm; this.memberForm = this.currentForm;
if(this.memberForm) { if(this.memberForm) {
this.memberForm.valueChanges.subscribe(data=>{ this.memberForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true;
break;
} else if (typeof item === 'number' && (<number>item) !== 0) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
@ -107,6 +108,7 @@ export class AddMemberComponent implements AfterViewChecked {
confirmCancel(confirmed: boolean) { confirmCancel(confirmed: boolean) {
this.addMemberOpened = false; this.addMemberOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.memberForm.reset();
} }
openAddMemberModal(): void { openAddMemberModal(): void {

View File

@ -5,15 +5,15 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a> <a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
<li class="nav-item" *ngIf="isSessionValid"> <li class="nav-item" *ngIf="isSessionValid">
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a> <a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid"> <li class="nav-item" *ngIf="isSessionValid">
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a> <a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
</li> </li>
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
</li>
</ul> </ul>
</nav> </nav>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -51,7 +51,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
isPublic: number; isPublic: number;
page: number = 1; page: number = 1;
pageSize: number = 3; pageSize: number = 15;
totalPage: number; totalPage: number;
totalRecordCount: number; totalRecordCount: number;

View File

@ -18,6 +18,8 @@ import { ProjectService } from './project.service';
import { MemberService } from './member/member.service'; import { MemberService } from './member/member.service';
import { ProjectRoutingResolver } from './project-routing-resolver.service'; import { ProjectRoutingResolver } from './project-routing-resolver.service';
import { TargetExistsValidatorDirective } from '../shared/target-exists-directive';
@NgModule({ @NgModule({
imports: [ imports: [
SharedModule, SharedModule,
@ -32,7 +34,8 @@ import { ProjectRoutingResolver } from './project-routing-resolver.service';
ListProjectComponent, ListProjectComponent,
ProjectDetailComponent, ProjectDetailComponent,
MemberComponent, MemberComponent,
AddMemberComponent AddMemberComponent,
TargetExistsValidatorDirective
], ],
exports: [ProjectComponent, ListProjectComponent], exports: [ProjectComponent, ListProjectComponent],
providers: [ProjectRoutingResolver, ProjectService, MemberService] providers: [ProjectRoutingResolver, ProjectService, MemberService]

View File

@ -62,4 +62,12 @@ export class ProjectService {
.map(response=>response.status) .map(response=>response.status)
.catch(error=>Observable.throw(error)); .catch(error=>Observable.throw(error));
} }
checkProjectExists(projectName: string): Observable<any> {
return this.http
.head(`/api/projects/?project_name=${projectName}`)
.map(response=>response.status)
.catch(error=>Observable.throw(error));
}
} }

View File

@ -1,18 +1,11 @@
<clr-modal [(clrModalOpen)]="createEditDestinationOpened"> <clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #targetForm="ngForm"> <form #targetForm="ngForm">
<section class="form-block"> <section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group"> <div class="form-group">
<label for="destination_name" class="col-md-4">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label> <label for="destination_name" class="col-md-4 form-group-label-override">{{ 'DESTINATION.NAME' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label class="col-md-8" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [disabled]="testOngoing" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required> <input type="text" id="destination_name" [disabled]="testOngoing" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)"> <span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
@ -21,7 +14,7 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_url" class="col-md-4">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label> <label for="destination_url" class="col-md-4 form-group-label-override">{{ 'DESTINATION.URL' | translate }}<span style="color: red">*</span></label>
<label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label class="col-md-8" for="destination_url" aria-haspopup="true" role="tooltip" [class.invalid]="targetEndpoint.errors && (targetEndpoint.dirty || targetEndpoint.touched)" [class.valid]="targetEndpoint.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required> <input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required>
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)"> <span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
@ -30,17 +23,17 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_username" class="col-md-4">{{ 'DESTINATION.USERNAME' | translate }}</label> <label for="destination_username" class="col-md-4 form-group-label-override">{{ 'DESTINATION.USERNAME' | translate }}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel"> <input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="target.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_password" class="col-md-4">{{ 'DESTINATION.PASSWORD' | translate }}</label> <label for="destination_password" class="col-md-4 form-group-label-override">{{ 'DESTINATION.PASSWORD' | translate }}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel"> <input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="target.password" size="20" name="password" #password="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span> <span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div> </div>
</section> </section>
</form> </form>

View File

@ -15,7 +15,7 @@ import { TranslateService } from '@ngx-translate/core';
selector: 'create-edit-destination', selector: 'create-edit-destination',
templateUrl: './create-edit-destination.component.html' templateUrl: './create-edit-destination.component.html'
}) })
export class CreateEditDestinationComponent { export class CreateEditDestinationComponent implements AfterViewChecked {
modalTitle: string; modalTitle: string;
createEditDestinationOpened: boolean; createEditDestinationOpened: boolean;
@ -27,9 +27,13 @@ export class CreateEditDestinationComponent {
actionType: ActionType; actionType: ActionType;
target: Target = new Target(); target: Target = new Target();
initVal: Target = new Target();
targetForm: NgForm; targetForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('targetForm') @ViewChild('targetForm')
currentForm: NgForm; currentForm: NgForm;
@ -47,7 +51,6 @@ export class CreateEditDestinationComponent {
openCreateEditTarget(targetId?: number) { openCreateEditTarget(targetId?: number) {
this.target = new Target(); this.target = new Target();
this.createEditDestinationOpened = true; this.createEditDestinationOpened = true;
this.hasChanged = false; this.hasChanged = false;
@ -62,7 +65,13 @@ export class CreateEditDestinationComponent {
this.replicationService this.replicationService
.getTarget(targetId) .getTarget(targetId)
.subscribe( .subscribe(
target=>this.target=target, target=>{
this.target = target;
this.initVal.name = this.target.name;
this.initVal.endpoint = this.target.endpoint;
this.initVal.username = this.target.username;
this.initVal.password = this.target.password;
},
error=>this.messageService error=>this.messageService
.announceMessage(error.status, 'DESTINATION.FAILED_TO_GET_TARGET', AlertType.DANGER) .announceMessage(error.status, 'DESTINATION.FAILED_TO_GET_TARGET', AlertType.DANGER)
); );
@ -171,22 +180,26 @@ export class CreateEditDestinationComponent {
this.inlineAlert.close(); this.inlineAlert.close();
} }
mappedName: {} = {
'targetName': 'name',
'endpointUrl': 'endpoint',
'username': 'username',
'password': 'password'
};
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
this.targetForm = this.currentForm; this.targetForm = this.currentForm;
if(this.targetForm) { if(this.targetForm) {
this.targetForm.valueChanges.subscribe(data=>{ this.targetForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let current = data[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let origin = this.initVal[this.mappedName[i]];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });

View File

@ -16,8 +16,8 @@
<clr-dg-cell>{{t.tag}}</clr-dg-cell> <clr-dg-cell>{{t.tag}}</clr-dg-cell>
<clr-dg-cell>{{t.pullCommand}}</clr-dg-cell> <clr-dg-cell>{{t.pullCommand}}</clr-dg-cell>
<clr-dg-cell> <clr-dg-cell>
<clr-icon shape="check" *ngIf="t.verified" style="color: #1D5100;"></clr-icon> <clr-icon shape="check" *ngIf="t.signed" style="color: #1D5100;"></clr-icon>
<clr-icon shape="close" *ngIf="!t.verified" style="color: #C92100;"></clr-icon> <clr-icon shape="close" *ngIf="!t.signed" style="color: #C92100;"></clr-icon>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell>{{t.author}}</clr-dg-cell> <clr-dg-cell>{{t.author}}</clr-dg-cell>
<clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell> <clr-dg-cell>{{t.created | date: 'yyyy/MM/dd'}}</clr-dg-cell>

View File

@ -26,6 +26,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
repoName: string; repoName: string;
tags: TagView[]; tags: TagView[];
registryUrl: string;
private subscription: Subscription; private subscription: Subscription;
@ -66,6 +67,7 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
this.projectId = this.route.snapshot.params['id']; this.projectId = this.route.snapshot.params['id'];
this.repoName = this.route.snapshot.params['repo']; this.repoName = this.route.snapshot.params['repo'];
this.tags = []; this.tags = [];
this.registryUrl = this.appConfigService.getConfig().registry_url;
this.retrieve(); this.retrieve();
} }
@ -87,10 +89,10 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
let data = JSON.parse(t.manifest.history[0].v1Compatibility); let data = JSON.parse(t.manifest.history[0].v1Compatibility);
tag.architecture = data['architecture']; tag.architecture = data['architecture'];
tag.author = data['author']; tag.author = data['author'];
tag.verified = t.signed; tag.signed = t.signed;
tag.created = data['created']; tag.created = data['created'];
tag.dockerVersion = data['docker_version']; tag.dockerVersion = data['docker_version'];
tag.pullCommand = 'docker pull ' + t.manifest.name + ':' + t.tag; tag.pullCommand = 'docker pull ' + this.registryUrl + '/' + t.manifest.name + ':' + t.tag;
tag.os = data['os']; tag.os = data['os'];
this.tags.push(tag); this.tags.push(tag);
}); });
@ -100,18 +102,20 @@ export class TagRepositoryComponent implements OnInit, OnDestroy {
deleteTag(tag: TagView) { deleteTag(tag: TagView) {
if (tag) { if (tag) {
let titleKey: string, summaryKey: string; let titleKey: string, summaryKey: string, content: string;
if (tag.verified) { if (tag.signed) {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG_DENIED';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG_DENIED';
content = 'notary -s https://' + this.registryUrl + ' -d ~/.docker/trust remove -p ' + this.registryUrl + '/' + this.repoName + ':' + tag.tag;
} else { } else {
titleKey = 'REPOSITORY.DELETION_TITLE_TAG'; titleKey = 'REPOSITORY.DELETION_TITLE_TAG';
summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG'; summaryKey = 'REPOSITORY.DELETION_SUMMARY_TAG';
content = tag.tag;
} }
let message = new ConfirmationMessage( let message = new ConfirmationMessage(
titleKey, titleKey,
summaryKey, summaryKey,
tag.tag, content,
tag, tag,
ConfirmationTargets.TAG); ConfirmationTargets.TAG);
this.deletionDialogService.openComfirmDialog(message); this.deletionDialogService.openComfirmDialog(message);

View File

@ -1,7 +1,7 @@
export class TagView { export class TagView {
tag: string; tag: string;
pullCommand: string; pullCommand: string;
verified: boolean; signed: boolean;
author: string; author: string;
created: Date; created: Date;
dockerVersion: string; dockerVersion: string;

View File

@ -1,75 +1,68 @@
<clr-modal [(clrModalOpen)]="createEditPolicyOpened"> <clr-modal [(clrModalOpen)]="createEditPolicyOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
<h3 class="modal-title">{{modalTitle}}</h3> <h3 class="modal-title">{{modalTitle}}</h3>
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert> <inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
<div class="modal-body"> <div class="modal-body">
<form #policyForm="ngForm"> <form #policyForm="ngForm">
<section class="form-block"> <section class="form-block">
<clr-alert [clrAlertType]="'alert-danger'" [(clrAlertClosed)]="!errorMessageOpened" (clrAlertClosedChange)="onErrorMessageClose()">
<div class="alert-item">
<span class="alert-text">
{{errorMessage}}
</span>
</div>
</clr-alert>
<div class="form-group"> <div class="form-group">
<label for="policy_name" class="col-md-4">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label> <label for="policy_name" class="col-md-4 form-group-label-override">{{'REPLICATION.NAME' | translate}}<span style="color: red">*</span></label>
<label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" [class.valid]="name.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="policy_name" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="name.errors && (name.dirty || name.touched)" [class.valid]="name.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required> <input type="text" id="policy_name" [(ngModel)]="createEditPolicy.name" name="name" #name="ngModel" required [disabled]="readonly">
<span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)"> <span class="tooltip-content" *ngIf="name.errors && name.errors.required && (name.dirty || name.touched)">
{{'REPLICATION.NAME_IS_REQUIRED'}} {{'REPLICATION.NAME_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="policy_description" class="col-md-4">{{'REPLICATION.DESCRIPTION' | translate}}</label> <label for="policy_description" class="col-md-4 form-group-label-override">{{'REPLICATION.DESCRIPTION' | translate}}</label>
<input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel"> <input type="text" class="col-md-8" id="policy_description" [(ngModel)]="createEditPolicy.description" name="description" size="20" #description="ngModel" [disabled]="readonly">
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label> <label class="col-md-4">{{'REPLICATION.ENABLE' | translate}}</label>
<div class="checkbox-inline"> <div class="checkbox-inline">
<input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel"> <input type="checkbox" id="policy_enable" [(ngModel)]="createEditPolicy.enable" name="enable" #enable="ngModel" [disabled]="untoggleable">
<label for="policy_enable"></label> <label for="policy_enable"></label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_name" class="col-md-4">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label> <label for="destination_name" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_NAME' | translate}}<span style="color: red">*</span></label>
<div class="select" *ngIf="!isCreateDestination"> <div class="select" *ngIf="!isCreateDestination">
<select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing"> <select id="destination_name" [(ngModel)]="createEditPolicy.targetId" name="targetId" (change)="selectTarget()" [disabled]="testOngoing || readonly">
<option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option> <option *ngFor="let t of targets" [value]="t.id" [selected]="t.id == createEditPolicy.targetId">{{t.name}}</option>
</select> </select>
</div> </div>
<label class="col-md-8" *ngIf="isCreateDestination" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label class="col-md-8" *ngIf="isCreateDestination" for="destination_name" aria-haspopup="true" role="tooltip" [class.invalid]="targetName.errors && (targetName.dirty || targetName.touched)" [class.valid]="targetName.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="20" #targetName="ngModel" value="" required> <input type="text" id="destination_name" [(ngModel)]="createEditPolicy.targetName" name="targetName" size="8" #targetName="ngModel" value="" required>
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)"> <span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
{{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}} {{'REPLICATION.DESTINATION_NAME_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
<div class="checkbox-inline"> <div class="checkbox-inline" *ngIf="showNewDestination">
<input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing"> <input type="checkbox" id="check_new" (click)="newDestination(checkedAddNew.checked)" #checkedAddNew [checked]="isCreateDestination" [disabled]="testOngoing || readonly">
<label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label> <label for="check_new">{{'REPLICATION.NEW_DESTINATION' | translate}}</label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_url" class="col-md-4">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label> <label for="destination_url" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_URL' | translate}}<span style="color: red">*</span></label>
<label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)" [class.valid]="endpointUrl.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right"> <label for="destination_url" class="col-md-8" aria-haspopup="true" role="tooltip" [class.invalid]="endpointUrl.errors && (endpointUrl.dirty || endpointUrl.touched)" [class.valid]="endpointUrl.valid" class="tooltip tooltip-validation tooltip-sm tooltip-bottom-right">
<input type="text" id="destination_url" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel"> <input type="text" id="destination_url" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.endpointUrl" size="20" name="endpointUrl" required #endpointUrl="ngModel">
<span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)"> <span class="tooltip-content" *ngIf="endpointUrl.errors && endpointUrl.errors.required && (endpointUrl.dirty || endpointUrl.touched)">
{{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}} {{'REPLICATION.DESTINATION_URL_IS_REQUIRED' | translate}}
</span> </span>
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_username" class="col-md-4">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label> <label for="destination_username" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_USERNAME' | translate}}</label>
<input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel"> <input type="text" class="col-md-8" id="destination_username" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.username" size="20" name="username" #username="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="destination_password" class="col-md-4">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label> <label for="destination_password" class="col-md-4 form-group-label-override">{{'REPLICATION.DESTINATION_PASSWORD' | translate}}</label>
<input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel"> <input type="password" class="col-md-8" id="destination_password" [disabled]="testOngoing || readonly" [(ngModel)]="createEditPolicy.password" size="20" name="password" #password="ngModel">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="spin" class="col-md-4"></label> <label for="spin" class="col-md-4"></label>
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span> <span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
<span [style.color]="!pingStatus ? 'red': ''">{{ pingTestMessage }}</span> <span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
</div> </div>
</section> </section>
</form> </form>

View File

@ -24,6 +24,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
modalTitle: string; modalTitle: string;
createEditPolicyOpened: boolean; createEditPolicyOpened: boolean;
createEditPolicy: CreateEditPolicy = new CreateEditPolicy(); createEditPolicy: CreateEditPolicy = new CreateEditPolicy();
initVal: CreateEditPolicy = new CreateEditPolicy();
actionType: ActionType; actionType: ActionType;
@ -40,6 +41,9 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
policyForm: NgForm; policyForm: NgForm;
staticBackdrop: boolean = true;
closable: boolean = false;
@ViewChild('policyForm') @ViewChild('policyForm')
currentForm: NgForm; currentForm: NgForm;
@ -48,6 +52,18 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
@ViewChild(InlineAlertComponent) @ViewChild(InlineAlertComponent)
inlineAlert: InlineAlertComponent; inlineAlert: InlineAlertComponent;
get readonly(): boolean {
return this.actionType === ActionType.EDIT && this.createEditPolicy.enable;
}
get untoggleable(): boolean {
return this.actionType === ActionType.EDIT && this.initVal.enable;
}
get showNewDestination(): boolean {
return this.actionType === ActionType.ADD_NEW || !this.createEditPolicy.enable;
}
constructor( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private messageService: MessageService, private messageService: MessageService,
@ -67,6 +83,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.endpointUrl = initialTarget.endpoint; this.createEditPolicy.endpointUrl = initialTarget.endpoint;
this.createEditPolicy.username = initialTarget.username; this.createEditPolicy.username = initialTarget.username;
this.createEditPolicy.password = initialTarget.password; this.createEditPolicy.password = initialTarget.password;
this.initVal.targetId = this.createEditPolicy.targetId;
this.initVal.endpointUrl = this.createEditPolicy.endpointUrl;
this.initVal.username = this.createEditPolicy.username;
this.initVal.password = this.createEditPolicy.password;
} }
}, },
error=>this.messageService.announceMessage(error.status, 'Error occurred while get targets.', AlertType.DANGER) error=>this.messageService.announceMessage(error.status, 'Error occurred while get targets.', AlertType.DANGER)
@ -78,6 +99,7 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
openCreateEditPolicy(policyId?: number): void { openCreateEditPolicy(policyId?: number): void {
this.createEditPolicyOpened = true; this.createEditPolicyOpened = true;
this.createEditPolicy = new CreateEditPolicy(); this.createEditPolicy = new CreateEditPolicy();
this.isCreateDestination = false; this.isCreateDestination = false;
this.hasChanged = false; this.hasChanged = false;
@ -97,7 +119,11 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.createEditPolicy.name = policy.name; this.createEditPolicy.name = policy.name;
this.createEditPolicy.description = policy.description; this.createEditPolicy.description = policy.description;
this.createEditPolicy.enable = policy.enabled === 1? true : false; this.createEditPolicy.enable = policy.enabled === 1? true : false;
this.prepareTargets(policy.target_id); this.prepareTargets(policy.target_id);
this.initVal.name = this.createEditPolicy.name;
this.initVal.description = this.createEditPolicy.description;
this.initVal.enable = this.createEditPolicy.enable;
} }
) )
} else { } else {
@ -218,12 +244,14 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'}); this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
} else { } else {
this.createEditPolicyOpened = false; this.createEditPolicyOpened = false;
this.policyForm.reset();
} }
} }
confirmCancel(confirmed: boolean) { confirmCancel(confirmed: boolean) {
this.createEditPolicyOpened = false; this.createEditPolicyOpened = false;
this.inlineAlert.close(); this.inlineAlert.close();
this.policyForm.reset();
} }
ngAfterViewChecked(): void { ngAfterViewChecked(): void {
@ -231,24 +259,20 @@ export class CreateEditPolicyComponent implements OnInit, AfterViewChecked {
if(this.policyForm) { if(this.policyForm) {
this.policyForm.valueChanges.subscribe(data=>{ this.policyForm.valueChanges.subscribe(data=>{
for(let i in data) { for(let i in data) {
let item = data[i]; let origin = this.initVal[i];
if(typeof item === 'string' && (<string>item).trim().length !== 0) { let current = data[i];
this.hasChanged = true; if(current && current !== origin) {
break;
} else if (typeof item === 'boolean' && (<boolean>item)) {
this.hasChanged = true; this.hasChanged = true;
break; break;
} else { } else {
this.hasChanged = false; this.hasChanged = false;
this.inlineAlert.close(); this.inlineAlert.close();
break;
} }
} }
}); });
} }
} }
testConnection() { testConnection() {
this.pingStatus = true; this.pingStatus = true;
this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res); this.translateService.get('REPLICATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);

View File

@ -8,10 +8,17 @@
<clr-dg-row *ngFor="let p of policies;let i = index;" [clrDgItem]="p" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''"> <clr-dg-row *ngFor="let p of policies;let i = index;" [clrDgItem]="p" (click)="selectPolicy(p)" [style.backgroundColor]="(!projectless && selectedId === p.id) ? '#eee' : ''">
<clr-dg-action-overflow> <clr-dg-action-overflow>
<button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button> <button class="action-item" (click)="editPolicy(p)">{{'REPLICATION.EDIT_POLICY' | translate}}</button>
<button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.ENABLE' : 'REPLICATION.DISABLE') | translate}}</button> <button class="action-item" (click)="togglePolicy(p)">{{ (p.enabled === 0 ? 'REPLICATION.TOGGLE_ENABLE_TITLE' : 'REPLICATION.TOGGLE_DISABLE_TITLE') | translate}}</button>
<button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button> <button class="action-item" (click)="deletePolicy(p)">{{'REPLICATION.DELETE_POLICY' | translate}}</button>
</clr-dg-action-overflow> </clr-dg-action-overflow>
<clr-dg-cell>{{p.name}}</clr-dg-cell> <clr-dg-cell>
<template [ngIf]="projectless">
<a href="javascript:void(0)" [routerLink]="['/harbor', 'projects', p.project_id, 'replication']">{{p.name}}</a>
</template>
<template [ngIf]="!projectless">
{{p.name}}
</template>
</clr-dg-cell>
<clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell> <clr-dg-cell *ngIf="projectless">{{p.project_name}}</clr-dg-cell>
<clr-dg-cell>{{p.description}}</clr-dg-cell> <clr-dg-cell>{{p.description}}</clr-dg-cell>
<clr-dg-cell>{{p.target_name}}</clr-dg-cell> <clr-dg-cell>{{p.target_name}}</clr-dg-cell>

View File

@ -28,31 +28,53 @@ export class ListPolicyComponent implements OnDestroy {
@Output() editOne = new EventEmitter<number>(); @Output() editOne = new EventEmitter<number>();
@Output() toggleOne = new EventEmitter<Policy>(); @Output() toggleOne = new EventEmitter<Policy>();
toggleSubscription: Subscription;
subscription: Subscription; subscription: Subscription;
constructor( constructor(
private replicationService: ReplicationService, private replicationService: ReplicationService,
private toggleConfirmDialogService: ConfirmationDialogService,
private deletionDialogService: ConfirmationDialogService, private deletionDialogService: ConfirmationDialogService,
private messageService: MessageService) { private messageService: MessageService) {
this.subscription = this.subscription = this.deletionDialogService this.toggleSubscription = this.toggleConfirmDialogService
.confirmationConfirm$
.subscribe(
message=> {
if(message &&
message.source === ConfirmationTargets.TOGGLE_CONFIRM &&
message.state === ConfirmationState.CONFIRMED) {
let policy: Policy = message.data;
policy.enabled = policy.enabled === 0 ? 1 : 0;
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled);
this.replicationService
.enablePolicy(policy.id, policy.enabled)
.subscribe(
res => console.log('Successful toggled policy status'),
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER)
);
}
}
);
this.subscription = this.deletionDialogService
.confirmationConfirm$ .confirmationConfirm$
.subscribe( .subscribe(
message => { message => {
if (message && if (message &&
message.source === ConfirmationTargets.POLICY && message.source === ConfirmationTargets.POLICY &&
message.state === ConfirmationState.CONFIRMED) { message.state === ConfirmationState.CONFIRMED) {
this.replicationService this.replicationService
.deletePolicy(message.data) .deletePolicy(message.data)
.subscribe( .subscribe(
response => { response => {
console.log('Successful delete policy with ID:' + message.data); console.log('Successful delete policy with ID:' + message.data);
this.reload.emit(true); this.reload.emit(true);
}, },
error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER) error => this.messageService.announceMessage(error.status, 'Failed to delete policy with ID:' + message.data, AlertType.DANGER)
); );
} }
}); }
);
} }
@ -60,6 +82,9 @@ export class ListPolicyComponent implements OnDestroy {
if (this.subscription) { if (this.subscription) {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
if(this.toggleSubscription) {
this.toggleSubscription.unsubscribe();
}
} }
selectPolicy(policy: Policy): void { selectPolicy(policy: Policy): void {
@ -74,13 +99,14 @@ export class ListPolicyComponent implements OnDestroy {
} }
togglePolicy(policy: Policy) { togglePolicy(policy: Policy) {
policy.enabled = policy.enabled === 0 ? 1 : 0; let toggleConfirmMessage: ConfirmationMessage = new ConfirmationMessage(
console.log('Enable policy ID:' + policy.id + ' with activation status ' + policy.enabled); policy.enabled === 1 ? 'REPLICATION.TOGGLE_DISABLE_TITLE' : 'REPLICATION.TOGGLE_ENABLE_TITLE',
this.replicationService.enablePolicy(policy.id, policy.enabled) policy.enabled === 1 ? 'REPLICATION.CONFIRM_TOGGLE_DISABLE_POLICY': 'REPLICATION.CONFIRM_TOGGLE_ENABLE_POLICY',
.subscribe( policy.name,
res => console.log('Successful toggled policy status'), policy,
error => this.messageService.announceMessage(error.status, "Failed to toggle policy status.", AlertType.DANGER) ConfirmationTargets.TOGGLE_CONFIRM
); );
this.toggleConfirmDialogService.openComfirmDialog(toggleConfirmMessage);
} }
deletePolicy(policy: Policy) { deletePolicy(policy: Policy) {

View File

@ -2,4 +2,9 @@
margin: 0px !important; margin: 0px !important;
padding: 0px !important; padding: 0px !important;
margin-top: -5px !important; margin-top: -5px !important;
}
.spinner-pos {
margin-right: 0px !important;
top: 2px;
} }

View File

@ -4,82 +4,66 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="username" class="required form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label> <label for="username" class="required form-group-label-override">{{'PROFILE.USER_NAME' | translate}}</label>
<label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("username")'> <label for="username" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("username")'>
<input type="text" placeholder='{{"PLACEHOLDER.USER_NAME" | translate}}' required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="28" <input type="text" required pattern='[^"~#$%]+' maxLengthExt="20" #usernameInput="ngModel" name="username" [(ngModel)]="newUser.username" id="username" size="40"
(input)='handleValidation("username", false)' (input)='handleValidation("username", false)'
(focusout)='handleValidation("username", true)'> (focusout)='handleValidation("username", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{usernameTooltip | translate}} {{usernameTooltip | translate}}
</span> </span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("username")'></span> </label><span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("username")'></span>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label> <label for="email" class="required form-group-label-override">{{'PROFILE.EMAIL' | translate}}</label>
<label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("email")'> <label for="email" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("email")'>
<input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email" <input name="email" type="text" #eamilInput="ngModel" [(ngModel)]="newUser.email"
placeholder='{{"PLACEHOLDER.MAIL" | translate}}'
required required
pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="28" pattern='^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$' id="email" size="40"
(input)='handleValidation("email", false)' (input)='handleValidation("email", false)'
(focusout)='handleValidation("email", true)'> (focusout)='handleValidation("email", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{emailTooltip | translate}} {{emailTooltip | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left"> <span class="spinner spinner-inline spinner-pos" [hidden]='isChecking("email")'></span>
<clr-icon shape="info" class="is-info" size="24"></clr-icon> <label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'TOOLTIP.SIGN_UP_MAIL' | translate}}</label>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_MAIL' | translate}}
</span>
</label><span class="spinner spinner-inline" [hidden]='isChecking("email")'></span>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="realname" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label> <label for="realname" class="required form-group-label-override">{{'PROFILE.FULL_NAME' | translate}}</label>
<label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("realname")'> <label for="realname" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("realname")'>
<input type="text" placeholder='{{"PLACEHOLDER.FULL_NAME" | translate}}' name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="28" <input type="text" name="realname" #fullNameInput="ngModel" [(ngModel)]="newUser.realname" required maxLengthExt="20" id="realname" size="40"
(input)='handleValidation("realname", false)' (input)='handleValidation("realname", false)'
(focusout)='handleValidation("realname", true)'> (focusout)='handleValidation("realname", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.FULL_NAME' | translate}} {{'TOOLTIP.FULL_NAME' | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left">
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.SIGN_UP_REAL_NAME' | translate}}
</span>
</label>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="newPassword" class="required form-group-label-override">{{'PROFILE.PASSWORD' | translate}}</label> <label for="newPassword" class="required form-group-label-override">{{'PROFILE.PASSWORD' | translate}}</label>
<label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword")'> <label for="newPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("newPassword")'>
<input type="password" id="newPassword" placeholder='{{"PLACEHOLDER.NEW_PWD" | translate}}' <input type="password" id="newPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="newPassword" name="newPassword"
[(ngModel)]="newUser.password" [(ngModel)]="newUser.password"
#newPassInput="ngModel" size="28" #newPassInput="ngModel" size="40"
(input)='handleValidation("newPassword", false)' (input)='handleValidation("newPassword", false)'
(focusout)='handleValidation("newPassword", true)'> (focusout)='handleValidation("newPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}} {{'TOOLTIP.PASSWORD' | translate}}
</span> </span>
</label> </label>
<label *ngIf="isSelfRegistration" role="tooltip" aria-haspopup="true" class="tooltip tooltip-bottom-left"> <label class="sub-label-for-input" *ngIf="isSelfRegistration">{{'CHANGE_PWD.PASS_TIPS' | translate}}</label>
<clr-icon shape="info" class="is-info" size="24"></clr-icon>
<span class="tooltip-content">
{{'TOOLTIP.PASSWORD' | translate}}
</span>
</label>
</div> </div>
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="confirmPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label> <label for="confirmPassword" class="required form-group-label-override">{{'CHANGE_PWD.CONFIRM_PWD' | translate}}</label>
<label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("confirmPassword")'> <label for="confirmPassword" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("confirmPassword")'>
<input type="password" id="confirmPassword" placeholder='{{"PLACEHOLDER.CONFIRM_PWD" | translate}}' <input type="password" id="confirmPassword"
required required
pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{7,}$" pattern="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$"
name="confirmPassword" name="confirmPassword"
[(ngModel)]="confirmedPwd" [(ngModel)]="confirmedPwd"
#confirmPassInput="ngModel" size="28" #confirmPassInput="ngModel" size="40"
(input)='handleValidation("confirmPassword", false)' (input)='handleValidation("confirmPassword", false)'
(focusout)='handleValidation("confirmPassword", true)'> (focusout)='handleValidation("confirmPassword", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
@ -90,7 +74,7 @@
<div class="form-group form-group-override"> <div class="form-group form-group-override">
<label for="comment" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label> <label for="comment" class="form-group-label-override">{{'PROFILE.COMMENT' | translate}}</label>
<label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("comment")'> <label for="comment" aria-haspopup="true" role="tooltip" class="tooltip tooltip-validation tooltip-md tooltip-bottom-left" [class.invalid]='getValidationState("comment")'>
<input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="28" <input type="text" #commentInput="ngModel" name="comment" [(ngModel)]="newUser.comment" maxLengthExt="20" id="comment" size="40"
(input)='handleValidation("comment", false)' (input)='handleValidation("comment", false)'
(focusout)='handleValidation("comment", true)'> (focusout)='handleValidation("comment", true)'>
<span class="tooltip-content"> <span class="tooltip-content">
@ -100,5 +84,4 @@
</div> </div>
</section> </section>
</form> </form>
</div> </div>
<div style="height: 15px;"></div>

View File

@ -6,13 +6,11 @@ import {
CanActivateChild, CanActivateChild,
NavigationExtras NavigationExtras
} from '@angular/router'; } from '@angular/router';
import { SessionService } from '../../shared/session.service'; import { SessionService } from '../../shared/session.service';
import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const'; import { CommonRoutes, AdmiralQueryParamKey } from '../../shared/shared.const';
import { AppConfigService } from '../../app-config.service'; import { AppConfigService } from '../../app-config.service';
import { maintainUrlQueryParmas } from '../../shared/shared.utils'; import { maintainUrlQueryParmas } from '../../shared/shared.utils';
@Injectable() @Injectable()
export class AuthCheckGuard implements CanActivate, CanActivateChild { export class AuthCheckGuard implements CanActivate, CanActivateChild {
constructor( constructor(
@ -50,7 +48,6 @@ export class AuthCheckGuard implements CanActivate, CanActivateChild {
this.router.navigateByUrl(keyRemovedUrl); this.router.navigateByUrl(keyRemovedUrl);
return resolve(false); return resolve(false);
} }
} }

View File

@ -19,6 +19,7 @@ export const enum ConfirmationTargets {
PROJECT_MEMBER, PROJECT_MEMBER,
USER, USER,
POLICY, POLICY,
TOGGLE_CONFIRM,
TARGET, TARGET,
REPOSITORY, REPOSITORY,
TAG, TAG,

View File

@ -0,0 +1,66 @@
import { Directive, OnChanges, Input, SimpleChanges } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, Validators, ValidatorFn, AbstractControl } from '@angular/forms';
import { ProjectService} from '../project/project.service';
import { MemberService } from '../project/member/member.service';
import { Member } from '../project/member/member';
@Directive({
selector: '[targetExists]',
providers: [
ProjectService, MemberService,
{ provide: NG_ASYNC_VALIDATORS, useExisting: TargetExistsValidatorDirective, multi: true},
]
})
export class TargetExistsValidatorDirective implements Validator, OnChanges {
@Input() targetExists: string;
@Input() projectId: number;
private valFn = Validators.nullValidator;
constructor(
private projectService: ProjectService,
private memberService: MemberService) {}
ngOnChanges(changes: SimpleChanges): void {
const change = changes['targetExists'];
if (change) {
const target: string = change.currentValue;
this.valFn = this.targetExistsValidator(target);
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): {[key: string]: any} {
return this.valFn(control);
}
targetExistsValidator(target: string): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
console.log('Target:' + target + ', validate value:' + control.value);
switch(target) {
case 'PROJECT_NAME':
return new Promise(resolve=>{
this.projectService
.checkProjectExists(control.value)
.subscribe(res=>resolve({'targetExists': true}),error=>resolve(null));
});
case 'MEMBER_NAME':
return new Promise(resolve=>{
this.memberService
.listMembers(this.projectId, control.value)
.subscribe((members: Member[])=>{
return members.filter(m=>{
if(m.username === control.value) {
return true;
}
return null;
}).length > 0 ?
resolve({'targetExists': true}) : resolve(null);
},error=>resolve(null));
});
}
}
}
}

View File

@ -56,7 +56,7 @@
"TITLE": "User Profile", "TITLE": "User Profile",
"USER_NAME": "Username", "USER_NAME": "Username",
"EMAIL": "Email", "EMAIL": "Email",
"FULL_NAME": "Full name", "FULL_NAME": "First and last name",
"COMMENT": "Comments", "COMMENT": "Comments",
"PASSWORD": "Password", "PASSWORD": "Password",
"SAVE_SUCCESS": "User profile saved successfully" "SAVE_SUCCESS": "User profile saved successfully"
@ -66,7 +66,8 @@
"CURRENT_PWD": "Current Password", "CURRENT_PWD": "Current Password",
"NEW_PWD": "New Password", "NEW_PWD": "New Password",
"CONFIRM_PWD": "Confirm Password", "CONFIRM_PWD": "Confirm Password",
"SAVE_SUCCESS": "User password changed successfully" "SAVE_SUCCESS": "User password changed successfully",
"PASS_TIPS": "At least 8 chars with 1 uppercase, 1 lowercase and 1 number"
}, },
"ACCOUNT_SETTINGS": { "ACCOUNT_SETTINGS": {
"PROFILE": "User Profile", "PROFILE": "User Profile",
@ -152,6 +153,7 @@
"DELETE": "Delete", "DELETE": "Delete",
"ITEMS": "item(s)", "ITEMS": "item(s)",
"ACTIONS": "Actions", "ACTIONS": "Actions",
"USERNAME_IS_REQUIRED": "Username is required",
"USERNAME_DOES_NOT_EXISTS": "Username does not exist.", "USERNAME_DOES_NOT_EXISTS": "Username does not exist.",
"USERNAME_ALREADY_EXISTS": "Username already exists.", "USERNAME_ALREADY_EXISTS": "Username already exists.",
"UNKNOWN_ERROR": "Unknown error occurred while adding member.", "UNKNOWN_ERROR": "Unknown error occurred while adding member.",
@ -229,7 +231,11 @@
"CREATION_TIME": "Start Time", "CREATION_TIME": "Start Time",
"END_TIME": "End Time", "END_TIME": "End Time",
"LOGS": "Logs", "LOGS": "Logs",
"ITEMS": "item(s)" "ITEMS": "item(s)",
"TOGGLE_ENABLE_TITLE": "Enable Policy",
"CONFIRM_TOGGLE_ENABLE_POLICY": "After enabling the replication policy, all repositories under the project will be replicated to the destination registry. Please confirm to continue.",
"TOGGLE_DISABLE_TITLE": "Disable Policy",
"CONFIRM_TOGGLE_DISABLE_POLICY": "After disabling the policy, all unfinished replication jobs of this policy will be stopped and canceled. Please confirm to continue."
}, },
"DESTINATION": { "DESTINATION": {
"NEW_ENDPOINT": "New Endpoint", "NEW_ENDPOINT": "New Endpoint",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "Confirm Tag Deletion", "DELETION_TITLE_TAG": "Confirm Tag Deletion",
"DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?", "DELETION_SUMMARY_TAG": "Do you want to delete tag {{param}}?",
"DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted", "DELETION_TITLE_TAG_DENIED": "Signed Tag can't be deleted",
"DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted.", "DELETION_SUMMARY_TAG_DENIED": "The tag must be removed from the Notary before it can be deleted. {{param}}",
"FILTER_FOR_REPOSITORIES": "Filter for repositories", "FILTER_FOR_REPOSITORIES": "Filter for repositories",
"TAG": "Tag", "TAG": "Tag",
"SIGNED": "Signed", "SIGNED": "Signed",

View File

@ -66,7 +66,8 @@
"CURRENT_PWD": "当前密码", "CURRENT_PWD": "当前密码",
"NEW_PWD": "新密码", "NEW_PWD": "新密码",
"CONFIRM_PWD": "确认密码", "CONFIRM_PWD": "确认密码",
"SAVE_SUCCESS": "更改用户密码成功" "SAVE_SUCCESS": "更改用户密码成功",
"PASS_TIPS": "至少8个字符且需包含至少一个大写字符、小写字符或者数字"
}, },
"ACCOUNT_SETTINGS": { "ACCOUNT_SETTINGS": {
"PROFILE": "用户设置", "PROFILE": "用户设置",
@ -152,8 +153,9 @@
"DELETE": "删除", "DELETE": "删除",
"ITEMS": "条记录", "ITEMS": "条记录",
"ACTIONS": "操作", "ACTIONS": "操作",
"USERNAME_DOES_NOT_EXISTS": "用户名不存在", "USERNAME_IS_REQUIRED": "用户名为必填项。",
"USERNAME_ALREADY_EXISTS": "用户名已存在", "USERNAME_DOES_NOT_EXISTS": "用户名不存在。",
"USERNAME_ALREADY_EXISTS": "用户名已存在。",
"UNKNOWN_ERROR": "添加成员时发生未知错误。", "UNKNOWN_ERROR": "添加成员时发生未知错误。",
"FILTER_PLACEHOLDER": "过滤成员", "FILTER_PLACEHOLDER": "过滤成员",
"DELETION_TITLE": "删除项目成员确认", "DELETION_TITLE": "删除项目成员确认",
@ -229,7 +231,11 @@
"CREATION_TIME": "创建时间", "CREATION_TIME": "创建时间",
"END_TIME": "结束时间", "END_TIME": "结束时间",
"LOGS": "日志", "LOGS": "日志",
"ITEMS": "条记录" "ITEMS": "条记录",
"TOGGLE_ENABLE_TITLE": "启用策略",
"CONFIRM_TOGGLE_ENABLE_POLICY": "启用策略后,该项目下的所有镜像仓库将复制到目标实例。请确认继续。",
"TOGGLE_DISABLE_TITLE": "停用策略",
"CONFIRM_TOGGLE_DISABLE_POLICY": "停用策略后,所有未完成的复制任务将被终止和取消。请确认继续。"
}, },
"DESTINATION": { "DESTINATION": {
"NEW_ENDPOINT": "新建目标", "NEW_ENDPOINT": "新建目标",
@ -268,7 +274,7 @@
"DELETION_TITLE_TAG": "删除镜像标签确认", "DELETION_TITLE_TAG": "删除镜像标签确认",
"DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?", "DELETION_SUMMARY_TAG": "确认删除镜像标签 {{param}}?",
"DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除", "DELETION_TITLE_TAG_DENIED": "已签名的镜像不能被删除",
"DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。", "DELETION_SUMMARY_TAG_DENIED": "要删除此镜像标签必须首先从Notary中删除。{{param}}",
"FILTER_FOR_REPOSITORIES": "过滤镜像仓库", "FILTER_FOR_REPOSITORIES": "过滤镜像仓库",
"TAG": "标签", "TAG": "标签",
"SIGNED": "已签名", "SIGNED": "已签名",

View File

@ -1,4 +1,9 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
.datagrid-content-wrapper { .datagrid-content-wrapper {
overflow: hidden; overflow: hidden;
}
.form-group-label-override {
font-size: 14px;
font-weight: 400;
} }

19
tests/notarytest.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/sh
set -e
TIMEOUT=10
while [ $TIMEOUT -gt 0 ]; do
STATUS=$(curl -s -o /dev/null -w '%{http_code}' https://127.0.0.1/notary/v2/ -kv)
if [ $STATUS -eq 401 ]; then
echo "Notary is running success."
break
fi
TIMEOUT=$(($TIMEOUT - 1))
sleep 5
done
if [ $TIMEOUT -eq 0 ]; then
echo "Notary is running fail."
exit 1
fi