diff --git a/api/auth.go b/api/auth.go
index d70aa81..931712b 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -67,10 +67,6 @@ func (api *WebAPI) Login(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
sess.Set("manager", manager)
- logrus.Debug("SET")
- logrus.Debug(sess.ID())
- logrus.Debug(manager)
-
r.OkJson(LoginResponse{
Manager: manager,
Ok: true,
@@ -135,8 +131,6 @@ func (api *WebAPI) AccountDetails(r *Request) {
sess := api.Session.StartFasthttp(r.Ctx)
manager := sess.Get("manager")
- logrus.Debug("GET")
- logrus.Debug(sess.ID())
if manager == nil {
r.OkJson(AccountDetails{
diff --git a/api/main.go b/api/main.go
index f5bf5fb..4b9d9e4 100644
--- a/api/main.go
+++ b/api/main.go
@@ -5,6 +5,7 @@ import (
"github.com/Sirupsen/logrus"
"github.com/buaazp/fasthttprouter"
"github.com/kataras/go-sessions"
+ "github.com/robfig/cron"
"github.com/simon987/task_tracker/config"
"github.com/simon987/task_tracker/storage"
"github.com/valyala/fasthttp"
@@ -16,6 +17,7 @@ type WebAPI struct {
Database *storage.Database
SessionConfig sessions.Config
Session *sessions.Sessions
+ Cron *cron.Cron
}
type Info struct {
@@ -32,10 +34,23 @@ func Index(r *Request) {
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 {
api := new(WebAPI)
api.Database = &storage.Database{}
+ api.setupMonitoring()
api.router = &fasthttprouter.Router{}
@@ -71,6 +86,9 @@ func New() *WebAPI {
api.router.GET("/project/get/:id", LogRequestMiddleware(api.ProjectGet))
api.router.POST("/project/update/:id", LogRequestMiddleware(api.ProjectUpdate))
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.GET("/task/get/:project", LogRequestMiddleware(api.TaskGetFromProject))
diff --git a/api/monitoring.go b/api/monitoring.go
new file mode 100644
index 0000000..05d5db7
--- /dev/null
+++ b/api/monitoring.go
@@ -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,
+ })
+}
diff --git a/api/project.go b/api/project.go
index df6bfd2..1294603 100644
--- a/api/project.go
+++ b/api/project.go
@@ -49,6 +49,12 @@ type GetAllProjectsResponse struct {
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) {
createReq := &CreateProjectRequest{}
@@ -203,3 +209,16 @@ func (api *WebAPI) ProjectGetAllProjects(r *Request) {
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,
+ })
+}
diff --git a/config.yml b/config.yml
index a5de15b..c8cbd16 100755
--- a/config.yml
+++ b/config.yml
@@ -3,7 +3,7 @@ server:
database:
conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable"
- log_levels: ["debug", "error"]
+ log_levels: ["debug", "error", "trace", "info", "warn"]
git:
webhook_secret: "very_secret_secret"
@@ -19,3 +19,7 @@ log:
session:
cookie_name: "tt"
expiration: "25m"
+
+monitoring:
+ snapshot_interval: "10s"
+ history_length: "3000h"
diff --git a/config/config.go b/config/config.go
index 3bf4d69..e072d64 100644
--- a/config/config.go
+++ b/config/config.go
@@ -16,6 +16,8 @@ var Cfg struct {
DbLogLevels []logrus.Level
SessionCookieName string
SessionCookieExpiration time.Duration
+ MonitoringInterval time.Duration
+ MonitoringHistory time.Duration
}
func SetupConfig() {
@@ -40,5 +42,14 @@ func SetupConfig() {
}
Cfg.SessionCookieName = viper.GetString("session.cookie_name")
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)
+ }
}
diff --git a/storage/monitoring.go b/storage/monitoring.go
index d3278b0..50b0eb8 100644
--- a/storage/monitoring.go
+++ b/storage/monitoring.go
@@ -1,44 +1,102 @@
package storage
+import (
+ "github.com/Sirupsen/logrus"
+ "time"
+)
+
type ProjectMonitoringSnapshot struct {
- NewTaskCount int64
- FailedTaskCount int64
- ClosedTaskCount int64
- WorkerAccessCount int64
- TimeStamp int64
+ NewTaskCount int64 `json:"new_task_count"`
+ FailedTaskCount int64 `json:"failed_task_count"`
+ ClosedTaskCount int64 `json:"closed_task_count"`
+ WorkerAccessCount int64 `json:"worker_access_count"`
+ AwaitingVerificationCount int64 `json:"awaiting_verification_count"`
+ TimeStamp int64 `json:"time_stamp"`
}
func (database *Database) MakeProjectSnapshots() {
+ startTime := time.Now()
db := database.getDB()
_, err := db.Exec(`
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 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),
closed_task_count,
(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')
FROM project`)
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()
+ snapshots := make([]ProjectMonitoringSnapshot, 0)
+
rows, err := db.Query(`SELECT new_task_count, failed_task_count, closed_task_count,
- worker_access_count, timestamp FROM project_monitoring_snapshot
- WHERE project=$1 AND timestamp BETWEEN $2 AND $3`, pid, from, to)
+ worker_access_count, awaiting_verification_task_count, timestamp FROM project_monitoring_snapshot
+ WHERE project=$1 AND timestamp BETWEEN $2 AND $3 ORDER BY TIMESTAMP DESC `, pid, from, to)
handleErr(err)
for rows.Next() {
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)
+
+ 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
}
diff --git a/storage/project.go b/storage/project.go
index a8427c7..42299cb 100644
--- a/storage/project.go
+++ b/storage/project.go
@@ -156,3 +156,31 @@ func (database Database) GetAllProjects() *[]Project {
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
+}
diff --git a/storage/task.go b/storage/task.go
index 9ed090b..29f66e3 100644
--- a/storage/task.go
+++ b/storage/task.go
@@ -47,7 +47,7 @@ func (database *Database) SaveTask(task *Task, project int64, hash64 int64) erro
if err != nil {
logrus.WithError(err).WithFields(logrus.Fields{
"task": task,
- }).Warn("Database.saveTask INSERT task ERROR")
+ }).Trace("Database.saveTask INSERT task ERROR")
return err
}
diff --git a/test/config.yml b/test/config.yml
index 0324e87..c5c0edc 100644
--- a/test/config.yml
+++ b/test/config.yml
@@ -3,8 +3,7 @@ server:
database:
conn_str: "user=task_tracker dbname=task_tracker_test sslmode=disable"
- log_levels: ["debug", "error"]
-# log_levels: ["debug", "error", "trace", "info", "warning"]
+ log_levels: ["debug", "error", "trace", "info", "warn"]
git:
webhook_secret: "very_secret_secret"
@@ -16,4 +15,8 @@ log:
session:
cookie_name: "tt_test"
- expiration: "25m"
\ No newline at end of file
+ expiration: "25m"
+
+monitoring:
+ snapshot_interval: "10h"
+ history_length: "10h"
diff --git a/web/angular/src/app/account-details/account-details.component.html b/web/angular/src/app/account-details/account-details.component.html
index 342f05c..bbca006 100644
--- a/web/angular/src/app/account-details/account-details.component.html
+++ b/web/angular/src/app/account-details/account-details.component.html
@@ -1,3 +1,29 @@
-
- {{authService.account | json}}
-
+
+
+
+
+ {{"account.title" | translate}}
+ {{"account.subtitle" | translate}}
+
+
+
+
+
+
+ {{"account.username" | translate}}:
+ {{account.username}}
+
+
+
+
+ {{"account.metadata" | translate}}
+ {{account | json}}
+
+
+
+
+
+
+
+
+
diff --git a/web/angular/src/app/account-details/account-details.component.ts b/web/angular/src/app/account-details/account-details.component.ts
index 0f9ef4e..1c7f8d5 100644
--- a/web/angular/src/app/account-details/account-details.component.ts
+++ b/web/angular/src/app/account-details/account-details.component.ts
@@ -8,10 +8,13 @@ import {AuthService} from "../auth.service";
})
export class AccountDetailsComponent implements OnInit {
+ account: Manager;
+
constructor(private authService: AuthService) {
}
ngOnInit() {
+ this.account = this.authService.account;
}
}
diff --git a/web/angular/src/app/api.service.ts b/web/angular/src/app/api.service.ts
index 24deacd..edfeaf5 100755
--- a/web/angular/src/app/api.service.ts
+++ b/web/angular/src/app/api.service.ts
@@ -18,7 +18,7 @@ export class ApiService {
}
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() {
@@ -49,5 +49,12 @@ export class ApiService {
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)
+ }
}
diff --git a/web/angular/src/app/app.module.ts b/web/angular/src/app/app.module.ts
index 17db517..d98b5b3 100755
--- a/web/angular/src/app/app.module.ts
+++ b/web/angular/src/app/app.module.ts
@@ -16,6 +16,7 @@ import {
MatFormFieldModule,
MatIconModule,
MatInputModule,
+ MatListModule,
MatMenuModule,
MatPaginatorIntl,
MatPaginatorModule,
@@ -98,7 +99,8 @@ export function createTranslateLoader(http: HttpClient) {
),
MatSelectModule,
MatProgressBarModule,
- MatTabsModule
+ MatTabsModule,
+ MatListModule
],
exports: [],
diff --git a/web/angular/src/app/auth.service.ts b/web/angular/src/app/auth.service.ts
new file mode 100644
index 0000000..a472f74
--- /dev/null
+++ b/web/angular/src/app/auth.service.ts
@@ -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);
+ }
+ )
+ }
+}
diff --git a/web/angular/src/app/login/login.component.ts b/web/angular/src/app/login/login.component.ts
index 6ae87b7..7166dd2 100644
--- a/web/angular/src/app/login/login.component.ts
+++ b/web/angular/src/app/login/login.component.ts
@@ -28,16 +28,7 @@ export class LoginComponent implements OnInit {
}
register() {
- this.apiService.register(this.credentials)
- .subscribe(
- () => {
- this.router.navigateByUrl("/account")
- },
- error => {
- console.log(error);
- this.messengerService.show(error.error.message);
- }
- )
+ this.authService.register(this.credentials)
}
canCreate(): boolean {
diff --git a/web/angular/src/app/logs/logs.component.css b/web/angular/src/app/logs/logs.component.css
index ac48790..9badf08 100644
--- a/web/angular/src/app/logs/logs.component.css
+++ b/web/angular/src/app/logs/logs.component.css
@@ -10,3 +10,8 @@
.mat-cell {
text-align: left;
}
+
+.checkbox-wrapper mat-checkbox {
+ margin: 3px 5px;
+ vertical-align: middle;
+}
diff --git a/web/angular/src/app/logs/logs.component.html b/web/angular/src/app/logs/logs.component.html
index fe4994c..0fb816a 100644
--- a/web/angular/src/app/logs/logs.component.html
+++ b/web/angular/src/app/logs/logs.component.html
@@ -1,22 +1,31 @@
-
-
-
+
+
+
+
+ {{"logs.fatal" | translate}}
+ {{"logs.panic" | translate}}
+ {{"logs.error" | translate}}
+ {{"logs.warn" | translate}}
+ {{"logs.info" | translate}}
+ {{"logs.debug" | translate}}
+
- {{"logs.level" | translate}}
- {{entry.level}}
+ {{("logs." + entry.level) | translate}}
- {{"logs.time" | translate}}
- {{entry.timestamp}}
+ {{entry.timestamp}}
{{"logs.message" | translate}}
diff --git a/web/angular/src/app/logs/logs.component.ts b/web/angular/src/app/logs/logs.component.ts
index 80a6332..0629ee3 100644
--- a/web/angular/src/app/logs/logs.component.ts
+++ b/web/angular/src/app/logs/logs.component.ts
@@ -1,6 +1,6 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {ApiService} from "../api.service";
-import {LogEntry} from "../models/logentry";
+import {getLogLevel, LogEntry} from "../models/logentry";
import _ from "lodash"
import * as moment from "moment";
@@ -44,9 +44,9 @@ export class LogsComponent implements OnInit {
this.data.data = _.map(data["logs"], (entry) => {
return {
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),
- level: entry.level
+ level: getLogLevel(entry.level),
}
});
}
diff --git a/web/angular/src/app/models/logentry.ts b/web/angular/src/app/models/logentry.ts
index b1caa99..64f156c 100644
--- a/web/angular/src/app/models/logentry.ts
+++ b/web/angular/src/app/models/logentry.ts
@@ -4,3 +4,32 @@ export interface LogEntry {
data: any,
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;
+ }
+}
diff --git a/web/angular/src/app/models/monitoring.ts b/web/angular/src/app/models/monitoring.ts
new file mode 100644
index 0000000..dba1604
--- /dev/null
+++ b/web/angular/src/app/models/monitoring.ts
@@ -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
+}
diff --git a/web/angular/src/app/project-dashboard/project-dashboard.component.css b/web/angular/src/app/project-dashboard/project-dashboard.component.css
index d355e0b..9611819 100644
--- a/web/angular/src/app/project-dashboard/project-dashboard.component.css
+++ b/web/angular/src/app/project-dashboard/project-dashboard.component.css
@@ -1,5 +1,6 @@
#timeline-wrapper {
width: 100%;
+ display: none;
}
#status-pie-wrapper {
@@ -12,3 +13,28 @@
height: 50%;
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;
+ }
+}
diff --git a/web/angular/src/app/project-dashboard/project-dashboard.component.html b/web/angular/src/app/project-dashboard/project-dashboard.component.html
index f26f4ae..dc194c9 100644
--- a/web/angular/src/app/project-dashboard/project-dashboard.component.html
+++ b/web/angular/src/app/project-dashboard/project-dashboard.component.html
@@ -3,6 +3,13 @@
{{"dashboard.title" | translate}} "{{project.name}}"
+
+
{{"project.git_repo" | translate}}:
{{project.git_repo}}
@@ -10,12 +17,13 @@
{{"project.motd" | translate}}:
{{project.motd}}
+
-
+
+
+
priority_high
+
{{"dashboard.empty" | translate}}
+
+
+
+
Small screen stats
+
Latest monitoring snapshot:
+
{{ lastSnapshot | json }}
+
Assignees
+
{{ assignees | json }}
diff --git a/web/angular/src/app/project-dashboard/project-dashboard.component.ts b/web/angular/src/app/project-dashboard/project-dashboard.component.ts
index 9042eab..7dda45f 100644
--- a/web/angular/src/app/project-dashboard/project-dashboard.component.ts
+++ b/web/angular/src/app/project-dashboard/project-dashboard.component.ts
@@ -3,7 +3,8 @@ import {ApiService} from "../api.service";
import {Project} from "../models/project";
import {ActivatedRoute} from "@angular/router";
-import {Chart, ChartData, Point} from "chart.js";
+import {Chart} from "chart.js";
+import {AssignedTasks, MonitoringSnapshot} from "../models/monitoring";
@Component({
@@ -15,22 +16,30 @@ export class ProjectDashboardComponent implements OnInit {
private projectId;
project: Project;
+ noTasks = false;
+
private timeline: Chart;
private statusPie: Chart;
- private assigneesPir: Chart;
+ private assigneesPie: Chart;
+
private colors = {
new: "#76FF03",
failed: "#FF3D00",
closed: "#E0E0E0",
- awaiting: "#FFB74D"
+ awaiting: "#FFB74D",
+ random: [
+ "#3D5AFE", "#2979FF", "#2196F3",
+ "#7C4DFF", "#673AB7", "#7C4DFF",
+ "#FFC400", "#FFD740", "#FFC107",
+ "#FF3D00", "#FF6E40", "#FF5722",
+ "#76FF03", "#B2FF59", "#8BC34A"
+ ]
};
- tmpLabels = [];
- tmpNew = [];
- tmpFailed = [];
- tmpClosed = [];
- tmpAwaiting = [];
+ snapshots: MonitoringSnapshot[] = [];
+ lastSnapshot: MonitoringSnapshot;
+ assignees: AssignedTasks[];
constructor(private apiService: ApiService, private route: ActivatedRoute) {
}
@@ -42,19 +51,110 @@ export class ProjectDashboardComponent implements OnInit {
this.getProject();
});
+ }
- let n = 40;
- 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))
- }
+ public refresh() {
- this.setupTimeline();
- this.setupStatusPie();
- this.setupAssigneesPie();
+ this.apiService.getMonitoringSnapshots(60, this.projectId)
+ .subscribe((data: any) => {
+ 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() {
@@ -64,49 +164,8 @@ export class ProjectDashboardComponent implements OnInit {
this.timeline = new Chart(ctx, {
type: "bar",
data: {
- labels: this.tmpLabels,
- datasets: [
- {
- 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,
- },
- ],
+ labels: this.snapshots.map(s => s.time_stamp as any),
+ datasets: this.makeTimelineDataset(this.snapshots),
},
options: {
title: {
@@ -124,10 +183,6 @@ export class ProjectDashboardComponent implements OnInit {
ticks: {
source: "auto"
},
- time: {
- unit: "minute",
- unitStepSize: 10,
- }
}]
},
tooltips: {
@@ -136,12 +191,20 @@ export class ProjectDashboardComponent implements OnInit {
mode: "index",
position: "nearest",
},
+ responsive: true
}
})
}
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 ctx = elem.getContext("2d");
@@ -158,10 +221,10 @@ export class ProjectDashboardComponent implements OnInit {
{
label: "Task status",
data: [
- 10,
- 24,
- 301,
- 90,
+ this.lastSnapshot.new_task_count,
+ this.lastSnapshot.failed_task_count,
+ this.lastSnapshot.closed_task_count,
+ this.lastSnapshot.awaiting_verification_count,
],
backgroundColor: [
this.colors.new,
@@ -186,7 +249,7 @@ export class ProjectDashboardComponent implements OnInit {
animation: {
animateScale: true,
animateRotate: true
- }
+ },
}
});
}
@@ -196,30 +259,19 @@ export class ProjectDashboardComponent implements OnInit {
let elem = document.getElementById("assignees-pie") as any;
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",
data: {
- labels: [
- "marc",
- "simon",
- "bernie",
- "natasha",
- ],
+ labels: this.assignees.map(x => x.assignee),
datasets: [
{
label: "Task status",
- data: [
- 10,
- 24,
- 1,
- 23,
- ],
- backgroundColor: [
- this.colors.new,
- this.colors.failed,
- this.colors.closed,
- this.colors.awaiting
- ],
+ data: this.assignees.map(x => x.task_count),
+ backgroundColor: colors,
}
],
},
@@ -237,23 +289,35 @@ export class ProjectDashboardComponent implements OnInit {
animation: {
animateScale: true,
animateRotate: true
- }
+ },
}
});
}
private getProject() {
- this.apiService.getProject(this.projectId).subscribe(data => {
- this.project = {
- id: data["project"]["id"],
- name: data["project"]["name"],
- clone_url: data["project"]["clone_url"],
- git_repo: data["project"]["git_repo"],
- motd: data["project"]["motd"],
- priority: data["project"]["priority"],
- version: data["project"]["version"],
- public: data["project"]["public"],
- }
+ this.apiService.getProject(this.projectId).subscribe((data: any) => {
+ this.project = data.project;
+
+ this.apiService.getMonitoringSnapshots(60, this.projectId)
+ .subscribe((data: any) => {
+ this.snapshots = data.snapshots;
+ this.lastSnapshot = this.snapshots ? this.snapshots.sort((a, b) => {
+ return b.time_stamp - a.time_stamp
+ })[0] : null;
+
+ this.setupTimeline();
+ this.setupStatusPie();
+
+ if (!this.snapshots) {
+ return
+ }
+
+ this.apiService.getAssigneeStats(this.projectId)
+ .subscribe((data: any) => {
+ this.assignees = data.assignees;
+ this.setupAssigneesPie();
+ });
+ })
})
}
}
diff --git a/web/angular/src/assets/i18n/en.json b/web/angular/src/assets/i18n/en.json
index 1ecf551..381fbea 100644
--- a/web/angular/src/assets/i18n/en.json
+++ b/web/angular/src/assets/i18n/en.json
@@ -9,7 +9,7 @@
},
"logs": {
"filter": "Filter",
- "time": "Time",
+ "time": "Time (UTC)",
"level": "Level",
"message": "Message",
"data": "Details",
@@ -18,7 +18,14 @@
"of": "of",
"items_per_page": "Items per 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",
@@ -47,11 +54,13 @@
"create": "Create",
"git_repo": "Git repository name",
"motd": "Message of the day",
- "update": "Update"
+ "update": "Edit"
},
"dashboard": {
"title": "Dashboard for",
- "metadata": "Project metadata"
+ "metadata": "Project metadata",
+ "empty": "No tasks",
+ "refresh": "Refresh"
},
"login": {
"title": "Login",
@@ -64,5 +73,11 @@
"create_account": {
"title": "Register",
"create": "Create account"
+ },
+ "account": {
+ "metadata": "Account metadata",
+ "title": "Account details",
+ "subtitle": "toto: subtitle",
+ "username": "Username"
}
}
diff --git a/web/angular/src/assets/i18n/fr.json b/web/angular/src/assets/i18n/fr.json
index ceb75e9..7294589 100644
--- a/web/angular/src/assets/i18n/fr.json
+++ b/web/angular/src/assets/i18n/fr.json
@@ -9,7 +9,7 @@
},
"logs": {
"filter": "Filtrer",
- "time": "Date",
+ "time": "Date (UTC)",
"level": "Niveau",
"message": "Message",
"data": "Details",
@@ -18,7 +18,14 @@
"of": "de",
"items_per_page": "Items par page",
"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": "Projets",
@@ -52,7 +59,9 @@
},
"dashboard": {
"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": {
"title": "Ouvrir un session",
@@ -65,6 +74,12 @@
"create_account": {
"title": "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"
}
}
diff --git a/web/angular/src/index.html b/web/angular/src/index.html
index bbd1d7a..ac0ce34 100644
--- a/web/angular/src/index.html
+++ b/web/angular/src/index.html
@@ -6,7 +6,7 @@
-
+
diff --git a/web/angular/src/styles.css b/web/angular/src/styles.css
index 363904a..af08975 100644
--- a/web/angular/src/styles.css
+++ b/web/angular/src/styles.css
@@ -78,3 +78,11 @@ body {
.mat-tab-body {
padding-top: 1em;
}
+
+pre {
+ color: #616161;
+}
+
+.hidden {
+ display: none !important;
+}