diff --git a/Deploy/templates/ui/app.conf b/Deploy/templates/ui/app.conf index 090bcdc47..6d8f15728 100644 --- a/Deploy/templates/ui/app.conf +++ b/Deploy/templates/ui/app.conf @@ -2,8 +2,8 @@ appname = registry runmode = dev [lang] -types = en-US|zh-CN|de-DE|ru-RU -names = en-US|zh-CN|de-DE|ru-RU +types = en-US|zh-CN +names = English|中文 [dev] httpport = 80 diff --git a/api/project.go b/api/project.go index b0c404c2b..90ccbdb8c 100644 --- a/api/project.go +++ b/api/project.go @@ -113,23 +113,50 @@ func (p *ProjectAPI) Head() { // Get ... func (p *ProjectAPI) Get() { - queryProject := models.Project{UserID: p.userID} + var projectList []models.Project projectName := p.GetString("project_name") if len(projectName) > 0 { - queryProject.Name = "%" + projectName + "%" + projectName = "%" + projectName + "%" + } + var public int + var err error + isPublic := p.GetString("is_public") + if len(isPublic) > 0 { + public, err = strconv.Atoi(isPublic) + if err != nil { + log.Errorf("Error parsing public property: %d, error: %v", isPublic, err) + p.CustomAbort(http.StatusBadRequest, "invalid project Id") + } + } + isAdmin := false + if public == 1 { + projectList, err = dao.GetPublicProjects(projectName) + } else { + isAdmin, err = dao.IsAdminRole(p.userID) + if err != nil { + log.Errorf("Error occured in check admin, error: %v", err) + p.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if isAdmin { + projectList, err = dao.GetAllProjects(projectName) + } else { + projectList, err = dao.GetUserRelevantProjects(p.userID, projectName) + } } - public, _ := p.GetInt("is_public") - queryProject.Public = public - - projectList, err := dao.QueryProject(queryProject) if err != nil { - log.Errorf("Error occurred in QueryProject, error: %v", err) + log.Errorf("Error occured in get projects info, error: %v", err) p.CustomAbort(http.StatusInternalServerError, "Internal error.") } for i := 0; i < len(projectList); i++ { - if isProjectAdmin(p.userID, projectList[i].ProjectID) { - projectList[i].Togglable = true + if public != 1 { + if isAdmin { + projectList[i].Role = models.PROJECTADMIN + } + if projectList[i].Role == models.PROJECTADMIN { + projectList[i].Togglable = true + } } + projectList[i].RepoCount = getRepoCountByProject(projectList[i].Name) } p.Data["json"] = projectList p.ServeJSON() diff --git a/api/search.go b/api/search.go index d47bb5890..3acdc7ca8 100644 --- a/api/search.go +++ b/api/search.go @@ -55,13 +55,13 @@ func (s *SearchAPI) Get() { var projects []models.Project if isSysAdmin { - projects, err = dao.GetAllProjects() + projects, err = dao.GetAllProjects("") if err != nil { log.Errorf("failed to get all projects: %v", err) s.CustomAbort(http.StatusInternalServerError, "internal error") } } else { - projects, err = dao.GetUserRelevantProjects(userID) + projects, err = dao.SearchProjects(userID) if err != nil { log.Errorf("failed to get user %d 's relevant projects: %v", userID, err) s.CustomAbort(http.StatusInternalServerError, "internal error") diff --git a/api/statistic.go b/api/statistic.go new file mode 100644 index 000000000..67a7cc388 --- /dev/null +++ b/api/statistic.go @@ -0,0 +1,117 @@ +/* + Copyright (c) 2016 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 api + +import ( + "net/http" + "strings" + + "github.com/vmware/harbor/dao" + "github.com/vmware/harbor/models" + svc_utils "github.com/vmware/harbor/service/utils" + "github.com/vmware/harbor/utils/log" +) + +// StatisticAPI handles request to /api/statistics/ +type StatisticAPI struct { + BaseAPI + userID int +} + +//Prepare validates the URL and the user +func (s *StatisticAPI) Prepare() { + s.userID = s.ValidateUser() +} + +// Get total projects and repos of the user +func (s *StatisticAPI) Get() { + isAdmin, err := dao.IsAdminRole(s.userID) + if err != nil { + log.Errorf("Error occured in check admin, error: %v", err) + s.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + var projectList []models.Project + if isAdmin { + projectList, err = dao.GetAllProjects("") + } else { + projectList, err = dao.GetUserRelevantProjects(s.userID, "") + } + if err != nil { + log.Errorf("Error occured in QueryProject, error: %v", err) + s.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + proMap := map[string]int{} + proMap["my_project_count"] = 0 + proMap["my_repo_count"] = 0 + proMap["public_project_count"] = 0 + proMap["public_repo_count"] = 0 + var publicProjects []models.Project + publicProjects, err = dao.GetPublicProjects("") + if err != nil { + log.Errorf("Error occured in QueryPublicProject, error: %v", err) + s.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + proMap["public_project_count"] = len(publicProjects) + for i := 0; i < len(publicProjects); i++ { + proMap["public_repo_count"] += getRepoCountByProject(publicProjects[i].Name) + } + if isAdmin { + proMap["total_project_count"] = len(projectList) + proMap["total_repo_count"] = getTotalRepoCount() + } + for i := 0; i < len(projectList); i++ { + if isAdmin { + projectList[i].Role = models.PROJECTADMIN + } + if projectList[i].Role == models.PROJECTADMIN || projectList[i].Role == models.DEVELOPER || + projectList[i].Role == models.GUEST { + proMap["my_project_count"]++ + proMap["my_repo_count"] += getRepoCountByProject(projectList[i].Name) + } + } + s.Data["json"] = proMap + s.ServeJSON() +} + +//getReposByProject returns repo numbers of specified project +func getRepoCountByProject(projectName string) int { + repoList, err := svc_utils.GetRepoFromCache() + if err != nil { + log.Errorf("Failed to get repo from cache, error: %v", err) + return 0 + } + var resp int + if len(projectName) > 0 { + for _, r := range repoList { + if strings.Contains(r, "/") && r[0:strings.LastIndex(r, "/")] == projectName { + resp++ + } + } + return resp + } + return 0 +} + +//getTotalRepoCount returns total repo count +func getTotalRepoCount() int { + repoList, err := svc_utils.GetRepoFromCache() + if err != nil { + log.Errorf("Failed to get repo from cache, error: %v", err) + return 0 + } + return len(repoList) + +} diff --git a/controllers/ng/base.go b/controllers/ng/base.go index c0eb02c54..3b0b94fc6 100644 --- a/controllers/ng/base.go +++ b/controllers/ng/base.go @@ -8,7 +8,6 @@ import ( "github.com/astaxie/beego" "github.com/beego/i18n" - "github.com/vmware/harbor/dao" "github.com/vmware/harbor/utils/log" ) @@ -74,6 +73,7 @@ func (b *BaseController) Prepare() { b.Data["Lang"] = curLang.Lang b.Data["CurLang"] = curLang.Name b.Data["RestLangs"] = restLangs + b.Data["SupportLanguages"] = supportLanguages authMode := strings.ToLower(os.Getenv("AUTH_MODE")) if authMode == "" { @@ -82,28 +82,6 @@ func (b *BaseController) Prepare() { b.AuthMode = authMode b.Data["AuthMode"] = b.AuthMode - selfRegistration := strings.ToLower(os.Getenv("SELF_REGISTRATION")) - - if selfRegistration == "on" { - b.SelfRegistration = true - } - - sessionUserID := b.GetSession("userId") - if sessionUserID != nil { - b.Data["Username"] = b.GetSession("username") - b.Data["UserId"] = sessionUserID.(int) - - var err error - b.IsAdmin, err = dao.IsAdminRole(sessionUserID.(int)) - if err != nil { - log.Errorf("Error occurred in IsAdminRole:%v", err) - b.CustomAbort(http.StatusInternalServerError, "Internal error.") - } - } - - b.Data["IsAdmin"] = b.IsAdmin - b.Data["SelfRegistration"] = b.SelfRegistration - } func (bc *BaseController) Forward(title, templateName string) { @@ -120,6 +98,27 @@ func (bc *BaseController) Forward(title, templateName string) { var langTypes []*langType +type CommonController struct { + BaseController +} + +func (cc *CommonController) Render() error { + return nil +} + +func (cc *CommonController) LogOut() { + cc.DestroySession() +} + +func (cc *CommonController) SwitchLanguage() { + lang := cc.GetString("lang") + if _, exist := supportLanguages[lang]; exist { + cc.SetSession("lang", lang) + cc.Data["Lang"] = lang + } + cc.Redirect(cc.Ctx.Request.Header.Get("Referer"), http.StatusFound) +} + func init() { //conf/app.conf -> os.Getenv("config_path") diff --git a/controllers/ng/navigationheader.go b/controllers/ng/navigationheader.go new file mode 100644 index 000000000..69276bbb9 --- /dev/null +++ b/controllers/ng/navigationheader.go @@ -0,0 +1,37 @@ +package ng + +import ( + "net/http" + + "github.com/vmware/harbor/dao" + "github.com/vmware/harbor/models" + "github.com/vmware/harbor/utils/log" +) + +type NavigationHeaderController struct { + BaseController +} + +func (nhc *NavigationHeaderController) Get() { + sessionUserID := nhc.GetSession("userId") + var hasLoggedIn bool + var isAdmin int + if sessionUserID != nil { + hasLoggedIn = true + userID := sessionUserID.(int) + u, err := dao.GetUser(models.User{UserID: userID}) + if err != nil { + log.Errorf("Error occurred in GetUser, error: %v", err) + nhc.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if u == nil { + log.Warningf("User was deleted already, user id: %d, canceling request.", userID) + nhc.CustomAbort(http.StatusUnauthorized, "") + } + isAdmin = u.HasAdminRole + } + nhc.Data["HasLoggedIn"] = hasLoggedIn + nhc.Data["IsAdmin"] = isAdmin + nhc.TplName = "ng/navigation-header.htm" + nhc.Render() +} diff --git a/controllers/ng/optionalmenu.go b/controllers/ng/optionalmenu.go new file mode 100644 index 000000000..6945681dc --- /dev/null +++ b/controllers/ng/optionalmenu.go @@ -0,0 +1,37 @@ +package ng + +import ( + "net/http" + + "github.com/vmware/harbor/dao" + "github.com/vmware/harbor/models" + "github.com/vmware/harbor/utils/log" +) + +type OptionalMenuController struct { + BaseController +} + +func (omc *OptionalMenuController) Get() { + sessionUserID := omc.GetSession("userId") + + var hasLoggedIn bool + if sessionUserID != nil { + hasLoggedIn = true + userID := sessionUserID.(int) + u, err := dao.GetUser(models.User{UserID: userID}) + if err != nil { + log.Errorf("Error occurred in GetUser, error: %v", err) + omc.CustomAbort(http.StatusInternalServerError, "Internal error.") + } + if u == nil { + log.Warningf("User was deleted already, user id: %d, canceling request.", userID) + omc.CustomAbort(http.StatusUnauthorized, "") + } + omc.Data["Username"] = u.Username + } + omc.Data["HasLoggedIn"] = hasLoggedIn + omc.TplName = "ng/optional-menu.htm" + omc.Render() + +} diff --git a/controllers/ng/password.go b/controllers/ng/password.go index 9b16bba93..f6f169c4a 100644 --- a/controllers/ng/password.go +++ b/controllers/ng/password.go @@ -14,18 +14,6 @@ import ( "github.com/vmware/harbor/utils/log" ) -type CommonController struct { - BaseController -} - -func (cc *CommonController) Render() error { - return nil -} - -func (cc *CommonController) LogOut() { - cc.DestroySession() -} - type messageDetail struct { Hint string URL string diff --git a/controllers/ng/search.go b/controllers/ng/search.go new file mode 100644 index 000000000..341c864a0 --- /dev/null +++ b/controllers/ng/search.go @@ -0,0 +1,9 @@ +package ng + +type SearchController struct { + BaseController +} + +func (sc *SearchController) Get() { + sc.Forward("Search", "search.htm") +} diff --git a/dao/dao_test.go b/dao/dao_test.go index 0ef18faec..8e300740d 100644 --- a/dao/dao_test.go +++ b/dao/dao_test.go @@ -353,7 +353,7 @@ func TestChangeUserPasswordWithIncorrectOldPassword(t *testing.T) { } func TestQueryRelevantProjectsWhenNoProjectAdded(t *testing.T) { - projects, err := GetUserRelevantProjects(currentUser.UserID) + projects, err := SearchProjects(currentUser.UserID) if err != nil { t.Errorf("Error occurred in QueryRelevantProjects: %v", err) } @@ -572,39 +572,6 @@ func TestIsProjectPublic(t *testing.T) { } } -func TestQueryProject(t *testing.T) { - query1 := models.Project{ - UserID: 1, - } - projects, err := QueryProject(query1) - if err != nil { - t.Errorf("Error in Query Project: %v, query: %+v", err, query1) - } - if len(projects) != 2 { - t.Errorf("Expecting get 2 projects, but actual: %d, the list: %+v", len(projects), projects) - } - query2 := models.Project{ - Public: 1, - } - projects, err = QueryProject(query2) - if err != nil { - t.Errorf("Error in Query Project: %v, query: %+v", err, query2) - } - if len(projects) != 1 { - t.Errorf("Expecting get 1 project, but actual: %d, the list: %+v", len(projects), projects) - } - query3 := models.Project{ - UserID: 9, - } - projects, err = QueryProject(query3) - if err != nil { - t.Errorf("Error in Query Project: %v, query: %+v", err, query3) - } - if len(projects) != 0 { - t.Errorf("Expecting get 0 project, but actual: %d, the list: %+v", len(projects), projects) - } -} - func TestGetUserProjectRoles(t *testing.T) { r, err := GetUserProjectRoles(currentUser.UserID, currentProject.ProjectID) if err != nil { @@ -632,20 +599,20 @@ func TestProjectPermission(t *testing.T) { } func TestGetUserRelevantProjects(t *testing.T) { - projects, err := GetUserRelevantProjects(currentUser.UserID) + projects, err := GetUserRelevantProjects(currentUser.UserID, "") if err != nil { t.Errorf("Error occurred in GetUserRelevantProjects: %v", err) } - if len(projects) != 2 { - t.Errorf("Expected length of relevant projects is 2, but actual: %d, the projects: %+v", len(projects), projects) + if len(projects) != 1 { + t.Errorf("Expected length of relevant projects is 1, but actual: %d, the projects: %+v", len(projects), projects) } - if projects[1].Name != projectName { + if projects[0].Name != projectName { t.Errorf("Expected project name in the list: %s, actual: %s", projectName, projects[1].Name) } } func TestGetAllProjects(t *testing.T) { - projects, err := GetAllProjects() + projects, err := GetAllProjects("") if err != nil { t.Errorf("Error occurred in GetAllProjects: %v", err) } @@ -657,6 +624,19 @@ func TestGetAllProjects(t *testing.T) { } } +func TestGetPublicProjects(t *testing.T) { + projects, err := GetPublicProjects("") + if err != nil { + t.Errorf("Error occurred in getProjects: %v", err) + } + if len(projects) != 1 { + t.Errorf("Expected length of projects is 1, but actual: %d, the projects: %+v", len(projects), projects) + } + if projects[0].Name != "library" { + t.Errorf("Expected project name in the list: %s, actual: %s", "library", projects[0].Name) + } +} + func TestAddProjectMember(t *testing.T) { err := AddProjectMember(currentProject.ProjectID, 1, models.DEVELOPER) if err != nil { diff --git a/dao/project.go b/dao/project.go index 6e01b070d..730b1f331 100644 --- a/dao/project.go +++ b/dao/project.go @@ -79,42 +79,6 @@ func IsProjectPublic(projectName string) bool { return project.Public == 1 } -// QueryProject querys the projects based on publicity and user, disregarding the names etc. -func QueryProject(query models.Project) ([]models.Project, error) { - o := orm.NewOrm() - - sql := `select distinct - p.project_id, p.owner_id, p.name,p.creation_time, p.update_time, p.public - from project p - left join project_member pm on p.project_id = pm.project_id - where p.deleted = 0 ` - - queryParam := make([]interface{}, 1) - - if query.Public == 1 { - sql += ` and p.public = ?` - queryParam = append(queryParam, query.Public) - } else if isAdmin, _ := IsAdminRole(query.UserID); isAdmin == false { - sql += ` and (pm.user_id = ?) ` - queryParam = append(queryParam, query.UserID) - } - - if query.Name != "" { - sql += " and p.name like ? " - queryParam = append(queryParam, query.Name) - } - - sql += " order by p.name " - - var r []models.Project - _, err := o.Raw(sql, queryParam).QueryRows(&r) - - if err != nil { - return nil, err - } - return r, nil -} - //ProjectExists returns whether the project exists according to its name of ID. func ProjectExists(nameOrID interface{}) (bool, error) { o := orm.NewOrm() @@ -208,11 +172,11 @@ func ToggleProjectPublicity(projectID int64, publicity int) error { return err } -// GetUserRelevantProjects returns a project list, +// SearchProjects returns a project list, // which satisfies the following conditions: // 1. the project is not deleted // 2. the prject is public or the user is a member of the project -func GetUserRelevantProjects(userID int) ([]models.Project, error) { +func SearchProjects(userID int) ([]models.Project, error) { o := orm.NewOrm() sql := `select distinct p.project_id, p.name, p.public from project p @@ -228,14 +192,66 @@ func GetUserRelevantProjects(userID int) ([]models.Project, error) { return projects, nil } -// GetAllProjects returns all projects which are not deleted -func GetAllProjects() ([]models.Project, error) { +// GetUserRelevantProjects returns the projects of the user which are not deleted and name like projectName +func GetUserRelevantProjects(userID int, projectName string) ([]models.Project, error) { o := orm.NewOrm() - sql := `select project_id, name, public + sql := `select distinct + p.project_id, p.owner_id, p.name,p.creation_time, p.update_time, p.public, pm.role role + from project p + left join project_member pm on p.project_id = pm.project_id + where p.deleted = 0 and pm.user_id= ?` + + queryParam := make([]interface{}, 1) + queryParam = append(queryParam, userID) + if projectName != "" { + sql += " and p.name like ? " + queryParam = append(queryParam, projectName) + } + sql += " order by p.name " + var r []models.Project + _, err := o.Raw(sql, queryParam).QueryRows(&r) + if err != nil { + return nil, err + } + return r, nil +} + +//GetPublicProjects returns all public projects whose name like projectName +func GetPublicProjects(projectName string) ([]models.Project, error) { + publicProjects, err := getProjects(1, projectName) + if err != nil { + return nil, err + } + return publicProjects, nil +} + +// GetAllProjects returns all projects which are not deleted and name like projectName +func GetAllProjects(projectName string) ([]models.Project, error) { + allProjects, err := getProjects(0, projectName) + if err != nil { + return nil, err + } + return allProjects, nil +} + +func getProjects(public int, projectName string) ([]models.Project, error) { + o := orm.NewOrm() + sql := `select project_id, owner_id, creation_time, update_time, name, public from project where deleted = 0` + queryParam := make([]interface{}, 1) + if public == 1 { + sql += " and public = ? " + queryParam = append(queryParam, public) + } + if len(projectName) > 0 { + sql += " and name like ? " + queryParam = append(queryParam, projectName) + } + sql += " order by name " var projects []models.Project - if _, err := o.Raw(sql).QueryRows(&projects); err != nil { + log.Debugf("sql xxx", sql) + if _, err := o.Raw(sql, queryParam).QueryRows(&projects); err != nil { return nil, err } return projects, nil diff --git a/models/project.go b/models/project.go index e240c609f..2ebd5fc32 100644 --- a/models/project.go +++ b/models/project.go @@ -34,4 +34,6 @@ type Project struct { Togglable bool UpdateTime time.Time `orm:"update_time" json:"update_time"` + Role int `json:"role_id"` + RepoCount int `json:"repo_count"` } diff --git a/static/ng/resources/css/dashboard.css b/static/ng/resources/css/dashboard.css index 215e5dc27..aac91bac8 100644 --- a/static/ng/resources/css/dashboard.css +++ b/static/ng/resources/css/dashboard.css @@ -12,7 +12,3 @@ text-align: left; } -.single { - margin-right: 0; -} - diff --git a/static/ng/resources/css/header.css b/static/ng/resources/css/header.css index c669545cf..4d8f86da1 100644 --- a/static/ng/resources/css/header.css +++ b/static/ng/resources/css/header.css @@ -21,11 +21,7 @@ nav .container-custom { } .navbar-brand > img { - height: 60px; - width: 100px; margin-top: -30px; - filter: brightness(0) invert(1); - -webkit-filter: brightness(0) invert(1); } .navbar-form { @@ -40,21 +36,21 @@ nav .container-custom { } .nav-custom li { - float: left; - padding: 10px 0 0 0; - margin-right: 12px; - list-style: none; - display: inline-block; + float: left; + padding: 10px 0 0 0; + margin-right: 12px; + list-style: none; + display: inline-block; } .nav-custom li a { - font-size: 14px; - color: #FFFFFF; - text-decoration: none; + font-size: 14px; + color: #FFFFFF; + text-decoration: none; } .nav-custom .active { - border-bottom: 3px solid #EFEFEF; + border-bottom: 3px solid #EFEFEF; } .dropdown { diff --git a/static/ng/resources/css/search.css b/static/ng/resources/css/search.css new file mode 100644 index 000000000..d2b2b9f27 --- /dev/null +++ b/static/ng/resources/css/search.css @@ -0,0 +1,9 @@ +.search-result { + min-height: 200px; + max-height: 200px; + overflow-y: auto; +} + +.search-result li { + margin-bottom: 15px; +} \ No newline at end of file diff --git a/static/ng/resources/img/Harbor_Logo_rec.png b/static/ng/resources/img/Harbor_Logo_rec.png old mode 100755 new mode 100644 index cccdbedf1..3a603109e Binary files a/static/ng/resources/img/Harbor_Logo_rec.png and b/static/ng/resources/img/Harbor_Logo_rec.png differ diff --git a/static/ng/resources/js/components/details/retrieve-projects.directive.html b/static/ng/resources/js/components/details/retrieve-projects.directive.html index 573d78b4e..8e1e90491 100644 --- a/static/ng/resources/js/components/details/retrieve-projects.directive.html +++ b/static/ng/resources/js/components/details/retrieve-projects.directive.html @@ -1,10 +1,10 @@ -