mirror of
				https://github.com/simon987/task_tracker.git
				synced 2025-10-28 06:26:53 +00:00 
			
		
		
		
	Web dashboard, task release, logs api
This commit is contained in:
		
							parent
							
								
									0346dd8b6b
								
							
						
					
					
						commit
						cbd32daf02
					
				| @ -1,5 +1,8 @@ | ||||
| 
 | ||||
| <a href='https://github.com/jpoles1/gopherbadger' target='_blank'></a> | ||||
| 
 | ||||
| ### Running tests | ||||
| ```bash | ||||
| cd test/ | ||||
| go test | ||||
| go test -bench | ||||
| ``` | ||||
| @ -2,7 +2,6 @@ package api | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| ) | ||||
| @ -11,7 +10,6 @@ type Request struct { | ||||
| 	Ctx *fasthttp.RequestCtx | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| func (r *Request) OkJson(object interface{}) { | ||||
| 
 | ||||
| 	resp, err := json.Marshal(object) | ||||
| @ -26,8 +24,9 @@ func (r *Request) Json(object interface{}, code int) { | ||||
| 
 | ||||
| 	resp, err := json.Marshal(object) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprint(r.Ctx,"Error during json encoding of error") | ||||
| 		logrus.Error("Error during json encoding of error") | ||||
| 		logrus.WithError(err).WithFields(logrus.Fields{ | ||||
| 			"code": code, | ||||
| 		}).Error("Error during json encoding of object") | ||||
| 	} | ||||
| 
 | ||||
| 	r.Ctx.Response.SetStatusCode(code) | ||||
|  | ||||
							
								
								
									
										70
									
								
								api/log.go
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								api/log.go
									
									
									
									
									
								
							| @ -5,41 +5,56 @@ import ( | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/valyala/fasthttp" | ||||
| 	"src/task_tracker/config" | ||||
| 	"src/task_tracker/storage" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type RequestHandler func(*Request) | ||||
| 
 | ||||
| type LogEntry struct { | ||||
| type GetLogRequest struct { | ||||
| 	Level logrus.Level `json:"level"` | ||||
| 	Since int64        `json:"since"` | ||||
| } | ||||
| 
 | ||||
| type LogRequest struct { | ||||
| 	Scope     string `json:"scope"` | ||||
| 	Message   string `json:"Message"` | ||||
| 	TimeStamp int64  `json:"timestamp"` | ||||
| } | ||||
| 
 | ||||
| func (e *LogEntry) Time() time.Time { | ||||
| type GetLogResponse struct { | ||||
| 	Ok      bool                `json:"ok"` | ||||
| 	Message string              `json:"message"` | ||||
| 	Logs    *[]storage.LogEntry `json:"logs"` | ||||
| } | ||||
| 
 | ||||
| func (e *LogRequest) Time() time.Time { | ||||
| 
 | ||||
| 	t := time.Unix(e.TimeStamp, 0) | ||||
| 	return t | ||||
| } | ||||
| 
 | ||||
| func LogRequest(h RequestHandler) fasthttp.RequestHandler { | ||||
| func LogRequestMiddleware(h RequestHandler) fasthttp.RequestHandler { | ||||
| 	return fasthttp.RequestHandler(func(ctx *fasthttp.RequestCtx) { | ||||
| 
 | ||||
| 		logrus.WithFields(logrus.Fields{ | ||||
| 			"path": string(ctx.Path()), | ||||
| 		}).Info(string(ctx.Method())) | ||||
| 			"path":   string(ctx.Path()), | ||||
| 			"header": ctx.Request.Header.String(), | ||||
| 		}).Trace(string(ctx.Method())) | ||||
| 
 | ||||
| 		h(&Request{Ctx: ctx}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func SetupLogger() { | ||||
| func (api *WebAPI) SetupLogger() { | ||||
| 	logrus.SetLevel(config.Cfg.LogLevel) | ||||
| 
 | ||||
| 	api.Database.SetupLoggerHook() | ||||
| } | ||||
| 
 | ||||
| func parseLogEntry(r *Request) *LogEntry { | ||||
| func parseLogEntry(r *Request) *LogRequest { | ||||
| 
 | ||||
| 	entry := LogEntry{} | ||||
| 	entry := LogRequest{} | ||||
| 
 | ||||
| 	if r.GetJson(&entry) { | ||||
| 		if len(entry.Message) == 0 { | ||||
| @ -89,3 +104,42 @@ func LogError(r *Request) { | ||||
| 		"scope": entry.Scope, | ||||
| 	}).WithTime(entry.Time()).Error(entry.Message) | ||||
| } | ||||
| 
 | ||||
| func (api *WebAPI) GetLog(r *Request) { | ||||
| 
 | ||||
| 	req := &GetLogRequest{} | ||||
| 	if r.GetJson(req) { | ||||
| 		if req.isValid() { | ||||
| 
 | ||||
| 			logs := api.Database.GetLogs(req.Since, req.Level) | ||||
| 
 | ||||
| 			logrus.WithFields(logrus.Fields{ | ||||
| 				"getLogRequest": req, | ||||
| 				"logCount":      len(*logs), | ||||
| 			}).Trace("Get log request") | ||||
| 
 | ||||
| 			r.OkJson(GetLogResponse{ | ||||
| 				Ok:   true, | ||||
| 				Logs: logs, | ||||
| 			}) | ||||
| 		} else { | ||||
| 			logrus.WithFields(logrus.Fields{ | ||||
| 				"getLogRequest": req, | ||||
| 			}).Warn("Invalid log request") | ||||
| 
 | ||||
| 			r.Json(GetLogResponse{ | ||||
| 				Ok:      false, | ||||
| 				Message: "Invalid log request", | ||||
| 			}, 400) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (r GetLogRequest) isValid() bool { | ||||
| 
 | ||||
| 	if r.Since <= 0 { | ||||
| 		return false | ||||
| 	} | ||||
| 
 | ||||
| 	return true | ||||
| } | ||||
|  | ||||
							
								
								
									
										33
									
								
								api/main.go
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								api/main.go
									
									
									
									
									
								
							| @ -30,9 +30,8 @@ func Index(r *Request) { | ||||
| 
 | ||||
| func New() *WebAPI { | ||||
| 
 | ||||
| 	SetupLogger() | ||||
| 
 | ||||
| 	api := new(WebAPI) | ||||
| 	api.Database = &storage.Database{} | ||||
| 
 | ||||
| 	api.router = &fasthttprouter.Router{} | ||||
| 
 | ||||
| @ -41,24 +40,28 @@ func New() *WebAPI { | ||||
| 		Name:    info.Name, | ||||
| 	} | ||||
| 
 | ||||
| 	api.router.GET("/", LogRequest(Index)) | ||||
| 	api.router.GET("/", LogRequestMiddleware(Index)) | ||||
| 
 | ||||
| 	api.router.POST("/log/trace", LogRequest(LogTrace)) | ||||
| 	api.router.POST("/log/info", LogRequest(LogInfo)) | ||||
| 	api.router.POST("/log/warn", LogRequest(LogWarn)) | ||||
| 	api.router.POST("/log/error", LogRequest(LogError)) | ||||
| 	api.router.POST("/log/trace", LogRequestMiddleware(LogTrace)) | ||||
| 	api.router.POST("/log/info", LogRequestMiddleware(LogInfo)) | ||||
| 	api.router.POST("/log/warn", LogRequestMiddleware(LogWarn)) | ||||
| 	api.router.POST("/log/error", LogRequestMiddleware(LogError)) | ||||
| 
 | ||||
| 	api.router.POST("/worker/create", LogRequest(api.WorkerCreate)) | ||||
| 	api.router.GET("/worker/get/:id", LogRequest(api.WorkerGet)) | ||||
| 	api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate)) | ||||
| 	api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet)) | ||||
| 
 | ||||
| 	api.router.POST("/project/create", LogRequest(api.ProjectCreate)) | ||||
| 	api.router.GET("/project/get/:id", LogRequest(api.ProjectGet)) | ||||
| 	api.router.POST("/project/create", LogRequestMiddleware(api.ProjectCreate)) | ||||
| 	api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet)) | ||||
| 	api.router.GET("/project/stats/:id", LogRequestMiddleware(api.ProjectGetStats)) | ||||
| 
 | ||||
| 	api.router.POST("/task/create", LogRequest(api.TaskCreate)) | ||||
| 	api.router.GET("/task/get/:project", LogRequest(api.TaskGetFromProject)) | ||||
| 	api.router.GET("/task/get", LogRequest(api.TaskGet)) | ||||
| 	api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate)) | ||||
| 	api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject)) | ||||
| 	api.router.GET("/task/get", LogRequestMiddleware(api.TaskGet)) | ||||
| 	api.router.POST("/task/release", LogRequestMiddleware(api.TaskRelease)) | ||||
| 
 | ||||
| 	api.router.POST("/git/receivehook", LogRequest(api.ReceiveGitWebHook)) | ||||
| 	api.router.POST("/git/receivehook", LogRequestMiddleware(api.ReceiveGitWebHook)) | ||||
| 
 | ||||
| 	api.router.POST("/logs", LogRequestMiddleware(api.GetLog)) | ||||
| 
 | ||||
| 	return api | ||||
| } | ||||
|  | ||||
| @ -12,6 +12,7 @@ type CreateProjectRequest struct { | ||||
| 	GitRepo  string `json:"git_repo"` | ||||
| 	Version  string `json:"version"` | ||||
| 	Priority int64  `json:"priority"` | ||||
| 	Motd     string `json:"motd"` | ||||
| } | ||||
| 
 | ||||
| type CreateProjectResponse struct { | ||||
| @ -26,6 +27,12 @@ type GetProjectResponse struct { | ||||
| 	Project *storage.Project `json:"project,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type GetProjectStatsResponse struct { | ||||
| 	Ok      bool                  `json:"ok"` | ||||
| 	Message string                `json:"message,omitempty"` | ||||
| 	Stats   *storage.ProjectStats `json:"stats,omitempty"` | ||||
| } | ||||
| 
 | ||||
| func (api *WebAPI) ProjectCreate(r *Request) { | ||||
| 
 | ||||
| 	createReq := &CreateProjectRequest{} | ||||
| @ -37,6 +44,7 @@ func (api *WebAPI) ProjectCreate(r *Request) { | ||||
| 			CloneUrl: createReq.CloneUrl, | ||||
| 			GitRepo:  createReq.GitRepo, | ||||
| 			Priority: createReq.Priority, | ||||
| 			Motd:     createReq.Motd, | ||||
| 		} | ||||
| 
 | ||||
| 		if isValidProject(project) { | ||||
| @ -52,6 +60,9 @@ func (api *WebAPI) ProjectCreate(r *Request) { | ||||
| 					Ok: true, | ||||
| 					Id: id, | ||||
| 				}) | ||||
| 				logrus.WithFields(logrus.Fields{ | ||||
| 					"project": project, | ||||
| 				}).Debug("Created project") | ||||
| 			} | ||||
| 		} else { | ||||
| 			logrus.WithFields(logrus.Fields{ | ||||
| @ -94,3 +105,23 @@ func (api *WebAPI) ProjectGet(r *Request) { | ||||
| 		}, 404) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (api *WebAPI) ProjectGetStats(r *Request) { | ||||
| 
 | ||||
| 	id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64) | ||||
| 	handleErr(err, r) | ||||
| 
 | ||||
| 	stats := api.Database.GetProjectStats(id) | ||||
| 
 | ||||
| 	if stats != nil && stats.Project != nil { | ||||
| 		r.OkJson(GetProjectStatsResponse{ | ||||
| 			Ok:    true, | ||||
| 			Stats: stats, | ||||
| 		}) | ||||
| 	} else { | ||||
| 		r.Json(GetProjectStatsResponse{ | ||||
| 			Ok:      false, | ||||
| 			Message: "Project not found", | ||||
| 		}, 404) | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										41
									
								
								api/task.go
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								api/task.go
									
									
									
									
									
								
							| @ -15,6 +15,17 @@ type CreateTaskRequest struct { | ||||
| 	Priority   int64  `json:"priority"` | ||||
| } | ||||
| 
 | ||||
| type ReleaseTaskRequest struct { | ||||
| 	TaskId   int64      `json:"task_id"` | ||||
| 	Success  bool       `json:"success"` | ||||
| 	WorkerId *uuid.UUID `json:"worker_id"` | ||||
| } | ||||
| 
 | ||||
| type ReleaseTaskResponse struct { | ||||
| 	Ok      bool   `json:"ok"` | ||||
| 	Message string `json:"message,omitempty"` | ||||
| } | ||||
| 
 | ||||
| type CreateTaskResponse struct { | ||||
| 	Ok      bool   `json:"ok"` | ||||
| 	Message string `json:"message,omitempty"` | ||||
| @ -137,3 +148,33 @@ func (api WebAPI) workerFromQueryArgs(r *Request) (*storage.Worker, error) { | ||||
| 
 | ||||
| 	return worker, nil | ||||
| } | ||||
| 
 | ||||
| func (api *WebAPI) TaskRelease(r *Request) { | ||||
| 
 | ||||
| 	req := ReleaseTaskRequest{} | ||||
| 	if r.GetJson(req) { | ||||
| 
 | ||||
| 		res := api.Database.ReleaseTask(req.TaskId, req.WorkerId, req.Success) | ||||
| 
 | ||||
| 		response := ReleaseTaskResponse{ | ||||
| 			Ok: res, | ||||
| 		} | ||||
| 
 | ||||
| 		if !res { | ||||
| 			response.Message = "Could not find a task with the specified Id assigned to this workerId" | ||||
| 
 | ||||
| 			logrus.WithFields(logrus.Fields{ | ||||
| 				"releaseTaskRequest": req, | ||||
| 				"taskUpdated":        res, | ||||
| 			}).Warn("Release task: NOT FOUND") | ||||
| 		} else { | ||||
| 
 | ||||
| 			logrus.WithFields(logrus.Fields{ | ||||
| 				"releaseTaskRequest": req, | ||||
| 				"taskUpdated":        res, | ||||
| 			}).Trace("Release task") | ||||
| 		} | ||||
| 
 | ||||
| 		r.OkJson(response) | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										1
									
								
								config.yml
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										1
									
								
								config.yml
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @ -3,6 +3,7 @@ server: | ||||
| 
 | ||||
| database: | ||||
|   conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable" | ||||
|   log_levels: ["debug", "error"] | ||||
| 
 | ||||
| git: | ||||
|   webhook_secret: "very_secret_secret" | ||||
|  | ||||
| @ -12,6 +12,7 @@ var Cfg struct { | ||||
| 	WebHookHash      string | ||||
| 	WebHookSigHeader string | ||||
| 	LogLevel         logrus.Level | ||||
| 	DbLogLevels      []logrus.Level | ||||
| } | ||||
| 
 | ||||
| func SetupConfig() { | ||||
| @ -30,4 +31,8 @@ func SetupConfig() { | ||||
| 	Cfg.WebHookHash = viper.GetString("git.webhook_hash") | ||||
| 	Cfg.WebHookSigHeader = viper.GetString("git.webhook_sig_header") | ||||
| 	Cfg.LogLevel, _ = logrus.ParseLevel(viper.GetString("log.level")) | ||||
| 	for _, level := range viper.GetStringSlice("database.log_levels") { | ||||
| 		newLevel, _ := logrus.ParseLevel(level) | ||||
| 		Cfg.DbLogLevels = append(Cfg.DbLogLevels, newLevel) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -17,8 +17,8 @@ func main() { | ||||
| 
 | ||||
| 	config.SetupConfig() | ||||
| 
 | ||||
| 	tmpDebugSetup() | ||||
| 
 | ||||
| 	webApi := api.New() | ||||
| 	webApi.SetupLogger() | ||||
| 	tmpDebugSetup() | ||||
| 	webApi.Run() | ||||
| } | ||||
|  | ||||
							
								
								
									
										17
									
								
								schema.sql
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							
							
						
						
									
										17
									
								
								schema.sql
									
									
									
									
									
										
										
										Normal file → Executable file
									
								
							| @ -1,5 +1,6 @@ | ||||
| DROP TABLE IF EXISTS workerIdentity, Worker, Project, Task; | ||||
| DROP TYPE IF EXISTS Status; | ||||
| DROP TABLE IF EXISTS workeridentity, Worker, Project, Task, log_entry; | ||||
| DROP TYPE IF EXISTS status; | ||||
| DROP TYPE IF EXISTS loglevel; | ||||
| 
 | ||||
| CREATE TYPE status as ENUM ( | ||||
|   'new', | ||||
| @ -7,6 +8,10 @@ CREATE TYPE status as ENUM ( | ||||
|   'closed' | ||||
|   ); | ||||
| 
 | ||||
| CREATE TYPE loglevel as ENUM ( | ||||
|   'fatal', 'panic', 'error', 'warning', 'info', 'debug', 'trace' | ||||
|   ); | ||||
| 
 | ||||
| CREATE TABLE workerIdentity | ||||
| ( | ||||
|   id          SERIAL PRIMARY KEY, | ||||
| @ -45,4 +50,12 @@ CREATE TABLE task | ||||
|   recipe      TEXT | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE log_entry | ||||
| ( | ||||
|   level        loglevel, | ||||
|   message      TEXT, | ||||
|   message_data TEXT, | ||||
|   timestamp    INT | ||||
| ); | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -2,7 +2,6 @@ package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"fmt" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	_ "github.com/lib/pq" | ||||
| 	"io/ioutil" | ||||
| @ -11,6 +10,7 @@ import ( | ||||
| ) | ||||
| 
 | ||||
| type Database struct { | ||||
| 	db *sql.DB | ||||
| } | ||||
| 
 | ||||
| func (database *Database) Reset() { | ||||
| @ -25,29 +25,24 @@ func (database *Database) Reset() { | ||||
| 	_, err = db.Exec(string(buffer)) | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	db.Close() | ||||
| 	file.Close() | ||||
| 
 | ||||
| 	logrus.Info("Database has been reset") | ||||
| } | ||||
| 
 | ||||
| func (database *Database) getDB () *sql.DB { | ||||
| 	db, err := sql.Open("postgres", config.Cfg.DbConnStr) | ||||
| 	if err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| func (database *Database) getDB() *sql.DB { | ||||
| 
 | ||||
| 	if database.db == nil { | ||||
| 		db, err := sql.Open("postgres", config.Cfg.DbConnStr) | ||||
| 		if err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
| 
 | ||||
| 		database.db = db | ||||
| 	} else { | ||||
| 		err := database.db.Ping() | ||||
| 		handleErr(err) | ||||
| 	} | ||||
| 
 | ||||
| 	return db | ||||
| 	return database.db | ||||
| } | ||||
| 
 | ||||
| func (database *Database) Test() { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 
 | ||||
| 	rows, err := db.Query("SELECT name FROM Task") | ||||
| 	if err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
| 	fmt.Println(rows) | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										71
									
								
								storage/log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								storage/log.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,71 @@ | ||||
| package storage | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/json" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"src/task_tracker/config" | ||||
| ) | ||||
| 
 | ||||
| type LogEntry struct { | ||||
| 	Message   string `json:"message"` | ||||
| 	Timestamp int64  `json:"timestamp"` | ||||
| 	Data      string `json:"data"` | ||||
| 	Level     string `json:"level"` | ||||
| } | ||||
| type sqlLogHook struct { | ||||
| 	database *Database | ||||
| } | ||||
| 
 | ||||
| func (h sqlLogHook) Levels() []logrus.Level { | ||||
| 	return config.Cfg.DbLogLevels | ||||
| } | ||||
| 
 | ||||
| func (h sqlLogHook) Fire(entry *logrus.Entry) error { | ||||
| 
 | ||||
| 	db := h.database.getDB() | ||||
| 
 | ||||
| 	jsonData, err := json.Marshal(entry.Data) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	_, err = db.Exec("INSERT INTO log_entry (message, level, message_data, timestamp) VALUES ($1,$2,$3,$4)", | ||||
| 		entry.Message, entry.Level.String(), jsonData, entry.Time.Unix()) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (database *Database) SetupLoggerHook() { | ||||
| 	hook := sqlLogHook{} | ||||
| 	hook.database = database | ||||
| 	logrus.AddHook(hook) | ||||
| } | ||||
| 
 | ||||
| func (database *Database) GetLogs(since int64, level logrus.Level) *[]LogEntry { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	logs := getLogs(since, level, db) | ||||
| 
 | ||||
| 	return logs | ||||
| } | ||||
| 
 | ||||
| func getLogs(since int64, level logrus.Level, db *sql.DB) *[]LogEntry { | ||||
| 
 | ||||
| 	var logs []LogEntry | ||||
| 
 | ||||
| 	rows, err := db.Query("SELECT * FROM log_entry WHERE timestamp > $1 AND level=$2", | ||||
| 		since, level.String()) | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	for rows.Next() { | ||||
| 
 | ||||
| 		e := LogEntry{} | ||||
| 
 | ||||
| 		err := rows.Scan(&e.Level, &e.Message, &e.Data, &e.Timestamp) | ||||
| 		handleErr(err) | ||||
| 
 | ||||
| 		logs = append(logs, e) | ||||
| 	} | ||||
| 
 | ||||
| 	return &logs | ||||
| } | ||||
| @ -3,6 +3,7 @@ package storage | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"github.com/google/uuid" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| @ -13,22 +14,34 @@ type Project struct { | ||||
| 	CloneUrl string `json:"clone_url"` | ||||
| 	GitRepo  string `json:"git_repo"` | ||||
| 	Version  string `json:"version"` | ||||
| 	Motd     string `json:"motd"` | ||||
| } | ||||
| 
 | ||||
| type AssignedTasks struct { | ||||
| 	Assignee  uuid.UUID `json:"assignee"` | ||||
| 	TaskCount int64     `json:"task_count"` | ||||
| } | ||||
| 
 | ||||
| type ProjectStats struct { | ||||
| 	Project         *Project         `json:"project"` | ||||
| 	NewTaskCount    int64            `json:"new_task_count"` | ||||
| 	FailedTaskCount int64            `json:"failed_task_count"` | ||||
| 	ClosedTaskCount int64            `json:"closed_task_count"` | ||||
| 	Assignees       []*AssignedTasks `json:"assignees"` | ||||
| } | ||||
| 
 | ||||
| func (database *Database) SaveProject(project *Project) (int64, error) { | ||||
| 	db := database.getDB() | ||||
| 	id, projectErr := saveProject(project, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return id, projectErr | ||||
| } | ||||
| 
 | ||||
| func saveProject(project *Project, db *sql.DB) (int64, error) { | ||||
| 
 | ||||
| 	row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority) | ||||
| 		VALUES ($1,$2,$3,$4,$5) RETURNING id`, | ||||
| 		project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority) | ||||
| 	row := db.QueryRow(`INSERT INTO project (name, git_repo, clone_url, version, priority, motd) | ||||
| 		VALUES ($1,$2,$3,$4,$5,$6) RETURNING id`, | ||||
| 		project.Name, project.GitRepo, project.CloneUrl, project.Version, project.Priority, project.Motd) | ||||
| 
 | ||||
| 	var id int64 | ||||
| 	err := row.Scan(&id) | ||||
| @ -40,6 +53,8 @@ func saveProject(project *Project, db *sql.DB) (int64, error) { | ||||
| 		return -1, err | ||||
| 	} | ||||
| 
 | ||||
| 	project.Id = id | ||||
| 
 | ||||
| 	logrus.WithFields(logrus.Fields{ | ||||
| 		"id":      id, | ||||
| 		"project": project, | ||||
| @ -52,8 +67,6 @@ func (database *Database) GetProject(id int64) *Project { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	project := getProject(id, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 	return project | ||||
| } | ||||
| 
 | ||||
| @ -80,8 +93,8 @@ func getProject(id int64, db *sql.DB) *Project { | ||||
| func scanProject(row *sql.Row) (*Project, error) { | ||||
| 
 | ||||
| 	project := &Project{} | ||||
| 	err := row.Scan(&project.Id, &project.Priority, &project.Name, &project.CloneUrl, &project.GitRepo, | ||||
| 		&project.Version) | ||||
| 	err := row.Scan(&project.Id, &project.Priority, &project.Motd, &project.Name, &project.CloneUrl, | ||||
| 		&project.GitRepo, &project.Version) | ||||
| 
 | ||||
| 	return project, err | ||||
| } | ||||
| @ -90,20 +103,18 @@ func (database *Database) GetProjectWithRepoName(repoName string) *Project { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	project := getProjectWithRepoName(repoName, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 	return project | ||||
| } | ||||
| 
 | ||||
| func getProjectWithRepoName(repoName string, db *sql.DB) *Project { | ||||
| 
 | ||||
| 	row := db.QueryRow(`SELECT * FROm project WHERE LOWER(git_repo)=$1`, strings.ToLower(repoName)) | ||||
| 	row := db.QueryRow(`SELECT * FROM project WHERE LOWER(git_repo)=$1`, strings.ToLower(repoName)) | ||||
| 
 | ||||
| 	project, err := scanProject(row) | ||||
| 	if err != nil { | ||||
| 		logrus.WithError(err).WithFields(logrus.Fields{ | ||||
| 			"repoName": repoName, | ||||
| 		}).Error("Database.getProjectWithRepoName SELECT project NOT FOUND") | ||||
| 		}).Warn("Database.getProjectWithRepoName SELECT project NOT FOUND") | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| @ -114,15 +125,13 @@ func (database *Database) UpdateProject(project *Project) { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	updateProject(project, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| } | ||||
| 
 | ||||
| func updateProject(project *Project, db *sql.DB) { | ||||
| 
 | ||||
| 	res, err := db.Exec(`UPDATE project  | ||||
| 		SET (priority, name, clone_url, git_repo, version) = ($1,$2,$3,$4,$5) WHERE id=$6`, | ||||
| 		project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Id) | ||||
| 		SET (priority, name, clone_url, git_repo, version, motd) = ($1,$2,$3,$4,$5,$6) WHERE id=$7`, | ||||
| 		project.Priority, project.Name, project.CloneUrl, project.GitRepo, project.Version, project.Motd, project.Id) | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	rowsAffected, _ := res.RowsAffected() | ||||
| @ -134,3 +143,47 @@ func updateProject(project *Project, db *sql.DB) { | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| func (database *Database) GetProjectStats(id int64) *ProjectStats { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	stats := getProjectStats(id, db) | ||||
| 
 | ||||
| 	return stats | ||||
| } | ||||
| 
 | ||||
| func getProjectStats(id int64, db *sql.DB) *ProjectStats { | ||||
| 
 | ||||
| 	stats := ProjectStats{} | ||||
| 
 | ||||
| 	stats.Project = getProject(id, db) | ||||
| 
 | ||||
| 	if stats.Project != nil { | ||||
| 		row := db.QueryRow(`SELECT  | ||||
|        SUM(CASE WHEN status='new' THEN 1 ELSE 0 END) newCount, | ||||
|        SUM(CASE WHEN status='failed' THEN 1 ELSE 0 END) failedCount, | ||||
|        SUM(CASE WHEN status='closed' THEN 1 ELSE 0 END) closedCount | ||||
|        FROM task WHERE project=$1 GROUP BY project`, id) | ||||
| 
 | ||||
| 		err := row.Scan(&stats.NewTaskCount, &stats.FailedTaskCount, &stats.ClosedTaskCount) | ||||
| 		if err != nil { | ||||
| 			logrus.WithError(err).WithFields(logrus.Fields{ | ||||
| 				"id": id, | ||||
| 			}).Warn("???") //todo | ||||
| 			return nil | ||||
| 		} | ||||
| 
 | ||||
| 		rows, err := db.Query(`SELECT assignee, COUNT(*) FROM TASK | ||||
|   			LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1 GROUP BY assignee`, id) | ||||
| 
 | ||||
| 		for rows.Next() { | ||||
| 			assignee := AssignedTasks{} | ||||
| 			err = rows.Scan(&assignee.Assignee, &assignee.TaskCount) | ||||
| 			handleErr(err) | ||||
| 
 | ||||
| 			stats.Assignees = append(stats.Assignees, &assignee) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return &stats | ||||
| } | ||||
|  | ||||
| @ -21,8 +21,6 @@ func (database *Database) SaveTask(task *Task, project int64) error { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	taskErr := saveTask(task, project, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return taskErr | ||||
| } | ||||
| @ -55,8 +53,6 @@ func (database *Database) GetTask(worker *Worker) *Task { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	task := getTask(worker, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return task | ||||
| } | ||||
| @ -112,12 +108,41 @@ func getTaskById(id int64, db *sql.DB) *Task { | ||||
| 	return task | ||||
| } | ||||
| 
 | ||||
| func (database Database) ReleaseTask(id int64, workerId *uuid.UUID, success bool) bool { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	res := releaseTask(workerId, id, success, db) | ||||
| 
 | ||||
| 	return res | ||||
| } | ||||
| 
 | ||||
| func releaseTask(workerId *uuid.UUID, id int64, success bool, db *sql.DB) bool { | ||||
| 
 | ||||
| 	var res sql.Result | ||||
| 	var err error | ||||
| 	if success { | ||||
| 		res, err = db.Exec(`UPDATE task SET (status, assignee) = ('closed', NULL) | ||||
| 		WHERE id=$2 AND task.assignee=$2`, id, workerId) | ||||
| 	} else { | ||||
| 		res, err = db.Exec(`UPDATE task SET (status, assignee, retries) =  | ||||
|   		(CASE WHEN retries+1 >= max_retries THEN 'failed' ELSE 'new' END, NULL, retries+1) | ||||
| 		WHERE id=$2 AND assignee=$2`, id, workerId) | ||||
| 	} | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	rowsAffected, _ := res.RowsAffected() | ||||
| 
 | ||||
| 	logrus.WithFields(logrus.Fields{ | ||||
| 		"rowsAffected": rowsAffected, | ||||
| 	}) | ||||
| 
 | ||||
| 	return rowsAffected == 1 | ||||
| } | ||||
| 
 | ||||
| func (database *Database) GetTaskFromProject(worker *Worker, project int64) *Task { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	task := getTaskFromProject(worker, project, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return task | ||||
| } | ||||
| @ -165,7 +190,7 @@ func scanTask(row *sql.Row) *Task { | ||||
| 
 | ||||
| 	err := row.Scan(&task.Id, &task.Priority, &project.Id, &task.Assignee, | ||||
| 		&task.Retries, &task.MaxRetries, &task.Status, &task.Recipe, &project.Id, | ||||
| 		&project.Priority, &project.Name, &project.CloneUrl, &project.GitRepo, &project.Version) | ||||
| 		&project.Priority, &project.Motd, &project.Name, &project.CloneUrl, &project.GitRepo, &project.Version) | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return task | ||||
|  | ||||
| @ -22,16 +22,12 @@ func (database *Database) SaveWorker(worker *Worker) { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	saveWorker(worker, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| } | ||||
| 
 | ||||
| func (database *Database) GetWorker(id uuid.UUID) *Worker { | ||||
| 
 | ||||
| 	db := database.getDB() | ||||
| 	worker := getWorker(id, db) | ||||
| 	err := db.Close() | ||||
| 	handleErr(err) | ||||
| 	return worker | ||||
| } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										28
									
								
								test/api_index_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								test/api_index_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package test | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"io/ioutil" | ||||
| 	"src/task_tracker/api" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestIndex(t *testing.T) { | ||||
| 
 | ||||
| 	r := Get("/") | ||||
| 
 | ||||
| 	body, _ := ioutil.ReadAll(r.Body) | ||||
| 	var info api.Info | ||||
| 	err := json.Unmarshal(body, &info) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		t.Error(err.Error()) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(info.Name) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 	if len(info.Version) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| @ -1,17 +1,20 @@ | ||||
| package test | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/Sirupsen/logrus" | ||||
| 	"io/ioutil" | ||||
| 	"src/task_tracker/api" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| func TestTraceValid(t *testing.T) { | ||||
| 
 | ||||
| 	r := Post("/log/trace", api.LogEntry{ | ||||
| 		Scope:"test", | ||||
| 		Message:"This is a test message", | ||||
| 	r := Post("/log/trace", api.LogRequest{ | ||||
| 		Scope:     "test", | ||||
| 		Message:   "This is a test message", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 
 | ||||
| @ -21,8 +24,8 @@ func TestTraceValid(t *testing.T) { | ||||
| } | ||||
| 
 | ||||
| func TestTraceInvalidScope(t *testing.T) { | ||||
| 	r := Post("/log/trace", api.LogEntry{ | ||||
| 		Message:"this is a test message", | ||||
| 	r := Post("/log/trace", api.LogRequest{ | ||||
| 		Message:   "this is a test message", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 
 | ||||
| @ -30,9 +33,9 @@ func TestTraceInvalidScope(t *testing.T) { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| 
 | ||||
| 	r = Post("/log/trace", api.LogEntry{ | ||||
| 		Scope:"", | ||||
| 		Message:"this is a test message", | ||||
| 	r = Post("/log/trace", api.LogRequest{ | ||||
| 		Scope:     "", | ||||
| 		Message:   "this is a test message", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 
 | ||||
| @ -45,9 +48,9 @@ func TestTraceInvalidScope(t *testing.T) { | ||||
| } | ||||
| 
 | ||||
| func TestTraceInvalidMessage(t *testing.T) { | ||||
| 	r := Post("/log/trace", api.LogEntry{ | ||||
| 		Scope:"test", | ||||
| 		Message:"", | ||||
| 	r := Post("/log/trace", api.LogRequest{ | ||||
| 		Scope:     "test", | ||||
| 		Message:   "", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 
 | ||||
| @ -60,10 +63,9 @@ func TestTraceInvalidMessage(t *testing.T) { | ||||
| } | ||||
| 
 | ||||
| func TestTraceInvalidTime(t *testing.T) { | ||||
| 	r := Post("/log/trace", api.LogEntry{ | ||||
| 		Scope: "test", | ||||
| 		Message:"test", | ||||
| 
 | ||||
| 	r := Post("/log/trace", api.LogRequest{ | ||||
| 		Scope:   "test", | ||||
| 		Message: "test", | ||||
| 	}) | ||||
| 	if r.StatusCode != 500 { | ||||
| 		t.Fail() | ||||
| @ -75,10 +77,10 @@ func TestTraceInvalidTime(t *testing.T) { | ||||
| 
 | ||||
| func TestWarnValid(t *testing.T) { | ||||
| 
 | ||||
| 	r := Post("/log/warn", api.LogEntry{ | ||||
| 		Scope: "test", | ||||
| 		Message:"test", | ||||
| 		TimeStamp:time.Now().Unix(), | ||||
| 	r := Post("/log/warn", api.LogRequest{ | ||||
| 		Scope:     "test", | ||||
| 		Message:   "test", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 	if r.StatusCode != 200 { | ||||
| 		t.Fail() | ||||
| @ -87,10 +89,10 @@ func TestWarnValid(t *testing.T) { | ||||
| 
 | ||||
| func TestInfoValid(t *testing.T) { | ||||
| 
 | ||||
| 	r := Post("/log/info", api.LogEntry{ | ||||
| 		Scope: "test", | ||||
| 		Message:"test", | ||||
| 		TimeStamp:time.Now().Unix(), | ||||
| 	r := Post("/log/info", api.LogRequest{ | ||||
| 		Scope:     "test", | ||||
| 		Message:   "test", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 	if r.StatusCode != 200 { | ||||
| 		t.Fail() | ||||
| @ -99,12 +101,82 @@ func TestInfoValid(t *testing.T) { | ||||
| 
 | ||||
| func TestErrorValid(t *testing.T) { | ||||
| 
 | ||||
| 	r := Post("/log/error", api.LogEntry{ | ||||
| 		Scope: "test", | ||||
| 		Message:"test", | ||||
| 		TimeStamp:time.Now().Unix(), | ||||
| 	r := Post("/log/error", api.LogRequest{ | ||||
| 		Scope:     "test", | ||||
| 		Message:   "test", | ||||
| 		TimeStamp: time.Now().Unix(), | ||||
| 	}) | ||||
| 	if r.StatusCode != 200 { | ||||
| 		t.Fail() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetLogs(t *testing.T) { | ||||
| 
 | ||||
| 	now := time.Now() | ||||
| 
 | ||||
| 	logrus.WithTime(now.Add(time.Second * -100)).WithFields(logrus.Fields{ | ||||
| 		"test": "value", | ||||
| 	}).Debug("This is a test log") | ||||
| 
 | ||||
| 	logrus.WithTime(now.Add(time.Second * -200)).WithFields(logrus.Fields{ | ||||
| 		"test": "value", | ||||
| 	}).Debug("This one shouldn't be returned") | ||||
| 
 | ||||
| 	logrus.WithTime(now.Add(time.Second * -100)).WithFields(logrus.Fields{ | ||||
| 		"test": "value", | ||||
| 	}).Error("error") | ||||
| 
 | ||||
| 	r := getLogs(time.Now().Add(time.Second*-150).Unix(), logrus.DebugLevel) | ||||
| 
 | ||||
| 	if r.Ok != true { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(*r.Logs) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	debugFound := false | ||||
| 	for _, log := range *r.Logs { | ||||
| 		if log.Message == "This one shouldn't be returned" { | ||||
| 			t.Error() | ||||
| 		} else if log.Message == "error" { | ||||
| 			t.Error() | ||||
| 		} else if log.Message == "This is a test log" { | ||||
| 			debugFound = true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !debugFound { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetLogsInvalid(t *testing.T) { | ||||
| 
 | ||||
| 	r := getLogs(-1, logrus.ErrorLevel) | ||||
| 
 | ||||
| 	if r.Ok != false { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(r.Message) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func getLogs(since int64, level logrus.Level) *api.GetLogResponse { | ||||
| 
 | ||||
| 	r := Post(fmt.Sprintf("/logs"), api.GetLogRequest{ | ||||
| 		Since: since, | ||||
| 		Level: level, | ||||
| 	}) | ||||
| 
 | ||||
| 	resp := &api.GetLogResponse{} | ||||
| 	data, _ := ioutil.ReadAll(r.Body) | ||||
| 	err := json.Unmarshal(data, resp) | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return resp | ||||
| } | ||||
|  | ||||
| @ -3,6 +3,7 @@ package test | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"github.com/google/uuid" | ||||
| 	"io/ioutil" | ||||
| 	"net/http" | ||||
| 	"src/task_tracker/api" | ||||
| @ -17,6 +18,7 @@ func TestCreateGetProject(t *testing.T) { | ||||
| 		GitRepo:  "drone/webhooktest", | ||||
| 		Version:  "Test Version", | ||||
| 		Priority: 123, | ||||
| 		Motd:     "motd", | ||||
| 	}) | ||||
| 
 | ||||
| 	id := resp.Id | ||||
| @ -51,6 +53,9 @@ func TestCreateGetProject(t *testing.T) { | ||||
| 	if getResp.Project.Priority != 123 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 	if getResp.Project.Motd != "motd" { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCreateProjectInvalid(t *testing.T) { | ||||
| @ -114,6 +119,82 @@ func TestGetProjectNotFound(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetProjectStats(t *testing.T) { | ||||
| 
 | ||||
| 	r := createProject(api.CreateProjectRequest{ | ||||
| 		Motd:     "motd", | ||||
| 		Name:     "Name", | ||||
| 		Version:  "version", | ||||
| 		CloneUrl: "http://github.com/drone/test", | ||||
| 		GitRepo:  "drone/test", | ||||
| 		Priority: 3, | ||||
| 	}) | ||||
| 
 | ||||
| 	pid := r.Id | ||||
| 
 | ||||
| 	createTask(api.CreateTaskRequest{ | ||||
| 		Priority:   1, | ||||
| 		Project:    pid, | ||||
| 		MaxRetries: 0, | ||||
| 		Recipe:     "{}", | ||||
| 	}) | ||||
| 	createTask(api.CreateTaskRequest{ | ||||
| 		Priority:   2, | ||||
| 		Project:    pid, | ||||
| 		MaxRetries: 0, | ||||
| 		Recipe:     "{}", | ||||
| 	}) | ||||
| 	createTask(api.CreateTaskRequest{ | ||||
| 		Priority:   3, | ||||
| 		Project:    pid, | ||||
| 		MaxRetries: 0, | ||||
| 		Recipe:     "{}", | ||||
| 	}) | ||||
| 
 | ||||
| 	stats := getProjectStats(pid) | ||||
| 
 | ||||
| 	if stats.Ok != true { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if stats.Stats.Project.Id != pid { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if stats.Stats.NewTaskCount != 3 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if stats.Stats.Assignees[0].Assignee != uuid.Nil { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 	if stats.Stats.Assignees[0].TaskCount != 3 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetProjectStatsNotFound(t *testing.T) { | ||||
| 
 | ||||
| 	r := createProject(api.CreateProjectRequest{ | ||||
| 		Motd:     "eeeeeeeeej", | ||||
| 		Name:     "Namaaaaaaaaaaaa", | ||||
| 		Version:  "versionsssssssss", | ||||
| 		CloneUrl: "http://github.com/drone/test1", | ||||
| 		GitRepo:  "drone/test1", | ||||
| 		Priority: 1, | ||||
| 	}) | ||||
| 	s := getProjectStats(r.Id) | ||||
| 
 | ||||
| 	if s.Ok != false { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(s.Message) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func createProject(req api.CreateProjectRequest) *api.CreateProjectResponse { | ||||
| 
 | ||||
| 	r := Post("/project/create", req) | ||||
| @ -137,3 +218,15 @@ func getProject(id int64) (*api.GetProjectResponse, *http.Response) { | ||||
| 
 | ||||
| 	return &getResp, r | ||||
| } | ||||
| 
 | ||||
| func getProjectStats(id int64) *api.GetProjectStatsResponse { | ||||
| 
 | ||||
| 	r := Get(fmt.Sprintf("/project/stats/%d", id)) | ||||
| 
 | ||||
| 	var getResp api.GetProjectStatsResponse | ||||
| 	data, _ := ioutil.ReadAll(r.Body) | ||||
| 	err := json.Unmarshal(data, &getResp) | ||||
| 	handleErr(err) | ||||
| 
 | ||||
| 	return &getResp | ||||
| } | ||||
|  | ||||
							
								
								
									
										28
									
								
								test/api_task_bench_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								test/api_task_bench_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| package test | ||||
| 
 | ||||
| import ( | ||||
| 	"src/task_tracker/api" | ||||
| 	"strconv" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func BenchmarkCreateTask(b *testing.B) { | ||||
| 
 | ||||
| 	resp := createProject(api.CreateProjectRequest{ | ||||
| 		Name:     "BenchmarkCreateTask" + strconv.Itoa(b.N), | ||||
| 		Priority: 1, | ||||
| 		GitRepo:  "benchmark_test" + strconv.Itoa(b.N), | ||||
| 		Version:  "f09e8c9r0w839x0c43", | ||||
| 		CloneUrl: "http://localhost", | ||||
| 	}) | ||||
| 
 | ||||
| 	b.ResetTimer() | ||||
| 	for i := 0; i < b.N; i++ { | ||||
| 		createTask(api.CreateTaskRequest{ | ||||
| 			Project:    resp.Id, | ||||
| 			Priority:   1, | ||||
| 			Recipe:     "{}", | ||||
| 			MaxRetries: 1, | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -38,11 +38,52 @@ func TestCreateTaskInvalidProject(t *testing.T) { | ||||
| 	}) | ||||
| 
 | ||||
| 	if resp.Ok != false { | ||||
| 		t.Fail() | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(resp.Message) <= 0 { | ||||
| 		t.Fail() | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetTaskInvalidWid(t *testing.T) { | ||||
| 
 | ||||
| 	resp := getTask(nil) | ||||
| 
 | ||||
| 	if resp.Ok != false { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(resp.Message) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetTaskInvalidWorker(t *testing.T) { | ||||
| 
 | ||||
| 	id := uuid.New() | ||||
| 	resp := getTask(&id) | ||||
| 
 | ||||
| 	if resp.Ok != false { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(resp.Message) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestGetTaskFromProjectInvalidWorker(t *testing.T) { | ||||
| 
 | ||||
| 	id := uuid.New() | ||||
| 	resp := getTaskFromProject(1, &id) | ||||
| 
 | ||||
| 	if resp.Ok != false { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(resp.Message) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -54,11 +95,28 @@ func TestCreateTaskInvalidRetries(t *testing.T) { | ||||
| 	}) | ||||
| 
 | ||||
| 	if resp.Ok != false { | ||||
| 		t.Fail() | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(resp.Message) <= 0 { | ||||
| 		t.Fail() | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestCreateTaskInvalidRecipe(t *testing.T) { | ||||
| 
 | ||||
| 	resp := createTask(api.CreateTaskRequest{ | ||||
| 		Project:    1, | ||||
| 		Recipe:     "", | ||||
| 		MaxRetries: 3, | ||||
| 	}) | ||||
| 
 | ||||
| 	if resp.Ok != false { | ||||
| 		t.Error() | ||||
| 	} | ||||
| 
 | ||||
| 	if len(resp.Message) <= 0 { | ||||
| 		t.Error() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| @ -212,11 +270,33 @@ func TestNoMoreTasks(t *testing.T) { | ||||
| 
 | ||||
| 	wid := genWid() | ||||
| 
 | ||||
| 	for i := 0; i < 30; i++ { | ||||
| 	for i := 0; i < 15; i++ { | ||||
| 		getTask(wid) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestReleaseTaskSuccess(t *testing.T) { | ||||
| 
 | ||||
| 	//wid := genWid() | ||||
| 
 | ||||
| 	pid := createProject(api.CreateProjectRequest{ | ||||
| 		Priority: 0, | ||||
| 		GitRepo:  "testreleasetask", | ||||
| 		CloneUrl: "lllllllll", | ||||
| 		Version:  "11111111111111111", | ||||
| 		Name:     "testreleasetask", | ||||
| 		Motd:     "", | ||||
| 	}).Id | ||||
| 
 | ||||
| 	createTask(api.CreateTaskRequest{ | ||||
| 		Priority:   0, | ||||
| 		Project:    pid, | ||||
| 		Recipe:     "{}", | ||||
| 		MaxRetries: 3, | ||||
| 	}) | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func createTask(request api.CreateTaskRequest) *api.CreateTaskResponse { | ||||
| 
 | ||||
| 	r := Post("/task/create", request) | ||||
|  | ||||
| @ -3,6 +3,8 @@ server: | ||||
| 
 | ||||
| database: | ||||
|   conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable" | ||||
|   #  log_levels: ["debug", "error"] | ||||
|   log_levels: ["debug", "error", "trace", "info", "warning"] | ||||
| 
 | ||||
| git: | ||||
|   webhook_secret: "very_secret_secret" | ||||
|  | ||||
| @ -12,6 +12,7 @@ func TestMain(m *testing.M) { | ||||
| 	config.SetupConfig() | ||||
| 
 | ||||
| 	testApi := api.New() | ||||
| 	testApi.SetupLogger() | ||||
| 	testApi.Database.Reset() | ||||
| 	go testApi.Run() | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| DROP TABLE IF EXISTS workerIdentity, Worker, Project, Task; | ||||
| DROP TYPE IF EXISTS Status; | ||||
| DROP TABLE IF EXISTS workeridentity, Worker, Project, Task, log_entry; | ||||
| DROP TYPE IF EXISTS status; | ||||
| DROP TYPE IF EXISTS loglevel; | ||||
| 
 | ||||
| CREATE TYPE status as ENUM ( | ||||
|   'new', | ||||
| @ -7,6 +8,10 @@ CREATE TYPE status as ENUM ( | ||||
|   'closed' | ||||
|   ); | ||||
| 
 | ||||
| CREATE TYPE loglevel as ENUM ( | ||||
|   'fatal', 'panic', 'error', 'warning', 'info', 'debug', 'trace' | ||||
|   ); | ||||
| 
 | ||||
| CREATE TABLE workerIdentity | ||||
| ( | ||||
|   id          SERIAL PRIMARY KEY, | ||||
| @ -27,6 +32,7 @@ CREATE TABLE project | ||||
| ( | ||||
|   id        SERIAL PRIMARY KEY, | ||||
|   priority  INTEGER DEFAULT 0, | ||||
|   motd      TEXT    DEFAULT '', | ||||
|   name      TEXT UNIQUE, | ||||
|   clone_url TEXT, | ||||
|   git_repo  TEXT UNIQUE, | ||||
| @ -45,4 +51,12 @@ CREATE TABLE task | ||||
|   recipe      TEXT | ||||
| ); | ||||
| 
 | ||||
| CREATE TABLE log_entry | ||||
| ( | ||||
|   level        loglevel, | ||||
|   message      TEXT, | ||||
|   message_data TEXT, | ||||
|   timestamp    INT | ||||
| ); | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										4
									
								
								updateBadges.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								updateBadges.sh
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,4 @@ | ||||
| #!/usr/bin/env bash | ||||
| 
 | ||||
| gopherbadger -md="README.md" -png=false -style flat-square | ||||
| rm coverage.out | ||||
							
								
								
									
										13
									
								
								web/angular/.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/angular/.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| # Editor configuration, see https://editorconfig.org | ||||
| root = true | ||||
| 
 | ||||
| [*] | ||||
| charset = utf-8 | ||||
| indent_style = space | ||||
| indent_size = 4 | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
| 
 | ||||
| [*.md] | ||||
| max_line_length = off | ||||
| trim_trailing_whitespace = false | ||||
							
								
								
									
										44
									
								
								web/angular/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/angular/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| # See http://help.github.com/ignore-files/ for more about ignoring files. | ||||
| 
 | ||||
| # compiled output | ||||
| /dist | ||||
| /tmp | ||||
| /out-tsc | ||||
| 
 | ||||
| # dependencies | ||||
| /node_modules | ||||
| 
 | ||||
| # profiling files | ||||
| chrome-profiler-events.json | ||||
| speed-measure-plugin.json | ||||
| 
 | ||||
| # IDEs and editors | ||||
| /.idea | ||||
| .project | ||||
| .classpath | ||||
| .c9/ | ||||
| *.launch | ||||
| .settings/ | ||||
| *.sublime-workspace | ||||
| 
 | ||||
| # IDE - VSCode | ||||
| .vscode/* | ||||
| !.vscode/settings.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/extensions.json | ||||
| .history/* | ||||
| 
 | ||||
| # misc | ||||
| /.sass-cache | ||||
| /connect.lock | ||||
| /coverage | ||||
| /libpeerconnection.log | ||||
| npm-debug.log | ||||
| yarn-error.log | ||||
| testem.log | ||||
| /typings | ||||
| 
 | ||||
| # System Files | ||||
| .DS_Store | ||||
| Thumbs.db | ||||
							
								
								
									
										27
									
								
								web/angular/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								web/angular/README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| # Angular | ||||
| 
 | ||||
| This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.2.2. | ||||
| 
 | ||||
| ## Development server | ||||
| 
 | ||||
| Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. | ||||
| 
 | ||||
| ## Code scaffolding | ||||
| 
 | ||||
| Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. | ||||
| 
 | ||||
| ## Build | ||||
| 
 | ||||
| Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. | ||||
| 
 | ||||
| ## Running unit tests | ||||
| 
 | ||||
| Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). | ||||
| 
 | ||||
| ## Running end-to-end tests | ||||
| 
 | ||||
| Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). | ||||
| 
 | ||||
| ## Further help | ||||
| 
 | ||||
| To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). | ||||
							
								
								
									
										135
									
								
								web/angular/angular.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								web/angular/angular.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,135 @@ | ||||
| { | ||||
|     "$schema": "./node_modules/@angular/cli/lib/config/schema.json", | ||||
|     "version": 1, | ||||
|     "newProjectRoot": "projects", | ||||
|     "projects": { | ||||
|         "angular": { | ||||
|             "root": "", | ||||
|             "sourceRoot": "src", | ||||
|             "projectType": "application", | ||||
|             "prefix": "app", | ||||
|             "schematics": {}, | ||||
|             "architect": { | ||||
|                 "build": { | ||||
|                     "builder": "@angular-devkit/build-angular:browser", | ||||
|                     "options": { | ||||
|                         "outputPath": "dist/angular", | ||||
|                         "index": "src/index.html", | ||||
|                         "main": "src/main.ts", | ||||
|                         "polyfills": "src/polyfills.ts", | ||||
|                         "tsConfig": "src/tsconfig.app.json", | ||||
|                         "assets": [ | ||||
|                             "src/favicon.ico", | ||||
|                             "src/assets" | ||||
|                         ], | ||||
|                         "styles": [ | ||||
|                             "src/styles.css" | ||||
|                         ], | ||||
|                         "scripts": [] | ||||
|                     }, | ||||
|                     "configurations": { | ||||
|                         "production": { | ||||
|                             "fileReplacements": [ | ||||
|                                 { | ||||
|                                     "replace": "src/environments/environment.ts", | ||||
|                                     "with": "src/environments/environment.prod.ts" | ||||
|                                 } | ||||
|                             ], | ||||
|                             "optimization": true, | ||||
|                             "outputHashing": "all", | ||||
|                             "sourceMap": false, | ||||
|                             "extractCss": true, | ||||
|                             "namedChunks": false, | ||||
|                             "aot": true, | ||||
|                             "extractLicenses": true, | ||||
|                             "vendorChunk": false, | ||||
|                             "buildOptimizer": true, | ||||
|                             "budgets": [ | ||||
|                                 { | ||||
|                                     "type": "initial", | ||||
|                                     "maximumWarning": "2mb", | ||||
|                                     "maximumError": "5mb" | ||||
|                                 } | ||||
|                             ] | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "serve": { | ||||
|                     "builder": "@angular-devkit/build-angular:dev-server", | ||||
|                     "options": { | ||||
|                         "browserTarget": "angular:build" | ||||
|                     }, | ||||
|                     "configurations": { | ||||
|                         "production": { | ||||
|                             "browserTarget": "angular:build:production" | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "extract-i18n": { | ||||
|                     "builder": "@angular-devkit/build-angular:extract-i18n", | ||||
|                     "options": { | ||||
|                         "browserTarget": "angular:build" | ||||
|                     } | ||||
|                 }, | ||||
|                 "test": { | ||||
|                     "builder": "@angular-devkit/build-angular:karma", | ||||
|                     "options": { | ||||
|                         "main": "src/test.ts", | ||||
|                         "polyfills": "src/polyfills.ts", | ||||
|                         "tsConfig": "src/tsconfig.spec.json", | ||||
|                         "karmaConfig": "src/karma.conf.js", | ||||
|                         "styles": [ | ||||
|                             "src/styles.css" | ||||
|                         ], | ||||
|                         "scripts": [], | ||||
|                         "assets": [ | ||||
|                             "src/favicon.ico", | ||||
|                             "src/assets" | ||||
|                         ] | ||||
|                     } | ||||
|                 }, | ||||
|                 "lint": { | ||||
|                     "builder": "@angular-devkit/build-angular:tslint", | ||||
|                     "options": { | ||||
|                         "tsConfig": [ | ||||
|                             "src/tsconfig.app.json", | ||||
|                             "src/tsconfig.spec.json" | ||||
|                         ], | ||||
|                         "exclude": [ | ||||
|                             "**/node_modules/**" | ||||
|                         ] | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         "angular-e2e": { | ||||
|             "root": "e2e/", | ||||
|             "projectType": "application", | ||||
|             "prefix": "", | ||||
|             "architect": { | ||||
|                 "e2e": { | ||||
|                     "builder": "@angular-devkit/build-angular:protractor", | ||||
|                     "options": { | ||||
|                         "protractorConfig": "e2e/protractor.conf.js", | ||||
|                         "devServerTarget": "angular:serve" | ||||
|                     }, | ||||
|                     "configurations": { | ||||
|                         "production": { | ||||
|                             "devServerTarget": "angular:serve:production" | ||||
|                         } | ||||
|                     } | ||||
|                 }, | ||||
|                 "lint": { | ||||
|                     "builder": "@angular-devkit/build-angular:tslint", | ||||
|                     "options": { | ||||
|                         "tsConfig": "e2e/tsconfig.e2e.json", | ||||
|                         "exclude": [ | ||||
|                             "**/node_modules/**" | ||||
|                         ] | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     "defaultProject": "angular" | ||||
| } | ||||
							
								
								
									
										29
									
								
								web/angular/e2e/protractor.conf.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								web/angular/e2e/protractor.conf.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,29 @@ | ||||
| // Protractor configuration file, see link for more information
 | ||||
| // https://github.com/angular/protractor/blob/master/lib/config.ts
 | ||||
| 
 | ||||
| const {SpecReporter} = require('jasmine-spec-reporter'); | ||||
| 
 | ||||
| exports.config = { | ||||
|     allScriptsTimeout: 11000, | ||||
|     specs: [ | ||||
|         './src/**/*.e2e-spec.ts' | ||||
|     ], | ||||
|     capabilities: { | ||||
|         'browserName': 'chrome' | ||||
|     }, | ||||
|     directConnect: true, | ||||
|     baseUrl: 'http://localhost:4200/', | ||||
|     framework: 'jasmine', | ||||
|     jasmineNodeOpts: { | ||||
|         showColors: true, | ||||
|         defaultTimeoutInterval: 30000, | ||||
|         print: function () { | ||||
|         } | ||||
|     }, | ||||
|     onPrepare() { | ||||
|         require('ts-node').register({ | ||||
|             project: require('path').join(__dirname, './tsconfig.e2e.json') | ||||
|         }); | ||||
|         jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}})); | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										23
									
								
								web/angular/e2e/src/app.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								web/angular/e2e/src/app.e2e-spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| import {AppPage} from './app.po'; | ||||
| import {browser, logging} from 'protractor'; | ||||
| 
 | ||||
| describe('workspace-project App', () => { | ||||
|     let page: AppPage; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         page = new AppPage(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should display welcome message', () => { | ||||
|         page.navigateTo(); | ||||
|         expect(page.getTitleText()).toEqual('Welcome to angular!'); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(async () => { | ||||
|         // Assert that there are no errors emitted from the browser
 | ||||
|         const logs = await browser.manage().logs().get(logging.Type.BROWSER); | ||||
|         expect(logs).not.toContain(jasmine.objectContaining({ | ||||
|             level: logging.Level.SEVERE, | ||||
|         })); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										11
									
								
								web/angular/e2e/src/app.po.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/angular/e2e/src/app.po.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| import {browser, by, element} from 'protractor'; | ||||
| 
 | ||||
| export class AppPage { | ||||
|     navigateTo() { | ||||
|         return browser.get('/'); | ||||
|     } | ||||
| 
 | ||||
|     getTitleText() { | ||||
|         return element(by.css('app-root h1')).getText(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								web/angular/e2e/tsconfig.e2e.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/angular/e2e/tsconfig.e2e.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| { | ||||
|     "extends": "../tsconfig.json", | ||||
|     "compilerOptions": { | ||||
|         "outDir": "../out-tsc/app", | ||||
|         "module": "commonjs", | ||||
|         "target": "es5", | ||||
|         "types": [ | ||||
|             "jasmine", | ||||
|             "jasminewd2", | ||||
|             "node" | ||||
|         ] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11327
									
								
								web/angular/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11327
									
								
								web/angular/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										53
									
								
								web/angular/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								web/angular/package.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| { | ||||
|     "name": "angular", | ||||
|     "version": "0.0.0", | ||||
|     "scripts": { | ||||
|         "ng": "ng", | ||||
|         "start": "ng serve", | ||||
|         "build": "ng build", | ||||
|         "test": "ng test", | ||||
|         "lint": "ng lint", | ||||
|         "e2e": "ng e2e" | ||||
|     }, | ||||
|     "private": true, | ||||
|     "dependencies": { | ||||
|         "@angular/animations": "^7.2.1", | ||||
|         "@angular/cdk": "^7.2.1", | ||||
|         "@angular/common": "~7.2.0", | ||||
|         "@angular/compiler": "~7.2.0", | ||||
|         "@angular/core": "~7.2.0", | ||||
|         "@angular/forms": "~7.2.0", | ||||
|         "@angular/material": "^7.2.1", | ||||
|         "@angular/platform-browser": "~7.2.0", | ||||
|         "@angular/platform-browser-dynamic": "~7.2.0", | ||||
|         "@angular/router": "~7.2.0", | ||||
|         "core-js": "^2.5.4", | ||||
|         "d3": "^5.7.0", | ||||
|         "lodash": "^4.17.11", | ||||
|         "moment": "^2.23.0", | ||||
|         "rxjs": "~6.3.3", | ||||
|         "tslib": "^1.9.0", | ||||
|         "zone.js": "~0.8.26" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@angular-devkit/build-angular": "~0.12.0", | ||||
|         "@angular/cli": "~7.2.2", | ||||
|         "@angular/compiler-cli": "~7.2.0", | ||||
|         "@angular/language-service": "~7.2.0", | ||||
|         "@types/node": "~8.9.4", | ||||
|         "@types/jasmine": "~2.8.8", | ||||
|         "@types/jasminewd2": "~2.0.3", | ||||
|         "codelyzer": "~4.5.0", | ||||
|         "jasmine-core": "~2.99.1", | ||||
|         "jasmine-spec-reporter": "~4.2.1", | ||||
|         "karma": "~3.1.1", | ||||
|         "karma-chrome-launcher": "~2.2.0", | ||||
|         "karma-coverage-istanbul-reporter": "~2.0.1", | ||||
|         "karma-jasmine": "~1.1.2", | ||||
|         "karma-jasmine-html-reporter": "^0.2.2", | ||||
|         "protractor": "~5.4.0", | ||||
|         "ts-node": "~7.0.0", | ||||
|         "tslint": "~5.11.0", | ||||
|         "typescript": "~3.2.2" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								web/angular/src/app/api.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/angular/src/app/api.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| import {Injectable} from '@angular/core'; | ||||
| import {HttpClient} from "@angular/common/http"; | ||||
| 
 | ||||
| @Injectable() | ||||
| export class ApiService { | ||||
| 
 | ||||
|     private url: string = "http://localhost:42901"; | ||||
| 
 | ||||
|     constructor( | ||||
|         private http: HttpClient, | ||||
|     ) { | ||||
|     } | ||||
| 
 | ||||
|     getLogs() { | ||||
|         return this.http.get(this.url + "/logs"); | ||||
|     } | ||||
| 
 | ||||
|     getProjectStats(id: number) { | ||||
|         return this.http.get(this.url + "/project/stats/" + id) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										16
									
								
								web/angular/src/app/app-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								web/angular/src/app/app-routing.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| import {NgModule} from '@angular/core'; | ||||
| import {RouterModule, Routes} from '@angular/router'; | ||||
| import {LogsComponent} from "./logs/logs.component"; | ||||
| import {ProjectDashboardComponent} from "./project-dashboard/project-dashboard.component"; | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     {path: "log", component: LogsComponent}, | ||||
|     {path: "project", component: ProjectDashboardComponent} | ||||
| ]; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [RouterModule.forRoot(routes)], | ||||
|     exports: [RouterModule] | ||||
| }) | ||||
| export class AppRoutingModule { | ||||
| } | ||||
							
								
								
									
										0
									
								
								web/angular/src/app/app.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								web/angular/src/app/app.component.css
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										8
									
								
								web/angular/src/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								web/angular/src/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <mat-toolbar> | ||||
|     <a [routerLink]="''">Index</a> | ||||
|     <a [routerLink]="'log'">Logs</a> | ||||
|     <a [routerLink]="'project'">Project</a> | ||||
| </mat-toolbar> | ||||
| 
 | ||||
| 
 | ||||
| <router-outlet></router-outlet> | ||||
							
								
								
									
										10
									
								
								web/angular/src/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/angular/src/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| import {Component} from '@angular/core'; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'app-root', | ||||
|     templateUrl: './app.component.html', | ||||
|     styleUrls: ['./app.component.css'] | ||||
| }) | ||||
| export class AppComponent { | ||||
|     title = 'angular'; | ||||
| } | ||||
							
								
								
									
										56
									
								
								web/angular/src/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								web/angular/src/app/app.module.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| import {BrowserModule} from '@angular/platform-browser'; | ||||
| import {NgModule} from '@angular/core'; | ||||
| 
 | ||||
| import {AppRoutingModule} from './app-routing.module'; | ||||
| import {AppComponent} from './app.component'; | ||||
| import {LogsComponent} from './logs/logs.component'; | ||||
| 
 | ||||
| import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; | ||||
| import { | ||||
|     MatCardModule, | ||||
|     MatExpansionModule, | ||||
|     MatFormFieldModule, | ||||
|     MatIconModule, | ||||
|     MatInputModule, | ||||
|     MatMenuModule, | ||||
|     MatPaginatorModule, | ||||
|     MatSortModule, | ||||
|     MatTableModule, | ||||
|     MatToolbarModule, | ||||
|     MatTreeModule | ||||
| } from "@angular/material"; | ||||
| import {ApiService} from "./api.service"; | ||||
| import {HttpClientModule} from "@angular/common/http"; | ||||
| import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.component'; | ||||
| 
 | ||||
| @NgModule({ | ||||
|     declarations: [ | ||||
|         AppComponent, | ||||
|         LogsComponent, | ||||
|         ProjectDashboardComponent | ||||
|     ], | ||||
|     imports: [ | ||||
|         BrowserModule, | ||||
|         AppRoutingModule, | ||||
|         MatMenuModule, | ||||
|         MatIconModule, | ||||
|         MatTableModule, | ||||
|         MatPaginatorModule, | ||||
|         MatSortModule, | ||||
|         MatFormFieldModule, | ||||
|         MatInputModule, | ||||
|         MatToolbarModule, | ||||
|         MatCardModule, | ||||
|         MatExpansionModule, | ||||
|         MatTreeModule, | ||||
|         BrowserAnimationsModule, | ||||
|         HttpClientModule, | ||||
|     ], | ||||
|     exports: [], | ||||
|     providers: [ | ||||
|         ApiService, | ||||
|     ], | ||||
|     bootstrap: [AppComponent] | ||||
| }) | ||||
| export class AppModule { | ||||
| } | ||||
							
								
								
									
										6
									
								
								web/angular/src/app/logs/logentry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								web/angular/src/app/logs/logentry.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| interface LogEntry { | ||||
|     level: string, | ||||
|     message: string, | ||||
|     data: any, | ||||
|     timestamp: string, | ||||
| } | ||||
							
								
								
									
										12
									
								
								web/angular/src/app/logs/logs.component.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/angular/src/app/logs/logs.component.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| .table-container { | ||||
|     height: 600px; | ||||
| } | ||||
| 
 | ||||
| .mat-table { | ||||
|     height: 100%; | ||||
|     overflow: scroll; | ||||
| } | ||||
| 
 | ||||
| .mat-cell { | ||||
|     text-align: left; | ||||
| } | ||||
							
								
								
									
										32
									
								
								web/angular/src/app/logs/logs.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web/angular/src/app/logs/logs.component.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,32 @@ | ||||
| <div class="table-container"> | ||||
|     <mat-form-field> | ||||
|         <input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter"> | ||||
|     </mat-form-field> | ||||
|     <mat-table [dataSource]="data" class="mat-elevation-z8" matSort matSortActive="timestamp" matSortDirection="desc"> | ||||
| 
 | ||||
|         <ng-container matColumnDef="level"> | ||||
|             <mat-header-cell style="flex: 0 0 6em" mat-sort-header *matHeaderCellDef>Level</mat-header-cell> | ||||
|             <mat-cell style="flex: 0 0 6em" *matCellDef="let entry"> {{entry.level}} </mat-cell> | ||||
|         </ng-container> | ||||
|         <ng-container matColumnDef="timestamp"> | ||||
|             <mat-header-cell style="flex: 0 0 21em" mat-sort-header *matHeaderCellDef>Time</mat-header-cell> | ||||
|             <mat-cell style="flex: 0 0 17em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell> | ||||
|         </ng-container> | ||||
|         <ng-container matColumnDef="message"> | ||||
|             <mat-header-cell mat-sort-header *matHeaderCellDef>Message</mat-header-cell> | ||||
|             <mat-cell style="flex: 0 0 30em" *matCellDef="let entry"> {{entry.message}} </mat-cell> | ||||
|         </ng-container> | ||||
|         <ng-container matColumnDef="data"> | ||||
|             <mat-header-cell mat-sort-header *matHeaderCellDef>Data</mat-header-cell> | ||||
|             <mat-cell *matCellDef="let entry"> | ||||
|                 <pre>{{entry.data}}</pre> | ||||
|             </mat-cell> | ||||
|         </ng-container> | ||||
| 
 | ||||
|         <mat-header-row *matHeaderRowDef="logsCols"></mat-header-row> | ||||
|         <mat-row *matRowDef="let row; columns: logsCols;"></mat-row> | ||||
|     </mat-table> | ||||
| 
 | ||||
|     <mat-paginator [length]="logs.length" [pageSizeOptions]="[5,10,25,100]" [pageSize]="5"></mat-paginator> | ||||
| </div> | ||||
| 
 | ||||
							
								
								
									
										25
									
								
								web/angular/src/app/logs/logs.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								web/angular/src/app/logs/logs.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| import {async, ComponentFixture, TestBed} from '@angular/core/testing'; | ||||
| 
 | ||||
| import {LogsComponent} from './logs.component'; | ||||
| 
 | ||||
| describe('LogsComponent', () => { | ||||
|     let component: LogsComponent; | ||||
|     let fixture: ComponentFixture<LogsComponent>; | ||||
| 
 | ||||
|     beforeEach(async(() => { | ||||
|         TestBed.configureTestingModule({ | ||||
|             declarations: [LogsComponent] | ||||
|         }) | ||||
|             .compileComponents(); | ||||
|     })); | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|         fixture = TestBed.createComponent(LogsComponent); | ||||
|         component = fixture.componentInstance; | ||||
|         fixture.detectChanges(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should create', () => { | ||||
|         expect(component).toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										55
									
								
								web/angular/src/app/logs/logs.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								web/angular/src/app/logs/logs.component.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| import {Component, OnInit, ViewChild} from '@angular/core'; | ||||
| import {ApiService} from "../api.service"; | ||||
| 
 | ||||
| import _ from "lodash" | ||||
| import * as moment from "moment"; | ||||
| import {MatPaginator, MatSort, MatTableDataSource} from "@angular/material"; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'app-logs', | ||||
|     templateUrl: './logs.component.html', | ||||
|     styleUrls: ['./logs.component.css'] | ||||
| }) | ||||
| export class LogsComponent implements OnInit { | ||||
| 
 | ||||
|     private logs: LogEntry[] = []; | ||||
|     private data: MatTableDataSource<LogEntry>; | ||||
|     private logsCols: string[] = ["level", "timestamp", "message", "data"]; | ||||
| 
 | ||||
|     @ViewChild(MatPaginator) paginator: MatPaginator; | ||||
|     @ViewChild(MatSort) sort: MatSort; | ||||
| 
 | ||||
|     constructor(private apiService: ApiService) { | ||||
|         this.data = new MatTableDataSource<LogEntry>(this.logs) | ||||
|     } | ||||
| 
 | ||||
|     ngOnInit() { | ||||
|         this.getLogs(); | ||||
| 
 | ||||
|         this.data.paginator = this.paginator; | ||||
|         this.data.sort = this.sort; | ||||
|         // interval(5000).subscribe(() => {
 | ||||
|         //     this.getLogs();
 | ||||
|         // })
 | ||||
|     } | ||||
| 
 | ||||
|     applyFilter(filter: string) { | ||||
|         this.data.filter = filter.trim().toLowerCase(); | ||||
|     } | ||||
| 
 | ||||
|     private getLogs() { | ||||
|         this.apiService.getLogs().subscribe( | ||||
|             data => { | ||||
|                 this.data.data = _.map(data, (entry) => { | ||||
|                     return <LogEntry>{ | ||||
|                         message: entry.message, | ||||
|                         timestamp: moment.unix(entry.timestamp).toISOString(), | ||||
|                         data: JSON.stringify(JSON.parse(entry.data), null, 2), | ||||
|                         level: entry.level.toUpperCase() | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -0,0 +1,44 @@ | ||||
| #status { | ||||
|     height: 360px; | ||||
|     position: relative; | ||||
|     width: 360px; | ||||
|     margin: 1em; | ||||
| } | ||||
| 
 | ||||
| #assignees { | ||||
|     height: 360px; | ||||
|     position: relative; | ||||
|     width: 360px; | ||||
|     margin: 1em; | ||||
| } | ||||
| 
 | ||||
| .pie-label { | ||||
|     left: 130px; | ||||
|     padding: 10px; | ||||
|     position: absolute; | ||||
|     text-align: center; | ||||
|     top: 150px; | ||||
|     width: 80px; | ||||
|     z-index: 10; | ||||
|     font-family: Roboto, "Helvetica Neue", sans-serif; | ||||
|     font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .tooltip { | ||||
|     background: #eee; | ||||
|     box-shadow: 0 0 5px #999999; | ||||
|     color: #333; | ||||
|     display: none; | ||||
|     font-size: 12px; | ||||
|     left: 130px; | ||||
|     padding: 10px; | ||||
|     position: absolute; | ||||
|     text-align: center; | ||||
|     top: 95px; | ||||
|     width: 80px; | ||||
|     z-index: 10; | ||||
| } | ||||
| 
 | ||||
| /*---------*/ | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,42 @@ | ||||
| <div> | ||||
|     <mat-card> | ||||
|         <mat-card-title *ngIf="projectStats">Stats for project "{{projectStats["project"]["name"]}}"</mat-card-title> | ||||
|         <mat-card-content style="padding: 2em 0 1em"> | ||||
| 
 | ||||
|             <p *ngIf="projectStats">Git repository: <a target="_blank" | ||||
|                                                        [href]="projectStats['project']['clone_url']">{{projectStats["project"]["git_repo"]}}</a> | ||||
|             </p> | ||||
|             <p>Message of the day: </p> | ||||
|             <pre *ngIf="projectStats">{{projectStats["project"]["motd"]}}</pre> | ||||
| 
 | ||||
|             <div style="display: flex; align-items: center; justify-content: center"> | ||||
|                 <div id="line"></div> | ||||
| 
 | ||||
|                 <div id="status"> | ||||
|                     <div class="tooltip" id="stooltip"> | ||||
|                         <div class="label"></div> | ||||
|                         <div class="count"></div> | ||||
|                         <div class="percent"></div> | ||||
|                     </div> | ||||
|                     <div class="pie-label">Task Status</div> | ||||
|                 </div> | ||||
|                 <div id="assignees"> | ||||
|                     <div class="tooltip" id="atooltip"> | ||||
|                         <div class="label"></div> | ||||
|                         <div class="count"></div> | ||||
|                         <div class="percent"></div> | ||||
|                     </div> | ||||
|                     <div class="pie-label">Assignees</div> | ||||
|                 </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <mat-expansion-panel *ngIf="projectStats" style="margin-top: 1em"> | ||||
|                 <mat-expansion-panel-header> | ||||
|                     <mat-panel-title>Project metadata</mat-panel-title> | ||||
|                 </mat-expansion-panel-header> | ||||
|                 <pre>{{projectStats | json}}</pre> | ||||
|             </mat-expansion-panel> | ||||
| 
 | ||||
|         </mat-card-content> | ||||
|     </mat-card> | ||||
| </div> | ||||
| @ -0,0 +1,267 @@ | ||||
| import {Component, OnInit} from '@angular/core'; | ||||
| 
 | ||||
| import * as d3 from "d3" | ||||
| import * as _ from "lodash" | ||||
| import {interval} from "rxjs"; | ||||
| import {ApiService} from "../api.service"; | ||||
| 
 | ||||
| @Component({ | ||||
|     selector: 'app-project-dashboard', | ||||
|     templateUrl: './project-dashboard.component.html', | ||||
|     styleUrls: ['./project-dashboard.component.css'] | ||||
| }) | ||||
| export class ProjectDashboardComponent implements OnInit { | ||||
| 
 | ||||
|     projectStats; | ||||
|     private pieWidth = 360; | ||||
|     private pieHeight = 360; | ||||
|     private pieRadius = Math.min(this.pieWidth, this.pieHeight) / 2; | ||||
|     private pieArc = d3.arc() | ||||
|         .innerRadius(this.pieRadius / 2) | ||||
|         .outerRadius(this.pieRadius); | ||||
| 
 | ||||
|     private pieFun = d3.pie().value((d) => d.count); | ||||
|     private statusColor = d3.scaleOrdinal().range(['#31a6a2', '#8c2627', '#62f24b']); | ||||
|     private assigneesColor = d3.scaleOrdinal().range(["", "#AAAAAA"].concat(d3.schemePaired)); | ||||
| 
 | ||||
|     private statusData: any[]; | ||||
|     private assigneesData: any[]; | ||||
| 
 | ||||
|     private newTaskCounts: any[] = []; | ||||
|     private failedTaskCounts: any[] = []; | ||||
|     private closedTaskCounts: any[] = []; | ||||
| 
 | ||||
|     private newTaskPath: any; | ||||
|     private failedTaskPath: any; | ||||
|     private closedTaskPath: any; | ||||
| 
 | ||||
|     private maxY: number = 10; | ||||
|     private yAxis: any; | ||||
|     private yScale: any; | ||||
|     private xScale: any; | ||||
|     private range: number; | ||||
|     private line: any; | ||||
| 
 | ||||
|     private statusPath: any; | ||||
|     private statusSvg: any; | ||||
|     private assigneesPath: any; | ||||
|     private assigneesSvg: any; | ||||
| 
 | ||||
|     constructor(private apiService: ApiService) { | ||||
|     } | ||||
| 
 | ||||
|     setupStatusPieChart() { | ||||
|         let tooltip = d3.select("#stooltip"); | ||||
| 
 | ||||
|         this.statusSvg = d3.select('#status') | ||||
|             .append('svg') | ||||
|             .attr('width', this.pieWidth) | ||||
|             .attr('height', this.pieHeight) | ||||
|             .append("g") | ||||
|             .attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")"); | ||||
| 
 | ||||
|         this.statusPath = this.statusSvg.selectAll("path") | ||||
|             .data(this.pieFun(this.statusData)) | ||||
|             .enter() | ||||
|             .append('path') | ||||
|             .attr('d', this.pieArc) | ||||
|             .attr('fill', (d) => this.statusColor(d.data.label)); | ||||
| 
 | ||||
|         this.setupToolTip(this.statusPath, tooltip) | ||||
|     } | ||||
| 
 | ||||
|     setupAssigneesPieChart() { | ||||
|         let tooltip = d3.select("#atooltip"); | ||||
| 
 | ||||
|         this.assigneesSvg = d3.select('#assignees') | ||||
|             .append('svg') | ||||
|             .attr('width', this.pieWidth) | ||||
|             .attr('height', this.pieHeight) | ||||
|             .append("g") | ||||
|             .attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")"); | ||||
| 
 | ||||
|         this.assigneesPath = this.assigneesSvg.selectAll("path") | ||||
|             .data(this.pieFun(this.assigneesData)) | ||||
|             .enter() | ||||
|             .append('path') | ||||
|             .attr('d', this.pieArc) | ||||
|             .attr('fill', (d) => this.assigneesColor(d.data.label)); | ||||
| 
 | ||||
|         this.setupToolTip(this.assigneesPath, tooltip) | ||||
|     } | ||||
| 
 | ||||
|     setupToolTip(x, tooltip) { | ||||
|         x.on('mouseover', (d) => { | ||||
|             let total = d3.sum(this.assigneesData.map((d) => d.count)); | ||||
|             let percent = Math.round(1000 * d.data.count / total) / 10; | ||||
|             tooltip.select('.label').html(d.data.label); | ||||
|             tooltip.select('.count').html(d.data.count); | ||||
|             tooltip.select('.percent').html(percent + '%'); | ||||
|             tooltip.style('display', 'block'); | ||||
|         }); | ||||
|         x.on('mouseout', function () { | ||||
|             tooltip.style('display', 'none'); | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     setupLine() { | ||||
|         let margin = {top: 50, right: 50, bottom: 50, left: 50}; | ||||
|         this.range = 600; | ||||
|         let width = 750; | ||||
|         let height = 250; | ||||
| 
 | ||||
|         this.xScale = d3.scaleLinear() | ||||
|             .domain([this.range, 0]) | ||||
|             .range([width, 0]); | ||||
| 
 | ||||
|         this.yScale = d3.scaleLinear() | ||||
|             .domain([0, this.maxY]) | ||||
|             .range([height, 0]); | ||||
| 
 | ||||
|         this.line = d3.line() | ||||
|             .x((d, i) => this.xScale(i)) | ||||
|             .y((d) => this.yScale(d.y)) | ||||
|             .curve(d3.curveMonotoneX); | ||||
| 
 | ||||
|         let svg = d3.select("#line").append("svg") | ||||
|             .attr("width", width + margin.left + margin.right) | ||||
|             .attr("height", height + margin.top + margin.bottom) | ||||
|             .append("g") | ||||
|             .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | ||||
| 
 | ||||
|         svg.append("defs").append("clipPath") | ||||
|             .attr("id", "clip") | ||||
|             .append("rect") | ||||
|             .attr("width", width) | ||||
|             .attr("height", height); | ||||
| 
 | ||||
|         this.newTaskPath = svg | ||||
|             .append("path") | ||||
|             .attr("clip-path", "url(#clip)") | ||||
|             .datum(this.newTaskCounts) | ||||
|             .attr("class", "line-new") | ||||
|             .attr("d", this.line); | ||||
|         this.failedTaskPath = svg | ||||
|             .append("path") | ||||
|             .attr("clip-path", "url(#clip)") | ||||
|             .datum(this.failedTaskCounts) | ||||
|             .attr("class", "line-failed") | ||||
|             .attr("d", this.line); | ||||
|         this.closedTaskPath = svg | ||||
|             .append("path") | ||||
|             .attr("clip-path", "url(#clip)") | ||||
|             .datum(this.closedTaskCounts) | ||||
|             .attr("class", "line-closed") | ||||
|             .attr("d", this.line); | ||||
| 
 | ||||
|         let xAxis = svg.append("g") | ||||
|             .attr("class", "x-axis") | ||||
|             .attr("transform", "translate(0," + height + ")"); | ||||
|         xAxis.call(d3.axisBottom(this.xScale).tickFormat((d) => (d - 600) + "s")); | ||||
| 
 | ||||
|         this.yAxis = svg.append("g") | ||||
|             .attr("class", "y-axis"); | ||||
|         this.yAxis.call(d3.axisLeft(this.yScale)); | ||||
|     } | ||||
| 
 | ||||
|     getStats() { | ||||
|         this.apiService.getProjectStats(2).subscribe((data) => { | ||||
| 
 | ||||
|             this.projectStats = data["stats"]; | ||||
| 
 | ||||
|             this.updateLine(); | ||||
|             this.updatePie(); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private updateLine() { | ||||
| 
 | ||||
|         let newVal = {"y": this.projectStats["new_task_count"]}; | ||||
|         let failedVal = {"y": this.projectStats["failed_task_count"]}; | ||||
|         let closedVal = {"y": this.projectStats["closed_task_count"]}; | ||||
| 
 | ||||
|         //Adjust y axis
 | ||||
|         this.maxY = Math.max(newVal["y"], this.maxY); | ||||
|         this.yScale.domain([0, this.maxY]); | ||||
|         this.yAxis.call(d3.axisLeft(this.yScale)); | ||||
| 
 | ||||
|         this.newTaskPath | ||||
|             .attr("d", this.line) | ||||
|             .attr("transform", null); | ||||
|         this.failedTaskPath | ||||
|             .attr("d", this.line) | ||||
|             .attr("transform", null); | ||||
|         this.closedTaskPath | ||||
|             .attr("d", this.line) | ||||
|             .attr("transform", null); | ||||
| 
 | ||||
|         //remove fist element
 | ||||
|         if (this.newTaskCounts.length >= this.range) { | ||||
|             this.newTaskCounts.shift(); | ||||
|             this.newTaskPath | ||||
|                 .transition() | ||||
|                 .attr("transform", "translate(" + this.xScale(-1) + ")"); | ||||
|         } | ||||
|         if (this.failedTaskCounts.length >= this.range) { | ||||
|             this.failedTaskCounts.shift(); | ||||
|             this.failedTaskPath | ||||
|                 .transition() | ||||
|                 .attr("transform", "translate(" + this.xScale(-1) + ")"); | ||||
|         } | ||||
|         if (this.closedTaskCounts.length >= this.range) { | ||||
|             this.closedTaskCounts.shift(); | ||||
|             this.closedTaskPath | ||||
|                 .transition() | ||||
|                 .attr("transform", "translate(" + this.xScale(-1) + ")"); | ||||
|         } | ||||
| 
 | ||||
|         this.newTaskCounts.push(newVal); | ||||
|         this.failedTaskCounts.push(failedVal); | ||||
|         this.closedTaskCounts.push(closedVal); | ||||
|     } | ||||
| 
 | ||||
|     private updatePie() { | ||||
|         this.statusData = [ | ||||
|             {label: "New", count: this.projectStats["new_task_count"]}, | ||||
|             {label: "Failed", count: this.projectStats["failed_task_count"]}, | ||||
|             {label: "Closed", count: this.projectStats["closed_task_count"]}, | ||||
|         ]; | ||||
|         this.assigneesData = _.map(this.projectStats["assignees"], (assignedTasks) => { | ||||
|             return { | ||||
|                 label: assignedTasks["assignee"] == "00000000-0000-0000-0000-000000000000" ? "unassigned" : assignedTasks["assignee"], | ||||
|                 count: assignedTasks["task_count"] | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         this.statusSvg.selectAll("path") | ||||
|             .data(this.pieFun(this.statusData)); | ||||
|         this.statusPath | ||||
|             .attr('d', this.pieArc) | ||||
|             .attr('fill', (d) => this.statusColor(d.data.label)); | ||||
|         this.assigneesSvg.selectAll("path") | ||||
|             .data(this.pieFun(this.assigneesData)); | ||||
|         this.assigneesPath | ||||
|             .attr('d', this.pieArc) | ||||
|             .attr('fill', (d) => this.assigneesColor(d.data.label)); | ||||
|     } | ||||
| 
 | ||||
|     ngOnInit() { | ||||
|         this.statusData = [ | ||||
|             {label: 'new', count: 0}, | ||||
|             {label: 'failed', count: 0}, | ||||
|             {label: 'closed', count: 0}, | ||||
|         ]; | ||||
|         this.assigneesData = [ | ||||
|             {label: 'null', count: 0}, | ||||
|         ]; | ||||
| 
 | ||||
|         this.setupStatusPieChart(); | ||||
|         this.setupAssigneesPieChart(); | ||||
|         this.setupLine(); | ||||
| 
 | ||||
|         this.getStats(); | ||||
|         interval(1000).subscribe(() => { | ||||
|             this.getStats() | ||||
|         }) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								web/angular/src/app/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								web/angular/src/app/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| { | ||||
|     "compilerOptions": { | ||||
|         "module": "commonjs", | ||||
|         "target": "es5", | ||||
|         "sourceMap": true | ||||
|     }, | ||||
|     "exclude": [ | ||||
|         "node_modules" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										0
									
								
								web/angular/src/assets/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								web/angular/src/assets/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										11
									
								
								web/angular/src/browserslist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/angular/src/browserslist
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers | ||||
| # For additional information regarding the format and rule options, please see: | ||||
| # https://github.com/browserslist/browserslist#queries | ||||
| # | ||||
| # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed | ||||
| 
 | ||||
| > 0.5% | ||||
| last 2 versions | ||||
| Firefox ESR | ||||
| not dead | ||||
| not IE 9-11 | ||||
							
								
								
									
										3
									
								
								web/angular/src/environments/environment.prod.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web/angular/src/environments/environment.prod.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| export const environment = { | ||||
|     production: true | ||||
| }; | ||||
							
								
								
									
										16
									
								
								web/angular/src/environments/environment.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								web/angular/src/environments/environment.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| // This file can be replaced during build by using the `fileReplacements` array.
 | ||||
| // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
 | ||||
| // The list of file replacements can be found in `angular.json`.
 | ||||
| 
 | ||||
| export const environment = { | ||||
|     production: false | ||||
| }; | ||||
| 
 | ||||
| /* | ||||
|  * For easier debugging in development mode, you can import the following file | ||||
|  * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. | ||||
|  * | ||||
|  * This import should be commented out in production mode because it will have a negative impact | ||||
|  * on performance if an error is thrown. | ||||
|  */ | ||||
| // import 'zone.js/dist/zone-error';  // Included with Angular CLI.
 | ||||
							
								
								
									
										
											BIN
										
									
								
								web/angular/src/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/angular/src/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.3 KiB | 
							
								
								
									
										15
									
								
								web/angular/src/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/angular/src/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| <!doctype html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <title>Angular</title> | ||||
|     <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | ||||
|     <base href="/"> | ||||
| 
 | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
|     <link rel="icon" type="image/x-icon" href="favicon.ico"> | ||||
| </head> | ||||
| <body> | ||||
| <app-root></app-root> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										31
									
								
								web/angular/src/karma.conf.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								web/angular/src/karma.conf.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| // Karma configuration file, see link for more information
 | ||||
| // https://karma-runner.github.io/1.0/config/configuration-file.html
 | ||||
| 
 | ||||
| module.exports = function (config) { | ||||
|     config.set({ | ||||
|         basePath: '', | ||||
|         frameworks: ['jasmine', '@angular-devkit/build-angular'], | ||||
|         plugins: [ | ||||
|             require('karma-jasmine'), | ||||
|             require('karma-chrome-launcher'), | ||||
|             require('karma-jasmine-html-reporter'), | ||||
|             require('karma-coverage-istanbul-reporter'), | ||||
|             require('@angular-devkit/build-angular/plugins/karma') | ||||
|         ], | ||||
|         client: { | ||||
|             clearContext: false // leave Jasmine Spec Runner output visible in browser
 | ||||
|         }, | ||||
|         coverageIstanbulReporter: { | ||||
|             dir: require('path').join(__dirname, '../coverage'), | ||||
|             reports: ['html', 'lcovonly', 'text-summary'], | ||||
|             fixWebpackSourcePaths: true | ||||
|         }, | ||||
|         reporters: ['progress', 'kjhtml'], | ||||
|         port: 9876, | ||||
|         colors: true, | ||||
|         logLevel: config.LOG_INFO, | ||||
|         autoWatch: true, | ||||
|         browsers: ['Chrome'], | ||||
|         singleRun: false | ||||
|     }); | ||||
| }; | ||||
							
								
								
									
										12
									
								
								web/angular/src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								web/angular/src/main.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| import {enableProdMode} from '@angular/core'; | ||||
| import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; | ||||
| 
 | ||||
| import {AppModule} from './app/app.module'; | ||||
| import {environment} from './environments/environment'; | ||||
| 
 | ||||
| if (environment.production) { | ||||
|     enableProdMode(); | ||||
| } | ||||
| 
 | ||||
| platformBrowserDynamic().bootstrapModule(AppModule) | ||||
|     .catch(err => console.error(err)); | ||||
							
								
								
									
										85
									
								
								web/angular/src/polyfills.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								web/angular/src/polyfills.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| /** | ||||
|  * This file includes polyfills needed by Angular and is loaded before the app. | ||||
|  * You can add your own extra polyfills to this file. | ||||
|  * | ||||
|  * This file is divided into 2 sections: | ||||
|  *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. | ||||
|  *   2. Application imports. Files imported after ZoneJS that should be loaded before your main | ||||
|  *      file. | ||||
|  * | ||||
|  * The current setup is for so-called "evergreen" browsers; the last versions of browsers that | ||||
|  * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), | ||||
|  * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. | ||||
|  * | ||||
|  * Learn more in https://angular.io/guide/browser-support
 | ||||
|  */ | ||||
| 
 | ||||
| /*************************************************************************************************** | ||||
|  * BROWSER POLYFILLS | ||||
|  */ | ||||
| 
 | ||||
| /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills. | ||||
|  *  This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot | ||||
|  */ | ||||
| 
 | ||||
| // import 'core-js/es6/symbol';
 | ||||
| // import 'core-js/es6/object';
 | ||||
| // import 'core-js/es6/function';
 | ||||
| // import 'core-js/es6/parse-int';
 | ||||
| // import 'core-js/es6/parse-float';
 | ||||
| // import 'core-js/es6/number';
 | ||||
| // import 'core-js/es6/math';
 | ||||
| // import 'core-js/es6/string';
 | ||||
| // import 'core-js/es6/date';
 | ||||
| // import 'core-js/es6/array';
 | ||||
| // import 'core-js/es6/regexp';
 | ||||
| // import 'core-js/es6/map';
 | ||||
| // import 'core-js/es6/weak-map';
 | ||||
| // import 'core-js/es6/set';
 | ||||
| 
 | ||||
| /** IE10 and IE11 requires the following for NgClass support on SVG elements */ | ||||
| // import 'classlist.js';  // Run `npm install --save classlist.js`.
 | ||||
| 
 | ||||
| /** IE10 and IE11 requires the following for the Reflect API. */ | ||||
| // import 'core-js/es6/reflect';
 | ||||
| 
 | ||||
| /** | ||||
|  * Web Animations `@angular/platform-browser/animations` | ||||
|  * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. | ||||
|  * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). | ||||
|  */ | ||||
| // import 'web-animations-js';  // Run `npm install --save web-animations-js`.
 | ||||
| 
 | ||||
| /** | ||||
|  * By default, zone.js will patch all possible macroTask and DomEvents | ||||
|  * user can disable parts of macroTask/DomEvents patch by setting following flags | ||||
|  * because those flags need to be set before `zone.js` being loaded, and webpack | ||||
|  * will put import in the top of bundle, so user need to create a separate file | ||||
|  * in this directory (for example: zone-flags.ts), and put the following flags | ||||
|  * into that file, and then add the following code before importing zone.js. | ||||
|  * import './zone-flags.ts'; | ||||
|  * | ||||
|  * The flags allowed in zone-flags.ts are listed here. | ||||
|  * | ||||
|  * The following flags will work for all browsers. | ||||
|  * | ||||
|  * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 | ||||
|  * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 | ||||
|  * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
 | ||||
|  * | ||||
|  *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js | ||||
|  *  with the following flag, it will bypass `zone.js` patch for IE/Edge | ||||
|  * | ||||
|  *  (window as any).__Zone_enable_cross_context_check = true; | ||||
|  * | ||||
|  */ | ||||
| 
 | ||||
| /*************************************************************************************************** | ||||
|  * Zone JS is required by default for Angular itself. | ||||
|  */ | ||||
| import 'zone.js/dist/zone'; // Included with Angular CLI.
 | ||||
| 
 | ||||
| 
 | ||||
| /*************************************************************************************************** | ||||
|  * APPLICATION IMPORTS | ||||
|  */ | ||||
							
								
								
									
										36
									
								
								web/angular/src/styles.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/angular/src/styles.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,36 @@ | ||||
| /* You can add global styles to this file, and also import other style files */ | ||||
| @import "~@angular/material/prebuilt-themes/indigo-pink.css"; | ||||
| 
 | ||||
| .line-new { | ||||
|     fill: none; | ||||
|     stroke: #31a6a2; | ||||
|     stroke-width: 3; | ||||
| } | ||||
| 
 | ||||
| .line-failed { | ||||
|     fill: none; | ||||
|     stroke: #8c2627; | ||||
|     stroke-width: 3; | ||||
| } | ||||
| 
 | ||||
| .line-closed { | ||||
|     fill: none; | ||||
|     stroke: #62f24b; | ||||
|     stroke-width: 3; | ||||
| } | ||||
| 
 | ||||
| .overlay { | ||||
|     fill: none; | ||||
|     pointer-events: all; | ||||
| } | ||||
| 
 | ||||
| /* Style the dots by assigning a fill and stroke */ | ||||
| .dot { | ||||
|     fill: #ffab00; | ||||
|     stroke: #fff; | ||||
| } | ||||
| 
 | ||||
| .focus circle { | ||||
|     fill: none; | ||||
|     stroke: steelblue; | ||||
| } | ||||
							
								
								
									
										17
									
								
								web/angular/src/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/angular/src/test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| // This file is required by karma.conf.js and loads recursively all the .spec and framework files
 | ||||
| 
 | ||||
| import 'zone.js/dist/zone-testing'; | ||||
| import {getTestBed} from '@angular/core/testing'; | ||||
| import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; | ||||
| 
 | ||||
| declare const require: any; | ||||
| 
 | ||||
| // First, initialize the Angular testing environment.
 | ||||
| getTestBed().initTestEnvironment( | ||||
|     BrowserDynamicTestingModule, | ||||
|     platformBrowserDynamicTesting() | ||||
| ); | ||||
| // Then we find all the tests.
 | ||||
| const context = require.context('./', true, /\.spec\.ts$/); | ||||
| // And load the modules.
 | ||||
| context.keys().map(context); | ||||
							
								
								
									
										11
									
								
								web/angular/src/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/angular/src/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| { | ||||
|     "extends": "../tsconfig.json", | ||||
|     "compilerOptions": { | ||||
|         "outDir": "../out-tsc/app", | ||||
|         "types": [] | ||||
|     }, | ||||
|     "exclude": [ | ||||
|         "test.ts", | ||||
|         "**/*.spec.ts" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										18
									
								
								web/angular/src/tsconfig.spec.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/angular/src/tsconfig.spec.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| { | ||||
|     "extends": "../tsconfig.json", | ||||
|     "compilerOptions": { | ||||
|         "outDir": "../out-tsc/spec", | ||||
|         "types": [ | ||||
|             "jasmine", | ||||
|             "node" | ||||
|         ] | ||||
|     }, | ||||
|     "files": [ | ||||
|         "test.ts", | ||||
|         "polyfills.ts" | ||||
|     ], | ||||
|     "include": [ | ||||
|         "**/*.spec.ts", | ||||
|         "**/*.d.ts" | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										17
									
								
								web/angular/src/tslint.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								web/angular/src/tslint.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| { | ||||
|     "extends": "../tslint.json", | ||||
|     "rules": { | ||||
|         "directive-selector": [ | ||||
|             true, | ||||
|             "attribute", | ||||
|             "app", | ||||
|             "camelCase" | ||||
|         ], | ||||
|         "component-selector": [ | ||||
|             true, | ||||
|             "element", | ||||
|             "app", | ||||
|             "kebab-case" | ||||
|         ] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								web/angular/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								web/angular/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| { | ||||
|     "compileOnSave": false, | ||||
|     "compilerOptions": { | ||||
|         "baseUrl": "./", | ||||
|         "outDir": "./dist/out-tsc", | ||||
|         "sourceMap": true, | ||||
|         "declaration": false, | ||||
|         "module": "es2015", | ||||
|         "moduleResolution": "node", | ||||
|         "emitDecoratorMetadata": true, | ||||
|         "experimentalDecorators": true, | ||||
|         "importHelpers": true, | ||||
|         "target": "es5", | ||||
|         "typeRoots": [ | ||||
|             "node_modules/@types" | ||||
|         ], | ||||
|         "lib": [ | ||||
|             "es2018", | ||||
|             "dom" | ||||
|         ] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										131
									
								
								web/angular/tslint.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								web/angular/tslint.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| { | ||||
|     "rulesDirectory": [ | ||||
|         "codelyzer" | ||||
|     ], | ||||
|     "rules": { | ||||
|         "arrow-return-shorthand": true, | ||||
|         "callable-types": true, | ||||
|         "class-name": true, | ||||
|         "comment-format": [ | ||||
|             true, | ||||
|             "check-space" | ||||
|         ], | ||||
|         "curly": true, | ||||
|         "deprecation": { | ||||
|             "severity": "warn" | ||||
|         }, | ||||
|         "eofline": true, | ||||
|         "forin": true, | ||||
|         "import-blacklist": [ | ||||
|             true, | ||||
|             "rxjs/Rx" | ||||
|         ], | ||||
|         "import-spacing": true, | ||||
|         "indent": [ | ||||
|             true, | ||||
|             "spaces" | ||||
|         ], | ||||
|         "interface-over-type-literal": true, | ||||
|         "label-position": true, | ||||
|         "max-line-length": [ | ||||
|             true, | ||||
|             140 | ||||
|         ], | ||||
|         "member-access": false, | ||||
|         "member-ordering": [ | ||||
|             true, | ||||
|             { | ||||
|                 "order": [ | ||||
|                     "static-field", | ||||
|                     "instance-field", | ||||
|                     "static-method", | ||||
|                     "instance-method" | ||||
|                 ] | ||||
|             } | ||||
|         ], | ||||
|         "no-arg": true, | ||||
|         "no-bitwise": true, | ||||
|         "no-console": [ | ||||
|             true, | ||||
|             "debug", | ||||
|             "info", | ||||
|             "time", | ||||
|             "timeEnd", | ||||
|             "trace" | ||||
|         ], | ||||
|         "no-construct": true, | ||||
|         "no-debugger": true, | ||||
|         "no-duplicate-super": true, | ||||
|         "no-empty": false, | ||||
|         "no-empty-interface": true, | ||||
|         "no-eval": true, | ||||
|         "no-inferrable-types": [ | ||||
|             true, | ||||
|             "ignore-params" | ||||
|         ], | ||||
|         "no-misused-new": true, | ||||
|         "no-non-null-assertion": true, | ||||
|         "no-redundant-jsdoc": true, | ||||
|         "no-shadowed-variable": true, | ||||
|         "no-string-literal": false, | ||||
|         "no-string-throw": true, | ||||
|         "no-switch-case-fall-through": true, | ||||
|         "no-trailing-whitespace": true, | ||||
|         "no-unnecessary-initializer": true, | ||||
|         "no-unused-expression": true, | ||||
|         "no-use-before-declare": true, | ||||
|         "no-var-keyword": true, | ||||
|         "object-literal-sort-keys": false, | ||||
|         "one-line": [ | ||||
|             true, | ||||
|             "check-open-brace", | ||||
|             "check-catch", | ||||
|             "check-else", | ||||
|             "check-whitespace" | ||||
|         ], | ||||
|         "prefer-const": true, | ||||
|         "quotemark": [ | ||||
|             true, | ||||
|             "single" | ||||
|         ], | ||||
|         "radix": true, | ||||
|         "semicolon": [ | ||||
|             true, | ||||
|             "always" | ||||
|         ], | ||||
|         "triple-equals": [ | ||||
|             true, | ||||
|             "allow-null-check" | ||||
|         ], | ||||
|         "typedef-whitespace": [ | ||||
|             true, | ||||
|             { | ||||
|                 "call-signature": "nospace", | ||||
|                 "index-signature": "nospace", | ||||
|                 "parameter": "nospace", | ||||
|                 "property-declaration": "nospace", | ||||
|                 "variable-declaration": "nospace" | ||||
|             } | ||||
|         ], | ||||
|         "unified-signatures": true, | ||||
|         "variable-name": false, | ||||
|         "whitespace": [ | ||||
|             true, | ||||
|             "check-branch", | ||||
|             "check-decl", | ||||
|             "check-operator", | ||||
|             "check-separator", | ||||
|             "check-type" | ||||
|         ], | ||||
|         "no-output-on-prefix": true, | ||||
|         "use-input-property-decorator": true, | ||||
|         "use-output-property-decorator": true, | ||||
|         "use-host-property-decorator": true, | ||||
|         "no-input-rename": true, | ||||
|         "no-output-rename": true, | ||||
|         "use-life-cycle-interface": true, | ||||
|         "use-pipe-transform-interface": true, | ||||
|         "component-class-suffix": true, | ||||
|         "directive-class-suffix": true | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user