From ef3cf7454fdb728fa178b008f16e56f236769378 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Wed, 7 Jun 2017 18:23:58 +0800 Subject: [PATCH 1/9] enable pagination for user list --- src/ui_ng/src/app/user/user.component.html | 9 ++++++--- src/ui_ng/src/app/user/user.component.ts | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ui_ng/src/app/user/user.component.html b/src/ui_ng/src/app/user/user.component.html index e8ed38f66..6c351efd7 100644 --- a/src/ui_ng/src/app/user/user.component.html +++ b/src/ui_ng/src/app/user/user.component.html @@ -12,12 +12,12 @@
- + {{'USER.COLUMN_NAME' | translate}} {{'USER.COLUMN_ADMIN' | translate}} {{'USER.COLUMN_EMAIL' | translate}} {{'USER.COLUMN_REG_NAME' | translate}} - + @@ -27,7 +27,10 @@ {{user.email}} {{user.creation_time | date: 'short'}} - {{users.length}} {{'USER.ITEMS' | translate}} + {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} users + {{'USER.ITEMS' | translate}} + +
diff --git a/src/ui_ng/src/app/user/user.component.ts b/src/ui_ng/src/app/user/user.component.ts index cd90df25a..3195d9ff6 100644 --- a/src/ui_ng/src/app/user/user.component.ts +++ b/src/ui_ng/src/app/user/user.component.ts @@ -38,7 +38,7 @@ import { AppConfigService } from '../app-config.service'; export class UserComponent implements OnInit, OnDestroy { users: User[] = []; originalUsers: Promise; - private onGoing: boolean = false; + private onGoing: boolean = true; private adminMenuText: string = ""; private adminColumn: string = ""; private deletionSubscription: Subscription; From b2380818fd3a8e88a9164864c4a05a1a3a2270fc Mon Sep 17 00:00:00 2001 From: Wenkai Yin Date: Wed, 7 Jun 2017 18:26:25 +0800 Subject: [PATCH 2/9] support query logs according to conditions --- docs/swagger.yaml | 69 +++++++++++++++++++--- src/common/dao/accesslog.go | 14 +++-- src/common/models/accesslog.go | 20 +++---- src/common/utils/utils.go | 11 ++++ src/common/utils/utils_test.go | 20 +++++++ src/ui/api/harborapi_test.go | 27 ++------- src/ui/api/log.go | 25 ++++++++ src/ui/api/project.go | 61 ++++++++++--------- src/ui/api/project_test.go | 18 +++--- src/ui/router.go | 2 +- tests/apitests/apilib/access_log_filter.go | 26 ++++---- 11 files changed, 195 insertions(+), 98 deletions(-) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c2a57e99a..65575a4e9 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -217,8 +217,8 @@ paths: description: Project ID does not exist. 500: description: Unexpected internal errors. - /projects/{project_id}/logs/filter: - post: + /projects/{project_id}/logs: + get: summary: Get access logs accompany with a relevant project. description: | This endpoint let user search access logs filtered by operations and date time ranges. @@ -229,11 +229,36 @@ paths: format: int64 required: true description: Relevant project ID - - name: access_log - in: body - schema: - $ref: '#/definitions/AccessLogFilter' - description: Search results of access logs. + - name: username + in: query + type: string + required: false + description: Username of the operator. + - name: repository + in: query + type: string + required: false + description: The name of repository + - name: tag + in: query + type: string + required: false + description: The name of tag + - name: operation + in: query + type: string + required: false + description: The operation + - name: begin_timestamp + in: query + type: string + required: false + description: The begin timestamp + - name: end_timestamp + in: query + type: string + required: false + description: The end timestamp - name: page in: query type: integer @@ -861,6 +886,36 @@ paths: description: | This endpoint let user see the recent operation logs of the projects which he is member of parameters: + - name: username + in: query + type: string + required: false + description: Username of the operator. + - name: repository + in: query + type: string + required: false + description: The name of repository + - name: tag + in: query + type: string + required: false + description: The name of tag + - name: operation + in: query + type: string + required: false + description: The operation + - name: begin_timestamp + in: query + type: string + required: false + description: The begin timestamp + - name: end_timestamp + in: query + type: string + required: false + description: The end timestamp - name: page in: query type: integer diff --git a/src/common/dao/accesslog.go b/src/common/dao/accesslog.go index 654744648..b99cd7bbf 100644 --- a/src/common/dao/accesslog.go +++ b/src/common/dao/accesslog.go @@ -67,13 +67,19 @@ func logQueryConditions(query *models.LogQueryParam) orm.QuerySeter { qs = qs.Filter("username__contains", query.Username) } if len(query.Repository) != 0 { - qs = qs.Filter("repo_name", query.Repository) + qs = qs.Filter("repo_name__contains", query.Repository) } if len(query.Tag) != 0 { - qs = qs.Filter("repo_tag", query.Tag) + qs = qs.Filter("repo_tag__contains", query.Tag) } - if len(query.Operations) > 0 { - qs = qs.Filter("operation__in", query.Operations) + operations := []string{} + for _, operation := range query.Operations { + if len(operation) > 0 { + operations = append(operations, operation) + } + } + if len(operations) > 0 { + qs = qs.Filter("operation__in", operations) } if query.BeginTime != nil { qs = qs.Filter("op_time__gte", query.BeginTime) diff --git a/src/common/models/accesslog.go b/src/common/models/accesslog.go index 4a52d073a..6ddad661c 100644 --- a/src/common/models/accesslog.go +++ b/src/common/models/accesslog.go @@ -19,19 +19,15 @@ import ( ) // AccessLog holds information about logs which are used to record the actions that user take to the resourses. -// TODO remove useless attrs type AccessLog struct { - LogID int `orm:"pk;auto;column(log_id)" json:"log_id"` - Username string `orm:"column(username)" json:"username"` - ProjectID int64 `orm:"column(project_id)" json:"project_id"` - RepoName string `orm:"column(repo_name)" json:"repo_name"` - RepoTag string `orm:"column(repo_tag)" json:"repo_tag"` - GUID string `orm:"column(GUID)" json:"guid"` - Operation string `orm:"column(operation)" json:"operation"` - OpTime time.Time `orm:"column(op_time)" json:"op_time"` - Keywords string `orm:"-" json:"keywords"` - BeginTimestamp int64 `orm:"-" json:"begin_timestamp"` - EndTimestamp int64 `orm:"-" json:"end_timestamp"` + LogID int `orm:"pk;auto;column(log_id)" json:"log_id"` + Username string `orm:"column(username)" json:"username"` + ProjectID int64 `orm:"column(project_id)" json:"project_id"` + RepoName string `orm:"column(repo_name)" json:"repo_name"` + RepoTag string `orm:"column(repo_tag)" json:"repo_tag"` + GUID string `orm:"column(GUID)" json:"guid"` + Operation string `orm:"column(operation)" json:"operation"` + OpTime time.Time `orm:"column(op_time)" json:"op_time"` } // LogQueryParam is used to set query conditions when listing diff --git a/src/common/utils/utils.go b/src/common/utils/utils.go index 1ebdd5cfd..c8cdbc750 100644 --- a/src/common/utils/utils.go +++ b/src/common/utils/utils.go @@ -19,6 +19,7 @@ import ( "fmt" "net" "net/url" + "strconv" "strings" "time" @@ -114,3 +115,13 @@ func TestTCPConn(addr string, timeout, interval int) error { return fmt.Errorf("failed to connect to tcp:%s after %d seconds", addr, timeout) } } + +// ParseTimeStamp parse timestamp to time +func ParseTimeStamp(timestamp string) (*time.Time, error) { + i, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return nil, err + } + t := time.Unix(i, 0) + return &t, nil +} diff --git a/src/common/utils/utils_test.go b/src/common/utils/utils_test.go index 241ff72e3..05f041cfc 100644 --- a/src/common/utils/utils_test.go +++ b/src/common/utils/utils_test.go @@ -17,8 +17,12 @@ package utils import ( "encoding/base64" "net/http/httptest" + "strconv" "strings" "testing" + "time" + + "github.com/stretchr/testify/assert" ) func TestParseEndpoint(t *testing.T) { @@ -191,3 +195,19 @@ func TestTestTCPConn(t *testing.T) { t.Fatalf("failed to test tcp connection of %s: %v", addr, err) } } + +func TestParseTimeStamp(t *testing.T) { + // invalid input + _, err := ParseTimeStamp("") + assert.NotNil(t, err) + + // invalid input + _, err = ParseTimeStamp("invalid") + assert.NotNil(t, err) + + // valid + now := time.Now().Unix() + result, err := ParseTimeStamp(strconv.FormatInt(now, 10)) + assert.Nil(t, err) + assert.Equal(t, now, result.Unix()) +} diff --git a/src/ui/api/harborapi_test.go b/src/ui/api/harborapi_test.go index 135225287..7dd50f37c 100644 --- a/src/ui/api/harborapi_test.go +++ b/src/ui/api/harborapi_test.go @@ -96,7 +96,7 @@ func init() { beego.Router("/api/users/:id([0-9]+)/password", &UserAPI{}, "put:ChangePassword") beego.Router("/api/users/:id/sysadmin", &UserAPI{}, "put:ToggleUserAdminRole") beego.Router("/api/projects/:id/publicity", &ProjectAPI{}, "put:ToggleProjectPublic") - beego.Router("/api/projects/:id([0-9]+)/logs/filter", &ProjectAPI{}, "post:FilterAccessLog") + beego.Router("/api/projects/:id([0-9]+)/logs", &ProjectAPI{}, "get:Logs") beego.Router("/api/projects/:pid([0-9]+)/members/?:mid", &ProjectMemberAPI{}, "get:Get;post:Post;delete:Delete;put:Put") beego.Router("/api/repositories", &RepositoryAPI{}) beego.Router("/api/statistics", &StatisticAPI{}) @@ -379,27 +379,12 @@ func (a testapi) ToggleProjectPublicity(prjUsr usrInfo, projectID string, ispubl } //Get access logs accompany with a relevant project. -func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLogFilter) (int, []byte, error) { - //func (a testapi) ProjectLogsFilter(prjUsr usrInfo, projectID string, accessLog apilib.AccessLog) (int, apilib.AccessLog, error) { - _sling := sling.New().Post(a.basePath) +func (a testapi) ProjectLogs(prjUsr usrInfo, projectID string, query *apilib.LogQuery) (int, []byte, error) { + _sling := sling.New().Get(a.basePath). + Path("/api/projects/" + projectID + "/logs"). + QueryStruct(query) - path := "/api/projects/" + projectID + "/logs/filter" - - _sling = _sling.Path(path) - - // body params - _sling = _sling.BodyJSON(accessLog) - - //var successPayload []apilib.AccessLog - - httpStatusCode, body, err := request(_sling, jsonAcceptHeader, prjUsr) - /* - if err == nil && httpStatusCode == 200 { - err = json.Unmarshal(body, &successPayload) - } - */ - return httpStatusCode, body, err - // return httpStatusCode, successPayload, err + return request(_sling, jsonAcceptHeader, prjUsr) } //-------------------------Member Test---------------------------------------// diff --git a/src/ui/api/log.go b/src/ui/api/log.go index ec92bfbdf..d9fdb6640 100644 --- a/src/ui/api/log.go +++ b/src/ui/api/log.go @@ -19,6 +19,7 @@ import ( "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils" ) //LogAPI handles request api/logs @@ -43,12 +44,36 @@ func (l *LogAPI) Prepare() { func (l *LogAPI) Get() { page, size := l.GetPaginationParams() query := &models.LogQueryParam{ + Username: l.GetString("username"), + Repository: l.GetString("repository"), + Tag: l.GetString("tag"), + Operations: l.GetStrings("operation"), Pagination: &models.Pagination{ Page: page, Size: size, }, } + timestamp := l.GetString("begin_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + l.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp)) + return + } + query.BeginTime = t + } + + timestamp = l.GetString("end_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + l.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp)) + return + } + query.EndTime = t + } + if !l.isSysAdmin { projects, err := l.ProjectMgr.GetByMember(l.username) if err != nil { diff --git a/src/ui/api/project.go b/src/ui/api/project.go index 36596c2cb..c2a48959c 100644 --- a/src/ui/api/project.go +++ b/src/ui/api/project.go @@ -18,11 +18,11 @@ import ( "fmt" "net/http" "regexp" - "strings" "github.com/vmware/harbor/src/common" "github.com/vmware/harbor/src/common/dao" "github.com/vmware/harbor/src/common/models" + "github.com/vmware/harbor/src/common/utils" "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/ui/config" @@ -368,8 +368,8 @@ func (p *ProjectAPI) ToggleProjectPublic() { } } -// FilterAccessLog handles GET to /api/projects/{}/logs -func (p *ProjectAPI) FilterAccessLog() { +// Logs ... +func (p *ProjectAPI) Logs() { if !p.SecurityCtx.IsAuthenticated() { p.HandleUnauthorized() return @@ -380,51 +380,54 @@ func (p *ProjectAPI) FilterAccessLog() { return } - var query models.AccessLog - p.DecodeJSONReq(&query) - - queryParm := &models.LogQueryParam{ + page, size := p.GetPaginationParams() + query := &models.LogQueryParam{ ProjectIDs: []int64{p.project.ProjectID}, - Username: query.Username, - Repository: query.RepoName, - Tag: query.RepoTag, + Username: p.GetString("username"), + Repository: p.GetString("repository"), + Tag: p.GetString("tag"), + Operations: p.GetStrings("operation"), + Pagination: &models.Pagination{ + Page: page, + Size: size, + }, } - if len(query.Keywords) > 0 { - queryParm.Operations = strings.Split(query.Keywords, "/") + timestamp := p.GetString("begin_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + p.HandleBadRequest(fmt.Sprintf("invalid begin_timestamp: %s", timestamp)) + return + } + query.BeginTime = t } - if query.BeginTimestamp > 0 { - beginTime := time.Unix(query.BeginTimestamp, 0) - queryParm.BeginTime = &beginTime + timestamp = p.GetString("end_timestamp") + if len(timestamp) > 0 { + t, err := utils.ParseTimeStamp(timestamp) + if err != nil { + p.HandleBadRequest(fmt.Sprintf("invalid end_timestamp: %s", timestamp)) + return + } + query.EndTime = t } - if query.EndTimestamp > 0 { - endTime := time.Unix(query.EndTimestamp, 0) - queryParm.EndTime = &endTime - } - - page, pageSize := p.GetPaginationParams() - queryParm.Pagination = &models.Pagination{ - Page: page, - Size: pageSize, - } - - total, err := dao.GetTotalOfAccessLogs(queryParm) + total, err := dao.GetTotalOfAccessLogs(query) if err != nil { p.HandleInternalServerError(fmt.Sprintf( "failed to get total of access log: %v", err)) return } - logs, err := dao.GetAccessLogs(queryParm) + logs, err := dao.GetAccessLogs(query) if err != nil { p.HandleInternalServerError(fmt.Sprintf( "failed to get access log: %v", err)) return } - p.SetPaginationHeader(total, page, pageSize) + p.SetPaginationHeader(total, page, size) p.Data["json"] = logs p.ServeJSON() } diff --git a/src/ui/api/project_test.go b/src/ui/api/project_test.go index 3f725af6a..f819b53e4 100644 --- a/src/ui/api/project_test.go +++ b/src/ui/api/project_test.go @@ -320,19 +320,19 @@ func TestProjectLogsFilter(t *testing.T) { apiTest := newHarborAPI() - endTimestamp := time.Now().Unix() - startTimestamp := endTimestamp - 3600 - accessLog := &apilib.AccessLogFilter{ + query := &apilib.LogQuery{ Username: "admin", - Keywords: "", - BeginTimestamp: startTimestamp, - EndTimestamp: endTimestamp, + Repository: "", + Tag: "", + Operation: []string{""}, + BeginTimestamp: 0, + EndTimestamp: time.Now().Unix(), } //-------------------case1: Response Code=200------------------------------// fmt.Println("case 1: respose code:200") projectID := "1" - httpStatusCode, _, err := apiTest.ProjectLogsFilter(*admin, projectID, *accessLog) + httpStatusCode, _, err := apiTest.ProjectLogs(*admin, projectID, query) if err != nil { t.Error("Error while search access logs") t.Log(err) @@ -342,7 +342,7 @@ func TestProjectLogsFilter(t *testing.T) { //-------------------case2: Response Code=401:User need to log in first.------------------------------// fmt.Println("case 2: respose code:401:User need to log in first.") projectID = "1" - httpStatusCode, _, err = apiTest.ProjectLogsFilter(*unknownUsr, projectID, *accessLog) + httpStatusCode, _, err = apiTest.ProjectLogs(*unknownUsr, projectID, query) if err != nil { t.Error("Error while search access logs") t.Log(err) @@ -352,7 +352,7 @@ func TestProjectLogsFilter(t *testing.T) { //-------------------case3: Response Code=404:Project does not exist.-------------------------// fmt.Println("case 3: respose code:404:Illegal format of provided ID value.") projectID = "11111" - httpStatusCode, _, err = apiTest.ProjectLogsFilter(*admin, projectID, *accessLog) + httpStatusCode, _, err = apiTest.ProjectLogs(*admin, projectID, query) if err != nil { t.Error("Error while search access logs") t.Log(err) diff --git a/src/ui/router.go b/src/ui/router.go index 69f0f9912..a2ae3b01e 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -66,7 +66,7 @@ func initRouters() { beego.Router("/api/projects/", &api.ProjectAPI{}, "get:List;post:Post;head:Head") beego.Router("/api/projects/:id([0-9]+)", &api.ProjectAPI{}) beego.Router("/api/projects/:id([0-9]+)/publicity", &api.ProjectAPI{}, "put:ToggleProjectPublic") - beego.Router("/api/projects/:id([0-9]+)/logs/filter", &api.ProjectAPI{}, "post:FilterAccessLog") + beego.Router("/api/projects/:id([0-9]+)/logs", &api.ProjectAPI{}, "get:Logs") beego.Router("/api/statistics", &api.StatisticAPI{}) beego.Router("/api/users/?:id", &api.UserAPI{}) beego.Router("/api/users/:id([0-9]+)/password", &api.UserAPI{}, "put:ChangePassword") diff --git a/tests/apitests/apilib/access_log_filter.go b/tests/apitests/apilib/access_log_filter.go index 9c6b87296..0b6b63d68 100644 --- a/tests/apitests/apilib/access_log_filter.go +++ b/tests/apitests/apilib/access_log_filter.go @@ -1,10 +1,10 @@ -/* +/* * Harbor API * * These APIs provide services for manipulating Harbor project. * * OpenAPI spec version: 0.3.0 - * + * * Generated by: https://github.com/swagger-api/swagger-codegen.git * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,17 +22,13 @@ package apilib -type AccessLogFilter struct { - - // Relevant user's name that accessed this project. - Username string `json:"username,omitempty"` - - // Operation name specified when project created. - Keywords string `json:"keywords,omitempty"` - - // Begin timestamp for querying access logs. - BeginTimestamp int64 `json:"begin_timestamp,omitempty"` - - // End timestamp for querying accessl logs. - EndTimestamp int64 `json:"end_timestamp,omitempty"` +type LogQuery struct { + Username string `json:"username"` + Repository string `json:"repository"` + Tag string `json:"tag"` + Operation []string `json:"operation"` + BeginTimestamp int64 `json:"begin_timestamp"` + EndTimestamp int64 `json:"end_timestamp"` + Page int64 `json:"page"` + PageSize int64 `json:"page_size"` } From 0c15dac2418e86af905db25592a1122a8f8a50f4 Mon Sep 17 00:00:00 2001 From: kunw Date: Thu, 8 Jun 2017 18:37:16 +0800 Subject: [PATCH 3/9] Fix global message display issue --- src/ui_ng/src/app/global-message/message.component.html | 2 +- src/ui_ng/src/app/global-message/message.component.ts | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ui_ng/src/app/global-message/message.component.html b/src/ui_ng/src/app/global-message/message.component.html index 349f4b762..d99d548b6 100644 --- a/src/ui_ng/src/app/global-message/message.component.html +++ b/src/ui_ng/src/app/global-message/message.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/ui_ng/src/app/global-message/message.component.ts b/src/ui_ng/src/app/global-message/message.component.ts index 5e10ddb52..0d86ce234 100644 --- a/src/ui_ng/src/app/global-message/message.component.ts +++ b/src/ui_ng/src/app/global-message/message.component.ts @@ -33,8 +33,7 @@ export class MessageComponent implements OnInit, OnDestroy { globalMessageOpened: boolean; messageText: string = ""; timer: any = null; - hideOuter: boolean = true; - + appLevelMsgSub: Subscription; msgSub: Subscription; clearSub: Subscription; @@ -79,8 +78,6 @@ export class MessageComponent implements OnInit, OnDestroy { let hackDom: any = queryDoms[0]; hackDom.className += ' alert-global alert-global-align'; } - - this.hideOuter = false; }, 0); } @@ -138,6 +135,5 @@ export class MessageComponent implements OnInit, OnDestroy { clearTimeout(this.timer); } this.globalMessageOpened = false; - this.hideOuter = true; } } \ No newline at end of file From a905d72c5981910c9da1caba6f3e543d46471050 Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Fri, 9 Jun 2017 09:13:59 +0800 Subject: [PATCH 4/9] fix typo and refine message in content trust interceptor --- src/ui/proxy/interceptors.go | 5 +++-- src/ui/proxy/proxy.go | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/proxy/interceptors.go b/src/ui/proxy/interceptors.go index 84d9f1a47..c68e86ee3 100644 --- a/src/ui/proxy/interceptors.go +++ b/src/ui/proxy/interceptors.go @@ -60,6 +60,7 @@ func (ec envPolicyChecker) contentTrustEnabled(name string) bool { return os.Getenv("PROJECT_CONTENT_TRUST") == "1" } func (ec envPolicyChecker) vulnerableEnabled(name string) bool { + // TODO: May need get more information in vulnerable policies. return os.Getenv("PROJECT_VULNERABBLE") == "1" } @@ -161,8 +162,8 @@ func (cth contentTrustHandler) ServeHTTP(rw http.ResponseWriter, req *http.Reque log.Debugf("Passing the response to outter responseWriter") copyResp(rec, rw) } else { - log.Debugf("digest miamatch, failing the response.") - http.Error(rw, "Failure in content trust handler", http.StatusPreconditionFailed) + log.Debugf("digest mismatch, failing the response.") + http.Error(rw, "The image is not signed in Notary.", http.StatusPreconditionFailed) } } diff --git a/src/ui/proxy/proxy.go b/src/ui/proxy/proxy.go index 8e63ec67f..b580d7aff 100644 --- a/src/ui/proxy/proxy.go +++ b/src/ui/proxy/proxy.go @@ -41,6 +41,7 @@ func Init(urls ...string) error { return err } Proxy = httputil.NewSingleHostReverseProxy(targetURL) + //TODO: add vulnerable interceptor. handlers = handlerChain{head: urlHandler{next: contentTrustHandler{next: Proxy}}} return nil } From c342b6a56030bff002f6dc2bbe76ccd07e8948f0 Mon Sep 17 00:00:00 2001 From: kunw Date: Fri, 9 Jun 2017 10:54:27 +0800 Subject: [PATCH 5/9] Update for rule creation and styles. --- src/ui_ng/lib/src/replication/replication.component.html.ts | 2 +- src/ui_ng/lib/src/replication/replication.component.ts | 3 --- .../repository-stackview/repository-stackview.component.css.ts | 1 - src/ui_ng/lib/src/repository/repository.component.css.ts | 1 - 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/ui_ng/lib/src/replication/replication.component.html.ts b/src/ui_ng/lib/src/replication/replication.component.html.ts index e37406d78..9c8f4984c 100644 --- a/src/ui_ng/lib/src/replication/replication.component.html.ts +++ b/src/ui_ng/lib/src/replication/replication.component.html.ts @@ -3,7 +3,7 @@ export const REPLICATION_TEMPLATE: string = `
- +
diff --git a/src/ui_ng/lib/src/replication/replication.component.ts b/src/ui_ng/lib/src/replication/replication.component.ts index e09447d64..06f42e638 100644 --- a/src/ui_ng/lib/src/replication/replication.component.ts +++ b/src/ui_ng/lib/src/replication/replication.component.ts @@ -114,9 +114,6 @@ export class ReplicationComponent implements OnInit { } ngOnInit() { - if(!this.projectId) { - this.errorHandler.warning('Project ID is unset.'); - } this.currentRuleStatus = this.ruleStatus[0]; this.currentJobStatus = this.jobStatus[0]; this.currentJobSearchOption = 0; diff --git a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts index b9a0b8086..fdbaab321 100644 --- a/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts +++ b/src/ui_ng/lib/src/repository-stackview/repository-stackview.component.css.ts @@ -1,7 +1,6 @@ export const REPOSITORY_STACKVIEW_STYLES: string = ` .option-right { padding-right: 16px; - margin-top: 32px; margin-bottom: 12px; } diff --git a/src/ui_ng/lib/src/repository/repository.component.css.ts b/src/ui_ng/lib/src/repository/repository.component.css.ts index fca99e94a..f443c6a1b 100644 --- a/src/ui_ng/lib/src/repository/repository.component.css.ts +++ b/src/ui_ng/lib/src/repository/repository.component.css.ts @@ -1,5 +1,4 @@ export const REPOSITORY_STYLE = `.option-right { padding-right: 16px; - margin-top: 32px; margin-bottom: 12px; }`; \ No newline at end of file From 85c9aeafad68aa59aaf99b817e167f023bef48d8 Mon Sep 17 00:00:00 2001 From: kunw Date: Fri, 9 Jun 2017 14:37:38 +0800 Subject: [PATCH 6/9] Update audit log query params. --- src/ui_ng/src/app/log/audit-log.component.ts | 4 ++-- src/ui_ng/src/app/log/audit-log.service.ts | 17 ++++++++--------- src/ui_ng/src/app/log/audit-log.ts | 12 ++++++------ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/ui_ng/src/app/log/audit-log.component.ts b/src/ui_ng/src/app/log/audit-log.component.ts index 6444b7a26..b7b8a27e5 100644 --- a/src/ui_ng/src/app/log/audit-log.component.ts +++ b/src/ui_ng/src/app/log/audit-log.component.ts @@ -151,7 +151,7 @@ export class AuditLogComponent implements OnInit { for(var i in this.filterOptions) { let filterOption = this.filterOptions[i]; if(filterOption.checked) { - operationFilter.push(this.filterOptions[i].key); + operationFilter.push('operation=' + this.filterOptions[i].key); }else{ selectAll = false; } @@ -159,7 +159,7 @@ export class AuditLogComponent implements OnInit { if(selectAll) { operationFilter = []; } - this.queryParam.keywords = operationFilter.join('/'); + this.queryParam.keywords = operationFilter.join('&'); this.retrieve(); } diff --git a/src/ui_ng/src/app/log/audit-log.service.ts b/src/ui_ng/src/app/log/audit-log.service.ts index 4b1d8f11a..48c8bcb6a 100644 --- a/src/ui_ng/src/app/log/audit-log.service.ts +++ b/src/ui_ng/src/app/log/audit-log.service.ts @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. import { Injectable } from '@angular/core'; -import { Http, Headers, RequestOptions } from '@angular/http'; +import { Http, Headers, RequestOptions, URLSearchParams } from '@angular/http'; import { AuditLog } from './audit-log'; @@ -35,15 +35,14 @@ export class AuditLogService { constructor(private http: Http) {} listAuditLogs(queryParam: AuditLog): Observable { + let params: URLSearchParams = new URLSearchParams(queryParam.keywords); + params.set('begin_timestamp', queryParam.begin_timestamp); + params.set('end_timestamp', queryParam.end_timestamp); + params.set('username', queryParam.username); + params.set('page', queryParam.page); + params.set('page_size', queryParam.page_size); return this.http - .post(`/api/projects/${queryParam.project_id}/logs/filter?page=${queryParam.page}&page_size=${queryParam.page_size}`, { - begin_timestamp: queryParam.begin_timestamp, - end_timestamp: queryParam.end_timestamp, - keywords: queryParam.keywords, - operation: queryParam.operation, - project_id: queryParam.project_id, - username: queryParam.username - }) + .get(`/api/projects/${queryParam.project_id}/logs`, {params: params}) .map(response => response) .catch(error => Observable.throw(error)); } diff --git a/src/ui_ng/src/app/log/audit-log.ts b/src/ui_ng/src/app/log/audit-log.ts index d90da1ad7..ac94cd35b 100644 --- a/src/ui_ng/src/app/log/audit-log.ts +++ b/src/ui_ng/src/app/log/audit-log.ts @@ -30,18 +30,18 @@ } */ export class AuditLog { - log_id: number; - project_id: number; + log_id: number | string; + project_id: number | string; username: string; repo_name: string; repo_tag: string; operation: string; op_time: Date; - begin_timestamp: number = 0; - end_timestamp: number = 0; + begin_timestamp: number | string; + end_timestamp: number | string; keywords: string; - page: number; - page_size: number; + page: number | string; + page_size: number | string; fromTime: string; toTime: string; } \ No newline at end of file From 58c499397465ae7738c1043bc788387f4354cecb Mon Sep 17 00:00:00 2001 From: Tan Jiang Date: Fri, 9 Jun 2017 14:28:51 +0800 Subject: [PATCH 7/9] add handlers in statemachine --- src/common/models/clair.go | 59 +++++++++++++++++++++++++ src/jobservice/api/scan.go | 6 ++- src/jobservice/job/job_test.go | 2 +- src/jobservice/job/jobs.go | 12 ++--- src/jobservice/job/statemachine.go | 22 +++++++++- src/jobservice/scan/context.go | 44 +++++++++++++++++++ src/jobservice/scan/handlers.go | 70 ++++++++++++++++++++++++++++++ 7 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 src/common/models/clair.go create mode 100644 src/jobservice/scan/context.go create mode 100644 src/jobservice/scan/handlers.go diff --git a/src/common/models/clair.go b/src/common/models/clair.go new file mode 100644 index 000000000..13f123ad5 --- /dev/null +++ b/src/common/models/clair.go @@ -0,0 +1,59 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package models + +//ClairLayer ... +type ClairLayer struct { + Name string `json:"Name,omitempty"` + NamespaceNames []string `json:"NamespaceNames,omitempty"` + Path string `json:"Path,omitempty"` + Headers map[string]string `json:"Headers,omitempty"` + ParentName string `json:"ParentName,omitempty"` + Format string `json:"Format,omitempty"` + Features []ClairFeature `json:"Features,omitempty"` +} + +//ClairFeature ... +type ClairFeature struct { + Name string `json:"Name,omitempty"` + NamespaceName string `json:"NamespaceName,omitempty"` + VersionFormat string `json:"VersionFormat,omitempty"` + Version string `json:"Version,omitempty"` + Vulnerabilities []ClairVulnerability `json:"Vulnerabilities,omitempty"` + AddedBy string `json:"AddedBy,omitempty"` +} + +//ClairVulnerability ... +type ClairVulnerability struct { + Name string `json:"Name,omitempty"` + NamespaceName string `json:"NamespaceName,omitempty"` + Description string `json:"Description,omitempty"` + Link string `json:"Link,omitempty"` + Severity string `json:"Severity,omitempty"` + Metadata map[string]interface{} `json:"Metadata,omitempty"` + FixedBy string `json:"FixedBy,omitempty"` + FixedIn []ClairFeature `json:"FixedIn,omitempty"` +} + +//ClairError ... +type ClairError struct { + Message string `json:"Message,omitempty"` +} + +//ClairLayerEnvelope ... +type ClairLayerEnvelope struct { + Layer *ClairLayer `json:"Layer,omitempty"` + Error *ClairError `json:"Error,omitempty"` +} diff --git a/src/jobservice/api/scan.go b/src/jobservice/api/scan.go index 123c2b763..5ead3f7de 100644 --- a/src/jobservice/api/scan.go +++ b/src/jobservice/api/scan.go @@ -22,6 +22,7 @@ import ( "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/common/utils/registry/auth" "github.com/vmware/harbor/src/jobservice/config" + "github.com/vmware/harbor/src/jobservice/job" "github.com/vmware/harbor/src/jobservice/utils" ) @@ -83,5 +84,8 @@ func (isj *ImageScanJob) Post() { isj.RenderError(http.StatusInternalServerError, "Failed to insert scan job data.") return } - log.Debugf("job id: %d", jid) + log.Debugf("Scan job id: %d", jid) + sj := job.NewScanJob(jid) + log.Debugf("Sent job to scheduler, job: %v", sj) + job.Schedule(sj) } diff --git a/src/jobservice/job/job_test.go b/src/jobservice/job/job_test.go index 8a4321ce0..03cc0863a 100644 --- a/src/jobservice/job/job_test.go +++ b/src/jobservice/job/job_test.go @@ -124,7 +124,7 @@ func TestScanJob(t *testing.T) { assert.Nil(err) j, err := dao.GetScanJob(scanJobID) assert.Equal(models.JobRetrying, j.Status) - assert.Equal("sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f", sj.parm.digest) + assert.Equal("sha256:0204dc6e09fa57ab99ac40e415eb637d62c8b2571ecbbc9ca0eb5e2ad2b5c56f", sj.parm.Digest) sj2 := NewScanJob(99999) err = sj2.Init() assert.NotNil(err) diff --git a/src/jobservice/job/jobs.go b/src/jobservice/job/jobs.go index 4ecd0dd76..4a5eb48c6 100644 --- a/src/jobservice/job/jobs.go +++ b/src/jobservice/job/jobs.go @@ -176,9 +176,9 @@ type ScanJob struct { //ScanJobParm wraps the parms of a image scan job. type ScanJobParm struct { - repository string - tag string - digest string + Repository string + Tag string + Digest string } //ID returns the id of the scan @@ -216,9 +216,9 @@ func (sj *ScanJob) Init() error { return fmt.Errorf("The job doesn't exist in DB, job id: %d", sj.id) } sj.parm = &ScanJobParm{ - repository: job.Repository, - tag: job.Tag, - digest: job.Digest, + Repository: job.Repository, + Tag: job.Tag, + Digest: job.Digest, } return nil } diff --git a/src/jobservice/job/statemachine.go b/src/jobservice/job/statemachine.go index faeb14b96..52d6a1f1f 100644 --- a/src/jobservice/job/statemachine.go +++ b/src/jobservice/job/statemachine.go @@ -22,6 +22,7 @@ import ( "github.com/vmware/harbor/src/common/utils/log" "github.com/vmware/harbor/src/jobservice/config" "github.com/vmware/harbor/src/jobservice/replication" + "github.com/vmware/harbor/src/jobservice/scan" ) // SM is the state machine to handle job, it handles one job at a time. @@ -231,7 +232,12 @@ func (sm *SM) initTransitions() error { return fmt.Errorf("unsupported operation: %s", jobParm.Operation) } case ScanType: - log.Debugf("TODO for scan job, job: %v", sm.CurrentJob) + scanJob, ok := sm.CurrentJob.(*ScanJob) + if !ok { + //Shouldn't be here. + return fmt.Errorf("The job: %v is not a type of ScanJob", sm.CurrentJob) + } + addImgScanTransition(sm, scanJob.parm) return nil default: return fmt.Errorf("Unsupported job type: %v", sm.CurrentJob.Type()) @@ -247,6 +253,20 @@ func addTestTransition(sm *SM) error { } */ +func addImgScanTransition(sm *SM, parm *ScanJobParm) { + ctx := &scan.JobContext{ + Repository: parm.Repository, + Tag: parm.Tag, + Digest: parm.Digest, + Logger: sm.Logger, + } + + sm.AddTransition(models.JobRunning, scan.StateInitialize, &scan.Initializer{Context: ctx}) + sm.AddTransition(scan.StateInitialize, scan.StateScanLayer, &scan.LayerScanHandler{Context: ctx}) + sm.AddTransition(scan.StateScanLayer, scan.StateSummarize, &scan.SummarizeHandler{Context: ctx}) + sm.AddTransition(scan.StateSummarize, models.JobFinished, &StatusUpdater{sm.CurrentJob, models.JobFinished}) +} + func addImgTransferTransition(sm *SM, parm *RepJobParm) { base := replication.InitBaseHandler(parm.Repository, parm.LocalRegURL, config.JobserviceSecret(), parm.TargetURL, parm.TargetUsername, parm.TargetPassword, diff --git a/src/jobservice/scan/context.go b/src/jobservice/scan/context.go new file mode 100644 index 000000000..bbdb1097d --- /dev/null +++ b/src/jobservice/scan/context.go @@ -0,0 +1,44 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scan + +import ( + "github.com/vmware/harbor/src/common/utils/log" +) + +const ( + // StateInitialize in this state the handler will initialize the job context. + StateInitialize = "initialize" + // StateScanLayer in this state the handler will POST layer of clair to scan layer by layer of the image. + StateScanLayer = "scanlayer" + // StateSummarize in this state, the layers are scanned by clair it will call clair API to update vulnerability overview in Harbor DB. After this state, the job is finished. + StateSummarize = "summarize" +) + +//JobContext is for sharing data across handlers in a execution of a scan job. +type JobContext struct { + Repository string + Tag string + Digest string + //the digests of layers + layers []string + //each layer name has to be unique, so it should be ${img-digest}-${layer-digest} + layerNames []string + //the index of current layer + current int + //token for accessing the registry + token string + Logger *log.Logger +} diff --git a/src/jobservice/scan/handlers.go b/src/jobservice/scan/handlers.go new file mode 100644 index 000000000..e27296f6d --- /dev/null +++ b/src/jobservice/scan/handlers.go @@ -0,0 +1,70 @@ +// Copyright (c) 2017 VMware, Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scan + +import ( + "github.com/vmware/harbor/src/common/models" +) + +// Initializer will handle the initialise state pull the manifest, prepare token. +type Initializer struct { + Context *JobContext +} + +// Enter ... +func (iz *Initializer) Enter() (string, error) { + logger := iz.Context.Logger + logger.Infof("Entered scan initializer") + return StateScanLayer, nil +} + +// Exit ... +func (iz *Initializer) Exit() error { + return nil +} + +//LayerScanHandler will call clair API to trigger scanning. +type LayerScanHandler struct { + Context *JobContext +} + +// Enter ... +func (ls *LayerScanHandler) Enter() (string, error) { + logger := ls.Context.Logger + logger.Infof("Entered scan layer handler") + return StateSummarize, nil +} + +// Exit ... +func (ls *LayerScanHandler) Exit() error { + return nil +} + +// SummarizeHandler will summarize the vulnerability and feature information of Clair, and store into Harbor's DB. +type SummarizeHandler struct { + Context *JobContext +} + +// Enter ... +func (sh *SummarizeHandler) Enter() (string, error) { + logger := sh.Context.Logger + logger.Infof("Entered summarize handler") + return models.JobFinished, nil +} + +// Exit ... +func (sh *SummarizeHandler) Exit() error { + return nil +} From dc56b50b1e5c7cca3cb522dc9cfec757badfdc19 Mon Sep 17 00:00:00 2001 From: Steven Zou Date: Fri, 9 Jun 2017 15:46:16 +0800 Subject: [PATCH 8/9] add push-image button component --- src/ui_ng/lib/package.json | 145 +++++++++--------- src/ui_ng/lib/src/harbor-library.module.ts | 5 +- src/ui_ng/lib/src/index.ts | 3 +- .../src/push-image/copy-input.component.ts | 48 ++++++ .../lib/src/push-image/copy-input.html.ts | 15 ++ src/ui_ng/lib/src/push-image/index.ts | 11 ++ .../push-image/push-image.component.spec.ts | 64 ++++++++ .../src/push-image/push-image.component.ts | 51 ++++++ .../lib/src/push-image/push-image.css.ts | 42 +++++ .../lib/src/push-image/push-image.html.ts | 34 ++++ src/ui_ng/lib/src/shared/shared.module.ts | 3 + 11 files changed, 347 insertions(+), 74 deletions(-) create mode 100644 src/ui_ng/lib/src/push-image/copy-input.component.ts create mode 100644 src/ui_ng/lib/src/push-image/copy-input.html.ts create mode 100644 src/ui_ng/lib/src/push-image/index.ts create mode 100644 src/ui_ng/lib/src/push-image/push-image.component.spec.ts create mode 100644 src/ui_ng/lib/src/push-image/push-image.component.ts create mode 100644 src/ui_ng/lib/src/push-image/push-image.css.ts create mode 100644 src/ui_ng/lib/src/push-image/push-image.html.ts diff --git a/src/ui_ng/lib/package.json b/src/ui_ng/lib/package.json index 32060452f..df516dae1 100644 --- a/src/ui_ng/lib/package.json +++ b/src/ui_ng/lib/package.json @@ -1,73 +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", - "@webcomponents/custom-elements": "1.0.0-alpha.3", - "web-animations-js": "^2.2.1", - "clarity-angular": "^0.9.7", - "clarity-icons": "^0.9.7", - "clarity-ui": "^0.9.7", - "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", - "intl": "^1.2.5" - }, - "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" - } -} \ No newline at end of file + "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" + } +} diff --git a/src/ui_ng/lib/src/harbor-library.module.ts b/src/ui_ng/lib/src/harbor-library.module.ts index 186311cbf..27a645976 100644 --- a/src/ui_ng/lib/src/harbor-library.module.ts +++ b/src/ui_ng/lib/src/harbor-library.module.ts @@ -21,6 +21,7 @@ import { CONFIRMATION_DIALOG_DIRECTIVES } from './confirmation-dialog/index'; import { INLINE_ALERT_DIRECTIVES } from './inline-alert/index'; import { DATETIME_PICKER_DIRECTIVES } from './datetime-picker/index'; import { VULNERABILITY_DIRECTIVES } from './vulnerability-scanning/index'; +import { PUSH_IMAGE_BUTTON_DIRECTIVES } from './push-image/index'; import { SystemInfoService, @@ -142,7 +143,8 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co LIST_REPLICATION_RULE_DIRECTIVES, CREATE_EDIT_RULE_DIRECTIVES, DATETIME_PICKER_DIRECTIVES, - VULNERABILITY_DIRECTIVES + VULNERABILITY_DIRECTIVES, + PUSH_IMAGE_BUTTON_DIRECTIVES ], exports: [ LOG_DIRECTIVES, @@ -160,6 +162,7 @@ export function initConfig(translateInitializer: TranslateServiceInitializer, co CREATE_EDIT_RULE_DIRECTIVES, DATETIME_PICKER_DIRECTIVES, VULNERABILITY_DIRECTIVES, + PUSH_IMAGE_BUTTON_DIRECTIVES, TranslateModule ], providers: [] diff --git a/src/ui_ng/lib/src/index.ts b/src/ui_ng/lib/src/index.ts index 6b5ee7e77..904b3785b 100644 --- a/src/ui_ng/lib/src/index.ts +++ b/src/ui_ng/lib/src/index.ts @@ -12,4 +12,5 @@ export * from './tag/index'; export * from './list-replication-rule/index'; export * from './replication/index'; export * from './vulnerability-scanning/index'; -export * from './i18n/index'; \ No newline at end of file +export * from './i18n/index'; +export * from './push-image/index'; \ No newline at end of file diff --git a/src/ui_ng/lib/src/push-image/copy-input.component.ts b/src/ui_ng/lib/src/push-image/copy-input.component.ts new file mode 100644 index 000000000..081a7c8f1 --- /dev/null +++ b/src/ui_ng/lib/src/push-image/copy-input.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +import { COPY_INPUT_HTML } from './copy-input.html'; +import { PUSH_IMAGE_STYLE } from './push-image.css'; + +export const enum CopyStatus { + NORMAL, SUCCESS, ERROR +} + +@Component({ + selector: 'hbr-copy-input', + styles: [PUSH_IMAGE_STYLE], + template: COPY_INPUT_HTML, + + providers: [] +}) +export class CopyInputComponent { + @Input() inputSize: number = 40; + @Input() headerTitle: string = "Copy Input"; + @Input() defaultValue: string = "N/A"; + + state: CopyStatus = CopyStatus.NORMAL; + + @Output() onCopySuccess: EventEmitter = new EventEmitter(); + @Output() onCopyError: EventEmitter = new EventEmitter(); + + onSuccess($event: any): void { + this.state = CopyStatus.SUCCESS; + this.onCopySuccess.emit($event); + } + + onError(error: any): void { + this.state = CopyStatus.ERROR; + this.onCopyError.emit(error); + } + + reset(): void { + this.state = CopyStatus.NORMAL; + } + + public get isCopied(): boolean { + return this.state === CopyStatus.SUCCESS; + } + + public get hasCopyError(): boolean { + return this.state === CopyStatus.ERROR; + } +} diff --git a/src/ui_ng/lib/src/push-image/copy-input.html.ts b/src/ui_ng/lib/src/push-image/copy-input.html.ts new file mode 100644 index 000000000..408345d66 --- /dev/null +++ b/src/ui_ng/lib/src/push-image/copy-input.html.ts @@ -0,0 +1,15 @@ +export const COPY_INPUT_HTML: string = ` +
+
+ {{headerTitle}} +
+
+ + + + + + +
+
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/push-image/index.ts b/src/ui_ng/lib/src/push-image/index.ts new file mode 100644 index 000000000..4a46845ec --- /dev/null +++ b/src/ui_ng/lib/src/push-image/index.ts @@ -0,0 +1,11 @@ +import { Type } from "@angular/core"; +import { PushImageButtonComponent } from './push-image.component'; +import { CopyInputComponent } from './copy-input.component'; + +export * from "./push-image.component"; +export * from './copy-input.component'; + +export const PUSH_IMAGE_BUTTON_DIRECTIVES: Type[] = [ + CopyInputComponent, + PushImageButtonComponent +]; \ No newline at end of file diff --git a/src/ui_ng/lib/src/push-image/push-image.component.spec.ts b/src/ui_ng/lib/src/push-image/push-image.component.spec.ts new file mode 100644 index 000000000..9b266712b --- /dev/null +++ b/src/ui_ng/lib/src/push-image/push-image.component.spec.ts @@ -0,0 +1,64 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { HttpModule } from '@angular/http'; +import { DebugElement } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { PushImageButtonComponent } from './push-image.component'; +import { CopyInputComponent } from './copy-input.component'; +import { InlineAlertComponent } from '../inline-alert/inline-alert.component'; + +import { SERVICE_CONFIG, IServiceConfig } from '../service.config'; +import { SharedModule } from '../shared/shared.module'; +import { click } from '../utils'; + +describe('PushImageButtonComponent (inline template)', () => { + let component: PushImageButtonComponent; + let fixture: ComponentFixture; + let serviceConfig: IServiceConfig; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + SharedModule + ], + declarations: [InlineAlertComponent, CopyInputComponent, PushImageButtonComponent], + providers: [ + { provide: SERVICE_CONFIG, useValue: {} } + ] + }); + + })); + + beforeEach(() => { + fixture = TestBed.createComponent(PushImageButtonComponent); + component = fixture.componentInstance; + component.projectName = "testing"; + component.registryUrl = "https://testing.harbor.com" + serviceConfig = TestBed.get(SERVICE_CONFIG); + + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should open the drop-down panel', async(() => { + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + let el: HTMLElement = fixture.nativeElement.querySelector('button'); + expect(el).not.toBeNull(); + el.click(); + + fixture.detectChanges(); + let copyInputs: HTMLInputElement[] = fixture.nativeElement.querySelectorAll('.command-input'); + expect(copyInputs.length).toEqual(2); + + expect(copyInputs[0].value.trim()).toEqual(`docker tag SOURCE_IMAGE[:TAG] ${component.registryUrl}/${component.projectName}/IMAGE[:TAG]`); + expect(copyInputs[1].value.trim()).toEqual(`docker push ${component.registryUrl}/${component.projectName}/IMAGE[:TAG]`); + }); + })); + +}); diff --git a/src/ui_ng/lib/src/push-image/push-image.component.ts b/src/ui_ng/lib/src/push-image/push-image.component.ts new file mode 100644 index 000000000..ab776ebc9 --- /dev/null +++ b/src/ui_ng/lib/src/push-image/push-image.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { CopyInputComponent } from './copy-input.component'; +import { InlineAlertComponent } from '../inline-alert/inline-alert.component'; + +import { PUSH_IMAGE_STYLE } from './push-image.css'; +import { PUSH_IMAGE_HTML } from './push-image.html'; + +@Component({ + selector: 'hbr-push-image-button', + template: PUSH_IMAGE_HTML, + styles: [PUSH_IMAGE_STYLE], + + providers: [] +}) +export class PushImageButtonComponent { + @Input() registryUrl: string = "unknown"; + @Input() projectName: string = "unknown"; + + @ViewChild("tagCopy") tagCopyInput: CopyInputComponent; + @ViewChild("pushCopy") pushCopyInput: CopyInputComponent; + @ViewChild("copyAlert") copyAlert: InlineAlertComponent; + + + public get tagCommand(): string { + return `docker tag SOURCE_IMAGE[:TAG] ${this.registryUrl}/${this.projectName}/IMAGE[:TAG]`; + } + + public get pushCommand(): string { + return `docker push ${this.registryUrl}/${this.projectName}/IMAGE[:TAG]`; + } + + onclick(): void { + if (this.tagCopyInput) { + this.tagCopyInput.reset(); + } + + if (this.pushCopyInput) { + this.pushCopyInput.reset(); + } + + if(this.copyAlert){ + this.copyAlert.close(); + } + } + + onCpError($event: any): void { + if(this.copyAlert){ + this.copyAlert.showInlineError("PUSH_IMAGE.COPY_ERROR"); + } + } +} diff --git a/src/ui_ng/lib/src/push-image/push-image.css.ts b/src/ui_ng/lib/src/push-image/push-image.css.ts new file mode 100644 index 000000000..4fdca4ade --- /dev/null +++ b/src/ui_ng/lib/src/push-image/push-image.css.ts @@ -0,0 +1,42 @@ +export const PUSH_IMAGE_STYLE: string = ` +.commands-container { + min-width: 360px; + max-width: 720px; + padding-left: 24px; +} + +.h5-override { + display: inline-block !important; + margin-top: 0px !important; + margin-bottom: 12px; +} + +.commands-section { + margin-top: 12px; + margin-bottom: 24px; +} + +.info-tips-icon { + color: grey; +} + +.info-tips-icon:hover { + color: #007CBB; + cursor: pointer; +} + +.command-title { + font-size: 14px; + padding-left: 6px; +} + +.command-input { + font-size: 14px; + font-weight: 500; +} + +:host>>>.dropdown-menu { + min-width: 360px; + max-width: 720px; +} +`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/push-image/push-image.html.ts b/src/ui_ng/lib/src/push-image/push-image.html.ts new file mode 100644 index 000000000..e75112f62 --- /dev/null +++ b/src/ui_ng/lib/src/push-image/push-image.html.ts @@ -0,0 +1,34 @@ +export const PUSH_IMAGE_HTML: string = ` +
+ + + + +
+`; \ No newline at end of file diff --git a/src/ui_ng/lib/src/shared/shared.module.ts b/src/ui_ng/lib/src/shared/shared.module.ts index dba8417cd..5e788e186 100644 --- a/src/ui_ng/lib/src/shared/shared.module.ts +++ b/src/ui_ng/lib/src/shared/shared.module.ts @@ -9,6 +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'; /*export function HttpLoaderFactory(http: Http) { return new TranslateHttpLoader(http, 'i18n/lang/', '-lang.json'); @@ -40,6 +41,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { CommonModule, HttpModule, FormsModule, + ClipboardModule, CookieModule.forRoot(), ClarityModule.forRoot(), TranslateModule.forRoot({ @@ -59,6 +61,7 @@ export function GeneralTranslatorLoader(http: Http, config: IServiceConfig) { HttpModule, FormsModule, CookieModule, + ClipboardModule, ClarityModule, TranslateModule ], From 93a821910866b7bc5ed4abaecde1aad702cffca2 Mon Sep 17 00:00:00 2001 From: kunw Date: Fri, 9 Jun 2017 15:13:49 +0800 Subject: [PATCH 9/9] Fix timestamp value in query params --- src/ui_ng/src/app/log/audit-log.service.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/ui_ng/src/app/log/audit-log.service.ts b/src/ui_ng/src/app/log/audit-log.service.ts index 48c8bcb6a..6cde48bda 100644 --- a/src/ui_ng/src/app/log/audit-log.service.ts +++ b/src/ui_ng/src/app/log/audit-log.service.ts @@ -36,11 +36,21 @@ export class AuditLogService { listAuditLogs(queryParam: AuditLog): Observable { let params: URLSearchParams = new URLSearchParams(queryParam.keywords); - params.set('begin_timestamp', queryParam.begin_timestamp); - params.set('end_timestamp', queryParam.end_timestamp); - params.set('username', queryParam.username); - params.set('page', queryParam.page); - params.set('page_size', queryParam.page_size); + if(queryParam.begin_timestamp) { + params.set('begin_timestamp', queryParam.begin_timestamp); + } + if(queryParam.end_timestamp) { + params.set('end_timestamp', queryParam.end_timestamp); + } + if(queryParam.username) { + params.set('username', queryParam.username); + } + if(queryParam.page) { + params.set('page', queryParam.page); + } + if(queryParam.page_size) { + params.set('page_size', queryParam.page_size); + } return this.http .get(`/api/projects/${queryParam.project_id}/logs`, {params: params}) .map(response => response)