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"
}
}