import { Component, OnDestroy, OnInit, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core'; import { DeploymentNotificationService } from '../services/deployment-notification.service'; import { Subscription, BehaviorSubject } from 'rxjs'; import { WebSocketService } from '../services/websocket.service'; import { WebSocketMessage } from '../message-types/websocket-message'; import { DashboardConfigService } from '../services/config.service'; import { HttpClient } from '@angular/common/http'; import { delay, retryWhen, tap } from 'rxjs/operators'; import { FSMUpdateService } from '../services/fsmupdate.service'; import { MessagesComponent } from '../messages/messages.component'; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.scss'] }) export class DashboardComponent implements OnInit, OnDestroy { deploying = false; polling = new BehaviorSubject(false); deploymentNotificationSubscription: Subscription; @ViewChild('webiframe', {static: false}) webiframe: ElementRef; @ViewChild(MessagesComponent, {static: false}) messages: MessagesComponent; @ViewChild('urlbar', {static: false}) urlbar: ElementRef; layout = this.configService.layout; hideMessages = this.configService.hideMessages; showUrlBar = this.configService.showUrlBar; iframeUrl = this.configService.iframeUrl; actualIframeUrl = this.iframeUrl.value; terminalMenuItem = this.configService.terminalMenuItem; iframeReloadSubscription: Subscription; command_handlers = { 'dashboard.reloadFrontend': this.reloadFrontendHandlder.bind(this), 'dashboard.reloadIframe': this.reloadIframeHandler.bind(this) }; constructor(private deploymentNotificationService: DeploymentNotificationService, private webSocketService: WebSocketService, private changeDetectorRef: ChangeDetectorRef, private http: HttpClient, private configService: DashboardConfigService, private fsmUpdateService: FSMUpdateService) {} ngOnInit() { this.webSocketService.connect(); this.configService.init(); this.subscribeCheckSolution(); this.hideIframeUntilResponseOk(); this.subscribeResizeOnLayoutChange(); this.initCommandHandling(); this.initDeploymentNotifications(); this.sendReady(); } subscribeCheckSolution() { this.fsmUpdateService.init(); this.fsmUpdateService.in_accepted_state.subscribe(in_accepted_state => { if (in_accepted_state) { window.parent.postMessage('check solution', '*'); } }); } hideIframeUntilResponseOk() { // TODO: hide iframe and show it after this whole deal... this.reloadIframeWhenResponseOk(); } subscribeResizeOnLayoutChange() { this.configService.layout.subscribe(() => { this.emitResizeEvent(); setTimeout(() => this.messages.scrollToBottom(), 0); }); } initCommandHandling() { this.webSocketService.observeControl('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 && this.configService.reloadIframeOnDeploy.value) { this.reloadIframeWhenResponseOk(); } }); } sendReady() { setTimeout(() => this.webSocketService.send({'key': 'frontend.ready'})); } reloadFrontendHandlder(message: WebSocketMessage) { setTimeout(() => window.location.reload(), 2000); } reloadIframeHandler(message: WebSocketMessage) { setTimeout(() => this.reloadIframeNoSubmit(), 200); } setLayout(layout: string) { this.layout.next(layout); } emitResizeEvent() { // 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.terminalMenuItem.next(item); } iframeLoad() { if (this.webiframe && this.iframeUrl.value) { const href = this.webiframe.nativeElement.contentWindow.frames.location.href; const match = href.match(/.*?\/\/.*?(\/.*)/); if (match !== null) { // iframes on Firefox can have an about:blank // contentWindow after firing a (load) event const niceURL = 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; } reloadIframeWhenResponseOk() { if (this.polling.value) { this.iframeReloadSubscription.unsubscribe(); } this.polling.next(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.next(false); this.reloadIframe(); }}), delay(1000) ))).subscribe(); } ngOnDestroy() { if (this.deploymentNotificationSubscription) { this.deploymentNotificationSubscription.unsubscribe(); } if (this.iframeReloadSubscription) { this.iframeReloadSubscription.unsubscribe(); } } }