dnet-docker/dnet-app/frontends/is/src/app/wf-history/wf-history.component.ts

387 lines
11 KiB
TypeScript

import { HttpParams } from '@angular/common/http';
import { AfterViewInit, Component, ElementRef, Inject, Injectable, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Params } from '@angular/router';
import { combineLatest } from 'rxjs';
import { ISClient } from '../common/is.client';
import { KeyValue, WfHistoryEntry } from '../common/is.model';
import { MatSnackBar } from '@angular/material/snack-bar';
import mermaid from 'mermaid';
@Component({
selector: 'app-wf-history',
templateUrl: './wf-history.component.html',
styleUrls: []
})
export class WfHistoryComponent implements OnInit {
entries: WfHistoryEntry[] = [];
total: number = 100
from?: Date;
to?: Date;
constructor(public client: WfHistoryClient, public route: ActivatedRoute, public dialog: MatDialog) {
}
ngOnInit() {
combineLatest([this.route.params, this.route.queryParams],
(params: Params, queryParams: Params) => ({ params, queryParams })
).subscribe((res: { params: Params; queryParams: Params }) => {
const { params, queryParams } = res;
let totalP = queryParams['total'];
let fromP = queryParams['from'];
let toP = queryParams['to'];
if (totalP) { this.total = parseInt(totalP); }
if (fromP) { this.from = new Date(fromP); }
if (toP) { this.to = new Date(toP) }
this.client.loadWfHistory(this.total, this.from, this.to, (data: WfHistoryEntry[]) => this.entries = data);
});
}
}
@Component({
selector: 'wf-dialog',
templateUrl: './wf-history.dialog.html',
styleUrls: []
})
export class WfHistoryDialog implements OnInit {
dsId: string = '';
apiId: string = '';
confId: string = '';
processId: string = '';
mode: number = 1;
entries: WfHistoryEntry[] = [];
constructor(public client: WfHistoryClient, public dialogRef: MatDialogRef<WfHistoryDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public dialog: MatDialog, public snackBar: MatSnackBar) {
this.dsId = data.dsId;
this.apiId = data.apiId;
this.confId = data.confId;
this.processId = data.processId;
if (this.processId) { this.mode = 4 }
else if (this.confId) { this.mode = 3 }
else if (this.apiId) { this.mode = 2; }
else if (this.dsId) { this.mode = 1 }
else {
this.snackBar.open("One of dsId, apiId, confId or processId is expected", 'ERROR', { duration: 5000 });
}
}
ngOnInit() {
this.reload(this.mode);
};
reload(mode: number) {
this.mode = mode;
if (mode == 1) {
this.client.recentHistory(this.dsId, 1, (data: WfHistoryEntry[]) => this.entries = data);
} else if (mode == 2) {
this.client.recentHistory(this.apiId, 2, (data: WfHistoryEntry[]) => this.entries = data);
} else if (mode == 3) {
this.client.recentHistory(this.confId, 3, (data: WfHistoryEntry[]) => this.entries = data);
} else if (mode == 4) {
this.client.recentHistory(this.processId, 4, (data: WfHistoryEntry[]) => this.entries = data);
} else {
this.snackBar.open("invalid mode", 'ERROR', { duration: 5000 });
return;
}
}
}
@Component({
selector: 'wf-history-table',
templateUrl: './wf-history-table.component.html',
styleUrls: []
})
export class WfHistoryTableComponent implements AfterViewInit, OnInit, OnChanges {
@Input() entries: WfHistoryEntry[] = [];
historyDatasource: MatTableDataSource<WfHistoryEntry> = new MatTableDataSource<WfHistoryEntry>([]);
colums: string[] = ['graph', 'processId', 'name', 'family', 'dsName', 'status', 'startDate', 'endDate'];
total: number = 100
from?: Date;
to?: Date;
constructor(public dialog: MatDialog) { }
ngOnInit() {
this.historyDatasource.data = this.entries;
};
ngOnChanges() {
this.historyDatasource.data = this.entries;
}
@ViewChild(MatSort) sort: MatSort | undefined
ngAfterViewInit() {
if (this.sort) this.historyDatasource.sort = this.sort;
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.historyDatasource.filter = filterValue;
}
openWfDialog(wf: WfHistoryEntry): void {
const wfDialogRef = this.dialog.open(WfHistoryDetailsDialog, { data: wf });
}
openGraphDialog(wf: WfHistoryEntry): void {
const wfDialogRef = this.dialog.open(WfRuntimeGraphDialog, { data: wf, width: '80%' });
}
}
@Component({
selector: 'wf-history-details',
templateUrl: 'wf-history-details.dialog.html',
styleUrls: []
})
export class WfHistoryDetailsDialog {
startDate: string = '';
endDate: string = '';
duration: string = '';
wfDatasource: MatTableDataSource<KeyValue> = new MatTableDataSource<KeyValue>([]);
colums: string[] = ['k', 'v'];
selectedElement?: KeyValue;
constructor(
public dialogRef: MatDialogRef<WfHistoryDetailsDialog>,
@Inject(MAT_DIALOG_DATA) public data: WfHistoryEntry,
) {
let list: KeyValue[] = [];
for (let [key, value] of new Map(Object.entries(data.inputParams))) {
list.push({ k: '[INPUT] ' + key, v: value });
}
for (let [key, value] of new Map(Object.entries(data.outputParams))) {
list.push({ k: '[OUTPUT] ' + key, v: value });
}
this.wfDatasource.data = list;
this.startDate = data.startDate;
this.endDate = data.endDate;
this.duration = this.calculateDateDiff(new Date(data.startDate), new Date(data.endDate));
}
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
this.wfDatasource.filter = filterValue;
}
onNoClick(): void {
this.dialogRef.close();
}
selectElement(elem: KeyValue) {
this.selectedElement = elem;
}
calculateDateDiff(from: Date, to: Date): string {
let start = from.getTime();
let end = to.getTime();
if (start <= 0 || end <= 0) {
return '-';
}
var seconds = 0;
var minutes = 0;
var hours = 0;
var days = 0;
if (end > start) {
seconds = Math.round((end - start) / 1000);
if (seconds > 60) {
minutes = Math.floor(seconds / 60);
seconds = seconds % 60;
if (minutes > 60) {
hours = Math.floor(minutes / 60);
minutes = minutes % 60;
if (hours > 24) {
days = Math.floor(hours / 24);
hours = hours % 24;
}
}
}
}
var res = '';
if (days > 0) {
if (res) { res += ', '; }
res += days + " day(s)"
}
if (hours > 0) {
if (res) { res += ', '; }
res += hours + " hour(s)"
}
if (minutes > 0) {
if (res) { res += ', '; }
res += minutes + " minute(s)"
}
if (seconds > 0) {
if (res) { res += ', '; }
res += seconds + " second(s)"
}
if (!res) {
res = '0 seconds';
}
return res;
}
}
@Injectable({
providedIn: 'root'
})
export class WfHistoryClient extends ISClient {
loadWfHistory(total: number, from: Date | undefined, to: Date | undefined, onSuccess: Function): void {
let params = new HttpParams();
if (total && total > 0) { params = params.append('total', total); }
if (from) { params = params.append('from', from.toLocaleDateString()); }
if (to) { params = params.append('to', to.toLocaleDateString()); }
this.httpGetWithOptions<WfHistoryEntry[]>('/proxy/byType/wf_manager/api/history', { params: params }, onSuccess);
}
recentHistory(id: string, mode: number, onSuccess: Function) {
let url = '/proxy/byType/wf_manager/api';
if (mode == 1) {
url += '/history/byDsId/' + encodeURIComponent(id);
} else if (mode == 2) {
url += '/history/byApiId/' + encodeURIComponent(id);
} else if (mode == 3) {
url += '/history/byConf/' + encodeURIComponent(id);
} else if (mode == 4) {
url += '/proc/' + encodeURIComponent(id);
} else {
this.snackBar.open("invalid mode", 'ERROR', { duration: 5000 });
return;
}
this.httpGet(url, onSuccess);
}
findProcess(id: string, onSuccess: Function) {
this.httpGet<WfHistoryEntry>('/proxy/byType/wf_manager/api/proc/' + encodeURIComponent(id), onSuccess);
}
}
@Component({
selector: 'wf-runtime-graph-dialog',
templateUrl: './wf-runtime-graph.dialog.html',
styleUrls: []
})
export class WfRuntimeGraphDialog implements AfterViewInit {
wf: WfHistoryEntry;
@ViewChild('mermaidGraph', { static: false }) mermaidGraph!: ElementRef;
constructor(public dialogRef: MatDialogRef<WfHistoryDialog>, @Inject(MAT_DIALOG_DATA) public data: any, public dialog: MatDialog, public snackBar: MatSnackBar, public client: WfHistoryClient) {
this.wf = data;
}
public ngAfterViewInit(): void {
this.updateGraph();
}
public refresh(): void {
this.client.findProcess(this.wf.processId, (data: WfHistoryEntry) => {
this.wf = data;
this.updateGraph();
});
}
private updateGraph(): void {
/* flowchart TD
A[Start] --> B{Is it?}
B -- Yes --> C[OK]
C --> D[Rethink]
D --> B
B -- No ----> E[End]
*/
let code = "%%{ init: { 'theme': 'base', 'themeVariables': { 'fontSize' : '11px' } } }%%\n";
let cssMap: any = [];
cssMap['ready'] = 'graphReadyNode';
cssMap['failed'] = 'graphFailedNode';
cssMap['completed'] = 'graphCompletedNode';
cssMap['running'] = 'graphRunningNode';
let cssJoinMap: any = [];
cssJoinMap['ready'] = 'graphReadyJoinNode';
cssJoinMap['failed'] = 'graphFailedJoinNode';
cssJoinMap['completed'] = 'graphCompletedJoinNode';
cssJoinMap['running'] = 'graphRunningJoinNode';
code += 'flowchart TD\n';
code += "\tstart([" + this.graphNodeText('start') + "])\n";
code += "\tclass start graphStartNode\n";
for (const [key, value] of Object.entries(this.wf.graph.nodes)) {
let from: string = key;
let node: any = value;
if (node.start) { code += "\tstart --> " + from + "\n"; }
code += "\t" + from;
if (node.join && node.name != 'success') {
code += '{{' + this.graphNodeText(from, node.type, node.progressMessage) + '}}';
code += "\n\tclass " + from + " " + cssJoinMap[node.status];
} else {
code += '(' + this.graphNodeText(from, node.type, node.progressMessage) + ')';
code += "\n\tclass " + from + " " + cssMap[node.status];
}
code += "\n";
this.wf.graph.arcs.forEach((arc: any) => {
if (arc.from == from) {
code += "\t\t" + from;
if (arc.condition) { code += ' -- ' + arc.condition; }
code += ' --> ' + arc.to + "\n";
}
});
};
code += "classDef default font-size:9pt\n";
const element: Element = this.mermaidGraph.nativeElement;
element.innerHTML = code;
element.removeAttribute('data-processed');
mermaid.initialize({ theme: "base", flowchart: { titleTopMargin: 0, useMaxWidth: false, htmlLabels: false } });
mermaid.run();
}
graphNodeText(name: string, type?: string, message?: string) {
let res = '"`' + name;
if (type) { res += '\n**(' + type + ')**'; }
if (message) { res += '\n**' + message + '**'; }
res += '`"';
return res;
}
}