All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.app.modules.scenarios.components.execution.detail.execution.component.ts Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-FileCopyrightText: 2017-2024 Enedis
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 */

import {
    AfterViewChecked,
    AfterViewInit,
    Component,
    ElementRef,
    EventEmitter,
    Input,
    OnDestroy,
    OnInit,
    Output,
    Renderer2,
    TemplateRef,
    ViewChild
} from '@angular/core';
import { Location } from '@angular/common';
import { fromEvent, merge, Observable, Subscription, timer } from 'rxjs';
import {
    auditTime,
    debounceTime,
    delay,
    delayWhen,
    retryWhen,
    scan,
    takeWhile,
    throttleTime,
    timestamp
} from 'rxjs/operators';
import { FileSaverService } from 'ngx-filesaver';
import { NgbOffcanvas } from '@ng-bootstrap/ng-bootstrap';
import { NGX_MONACO_EDITOR_CONFIG } from 'ngx-monaco-editor-v2';

import { Authorization, Execution, GwtTestCase, ScenarioExecutionReport, StepExecutionReport } from '@model';
import { ScenarioExecutionService } from 'src/app/core/services/scenario-execution.service';
import { ExecutionStatus } from '@core/model/scenario/execution-status';
import { StringifyPipe } from '@shared/pipes';
import { findScrollContainer } from '@shared/tools';
import { TranslateService } from "@ngx-translate/core";
import { DatasetUtils } from "@shared/tools/dataset-utils";

@Component({
    selector: 'chutney-scenario-execution',
    providers: [
        Location,
        StringifyPipe,
        {
            provide: NGX_MONACO_EDITOR_CONFIG,
            useValue: {
                defaultOptions: {
                    // readOnly: true
                }
            }
        }
    ],
    templateUrl: './execution.component.html',
    styleUrls: ['./execution.component.scss']
})
export class ScenarioExecutionComponent implements OnInit, OnDestroy, AfterViewInit, AfterViewChecked {
    @Input() execution: Execution;
    @Input() scenario: GwtTestCase;
    @Input() stickyTop: string = '0';
    @Input() stickyTopElementSelector: string;
    @Output() onExecutionStatusUpdate = new EventEmitter<{ status: ExecutionStatus, error: string }>();

    Object = Object;
    ExecutionStatus = ExecutionStatus;
    Authorization = Authorization;

    executionError: String;

    scenarioExecutionReport: ScenarioExecutionReport;
    selectedStep: StepExecutionReport;

    hasContextVariables = false;
    collapseContextVariables = true;
    collapseDataset = true;

    private scenarioExecutionAsyncSubscription: Subscription;
    private resizeLeftPanelSubscription: Subscription;

    @ViewChild('leftPanel') leftPanel;
    @ViewChild('grab') grabPanel;
    @ViewChild('rightPanel') rightPanel;
    @ViewChild('reportHeader') reportHeader;

    private stickyTopElement: HTMLElement;
    private stickyTopElementHeight: number = 0;
    private stickyTopElementResizeObserver: ResizeObserver;

    constructor(
        private scenarioExecutionService: ScenarioExecutionService,
        private fileSaverService: FileSaverService,
        private stringify: StringifyPipe,
        private renderer: Renderer2,
        private offcanvasService: NgbOffcanvas,
        private elementRef: ElementRef,
        private datasetUtils: DatasetUtils,
        private translateService: TranslateService) {
    }

    ngOnInit() {
        if (this.scenario) {
            this.loadScenarioExecution(this.execution.executionId);
        } else {
            this.scenarioExecutionReport = JSON.parse(this.execution.report);
            this.afterReportUpdate();
        }
    }

    ngAfterViewInit(): void {
        if(this.leftPanel) {
            this.resizeLeftPanelSubscription = merge(
                fromEvent(window, 'resize'),
                fromEvent(findScrollContainer(this.leftPanel.nativeElement),'scroll')
            ).pipe(
                throttleTime(150),
                debounceTime(150)
            ).subscribe(() => {
                this.setLeftPanelStyle();
            });
        }

        this.setReportHeaderTop();

        if (this.stickyTopElementSelector) {
            this.stickyTopElement = document.querySelector(this.stickyTopElementSelector) as HTMLElement;
            this.stickyTopElementHeight = this.stickyTopElement.offsetHeight;

            this.stickyTopElementResizeObserver = new ResizeObserver((entries) => {
                this.stickyTopElementHeight = this.stickyTopElement.offsetHeight;
                this.setReportHeaderTop();
                this.setLeftPanelStyle();
            });
            this.stickyTopElementResizeObserver.observe(this.stickyTopElement);
        }
    }

