mirror of
https://github.com/simon987/task_tracker.git
synced 2025-04-19 18:16:45 +00:00
worker stats dashboard, fixed logs page, mobile support for navbar & project dashboard
This commit is contained in:
parent
4ef4752c14
commit
a6802c7109
@ -78,6 +78,7 @@ func New() *WebAPI {
|
|||||||
api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate))
|
api.router.POST("/worker/create", LogRequestMiddleware(api.WorkerCreate))
|
||||||
api.router.POST("/worker/update", LogRequestMiddleware(api.WorkerUpdate))
|
api.router.POST("/worker/update", LogRequestMiddleware(api.WorkerUpdate))
|
||||||
api.router.GET("/worker/get/:id", LogRequestMiddleware(api.WorkerGet))
|
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/grant", LogRequestMiddleware(api.WorkerGrantAccess))
|
||||||
api.router.POST("/access/remove", LogRequestMiddleware(api.WorkerRemoveAccess))
|
api.router.POST("/access/remove", LogRequestMiddleware(api.WorkerRemoveAccess))
|
||||||
|
@ -44,6 +44,12 @@ type WorkerAccessResponse struct {
|
|||||||
Message string `json:"message"`
|
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) {
|
func (api *WebAPI) WorkerCreate(r *Request) {
|
||||||
|
|
||||||
workerReq := &CreateWorkerRequest{}
|
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) {
|
func (api *WebAPI) workerCreate(request *CreateWorkerRequest, identity *storage.Identity) (*storage.Worker, error) {
|
||||||
|
|
||||||
if request.Alias == "" {
|
if request.Alias == "" {
|
||||||
|
@ -19,7 +19,8 @@ CREATE TABLE worker
|
|||||||
alias TEXT,
|
alias TEXT,
|
||||||
created INTEGER,
|
created INTEGER,
|
||||||
identity INTEGER REFERENCES worker_identity (id),
|
identity INTEGER REFERENCES worker_identity (id),
|
||||||
secret BYTEA
|
secret BYTEA,
|
||||||
|
closed_task_count INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE project
|
CREATE TABLE project
|
||||||
@ -103,6 +104,7 @@ CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS
|
|||||||
$$
|
$$
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE project SET closed_task_count=closed_task_count + 1 WHERE id = OLD.project;
|
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;
|
RETURN OLD;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE 'plpgsql';
|
$$ LANGUAGE 'plpgsql';
|
||||||
|
@ -19,6 +19,11 @@ type Worker struct {
|
|||||||
Secret []byte `json:"secret"`
|
Secret []byte `json:"secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WorkerStats struct {
|
||||||
|
Alias string `json:"alias"`
|
||||||
|
ClosedTaskCount int64 `json:"closed_task_count"`
|
||||||
|
}
|
||||||
|
|
||||||
func (database *Database) SaveWorker(worker *Worker) {
|
func (database *Database) SaveWorker(worker *Worker) {
|
||||||
|
|
||||||
db := database.getDB()
|
db := database.getDB()
|
||||||
@ -163,3 +168,19 @@ func (database *Database) UpdateWorker(worker *Worker) bool {
|
|||||||
|
|
||||||
return rowsAffected == 1
|
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
|
||||||
|
}
|
||||||
|
@ -19,7 +19,8 @@ CREATE TABLE worker
|
|||||||
alias TEXT,
|
alias TEXT,
|
||||||
created INTEGER,
|
created INTEGER,
|
||||||
identity INTEGER REFERENCES worker_identity (id),
|
identity INTEGER REFERENCES worker_identity (id),
|
||||||
secret BYTEA
|
secret BYTEA,
|
||||||
|
closed_task_count INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE project
|
CREATE TABLE project
|
||||||
@ -76,8 +77,8 @@ CREATE TABLE log_entry
|
|||||||
CREATE TABLE manager
|
CREATE TABLE manager
|
||||||
(
|
(
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
username TEXT,
|
username TEXT UNIQUE,
|
||||||
password TEXT,
|
password BYTEA,
|
||||||
website_admin BOOLEAN
|
website_admin BOOLEAN
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -103,6 +104,7 @@ CREATE OR REPLACE FUNCTION on_task_delete_proc() RETURNS TRIGGER AS
|
|||||||
$$
|
$$
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE project SET closed_task_count=closed_task_count + 1 WHERE id = OLD.project;
|
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;
|
RETURN OLD;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE 'plpgsql';
|
$$ LANGUAGE 'plpgsql';
|
||||||
|
@ -17,8 +17,8 @@ export class ApiService {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogs() {
|
getLogs(level: number) {
|
||||||
return this.http.post(this.url + "/logs", "{\"level\":4, \"since\":1}", this.options);
|
return this.http.post(this.url + "/logs", {level: level, since: 1}, this.options);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjects() {
|
getProjects() {
|
||||||
@ -57,4 +57,8 @@ export class ApiService {
|
|||||||
return this.http.get(this.url + `/project/assignees/${project}`, this.options)
|
return this.http.get(this.url + `/project/assignees/${project}`, this.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWorkerStats() {
|
||||||
|
return this.http.get(this.url + `/worker/stats`, this.options)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import {filter} from "rxjs/operators";
|
|||||||
import {TranslateService} from "@ngx-translate/core";
|
import {TranslateService} from "@ngx-translate/core";
|
||||||
import {LoginComponent} from "./login/login.component";
|
import {LoginComponent} from "./login/login.component";
|
||||||
import {AccountDetailsComponent} from "./account-details/account-details.component";
|
import {AccountDetailsComponent} from "./account-details/account-details.component";
|
||||||
|
import {WorkerDashboardComponent} from "./worker-dashboard/worker-dashboard.component";
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: "log", component: LogsComponent},
|
{path: "log", component: LogsComponent},
|
||||||
@ -18,7 +19,8 @@ const routes: Routes = [
|
|||||||
{path: "projects", component: ProjectListComponent},
|
{path: "projects", component: ProjectListComponent},
|
||||||
{path: "project/:id", component: ProjectDashboardComponent},
|
{path: "project/:id", component: ProjectDashboardComponent},
|
||||||
{path: "project/:id/update", component: UpdateProjectComponent},
|
{path: "project/:id/update", component: UpdateProjectComponent},
|
||||||
{path: "new_project", component: CreateProjectComponent}
|
{path: "new_project", component: CreateProjectComponent},
|
||||||
|
{path: "workers", component: WorkerDashboardComponent}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -8,3 +8,17 @@
|
|||||||
|
|
||||||
.nav-link {
|
.nav-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.large-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.large-nav {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,18 +1,47 @@
|
|||||||
<mat-toolbar color="primary">
|
<mat-toolbar color="primary">
|
||||||
<button mat-button [class.mat-accent]="router.url == '/'" class="nav-title" [routerLink]="''">{{"nav.title" | translate}}</button>
|
<div class="large-nav">
|
||||||
<button mat-button [class.mat-accent]="router.url == '/log'" class="nav-link" [routerLink]="'log'">{{"nav.logs" | translate}}</button>
|
<button mat-button [class.mat-accent]="router.url == '/'" class="nav-title"
|
||||||
<button mat-button [class.mat-accent]="router.url == '/projects'" class="nav-link" [routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
|
[routerLink]="''">{{"nav.title" | translate}}</button>
|
||||||
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link" [routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
|
<button mat-button [class.mat-accent]="router.url == '/log'" class="nav-link"
|
||||||
|
[routerLink]="'log'">{{"nav.logs" | translate}}</button>
|
||||||
|
<button mat-button [class.mat-accent]="router.url == '/projects'" class="nav-link"
|
||||||
|
[routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
|
||||||
|
<button mat-button [class.mat-accent]="router.url == '/new_project'" class="nav-link"
|
||||||
|
[routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
|
||||||
|
<button mat-button [class.mat-accent]="router.url == '/workers'" class="nav-link"
|
||||||
|
[routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button>
|
||||||
|
</div>
|
||||||
|
<div class="small-nav">
|
||||||
|
<button mat-button [matMenuTriggerFor]="smallNav">
|
||||||
|
<mat-icon>more_vert</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #smallNav>
|
||||||
|
<button mat-menu-item [class.mat-accent]="router.url == '/'" class="nav-title"
|
||||||
|
[routerLink]="''">{{"nav.title" | translate}}</button>
|
||||||
|
<button mat-menu-item [class.mat-accent]="router.url == '/log'" class="nav-link"
|
||||||
|
[routerLink]="'log'">{{"nav.logs" | translate}}</button>
|
||||||
|
<button mat-menu-item [class.mat-accent]="router.url == '/projects'" class="nav-link"
|
||||||
|
[routerLink]="'projects'">{{"nav.project_list" | translate}}</button>
|
||||||
|
<button mat-menu-item [class.mat-accent]="router.url == '/new_project'" class="nav-link"
|
||||||
|
[routerLink]="'new_project'">{{"nav.new_project" | translate}}</button>
|
||||||
|
<button mat-menu-item [class.mat-accent]="router.url == '/workers'" class="nav-link"
|
||||||
|
[routerLink]="'workers'">{{"nav.worker_dashboard" | translate}}</button>
|
||||||
|
</mat-menu>
|
||||||
|
</div>
|
||||||
<span class="nav-spacer"></span>
|
<span class="nav-spacer"></span>
|
||||||
<button mat-button [class.mat-accent]="router.url == '/login'" class="nav-link"
|
<button mat-button [class.mat-accent]="router.url == '/login'" class="nav-link"
|
||||||
[routerLink]="'login'">{{"nav.login" | translate}}</button>
|
[routerLink]="'login'">{{"nav.login" | translate}}</button>
|
||||||
<mat-form-field [floatLabel]="'never'">
|
|
||||||
<mat-select [placeholder]="'nav.langSelect' | translate" (selectionChange)="langChange($event)">
|
<button mat-button [matMenuTriggerFor]="langMenu">
|
||||||
<mat-option *ngFor="let lang of langList" [value]="lang.lang">
|
{{"nav.lang_select" | translate}}
|
||||||
|
<mat-icon>arrow_drop_down</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-menu #langMenu>
|
||||||
|
<button mat-menu-item *ngFor="let lang of langList" (click)="langChange(lang)">
|
||||||
{{lang.display}}
|
{{lang.display}}
|
||||||
</mat-option>
|
</button>
|
||||||
</mat-select>
|
</mat-menu>
|
||||||
</mat-form-field>
|
|
||||||
|
|
||||||
</mat-toolbar>
|
</mat-toolbar>
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
import {TranslateService} from "@ngx-translate/core";
|
import {TranslateService} from "@ngx-translate/core";
|
||||||
import {MatSelectChange} from "@angular/material";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -10,8 +9,8 @@ import {MatSelectChange} from "@angular/material";
|
|||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
|
||||||
langChange(event: MatSelectChange) {
|
langChange(lang: any) {
|
||||||
this.translate.use(event.value)
|
this.translate.use(lang.lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
langList: any[] = [
|
langList: any[] = [
|
||||||
|
@ -9,6 +9,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
|||||||
import {
|
import {
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatButtonToggleModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
MatCheckboxModule,
|
MatCheckboxModule,
|
||||||
MatDividerModule,
|
MatDividerModule,
|
||||||
@ -45,6 +46,7 @@ import {TranslateHttpLoader} from "@ngx-translate/http-loader";
|
|||||||
import {TranslatedPaginator} from "./TranslatedPaginatorConfiguration";
|
import {TranslatedPaginator} from "./TranslatedPaginatorConfiguration";
|
||||||
import {LoginComponent} from './login/login.component';
|
import {LoginComponent} from './login/login.component';
|
||||||
import {AccountDetailsComponent} from './account-details/account-details.component';
|
import {AccountDetailsComponent} from './account-details/account-details.component';
|
||||||
|
import {WorkerDashboardComponent} from './worker-dashboard/worker-dashboard.component';
|
||||||
|
|
||||||
|
|
||||||
export function createTranslateLoader(http: HttpClient) {
|
export function createTranslateLoader(http: HttpClient) {
|
||||||
@ -63,6 +65,7 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
SnackBarComponent,
|
SnackBarComponent,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
AccountDetailsComponent,
|
AccountDetailsComponent,
|
||||||
|
WorkerDashboardComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@ -100,7 +103,8 @@ export function createTranslateLoader(http: HttpClient) {
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatProgressBarModule,
|
MatProgressBarModule,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
MatListModule
|
MatListModule,
|
||||||
|
MatButtonToggleModule
|
||||||
|
|
||||||
],
|
],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-wrapper mat-checkbox {
|
mat-button-toggle-group {
|
||||||
margin: 3px 5px;
|
margin: 3px 5px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<div class="checkbox-wrapper">
|
|
||||||
<mat-form-field style="margin-right: 10px">
|
<mat-form-field style="margin-right: 10px">
|
||||||
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
|
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-checkbox>{{"logs.fatal" | translate}}</mat-checkbox>
|
<mat-button-toggle-group name="level" aria-label="Font Style" (change)="filterLevelChange($event)">
|
||||||
<mat-checkbox>{{"logs.panic" | translate}}</mat-checkbox>
|
<mat-button-toggle value="1">{{"logs.fatal" | translate}}</mat-button-toggle>
|
||||||
<mat-checkbox>{{"logs.error" | translate}}</mat-checkbox>
|
<mat-button-toggle value="2">{{"logs.panic" | translate}}</mat-button-toggle>
|
||||||
<mat-checkbox>{{"logs.warn" | translate}}</mat-checkbox>
|
<mat-button-toggle value="3">{{"logs.error" | translate}}</mat-button-toggle>
|
||||||
<mat-checkbox>{{"logs.info" | translate}}</mat-checkbox>
|
<mat-button-toggle value="4">{{"logs.warn" | translate}}</mat-button-toggle>
|
||||||
<mat-checkbox>{{"logs.debug" | translate}}</mat-checkbox>
|
<mat-button-toggle value="5">{{"logs.info" | translate}}</mat-button-toggle>
|
||||||
</div>
|
<mat-button-toggle value="6">{{"logs.debug" | translate}}</mat-button-toggle>
|
||||||
|
<mat-button-toggle value="7">{{"logs.trace" | translate}}</mat-button-toggle>
|
||||||
|
</mat-button-toggle-group>
|
||||||
|
|
||||||
|
<button mat-raised-button style="float: right"
|
||||||
|
[title]="'dashboard.refresh' | translate"
|
||||||
|
(click)="refresh()">
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
<div class="mat-elevation-z8">
|
<div class="mat-elevation-z8">
|
||||||
|
|
||||||
<mat-table [dataSource]="data" matSort matSortActive="timestamp"
|
<mat-table [dataSource]="data" matSort matSortActive="timestamp"
|
||||||
|
@ -4,7 +4,7 @@ import {getLogLevel, LogEntry} from "../models/logentry";
|
|||||||
|
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import * as moment from "moment";
|
import * as moment from "moment";
|
||||||
import {MatPaginator, MatSort, MatTableDataSource} from "@angular/material";
|
import {MatButtonToggleChange, MatPaginator, MatSort, MatTableDataSource} from "@angular/material";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-logs',
|
selector: 'app-logs',
|
||||||
@ -15,6 +15,7 @@ export class LogsComponent implements OnInit {
|
|||||||
|
|
||||||
logs: LogEntry[] = [];
|
logs: LogEntry[] = [];
|
||||||
data: MatTableDataSource<LogEntry>;
|
data: MatTableDataSource<LogEntry>;
|
||||||
|
filterLevel: number = 1;
|
||||||
logsCols: string[] = ["level", "timestamp", "message", "data"];
|
logsCols: string[] = ["level", "timestamp", "message", "data"];
|
||||||
|
|
||||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||||
@ -25,21 +26,25 @@ export class LogsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.getLogs();
|
|
||||||
|
|
||||||
this.data.paginator = this.paginator;
|
this.data.paginator = this.paginator;
|
||||||
this.data.sort = this.sort;
|
this.data.sort = this.sort;
|
||||||
// interval(5000).subscribe(() => {
|
|
||||||
// this.getLogs();
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFilter(filter: string) {
|
applyFilter(filter: string) {
|
||||||
this.data.filter = filter.trim().toLowerCase();
|
this.data.filter = filter.trim().toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLogs() {
|
filterLevelChange(event: MatButtonToggleChange) {
|
||||||
this.apiService.getLogs().subscribe(
|
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 => {
|
data => {
|
||||||
this.data.data = _.map(data["logs"], (entry) => {
|
this.data.data = _.map(data["logs"], (entry) => {
|
||||||
return <LogEntry>{
|
return <LogEntry>{
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
<div class="container">
|
||||||
|
<mat-card class="mat-elevation-z8">
|
||||||
|
<mat-card-header style="float:left">
|
||||||
|
<mat-card-title>{{"workers.title" | translate}}</mat-card-title>
|
||||||
|
<mat-card-subtitle>{{"workers.subtitle" | translate}}</mat-card-subtitle>
|
||||||
|
</mat-card-header>
|
||||||
|
|
||||||
|
<button mat-raised-button style="float: right"
|
||||||
|
[title]="'dashboard.refresh' | translate"
|
||||||
|
(click)="refresh()"
|
||||||
|
>
|
||||||
|
<mat-icon>refresh</mat-icon>
|
||||||
|
</button>
|
||||||
|
<mat-card-content>
|
||||||
|
<canvas id="worker-stats"></canvas>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"nav": {
|
"nav": {
|
||||||
"title": "task_tracker",
|
"title": "task_tracker",
|
||||||
"langSelect": "Language",
|
"lang_select": "Language",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"project_list": "Projects",
|
"project_list": "Projects",
|
||||||
"new_project": "New Project",
|
"new_project": "New Project",
|
||||||
"login": "Login"
|
"login": "Login",
|
||||||
|
"worker_dashboard": "Workers"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
@ -40,7 +41,8 @@
|
|||||||
"new_project": "New project",
|
"new_project": "New project",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"new_account": "Create account",
|
"new_account": "Create account",
|
||||||
"account": "Account details"
|
"account": "Account details",
|
||||||
|
"workers": "Workers"
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"name": "Project name",
|
"name": "Project name",
|
||||||
@ -67,8 +69,7 @@
|
|||||||
"login": "Login",
|
"login": "Login",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"repeat_password": "Repeat password",
|
"repeat_password": "Repeat password"
|
||||||
"create_account": ""
|
|
||||||
},
|
},
|
||||||
"create_account": {
|
"create_account": {
|
||||||
"title": "Register",
|
"title": "Register",
|
||||||
@ -79,5 +80,9 @@
|
|||||||
"title": "Account details",
|
"title": "Account details",
|
||||||
"subtitle": "toto: subtitle",
|
"subtitle": "toto: subtitle",
|
||||||
"username": "Username"
|
"username": "Username"
|
||||||
|
},
|
||||||
|
"workers": {
|
||||||
|
"title": "Completed tasks per worker",
|
||||||
|
"subtitle": "Real-time data for all projects"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"nav": {
|
"nav": {
|
||||||
"title": "task_tracker (fr)",
|
"title": "task_tracker (fr)",
|
||||||
"langSelect": "Langue",
|
"lang_select": "Langue",
|
||||||
"logs": "Journal",
|
"logs": "Journal",
|
||||||
"project_list": "Projets",
|
"project_list": "Projets",
|
||||||
"new_project": "Nouveau projet",
|
"new_project": "Nouveau projet",
|
||||||
"login": "Ouvrir un session"
|
"login": "Ouvrir un session",
|
||||||
|
"worker_dashboard": "Workers"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"filter": "Filtrer",
|
"filter": "Filtrer",
|
||||||
@ -41,7 +42,8 @@
|
|||||||
"update": "Modifier",
|
"update": "Modifier",
|
||||||
"login": "Ouverture de session",
|
"login": "Ouverture de session",
|
||||||
"new_account": "Création de compte",
|
"new_account": "Création de compte",
|
||||||
"account": "Compte"
|
"account": "Compte",
|
||||||
|
"workers": "Workers"
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"name": "Nom du projet",
|
"name": "Nom du projet",
|
||||||
@ -80,6 +82,10 @@
|
|||||||
"title": "Détails du compte",
|
"title": "Détails du compte",
|
||||||
"subtitle": "toto: sous-titre",
|
"subtitle": "toto: sous-titre",
|
||||||
"username": "Nom d'utilisateur"
|
"username": "Nom d'utilisateur"
|
||||||
|
},
|
||||||
|
"workers": {
|
||||||
|
"title": "Tâches complétés par worker",
|
||||||
|
"subtitle": "Données en temps réél pour tous les projets"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user