Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
031a9916a0 | ||
|
|
68f9a2c57d | ||
|
|
f2720c218b | ||
|
|
f7a2182ba6 | ||
|
|
f17fe18cef | ||
|
|
a3993e8b87 | ||
|
|
abf7d8ced8 | ||
|
|
b45f624ab3 | ||
|
|
9df954bcb1 | ||
|
|
52e80dff03 | ||
|
|
f51aebe1de | ||
|
|
25433a1ccd | ||
|
|
d6874335cd | ||
|
|
46fe26861c | ||
|
|
ac1bb81162 | ||
|
|
38f3bfd6e5 | ||
|
|
f90fd78507 | ||
|
|
5e85869494 | ||
|
|
f8e9586be3 |
@@ -1,244 +1,6 @@
|
||||
# frontend-tutorial-framework
|
||||
|
||||
This is the Angular frontend of TFW.
|
||||
This is the Angular frontend of TFW. The main exposed features are our pre-implemented components based on the `src/app/services/websocket.service.ts` service.
|
||||
This service provides an *RxJS* based communication API to the framework backend (TFW server and event handlers). Another useful features are a bunch of pre-designed layouts and dynamic switching between them.
|
||||
|
||||
The main exposed features are our pre-implemented components based on the `src/app/services/websocket.service.ts` service.
|
||||
This service provides an RxJS based communication API to the framework backend (TFW server and event handlers).
|
||||
|
||||
Another useful features are a bunch of pre-designed layouts and dynamic switching between them.
|
||||
|
||||
To learn more about the framework, see the [baseimage-tutorial-framework](https://github.com/avatao-content/baseimage-tutorial-framework) repo.
|
||||
For more on creating, building and running TFW-based tutorials (not just the frontend) consult [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework).
|
||||
|
||||
## Components
|
||||
|
||||
In this section we are going to explore the various pre-made components this project offers.
|
||||
|
||||
Generally these components connect to a TFW event handler running on the backend.
|
||||
Communication is handled via simpe APIs exposed by these event handlers – over TFW messages.
|
||||
|
||||
These APIs are documented in the [baseimage-tutorial-framework](https://github.com/avatao-content/baseimage-tutorial-framework) repository README and as docstrings in the [lib/tfw/components](https://github.com/avatao-content/baseimage-tutorial-framework/tree/master/lib/tfw/components) directory (this is where the implementations of our pre-written event handlers live).
|
||||
|
||||
Frontend components expose APIs as well (e.g. changing layouts).
|
||||
These are documented in the API section of this README.
|
||||
|
||||
## Configuration
|
||||
|
||||
Generally it is unadvised to directly modify the source code of our pre-written components (this is hard for us to support and is prone to break).
|
||||
|
||||
For this reason most components are extensively configurable through the `src/app/config.ts` file.
|
||||
These configurations range from the enabling of different layouts to how frequently should our IDE save automatically.
|
||||
|
||||
Should you encounter any missing features, feel free to contact team TFW and we'll consider implementing them as configuration options (a common example would be making a configuration option dynamic).
|
||||
|
||||
### Terminal (webshell)
|
||||
|
||||
This is a full-fledged xterm terminal emulator which runs right in your browser and is based on xterm.js.
|
||||
The emulator is connected to a `TerminalEventHandler` instance on the backend over websockets.
|
||||
|
||||
This event handler spawns a `bash` session and a `pty` (pseudoterminal).
|
||||
It connects the master end of the `pty` to the emulator running in your browser and the slave end to `bash`.
|
||||
|
||||
This essentially provides a fully functional terminal session for your users in the browser (a convenient alternative for an SSH session).
|
||||
|
||||
This terminal is fully under your control:
|
||||
You can write to it (and thus execute commands) and read what commands were executed by the user using the API exposed by the `TerminalEventHandler`.
|
||||
|
||||
This enables you to pre-type or execute commands for the user and figure out what they are doing in the terminal.
|
||||
|
||||
### Console
|
||||
|
||||
Not unlike how a desktop IDE displays the output of your application, TFW provides a similar component as well.
|
||||
|
||||
The console can appear in place of the terminal and allows you to display the output of a supervisor process in real time.
|
||||
|
||||
This means that if you type `print('cats like cheese')` in your application code and run it, you will see `cats like cheese` appear on the console! Pretty neat, right?
|
||||
|
||||
You can control the displaying of process logs to the console using the `console.rewriteContentWithProcessLogsOnDeploy` key in `config.ts`.
|
||||
The value of `stdout` or `stderr` will cause the console to display the respective stream, while an empty string will disable any automatic output to the console altogether.
|
||||
|
||||
We recommend redirecting `stdout` and `stderr` to the same file and displaying the together.
|
||||
|
||||
The `console.showLiveLogs` key enables real time output from the standard stream you've selected.
|
||||
|
||||
### IDE (webIde)
|
||||
|
||||
This component is a simple text editor based on ACE.
|
||||
It always shows all files in a given folder and allows you to switch between them using the tabs on top.
|
||||
The IDE automatically saves any changes made to the files (the interval is configurable).
|
||||
|
||||
It connects to an `IdeEventHandler` instance on the backend which handles the reading/writing of files and the selection of directories as well.
|
||||
|
||||
It is also capable of dynamically displaying any changes made to these files from the terminal or from another process (this means that you always see a live view of the files).
|
||||
|
||||
This component provides an optional 'Deploy' button, which can be configured to restart a process in supervisord.
|
||||
You can enable this button by editig the `ide.showDeployButton` key of `config.ts`.
|
||||
To configure which process the button should restart edit `ide.deployProcessName`.
|
||||
|
||||
### Messages
|
||||
|
||||
This is a simple chat-like component you can use to instruct, help and guide your users.
|
||||
It displays messages sent to the frontend with the key `message.`
|
||||
|
||||
This component can be used as a simple chatbot as well.
|
||||
|
||||
You can provide a list of messages to queue and they will be automatically displayed one after the other.
|
||||
There is a wait time between each message so that the user can properly read them.
|
||||
This wait time is calculated from the length of the last message, and can be configured using the `messages.messageQueueWPM` key in `config.ts`.
|
||||
|
||||
You can use the `MessageSender` class to send messages from the TFW server or event handlers written in Python.
|
||||
|
||||
### Web – customisable component
|
||||
|
||||
In some of our layouts there is space allocated for a custom webservice or Angular component.
|
||||
This allows you to embed your own website in the TFW frontend.
|
||||
|
||||
There are two ways to do this:
|
||||
|
||||
Should you prefer to avoid Angular you can run your own webserver on the backend and use our dashboard's `iframe`ing capabilities to include it.
|
||||
To enable this feature you have to edit `src/app/config.ts` and to set `config.dashboard.iframeUrl` to the route your server is listening on.
|
||||
|
||||
Note that setting up a custom server is documented in the [test-tutorial-framework](https://github.com/avatao-content/test-tutorial-framework) repo.
|
||||
|
||||
Alternatively you can create your own Angular component(s) in `src/app/web`.
|
||||
Just rewrite `WebComponent` as you please or even nest more components into it if needed.
|
||||
Note that you must set `config.dashboard.iframeUrl` to an empty string(`''`) to enable the displaying of `WebComponent` (this also disables `iframe`ing).
|
||||
|
||||
### Dashboard
|
||||
|
||||
The dashboard is the component that composes all others and organises them into layouts.
|
||||
|
||||
Edit `src/app/config.ts` to change the layout settings.
|
||||
|
||||
Set `config.dashboard.currentLayout` to the layout you want to be displayed by default.
|
||||
|
||||
You can specify the layouts you allow in `config.dashboard.enabledLayouts` (the user can switch between them using a sidebar).
|
||||
This list must include the value of `currentLayout`.
|
||||
|
||||
Available layouts– with self explaining names:
|
||||
- `terminal-ide-web`
|
||||
- `terminal-ide-vertical`
|
||||
- `terminal-ide-horizontal`
|
||||
- `terminal-only`
|
||||
- `terminal-web`
|
||||
- `ide-web-vertical`
|
||||
- `ide-only`
|
||||
- `web-only`
|
||||
|
||||
|
||||
## API
|
||||
|
||||
Supported frontend APIs are documented here.
|
||||
|
||||
### Dynamic configuration
|
||||
|
||||
Many configuration options are changeable dynamically, like so (these are stuff from `config.ts`):
|
||||
```
|
||||
{
|
||||
"key": ...component name...,
|
||||
"data":
|
||||
{
|
||||
"command": ...configuration key...,
|
||||
"value": ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Messages
|
||||
|
||||
To send a messages message you can use the following message:
|
||||
```
|
||||
{
|
||||
"key": "message",
|
||||
"data":
|
||||
{
|
||||
"originator": ...string...,
|
||||
"message": ...string...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can queue a list of messages like so:
|
||||
```
|
||||
{
|
||||
"key": "queueMessages",
|
||||
"data":
|
||||
{
|
||||
"messages":
|
||||
[
|
||||
{
|
||||
"originator": ...string...,
|
||||
"message": ...string...
|
||||
},
|
||||
{
|
||||
"originator": ...string...,
|
||||
"message": ...string...
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dashboard
|
||||
|
||||
Changing layouts is possible using the following message (note that the layout you switch to must be enabled):
|
||||
```
|
||||
{
|
||||
"key": "dashboard",
|
||||
"data":
|
||||
{
|
||||
"command": "layout",
|
||||
"value": ...string...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can hide and show the messages component with the following message:
|
||||
```
|
||||
{
|
||||
"key": "dashboard",
|
||||
"data":
|
||||
{
|
||||
"command": "hideMessages",
|
||||
"value": ...boolean...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To switch between the terminal and console use this message (where `value` is `terminal` or `console`:
|
||||
```
|
||||
{
|
||||
"key": "dashboard",
|
||||
"data":
|
||||
{
|
||||
"command": "terminalMenuItem",
|
||||
"value": ...string...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Console
|
||||
|
||||
You can write to the console like so (this is only practical when `rewriteContentWithProcessLogsOnDeploy` equals an empty string):
|
||||
```
|
||||
{
|
||||
"key": "console",
|
||||
"data":
|
||||
{
|
||||
"command": "write",
|
||||
"content": ...string...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reading the contents is also possible:
|
||||
```
|
||||
{
|
||||
"key": "console",
|
||||
"data":
|
||||
{
|
||||
"command": "read"
|
||||
}
|
||||
}
|
||||
```
|
||||
To learn more about the framework and get started, please consult our [wiki](https://github.com/avatao-content/baseimage-tutorial-framework/wiki).
|
||||
|
||||
@@ -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()">↻</button>
|
||||
@@ -20,6 +18,7 @@
|
||||
</div>
|
||||
<iframe class="iframe"
|
||||
#webiframe
|
||||
name="webiframe"
|
||||
scrolling="yes"
|
||||
frameborder="0"
|
||||
(load)="iframeLoad()"
|
||||
|
||||
@@ -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,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
reloadIframeHandler(message: WebSocketMessage) {
|
||||
setTimeout(() => this.reloadIframeNoSubmit(), 200);
|
||||
setTimeout(() => this.reloadIframe(), 200);
|
||||
}
|
||||
|
||||
setLayout(layout: string) {
|
||||
@@ -114,14 +118,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,18 +129,16 @@ 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;
|
||||
const niceURL = href.match(/.*?\/\/.*?(\/.*)/)[1];
|
||||
this.actualIframeUrl = niceURL;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,35 +151,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,6 @@
|
||||
|
||||
<ngx-monaco-editor [(ngModel)]="code"
|
||||
class="tfw-ide-editor"
|
||||
(keyup)="editorWriteHandler()"
|
||||
(keydown)="editorWriteHandler()"
|
||||
[options]="editorOptions"
|
||||
></ngx-monaco-editor>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -31,6 +31,7 @@ export class IdeComponent implements OnInit {
|
||||
|
||||
files: string[];
|
||||
filename = '';
|
||||
lastFilename = '';
|
||||
code = 'Loading your file...';
|
||||
|
||||
codeState = CodeState.SAVED;
|
||||
@@ -94,7 +95,10 @@ export class IdeComponent implements OnInit {
|
||||
}
|
||||
this.code = (message.content != null) ? message.content : this.code;
|
||||
this.files = message.files;
|
||||
this.autoDetectEditorLanguage(this.filename);
|
||||
if (this.lastFilename !== this.filename) {
|
||||
this.autoDetectEditorLanguage(this.filename);
|
||||
}
|
||||
this.lastFilename = this.filename;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,5 +29,8 @@ export const LanguageMap: { [extension: string]: string; } = {
|
||||
yml: 'yaml',
|
||||
css: 'css',
|
||||
less: 'less',
|
||||
scss: 'scss'
|
||||
scss: 'scss',
|
||||
sh: 'shell',
|
||||
kt: 'kotlin',
|
||||
kts: 'kotlin'
|
||||
};
|
||||
|
||||
@@ -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,7 +1,8 @@
|
||||
<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>
|
||||
@@ -9,9 +10,11 @@
|
||||
</div>
|
||||
<div class="tfw-grid-message-body" [innerHtml]="message.message"></div>
|
||||
</div>
|
||||
<div *ngIf="showTypingIndicator" class="tfw-grid-message jumping-circle-container">
|
||||
<div class="jumping-circle" id="jc1"></div>
|
||||
<div class="jumping-circle" id="jc2"></div>
|
||||
<div class="jumping-circle" id="jc3"></div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</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 { 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user