Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68f9a2c57d | ||
|
|
f2720c218b | ||
|
|
f7a2182ba6 | ||
|
|
f17fe18cef | ||
|
|
a3993e8b87 | ||
|
|
abf7d8ced8 | ||
|
|
b45f624ab3 | ||
|
|
9df954bcb1 | ||
|
|
52e80dff03 | ||
|
|
f51aebe1de | ||
|
|
25433a1ccd | ||
|
|
d6874335cd | ||
|
|
46fe26861c |
@@ -1,13 +1,11 @@
|
|||||||
<div [attr.class]="layout | async">
|
<div [attr.class]="layout | async">
|
||||||
<div class="tfw-grid-main-components">
|
<div class="tfw-grid-main-components">
|
||||||
<div class="tfw-header"><app-header></app-header></div>
|
<div class="tfw-header"><app-header></app-header></div>
|
||||||
<div [ngClass]="{'hide-attribute': hideMessages | async}"
|
<div [ngClass]="{'hide-attribute': hideMessages | async}" class="tfw-messages">
|
||||||
class="tfw-messages"
|
<app-messages></app-messages>
|
||||||
#tfwmessages>
|
|
||||||
<app-messages (newMessageEvent)="scrollMessagesToBottom()"></app-messages>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="tfw-web tao-grid-top-left"
|
<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="iframeUrl | async" class="iframe-container">
|
||||||
<div *ngIf="showUrlBar | async" class="urlbar-container">
|
<div *ngIf="showUrlBar | async" class="urlbar-container">
|
||||||
<button class="refresh btn btn-sm rounded-circle" (click)="reloadIframe()">↻</button>
|
<button class="refresh btn btn-sm rounded-circle" (click)="reloadIframe()">↻</button>
|
||||||
@@ -20,6 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<iframe class="iframe"
|
<iframe class="iframe"
|
||||||
#webiframe
|
#webiframe
|
||||||
|
name="webiframe"
|
||||||
scrolling="yes"
|
scrolling="yes"
|
||||||
frameborder="0"
|
frameborder="0"
|
||||||
(load)="iframeLoad()"
|
(load)="iframeLoad()"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Component, OnDestroy, OnInit, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core';
|
import { Component, OnDestroy, OnInit, ChangeDetectorRef, ElementRef, ViewChild } from '@angular/core';
|
||||||
import { DeploymentNotificationService } from '../services/deployment-notification.service';
|
import { DeploymentNotificationService } from '../services/deployment-notification.service';
|
||||||
import { Subscription, BehaviorSubject } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { WebSocketService } from '../services/websocket.service';
|
import { WebSocketService } from '../services/websocket.service';
|
||||||
import { WebSocketMessage } from '../message-types/websocket-message';
|
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 { HttpClient } from '@angular/common/http';
|
||||||
import { delay, retryWhen, tap } from 'rxjs/operators';
|
|
||||||
import { FSMUpdateService } from '../services/fsmupdate.service';
|
import { FSMUpdateService } from '../services/fsmupdate.service';
|
||||||
|
import { MessagesComponent } from '../messages/messages.component';
|
||||||
|
import { StatusCodePoller } from './statuscodepoller';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
@@ -15,10 +16,9 @@ import { FSMUpdateService } from '../services/fsmupdate.service';
|
|||||||
})
|
})
|
||||||
export class DashboardComponent implements OnInit, OnDestroy {
|
export class DashboardComponent implements OnInit, OnDestroy {
|
||||||
deploying = false;
|
deploying = false;
|
||||||
polling = new BehaviorSubject<boolean>(false);
|
|
||||||
deploymentNotificationSubscription: Subscription;
|
deploymentNotificationSubscription: Subscription;
|
||||||
@ViewChild('webiframe', {static: false}) webiframe: ElementRef;
|
@ViewChild('webiframe', {static: false}) webiframe: ElementRef;
|
||||||
@ViewChild('tfwmessages', {static: false}) messages: ElementRef;
|
@ViewChild(MessagesComponent, {static: false}) messages: MessagesComponent;
|
||||||
@ViewChild('urlbar', {static: false}) urlbar: ElementRef;
|
@ViewChild('urlbar', {static: false}) urlbar: ElementRef;
|
||||||
|
|
||||||
layout = this.configService.layout;
|
layout = this.configService.layout;
|
||||||
@@ -27,7 +27,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
iframeUrl = this.configService.iframeUrl;
|
iframeUrl = this.configService.iframeUrl;
|
||||||
actualIframeUrl = this.iframeUrl.value;
|
actualIframeUrl = this.iframeUrl.value;
|
||||||
terminalMenuItem = this.configService.terminalMenuItem;
|
terminalMenuItem = this.configService.terminalMenuItem;
|
||||||
iframeReloadSubscription: Subscription;
|
iframeReloadPoller: StatusCodePoller;
|
||||||
|
|
||||||
command_handlers = {
|
command_handlers = {
|
||||||
'dashboard.reloadFrontend': this.reloadFrontendHandlder.bind(this),
|
'dashboard.reloadFrontend': this.reloadFrontendHandlder.bind(this),
|
||||||
@@ -38,6 +38,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
private webSocketService: WebSocketService,
|
private webSocketService: WebSocketService,
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private http: HttpClient,
|
private http: HttpClient,
|
||||||
|
private configReadyService: ConfigReadyService,
|
||||||
private configService: DashboardConfigService,
|
private configService: DashboardConfigService,
|
||||||
private fsmUpdateService: FSMUpdateService) {}
|
private fsmUpdateService: FSMUpdateService) {}
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
this.webSocketService.connect();
|
this.webSocketService.connect();
|
||||||
this.configService.init();
|
this.configService.init();
|
||||||
this.subscribeCheckSolution();
|
this.subscribeCheckSolution();
|
||||||
|
this.iframeReloadPoller = new StatusCodePoller(this.iframeUrl, this.http);
|
||||||
this.hideIframeUntilResponseOk();
|
this.hideIframeUntilResponseOk();
|
||||||
this.subscribeResizeOnLayoutChange();
|
this.subscribeResizeOnLayoutChange();
|
||||||
this.initCommandHandling();
|
this.initCommandHandling();
|
||||||
@@ -62,14 +64,16 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hideIframeUntilResponseOk() {
|
hideIframeUntilResponseOk() {
|
||||||
// TODO: hide iframe and show it after this whole deal...
|
this.iframeReloadPoller.ok.subscribe(() => this.reloadIframe());
|
||||||
this.reloadIframeWhenResponseOk();
|
this.configReadyService.configDone.subscribe(() =>
|
||||||
|
this.iframeReloadPoller.start()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeResizeOnLayoutChange() {
|
subscribeResizeOnLayoutChange() {
|
||||||
this.configService.layout.subscribe(() => {
|
this.configService.layout.subscribe(() => {
|
||||||
this.emitResizeEvent();
|
this.emitResizeEvent();
|
||||||
setTimeout(() => this.scrollMessagesToBottom(), 0);
|
setTimeout(() => this.messages.scrollToBottom(), 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +89,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
(deploying) => {
|
(deploying) => {
|
||||||
this.deploying = deploying;
|
this.deploying = deploying;
|
||||||
if (!deploying && this.configService.reloadIframeOnDeploy.value) {
|
if (!deploying && this.configService.reloadIframeOnDeploy.value) {
|
||||||
this.reloadIframeWhenResponseOk();
|
this.iframeReloadPoller.start();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,7 +103,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadIframeHandler(message: WebSocketMessage) {
|
reloadIframeHandler(message: WebSocketMessage) {
|
||||||
setTimeout(() => this.reloadIframeNoSubmit(), 200);
|
setTimeout(() => this.reloadIframe(), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLayout(layout: string) {
|
setLayout(layout: string) {
|
||||||
@@ -114,14 +118,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
reloadIframe() {
|
reloadIframe() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.webiframe.nativeElement.contentWindow.location.reload(true);
|
this.webiframe.nativeElement.contentWindow.frames.location.href = this.iframeUrl.value;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,20 +129,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
this.terminalMenuItem.next(item);
|
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() {
|
iframeLoad() {
|
||||||
if (this.webiframe && this.iframeUrl.value) {
|
if (this.webiframe && this.iframeUrl.value) {
|
||||||
const href = this.webiframe.nativeElement.contentWindow.frames.location.href;
|
const href = this.webiframe.nativeElement.contentWindow.frames.location.href;
|
||||||
const niceURL = href.match(/.*?\/\/.*?(\/.*)/)[1];
|
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;
|
this.actualIframeUrl = niceURL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
changeIframeURL() {
|
changeIframeURL() {
|
||||||
const userGivenValue = this.urlbar.nativeElement.value.trim();
|
const userGivenValue = this.urlbar.nativeElement.value.trim();
|
||||||
@@ -156,35 +151,13 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.webiframe.nativeElement.contentWindow.frames.location.href = this.urlbar.nativeElement.value;
|
this.iframeUrl.next(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() {
|
ngOnDestroy() {
|
||||||
if (this.deploymentNotificationSubscription) {
|
if (this.deploymentNotificationSubscription) {
|
||||||
this.deploymentNotificationSubscription.unsubscribe();
|
this.deploymentNotificationSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
this.iframeReloadPoller.stop();
|
||||||
if (this.iframeReloadSubscription) {
|
|
||||||
this.iframeReloadSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -75,7 +75,8 @@
|
|||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2px solid;
|
border: 2px solid $tao-warm-yellow-600;
|
||||||
|
border-top: 2px solid $tao-warm-yellow-200;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
animation: spin 2s linear infinite;
|
animation: spin 2s linear infinite;
|
||||||
|
|||||||
@@ -30,5 +30,7 @@ export const LanguageMap: { [extension: string]: string; } = {
|
|||||||
css: 'css',
|
css: 'css',
|
||||||
less: 'less',
|
less: 'less',
|
||||||
scss: 'scss',
|
scss: 'scss',
|
||||||
sh: 'shell'
|
sh: 'shell',
|
||||||
|
kt: 'kotlin',
|
||||||
|
kts: 'kotlin'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import { WebSocketMessage } from './websocket-message';
|
import { WebSocketMessage } from './websocket-message';
|
||||||
|
|
||||||
export interface MessageConfig extends WebSocketMessage {
|
|
||||||
originator?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MessageData {
|
export interface MessageData {
|
||||||
originator?: string;
|
originator?: string;
|
||||||
timestamp?: Date;
|
timestamp?: Date;
|
||||||
typing?: boolean;
|
typing?: boolean;
|
||||||
|
command?: any;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<div class="tfw-messages-main">
|
<div class="tfw-messages-main">
|
||||||
<div class="tfw-grid-message"
|
<div class="tfw-grid-message"
|
||||||
*ngFor="let message of messages.slice(); let last = last"
|
*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">
|
<div class="tfw-grid-message-header">
|
||||||
<img class="tao-grid-center-left" src="images/avataobot.svg"/>
|
<img class="tao-grid-center-left" src="images/avataobot.svg"/>
|
||||||
<div class="tao-grid-center-left originator">{{message.originator}}</div>
|
<div class="tao-grid-center-left originator">{{message.originator}}</div>
|
||||||
@@ -9,7 +10,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="tfw-grid-message-body" [innerHtml]="message.message"></div>
|
<div class="tfw-grid-message-body" [innerHtml]="message.message"></div>
|
||||||
</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="jc1"></div>
|
||||||
<div class="jumping-circle" id="jc2"></div>
|
<div class="jumping-circle" id="jc2"></div>
|
||||||
<div class="jumping-circle" id="jc3"></div>
|
<div class="jumping-circle" id="jc3"></div>
|
||||||
|
|||||||
@@ -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 { MessageData, Message } from '../message-types/bot-messages';
|
||||||
import { MarkdownService } from '../services/markdown.service';
|
import { MarkdownService } from '../services/markdown.service';
|
||||||
import { WebSocketService } from '../services/websocket.service';
|
import { WebSocketService } from '../services/websocket.service';
|
||||||
@@ -19,7 +19,8 @@ export class MessagesComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private markdownService: MarkdownService,
|
private markdownService: MarkdownService,
|
||||||
private websocketService: WebSocketService,
|
private websocketService: WebSocketService,
|
||||||
private changeDetectorRef: ChangeDetectorRef
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private ref: ElementRef
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -27,6 +28,7 @@ export class MessagesComponent implements OnInit {
|
|||||||
message => {
|
message => {
|
||||||
this.writeMessage(message);
|
this.writeMessage(message);
|
||||||
this.newMessageEvent.emit();
|
this.newMessageEvent.emit();
|
||||||
|
this.scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.websocketService.connect();
|
this.websocketService.connect();
|
||||||
@@ -41,6 +43,11 @@ export class MessagesComponent implements OnInit {
|
|||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const element = this.ref.nativeElement.parentElement;
|
||||||
|
element.scrollTop = element.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
transformMessage(message: MessageData) {
|
transformMessage(message: MessageData) {
|
||||||
message.message = this.convertMarkdownToHTML(message.message);
|
message.message = this.convertMarkdownToHTML(message.message);
|
||||||
if (!message.timestamp) {
|
if (!message.timestamp) {
|
||||||
@@ -52,4 +59,16 @@ export class MessagesComponent implements OnInit {
|
|||||||
convertMarkdownToHTML(text: string) {
|
convertMarkdownToHTML(text: string) {
|
||||||
return this.markdownService.convertToHtml(text);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user