added optional task unique field

This commit is contained in:
simon987 2019-01-29 18:16:40 -05:00
parent f250a2180c
commit 64152bfc08
35 changed files with 877 additions and 156 deletions

View File

@ -72,7 +72,8 @@ func (api *WebAPI) ReceiveGitWebHook(r *Request) {
version := getVersion(payload) version := getVersion(payload)
project.Version = version project.Version = version
api.Database.UpdateProject(project) err := api.Database.UpdateProject(project)
handleErr(err, r)
} }
func signatureValid(r *Request) (matches bool) { func signatureValid(r *Request) (matches bool) {

View File

@ -1,6 +1,7 @@
package api package api
import ( import (
"fmt"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/buaazp/fasthttprouter" "github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -48,6 +49,7 @@ func New() *WebAPI {
api.router.POST("/log/error", LogRequestMiddleware(LogError)) api.router.POST("/log/error", LogRequestMiddleware(LogError))
api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate)) api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate))
api.router.POST("/worker/update", LogRequestMiddleware(api.WorkerUpdate))
api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet)) api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet))
api.router.POST("/access/grant", LogRequestMiddleware(api.WorkerGrantAccess)) api.router.POST("/access/grant", LogRequestMiddleware(api.WorkerGrantAccess))
@ -55,6 +57,7 @@ func New() *WebAPI {
api.router.POST("/project/create", LogRequestMiddleware(api.ProjectCreate)) api.router.POST("/project/create", LogRequestMiddleware(api.ProjectCreate))
api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet)) api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet))
api.router.POST("/project/update/:id", LogRequestMiddleware(api.ProjectUpdate))
api.router.GET("/project/stats/:id", LogRequestMiddleware(api.ProjectGetStats)) api.router.GET("/project/stats/:id", LogRequestMiddleware(api.ProjectGetStats))
api.router.GET("/project/stats", LogRequestMiddleware(api.ProjectGetAllStats)) api.router.GET("/project/stats", LogRequestMiddleware(api.ProjectGetAllStats))
@ -67,6 +70,19 @@ func New() *WebAPI {
api.router.POST("/logs", LogRequestMiddleware(api.GetLog)) api.router.POST("/logs", LogRequestMiddleware(api.GetLog))
api.router.NotFound = func(ctx *fasthttp.RequestCtx) {
if ctx.Request.Header.IsOptions() {
ctx.Response.Header.Add("Access-Control-Allow-Headers", "Content-Type")
ctx.Response.Header.Add("Access-Control-Allow-Methods", "GET, POST, OPTION")
ctx.Response.Header.Add("Access-Control-Allow-Origin", "*")
} else {
ctx.SetStatusCode(404)
_, _ = fmt.Fprintf(ctx, "Not found")
}
}
return api return api
} }

View File

