// Copyright (C) 2018 Avatao.com Innovative Learning Kft. // All Rights Reserved. See LICENSE file for details. import { Component, OnDestroy, OnInit, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core'; import { DeploymentNotificationService } from '../services/deployment-notification.service'; import { Subscription } from 'rxjs'; import { WebSocketService } from '../services/websocket.service'; import { WebSocketMessage } from '../message-types/websocket-message'; import { HideMessagesMessage, LayoutMessage, TerminalMenuItemMessage } from '../message-types/dashboard-messages'; import { config } from '../config'; import { ProcessLogService } from '../services/processlog.service'; import { LogMessage } from '../message-types/log-message'; import { HttpClient } from '@angular/common/http'; import { delay, retryWhen, tap } from 'rxjs/operators'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent implements OnInit, OnDestroy { deploying = false; polling = false; deploymentNotificationSubscription: Subscription; @ViewChild('webiframe', {static: true}) webiframe: ElementRef; @ViewChild('tfwmessages', {static: true}) messages: ElementRef; @ViewChild('urlbar', {static: true}) urlbar: ElementRef; layout: string = config.dashboard.currentLayout; hideMessages: boolean = config.dashboard.hideMessages; iframeUrl: string = config.dashboard.iframeUrl; showUrlBar = config.dashboard.showUrlBar; actualIframeUrl: string = this.iframeUrl; selectedTerminalMenuItem: string = config.dashboard.terminalOrConsole; iframeReloadSubscription: Subscription; command_handlers = { 'dashboard.layout': this.layoutHandler.bind(this), 'dashboard.hideMessages': this.hideMessagesHandler.bind(this), 'dashboard.terminalMenuItem': this.terminalMenuItemHandler.bind(this), 'dashboard.reloadFrontend': this.reloadFrontendHandlder.bind(this), 'dashboard.reloadIframe': this.reloadIframeHandler.bind(this) }; constructor(private deploymentNotificationService: DeploymentNotificationService, private webSocketService: WebSocketService, private changeDetectorRef: ChangeDetectorRef, private processLogService: ProcessLogService, private http: HttpClient) {} ngOnInit() { this.webSocketService.connect(); this.initCommandHandling(); this.initDeploymentNotifications(); this.recoverIfNeeded(); this.triggerFirstFSMStepIfNeeded(); } initCommandHandling() { this.webSocketService.observeKey('dashboard').subscribe(message => { this.command_handlers[message.key](message); this.changeDetectorRef.detectChanges(); }); } initDeploymentNotifications() { this.deploymentNotificationSubscription = this.deploymentNotificationService.deploying.subscribe( (deploying) => { this.deploying = deploying; if (!deploying && config.ide.reloadIframeOnDeploy) { if (this.polling) { this.iframeReloadSubscription.unsubscribe(); } this.pollingServerForIframeReload(); } }); } triggerFirstFSMStepIfNeeded() { if (config.dashboard.triggerFirstFSMStep) { setTimeout(() => { this.webSocketService.sendJSON({ 'key': 'fsm.step', 'trigger': config.dashboard.triggerFirstFSMStep }); }); } } recoverIfNeeded() { if (config.dashboard.recoverAfterPageReload) { setTimeout(() => this.webSocketService.sendJSON({'key': 'recover'})); } } layoutHandler(message: LayoutMessage) { if (config.dashboard.enabledLayouts.includes(message.value)) { this.setLayout(message.value); } else { console.log('Invalid ide layout "' + message.value + '" received!'); } } hideMessagesHandler(message: HideMessagesMessage) { this.hideMessages = message.value; } terminalMenuItemHandler(message: TerminalMenuItemMessage) { this.selectTerminalMenuItem(message.value); } reloadFrontendHandlder(message: WebSocketMessage) { setTimeout(() => window.location.reload(), 2000); } reloadIframeHandler(message: WebSocketMessage) { setTimeout(() => this.reloadIframeNoSubmit(), 200); } setLayout(layout: string) { this.layout = layout; // We need to trigger a 'resize' event manually, otherwise editor stays collapsed // editor 'resize' event listener requires a parameter of force=true setTimeout(() => window.dispatchEvent(new Event('resize', {force: true} as any)), 0); } reloadIframe() { setTimeout(() => { this.webiframe.nativeElement.contentWindow.location.reload(true); }); } reloadIframeNoSubmit() { // Sometimes it is needed to reload the iframe without resending the previous form data setTimeout(() => { this.webiframe.nativeElement.contentWindow.location = this.webiframe.nativeElement.contentWindow.location.href; }) } selectTerminalMenuItem(item: string) { if (!item.match('(terminal|console)')) { return; } this.selectedTerminalMenuItem = item; } setConsoleContentIfNoLiveLogs(logs: LogMessage) { this.processLogService.emitNewLogsIfNoLiveLogs(logs); if (config.ide.showConsoleOnDeploy) { this.selectTerminalMenuItem('console'); } } scrollMessagesToBottom(): void { const element = this.messages.nativeElement; // This must be done in the Angular event loop to avoid messing up // change detection (not in the template like ConsoleComponent does) element.scrollTop = element.scrollHeight; } iframeLoad(): void { if (this.webiframe) { const href = this.webiframe.nativeElement.contentWindow.frames.location.href; const niceURL = href.match(/.*?\/\/.*?(\/.*)/)[1]; this.actualIframeUrl = niceURL; } } changeIframeURL() { const userGivenValue = this.urlbar.nativeElement.value.trim(); if( userGivenValue == '/' || userGivenValue.startsWith('dashboard') || userGivenValue.startsWith('/dashboard') ) return; this.webiframe.nativeElement.contentWindow.frames.location.href = this.urlbar.nativeElement.value; } pollingServerForIframeReload() { this.polling = true; this.iframeReloadSubscription = this.http.get(this.actualIframeUrl, {observe: 'response'}).pipe( retryWhen(errors => errors.pipe( tap( response => { if (response.status === 200) { this.iframeReloadSubscription.unsubscribe(); this.polling = false; this.reloadIframe(); } } ), delay(1000) ) ) ).subscribe(); } ngOnDestroy() { if (this.deploymentNotificationSubscription) { this.deploymentNotificationSubscription.unsubscribe(); } if (this.iframeReloadSubscription) { this.iframeReloadSubscription.unsubscribe(); } } }