    private setReportHeaderTop() {
        var top = '0px';
        if (this.stickyTopElement) {
            const elemHeight = this.stickyTopElement.offsetHeight;
            top += ` + ${elemHeight}px`;
        }
        top += ` + (${this.stickyTop})`;
        this.renderer.setStyle(this.reportHeader.nativeElement, 'top', `calc(${top})`);
    }

    ngAfterViewChecked(): void {
        this.setLeftPanelStyle();
    }

    ngOnDestroy() {
        this.unsubscribeScenarioExecutionAsyncSubscription();
        if (this.resizeLeftPanelSubscription) this.resizeLeftPanelSubscription.unsubscribe();
        if (this.stickyTopElementResizeObserver) this.stickyTopElementResizeObserver.unobserve(this.stickyTopElement);
    }

    loadScenarioExecution(executionId: number) {
        this.executionError = '';
        this.execution.executionId = executionId;
        this.scenarioExecutionService.findExecutionReport(this.scenario.id, executionId)
            .subscribe({
                next: (scenarioExecutionReport: ScenarioExecutionReport) => {
                    if (scenarioExecutionReport?.report?.status === ExecutionStatus.RUNNING) {
                        this.observeScenarioExecution(executionId);
                    } else {
                        this.scenarioExecutionReport = scenarioExecutionReport;
                        if(scenarioExecutionReport?.report) {
                            this.afterReportUpdate();
                        }
                    }
                },
                error: error => {
                    console.error(error.message);
                    this.executionError = 'Cannot find execution n°' + executionId;
                    this.scenarioExecutionReport = null;
                }
            });
    }

    private afterReportUpdate() {
        this.hasContextVariables = this.scenarioExecutionReport.contextVariables && Object.getOwnPropertyNames(this.scenarioExecutionReport.contextVariables).length > 0;
        this.computeAllStepRowId();
        this.selectFailedStep();
    }

    private selectFailedStep() {
        let failedStep = this.getFailureSteps(this.scenarioExecutionReport);
        if (failedStep?.length > 0) {
            timer(500).subscribe(() => {
                this.selectStep(failedStep[0], true);
            });
        }
    }

    protected getDataset(execution: ScenarioExecutionReport) {
        if (!execution || !execution.dataset) {
            return ''
        }
        return this.datasetUtils.getExecutionDatasetName(execution.dataset)
    }

    stopScenario() {
        this.scenarioExecutionService.stopScenario(this.scenario.id, this.execution.executionId).subscribe({
            error: (error) => {
                const body = JSON.parse(error._body);
                this.executionError = 'Cannot stop scenario : ' + error.status + ' ' + error.statusText + ' ' + body.message;
            }
        });
    }

    pauseScenario() {
        this.scenarioExecutionService.pauseScenario(this.scenario.id, this.execution.executionId).subscribe({
            error: (error) => {
                const body = JSON.parse(error._body);
                this.executionError = 'Cannot pause scenario : ' + error.status + ' ' + error.statusText + ' ' + body.message;
            }
        });
    }

    resumeScenario() {
        this.scenarioExecutionService.resumeScenario(this.scenario.id, this.execution.executionId)
            .pipe(
                delay(1000)
            )
            .subscribe({
                next: () => this.loadScenarioExecution(Number(this.execution.executionId)),
                error: (error) => {
                    const body = JSON.parse(error._body);
                    this.executionError = 'Cannot resume scenario : ' + error.status + ' ' + error.statusText + ' ' + body.message;
                }
            });
    }

    isRunning() {
        return ExecutionStatus.RUNNING === this.scenarioExecutionReport?.report?.status;
    }

    isPaused() {
        return ExecutionStatus.PAUSED === this.scenarioExecutionReport?.report?.status;
    }

    hasNotBeenExecuted() {
        return ExecutionStatus.NOT_EXECUTED === this.scenarioExecutionReport?.report?.status;
    }

    toggleContextVariables() {
        this.collapseContextVariables = !this.collapseContextVariables;
        timer(250).subscribe(() => this.setLefPanelHeight());
    }

    toggleDatasetVariables() {
        this.collapseDataset = !this.collapseDataset;
        timer(250).subscribe(() => this.setLefPanelHeight());
    }

    private observeScenarioExecution(executionId: number) {
        this.unsubscribeScenarioExecutionAsyncSubscription();
        this.scenarioExecutionAsyncSubscription =
            this.subscribeToScenarioExecutionReports(
                this.scenarioExecutionService.observeScenarioExecution(this.scenario.id, executionId));
    }

