mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-24 01:27:49 +01:00
Merge remote-tracking branch 'upstream/master' into 170621_auth
Conflicts: src/ui/filter/security.go
This commit is contained in:
commit
b960565d11
1
.gitignore
vendored
1
.gitignore
vendored
@ -44,3 +44,4 @@ src/ui_ng/aot/**/*.json
|
||||
**/*ngfactory.ts
|
||||
**/aot
|
||||
**/dist
|
||||
**/.bin
|
@ -74,12 +74,13 @@ before_script:
|
||||
- sudo chmod 777 /tmp/registry.db
|
||||
|
||||
script:
|
||||
- sudo cp ./src/ui_ng/package.json ./src/ui_ng/src
|
||||
- sudo mkdir -p /etc/ui/ca/
|
||||
- sudo mv ./tests/ca.crt /etc/ui/ca/
|
||||
- sudo mkdir -p /harbor
|
||||
- sudo mv ./VERSION /harbor/VERSION
|
||||
- sudo service mysql stop
|
||||
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2
|
||||
- sudo make run_clarity_ut CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4
|
||||
- cat ./src/ui_ng/lib/npm-ut-test-results
|
||||
- sudo ./tests/testprepare.sh
|
||||
- sudo docker-compose -f ./make/docker-compose.test.yml up -d
|
||||
@ -100,7 +101,7 @@ script:
|
||||
- docker-compose -f make/docker-compose.test.yml down
|
||||
- sudo rm -rf /data/config/*
|
||||
- ls /data/cert
|
||||
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2 NOTARYFLAG=true
|
||||
- sudo make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4 NOTARYFLAG=true
|
||||
|
||||
- docker ps
|
||||
- ./tests/notarytest.sh
|
||||
|
@ -50,13 +50,13 @@ You can compile the code by one of the three approaches:
|
||||
* Build, install and bring up Harbor without Notary:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2
|
||||
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4
|
||||
```
|
||||
|
||||
* Build, install and bring up Harbor with Notary:
|
||||
|
||||
```sh
|
||||
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.1.2 NOTARYFLAG=true
|
||||
$ make install GOBUILDIMAGE=golang:1.7.3 COMPILETAG=compile_golangimage CLARITYIMAGE=vmware/harbor-clarity-ui-builder:1.2.4 NOTARYFLAG=true
|
||||
```
|
||||
|
||||
#### II. Compile code with your own Golang environment, then build Harbor
|
||||
|
@ -1809,6 +1809,18 @@ definitions:
|
||||
type: integer
|
||||
format: int
|
||||
description: The public status of the project.
|
||||
enable_content_trust:
|
||||
type: boolean
|
||||
description: Whether content trust is enabled or not. If it is enabled, user cann't pull unsigned images from this project.
|
||||
prevent_vulnerable_images_from_running:
|
||||
type: boolean
|
||||
description: Whether prevent the vulnerable images from running.
|
||||
prevent_vulnerable_images_from_running_severity:
|
||||
type: string
|
||||
description: If the vulnerability is high than severity defined here, the images cann't be pulled.
|
||||
automatically_scan_images_on_push:
|
||||
type: boolean
|
||||
description: Whether scan images automatically when pushing.
|
||||
Project:
|
||||
type: object
|
||||
properties:
|
||||
@ -1849,6 +1861,18 @@ definitions:
|
||||
repo_count:
|
||||
type: integer
|
||||
description: The number of the repositories under this project.
|
||||
enable_content_trust:
|
||||
type: boolean
|
||||
description: Whether content trust is enabled or not. If it is enabled, user cann't pull unsigned images from this project.
|
||||
prevent_vulnerable_images_from_running:
|
||||
type: boolean
|
||||
description: Whether prevent the vulnerable images from running.
|
||||
prevent_vulnerable_images_from_running_severity:
|
||||
type: string
|
||||
description: If the vulnerability is high than severity defined here, the images cann't be pulled.
|
||||
automatically_scan_images_on_push:
|
||||
type: boolean
|
||||
description: Whether scan images automatically when pushing.
|
||||
Manifest:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -1,41 +1,39 @@
|
||||
{
|
||||
"project": {
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"name": "Harbor"
|
||||
},
|
||||
"apps": [
|
||||
{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"images",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "app",
|
||||
"mobile": false,
|
||||
"styles": [
|
||||
"../node_modules/clarity-icons/clarity-icons.min.css",
|
||||
"../node_modules/clarity-ui/clarity-ui.min.css",
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"../node_modules/core-js/client/shim.min.js",
|
||||
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
|
||||
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
|
||||
"../node_modules/clarity-icons/clarity-icons.min.js",
|
||||
"../node_modules/web-animations-js/web-animations.min.js"
|
||||
],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
"apps": [{
|
||||
"root": "src",
|
||||
"outDir": "dist",
|
||||
"assets": [
|
||||
"images",
|
||||
"favicon.ico"
|
||||
],
|
||||
"index": "index.html",
|
||||
"main": "main.ts",
|
||||
"test": "test.ts",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"prefix": "app",
|
||||
"mobile": false,
|
||||
"styles": [
|
||||
"../node_modules/clarity-icons/clarity-icons.min.css",
|
||||
"../node_modules/clarity-ui/clarity-ui.min.css",
|
||||
"styles.css"
|
||||
],
|
||||
"scripts": [
|
||||
"../node_modules/core-js/client/shim.min.js",
|
||||
"../node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
|
||||
"../node_modules/@webcomponents/custom-elements/custom-elements.min.js",
|
||||
"../node_modules/clarity-icons/clarity-icons.min.js",
|
||||
"../node_modules/web-animations-js/web-animations.min.js"
|
||||
],
|
||||
"environmentSource": "environments/environment.ts",
|
||||
"environments": {
|
||||
"dev": "environments/environment.ts",
|
||||
"prod": "environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
}],
|
||||
"addons": [],
|
||||
"packages": [],
|
||||
"e2e": {
|
||||
@ -64,4 +62,4 @@
|
||||
"service": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ if [ ! -z "$npm_proxy" -a "$npm_proxy" != " " ]; then
|
||||
npm config set proxy $npm_proxy
|
||||
fi
|
||||
|
||||
cp ./src/package.json .
|
||||
npm install
|
||||
|
||||
./node_modules/.bin/ngc -p tsconfig-aot.json
|
||||
|
@ -99,3 +99,13 @@ type BaseProjectCollection struct {
|
||||
Public bool
|
||||
Member string
|
||||
}
|
||||
|
||||
// ProjectRequest holds informations that need for creating project API
|
||||
type ProjectRequest struct {
|
||||
Name string `json:"project_name"`
|
||||
Public int `json:"public"`
|
||||
EnableContentTrust bool `json:"enable_content_trust"`
|
||||
PreventVulnerableImagesFromRunning bool `json:"prevent_vulnerable_images_from_running"`
|
||||
PreventVulnerableImagesFromRunningSeverity string `json:"prevent_vulnerable_images_from_running_severity"`
|
||||
AutomaticallyScanImagesOnPush bool `json:"automatically_scan_images_on_push"`
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ package secret
|
||||
|
||||
import (
|
||||
"github.com/vmware/harbor/src/common/secret"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
)
|
||||
|
||||
// SecurityContext implements security.Context interface based on secret store
|
||||
@ -35,9 +36,15 @@ func NewSecurityContext(secret string, store *secret.Store) *SecurityContext {
|
||||
// IsAuthenticated returns true if the secret is valid
|
||||
func (s *SecurityContext) IsAuthenticated() bool {
|
||||
if s.store == nil {
|
||||
log.Debug("secret store is nil")
|
||||
return false
|
||||
}
|
||||
return s.store.IsValid(s.secret)
|
||||
valid := s.store.IsValid(s.secret)
|
||||
if !valid {
|
||||
log.Debugf("invalid secret: %s", s.secret)
|
||||
}
|
||||
|
||||
return valid
|
||||
}
|
||||
|
||||
// GetUsername returns the corresponding username of the secret
|
||||
|
@ -27,7 +27,6 @@ import (
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema1"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
comutils "github.com/vmware/harbor/src/common/utils"
|
||||
"github.com/vmware/harbor/src/common/utils/log"
|
||||
@ -182,13 +181,13 @@ func (c *Checker) Enter() (string, error) {
|
||||
}
|
||||
|
||||
func (c *Checker) enter() (string, error) {
|
||||
project, err := dao.GetProjectByName(c.project)
|
||||
project, err := getProject(c.project)
|
||||
if err != nil {
|
||||
c.logger.Errorf("an error occurred while getting project %s in DB: %v", c.project, err)
|
||||
c.logger.Errorf("failed to get project %s from %s: %v", c.project, c.srcURL, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = c.createProject(project.Public)
|
||||
err = c.createProject(project)
|
||||
if err == nil {
|
||||
c.logger.Infof("project %s is created on %s with user %s", c.project, c.dstURL, c.dstUsr)
|
||||
return StatePullManifest, nil
|
||||
@ -207,16 +206,61 @@ func (c *Checker) enter() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
func (c *Checker) createProject(public int) error {
|
||||
project := struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
Public int `json:"public"`
|
||||
}{
|
||||
ProjectName: c.project,
|
||||
Public: public,
|
||||
func getProject(name string) (*models.Project, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, buildProjectURL(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := json.Marshal(project)
|
||||
req.URL.Query().Set("name", name)
|
||||
req.URL.Query().Encode()
|
||||
req.AddCookie(&http.Cookie{
|
||||
Name: models.UISecretCookie,
|
||||
Value: config.JobserviceSecret(),
|
||||
})
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
if err = json.Unmarshal(data, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var project *models.Project
|
||||
for _, p := range list {
|
||||
if p.Name == name {
|
||||
project = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if project == nil {
|
||||
return nil, fmt.Errorf("project %s not found", name)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (c *Checker) createProject(project *models.Project) error {
|
||||
pro := &models.ProjectRequest{
|
||||
Name: project.Name,
|
||||
Public: project.Public,
|
||||
EnableContentTrust: project.EnableContentTrust,
|
||||
PreventVulnerableImagesFromRunning: project.PreventVulnerableImagesFromRunning,
|
||||
PreventVulnerableImagesFromRunningSeverity: project.PreventVulnerableImagesFromRunningSeverity,
|
||||
AutomaticallyScanImagesOnPush: project.AutomaticallyScanImagesOnPush,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(pro)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -263,6 +307,10 @@ func (c *Checker) createProject(public int) error {
|
||||
c.project, c.dstURL, c.dstUsr, resp.StatusCode, string(message))
|
||||
}
|
||||
|
||||
func buildProjectURL() string {
|
||||
return strings.TrimRight(config.LocalUIURL(), "/") + "/api/projects/"
|
||||
}
|
||||
|
||||
// ManifestPuller pulls the manifest of a tag. And if no tag needs to be pulled,
|
||||
// the next state that state machine should enter is "finished".
|
||||
type ManifestPuller struct {
|
||||
|
@ -36,11 +36,6 @@ type ProjectAPI struct {
|
||||
project *models.Project
|
||||
}
|
||||
|
||||
type projectReq struct {
|
||||
ProjectName string `json:"project_name"`
|
||||
Public int `json:"public"`
|
||||
}
|
||||
|
||||
const projectNameMaxLen int = 30
|
||||
const projectNameMinLen int = 2
|
||||
const restrictedNameChars = `[a-z0-9]+(?:[._-][a-z0-9]+)*`
|
||||
@ -95,7 +90,7 @@ func (p *ProjectAPI) Post() {
|
||||
p.RenderError(http.StatusForbidden, "Only system admin can create project")
|
||||
return
|
||||
}
|
||||
var pro projectReq
|
||||
var pro *models.ProjectRequest
|
||||
p.DecodeJSONReq(&pro)
|
||||
err = validateProjectReq(pro)
|
||||
if err != nil {
|
||||
@ -104,10 +99,10 @@ func (p *ProjectAPI) Post() {
|
||||
return
|
||||
}
|
||||
|
||||
exist, err := p.ProjectMgr.Exist(pro.ProjectName)
|
||||
exist, err := p.ProjectMgr.Exist(pro.Name)
|
||||
if err != nil {
|
||||
p.HandleInternalServerError(fmt.Sprintf("failed to check the existence of project %s: %v",
|
||||
pro.ProjectName, err))
|
||||
pro.Name, err))
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
@ -116,9 +111,13 @@ func (p *ProjectAPI) Post() {
|
||||
}
|
||||
|
||||
projectID, err := p.ProjectMgr.Create(&models.Project{
|
||||
Name: pro.ProjectName,
|
||||
Public: pro.Public,
|
||||
OwnerName: p.SecurityCtx.GetUsername(),
|
||||
Name: pro.Name,
|
||||
Public: pro.Public,
|
||||
OwnerName: p.SecurityCtx.GetUsername(),
|
||||
EnableContentTrust: pro.EnableContentTrust,
|
||||
PreventVulnerableImagesFromRunning: pro.PreventVulnerableImagesFromRunning,
|
||||
PreventVulnerableImagesFromRunningSeverity: pro.PreventVulnerableImagesFromRunningSeverity,
|
||||
AutomaticallyScanImagesOnPush: pro.AutomaticallyScanImagesOnPush,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to add project, error: %v", err)
|
||||
@ -136,7 +135,7 @@ func (p *ProjectAPI) Post() {
|
||||
models.AccessLog{
|
||||
Username: p.SecurityCtx.GetUsername(),
|
||||
ProjectID: projectID,
|
||||
RepoName: pro.ProjectName + "/",
|
||||
RepoName: pro.Name + "/",
|
||||
RepoTag: "N/A",
|
||||
Operation: "create",
|
||||
OpTime: time.Now(),
|
||||
@ -349,7 +348,7 @@ func (p *ProjectAPI) ToggleProjectPublic() {
|
||||
return
|
||||
}
|
||||
|
||||
var req projectReq
|
||||
var req *models.ProjectRequest
|
||||
p.DecodeJSONReq(&req)
|
||||
if req.Public != 0 && req.Public != 1 {
|
||||
p.HandleBadRequest("public should be 0 or 1")
|
||||
@ -431,9 +430,9 @@ func (p *ProjectAPI) Logs() {
|
||||
}
|
||||
|
||||
// TODO move this to package models
|
||||
func validateProjectReq(req projectReq) error {
|
||||
pn := req.ProjectName
|
||||
if isIllegalLength(req.ProjectName, projectNameMinLen, projectNameMaxLen) {
|
||||
func validateProjectReq(req *models.ProjectRequest) error {
|
||||
pn := req.Name
|
||||
if isIllegalLength(req.Name, projectNameMinLen, projectNameMaxLen) {
|
||||
return fmt.Errorf("Project name is illegal in length. (greater than 2 or less than 30)")
|
||||
}
|
||||
validProjectName := regexp.MustCompile(`^` + restrictedNameChars + `$`)
|
||||
|
@ -762,19 +762,23 @@ func (ra *RepositoryAPI) VulnerabilityDetails() {
|
||||
ra.HandleForbidden(ra.SecurityCtx.GetUsername())
|
||||
return
|
||||
}
|
||||
res := []*models.VulnerabilityItem{}
|
||||
overview, err := dao.GetImgScanOverview(digest)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("failed to get the scan overview, error: %v", err))
|
||||
return
|
||||
}
|
||||
clairClient := clair.NewClient(config.ClairEndpoint(), nil)
|
||||
log.Debugf("The key for getting details: %s", overview.DetailsKey)
|
||||
details, err := clairClient.GetResult(overview.DetailsKey)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("Failed to get scan details from Clair, error: %v", err))
|
||||
return
|
||||
if overview != nil {
|
||||
clairClient := clair.NewClient(config.ClairEndpoint(), nil)
|
||||
log.Debugf("The key for getting details: %s", overview.DetailsKey)
|
||||
details, err := clairClient.GetResult(overview.DetailsKey)
|
||||
if err != nil {
|
||||
ra.HandleInternalServerError(fmt.Sprintf("Failed to get scan details from Clair, error: %v", err))
|
||||
return
|
||||
}
|
||||
res = transformVulnerabilities(details)
|
||||
}
|
||||
ra.Data["json"] = transformVulnerabilities(details)
|
||||
ra.Data["json"] = res
|
||||
ra.ServeJSON()
|
||||
}
|
||||
|
||||
|
@ -50,7 +50,7 @@ func Init() {
|
||||
// integration with admiral
|
||||
if config.WithAdmiral() {
|
||||
reqCtxModifiers = []ReqCtxModifier{
|
||||
&secretReqCtxModifier{},
|
||||
&secretReqCtxModifier{config.SecretStore},
|
||||
&basicAuthReqCtxModifier{},
|
||||
&tokenReqCtxModifier{},
|
||||
&unauthorizedReqCtxModifier{}}
|
||||
@ -59,7 +59,7 @@ func Init() {
|
||||
|
||||
// standalone
|
||||
reqCtxModifiers = []ReqCtxModifier{
|
||||
&secretReqCtxModifier{},
|
||||
&secretReqCtxModifier{config.SecretStore},
|
||||
&basicAuthReqCtxModifier{},
|
||||
&sessionReqCtxModifier{},
|
||||
&unauthorizedReqCtxModifier{}}
|
||||
|
@ -117,6 +117,10 @@ func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
|
||||
query += fmt.Sprintf("$filter=%s eq '%s'", k, v)
|
||||
}
|
||||
|
||||
if len(query) == 0 {
|
||||
query = "?expand=true"
|
||||
}
|
||||
|
||||
path := "/projects" + query
|
||||
data, err := p.send(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
@ -129,7 +133,6 @@ func (p *ProjectManager) filter(m map[string]string) ([]*project, error) {
|
||||
// parse the response of GET /projects?xxx to project list
|
||||
func parse(b []byte) ([]*project, error) {
|
||||
documents := &struct {
|
||||
//TotalCount int64 `json:"totalCount"`
|
||||
//DocumentCount int64 `json:"documentCount"`
|
||||
Projects map[string]*project `json:"documents"`
|
||||
}{}
|
||||
@ -292,25 +295,10 @@ func (p *ProjectManager) getIDbyHarborIDOrName(projectIDOrName interface{}) (str
|
||||
|
||||
// GetPublic ...
|
||||
func (p *ProjectManager) GetPublic() ([]*models.Project, error) {
|
||||
m := map[string]string{
|
||||
"isPublic": "true",
|
||||
}
|
||||
|
||||
projects, err := p.filter(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
for _, p := range projects {
|
||||
project, err := convert(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, project)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
t := true
|
||||
return p.GetAll(&models.ProjectQueryParam{
|
||||
Public: &t,
|
||||
})
|
||||
}
|
||||
|
||||
// GetByMember ...
|
||||
@ -375,12 +363,37 @@ func (p *ProjectManager) Update(projectIDOrName interface{}, project *models.Pro
|
||||
|
||||
// GetAll ...
|
||||
func (p *ProjectManager) GetAll(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) ([]*models.Project, error) {
|
||||
return nil, errors.New("get all projects is unsupported")
|
||||
m := map[string]string{}
|
||||
if query != nil {
|
||||
if len(query.Name) > 0 {
|
||||
m["name"] = query.Name
|
||||
}
|
||||
if query.Public != nil {
|
||||
m["isPublic"] = strconv.FormatBool(*query.Public)
|
||||
}
|
||||
}
|
||||
|
||||
projects, err := p.filter(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := []*models.Project{}
|
||||
for _, p := range projects {
|
||||
project, err := convert(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, project)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetTotal ...
|
||||
func (p *ProjectManager) GetTotal(query *models.ProjectQueryParam, base ...*models.BaseProjectCollection) (int64, error) {
|
||||
return 0, errors.New("get total of projects is unsupported")
|
||||
projects, err := p.GetAll(query)
|
||||
return int64(len(projects)), err
|
||||
}
|
||||
|
||||
// GetHasReadPerm returns all projects that user has read perm to
|
||||
|
@ -183,11 +183,7 @@ func TestGet(t *testing.T) {
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
// get by invalid input type
|
||||
_, err = pm.Get([]string{})
|
||||
@ -234,11 +230,7 @@ func TestIsPublic(t *testing.T) {
|
||||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
public, err = pm.IsPublic(id)
|
||||
assert.Nil(t, err)
|
||||
@ -255,11 +247,7 @@ func TestIsPublic(t *testing.T) {
|
||||
Public: 0,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
public, err = pm.IsPublic(id)
|
||||
assert.Nil(t, err)
|
||||
@ -289,11 +277,7 @@ func TestExist(t *testing.T) {
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
exist, err = pm.Exist(id)
|
||||
assert.Nil(t, err)
|
||||
@ -322,11 +306,7 @@ func TestGetRoles(t *testing.T) {
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
roles, err = pm.GetRoles("user01", id)
|
||||
assert.Nil(t, err)
|
||||
@ -348,11 +328,7 @@ func TestGetPublic(t *testing.T) {
|
||||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
projects, err = pm.GetPublic()
|
||||
assert.Nil(t, nil)
|
||||
@ -386,11 +362,7 @@ func TestCreate(t *testing.T) {
|
||||
AutomaticallyScanImagesOnPush: true,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
}
|
||||
}(id)
|
||||
defer delete(t, id)
|
||||
|
||||
project, err := pm.Get(id)
|
||||
assert.Nil(t, err)
|
||||
@ -402,6 +374,8 @@ func TestCreate(t *testing.T) {
|
||||
assert.True(t, project.AutomaticallyScanImagesOnPush)
|
||||
}
|
||||
|
||||
// TODO get the case back after Admiral'API is fixed
|
||||
/*
|
||||
func TestDelete(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
|
||||
@ -427,7 +401,7 @@ func TestDelete(t *testing.T) {
|
||||
err = pm.Delete(name)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
*/
|
||||
func TestUpdate(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
err := pm.Update(nil, nil)
|
||||
@ -436,17 +410,94 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
func TestGetAll(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
_, err := pm.GetAll(nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
name1 := "project_for_test_get_all_01"
|
||||
id1, err := pm.Create(&models.Project{
|
||||
Name: name1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer delete(t, id1)
|
||||
|
||||
name2 := "project_for_test_get_all_02"
|
||||
id2, err := pm.Create(&models.Project{
|
||||
Name: name2,
|
||||
Public: 1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer delete(t, id2)
|
||||
|
||||
// no filter
|
||||
projects, err := pm.GetAll(nil)
|
||||
require.Nil(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id1 {
|
||||
found1 = true
|
||||
}
|
||||
if project.ProjectID == id2 {
|
||||
found2 = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found1)
|
||||
assert.True(t, found2)
|
||||
|
||||
// filter by name
|
||||
projects, err = pm.GetAll(&models.ProjectQueryParam{
|
||||
Name: name1,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
found1 = false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id1 {
|
||||
found1 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found1)
|
||||
|
||||
// filter by public
|
||||
value := true
|
||||
projects, err = pm.GetAll(&models.ProjectQueryParam{
|
||||
Public: &value,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
found2 = false
|
||||
for _, project := range projects {
|
||||
if project.ProjectID == id2 {
|
||||
found2 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, found2)
|
||||
}
|
||||
|
||||
func TestGetTotal(t *testing.T) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
_, err := pm.GetTotal(nil)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
total1, err := pm.GetTotal(nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
name := "project_for_test_get_total"
|
||||
id, err := pm.Create(&models.Project{
|
||||
Name: name,
|
||||
})
|
||||
require.Nil(t, err)
|
||||
defer delete(t, id)
|
||||
|
||||
total2, err := pm.GetTotal(nil)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, total1+1, total2)
|
||||
}
|
||||
|
||||
// TODO add test case
|
||||
func TestGetHasReadPerm(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func delete(t *testing.T, id int64) {
|
||||
pm := NewProjectManager(endpoint, token)
|
||||
if err := pm.Delete(id); err != nil {
|
||||
t.Logf("failed to delete project %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ func TestPMSPolicyChecker(t *testing.T) {
|
||||
require.Nil(t, err)
|
||||
defer func(id int64) {
|
||||
if err := pm.Delete(id); err != nil {
|
||||
require.Nil(t, err)
|
||||
t.Logf("failed to delete project %d: %v", id, err)
|
||||
}
|
||||
}(id)
|
||||
project, err := pm.Get(id)
|
||||
|
@ -40,10 +40,10 @@ func initRouters() {
|
||||
beego.Router("/harbor/sign-up", &controllers.IndexController{})
|
||||
beego.Router("/harbor/dashboard", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/repository", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/replication", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/member", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/log", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/repositories", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/replications", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/members", &controllers.IndexController{})
|
||||
beego.Router("/harbor/projects/:id/logs", &controllers.IndexController{})
|
||||
beego.Router("/harbor/tags/:id/*", &controllers.IndexController{})
|
||||
|
||||
beego.Router("/harbor/users", &controllers.IndexController{})
|
||||
|
@ -76,10 +76,12 @@ If **projectId** is set to the id of specified project, then only show the repli
|
||||
<hbr-endpoint></hbr-endpoint>
|
||||
```
|
||||
|
||||
* **Repository and Tag Management View[updating]**
|
||||
* **Repository and Tag Management View**
|
||||
|
||||
**projectId** is used to specify which projects the repositories are from.
|
||||
|
||||
**projectName** is used to generate the related commands for pushing images.
|
||||
|
||||
**hasSignedIn** is a user session related property to determined whether a valid user signed in session existing. This component supports anonymous user.
|
||||
|
||||
**hasProjectAdminRole** is a user session related property to determined whether the current user has project administrator role. Some action menus might be disabled based on this property.
|
||||
@ -87,7 +89,7 @@ If **projectId** is set to the id of specified project, then only show the repli
|
||||
**tagClickEvent** is an @output event emitter for you to catch the tag click events.
|
||||
|
||||
```
|
||||
<hbr-repository-stackview [projectId]="..." [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
|
||||
<hbr-repository-stackview [projectId]="..." [projectName]="" [hasSignedIn]="..." [hasProjectAdminRole]="..." (tagClickEvent)="watchTagClickEvent($event)"></hbr-repository-stackview>
|
||||
|
||||
...
|
||||
|
||||
@ -98,6 +100,19 @@ watchTagClickEvent(tag: Tag): void {
|
||||
|
||||
```
|
||||
|
||||
* **Tag detail view**
|
||||
|
||||
This view is linked by the repository stack view only when the Clair is enabled in Harbor.
|
||||
|
||||
**tagId** is an @Input property and used to specify the tag of which details are displayed.
|
||||
|
||||
**repositoryId** is an @Input property and used to specified the repository to which the tag is belonged.
|
||||
|
||||
**backEvt** is an @Output event emitter and used to distribute the click event of the back arrow in the detail page.
|
||||
|
||||
```
|
||||
<hbr-tag-detail (backEvt)="goBack($event)" [tagId]="..." [repositoryId]="..."></hbr-tag-detail>
|
||||
```
|
||||
## Configurations
|
||||
All the related configurations are defined in the **HarborModuleConfig** interface.
|
||||
|
||||
@ -111,6 +126,7 @@ export const DefaultServiceConfig: IServiceConfig = {
|
||||
targetBaseEndpoint: "/api/targets",
|
||||
replicationRuleEndpoint: "/api/policies/replication",
|
||||
replicationJobEndpoint: "/api/jobs/replication",
|
||||
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||
enablei18Support: false,
|
||||
defaultLang: DEFAULT_LANG, //'en-us'
|
||||
langCookieKey: DEFAULT_LANG_COOKIE_KEY, //'harbor-lang'
|
||||
@ -147,6 +163,8 @@ It supports partially overriding. For the items not overridden, default values w
|
||||
|
||||
* **replicationJobEndpoint:** The base endpoint of the service used to handle the replication jobs. Default is "/api/jobs/replication".
|
||||
|
||||
* **vulnerabilityScanningBaseEndpoint:** The base endpoint of the service used to handle the vulnerability scanning results.Default value is "/api/repositories".
|
||||
|
||||
* **langCookieKey:** The cookie key used to store the current used language preference. Default is "harbor-lang".
|
||||
|
||||
* **supportedLangs:** Declare what languages are supported. Default is ['en-us', 'zh-cn', 'es-es'].
|
||||
@ -215,11 +233,14 @@ HarborLibraryModule.forRoot({
|
||||
...
|
||||
|
||||
```
|
||||
**3. user session(Ongoing/Discussing)**
|
||||
Some components may need the user authorization and authentication information to display different views. There might be two alternatives to select:
|
||||
**3. user session**
|
||||
Some components may need the user authorization and authentication information to display different views. The following way of handing user session is supported by the library.
|
||||
* Use @Input properties or interface to let top component or page to pass the required user session information in.
|
||||
* Component retrieves the required information from some API provided by top component or page when necessary.
|
||||
|
||||
```
|
||||
//In the above repository stack view, the user session informations are passed via @input properties.
|
||||
[hasSignedIn]="..." [hasProjectAdminRole]="..."
|
||||
```
|
||||
**4. services**
|
||||
The library has its own service implementations to communicate with backend APIs and transfer data. If you want to use your own data handling logic, you can implement your own services based on the defined interfaces.
|
||||
|
||||
@ -606,9 +627,9 @@ export class MyScanningResultService extends ScanningResultService {
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
|
||||
...
|
||||
}
|
||||
getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary{
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the detailed vulnerabilities scanning results.
|
||||
@ -619,9 +640,24 @@ export class MyScanningResultService extends ScanningResultService {
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
|
||||
...
|
||||
}
|
||||
getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[]{
|
||||
...
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Start a new vulnerability scanning
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repoName
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
@ -656,4 +692,4 @@ HarborLibraryModule.forRoot({
|
||||
})
|
||||
...
|
||||
|
||||
```
|
||||
```
|
||||
|
@ -1,74 +1,74 @@
|
||||
{
|
||||
"name": "harbor-ui",
|
||||
"version": "0.1.0",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"scripts": {
|
||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"test:once": "karma start karma.conf.js --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor",
|
||||
"cleanup": "rimraf dist",
|
||||
"copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist",
|
||||
"transpile": "ngc -p tsconfig.json",
|
||||
"package": "rollup -c",
|
||||
"minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js",
|
||||
"build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^4.1.0",
|
||||
"@angular/common": "^4.1.0",
|
||||
"@angular/compiler": "^4.1.0",
|
||||
"@angular/core": "^4.1.0",
|
||||
"@angular/forms": "^4.1.0",
|
||||
"@angular/http": "^4.1.0",
|
||||
"@angular/platform-browser": "^4.1.0",
|
||||
"@angular/platform-browser-dynamic": "^4.1.0",
|
||||
"@angular/router": "^4.1.0",
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "^0.9.7",
|
||||
"clarity-icons": "^0.9.7",
|
||||
"clarity-ui": "^0.9.7",
|
||||
"core-js": "^2.4.1",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-clipboard": "^8.0.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"zone.js": "^0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.0.0",
|
||||
"@angular/compiler-cli": "^4.0.1",
|
||||
"@types/core-js": "^0.9.41",
|
||||
"@types/jasmine": "~2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~2.0.0-beta.4",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
"jasmine-core": "2.4.1",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-remap-istanbul": "^0.2.1",
|
||||
"protractor": "^4.0.9",
|
||||
"rollup": "^0.41.6",
|
||||
"ts-node": "1.2.1",
|
||||
"tslint": "^4.1.1",
|
||||
"typescript": "~2.2.0",
|
||||
"typings": "^1.4.0",
|
||||
"uglify-js": "^2.8.22",
|
||||
"webdriver-manager": "10.2.5",
|
||||
"rimraf": "^2.6.1",
|
||||
"copyfiles": "^1.2.0"
|
||||
}
|
||||
}
|
||||
"name": "harbor-ui",
|
||||
"version": "0.2.0",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"scripts": {
|
||||
"start": "ng serve --host 0.0.0.0 --port 4500 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"test:once": "karma start karma.conf.js --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor",
|
||||
"cleanup": "rimraf dist",
|
||||
"copy": "copyfiles -f README.md LICENSE AUTHORS pkg/package.json dist",
|
||||
"transpile": "ngc -p tsconfig.json",
|
||||
"package": "rollup -c",
|
||||
"minify": "uglifyjs dist/bundles/harborui.umd.js --screw-ie8 --compress --mangle --comments --output dist/bundles/harborui.umd.min.js",
|
||||
"build": "npm run cleanup && npm run transpile && npm run package && npm run minify && npm run copy"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~4.1.3",
|
||||
"@angular/common": "~4.1.3",
|
||||
"@angular/compiler": "~4.1.3",
|
||||
"@angular/core": "~4.1.3",
|
||||
"@angular/forms": "~4.1.3",
|
||||
"@angular/http": "~4.1.3",
|
||||
"@angular/platform-browser": "~4.1.3",
|
||||
"@angular/platform-browser-dynamic": "~4.1.3",
|
||||
"@angular/router": "~4.1.3",
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "~0.9.8",
|
||||
"clarity-icons": "~0.9.8",
|
||||
"clarity-ui": "~0.9.8",
|
||||
"core-js": "^2.4.1",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"zone.js": "^0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.0.0",
|
||||
"@angular/compiler-cli": "~4.1.3",
|
||||
"@types/core-js": "^0.9.41",
|
||||
"@types/jasmine": "~2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~2.0.0-beta.4",
|
||||
"copyfiles": "^1.2.0",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
"jasmine-core": "2.4.1",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-remap-istanbul": "^0.2.1",
|
||||
"protractor": "^4.0.9",
|
||||
"rimraf": "^2.6.1",
|
||||
"rollup": "^0.41.6",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"ts-node": "1.2.1",
|
||||
"tslint": "^4.1.1",
|
||||
"typescript": "~2.2.0",
|
||||
"typings": "^1.4.0",
|
||||
"uglify-js": "^2.8.22",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "harbor-ui",
|
||||
"version": "0.1.42",
|
||||
"version": "0.2.0",
|
||||
"description": "Harbor shared UI components based on Clarity and Angular4",
|
||||
"author": "VMware",
|
||||
"module": "index.js",
|
||||
@ -28,18 +28,19 @@
|
||||
"@angular/platform-browser": "^4.0.1",
|
||||
"@angular/platform-browser-dynamic": "^4.0.1",
|
||||
"@angular/router": "^4.0.1",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"clarity-angular": "^0.9.0",
|
||||
"clarity-icons": "^0.9.0",
|
||||
"clarity-ui": "^0.9.0",
|
||||
"core-js": "^2.4.1",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"zone.js": "^0.8.4",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"ngx-cookie": "^1.0.0"
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "^0.9.8",
|
||||
"clarity-icons": "^0.9.8",
|
||||
"clarity-ui": "^0.9.8",
|
||||
"core-js": "^2.4.1",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"zone.js": "^0.8.4"
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import resolve from 'rollup-plugin-node-resolve';
|
||||
|
||||
export default {
|
||||
entry: 'dist/index.js',
|
||||
dest: 'dist/bundles/harborui.umd.js',
|
||||
@ -11,10 +13,13 @@ export default {
|
||||
'@angular/forms',
|
||||
'@angular/platform-browser',
|
||||
'@angular/http',
|
||||
'@angular/router',
|
||||
'clarity-angular',
|
||||
'@ngx-translate/core',
|
||||
'@ngx-translate/http-loader',
|
||||
'ngx-cookie',
|
||||
'rxjs',
|
||||
'rxjs/Rx',
|
||||
'rxjs/Subject',
|
||||
'rxjs/Observable',
|
||||
'rxjs/add/observable/of',
|
||||
@ -29,8 +34,14 @@ export default {
|
||||
'@angular/forms': 'ng.forms',
|
||||
'@angular/http': 'ng.http',
|
||||
'@angular/platform-browser': 'ng.platformBrowser',
|
||||
'@angular/router': 'ng.router',
|
||||
'clarity-angular': 'ng.clarity',
|
||||
'ngx-cookie': 'ngx.cookie',
|
||||
'@ngx-translate/core': 'ngx.translate',
|
||||
'@ngx-translate/http-loader': 'ngx.translate',
|
||||
'rxjs': 'rxjs',
|
||||
'rxjs/Subject': 'rxjs.Subject',
|
||||
'rxjs/Rx': 'Rx',
|
||||
'rxjs/Observable': 'Rx',
|
||||
'rxjs/ReplaySubject': 'Rx',
|
||||
'rxjs/add/operator/map': 'Rx.Observable.prototype',
|
||||
@ -50,5 +61,10 @@ export default {
|
||||
|
||||
// console.warn everything else
|
||||
console.warn(warning.message);
|
||||
}
|
||||
},
|
||||
plugins: [resolve({
|
||||
customResolveOptions: {
|
||||
moduleDirectory: 'node_modules'
|
||||
}
|
||||
})]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{modalTitle}}</h3>
|
||||
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning" *ngIf="!editable">
|
||||
<div class="alert-item">
|
||||
@ -40,7 +40,6 @@ export const CREATE_EDIT_ENDPOINT_TEMPLATE: string = `<clr-modal [(clrModalOpen)
|
||||
<div class="form-group">
|
||||
<label for="spin" class="col-md-4"></label>
|
||||
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
|
||||
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
@ -11,7 +11,13 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ViewChild,
|
||||
AfterViewChecked
|
||||
} from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { EndpointService } from '../service/endpoint.service';
|
||||
@ -35,22 +41,18 @@ const FAKE_PASSWORD = 'rjGcfuRu';
|
||||
@Component({
|
||||
selector: 'create-edit-endpoint',
|
||||
template: CREATE_EDIT_ENDPOINT_TEMPLATE,
|
||||
styles: [ CREATE_EDIT_ENDPOINT_STYLE ]
|
||||
styles: [CREATE_EDIT_ENDPOINT_STYLE]
|
||||
})
|
||||
export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
|
||||
modalTitle: string;
|
||||
createEditDestinationOpened: boolean;
|
||||
|
||||
editable: boolean;
|
||||
|
||||
testOngoing: boolean;
|
||||
pingTestMessage: string;
|
||||
pingStatus: boolean;
|
||||
|
||||
actionType: ActionType;
|
||||
|
||||
target: Endpoint = this.initEndpoint;
|
||||
target: Endpoint = this.initEndpoint;
|
||||
initVal: Endpoint = this.initEndpoint;
|
||||
|
||||
targetForm: NgForm;
|
||||
@ -69,7 +71,7 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
inlineAlert: InlineAlertComponent;
|
||||
|
||||
@Output() reload = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
|
||||
get initEndpoint(): Endpoint {
|
||||
return {
|
||||
@ -84,10 +86,10 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
constructor(
|
||||
private endpointService: EndpointService,
|
||||
private errorHandler: ErrorHandler,
|
||||
private translateService: TranslateService) {}
|
||||
private translateService: TranslateService) { }
|
||||
|
||||
openCreateEditTarget(editable: boolean, targetId?: number | string) {
|
||||
|
||||
|
||||
this.target = this.initEndpoint;
|
||||
this.editable = editable;
|
||||
this.createEditDestinationOpened = true;
|
||||
@ -95,38 +97,32 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
this.endpointHasChanged = false;
|
||||
this.targetNameHasChanged = false;
|
||||
|
||||
this.pingTestMessage = '';
|
||||
this.pingStatus = true;
|
||||
this.testOngoing = false;
|
||||
this.testOngoing = false;
|
||||
|
||||
if(targetId) {
|
||||
if (targetId) {
|
||||
this.actionType = ActionType.EDIT;
|
||||
this.translateService.get('DESTINATION.TITLE_EDIT').subscribe(res=>this.modalTitle=res);
|
||||
this.translateService.get('DESTINATION.TITLE_EDIT').subscribe(res => this.modalTitle = res);
|
||||
toPromise<Endpoint>(this.endpointService
|
||||
.getEndpoint(targetId))
|
||||
.then(
|
||||
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 = FAKE_PASSWORD;
|
||||
this.target.password = this.initVal.password;
|
||||
})
|
||||
.catch(error=>this.errorHandler.error(error));
|
||||
.getEndpoint(targetId))
|
||||
.then(
|
||||
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 = FAKE_PASSWORD;
|
||||
this.target.password = this.initVal.password;
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
} else {
|
||||
this.actionType = ActionType.ADD_NEW;
|
||||
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res=>this.modalTitle=res);
|
||||
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res => this.modalTitle = res);
|
||||
}
|
||||
}
|
||||
|
||||
testConnection() {
|
||||
this.translateService.get('DESTINATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
|
||||
this.pingStatus = true;
|
||||
this.testOngoing = !this.testOngoing;
|
||||
|
||||
let payload: Endpoint = this.initEndpoint;
|
||||
if(this.endpointHasChanged) {
|
||||
if (this.endpointHasChanged) {
|
||||
payload.endpoint = this.target.endpoint;
|
||||
payload.username = this.target.username;
|
||||
payload.password = this.target.password;
|
||||
@ -134,42 +130,41 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
payload.id = this.target.id;
|
||||
}
|
||||
|
||||
this.testOngoing = true;
|
||||
toPromise<Endpoint>(this.endpointService
|
||||
.pingEndpoint(payload))
|
||||
.then(
|
||||
response=>{
|
||||
this.pingStatus = true;
|
||||
this.translateService.get('DESTINATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res);
|
||||
this.testOngoing = !this.testOngoing;
|
||||
}).catch(
|
||||
error=>{
|
||||
this.pingStatus = false;
|
||||
this.translateService.get('DESTINATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res);
|
||||
this.testOngoing = !this.testOngoing;
|
||||
});
|
||||
response => {
|
||||
this.testOngoing = false;
|
||||
this.inlineAlert.showInlineSuccess({ message: "DESTINATION.TEST_CONNECTION_SUCCESS" });
|
||||
}).catch(
|
||||
error => {
|
||||
this.testOngoing = false;
|
||||
this.inlineAlert.showInlineError('DESTINATION.TEST_CONNECTION_FAILURE');
|
||||
});
|
||||
}
|
||||
|
||||
changedTargetName($event: any) {
|
||||
if(this.editable) {
|
||||
if (this.editable) {
|
||||
this.targetNameHasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
clearPassword($event: any) {
|
||||
if(this.editable) {
|
||||
if (this.editable) {
|
||||
this.target.password = '';
|
||||
this.endpointHasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
switch(this.actionType) {
|
||||
case ActionType.ADD_NEW:
|
||||
this.addEndpoint();
|
||||
break;
|
||||
case ActionType.EDIT:
|
||||
this.updateEndpoint();
|
||||
break;
|
||||
switch (this.actionType) {
|
||||
case ActionType.ADD_NEW:
|
||||
this.addEndpoint();
|
||||
break;
|
||||
case ActionType.EDIT:
|
||||
this.updateEndpoint();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,31 +172,31 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
toPromise<number>(this.endpointService
|
||||
.createEndpoint(this.target))
|
||||
.then(
|
||||
response=>{
|
||||
this.translateService.get('DESTINATION.CREATED_SUCCESS')
|
||||
.subscribe(res=>this.errorHandler.info(res));
|
||||
this.createEditDestinationOpened = false;
|
||||
this.reload.emit(true);
|
||||
})
|
||||
response => {
|
||||
this.translateService.get('DESTINATION.CREATED_SUCCESS')
|
||||
.subscribe(res => this.errorHandler.info(res));
|
||||
this.createEditDestinationOpened = false;
|
||||
this.reload.emit(true);
|
||||
})
|
||||
.catch(
|
||||
error=>{
|
||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(res=>{
|
||||
this.errorHandler.error(res);
|
||||
});
|
||||
}
|
||||
error => {
|
||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(res => {
|
||||
this.inlineAlert.showInlineError(res);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateEndpoint() {
|
||||
if(!(this.targetNameHasChanged || this.endpointHasChanged)) {
|
||||
if (!(this.targetNameHasChanged || this.endpointHasChanged)) {
|
||||
this.createEditDestinationOpened = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
let payload: Endpoint = this.initEndpoint;
|
||||
if(this.targetNameHasChanged) {
|
||||
if (this.targetNameHasChanged) {
|
||||
payload.name = this.target.name;
|
||||
delete payload.endpoint;
|
||||
}
|
||||
@ -210,47 +205,47 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
payload.username = this.target.username;
|
||||
payload.password = this.target.password;
|
||||
delete payload.name;
|
||||
}
|
||||
}
|
||||
|
||||
if(!this.target.id) { return; }
|
||||
if (!this.target.id) { return; }
|
||||
toPromise<number>(this.endpointService
|
||||
.updateEndpoint(this.target.id, payload))
|
||||
.then(
|
||||
response=>{
|
||||
this.translateService.get('DESTINATION.UPDATED_SUCCESS')
|
||||
.subscribe(res=>this.errorHandler.info(res));
|
||||
this.createEditDestinationOpened = false;
|
||||
this.reload.emit(true);
|
||||
})
|
||||
response => {
|
||||
this.translateService.get('DESTINATION.UPDATED_SUCCESS')
|
||||
.subscribe(res => this.errorHandler.info(res));
|
||||
this.createEditDestinationOpened = false;
|
||||
this.reload.emit(true);
|
||||
})
|
||||
.catch(
|
||||
error=>{
|
||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(res=>{
|
||||
this.errorHandler.error(res);
|
||||
});
|
||||
}
|
||||
error => {
|
||||
let errorMessageKey = this.handleErrorMessageKey(error.status);
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(res => {
|
||||
this.inlineAlert.showInlineError(res);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleErrorMessageKey(status: number): string {
|
||||
switch(status) {
|
||||
case 409:this
|
||||
switch (status) {
|
||||
case 409: this
|
||||
return 'DESTINATION.CONFLICT_NAME';
|
||||
case 400:
|
||||
return 'DESTINATION.INVALID_NAME';
|
||||
default:
|
||||
return 'UNKNOWN_ERROR';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
if(this.hasChanged) {
|
||||
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
|
||||
if (this.hasChanged) {
|
||||
this.inlineAlert.showInlineConfirmation({ message: 'ALERT.FORM_CHANGE_CONFIRMATION' });
|
||||
} else {
|
||||
this.createEditDestinationOpened = false;
|
||||
if(this.targetForm)
|
||||
if (this.targetForm)
|
||||
this.targetForm.reset();
|
||||
}
|
||||
}
|
||||
@ -262,20 +257,20 @@ export class CreateEditEndpointComponent implements AfterViewChecked {
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
this.targetForm = this.currentForm;
|
||||
if(this.targetForm) {
|
||||
let comparison: {[key: string]: any} = {
|
||||
if (this.targetForm) {
|
||||
let comparison: { [key: string]: any } = {
|
||||
targetName: this.initVal.name,
|
||||
endpointUrl: this.initVal.endpoint,
|
||||
username: this.initVal.username,
|
||||
password: this.initVal.password
|
||||
};
|
||||
let self: CreateEditEndpointComponent | any = this;
|
||||
if(self) {
|
||||
self.targetForm.valueChanges.subscribe((data: any)=>{
|
||||
for(let key in data) {
|
||||
if (self) {
|
||||
self.targetForm.valueChanges.subscribe((data: any) => {
|
||||
for (let key in data) {
|
||||
let current = data[key];
|
||||
let origin: string = comparison[key];
|
||||
if(((this.actionType === ActionType.EDIT && this.editable && !current) || current) &&
|
||||
if (((this.actionType === ActionType.EDIT && this.editable && !current) || current) &&
|
||||
current !== origin) {
|
||||
this.hasChanged = true;
|
||||
break;
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const CREATE_EDIT_RULE_TEMPLATE: string = `
|
||||
<clr-modal [(clrModalOpen)]="createEditRuleOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{modalTitle}}</h3>
|
||||
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
<hbr-inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></hbr-inline-alert>
|
||||
<div class="modal-body" style="max-height: 85vh;">
|
||||
<form #ruleForm="ngForm">
|
||||
<section class="form-block">
|
||||
|
@ -60,6 +60,7 @@ export const DefaultServiceConfig: IServiceConfig = {
|
||||
targetBaseEndpoint: "/api/targets",
|
||||
replicationRuleEndpoint: "/api/policies/replication",
|
||||
replicationJobEndpoint: "/api/jobs/replication",
|
||||
vulnerabilityScanningBaseEndpoint: "/api/repositories",
|
||||
enablei18Support: false,
|
||||
defaultLang: DEFAULT_LANG,
|
||||
langCookieKey: DEFAULT_LANG_COOKIE_KEY,
|
||||
|
@ -13,8 +13,9 @@ export class TranslateServiceInitializer {
|
||||
|
||||
public init(config: i18nConfig = {}): void {
|
||||
let selectedLang: string = config.defaultLang ? config.defaultLang : DEFAULT_LANG;
|
||||
let supportedLangs: string[] = config.supportedLangs ? config.supportedLangs : DEFAULT_SUPPORTING_LANGS;
|
||||
|
||||
this.translateService.addLangs(config.supportedLangs ? config.supportedLangs : DEFAULT_SUPPORTING_LANGS);
|
||||
this.translateService.addLangs(supportedLangs);
|
||||
this.translateService.setDefaultLang(selectedLang);
|
||||
|
||||
if (config.enablei18Support) {
|
||||
@ -25,9 +26,11 @@ export class TranslateServiceInitializer {
|
||||
langSetting = this.translateService.getBrowserCultureLang().toLowerCase();
|
||||
}
|
||||
|
||||
if (config.supportedLangs && config.supportedLangs.length > 0) {
|
||||
if (config.supportedLangs.find(lang => lang === langSetting)) {
|
||||
selectedLang = langSetting;
|
||||
if (langSetting && langSetting.trim() !== "") {
|
||||
if (supportedLangs && supportedLangs.length > 0) {
|
||||
if (supportedLangs.find(lang => lang === langSetting)) {
|
||||
selectedLang = langSetting;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,4 +13,5 @@ export * from './list-replication-rule/index';
|
||||
export * from './replication/index';
|
||||
export * from './vulnerability-scanning/index';
|
||||
export * from './i18n/index';
|
||||
export * from './push-image/index';
|
||||
export * from './push-image/index';
|
||||
export * from './third-party/index';
|
@ -22,7 +22,7 @@ import { INLINE_ALERT_STYLE } from './inline-alert.component.css';
|
||||
import { INLINE_ALERT_TEMPLATE } from './inline-alert.component.html';
|
||||
|
||||
@Component({
|
||||
selector: 'inline-alert',
|
||||
selector: 'hbr-inline-alert',
|
||||
template: INLINE_ALERT_TEMPLATE,
|
||||
styles: [ INLINE_ALERT_STYLE ]
|
||||
})
|
||||
|
@ -12,36 +12,26 @@ import { ErrorHandler } from '../error-handler/index';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
|
||||
import { click } from '../utils';
|
||||
|
||||
describe('RecentLogComponent (inline template)', () => {
|
||||
let component: RecentLogComponent;
|
||||
let fixture: ComponentFixture<RecentLogComponent>;
|
||||
let serviceConfig: IServiceConfig;
|
||||
let logService: AccessLogService;
|
||||
let spy: jasmine.Spy;
|
||||
let mockItems: AccessLogItem[] = [{
|
||||
log_id: 23,
|
||||
user_id: 45,
|
||||
project_id: 11,
|
||||
repo_name: "myproject/",
|
||||
repo_tag: "N/A",
|
||||
operation: "create",
|
||||
op_time: "2017-04-11T10:26:22Z",
|
||||
username: "user91"
|
||||
}, {
|
||||
log_id: 18,
|
||||
user_id: 1,
|
||||
project_id: 5,
|
||||
repo_name: "demo2/vmware/harbor-ui",
|
||||
repo_tag: "0.6",
|
||||
operation: "push",
|
||||
op_time: "2017-03-09T02:29:59Z",
|
||||
username: "admin"
|
||||
}];
|
||||
let mockItems: AccessLogItem[] = [];
|
||||
let mockData: AccessLog = {
|
||||
metadata: {
|
||||
xTotalCount: 2
|
||||
xTotalCount: 18
|
||||
},
|
||||
data: mockItems
|
||||
data: []
|
||||
};
|
||||
let mockData2: AccessLog = {
|
||||
metadata: {
|
||||
xTotalCount: 1
|
||||
},
|
||||
data: []
|
||||
};
|
||||
let testConfig: IServiceConfig = {
|
||||
logBaseEndpoint: "/api/logs/testing"
|
||||
@ -68,8 +58,36 @@ describe('RecentLogComponent (inline template)', () => {
|
||||
serviceConfig = TestBed.get(SERVICE_CONFIG);
|
||||
logService = fixture.debugElement.injector.get(AccessLogService);
|
||||
|
||||
//Mock data
|
||||
for (let i = 0; i < 18; i++) {
|
||||
let item: AccessLogItem = {
|
||||
log_id: 23 + i,
|
||||
user_id: 45 + i,
|
||||
project_id: 11 + i,
|
||||
repo_name: "myproject/demo" + i,
|
||||
repo_tag: "N/A",
|
||||
operation: "create",
|
||||
op_time: "2017-04-11T10:26:22Z",
|
||||
username: "user91" + i
|
||||
};
|
||||
mockItems.push(item);
|
||||
}
|
||||
mockData2.data = mockItems.slice(0, 1);
|
||||
mockData.data = mockItems;
|
||||
|
||||
spy = spyOn(logService, 'getRecentLogs')
|
||||
.and.returnValue(Promise.resolve(mockData));
|
||||
.and.callFake(function (params: RequestQueryParams) {
|
||||
if (params && params.get('repository')) {
|
||||
return Promise.resolve(mockData2);
|
||||
} else {
|
||||
if (params.get('page') == '1') {
|
||||
mockData.data = mockItems.slice(0, 15);
|
||||
} else {
|
||||
mockData.data = mockItems.slice(15, 18)
|
||||
}
|
||||
return Promise.resolve(mockData);
|
||||
}
|
||||
});
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@ -83,54 +101,17 @@ describe('RecentLogComponent (inline template)', () => {
|
||||
expect(serviceConfig.logBaseEndpoint).toEqual("/api/logs/testing");
|
||||
});
|
||||
|
||||
it('should inject and call the AccessLogService', () => {
|
||||
it('should get data from AccessLogService', async(() => {
|
||||
expect(logService).toBeTruthy();
|
||||
expect(spy.calls.any()).toBe(true, 'getRecentLogs called');
|
||||
});
|
||||
|
||||
it('should get data from AccessLogService', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => { // wait for async getRecentLogs
|
||||
fixture.detectChanges();
|
||||
expect(component.recentLogs).toBeTruthy();
|
||||
expect(component.logsCache).toBeTruthy();
|
||||
expect(component.recentLogs.length).toEqual(2);
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support filtering list by keywords', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
component.doFilter('push');
|
||||
fixture.detectChanges();
|
||||
expect(component.recentLogs.length).toEqual(1);
|
||||
let log: AccessLogItem = component.recentLogs[0];
|
||||
expect(log).toBeTruthy();
|
||||
expect(log.username).toEqual('admin');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support refreshing', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
component.doFilter('push');
|
||||
fixture.detectChanges();
|
||||
expect(component.recentLogs.length).toEqual(1);
|
||||
});
|
||||
|
||||
component.refresh();
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.recentLogs.length).toEqual(1);
|
||||
expect(component.recentLogs.length).toEqual(15);
|
||||
});
|
||||
}));
|
||||
|
||||
@ -143,8 +124,95 @@ describe('RecentLogComponent (inline template)', () => {
|
||||
expect(de).toBeTruthy();
|
||||
let el: HTMLElement = de.nativeElement;
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('user91');
|
||||
expect(el.textContent.trim()).toEqual('user910');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support pagination', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
|
||||
expect(el).toBeTruthy();
|
||||
el.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
|
||||
expect(els).toBeTruthy();
|
||||
expect(els.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support filtering list by keywords', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.search-btn');
|
||||
expect(el).toBeTruthy("Not found search icon");
|
||||
click(el);
|
||||
|
||||
fixture.detectChanges();
|
||||
let el2: HTMLInputElement = fixture.nativeElement.querySelector('input');
|
||||
expect(el2).toBeTruthy("Not found input");
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
component.doFilter("demo0");
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
|
||||
expect(els).toBeTruthy();
|
||||
expect(els.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should support refreshing', async(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let el: HTMLButtonElement = fixture.nativeElement.querySelector('.pagination-next');
|
||||
expect(el).toBeTruthy();
|
||||
el.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
|
||||
expect(els).toBeTruthy();
|
||||
expect(els.length).toEqual(4)
|
||||
|
||||
let refreshEl: HTMLElement = fixture.nativeElement.querySelector(".refresh-btn");
|
||||
expect(refreshEl).toBeTruthy("Not found refresh button");
|
||||
refreshEl.click();
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
|
||||
let els: HTMLElement[] = fixture.nativeElement.querySelectorAll('.datagrid-row');
|
||||
expect(els).toBeTruthy();
|
||||
expect(els.length).toEqual(16);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -41,7 +41,9 @@ export class RecentLogComponent implements OnInit {
|
||||
@Input() withTitle: boolean = false;
|
||||
|
||||
pageSize: number = DEFAULT_PAGE_SIZE;
|
||||
currentPage: number = 0;
|
||||
currentPage: number = 1;//Double bound to pagination component
|
||||
currentPagePvt: number = 0; //Used to confirm whether page is changed
|
||||
currentState: State;
|
||||
|
||||
opTimeComparator: Comparator<AccessLogItem> = new CustomComparator<AccessLogItem>('op_time', 'date');
|
||||
|
||||
@ -61,28 +63,45 @@ export class RecentLogComponent implements OnInit {
|
||||
}
|
||||
|
||||
public doFilter(terms: string): void {
|
||||
if (terms.trim() === "") {
|
||||
//Clear search results
|
||||
this.recentLogs = this.logsCache.data.filter(log => log.username != "");
|
||||
return;
|
||||
this.currentTerm = terms.trim();
|
||||
//Trigger data loading and start from first page
|
||||
this.loading = true;
|
||||
this.currentPage = 1;
|
||||
if (this.currentPagePvt === 1) {
|
||||
//Force reloading
|
||||
let st: State = this.currentState;
|
||||
if (!st) {
|
||||
st = {
|
||||
page: {}
|
||||
};
|
||||
}
|
||||
st.page.from = 0;
|
||||
st.page.to = this.pageSize - 1;
|
||||
st.page.size = this.pageSize;
|
||||
|
||||
this.currentPagePvt = 0;//Reset pvt
|
||||
|
||||
this.load(st);
|
||||
}
|
||||
this.currentTerm = terms;
|
||||
this.recentLogs = this.logsCache.data.filter(log => this.isMatched(terms, log));
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.currentTerm = "";
|
||||
this.currentPage = 0;
|
||||
this.load({});
|
||||
this.doFilter("");
|
||||
}
|
||||
|
||||
load(state: State) {
|
||||
//Keep it for future filter
|
||||
this.currentState = state;
|
||||
|
||||
let pageNumber: number = this._calculatePage(state);
|
||||
if (pageNumber !== this.currentPage) {
|
||||
if (pageNumber !== this.currentPagePvt) {
|
||||
//load data
|
||||
let params: RequestQueryParams = new RequestQueryParams();
|
||||
params.set("page", '' + pageNumber);
|
||||
params.set("page_size", '' + this.pageSize);
|
||||
if (this.currentTerm && this.currentTerm !== "") {
|
||||
params.set('repository', this.currentTerm);
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
toPromise<AccessLog>(this.logService.getRecentLogs(params))
|
||||
@ -96,7 +115,7 @@ export class RecentLogComponent implements OnInit {
|
||||
//Do customized sorting
|
||||
this._doSorting(state);
|
||||
|
||||
this.currentPage = pageNumber;
|
||||
this.currentPagePvt = pageNumber;
|
||||
|
||||
this.loading = false;
|
||||
})
|
||||
@ -105,6 +124,8 @@ export class RecentLogComponent implements OnInit {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
} else {
|
||||
//Column sorting and filtering
|
||||
|
||||
this.recentLogs = this.logsCache.data.filter(log => log.username != "");//Reset data
|
||||
|
||||
//Do customized filter
|
||||
|
@ -33,7 +33,7 @@ export const LOG_TEMPLATE: string = `
|
||||
<clr-dg-footer>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
of {{pagination.totalItems}} {{'AUDIT_LOG.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||
<clr-dg-pagination #pagination [(clrDgPage)]="currentPage" [clrDgPageSize]="pageSize" [clrDgTotalItems]="totalCount"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
|
@ -39,4 +39,8 @@ export const PUSH_IMAGE_STYLE: string = `
|
||||
min-width: 360px;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.btn-font {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
`;
|
@ -1,7 +1,7 @@
|
||||
export const PUSH_IMAGE_HTML: string = `
|
||||
<div>
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-right'">
|
||||
<button class="btn btn-link" clrDropdownToggle (click)="onclick()">
|
||||
<button class="btn btn-link btn-font" clrDropdownToggle (click)="onclick()">
|
||||
{{ 'PUSH_IMAGE.TITLE' | translate | uppercase}}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
@ -10,14 +10,16 @@ export const PUSH_IMAGE_HTML: string = `
|
||||
<section>
|
||||
<span><h5 class="h5-override">{{ 'PUSH_IMAGE.TITLE' | translate }}</h5></span>
|
||||
<span>
|
||||
<a href="javascript:void(0)" role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<span class="tooltip-content">{{ 'PUSH_IMAGE.TOOLTIP' | translate }}</span>
|
||||
</a>
|
||||
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'md'">
|
||||
<clr-icon shape="info-circle" class="info-tips-icon" size="24"></clr-icon>
|
||||
<clr-tooltip-content>
|
||||
{{ 'PUSH_IMAGE.TOOLTIP' | translate }}
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
</span>
|
||||
</section>
|
||||
<section>
|
||||
<inline-alert #copyAlert></inline-alert>
|
||||
<hbr-inline-alert #copyAlert></hbr-inline-alert>
|
||||
</section>
|
||||
<section>
|
||||
<article class="commands-section">
|
||||
|
@ -9,7 +9,7 @@ export const REPLICATION_STYLE: string = `
|
||||
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 12px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
|
@ -1,13 +1,13 @@
|
||||
export const REPLICATION_TEMPLATE: string = `
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between" style="height:24px;">
|
||||
<div class="row flex-items-xs-between" style="height:32px;">
|
||||
<div class="flex-xs-middle option-left">
|
||||
<button *ngIf="projectId" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
|
||||
<create-edit-rule [projectId]="projectId" (reload)="reloadRules($event)"></create-edit-rule>
|
||||
</div>
|
||||
<div class="flex-xs-middle option-right">
|
||||
<div class="select" style="float: left; top: 9px;">
|
||||
<div class="select" style="float: left; top: 8px;">
|
||||
<select (change)="doFilterRuleStatus($event)">
|
||||
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
|
||||
</select>
|
||||
|
@ -1,9 +1,10 @@
|
||||
export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="height: 24px;">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="height: 32px;">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<hbr-push-image-button style="display: inline-block;" [registryUrl]="registryUrl" [projectName]="projectName"></hbr-push-image-button>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder="{{'REPOSITORY.FILTER_FOR_REPOSITORIES' | translate}}" (filter)="doSearchRepoNames($event)"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></span>
|
||||
</div>
|
||||
@ -21,7 +22,7 @@ export const REPOSITORY_STACKVIEW_TEMPLATE: string = `
|
||||
<clr-dg-cell>{{r.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.tags_count}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{r.pull_count}}</clr-dg-cell>
|
||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
||||
<hbr-tag *clrIfExpanded ngProjectAs="clr-dg-row-detail" (tagClickEvent)="watchTagClickEvt($event)" class="sub-grid-custom" [repoName]="r.name" [registryUrl]="registryUrl" [withNotary]="withNotary" [withClair]="withClair" [hasSignedIn]="hasSignedIn" [hasProjectAdminRole]="hasProjectAdminRole" [projectId]="projectId" [isEmbedded]="true" (refreshRepo)="refresh($event)"></hbr-tag>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
||||
|
@ -14,6 +14,9 @@ import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { RepositoryService, RepositoryDefaultService } from '../service/repository.service';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
import { SystemInfoService, SystemInfoDefaultService } from '../service/system-info.service';
|
||||
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||
import { PUSH_IMAGE_BUTTON_DIRECTIVES } from '../push-image/index';
|
||||
import { INLINE_ALERT_DIRECTIVES } from '../inline-alert/index';
|
||||
|
||||
import { click } from '../utils';
|
||||
|
||||
@ -90,7 +93,10 @@ describe('RepositoryComponentStackview (inline template)', () => {
|
||||
RepositoryStackviewComponent,
|
||||
TagComponent,
|
||||
ConfirmationDialogComponent,
|
||||
FilterComponent
|
||||
FilterComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
PUSH_IMAGE_BUTTON_DIRECTIVES,
|
||||
INLINE_ALERT_DIRECTIVES
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
|
@ -30,7 +30,7 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
import { Tag } from '../service/interface';
|
||||
import { Tag, TagClickEvent } from '../service/interface';
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-repository-stackview',
|
||||
@ -41,10 +41,11 @@ import { Tag } from '../service/interface';
|
||||
export class RepositoryStackviewComponent implements OnInit {
|
||||
|
||||
@Input() projectId: number;
|
||||
@Input() projectName: string = "unknown";
|
||||
|
||||
@Input() hasSignedIn: boolean;
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Output() tagClickEvent = new EventEmitter<Tag>();
|
||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||
|
||||
lastFilteredRepoName: string;
|
||||
repositories: Repository[];
|
||||
@ -72,6 +73,10 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
return this.systemInfo ? this.systemInfo.with_notary : false;
|
||||
}
|
||||
|
||||
public get withClair(): boolean {
|
||||
return this.systemInfo ? this.systemInfo.with_clair : false;
|
||||
}
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.REPOSITORY &&
|
||||
@ -132,7 +137,7 @@ export class RepositoryStackviewComponent implements OnInit {
|
||||
this.retrieve();
|
||||
}
|
||||
|
||||
watchTagClickEvt(tag: Tag): void {
|
||||
this.tagClickEvent.emit(tag);
|
||||
watchTagClickEvt(tagClickEvt: TagClickEvent): void {
|
||||
this.tagClickEvent.emit(tagClickEvt);
|
||||
}
|
||||
}
|
@ -45,7 +45,7 @@ export interface Tag extends Base {
|
||||
author: string;
|
||||
created: Date;
|
||||
signature?: string;
|
||||
vulnerability?: VulnerabilitySummary;
|
||||
scan_overview?: VulnerabilitySummary;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -145,6 +145,7 @@ export interface AccessLogItem {
|
||||
*
|
||||
*/
|
||||
export interface SystemInfo {
|
||||
with_clair?: boolean;
|
||||
with_notary?: boolean;
|
||||
with_admiral?: boolean;
|
||||
admiral_endpoint?: string;
|
||||
@ -156,9 +157,8 @@ export interface SystemInfo {
|
||||
harbor_version?: string;
|
||||
}
|
||||
|
||||
//Not finalized yet
|
||||
export enum VulnerabilitySeverity {
|
||||
NONE, UNKNOWN, LOW, MEDIUM, HIGH
|
||||
_SEVERITY, NONE, UNKNOWN, LOW, MEDIUM, HIGH
|
||||
}
|
||||
|
||||
export interface VulnerabilityBase {
|
||||
@ -170,16 +170,31 @@ export interface VulnerabilityBase {
|
||||
|
||||
export interface VulnerabilityItem extends VulnerabilityBase {
|
||||
fixedVersion: string;
|
||||
layer: string;
|
||||
layer?: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface VulnerabilitySummary {
|
||||
total_package: number;
|
||||
package_with_none: number;
|
||||
package_with_high?: number;
|
||||
package_with_medium?: number;
|
||||
package_With_low?: number;
|
||||
package_with_unknown?: number;
|
||||
complete_timestamp: Date;
|
||||
image_digest?: string;
|
||||
scan_status: string;
|
||||
job_id?: number;
|
||||
severity: VulnerabilitySeverity;
|
||||
components: VulnerabilityComponents;
|
||||
update_time: Date; //Use as complete timestamp
|
||||
}
|
||||
|
||||
export interface VulnerabilityComponents {
|
||||
total: number;
|
||||
summary: VulnerabilitySeverityMetrics[];
|
||||
}
|
||||
|
||||
export interface VulnerabilitySeverityMetrics {
|
||||
severity: VulnerabilitySeverity;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface TagClickEvent {
|
||||
project_id: string | number;
|
||||
repository_name: string;
|
||||
tag_name: string;
|
||||
}
|
@ -3,7 +3,8 @@ import 'rxjs/add/observable/of';
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { Http, URLSearchParams } from '@angular/http';
|
||||
import { HTTP_JSON_OPTIONS } from '../utils';
|
||||
import { buildHttpRequestOptions, HTTP_JSON_OPTIONS } from '../utils';
|
||||
import { RequestQueryParams } from './RequestQueryParams';
|
||||
|
||||
import {
|
||||
VulnerabilityItem,
|
||||
@ -27,7 +28,7 @@ export abstract class ScanningResultService {
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
|
||||
abstract getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary;
|
||||
|
||||
/**
|
||||
* Get the detailed vulnerabilities scanning results.
|
||||
@ -38,30 +39,60 @@ export abstract class ScanningResultService {
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
|
||||
abstract getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[];
|
||||
|
||||
|
||||
/**
|
||||
* Start a new vulnerability scanning
|
||||
*
|
||||
* @abstract
|
||||
* @param {string} repoName
|
||||
* @param {string} tagId
|
||||
* @returns {(Observable<any> | Promise<any> | any)}
|
||||
*
|
||||
* @memberOf ScanningResultService
|
||||
*/
|
||||
abstract startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ScanningResultDefaultService extends ScanningResultService {
|
||||
_baseUrl: string = '/api/repositories';
|
||||
|
||||
constructor(
|
||||
private http: Http,
|
||||
@Inject(SERVICE_CONFIG) private config: IServiceConfig) {
|
||||
super();
|
||||
if (this.config && this.config.vulnerabilityScanningBaseEndpoint) {
|
||||
this._baseUrl = this.config.vulnerabilityScanningBaseEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
getVulnerabilityScanningSummary(tagId: string): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
|
||||
if (!tagId || tagId.trim() === '') {
|
||||
getVulnerabilityScanningSummary(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilitySummary> | Promise<VulnerabilitySummary> | VulnerabilitySummary {
|
||||
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
|
||||
return Observable.of({});
|
||||
}
|
||||
|
||||
getVulnerabilityScanningResults(tagId: string): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
|
||||
if (!tagId || tagId.trim() === '') {
|
||||
getVulnerabilityScanningResults(repoName: string, tagId: string, queryParams?: RequestQueryParams): Observable<VulnerabilityItem[]> | Promise<VulnerabilityItem[]> | VulnerabilityItem[] {
|
||||
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
|
||||
return Observable.of([]);
|
||||
return this.http.get(`${this._baseUrl}/${repoName}/tags/${tagId}/vulnerability/details`, buildHttpRequestOptions(queryParams)).toPromise()
|
||||
.then(response => response.json() as VulnerabilityItem[])
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
|
||||
startVulnerabilityScanning(repoName: string, tagId: string): Observable<any> | Promise<any> | any {
|
||||
if (!repoName || repoName.trim() === '' || !tagId || tagId.trim() === '') {
|
||||
return Promise.reject('Bad argument');
|
||||
}
|
||||
|
||||
return this.http.post(`${this._baseUrl}/${repoName}/tags/${tagId}/scan`, null).toPromise()
|
||||
.then(() => { return true })
|
||||
.catch(error => Promise.reject(error));
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { TranslatorJsonLoader } from '../i18n/local-json.loader';
|
||||
import { IServiceConfig, SERVICE_CONFIG } from '../service.config';
|
||||
import { CookieService, CookieModule } from 'ngx-cookie';
|
||||
import { ClipboardModule } from 'ngx-clipboard';
|
||||
import { ClipboardModule } from '../third-party/ngx-clipboard/index';
|
||||
|
||||
/*export function HttpLoaderFactory(http: Http) {
|
||||
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
|
||||
|
@ -7,10 +7,10 @@ export const TAG_DETAIL_HTML: string = `
|
||||
</div>
|
||||
<div class="title-block">
|
||||
<div class="tag-name">
|
||||
{{tagDetails.name}}:v{{tagDetails.docker_version}}
|
||||
{{tagDetails.name}}
|
||||
</div>
|
||||
<div class="tag-timestamp">
|
||||
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{tagDetails.author}}
|
||||
{{'TAG.CREATION_TIME_PREFIX' | translate }} {{tagDetails.created | date }} {{'TAG.CREATOR_PREFIX' | translate }} {{author | translate}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,11 +23,13 @@ export const TAG_DETAIL_HTML: string = `
|
||||
<div class="image-detail-label">
|
||||
<div>{{'TAG.ARCHITECTURE' | translate }}</div>
|
||||
<div>{{'TAG.OS' | translate }}</div>
|
||||
<div>{{'TAG.DOCKER_VERSION' | translate }}</div>
|
||||
<div>{{'TAG.SCAN_COMPLETION_TIME' | translate }}</div>
|
||||
</div>
|
||||
<div class="image-detail-value">
|
||||
<div>{{tagDetails.architecture}}</div>
|
||||
<div>{{tagDetails.os}}</div>
|
||||
<div>{{tagDetails.docker_version}}</div>
|
||||
<div>{{scanCompletedDatetime | date}}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,8 +48,8 @@ export const TAG_DETAIL_HTML: string = `
|
||||
</div>
|
||||
</div>
|
||||
<div class="second-column">
|
||||
<div>{{highCount}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}</div>
|
||||
<div class="second-row">{{mediumCount}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}</div>
|
||||
<div>{{highCount}} {{getPackageText(highCount) | translate}} {{'VULNERABILITY.SEVERITY.HIGH' | translate }} {{suffixForHigh | translate }}</div>
|
||||
<div class="second-row">{{mediumCount}} {{getPackageText(mediumCount) | translate}} {{'VULNERABILITY.SEVERITY.MEDIUM' | translate }} {{suffixForMedium | translate }}</div>
|
||||
</div>
|
||||
<div class="third-column">
|
||||
<div>
|
||||
@ -58,8 +60,8 @@ export const TAG_DETAIL_HTML: string = `
|
||||
</div>
|
||||
</div>
|
||||
<div class="fourth-column">
|
||||
<div>{{lowCount}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}</div>
|
||||
<div class="second-row">{{unknownCount}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}</div>
|
||||
<div>{{lowCount}} {{getPackageText(lowCount) | translate}} {{'VULNERABILITY.SEVERITY.LOW' | translate }} {{suffixForLow | translate }}</div>
|
||||
<div class="second-row">{{unknownCount}} {{getPackageText(unknownCount) | translate}} {{'VULNERABILITY.SEVERITY.UNKNOWN' | translate }} {{suffixForUnknown | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,7 +69,7 @@ export const TAG_DETAIL_HTML: string = `
|
||||
</section>
|
||||
<section class="detail-section">
|
||||
<div class="vulnerability-block">
|
||||
<hbr-vulnerabilities-grid tagId="tagId"></hbr-vulnerabilities-grid>
|
||||
<hbr-vulnerabilities-grid [repositoryId]="repositoryId" [tagId]="tagId"></hbr-vulnerabilities-grid>
|
||||
</div>
|
||||
<div>
|
||||
<ng-content></ng-content>
|
||||
|
@ -5,25 +5,40 @@ import { ResultGridComponent } from '../vulnerability-scanning/result-grid.compo
|
||||
import { TagDetailComponent } from './tag-detail.component';
|
||||
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Tag, VulnerabilitySummary } from '../service/interface';
|
||||
import { Tag, VulnerabilitySummary, VulnerabilityItem, VulnerabilitySeverity } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||
import { FilterComponent } from '../filter/index';
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
|
||||
describe('TagDetailComponent (inline template)', () => {
|
||||
|
||||
let comp: TagDetailComponent;
|
||||
let fixture: ComponentFixture<TagDetailComponent>;
|
||||
let tagService: TagService;
|
||||
let scanningService: ScanningResultService;
|
||||
let spy: jasmine.Spy;
|
||||
let vulSpy: jasmine.Spy;
|
||||
let mockVulnerability: VulnerabilitySummary = {
|
||||
total_package: 124,
|
||||
package_with_none: 92,
|
||||
package_with_high: 10,
|
||||
package_with_medium: 6,
|
||||
package_With_low: 13,
|
||||
package_with_unknown: 3,
|
||||
complete_timestamp: new Date()
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.finished,
|
||||
severity: 5,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 124,
|
||||
summary: [{
|
||||
severity: 1,
|
||||
count: 90
|
||||
}, {
|
||||
severity: 3,
|
||||
count: 10
|
||||
}, {
|
||||
severity: 4,
|
||||
count: 10
|
||||
}, {
|
||||
severity: 5,
|
||||
count: 13
|
||||
}]
|
||||
}
|
||||
};
|
||||
let mockTag: Tag = {
|
||||
"digest": "sha256:e5c82328a509aeb7c18c1d7fb36633dc638fcf433f651bdcda59c1cc04d3ee55",
|
||||
@ -34,7 +49,7 @@ describe('TagDetailComponent (inline template)', () => {
|
||||
"author": "steven",
|
||||
"created": new Date("2016-11-08T22:41:15.912313785Z"),
|
||||
"signature": null,
|
||||
vulnerability: mockVulnerability
|
||||
scan_overview: mockVulnerability
|
||||
};
|
||||
|
||||
let config: IServiceConfig = {
|
||||
@ -70,6 +85,22 @@ describe('TagDetailComponent (inline template)', () => {
|
||||
tagService = fixture.debugElement.injector.get(TagService);
|
||||
spy = spyOn(tagService, 'getTag').and.returnValues(Promise.resolve(mockTag));
|
||||
|
||||
let mockData: VulnerabilityItem[] = [];
|
||||
for (let i = 0; i < 30; i++) {
|
||||
let res: VulnerabilityItem = {
|
||||
id: "CVE-2016-" + (8859 + i),
|
||||
severity: i % 2 === 0 ? VulnerabilitySeverity.HIGH : VulnerabilitySeverity.MEDIUM,
|
||||
package: "package_" + i,
|
||||
layer: "layer_" + i,
|
||||
version: '4.' + i + ".0",
|
||||
fixedVersion: '4.' + i + '.11',
|
||||
description: "Mock data"
|
||||
};
|
||||
mockData.push(res);
|
||||
}
|
||||
scanningService = fixture.debugElement.injector.get(ScanningResultService);
|
||||
vulSpy = spyOn(scanningService, 'getVulnerabilityScanningResults').and.returnValue(Promise.resolve(mockData));
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@ -85,7 +116,7 @@ describe('TagDetailComponent (inline template)', () => {
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.tag-name');
|
||||
expect(el).toBeTruthy();
|
||||
expect(el.textContent.trim()).toEqual('nginx:v1.12.3');
|
||||
expect(el.textContent.trim()).toEqual('nginx');
|
||||
});
|
||||
}));
|
||||
|
||||
@ -113,7 +144,7 @@ describe('TagDetailComponent (inline template)', () => {
|
||||
expect(el).toBeTruthy();
|
||||
let el2: HTMLElement = el.querySelector('div');
|
||||
expect(el2).toBeTruthy();
|
||||
expect(el2.textContent.trim()).toEqual("10 VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
|
||||
expect(el2.textContent.trim()).toEqual("13 VULNERABILITY.PACKAGES VULNERABILITY.SEVERITY.HIGH VULNERABILITY.PLURAL");
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
|
||||
import { TAG_DETAIL_STYLES } from './tag-detail.component.css';
|
||||
import { TAG_DETAIL_HTML } from './tag-detail.component.html';
|
||||
|
||||
import { TagService, Tag } from '../service/index';
|
||||
import { TagService, Tag, VulnerabilitySeverity } from '../service/index';
|
||||
import { toPromise } from '../utils';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
|
||||
@ -15,6 +15,11 @@ import { ErrorHandler } from '../error-handler/index';
|
||||
providers: []
|
||||
})
|
||||
export class TagDetailComponent implements OnInit {
|
||||
_highCount: number = 0;
|
||||
_mediumCount: number = 0;
|
||||
_lowCount: number = 0;
|
||||
_unknownCount: number = 0;
|
||||
|
||||
@Input() tagId: string;
|
||||
@Input() repositoryId: string;
|
||||
tagDetails: Tag = {
|
||||
@ -36,7 +41,32 @@ export class TagDetailComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
if (this.repositoryId && this.tagId) {
|
||||
toPromise<Tag>(this.tagService.getTag(this.repositoryId, this.tagId))
|
||||
.then(response => this.tagDetails = response)
|
||||
.then(response => {
|
||||
this.tagDetails = response;
|
||||
if (this.tagDetails &&
|
||||
this.tagDetails.scan_overview &&
|
||||
this.tagDetails.scan_overview.components &&
|
||||
this.tagDetails.scan_overview.components.summary) {
|
||||
this.tagDetails.scan_overview.components.summary.forEach(item => {
|
||||
switch (item.severity) {
|
||||
case VulnerabilitySeverity.UNKNOWN:
|
||||
this._unknownCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.LOW:
|
||||
this._lowCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
this._mediumCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
this._highCount += item.count;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error))
|
||||
}
|
||||
}
|
||||
@ -45,29 +75,33 @@ export class TagDetailComponent implements OnInit {
|
||||
this.backEvt.emit(this.tagId);
|
||||
}
|
||||
|
||||
getPackageText(count: number): string {
|
||||
return count > 1 ? "VULNERABILITY.PACKAGES" : "VULNERABILITY.PACKAGE";
|
||||
}
|
||||
|
||||
public get author(): string {
|
||||
return this.tagDetails && this.tagDetails.author ? this.tagDetails.author : 'TAG.ANONYMITY';
|
||||
}
|
||||
|
||||
public get highCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_with_high : 0;
|
||||
return this._highCount;
|
||||
}
|
||||
|
||||
public get mediumCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_with_medium : 0;
|
||||
return this._mediumCount;
|
||||
}
|
||||
|
||||
public get lowCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_With_low : 0;
|
||||
return this._lowCount;
|
||||
}
|
||||
|
||||
public get unknownCount(): number {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.package_with_unknown : 0;
|
||||
return this._unknownCount;
|
||||
}
|
||||
|
||||
public get scanCompletedDatetime(): Date {
|
||||
return this.tagDetails && this.tagDetails.vulnerability ?
|
||||
this.tagDetails.vulnerability.complete_timestamp : new Date();
|
||||
return this.tagDetails && this.tagDetails.scan_overview ?
|
||||
this.tagDetails.scan_overview.update_time : new Date();
|
||||
}
|
||||
|
||||
public get suffixForHigh(): string {
|
||||
|
@ -30,4 +30,16 @@ export const TAG_STYLE = `
|
||||
:host >>> .datagrid .datagrid-body .datagrid-row-master {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.truncated {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow:ellipsis;
|
||||
}
|
||||
|
||||
.copy-failed {
|
||||
color: red;
|
||||
margin-right: 6px;
|
||||
}
|
||||
`;
|
@ -4,33 +4,38 @@ export const TAG_TEMPLATE = `
|
||||
<h3 class="modal-title">{{ manifestInfoTitle | translate }}</h3>
|
||||
<div class="modal-body">
|
||||
<div class="row col-md-12">
|
||||
<textarea rows="3" (click)="selectAndCopy($event)">{{digestId}}</textarea>
|
||||
<textarea rows="2" #digestTarget>{{digestId}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" (click)="showTagManifestOpened = false">{{'BUTTON.OK' | translate}}</button>
|
||||
<span class="copy-failed" [hidden]="!copyFailed">{{'TAG.COPY_ERROR' | translate}}</span>
|
||||
<button type="button" class="btn btn-primary" [ngxClipboard]="digestTarget" (cbOnSuccess)="onSuccess($event)" (cbOnError)="onError($event)">{{'BUTTON.COPY' | translate}}</button>
|
||||
</div>
|
||||
</clr-modal>
|
||||
|
||||
<h2 *ngIf="!isEmbedded" class="sub-header-title">{{repoName}}</h2>
|
||||
<clr-datagrid [clrDgLoading]="loading" [class.embeded-datagrid]="isEmbedded">
|
||||
<clr-dg-column [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
||||
<clr-dg-column *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'docker_version'">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'architecture'">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'os'">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 80px;" [clrDgField]="'name'">{{'REPOSITORY.TAG' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="min-width: 180px;">{{'REPOSITORY.PULL_COMMAND' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 80px;" *ngIf="withNotary">{{'REPOSITORY.SIGNED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 150px;" *ngIf="withClair">{{'VULNERABILITY.SINGULAR' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 100px;">{{'REPOSITORY.AUTHOR' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 160px;"[clrDgSortBy]="createdComparator">{{'REPOSITORY.CREATED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 80px;" [clrDgField]="'docker_version'" *ngIf="!withClair">{{'REPOSITORY.DOCKER_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 80px;" [clrDgField]="'architecture'" *ngIf="!withClair">{{'REPOSITORY.ARCHITECTURE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column style="width: 80px;" [clrDgField]="'os'" *ngIf="!withClair">{{'REPOSITORY.OS' | translate}}</clr-dg-column>
|
||||
<clr-dg-placeholder>{{'TGA.PLACEHOLDER' | translate }}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let t of tags" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showDigestId(t)">{{'REPOSITORY.COPY_DIGEST_ID' | translate}}</button>
|
||||
<button class="action-item" [hidden]="!hasProjectAdminRole" (click)="deleteTag(t)">{{'REPOSITORY.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell><a href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a></clr-dg-cell>
|
||||
<clr-dg-cell>docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell *ngIf="withNotary" [ngSwitch]="t.signature !== null">
|
||||
<clr-dg-cell style="width: 80px;" [ngSwitch]="withClair">
|
||||
<a *ngSwitchCase="true" href="javascript:void(0)" (click)="onTagClick(t)">{{t.name}}</a>
|
||||
<span *ngSwitchDefault>{{t.name}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell style="min-width: 180px;" class="truncated" title="docker pull {{registryUrl}}/{{repoName}}:{{t.name}}">docker pull {{registryUrl}}/{{repoName}}:{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 80px;" *ngIf="withNotary" [ngSwitch]="t.signature !== null">
|
||||
<clr-icon shape="check" *ngSwitchCase="true" style="color: #1D5100;"></clr-icon>
|
||||
<clr-icon shape="close" *ngSwitchCase="false" style="color: #C92100;"></clr-icon>
|
||||
<a href="javascript:void(0)" *ngSwitchDefault role="tooltip" aria-haspopup="true" class="tooltip tooltip-top-right">
|
||||
@ -38,15 +43,18 @@ export const TAG_TEMPLATE = `
|
||||
<span class="tooltip-content">{{'REPOSITORY.NOTARY_IS_UNDETERMINED' | translate}}</span>
|
||||
</a>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.author}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.created | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.docker_version}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.architecture}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.os}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 150px;" *ngIf="withClair">
|
||||
<hbr-vulnerability-bar [tagId]="t.name" [summary]="t.scan_overview" (startScanning)="scanTag($event)"></hbr-vulnerability-bar>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 100px;">{{t.author}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 160px;">{{t.created | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.docker_version}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.architecture}}</clr-dg-cell>
|
||||
<clr-dg-cell style="width: 80px;" *ngIf="!withClair">{{t.os}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} {{'REPOSITORY.OF' | translate}}
|
||||
{{pagination.totalItems}} {{'REPOSITORY.ITEMS' | translate}}
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="5"></clr-dg-pagination>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>`;
|
@ -1,4 +1,4 @@
|
||||
import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { ComponentFixture, TestBed, async, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { DebugElement } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
@ -10,10 +10,14 @@ import { TagComponent } from './tag.component';
|
||||
import { ErrorHandler } from '../error-handler/error-handler';
|
||||
import { Tag } from '../service/interface';
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { TagService, TagDefaultService } from '../service/tag.service';
|
||||
import { TagService, TagDefaultService, ScanningResultService, ScanningResultDefaultService } from '../service/index';
|
||||
import { VULNERABILITY_DIRECTIVES } from '../vulnerability-scanning/index';
|
||||
import { FILTER_DIRECTIVES } from '../filter/index'
|
||||
|
||||
import { Observable, Subscription } from 'rxjs/Rx';
|
||||
|
||||
describe('TagComponent (inline template)', () => {
|
||||
|
||||
describe('TagComponent (inline template)', ()=> {
|
||||
|
||||
let comp: TagComponent;
|
||||
let fixture: ComponentFixture<TagComponent>;
|
||||
let tagService: TagService;
|
||||
@ -32,27 +36,30 @@ describe('TagComponent (inline template)', ()=> {
|
||||
];
|
||||
|
||||
let config: IServiceConfig = {
|
||||
repositoryBaseEndpoint: '/api/repositories/testing'
|
||||
repositoryBaseEndpoint: '/api/repositories/testing'
|
||||
};
|
||||
|
||||
beforeEach(async(()=>{
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
TagComponent,
|
||||
ConfirmationDialogComponent
|
||||
ConfirmationDialogComponent,
|
||||
VULNERABILITY_DIRECTIVES,
|
||||
FILTER_DIRECTIVES
|
||||
],
|
||||
providers: [
|
||||
ErrorHandler,
|
||||
{ provide: SERVICE_CONFIG, useValue: config },
|
||||
{ provide: TagService, useClass: TagDefaultService }
|
||||
{ provide: TagService, useClass: TagDefaultService },
|
||||
{ provide: ScanningResultService, useClass: ScanningResultDefaultService }
|
||||
]
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(()=>{
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(TagComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
@ -68,15 +75,15 @@ describe('TagComponent (inline template)', ()=> {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should load data', async(()=>{
|
||||
it('should load data', async(() => {
|
||||
expect(spy.calls.any).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should load and render data', async(()=>{
|
||||
it('should load and render data', async(() => {
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(()=>{
|
||||
fixture.whenStable().then(() => {
|
||||
fixture.detectChanges();
|
||||
let de: DebugElement = fixture.debugElement.query(del=>del.classes['datagrid-cell']);
|
||||
let de: DebugElement = fixture.debugElement.query(del => del.classes['datagrid-cell']);
|
||||
fixture.detectChanges();
|
||||
expect(de).toBeTruthy();
|
||||
let el: HTMLElement = de.nativeElement;
|
||||
|
@ -11,7 +11,18 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit, ViewChild, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
ElementRef,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
|
||||
import { TagService } from '../service/tag.service';
|
||||
|
||||
@ -22,24 +33,30 @@ import { ConfirmationDialogComponent } from '../confirmation-dialog/confirmation
|
||||
import { ConfirmationMessage } from '../confirmation-dialog/confirmation-message';
|
||||
import { ConfirmationAcknowledgement } from '../confirmation-dialog/confirmation-state-message';
|
||||
|
||||
import { Tag } from '../service/interface';
|
||||
import { Tag, TagClickEvent } from '../service/interface';
|
||||
|
||||
import { TAG_TEMPLATE } from './tag.component.html';
|
||||
import { TAG_STYLE } from './tag.component.css';
|
||||
|
||||
import { toPromise, CustomComparator } from '../utils';
|
||||
import { toPromise, CustomComparator, VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { State, Comparator } from 'clarity-angular';
|
||||
|
||||
import { ScanningResultService } from '../service/index';
|
||||
|
||||
import { Observable, Subscription } from 'rxjs/Rx';
|
||||
|
||||
const STATE_CHECK_INTERVAL: number = 2000;//2s
|
||||
|
||||
@Component({
|
||||
selector: 'hbr-tag',
|
||||
template: TAG_TEMPLATE,
|
||||
styles: [TAG_STYLE],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TagComponent implements OnInit {
|
||||
export class TagComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input() projectId: number;
|
||||
@Input() repoName: string;
|
||||
@ -49,9 +66,10 @@ export class TagComponent implements OnInit {
|
||||
@Input() hasProjectAdminRole: boolean;
|
||||
@Input() registryUrl: string;
|
||||
@Input() withNotary: boolean;
|
||||
@Input() withClair: boolean;
|
||||
|
||||
@Output() refreshRepo = new EventEmitter<boolean>();
|
||||
@Output() tagClickEvent = new EventEmitter<Tag>();
|
||||
@Output() tagClickEvent = new EventEmitter<TagClickEvent>();
|
||||
|
||||
tags: Tag[];
|
||||
|
||||
@ -66,13 +84,22 @@ export class TagComponent implements OnInit {
|
||||
|
||||
loading: boolean = false;
|
||||
|
||||
stateCheckTimer: Subscription;
|
||||
tagsInScanning: { [key: string]: any } = {};
|
||||
scanningTagCount: number = 0;
|
||||
|
||||
copyFailed: boolean = false;
|
||||
|
||||
@ViewChild('confirmationDialog')
|
||||
confirmationDialog: ConfirmationDialogComponent;
|
||||
|
||||
@ViewChild('digestTarget') textInput: ElementRef;
|
||||
|
||||
constructor(
|
||||
private errorHandler: ErrorHandler,
|
||||
private tagService: TagService,
|
||||
private translateService: TranslateService,
|
||||
private scanningService: ScanningResultService,
|
||||
private ref: ChangeDetectorRef) { }
|
||||
|
||||
confirmDeletion(message: ConfirmationAcknowledgement) {
|
||||
@ -108,11 +135,24 @@ export class TagComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.retrieve();
|
||||
|
||||
this.stateCheckTimer = Observable.timer(STATE_CHECK_INTERVAL, STATE_CHECK_INTERVAL).subscribe(() => {
|
||||
if (this.scanningTagCount > 0) {
|
||||
this.updateScanningStates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.stateCheckTimer) {
|
||||
this.stateCheckTimer.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
retrieve() {
|
||||
this.tags = [];
|
||||
this.loading = true;
|
||||
|
||||
toPromise<Tag[]>(this.tagService
|
||||
.getTags(this.repoName))
|
||||
.then(items => {
|
||||
@ -160,16 +200,76 @@ export class TagComponent implements OnInit {
|
||||
this.manifestInfoTitle = 'REPOSITORY.COPY_DIGEST_ID';
|
||||
this.digestId = tag.digest;
|
||||
this.showTagManifestOpened = true;
|
||||
this.copyFailed = false;
|
||||
}
|
||||
}
|
||||
|
||||
onTagClick(tag: Tag): void {
|
||||
if (tag) {
|
||||
let evt: TagClickEvent = {
|
||||
project_id: this.projectId,
|
||||
repository_name: this.repoName,
|
||||
tag_name: tag.name
|
||||
};
|
||||
this.tagClickEvent.emit(evt);
|
||||
}
|
||||
}
|
||||
|
||||
selectAndCopy($event: any) {
|
||||
$event.target.select();
|
||||
scanTag(tagId: string): void {
|
||||
//Double check
|
||||
if (this.tagsInScanning[tagId]) {
|
||||
return;
|
||||
}
|
||||
toPromise<any>(this.scanningService.startVulnerabilityScanning(this.repoName, tagId))
|
||||
.then(() => {
|
||||
//Add to scanning map
|
||||
this.tagsInScanning[tagId] = tagId;
|
||||
//Counting
|
||||
this.scanningTagCount += 1;
|
||||
})
|
||||
.catch(error => this.errorHandler.error(error));
|
||||
}
|
||||
|
||||
onTagClick(tag: Tag): void {
|
||||
if (tag) {
|
||||
this.tagClickEvent.emit(tag);
|
||||
updateScanningStates(): void {
|
||||
toPromise<Tag[]>(this.tagService
|
||||
.getTags(this.repoName))
|
||||
.then(items => {
|
||||
console.debug("updateScanningStates called!");
|
||||
//Reset the scanning states
|
||||
this.tagsInScanning = {};
|
||||
this.scanningTagCount = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.scan_overview) {
|
||||
if (item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.pending ||
|
||||
item.scan_overview.scan_status === VULNERABILITY_SCAN_STATUS.running) {
|
||||
this.tagsInScanning[item.name] = item.name;
|
||||
this.scanningTagCount += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.tags = items;
|
||||
})
|
||||
.catch(error => {
|
||||
this.errorHandler.error(error);
|
||||
});
|
||||
let hnd = setInterval(() => this.ref.markForCheck(), 100);
|
||||
setTimeout(() => clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
onSuccess($event: any): void {
|
||||
this.copyFailed = false;
|
||||
//Directly close dialog
|
||||
this.showTagManifestOpened = false;
|
||||
}
|
||||
|
||||
onError($event: any): void {
|
||||
//Show error
|
||||
this.copyFailed = true;
|
||||
//Select all text
|
||||
if(this.textInput){
|
||||
this.textInput.nativeElement.select();
|
||||
}
|
||||
}
|
||||
}
|
2
src/ui_ng/lib/src/third-party/index.ts
vendored
Normal file
2
src/ui_ng/lib/src/third-party/index.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ngx-window-token/index';
|
||||
export * from './ngx-clipboard/index';
|
49
src/ui_ng/lib/src/third-party/ngx-clipboard/clipboard.directive.ts
vendored
Normal file
49
src/ui_ng/lib/src/third-party/ngx-clipboard/clipboard.directive.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
import { ClipboardService } from './clipboard.service';
|
||||
import { Directive, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, Renderer, ElementRef } from '@angular/core';
|
||||
|
||||
@Directive({
|
||||
selector: '[ngxClipboard]'
|
||||
})
|
||||
export class ClipboardDirective implements OnInit, OnDestroy {
|
||||
@Input('ngxClipboard') public targetElm: HTMLInputElement;
|
||||
|
||||
@Input() public cbContent: string;
|
||||
|
||||
@Output() public cbOnSuccess: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@Output() public cbOnError: EventEmitter<any> = new EventEmitter<any>();
|
||||
constructor(
|
||||
private clipboardSrv: ClipboardService,
|
||||
private renderer: Renderer
|
||||
|
||||
) { }
|
||||
|
||||
public ngOnInit() { }
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.clipboardSrv.destroy();
|
||||
}
|
||||
|
||||
@HostListener('click', ['$event.target']) private onClick(button: ElementRef) {
|
||||
if (!this.clipboardSrv.isSupported) {
|
||||
this.handleResult(false, undefined);
|
||||
} else if (this.targetElm && this.clipboardSrv.isTargetValid(this.targetElm)) {
|
||||
this.handleResult(this.clipboardSrv.copyFromInputElement(this.targetElm, this.renderer),
|
||||
this.targetElm.value);
|
||||
} else if (this.cbContent) {
|
||||
this.handleResult(this.clipboardSrv.copyFromContent(this.cbContent, this.renderer), this.cbContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires an event based on the copy operation result.
|
||||
* @param {Boolean} succeeded
|
||||
*/
|
||||
private handleResult(succeeded: Boolean, copiedContent: string) {
|
||||
if (succeeded) {
|
||||
this.cbOnSuccess.emit({ isSuccess: true, content: copiedContent });
|
||||
} else {
|
||||
this.cbOnError.emit({ isSuccess: false });
|
||||
}
|
||||
}
|
||||
}
|
110
src/ui_ng/lib/src/third-party/ngx-clipboard/clipboard.service.ts
vendored
Normal file
110
src/ui_ng/lib/src/third-party/ngx-clipboard/clipboard.service.ts
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
import { Inject, InjectionToken, Injectable, Optional, Renderer, SkipSelf } from '@angular/core';
|
||||
import { DOCUMENT } from '@angular/platform-browser';
|
||||
import { WINDOW } from "../ngx-window-token/index";
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ClipboardService {
|
||||
private tempTextArea: HTMLTextAreaElement;
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private document: any,
|
||||
@Inject(WINDOW) private window: any,
|
||||
) { }
|
||||
public get isSupported(): boolean {
|
||||
return !!this.document.queryCommandSupported && !!this.document.queryCommandSupported('copy');
|
||||
}
|
||||
|
||||
public isTargetValid(element: HTMLInputElement | HTMLTextAreaElement): boolean {
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
||||
if (element.hasAttribute('disabled')) {
|
||||
// tslint:disable-next-line:max-line-length
|
||||
throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
throw new Error('Target should be input or textarea');
|
||||
}
|
||||
|
||||
/**
|
||||
* copyFromInputElement
|
||||
*/
|
||||
public copyFromInputElement(targetElm: HTMLInputElement | HTMLTextAreaElement, renderer: Renderer): boolean {
|
||||
try {
|
||||
this.selectTarget(targetElm, renderer);
|
||||
const re = this.copyText();
|
||||
this.clearSelection(targetElm, this.window);
|
||||
return re;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a fake textarea element, sets its value from `text` property,
|
||||
* and makes a selection on it.
|
||||
*/
|
||||
public copyFromContent(content: string, renderer: Renderer) {
|
||||
if (!this.tempTextArea) {
|
||||
this.tempTextArea = this.createTempTextArea(this.document, this.window);
|
||||
this.document.body.appendChild(this.tempTextArea);
|
||||
}
|
||||
this.tempTextArea.value = content;
|
||||
return this.copyFromInputElement(this.tempTextArea, renderer);
|
||||
}
|
||||
|
||||
// remove temporary textarea if any
|
||||
public destroy() {
|
||||
if (this.tempTextArea) {
|
||||
this.document.body.removeChild(this.tempTextArea);
|
||||
this.tempTextArea = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// select the target html input element
|
||||
private selectTarget(inputElement: HTMLInputElement | HTMLTextAreaElement, renderer: Renderer): number | undefined {
|
||||
renderer.invokeElementMethod(inputElement, 'select');
|
||||
renderer.invokeElementMethod(inputElement, 'setSelectionRange', [0, inputElement.value.length]);
|
||||
return inputElement.value.length;
|
||||
}
|
||||
|
||||
private copyText(): boolean {
|
||||
return this.document.execCommand('copy');
|
||||
}
|
||||
// Removes current selection and focus from `target` element.
|
||||
private clearSelection(inputElement: HTMLInputElement | HTMLTextAreaElement, window: Window) {
|
||||
// tslint:disable-next-line:no-unused-expression
|
||||
inputElement && inputElement.blur();
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
|
||||
// create a fake textarea for copy command
|
||||
private createTempTextArea(doc: Document, window: Window): HTMLTextAreaElement {
|
||||
const isRTL = doc.documentElement.getAttribute('dir') === 'rtl';
|
||||
let ta: HTMLTextAreaElement;
|
||||
ta = doc.createElement('textarea');
|
||||
// Prevent zooming on iOS
|
||||
ta.style.fontSize = '12pt';
|
||||
// Reset box model
|
||||
ta.style.border = '0';
|
||||
ta.style.padding = '0';
|
||||
ta.style.margin = '0';
|
||||
// Move element out of screen horizontally
|
||||
ta.style.position = 'absolute';
|
||||
ta.style[isRTL ? 'right' : 'left'] = '-9999px';
|
||||
// Move element to the same position vertically
|
||||
let yPosition = window.pageYOffset || doc.documentElement.scrollTop;
|
||||
ta.style.top = yPosition + 'px';
|
||||
ta.setAttribute('readonly', '');
|
||||
return ta;
|
||||
}
|
||||
}
|
||||
// this pattern is mentioned in https://github.com/angular/angular/issues/13854 in #43
|
||||
export function CLIPBOARD_SERVICE_PROVIDER_FACTORY(doc: Document, win: Window, parentDispatcher: ClipboardService) {
|
||||
return parentDispatcher || new ClipboardService(doc, win);
|
||||
};
|
||||
|
||||
export const CLIPBOARD_SERVICE_PROVIDER = {
|
||||
provide: ClipboardService,
|
||||
deps: [DOCUMENT, WINDOW, [new Optional(), new SkipSelf(), ClipboardService]],
|
||||
useFactory: CLIPBOARD_SERVICE_PROVIDER_FACTORY
|
||||
};
|
15
src/ui_ng/lib/src/third-party/ngx-clipboard/index.ts
vendored
Normal file
15
src/ui_ng/lib/src/third-party/ngx-clipboard/index.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
import { ClipboardDirective } from './clipboard.directive';
|
||||
import { CLIPBOARD_SERVICE_PROVIDER } from './clipboard.service';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { WindowTokenModule } from '../ngx-window-token/index';
|
||||
export * from './clipboard.directive';
|
||||
export * from './clipboard.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, WindowTokenModule],
|
||||
declarations: [ClipboardDirective],
|
||||
exports: [ClipboardDirective],
|
||||
providers: [CLIPBOARD_SERVICE_PROVIDER]
|
||||
})
|
||||
export class ClipboardModule { }
|
1
src/ui_ng/lib/src/third-party/ngx-window-token/index.ts
vendored
Normal file
1
src/ui_ng/lib/src/third-party/ngx-window-token/index.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export { WindowTokenModule, WINDOW } from './window-token';
|
16
src/ui_ng/lib/src/third-party/ngx-window-token/window-token.ts
vendored
Normal file
16
src/ui_ng/lib/src/third-party/ngx-window-token/window-token.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const WINDOW = new InjectionToken<Window>('WindowToken');
|
||||
|
||||
export function _window(): Window {
|
||||
return window;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
providers: [{
|
||||
provide: WINDOW,
|
||||
useFactory: _window
|
||||
}]
|
||||
})
|
||||
export class WindowTokenModule { }
|
@ -74,18 +74,18 @@ export function buildHttpRequestOptions(params: RequestQueryParams): RequestOpti
|
||||
|
||||
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
|
||||
export const ButtonClickEvents = {
|
||||
left: { button: 0 },
|
||||
right: { button: 2 }
|
||||
left: { button: 0 },
|
||||
right: { button: 2 }
|
||||
};
|
||||
|
||||
|
||||
/** Simulate element click. Defaults to mouse left-button click event. */
|
||||
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
|
||||
if (el instanceof HTMLElement) {
|
||||
el.click();
|
||||
} else {
|
||||
el.triggerEventHandler('click', eventObj);
|
||||
}
|
||||
if (el instanceof HTMLElement) {
|
||||
el.click();
|
||||
} else {
|
||||
el.triggerEventHandler('click', eventObj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,33 +94,45 @@ export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClic
|
||||
*/
|
||||
export class CustomComparator<T> implements Comparator<T> {
|
||||
|
||||
fieldName: string;
|
||||
type: string;
|
||||
fieldName: string;
|
||||
type: string;
|
||||
|
||||
constructor(fieldName: string, type: string) {
|
||||
this.fieldName = fieldName;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
compare(a: {[key: string]: any| any[]}, b: {[key: string]: any| any[]}) {
|
||||
let comp = 0;
|
||||
if(a && b) {
|
||||
let fieldA = a[this.fieldName];
|
||||
let fieldB = b[this.fieldName];
|
||||
switch(this.type) {
|
||||
case "number":
|
||||
comp = fieldB - fieldA;
|
||||
break;
|
||||
case "date":
|
||||
comp = new Date(fieldB).getTime() - new Date(fieldA).getTime();
|
||||
break;
|
||||
}
|
||||
constructor(fieldName: string, type: string) {
|
||||
this.fieldName = fieldName;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
compare(a: { [key: string]: any | any[] }, b: { [key: string]: any | any[] }) {
|
||||
let comp = 0;
|
||||
if (a && b) {
|
||||
let fieldA = a[this.fieldName];
|
||||
let fieldB = b[this.fieldName];
|
||||
switch (this.type) {
|
||||
case "number":
|
||||
comp = fieldB - fieldA;
|
||||
break;
|
||||
case "date":
|
||||
comp = new Date(fieldB).getTime() - new Date(fieldA).getTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default page size
|
||||
*/
|
||||
export const DEFAULT_PAGE_SIZE: number = 15;
|
||||
export const DEFAULT_PAGE_SIZE: number = 15;
|
||||
|
||||
/**
|
||||
* The state of vulnerability scanning
|
||||
*/
|
||||
export const VULNERABILITY_SCAN_STATUS = {
|
||||
unknown: "n/a",
|
||||
pending: "pending",
|
||||
running: "running",
|
||||
error: "error",
|
||||
stopped: "stopped",
|
||||
finished: "finished"
|
||||
};
|
@ -3,6 +3,7 @@ import { ResultGridComponent } from './result-grid.component';
|
||||
import { ResultBarChartComponent } from './result-bar-chart.component';
|
||||
import { ResultTipComponent } from './result-tip.component';
|
||||
|
||||
export * from './result-tip.component';
|
||||
export * from "./result-grid.component";
|
||||
export * from './result-bar-chart.component';
|
||||
|
||||
|
@ -11,6 +11,7 @@ import { ScanningResultService, ScanningResultDefaultService } from '../service/
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
|
||||
describe('ResultBarChartComponent (inline template)', () => {
|
||||
let component: ResultBarChartComponent;
|
||||
@ -20,13 +21,25 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||
};
|
||||
let mockData: VulnerabilitySummary = {
|
||||
total_package: 124,
|
||||
package_with_none: 92,
|
||||
package_with_high: 10,
|
||||
package_with_medium: 6,
|
||||
package_With_low: 13,
|
||||
package_with_unknown: 3,
|
||||
complete_timestamp: new Date()
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.finished,
|
||||
severity: 5,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 124,
|
||||
summary: [{
|
||||
severity: 1,
|
||||
count: 90
|
||||
}, {
|
||||
severity: 3,
|
||||
count: 10
|
||||
}, {
|
||||
severity: 4,
|
||||
count: 10
|
||||
}, {
|
||||
severity: 5,
|
||||
count: 13
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
@ -115,7 +128,7 @@ describe('ResultBarChartComponent (inline template)', () => {
|
||||
|
||||
let el: HTMLElement = fixture.nativeElement.querySelector('.bar-block-none');
|
||||
expect(el).not.toBeNull();
|
||||
expect(el.style.width).toEqual("74px");
|
||||
expect(el.style.width).toEqual("73px");
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -2,11 +2,14 @@ import {
|
||||
Component,
|
||||
Input,
|
||||
Output,
|
||||
EventEmitter
|
||||
EventEmitter,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { VulnerabilitySummary } from '../service/index';
|
||||
import { SCANNING_STYLES } from './scanning.css';
|
||||
import { BAR_CHART_COMPONENT_HTML } from './scanning.html';
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
import { VulnerabilitySeverity } from '../service/index';
|
||||
|
||||
export enum ScanState {
|
||||
COMPLETED, //Scanning work successfully completed
|
||||
@ -22,18 +25,47 @@ export enum ScanState {
|
||||
styles: [SCANNING_STYLES],
|
||||
template: BAR_CHART_COMPONENT_HTML
|
||||
})
|
||||
export class ResultBarChartComponent {
|
||||
export class ResultBarChartComponent implements OnInit {
|
||||
@Input() tagId: string = "";
|
||||
@Input() state: ScanState = ScanState.UNKNOWN;
|
||||
@Input() state: ScanState = ScanState.PENDING;
|
||||
@Input() summary: VulnerabilitySummary = {
|
||||
total_package: 0,
|
||||
package_with_none: 0,
|
||||
complete_timestamp: new Date()
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
|
||||
severity: VulnerabilitySeverity.UNKNOWN,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 0,
|
||||
summary: []
|
||||
}
|
||||
};
|
||||
@Output() startScanning: EventEmitter<string> = new EventEmitter<string>();
|
||||
scanningInProgress: boolean = false;
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.summary && this.summary.scan_status) {
|
||||
switch (this.summary.scan_status) {
|
||||
case VULNERABILITY_SCAN_STATUS.unknown:
|
||||
this.state = ScanState.UNKNOWN;
|
||||
break;
|
||||
case VULNERABILITY_SCAN_STATUS.error:
|
||||
this.state = ScanState.ERROR;
|
||||
break;
|
||||
case VULNERABILITY_SCAN_STATUS.pending:
|
||||
this.state = ScanState.QUEUED;
|
||||
break;
|
||||
case VULNERABILITY_SCAN_STATUS.stopped:
|
||||
this.state = ScanState.PENDING;
|
||||
break;
|
||||
case VULNERABILITY_SCAN_STATUS.finished:
|
||||
this.state = ScanState.COMPLETED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get completed(): boolean {
|
||||
return this.state === ScanState.COMPLETED;
|
||||
}
|
||||
@ -60,6 +92,7 @@ export class ResultBarChartComponent {
|
||||
|
||||
scanNow(): void {
|
||||
if (this.tagId && this.tagId !== '') {
|
||||
this.scanningInProgress = true;
|
||||
this.startScanning.emit(this.tagId);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import {
|
||||
ScanningResultService,
|
||||
VulnerabilityItem
|
||||
VulnerabilityItem,
|
||||
VulnerabilitySeverity
|
||||
} from '../service/index';
|
||||
import { ErrorHandler } from '../error-handler/index';
|
||||
|
||||
@ -16,7 +17,10 @@ import { SCANNING_STYLES } from './scanning.css';
|
||||
})
|
||||
export class ResultGridComponent implements OnInit {
|
||||
scanningResults: VulnerabilityItem[] = [];
|
||||
dataCache: VulnerabilityItem[] = [];
|
||||
|
||||
@Input() tagId: string;
|
||||
@Input() repositoryId: string;
|
||||
|
||||
constructor(
|
||||
private scanningService: ScanningResultService,
|
||||
@ -24,26 +28,50 @@ export class ResultGridComponent implements OnInit {
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadResults(this.tagId);
|
||||
this.loadResults(this.repositoryId, this.tagId);
|
||||
}
|
||||
|
||||
showDetail(result: VulnerabilityItem): void {
|
||||
console.log(result.id);
|
||||
}
|
||||
|
||||
loadResults(tagId: string): void {
|
||||
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(tagId))
|
||||
loadResults(repositoryId: string, tagId: string): void {
|
||||
toPromise<VulnerabilityItem[]>(this.scanningService.getVulnerabilityScanningResults(repositoryId, tagId))
|
||||
.then((results: VulnerabilityItem[]) => {
|
||||
this.scanningResults = results;
|
||||
this.dataCache = results;
|
||||
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
|
||||
})
|
||||
.catch(error => { this.errorHandler.error(error) })
|
||||
}
|
||||
|
||||
//TODO: Should query from back-end service
|
||||
filterVulnerabilities(terms: string): void {
|
||||
console.log(terms);
|
||||
if (terms.trim() === '') {
|
||||
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => item.id !== '');
|
||||
} else {
|
||||
this.scanningResults = this.dataCache.filter((item: VulnerabilityItem) => this._regexpFilter(terms, item.package));
|
||||
}
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.loadResults(this.tagId);
|
||||
this.loadResults(this.repositoryId, this.tagId);
|
||||
}
|
||||
|
||||
severityText(severity: VulnerabilitySeverity): string {
|
||||
switch (severity) {
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
return 'VULNERABILITY.SEVERITY.HIGH';
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
return 'VULNERABILITY.SEVERITY.MEDIUM';
|
||||
case VulnerabilitySeverity.LOW:
|
||||
return 'VULNERABILITY.SEVERITY.LOW';
|
||||
case VulnerabilitySeverity.NONE:
|
||||
return 'VULNERABILITY.SEVERITY.NEGLIGIBLE';
|
||||
case VulnerabilitySeverity.UNKNOWN:
|
||||
return 'VULNERABILITY.SEVERITY.UNKNOWN';
|
||||
default:
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
_regexpFilter(terms: string, testedValue: any): boolean {
|
||||
let reg = new RegExp('.*' + terms + '.*', 'i');
|
||||
return reg.test(testedValue);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import { ResultTipComponent } from './result-tip.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { SERVICE_CONFIG, IServiceConfig } from '../service.config';
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
|
||||
describe('ResultTipComponent (inline template)', () => {
|
||||
let component: ResultTipComponent;
|
||||
@ -16,14 +17,26 @@ describe('ResultTipComponent (inline template)', () => {
|
||||
let testConfig: IServiceConfig = {
|
||||
vulnerabilityScanningBaseEndpoint: "/api/vulnerability/testing"
|
||||
};
|
||||
let mockData:VulnerabilitySummary = {
|
||||
total_package: 124,
|
||||
package_with_none: 90,
|
||||
package_with_high: 13,
|
||||
package_with_medium: 10,
|
||||
package_With_low: 10,
|
||||
package_with_unknown: 1,
|
||||
complete_timestamp: new Date()
|
||||
let mockData: VulnerabilitySummary = {
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.finished,
|
||||
severity: 5,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 124,
|
||||
summary: [{
|
||||
severity: 1,
|
||||
count: 90
|
||||
}, {
|
||||
severity: 3,
|
||||
count: 10
|
||||
}, {
|
||||
severity: 4,
|
||||
count: 10
|
||||
}, {
|
||||
severity: 5,
|
||||
count: 13
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(async(() => {
|
||||
|
@ -4,6 +4,7 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { SCANNING_STYLES } from './scanning.css';
|
||||
import { TIP_COMPONENT_HTML } from './scanning.html';
|
||||
import { VULNERABILITY_SCAN_STATUS } from '../utils';
|
||||
|
||||
export const MIN_TIP_WIDTH = 5;
|
||||
export const MAX_TIP_WIDTH = 100;
|
||||
@ -15,24 +16,64 @@ export const MAX_TIP_WIDTH = 100;
|
||||
})
|
||||
export class ResultTipComponent implements OnInit {
|
||||
_tipTitle: string = "";
|
||||
_highCount: number = 0;
|
||||
_mediumCount: number = 0;
|
||||
_lowCount: number = 0;
|
||||
_unknownCount: number = 0;
|
||||
_noneCount: number = 0;
|
||||
|
||||
totalPackages: number = 0;
|
||||
packagesWithVul: number = 0;
|
||||
|
||||
@Input() summary: VulnerabilitySummary = {
|
||||
total_package: 0,
|
||||
package_with_none: 0,
|
||||
complete_timestamp: new Date()
|
||||
scan_status: VULNERABILITY_SCAN_STATUS.unknown,
|
||||
severity: VulnerabilitySeverity.UNKNOWN,
|
||||
update_time: new Date(),
|
||||
components: {
|
||||
total: 0,
|
||||
summary: []
|
||||
}
|
||||
};
|
||||
|
||||
constructor(private translate: TranslateService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.totalPackages = this.summary && this.summary.components ? this.summary.components.total : 0;
|
||||
if (this.summary && this.summary.components && this.summary.components.summary) {
|
||||
this.summary.components.summary.forEach(item => {
|
||||
if (item.severity != VulnerabilitySeverity.NONE) {
|
||||
this.packagesWithVul += item.count
|
||||
}
|
||||
|
||||
switch (item.severity) {
|
||||
case VulnerabilitySeverity.UNKNOWN:
|
||||
this._unknownCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.NONE:
|
||||
this._noneCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.LOW:
|
||||
this._lowCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.MEDIUM:
|
||||
this._mediumCount += item.count;
|
||||
break;
|
||||
case VulnerabilitySeverity.HIGH:
|
||||
this._highCount += item.count;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
this.translate.get('VULNERABILITY.CHART.TOOLTIPS_TITLE',
|
||||
{ totalVulnerability: this.totalVulnerabilities, totalPackages: this.summary.total_package })
|
||||
{ totalVulnerability: this.packagesWithVul, totalPackages: this.totalPackages })
|
||||
.subscribe((res: string) => this._tipTitle = res);
|
||||
}
|
||||
|
||||
tipWidth(severity: VulnerabilitySeverity): string {
|
||||
let n: number = 0;
|
||||
let m: number = this.summary ? this.summary.total_package : 0;
|
||||
let m: number = this.totalPackages;
|
||||
|
||||
if (m === 0) {
|
||||
return 0 + 'px';
|
||||
@ -59,8 +100,8 @@ export class ResultTipComponent implements OnInit {
|
||||
break;
|
||||
}
|
||||
|
||||
let width: number = Math.round((n/m)*MAX_TIP_WIDTH);
|
||||
if(width < MIN_TIP_WIDTH){
|
||||
let width: number = Math.round((n / m) * MAX_TIP_WIDTH);
|
||||
if (width < MIN_TIP_WIDTH) {
|
||||
width = MIN_TIP_WIDTH;
|
||||
}
|
||||
|
||||
@ -76,8 +117,8 @@ export class ResultTipComponent implements OnInit {
|
||||
return "VULNERABILITY.SINGULAR";
|
||||
}
|
||||
|
||||
public get totalVulnerabilities(): number {
|
||||
return this.summary.total_package - this.summary.package_with_none;
|
||||
public get completeTimestamp(): Date {
|
||||
return this.summary && this.summary.update_time ? this.summary.update_time : new Date();
|
||||
}
|
||||
|
||||
public get hasHigh(): boolean {
|
||||
@ -105,22 +146,22 @@ export class ResultTipComponent implements OnInit {
|
||||
}
|
||||
|
||||
public get highCount(): number {
|
||||
return this.summary && this.summary.package_with_high ? this.summary.package_with_high : 0;
|
||||
return this._highCount;
|
||||
}
|
||||
|
||||
public get mediumCount(): number {
|
||||
return this.summary && this.summary.package_with_medium ? this.summary.package_with_medium : 0;
|
||||
return this._mediumCount;
|
||||
}
|
||||
|
||||
public get lowCount(): number {
|
||||
return this.summary && this.summary.package_With_low ? this.summary.package_With_low : 0;
|
||||
return this._lowCount;
|
||||
}
|
||||
|
||||
public get unknownCount(): number {
|
||||
return this.summary && this.summary.package_with_unknown ? this.summary.package_with_unknown : 0;
|
||||
return this._unknownCount;
|
||||
}
|
||||
public get noneCount(): number {
|
||||
return this.summary && this.summary.package_with_none ? this.summary.package_with_none : 0;
|
||||
return this._noneCount;
|
||||
}
|
||||
|
||||
public get highSuffix(): string {
|
||||
@ -144,6 +185,6 @@ export class ResultTipComponent implements OnInit {
|
||||
}
|
||||
|
||||
public get maxWidth(): string {
|
||||
return MAX_TIP_WIDTH+"px";
|
||||
return (MAX_TIP_WIDTH + 20) + "px";
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,40 @@
|
||||
export const SCANNING_STYLES: string = `
|
||||
.bar-wrapper {
|
||||
width: 150px;
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
width: 120px;
|
||||
height: 12px;
|
||||
}
|
||||
.bar-state {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.bar-state-chart {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.bar-state-error {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.scanning-button {
|
||||
height: 24px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
vertical-align: middle;
|
||||
top: -6px;
|
||||
top: -12px;
|
||||
position: relative;
|
||||
}
|
||||
.tip-wrapper {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
max-height: 16px;
|
||||
max-width: 150px;
|
||||
height: 10px;
|
||||
max-width: 120px;
|
||||
}
|
||||
.tip-position {
|
||||
margin-left: -4px;
|
||||
|
@ -1,11 +1,11 @@
|
||||
export const TIP_COMPONENT_HTML: string = `
|
||||
<div class="tip-wrapper tip-position" [style.width]='maxWidth'>
|
||||
<clr-tooltip [clrTooltipDirection]="'top-right'" [clrTooltipSize]="'lg'">
|
||||
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(4)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(3)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(2)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(1)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(0)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-high" [style.width]='tipWidth(5)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-medium" [style.width]='tipWidth(4)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-low" [style.width]='tipWidth(3)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-unknown" [style.width]='tipWidth(2)'></div>
|
||||
<div class="tip-wrapper tip-block bar-block-none" [style.width]='tipWidth(1)'></div>
|
||||
<clr-tooltip-content>
|
||||
<div>
|
||||
<span class="bar-tooltip-font bar-tooltip-font-title">{{tipTitle}}</span>
|
||||
@ -34,7 +34,7 @@ export const TIP_COMPONENT_HTML: string = `
|
||||
</div>
|
||||
<div>
|
||||
<span class="bar-scanning-time">{{'VULNERABILITY.CHART.SCANNING_TIME' | translate}} </span>
|
||||
<span>{{summary.complete_timestamp | date}}</span>
|
||||
<span>{{completeTimestamp | date}}</span>
|
||||
</div>
|
||||
</clr-tooltip-content>
|
||||
</clr-tooltip>
|
||||
@ -56,23 +56,25 @@ export const GRID_COMPONENT_HTML: string = `
|
||||
<clr-dg-column [clrDgField]="'id'">{{'VULNERABILITY.GRID.COLUMN_ID' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'severity'">{{'VULNERABILITY.GRID.COLUMN_SEVERITY' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'package'">{{'VULNERABILITY.GRID.COLUMN_PACKAGE' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}} version</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'version'">{{'VULNERABILITY.GRID.COLUMN_VERSION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'fixedVersion'">{{'VULNERABILITY.GRID.COLUMN_FIXED' | translate}}</clr-dg-column>
|
||||
<clr-dg-column [clrDgField]="'layer'">{{'VULNERABILITY.GRID.COLUMN_LAYER' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>Description</clr-dg-column>
|
||||
|
||||
<clr-dg-placeholder>{{'VULNERABILITY.GRID.PLACEHOLDER' | translate}}</clr-dg-placeholder>
|
||||
<clr-dg-row *clrDgItems="let res of scanningResults">
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="showDetail(res)">Detail</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{res.id}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.severity}}</clr-dg-cell>
|
||||
<clr-dg-cell [ngSwitch]="res.severity">
|
||||
<span *ngSwitchCase="5" class="label label-danger">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="4" class="label label-warning">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="3" class="label label-success">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchCase="1" class="label label-info">{{severityText(res.severity) | translate}}</span>
|
||||
<span *ngSwitchDefault>{{severityText(res.severity) | translate}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.package}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.version}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.fixedVersion}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.layer}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{res.description}}</clr-dg-cell>
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
{{'VULNERABILITY.GRID.COLUMN_DESCRIPTION' | translate}}: {{res.description}}
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
@ -87,20 +89,20 @@ export const GRID_COMPONENT_HTML: string = `
|
||||
export const BAR_CHART_COMPONENT_HTML: string = `
|
||||
<div class="bar-wrapper">
|
||||
<div *ngIf="pending" class="bar-state">
|
||||
<button class="btn btn-link scanning-button" (click)="scanNow()">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
|
||||
<button class="btn btn-link scanning-button" (click)="scanNow()" [disabled]="scanningInProgress">{{'VULNERABILITY.STATE.PENDING' | translate}}</button>
|
||||
</div>
|
||||
<div *ngIf="queued" class="bar-state">
|
||||
<span>{{'VULNERABILITY.STATE.QUEUED' | translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="error" class="bar-state">
|
||||
<div *ngIf="error" class="bar-state bar-state-error">
|
||||
<clr-icon shape="info-circle" class="is-error" size="24"></clr-icon>
|
||||
<span style="margin-left:-5px;">{{'VULNERABILITY.STATE.ERROR' | translate}}</span>
|
||||
<span class="error-text">{{'VULNERABILITY.STATE.ERROR' | translate}}</span>
|
||||
</div>
|
||||
<div *ngIf="scanning" class="bar-state">
|
||||
<div *ngIf="scanning" class="bar-state bar-state-chart">
|
||||
<div>{{'VULNERABILITY.STATE.SCANNING' | translate}}</div>
|
||||
<div class="progress loop" style="height:2px;min-height:2px;"><progress></progress></div>
|
||||
</div>
|
||||
<div *ngIf="completed" class="bar-state">
|
||||
<div *ngIf="completed" class="bar-state bar-state-chart" style="z-index:1020;">
|
||||
<hbr-vulnerability-summary-chart [summary]="summary"></hbr-vulnerability-summary-chart>
|
||||
</div>
|
||||
<div *ngIf="unknown" class="bar-state">
|
||||
|
@ -1,71 +1,71 @@
|
||||
{
|
||||
"name": "harbor",
|
||||
"version": "1.1.0",
|
||||
"description": "Harbor UI with Clarity",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "ng serve --ssl 1 --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor",
|
||||
"build": "ngc -p tsconfig-aot.json"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^4.0.1",
|
||||
"@angular/common": "^4.0.1",
|
||||
"@angular/compiler": "^4.0.1",
|
||||
"@angular/compiler-cli": "^4.0.2",
|
||||
"@angular/core": "^4.0.1",
|
||||
"@angular/forms": "^4.0.1",
|
||||
"@angular/http": "^4.0.1",
|
||||
"@angular/platform-browser": "^4.0.1",
|
||||
"@angular/platform-browser-dynamic": "^4.0.1",
|
||||
"@angular/platform-server": "^4.0.2",
|
||||
"@angular/router": "^4.0.1",
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@types/jquery": "^2.0.41",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "^0.9.0",
|
||||
"clarity-icons": "^0.9.0",
|
||||
"clarity-ui": "^0.9.0",
|
||||
"core-js": "^2.4.1",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"zone.js": "^0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.0.0",
|
||||
"@angular/compiler-cli": "^4.0.1",
|
||||
"@types/core-js": "^0.9.34",
|
||||
"@types/jasmine": "~2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~2.0.0-beta.4",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
"jasmine-core": "2.4.1",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-remap-istanbul": "^0.2.1",
|
||||
"protractor": "4.0.9",
|
||||
"rollup": "^0.41.6",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-uglify": "^1.0.1",
|
||||
"ts-node": "1.2.1",
|
||||
"tslint": "^4.1.1",
|
||||
"typescript": "~2.2.0",
|
||||
"typings": "^1.4.0",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
||||
"name": "harbor",
|
||||
"version": "1.2.0",
|
||||
"description": "Harbor UI with Clarity",
|
||||
"angular-cli": {},
|
||||
"scripts": {
|
||||
"start": "ng serve --ssl 1 --ssl-key ssl/server.key --ssl-cert ssl/server.crt --host 0.0.0.0 --proxy-config proxy.config.json",
|
||||
"lint": "tslint \"src/**/*.ts\"",
|
||||
"test": "ng test --single-run",
|
||||
"pree2e": "webdriver-manager update",
|
||||
"e2e": "protractor",
|
||||
"build": "ngc -p tsconfig-aot.json"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "~4.1.3",
|
||||
"@angular/common": "~4.1.3",
|
||||
"@angular/compiler": "~4.1.3",
|
||||
"@angular/compiler-cli": "~4.1.3",
|
||||
"@angular/core": "~4.1.3",
|
||||
"@angular/forms": "~4.1.3",
|
||||
"@angular/http": "~4.1.3",
|
||||
"@angular/platform-browser": "~4.1.3",
|
||||
"@angular/platform-browser-dynamic": "~4.1.3",
|
||||
"@angular/router": "~4.1.3",
|
||||
"@ngx-translate/core": "^6.0.0",
|
||||
"@ngx-translate/http-loader": "0.0.3",
|
||||
"@types/jquery": "^2.0.41",
|
||||
"@webcomponents/custom-elements": "1.0.0-alpha.3",
|
||||
"clarity-angular": "^0.9.8",
|
||||
"clarity-icons": "^0.9.8",
|
||||
"clarity-ui": "^0.9.8",
|
||||
"core-js": "^2.4.1",
|
||||
"harbor-ui": "^0.2.19",
|
||||
"intl": "^1.2.5",
|
||||
"mutationobserver-shim": "^0.3.2",
|
||||
"ngx-cookie": "^1.0.0",
|
||||
"rxjs": "^5.0.1",
|
||||
"ts-helpers": "^1.1.1",
|
||||
"web-animations-js": "^2.2.1",
|
||||
"zone.js": "^0.8.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^1.0.0",
|
||||
"@angular/compiler-cli": "~4.1.3",
|
||||
"@types/core-js": "^0.9.34",
|
||||
"@types/jasmine": "~2.2.30",
|
||||
"@types/node": "^6.0.42",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"codelyzer": "~2.0.0-beta.4",
|
||||
"enhanced-resolve": "^3.0.0",
|
||||
"jasmine-core": "2.4.1",
|
||||
"jasmine-spec-reporter": "2.5.0",
|
||||
"karma": "1.2.0",
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.0.2",
|
||||
"karma-mocha-reporter": "^2.2.1",
|
||||
"karma-phantomjs-launcher": "^1.0.0",
|
||||
"karma-remap-istanbul": "^0.2.1",
|
||||
"protractor": "4.0.9",
|
||||
"rollup": "^0.41.6",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-node-resolve": "^3.0.0",
|
||||
"rollup-plugin-uglify": "^1.0.1",
|
||||
"ts-node": "1.2.1",
|
||||
"tslint": "^4.1.1",
|
||||
"typescript": "~2.2.0",
|
||||
"typings": "^1.4.0",
|
||||
"webdriver-manager": "10.2.5"
|
||||
}
|
||||
}
|
@ -31,20 +31,6 @@ export class AppComponent {
|
||||
private session: SessionService,
|
||||
private appConfigService: AppConfigService,
|
||||
private titleService: Title) {
|
||||
|
||||
translate.addLangs(supportedLangs);
|
||||
translate.setDefaultLang(enLang);
|
||||
|
||||
//If user has selected lang, then directly use it
|
||||
let langSetting = this.cookie.get("harbor-lang");
|
||||
if (!langSetting || langSetting.trim() === "") {
|
||||
//Use browser lang
|
||||
langSetting = translate.getBrowserCultureLang().toLowerCase();
|
||||
}
|
||||
|
||||
let selectedLang = this.isLangMatch(langSetting, supportedLangs) ? langSetting : enLang;
|
||||
translate.use(selectedLang);
|
||||
|
||||
//Override page title
|
||||
let key: string = "APP_TITLE.HARBOR";
|
||||
if (this.appConfigService.isIntegrationMode()) {
|
||||
@ -55,10 +41,4 @@ export class AppComponent {
|
||||
this.titleService.setTitle(res);
|
||||
});
|
||||
}
|
||||
|
||||
isLangMatch(browserLang: string, supportedLangs: string[]) {
|
||||
if (supportedLangs && supportedLangs.length > 0) {
|
||||
return supportedLangs.find(lang => lang === browserLang);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,9 +13,6 @@
|
||||
// limitations under the License.
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule, APP_INITIALIZER, LOCALE_ID } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpModule } from '@angular/http';
|
||||
import { ClarityModule } from 'clarity-angular';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
import { BaseModule } from './base/base.module';
|
||||
@ -24,17 +21,9 @@ import { SharedModule } from './shared/shared.module';
|
||||
import { AccountModule } from './account/account.module';
|
||||
import { ConfigurationModule } from './config/config.module';
|
||||
|
||||
import { TranslateModule, TranslateLoader, TranslateService, MissingTranslationHandler } from "@ngx-translate/core";
|
||||
import { MyMissingTranslationHandler } from './i18n/missing-trans.handler';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { Http } from '@angular/http';
|
||||
|
||||
import { TranslateService } from "@ngx-translate/core";
|
||||
import { AppConfigService } from './app-config.service';
|
||||
|
||||
export function HttpLoaderFactory(http: Http) {
|
||||
return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json');
|
||||
}
|
||||
|
||||
export function initConfig(configService: AppConfigService) {
|
||||
return () => configService.load();
|
||||
}
|
||||
@ -52,18 +41,7 @@ export function getCurrentLanguage(translateService: TranslateService) {
|
||||
BaseModule,
|
||||
AccountModule,
|
||||
HarborRoutingModule,
|
||||
ConfigurationModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: (HttpLoaderFactory),
|
||||
deps: [Http]
|
||||
},
|
||||
missingTranslationHandler: {
|
||||
provide: MissingTranslationHandler,
|
||||
useClass: MyMissingTranslationHandler
|
||||
}
|
||||
})
|
||||
ConfigurationModule
|
||||
],
|
||||
providers: [
|
||||
AppConfigService,
|
||||
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Project } from '../../project/project';
|
||||
import { Repository } from '../../repository/repository';
|
||||
import { Repository } from 'harbor-ui';
|
||||
|
||||
export class SearchResults {
|
||||
constructor(){
|
||||
|
@ -20,21 +20,21 @@ import { ClarityModule } from 'clarity-angular';
|
||||
import { CookieModule } from 'ngx-cookie';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpModule,
|
||||
ClarityModule.forRoot(),
|
||||
CookieModule.forRoot(),
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
exports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpModule,
|
||||
ClarityModule,
|
||||
BrowserAnimationsModule
|
||||
]
|
||||
imports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpModule,
|
||||
ClarityModule.forRoot(),
|
||||
CookieModule.forRoot(),
|
||||
BrowserAnimationsModule
|
||||
],
|
||||
exports: [
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
HttpModule,
|
||||
ClarityModule,
|
||||
BrowserAnimationsModule
|
||||
]
|
||||
})
|
||||
export class CoreModule {
|
||||
}
|
||||
|
@ -21,14 +21,14 @@ import { ProjectComponent } from './project/project.component';
|
||||
import { UserComponent } from './user/user.component';
|
||||
import { ReplicationManagementComponent } from './replication/replication-management/replication-management.component';
|
||||
|
||||
import { TotalReplicationComponent } from './replication/total-replication/total-replication.component';
|
||||
import { DestinationComponent } from './replication/destination/destination.component';
|
||||
import { TotalReplicationPageComponent } from './replication/total-replication/total-replication-page.component';
|
||||
import { DestinationPageComponent } from './replication/destination/destination-page.component';
|
||||
|
||||
import { ProjectDetailComponent } from './project/project-detail/project-detail.component';
|
||||
|
||||
import { RepositoryComponent } from './repository/repository.component';
|
||||
import { RepositoryPageComponent } from './repository/repository-page.component';
|
||||
import { TagRepositoryComponent } from './repository/tag-repository/tag-repository.component';
|
||||
import { ReplicationComponent } from './replication/replication.component';
|
||||
import { ReplicationPageComponent } from './replication/replication-page.component';
|
||||
import { MemberComponent } from './project/member/member.component';
|
||||
import { AuditLogComponent } from './log/audit-log.component';
|
||||
|
||||
@ -36,7 +36,7 @@ import { ProjectRoutingResolver } from './project/project-routing-resolver.servi
|
||||
import { SystemAdminGuard } from './shared/route/system-admin-activate.service';
|
||||
import { SignUpComponent } from './account/sign-up/sign-up.component';
|
||||
import { ResetPasswordComponent } from './account/password/reset-password.component';
|
||||
import { RecentLogComponent } from './log/recent-log.component';
|
||||
import { LogPageComponent } from './log/log-page.component';
|
||||
import { ConfigurationComponent } from './config/config.component';
|
||||
import { PageNotFoundComponent } from './shared/not-found/not-found.component'
|
||||
import { StartPageComponent } from './base/start-page/start.component';
|
||||
@ -48,6 +48,8 @@ import { LeavingConfigRouteDeactivate } from './shared/route/leaving-config-deac
|
||||
|
||||
import { MemberGuard } from './shared/route/member-guard-activate.service';
|
||||
|
||||
import { TagDetailPageComponent } from './repository/tag-detail/tag-detail-page.component';
|
||||
|
||||
const harborRoutes: Routes = [
|
||||
{ path: '', redirectTo: 'harbor', pathMatch: 'full' },
|
||||
{ path: 'reset_password', component: ResetPasswordComponent },
|
||||
@ -68,7 +70,7 @@ const harborRoutes: Routes = [
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
component: RecentLogComponent
|
||||
component: LogPageComponent
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
@ -83,11 +85,11 @@ const harborRoutes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'rules',
|
||||
component: TotalReplicationComponent
|
||||
component: TotalReplicationPageComponent
|
||||
},
|
||||
{
|
||||
path: 'endpoints',
|
||||
component: DestinationComponent
|
||||
component: DestinationPageComponent
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -108,20 +110,24 @@ const harborRoutes: Routes = [
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'repository',
|
||||
component: RepositoryComponent
|
||||
path: 'repositories',
|
||||
component: RepositoryPageComponent
|
||||
},
|
||||
{
|
||||
path: 'replication',
|
||||
component: ReplicationComponent,
|
||||
path: 'repositories/:repo/tags/:tag',
|
||||
component: TagDetailPageComponent
|
||||
},
|
||||
{
|
||||
path: 'replications',
|
||||
component: ReplicationPageComponent,
|
||||
canActivate: [SystemAdminGuard]
|
||||
},
|
||||
{
|
||||
path: 'member',
|
||||
path: 'members',
|
||||
component: MemberComponent
|
||||
},
|
||||
{
|
||||
path: 'log',
|
||||
path: 'logs',
|
||||
component: AuditLogComponent
|
||||
}
|
||||
]
|
||||
|
@ -1,5 +1,11 @@
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 2px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
@ -1,62 +1,64 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
|
||||
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refresh()"><clr-icon shape="refresh"></clr-icon></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'" >
|
||||
<button class="btn btn-link" clrDropdownToggle>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="top:12px;">
|
||||
<div class="row flex-items-xs-right option-right">
|
||||
<div class="flex-xs-middle">
|
||||
<button class="btn btn-link" (click)="toggleOptionalName(currentOption)">{{toggleName[currentOption] | translate}}</button>
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchAuditLogs($event)"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right option-right" [hidden]="currentOption === 0">
|
||||
<clr-dropdown [clrMenuPosition]="'bottom-left'">
|
||||
<button class="btn btn-link" clrDropdownToggle>
|
||||
{{'AUDIT_LOG.OPERATIONS' | translate}}
|
||||
<clr-icon shape="caret down"></clr-icon>
|
||||
</button>
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)">
|
||||
<clr-icon shape="check" [hidden]="!f.checked"></clr-icon>
|
||||
<ng-template [ngIf]="!f.checked"><span style="display: inline-block;width: 16px;"></span></ng-template>
|
||||
{{f.description | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<div class="flex-xs-middle">
|
||||
<clr-icon shape="date"></clr-icon>
|
||||
<label for="fromDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="fromTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
|
||||
<div class="dropdown-menu">
|
||||
<a href="javascript:void(0)" clrDropdownItem *ngFor="let f of filterOptions" (click)="toggleFilterOption(f.key)">
|
||||
<clr-icon shape="check" [hidden]="!f.checked"></clr-icon>
|
||||
<ng-template [ngIf]="!f.checked"><span style="display: inline-block;width: 16px;"></span></ng-template>
|
||||
{{f.description | translate}}
|
||||
</a>
|
||||
</div>
|
||||
</clr-dropdown>
|
||||
<div class="flex-xs-middle">
|
||||
<clr-icon shape="date"></clr-icon>
|
||||
<label for="fromDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="fromTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
|
||||
<input id="fromDateInput" type="date" #fromTime="ngModel" name="from" [(ngModel)]="queryParam.fromTime" dateValidator placeholder="dd/mm/yyyy" (change)="doSearchByStartTime(fromTime.value)">
|
||||
<span *ngIf="fromTimeInvalid" class="tooltip-content">
|
||||
{{'AUDIT_LOG.INVALID_DATE' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<clr-icon shape="date"></clr-icon>
|
||||
<label for="toDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="toTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
|
||||
<clr-icon shape="date"></clr-icon>
|
||||
<label for="toDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="toTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
|
||||
<input id="toDateInput" type="date" #toTime="ngModel" name="to" [(ngModel)]="queryParam.toTime" dateValidator placeholder="dd/mm/yyyy" (change)="doSearchByEndTime(toTime.value)">
|
||||
<span *ngIf="toTimeInvalid" class="tooltip-content">
|
||||
{{'AUDIT_LOG.INVALID_DATE' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top ">
|
||||
<clr-datagrid (clrDgRefresh)="retrieve($event)">
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let l of auditLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{totalRecordCount}} {{'AUDIT_LOG.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12 datagrid-margin-top ">
|
||||
<clr-datagrid (clrDgRefresh)="retrieve($event)">
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let l of auditLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{totalRecordCount}} {{'AUDIT_LOG.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
3
src/ui_ng/src/app/log/log-page.component.html
Normal file
3
src/ui_ng/src/app/log/log-page.component.html
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<hbr-log withTitle="true"></hbr-log>
|
||||
</div>
|
@ -11,13 +11,12 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
export class Tag {
|
||||
digest: string;
|
||||
name: string;
|
||||
architecture: string;
|
||||
os: string;
|
||||
docker_version: string;
|
||||
author: string;
|
||||
created: Date;
|
||||
signature?: {[key: string]: any | any[]}
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'log-page',
|
||||
templateUrl: './log-page.component.html'
|
||||
})
|
||||
|
||||
export class LogPageComponent {
|
||||
}
|
@ -15,17 +15,17 @@ import { NgModule } from '@angular/core';
|
||||
import { AuditLogComponent } from './audit-log.component';
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { RecentLogComponent } from './recent-log.component';
|
||||
import { LogPageComponent } from './log-page.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule],
|
||||
declarations: [
|
||||
AuditLogComponent,
|
||||
RecentLogComponent
|
||||
LogPageComponent
|
||||
],
|
||||
providers: [AuditLogService],
|
||||
exports: [
|
||||
AuditLogComponent,
|
||||
RecentLogComponent]
|
||||
LogPageComponent]
|
||||
})
|
||||
export class LogModule { }
|
@ -1,42 +0,0 @@
|
||||
.h2-log-override {
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
.action-head-pos {
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #00bfff;
|
||||
}
|
||||
|
||||
.custom-lines-button {
|
||||
padding: 0px !important;
|
||||
min-width: 25px !important;
|
||||
}
|
||||
|
||||
.lines-button-toggole {
|
||||
font-size: 16px;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.log-select {
|
||||
width: 130px;
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.item-divider {
|
||||
height: 24px;
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
background-color: #ccc;
|
||||
opacity: 0.55;
|
||||
margin-left: 12px;
|
||||
top: 8px;
|
||||
position: relative;
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<div>
|
||||
<h2 class="h2-log-override">{{'SIDE_NAV.LOGS' | translate}}</h2>
|
||||
<div class="row flex-items-xs-between flex-items-xs-bottom">
|
||||
<div></div>
|
||||
<div class="action-head-pos">
|
||||
<div class="select log-select">
|
||||
<select id="log_display_num" (change)="handleOnchange($event)">
|
||||
<option value="10">{{'RECENT_LOG.SUB_TITLE' | translate}} 10 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
|
||||
<option value="25">{{'RECENT_LOG.SUB_TITLE' | translate}} 25 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
|
||||
<option value="50">{{'RECENT_LOG.SUB_TITLE' | translate}} 50 {{'RECENT_LOG.SUB_TITLE_SUFIX' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="item-divider"></div>
|
||||
<grid-filter filterPlaceholder='{{"AUDIT_LOG.FILTER_PLACEHOLDER" | translate}}' (filter)="doFilter($event)" [currentValue]="currentTerm"></grid-filter>
|
||||
<span (click)="refresh()" class="refresh-btn">
|
||||
<clr-icon shape="refresh" [hidden]="inProgress" ng-disabled="inProgress"></clr-icon>
|
||||
<span class="spinner spinner-inline" [hidden]="inProgress === false"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'AUDIT_LOG.USERNAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.REPOSITORY_NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TAGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'AUDIT_LOG.TIMESTAMP' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let l of recentLogs">
|
||||
<clr-dg-cell>{{l.username}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.repo_tag}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{l.op_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (recentLogs ? recentLogs.length : 0) }} {{'AUDIT_LOG.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
@ -1,112 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { AuditLog } from './audit-log';
|
||||
import { SessionUser } from '../shared/session-user';
|
||||
|
||||
import { AuditLogService } from './audit-log.service';
|
||||
import { SessionService } from '../shared/session.service';
|
||||
import { MessageService } from '../global-message/message.service';
|
||||
import { AlertType } from '../shared/shared.const';
|
||||
import { errorHandler, accessErrorHandler } from '../shared/shared.utils';
|
||||
|
||||
@Component({
|
||||
selector: 'recent-log',
|
||||
templateUrl: './recent-log.component.html',
|
||||
styleUrls: ['recent-log.component.css']
|
||||
})
|
||||
|
||||
export class RecentLogComponent implements OnInit {
|
||||
sessionUser: SessionUser = null;
|
||||
recentLogs: AuditLog[];
|
||||
logsCache: AuditLog[];
|
||||
onGoing: boolean = false;
|
||||
lines: number = 10; //Support 10, 25 and 50
|
||||
currentTerm: string;
|
||||
|
||||
constructor(
|
||||
private session: SessionService,
|
||||
private msgService: MessageService,
|
||||
private logService: AuditLogService) {
|
||||
this.sessionUser = this.session.getCurrentUser();//Initialize session
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.retrieveLogs();
|
||||
}
|
||||
|
||||
handleOnchange($event: any) {
|
||||
this.currentTerm = '';
|
||||
if ($event && $event.target && $event.target["value"]) {
|
||||
this.lines = $event.target["value"];
|
||||
if (this.lines < 10) {
|
||||
this.lines = 10;
|
||||
}
|
||||
this.retrieveLogs();
|
||||
}
|
||||
}
|
||||
|
||||
public get logNumber(): number {
|
||||
return this.recentLogs?this.recentLogs.length:0;
|
||||
}
|
||||
|
||||
public get inProgress(): boolean {
|
||||
return this.onGoing;
|
||||
}
|
||||
|
||||
public doFilter(terms: string): void {
|
||||
if (terms.trim() === "") {
|
||||
this.recentLogs = this.logsCache.filter(log => log.username != "");
|
||||
return;
|
||||
}
|
||||
this.currentTerm = terms;
|
||||
this.recentLogs = this.logsCache.filter(log => this.isMatched(terms, log));
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
this.retrieveLogs();
|
||||
}
|
||||
|
||||
retrieveLogs(): void {
|
||||
if (this.lines < 10) {
|
||||
this.lines = 10;
|
||||
}
|
||||
|
||||
this.onGoing = true;
|
||||
this.logService.getRecentLogs(this.lines)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.onGoing = false;
|
||||
this.logsCache = response; //Keep the data
|
||||
this.recentLogs = this.logsCache.filter(log => log.username != "");//To display
|
||||
},
|
||||
error => {
|
||||
this.onGoing = false;
|
||||
if (!accessErrorHandler(error, this.msgService)) {
|
||||
this.msgService.announceMessage(error.status, errorHandler(error), AlertType.DANGER);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isMatched(terms: string, log: AuditLog): boolean {
|
||||
let reg = new RegExp('.*' + terms + '.*', 'i');
|
||||
return reg.test(log.username) ||
|
||||
reg.test(log.repo_name) ||
|
||||
reg.test(log.operation) ||
|
||||
reg.test(log.repo_tag);
|
||||
}
|
||||
}
|
@ -68,7 +68,7 @@ export class ListProjectComponent {
|
||||
goToLink(proId: number): void {
|
||||
this.searchTrigger.closeSearch(true);
|
||||
|
||||
let linkUrl = ['harbor', 'projects', proId, 'repository'];
|
||||
let linkUrl = ['harbor', 'projects', proId, 'repositories'];
|
||||
this.router.navigate(linkUrl);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,15 @@
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 24px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 18px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12" style="top: 8px;">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="flex-xs-middle option-left">
|
||||
<div class="flex-xs-middle option-left" style="position: relative; top: 10px;">
|
||||
<button *ngIf="hasProjectAdminRole" class="btn btn-link" (click)="openAddMemberModal()"><clr-icon shape="add"></clr-icon> {{'MEMBER.MEMBER' | translate }}</button>
|
||||
<add-member [projectId]="projectId" (added)="addedMember($event)"></add-member>
|
||||
</div>
|
||||
<div class="flex-xs-middle option-right">
|
||||
<grid-filter filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)" [currentValue]="searchMember"></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refresh()">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"MEMBER.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearch($event)" [currentValue]="searchMember"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,19 +3,19 @@
|
||||
|
||||
<h1 class="sub-header-title">{{currentProject.name}} <span class="role-label" *ngIf="isMember">{{roleName | translate}}</span></h1>
|
||||
<nav class="subnav sub-nav-bg-color">
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="repository" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||
<a class="nav-link" routerLink="member" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||
<a class="nav-link" routerLink="log" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
|
||||
<a class="nav-link" routerLink="replication" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="repositories" routerLinkActive="active">{{'PROJECT_DETAIL.REPOSITORIES' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||
<a class="nav-link" routerLink="members" routerLinkActive="active">{{'PROJECT_DETAIL.USERS' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSystemAdmin || isMember">
|
||||
<a class="nav-link" routerLink="logs" routerLinkActive="active">{{'PROJECT_DETAIL.LOGS' | translate}}</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="isSessionValid && isSystemAdmin">
|
||||
<a class="nav-link" routerLink="replications" routerLinkActive="active">{{'PROJECT_DETAIL.REPLICATION' | translate}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
@ -24,40 +24,44 @@ export class ProjectRoutingResolver implements Resolve<Project>{
|
||||
|
||||
constructor(
|
||||
private sessionService: SessionService,
|
||||
private projectService: ProjectService,
|
||||
private router: Router) {}
|
||||
private projectService: ProjectService,
|
||||
private router: Router) { }
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Project> {
|
||||
let projectId = route.params['id'];
|
||||
//Support both parameters and query parameters
|
||||
let projectId = route.params['id'];
|
||||
if (!projectId) {
|
||||
projectId = route.queryParams['project_id'];
|
||||
}
|
||||
return this.projectService
|
||||
.getProject(projectId)
|
||||
.toPromise()
|
||||
.then((project: Project)=> {
|
||||
if(project) {
|
||||
let currentUser = this.sessionService.getCurrentUser();
|
||||
if(currentUser) {
|
||||
let projectMembers = this.sessionService.getProjectMembers();
|
||||
if(projectMembers) {
|
||||
let currentMember = projectMembers.find(m=>m.user_id === currentUser.user_id);
|
||||
if(currentMember) {
|
||||
project.is_member = true;
|
||||
project.has_project_admin_role = (currentMember.role_name === 'projectAdmin');
|
||||
project.role_name = currentMember.role_name;
|
||||
}
|
||||
}
|
||||
if(currentUser.has_admin_role === 1) {
|
||||
project.has_project_admin_role = true;
|
||||
}
|
||||
}
|
||||
return project;
|
||||
} else {
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
return null;
|
||||
}
|
||||
}).catch(error=>{
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
return null;
|
||||
});
|
||||
|
||||
}
|
||||
.getProject(projectId)
|
||||
.toPromise()
|
||||
.then((project: Project) => {
|
||||
if (project) {
|
||||
let currentUser = this.sessionService.getCurrentUser();
|
||||
if (currentUser) {
|
||||
let projectMembers = this.sessionService.getProjectMembers();
|
||||
if (projectMembers) {
|
||||
let currentMember = projectMembers.find(m => m.user_id === currentUser.user_id);
|
||||
if (currentMember) {
|
||||
project.is_member = true;
|
||||
project.has_project_admin_role = (currentMember.role_name === 'projectAdmin');
|
||||
project.role_name = currentMember.role_name;
|
||||
}
|
||||
}
|
||||
if (currentUser.has_admin_role === 1) {
|
||||
project.has_project_admin_role = true;
|
||||
}
|
||||
}
|
||||
return project;
|
||||
} else {
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
return null;
|
||||
}
|
||||
}).catch(error => {
|
||||
this.router.navigate(['/harbor', 'projects']);
|
||||
return null;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
@ -4,10 +4,16 @@
|
||||
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.refresh-btn:hover {
|
||||
color: #007CBB;
|
||||
}
|
@ -6,23 +6,23 @@
|
||||
<statistics-panel></statistics-panel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="row flex-items-xs-between" style="height:32px;">
|
||||
<div class="option-left">
|
||||
<button *ngIf="projectCreationRestriction" class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'PROJECT.PROJECT' | translate}}</button>
|
||||
<create-project (create)="createProject($event)"></create-project>
|
||||
</div>
|
||||
<div class="option-right">
|
||||
<div class="select" style="float: left;">
|
||||
<div class="select" style="float: left; left:-6px; top:8px;">
|
||||
<select (change)="doFilterProjects($event)">
|
||||
<option value="0" [selected]="currentFilteredType === 0">{{projectTypes[0] | translate}}</option>
|
||||
<option value="1">{{projectTypes[1] | translate}}</option>
|
||||
<option value="2">{{projectTypes[2] | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<grid-filter filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)" [currentValue]="projectName"></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refresh()">
|
||||
<hbr-filter [withDivider]="true" filterPlaceholder='{{"PROJECT.FILTER_PLACEHOLDER" | translate}}' (filter)="doSearchProjects($event)" [currentValue]="projectName"></hbr-filter>
|
||||
<span class="refresh-btn" (click)="refresh()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<list-project [projects]="changedProjects" [filteredType]="projectTypes[currentFilteredType]" (toggle)="toggleProject($event)" (delete)="deleteProject($event)" (paginate)="retrieve($event)"></list-project>
|
||||
|
@ -1,4 +0,0 @@
|
||||
.form-group-label-override {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<clr-modal [(clrModalOpen)]="createEditDestinationOpened" [clrModalStaticBackdrop]="staticBackdrop" [clrModalClosable]="closable">
|
||||
<h3 class="modal-title">{{modalTitle}}</h3>
|
||||
<inline-alert class="modal-title" (confirmEvt)="confirmCancel($event)"></inline-alert>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-warning" *ngIf="!editable">
|
||||
<div class="alert-item">
|
||||
<span class="alert-text">
|
||||
{{'DESTINATION.CANNOT_EDIT' | translate}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<form #targetForm="ngForm">
|
||||
<section class="form-block">
|
||||
<div class="form-group">
|
||||
<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-left">
|
||||
<input type="text" id="destination_name" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.name" name="targetName" size="20" #targetName="ngModel" required (keyup)="changedTargetName($event)">
|
||||
<span class="tooltip-content" *ngIf="targetName.errors && targetName.errors.required && (targetName.dirty || targetName.touched)">
|
||||
{{ 'DESTINATION.NAME_IS_REQUIRED' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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-left">
|
||||
<input type="text" id="destination_url" [disabled]="testOngoing" [readonly]="!editable" [(ngModel)]="target.endpoint" size="20" name="endpointUrl" #targetEndpoint="ngModel" required (keyup)="clearPassword($event)">
|
||||
<span class="tooltip-content" *ngIf="targetEndpoint.errors && targetEndpoint.errors.required && (targetEndpoint.dirty || targetEndpoint.touched)">
|
||||
{{ 'DESTINATION.URL_IS_REQUIRED' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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" [readonly]="!editable" [(ngModel)]="target.username" size="20" name="username" #username="ngModel" (keyup)="clearPassword($event)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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" [readonly]="!editable" [(ngModel)]="target.password" size="20" name="password" #password="ngModel" (focus)="clearPassword($event)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="spin" class="col-md-4"></label>
|
||||
<span class="col-md-8 spinner spinner-inline" [hidden]="!testOngoing"></span>
|
||||
<span [style.color]="!pingStatus ? 'red': ''" class="form-group-label-override">{{ pingTestMessage }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline" (click)="testConnection()" [disabled]="testOngoing || targetEndpoint.errors">{{ 'DESTINATION.TEST_CONNECTION' | translate }}</button>
|
||||
<button type="button" class="btn btn-outline" (click)="onCancel()" [disabled]="testOngoing">{{ 'BUTTON.CANCEL' | translate }}</button>
|
||||
<button type="submit" class="btn btn-primary" (click)="onSubmit()" [disabled]="testOngoing || targetForm.form.invalid || !editable">{{ 'BUTTON.OK' | translate }}</button>
|
||||
</div>
|
||||
</clr-modal>
|
@ -1,279 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, Output, EventEmitter, ViewChild, AfterViewChecked } from '@angular/core';
|
||||
import { NgForm } from '@angular/forms';
|
||||
|
||||
import { ReplicationService } from '../replication.service';
|
||||
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
|
||||
import { ActionType } from '../../shared/shared.const';
|
||||
|
||||
import { InlineAlertComponent } from '../../shared/inline-alert/inline-alert.component';
|
||||
|
||||
import { Target } from '../target';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
const FAKE_PASSWORD = 'rjGcfuRu';
|
||||
|
||||
@Component({
|
||||
selector: 'create-edit-destination',
|
||||
templateUrl: './create-edit-destination.component.html',
|
||||
styleUrls: [ 'create-edit-destination.component.css' ]
|
||||
})
|
||||
export class CreateEditDestinationComponent implements AfterViewChecked {
|
||||
|
||||
modalTitle: string;
|
||||
createEditDestinationOpened: boolean;
|
||||
|
||||
editable: boolean;
|
||||
|
||||
testOngoing: boolean;
|
||||
pingTestMessage: string;
|
||||
pingStatus: boolean;
|
||||
|
||||
actionType: ActionType;
|
||||
|
||||
target: Target = new Target();
|
||||
initVal: Target = new Target();
|
||||
|
||||
targetForm: NgForm;
|
||||
|
||||
staticBackdrop: boolean = true;
|
||||
closable: boolean = false;
|
||||
|
||||
@ViewChild('targetForm')
|
||||
currentForm: NgForm;
|
||||
|
||||
hasChanged: boolean;
|
||||
|
||||
endpointHasChanged: boolean;
|
||||
targetNameHasChanged: boolean;
|
||||
|
||||
@ViewChild(InlineAlertComponent)
|
||||
inlineAlert: InlineAlertComponent;
|
||||
|
||||
@Output() reload = new EventEmitter<boolean>();
|
||||
|
||||
constructor(
|
||||
private replicationService: ReplicationService,
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private translateService: TranslateService) {}
|
||||
|
||||
openCreateEditTarget(editable: boolean, targetId?: number) {
|
||||
|
||||
this.target = new Target();
|
||||
this.createEditDestinationOpened = true;
|
||||
this.editable = editable;
|
||||
|
||||
this.hasChanged = false;
|
||||
this.endpointHasChanged = false;
|
||||
this.targetNameHasChanged = false;
|
||||
|
||||
this.pingTestMessage = '';
|
||||
this.pingStatus = true;
|
||||
this.testOngoing = false;
|
||||
|
||||
if(targetId) {
|
||||
this.actionType = ActionType.EDIT;
|
||||
this.translateService.get('DESTINATION.TITLE_EDIT').subscribe(res=>this.modalTitle=res);
|
||||
this.replicationService
|
||||
.getTarget(targetId)
|
||||
.subscribe(
|
||||
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 = FAKE_PASSWORD;
|
||||
this.target.password = this.initVal.password;
|
||||
},
|
||||
error=>this.messageHandlerService.handleError(error)
|
||||
);
|
||||
} else {
|
||||
this.actionType = ActionType.ADD_NEW;
|
||||
this.translateService.get('DESTINATION.TITLE_ADD').subscribe(res=>this.modalTitle=res);
|
||||
}
|
||||
}
|
||||
|
||||
testConnection() {
|
||||
this.translateService.get('DESTINATION.TESTING_CONNECTION').subscribe(res=>this.pingTestMessage=res);
|
||||
this.pingStatus = true;
|
||||
this.testOngoing = !this.testOngoing;
|
||||
|
||||
let payload: Target = new Target();
|
||||
if(this.endpointHasChanged) {
|
||||
payload.endpoint = this.target.endpoint;
|
||||
payload.username = this.target.username;
|
||||
payload.password = this.target.password;
|
||||
} else {
|
||||
payload.id = this.target.id;
|
||||
}
|
||||
|
||||
this.replicationService
|
||||
.pingTarget(payload)
|
||||
.subscribe(
|
||||
response=>{
|
||||
this.pingStatus = true;
|
||||
this.translateService.get('DESTINATION.TEST_CONNECTION_SUCCESS').subscribe(res=>this.pingTestMessage=res);
|
||||
this.testOngoing = !this.testOngoing;
|
||||
},
|
||||
error=>{
|
||||
this.pingStatus = false;
|
||||
this.translateService.get('DESTINATION.TEST_CONNECTION_FAILURE').subscribe(res=>this.pingTestMessage=res);
|
||||
this.testOngoing = !this.testOngoing;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
changedTargetName($event: any) {
|
||||
if(this.editable) {
|
||||
this.targetNameHasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
clearPassword($event: any) {
|
||||
if(this.editable) {
|
||||
this.target.password = '';
|
||||
this.endpointHasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
switch(this.actionType) {
|
||||
case ActionType.ADD_NEW:
|
||||
this.replicationService
|
||||
.createTarget(this.target)
|
||||
.subscribe(
|
||||
response=>{
|
||||
this.messageHandlerService.showSuccess('DESTINATION.CREATED_SUCCESS');
|
||||
this.createEditDestinationOpened = false;
|
||||
this.reload.emit(true);
|
||||
},
|
||||
error=>{
|
||||
let errorMessageKey = '';
|
||||
switch(error.status) {
|
||||
case 409:
|
||||
errorMessageKey = 'DESTINATION.CONFLICT_NAME';
|
||||
break;
|
||||
case 400:
|
||||
errorMessageKey = 'DESTINATION.INVALID_NAME';
|
||||
break;
|
||||
default:
|
||||
errorMessageKey = 'UNKNOWN_ERROR';
|
||||
}
|
||||
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(res=>{
|
||||
if(this.messageHandlerService.isAppLevel(error)) {
|
||||
this.messageHandlerService.handleError(error);
|
||||
this.createEditDestinationOpened = false;
|
||||
} else {
|
||||
this.inlineAlert.showInlineError(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
break;
|
||||
case ActionType.EDIT:
|
||||
if(!(this.targetNameHasChanged || this.endpointHasChanged)) {
|
||||
this.createEditDestinationOpened = false;
|
||||
return;
|
||||
}
|
||||
let payload: Target = new Target();
|
||||
if(this.targetNameHasChanged) {
|
||||
payload.name = this.target.name;
|
||||
}
|
||||
if (this.endpointHasChanged) {
|
||||
payload.endpoint = this.target.endpoint;
|
||||
payload.username = this.target.username;
|
||||
payload.password = this.target.password;
|
||||
}
|
||||
this.replicationService
|
||||
.updateTarget(payload, this.target.id)
|
||||
.subscribe(
|
||||
response=>{
|
||||
this.messageHandlerService.showSuccess('DESTINATION.UPDATED_SUCCESS');
|
||||
this.createEditDestinationOpened = false;
|
||||
this.reload.emit(true);
|
||||
},
|
||||
error=>{
|
||||
let errorMessageKey = '';
|
||||
switch(error.status) {
|
||||
case 409:
|
||||
errorMessageKey = 'DESTINATION.CONFLICT_NAME';
|
||||
break;
|
||||
case 400:
|
||||
errorMessageKey = 'DESTINATION.INVALID_NAME';
|
||||
break;
|
||||
default:
|
||||
errorMessageKey = 'UNKNOWN_ERROR';
|
||||
}
|
||||
this.translateService
|
||||
.get(errorMessageKey)
|
||||
.subscribe(res=>{
|
||||
if(this.messageHandlerService.isAppLevel(error)) {
|
||||
this.messageHandlerService.handleError(error);
|
||||
this.createEditDestinationOpened = false;
|
||||
} else {
|
||||
this.inlineAlert.showInlineError(res);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
if(this.hasChanged) {
|
||||
this.inlineAlert.showInlineConfirmation({message: 'ALERT.FORM_CHANGE_CONFIRMATION'});
|
||||
} else {
|
||||
this.createEditDestinationOpened = false;
|
||||
this.targetForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
confirmCancel(confirmed: boolean) {
|
||||
this.createEditDestinationOpened = false;
|
||||
this.inlineAlert.close();
|
||||
}
|
||||
|
||||
mappedName: {} = {
|
||||
'targetName': 'name',
|
||||
'endpointUrl': 'endpoint',
|
||||
'username': 'username',
|
||||
'password': 'password'
|
||||
};
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
this.targetForm = this.currentForm;
|
||||
if(this.targetForm) {
|
||||
this.targetForm.valueChanges.subscribe(data=>{
|
||||
for(let i in data) {
|
||||
let current = data[i];
|
||||
let origin = this.initVal[this.mappedName[i]];
|
||||
if(((this.actionType === ActionType.EDIT && this.editable && !current) || current) && current !== origin) {
|
||||
this.hasChanged = true;
|
||||
break;
|
||||
} else {
|
||||
this.hasChanged = false;
|
||||
this.inlineAlert.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<div style="margin-top: 24px;">
|
||||
<hbr-endpoint></hbr-endpoint>
|
||||
</div>
|
@ -11,14 +11,11 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
export class CreateEditPolicy {
|
||||
policyId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
enable: boolean;
|
||||
targetId: number;
|
||||
targetName: string;
|
||||
endpointUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'destination-page',
|
||||
templateUrl: 'destination-page.component.html'
|
||||
})
|
||||
export class DestinationPageComponent {
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 36px;
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="flex-items-xs-middle option-left">
|
||||
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'DESTINATION.ENDPOINT' | translate}}</button>
|
||||
<create-edit-destination (reload)="reload($event)"></create-edit-destination>
|
||||
</div>
|
||||
<div class="flex-items-xs-middle option-right">
|
||||
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_TARGETS_PLACEHOLDER" | translate}}' (filter)="doSearchTargets($event)" [currentValue]="targetName"></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refreshTargets()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>{{'DESTINATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'DESTINATION.URL' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'DESTINATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *clrDgItems="let t of targets" [clrDgItem]='t'>
|
||||
<clr-dg-action-overflow>
|
||||
<button class="action-item" (click)="editTarget(t)">{{'DESTINATION.TITLE_EDIT' | translate}}</button>
|
||||
<button class="action-item" (click)="deleteTarget(t)">{{'DESTINATION.DELETE' | translate}}</button>
|
||||
</clr-dg-action-overflow>
|
||||
<clr-dg-cell>{{t.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.endpoint}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{t.creation_time | date: 'short'}}</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>{{ (targets ? targets.length : 0) }} {{'DESTINATION.ITEMS' | translate}}</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
@ -1,157 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit, ViewChild, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Target } from '../target';
|
||||
import { ReplicationService } from '../replication.service';
|
||||
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
|
||||
|
||||
import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service';
|
||||
import { ConfirmationMessage } from '../../shared/confirmation-dialog/confirmation-message';
|
||||
|
||||
import { ConfirmationTargets, ConfirmationState, ConfirmationButtons } from '../../shared/shared.const';
|
||||
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
import { CreateEditDestinationComponent } from '../create-edit-destination/create-edit-destination.component';
|
||||
|
||||
@Component({
|
||||
selector: 'destination',
|
||||
templateUrl: 'destination.component.html',
|
||||
styleUrls: ['./destination.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DestinationComponent implements OnInit {
|
||||
|
||||
@ViewChild(CreateEditDestinationComponent)
|
||||
createEditDestinationComponent: CreateEditDestinationComponent;
|
||||
|
||||
targets: Target[];
|
||||
target: Target;
|
||||
|
||||
targetName: string;
|
||||
subscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private replicationService: ReplicationService,
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private deletionDialogService: ConfirmationDialogService,
|
||||
private ref: ChangeDetectorRef) {
|
||||
this.subscription = this.deletionDialogService.confirmationConfirm$.subscribe(message => {
|
||||
if (message &&
|
||||
message.source === ConfirmationTargets.TARGET &&
|
||||
message.state === ConfirmationState.CONFIRMED) {
|
||||
let targetId = message.data;
|
||||
this.replicationService
|
||||
.deleteTarget(targetId)
|
||||
.subscribe(
|
||||
response => {
|
||||
this.messageHandlerService.showSuccess('DESTINATION.DELETED_SUCCESS');
|
||||
this.reload(true);
|
||||
},
|
||||
error => {
|
||||
if(error && error.status === 412) {
|
||||
this.messageHandlerService.showError('DESTINATION.FAILED_TO_DELETE_TARGET_IN_USED', '');
|
||||
} else {
|
||||
this.messageHandlerService.handleError(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let hnd = setInterval(()=>ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.targetName = '';
|
||||
this.retrieve('');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
retrieve(targetName: string): void {
|
||||
this.replicationService
|
||||
.listTargets(targetName)
|
||||
.subscribe(
|
||||
targets => {
|
||||
this.targets = targets || [];
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
},
|
||||
error => this.messageHandlerService.handleError(error)
|
||||
);
|
||||
}
|
||||
|
||||
doSearchTargets(targetName: string) {
|
||||
this.targetName = targetName;
|
||||
this.retrieve(targetName);
|
||||
}
|
||||
|
||||
refreshTargets() {
|
||||
this.retrieve('');
|
||||
}
|
||||
|
||||
reload($event: any) {
|
||||
this.targetName = '';
|
||||
this.retrieve('');
|
||||
}
|
||||
|
||||
openModal() {
|
||||
this.createEditDestinationComponent.openCreateEditTarget(true);
|
||||
this.target = new Target();
|
||||
}
|
||||
|
||||
editTarget(target: Target) {
|
||||
if (target) {
|
||||
let editable = true;
|
||||
this.replicationService
|
||||
.listTargetPolicies(target.id)
|
||||
.subscribe(
|
||||
policies=>{
|
||||
if(policies && policies.length > 0) {
|
||||
for(let i = 0; i < policies.length; i++){
|
||||
let p = policies[i];
|
||||
if(p.enabled === 1) {
|
||||
editable = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.createEditDestinationComponent.openCreateEditTarget(editable, target.id);
|
||||
let hnd = setInterval(()=>this.ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
},
|
||||
error=>this.messageHandlerService.handleError(error)
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
deleteTarget(target: Target) {
|
||||
if (target) {
|
||||
let targetId = target.id;
|
||||
let deletionMessage = new ConfirmationMessage(
|
||||
'REPLICATION.DELETION_TITLE_TARGET',
|
||||
'REPLICATION.DELETION_SUMMARY_TARGET',
|
||||
target.name,
|
||||
target.id,
|
||||
ConfirmationTargets.TARGET,
|
||||
ConfirmationButtons.DELETE_CANCEL);
|
||||
this.deletionDialogService.openComfirmDialog(deletionMessage);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
<clr-datagrid (clrDgRefresh)="refresh($event)">
|
||||
<clr-dg-column>{{'REPLICATION.NAME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.STATUS' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.OPERATION' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.CREATION_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.END_TIME' | translate}}</clr-dg-column>
|
||||
<clr-dg-column>{{'REPLICATION.LOGS' | translate}}</clr-dg-column>
|
||||
<clr-dg-row *ngFor="let j of jobs" [clrDgItem]='j'>
|
||||
<clr-dg-cell>{{j.repository}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.status}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.operation}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.creation_time | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{j.update_time | date: 'short'}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<a href="/api/jobs/replication/{{j.id}}/log" target="_BLANK">
|
||||
<clr-icon shape="clipboard"></clr-icon>
|
||||
</a>
|
||||
</clr-dg-cell>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{ totalRecordCount }} {{'REPLICATION.ITEMS' | translate}}
|
||||
<clr-dg-pagination [clrDgPageSize]="pageOffset" [clrDgTotalItems]="totalPage"></clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
@ -1,44 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Job } from '../job';
|
||||
import { State } from 'clarity-angular';
|
||||
import { MessageHandlerService } from '../../shared/message-handler/message-handler.service';
|
||||
|
||||
@Component({
|
||||
selector: 'list-job',
|
||||
templateUrl: 'list-job.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ListJobComponent {
|
||||
@Input() jobs: Job[];
|
||||
@Input() totalRecordCount: number;
|
||||
@Input() totalPage: number;
|
||||
@Output() paginate = new EventEmitter<State>();
|
||||
|
||||
constructor(
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private ref: ChangeDetectorRef) {
|
||||
let hnd = setInterval(()=>ref.markForCheck(), 100);
|
||||
setTimeout(()=>clearInterval(hnd), 1000);
|
||||
}
|
||||
|
||||
pageOffset: number = 1;
|
||||
|
||||
refresh(state: State) {
|
||||
if(this.jobs) {
|
||||
this.paginate.emit(state);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
/*
|
||||
{
|
||||
"id": 1,
|
||||
"project_id": 1,
|
||||
"project_name": "library",
|
||||
"target_id": 1,
|
||||
"target_name": "target_01",
|
||||
"name": "sync_01",
|
||||
"enabled": 0,
|
||||
"description": "sync_01 desc.",
|
||||
"cron_str": "",
|
||||
"start_time": "0001-01-01T00:00:00Z",
|
||||
"creation_time": "2017-02-24T06:41:52Z",
|
||||
"update_time": "2017-02-24T06:41:52Z",
|
||||
"error_job_count": 0,
|
||||
"deleted": 0
|
||||
}
|
||||
*/
|
||||
|
||||
export class Policy {
|
||||
id: number;
|
||||
project_id: number;
|
||||
project_name: string;
|
||||
target_id: number;
|
||||
target_name: string;
|
||||
name: string;
|
||||
enabled: number;
|
||||
description: string;
|
||||
cron_str: string;
|
||||
start_time: Date;
|
||||
creation_time: Date;
|
||||
update_time: Date;
|
||||
error_job_count: number;
|
||||
deleted: number;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<div style="margin-top: 24px;">
|
||||
<hbr-replication [projectId]="projectIdentify" [withReplicationJob]='true'></hbr-replication>
|
||||
</div>
|
@ -11,26 +11,19 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
/*
|
||||
{
|
||||
"id": 1,
|
||||
"status": "running",
|
||||
"repository": "library/mysql",
|
||||
"policy_id": 1,
|
||||
"operation": "transfer",
|
||||
"tags": null,
|
||||
"creation_time": "2017-02-24T06:44:04Z",
|
||||
"update_time": "2017-02-24T06:44:04Z"
|
||||
}
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
*/
|
||||
export class Job {
|
||||
id: number;
|
||||
status: string;
|
||||
repository: string;
|
||||
policy_id: number;
|
||||
operation: string;
|
||||
tags: string;
|
||||
creation_time: Date;
|
||||
update_time: Date;
|
||||
@Component({
|
||||
selector: 'replicaton',
|
||||
templateUrl: 'replication-page.component.html'
|
||||
})
|
||||
export class ReplicationPageComponent implements OnInit {
|
||||
projectIdentify: string | number;
|
||||
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectIdentify = +this.route.snapshot.parent.params['id'];
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
.option-left {
|
||||
padding-left: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.option-right {
|
||||
padding-right: 16px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.option-left-down {
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.option-right-down {
|
||||
padding-right: 16px;
|
||||
margin-top: 24px;
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<div class="flex-xs-middle option-left">
|
||||
<button class="btn btn-link" (click)="openModal()"><clr-icon shape="add"></clr-icon> {{'REPLICATION.REPLICATION_RULE' | translate}}</button>
|
||||
<create-edit-policy [projectId]="projectId" (reload)="reloadPolicies($event)"></create-edit-policy>
|
||||
</div>
|
||||
<div class="flex-xs-middle option-right">
|
||||
<div class="select" style="float: left;">
|
||||
<select (change)="doFilterPolicyStatus($event)">
|
||||
<option *ngFor="let r of ruleStatus" value="{{r.key}}">{{r.description | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_POLICIES_PLACEHOLDER" | translate}}' (filter)="doSearchPolicies($event)" [currentValue]="search.policyName"></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refreshPolicies()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<list-policy [policies]="changedPolicies" [projectless]="false" [selectedId]="initSelectedId" (selectOne)="selectOnePolicy($event)" (editOne)="openEditPolicy($event)" (reload)="reloadPolicies($event)"></list-policy>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<div class="row flex-items-xs-between">
|
||||
<h5 class="flex-items-xs-bottom option-left-down" style="margin-left: 14px;">{{'REPLICATION.REPLICATION_JOBS' | translate}}</h5>
|
||||
<div class="flex-items-xs-bottom option-right-down">
|
||||
<button class="btn btn-link" (click)="toggleSearchJobOptionalName(currentJobSearchOption)">{{toggleJobSearchOption[currentJobSearchOption] | translate}}</button>
|
||||
<grid-filter filterPlaceholder='{{"REPLICATION.FILTER_JOBS_PLACEHOLDER" | translate}}' (filter)="doSearchJobs($event)" [currentValue]="search.repoName" ></grid-filter>
|
||||
<a href="javascript:void(0)" (click)="refreshJobs()">
|
||||
<clr-icon shape="refresh"></clr-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row flex-items-xs-right option-right" [hidden]="currentJobSearchOption === 0">
|
||||
<div class="select" style="float: left;">
|
||||
<select (change)="doFilterJobStatus($event)">
|
||||
<option *ngFor="let j of jobStatus" value="{{j.key}}" [selected]="currentJobStatus.key === j.key">{{j.description | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-items-xs-middle">
|
||||
<clr-icon shape="date"></clr-icon>
|
||||
<label for="fromDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="fromTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
|
||||
<input id="fromDateInput" type="date" #fromTime="ngModel" name="from" [(ngModel)]="search.startTime" dateValidator placeholder="dd/mm/yyyy" (change)="doJobSearchByStartTime(fromTime.value)">
|
||||
<span *ngIf="fromTimeInvalid" class="tooltip-content">
|
||||
{{'AUDIT_LOG.INVALID_DATE' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
<clr-icon shape="date"></clr-icon>
|
||||
<label for="toDateInput" aria-haspopup="true" role="tooltip" [class.invalid]="toTimeInvalid" class="tooltip tooltip-validation invalid tooltip-sm">
|
||||
<input id="toDateInput" type="date" #toTime="ngModel" name="to" [(ngModel)]="search.endTime" dateValidator placeholder="dd/mm/yyyy" (change)="doJobSearchByEndTime(toTime.value)">
|
||||
<span *ngIf="toTimeInvalid" class="tooltip-content">
|
||||
{{'AUDIT_LOG.INVALID_DATE' | translate }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
|
||||
<list-job [jobs]="changedJobs" [totalPage]="jobsTotalPage" [totalRecordCount]="jobsTotalRecordCount" (paginate)="fetchPolicyJobs($event)"></list-job>
|
||||
</div>
|
||||
</div>
|
@ -1,274 +0,0 @@
|
||||
// Copyright (c) 2017 VMware, Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NgModel } from '@angular/forms';
|
||||
|
||||
import { CreateEditPolicyComponent } from '../shared/create-edit-policy/create-edit-policy.component';
|
||||
|
||||
import { MessageHandlerService } from '../shared/message-handler/message-handler.service';
|
||||
|
||||
import { ReplicationService } from './replication.service';
|
||||
|
||||
import { SessionUser } from '../shared/session-user';
|
||||
import { Policy } from './policy';
|
||||
import { Job } from './job';
|
||||
import { Target } from './target';
|
||||
|
||||
import { State } from 'clarity-angular';
|
||||
|
||||
const ruleStatus = [
|
||||
{ 'key': 'all', 'description': 'REPLICATION.ALL_STATUS'},
|
||||
{ 'key': '1', 'description': 'REPLICATION.ENABLED'},
|
||||
{ 'key': '0', 'description': 'REPLICATION.DISABLED'}
|
||||
];
|
||||
|
||||
const jobStatus = [
|
||||
{ 'key': 'all', 'description': 'REPLICATION.ALL' },
|
||||
{ 'key': 'pending', 'description': 'REPLICATION.PENDING' },
|
||||
{ 'key': 'running', 'description': 'REPLICATION.RUNNING' },
|
||||
{ 'key': 'error', 'description': 'REPLICATION.ERROR' },
|
||||
{ 'key': 'retrying', 'description': 'REPLICATION.RETRYING' },
|
||||
{ 'key': 'stopped' , 'description': 'REPLICATION.STOPPED' },
|
||||
{ 'key': 'finished', 'description': 'REPLICATION.FINISHED' },
|
||||
{ 'key': 'canceled', 'description': 'REPLICATION.CANCELED' }
|
||||
];
|
||||
|
||||
const optionalSearch: {} = {0: 'REPLICATION.ADVANCED', 1: 'REPLICATION.SIMPLE'};
|
||||
|
||||
class SearchOption {
|
||||
policyId: number;
|
||||
policyName: string = '';
|
||||
repoName: string = '';
|
||||
status: string = '';
|
||||
startTime: string = '';
|
||||
startTimestamp: string = '';
|
||||
endTime: string = '';
|
||||
endTimestamp: string = '';
|
||||
page: number = 1;
|
||||
pageSize: number = 5;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'replicaton',
|
||||
templateUrl: 'replication.component.html',
|
||||
styleUrls: ['./replication.component.css']
|
||||
})
|
||||
export class ReplicationComponent implements OnInit {
|
||||
|
||||
projectId: number;
|
||||
|
||||
search: SearchOption = new SearchOption();
|
||||
|
||||
ruleStatus = ruleStatus;
|
||||
currentRuleStatus: {key: string, description: string};
|
||||
|
||||
jobStatus = jobStatus;
|
||||
currentJobStatus: {key: string, description: string};
|
||||
|
||||
changedPolicies: Policy[];
|
||||
changedJobs: Job[];
|
||||
initSelectedId: number;
|
||||
|
||||
policies: Policy[];
|
||||
jobs: Job[];
|
||||
|
||||
jobsTotalRecordCount: number;
|
||||
jobsTotalPage: number;
|
||||
|
||||
toggleJobSearchOption = optionalSearch;
|
||||
currentJobSearchOption: number;
|
||||
|
||||
@ViewChild(CreateEditPolicyComponent)
|
||||
createEditPolicyComponent: CreateEditPolicyComponent;
|
||||
|
||||
@ViewChild('fromTime') fromTimeInput: NgModel;
|
||||
@ViewChild('toTime') toTimeInput: NgModel;
|
||||
|
||||
get fromTimeInvalid(): boolean {
|
||||
return this.fromTimeInput.errors && this.fromTimeInput.errors.dateValidator && (this.fromTimeInput.dirty || this.fromTimeInput.touched);
|
||||
}
|
||||
|
||||
get toTimeInvalid(): boolean {
|
||||
return this.toTimeInput.errors && this.toTimeInput.errors.dateValidator && (this.toTimeInput.dirty || this.toTimeInput.touched);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private messageHandlerService: MessageHandlerService,
|
||||
private replicationService: ReplicationService,
|
||||
private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.projectId = +this.route.snapshot.parent.params['id'];
|
||||
this.currentRuleStatus = this.ruleStatus[0];
|
||||
this.currentJobStatus = this.jobStatus[0];
|
||||
this.currentJobSearchOption = 0;
|
||||
this.retrievePolicies();
|
||||
|
||||
let isCreate = this.route.snapshot.parent.queryParams['is_create'];
|
||||
if (isCreate && <boolean>isCreate) {
|
||||
this.openModal();
|
||||
}
|
||||
}
|
||||
|
||||
retrievePolicies(): void {
|
||||
this.replicationService
|
||||
.listPolicies(this.search.policyName, this.projectId)
|
||||
.subscribe(
|
||||
response=>{
|
||||
this.changedPolicies = response || [];
|
||||
if(this.changedPolicies && this.changedPolicies.length > 0) {
|
||||
this.initSelectedId = this.changedPolicies[0].id;
|
||||
}
|
||||
this.policies = this.changedPolicies;
|
||||
if(this.changedPolicies && this.changedPolicies.length > 0) {
|
||||
this.search.policyId = this.changedPolicies[0].id;
|
||||
this.fetchPolicyJobs();
|
||||
}
|
||||
},
|
||||
error=>this.messageHandlerService.handleError(error)
|
||||
);
|
||||
}
|
||||
|
||||
openModal(): void {
|
||||
this.createEditPolicyComponent.openCreateEditPolicy(true);
|
||||
}
|
||||
|
||||
openEditPolicy(policy: Policy) {
|
||||
if(policy) {
|
||||
let editable = true;
|
||||
if(policy.enabled === 1) {
|
||||
editable = false;
|
||||
}
|
||||
this.createEditPolicyComponent.openCreateEditPolicy(editable, policy.id);
|
||||
}
|
||||
}
|
||||
|
||||
fetchPolicyJobs(state?: State) {
|
||||
if(state) {
|
||||
this.search.page = state.page.to + 1;
|
||||
}
|
||||
this.replicationService
|
||||
.listJobs(this.search.policyId, this.search.status, this.search.repoName,
|
||||
this.search.startTimestamp, this.search.endTimestamp, this.search.page, this.search.pageSize)
|
||||
.subscribe(
|
||||
response=>{
|
||||
this.jobsTotalRecordCount = response.headers.get('x-total-count');
|
||||
this.jobsTotalPage = Math.ceil(this.jobsTotalRecordCount / this.search.pageSize);
|
||||
this.changedJobs = response.json();
|
||||
this.jobs = this.changedJobs;
|
||||
for(let i = 0; i < this.jobs.length; i++) {
|
||||
let j = this.jobs[i];
|
||||
if(j.status == 'retrying' || j.status == 'error') {
|
||||
this.messageHandlerService.showError('REPLICATION.FOUND_ERROR_IN_JOBS', '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
error=>this.messageHandlerService.handleError(error)
|
||||
);
|
||||
}
|
||||
|
||||
selectOnePolicy(policy: Policy) {
|
||||
if(policy) {
|
||||
this.search.policyId = policy.id;
|
||||
this.search.repoName = '';
|
||||
this.search.status = '';
|
||||
this.currentJobSearchOption = 0;
|
||||
this.currentJobStatus = { 'key': 'all', 'description': 'REPLICATION.ALL' };
|
||||
this.fetchPolicyJobs();
|
||||
}
|
||||
}
|
||||
|
||||
doSearchPolicies(policyName: string) {
|
||||
this.search.policyName = policyName;
|
||||
this.retrievePolicies();
|
||||
}
|
||||
|
||||
doFilterPolicyStatus($event: any) {
|
||||
if ($event && $event.target && $event.target["value"]) {
|
||||
let status = $event.target["value"];
|
||||
this.currentRuleStatus = this.ruleStatus.find(r=>r.key === status);
|
||||
if(this.currentRuleStatus.key === 'all') {
|
||||
this.changedPolicies = this.policies;
|
||||
} else {
|
||||
this.changedPolicies = this.policies.filter(policy=>policy.enabled === +this.currentRuleStatus.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
doFilterJobStatus($event: any) {
|
||||
if ($event && $event.target && $event.target["value"]) {
|
||||
let status = $event.target["value"];
|
||||
this.currentJobStatus = this.jobStatus.find(r=>r.key === status);
|
||||
if(this.currentJobStatus.key === 'all') {
|
||||
status = '';
|
||||
}
|
||||
this.search.status = status;
|
||||
this.doSearchJobs(this.search.repoName);
|
||||
}
|
||||
}
|
||||
|
||||
doSearchJobs(repoName: string) {
|
||||
this.search.repoName = repoName;
|
||||
this.fetchPolicyJobs();
|
||||
}
|
||||
|
||||
reloadPolicies(isReady: boolean) {
|
||||
if(isReady) {
|
||||
this.search.policyName = '';
|
||||
this.retrievePolicies();
|
||||
}
|
||||
}
|
||||
|
||||
refreshPolicies() {
|
||||
this.retrievePolicies();
|
||||
}
|
||||
|
||||
refreshJobs() {
|
||||
this.fetchPolicyJobs();
|
||||
}
|
||||
|
||||
toggleSearchJobOptionalName(option: number) {
|
||||
(option === 1) ? this.currentJobSearchOption = 0 : this.currentJobSearchOption = 1;
|
||||
}
|
||||
|
||||
convertDate(strDate: string): string {
|
||||
if(/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/.test(strDate)) {
|
||||
let parts = strDate.split(/[-\/]/);
|
||||
strDate = parts[2] /*Year*/ + '-' +parts[1] /*Month*/ + '-' + parts[0] /*Date*/;
|
||||
}
|
||||
return strDate;
|
||||
}
|
||||
|
||||
doJobSearchByStartTime(strDate: string) {
|
||||
this.search.startTimestamp = '';
|
||||
if(this.fromTimeInput.valid && strDate) {
|
||||
strDate = this.convertDate(strDate);
|
||||
this.search.startTimestamp = new Date(strDate).getTime() / 1000 + '';
|
||||
}
|
||||
this.fetchPolicyJobs();
|
||||
}
|
||||
|
||||
doJobSearchByEndTime(strDate: string) {
|
||||
this.search.endTimestamp = '';
|
||||
if(this.toTimeInput.valid && strDate) {
|
||||
strDate = this.convertDate(strDate);
|
||||
let oneDayOffset = 3600 * 24;
|
||||
this.search.endTimestamp = (new Date(strDate).getTime() / 1000 + oneDayOffset) + '';
|
||||
}
|
||||
this.fetchPolicyJobs();
|
||||
}
|
||||
}
|
@ -13,31 +13,29 @@
|
||||
// limitations under the License.
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ReplicationManagementComponent } from './replication-management/replication-management.component';
|
||||
|
||||
import { ReplicationComponent } from './replication.component';
|
||||
import { ListJobComponent } from './list-job/list-job.component';
|
||||
import { TotalReplicationComponent } from './total-replication/total-replication.component';
|
||||
import { DestinationComponent } from './destination/destination.component';
|
||||
import { CreateEditDestinationComponent } from './create-edit-destination/create-edit-destination.component';
|
||||
import { ReplicationManagementComponent } from './replication-management/replication-management.component';
|
||||
import { ReplicationPageComponent } from './replication-page.component';
|
||||
import { TotalReplicationPageComponent } from './total-replication/total-replication-page.component';
|
||||
import { DestinationPageComponent } from './destination/destination-page.component';
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
import { ReplicationService } from './replication.service';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
imports: [
|
||||
SharedModule,
|
||||
RouterModule
|
||||
],
|
||||
declarations: [
|
||||
ReplicationComponent,
|
||||
declarations: [
|
||||
ReplicationPageComponent,
|
||||
ReplicationManagementComponent,
|
||||
ListJobComponent,
|
||||
TotalReplicationComponent,
|
||||
DestinationComponent,
|
||||
CreateEditDestinationComponent
|
||||
TotalReplicationPageComponent,
|
||||
DestinationPageComponent
|
||||
],
|
||||
exports: [ ReplicationComponent ],
|
||||
providers: [ ReplicationService ]
|
||||
exports: [
|
||||
ReplicationPageComponent,
|
||||
DestinationPageComponent,
|
||||
TotalReplicationPageComponent
|
||||
]
|
||||
})
|
||||
export class ReplicationModule {}
|
||||
export class ReplicationModule { }
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user