@ -16,6 +16,20 @@ type CreateProjectRequest struct {
Public bool `json:"public"` Public bool `json:"public"`
} }
type UpdateProjectRequest struct {
Name string `json:"name"`
CloneUrl string `json:"clone_url"`
GitRepo string `json:"git_repo"`
Priority int64 `json:"priority"`
Motd string `json:"motd"`
Public bool `json:"public"`
}
type UpdateProjectResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
}
type CreateProjectResponse struct { type CreateProjectResponse struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Id int64 `json:"id,omitempty"` Id int64 `json:"id,omitempty"`
@ -86,10 +100,66 @@ func (api *WebAPI) ProjectCreate(r *Request) {
} }
} }
func (api *WebAPI) ProjectUpdate(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
handleErr(err, r) //todo handle invalid id
updateReq := &UpdateProjectRequest{}
if r.GetJson(updateReq) {
project := &storage.Project{
Id: id,
Name: updateReq.Name,
CloneUrl: updateReq.CloneUrl,
GitRepo: updateReq.GitRepo,
Priority: updateReq.Priority,
Motd: updateReq.Motd,
Public: updateReq.Public,
}
if isValidProject(project) {
err := api.Database.UpdateProject(project)
if err != nil {
r.Json(CreateProjectResponse{
Ok: false,
Message: err.Error(),
}, 500)
logrus.WithError(err).WithFields(logrus.Fields{
"project": project,
}).Warn("Error during project update")
} else {
r.OkJson(UpdateProjectResponse{
Ok: true,
})
logrus.WithFields(logrus.Fields{
"project": project,
}).Debug("Updated project")
}
} else {
logrus.WithFields(logrus.Fields{
"project": project,
}).Warn("Invalid project")
r.Json(CreateProjectResponse{
Ok: false,
Message: "Invalid project",
}, 400)
}
}
}
func isValidProject(project *storage.Project) bool { func isValidProject(project *storage.Project) bool {
if len(project.Name) <= 0 { if len(project.Name) <= 0 {
return false return false
} }
if project.Priority < 0 {
return false
}
return true return true
} }
@ -97,7 +167,7 @@ func isValidProject(project *storage.Project) bool {
func (api *WebAPI) ProjectGet(r *Request) { func (api *WebAPI) ProjectGet(r *Request) {
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64) id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
handleErr(err, r) handleErr(err, r) //todo handle invalid id
project := api.Database.GetProject(id) project := api.Database.GetProject(id)

View File

@ -1,8 +1,13 @@
package api package api
import ( import (
"bytes"
"crypto"
"crypto/hmac"
"encoding/hex"
"errors" "errors"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/dchest/siphash"
"github.com/google/uuid" "github.com/google/uuid"
"src/task_tracker/storage" "src/task_tracker/storage"
"strconv" "strconv"
@ -14,12 +19,13 @@ type CreateTaskRequest struct {
Recipe string `json:"recipe"` Recipe string `json:"recipe"`
Priority int64 `json:"priority"` Priority int64 `json:"priority"`
MaxAssignTime int64 `json:"max_assign_time"` MaxAssignTime int64 `json:"max_assign_time"`
Hash64 int64 `json:"hash_u64"`
UniqueString string `json:"unique_string"`
} }
type ReleaseTaskRequest struct { type ReleaseTaskRequest struct {
TaskId int64 `json:"task_id"` TaskId int64 `json:"task_id"`
Success bool `json:"success"` Success bool `json:"success"`
WorkerId *uuid.UUID `json:"worker_id"`
} }
type ReleaseTaskResponse struct { type ReleaseTaskResponse struct {
@ -51,8 +57,14 @@ func (api *WebAPI) TaskCreate(r *Request) {
MaxAssignTime: createReq.MaxAssignTime, MaxAssignTime: createReq.MaxAssignTime,
} }
if isTaskValid(task) { if createReq.IsValid() && isTaskValid(task) {
err := api.Database.SaveTask(task, createReq.Project)
if createReq.UniqueString != "" {
//TODO: Load key from config
createReq.Hash64 = int64(siphash.Hash(1, 2, []byte(createReq.UniqueString)))
}
err := api.Database.SaveTask(task, createReq.Project, createReq.Hash64)
if err != nil { if err != nil {
r.Json(CreateTaskResponse{ r.Json(CreateTaskResponse{
@ -76,6 +88,10 @@ func (api *WebAPI) TaskCreate(r *Request) {
} }
} }
func (req *CreateTaskRequest) IsValid() bool {
return req.Hash64 == 0 || req.UniqueString == ""
}
func isTaskValid(task *storage.Task) bool { func isTaskValid(task *storage.Task) bool {
if task.MaxRetries < 0 { if task.MaxRetries < 0 {
return false return false
@ -89,7 +105,7 @@ func isTaskValid(task *storage.Task) bool {
func (api *WebAPI) TaskGetFromProject(r *Request) { func (api *WebAPI) TaskGetFromProject(r *Request) {
worker, err := api.workerFromQueryArgs(r) worker, err := api.validateSignature(r)
if err != nil { if err != nil {
r.Json(GetTaskResponse{ r.Json(GetTaskResponse{
Ok: false, Ok: false,
@ -121,7 +137,7 @@ func (api *WebAPI) TaskGetFromProject(r *Request) {
func (api *WebAPI) TaskGet(r *Request) { func (api *WebAPI) TaskGet(r *Request) {
worker, err := api.workerFromQueryArgs(r) worker, err := api.validateSignature(r)
if err != nil { if err != nil {
r.Json(GetTaskResponse{ r.Json(GetTaskResponse{
Ok: false, Ok: false,
@ -138,9 +154,11 @@ func (api *WebAPI) TaskGet(r *Request) {
}) })
} }
func (api WebAPI) workerFromQueryArgs(r *Request) (*storage.Worker, error) { func (api WebAPI) validateSignature(r *Request) (*storage.Worker, error) {
widStr := string(r.Ctx.Request.Header.Peek("X-Worker-Id"))
signature := r.Ctx.Request.Header.Peek("X-Signature")
widStr := string(r.Ctx.QueryArgs().Peek("wid"))
wid, err := uuid.Parse(widStr) wid, err := uuid.Parse(widStr)
if err != nil { if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
@ -155,20 +173,53 @@ func (api WebAPI) workerFromQueryArgs(r *Request) (*storage.Worker, error) {
if worker == nil { if worker == nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
"wid": widStr, "wid": widStr,
}).Warn("Can't parse wid") }).Warn("Worker id does not match any valid worker")
return nil, errors.New("worker id does not match any valid worker") return nil, errors.New("worker id does not match any valid worker")
} }
var body []byte
if r.Ctx.Request.Header.IsGet() {
body = r.Ctx.Request.RequestURI()
} else {
body = r.Ctx.Request.Body()
}
mac := hmac.New(crypto.SHA256.New, worker.Secret)
mac.Write(body)
expectedMac := make([]byte, 64)
hex.Encode(expectedMac, mac.Sum(nil))
matches := bytes.Compare(expectedMac, signature) == 0
logrus.WithFields(logrus.Fields{
"expected": string(expectedMac),
"signature": string(signature),
"matches": matches,
}).Trace("Validating Worker signature")
if !matches {
return nil, errors.New("invalid signature")
}
return worker, nil return worker, nil
} }
func (api *WebAPI) TaskRelease(r *Request) { func (api *WebAPI) TaskRelease(r *Request) {
req := ReleaseTaskRequest{} worker, err := api.validateSignature(r)
if r.GetJson(req) { if err != nil {
r.Json(GetTaskResponse{
Ok: false,
Message: err.Error(),
}, 403)
return
}
res := api.Database.ReleaseTask(req.TaskId, req.WorkerId, req.Success) var req ReleaseTaskRequest
if r.GetJson(&req) {
res := api.Database.ReleaseTask(req.TaskId, &worker.Id, req.Success)
response := ReleaseTaskResponse{ response := ReleaseTaskResponse{
Ok: res, Ok: res,

View File

@ -3,17 +3,28 @@ package api
import ( import (
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/google/uuid" "github.com/google/uuid"
"math/rand"
"src/task_tracker/storage" "src/task_tracker/storage"
"time" "time"
) )
type CreateWorkerRequest struct { type CreateWorkerRequest struct {
Alias string `json:"alias"`
}
type UpdateWorkerRequest struct {
Alias string `json:"alias"`
}
type UpdateWorkerResponse struct {
Ok bool `json:"ok"`
Message string `json:"message,omitempty"`
} }
type CreateWorkerResponse struct { type CreateWorkerResponse struct {
Ok bool `json:"ok"` Ok bool `json:"ok"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
WorkerId uuid.UUID `json:"id,omitempty"` Worker *storage.Worker `json:"worker,omitempty"`
} }
type GetWorkerResponse struct { type GetWorkerResponse struct {
@ -55,13 +66,13 @@ func (api *WebAPI) WorkerCreate(r *Request) {
return return
} }
id, err := api.workerCreate(workerReq, getIdentity(r)) worker, err := api.workerCreate(workerReq, getIdentity(r))
if err != nil { if err != nil {
handleErr(err, r) handleErr(err, r)
} else { } else {
r.OkJson(CreateWorkerResponse{ r.OkJson(CreateWorkerResponse{
Ok: true, Ok: true,
WorkerId: id, Worker: worker,
}) })
} }
} }
@ -84,6 +95,9 @@ func (api *WebAPI) WorkerGet(r *Request) {
worker := api.Database.GetWorker(id) worker := api.Database.GetWorker(id)
if worker != nil { if worker != nil {
worker.Secret = nil
r.OkJson(GetWorkerResponse{ r.OkJson(GetWorkerResponse{
Ok: true, Ok: true,
Worker: worker, Worker: worker,
@ -136,22 +150,75 @@ func (api *WebAPI) WorkerRemoveAccess(r *Request) {
} }
} }
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (uuid.UUID, error) { func (api *WebAPI) WorkerUpdate(r *Request) {
worker, err := api.validateSignature(r)
if err != nil {
r.Json(GetTaskResponse{
Ok: false,
Message: err.Error(),
}, 403)
return
}
req := &UpdateWorkerRequest{}
if r.GetJson(req) {
worker.Alias = req.Alias
ok := api.Database.UpdateWorker(worker)
if ok {
r.OkJson(UpdateWorkerResponse{
Ok: true,
})
} else {
r.OkJson(UpdateWorkerResponse{
Ok: false,
Message: "Could not update worker",
})
}
}
}
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (*storage.Worker, error) {
if request.Alias == "" {
request.Alias = "default_alias"
}
worker := storage.Worker{ worker := storage.Worker{
Id: uuid.New(), Id: uuid.New(),
Created: time.Now().Unix(), Created: time.Now().Unix(),
Identity: identity, Identity: identity,
Secret: makeSecret(),
Alias: request.Alias,
} }
api.Database.SaveWorker(&worker) api.Database.SaveWorker(&worker)
return worker.Id, nil return &worker, nil
} }
func canCreateWorker(r *Request, cwr *CreateWorkerRequest, identity *storage.Identity) bool { func canCreateWorker(r *Request, cwr *CreateWorkerRequest, identity *storage.Identity) bool {
if cwr.Alias == "unassigned" {
//Reserved alias
return false
}
return true return true
} }
func makeSecret() []byte {
secret := make([]byte, 32)
for i := 0; i < 32; i++ {
secret[i] = byte(rand.Int31())
}
return secret
}
func getIdentity(r *Request) *storage.Identity { func getIdentity(r *Request) *storage.Identity {
identity := storage.Identity{ identity := storage.Identity{

View File

@ -1,9 +1,11 @@
package main package main
import ( import (
"math/rand"
"src/task_tracker/api" "src/task_tracker/api"
"src/task_tracker/config" "src/task_tracker/config"
"src/task_tracker/storage" "src/task_tracker/storage"
"time"
) )
func tmpDebugSetup() { func tmpDebugSetup() {
@ -15,6 +17,7 @@ func tmpDebugSetup() {
func main() { func main() {
rand.Seed(time.Now().UTC().UnixNano())
config.SetupConfig() config.SetupConfig()
webApi := api.New() webApi := api.New()

View File

@ -26,9 +26,10 @@ CREATE TABLE worker_identity
CREATE TABLE worker CREATE TABLE worker
( (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
alias TEXT DEFAULT NULL, alias TEXT,
created INTEGER, created INTEGER,
identity INTEGER REFERENCES workerIdentity (id) identity INTEGER REFERENCES worker_identity (id),
secret BYTEA
); );
CREATE TABLE project CREATE TABLE project
@ -61,7 +62,8 @@ CREATE TABLE task
status Status DEFAULT 'new', status Status DEFAULT 'new',
recipe TEXT, recipe TEXT,
max_assign_time INTEGER DEFAULT 0, max_assign_time INTEGER DEFAULT 0,
assign_time INTEGER DEFAULT 0 assign_time INTEGER DEFAULT 0,
hash64 BIGINT DEFAULT NULL UNIQUE
); );
CREATE TABLE log_entry CREATE TABLE log_entry

View File

@ -3,7 +3,6 @@ package storage
import ( import (
"database/sql" "database/sql"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/google/uuid"
"strings" "strings"
) )
@ -19,7 +18,7 @@ type Project struct {
} }
type AssignedTasks struct { type AssignedTasks struct {
Assignee uuid.UUID `json:"assignee"` Assignee string `json:"assignee"`
TaskCount int64 `json:"task_count"` TaskCount int64 `json:"task_count"`
} }
@ -116,14 +115,16 @@ func (database *Database) GetProjectWithRepoName(repoName string) *Project {
return project return project
} }
func (database *Database) UpdateProject(project *Project) { func (database *Database) UpdateProject(project *Project) error {
db := database.getDB() db := database.getDB()
res, err := db.Exec(`UPDATE project res, err := db.Exec(`UPDATE project
SET (priority, name, clone_url, git_repo, version, motd, public) = ($1,$2,$3,$4,$5,$6,$7) WHERE id=$8`, SET (priority, name, clone_url, git_repo, version, motd, public) = ($1,$2,$3,$4,$5,$6,$7) WHERE id=$8`,
project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Public, project.Id) project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Public, project.Id)
handleErr(err) if err != nil {
return err
}
rowsAffected, _ := res.RowsAffected() rowsAffected, _ := res.RowsAffected()
@ -132,7 +133,7 @@ func (database *Database) UpdateProject(project *Project) {
"rowsAffected": rowsAffected, "rowsAffected": rowsAffected,
}).Trace("Database.updateProject UPDATE project") }).Trace("Database.updateProject UPDATE project")
return return nil
} }
func (database *Database) GetProjectStats(id int64) *ProjectStats { func (database *Database) GetProjectStats(id int64) *ProjectStats {
@ -154,18 +155,27 @@ func (database *Database) GetProjectStats(id int64) *ProjectStats {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
"id": id, "id": id,
}).Trace("Get project stats: No task for this project") }).Trace("Get project stats: No task for this project")
return nil
} }
//todo: only expose worker alias rows, err := db.Query(`SELECT worker.alias, COUNT(*) as wc FROM TASK
rows, err := db.Query(`SELECT assignee, COUNT(*) FROM TASK LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1
LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1 GROUP BY assignee`, id) GROUP BY worker.id ORDER BY wc LIMIT 10`, id)
stats.Assignees = []*AssignedTasks{}
for rows.Next() { for rows.Next() {
assignee := AssignedTasks{} assignee := AssignedTasks{}
err = rows.Scan(&assignee.Assignee, &assignee.TaskCount) var assigneeAlias sql.NullString
err = rows.Scan(&assigneeAlias, &assignee.TaskCount)
handleErr(err) handleErr(err)
if assigneeAlias.Valid {
assignee.Assignee = assigneeAlias.String
} else {
assignee.Assignee = "unassigned"
}
stats.Assignees = append(stats.Assignees, &assignee) stats.Assignees = append(stats.Assignees, &assignee)
} }
} }
@ -182,8 +192,8 @@ func (database Database) GetAllProjectsStats() *[]ProjectStats {
SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) failedCount, SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) failedCount,
SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) closedCount, SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) closedCount,
p.* p.*
FROM task INNER JOIN project p on task.project = p.id FROM task RIGHT JOIN project p on task.project = p.id
GROUP BY p.id`) GROUP BY p.id ORDER BY p.name`)
handleErr(err) handleErr(err)
for rows.Next() { for rows.Next() {
@ -191,7 +201,7 @@ func (database Database) GetAllProjectsStats() *[]ProjectStats {
stats := ProjectStats{} stats := ProjectStats{}
p := &Project{} p := &Project{}
err := rows.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount, err := rows.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount,
&p.Id, &p.Priority, &p.Motd, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, &p.Public) &p.Id, &p.Priority, &p.Name, &p.CloneUrl, &p.GitRepo, &p.Version, &p.Motd, &p.Public)
handleErr(err) handleErr(err)
stats.Project = p stats.Project = p

View File

@ -2,6 +2,7 @@ package storage
import ( import (
"database/sql" "database/sql"
"fmt"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -19,13 +20,14 @@ type Task struct {
AssignTime int64 `json:"assign_time"` AssignTime int64 `json:"assign_time"`
} }
func (database *Database) SaveTask(task *Task, project int64) error { func (database *Database) SaveTask(task *Task, project int64, hash64 int64) error {
db := database.getDB() db := database.getDB()
res, err := db.Exec(` //TODO: For some reason it refuses to insert the 64-bit value unless I do that...
INSERT INTO task (project, max_retries, recipe, priority, max_assign_time) res, err := db.Exec(fmt.Sprintf(`
VALUES ($1,$2,$3,$4,$5)`, INSERT INTO task (project, max_retries, recipe, priority, max_assign_time, hash64)
VALUES ($1,$2,$3,$4,$5,NULLIF(%d, 0))`, hash64),
project, task.MaxRetries, task.Recipe, task.Priority, task.MaxAssignTime) project, task.MaxRetries, task.Recipe, task.Priority, task.MaxAssignTime)
if err != nil { if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{ logrus.WithError(err).WithFields(logrus.Fields{
@ -57,7 +59,7 @@ func (database *Database) GetTask(worker *Worker) *Task {
SELECT task.id SELECT task.id
FROM task FROM task
INNER JOIN project p on task.project = p.id INNER JOIN project p on task.project = p.id
WHERE assignee IS NULL WHERE assignee IS NULL AND task.status='new'
AND (p.public OR EXISTS ( AND (p.public OR EXISTS (
SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=p.id SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=p.id
)) ))
@ -88,7 +90,8 @@ func (database *Database) GetTask(worker *Worker) *Task {
func getTaskById(id int64, db *sql.DB) *Task { func getTaskById(id int64, db *sql.DB) *Task {
row := db.QueryRow(` row := db.QueryRow(`
SELECT * FROM task SELECT task.id, task.priority, task.project, assignee, retries, max_retries,
status, recipe, max_assign_time, assign_time, project.* FROM task
INNER JOIN project ON task.project = project.id INNER JOIN project ON task.project = project.id
WHERE task.id=$1`, id) WHERE task.id=$1`, id)
task := scanTask(row) task := scanTask(row)
@ -109,11 +112,11 @@ func (database Database) ReleaseTask(id int64, workerId *uuid.UUID, success bool
var err error var err error
if success { if success {
res, err = db.Exec(`UPDATE task SET (status, assignee) = ('closed', NULL) res, err = db.Exec(`UPDATE task SET (status, assignee) = ('closed', NULL)
WHERE id=$2 AND task.assignee=$2`, id, workerId) WHERE id=$1 AND task.assignee=$2`, id, workerId)
} else { } else {
res, err = db.Exec(`UPDATE task SET (status, assignee, retries) = res, err = db.Exec(`UPDATE task SET (status, assignee, retries) =
(CASE WHEN retries+1 >= max_retries THEN 'failed' ELSE 'new' END, NULL, retries+1) (CASE WHEN retries+1 >= max_retries THEN 'failed' ELSE 'new' END, NULL, retries+1)
WHERE id=$2 AND assignee=$2`, id, workerId) WHERE id=$1 AND assignee=$2`, id, workerId)
} }
handleErr(err) handleErr(err)
@ -138,7 +141,7 @@ func (database *Database) GetTaskFromProject(worker *Worker, projectId int64) *T
SELECT task.id SELECT task.id
FROM task FROM task
INNER JOIN project p on task.project = p.id INNER JOIN project p on task.project = p.id
WHERE assignee IS NULL AND p.id=$2 WHERE assignee IS NULL AND p.id=$2 AND status='new'
AND (p.public OR EXISTS ( AND (p.public OR EXISTS (
SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=$2 SELECT 1 FROM worker_has_access_to_project a WHERE a.worker=$1 AND a.project=$2
)) ))

View File

@ -16,6 +16,8 @@ type Worker struct {
Id uuid.UUID `json:"id"` Id uuid.UUID `json:"id"`
Created int64 `json:"created"` Created int64 `json:"created"`
Identity *Identity `json:"identity"` Identity *Identity `json:"identity"`
Alias string `json:"alias,omitempty"`
Secret []byte `json:"secret"`
} }
func (database *Database) SaveWorker(worker *Worker) { func (database *Database) SaveWorker(worker *Worker) {
@ -24,8 +26,8 @@ func (database *Database) SaveWorker(worker *Worker) {
identityId := getOrCreateIdentity(worker.Identity, db) identityId := getOrCreateIdentity(worker.Identity, db)
res, err := db.Exec("INSERT INTO worker (id, created, identity) VALUES ($1,$2,$3)", res, err := db.Exec("INSERT INTO worker (id, created, identity, secret, alias) VALUES ($1,$2,$3,$4,$5)",
worker.Id, worker.Created, identityId) worker.Id, worker.Created, identityId, worker.Secret, worker.Alias)
handleErr(err) handleErr(err)
var rowsAffected, _ = res.RowsAffected() var rowsAffected, _ = res.RowsAffected()
@ -41,8 +43,8 @@ func (database *Database) GetWorker(id uuid.UUID) *Worker {
worker := &Worker{} worker := &Worker{}
var identityId int64 var identityId int64
row := db.QueryRow("SELECT id, created, identity FROM worker WHERE id=$1", id) row := db.QueryRow("SELECT id, created, identity, secret, alias FROM worker WHERE id=$1", id)
err := row.Scan(&worker.Id, &worker.Created, &identityId) err := row.Scan(&worker.Id, &worker.Created, &identityId, &worker.Secret, &worker.Alias)
if err != nil { if err != nil {
logrus.WithFields(logrus.Fields{ logrus.WithFields(logrus.Fields{
"id": id, "id": id,
@ -64,7 +66,7 @@ func getIdentity(id int64, db *sql.DB) (*Identity, error) {
identity := &Identity{} identity := &Identity{}
row := db.QueryRow("SELECT remote_addr, user_agent FROM workeridentity WHERE id=$1", id) row := db.QueryRow("SELECT remote_addr, user_agent FROM worker_identity WHERE id=$1", id)
err := row.Scan(&identity.RemoteAddr, &identity.UserAgent) err := row.Scan(&identity.RemoteAddr, &identity.UserAgent)
if err != nil { if err != nil {
@ -80,7 +82,7 @@ func getIdentity(id int64, db *sql.DB) (*Identity, error) {
func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 { func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 {
res, err := db.Exec("INSERT INTO workeridentity (remote_addr, user_agent) VALUES ($1,$2) ON CONFLICT DO NOTHING", res, err := db.Exec("INSERT INTO worker_identity (remote_addr, user_agent) VALUES ($1,$2) ON CONFLICT DO NOTHING",
identity.RemoteAddr, identity.UserAgent) identity.RemoteAddr, identity.UserAgent)
handleErr(err) handleErr(err)
@ -89,7 +91,7 @@ func getOrCreateIdentity(identity *Identity, db *sql.DB) int64 {
"rowsAffected": rowsAffected, "rowsAffected": rowsAffected,
}).Trace("Database.saveWorker INSERT workerIdentity") }).Trace("Database.saveWorker INSERT workerIdentity")
row := db.QueryRow("SELECT (id) FROM workeridentity WHERE remote_addr=$1", identity.RemoteAddr) row := db.QueryRow("SELECT (id) FROM worker_identity WHERE remote_addr=$1", identity.RemoteAddr)
var rowId int64 var rowId int64
err = row.Scan(&rowId) err = row.Scan(&rowId)
@ -127,7 +129,7 @@ func (database *Database) GrantAccess(workerId *uuid.UUID, projectId int64) bool
return rowsAffected == 1 return rowsAffected == 1
} }
func (database Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool { func (database *Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool {
db := database.getDB() db := database.getDB()
res, err := db.Exec(`DELETE FROM worker_has_access_to_project WHERE worker=$1 AND project=$2`, res, err := db.Exec(`DELETE FROM worker_has_access_to_project WHERE worker=$1 AND project=$2`,
@ -144,3 +146,20 @@ func (database Database) RemoveAccess(workerId *uuid.UUID, projectId int64) bool
return rowsAffected == 1 return rowsAffected == 1
} }
func (database *Database) UpdateWorker(worker *Worker) bool {
db := database.getDB()
res, err := db.Exec(`UPDATE worker SET alias=$1 WHERE id=$2`,
worker.Alias, worker.Id)
handleErr(err)
rowsAffected, _ := res.RowsAffected()
logrus.WithFields(logrus.Fields{
"rowsAffected": rowsAffected,
"worker": worker,
}).Trace("Database.UpdateWorker UPDATE worker")
return rowsAffected == 1
}

View File

@ -13,7 +13,7 @@ import (
func TestWebHookNoSignature(t *testing.T) { func TestWebHookNoSignature(t *testing.T) {
r := Post("/git/receivehook", api.GitPayload{}) r := Post("/git/receivehook", api.GitPayload{}, nil)
if r.StatusCode != 403 { if r.StatusCode != 403 {
t.Error() t.Error()

View File

@ -9,7 +9,7 @@ import (
func TestIndex(t *testing.T) { func TestIndex(t *testing.T) {
r := Get("/") r := Get("/", nil)
body, _ := ioutil.ReadAll(r.Body) body, _ := ioutil.ReadAll(r.Body)
var info api.Info var info api.Info

View File

@ -16,7 +16,7 @@ func TestTraceValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "This is a test message", Message: "This is a test message",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
@ -27,7 +27,7 @@ func TestTraceInvalidScope(t *testing.T) {
r := Post("/log/trace", api.LogRequest{ r := Post("/log/trace", api.LogRequest{
Message: "this is a test message", Message: "this is a test message",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 500 { if r.StatusCode != 500 {
t.Fail() t.Fail()
@ -37,7 +37,7 @@ func TestTraceInvalidScope(t *testing.T) {
Scope: "", Scope: "",
Message: "this is a test message", Message: "this is a test message",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 500 { if r.StatusCode != 500 {
t.Fail() t.Fail()
@ -52,7 +52,7 @@ func TestTraceInvalidMessage(t *testing.T) {
Scope: "test", Scope: "test",
Message: "", Message: "",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 500 { if r.StatusCode != 500 {
t.Fail() t.Fail()
@ -66,7 +66,7 @@ func TestTraceInvalidTime(t *testing.T) {
r := Post("/log/trace", api.LogRequest{ r := Post("/log/trace", api.LogRequest{
Scope: "test", Scope: "test",
Message: "test", Message: "test",
}) }, nil)
if r.StatusCode != 500 { if r.StatusCode != 500 {
t.Fail() t.Fail()
} }
@ -81,7 +81,7 @@ func TestWarnValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "test", Message: "test",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
} }
@ -93,7 +93,7 @@ func TestInfoValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "test", Message: "test",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
} }
@ -105,7 +105,7 @@ func TestErrorValid(t *testing.T) {
Scope: "test", Scope: "test",
Message: "test", Message: "test",
TimeStamp: time.Now().Unix(), TimeStamp: time.Now().Unix(),
}) }, nil)
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Fail() t.Fail()
} }
@ -171,7 +171,7 @@ func getLogs(since int64, level logrus.Level) *api.GetLogResponse {
r := Post(fmt.Sprintf("/logs"), api.GetLogRequest{ r := Post(fmt.Sprintf("/logs"), api.GetLogRequest{
Since: since, Since: since,
Level: level, Level: level,
}) }, nil)
resp := &api.GetLogResponse{} resp := &api.GetLogResponse{}
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)

View File

@ -3,7 +3,6 @@ package test
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/google/uuid"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"src/task_tracker/api" "src/task_tracker/api"
@ -132,28 +131,30 @@ func TestGetProjectStats(t *testing.T) {
CloneUrl: "http://github.com/drone/test", CloneUrl: "http://github.com/drone/test",
GitRepo: "drone/test", GitRepo: "drone/test",
Priority: 3, Priority: 3,
Public: true,
}) })
pid := r.Id pid := r.Id
worker := genWid()
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Priority: 1, Priority: 1,
Project: pid, Project: pid,
MaxRetries: 0, MaxRetries: 0,
Recipe: "{}", Recipe: "{}",
}) }, worker)
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Priority: 2, Priority: 2,
Project: pid, Project: pid,
MaxRetries: 0, MaxRetries: 0,
Recipe: "{}", Recipe: "{}",
}) }, worker)
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Priority: 3, Priority: 3,
Project: pid, Project: pid,
MaxRetries: 0, MaxRetries: 0,
Recipe: "{}", Recipe: "{}",
}) }, worker)
stats := getProjectStats(pid) stats := getProjectStats(pid)
@ -169,7 +170,7 @@ func TestGetProjectStats(t *testing.T) {
t.Error() t.Error()
} }
if stats.Stats.Assignees[0].Assignee != uuid.Nil { if stats.Stats.Assignees[0].Assignee != "unassigned" {
t.Error() t.Error()
} }
if stats.Stats.Assignees[0].TaskCount != 3 { if stats.Stats.Assignees[0].TaskCount != 3 {
@ -189,19 +190,132 @@ func TestGetProjectStatsNotFound(t *testing.T) {
}) })
s := getProjectStats(r.Id) s := getProjectStats(r.Id)
if s.Ok != false { if s.Ok != true {
t.Error() t.Error()
} }
if len(s.Message) <= 0 { if s.Stats == nil {
t.Error() t.Error()
} }
} }
func TestUpdateProjectValid(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
Public: true,
Version: "versionA",
Motd: "MotdA",
Name: "NameA",
CloneUrl: "CloneUrlA",
GitRepo: "GitRepoA",
Priority: 1,
}).Id
updateResp := updateProject(api.UpdateProjectRequest{
Priority: 2,
GitRepo: "GitRepoB",
CloneUrl: "CloneUrlB",
Name: "NameB",
Motd: "MotdB",
Public: false,
}, pid)
if updateResp.Ok != true {
t.Error()
}
proj, _ := getProject(pid)
if proj.Project.Public != false {
t.Error()
}
if proj.Project.Motd != "MotdB" {
t.Error()
}
if proj.Project.CloneUrl != "CloneUrlB" {
t.Error()
}
if proj.Project.GitRepo != "GitRepoB" {
t.Error()
}
if proj.Project.Priority != 2 {
t.Error()
}
}
func TestUpdateProjectInvalid(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
Public: true,
Version: "lllllllllllll",
Motd: "2wwwwwwwwwwwwwww",
Name: "aaaaaaaaaaaaaaaaaaaaaa",
CloneUrl: "333333333333333",
GitRepo: "llllllllllllllllllls",
Priority: 1,
}).Id
updateResp := updateProject(api.UpdateProjectRequest{
Priority: -1,
GitRepo: "GitRepo------",
CloneUrl: "CloneUrlB000000",
Name: "NameB-0",
Motd: "MotdB000000",
Public: false,
}, pid)
if updateResp.Ok != false {
t.Error()
}
if len(updateResp.Message) <= 0 {
t.Error()
}
}
func TestUpdateProjectConstraintFail(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
Public: true,
Version: "testUpdateProjectConstraintFail",
Motd: "testUpdateProjectConstraintFail",
Name: "testUpdateProjectConstraintFail",
CloneUrl: "testUpdateProjectConstraintFail",
GitRepo: "testUpdateProjectConstraintFail",
Priority: 1,
}).Id
createProject(api.CreateProjectRequest{
Public: true,
Version: "testUpdateProjectConstraintFail_d",
Motd: "testUpdateProjectConstraintFail_d",
Name: "testUpdateProjectConstraintFail_d",
CloneUrl: "testUpdateProjectConstraintFail_d",
GitRepo: "testUpdateProjectConstraintFail_d",
Priority: 1,
})
updateResp := updateProject(api.UpdateProjectRequest{
Priority: 1,
GitRepo: "testUpdateProjectConstraintFail_d",
CloneUrl: "testUpdateProjectConstraintFail_d",
Name: "testUpdateProjectConstraintFail_d",
Motd: "testUpdateProjectConstraintFail_d",
}, pid)
if updateResp.Ok != false {
t.Error()
}
if len(updateResp.Message) <= 0 {
t.Error()
}
}
func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse { func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse {
r := Post("/project/create", req) r := Post("/project/create", req, nil)
var resp api.CreateProjectResponse var resp api.CreateProjectResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -213,7 +327,7 @@ func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse {
func getProject(id int64) (*api.GetProjectResponse, *http.Response) { func getProject(id int64) (*api.GetProjectResponse, *http.Response) {
r := Get(fmt.Sprintf("/project/get/%d", id)) r := Get(fmt.Sprintf("/project/get/%d", id), nil)
var getResp api.GetProjectResponse var getResp api.GetProjectResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -225,7 +339,7 @@ func getProject(id int64) (*api.GetProjectResponse, *http.Response) {
func getProjectStats(id int64) *api.GetProjectStatsResponse { func getProjectStats(id int64) *api.GetProjectStatsResponse {
r := Get(fmt.Sprintf("/project/stats/%d", id)) r := Get(fmt.Sprintf("/project/stats/%d", id), nil)
var getResp api.GetProjectStatsResponse var getResp api.GetProjectStatsResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -234,3 +348,15 @@ func getProjectStats(id int64) *api.GetProjectStatsResponse {
return &getResp return &getResp
} }
func updateProject(request api.UpdateProjectRequest, pid int64) *api.UpdateProjectResponse {
r := Post(fmt.Sprintf("/project/update/%d", pid), request, nil)
var resp api.UpdateProjectResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &resp)
handleErr(err)
return &resp
}

View File

@ -15,6 +15,8 @@ func BenchmarkCreateTask(b *testing.B) {
CloneUrl: "http://localhost", CloneUrl: "http://localhost",
}) })
worker := genWid()
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
@ -22,6 +24,6 @@ func BenchmarkCreateTask(b *testing.B) {
Priority: 1, Priority: 1,
Recipe: "{}", Recipe: "{}",
MaxRetries: 1, MaxRetries: 1,
}) }, worker)
} }
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"io/ioutil" "io/ioutil"
"src/task_tracker/api" "src/task_tracker/api"
"src/task_tracker/storage"
"testing" "testing"
) )
@ -18,11 +19,13 @@ func TestCreateTaskValid(t *testing.T) {
CloneUrl: "http://github.com/test/test", CloneUrl: "http://github.com/test/test",
}) })
worker := genWid()
resp := createTask(api.CreateTaskRequest{ resp := createTask(api.CreateTaskRequest{
Project: 1, Project: 1,
Recipe: "{}", Recipe: "{}",
MaxRetries: 3, MaxRetries: 3,
}) }, worker)
if resp.Ok != true { if resp.Ok != true {
t.Fail() t.Fail()
@ -31,11 +34,13 @@ func TestCreateTaskValid(t *testing.T) {
func TestCreateTaskInvalidProject(t *testing.T) { func TestCreateTaskInvalidProject(t *testing.T) {
worker := genWid()
resp := createTask(api.CreateTaskRequest{ resp := createTask(api.CreateTaskRequest{
Project: 123456, Project: 123456,
Recipe: "{}", Recipe: "{}",
MaxRetries: 3, MaxRetries: 3,
}) }, worker)
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -62,7 +67,9 @@ func TestGetTaskInvalidWid(t *testing.T) {
func TestGetTaskInvalidWorker(t *testing.T) { func TestGetTaskInvalidWorker(t *testing.T) {
id := uuid.New() id := uuid.New()
resp := getTask(&id) resp := getTask(&storage.Worker{
Id: id,
})
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -76,7 +83,9 @@ func TestGetTaskInvalidWorker(t *testing.T) {
func TestGetTaskFromProjectInvalidWorker(t *testing.T) { func TestGetTaskFromProjectInvalidWorker(t *testing.T) {
id := uuid.New() id := uuid.New()
resp := getTaskFromProject(1, &id) resp := getTaskFromProject(1, &storage.Worker{
Id: id,
})
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -89,10 +98,12 @@ func TestGetTaskFromProjectInvalidWorker(t *testing.T) {
func TestCreateTaskInvalidRetries(t *testing.T) { func TestCreateTaskInvalidRetries(t *testing.T) {
worker := genWid()
resp := createTask(api.CreateTaskRequest{ resp := createTask(api.CreateTaskRequest{
Project: 1, Project: 1,
MaxRetries: -1, MaxRetries: -1,
}) }, worker)
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -105,11 +116,13 @@ func TestCreateTaskInvalidRetries(t *testing.T) {
func TestCreateTaskInvalidRecipe(t *testing.T) { func TestCreateTaskInvalidRecipe(t *testing.T) {
worker := genWid()
resp := createTask(api.CreateTaskRequest{ resp := createTask(api.CreateTaskRequest{
Project: 1, Project: 1,
Recipe: "", Recipe: "",
MaxRetries: 3, MaxRetries: 3,
}) }, worker)
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -132,12 +145,14 @@ func TestCreateGetTask(t *testing.T) {
Public: true, Public: true,
}) })
worker := genWid()
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Project: resp.Id, Project: resp.Id,
Recipe: "{\"url\":\"test\"}", Recipe: "{\"url\":\"test\"}",
MaxRetries: 3, MaxRetries: 3,
Priority: 9999, Priority: 9999,
}) }, worker)
taskResp := getTaskFromProject(resp.Id, genWid()) taskResp := getTaskFromProject(resp.Id, genWid())
@ -194,26 +209,27 @@ func createTasks(prefix string) (int64, int64) {
Priority: 999, Priority: 999,
Public: true, Public: true,
}) })
worker := genWid()
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Project: lowP.Id, Project: lowP.Id,
Recipe: "low1", Recipe: "low1",
Priority: 0, Priority: 0,
}) }, worker)
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Project: lowP.Id, Project: lowP.Id,
Recipe: "low2", Recipe: "low2",
Priority: 1, Priority: 1,
}) }, worker)
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Project: highP.Id, Project: highP.Id,
Recipe: "high1", Recipe: "high1",
Priority: 100, Priority: 100,
}) }, worker)
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
Project: highP.Id, Project: highP.Id,
Recipe: "high2", Recipe: "high2",
Priority: 101, Priority: 101,
}) }, worker)
return lowP.Id, highP.Id return lowP.Id, highP.Id
} }
@ -274,7 +290,7 @@ func TestTaskPriority(t *testing.T) {
func TestTaskNoAccess(t *testing.T) { func TestTaskNoAccess(t *testing.T) {
wid := genWid() worker := genWid()
pid := createProject(api.CreateProjectRequest{ pid := createProject(api.CreateProjectRequest{
Name: "This is a private proj", Name: "This is a private proj",
@ -292,16 +308,16 @@ func TestTaskNoAccess(t *testing.T) {
MaxAssignTime: 10, MaxAssignTime: 10,
MaxRetries: 2, MaxRetries: 2,
Recipe: "---", Recipe: "---",
}) }, worker)
if createResp.Ok != true { if createResp.Ok != true {
t.Error() t.Error()
} }
grantAccess(wid, pid) grantAccess(&worker.Id, pid)
removeAccess(wid, pid) removeAccess(&worker.Id, pid)
tResp := getTaskFromProject(pid, wid) tResp := getTaskFromProject(pid, worker)
if tResp.Ok != false { if tResp.Ok != false {
t.Error() t.Error()
@ -316,7 +332,7 @@ func TestTaskNoAccess(t *testing.T) {
func TestTaskHasAccess(t *testing.T) { func TestTaskHasAccess(t *testing.T) {
wid := genWid() worker := genWid()
pid := createProject(api.CreateProjectRequest{ pid := createProject(api.CreateProjectRequest{
Name: "This is a private proj1", Name: "This is a private proj1",
@ -334,15 +350,15 @@ func TestTaskHasAccess(t *testing.T) {
MaxAssignTime: 10, MaxAssignTime: 10,
MaxRetries: 2, MaxRetries: 2,
Recipe: "---", Recipe: "---",
}) }, worker)
if createResp.Ok != true { if createResp.Ok != true {
t.Error() t.Error()
} }
grantAccess(wid, pid) grantAccess(&worker.Id, pid)
tResp := getTaskFromProject(pid, wid) tResp := getTaskFromProject(pid, worker)
if tResp.Ok != true { if tResp.Ok != true {
t.Error() t.Error()
@ -354,16 +370,16 @@ func TestTaskHasAccess(t *testing.T) {
func TestNoMoreTasks(t *testing.T) { func TestNoMoreTasks(t *testing.T) {
wid := genWid() worker := genWid()
for i := 0; i < 15; i++ { for i := 0; i < 15; i++ {
getTask(wid) getTask(worker)
} }
} }
func TestReleaseTaskSuccess(t *testing.T) { func TestReleaseTaskSuccess(t *testing.T) {
//wid := genWid() worker := genWid()
pid := createProject(api.CreateProjectRequest{ pid := createProject(api.CreateProjectRequest{
Priority: 0, Priority: 0,
@ -372,6 +388,7 @@ func TestReleaseTaskSuccess(t *testing.T) {
Version: "11111111111111111", Version: "11111111111111111",
Name: "testreleasetask", Name: "testreleasetask",
Motd: "", Motd: "",
Public: true,
}).Id }).Id
createTask(api.CreateTaskRequest{ createTask(api.CreateTaskRequest{
@ -379,13 +396,119 @@ func TestReleaseTaskSuccess(t *testing.T) {
Project: pid, Project: pid,
Recipe: "{}", Recipe: "{}",
MaxRetries: 3, MaxRetries: 3,
}) }, worker)
task := getTaskFromProject(pid, worker).Task
releaseResp := releaseTask(api.ReleaseTaskRequest{
TaskId: task.Id,
Success: true,
}, worker)
if releaseResp.Ok != true {
t.Error()
}
otherTask := getTaskFromProject(pid, worker)
//Shouldn't have more tasks available
if otherTask.Ok != false {
t.Error()
}
} }
func createTask(request api.CreateTaskRequest) *api.CreateTaskResponse { func TestCreateIntCollision(t *testing.T) {
r := Post("/task/create", request) pid := createProject(api.CreateProjectRequest{
Priority: 1,
GitRepo: "testcreateintcollision",
CloneUrl: "testcreateintcollision",
Motd: "testcreateintcollision",
Public: true,
Name: "testcreateintcollision",
Version: "testcreateintcollision",
}).Id
w := genWid()
if createTask(api.CreateTaskRequest{
Project: pid,
Hash64: 123,
Priority: 1,
Recipe: "{}",
}, w).Ok != true {
t.Error()
}
resp := createTask(api.CreateTaskRequest{
Project: pid,
Hash64: 123,
Priority: 1,
Recipe: "{}",
}, w)
if resp.Ok != false {
t.Error()
}
fmt.Println(resp.Message)
if len(resp.Message) <= 0 {
t.Error()
}
}
func TestCreateStringCollision(t *testing.T) {
pid := createProject(api.CreateProjectRequest{
Priority: 1,
GitRepo: "testcreatestringcollision",
CloneUrl: "testcreatestringcollision",
Motd: "testcreatestringcollision",
Public: true,
Name: "testcreatestringcollision",
Version: "testcreatestringcollision",
}).Id
w := genWid()
if createTask(api.CreateTaskRequest{
Project: pid,
UniqueString: "Hello, world",
Priority: 1,
Recipe: "{}",
}, w).Ok != true {
t.Error()
}
resp := createTask(api.CreateTaskRequest{
Project: pid,
UniqueString: "Hello, world",
Priority: 1,
Recipe: "{}",
}, w)
if !createTask(api.CreateTaskRequest{
Project: pid,
UniqueString: "This one should work",
Priority: 1,
Recipe: "{}",
}, w).Ok {
t.Error()
}
if resp.Ok != false {
t.Error()
}
fmt.Println(resp.Message)
if len(resp.Message) <= 0 {
t.Error()
}
}
func createTask(request api.CreateTaskRequest, worker *storage.Worker) *api.CreateTaskResponse {
r := Post("/task/create", request, worker)
var resp api.CreateTaskResponse var resp api.CreateTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -395,9 +518,9 @@ func createTask(request api.CreateTaskRequest) *api.CreateTaskResponse {
return &resp return &resp
} }
func getTask(wid *uuid.UUID) *api.GetTaskResponse { func getTask(worker *storage.Worker) *api.GetTaskResponse {
r := Get(fmt.Sprintf("/task/get?wid=%s", wid)) r := Get(fmt.Sprintf("/task/get"), worker)
var resp api.GetTaskResponse var resp api.GetTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -407,9 +530,9 @@ func getTask(wid *uuid.UUID) *api.GetTaskResponse {
return &resp return &resp
} }
func getTaskFromProject(project int64, wid *uuid.UUID) *api.GetTaskResponse { func getTaskFromProject(project int64, worker *storage.Worker) *api.GetTaskResponse {
r := Get(fmt.Sprintf("/task/get/%d?wid=%s", project, wid)) r := Get(fmt.Sprintf("/task/get/%d", project), worker)
var resp api.GetTaskResponse var resp api.GetTaskResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -418,3 +541,15 @@ func getTaskFromProject(project int64, wid *uuid.UUID) *api.GetTaskResponse {
return &resp return &resp
} }
func releaseTask(request api.ReleaseTaskRequest, worker *storage.Worker) *api.ReleaseTaskResponse {
r := Post("/task/release", request, worker)
var resp api.ReleaseTaskResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &resp)
handleErr(err)
return &resp
}

View File

@ -7,12 +7,15 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"src/task_tracker/api" "src/task_tracker/api"
"src/task_tracker/storage"
"testing" "testing"
) )
func TestCreateGetWorker(t *testing.T) { func TestCreateGetWorker(t *testing.T) {
resp, r := createWorker(api.CreateWorkerRequest{}) resp, r := createWorker(api.CreateWorkerRequest{
Alias: "my_worker_alias",
})
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Error() t.Error()
@ -22,12 +25,12 @@ func TestCreateGetWorker(t *testing.T) {
t.Error() t.Error()
} }
getResp, r := getWorker(resp.WorkerId.String()) getResp, r := getWorker(resp.Worker.Id.String())
if r.StatusCode != 200 { if r.StatusCode != 200 {
t.Error() t.Error()
} }
if resp.WorkerId != getResp.Worker.Id { if resp.Worker.Id != getResp.Worker.Id {
t.Error() t.Error()
} }
@ -37,6 +40,9 @@ func TestCreateGetWorker(t *testing.T) {
if len(getResp.Worker.Identity.UserAgent) <= 0 { if len(getResp.Worker.Identity.UserAgent) <= 0 {
t.Error() t.Error()
} }
if resp.Worker.Alias != "my_worker_alias" {
t.Error()
}
} }
func TestGetWorkerNotFound(t *testing.T) { func TestGetWorkerNotFound(t *testing.T) {
@ -70,7 +76,7 @@ func TestGrantAccessFailedProjectConstraint(t *testing.T) {
wid := genWid() wid := genWid()
resp := grantAccess(wid, 38274593) resp := grantAccess(&wid.Id, 38274593)
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -82,9 +88,9 @@ func TestGrantAccessFailedProjectConstraint(t *testing.T) {
func TestRemoveAccessFailedProjectConstraint(t *testing.T) { func TestRemoveAccessFailedProjectConstraint(t *testing.T) {
wid := genWid() worker := genWid()
resp := removeAccess(wid, 38274593) resp := removeAccess(&worker.Id, 38274593)
if resp.Ok != false { if resp.Ok != false {
t.Error() t.Error()
@ -138,8 +144,43 @@ func TestGrantAccessFailedWorkerConstraint(t *testing.T) {
} }
} }
func TestUpdateAliasValid(t *testing.T) {
wid := genWid()
updateResp := updateWorker(api.UpdateWorkerRequest{
Alias: "new alias",
}, wid)
if updateResp.Ok != true {
t.Error()
}
w, _ := getWorker(wid.Id.String())
if w.Worker.Alias != "new alias" {
t.Error()
}
}
func TestCreateWorkerAliasInvalid(t *testing.T) {
resp, _ := createWorker(api.CreateWorkerRequest{
Alias: "unassigned", //reserved alias
})
if resp.Ok != false {
t.Error()
}
if len(resp.Message) <= 0 {
t.Error()
}
}
func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) { func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http.Response) {
r := Post("/worker/create", req) r := Post("/worker/create", req, nil)
var resp *api.CreateWorkerResponse var resp *api.CreateWorkerResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -151,7 +192,7 @@ func createWorker(req api.CreateWorkerRequest) (*api.CreateWorkerResponse, *http
func getWorker(id string) (*api.GetWorkerResponse, *http.Response) { func getWorker(id string) (*api.GetWorkerResponse, *http.Response) {
r := Get(fmt.Sprintf("/worker/get/%s", id)) r := Get(fmt.Sprintf("/worker/get/%s", id), nil)
var resp *api.GetWorkerResponse var resp *api.GetWorkerResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -161,10 +202,10 @@ func getWorker(id string) (*api.GetWorkerResponse, *http.Response) {
return resp, r return resp, r
} }
func genWid() *uuid.UUID { func genWid() *storage.Worker {
resp, _ := createWorker(api.CreateWorkerRequest{}) resp, _ := createWorker(api.CreateWorkerRequest{})
return &resp.WorkerId return resp.Worker
} }
func grantAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse { func grantAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse {
@ -172,7 +213,7 @@ func grantAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse {
r := Post("/access/grant", api.WorkerAccessRequest{ r := Post("/access/grant", api.WorkerAccessRequest{
WorkerId: wid, WorkerId: wid,
ProjectId: project, ProjectId: project,
}) }, nil)
var resp *api.WorkerAccessResponse var resp *api.WorkerAccessResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -187,7 +228,7 @@ func removeAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse {
r := Post("/access/remove", api.WorkerAccessRequest{ r := Post("/access/remove", api.WorkerAccessRequest{
WorkerId: wid, WorkerId: wid,
ProjectId: project, ProjectId: project,
}) }, nil)
var resp *api.WorkerAccessResponse var resp *api.WorkerAccessResponse
data, _ := ioutil.ReadAll(r.Body) data, _ := ioutil.ReadAll(r.Body)
@ -196,3 +237,15 @@ func removeAccess(wid *uuid.UUID, project int64) *api.WorkerAccessResponse {
return resp return resp
} }
func updateWorker(request api.UpdateWorkerRequest, w *storage.Worker) *api.UpdateWorkerResponse {
r := Post("/worker/update", request, w)
var resp *api.UpdateWorkerResponse
data, _ := ioutil.ReadAll(r.Body)
err := json.Unmarshal(data, &resp)
handleErr(err)
return resp
}

View File

@ -2,26 +2,61 @@ package test
import ( import (
"bytes" "bytes"
"crypto"
"crypto/hmac"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"src/task_tracker/config" "src/task_tracker/config"
"src/task_tracker/storage"
) )
func Post(path string, x interface{}) *http.Response { func Post(path string, x interface{}, worker *storage.Worker) *http.Response {
body, err := json.Marshal(x) body, err := json.Marshal(x)
buf := bytes.NewBuffer(body) buf := bytes.NewBuffer(body)
r, err := http.Post("http://"+config.Cfg.ServerAddr+path, "application/json", buf) req, err := http.NewRequest("POST", "http://"+config.Cfg.ServerAddr+path, buf)
handleErr(err)
if worker != nil {
mac := hmac.New(crypto.SHA256.New, worker.Secret)
mac.Write(body)
sig := hex.EncodeToString(mac.Sum(nil))
req.Header.Add("X-Worker-Id", worker.Id.String())
req.Header.Add("X-Signature", sig)
}
client := http.Client{}
r, err := client.Do(req)
handleErr(err) handleErr(err)
return r return r
} }
func Get(path string) *http.Response { func Get(path string, worker *storage.Worker) *http.Response {
r, err := http.Get("http://" + config.Cfg.ServerAddr + path)
url := "http://" + config.Cfg.ServerAddr + path
req, err := http.NewRequest("GET", url, nil)
handleErr(err)
if worker != nil {
fmt.Println(worker.Secret)
mac := hmac.New(crypto.SHA256.New, worker.Secret)
mac.Write([]byte(path))
sig := hex.EncodeToString(mac.Sum(nil))
req.Header.Add("X-Worker-Id", worker.Id.String())
req.Header.Add("X-Signature", sig)
}
client := http.Client{}
r, err := client.Do(req)
handleErr(err) handleErr(err)
return r return r

View File

@ -26,9 +26,10 @@ CREATE TABLE worker_identity
CREATE TABLE worker CREATE TABLE worker
( (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
alias TEXT DEFAULT NULL, alias TEXT,
created INTEGER, created INTEGER,
identity INTEGER REFERENCES workerIdentity (id) identity INTEGER REFERENCES worker_identity (id),
secret BYTEA
); );
CREATE TABLE project CREATE TABLE project
@ -61,7 +62,8 @@ CREATE TABLE task
status Status DEFAULT 'new', status Status DEFAULT 'new',
recipe TEXT, recipe TEXT,
max_assign_time INTEGER DEFAULT 0, max_assign_time INTEGER DEFAULT 0,
assign_time INTEGER DEFAULT 0 assign_time INTEGER DEFAULT 0,
hash64 BIGINT UNIQUE
); );
CREATE TABLE log_entry CREATE TABLE log_entry

View File

@ -1,5 +1,6 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {Project} from "./models/project";
@Injectable() @Injectable()
export class ApiService { export class ApiService {
@ -26,4 +27,12 @@ export class ApiService {
getProject(id: number) { getProject(id: number) {
return this.http.get(this.url + "/project/get/" + id) return this.http.get(this.url + "/project/get/" + id)
} }
createProject(project: Project) {
return this.http.post(this.url + "/project/create", project)
}
updateProject(project: Project) {
return this.http.post(this.url + "/project/update/" + project.id, project)
}
} }

View File

@ -2,11 +2,11 @@
<ul> <ul>
<li><a [routerLink]="''">Index</a></li> <li><a [routerLink]="''">Index</a></li>
<li><a [routerLink]="'log'">Logs</a></li> <li><a [routerLink]="'log'">Logs</a></li>
<li><a [routerLink]="'project'">Project</a></li>
<li><a [routerLink]="'projects'">list</a></li> <li><a [routerLink]="'projects'">list</a></li>
<li><a [routerLink]="'new_project'">new project</a></li> <li><a [routerLink]="'new_project'">new project</a></li>
</ul> </ul>
<!--</mat-toolbar>--> <!--</mat-toolbar>-->
<messenger-snack-bar></messenger-snack-bar>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -20,18 +20,21 @@ import {
MatPaginatorModule, MatPaginatorModule,
MatSliderModule, MatSliderModule,
MatSlideToggleModule, MatSlideToggleModule,
MatSnackBarModule,
MatSortModule, MatSortModule,
MatTableModule, MatTableModule,
MatToolbarModule, MatToolbarModule,
MatTreeModule MatTreeModule
} from "@angular/material"; } from "@angular/material";
import {ApiService} from "./api.service"; import {ApiService} from "./api.service";
import {MessengerService} from "./messenger.service";
import {HttpClientModule} from "@angular/common/http"; import {HttpClientModule} from "@angular/common/http";
import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.component'; import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.component';
import {ProjectListComponent} from './project-list/project-list.component'; import {ProjectListComponent} from './project-list/project-list.component';
import {CreateProjectComponent} from './create-project/create-project.component'; import {CreateProjectComponent} from './create-project/create-project.component';
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {UpdateProjectComponent} from './update-project/update-project.component'; import {UpdateProjectComponent} from './update-project/update-project.component';
import {SnackBarComponent} from "./messenger/snack-bar.component";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -40,7 +43,8 @@ import {UpdateProjectComponent} from './update-project/update-project.component'
ProjectDashboardComponent, ProjectDashboardComponent,
ProjectListComponent, ProjectListComponent,
CreateProjectComponent, CreateProjectComponent,
UpdateProjectComponent UpdateProjectComponent,
SnackBarComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -65,12 +69,17 @@ import {UpdateProjectComponent} from './update-project/update-project.component'
MatSliderModule, MatSliderModule,
MatSlideToggleModule, MatSlideToggleModule,
MatCheckboxModule, MatCheckboxModule,
MatDividerModule MatDividerModule,
MatSnackBarModule,
], ],
exports: [], exports: [],
providers: [ providers: [
ApiService, ApiService,
MessengerService,
],
entryComponents: [
SnackBarComponent,
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -3,7 +3,7 @@
<mat-card-subtitle></mat-card-subtitle> <mat-card-subtitle></mat-card-subtitle>
<mat-card-content> <mat-card-content>
<form> <form (ngSubmit)="onSubmit()">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Project name</mat-label> <mat-label>Project name</mat-label>
<input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Project name"> <input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Project name">
@ -29,11 +29,10 @@
<input type="hidden" name="version" value="{{project.version}}"> <input type="hidden" name="version" value="{{project.version}}">
<input type="submit" value="Create">
</form> </form>
</mat-card-content> </mat-card-content>
<mat-card-actions>
<button>Create</button>
</mat-card-actions>
</mat-card> </mat-card>

View File

@ -1,5 +1,9 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Project} from "../models/project"; import {Project} from "../models/project";
import {ApiService} from "../api.service";
import {MessengerService} from "../messenger.service";
import {Router} from "@angular/router";
@Component({ @Component({
selector: 'app-create-project', selector: 'app-create-project',
@ -10,7 +14,9 @@ export class CreateProjectComponent implements OnInit {
private project = new Project(); private project = new Project();
constructor() { constructor(private apiService: ApiService,
private messengerService: MessengerService,
private router: Router) {
this.project.name = "test"; this.project.name = "test";
this.project.public = true; this.project.public = true;
} }
@ -18,4 +24,16 @@ export class CreateProjectComponent implements OnInit {
ngOnInit() { ngOnInit() {
} }
onSubmit() {
this.apiService.createProject(this.project).subscribe(
data => {
this.router.navigateByUrl("/project/" + data["id"]);
},
error => {
console.log(error.error.message);
this.messengerService.show(error.error.message);
}
)
}
} }

View File

@ -0,0 +1,22 @@
import {Injectable} from '@angular/core';
import {Subject} from "rxjs";
import {MessengerState} from "./messenger/messenger";
@Injectable()
export class MessengerService {
public messengerSubject = new Subject<MessengerState>();
show(message: string) {
this.messengerSubject.next({
message: message,
hidden: false,
})
}
hide() {
this.messengerSubject.next({
hidden: true,
})
}
}

View File

@ -0,0 +1,6 @@
export class MessengerState {
hidden: boolean;
message?: string;
}

View File

@ -0,0 +1,32 @@
import {Component, OnInit} from '@angular/core';
import {MessengerService} from "../messenger.service";
import {MessengerState} from "./messenger";
import {Subscription} from "rxjs";
import {MatSnackBar, MatSnackBarConfig} from "@angular/material";
@Component({
selector: 'messenger-snack-bar',
templateUrl: 'messenger-snack-bar.html',
styleUrls: ['messenger-snack-bar.css'],
})
export class SnackBarComponent implements OnInit {
private subscription: Subscription;
constructor(private messengerService: MessengerService, private snackBar: MatSnackBar) {
}
ngOnInit() {
this.subscription = this.messengerService.messengerSubject
.subscribe((state: MessengerState) => {
if (state.hidden) {
this.snackBar.dismiss();
} else {
this.snackBar.open(state.message, "Close", <MatSnackBarConfig>{
duration: 10 * 1000,
})
}
});
}
}

View File

@ -1,5 +1,6 @@
export class Project { export class Project {
public id: number;
public priority: number; public priority: number;
public motd: string; public motd: string;
public name: string; public name: string;

View File

@ -37,6 +37,8 @@
<pre>{{projectStats | json}}</pre> <pre>{{projectStats | json}}</pre>
</mat-expansion-panel> </mat-expansion-panel>
<a [routerLink]="'/project/' + projectStats.project.id + '/update'">Update</a>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
</div> </div>

View File

@ -56,14 +56,14 @@ export class ProjectDashboardComponent implements OnInit {
setupStatusPieChart() { setupStatusPieChart() {
let tooltip = d3.select("#stooltip"); let tooltip = d3.select("#stooltip");
this.statusSvg = d3.select('#status') this.statusSvg = d3.select("#status")
.append('svg') .append('svg')
.attr('width', this.pieWidth) .attr('width', this.pieWidth)
.attr('height', this.pieHeight) .attr('height', this.pieHeight)
.append("g") .append("g")
.attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")"); .attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")");
this.statusPath = this.statusSvg.selectAll("path") this.statusPath = this.statusSvg.selectAll()
.data(this.pieFun(this.statusData)) .data(this.pieFun(this.statusData))
.enter() .enter()
.append('path') .append('path')
@ -76,14 +76,14 @@ export class ProjectDashboardComponent implements OnInit {
setupAssigneesPieChart() { setupAssigneesPieChart() {
let tooltip = d3.select("#atooltip"); let tooltip = d3.select("#atooltip");
this.assigneesSvg = d3.select('#assignees') this.assigneesSvg = d3.select("#assignees")
.append('svg') .append('svg')
.attr('width', this.pieWidth) .attr('width', this.pieWidth)
.attr('height', this.pieHeight) .attr('height', this.pieHeight)
.append("g") .append("g")
.attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")"); .attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")");
this.assigneesPath = this.assigneesSvg.selectAll("path") this.assigneesPath = this.assigneesSvg.selectAll()
.data(this.pieFun(this.assigneesData)) .data(this.pieFun(this.assigneesData))
.enter() .enter()
.append('path') .append('path')
@ -229,10 +229,10 @@ export class ProjectDashboardComponent implements OnInit {
{label: "Failed", count: this.projectStats["failed_task_count"]}, {label: "Failed", count: this.projectStats["failed_task_count"]},
{label: "Closed", count: this.projectStats["closed_task_count"]}, {label: "Closed", count: this.projectStats["closed_task_count"]},
]; ];
this.assigneesData = _.map(this.projectStats["assignees"], (assignedTasks) => { this.assigneesData = _.map(this.projectStats["assignees"], assignedTask => {
return { return {
label: assignedTasks["assignee"] == "00000000-0000-0000-0000-000000000000" ? "unassigned" : assignedTasks["assignee"], label: assignedTask["assignee"],
count: assignedTasks["task_count"] count: assignedTask["task_count"],
} }
}); });
@ -256,6 +256,16 @@ export class ProjectDashboardComponent implements OnInit {
]; ];
this.assigneesData = [ this.assigneesData = [
{label: 'null', count: 0}, {label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
{label: 'null', count: 0},
]; ];
this.setupStatusPieChart(); this.setupStatusPieChart();

View File

@ -12,6 +12,7 @@
<pre>{{stats.project | json}}</pre> <pre>{{stats.project | json}}</pre>
<div style="display: flex;"> <div style="display: flex;">
<a [routerLink]="'/project/' + stats.project.id">Dashboard</a> <a [routerLink]="'/project/' + stats.project.id">Dashboard</a>
<a [routerLink]="'/project/' + stats.project.id + '/update'">Update</a>
</div> </div>
</mat-expansion-panel> </mat-expansion-panel>
</mat-accordion> </mat-accordion>

View File

@ -1,29 +1,29 @@
<mat-card> <mat-card>
<mat-card-title>Update project</mat-card-title> <mat-card-title>Update project</mat-card-title>
<mat-card-subtitle>Changes are saved in real time</mat-card-subtitle>
<mat-card-content> <mat-card-content>
<form> <form (ngSubmit)="onSubmit()" *ngIf="project != undefined">
<mat-form-field> <mat-form-field appearance="outline">
<input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Name"> <input type="text" matInput [(ngModel)]="project.name" name="name" placeholder="Name">
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field appearance="outline">
<textarea matInput placeholder="Message of the day"></textarea> <textarea matInput [(ngModel)]="project.motd" placeholder="Message of the day" name="motd"></textarea>
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field appearance="outline">
<input type="text" matInput [(ngModel)]="project.clone_url" name="clone_url" <input type="text" matInput [(ngModel)]="project.clone_url" name="clone_url"
placeholder="Git clone url"> placeholder="Git clone url">
</mat-form-field> </mat-form-field>
<mat-form-field> <mat-form-field appearance="outline">
<input type="text" matInput [(ngModel)]="project.git_repo" name="clone_url" <input type="text" matInput [(ngModel)]="project.git_repo" name="git_repo"
placeholder='Full repository name (e.g. "simon987/task_tracker")'> placeholder='Full repository name (e.g. "simon987/task_tracker")'>
<mat-hint align="start">Changes on the <strong>master</strong> branch will be tracked if webhooks are <mat-hint align="start">Changes on the <strong>master</strong> branch will be tracked if webhooks are
enabled enabled
</mat-hint> </mat-hint>
</mat-form-field> </mat-form-field>
<input type="hidden" name="version" value="{{project.version}}">
<input type="submit" value="Update">
</form> </form>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>

View File

@ -1,7 +1,8 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Project} from "../models/project"; import {Project} from "../models/project";
import {ApiService} from "../api.service"; import {ApiService} from "../api.service";
import {ActivatedRoute} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {MessengerService} from "../messenger.service";
@Component({ @Component({
selector: 'app-update-project', selector: 'app-update-project',
@ -10,7 +11,10 @@ import {ActivatedRoute} from "@angular/router";
}) })
export class UpdateProjectComponent implements OnInit { export class UpdateProjectComponent implements OnInit {
constructor(private apiService: ApiService, private route: ActivatedRoute) { constructor(private apiService: ApiService,
private route: ActivatedRoute,
private messengerService: MessengerService,
private router: Router) {
} }
private project: Project; private project: Project;
@ -26,15 +30,28 @@ export class UpdateProjectComponent implements OnInit {
private getProject() { private getProject() {
this.apiService.getProject(this.projectId).subscribe(data => { this.apiService.getProject(this.projectId).subscribe(data => {
this.project = <Project>{ this.project = <Project>{
id: data["project"]["id"],
name: data["project"]["name"], name: data["project"]["name"],
clone_url: data["project"]["clone_url"], clone_url: data["project"]["clone_url"],
git_repo: data["project"]["git_repo"], git_repo: data["project"]["git_repo"],
motd: data["project"]["motd"], motd: data["project"]["motd"],
priority: data["project"]["priority"], priority: data["project"]["priority"],
version: data["project"]["version"] version: data["project"]["version"],
public: data["project"]["public"],
} }
}) })
} }
onSubmit() {
this.apiService.updateProject(this.project).subscribe(
data => {
this.router.navigateByUrl("/project/" + this.project.id);
},
error => {
console.log(error.error.message);
this.messengerService.show(error.error.message);
}
)
}
} }