From 4ef4752c140670c0983b042f17c8fd018ed7a667 Mon Sep 17 00:00:00 2001 From: simon987 Date: Sat, 9 Feb 2019 13:16:58 -0500 Subject: [PATCH] monitoring setup, project dashboard, account page --- api/auth.go | 6 - api/main.go | 18 ++ api/monitoring.go | 52 ++++ api/project.go | 19 ++ config.yml | 6 +- config/config.go | 13 +- storage/monitoring.go | 82 +++++- storage/project.go | 28 ++ storage/task.go | 2 +- test/config.yml | 9 +- .../account-details.component.html | 32 ++- .../account-details.component.ts | 3 + web/angular/src/app/api.service.ts | 9 +- web/angular/src/app/app.module.ts | 4 +- web/angular/src/app/auth.service.ts | 50 ++++ web/angular/src/app/login/login.component.ts | 11 +- web/angular/src/app/logs/logs.component.css | 5 + web/angular/src/app/logs/logs.component.html | 23 +- web/angular/src/app/logs/logs.component.ts | 6 +- web/angular/src/app/models/logentry.ts | 29 ++ web/angular/src/app/models/monitoring.ts | 12 + .../project-dashboard.component.css | 26 ++ .../project-dashboard.component.html | 22 +- .../project-dashboard.component.ts | 268 +++++++++++------- web/angular/src/assets/i18n/en.json | 23 +- web/angular/src/assets/i18n/fr.json | 21 +- web/angular/src/index.html | 2 +- web/angular/src/styles.css | 8 + 28 files changed, 629 insertions(+), 160 deletions(-) create mode 100644 api/monitoring.go create mode 100644 web/angular/src/app/auth.service.ts create mode 100644 web/angular/src/app/models/monitoring.ts 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}}
+
-
+
@@ -23,6 +31,18 @@
+ +
+ 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; +}