mirror of
https://github.com/simon987/task_tracker.git
synced 2025-12-11 14:08:52 +00:00
Added i18n, started working on monitoring
This commit is contained in:
@@ -16,12 +16,8 @@ export class ApiService {
|
||||
return this.http.post(this.url + "/logs", "{\"level\":\"info\", \"since\":10000}");
|
||||
}
|
||||
|
||||
getProjectStats(id: number) {
|
||||
return this.http.get(this.url + "/project/stats/" + id)
|
||||
}
|
||||
|
||||
getProjects() {
|
||||
return this.http.get(this.url + "/project/stats")
|
||||
return this.http.get(this.url + "/project/list")
|
||||
}
|
||||
|
||||
getProject(id: number) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.nav-spacer {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<!--<mat-toolbar>-->
|
||||
<mat-toolbar color="primary">
|
||||
<span>{{ "nav.title" | translate }}</span>
|
||||
<span class="nav-spacer"></span>
|
||||
<mat-icon class="example-icon">favorite</mat-icon>
|
||||
<mat-icon class="example-icon">delete</mat-icon>
|
||||
<mat-form-field [floatLabel]="'never'">
|
||||
<mat-select [placeholder]="'nav.langSelect' | translate" (selectionChange)="langChange($event)">
|
||||
<mat-option *ngFor="let lang of langList" [value]="lang.lang">
|
||||
{{lang.display}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</mat-toolbar>
|
||||
<ul>
|
||||
<li><a [routerLink]="''">Index</a></li>
|
||||
<li><a [routerLink]="'log'">Logs</a></li>
|
||||
<li><a [routerLink]="'projects'">list</a></li>
|
||||
<li><a [routerLink]="'new_project'">new project</a></li>
|
||||
</ul>
|
||||
<!--</mat-toolbar>-->
|
||||
|
||||
<messenger-snack-bar></messenger-snack-bar>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {TranslateService} from "@ngx-translate/core";
|
||||
import {MatSelectChange} from "@angular/material";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -6,5 +8,24 @@ import {Component} from '@angular/core';
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'angular';
|
||||
|
||||
langChange(event: MatSelectChange) {
|
||||
this.translate.use(event.value)
|
||||
}
|
||||
|
||||
langList: any[] = [
|
||||
{lang: "fr", display: "Français"},
|
||||
{lang: "en", display: "English"},
|
||||
];
|
||||
|
||||
constructor(private translate: TranslateService) {
|
||||
|
||||
translate.addLangs([
|
||||
"en",
|
||||
"fr"
|
||||
]);
|
||||
|
||||
translate.setDefaultLang("en");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatSelectModule,
|
||||
MatSliderModule,
|
||||
MatSlideToggleModule,
|
||||
MatSnackBarModule,
|
||||
@@ -28,13 +29,21 @@ import {
|
||||
} from "@angular/material";
|
||||
import {ApiService} from "./api.service";
|
||||
import {MessengerService} from "./messenger.service";
|
||||
import {HttpClientModule} from "@angular/common/http";
|
||||
import {HttpClient, HttpClientModule} from "@angular/common/http";
|
||||
import {ProjectDashboardComponent} from './project-dashboard/project-dashboard.component';
|
||||
import {ProjectListComponent} from './project-list/project-list.component';
|
||||
import {CreateProjectComponent} from './create-project/create-project.component';
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {UpdateProjectComponent} from './update-project/update-project.component';
|
||||
import {SnackBarComponent} from "./messenger/snack-bar.component";
|
||||
import {TranslateLoader, TranslateModule} from "@ngx-translate/core";
|
||||
import {TranslateHttpLoader} from "@ngx-translate/http-loader";
|
||||
|
||||
|
||||
export function createTranslateLoader(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
}
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -71,6 +80,15 @@ import {SnackBarComponent} from "./messenger/snack-bar.component";
|
||||
MatCheckboxModule,
|
||||
MatDividerModule,
|
||||
MatSnackBarModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: (createTranslateLoader),
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}
|
||||
),
|
||||
MatSelectModule
|
||||
|
||||
],
|
||||
exports: [],
|
||||
|
||||
@@ -12,7 +12,7 @@ import {Router} from "@angular/router";
|
||||
})
|
||||
export class CreateProjectComponent implements OnInit {
|
||||
|
||||
private project = new Project();
|
||||
project = new Project();
|
||||
|
||||
constructor(private apiService: ApiService,
|
||||
private messengerService: MessengerService,
|
||||
|
||||
@@ -12,9 +12,9 @@ import {MatPaginator, MatSort, MatTableDataSource} from "@angular/material";
|
||||
})
|
||||
export class LogsComponent implements OnInit {
|
||||
|
||||
private logs: LogEntry[] = [];
|
||||
private data: MatTableDataSource<LogEntry>;
|
||||
private logsCols: string[] = ["level", "timestamp", "message", "data"];
|
||||
logs: LogEntry[] = [];
|
||||
data: MatTableDataSource<LogEntry>;
|
||||
logsCols: string[] = ["level", "timestamp", "message", "data"];
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
|
||||
import * as d3 from "d3"
|
||||
import * as _ from "lodash"
|
||||
import {interval} from "rxjs";
|
||||
import {ApiService} from "../api.service";
|
||||
import {ActivatedRoute} from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-dashboard',
|
||||
@@ -16,269 +11,6 @@ export class ProjectDashboardComponent implements OnInit {
|
||||
private projectId;
|
||||
projectStats;
|
||||
|
||||
private pieWidth = 360;
|
||||
private pieHeight = 360;
|
||||
private pieRadius = Math.min(this.pieWidth, this.pieHeight) / 2;
|
||||
private pieArc = d3.arc()
|
||||
.innerRadius(this.pieRadius / 2)
|
||||
.outerRadius(this.pieRadius);
|
||||
|
||||
private pieFun = d3.pie().value((d) => d.count);
|
||||
private statusColor = d3.scaleOrdinal().range(['#31a6a2', '#8c2627', '#62f24b']);
|
||||
private assigneesColor = d3.scaleOrdinal().range(["", "#AAAAAA"].concat(d3.schemePaired));
|
||||
|
||||
private statusData: any[];
|
||||
private assigneesData: any[];
|
||||
|
||||
private newTaskCounts: any[] = [];
|
||||
private failedTaskCounts: any[] = [];
|
||||
private closedTaskCounts: any[] = [];
|
||||
|
||||
private newTaskPath: any;
|
||||
private failedTaskPath: any;
|
||||
private closedTaskPath: any;
|
||||
|
||||
private maxY: number = 10;
|
||||
private yAxis: any;
|
||||
private yScale: any;
|
||||
private xScale: any;
|
||||
private range: number;
|
||||
private line: any;
|
||||
|
||||
private statusPath: any;
|
||||
private statusSvg: any;
|
||||
private assigneesPath: any;
|
||||
private assigneesSvg: any;
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
setupStatusPieChart() {
|
||||
let tooltip = d3.select("#stooltip");
|
||||
|
||||
this.statusSvg = d3.select("#status")
|
||||
.append('svg')
|
||||
.attr('width', this.pieWidth)
|
||||
.attr('height', this.pieHeight)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")");
|
||||
|
||||
this.statusPath = this.statusSvg.selectAll()
|
||||
.data(this.pieFun(this.statusData))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('d', this.pieArc)
|
||||
.attr('fill', (d) => this.statusColor(d.data.label));
|
||||
|
||||
this.setupToolTip(this.statusPath, tooltip)
|
||||
}
|
||||
|
||||
setupAssigneesPieChart() {
|
||||
let tooltip = d3.select("#atooltip");
|
||||
|
||||
this.assigneesSvg = d3.select("#assignees")
|
||||
.append('svg')
|
||||
.attr('width', this.pieWidth)
|
||||
.attr('height', this.pieHeight)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + this.pieRadius + "," + this.pieRadius + ")");
|
||||
|
||||
this.assigneesPath = this.assigneesSvg.selectAll()
|
||||
.data(this.pieFun(this.assigneesData))
|
||||
.enter()
|
||||
.append('path')
|
||||
.attr('d', this.pieArc)
|
||||
.attr('fill', (d) => this.assigneesColor(d.data.label));
|
||||
|
||||
this.setupToolTip(this.assigneesPath, tooltip)
|
||||
}
|
||||
|
||||
setupToolTip(x, tooltip) {
|
||||
x.on('mouseover', (d) => {
|
||||
let total = d3.sum(this.assigneesData.map((d) => d.count));
|
||||
let percent = Math.round(1000 * d.data.count / total) / 10;
|
||||
tooltip.select('.label').html(d.data.label);
|
||||
tooltip.select('.count').html(d.data.count);
|
||||
tooltip.select('.percent').html(percent + '%');
|
||||
tooltip.style('display', 'block');
|
||||
});
|
||||
x.on('mouseout', function () {
|
||||
tooltip.style('display', 'none');
|
||||
})
|
||||
}
|
||||
|
||||
setupLine() {
|
||||
let margin = {top: 50, right: 50, bottom: 50, left: 50};
|
||||
this.range = 600;
|
||||
let width = 750;
|
||||
let height = 250;
|
||||
|
||||
this.xScale = d3.scaleLinear()
|
||||
.domain([this.range, 0])
|
||||
.range([width, 0]);
|
||||
|
||||
this.yScale = d3.scaleLinear()
|
||||
.domain([0, this.maxY])
|
||||
.range([height, 0]);
|
||||
|
||||
this.line = d3.line()
|
||||
.x((d, i) => this.xScale(i))
|
||||
.y((d) => this.yScale(d.y))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
let svg = d3.select("#line").append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
svg.append("defs").append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
this.newTaskPath = svg
|
||||
.append("path")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.datum(this.newTaskCounts)
|
||||
.attr("class", "line-new")
|
||||
.attr("d", this.line);
|
||||
this.failedTaskPath = svg
|
||||
.append("path")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.datum(this.failedTaskCounts)
|
||||
.attr("class", "line-failed")
|
||||
.attr("d", this.line);
|
||||
this.closedTaskPath = svg
|
||||
.append("path")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.datum(this.closedTaskCounts)
|
||||
.attr("class", "line-closed")
|
||||
.attr("d", this.line);
|
||||
|
||||
let xAxis = svg.append("g")
|
||||
.attr("class", "x-axis")
|
||||
.attr("transform", "translate(0," + height + ")");
|
||||
xAxis.call(d3.axisBottom(this.xScale).tickFormat((d) => (d - 600) + "s"));
|
||||
|
||||
this.yAxis = svg.append("g")
|
||||
.attr("class", "y-axis");
|
||||
this.yAxis.call(d3.axisLeft(this.yScale));
|
||||
}
|
||||
|
||||
getStats() {
|
||||
this.apiService.getProjectStats(this.projectId).subscribe((data) => {
|
||||
|
||||
this.projectStats = data["stats"];
|
||||
|
||||
this.updateLine();
|
||||
this.updatePie();
|
||||
});
|
||||
}
|
||||
|
||||
private updateLine() {
|
||||
|
||||
let newVal = {"y": this.projectStats["new_task_count"]};
|
||||
let failedVal = {"y": this.projectStats["failed_task_count"]};
|
||||
let closedVal = {"y": this.projectStats["closed_task_count"]};
|
||||
|
||||
//Adjust y axis
|
||||
this.maxY = Math.max(newVal["y"], this.maxY);
|
||||
this.yScale.domain([0, this.maxY]);
|
||||
this.yAxis.call(d3.axisLeft(this.yScale));
|
||||
|
||||
this.newTaskPath
|
||||
.attr("d", this.line)
|
||||
.attr("transform", null);
|
||||
this.failedTaskPath
|
||||
.attr("d", this.line)
|
||||
.attr("transform", null);
|
||||
this.closedTaskPath
|
||||
.attr("d", this.line)
|
||||
.attr("transform", null);
|
||||
|
||||
//remove fist element
|
||||
if (this.newTaskCounts.length >= this.range) {
|
||||
this.newTaskCounts.shift();
|
||||
this.newTaskPath
|
||||
.transition()
|
||||
.attr("transform", "translate(" + this.xScale(-1) + ")");
|
||||
}
|
||||
if (this.failedTaskCounts.length >= this.range) {
|
||||
this.failedTaskCounts.shift();
|
||||
this.failedTaskPath
|
||||
.transition()
|
||||
.attr("transform", "translate(" + this.xScale(-1) + ")");
|
||||
}
|
||||
if (this.closedTaskCounts.length >= this.range) {
|
||||
this.closedTaskCounts.shift();
|
||||
this.closedTaskPath
|
||||
.transition()
|
||||
.attr("transform", "translate(" + this.xScale(-1) + ")");
|
||||
}
|
||||
|
||||
this.newTaskCounts.push(newVal);
|
||||
this.failedTaskCounts.push(failedVal);
|
||||
this.closedTaskCounts.push(closedVal);
|
||||
}
|
||||
|
||||
private updatePie() {
|
||||
this.statusData = [
|
||||
{label: "New", count: this.projectStats["new_task_count"]},
|
||||
{label: "Failed", count: this.projectStats["failed_task_count"]},
|
||||
{label: "Closed", count: this.projectStats["closed_task_count"]},
|
||||
];
|
||||
this.assigneesData = _.map(this.projectStats["assignees"], assignedTask => {
|
||||
return {
|
||||
label: assignedTask["assignee"],
|
||||
count: assignedTask["task_count"],
|
||||
}
|
||||
});
|
||||
|
||||
this.statusSvg.selectAll("path")
|
||||
.data(this.pieFun(this.statusData));
|
||||
this.statusPath
|
||||
.attr('d', this.pieArc)
|
||||
.attr('fill', (d) => this.statusColor(d.data.label));
|
||||
this.assigneesSvg.selectAll("path")
|
||||
.data(this.pieFun(this.assigneesData));
|
||||
this.assigneesPath
|
||||
.attr('d', this.pieArc)
|
||||
.attr('fill', (d) => this.assigneesColor(d.data.label));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.statusData = [
|
||||
{label: 'new', count: 0},
|
||||
{label: 'failed', count: 0},
|
||||
{label: 'closed', count: 0},
|
||||
];
|
||||
this.assigneesData = [
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
{label: 'null', count: 0},
|
||||
];
|
||||
|
||||
this.setupStatusPieChart();
|
||||
this.setupAssigneesPieChart();
|
||||
this.setupLine();
|
||||
|
||||
this.route.params.subscribe(params => {
|
||||
this.projectId = params["id"];
|
||||
this.getStats();
|
||||
interval(1000).subscribe(() => {
|
||||
// this.getStats()
|
||||
})
|
||||
}
|
||||
)
|
||||
ngOnInit(): void {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<mat-accordion>
|
||||
<mat-expansion-panel *ngFor="let stats of projects">
|
||||
<mat-expansion-panel *ngFor="let project of projects">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>{{stats.project.id}}: {{stats.project.name}}</mat-panel-title>
|
||||
<mat-panel-description>{{stats.project.motd}}</mat-panel-description>
|
||||
<mat-panel-title>{{project.id}}: {{project.name}}</mat-panel-title>
|
||||
<mat-panel-description>{{project.motd}}</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<pre>{{stats.project | json}}</pre>
|
||||
<pre>{{project | json}}</pre>
|
||||
<div style="display: flex;">
|
||||
<a [routerLink]="'/project/' + stats.project.id">Dashboard</a>
|
||||
<a [routerLink]="'/project/' + stats.project.id + '/update'">Update</a>
|
||||
<a [routerLink]="'/project/' + project.id">Dashboard</a>
|
||||
<a [routerLink]="'/project/' + project.id + '/update'">Update</a>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {ApiService} from "../api.service";
|
||||
import {Project} from "../models/project";
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-list',
|
||||
@@ -11,14 +12,14 @@ export class ProjectListComponent implements OnInit {
|
||||
constructor(private apiService: ApiService) {
|
||||
}
|
||||
|
||||
projects: any[];
|
||||
projects: Project[];
|
||||
|
||||
ngOnInit() {
|
||||
this.getProjects()
|
||||
}
|
||||
|
||||
getProjects() {
|
||||
this.apiService.getProjects().subscribe(data => this.projects = data["stats"]);
|
||||
this.apiService.getProjects().subscribe(data => this.projects = data["projects"]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class UpdateProjectComponent implements OnInit {
|
||||
private router: Router) {
|
||||
}
|
||||
|
||||
private project: Project;
|
||||
project: Project;
|
||||
private projectId: number;
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
6
web/angular/src/assets/i18n/en.json
Normal file
6
web/angular/src/assets/i18n/en.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"nav": {
|
||||
"title": "task_tracker",
|
||||
"langSelect": "Language"
|
||||
}
|
||||
}
|
||||
6
web/angular/src/assets/i18n/fr.json
Normal file
6
web/angular/src/assets/i18n/fr.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"nav": {
|
||||
"title": "task_tracker (fr)",
|
||||
"langSelect": "Langue"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user