monitoring setup, project dashboard, account page

This commit is contained in:
simon987
2019-02-09 13:16:58 -05:00
parent f577e76afa
commit 4ef4752c14
28 changed files with 629 additions and 160 deletions

View File

@@ -1,3 +1,29 @@
<pre>
{{authService.account | json}}
</pre>
<div class="container">
<mat-card class="mat-elevation-z8" *ngIf="account">
<mat-card-header>
<mat-card-title>{{"account.title" | translate}}</mat-card-title>
<mat-card-subtitle>{{"account.subtitle" | translate}}</mat-card-subtitle>
</mat-card-header>
 
<mat-card-content>
<mat-list>
<mat-list-item>
{{"account.username" | translate}}:&nbsp;
<pre>{{account.username}}</pre>
</mat-list-item>
</mat-list>
<mat-expansion-panel>
<mat-expansion-panel-header>{{"account.metadata" | translate}}</mat-expansion-panel-header>
<pre> {{account | json}}</pre>
</mat-expansion-panel>
</mat-card-content>
<mat-card-actions>
</mat-card-actions>
</mat-card>
</div>

View File

@@ -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;
}
}

View File

@@ -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)
}
}

View File

@@ -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: [],

View File

@@ -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);
}
)
}
}

View File

@@ -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 {

View File

@@ -10,3 +10,8 @@
.mat-cell {
text-align: left;
}
.checkbox-wrapper mat-checkbox {
margin: 3px 5px;
vertical-align: middle;
}

View File

@@ -1,22 +1,31 @@
<div class="container">
<div class="table-container">
<mat-form-field>
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
</mat-form-field>
<div class="checkbox-wrapper">
<mat-form-field style="margin-right: 10px">
<input matInput (keyup)="applyFilter($event.target.value)" [placeholder]="'logs.filter' | translate">
</mat-form-field>
<mat-checkbox>{{"logs.fatal" | translate}}</mat-checkbox>
<mat-checkbox>{{"logs.panic" | translate}}</mat-checkbox>
<mat-checkbox>{{"logs.error" | translate}}</mat-checkbox>
<mat-checkbox>{{"logs.warn" | translate}}</mat-checkbox>
<mat-checkbox>{{"logs.info" | translate}}</mat-checkbox>
<mat-checkbox>{{"logs.debug" | translate}}</mat-checkbox>
</div>
<div class="mat-elevation-z8">
<mat-table [dataSource]="data" matSort matSortActive="timestamp"
matSortDirection="desc">
<ng-container matColumnDef="level">
<mat-header-cell style="flex: 0 0 6em" mat-sort-header
<mat-header-cell style="flex: 0 0 9em" mat-sort-header
*matHeaderCellDef>{{"logs.level" | translate}}</mat-header-cell>
<mat-cell style="flex: 0 0 6em" *matCellDef="let entry"> {{entry.level}} </mat-cell>
<mat-cell style="flex: 0 0 8em"
*matCellDef="let entry"> {{("logs." + entry.level) | translate}} </mat-cell>
</ng-container>
<ng-container matColumnDef="timestamp">
<mat-header-cell style="flex: 0 0 21em" mat-sort-header
<mat-header-cell style="flex: 0 0 15em" mat-sort-header
*matHeaderCellDef>{{"logs.time" | translate}}</mat-header-cell>
<mat-cell style="flex: 0 0 17em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell>
<mat-cell style="flex: 0 0 12em" *matCellDef="let entry"> {{entry.timestamp}} </mat-cell>
</ng-container>
<ng-container matColumnDef="message">
<mat-header-cell mat-sort-header *matHeaderCellDef>{{"logs.message" | translate}}</mat-header-cell>

View File

@@ -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 <LogEntry>{
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),
}
});
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -3,6 +3,13 @@
<mat-card-title *ngIf="project">{{"dashboard.title" | translate}} "{{project.name}}"</mat-card-title>
<mat-card-content style="padding: 2em 0 1em">
<button mat-raised-button style="float: right"
[title]="'dashboard.refresh' | translate"
(click)="refresh()"
>
<mat-icon>refresh</mat-icon>
</button>
<p *ngIf="project">
{{"project.git_repo" | translate}}:
<a target="_blank" [href]="project['clone_url']">{{project.git_repo}}</a>
@@ -10,12 +17,13 @@
<p>{{"project.motd" | translate}}:</p>
<pre *ngIf="project">{{project.motd}}</pre>
<div style="display: flex; align-items: center; justify-content: center">
<div id="timeline-wrapper">
<canvas id="timeline"></canvas>
</div>
<div>
<div [class.hidden]="noTasks" id="side-charts">
<div id="status-pie-wrapper">
<canvas id="status-pie"></canvas>
</div>
@@ -23,6 +31,18 @@
<canvas id="assignees-pie"></canvas>
</div>
</div>
<div *ngIf="noTasks" id="no-tasks">
<mat-icon>priority_high</mat-icon>
<p>{{"dashboard.empty" | translate}}</p>
</div>
</div>
<div id="small-screen-stats">
<p>Small screen stats</p>
<p>Latest monitoring snapshot:</p>
<pre>{{ lastSnapshot | json }}</pre>
<p>Assignees</p>
<pre>{{ assignees | json }}</pre>
</div>
<mat-expansion-panel *ngIf="project" style="margin-top: 1em">

View File

@@ -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 = <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();
});
})
})
}
}

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>

View File

@@ -78,3 +78,11 @@ body {
.mat-tab-body {
padding-top: 1em;
}
pre {
color: #616161;
}
.hidden {
display: none !important;
}