Compare commits

...
Author SHA1 Message Date
raciborski f768b74893 Merge pull request #55 from avatao-content/message-buttons
Change message active button colors
2020-07-01 13:23:31 +02:00
Gabor PEK 670ac0f3dc Use global variables for shadow colors 2020-06-30 15:48:04 +02:00
Gabor PEK bca9861a4d Change message active button colors 2020-06-29 17:17:05 +02:00
ni-richard 3da348efca Merge pull request #54 from avatao-content/message-buttons
Message button support in bot messages
2020-06-23 08:17:13 +02:00
Gabor PEK 2f9aa9c298 Change message button type in send message 2020-06-22 13:43:55 +02:00
Gabor PEK d92f3da04d Refactor message buttons using Records for mapping 2020-06-22 12:19:33 +02:00
Gabor PEK aacd8aea60 Fix active message button style 2020-06-17 17:58:31 +02:00
Gabor PEK fb2735ccc7 Fix style of message buttons 2020-06-17 17:47:57 +02:00
Gabor PEK 31cf75f63b Add message buttons to control the user flow 2020-06-17 17:19:22 +02:00
Gabor PEK 0f324a108b Change look and UI of messages, add new Avatao logo 2020-06-12 17:04:29 +02:00
Gabor PEK 2b301edccc Redefine color palette 2020-06-08 11:24:39 +02:00
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
Kristóf Tóth 46fe26861c Fix Firefox iframe about:blank after load braindeath 2019-10-02 15:58:38 +02:00
Kristóf Tóth ac1bb81162 Add version file 2019-09-30 13:38:00 +02:00
Kristóf Tóth 38f3bfd6e5 Call editor writehandler on keydown instead of keyup 2019-09-27 14:49:02 +02:00
ni-richard f90fd78507 Update README.md 2019-09-17 05:25:42 +02:00
36 changed files with 7581 additions and 845 deletions
+3 -241
View File
@@ -1,244 +1,6 @@
# frontend-tutorial-framework # 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. To learn more about the framework and get started, please consult our [wiki](https://github.com/avatao-content/baseimage-tutorial-framework/wiki).
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"
}
}
```
+1
View File
@@ -0,0 +1 @@
chausie
+2 -1
View File
@@ -5,7 +5,8 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve --proxy-config proxy.conf.json", "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, "private": true,
"dependencies": { "dependencies": {
+4 -1
View File
@@ -17,7 +17,7 @@ import { TerminalComponent } from './terminal/terminal.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { TestmessengerComponent } from './testmessenger/testmessenger.component'; import { TestmessengerComponent } from './testmessenger/testmessenger.component';
import { DeploymentNotificationService } from './services/deployment-notification.service'; 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 { ConsoleComponent } from './console/console.component';
import { MonacoEditorModule } from 'ngx-monaco-editor'; import { MonacoEditorModule } from 'ngx-monaco-editor';
import { import {
@@ -28,6 +28,7 @@ import {
} from './services/config.service'; } from './services/config.service';
import { FSMUpdateService } from './services/fsmupdate.service'; import { FSMUpdateService } from './services/fsmupdate.service';
import { LoaderComponent } from './loader/loader.component'; import { LoaderComponent } from './loader/loader.component';
import {CapitalizePipe} from './pipes/capitalize.pipe';
@NgModule({ @NgModule({
@@ -41,6 +42,8 @@ import { LoaderComponent } from './loader/loader.component';
DashboardComponent, DashboardComponent,
TestmessengerComponent, TestmessengerComponent,
SafePipe, SafePipe,
SafeHtmlPipe,
CapitalizePipe,
ConsoleComponent, ConsoleComponent,
LoaderComponent LoaderComponent
], ],
+5 -6
View File
@@ -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 [ngClass]="{'hide-attribute': hideMessages | async}" class="tfw-messages">
<div [ngClass]="{'hide-attribute': hideMessages | async}" <app-messages></app-messages>
class="tfw-messages"
#tfwmessages>
<app-messages (newMessageEvent)="scrollMessagesToBottom()"></app-messages>
</div> </div>
<div class="tfw-header"><app-header></app-header></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()">&#8635;</button> <button class="refresh btn btn-sm rounded-circle" (click)="reloadIframe()">&#8635;</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()"
+6 -7
View File
@@ -57,7 +57,7 @@
.tfw-messages { .tfw-messages {
// Check whether the layout contains a web component // Check whether the layout contains a web component
div[class*="web"] & { div[class*="web"] & {
border: 2px solid $tao-gray-100; border: 2px solid $tao-base-color;
border-top: 0; border-top: 0;
border-left: 0; border-left: 0;
border-bottom: 0; border-bottom: 0;
@@ -65,17 +65,16 @@
} }
.tfw-header { .tfw-header {
padding: $small; padding: $tiny;
padding-top: $tiny; padding-top: $hair;
background-color: $tao-gray-50; background-color: $tao-base-color;
} }
@include set-scrollbar-style('dark', '.tfw-messages'); @include set-scrollbar-style('dark', '.tfw-messages');
.tfw-messages { .tfw-messages {
padding: $space; padding: $tiny;
padding-top: $hair; background-color: $tao-base-color;
background-color: $tao-gray-50;
overflow-y: scroll; overflow-y: scroll;
max-height: 95vmin; max-height: 95vmin;
+25 -51
View File
@@ -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,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
} }
reloadIframeHandler(message: WebSocketMessage) { reloadIframeHandler(message: WebSocketMessage) {
setTimeout(() => this.reloadIframeNoSubmit(), 200); this.iframeReloadPoller.ok.subscribe(() => this.reloadIframe());
this.iframeReloadPoller.start();
} }
setLayout(layout: string) { setLayout(layout: string) {
@@ -114,14 +119,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 +130,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 +152,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();
}
} }
} }
+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 -3
View File
@@ -1,4 +1,3 @@
<div class="tfw-grid-navbar tao-grid-center-center"> <div>
<img src="images/avatao_logo.svg" routerLink="/" class="tao-grid-center-left tfw-company-logo" alt=""> <img src="images/avatao-tutorial-framework-logo.svg" routerLink="/" class="tao-grid-center-left tfw-company-logo" alt="">
<span class="tfw-header-title">Tutorials</span>
</div> </div>
+2 -13
View File
@@ -1,19 +1,8 @@
@import "../../assets/scss/variables.scss"; @import "../../assets/scss/variables.scss";
.tfw-header-title {
color: $tao-blue-500;
font-size: $font-size-h3;
}
.tfw-grid-navbar {
display: grid;
grid-template-columns: $company-logo-width 1fr;
grid-column-gap: 8px;
width: 100%;
}
.tfw-company-logo { .tfw-company-logo {
display: block; display: block;
width: $company-logo-width; padding: $tiny;
width: 80%;
} }
+1 -1
View File
@@ -36,6 +36,6 @@
<ngx-monaco-editor [(ngModel)]="code" <ngx-monaco-editor [(ngModel)]="code"
class="tfw-ide-editor" class="tfw-ide-editor"
(keyup)="editorWriteHandler()" (keydown)="editorWriteHandler()"
[options]="editorOptions" [options]="editorOptions"
></ngx-monaco-editor> ></ngx-monaco-editor>
+2 -1
View File
@@ -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;
+11 -1
View File
@@ -32,6 +32,7 @@ export class IdeComponent implements OnInit {
files: string[]; files: string[];
filename = ''; filename = '';
lastFilename = ''; lastFilename = '';
reloadFile = false;
code = 'Loading your file...'; code = 'Loading your file...';
codeState = CodeState.SAVED; codeState = CodeState.SAVED;
@@ -85,10 +86,18 @@ export class IdeComponent implements OnInit {
} }
reloadHandler(message: WebSocketMessage) { reloadHandler(message: WebSocketMessage) {
if (!this.reloadFile) {
this.reloadFile = true;
this.requestCode(); this.requestCode();
} }
}
readHandler(message: IDEMessage) { readHandler(message: IDEMessage) {
if (!this.reloadFile && message.reload) {
return;
}
this.reloadFile = false;
if (this.codeState === CodeState.SAVED) { if (this.codeState === CodeState.SAVED) {
if (this.filename !== message.filename) { if (this.filename !== message.filename) {
this.filename = message.filename; this.filename = message.filename;
@@ -189,7 +198,8 @@ export class IdeComponent implements OnInit {
requestCode() { requestCode() {
this.webSocketService.send({ this.webSocketService.send({
'key': 'ide.read', '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', css: 'css',
less: 'less', less: 'less',
scss: 'scss', scss: 'scss',
sh: 'shell' sh: 'shell',
kt: 'kotlin',
kts: 'kotlin'
}; };
+4 -3
View File
@@ -1,13 +1,14 @@
import { WebSocketMessage } from './websocket-message'; import { WebSocketMessage } from './websocket-message';
export interface MessageConfig extends WebSocketMessage { export type MessageButton = 'yes' | 'no' | 'fix' | 'hint' | 'solution' | 'next';
originator?: string; export type MessageButtonMap = Record<MessageButton, {caption: string}>;
}
export interface MessageData { export interface MessageData {
originator?: string; originator?: string;
timestamp?: Date; timestamp?: Date;
typing?: boolean; typing?: boolean;
buttons?: MessageButton[];
command?: any;
message: string; message: string;
} }
+1
View File
@@ -4,4 +4,5 @@ export interface IDEMessage extends WebSocketMessage {
filename: string; filename: string;
content?: string; content?: string;
files?: string[]; files?: string[];
reload?: boolean;
} }
+17 -4
View File
@@ -1,15 +1,28 @@
<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"/>
<div class="tao-grid-center-left originator">{{message.originator}}</div> <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 class="timestamp tao-grid-center-right">{{message.timestamp | date:'HH:mm:ss'}}</div>
</div> </div>
<div class="tfw-grid-message-body" [innerHtml]="message.message"></div> <div class="tfw-grid-message-body" [innerHtml]="message.message | safeHtml"></div>
<div *ngIf="message.buttons !== undefined">
<div class="tfw-message-btn-divider"></div>
<div class="tfw-message-btn">
<button *ngFor="let messageButton of message.buttons"
class="{{messageButton}}"
(click)="sendButtonCommand(messageButton)">
{{buttonMap[messageButton].caption}}
</button>
</div> </div>
<div *ngIf="showTypingIndicator" class="tfw-grid-message jumping-circle-container"> </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="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>
+70 -17
View File
@@ -1,29 +1,73 @@
@import "../../assets/scss/variables.scss"; @import "../../assets/scss/variables.scss";
@import "../../assets/scss/mixins/layout"; @import "../../assets/scss/mixins/layout";
@import "../../assets/scss/mixins/button";
.tfw-next-button {
text-align: center;
}
.tfw-grid-message { .tfw-grid-message {
font-family: "Roboto";
font-style: normal;
display: grid; display: grid;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
grid-row-gap: $hair; grid-row-gap: $nano;
width: 100%; width: 100%;
padding: $hair;
background-color: $tao-gray-100;
border-radius: $tao-panel-border-radius-sm; border-radius: $tao-panel-border-radius-sm;
padding: $tiny;
font-size: $font-size-base; font-size: $font-size-base;
margin-bottom: $hair; margin-bottom: $hair;
animation-name: inflate; animation-name: inflate;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-timing-function: cubic-bezier(0.01, 0.1, 0, 1); animation-timing-function: cubic-bezier(0.01, 0.1, 0, 1);
color: white;
background-color: $tao-card-color;
.tfw-message-btn-divider {
opacity: 0.25;
border: 1px solid $tao-gray-color;
}
.tfw-message-btn {
color: $tao-btn-font-color;
padding-top: $tiny;
padding-bottom: $hair;
display: flex;
align-items: center;
text-align: center;
.no {
@include set-button-style($tao-red-color, $tao-shadow-red-color);
width: $xlarge;
}
.yes {
@include set-button-style($tao-green-color, $tao-shadow-green-color);
width: $xlarge;
}
.solution {
@include set-button-style(white, $tao-shadow-color);
width: $xlarge + $medium + $tiny;
}
.hint {
@include set-button-style($tao-blue-color, $tao-shadow-blue-color);
width: $xlarge;
}
.fix {
@include set-button-style($tao-yellow-color, $tao-shadow-yellow-color);
width: $xlarge + $medium + $tiny;
}
.next {
@include set-button-style($tao-blue-color, $tao-shadow-blue-color);
width: $xlarge;
}
}
} }
.highlighted-message { .highlighted-message {
box-shadow: 0px 0px 3px 0px rgba($tao-gray-300,0.65); // box-shadow: 0px 0px 8px 0px $tao-blue-color;
border: 3px single $tao-blue-color;
} }
@keyframes inflate { @keyframes inflate {
@@ -65,24 +109,33 @@
.tfw-grid-message-header { .tfw-grid-message-header {
display: grid; display: grid;
grid-template-columns: 1fr 5fr 8fr; grid-template-columns: 5fr 8fr;
grid-column-gap: 4px; grid-column-gap: 4px;
width: 100%; width: 100%;
img {
width: 12px;
}
.originator { .originator {
font-weight: 500; font-style: normal;
font-weight: bold;
font-size: $font-size-base;
line-height: 26px;
display: flex;
align-items: center;
color: $tao-message-originator-color;
} }
.timestamp { .timestamp {
font-size: $font-size-tiny; font-size: $font-size-base;
color: $tao-gray-color;
opacity: 0.37; opacity: 0.37;
} }
} }
.tfw-grid-message-body { .tfw-grid-message-body {
@include word-break() @include word-break();
line-height: 26px;
& p {
margin-bottom: $tiny !important;
}
} }
+37 -4
View File
@@ -1,5 +1,5 @@
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, MessageButton, MessageButtonMap} 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';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
@@ -13,13 +13,21 @@ export class MessagesComponent implements OnInit {
@Output() newMessageEvent: EventEmitter<any> = new EventEmitter(); @Output() newMessageEvent: EventEmitter<any> = new EventEmitter();
newMessage: Subject<MessageData> = new Subject<MessageData>(); newMessage: Subject<MessageData> = new Subject<MessageData>();
showTypingIndicator = false; showTypingIndicator = false;
messages: MessageData[] = []; messages: MessageData[] = [];
buttonMap: MessageButtonMap = {
fix: {caption: 'Ready to fix'},
solution: {caption: 'Show solution'},
hint: {caption: 'Hint'},
next: {caption: 'Next'},
yes: {caption: 'Yes'},
no: {caption: 'No'},
};
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 +35,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 +50,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 +66,23 @@ 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);
}
}
sendButtonCommand(messageButton: MessageButton) {
this.websocketService.send({
'key': 'message.button.click',
'value': messageButton
});
}
} }
+13
View File
@@ -0,0 +1,13 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'capitalize',
})
export class CapitalizePipe implements PipeTransform {
transform(value: string): string {
if (!value) {
return '';
}
return value[0].toLocaleUpperCase() + value.toLocaleLowerCase().slice(1);
}
}
+11
View File
@@ -11,3 +11,14 @@ export class SafePipe implements PipeTransform {
return this.sanitizer.bypassSecurityTrustResourceUrl(value); 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'); layout = new BehaviorSubject<string>('terminal-ide-web');
hideMessages = new BehaviorSubject<boolean>(false); hideMessages = new BehaviorSubject<boolean>(false);
iframeUrl = new BehaviorSubject<string>('/webservice'); iframeUrl = new BehaviorSubject<string>('');
showUrlBar = new BehaviorSubject<boolean>(false); showUrlBar = new BehaviorSubject<boolean>(false);
terminalMenuItem = new BehaviorSubject<string>('terminal'); terminalMenuItem = new BehaviorSubject<string>('terminal');
reloadIframeOnDeploy = new BehaviorSubject<boolean>(false); reloadIframeOnDeploy = new BehaviorSubject<boolean>(false);
+44 -13
View File
@@ -8,11 +8,16 @@ export class TerminadoService {
xterm: Terminal; xterm: Terminal;
ws: WebSocket; ws: WebSocket;
attached = false; attached = false;
private dataListener: any;
private resizeListener: any;
constructor() { constructor() {
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
Terminal.applyAddon(terminado); this.xterm = this.createTerminal();
this.xterm = new Terminal({ }
createTerminal() {
return new Terminal({
theme: { theme: {
foreground: '#ffffff', foreground: '#ffffff',
background: '#0C0C0C', // $tao-gray-800 background: '#0C0C0C', // $tao-gray-800
@@ -21,8 +26,8 @@ export class TerminadoService {
black: '#000000', black: '#000000',
red: '#FF5252', // $tao-red-500 red: '#FF5252', // $tao-red-500
brightRed: '#FF7171', // $tao-red-400 brightRed: '#FF7171', // $tao-red-400
green: '#2fd19f', // $tao-bright-green-500 green: '#60FDEB', // $tao-bright-green-500
brightGreen: '#2fd19f', // $tao-bright-green-500 brightGreen: '#60FDEB', // $tao-bright-green-500
brightYellow: '#FFD283', // $tao-warm-yellow-300 brightYellow: '#FFD283', // $tao-warm-yellow-300
yellow: '#FFB83B', // $tao-warm-yellow-500 yellow: '#FFB83B', // $tao-warm-yellow-500
magenta: '#FF8FC6', // $tao-pink-200 magenta: '#FF8FC6', // $tao-pink-200
@@ -37,26 +42,52 @@ export class TerminadoService {
}, },
fontSize: 14 fontSize: 14
}); });
const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
this.ws = new WebSocket(wsproto + window.location.host + '/terminal');
} }
attach(element: HTMLElement) { 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 = () => { this.ws.onopen = () => {
(<any>this.xterm).terminadoAttach(this.ws); this.attached = true;
this.xterm = this.createTerminal();
this.xterm.open(element); this.xterm.open(element);
this.fit(); this.fit();
this.xterm.blur(); // In order to reset the terminal state after a broken socket, we need to register the listeners manually.
this.attached = true; (<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() { detach() {
(<any>this.xterm).terminadoDetach(this.ws); if (!this.attached) {
this.xterm.destroy(); return;
this.ws.close(); }
this.attached = false; this.attached = false;
this.dataListener.dispose();
this.resizeListener.dispose();
} }
fit() { fit() {
+17 -3
View File
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable, Subject, Subscription } from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { filter, map } from 'rxjs/operators'; import { filter, map } from 'rxjs/operators';
import { WebSocketMessage } from '../message-types/websocket-message'; import { WebSocketMessage } from '../message-types/websocket-message';
@@ -17,14 +17,28 @@ export enum Intent {
@Injectable() @Injectable()
export class WebSocketService { export class WebSocketService {
private ws: WebSocketSubject<WebSocketMessage>; private ws: WebSocketSubject<WebSocketMessage>;
private subject: Subject<WebSocketMessage> = new Subject<WebSocketMessage>();
private subscription: Subscription;
constructor() {} constructor() {}
public connect() { public connect() {
if (!this.ws) { if (!this.ws) {
if (this.subscription) {
this.subscription.unsubscribe();
}
const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://'; const wsproto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
const connAddr = wsproto + window.location.host + '/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> { public observeAll<T extends WebSocketMessage>(key: string): Observable<T> {
return this.ws.pipe( return this.subject.pipe(
filter(message => message.key.startsWith(key)), filter(message => message.key.startsWith(key)),
map(message => <T> message) map(message => <T> message)
); );
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 11.98 16.98"><defs><style>.cls-1{fill:none;stroke:#277eec;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><title>avataobot</title><rect class="cls-1" x="2.46" y="7.47" width="7.05" height="7.05" transform="translate(9.53 -1.01) rotate(45)"/><polyline class="cls-1" points="10.98 1 5.99 5.99 1 1"/></svg>

Before

Width:  |  Height:  |  Size: 417 B

+1
View File
@@ -3,6 +3,7 @@
'grid'; 'grid';
@import @import
'mixins/button',
'mixins/scrollbar'; 'mixins/scrollbar';
@import @import
+20 -48
View File
@@ -2,7 +2,7 @@
// Spaces // Spaces
// //
$space: 24px; $space: 32px;
$sxlarge: 9 * $space; $sxlarge: 9 * $space;
$xxxlarge: 6 * $space; $xxxlarge: 6 * $space;
@@ -30,30 +30,6 @@ $tao-blue-700: #195684;
$tao-blue-800: #103B5B; $tao-blue-800: #103B5B;
$tao-blue-900: #081A2B; $tao-blue-900: #081A2B;
// Tao sky palette
$tao-sky-50: #F1FAFD;
$tao-sky-100: #C5E9F5;
$tao-sky-200: #9AD8EE;
$tao-sky-300: #6EC7E6;
$tao-sky-400: #42B7DF;
$tao-sky-500: #19A7D8;
$tao-sky-600: #0E8BA8;
$tao-sky-700: #04647A;
$tao-sky-800: #004251;
$tao-sky-900: #002028;
// Tao phtalo palette
$tao-phtalo-50: #F2FBFC;
$tao-phtalo-100: #C8EDF1;
$tao-phtalo-200: #9FDFE6;
$tao-phtalo-300: #75D1DB;
$tao-phtalo-400: #4CC3D0;
$tao-phtalo-500: #24B6C6;
$tao-phtalo-600: #16989E;
$tao-phtalo-700: #0C7575;
$tao-phtalo-800: #034C4F;
$tao-phtalo-900: #002426;
// Tao turqoise palette // Tao turqoise palette
$tao-turqoise-50: #F1FBFB; $tao-turqoise-50: #F1FBFB;
$tao-turqoise-100: #C3EFEF; $tao-turqoise-100: #C3EFEF;
@@ -90,29 +66,6 @@ $tao-warm-yellow-700: #CC8B36;
$tao-warm-yellow-800: #B2762F; $tao-warm-yellow-800: #B2762F;
$tao-warm-yellow-900: #996526; $tao-warm-yellow-900: #996526;
// Tao plum palette
$tao-plum-50: #F6F8FD;
$tao-plum-100: #DADFF8;
$tao-plum-200: #BEC7F3;
$tao-plum-300: #A2AFED;
$tao-plum-400: #8797E8;
$tao-plum-500: #6C80E3;
$tao-plum-600: #5E77BF;
$tao-plum-700: #4B5E99;
$tao-plum-800: #384672;
$tao-plum-900: #272F4C;
// Tao pink palette
$tao-pink-50: #FFF0F7;
$tao-pink-100: #FFBFDF;
$tao-pink-200: #FF8FC6;
$tao-pink-300: #FF5EAE;
$tao-pink-400: #FF2E95;
$tao-pink-500: #FF007E;
$tao-pink-600: #D8007C;
$tao-pink-700: #B20066;
$tao-pink-800: #8C0050;
$tao-pink-900: #66003A;
// Tao red palette // Tao red palette
$tao-red-50: #FFF5F5; $tao-red-50: #FFF5F5;
@@ -138,6 +91,25 @@ $tao-gray-700: #232323;
$tao-gray-800: #0C0C0C; $tao-gray-800: #0C0C0C;
$tao-gray-900: #000000; $tao-gray-900: #000000;
// Tao messages
$tao-base-color: #2A324C;
$tao-btn-font-color: #1F2836;
$tao-card-color: #363E56;
$tao-message-originator-color: #6697FF;
$tao-copy-color: #FFFFFF;
$tao-timestamp-color: #94A6B9;
$tao-gray-color: #94A6B9; // @25% opacity
$tao-shadow-color: rgba(255, 255, 255, 0.5);
$tao-yellow-color: #FFD68F;
$tao-shadow-yellow-color: rgba(255, 214, 143, 0.5);
$tao-blue-color: #6697FF;
$tao-shadow-blue-color: rgba(102, 151, 255, 0.5);
$tao-green-color: #48BEB0;
$tao-shadow-green-color: rgba(72, 190, 176, 0.5);
$tao-cyan-color: #60FDEB;
$tao-red-color: #FF4583;
$tao-shadow-red-color: rgba(255, 69, 131, 0.5);
// Panel border radius // Panel border radius
$tao-panel-border-radius: 14px; $tao-panel-border-radius: 14px;
+27
View File
@@ -0,0 +1,27 @@
@import "../variables.scss";
@mixin set-button-style($color, $shadow-color) {
box-sizing: border-box;
border-radius: 10px;
background: $color;
border: 1.5px solid $color;
font-style: normal;
font-weight: bold;
font-size: 14px;
line-height: 26px;
height: $space;
margin-right: $tiny;
color: $tao-btn-font-color;
outline: none;
&:active {
opacity: 0.5;
box-shadow: none;
}
&:hover {
box-shadow: 0 0 8px $shadow-color;
}
}
+16 -16
View File
@@ -2,8 +2,8 @@ $grid-columns-count: 100;
$grid-rows-count: 30; $grid-rows-count: 30;
$terminal-ide-web-layout: ( $terminal-ide-web-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 52),
'messages': (0, 20, 8, 60), 'header': (0, 20, 52, 60),
'ide': (56, 96, 0, 100), 'ide': (56, 96, 0, 100),
'terminal': (0, 56, 60, 100), 'terminal': (0, 56, 60, 100),
'web': (20, 56, 0, 60), 'web': (20, 56, 0, 60),
@@ -11,8 +11,8 @@ $terminal-ide-web-layout: (
); );
$terminal-web-layout: ( $terminal-web-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (), 'ide': (),
'terminal': (56, 96, 0, 100), 'terminal': (56, 96, 0, 100),
'web': (20, 56, 0, 100), 'web': (20, 56, 0, 100),
@@ -20,8 +20,8 @@ $terminal-web-layout: (
); );
$terminal-ide-vertical-layout: ( $terminal-ide-vertical-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (56, 96, 0, 100), 'ide': (56, 96, 0, 100),
'terminal': (20, 56, 0, 100), 'terminal': (20, 56, 0, 100),
'web': (), 'web': (),
@@ -29,8 +29,8 @@ $terminal-ide-vertical-layout: (
); );
$terminal-ide-horizontal-layout: ( $terminal-ide-horizontal-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (20, 96, 0, 60), 'ide': (20, 96, 0, 60),
'terminal': (20, 96, 60, 100), 'terminal': (20, 96, 60, 100),
'web': (), 'web': (),
@@ -38,8 +38,8 @@ $terminal-ide-horizontal-layout: (
); );
$ide-web-vertical-layout: ( $ide-web-vertical-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (56, 96, 0, 100), 'ide': (56, 96, 0, 100),
'terminal': (), 'terminal': (),
'web': (20, 56, 0, 100), 'web': (20, 56, 0, 100),
@@ -47,8 +47,8 @@ $ide-web-vertical-layout: (
); );
$terminal-only-layout: ( $terminal-only-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (), 'ide': (),
'terminal': (20, 96, 0, 100), 'terminal': (20, 96, 0, 100),
'web': (), 'web': (),
@@ -56,8 +56,8 @@ $terminal-only-layout: (
); );
$ide-only-layout: ( $ide-only-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (20, 96, 0, 100), 'ide': (20, 96, 0, 100),
'terminal': (), 'terminal': (),
'web': (), 'web': (),
@@ -65,8 +65,8 @@ $ide-only-layout: (
); );
$web-only-layout: ( $web-only-layout: (
'header': (0, 20, 0, 8), 'messages': (0, 20, 0, 92),
'messages': (0, 20, 8, 100), 'header': (0, 20, 92, 100),
'ide': (), 'ide': (),
'terminal': (), 'terminal': (),
'web': (20, 96, 0, 100), 'web': (20, 96, 0, 100),
+5 -4
View File
@@ -1,10 +1,10 @@
@mixin set-scrollbar-style($style, $selctor) { @mixin set-scrollbar-style($style, $selector) {
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 3px !important; width: 3px !important;
height: 3px !important; height: 3px !important;
} }
#{$selctor}::-webkit-scrollbar-track { #{$selector}::-webkit-scrollbar-track {
border-radius: 10px !important; border-radius: 10px !important;
width: 3px !important; width: 3px !important;
@if ($style == 'light') { @if ($style == 'light') {
@@ -15,7 +15,7 @@
}; };
} }
#{$selctor}::-webkit-scrollbar-thumb { #{$selector}::-webkit-scrollbar-thumb {
@if ($style == 'dark') { @if ($style == 'dark') {
background: #8a8a8a !important; background: #8a8a8a !important;
@@ -25,7 +25,7 @@
border-radius: 10px !important; border-radius: 10px !important;
} }
#{$selctor}::-webkit-scrollbar-thumb:hover { #{$selector}::-webkit-scrollbar-thumb:hover {
@if ($style == 'dark') { @if ($style == 'dark') {
background: #424242 !important; background: #424242 !important;
@@ -35,3 +35,4 @@
cursor: pointer !important; cursor: pointer !important;
} }
} }
+1
View File
@@ -5,6 +5,7 @@
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="images/favicon.ico"> <link rel="icon" type="image/x-icon" href="images/favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap" rel="stylesheet" type="text/css">
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>
+1
View File
@@ -8,5 +8,6 @@ body, html {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
overflow: hidden; overflow: hidden;
@include set-scrollbar-style('light', ''); @include set-scrollbar-style('light', '');
} }
+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