From a6802c7109357c536a4c82a9ff54a98c81d557ab Mon Sep 17 00:00:00 2001 From: simon987 Date: Sat, 9 Feb 2019 16:34:15 -0500 Subject: [PATCH] worker stats dashboard, fixed logs page, mobile support for navbar & project dashboard --- api/main.go | 1 + api/worker.go | 16 +++++ schema.sql | 12 ++-- storage/worker.go | 21 ++++++ test/schema.sql | 16 +++-- web/angular/src/app/api.service.ts | 8 ++- web/angular/src/app/app-routing.module.ts | 4 +- web/angular/src/app/app.component.css | 14 ++++ web/angular/src/app/app.component.html | 51 ++++++++++---- web/angular/src/app/app.component.ts | 5 +- web/angular/src/app/app.module.ts | 6 +- web/angular/src/app/logs/logs.component.css | 2 +- web/angular/src/app/logs/logs.component.html | 29 ++++---- web/angular/src/app/logs/logs.component.ts | 21 +++--- .../worker-dashboard.component.css | 0 .../worker-dashboard.component.html | 18 +++++ .../worker-dashboard.component.ts | 66 +++++++++++++++++++ web/angular/src/assets/i18n/en.json | 15 +++-- web/angular/src/assets/i18n/fr.json | 12 +++- 19 files changed, 259 insertions(+), 58 deletions(-) create mode 100644 web/angular/src/app/worker-dashboard/worker-dashboard.component.css create mode 100644 web/angular/src/app/worker-dashboard/worker-dashboard.component.html create mode 100644 web/angular/src/app/worker-dashboard/worker-dashboard.component.ts diff --git a/api/main.go b/api/main.go index 4b9d9e4..a84d592 100644 --- a/api/main.go +++ b/api/main.go @@ -78,6 +78,7 @@ func New() *WebAPI { api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate)) api.router.POST("/worker/update", LogRequestMiddleware(api.WorkerUpdate)) api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet)) + api.router.GET("/worker/stats", LogRequestMiddleware(api.GetAllWorkerStats)) api.router.POST("/access/grant", LogRequestMiddleware(api.WorkerGrantAccess)) api.router.POST("/access/remove", LogRequestMiddleware(api.WorkerRemoveAccess)) diff --git a/api/worker.go b/api/worker.go index d0f2a1b..611dab3 100644 --- a/api/worker.go +++ b/api/worker.go @@ -44,6 +44,12 @@ type WorkerAccessResponse struct { Message string `json:"message"` } +type GetAllWorkerStatsResponse struct { + Ok bool `json:"ok"` + Message string `json:"message,omitempty"` + Stats *[]storage.WorkerStats `json:"stats"` +} + func (api *WebAPI) WorkerCreate(r *Request) { workerReq := &CreateWorkerRequest{} @@ -208,6 +214,16 @@ func (api *WebAPI) WorkerUpdate(r *Request) { } } +func (api *WebAPI) GetAllWorkerStats(r *Request) { + + stats := api.Database.GetAllWorkerStats() + + r.OkJson(GetAllWorkerStatsResponse{ + Ok: true, + Stats: stats, + }) +} + func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (*storage.Worker, error) { if request.Alias == "" { diff --git a/schema.sql b/schema.sql index 674ca07..67b124b 100755 --- a/schema.sql +++ b/schema.sql @@ -15,11 +15,12 @@ CREATE TABLE worker_identity CREATE TABLE worker ( - id SERIAL PRIMARY KEY, - alias TEXT, - created INTEGER, - identity INTEGER REFERENCES worker_identity (id), - secret BYTEA + id SERIAL PRIMARY KEY, + alias TEXT, + created INTEGER, + identity INTEGER REFERENCES worker_identity (id), + secret BYTEA, + closed_task_count INTEGER DEFAULT 0 ); CREATE TABLE project @@ -103,6 +104,7 @@ CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS $$ BEGIN UPDATE project SET closed_task_count=closed_task_count + 1 WHERE id = OLD.project; + UPDATE worker SET closed_task_count=closed_task_count + 1 WHERE id = OLD.assignee; RETURN OLD; END; $$ LANGUAGE 'plpgsql'; diff --git a/storage/worker.go b/storage/worker.go index f089297..237a094 100644 --- a/storage/worker.go +++ b/storage/worker.go @@ -19,6 +19,11 @@ type Worker struct { Secret []byte `json:"secret"` } +type WorkerStats struct { + Alias string `json:"alias"` + ClosedTaskCount int64 `json:"closed_task_count"` +} + func (database *Database) SaveWorker(worker *Worker) { db := database.getDB() @@ -163,3 +168,19 @@ func (database *Database) UpdateWorker(worker *Worker) bool { return rowsAffected == 1 } + +func (database *Database) GetAllWorkerStats() *[]WorkerStats { + + db := database.getDB() + rows, err := db.Query(`SELECT alias, closed_task_count FROM worker WHERE closed_task_count>0 LIMIT 50`) + handleErr(err) + + stats := make([]WorkerStats, 0) + for rows.Next() { + s := WorkerStats{} + _ = rows.Scan(&s.Alias, &s.ClosedTaskCount) + stats = append(stats, s) + } + + return &stats +} diff --git a/test/schema.sql b/test/schema.sql index 1a7c13c..67b124b 100755 --- a/test/schema.sql +++ b/test/schema.sql @@ -15,11 +15,12 @@ CREATE TABLE worker_identity CREATE TABLE worker ( - id SERIAL PRIMARY KEY, - alias TEXT, - created INTEGER, - identity INTEGER REFERENCES worker_identity (id), - secret BYTEA + id SERIAL PRIMARY KEY, + alias TEXT, + created INTEGER, + identity INTEGER REFERENCES worker_identity (id), + secret BYTEA, + closed_task_count INTEGER DEFAULT 0 ); CREATE TABLE project @@ -76,8 +77,8 @@ CREATE TABLE log_entry CREATE TABLE manager ( id SERIAL PRIMARY KEY, - username TEXT, - password TEXT, + username TEXT UNIQUE, + password BYTEA, website_admin BOOLEAN ); @@ -103,6 +104,7 @@ CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS $$ BEGIN UPDATE project SET closed_task_count=closed_task_count + 1 WHERE id = OLD.project; + UPDATE worker SET closed_task_count=closed_task_count + 1 WHERE id = OLD.assignee; RETURN OLD; END; $$ LANGUAGE 'plpgsql'; diff --git a/web/angular/src/app/api.service.ts b/web/angular/src/app/api.service.ts index edfeaf5..7d9edf5 100755 --- a/web/angular/src/app/api.service.ts +++ b/web/angular/src/app/api.service.ts @@ -17,8 +17,8 @@ export class ApiService { ) { } - getLogs() { - return this.http.post(this.url + "/logs", "{\"level\":4, \"since\":1}", this.options); + getLogs(level: number) { + return this.http.post(this.url + "/logs", {level: level, since: 1}, this.options); } getProjects() { @@ -57,4 +57,8 @@ export class ApiService { return this.http.get(this.url + `/project/assignees/${project}`, this.options) } + getWorkerStats() { + return this.http.get(this.url + `/worker/stats`, this.options) + } + } diff --git a/web/angular/src/app/app-routing.module.ts b/web/angular/src/app/app-routing.module.ts index 7989baa..1171089 100755 --- a/web/angular/src/app/app-routing.module.ts +++ b/web/angular/src/app/app-routing.module.ts @@ -10,6 +10,7 @@ import {filter} from "rxjs/operators"; import {TranslateService} from "@ngx-translate/core"; import {LoginComponent} from "./login/login.component"; import {AccountDetailsComponent} from "./account-details/account-details.component"; +import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component"; const routes: Routes = [ {path: "log", component: LogsComponent}, @@ -18,7 +19,8 @@ const routes: Routes = [ {path: "projects", component: ProjectListComponent}, {path: "project/:id", component: ProjectDashboardComponent}, {path: "project/:id/update", component: UpdateProjectComponent}, - {path: "new_project", component: CreateProjectComponent} + {path: "new_project", component: CreateProjectComponent}, + {path: "workers", component: WorkerDashboardComponent} ]; @NgModule({ diff --git a/web/angular/src/app/app.component.css b/web/angular/src/app/app.component.css index 1417e5b..6558a6b 100755 --- a/web/angular/src/app/app.component.css +++ b/web/angular/src/app/app.component.css @@ -8,3 +8,17 @@ .nav-link { } + +.large-nav { + display: none; +} + +@media (min-width: 768px) { + .large-nav { + display: initial; + } + + .small-nav { + display: none; + } +} diff --git a/web/angular/src/app/app.component.html b/web/angular/src/app/app.component.html index 2d6ebad..435c16c 100755 --- a/web/angular/src/app/app.component.html +++ b/web/angular/src/app/app.component.html @@ -1,18 +1,47 @@ - - - - +
+ + + + + +
+
+ + + + + + + + +
- - - - {{lang.display}} - - - + + + + + +
diff --git a/web/angular/src/app/app.component.ts b/web/angular/src/app/app.component.ts index ed37894..68645e0 100644 --- a/web/angular/src/app/app.component.ts +++ b/web/angular/src/app/app.component.ts @@ -1,7 +1,6 @@ import {Component} from '@angular/core'; import {Router} from '@angular/router'; import {TranslateService} from "@ngx-translate/core"; -import {MatSelectChange} from "@angular/material"; @Component({ selector: 'app-root', @@ -10,8 +9,8 @@ import {MatSelectChange} from "@angular/material"; }) export class AppComponent { - langChange(event: MatSelectChange) { - this.translate.use(event.value) + langChange(lang: any) { + this.translate.use(lang.lang) } langList: any[] = [ diff --git a/web/angular/src/app/app.module.ts b/web/angular/src/app/app.module.ts index d98b5b3..b463cd5 100755 --- a/web/angular/src/app/app.module.ts +++ b/web/angular/src/app/app.module.ts @@ -9,6 +9,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { MatAutocompleteModule, MatButtonModule, + MatButtonToggleModule, MatCardModule, MatCheckboxModule, MatDividerModule, @@ -45,6 +46,7 @@ import {TranslateHttpLoader} from "@ngx-translate/http-loader"; import {TranslatedPaginator} from "./TranslatedPaginatorConfiguration"; import {LoginComponent} from './login/login.component'; import {AccountDetailsComponent} from './account-details/account-details.component'; +import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component'; export function createTranslateLoader(http: HttpClient) { @@ -63,6 +65,7 @@ export function createTranslateLoader(http: HttpClient) { SnackBarComponent, LoginComponent, AccountDetailsComponent, + WorkerDashboardComponent, ], imports: [ BrowserModule, @@ -100,7 +103,8 @@ export function createTranslateLoader(http: HttpClient) { MatSelectModule, MatProgressBarModule, MatTabsModule, - MatListModule + MatListModule, + MatButtonToggleModule ], exports: [], diff --git a/web/angular/src/app/logs/logs.component.css b/web/angular/src/app/logs/logs.component.css index 9badf08..31932f5 100644 --- a/web/angular/src/app/logs/logs.component.css +++ b/web/angular/src/app/logs/logs.component.css @@ -11,7 +11,7 @@ text-align: left; } -.checkbox-wrapper mat-checkbox { +mat-button-toggle-group { 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 0fb816a..5e82737 100644 --- a/web/angular/src/app/logs/logs.component.html +++ b/web/angular/src/app/logs/logs.component.html @@ -1,16 +1,23 @@
-
- - - - {{"logs.fatal" | translate}} - {{"logs.panic" | translate}} - {{"logs.error" | translate}} - {{"logs.warn" | translate}} - {{"logs.info" | translate}} - {{"logs.debug" | translate}} -
+ + + + + {{"logs.fatal" | translate}} + {{"logs.panic" | translate}} + {{"logs.error" | translate}} + {{"logs.warn" | translate}} + {{"logs.info" | translate}} + {{"logs.debug" | translate}} + {{"logs.trace" | translate}} + + +
; + filterLevel: number = 1; logsCols: string[] = ["level", "timestamp", "message", "data"]; @ViewChild(MatPaginator) paginator: MatPaginator; @@ -25,21 +26,25 @@ export class LogsComponent implements OnInit { } ngOnInit() { - this.getLogs(); - this.data.paginator = this.paginator; this.data.sort = this.sort; - // interval(5000).subscribe(() => { - // this.getLogs(); - // }) } applyFilter(filter: string) { this.data.filter = filter.trim().toLowerCase(); } - private getLogs() { - this.apiService.getLogs().subscribe( + filterLevelChange(event: MatButtonToggleChange) { + this.filterLevel = Number(event.value); + this.getLogs(Number(event.value)) + } + + private refresh() { + this.getLogs(this.filterLevel) + } + + private getLogs(level: number) { + this.apiService.getLogs(level).subscribe( data => { this.data.data = _.map(data["logs"], (entry) => { return { diff --git a/web/angular/src/app/worker-dashboard/worker-dashboard.component.css b/web/angular/src/app/worker-dashboard/worker-dashboard.component.css new file mode 100644 index 0000000..e69de29 diff --git a/web/angular/src/app/worker-dashboard/worker-dashboard.component.html b/web/angular/src/app/worker-dashboard/worker-dashboard.component.html new file mode 100644 index 0000000..b685c27 --- /dev/null +++ b/web/angular/src/app/worker-dashboard/worker-dashboard.component.html @@ -0,0 +1,18 @@ +
+ + + {{"workers.title" | translate}} + {{"workers.subtitle" | translate}} + + + + + + + +
diff --git a/web/angular/src/app/worker-dashboard/worker-dashboard.component.ts b/web/angular/src/app/worker-dashboard/worker-dashboard.component.ts new file mode 100644 index 0000000..5550b69 --- /dev/null +++ b/web/angular/src/app/worker-dashboard/worker-dashboard.component.ts @@ -0,0 +1,66 @@ +import {Component, OnInit} from '@angular/core'; +import {ApiService} from "../api.service"; + +import {Chart} from "chart.js"; + +@Component({ + selector: 'app-worker-dashboard', + templateUrl: './worker-dashboard.component.html', + styleUrls: ['./worker-dashboard.component.css'] +}) +export class WorkerDashboardComponent implements OnInit { + + private chart: Chart; + + constructor(private apiService: ApiService) { + } + + ngOnInit() { + this.setupChart(); + this.refresh() + } + + private refresh() { + this.apiService.getWorkerStats() + .subscribe((data: any) => { + this.updateChart(data.stats) + } + ) + } + + private setupChart() { + + let elem = document.getElementById("worker-stats") as any; + let ctx = elem.getContext("2d"); + + this.chart = new Chart(ctx, { + type: "bar", + data: { + labels: [], + datasets: [], + }, + options: { + title: { + display: false, + }, + legend: { + display: false + }, + tooltips: { + enabled: true, + }, + responsive: true + } + }) + } + + private updateChart(data) { + + this.chart.data.labels = data.map(w => w.alias); + this.chart.data.datasets = [{ + data: data.map(w => w.closed_task_count), + backgroundColor: "#FF3D00" + }]; + this.chart.update(); + } +} diff --git a/web/angular/src/assets/i18n/en.json b/web/angular/src/assets/i18n/en.json index 381fbea..d119885 100644 --- a/web/angular/src/assets/i18n/en.json +++ b/web/angular/src/assets/i18n/en.json @@ -1,11 +1,12 @@ { "nav": { "title": "task_tracker", - "langSelect": "Language", + "lang_select": "Language", "logs": "Logs", "project_list": "Projects", "new_project": "New Project", - "login": "Login" + "login": "Login", + "worker_dashboard": "Workers" }, "logs": { "filter": "Filter", @@ -40,7 +41,8 @@ "new_project": "New project", "login": "Login", "new_account": "Create account", - "account": "Account details" + "account": "Account details", + "workers": "Workers" }, "project": { "name": "Project name", @@ -67,8 +69,7 @@ "login": "Login", "username": "Username", "password": "Password", - "repeat_password": "Repeat password", - "create_account": "" + "repeat_password": "Repeat password" }, "create_account": { "title": "Register", @@ -79,5 +80,9 @@ "title": "Account details", "subtitle": "toto: subtitle", "username": "Username" + }, + "workers": { + "title": "Completed tasks per worker", + "subtitle": "Real-time data for all projects" } } diff --git a/web/angular/src/assets/i18n/fr.json b/web/angular/src/assets/i18n/fr.json index 7294589..354e830 100644 --- a/web/angular/src/assets/i18n/fr.json +++ b/web/angular/src/assets/i18n/fr.json @@ -1,11 +1,12 @@ { "nav": { "title": "task_tracker (fr)", - "langSelect": "Langue", + "lang_select": "Langue", "logs": "Journal", "project_list": "Projets", "new_project": "Nouveau projet", - "login": "Ouvrir un session" + "login": "Ouvrir un session", + "worker_dashboard": "Workers" }, "logs": { "filter": "Filtrer", @@ -41,7 +42,8 @@ "update": "Modifier", "login": "Ouverture de session", "new_account": "Création de compte", - "account": "Compte" + "account": "Compte", + "workers": "Workers" }, "project": { "name": "Nom du projet", @@ -80,6 +82,10 @@ "title": "Détails du compte", "subtitle": "toto: sous-titre", "username": "Nom d'utilisateur" + }, + "workers": { + "title": "Tâches complétés par worker", + "subtitle": "Données en temps réél pour tous les projets" } }