Compare commits

..
Author SHA1 Message Date
Gergo Turcsanyi 96d27a7d2d Fix iframe reload polling 2020-05-29 11:54:26 +02:00
R. Richard 4f3a6057a4 Prevent IDE reload race condition 2020-05-26 15:34:54 +02:00
R. Richard ea5bf30a95 Reconnect websockets automatically 2020-05-21 17:44:01 +02:00
R. Richard d8545c48be Stop filtering JS code in bot messages 2020-05-18 18:01:30 +02:00
ni-richard 031a9916a0 Make webservice optional 2020-05-15 14:46:33 +02:00
R. Richard 68f9a2c57d Enable Kotlin syntax highlighting 2020-03-31 14:42:11 +02:00
Kristóf Tóth f2720c218b Fix DEPLOYING spinner css 2019-12-19 15:43:56 +01:00
Kristóf Tóth f7a2182ba6 Add support for sending arbitrary TFW message on bot message click 2019-12-19 14:49:27 +01:00
Kristóf Tóth f17fe18cef Drain message queue on typing indicator click 2019-12-19 14:29:02 +01:00
Kristóf Tóth a3993e8b87 Add name attribute to iframe (allow magic links from messages) 2019-11-08 13:13:39 +01:00
Kristóf Tóth abf7d8ced8 Update packages (Firefox IDE scrolling bug fix) 2019-11-08 13:04:42 +01:00
Kristóf Tóth b45f624ab3 Extract status code polling logic into a class 2019-11-07 16:08:54 +01:00
Kristóf Tóth 9df954bcb1 Fix dashboard.iframeUrl by keeping iframeUrl updated 2019-10-21 15:55:39 +02:00
Kristóf Tóth 52e80dff03 Avoid polling iframe when url is not set 2019-10-14 14:58:40 +02:00
Kristóf Tóth f51aebe1de Remove unused message type 2019-10-07 13:33:02 +02:00
Kristóf Tóth 25433a1ccd Move scrolling logic to MessagesComponent 2019-10-03 13:04:32 +02:00
Kristóf Tóth d6874335cd Remove leftover debug console.log 2019-10-03 12:58:21 +02:00
18 changed files with 7360 additions and 486 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json",
"build": "ng build --prod --aot --build-optimizer"
"build": "ng build --prod --aot --build-optimizer",
"lint": "ng lint --fix"
},
"private": true,
"dependencies": {
+2 -1
View File
@@ -17,7 +17,7 @@ import { TerminalComponent } from './terminal/terminal.component';
import { AppRoutingModule } from './app-routing.module';
import { TestmessengerComponent } from './testmessenger/testmessenger.component';
import { DeploymentNotificationService } from './services/deployment-notification.service';
import { SafePipe } from './pipes/safe.pipe';
import { SafePipe, SafeHtmlPipe } from './pipes/safe.pipe';
import { ConsoleComponent } from './console/console.component';
import { MonacoEditorModule } from 'ngx-monaco-editor';
import {
@@ -41,6 +41,7 @@ import { LoaderComponent } from './loader/loader.component';
DashboardComponent,
TestmessengerComponent,
SafePipe,
SafeHtmlPipe,
ConsoleComponent,
LoaderComponent
],
+4 -5
View File
@@ -1,13 +1,11 @@
<div [attr.class]="layout | async">
<div class="tfw-grid-main-components">
<div class="tfw-header"><app-header></app-header></div>
<div [ngClass]="{'hide-attribute': hideMessages | async}"
class="tfw-messages"
#tfwmessages>
<app-messages (newMessageEvent)="scrollMessagesToBottom()"></app-messages>
<div [ngClass]="{'hide-attribute': hideMessages | async}" class="tfw-messages">
<app-messages></app-messages>
</div>
<div class="tfw-web tao-grid-top-left"
[ngClass]="{'deploy-blur': deploying || (polling | async)}">
[ngClass]="{'deploy-blur': deploying || (iframeReloadPoller.isPolling | async)}">
<div *ngIf="iframeUrl | async" class="iframe-container">
<div *ngIf="showUrlBar | async" class="urlbar-container">
<button class="refresh btn btn-sm rounded-circle" (click)="reloadIframe()">&#8635;</button>
@@ -20,6 +18,7 @@
</div>
<iframe class="iframe"
#webiframe
name="webiframe"
scrolling="yes"
frameborder="0"
(load)="iframeLoad()"
+19 -51
View File
@@ -1,12 +1,13 @@
import { Component, OnDestroy, OnInit, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core';
import { DeploymentNotificationService } from '../services/deployment-notification.service';
import { Subscription, BehaviorSubject } from 'rxjs';
import { Subscription } from 'rxjs';
import { WebSocketService } from '../services/websocket.service';
import { WebSocketMessage } from '../message-types/websocket-message';
import { DashboardConfigService } from '../services/config.service';
import { DashboardConfigService, ConfigReadyService } 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';
import { StatusCodePoller } from './statuscodepoller';
@Component({
selector: 'app-dashboard',
@@ -15,10 +16,9 @@ import { FSMUpdateService } from '../services/fsmupdate.service';
})
export class DashboardComponent implements OnInit, OnDestroy {
deploying = false;
polling = new BehaviorSubject<boolean>(false);
deploymentNotificationSubscription: Subscription;
@ViewChild('webiframe', {static: false}) webiframe: ElementRef;
@ViewChild('tfwmessages', {static: false}) messages: ElementRef;
@ViewChild(MessagesComponent, {static: false}) messages: MessagesComponent;
@ViewChild('urlbar', {static: false}) urlbar: ElementRef;
layout = this.configService.layout;
@@ -27,7 +27,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
iframeUrl = this.configService.iframeUrl;
actualIframeUrl = this.iframeUrl.value;
terminalMenuItem = this.configService.terminalMenuItem;
iframeReloadSubscription: Subscription;
iframeReloadPoller: StatusCodePoller;
command_handlers = {
'dashboard.reloadFrontend': this.reloadFrontendHandlder.bind(this),
@@ -38,6 +38,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
private webSocketService: WebSocketService,
private changeDetectorRef: ChangeDetectorRef,
private http: HttpClient,
private configReadyService: ConfigReadyService,
private configService: DashboardConfigService,
private fsmUpdateService: FSMUpdateService) {}
@@ -45,6 +46,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.webSocketService.connect();
this.configService.init();
this.subscribeCheckSolution();
this.iframeReloadPoller = new StatusCodePoller(this.iframeUrl, this.http);
this.hideIframeUntilResponseOk();
this.subscribeResizeOnLayoutChange();
this.initCommandHandling();
@@ -62,14 +64,16 @@ export class DashboardComponent implements OnInit, OnDestroy {
}
hideIframeUntilResponseOk() {
// TODO: hide iframe and show it after this whole deal...
this.reloadIframeWhenResponseOk();
this.iframeReloadPoller.ok.subscribe(() => this.reloadIframe());
this.configReadyService.configDone.subscribe(() =>
this.iframeReloadPoller.start()
);
}
subscribeResizeOnLayoutChange() {
this.configService.layout.subscribe(() => {
this.emitResizeEvent();
setTimeout(() => this.scrollMessagesToBottom(), 0);
setTimeout(() => this.messages.scrollToBottom(), 0);
});
}
@@ -85,7 +89,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
(deploying) => {
this.deploying = deploying;
if (!deploying && this.configService.reloadIframeOnDeploy.value) {
this.reloadIframeWhenResponseOk();
this.iframeReloadPoller.start();
}
});
}
@@ -99,7 +103,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
}
reloadIframeHandler(message: WebSocketMessage) {
setTimeout(() => this.reloadIframeNoSubmit(), 200);
this.iframeReloadPoller.ok.subscribe(() => this.reloadIframe());
this.iframeReloadPoller.start();
}
setLayout(layout: string) {
@@ -114,14 +119,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
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;
this.webiframe.nativeElement.contentWindow.frames.location.href = this.iframeUrl.value;
});
}
@@ -132,17 +130,9 @@ export class DashboardComponent implements OnInit, OnDestroy {
this.terminalMenuItem.next(item);
}
scrollMessagesToBottom() {
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() {
if (this.webiframe && this.iframeUrl.value) {
const href = this.webiframe.nativeElement.contentWindow.frames.location.href;
console.log(href, this.webiframe.nativeElement.contentWindow);
const match = href.match(/.*?\/\/.*?(\/.*)/);
if (match !== null) {
// iframes on Firefox can have an about:blank
@@ -162,35 +152,13 @@ export class DashboardComponent implements OnInit, OnDestroy {
) {
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();
this.iframeUrl.next(this.urlbar.nativeElement.value);
}
ngOnDestroy() {
if (this.deploymentNotificationSubscription) {
this.deploymentNotificationSubscription.unsubscribe();
}
if (this.iframeReloadSubscription) {
this.iframeReloadSubscription.unsubscribe();
}
this.iframeReloadPoller.stop();
}
}
+63
View File
@@ -0,0 +1,63 @@
import { HttpClient } from '@angular/common/http';
import { Subject, Subscription, BehaviorSubject } from 'rxjs';
import { retryWhen, delay, tap, skip } from 'rxjs/operators';
export class StatusCodePoller {
http: HttpClient;
url: BehaviorSubject<string>;
pollFreq: number;
okCode: number;
stopOnOk: boolean;
pollSubscription: Subscription;
urlSubscription: Subscription;
poll = new Subject<any>();
ok = new Subject<any>();
isPolling = new BehaviorSubject<boolean>(false);
constructor(
url: BehaviorSubject<string>, http: HttpClient,
pollFreq = 1000, okCode = 200,
stopOnOk = true
) {
this.url = url;
this.http = http;
this.pollFreq = pollFreq;
this.okCode = okCode;
this.stopOnOk = stopOnOk;
}
start() {
this.stop();
if (this.url.value === '') {
return;
}
this.urlSubscription = this.url.pipe(skip(1)).subscribe(() => this.stop());
this.isPolling.next(true);
this.pollSubscription = this.http.get(this.url.value, {observe: 'response'}).pipe(
retryWhen(errors =>
errors.pipe(
tap(
response => {
this.poll.next(response);
if (response.status === this.okCode) {
if (this.stopOnOk) {
this.stop();
}
this.ok.next(response);
}
}),
delay(this.pollFreq)
))).subscribe();
}
stop() {
this.isPolling.next(false);
if (this.pollSubscription) {
this.pollSubscription.unsubscribe();
}
if (this.urlSubscription) {
this.urlSubscription.unsubscribe();
}
}
}
+2 -1
View File
@@ -75,7 +75,8 @@
.loader {
border-radius: 50%;
border: 2px solid;
border: 2px solid $tao-warm-yellow-600;
border-top: 2px solid $tao-warm-yellow-200;
width: 15px;
height: 15px;
animation: spin 2s linear infinite;
+11 -1
View File
@@ -32,6 +32,7 @@ export class IdeComponent implements OnInit {
files: string[];
filename = '';
lastFilename = '';
reloadFile = false;
code = 'Loading your file...';
codeState = CodeState.SAVED;
@@ -85,10 +86,18 @@ export class IdeComponent implements OnInit {
}
reloadHandler(message: WebSocketMessage) {
if (!this.reloadFile) {
this.reloadFile = true;
this.requestCode();
}
}
readHandler(message: IDEMessage) {
if (!this.reloadFile && message.reload) {
return;
}
this.reloadFile = false;
if (this.codeState === CodeState.SAVED) {
if (this.filename !== message.filename) {
this.filename = message.filename;
@@ -189,7 +198,8 @@ export class IdeComponent implements OnInit {
requestCode() {
this.webSocketService.send({
'key': 'ide.read',
'filename': this.filename
'filename': this.filename,
'reload': this.reloadFile
});
}
}
+3 -1
View File
@@ -30,5 +30,7 @@ export const LanguageMap: { [extension: string]: string; } = {
css: 'css',
less: 'less',
scss: 'scss',
sh: 'shell'
sh: 'shell',
kt: 'kotlin',
kts: 'kotlin'
};
+1 -4
View File
@@ -1,13 +1,10 @@
import { WebSocketMessage } from './websocket-message';
export interface MessageConfig extends WebSocketMessage {
originator?: string;
}
export interface MessageData {
originator?: string;
timestamp?: Date;
typing?: boolean;
command?: any;
message: string;
}
+1
View File
@@ -4,4 +4,5 @@ export interface IDEMessage extends WebSocketMessage {
filename: string;
content?: string;
files?: string[];
reload?: boolean;
}
+6 -3
View File
@@ -1,15 +1,18 @@
<div class="tfw-messages-main">
<div class="tfw-grid-message"
*ngFor="let message of messages.slice(); let last = last"
[class.highlighted-message]="last">
[class.highlighted-message]="last"
(click)="sendMessageCommand(message)">
<div class="tfw-grid-message-header">
<img class="tao-grid-center-left" src="images/avataobot.svg"/>
<div class="tao-grid-center-left originator">{{message.originator}}</div>
<div class="timestamp tao-grid-center-right">{{message.timestamp | date:'HH:mm:ss'}}</div>
</div>
<div class="tfw-grid-message-body" [innerHtml]="message.message"></div>
<div class="tfw-grid-message-body" [innerHtml]="message.message | safeHtml"></div>
</div>
<div *ngIf="showTypingIndicator" class="tfw-grid-message jumping-circle-container">
<div *ngIf="showTypingIndicator"
class="tfw-grid-message jumping-circle-container"
(click)="drainMessageQueue()">
<div class="jumping-circle" id="jc1"></div>
<div class="jumping-circle" id="jc2"></div>
<div class="jumping-circle" id="jc3"></div>
+21 -2
View File
@@ -1,4 +1,4 @@
import { ChangeDetectorRef, Component, OnInit, EventEmitter, Output } from '@angular/core';
import { ChangeDetectorRef, Component, OnInit, EventEmitter, Output, ElementRef } from '@angular/core';
import { MessageData, Message } from '../message-types/bot-messages';
import { MarkdownService } from '../services/markdown.service';
import { WebSocketService } from '../services/websocket.service';
@@ -19,7 +19,8 @@ export class MessagesComponent implements OnInit {
constructor(
private markdownService: MarkdownService,
private websocketService: WebSocketService,
private changeDetectorRef: ChangeDetectorRef
private changeDetectorRef: ChangeDetectorRef,
private ref: ElementRef
) {}
ngOnInit() {
@@ -27,6 +28,7 @@ export class MessagesComponent implements OnInit {
message => {
this.writeMessage(message);
this.newMessageEvent.emit();
this.scrollToBottom();
});
this.websocketService.connect();
@@ -41,6 +43,11 @@ export class MessagesComponent implements OnInit {
this.changeDetectorRef.detectChanges();
}
scrollToBottom() {
const element = this.ref.nativeElement.parentElement;
element.scrollTop = element.scrollHeight;
}
transformMessage(message: MessageData) {
message.message = this.convertMarkdownToHTML(message.message);
if (!message.timestamp) {
@@ -52,4 +59,16 @@ export class MessagesComponent implements OnInit {
convertMarkdownToHTML(text: string) {
return this.markdownService.convertToHtml(text);
}
drainMessageQueue() {
this.websocketService.send({
'key': 'message.queue.drain'
});
}
sendMessageCommand(message: MessageData) {
if ('command' in message) {
this.websocketService.send(message.command);
}
}
}
+11
View File
@@ -11,3 +11,14 @@ export class SafePipe implements PipeTransform {
return this.sanitizer.bypassSecurityTrustResourceUrl(value);
}
}
@Pipe({
name: 'safeHtml'
})
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
transform(value) {
return this.sanitized.bypassSecurityTrustHtml(value);
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ export class DashboardConfigService extends ConfigServiceBase {
layout = new BehaviorSubject<string>('terminal-ide-web');
hideMessages = new BehaviorSubject<boolean>(false);
iframeUrl = new BehaviorSubject<string>('/webservice');
iframeUrl = new BehaviorSubject<string>('');
showUrlBar = new BehaviorSubject<boolean>(false);
terminalMenuItem = new BehaviorSubject<string>('terminal');
reloadIframeOnDeploy = new BehaviorSubject<boolean>(false);
+42 -11
View File
@@ -8,11 +8,16 @@ export class TerminadoService {
xterm: Terminal;
ws: WebSocket;
attached = false;
private dataListener: any;
private resizeListener: any;
constructor() {
Terminal.applyAddon(fit);
Terminal.applyAddon(terminado);
this.xterm = new Terminal({
this.xterm = this.createTerminal();
}
createTerminal() {
return new Terminal({
theme: {
foreground: '#ffffff',
background: '#0C0C0C', // $tao-gray-800
@@ -37,26 +42,52 @@ export class TerminadoService {
},
fontSize: 14
});
const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
this.ws = new WebSocket(wsproto + window.location.host + '/terminal');
}
attach(element: HTMLElement) {
if (this.attached) {
return;
}
const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
this.ws = new WebSocket(wsproto + window.location.host + '/terminal');
this.ws.onopen = () => {
(<any>this.xterm).terminadoAttach(this.ws);
this.attached = true;
this.xterm = this.createTerminal();
this.xterm.open(element);
this.fit();
this.xterm.blur();
this.attached = true;
// In order to reset the terminal state after a broken socket, we need to register the listeners manually.
(<any>this.xterm)._core.register(this.dataListener = this.xterm.onData(data => {
this.ws.send(JSON.stringify(['stdin', data]));
}));
(<any>this.xterm)._core.register(this.resizeListener = this.xterm.onResize((size: { rows: number, cols: number }) => {
this.ws.send(JSON.stringify(['set_size', size.rows, size.cols]));
}));
};
this.ws.onclose = () => {
this.detach();
this.xterm.destroy();
this.attach(element);
};
this.ws.onmessage = msg => {
const data = JSON.parse(msg.data);
if (data[0] === 'stdout') {
this.xterm.write(data[1]);
}
};
}
detach() {
(<any>this.xterm).terminadoDetach(this.ws);
this.xterm.destroy();
this.ws.close();
if (!this.attached) {
return;
}
this.attached = false;
this.dataListener.dispose();
this.resizeListener.dispose();
}
fit() {
+17 -3
View File
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Observable, Subject, Subscription } from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { filter, map } from 'rxjs/operators';
import { WebSocketMessage } from '../message-types/websocket-message';
@@ -17,14 +17,28 @@ export enum Intent {
@Injectable()
export class WebSocketService {
private ws: WebSocketSubject<WebSocketMessage>;
private subject: Subject<WebSocketMessage> = new Subject<WebSocketMessage>();
private subscription: Subscription;
constructor() {}
public connect() {
if (!this.ws) {
if (this.subscription) {
this.subscription.unsubscribe();
}
const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
const connAddr = wsproto + window.location.host + '/ws';
this.ws = webSocket<WebSocketMessage>(connAddr);
this.ws = webSocket<WebSocketMessage>({
url: connAddr,
closeObserver: {
next: closeEvent => {
this.ws = null;
this.connect();
}
}
});
this.subscription = this.ws.subscribe(msg => this.subject.next(msg));
}
}
@@ -35,7 +49,7 @@ export class WebSocketService {
}
public observeAll<T extends WebSocketMessage>(key: string): Observable<T> {
return this.ws.pipe(
return this.subject.pipe(
filter(message => message.key.startsWith(key)),
map(message => <T> message)
);
+6796
View File
File diff suppressed because it is too large Load Diff
+353 -396
View File
File diff suppressed because it is too large Load Diff