From ea0245b5508f5fb249f6a90b15a1c8ae3063ad11 Mon Sep 17 00:00:00 2001 From: Matej Kramny Date: Sat, 16 Apr 2016 20:42:57 +0100 Subject: [PATCH] Events - Complete more UI work - Show events on dashboard & project dashboard - Show list of tasks --- database/sql_migrations/v1.4.0.sql | 10 ++++ migration/versionHistory.go | 1 + models/Event.go | 14 +++++ public/html/dashboard.jade | 9 ++- public/html/projects/dashboard.jade | 14 ++++- public/html/projects/inventory/add.jade | 8 ++- public/html/projects/inventory/edit.jade | 9 +++ public/html/projects/inventory/list.jade | 2 +- public/html/projects/templates/list.jade | 4 +- public/js/controllers/dashboard.js | 6 +- public/js/controllers/projects/dashboard.js | 5 +- public/js/controllers/projects/inventory.js | 39 ++++++++++++- public/js/controllers/projects/templates.js | 4 +- routes/events.go | 63 ++++++++++++++++++++ routes/projects/inventory.go | 38 +++++++++++- routes/projects/templates.go | 64 +++++++++++++++++++-- routes/router.go | 3 + swagger.yml | 41 ++++++++++++- 18 files changed, 313 insertions(+), 21 deletions(-) create mode 100644 database/sql_migrations/v1.4.0.sql create mode 100644 models/Event.go create mode 100644 public/html/projects/inventory/edit.jade create mode 100644 routes/events.go diff --git a/database/sql_migrations/v1.4.0.sql b/database/sql_migrations/v1.4.0.sql new file mode 100644 index 00000000..9aa5e122 --- /dev/null +++ b/database/sql_migrations/v1.4.0.sql @@ -0,0 +1,10 @@ +CREATE TABLE `event` ( + `project_id` int(11) DEFAULT NULL, + `object_id` int(11) DEFAULT NULL, + `object_type` varchar(20) DEFAULT '', + `description` text, + `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + KEY `project_id` (`project_id`), + KEY `object_id` (`object_id`), + KEY `created` (`created`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/migration/versionHistory.go b/migration/versionHistory.go index 75b318b8..645e9c45 100644 --- a/migration/versionHistory.go +++ b/migration/versionHistory.go @@ -59,5 +59,6 @@ func init() { {Major: 1, Minor: 1}, {Major: 1, Minor: 2}, {Major: 1, Minor: 3}, + {Major: 1, Minor: 4}, } } diff --git a/models/Event.go b/models/Event.go new file mode 100644 index 00000000..206c983c --- /dev/null +++ b/models/Event.go @@ -0,0 +1,14 @@ +package models + +import "time" + +type Event struct { + ProjectID *int `db:"project_id" json:"project_id"` + ObjectID *int `db:"object_id" json:"object_id"` + ObjectType *string `db:"object_type" json:"object_type"` + Description *string `db:"description" json:"description"` + Created time.Time `db:"created" json:"created"` + + ObjectName string `db:"-" json:"object_name"` + ProjectName *string `db:"project_name" json:"project_name"` +} diff --git a/public/html/dashboard.jade b/public/html/dashboard.jade index 04cbc3c8..274e521c 100644 --- a/public/html/dashboard.jade +++ b/public/html/dashboard.jade @@ -1,8 +1,13 @@ .container-fluid .row .col-md-8 - ul - li Scheduled job {x} was successfully executed. Took 30s + h4.no-top-margin Events + ul.list-unstyled + li(ng-repeat="event in events") + a(ng-if="event.project_id != null" ui-sref="project.dashboard({ project_id: event.project_id })") {{ event.project_name }} + span(ng-if="event.project_id != null")   + span(ng-bind="event.object_name") + span - {{ event.description }} .col-md-4 .panel.panel-default .panel-heading Projects diff --git a/public/html/projects/dashboard.jade b/public/html/projects/dashboard.jade index 049613cb..01576e3b 100644 --- a/public/html/projects/dashboard.jade +++ b/public/html/projects/dashboard.jade @@ -1 +1,13 @@ -h3 Project activity \ No newline at end of file +.row + .col-sm-7 + h3.no-top-margin Project activity + ul.list-unstyled + li(ng-repeat="event in events") + a(ng-if="event.project_id != null" ui-sref="project.dashboard({ project_id: event.project_id })") {{ event.project_name }} + span(ng-if="event.project_id != null")   + span(ng-bind="event.object_name") + span - {{ event.description }} + .col-sm-5 + h4.no-top-margin Task history + ul.list-group + li.list-group-item(ng-repeat="task in history"): a(ng-click="openTask()") {{ task.playbook }} \ No newline at end of file diff --git a/public/html/projects/inventory/add.jade b/public/html/projects/inventory/add.jade index 1578cc8c..e400fd6e 100644 --- a/public/html/projects/inventory/add.jade +++ b/public/html/projects/inventory/add.jade @@ -5,26 +5,28 @@ .form-group label.control-label.col-sm-4 Name .col-sm-6 - input.form-control(type="text" ng-model="inventory.name") + input.form-control(type="text" ng-model="inventory.name" placeholder="Inventory Name") .form-group label.control-label.col-sm-4 Type .col-sm-6 - select.form-control(ng-model="inventory.type") + select.form-control(ng-model="inventory.type" ng-init="inventory.type = 'static'") option(value="static") Static option(disabled value="aws") AWS option(disabled value="do") DigitalOcean option(disabled value="gcloud") Google Cloud - .form-group + .form-group(ng-if="inventory.type != 'static'") label.control-label.col-sm-4 Remote inventory key .col-sm-6 select.form-control(ng-model="inventory.key_id" ng-options="key.id as key.name for key in remote_keys") option(value="") -- Select Key -- + .form-group label.control-label.col-sm-4 SSH Key .col-sm-6 select.form-control(ng-model="inventory.ssh_key_id" ng-options="key.id as key.name for key in sshKeys") option(value="") -- Select SSH Key -- + p.help-block Used to log into the servers in this inventory .modal-footer button.btn.btn-default.pull-left(ng-click="$dismiss()") Dismiss diff --git a/public/html/projects/inventory/edit.jade b/public/html/projects/inventory/edit.jade new file mode 100644 index 00000000..2f8d7dac --- /dev/null +++ b/public/html/projects/inventory/edit.jade @@ -0,0 +1,9 @@ +.modal-header + h3.modal-title Edit Inventory + +.modal-body + textarea.form-control(ng-model="inventory" rows="20") + +.modal-footer + button.btn.btn-default.pull-left(ng-click="$dismiss()") Cancel + button.btn.btn-success(ng-click="$close(inventory)") Save Changes \ No newline at end of file diff --git a/public/html/projects/inventory/list.jade b/public/html/projects/inventory/list.jade index 9d2078df..b6db63ab 100644 --- a/public/html/projects/inventory/list.jade +++ b/public/html/projects/inventory/list.jade @@ -10,5 +10,5 @@ table.table tbody: tr(ng-repeat="inv in inventory") td {{ inv.name }} td {{ inv.type }} - td: button.btn.btn-default.btn-xs(ng-click="editInventory(inv)") edit inventory + td: button.btn.btn-default.btn-xs(ng-click="edit(inv)") edit inventory td: button.btn.btn-danger.btn-xs.pull-right(ng-click="remove(inv)") delete \ No newline at end of file diff --git a/public/html/projects/templates/list.jade b/public/html/projects/templates/list.jade index 1120c621..e6eb3891 100644 --- a/public/html/projects/templates/list.jade +++ b/public/html/projects/templates/list.jade @@ -8,9 +8,11 @@ table.table th Inventory th Environment th Repository + th   tbody: tr(ng-repeat="tpl in templates") td {{ tpl.playbook }} td {{ sshKeysAssoc[tpl.ssh_key_id].name }} td {{ inventoryAssoc[tpl.inventory_id].name }} td {{ environmentAssoc[tpl.environment_id].name }} - td {{ reposAssoc[tpl.repository_id].name }} \ No newline at end of file + td {{ reposAssoc[tpl.repository_id].name }} + td: button.btn.btn-danger.btn-xs.pull-right(ng-click="remove(tpl)") remove \ No newline at end of file diff --git a/public/js/controllers/dashboard.js b/public/js/controllers/dashboard.js index f30ef832..24c2aa4d 100644 --- a/public/js/controllers/dashboard.js +++ b/public/js/controllers/dashboard.js @@ -4,7 +4,11 @@ define(['controllers/projects/edit'], function () { $http.get('/projects').success(function (projects) { $scope.projects = projects; - }) + }); + + $http.get('/events').success(function (events) { + $scope.events = events; + }); $scope.addProject = function () { $modal.open({ diff --git a/public/js/controllers/projects/dashboard.js b/public/js/controllers/projects/dashboard.js index 756adca9..ad9279ed 100644 --- a/public/js/controllers/projects/dashboard.js +++ b/public/js/controllers/projects/dashboard.js @@ -1,4 +1,7 @@ define(function () { - app.registerController('ProjectDashboardCtrl', ['$scope', '$http', function ($scope, $http) { + app.registerController('ProjectDashboardCtrl', ['$scope', '$http', 'Project', function ($scope, $http, Project) { + $http.get(Project.getURL() + '/events').success(function (events) { + $scope.events = events; + }); }]); }); \ No newline at end of file diff --git a/public/js/controllers/projects/inventory.js b/public/js/controllers/projects/inventory.js index c797642a..494f5da6 100644 --- a/public/js/controllers/projects/inventory.js +++ b/public/js/controllers/projects/inventory.js @@ -1,5 +1,5 @@ define(function () { - app.registerController('ProjectInventoryCtrl', ['$scope', '$http', '$uibModal', 'Project', function ($scope, $http, $modal, Project) { + app.registerController('ProjectInventoryCtrl', ['$scope', '$http', '$uibModal', 'Project', '$rootScope', function ($scope, $http, $modal, Project, $rootScope) { $scope.reload = function () { $http.get(Project.getURL() + '/inventory').success(function (inventory) { $scope.inventory = inventory; @@ -14,6 +14,43 @@ define(function () { }); } + $scope.add = function () { + $http.get(Project.getURL() + '/keys?type=ssh').success(function (keys) { + var scope = $rootScope.$new(); + scope.sshKeys = keys; + + $modal.open({ + templateUrl: '/tpl/projects/inventory/add.html', + scope: scope + }).result.then(function (inventory) { + $http.post(Project.getURL() + '/inventory', inventory) + .success(function () { + $scope.reload(); + }).error(function (_, status) { + swal('Erorr', 'Inventory not added: ' + status, 'error'); + }); + }); + }); + } + + $scope.edit = function (inventory) { + var scope = $rootScope.$new(); + scope.inventory = inventory.inventory; + + $modal.open({ + templateUrl: '/tpl/projects/inventory/edit.html', + scope: scope + }).result.then(function (v) { + inventory.inventory = v; + $http.put(Project.getURL() + '/inventory/' + inventory.id, inventory) + .success(function () { + $scope.reload(); + }).error(function (_, status) { + swal('Erorr', 'Inventory not updated: ' + status, 'error'); + }); + }); + } + $scope.reload(); }]); }); \ No newline at end of file diff --git a/public/js/controllers/projects/templates.js b/public/js/controllers/projects/templates.js index eda80b8e..52bf327f 100644 --- a/public/js/controllers/projects/templates.js +++ b/public/js/controllers/projects/templates.js @@ -39,8 +39,8 @@ define(function () { }); } - $scope.remove = function (environment) { - $http.delete(Project.getURL() + '/templates/' + environment.id).success(function () { + $scope.remove = function (template) { + $http.delete(Project.getURL() + '/templates/' + template.id).success(function () { $scope.reload(); }).error(function () { swal('error', 'could not delete template..', 'error'); diff --git a/routes/events.go b/routes/events.go new file mode 100644 index 00000000..6f194640 --- /dev/null +++ b/routes/events.go @@ -0,0 +1,63 @@ +package routes + +import ( + "github.com/ansible-semaphore/semaphore/database" + "github.com/ansible-semaphore/semaphore/models" + "github.com/gin-gonic/gin" + "github.com/masterminds/squirrel" +) + +func getEvents(c *gin.Context) { + user := c.MustGet("user").(*models.User) + + q := squirrel.Select("event.*, p.name as project_name"). + From("event"). + LeftJoin("project as p on event.project_id=p.id"). + OrderBy("created desc") + + projectObj, exists := c.Get("event") + if exists == true { + // limit query to project + project := projectObj.(models.Project) + q = q.Where("project_id=?", project.ID) + } else { + q = q.LeftJoin("project__user as pu on pu.project_id=p.id"). + Where("p.id IS NULL or pu.user_id=?", user.ID) + } + + var events []models.Event + + query, args, _ := q.ToSql() + if _, err := database.Mysql.Select(&events, query, args...); err != nil { + panic(err) + } + + for i, evt := range events { + if evt.ObjectID == nil || evt.ObjectType == nil { + continue + } + + var q squirrel.SelectBuilder + + switch *evt.ObjectType { + case "task": + q = squirrel.Select("tpl.playbook as name"). + From("task"). + Join("project__template as tpl on task.template_id=tpl.id") + default: + continue + } + + query, args, _ := q.ToSql() + name, err := database.Mysql.SelectNullStr(query, args...) + if err != nil { + panic(err) + } + + if name.Valid == true { + events[i].ObjectName = name.String + } + } + + c.JSON(200, events) +} diff --git a/routes/projects/inventory.go b/routes/projects/inventory.go index dd98c80d..824b041a 100644 --- a/routes/projects/inventory.go +++ b/routes/projects/inventory.go @@ -55,7 +55,13 @@ func GetInventory(c *gin.Context) { func AddInventory(c *gin.Context) { project := c.MustGet("project").(models.Project) - var inventory models.Inventory + var inventory struct { + Name string `json:"name" binding:"required"` + KeyID *int `json:"key_id"` + SshKeyID int `json:"ssh_key_id"` + Type string `json:"type"` + Inventory string `json:"inventory"` + } if err := c.Bind(&inventory); err != nil { return @@ -69,7 +75,7 @@ func AddInventory(c *gin.Context) { return } - if _, err := database.Mysql.Exec("insert into project__inventory set project_id=?, type=?, key_id=?, inventory=?", project.ID, inventory.Type, inventory.KeyID, inventory.Inventory); err != nil { + if _, err := database.Mysql.Exec("insert into project__inventory set project_id=?, name=?, type=?, key_id=?, ssh_key_id=?, inventory=?", project.ID, inventory.Name, inventory.Type, inventory.KeyID, inventory.SshKeyID, inventory.Inventory); err != nil { panic(err) } @@ -77,7 +83,33 @@ func AddInventory(c *gin.Context) { } func UpdateInventory(c *gin.Context) { - c.AbortWithStatus(501) + oldInventory := c.MustGet("inventory").(models.Inventory) + + var inventory struct { + Name string `json:"name" binding:"required"` + KeyID *int `json:"key_id"` + SshKeyID int `json:"ssh_key_id"` + Type string `json:"type"` + Inventory string `json:"inventory"` + } + + if err := c.Bind(&inventory); err != nil { + return + } + + switch inventory.Type { + case "static", "aws", "do", "gcloud": + break + default: + c.AbortWithStatus(400) + return + } + + if _, err := database.Mysql.Exec("update project__inventory set name=?, type=?, key_id=?, ssh_key_id=?, inventory=? where id=?", inventory.Name, inventory.Type, inventory.KeyID, inventory.SshKeyID, inventory.Inventory, oldInventory.ID); err != nil { + panic(err) + } + + c.AbortWithStatus(204) } func RemoveInventory(c *gin.Context) { diff --git a/routes/projects/templates.go b/routes/projects/templates.go index d1b84869..b69a09e9 100644 --- a/routes/projects/templates.go +++ b/routes/projects/templates.go @@ -1,14 +1,34 @@ package projects import ( + "database/sql" + "github.com/ansible-semaphore/semaphore/database" "github.com/ansible-semaphore/semaphore/models" + "github.com/ansible-semaphore/semaphore/util" "github.com/gin-gonic/gin" "github.com/masterminds/squirrel" ) func TemplatesMiddleware(c *gin.Context) { - c.AbortWithStatus(501) + project := c.MustGet("project").(models.Project) + templateID, err := util.GetIntParam("template_id", c) + if err != nil { + return + } + + var template models.Template + if err := database.Mysql.SelectOne(&template, "select * from project__template where project_id=? and id=?", project.ID, templateID); err != nil { + if err == sql.ErrNoRows { + c.AbortWithStatus(404) + return + } + + panic(err) + } + + c.Set("template", template) + c.Next() } func GetTemplates(c *gin.Context) { @@ -29,13 +49,49 @@ func GetTemplates(c *gin.Context) { } func AddTemplate(c *gin.Context) { - c.AbortWithStatus(501) + project := c.MustGet("project").(models.Project) + + var template models.Template + if err := c.Bind(&template); err != nil { + return + } + + res, err := database.Mysql.Exec("insert into project__template set ssh_key_id=?, project_id=?, inventory_id=?, repository_id=?, environment_id=?, playbook=?, arguments=?, override_args=?", template.SshKeyID, project.ID, template.InventoryID, template.RepositoryID, template.EnvironmentID, template.Playbook, template.Arguments, template.OverrideArguments) + if err != nil { + panic(err) + } + + insertID, err := res.LastInsertId() + if err != nil { + panic(err) + } + + template.ID = int(insertID) + + c.JSON(201, template) } func UpdateTemplate(c *gin.Context) { - c.AbortWithStatus(501) + oldTemplate := c.MustGet("template").(models.Template) + + var template models.Template + if err := c.Bind(&template); err != nil { + return + } + + if _, err := database.Mysql.Exec("update project__template set ssh_key_id=?, inventory_id=?, repository_id=?, environment_id=?, playbook=?, arguments=?, override_args=? where id=?", template.SshKeyID, template.InventoryID, template.RepositoryID, template.EnvironmentID, template.Playbook, template.Arguments, template.OverrideArguments, oldTemplate.ID); err != nil { + panic(err) + } + + c.AbortWithStatus(204) } func RemoveTemplate(c *gin.Context) { - c.AbortWithStatus(501) + tpl := c.MustGet("template").(models.Template) + + if _, err := database.Mysql.Exec("delete from project__template where id=?", tpl.ID); err != nil { + panic(err) + } + + c.AbortWithStatus(204) } diff --git a/routes/router.go b/routes/router.go index 6bb125b4..765c220a 100644 --- a/routes/router.go +++ b/routes/router.go @@ -43,6 +43,7 @@ func Route(r *gin.Engine) { api.GET("/projects", projects.GetProjects) api.POST("/projects", projects.AddProject) + api.GET("/events", getEvents) api.GET("/users", getUsers) api.POST("/users", addUser) @@ -54,6 +55,8 @@ func Route(r *gin.Engine) { api.GET("", projects.GetProject) + api.GET("/events", getEvents) + api.GET("/users", projects.GetUsers) api.POST("/users", projects.AddUser) api.POST("/users/:user_id/admin", projects.UserMiddleware, projects.MakeUserAdmin) diff --git a/swagger.yml b/swagger.yml index 895280f6..bd55e0e1 100644 --- a/swagger.yml +++ b/swagger.yml @@ -169,6 +169,17 @@ definitions: type: string override_args: type: boolean + Event: + type: object + properties: + project_id: + type: integer + object_id: + type: integer + object_type: + type: string + description: + type: string # securityDefinitions: # cookie: @@ -351,6 +362,17 @@ paths: responses: 200: description: Created project + /events: + get: + summary: Get Events related to Semaphore and projects you are part of + responses: + 200: + description: Array of events in chronological order + schema: + type: array + items: + $ref: '#/definitions/Event' + /project/{project_id}: parameters: - $ref: "#/parameters/project_id" @@ -364,6 +386,21 @@ paths: schema: $ref: "#/definitions/Project" + /project/{project_id}/events: + parameters: + - $ref: '#/parameters/project_id' + get: + tags: + - project + summary: Get Events related to this project + responses: + 200: + description: Array of events in chronological order + schema: + type: array + items: + $ref: '#/definitions/Event' + # User management /project/{project_id}/users: parameters: @@ -664,8 +701,10 @@ paths: schema: $ref: "#/definitions/Template" responses: - 204: + 201: description: template created + schema: + $ref: "#/definitions/Template" /project/{project_id}/template/{template_id}: parameters: - $ref: "#/parameters/project_id"