mirror of
https://github.com/simon987/task_tracker.git
synced 2025-04-19 18:16:45 +00:00
monitoring setup, project dashboard, account page
This commit is contained in:
parent
f577e76afa
commit
4ef4752c14
@ -67,10 +67,6 @@ func (api *WebAPI) Login(r *Request) {
|
|||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
sess.Set("manager", manager)
|
sess.Set("manager", manager)
|
||||||
|
|
||||||
logrus.Debug("SET")
|
|
||||||
logrus.Debug(sess.ID())
|
|
||||||
logrus.Debug(manager)
|
|
||||||
|
|
||||||
r.OkJson(LoginResponse{
|
r.OkJson(LoginResponse{
|
||||||
Manager: manager,
|
Manager: manager,
|
||||||
Ok: true,
|
Ok: true,
|
||||||
@ -135,8 +131,6 @@ func (api *WebAPI) AccountDetails(r *Request) {
|
|||||||
|
|
||||||
sess := api.Session.StartFasthttp(r.Ctx)
|
sess := api.Session.StartFasthttp(r.Ctx)
|
||||||
manager := sess.Get("manager")
|
manager := sess.Get("manager")
|
||||||
logrus.Debug("GET")
|
|
||||||
logrus.Debug(sess.ID())
|
|
||||||
|
|
||||||
if manager == nil {
|
if manager == nil {
|
||||||
r.OkJson(AccountDetails{
|
r.OkJson(AccountDetails{
|
||||||
|
18
api/main.go
18
api/main.go
@ -5,6 +5,7 @@ import (
|
|||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/buaazp/fasthttprouter"
|
"github.com/buaazp/fasthttprouter"
|
||||||
"github.com/kataras/go-sessions"
|
"github.com/kataras/go-sessions"
|
||||||
|
"github.com/robfig/cron"
|
||||||
"github.com/simon987/task_tracker/config"
|
"github.com/simon987/task_tracker/config"
|
||||||
"github.com/simon987/task_tracker/storage"
|
"github.com/simon987/task_tracker/storage"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
@ -16,6 +17,7 @@ type WebAPI struct {
|
|||||||
Database *storage.Database
|
Database *storage.Database
|
||||||
SessionConfig sessions.Config
|
SessionConfig sessions.Config
|
||||||
Session *sessions.Sessions
|
Session *sessions.Sessions
|
||||||
|
Cron *cron.Cron
|
||||||
}
|
}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
@ -32,10 +34,23 @@ func Index(r *Request) {
|
|||||||
r.OkJson(info)
|
r.OkJson(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *WebAPI) setupMonitoring() {
|
||||||
|
|
||||||
|
api.Cron = cron.New()
|
||||||
|
schedule := cron.Every(config.Cfg.MonitoringInterval)
|
||||||
|
api.Cron.Schedule(schedule, cron.FuncJob(api.Database.MakeProjectSnapshots))
|
||||||
|
api.Cron.Start()
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"every": config.Cfg.MonitoringInterval.String(),
|
||||||
|
}).Info("Started monitoring")
|
||||||
|
}
|
||||||
|
|
||||||
func New() *WebAPI {
|
func New() *WebAPI {
|
||||||
|
|
||||||
api := new(WebAPI)
|
api := new(WebAPI)
|
||||||
api.Database = &storage.Database{}
|
api.Database = &storage.Database{}
|
||||||
|
api.setupMonitoring()
|
||||||
|
|
||||||
api.router = &fasthttprouter.Router{}
|
api.router = &fasthttprouter.Router{}
|
||||||
|
|
||||||
@ -71,6 +86,9 @@ func New() *WebAPI {
|
|||||||
api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet))
|
api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet))
|
||||||
api.router.POST("/project/update/:id", LogRequestMiddleware(api.ProjectUpdate))
|
api.router.POST("/project/update/:id", LogRequestMiddleware(api.ProjectUpdate))
|
||||||
api.router.GET("/project/list", LogRequestMiddleware(api.ProjectGetAllProjects))
|
api.router.GET("/project/list", LogRequestMiddleware(api.ProjectGetAllProjects))
|
||||||
|
api.router.GET("/project/monitoring-between/:id", LogRequestMiddleware(api.GetSnapshotsBetween))
|
||||||
|
api.router.GET("/project/monitoring/:id", LogRequestMiddleware(api.GetNSnapshots))
|
||||||
|
api.router.GET("/project/assignees/:id", LogRequestMiddleware(api.ProjectGetAssigneeStats))
|
||||||
|
|
||||||
api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate))
|
api.router.POST("/task/create", LogRequestMiddleware(api.TaskCreate))
|
||||||
api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject))
|
api.router.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject))
|
||||||
|
52
api/monitoring.go
Normal file
52
api/monitoring.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/simon987/task_tracker/storage"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MonitoringSnapshotResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Snapshots *[]storage.ProjectMonitoringSnapshot `json:"snapshots,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *WebAPI) GetSnapshotsBetween(r *Request) {
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||||
|
from := r.Ctx.Request.URI().QueryArgs().GetUintOrZero("from")
|
||||||
|
to := r.Ctx.Request.URI().QueryArgs().GetUintOrZero("to")
|
||||||
|
if err != nil || id <= 0 || from <= 0 || to <= 0 || from >= math.MaxInt32 || to >= math.MaxInt32 {
|
||||||
|
r.Json(MonitoringSnapshotResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Invalid request",
|
||||||
|
}, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots := api.Database.GetMonitoringSnapshotsBetween(id, from, to)
|
||||||
|
r.OkJson(MonitoringSnapshotResponse{
|
||||||
|
Ok: true,
|
||||||
|
Snapshots: snapshots,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *WebAPI) GetNSnapshots(r *Request) {
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||||
|
count := r.Ctx.Request.URI().QueryArgs().GetUintOrZero("count")
|
||||||
|
if err != nil || id <= 0 || count <= 0 || count >= 1000 {
|
||||||
|
r.Json(MonitoringSnapshotResponse{
|
||||||
|
Ok: false,
|
||||||
|
Message: "Invalid request",
|
||||||
|
}, 400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots := api.Database.GetNMonitoringSnapshots(id, count)
|
||||||
|
r.OkJson(MonitoringSnapshotResponse{
|
||||||
|
Ok: true,
|
||||||
|
Snapshots: snapshots,
|
||||||
|
})
|
||||||
|
}
|
@ -49,6 +49,12 @@ type GetAllProjectsResponse struct {
|
|||||||
Projects *[]storage.Project `json:"projects,omitempty"`
|
Projects *[]storage.Project `json:"projects,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetAssigneeStatsResponse struct {
|
||||||
|
Ok bool `json:"ok"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Assignees *[]storage.AssignedTasks `json:"assignees"`
|
||||||
|
}
|
||||||
|
|
||||||
func (api *WebAPI) ProjectCreate(r *Request) {
|
func (api *WebAPI) ProjectCreate(r *Request) {
|
||||||
|
|
||||||
createReq := &CreateProjectRequest{}
|
createReq := &CreateProjectRequest{}
|
||||||
@ -203,3 +209,16 @@ func (api *WebAPI) ProjectGetAllProjects(r *Request) {
|
|||||||
Projects: projects,
|
Projects: projects,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *WebAPI) ProjectGetAssigneeStats(r *Request) {
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(r.Ctx.UserValue("id").(string), 10, 64)
|
||||||
|
handleErr(err, r) //todo handle invalid id
|
||||||
|
|
||||||
|
stats := api.Database.GetAssigneeStats(id, 16)
|
||||||
|
|
||||||
|
r.OkJson(GetAssigneeStatsResponse{
|
||||||
|
Ok: true,
|
||||||
|
Assignees: stats,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@ server:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable"
|
conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable"
|
||||||
log_levels: ["debug", "error"]
|
log_levels: ["debug", "error", "trace", "info", "warn"]
|
||||||
|
|
||||||
git:
|
git:
|
||||||
webhook_secret: "very_secret_secret"
|
webhook_secret: "very_secret_secret"
|
||||||
@ -19,3 +19,7 @@ log:
|
|||||||
session:
|
session:
|
||||||
cookie_name: "tt"
|
cookie_name: "tt"
|
||||||
expiration: "25m"
|
expiration: "25m"
|
||||||
|
|
||||||
|
monitoring:
|
||||||
|
snapshot_interval: "10s"
|
||||||
|
history_length: "3000h"
|
||||||
|
@ -16,6 +16,8 @@ var Cfg struct {
|
|||||||
DbLogLevels []logrus.Level
|
DbLogLevels []logrus.Level
|
||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
SessionCookieExpiration time.Duration
|
SessionCookieExpiration time.Duration
|
||||||
|
MonitoringInterval time.Duration
|
||||||
|
MonitoringHistory time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupConfig() {
|
func SetupConfig() {
|
||||||
@ -40,5 +42,14 @@ func SetupConfig() {
|
|||||||
}
|
}
|
||||||
Cfg.SessionCookieName = viper.GetString("session.cookie_name")
|
Cfg.SessionCookieName = viper.GetString("session.cookie_name")
|
||||||
Cfg.SessionCookieExpiration, err = time.ParseDuration(viper.GetString("session.expiration"))
|
Cfg.SessionCookieExpiration, err = time.ParseDuration(viper.GetString("session.expiration"))
|
||||||
|
Cfg.MonitoringInterval, err = time.ParseDuration(viper.GetString("monitoring.snapshot_interval"))
|
||||||
|
handleErr(err)
|
||||||
|
Cfg.MonitoringHistory, err = time.ParseDuration(viper.GetString("monitoring.history_length"))
|
||||||
|
handleErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,44 +1,102 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type ProjectMonitoringSnapshot struct {
|
type ProjectMonitoringSnapshot struct {
|
||||||
NewTaskCount int64
|
NewTaskCount int64 `json:"new_task_count"`
|
||||||
FailedTaskCount int64
|
FailedTaskCount int64 `json:"failed_task_count"`
|
||||||
ClosedTaskCount int64
|
ClosedTaskCount int64 `json:"closed_task_count"`
|
||||||
WorkerAccessCount int64
|
WorkerAccessCount int64 `json:"worker_access_count"`
|
||||||
TimeStamp int64
|
AwaitingVerificationCount int64 `json:"awaiting_verification_count"`
|
||||||
|
TimeStamp int64 `json:"time_stamp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) MakeProjectSnapshots() {
|
func (database *Database) MakeProjectSnapshots() {
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
db := database.getDB()
|
db := database.getDB()
|
||||||
|
|
||||||
_, err := db.Exec(`
|
_, err := db.Exec(`
|
||||||
INSERT INTO project_monitoring_snapshot
|
INSERT INTO project_monitoring_snapshot
|
||||||
(project, new_task_count, failed_task_count, closed_task_count, worker_access_count, timestamp)
|
(project, new_task_count, failed_task_count, closed_task_count, worker_access_count,
|
||||||
|
awaiting_verification_task_count, timestamp)
|
||||||
SELECT id,
|
SELECT id,
|
||||||
(SELECT COUNT(*) FROM task WHERE task.project = project.id AND status = 1),
|
(SELECT COUNT(*) FROM task
|
||||||
|
LEFT JOIN worker_verifies_task wvt on task.id = wvt.task
|
||||||
|
WHERE task.project = project.id AND status = 1 AND wvt.task IS NULL),
|
||||||
(SELECT COUNT(*) FROM task WHERE task.project = project.id AND status = 2),
|
(SELECT COUNT(*) FROM task WHERE task.project = project.id AND status = 2),
|
||||||
closed_task_count,
|
closed_task_count,
|
||||||
(SELECT COUNT(*) FROM worker_has_access_to_project wa WHERE wa.project = project.id),
|
(SELECT COUNT(*) FROM worker_has_access_to_project wa WHERE wa.project = project.id),
|
||||||
|
(SELECT COUNT(*) FROM worker_verifies_task INNER JOIN task t on worker_verifies_task.task = t.id
|
||||||
|
WHERE t.project = project.id),
|
||||||
extract(epoch from now() at time zone 'utc')
|
extract(epoch from now() at time zone 'utc')
|
||||||
FROM project`)
|
FROM project`)
|
||||||
handleErr(err)
|
handleErr(err)
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"took": time.Now().Sub(startTime),
|
||||||
|
}).Trace("Took monitoring snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (database *Database) GetMonitoringSnapshots(pid int64, from int64, to int64) (ss *[]ProjectMonitoringSnapshot) {
|
func (database *Database) GetMonitoringSnapshotsBetween(pid int64, from int, to int) (ss *[]ProjectMonitoringSnapshot) {
|
||||||
|
|
||||||
db := database.getDB()
|
db := database.getDB()
|
||||||
|
|
||||||
|
snapshots := make([]ProjectMonitoringSnapshot, 0)
|
||||||
|
|
||||||
rows, err := db.Query(`SELECT new_task_count, failed_task_count, closed_task_count,
|
rows, err := db.Query(`SELECT new_task_count, failed_task_count, closed_task_count,
|
||||||
worker_access_count, timestamp FROM project_monitoring_snapshot
|
worker_access_count, awaiting_verification_task_count, timestamp FROM project_monitoring_snapshot
|
||||||
WHERE project=$1 AND timestamp BETWEEN $2 AND $3`, pid, from, to)
|
WHERE project=$1 AND timestamp BETWEEN $2 AND $3 ORDER BY TIMESTAMP DESC `, pid, from, to)
|
||||||
handleErr(err)
|
handleErr(err)
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
|
|
||||||
s := ProjectMonitoringSnapshot{}
|
s := ProjectMonitoringSnapshot{}
|
||||||
err := rows.Scan(&s.NewTaskCount, &s.FailedTaskCount, &s.ClosedTaskCount, &s.WorkerAccessCount, &s.TimeStamp)
|
err := rows.Scan(&s.NewTaskCount, &s.FailedTaskCount, &s.ClosedTaskCount, &s.WorkerAccessCount,
|
||||||
|
&s.AwaitingVerificationCount, &s.TimeStamp)
|
||||||
handleErr(err)
|
handleErr(err)
|
||||||
|
|
||||||
|
snapshots = append(snapshots, s)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"snapshotCount": len(snapshots),
|
||||||
|
"projectId": pid,
|
||||||
|
"from": from,
|
||||||
|
"to": to,
|
||||||
|
}).Trace("Database.GetMonitoringSnapshotsBetween SELECT")
|
||||||
|
|
||||||
|
return &snapshots
|
||||||
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetNMonitoringSnapshots(pid int64, count int) (ss *[]ProjectMonitoringSnapshot) {
|
||||||
|
|
||||||
|
db := database.getDB()
|
||||||
|
|
||||||
|
snapshots := make([]ProjectMonitoringSnapshot, 0)
|
||||||
|
|
||||||
|
rows, err := db.Query(`SELECT new_task_count, failed_task_count, closed_task_count,
|
||||||
|
worker_access_count, awaiting_verification_task_count, timestamp FROM project_monitoring_snapshot
|
||||||
|
WHERE project=$1 ORDER BY TIMESTAMP DESC LIMIT $2`, pid, count)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
s := ProjectMonitoringSnapshot{}
|
||||||
|
err := rows.Scan(&s.NewTaskCount, &s.FailedTaskCount, &s.ClosedTaskCount, &s.WorkerAccessCount,
|
||||||
|
&s.AwaitingVerificationCount, &s.TimeStamp)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
snapshots = append(snapshots, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.WithFields(logrus.Fields{
|
||||||
|
"snapshotCount": len(snapshots),
|
||||||
|
"projectId": pid,
|
||||||
|
"count": count,
|
||||||
|
}).Trace("Database.GetNMonitoringSnapshots SELECT")
|
||||||
|
|
||||||
|
return &snapshots
|
||||||
}
|
}
|
||||||
|
@ -156,3 +156,31 @@ func (database Database) GetAllProjects() *[]Project {
|
|||||||
|
|
||||||
return &projects
|
return &projects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (database *Database) GetAssigneeStats(pid int64, count int64) *[]AssignedTasks {
|
||||||
|
|
||||||
|
db := database.getDB()
|
||||||
|
assignees := make([]AssignedTasks, 0)
|
||||||
|
|
||||||
|
rows, err := db.Query(`SELECT worker.alias, COUNT(*) as wc FROM TASK
|
||||||
|
LEFT JOIN worker ON TASK.assignee = worker.id WHERE project=$1
|
||||||
|
GROUP BY worker.id ORDER BY wc LIMIT $2`, pid, count)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
assignee := AssignedTasks{}
|
||||||
|
var assigneeAlias sql.NullString
|
||||||
|
err = rows.Scan(&assigneeAlias, &assignee.TaskCount)
|
||||||
|
handleErr(err)
|
||||||
|
|
||||||
|
if assigneeAlias.Valid {
|
||||||
|
assignee.Assignee = assigneeAlias.String
|
||||||
|
} else {
|
||||||
|
assignee.Assignee = "unassigned"
|
||||||
|
}
|
||||||
|
|
||||||
|
assignees = append(assignees, assignee)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &assignees
|
||||||
|
}
|
||||||
|
@ -47,7 +47,7 @@ func (database *Database) SaveTask(task *Task, project int64, hash64 int64) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).WithFields(logrus.Fields{
|
logrus.WithError(err).WithFields(logrus.Fields{
|
||||||
"task": task,
|
"task": task,
|
||||||
}).Warn("Database.saveTask INSERT task ERROR")
|
}).Trace("Database.saveTask INSERT task ERROR")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,7 @@ server:
|
|||||||
|
|
||||||
database:
|
database:
|
||||||
conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable"
|
conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable"
|
||||||
log_levels: ["debug", "error"]
|
log_levels: ["debug", "error", "trace", "info", "warn"]
|
||||||
# log_levels: ["debug", "error", "trace", "info", "warning"]
|
|
||||||
|
|
||||||
git:
|
git:
|
||||||
webhook_secret: "very_secret_secret"
|
webhook_secret: "very_secret_secret"
|
||||||
@ -17,3 +16,7 @@ log:
|
|||||||
session:
|
session:
|
||||||
cookie_name: "tt_test"
|
cookie_name: "tt_test"
|
||||||
expiration: "25m"
|
expiration: "25m"
|
||||||
|
|
||||||
|
monitoring:
|
||||||
|
snapshot_interval: "10h"
|
||||||
|
history_length: "10h"
|
||||||
|
@ -1,3 +1,29 @@
|
|||||||
<pre>
|
<div class="container">
|
||||||
{{authService.account | json}}
|
<mat-card class="mat-elevation-z8" *ngIf="account">
|
||||||
</pre>
|
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title>{{"account.title" | translate}}</mat-card-title>
|
||||||
|
<mat-card-subtitle>{{"account.subtitle" | translate}}</mat-card-subtitle>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<mat-card-content>
|
||||||
|
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item>
|
||||||
|
{{"account.username" | translate}}:
|
||||||
|
<pre>{{account.username}}</pre>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>{{"account.metadata" | translate}}</mat-expansion-panel-header>
|
||||||
|
<pre> {{account | json}}</pre>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
</mat-card-content>
|
||||||
|
|
||||||
|
<mat-card-actions>
|
||||||
|
|
||||||
|
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
@ -8,10 +8,13 @@ import {AuthService} from "../auth.service";
|
|||||||
})
|
})
|
||||||
export class AccountDetailsComponent implements OnInit {
|
export class AccountDetailsComponent implements OnInit {
|
||||||
|
|
||||||
|
account: Manager;
|
||||||
|
|
||||||
constructor(private authService: AuthService) {
|
constructor(private authService: AuthService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.account = this.authService.account;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getLogs() {
|
getLogs() {
|
||||||
return this.http.post(this.url + "/logs", "{\"level\":6, \"since\":1}", this.options);
|
return this.http.post(this.url + "/logs", "{\"level\":4, \"since\":1}", this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjects() {
|
getProjects() {
|
||||||
@ -49,5 +49,12 @@ export class ApiService {
|
|||||||
return this.http.get(this.url + "/account", this.options)
|
return this.http.get(this.url + "/account", this.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMonitoringSnapshots(count: number, project: number) {
|
||||||
|
return this.http.get(this.url + `/project/monitoring/${project}?count=${count}`, this.options)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssigneeStats(project: number) {
|
||||||
|
return this.http.get(this.url + `/project/assignees/${project}`, this.options)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatIconModule,
|
MatIconModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
MatListModule,
|
||||||
MatMenuModule,
|
MatMenuModule,
|
||||||
MatPaginatorIntl,
|
MatPaginatorIntl,
|
||||||
MatPaginatorModule,
|
MatPaginatorModule,
|
||||||
@ -98,7 +99,8 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
),
|
),
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatTabsModule
|
MatTabsModule,
|
||||||
|
MatListModule
|
||||||
|
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
50
web/angular/src/app/auth.service.ts
Normal file
50
web/angular/src/app/auth.service.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ApiService} from "./api.service";
|
||||||
|
import {Credentials} from "./models/credentials";
|
||||||
|
import {MessengerService} from "./messenger.service";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthService {
|
||||||
|
|
||||||
|
account: Manager;
|
||||||
|
|
||||||
|
constructor(private apiService: ApiService,
|
||||||
|
private messengerService: MessengerService,
|
||||||
|
private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public login(credentials: Credentials) {
|
||||||
|
return this.apiService.login(credentials)
|
||||||
|
.subscribe(
|
||||||
|
() => {
|
||||||
|
this.apiService.getAccountDetails()
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
this.account = data.manager;
|
||||||
|
this.router.navigateByUrl("/account");
|
||||||
|
})
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
console.log(error);
|
||||||
|
this.messengerService.show(error.error.message);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public register(credentials: Credentials) {
|
||||||
|
return this.apiService.register(credentials)
|
||||||
|
.subscribe(() =>
|
||||||
|
this.apiService.getAccountDetails()
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
this.account = data.manager;
|
||||||
|
this.router.navigateByUrl("/account");
|
||||||
|
}),
|
||||||
|
error => {
|
||||||
|
console.log(error);
|
||||||
|
this.messengerService.show(error.error.message);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -28,16 +28,7 @@ export class LoginComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
register() {
|
register() {
|
||||||
this.apiService.register(this.credentials)
|
this.authService.register(this.credentials)
|
||||||
.subscribe(
|
|
||||||
() => {
|
|
||||||
this.router.navigateByUrl("/account")
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
console.log(error);
|
|
||||||
this.messengerService.show(error.error.message);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
canCreate(): boolean {
|
canCreate(): boolean {
|
||||||
|
@ -10,3 +10,8 @@
|
|||||||
.mat-cell {
|
.mat-cell {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-wrapper mat-checkbox {
|
||||||
|
margin: 3px 5px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<mat-form-field>
|
<div class="checkbox-wrapper">
|
||||||
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
|
<mat-form-field style="margin-right: 10px">
|
||||||
</mat-form-field>
|
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-checkbox>{{"logs.fatal" | translate}}</mat-checkbox>
|
||||||
|
<mat-checkbox>{{"logs.panic" | translate}}</mat-checkbox>
|
||||||
|
<mat-checkbox>{{"logs.error" | translate}}</mat-checkbox>
|
||||||
|
<mat-checkbox>{{"logs.warn" | translate}}</mat-checkbox>
|
||||||
|
<mat-checkbox>{{"logs.info" | translate}}</mat-checkbox>
|
||||||
|
<mat-checkbox>{{"logs.debug" | translate}}</mat-checkbox>
|
||||||
|
</div>
|
||||||
<div class="mat-elevation-z8">
|
<div class="mat-elevation-z8">
|
||||||
|
|
||||||
<mat-table [dataSource]="data" matSort matSortActive="timestamp"
|
<mat-table [dataSource]="data" matSort matSortActive="timestamp"
|
||||||
matSortDirection="desc">
|
matSortDirection="desc">
|
||||||
|
|
||||||
<ng-container matColumnDef="level">
|
<ng-container matColumnDef="level">
|
||||||
<mat-header-cell style="flex: 0 0 6em" mat-sort-header
|
<mat-header-cell style="flex: 0 0 9em" mat-sort-header
|
||||||
*matHeaderCellDef>{{"logs.level" | translate}}</mat-header-cell>
|
*matHeaderCellDef>{{"logs.level" | translate}}</mat-header-cell>
|
||||||
<mat-cell style="flex: 0 0 6em" *matCellDef="let entry"> {{entry.level}} </mat-cell>
|
<mat-cell style="flex: 0 0 8em"
|
||||||
|
*matCellDef="let entry"> {{("logs." + entry.level) | translate}} </mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="timestamp">
|
<ng-container matColumnDef="timestamp">
|
||||||
<mat-header-cell style="flex: 0 0 21em" mat-sort-header
|
<mat-header-cell style="flex: 0 0 15em" mat-sort-header
|
||||||
*matHeaderCellDef>{{"logs.time" | translate}}</mat-header-cell>
|
*matHeaderCellDef>{{"logs.time" | translate}}</mat-header-cell>
|
||||||
<mat-cell style="flex: 0 0 17em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell>
|
<mat-cell style="flex: 0 0 12em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="message">
|
<ng-container matColumnDef="message">
|
||||||
<mat-header-cell mat-sort-header *matHeaderCellDef>{{"logs.message" | translate}}</mat-header-cell>
|
<mat-header-cell mat-sort-header *matHeaderCellDef>{{"logs.message" | translate}}</mat-header-cell>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {Component, OnInit, ViewChild} from '@angular/core';
|
import {Component, OnInit, ViewChild} from '@angular/core';
|
||||||
import {ApiService} from "../api.service";
|
import {ApiService} from "../api.service";
|
||||||
import {LogEntry} from "../models/logentry";
|
import {getLogLevel, LogEntry} from "../models/logentry";
|
||||||
|
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
@ -44,9 +44,9 @@ export class LogsComponent implements OnInit {
|
|||||||
this.data.data = _.map(data["logs"], (entry) => {
|
this.data.data = _.map(data["logs"], (entry) => {
|
||||||
return <LogEntry>{
|
return <LogEntry>{
|
||||||
message: entry.message,
|
message: entry.message,
|
||||||
timestamp: moment.unix(entry.timestamp).toISOString(),
|
timestamp: moment.unix(entry.timestamp).format("YYYY-MM-DD HH:mm:ss"),
|
||||||
data: JSON.stringify(JSON.parse(entry.data), null, 2),
|
data: JSON.stringify(JSON.parse(entry.data), null, 2),
|
||||||
level: entry.level
|
level: getLogLevel(entry.level),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,3 +4,32 @@ export interface LogEntry {
|
|||||||
data: any,
|
data: any,
|
||||||
timestamp: string,
|
timestamp: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
FATAL = "fatal",
|
||||||
|
PANIC = "panic",
|
||||||
|
ERROR = "error",
|
||||||
|
WARN = "warn",
|
||||||
|
INFO = "info",
|
||||||
|
DEBUG = "debug",
|
||||||
|
TRACE = "trace",
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogLevel(level: number): string {
|
||||||
|
switch (level) {
|
||||||
|
case 1:
|
||||||
|
return LogLevel.FATAL;
|
||||||
|
case 2:
|
||||||
|
return LogLevel.PANIC;
|
||||||
|
case 3:
|
||||||
|
return LogLevel.ERROR;
|
||||||
|
case 4:
|
||||||
|
return LogLevel.WARN;
|
||||||
|
case 5:
|
||||||
|
return LogLevel.INFO;
|
||||||
|
case 6:
|
||||||
|
return LogLevel.DEBUG;
|
||||||
|
case 7:
|
||||||
|
return LogLevel.TRACE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
12
web/angular/src/app/models/monitoring.ts
Normal file
12
web/angular/src/app/models/monitoring.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export interface MonitoringSnapshot {
|
||||||
|
new_task_count: number
|
||||||
|
failed_task_count: number
|
||||||
|
closed_task_count: number
|
||||||
|
awaiting_verification_count: number
|
||||||
|
time_stamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignedTasks {
|
||||||
|
assignee: string
|
||||||
|
task_count: number
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
#timeline-wrapper {
|
#timeline-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#status-pie-wrapper {
|
#status-pie-wrapper {
|
||||||
@ -12,3 +13,28 @@
|
|||||||
height: 50%;
|
height: 50%;
|
||||||
width: 400px;
|
width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#no-tasks {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#side-charts {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
#side-charts {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
#timeline-wrapper {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#small-screen-stats {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,6 +3,13 @@
|
|||||||
<mat-card-title *ngIf="project">{{"dashboard.title" | translate}} "{{project.name}}"</mat-card-title>
|
<mat-card-title *ngIf="project">{{"dashboard.title" | translate}} "{{project.name}}"</mat-card-title>
|
||||||
<mat-card-content style="padding: 2em 0 1em">
|
<mat-card-content style="padding: 2em 0 1em">
|
||||||
|
|
||||||
|
<button mat-raised-button style="float: right"
|
||||||
|
[title]="'dashboard.refresh' | translate"
|
||||||
|
(click)="refresh()"
|
||||||
|
>
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<p *ngIf="project">
|
<p *ngIf="project">
|
||||||
{{"project.git_repo" | translate}}:
|
{{"project.git_repo" | translate}}:
|
||||||
<a target="_blank" [href]="project['clone_url']">{{project.git_repo}}</a>
|
<a target="_blank" [href]="project['clone_url']">{{project.git_repo}}</a>
|
||||||
@ -10,12 +17,13 @@
|
|||||||
<p>{{"project.motd" | translate}}:</p>
|
<p>{{"project.motd" | translate}}:</p>
|
||||||
<pre *ngIf="project">{{project.motd}}</pre>
|
<pre *ngIf="project">{{project.motd}}</pre>
|
||||||
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; justify-content: center">
|
<div style="display: flex; align-items: center; justify-content: center">
|
||||||
<div id="timeline-wrapper">
|
<div id="timeline-wrapper">
|
||||||
<canvas id="timeline"></canvas>
|
<canvas id="timeline"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div [class.hidden]="noTasks" id="side-charts">
|
||||||
<div id="status-pie-wrapper">
|
<div id="status-pie-wrapper">
|
||||||
<canvas id="status-pie"></canvas>
|
<canvas id="status-pie"></canvas>
|
||||||
</div>
|
</div>
|
||||||
@ -23,6 +31,18 @@
|
|||||||
<canvas id="assignees-pie"></canvas>
|
<canvas id="assignees-pie"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="noTasks" id="no-tasks">
|
||||||
|
<mat-icon>priority_high</mat-icon>
|
||||||
|
<p>{{"dashboard.empty" | translate}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="small-screen-stats">
|
||||||
|
<p>Small screen stats</p>
|
||||||
|
<p>Latest monitoring snapshot:</p>
|
||||||
|
<pre>{{ lastSnapshot | json }}</pre>
|
||||||
|
<p>Assignees</p>
|
||||||
|
<pre>{{ assignees | json }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-expansion-panel *ngIf="project" style="margin-top: 1em">
|
<mat-expansion-panel *ngIf="project" style="margin-top: 1em">
|
||||||
|
@ -3,7 +3,8 @@ import {ApiService} from "../api.service";
|
|||||||
import {Project} from "../models/project";
|
import {Project} from "../models/project";
|
||||||
import {ActivatedRoute} from "@angular/router";
|
import {ActivatedRoute} from "@angular/router";
|
||||||
|
|
||||||
import {Chart, ChartData, Point} from "chart.js";
|
import {Chart} from "chart.js";
|
||||||
|
import {AssignedTasks, MonitoringSnapshot} from "../models/monitoring";
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -15,22 +16,30 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
|
|
||||||
private projectId;
|
private projectId;
|
||||||
project: Project;
|
project: Project;
|
||||||
|
noTasks = false;
|
||||||
|
|
||||||
private timeline: Chart;
|
private timeline: Chart;
|
||||||
private statusPie: Chart;
|
private statusPie: Chart;
|
||||||
private assigneesPir: Chart;
|
private assigneesPie: Chart;
|
||||||
|
|
||||||
|
|
||||||
private colors = {
|
private colors = {
|
||||||
new: "#76FF03",
|
new: "#76FF03",
|
||||||
failed: "#FF3D00",
|
failed: "#FF3D00",
|
||||||
closed: "#E0E0E0",
|
closed: "#E0E0E0",
|
||||||
awaiting: "#FFB74D"
|
awaiting: "#FFB74D",
|
||||||
|
random: [
|
||||||
|
"#3D5AFE", "#2979FF", "#2196F3",
|
||||||
|
"#7C4DFF", "#673AB7", "#7C4DFF",
|
||||||
|
"#FFC400", "#FFD740", "#FFC107",
|
||||||
|
"#FF3D00", "#FF6E40", "#FF5722",
|
||||||
|
"#76FF03", "#B2FF59", "#8BC34A"
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
tmpLabels = [];
|
snapshots: MonitoringSnapshot[] = [];
|
||||||
tmpNew = [];
|
lastSnapshot: MonitoringSnapshot;
|
||||||
tmpFailed = [];
|
assignees: AssignedTasks[];
|
||||||
tmpClosed = [];
|
|
||||||
tmpAwaiting = [];
|
|
||||||
|
|
||||||
constructor(private apiService: ApiService, private route: ActivatedRoute) {
|
constructor(private apiService: ApiService, private route: ActivatedRoute) {
|
||||||
}
|
}
|
||||||
@ -42,19 +51,110 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
this.getProject();
|
this.getProject();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
let n = 40;
|
public refresh() {
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
this.tmpLabels.push((1549501926 + 600 * i) * 1000);
|
|
||||||
this.tmpNew.push(Math.ceil(Math.random() * 30))
|
|
||||||
this.tmpClosed.push(Math.ceil(Math.random() * 100))
|
|
||||||
this.tmpFailed.push(Math.ceil(Math.random() * 13))
|
|
||||||
this.tmpAwaiting.push(Math.ceil(Math.random() * 40))
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setupTimeline();
|
this.apiService.getMonitoringSnapshots(60, this.projectId)
|
||||||
this.setupStatusPie();
|
.subscribe((data: any) => {
|
||||||
this.setupAssigneesPie();
|
this.snapshots = data.snapshots;
|
||||||
|
this.lastSnapshot = this.snapshots ? this.snapshots.sort((a, b) => {
|
||||||
|
return b.time_stamp - a.time_stamp
|
||||||
|
})[0] : null;
|
||||||
|
|
||||||
|
if (this.lastSnapshot == null || (this.lastSnapshot.awaiting_verification_count == 0 &&
|
||||||
|
this.lastSnapshot.closed_task_count == 0 &&
|
||||||
|
this.lastSnapshot.new_task_count == 0 &&
|
||||||
|
this.lastSnapshot.failed_task_count == 0)) {
|
||||||
|
this.noTasks = true;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.noTasks = false;
|
||||||
|
|
||||||
|
this.timeline.data.labels = this.snapshots.map(s => s.time_stamp as any);
|
||||||
|
this.timeline.data.datasets = this.makeTimelineDataset(this.snapshots);
|
||||||
|
this.timeline.update();
|
||||||
|
this.statusPie.data.datasets = [
|
||||||
|
{
|
||||||
|
label: "Task status",
|
||||||
|
data: [
|
||||||
|
this.lastSnapshot.new_task_count,
|
||||||
|
this.lastSnapshot.failed_task_count,
|
||||||
|
this.lastSnapshot.closed_task_count,
|
||||||
|
this.lastSnapshot.awaiting_verification_count,
|
||||||
|
],
|
||||||
|
backgroundColor: [
|
||||||
|
this.colors.new,
|
||||||
|
this.colors.failed,
|
||||||
|
this.colors.closed,
|
||||||
|
this.colors.awaiting
|
||||||
|
],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.statusPie.update();
|
||||||
|
|
||||||
|
this.apiService.getAssigneeStats(this.projectId)
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
this.assignees = data.assignees;
|
||||||
|
let colors = this.assignees.map(() => {
|
||||||
|
return this.colors.random[Math.floor(Math.random() * this.colors.random.length)]
|
||||||
|
});
|
||||||
|
this.assigneesPie.data.labels = this.assignees.map(x => x.assignee);
|
||||||
|
this.assigneesPie.data.datasets = [
|
||||||
|
{
|
||||||
|
label: "Task status",
|
||||||
|
data: this.assignees.map(x => x.task_count),
|
||||||
|
backgroundColor: colors,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.assigneesPie.update();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private makeTimelineDataset(snapshots: MonitoringSnapshot[]) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: "New",
|
||||||
|
type: "line",
|
||||||
|
fill: false,
|
||||||
|
borderColor: this.colors.new,
|
||||||
|
backgroundColor: this.colors.new,
|
||||||
|
data: snapshots.map(s => s.new_task_count),
|
||||||
|
pointRadius: 0,
|
||||||
|
lineTension: 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Failed",
|
||||||
|
type: "line",
|
||||||
|
fill: false,
|
||||||
|
borderColor: this.colors.failed,
|
||||||
|
backgroundColor: this.colors.failed,
|
||||||
|
data: snapshots.map(s => s.failed_task_count),
|
||||||
|
pointRadius: 0,
|
||||||
|
lineTension: 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Closed",
|
||||||
|
type: "line",
|
||||||
|
fill: false,
|
||||||
|
borderColor: this.colors.closed,
|
||||||
|
backgroundColor: this.colors.closed,
|
||||||
|
pointRadius: 0,
|
||||||
|
data: snapshots.map(s => s.closed_task_count),
|
||||||
|
lineTension: 0.2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Awaiting verification",
|
||||||
|
type: "line",
|
||||||
|
fill: false,
|
||||||
|
borderColor: this.colors.awaiting,
|
||||||
|
backgroundColor: this.colors.awaiting,
|
||||||
|
data: snapshots.map(s => s.awaiting_verification_count),
|
||||||
|
pointRadius: 0,
|
||||||
|
lineTension: 0.2,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupTimeline() {
|
private setupTimeline() {
|
||||||
@ -64,49 +164,8 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
this.timeline = new Chart(ctx, {
|
this.timeline = new Chart(ctx, {
|
||||||
type: "bar",
|
type: "bar",
|
||||||
data: {
|
data: {
|
||||||
labels: this.tmpLabels,
|
labels: this.snapshots.map(s => s.time_stamp as any),
|
||||||
datasets: [
|
datasets: this.makeTimelineDataset(this.snapshots),
|
||||||
{
|
|
||||||
label: "New",
|
|
||||||
type: "line",
|
|
||||||
fill: false,
|
|
||||||
borderColor: this.colors.new,
|
|
||||||
backgroundColor: this.colors.new,
|
|
||||||
data: this.tmpNew,
|
|
||||||
pointRadius: 0,
|
|
||||||
lineTension: 0.2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Failed",
|
|
||||||
type: "line",
|
|
||||||
fill: false,
|
|
||||||
borderColor: this.colors.failed,
|
|
||||||
backgroundColor: this.colors.failed,
|
|
||||||
data: this.tmpFailed,
|
|
||||||
pointRadius: 0,
|
|
||||||
lineTension: 0.2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Closed",
|
|
||||||
type: "line",
|
|
||||||
fill: false,
|
|
||||||
borderColor: this.colors.closed,
|
|
||||||
backgroundColor: this.colors.closed,
|
|
||||||
pointRadius: 0,
|
|
||||||
data: this.tmpClosed,
|
|
||||||
lineTension: 0.2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Awaiting verification",
|
|
||||||
type: "line",
|
|
||||||
fill: false,
|
|
||||||
borderColor: this.colors.awaiting,
|
|
||||||
backgroundColor: this.colors.awaiting,
|
|
||||||
data: this.tmpAwaiting,
|
|
||||||
pointRadius: 0,
|
|
||||||
lineTension: 0.2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
title: {
|
title: {
|
||||||
@ -124,10 +183,6 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
ticks: {
|
ticks: {
|
||||||
source: "auto"
|
source: "auto"
|
||||||
},
|
},
|
||||||
time: {
|
|
||||||
unit: "minute",
|
|
||||||
unitStepSize: 10,
|
|
||||||
}
|
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
tooltips: {
|
tooltips: {
|
||||||
@ -136,12 +191,20 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
mode: "index",
|
mode: "index",
|
||||||
position: "nearest",
|
position: "nearest",
|
||||||
},
|
},
|
||||||
|
responsive: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupStatusPie() {
|
private setupStatusPie() {
|
||||||
|
|
||||||
|
if (this.lastSnapshot == null || (this.lastSnapshot.awaiting_verification_count == 0 &&
|
||||||
|
this.lastSnapshot.closed_task_count == 0 &&
|
||||||
|
this.lastSnapshot.new_task_count == 0 &&
|
||||||
|
this.lastSnapshot.failed_task_count == 0)) {
|
||||||
|
this.noTasks = true;
|
||||||
|
}
|
||||||
|
|
||||||
let elem = document.getElementById("status-pie") as any;
|
let elem = document.getElementById("status-pie") as any;
|
||||||
let ctx = elem.getContext("2d");
|
let ctx = elem.getContext("2d");
|
||||||
|
|
||||||
@ -158,10 +221,10 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
{
|
{
|
||||||
label: "Task status",
|
label: "Task status",
|
||||||
data: [
|
data: [
|
||||||
10,
|
this.lastSnapshot.new_task_count,
|
||||||
24,
|
this.lastSnapshot.failed_task_count,
|
||||||
301,
|
this.lastSnapshot.closed_task_count,
|
||||||
90,
|
this.lastSnapshot.awaiting_verification_count,
|
||||||
],
|
],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
this.colors.new,
|
this.colors.new,
|
||||||
@ -186,7 +249,7 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
animation: {
|
animation: {
|
||||||
animateScale: true,
|
animateScale: true,
|
||||||
animateRotate: true
|
animateRotate: true
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -196,30 +259,19 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
let elem = document.getElementById("assignees-pie") as any;
|
let elem = document.getElementById("assignees-pie") as any;
|
||||||
let ctx = elem.getContext("2d");
|
let ctx = elem.getContext("2d");
|
||||||
|
|
||||||
this.statusPie = new Chart(ctx, {
|
let colors = this.assignees.map(() => {
|
||||||
|
return this.colors.random[Math.floor(Math.random() * this.colors.random.length)]
|
||||||
|
});
|
||||||
|
|
||||||
|
this.assigneesPie = new Chart(ctx, {
|
||||||
type: "doughnut",
|
type: "doughnut",
|
||||||
data: {
|
data: {
|
||||||
labels: [
|
labels: this.assignees.map(x => x.assignee),
|
||||||
"marc",
|
|
||||||
"simon",
|
|
||||||
"bernie",
|
|
||||||
"natasha",
|
|
||||||
],
|
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: "Task status",
|
label: "Task status",
|
||||||
data: [
|
data: this.assignees.map(x => x.task_count),
|
||||||
10,
|
backgroundColor: colors,
|
||||||
24,
|
|
||||||
1,
|
|
||||||
23,
|
|
||||||
],
|
|
||||||
backgroundColor: [
|
|
||||||
this.colors.new,
|
|
||||||
this.colors.failed,
|
|
||||||
this.colors.closed,
|
|
||||||
this.colors.awaiting
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -237,23 +289,35 @@ export class ProjectDashboardComponent implements OnInit {
|
|||||||
animation: {
|
animation: {
|
||||||
animateScale: true,
|
animateScale: true,
|
||||||
animateRotate: true
|
animateRotate: true
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProject() {
|
private getProject() {
|
||||||
this.apiService.getProject(this.projectId).subscribe(data => {
|
this.apiService.getProject(this.projectId).subscribe((data: any) => {
|
||||||
this.project = <Project>{
|
this.project = data.project;
|
||||||
id: data["project"]["id"],
|
|
||||||
name: data["project"]["name"],
|
this.apiService.getMonitoringSnapshots(60, this.projectId)
|
||||||
clone_url: data["project"]["clone_url"],
|
.subscribe((data: any) => {
|
||||||
git_repo: data["project"]["git_repo"],
|
this.snapshots = data.snapshots;
|
||||||
motd: data["project"]["motd"],
|
this.lastSnapshot = this.snapshots ? this.snapshots.sort((a, b) => {
|
||||||
priority: data["project"]["priority"],
|
return b.time_stamp - a.time_stamp
|
||||||
version: data["project"]["version"],
|
})[0] : null;
|
||||||
public: data["project"]["public"],
|
|
||||||
}
|
this.setupTimeline();
|
||||||
|
this.setupStatusPie();
|
||||||
|
|
||||||
|
if (!this.snapshots) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiService.getAssigneeStats(this.projectId)
|
||||||
|
.subscribe((data: any) => {
|
||||||
|
this.assignees = data.assignees;
|
||||||
|
this.setupAssigneesPie();
|
||||||
|
});
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
"time": "Time",
|
"time": "Time (UTC)",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"data": "Details",
|
"data": "Details",
|
||||||
@ -18,7 +18,14 @@
|
|||||||
"of": "of",
|
"of": "of",
|
||||||
"items_per_page": "Items per page",
|
"items_per_page": "Items per page",
|
||||||
"next_page": "Next page",
|
"next_page": "Next page",
|
||||||
"prev_page": "Previous page"
|
"prev_page": "Previous page",
|
||||||
|
"fatal": "Fatal",
|
||||||
|
"panic": "Panic",
|
||||||
|
"error": "Error",
|
||||||
|
"warn": "Warning",
|
||||||
|
"info": "Info",
|
||||||
|
"debug": "Debug",
|
||||||
|
"trace": "Trace"
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
@ -47,11 +54,13 @@
|
|||||||
"create": "Create",
|
"create": "Create",
|
||||||
"git_repo": "Git repository name",
|
"git_repo": "Git repository name",
|
||||||
"motd": "Message of the day",
|
"motd": "Message of the day",
|
||||||
"update": "Update"
|
"update": "Edit"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard for",
|
"title": "Dashboard for",
|
||||||
"metadata": "Project metadata"
|
"metadata": "Project metadata",
|
||||||
|
"empty": "No tasks",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
@ -64,5 +73,11 @@
|
|||||||
"create_account": {
|
"create_account": {
|
||||||
"title": "Register",
|
"title": "Register",
|
||||||
"create": "Create account"
|
"create": "Create account"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"metadata": "Account metadata",
|
||||||
|
"title": "Account details",
|
||||||
|
"subtitle": "toto: subtitle",
|
||||||
|
"username": "Username"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"filter": "Filtrer",
|
"filter": "Filtrer",
|
||||||
"time": "Date",
|
"time": "Date (UTC)",
|
||||||
"level": "Niveau",
|
"level": "Niveau",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
"data": "Details",
|
"data": "Details",
|
||||||
@ -18,7 +18,14 @@
|
|||||||
"of": "de",
|
"of": "de",
|
||||||
"items_per_page": "Items par page",
|
"items_per_page": "Items par page",
|
||||||
"next_page": "Page suivante",
|
"next_page": "Page suivante",
|
||||||
"prev_page": "Page précédante"
|
"prev_page": "Page précédante",
|
||||||
|
"fatal": "Fatal",
|
||||||
|
"panic": "Panique",
|
||||||
|
"error": "Erreur",
|
||||||
|
"warn": "Avertissement",
|
||||||
|
"info": "Information",
|
||||||
|
"debug": "Débugage",
|
||||||
|
"trace": "Trace"
|
||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"projects": "Projets",
|
"projects": "Projets",
|
||||||
@ -52,7 +59,9 @@
|
|||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Tableau de bord pour ",
|
"title": "Tableau de bord pour ",
|
||||||
"metadata": "Métadonnés du projet"
|
"metadata": "Métadonnés du projet",
|
||||||
|
"empty": "Aucune tâche",
|
||||||
|
"refresh": "Rafraîchir"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Ouvrir un session",
|
"title": "Ouvrir un session",
|
||||||
@ -65,6 +74,12 @@
|
|||||||
"create_account": {
|
"create_account": {
|
||||||
"title": "Créer un compte",
|
"title": "Créer un compte",
|
||||||
"create": "Créer un compte"
|
"create": "Créer un compte"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"metadata": "Métadonnés du compte",
|
||||||
|
"title": "Détails du compte",
|
||||||
|
"subtitle": "toto: sous-titre",
|
||||||
|
"username": "Nom d'utilisateur"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<base href="/">
|
<base href="/">
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -78,3 +78,11 @@ body {
|
|||||||
.mat-tab-body {
|
.mat-tab-body {
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: #616161;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user