    private subscribeToScenarioExecutionReports(scenarioExecutionReportsObservable: Observable): Subscription {
        let executionStatus: ExecutionStatus;
        let executionError: string;
        return scenarioExecutionReportsObservable
            .pipe(
                auditTime(500),
                retryWhen(errors =>
                    errors.pipe(
                        timestamp(),
                        scan((retryCountAccumulator, error) => this.updateAccumulator(retryCountAccumulator, error), { count: 0, timestamp: 0 , lastTimestamp: 0, beforeLastTimestamp: 0}),
                        delayWhen(() => timer(500)),
                        takeWhile((retryCountAccumulator) => !this.retriedTwiceInLessThanOneSecond(retryCountAccumulator) )
                    )
                )
            )
            .subscribe({
                next: (scenarioExecutionReport: ScenarioExecutionReport) => {
                    executionStatus = ExecutionStatus[scenarioExecutionReport.report.status];
                    executionError = this.getExecutionError(scenarioExecutionReport);
                    if (this.scenarioExecutionReport) {
                        this.scenarioExecutionReport.report.duration = scenarioExecutionReport.report.duration;
                        this.updateStepExecutionReport(this.scenarioExecutionReport.report, scenarioExecutionReport.report, []);
                    } else {
                        this.scenarioExecutionReport = scenarioExecutionReport;
                    }
                    this.afterReportUpdate();
                },
                error: (error) => {
                    if (error.status) {
                        this.executionError = error.status + ' ' + error.statusText + ' ' + error._body;
                    } else {
                        this.executionError = error.error;
                        executionError = error.error;
                    }
                    this.scenarioExecutionReport = null;
                },
                complete: () => {
                    this.onExecutionStatusUpdate.emit({ status: executionStatus, error: executionError });
                    this.loadScenarioExecution(this.execution.executionId);
                }
            });
    }

    private updateAccumulator(retryCountAccumulator: { count: number; timestamp: number; lastTimestamp: number; beforeLastTimestamp: number }, error: any) {
        return {
            count: retryCountAccumulator.count + 1,
            timestamp: error.timestamp,
            lastTimestamp: retryCountAccumulator.timestamp,
            beforeLastTimestamp: retryCountAccumulator.lastTimestamp
        };
    }

    private retriedTwiceInLessThanOneSecond(retryCountAccumulator: { count: number; timestamp: number; lastTimestamp: number; beforeLastTimestamp: number }) {
        let retriedTwice = retryCountAccumulator.count > 2;
        let lastRetryWasInLessThanOneSecond = retryCountAccumulator.timestamp - retryCountAccumulator.lastTimestamp < 1000;
        let beforeLastRetryWasInLessThanOneSecond = retryCountAccumulator.lastTimestamp - retryCountAccumulator.beforeLastTimestamp < 1000;
        return retriedTwice && lastRetryWasInLessThanOneSecond && beforeLastRetryWasInLessThanOneSecond;
    }

    private getExecutionError(scenarioExecutionReport: ScenarioExecutionReport) {
        return this.getFailureSteps(scenarioExecutionReport)
            .map(step => step.errors)
            .flat()
            .toString();
    }

    private getFailureSteps(scenarioExecutionReport: ScenarioExecutionReport) {
        return scenarioExecutionReport?.report?.steps?.filter((step) => step.status === ExecutionStatus.FAILURE);
    }

    private unsubscribeScenarioExecutionAsyncSubscription() {
        if (this.scenarioExecutionAsyncSubscription) this.scenarioExecutionAsyncSubscription.unsubscribe();
    }

    private updateStepExecutionReport(oldStepExecutionReport: StepExecutionReport, newStepExecutionReport: StepExecutionReport, depths: Array) {
        if (oldStepExecutionReport.status !== newStepExecutionReport.status || (newStepExecutionReport.status === 'FAILURE' && oldStepExecutionReport.strategy === 'retry-with-timeout')) {
            if (depths.length === 0) {
                this.scenarioExecutionReport.report = newStepExecutionReport;
            } else if (depths.length === 1) {
                this.updateReport(this.scenarioExecutionReport.report.steps[depths[0]], newStepExecutionReport);
            } else {
                let stepReport = this.scenarioExecutionReport.report.steps[depths[0]];
                for (let i = 1; i < depths.length - 1; i++) {
                    stepReport = stepReport.steps[depths[i]];
                }
                this.updateReport(stepReport.steps[depths[depths.length - 1]], newStepExecutionReport);
            }
        } else {
            for (let i = 0; i < oldStepExecutionReport.steps.length; i++) {
                this.updateStepExecutionReport(oldStepExecutionReport.steps[i], newStepExecutionReport.steps[i], depths.concat(i));
            }

            if (newStepExecutionReport.steps.length > oldStepExecutionReport.steps.length) {
                for (let i = oldStepExecutionReport.steps.length; i < newStepExecutionReport.steps.length; i++) {
                    oldStepExecutionReport.steps.push(newStepExecutionReport.steps[i]);
                }
            }
        }
    }

