387 lines
11 KiB
TypeScript
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;
|
|
}
|
|
|
|
|
|
}
|