mirror of
https://github.com/avatao-content/frontend-tutorial-framework
synced 2024-12-04 19:01:32 +00:00
Merge branch 'dynamic_configger' into chausie
This commit is contained in:
commit
074fa89db7
@ -7,16 +7,15 @@ import { TerminalComponent } from './terminal/terminal.component';
|
||||
import { MessagesComponent } from './messages/messages.component';
|
||||
import { ConsoleComponent } from './console/console.component';
|
||||
import { TestmessengerComponent } from './testmessenger/testmessenger.component';
|
||||
import { config } from './config';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full'},
|
||||
{ path: config.dashboard.route, component: DashboardComponent},
|
||||
{ path: config.ide.route, component: IdeComponent },
|
||||
{ path: config.terminal.route, component: TerminalComponent },
|
||||
{ path: config.messages.route, component: MessagesComponent },
|
||||
{ path: config.console.route, component: ConsoleComponent },
|
||||
{ path: config.testmessenger.route, component: TestmessengerComponent }
|
||||
{ path: 'dashboard', component: DashboardComponent},
|
||||
{ path: 'ide', component: IdeComponent },
|
||||
{ path: 'terminal', component: TerminalComponent },
|
||||
{ path: 'messages', component: MessagesComponent },
|
||||
{ path: 'console', component: ConsoleComponent },
|
||||
{ path: 'testmessenger', component: TestmessengerComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@ -1 +1,2 @@
|
||||
<router-outlet></router-outlet>
|
||||
<div #router><router-outlet></router-outlet></div>
|
||||
<app-loader [toBlur]="router"></app-loader>
|
||||
|
@ -0,0 +1,3 @@
|
||||
.blur {
|
||||
filter: blur(3px);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { config } from './config';
|
||||
import { SiteConfigService } from './services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@ -8,18 +8,27 @@ import { config } from './config';
|
||||
styleUrls: ['./app.component.scss']
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
documentTitle: string = config.documentTitle;
|
||||
|
||||
constructor(private titleService: Title) {}
|
||||
constructor(
|
||||
private titleService: Title,
|
||||
private configService: SiteConfigService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle(this.documentTitle);
|
||||
if (config.dashboard.askReloadSite) {
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
const confirmationMessage = 'Refreshing this page may mess up your challenge/tutorial state. Are you sure?';
|
||||
event.returnValue = confirmationMessage;
|
||||
return confirmationMessage;
|
||||
});
|
||||
}
|
||||
this.configService.init();
|
||||
this.configService.documentTitle.subscribe(title => this.titleService.setTitle(title));
|
||||
|
||||
this.configService.askReloadSite.subscribe(askReloadSite => {
|
||||
if (askReloadSite) {
|
||||
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
} else {
|
||||
window.removeEventListener('beforeunload', this.beforeUnloadHandler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
beforeUnloadHandler(event) {
|
||||
const confirmationMessage = 'Refreshing this page may mess up your challenge/tutorial state. Are you sure?';
|
||||
event.returnValue = confirmationMessage;
|
||||
return confirmationMessage;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import { AppComponent } from './app.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { HeaderComponent } from './header/header.component';
|
||||
import { SidebarComponent } from './sidebar/sidebar.component';
|
||||
import { WebComponent } from './web/web.component';
|
||||
import { MarkdownService } from './services/markdown.service';
|
||||
import { TerminadoService } from './services/terminado.service';
|
||||
import { IdeComponent } from './ide/ide.component';
|
||||
@ -21,6 +20,13 @@ import { DeploymentNotificationService } from './services/deployment-notificatio
|
||||
import { SafePipe } from './pipes/safe.pipe';
|
||||
import { ConsoleComponent } from './console/console.component';
|
||||
import { MonacoEditorModule } from 'ngx-monaco-editor';
|
||||
import {
|
||||
ConfigReadyService,
|
||||
IdeConfigService,
|
||||
DashboardConfigService,
|
||||
SiteConfigService
|
||||
} from './services/config.service';
|
||||
import { LoaderComponent } from './loader/loader.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@ -28,14 +34,14 @@ import { MonacoEditorModule } from 'ngx-monaco-editor';
|
||||
AppComponent,
|
||||
HeaderComponent,
|
||||
SidebarComponent,
|
||||
WebComponent,
|
||||
IdeComponent,
|
||||
MessagesComponent,
|
||||
TerminalComponent,
|
||||
DashboardComponent,
|
||||
TestmessengerComponent,
|
||||
SafePipe,
|
||||
ConsoleComponent
|
||||
ConsoleComponent,
|
||||
LoaderComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@ -50,9 +56,13 @@ import { MonacoEditorModule } from 'ngx-monaco-editor';
|
||||
WebSocketService,
|
||||
TerminadoService,
|
||||
DeploymentNotificationService,
|
||||
ConfigReadyService,
|
||||
IdeConfigService,
|
||||
DashboardConfigService,
|
||||
SiteConfigService
|
||||
],
|
||||
bootstrap: [
|
||||
AppComponent
|
||||
]
|
||||
})
|
||||
export class AppModule { }
|
||||
export class AppModule {}
|
||||
|
@ -1,53 +0,0 @@
|
||||
export const config = {
|
||||
documentTitle: 'Avatao Tutorials',
|
||||
dashboard: {
|
||||
route: 'dashboard',
|
||||
triggerFirstFSMStep: 'step_1',
|
||||
askReloadSite: false,
|
||||
sendReadyAfterPageReload: true,
|
||||
terminalOrConsole: 'terminal',
|
||||
currentLayout: 'terminal-ide-web',
|
||||
enabledLayouts: [
|
||||
'terminal-ide-web',
|
||||
'terminal-ide-vertical',
|
||||
'terminal-web',
|
||||
'ide-web-vertical',
|
||||
'terminal-ide-horizontal',
|
||||
'terminal-only',
|
||||
'ide-only',
|
||||
'web-only'
|
||||
],
|
||||
iframeUrl: '/webservice',
|
||||
showUrlBar: false,
|
||||
hideMessages: false
|
||||
},
|
||||
ide: {
|
||||
route: 'ide',
|
||||
autoSaveInterval: 444,
|
||||
defaultCode: 'Loading your file...',
|
||||
defaultLanguage: 'text',
|
||||
deployButtonText: {
|
||||
'TODEPLOY': 'Deploy',
|
||||
'DEPLOYED': 'Deployed',
|
||||
'DEPLOYING': 'Reloading app...',
|
||||
'FAILED': 'Deployment failed. Retry'
|
||||
},
|
||||
showDeployButton: true,
|
||||
reloadIframeOnDeploy: false,
|
||||
autoDetectFileLanguage: true,
|
||||
showMiniMap: false
|
||||
},
|
||||
terminal: {
|
||||
route: 'shell'
|
||||
},
|
||||
messages: {
|
||||
route: 'messages'
|
||||
},
|
||||
console: {
|
||||
route: 'console',
|
||||
defaultContent: '',
|
||||
},
|
||||
testmessenger: {
|
||||
route: 'testmessenger'
|
||||
}
|
||||
};
|
@ -2,7 +2,6 @@ import { Component, OnInit, Output, EventEmitter } from '@angular/core';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
import { WebSocketMessage } from '../message-types/websocket-message';
|
||||
import { ConsoleReadMessage, ConsoleWriteMessage } from '../message-types/console-messages';
|
||||
import { config } from '../config';
|
||||
|
||||
@Component({
|
||||
selector: 'app-console',
|
||||
@ -10,7 +9,7 @@ import { config } from '../config';
|
||||
styleUrls: ['./console.component.scss']
|
||||
})
|
||||
export class ConsoleComponent implements OnInit {
|
||||
console_content: string = config.console.defaultContent;
|
||||
console_content = '';
|
||||
|
||||
@Output() contentChanged = new EventEmitter<string>();
|
||||
|
||||
|
@ -1,31 +1,30 @@
|
||||
<div [attr.class]="layout">
|
||||
<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}"
|
||||
<div [ngClass]="{'hide-attribute': hideMessages | async}"
|
||||
class="tfw-messages"
|
||||
#tfwmessages>
|
||||
<app-messages (newMessageEvent)="scrollMessagesToBottom()"></app-messages>
|
||||
</div>
|
||||
<div class="tfw-web tao-grid-top-left"
|
||||
[ngClass]="{'deploy-blur': deploying || polling}">
|
||||
<app-web *ngIf="!iframeUrl"></app-web>
|
||||
<div *ngIf="showUrlBar" class="urlbar-container">
|
||||
<button class="refresh btn btn-sm rounded-circle" (click)="reloadIframe()">↻</button>
|
||||
<input type="text"
|
||||
#urlbar
|
||||
class="urlbar form-control"
|
||||
value="{{actualIframeUrl}}"
|
||||
(keyup.enter)="changeIframeURL()">
|
||||
<button class="go btn btn-sm rounded-circle" (click)="changeIframeURL()">⇨</button>
|
||||
</div>
|
||||
<div *ngIf="iframeUrl" class="iframe-container">
|
||||
<iframe class="iframe"
|
||||
#webiframe
|
||||
scrolling="yes"
|
||||
frameborder="0"
|
||||
(load)="iframeLoad()"
|
||||
[src]="iframeUrl | safe">
|
||||
</iframe>
|
||||
<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>
|
||||
<input type="text"
|
||||
#urlbar
|
||||
class="urlbar form-control"
|
||||
value="{{actualIframeUrl}}"
|
||||
(keyup.enter)="changeIframeURL()">
|
||||
<button class="go btn btn-sm rounded-circle" (click)="changeIframeURL()">⇨</button>
|
||||
</div>
|
||||
<iframe class="iframe"
|
||||
#webiframe
|
||||
scrolling="yes"
|
||||
frameborder="0"
|
||||
(load)="iframeLoad()"
|
||||
[src]="iframeUrl | async | safe">
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tfw-ide">
|
||||
@ -35,17 +34,17 @@
|
||||
<div class="btn-group btn-group-sm flex-wrap tao-grid-center-left tfw-console-terminal-menu">
|
||||
<button class="tfw-console-terminal-menu-button"
|
||||
(click)="selectTerminalMenuItem('terminal')"
|
||||
[class.selected]="selectedTerminalMenuItem === 'terminal'">TERMINAL</button>
|
||||
[class.selected]="(terminalMenuItem | async) === 'terminal'">TERMINAL</button>
|
||||
<button class="tfw-console-terminal-menu-button"
|
||||
(click)="selectTerminalMenuItem('console')"
|
||||
[class.selected]="selectedTerminalMenuItem === 'console'">CONSOLE</button>
|
||||
[class.selected]="(terminalMenuItem | async) === 'console'">CONSOLE</button>
|
||||
</div>
|
||||
<hr>
|
||||
<app-terminal [class.hidden]="selectedTerminalMenuItem !== 'terminal'"></app-terminal>
|
||||
<app-console [class.hidden]="selectedTerminalMenuItem !== 'console'"></app-console>
|
||||
<app-terminal [class.hidden]="(terminalMenuItem | async) !== 'terminal'"></app-terminal>
|
||||
<app-console [class.hidden]="(terminalMenuItem | async) !== 'console'"></app-console>
|
||||
</div>
|
||||
<div class="tfw-sidebar">
|
||||
<app-sidebar (layoutChanged)="setLayout($event)" [layout]="layout"></app-sidebar>
|
||||
<app-sidebar (layoutChanged)="setLayout($event)" [layout]="layout | async"></app-sidebar>
|
||||
</div>
|
||||
|
||||
<div class="tfw-terminal-footer"></div>
|
||||
|
@ -66,6 +66,7 @@
|
||||
|
||||
.tfw-header {
|
||||
padding: $small;
|
||||
padding-top: $tiny;
|
||||
background-color: $tao-gray-50;
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,7 @@ import { DeploymentNotificationService } from '../services/deployment-notificati
|
||||
import { Subscription } from 'rxjs';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
import { WebSocketMessage } from '../message-types/websocket-message';
|
||||
import { HideMessagesMessage, LayoutMessage, TerminalMenuItemMessage } from '../message-types/dashboard-messages';
|
||||
import { config } from '../config';
|
||||
import { LogMessage } from '../message-types/log-message';
|
||||
import { DashboardConfigService } from '../services/config.service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { delay, retryWhen, tap } from 'rxjs/operators';
|
||||
|
||||
@ -18,22 +16,19 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
deploying = false;
|
||||
polling = false;
|
||||
deploymentNotificationSubscription: Subscription;
|
||||
@ViewChild('webiframe', {static: true}) webiframe: ElementRef;
|
||||
@ViewChild('tfwmessages', {static: true}) messages: ElementRef;
|
||||
@ViewChild('urlbar', {static: true}) urlbar: ElementRef;
|
||||
@ViewChild('webiframe', {static: false}) webiframe: ElementRef;
|
||||
@ViewChild('tfwmessages', {static: false}) messages: ElementRef;
|
||||
@ViewChild('urlbar', {static: false}) urlbar: ElementRef;
|
||||
|
||||
layout: string = config.dashboard.currentLayout;
|
||||
hideMessages: boolean = config.dashboard.hideMessages;
|
||||
iframeUrl: string = config.dashboard.iframeUrl;
|
||||
showUrlBar = config.dashboard.showUrlBar;
|
||||
actualIframeUrl: string = this.iframeUrl;
|
||||
selectedTerminalMenuItem: string = config.dashboard.terminalOrConsole;
|
||||
layout = this.configService.layout;
|
||||
hideMessages = this.configService.hideMessages;
|
||||
showUrlBar = this.configService.showUrlBar;
|
||||
iframeUrl = this.configService.iframeUrl;
|
||||
actualIframeUrl = this.iframeUrl.value;
|
||||
terminalMenuItem = this.configService.terminalMenuItem;
|
||||
iframeReloadSubscription: Subscription;
|
||||
|
||||
command_handlers = {
|
||||
'dashboard.layout': this.layoutHandler.bind(this),
|
||||
'dashboard.hideMessages': this.hideMessagesHandler.bind(this),
|
||||
'dashboard.terminalMenuItem': this.terminalMenuItemHandler.bind(this),
|
||||
'dashboard.reloadFrontend': this.reloadFrontendHandlder.bind(this),
|
||||
'dashboard.reloadIframe': this.reloadIframeHandler.bind(this)
|
||||
};
|
||||
@ -41,13 +36,20 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
constructor(private deploymentNotificationService: DeploymentNotificationService,
|
||||
private webSocketService: WebSocketService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private http: HttpClient) {}
|
||||
private http: HttpClient,
|
||||
private configService: DashboardConfigService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.webSocketService.connect();
|
||||
this.configService.init();
|
||||
this.subscribeResizeOnLayoutChange();
|
||||
this.initCommandHandling();
|
||||
this.initDeploymentNotifications();
|
||||
this.sendReadyIfNeeded();
|
||||
this.sendReady();
|
||||
}
|
||||
|
||||
subscribeResizeOnLayoutChange() {
|
||||
this.configService.layout.subscribe(() => this.emitResizeEvent());
|
||||
}
|
||||
|
||||
initCommandHandling() {
|
||||
@ -61,7 +63,7 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
this.deploymentNotificationSubscription = this.deploymentNotificationService.deploying.subscribe(
|
||||
(deploying) => {
|
||||
this.deploying = deploying;
|
||||
if (!deploying && config.ide.reloadIframeOnDeploy) {
|
||||
if (!deploying && this.configService.reloadIframeOnDeploy.value) {
|
||||
if (this.polling) {
|
||||
this.iframeReloadSubscription.unsubscribe();
|
||||
}
|
||||
@ -70,26 +72,8 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
sendReadyIfNeeded() {
|
||||
if (config.dashboard.sendReadyAfterPageReload) {
|
||||
setTimeout(() => this.webSocketService.send({'key': 'frontend.ready'}));
|
||||
}
|
||||
}
|
||||
|
||||
layoutHandler(message: LayoutMessage) {
|
||||
if (config.dashboard.enabledLayouts.includes(message.value)) {
|
||||
this.setLayout(message.value);
|
||||
} else {
|
||||
console.log('Invalid ide layout "' + message.value + '" received!');
|
||||
}
|
||||
}
|
||||
|
||||
hideMessagesHandler(message: HideMessagesMessage) {
|
||||
this.hideMessages = message.value;
|
||||
}
|
||||
|
||||
terminalMenuItemHandler(message: TerminalMenuItemMessage) {
|
||||
this.selectTerminalMenuItem(message.value);
|
||||
sendReady() {
|
||||
setTimeout(() => this.webSocketService.send({'key': 'frontend.ready'}));
|
||||
}
|
||||
|
||||
reloadFrontendHandlder(message: WebSocketMessage) {
|
||||
@ -101,7 +85,11 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
setLayout(layout: string) {
|
||||
this.layout = layout;
|
||||
this.layout.next(layout);
|
||||
this.emitResizeEvent();
|
||||
}
|
||||
|
||||
emitResizeEvent() {
|
||||
// We need to trigger a 'resize' event manually, otherwise editor stays collapsed
|
||||
// editor 'resize' event listener requires a parameter of force=true
|
||||
setTimeout(() => window.dispatchEvent(new Event('resize', {force: true} as any)), 0);
|
||||
@ -124,18 +112,18 @@ export class DashboardComponent implements OnInit, OnDestroy {
|
||||
if (!item.match('(terminal|console)')) {
|
||||
return;
|
||||
}
|
||||
this.selectedTerminalMenuItem = item;
|
||||
this.terminalMenuItem.next(item);
|
||||
}
|
||||
|
||||
scrollMessagesToBottom(): void {
|
||||
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(): void {
|
||||
if (this.webiframe) {
|
||||
iframeLoad() {
|
||||
if (this.webiframe && this.iframeUrl.value) {
|
||||
const href = this.webiframe.nativeElement.contentWindow.frames.location.href;
|
||||
const niceURL = href.match(/.*?\/\/.*?(\/.*)/)[1];
|
||||
this.actualIframeUrl = niceURL;
|
||||
|
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
|
||||
<div class="btn-group-sm tfw-deploy-btn-group">
|
||||
<button *ngIf="showDeployButton"
|
||||
<button *ngIf="showDeployButton | async"
|
||||
type="submit"
|
||||
class="btn tfw-deploy-btn tao-grid-top-center"
|
||||
(click)="sendCodeIfDirty(); deployCode()"
|
||||
@ -23,13 +23,13 @@
|
||||
[class.deploy]="deployButtonState === DeployButtonState.DEPLOYING"
|
||||
[class.disabled]="deployButtonState === DeployButtonState.DEPLOYING || deployButtonState === DeployButtonState.DEPLOYED"
|
||||
[class.failed]="deployButtonState === DeployButtonState.FAILED">
|
||||
<span *ngIf="deployButtonState === DeployButtonState.TODEPLOY">{{deployButtonText['TODEPLOY']}}</span>
|
||||
<span *ngIf="deployButtonState === DeployButtonState.TODEPLOY">{{(deployButtonText | async)['TODEPLOY']}}</span>
|
||||
<span *ngIf="deployButtonState === DeployButtonState.DEPLOYED">
|
||||
<img src="images/greentick_icon.svg"/>
|
||||
<span>{{deployButtonText['DEPLOYED']}}</span>
|
||||
<span>{{(deployButtonText | async)['DEPLOYED']}}</span>
|
||||
</span>
|
||||
<span *ngIf="deployButtonState === DeployButtonState.DEPLOYING"><div class="loader"></div>{{deployButtonText['DEPLOYING']}}</span>
|
||||
<span *ngIf="deployButtonState === DeployButtonState.FAILED">{{deployButtonText['FAILED']}}</span>
|
||||
<span *ngIf="deployButtonState === DeployButtonState.DEPLOYING"><div class="loader"></div>{{(deployButtonText | async)['DEPLOYING']}}</span>
|
||||
<span *ngIf="deployButtonState === DeployButtonState.FAILED">{{(deployButtonText | async)['FAILED']}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@ import { WebSocketMessage } from '../message-types/websocket-message';
|
||||
import { IDEMessage } from '../message-types/ide-message';
|
||||
import { DeployMessage } from '../message-types/deploy-message';
|
||||
import { DeploymentNotificationService } from '../services/deployment-notification.service';
|
||||
import { config } from '../config';
|
||||
import { IdeConfigService } from '../services/config.service';
|
||||
import { LanguageMap } from './language-map';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@ -31,19 +31,19 @@ export class IdeComponent implements OnInit {
|
||||
|
||||
files: string[];
|
||||
filename = '';
|
||||
code: string = config.ide.defaultCode;
|
||||
code = 'Loading your file...';
|
||||
|
||||
codeState = CodeState.SAVED;
|
||||
deployButtonState = DeployButtonState.DEPLOYED;
|
||||
deployButtonText = config.ide.deployButtonText;
|
||||
showDeployButton: boolean = config.ide.showDeployButton;
|
||||
deployButtonText = this.configService.deployButtonText;
|
||||
showDeployButton = this.configService.showDeployButton;
|
||||
autosave = null;
|
||||
|
||||
editorOptions = {
|
||||
theme: 'vs-dark',
|
||||
language: config.ide.defaultLanguage,
|
||||
language: 'text',
|
||||
glyphMargin: false,
|
||||
minimap: {enabled: config.ide.showMiniMap}
|
||||
minimap: {enabled: false}
|
||||
};
|
||||
|
||||
command_handlers = {
|
||||
@ -54,10 +54,12 @@ export class IdeComponent implements OnInit {
|
||||
|
||||
constructor(private webSocketService: WebSocketService,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private deploymentNotificationService: DeploymentNotificationService) { }
|
||||
private deploymentNotificationService: DeploymentNotificationService,
|
||||
private configService: IdeConfigService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.webSocketService.connect();
|
||||
this.configService.init();
|
||||
this.subscribeWS();
|
||||
this.subscribeFirstLanguageDetection();
|
||||
this.requestCode();
|
||||
@ -77,7 +79,7 @@ export class IdeComponent implements OnInit {
|
||||
|
||||
subscribeFirstLanguageDetection() {
|
||||
this.webSocketService.observeKey<IDEMessage>('ide.read').pipe(first()).subscribe(
|
||||
() => this.autoDetectEditorLanguageIfEnabled(this.filename)
|
||||
() => this.autoDetectEditorLanguage(this.filename)
|
||||
);
|
||||
}
|
||||
|
||||
@ -108,10 +110,7 @@ export class IdeComponent implements OnInit {
|
||||
this.deploymentNotificationService.deploying.next(false);
|
||||
}
|
||||
|
||||
autoDetectEditorLanguageIfEnabled(filename: string) {
|
||||
if (!config.ide.autoDetectFileLanguage) {
|
||||
return;
|
||||
}
|
||||
autoDetectEditorLanguage(filename: string) {
|
||||
const extension = filename.substr(filename.lastIndexOf('.') + 1);
|
||||
let language = LanguageMap[extension];
|
||||
language = (language === undefined) ? 'text' : language;
|
||||
@ -132,7 +131,7 @@ export class IdeComponent implements OnInit {
|
||||
}
|
||||
this.autosave = setInterval(
|
||||
() => { this.sendCodeIfDirty(); },
|
||||
config.ide.autoSaveInterval
|
||||
this.configService.autoSaveInterval.value
|
||||
);
|
||||
}
|
||||
|
||||
@ -142,7 +141,7 @@ export class IdeComponent implements OnInit {
|
||||
}
|
||||
this.filename = file;
|
||||
this.requestCode();
|
||||
this.autoDetectEditorLanguageIfEnabled(this.filename);
|
||||
this.autoDetectEditorLanguage(this.filename);
|
||||
}
|
||||
|
||||
editorWriteHandler() {
|
||||
|
7
src/app/loader/loader.component.html
Normal file
7
src/app/loader/loader.component.html
Normal file
@ -0,0 +1,7 @@
|
||||
<div *ngIf="loading" class="loading-container">
|
||||
<div class=" loader">
|
||||
<div class="outer"></div>
|
||||
<div class="middle"></div>
|
||||
<div class="inner"></div>
|
||||
</div>
|
||||
</div>
|
62
src/app/loader/loader.component.scss
Normal file
62
src/app/loader/loader.component.scss
Normal file
@ -0,0 +1,62 @@
|
||||
.loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: 99999;
|
||||
filter: blur(0px)!important;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: relative;
|
||||
filter: blur(0px) !important;
|
||||
}
|
||||
|
||||
.outer,
|
||||
.middle,
|
||||
.inner {
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #277EEC;
|
||||
border-right-color: #277EEC;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.outer {
|
||||
width: 3.5em;
|
||||
height: 3.5em;
|
||||
margin-left: -1.75em;
|
||||
margin-top: -1.75em;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.middle {
|
||||
width: 2.1em;
|
||||
height: 2.1em;
|
||||
margin-left: -1.05em;
|
||||
margin-top: -1.05em;
|
||||
animation: spin 1.75s linear reverse infinite;
|
||||
}
|
||||
|
||||
.inner {
|
||||
width: 0.8em;
|
||||
height: 0.8em;
|
||||
margin-left: -0.4em;
|
||||
margin-top: -0.4em;
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
29
src/app/loader/loader.component.ts
Normal file
29
src/app/loader/loader.component.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Component, Input, AfterViewInit, OnInit } from '@angular/core';
|
||||
import { ConfigReadyService } from '../services/config.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-loader',
|
||||
templateUrl: './loader.component.html',
|
||||
styleUrls: ['./loader.component.scss']
|
||||
})
|
||||
export class LoaderComponent implements OnInit, AfterViewInit {
|
||||
@Input() toBlur: HTMLElement;
|
||||
loading = true;
|
||||
|
||||
constructor(private configReadyService: ConfigReadyService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.configReadyService.init();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.toBlur.classList.add('blur');
|
||||
this.configReadyService.configDone.subscribe(
|
||||
() => {
|
||||
this.loading = false;
|
||||
this.toBlur.classList.remove('blur');
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -2,9 +2,7 @@ import { ChangeDetectorRef, Component, OnInit, EventEmitter, Output } from '@ang
|
||||
import { MessageData, Message } from '../message-types/bot-messages';
|
||||
import { MarkdownService } from '../services/markdown.service';
|
||||
import { WebSocketService } from '../services/websocket.service';
|
||||
|
||||
import { config } from '../config';
|
||||
import { Subject, Observer, BehaviorSubject } from 'rxjs';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-messages',
|
||||
|
52
src/app/services/config.service.base.ts
Normal file
52
src/app/services/config.service.base.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { WebSocketService } from './websocket.service';
|
||||
import { Subject, BehaviorSubject, merge } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export abstract class ConfigServiceBase {
|
||||
initDone = false;
|
||||
keys = new Array<string>();
|
||||
mandatoryConfigs = new Array<any>();
|
||||
configDone = new Subject<void>();
|
||||
|
||||
constructor(private webSocketService: WebSocketService) {}
|
||||
|
||||
init() {
|
||||
if (!this.initDone) {
|
||||
this.waitForMandatoryConfigs();
|
||||
this.subscribeConfigKeys();
|
||||
this.initDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
waitForMandatoryConfigs() {
|
||||
const firstConfigs = new Array<any>();
|
||||
this.mandatoryConfigs.forEach((config) => {
|
||||
const requiredEmitCount = config instanceof BehaviorSubject ? 2 : 1;
|
||||
firstConfigs.push(config.pipe(take(requiredEmitCount)));
|
||||
});
|
||||
|
||||
merge(...firstConfigs).subscribe(undefined, undefined, () => {
|
||||
this.configDone.next();
|
||||
this.configDone.complete();
|
||||
});
|
||||
}
|
||||
|
||||
subscribeConfigKeys() {
|
||||
this.webSocketService.connect();
|
||||
this.keys.forEach(key =>
|
||||
this.webSocketService.observeKey<any>(key).subscribe(this.handleConfig.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
handleConfig(message: any) {
|
||||
Object.keys(message).filter(key => key !== 'key').forEach(key => {
|
||||
if (this[key] === undefined) {
|
||||
console.log(`Invalid ${this.keys} config key "${key}"!`);
|
||||
} else {
|
||||
this[key].next(message[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
83
src/app/services/config.service.ts
Normal file
83
src/app/services/config.service.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { WebSocketService } from './websocket.service';
|
||||
import { Subject, BehaviorSubject, merge } from 'rxjs';
|
||||
import { ConfigServiceBase } from './config.service.base';
|
||||
|
||||
@Injectable()
|
||||
export class DashboardConfigService extends ConfigServiceBase {
|
||||
keys = ['frontend.dashboard'];
|
||||
|
||||
layout = new BehaviorSubject<string>('terminal-ide-web');
|
||||
hideMessages = new BehaviorSubject<boolean>(false);
|
||||
iframeUrl = new BehaviorSubject<string>('/webservice');
|
||||
showUrlBar = new BehaviorSubject<boolean>(false);
|
||||
terminalMenuItem = new BehaviorSubject<string>('terminal');
|
||||
reloadIframeOnDeploy = new BehaviorSubject<boolean>(false);
|
||||
enabledLayouts = new BehaviorSubject<Array<string>>([
|
||||
'terminal-ide-web',
|
||||
'terminal-ide-vertical',
|
||||
'terminal-web',
|
||||
'ide-web-vertical',
|
||||
'terminal-ide-horizontal',
|
||||
'terminal-only',
|
||||
'ide-only',
|
||||
'web-only'
|
||||
]);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IdeConfigService extends ConfigServiceBase {
|
||||
keys = ['frontend.ide'];
|
||||
|
||||
autoSaveInterval = new BehaviorSubject<number>(444);
|
||||
showDeployButton = new BehaviorSubject<boolean>(true);
|
||||
deployButtonText = new BehaviorSubject<any>({
|
||||
'TODEPLOY': 'Deploy',
|
||||
'DEPLOYED': 'Deployed',
|
||||
'DEPLOYING': 'Reloading app...',
|
||||
'FAILED': 'Deployment failed'
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class SiteConfigService extends ConfigServiceBase {
|
||||
keys = ['frontend.site'];
|
||||
|
||||
askReloadSite = new BehaviorSubject<boolean>(false);
|
||||
documentTitle = new BehaviorSubject<string>('Avatao Tutorials');
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ConfigReadyService {
|
||||
configDone = new Subject<void>();
|
||||
readyNotifiers: Array<any>;
|
||||
|
||||
constructor(
|
||||
private webSocketService: WebSocketService,
|
||||
private ide: IdeConfigService,
|
||||
private dashboard: DashboardConfigService,
|
||||
private site: SiteConfigService
|
||||
) {
|
||||
this.readyNotifiers = [
|
||||
this.ide.configDone,
|
||||
this.dashboard.configDone,
|
||||
this.site.configDone
|
||||
];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.webSocketService.connect();
|
||||
|
||||
const recvdConfigDone = new Subject<void>();
|
||||
this.readyNotifiers.push(recvdConfigDone);
|
||||
this.webSocketService.observeKey<any>('frontend.ready').subscribe(() => {
|
||||
recvdConfigDone.next();
|
||||
recvdConfigDone.complete();
|
||||
});
|
||||
|
||||
merge(...this.readyNotifiers).subscribe(undefined, undefined, () => {
|
||||
this.configDone.next();
|
||||
this.configDone.complete();
|
||||
});
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import * as terminado from 'xterm/lib/addons/terminado/terminado';
|
||||
export class TerminadoService {
|
||||
xterm: Terminal;
|
||||
ws: WebSocket;
|
||||
attached = false;
|
||||
|
||||
constructor() {
|
||||
Terminal.applyAddon(fit);
|
||||
@ -47,6 +48,7 @@ export class TerminadoService {
|
||||
this.xterm.open(element);
|
||||
this.fit();
|
||||
this.xterm.blur();
|
||||
this.attached = true;
|
||||
};
|
||||
}
|
||||
|
||||
@ -54,9 +56,12 @@ export class TerminadoService {
|
||||
(<any>this.xterm).terminadoDetach(this.ws);
|
||||
this.xterm.destroy();
|
||||
this.ws.close();
|
||||
this.attached = false;
|
||||
}
|
||||
|
||||
fit() {
|
||||
(<any>this.xterm).fit();
|
||||
if (this.attached) {
|
||||
(<any>this.xterm).fit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
<div class="tfw-ide-pin" *ngFor="let layoutIter of enabledLayouts">
|
||||
|
||||
<div class="tfw-ide-pin" *ngFor="let layoutIter of (enabledLayouts | async)">
|
||||
<div>
|
||||
<button [class.active]="layout === layoutIter"
|
||||
(active)="layout === layoutIter"
|
||||
@ -7,5 +6,4 @@
|
||||
(click)="setLayout(layoutIter)"
|
||||
class="btn"><img src="images/{{layoutIter}}.svg"></button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -1,18 +1,17 @@
|
||||
import { Component, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { config } from '../config';
|
||||
import { DashboardConfigService } from '../services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sidebar',
|
||||
templateUrl: './sidebar.component.html',
|
||||
styleUrls: ['./sidebar.component.scss']
|
||||
})
|
||||
|
||||
export class SidebarComponent {
|
||||
@Input() layout: string;
|
||||
@Output() layoutChanged = new EventEmitter<string>();
|
||||
enabledLayouts: string[] = config.dashboard.enabledLayouts;
|
||||
enabledLayouts = this.configService.enabledLayouts;
|
||||
|
||||
constructor() {}
|
||||
constructor(private configService: DashboardConfigService) {}
|
||||
|
||||
setLayout(layout: string) {
|
||||
this.layout = layout;
|
||||
|
@ -1,2 +0,0 @@
|
||||
<h2>WebComponent is here!</h2>
|
||||
<p>Edit me if you want to write your own component in Angular!</p>
|
@ -1,11 +0,0 @@
|
||||
// Edit me if you want to write your own component in Angular!
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-web',
|
||||
templateUrl: './web.component.html',
|
||||
styleUrls: ['./web.component.scss']
|
||||
})
|
||||
export class WebComponent {
|
||||
constructor() {}
|
||||
}
|
@ -2,8 +2,8 @@ $grid-columns-count: 100;
|
||||
$grid-rows-count: 30;
|
||||
|
||||
$terminal-ide-web-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 4, 60),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 60),
|
||||
'ide': (56, 96, 0, 100),
|
||||
'terminal': (0, 56, 60, 100),
|
||||
'web': (20, 56, 0, 60),
|
||||
@ -11,8 +11,8 @@ $terminal-ide-web-layout: (
|
||||
);
|
||||
|
||||
$terminal-web-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 2, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (),
|
||||
'terminal': (56, 96, 0, 100),
|
||||
'web': (20, 56, 0, 100),
|
||||
@ -20,8 +20,8 @@ $terminal-web-layout: (
|
||||
);
|
||||
|
||||
$terminal-ide-vertical-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 2, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (56, 96, 0, 100),
|
||||
'terminal': (20, 56, 0, 100),
|
||||
'web': (),
|
||||
@ -29,8 +29,8 @@ $terminal-ide-vertical-layout: (
|
||||
);
|
||||
|
||||
$terminal-ide-horizontal-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 2, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (20, 96, 0, 60),
|
||||
'terminal': (20, 96, 60, 100),
|
||||
'web': (),
|
||||
@ -38,8 +38,8 @@ $terminal-ide-horizontal-layout: (
|
||||
);
|
||||
|
||||
$ide-web-vertical-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 4, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (56, 96, 0, 100),
|
||||
'terminal': (),
|
||||
'web': (20, 56, 0, 100),
|
||||
@ -47,8 +47,8 @@ $ide-web-vertical-layout: (
|
||||
);
|
||||
|
||||
$terminal-only-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 4, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (),
|
||||
'terminal': (20, 96, 0, 100),
|
||||
'web': (),
|
||||
@ -56,8 +56,8 @@ $terminal-only-layout: (
|
||||
);
|
||||
|
||||
$ide-only-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 4, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (20, 96, 0, 100),
|
||||
'terminal': (),
|
||||
'web': (),
|
||||
@ -65,8 +65,8 @@ $ide-only-layout: (
|
||||
);
|
||||
|
||||
$web-only-layout: (
|
||||
'header': (0, 20, 0, 4),
|
||||
'messages': (0, 20, 4, 100),
|
||||
'header': (0, 20, 0, 8),
|
||||
'messages': (0, 20, 8, 100),
|
||||
'ide': (),
|
||||
'terminal': (),
|
||||
'web': (20, 96, 0, 100),
|
||||
@ -142,7 +142,7 @@ $layouts: (
|
||||
|
||||
@if (length($tfw-component) > 0) {
|
||||
$columns-count: convert-ratio(nth($tfw-component,2), $grid-columns-count) - convert-ratio(nth($tfw-component,1), $grid-columns-count);
|
||||
$rows-count: convert-ratio(nth($tfw-component,4), $grid-columns-count) - convert-ratio(nth($tfw-component,3), $grid-columns-count);
|
||||
$rows-count: convert-ratio(nth($tfw-component,4), $grid-rows-count) - convert-ratio(nth($tfw-component,3), $grid-rows-count);
|
||||
|
||||
min-width: #{$columns-count / $grid-columns-count * 100}vw;
|
||||
min-height: #{$rows-count / $grid-rows-count * 100}vh;
|
||||
|
@ -1,6 +1,7 @@
|
||||
@mixin set-scrollbar-style($style, $selctor) {
|
||||
::-webkit-scrollbar {
|
||||
width: 3px !important;
|
||||
height: 3px !important;
|
||||
}
|
||||
|
||||
#{$selctor}::-webkit-scrollbar-track {
|
||||
|
Loading…
Reference in New Issue
Block a user