    private updateReport(oldReport: StepExecutionReport, report: StepExecutionReport) {
        oldReport.name = report.name;
        oldReport.duration = report.duration;
        oldReport.status = report.status;
        oldReport.startDate = report.startDate;
        oldReport.information = report.information;
        oldReport.errors = report.errors;
        oldReport.type = report.type;
        oldReport.strategy = report.strategy;
        oldReport.targetName = report.targetName;
        oldReport.targetUrl = report.targetUrl;
        oldReport.evaluatedInputs = report.evaluatedInputs;
        oldReport.stepOutputs = report.stepOutputs;

        for (let i = 0; i < oldReport.steps.length; i++) {
            this.updateReport(oldReport.steps[i], report.steps[i]);
        }
    }

    exportReport() {
        const fileName = `${this.scenario.title}.execution.${this.execution.executionId}.chutney.json`;
        this.execution.report = JSON.stringify(this.scenarioExecutionReport);
        this.fileSaverService.saveText(JSON.stringify(this.execution), fileName);
    }

////////////////////////////////////////////////////// REPORT new view

    private setLeftPanelStyle() {
        if(this.leftPanel) {
            this.setLefPanelHeight();
            this.setLefPanelTop();
        }
    }

    private leftPanelHeight = 0;
    private setLefPanelHeight() {
        const leftPanelCH = this.leftPanel.nativeElement.getBoundingClientRect().y;
        if (this.leftPanelHeight != leftPanelCH) {
            this.leftPanelHeight = leftPanelCH;
            this.renderer.setStyle(this.leftPanel.nativeElement, 'height', `calc(100vh - ${this.leftPanelHeight}px)`);
        }
    }

    private leftPanelTopStateHeight = 0;
    private setLefPanelTop() {
        const newHeight = this.reportHeader.nativeElement.offsetHeight + this.stickyTopElementHeight;
        if (this.leftPanelTopStateHeight != newHeight) {
            this.leftPanelTopStateHeight = newHeight;
            this.renderer.setStyle(this.leftPanel.nativeElement, 'top', `calc(${this.stickyTop}  + ${newHeight}px + 0.5rem)`);
        }
    }

    copy(text: any) {
        navigator.clipboard.writeText( this.stringify.transform(text, {space: 4}));
    }

    private panelState = 1;
    togglePanels() {
        this.panelState = (this.panelState + 1) % 3;
        switch (this.panelState) {
            case 0:
                this.leftPanel.nativeElement.className = this.leftPanel.nativeElement.className.replace(/ d-none/g, '');
                this.leftPanel.nativeElement.style.width = '100%';
                this.grabPanel.nativeElement.className += ' d-none';
                this.rightPanel.nativeElement.className += ' d-none';
                break;
            case 1:
                this.leftPanel.nativeElement.className = this.leftPanel.nativeElement.className.replace(/ d-none/g, '');
                this.leftPanel.nativeElement.style.width = '30%';
                this.grabPanel.nativeElement.className = this.grabPanel.nativeElement.className.replace(/ d-none/g, '');
                this.rightPanel.nativeElement.className = this.rightPanel.nativeElement.className.replace(/ d-none/g, '');
                this.rightPanel.nativeElement.style.width = '70%';
                break;
            case 2:
                this.leftPanel.nativeElement.className += ' d-none';
                this.grabPanel.nativeElement.className += ' d-none';
                this.rightPanel.nativeElement.className = this.rightPanel.nativeElement.className.replace(/ d-none/g, '');
                this.rightPanel.nativeElement.style.width = '100%';
                break;
        }
    }

    togglePayloads() {
        this.prettyPrintToggle = !this.prettyPrintToggle;

        timer(250).subscribe(() => this.setLefPanelHeight());
    }

    private inOutCtxToggle_onClass = 'm-0 text-wrap text-break';
    private inOutCtxToggle_offClass = 'm-0 d-block overflow-auto';
    prettyPrintToggle = true;
    inOutCtxToggleClass(toggleValue: boolean): string {
        return toggleValue ? this.inOutCtxToggle_onClass : this.inOutCtxToggle_offClass;
    }

    toggleInputsOutputs() {
        this.toggleInputs();
        this.toggleOutputs();
    }

