Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: IDE Api Calls from UI #159

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { AtobPipe } from './atob.pipe';
import { MarkdownModule } from 'ngx-markdown';
import { DynamicHooksModule } from 'ngx-dynamic-hooks';
import { CtrComponent } from './scenario/ctr.component';
import { IdeExecComponent } from './scenario/ideExec.component';
import { CtrService } from './scenario/ctr.service';
import { IDEApiExecService } from './scenario/ide.service';
import { ScenarioService } from './services/scenario.service';
import { CourseService } from './services/course.service';
import { SettingsService } from './services/settings.service';
Expand Down Expand Up @@ -74,6 +76,7 @@ export function jwtOptionsFactory() {
ScenarioCardComponent,
StepComponent,
CtrComponent,
IdeExecComponent,
VMClaimComponent,
AtobPipe,
HfMarkdownComponent,
Expand All @@ -97,7 +100,10 @@ export function jwtOptionsFactory() {
sanitize: false,
convertHTMLEntities: false,
},
globalParsers: [{ component: CtrComponent }],
globalParsers: [
{ component: CtrComponent },
{ component: IdeExecComponent },
],
}),
JwtModule.forRoot({
jwtOptionsProvider: {
Expand All @@ -110,6 +116,7 @@ export function jwtOptionsFactory() {
AppComponent,
AuthGuard,
CtrService,
IDEApiExecService,
CourseService,
SettingsService,
ScenarioService,
Expand Down
18 changes: 17 additions & 1 deletion src/app/hf-markdown/hf-markdown.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Component, Input, OnChanges } from '@angular/core';
import { MarkdownService } from 'ngx-markdown';
import { CtrService } from '../scenario/ctr.service';
import { IDEApiExecService } from '../scenario/ide.service';
import { IDEApiExec } from '../scenario/IDEApiExec';
import { VM } from '../VM';

// Replacement for lodash's escape
Expand Down Expand Up @@ -31,6 +33,7 @@ export class HfMarkdownComponent implements OnChanges {

constructor(
public markdownService: MarkdownService,
public ideService: IDEApiExecService,
private ctrService: CtrService,
) {
this.markdownService.renderer.code = (code: string, language = '') => {
Expand Down Expand Up @@ -65,6 +68,20 @@ export class HfMarkdownComponent implements OnChanges {
>${escape(code)}</ctr>`;
},

ide(code: string, target: string, endpoint: string, title: string) {
const exec: IDEApiExec = {
target: target,
apiEndpoint: endpoint,
postBody: code,
};
const id = this.ideService.registerExec(exec);
return `<ide-exec
target="${target}"
title="${title}"
execId="${id}"
></ide-exec>`;
},

hidden(code: string, summary: string) {
return `
<details>
Expand Down Expand Up @@ -178,7 +195,6 @@ ${token}`;
}

private replaceVmInfoTokens(content: string) {
console.log(this.context.vmInfo);
return content.replace(
/\$\{vminfo:([^:]*):([^}]*)\}/g,
(match, vmName, propName) => {
Expand Down
5 changes: 5 additions & 0 deletions src/app/scenario/IDEApiExec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class IDEApiExec {
target: string;
apiEndpoint: string;
postBody: string;
}
29 changes: 29 additions & 0 deletions src/app/scenario/ide.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Injectable } from '@angular/core';
import { IDEApiExec } from './IDEApiExec';
import { Subject } from 'rxjs';

@Injectable()
export class IDEApiExecService {
private execStream = new Subject<IDEApiExec>();
private execs: Map<string, IDEApiExec> = new Map();

// Save code inside map and return unique ID (at least very unlikely that two IDs collide)
public registerExec(exec: IDEApiExec) {
const n = 5;
const id = (Math.random().toString(36) + '0000').slice(2, n + 2); // Generate random ID with 5 Characters
this.execs.set(id, exec);
return id;
}

// Send the code stored inside the map
public sendCodeById(id: string) {
const exec = this.execs.get(id);
if (!exec) return;

this.execStream.next(exec);
}

public getExecStream() {
return this.execStream.asObservable();
}
}
24 changes: 24 additions & 0 deletions src/app/scenario/ideExec.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.api-exec {
margin-bottom: 0;
margin-top: 0;
padding: 5px 10px;
word-wrap: break;
white-space: break-spaces;
background: white !important;
color: black;
cursor: pointer;
overflow-x: auto;
border: 1px solid var(--clr-color-neutral-400, #cccccc);
border-top: 5px solid #54cbf2;
&[executed='true'] {
border-top: 5px solid var(--clr-color-success-500, green);
}
}

i {
font-size: 0.7em;
}

b {
color: #282828;
}
34 changes: 34 additions & 0 deletions src/app/scenario/ideExec.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Component, Input } from '@angular/core';
import { IDEApiExecService } from './ide.service';

@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'ide-exec',
template: `
<div [attr.executed]="executed" class="api-exec" (click)="ctr()">
<clr-icon shape="display"></clr-icon>
{{ title }}
</div>
<i>
<clr-icon [attr.shape]="shape"></clr-icon> {{ statusText }}
<b>{{ target }}</b>
</i>
`,
styleUrls: ['ideExec.component.scss'],
})
export class IdeExecComponent {
@Input() target = '';
@Input() title: string;
@Input() execId: string;

public shape = 'angle';
public statusText = 'Editor on';
public executed = false;

constructor(private ideExecService: IDEApiExecService) {}

public ctr() {
this.ideExecService.sendCodeById(this.execId);
this.executed = true;
}
}
1 change: 1 addition & 0 deletions src/app/scenario/step.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ <h4 class="card-title">
<button
clrTabLink
[clrTabLinkInOverflow]="!webinterface.hasOwnTab"
[id]="v.key + '-' + webinterface.name"
>
{{ v.key }} - {{ webinterface.name }}
</button>
Expand Down
24 changes: 18 additions & 6 deletions src/app/scenario/step.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,11 @@ import { VMService } from '../services/vm.service';
import { ShellService } from '../services/shell.service';
import { atou } from '../unicode';
import { ProgressService } from '../services/progress.service';
import {
HfMarkdownComponent,
HfMarkdownRenderContext,
} from '../hf-markdown/hf-markdown.component';
import { HfMarkdownRenderContext } from '../hf-markdown/hf-markdown.component';
import { GuacTerminalComponent } from './guacTerminal.component';
import { JwtHelperService } from '@auth0/angular-jwt';
import { IDEApiExecService } from './ide.service';
import { IDEApiExec } from './IDEApiExec';

type Service = {
name: string;
Expand Down Expand Up @@ -109,6 +108,7 @@ export class StepComponent implements OnInit, AfterViewInit, OnDestroy {
private route: ActivatedRoute,
private router: Router,
private ctr: CtrService,
private ideExec: IDEApiExecService,
private ssService: SessionService,
private scenarioService: ScenarioService,
private stepService: StepService,
Expand Down Expand Up @@ -181,7 +181,6 @@ export class StepComponent implements OnInit, AfterViewInit, OnDestroy {
const services = JSON.parse(JSON.parse(stringContent)); //TODO: See if we can skip one stringify somwhere, so we dont have to parse twice
services.forEach((service: Service) => {
if (service.hasWebinterface) {
console.log(service);
const webinterface = {
name: service.name ?? 'Service',
port: service.port ?? 80,
Expand Down Expand Up @@ -248,14 +247,27 @@ export class StepComponent implements OnInit, AfterViewInit, OnDestroy {
});

this.ctr.getCodeStream().subscribe((c: CodeExec) => {
// watch for tab changes
// watch for tab changes when clicking CTRs
this.tabs.forEach((i: ClrTab) => {
if (c.target.toLowerCase() == i.tabLink.tabLinkId.toLowerCase()) {
i.ifActiveService.current = i.id;
}
});
});

this.ideExec.getExecStream().subscribe((exec: IDEApiExec) => {
// watch for tab changes when exectuing IDE Api calls
this.tabs.forEach((i: ClrTab) => {
// TODO 14.02.2023 Better tab selection for IDE tabs. A little bit tricky with only the name flag.
if (
exec.target.toLowerCase() + '-ide' ==
i.tabLink.tabLinkId.toLowerCase()
) {
i.ifActiveService.current = i.id;
}
});
});

this.shellService.watch().subscribe((ss: Map<string, string>) => {
this.shellStatus = ss;
});
Expand Down
46 changes: 41 additions & 5 deletions src/app/scenario/terminal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import { HostListener } from '@angular/core';
import { interval, Subscription, timer } from 'rxjs';
import { themes } from './terminal-themes/themes';
import { SettingsService } from '../services/settings.service';
import { IDEApiExecService } from './ide.service';
import { IDEApiExec } from './IDEApiExec';
import { HttpClient, HttpHeaders } from '@angular/common/http';

const WS_CODE_NORMAL_CLOSURE = 1000;

Expand Down Expand Up @@ -49,6 +52,9 @@ export class TerminalComponent implements OnChanges, AfterViewInit, OnDestroy {
public mutationObserver: MutationObserver;
private subscription = new Subscription();

private token: string;
private legacyEndpoint: string;

private DEFAULT_FONT_SIZE = 16;
private DEFAULT_TERMINAL_THEME = 'default';

Expand All @@ -57,8 +63,10 @@ export class TerminalComponent implements OnChanges, AfterViewInit, OnDestroy {
constructor(
private jwtHelper: JwtHelperService,
private ctrService: CtrService,
private ideApiExecService: IDEApiExecService,
private shellService: ShellService,
private settingsService: SettingsService,
private http: HttpClient,
) {}

@HostListener('window:resize')
Expand All @@ -77,6 +85,7 @@ export class TerminalComponent implements OnChanges, AfterViewInit, OnDestroy {
}

private buildSocket() {
this.legacyEndpoint = this.endpoint;
if (
!this.endpoint.startsWith('wss://') &&
!this.endpoint.startsWith('ws://')
Expand All @@ -88,11 +97,7 @@ export class TerminalComponent implements OnChanges, AfterViewInit, OnDestroy {
}
}
this.socket = new WebSocket(
this.endpoint +
'/shell/' +
this.vmid +
'/connect?auth=' +
this.jwtHelper.tokenGetter(),
this.endpoint + '/shell/' + this.vmid + '/connect?auth=' + this.token,
);

// Check if current browser is firefox by useragent and use "duck-typing" as a fallback.
Expand Down Expand Up @@ -156,6 +161,36 @@ export class TerminalComponent implements OnChanges, AfterViewInit, OnDestroy {
}
});

this.subscription.add(
this.ideApiExecService.getExecStream().subscribe((exec: IDEApiExec) => {
// if the ide exec is target at us, execute it with the correct endpoint and VM id
if (exec.target.toLowerCase() == this.vmname.toLowerCase()) {
// break up the code by lines
// Handle execution
const url =
'https://' +
this.legacyEndpoint +
'/pa/' +
this.token +
'/' +
this.vmid +
'/7331' +
exec.apiEndpoint;

const headers = new HttpHeaders().set(
'Content-Type',
'application/json',
);
this.http
.post(url, exec.postBody, {
headers: headers,
observe: 'response',
responseType: 'json',
})
.subscribe();
}
}),
);
this.subscription.add(
interval(5000).subscribe(() => {
this.socket.send(''); // websocket keepalive
Expand All @@ -171,6 +206,7 @@ export class TerminalComponent implements OnChanges, AfterViewInit, OnDestroy {

ngOnChanges() {
if (this.vmid != null && this.endpoint != null) {
this.token = this.jwtHelper.tokenGetter();
this.closeSocket();
this.buildSocket();
}
Expand Down