import {
Component,
OnInit,
ChangeDetectionStrategy,
Input
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { AnalysesService } from '../../services/analysis.service';
import { switchMap, tap } from 'rxjs/operators';
import { of, Subscription } from 'rxjs';
import { FlatTreeControl } from '@angular/cdk/tree';
import { Operation } from '../../models/operation';
import {
MatTreeFlattener,
MatTreeFlatDataSource
} from '@angular/material/tree';
/**
* Operation category with nested structure.
* Each node has a name and an optional list of children.
*/
interface OperationCategoryNode {
name: string;
children?: Operation[];
}
/**
* Flat node with expandable and level information
*/
interface ExampleFlatNode {
expandable: boolean;
name: string;
level: number;
}
/**
* Presentational states of operation execution process
*/
enum operationExecution {
InProcess,
RunningAnalysis,
PreparingData,
Finished
}
/**
* Executed Operation Component
* used for displaying the list of executed operations (tree) and their results
*/
@Component({
selector: 'mev-executed-operation',
templateUrl: './executed-operation.component.html',
styleUrls: ['./executed-operation.component.scss'],
changeDetection: ChangeDetectionStrategy.Default
})
export class ExecutedOperationComponent implements OnInit {
private executedOperationResultSubscription: Subscription = new Subscription();
execOperations;
execOperationResult;
selectedExecOperationId: string;
selectedExecOperationName: string;
data;
@Input() execOperationId: string;
outputs;
operationExecutionStateType = operationExecution;
operationExecutionState: operationExecution;
categories = new Set();
subcategories = [];
activeNode: Operation;
treeControl = new FlatTreeControl<ExampleFlatNode>(
node => node.level,
node => node.expandable
);
hasChild = (_: number, node: ExampleFlatNode) => node.expandable;
private _transformer = (node: OperationCategoryNode, level: number) => {
return {
expandable: !!node.children && node.children.length > 0,
level: level,
...node
};
};
treeFlattener = new MatTreeFlattener(
this._transformer,
node => node.level,
node => node.expandable,
node => node.children
);
dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
constructor(
private route: ActivatedRoute,
private apiService: AnalysesService
) {}
/**
* Initialize the datasource for the Operation Tree
*/
ngOnInit(): void {
const operationTree = {};
const workspaceId = this.route.snapshot.paramMap.get('workspaceId');
this.executedOperationResultSubscription = this.apiService
.getExecOperations(workspaceId)
.pipe(
tap(operations => {
this.execOperations = operations;
operations.forEach(execOperation => {
execOperation = { ...execOperation, name: execOperation.job_name };
const categories = execOperation.operation.categories;
const operation = execOperation.operation.operation_name;
categories.forEach(category => {
if (category in operationTree) {
const level = operationTree[category];
if (operation in level) {
level[operation].push(execOperation);
} else {
level[operation] = [execOperation];
}
} else {
const level = {};
level[operation] = [execOperation];
operationTree[category] = level;
}
});
const operationTreeArr = [];
for (const elem in operationTree) {
const categoryLevel = { name: elem, children: [] };
for (const subElem in operationTree[elem]) {
categoryLevel.children.push({
name: subElem,
children: operationTree[elem][subElem]
});
}
operationTreeArr.push(categoryLevel);
}
operationTreeArr.sort(function(a, b) {
var nameA = a.name.toUpperCase();
var nameB = b.name.toUpperCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
this.dataSource.data = operationTreeArr.sort();
});
}),
switchMap(() => {
if (this.execOperationId) {
this.operationExecutionState = operationExecution.InProcess;
this.selectedExecOperationId = this.execOperationId;
const execOperation = this.getExecOperationByOperationId(
this.execOperationId
);
// expand the parent nodes if there is an operation selected
const nodesToExpand = [
execOperation.operation.operation_name,
...execOperation.operation.categories
];
this.treeControl.dataNodes.forEach((node, i) => {
if (nodesToExpand.includes(node.name)) {
this.treeControl.expand(this.treeControl.dataNodes[i]);
}
});
this.activeNode = execOperation;
this.selectedExecOperationName = execOperation.job_name;
// if the opertion has been already completed, just extract its data from the list
if (execOperation?.outputs || execOperation?.error_messages) {
this.outputs = {
operation: execOperation.operation,
job_name: execOperation.job_name,
...execOperation.outputs,
...execOperation.inputs,
error_messages: execOperation.error_messages
};
return of({ body: execOperation });
}
return this.apiService.getExecutedOperationResult(
this.execOperationId
);
}
return of();
})
)
.subscribe((response: any) => {
this.updateOperationExecutionState(response.status);
this.outputs = {
operation: response?.body?.operation,
job_name: response?.body?.job_name,
...response?.body?.outputs,
...response?.body?.inputs,
error_messages: response?.body?.error_messages
};
});
}
/**
* Find the operation data by OperationID
*/
getOutputs(operationId) {
const idx = this.execOperations.findIndex(val => val.id === operationId);
return this.execOperations[idx].outputs;
}
/**
* Function is triggered when the user selects an operation in the tree
*/
onSelectExecOperation(execOperation) {
this.activeNode = execOperation;
this.operationExecutionState = operationExecution.InProcess;
this.executedOperationResultSubscription.unsubscribe();
this.execOperationId = execOperation.id;
this.selectedExecOperationName = execOperation.job_name;
// if the operation has been already completed, just extract its data from the list
if (execOperation.outputs || execOperation.error_messages) {
this.outputs = {
operation: execOperation.operation,
job_name: execOperation.job_name,
...execOperation.outputs,
...execOperation.inputs,
error_messages: execOperation.error_messages
};
this.operationExecutionState = operationExecution.Finished;
} else {
this.executedOperationResultSubscription = this.apiService
.getExecutedOperationResult(this.execOperationId)
.subscribe(response => {
this.outputs = {
operation: response?.body?.operation,
job_name: response?.body?.job_name,
...response?.body?.outputs,
...response?.body?.inputs,
error_messages: response?.body?.error_messages
};
this.updateOperationExecutionState(response.status);
});
}
}
/**
* Update the operation execution state depending on the server response
*/
updateOperationExecutionState(status) {
if (status === 204) {
this.operationExecutionState = operationExecution.RunningAnalysis;
} else if (status === 202 || status === 208) {
this.operationExecutionState = operationExecution.PreparingData;
} else {
this.operationExecutionState = operationExecution.Finished;
this.execOperations.filter(
op => op.id === this.execOperationId
)[0].execution_stop_datetime = new Date();
}
}
/**
* Function to check if an operation failed
*/
isOperationFailed(operation): boolean {
return operation.job_failed;
}
/**
* Function to check if an operation is still executing
*/
isOperationExecuting(operation): boolean {
if (operation.execution_stop_datetime) return false;
// if execution_stop_datetime is null, check if the list of operations has an updated value
const arr = this.execOperations.filter(op => op.id === operation.id);
if (arr.length) {
return !arr[0].execution_stop_datetime;
}
return true;
}
getExecOperationByOperationId(operationId: string) {
const idx = this.execOperations.findIndex(val => val.id === operationId);
return this.execOperations[idx];
}
public ngOnDestroy(): void {
this.executedOperationResultSubscription.unsubscribe();
}
}
<section class="container">
<p>
The analysis pane on the left side displays the list of executed opertations.
Expand and collapse the list of operations clicking on an operation category.
Click on an operation to view results.
</p>
<p *ngIf="dataSource.data.length === 0">
No executed operations available
</p>
<div *ngIf="dataSource.data.length !== 0" class="vertical-tab-container">
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
<!-- tree node template for leaf nodes -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding [ngClass]="{ 'active-node': activeNode?.id === node.id }">
<!-- use a disabled button to provide padding for tree leaf -->
<!-- <button mat-icon-button disabled></button> -->
<button (click)="onSelectExecOperation(node)">
{{ node.name }}
<mat-icon *ngIf="isOperationExecuting(node)" class="mat-icon--timer">timer</mat-icon>
<mat-icon *ngIf="node.job_failed" class="mat-icon--error">error_outline</mat-icon>
</button>
</mat-tree-node>
<!-- tree node template for expandable nodes -->
<mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding>
<button matTreeNodeToggle [attr.aria-label]="'Toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
{{node.name}}
</button>
</mat-tree-node>
</mat-tree>
<mat-divider [vertical]="true"></mat-divider>
<div class="operation-container">
<!-- <div *ngIf="selectedExecOperationId && isInProgress" class="operation-status">Building chart, please wait...</div> -->
<div *ngIf="operationExecutionState === operationExecutionStateType.RunningAnalysis" class="operation-status">
Running analysis, please wait...
</div>
<div *ngIf="operationExecutionState === operationExecutionStateType.PreparingData" class="operation-status">
Preparing data and visualization...
</div>
<div *ngIf="operationExecutionState === operationExecutionStateType.InProcess" class="operation-status">
Please wait...
</div>
<mev-analysis-result *ngIf="operationExecutionState === operationExecutionStateType.Finished" [outputs]="outputs"></mev-analysis-result>
</div>
</div>
</section>