    inputsDNoneToggle = true;
    private toggleInputs() {
        this.inputsDNoneToggle = !this.inputsDNoneToggle;
        this.querySelector('.report-raw .inputs').forEach(e => {
            this.toggleDisplayNone(e, this.inputsDNoneToggle);
        });
    }

    outputsDNoneToggle = true;
    private toggleOutputs() {
        this.outputsDNoneToggle = !this.outputsDNoneToggle;
        this.querySelector('.report-raw .outputs').forEach(e => {
            this.toggleDisplayNone(e, this.outputsDNoneToggle);
        });
    }

    toggleInfosErrors() {
        this.toggleInfos();
        this.toggleErrors();
    }

    infosToggle = true;
    private toggleInfos() {
        this.infosToggle = !this.infosToggle;
        this.querySelector('.report-raw .infos').forEach(e => {
            this.toggleDisplayNone(e, this.infosToggle);
        });
    }

    errorsToggle = true;
    private toggleErrors() {
        this.errorsToggle = !this.errorsToggle;
        this.querySelector('.report-raw .errors').forEach(e => {
            this.toggleDisplayNone(e, this.errorsToggle);
        });
    }

    private toggleDisplayNone(elem: any, on: boolean) {
        if (on) {
            elem.className = elem.className.replace(' d-none', '');
        } else {
            elem.className += ' d-none';
        }
    }

    toggleStepCollapsed(step: any, event: Event = null) {
        if (event != null) event.stopPropagation();
        step.collapsed = !step.collapsed;
    }

    isStepCollapsed(step: any): boolean {
        if ('collapsed' in step) {
            return step.collapsed as boolean;
        } else {
            step.collapsed = false;
            return false;
        }
    }

    setAllStepsCollapsed(collapsed: boolean, parentStep: StepExecutionReport = null) {
        if (parentStep == null) {
            this.scenarioExecutionReport.report.steps.forEach(step => {
                step['collapsed'] = collapsed;
                this.setAllStepsCollapsed(collapsed, step);
            });
            if (!collapsed) {
                timer(500).subscribe(() => {
                    this.selectStep(this.selectedStep, true);
                });
            }
        } else {
            parentStep['collapsed'] = collapsed;
            parentStep.steps.forEach(step => {
                step['collapsed'] = collapsed;
                this.setAllStepsCollapsed(collapsed, step);
            });
        }
    }

    selectStep(step: StepExecutionReport = null, scrollIntoView: boolean = false) {
        this.selectedStep = step;
        if (!this.collapseContextVariables) {
            this.toggleContextVariables();
        }
        if (scrollIntoView && step) {
            document.getElementById(step['rowId']).scrollIntoView({behavior: 'smooth', block: 'start'});
        }
    }

    private computeAllStepRowId() {
        this.scenarioExecutionReport?.report?.steps?.forEach((s, i) => {
            this.computeStepRowId(s, `${i}`);
        });
    }

    private computeStepRowId(step: StepExecutionReport, parentId: string) {
        step['rowId'] = `${parentId}`;
        step.steps.forEach((s, i) => {
            s['rowId'] = `${parentId}-${i}`;
            this.computeStepRowId(s, s['rowId']);
        });
    }

    private querySelector(selectors: any, all: boolean = true): any | [any] {
        if (all) {
            return this.elementRef.nativeElement.querySelectorAll(selectors);
        } else {
            return this.elementRef.nativeElement.querySelector(selectors);
        }
    }

////////////////////////////////////////////////////// MONACO canva view

    enableEditorView(value: any) {
        return this.stringify.transform(value).length > 200;
    }

    private _theme = 'vs-dark';
    get editorTheme(): string {
        return this._theme;
    }
    set editorTheme(theme: string) {
        this._theme = (theme && theme.trim()) || 'vs-dark';
        this.updateEditorOptions();
    }

    private _editorLanguage = 'json';
    get editorLanguage(): string {
        return this._editorLanguage;
    }
    set editorLanguage(lang: string) {
        this._editorLanguage = (lang && lang.trim()) || 'json';
        this.updateEditorOptions();
    }


    editorOptions = {theme: this.editorTheme, language: this.editorLanguage};
    code: string;

    openOffCanva(content: TemplateRef, value: any) {
        this.code = this.stringify.transform(value, {space: 4});
		this.offcanvasService.open(content, { position: 'bottom', panelClass: 'offcanvas-panel-report' });
	}

    exportEditorContent() {
        this.fileSaverService.saveText(this.code, 'content');
    }

    copyEditorContent() {
        navigator.clipboard.writeText(this.code);
    }

    private updateEditorOptions() {
        this.editorOptions = {theme: this.editorTheme, language: this.editorLanguage};
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy