File

projects/web-mev/src/app/features/analysis/components/executed-operation/executed-operation.component.ts

Description

Executed Operation Component used for displaying the list of executed operations (tree) and their results

Implements

OnInit

Metadata

changeDetection ChangeDetectionStrategy.Default
selector mev-executed-operation
styleUrls ./executed-operation.component.scss
templateUrl ./executed-operation.component.html

Index

Properties
Methods
Inputs

Constructor

constructor(route: ActivatedRoute, apiService: AnalysesService)
Parameters :
Name Type Optional
route ActivatedRoute No
apiService AnalysesService No

Inputs

execOperationId
Type : string

Methods

getExecOperationByOperationId
getExecOperationByOperationId(operationId: string)
Parameters :
Name Type Optional
operationId string No
Returns : any
getOutputs
getOutputs(operationId)

Find the operation data by OperationID

Parameters :
Name Optional
operationId No
Returns : any
isOperationExecuting
isOperationExecuting(operation)

Function to check if an operation is still executing

Parameters :
Name Optional
operation No
Returns : boolean
isOperationFailed
isOperationFailed(operation)

Function to check if an operation failed

Parameters :
Name Optional
operation No
Returns : boolean
Public ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()

Initialize the datasource for the Operation Tree

Returns : void
onSelectExecOperation
onSelectExecOperation(execOperation)

Function is triggered when the user selects an operation in the tree

Parameters :
Name Optional
execOperation No
Returns : void
updateOperationExecutionState
updateOperationExecutionState(status)

Update the operation execution state depending on the server response

Parameters :
Name Optional
status No
Returns : void

Properties

Private _transformer
Default value : () => {...}
activeNode
Type : Operation
categories
Default value : new Set()
data
dataSource
Default value : new MatTreeFlatDataSource(this.treeControl, this.treeFlattener)
execOperationResult
execOperations
Private executedOperationResultSubscription
Type : Subscription
Default value : new Subscription()
hasChild
Default value : () => {...}
operationExecutionState
Type : operationExecution
operationExecutionStateType
Default value : operationExecution
outputs
selectedExecOperationId
Type : string
selectedExecOperationName
Type : string
subcategories
Type : []
Default value : []
treeControl
Default value : new FlatTreeControl<ExampleFlatNode>( node => node.level, node => node.expandable )
treeFlattener
Default value : new MatTreeFlattener( this._transformer, node => node.level, node => node.expandable, node => node.children )
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>

./executed-operation.component.scss

.container {
  font-size: 14px !important;
  max-width: 98%;
  margin: 20px;
}

.operation-status {
  font-size: 14px;
  color: #666;
}

.operation-container {
  margin: 0 20px;
  width: 98%;
}

.mat-form-field {
  width: 350px;
}

::ng-deep {
  .category label {
    text-transform: uppercase;
  }

  .subcategory label {
    padding-left: 40px;
  }

  .operation span {
    padding-left: 50px;
  }
}

.vertical-tab-container {
  margin: 20px 0;
  display: flex;
}

mat-tree {
  background-color: #ddebf2 !important;
  width: 300px;
  min-height: 500px;

  mat-tree-node {
    padding-right: 15px;
  }

  button {
    display: flex;
    align-items: center;
    border: 0;
    background: none;
    white-space: normal;
    padding-right: 15px;
    padding-left: 15px;
    text-align: left;
    overflow: hidden;

    &:hover {
      color: grey;
    }

    .mat-icon--error {
      font-size: 16px;
      color: #b94e4e;
    }

    .mat-icon--timer {
      font-size: 16px;
    }
  }

  .active-node {
    font-weight: bold;
    background-color: #eee !important;
  }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""