diff --git a/package.json b/package.json index 6fa88c3e..728748dc 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,8 @@ "tree-kill": "^1.2.1", "typeface-roboto": "^0.0.75", "xterm": "^4.1.0", + "xterm-addon-attach": "^0.5.0", + "xterm-addon-fit": "^0.3.0", "yargs": "^15.0.2", "zone.js": "^0.10.2" }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 110717b4..657c1be0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -268,6 +268,10 @@ import { OpenFileExplorerActionComponent } from './components/project-map/contex import { NgxChildProcessModule } from 'ngx-childprocess'; import { ServerResolve } from './resolvers/server-resolve'; import { ProjectMapGuard } from './guards/project-map-guard'; +import { HttpConsoleActionComponent } from './components/project-map/context-menu/actions/http-console/http-console-action.component'; +import { WebConsoleComponent } from './components/project-map/web-console/web-console.component'; +import { ConsoleWrapperComponent } from './components/project-map/console-wrapper/console-wrapper.component'; +import { NodeConsoleService } from './services/nodeConsole.service'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -449,7 +453,10 @@ if (environment.production) { SystemStatusComponent, StatusInfoComponent, StatusChartComponent, - OpenFileExplorerActionComponent + OpenFileExplorerActionComponent, + HttpConsoleActionComponent, + WebConsoleComponent, + ConsoleWrapperComponent ], imports: [ BrowserModule, @@ -541,6 +548,7 @@ if (environment.production) { Gns3vmService, ThemeService, GoogleAnalyticsService, + NodeConsoleService, ServerResolve, ProjectMapGuard, Title diff --git a/src/app/components/project-map/console-wrapper/console-wrapper.component.html b/src/app/components/project-map/console-wrapper/console-wrapper.component.html new file mode 100644 index 00000000..06b226db --- /dev/null +++ b/src/app/components/project-map/console-wrapper/console-wrapper.component.html @@ -0,0 +1,47 @@ +
+
+ +
+ + + + +
GNS3 console
+
+
+ + + +
{{node.name}}
+ +
+
+ +
+ + + +
+ +
+
diff --git a/src/app/components/project-map/console-wrapper/console-wrapper.component.scss b/src/app/components/project-map/console-wrapper/console-wrapper.component.scss new file mode 100644 index 00000000..374dd797 --- /dev/null +++ b/src/app/components/project-map/console-wrapper/console-wrapper.component.scss @@ -0,0 +1,96 @@ +.consoleWrapper { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + position: fixed; + bottom: 20px; + left: 20px; + height: 180px; + width: 600px; + background: #263238; + color: white; + overflow: hidden; + font-size: 12px; +} + +.lightTheme { + background: white!important; + color: black; +} + +.filterButton { + background: transparent; + color: white; + border: none; + margin-top: 0px; + outline: none; + color: #dbd5d5; + font-weight: bold; + padding: 0px; +} + +.consoleFiltering { + display: flex; +} + +.consoleHeader { + width: 100%; + height: 30px; + font-size: 12px; + overflow: hidden; + display: flex; + padding: 2px; + justify-content: space-between; +} + +.console { + width: 596px; + height: 120px; + overflow-y: scroll; + padding: 2px; + color: #dbd5d5; + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +.consoleInput { + width: 100%; + height: 30px; + padding: 2px; + display: flex; +} + +.commandLine { + background-color: transparent; + color: white; + border: none; +} + +.inputIcon { + margin-top: 2px; +} + +mat-icon { + font-size: 20px; + width: 20px; + height: 20px; +} + +input:focus{ + outline: none; +} + +::-webkit-scrollbar { + width: 0.5em; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); +} + +::-webkit-scrollbar-thumb { + background-color: darkgrey; + outline: 1px solid #263238; +} + +.closeButton { + cursor: pointer; +} diff --git a/src/app/components/project-map/console-wrapper/console-wrapper.component.spec.ts b/src/app/components/project-map/console-wrapper/console-wrapper.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/console-wrapper/console-wrapper.component.ts b/src/app/components/project-map/console-wrapper/console-wrapper.component.ts new file mode 100644 index 00000000..e8ca2654 --- /dev/null +++ b/src/app/components/project-map/console-wrapper/console-wrapper.component.ts @@ -0,0 +1,129 @@ +import { Component, OnInit, AfterViewInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { Project } from '../../../models/project'; +import { Server } from '../../../models/server'; +import { ResizeEvent } from 'angular-resizable-element'; +import { ThemeService } from '../../../services/theme.service'; +import { FormControl } from '@angular/forms'; +import { NodeConsoleService } from '../../../services/nodeConsole.service'; +import { Node } from '../../../cartography/models/node'; + + +@Component({ + selector: 'app-console-wrapper', + templateUrl: './console-wrapper.component.html', + styleUrls: ['./console-wrapper.component.scss'] +}) +export class ConsoleWrapperComponent implements OnInit { + @Input() server: Server; + @Input() project: Project; + @Output() closeConsole = new EventEmitter(); + + filters: string[] = ['all', 'errors', 'warnings', 'info', 'map updates', 'server requests']; + selectedFilter: string = 'all'; + + public style: object = {}; + public styleInside: object = { height: `120px` }; + + public isDraggingEnabled: boolean = false; + public isLightThemeEnabled: boolean = false; + + constructor( + private consoleService: NodeConsoleService, + private themeService: ThemeService + ) {} + + nodes: Node[] = []; + selected = new FormControl(0); + + ngOnInit() { + this.themeService.getActualTheme() === 'light' ? this.isLightThemeEnabled = true : this.isLightThemeEnabled = false; + this.style = { bottom: '20px', left: '20px', width: '720px', height: '456px'}; + + this.consoleService.nodeConsoleTrigger.subscribe((node) => { + this.addTab(node, true); + }); + + this.consoleService.closeNodeConsoleTrigger.subscribe((node) => { + let index = this.nodes.findIndex(n => n.node_id === node.node_id) + this.removeTab(index); + }); + } + + addTab(node: Node, selectAfterAdding: boolean) { + this.nodes.push(node); + + if (selectAfterAdding) { + this.selected.setValue(this.nodes.length); + } + } + + removeTab(index: number) { + this.nodes.splice(index, 1); + } + + toggleDragging(value: boolean) { + this.isDraggingEnabled = value; + } + + dragWidget(event) { + let x: number = Number(event.movementX); + let y: number = Number(event.movementY); + + let width: number = Number(this.style['width'].split('px')[0]); + let height: number = Number(this.style['height'].split('px')[0]); + let left: number = Number(this.style['left'].split('px')[0]) + x; + if (this.style['top']) { + let top: number = Number(this.style['top'].split('px')[0]) + y; + this.style = { + position: 'fixed', + left: `${left}px`, + top: `${top}px`, + width: `${width}px`, + height: `${height}px` + }; + } else { + let bottom: number = Number(this.style['bottom'].split('px')[0]) - y; + this.style = { + position: 'fixed', + left: `${left}px`, + bottom: `${bottom}px`, + width: `${width}px`, + height: `${height}px` + }; + } + } + + validate(event: ResizeEvent): boolean { + if ( + event.rectangle.width && + event.rectangle.height && + (event.rectangle.width < 720 || + event.rectangle.height < 456) + ) { + return false; + } + return true; + } + + onResizeEnd(event: ResizeEvent): void { + this.style = { + position: 'fixed', + left: `${event.rectangle.left}px`, + top: `${event.rectangle.top}px`, + width: `${event.rectangle.width}px`, + height: `${event.rectangle.height}px` + }; + + this.styleInside = { + height: `${event.rectangle.height - 60}px`, + width: `${event.rectangle.width}px` + }; + + this.consoleService.resizeTerminal(); + } + + close() { + this.closeConsole.emit(false); + } +} diff --git a/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.html b/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.html new file mode 100644 index 00000000..55f6dcce --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.ts b/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.ts new file mode 100644 index 00000000..bf16ecfd --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/http-console/http-console-action.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeConsoleService } from '../../../../../services/nodeConsole.service'; +import { ToasterService } from '../../../../../services/toaster.service'; + + +@Component({ + selector: 'app-http-console-action', + templateUrl: './http-console-action.component.html' +}) +export class HttpConsoleActionComponent implements OnInit { + @Input() server: Server; + @Input() nodes: Node[]; + + constructor( + private consoleService: NodeConsoleService, + private toasterService: ToasterService + ) { } + + ngOnInit() {} + + openConsole() { + this.nodes.forEach(n => { + if (n.status === 'started') { + this.consoleService.openConsoleForNode(n); + } else { + this.toasterService.error('To open console please start the node'); + } + }); + } +} diff --git a/src/app/components/project-map/context-menu/context-menu.component.html b/src/app/components/project-map/context-menu/context-menu.component.html index 3bc02127..6b35ff5d 100644 --- a/src/app/components/project-map/context-menu/context-menu.component.html +++ b/src/app/components/project-map/context-menu/context-menu.component.html @@ -15,6 +15,11 @@ + - -
-
-
- - - - - - - - - -
- -
- close -
-
- -
- - {{event.message}}
-
-
- -
- keyboard_arrow_right - +
+
+ + + + + + + + +
+ +
+ + {{event.message}}
+
+
+ +
+ keyboard_arrow_right + +
diff --git a/src/app/components/project-map/log-console/log-console.component.scss b/src/app/components/project-map/log-console/log-console.component.scss index 374dd797..f8302fed 100644 --- a/src/app/components/project-map/log-console/log-console.component.scss +++ b/src/app/components/project-map/log-console/log-console.component.scss @@ -33,7 +33,7 @@ .consoleHeader { width: 100%; - height: 30px; + height: 40px; font-size: 12px; overflow: hidden; display: flex; diff --git a/src/app/components/project-map/log-console/log-console.component.ts b/src/app/components/project-map/log-console/log-console.component.ts index 8e6d4657..9fbec2ba 100644 --- a/src/app/components/project-map/log-console/log-console.component.ts +++ b/src/app/components/project-map/log-console/log-console.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core'; +import { Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Subscription } from 'rxjs'; import { ProjectWebServiceHandler } from '../../../handlers/project-web-service-handler'; import { NodeService } from '../../../services/node.service'; @@ -14,18 +14,21 @@ import { HttpServer } from '../../../services/http-server.service'; import { LogEvent } from '../../../models/logEvent'; import { ResizeEvent } from 'angular-resizable-element'; import { ThemeService } from '../../../services/theme.service'; +import { FormControl } from '@angular/forms'; @Component({ + changeDetection: ChangeDetectionStrategy.OnPush, selector: 'app-log-console', templateUrl: './log-console.component.html', styleUrls: ['./log-console.component.scss'] }) export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { - @Input() project: Project; @Input() server: Server; - @Output() closeConsole = new EventEmitter(); + @Input() project: Project; + @ViewChild('console', {static: false}) console: ElementRef; + private nodeSubscription: Subscription; private linkSubscription: Subscription; private drawingSubscription: Subscription; @@ -33,11 +36,11 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { private errorSubscription: Subscription; private warningSubscription: Subscription; private infoSubscription: Subscription; - command: string = ''; - filters: string[] = ['all', 'errors', 'warnings', 'info', 'map updates', 'server requests']; - selectedFilter: string = 'all'; - filteredEvents: LogEvent[] = []; + public command: string = ''; + public filters: string[] = ['all', 'errors', 'warnings', 'info', 'map updates', 'server requests']; + public selectedFilter: string = 'all'; + public filteredEvents: LogEvent[] = []; private regexStart: RegExp = /^start (.*?)$/; private regexStop: RegExp = /^stop (.*?)$/; @@ -47,10 +50,9 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { private regexConsole: RegExp = /^console (.*?)$/; public style: object = {}; - public styleInside: object = { height: `120px` }; - - isDraggingEnabled: boolean = false; + public isDraggingEnabled: boolean = false; public isLightThemeEnabled: boolean = false; + public selected = new FormControl(0); constructor( private projectWebServiceHandler: ProjectWebServiceHandler, @@ -58,11 +60,14 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { private nodesDataSource: NodesDataSource, private logEventsDataSource: LogEventsDataSource, private httpService: HttpServer, - private themeService: ThemeService + private themeService: ThemeService, + private cd: ChangeDetectorRef ) {} ngOnInit() { - this.themeService.getActualTheme() === 'light' ? this.isLightThemeEnabled = true : this.isLightThemeEnabled = false; + this.themeService.getActualTheme() === 'light' ? this.isLightThemeEnabled = true : this.isLightThemeEnabled = false + this.style = { bottom: '20px', left: '20px', width: '720px', height: '340px'}; + this.nodeSubscription = this.projectWebServiceHandler.nodeNotificationEmitter.subscribe((event) => { let node: Node = event.event as Node; let message: string = ''; @@ -118,40 +123,6 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { message: message }); }); - - this.style = { bottom: '20px', left: '20px', width: '600px', height: '180px'}; - } - - toggleDragging(value: boolean) { - this.isDraggingEnabled = value; - } - - dragWidget(event) { - let x: number = Number(event.movementX); - let y: number = Number(event.movementY); - - let width: number = Number(this.style['width'].split('px')[0]); - let height: number = Number(this.style['height'].split('px')[0]); - let left: number = Number(this.style['left'].split('px')[0]) + x; - if (this.style['top']) { - let top: number = Number(this.style['top'].split('px')[0]) + y; - this.style = { - position: 'fixed', - left: `${left}px`, - top: `${top}px`, - width: `${width}px`, - height: `${height}px` - }; - } else { - let bottom: number = Number(this.style['bottom'].split('px')[0]) - y; - this.style = { - position: 'fixed', - left: `${left}px`, - bottom: `${bottom}px`, - width: `${width}px`, - height: `${height}px` - }; - } } ngAfterViewInit() { @@ -168,36 +139,10 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { this.infoSubscription.unsubscribe(); } - validate(event: ResizeEvent): boolean { - if ( - event.rectangle.width && - event.rectangle.height && - (event.rectangle.width < 600 || - event.rectangle.height < 180) - ) { - return false; - } - return true; - } - - onResizeEnd(event: ResizeEvent): void { - this.style = { - position: 'fixed', - left: `${event.rectangle.left}px`, - top: `${event.rectangle.top}px`, - width: `${event.rectangle.width}px`, - height: `${event.rectangle.height}px` - }; - - this.styleInside = { - height: `${event.rectangle.height - 60}px`, - width: `${event.rectangle.width}px` - }; - } - applyFilter(filter: string) { this.selectedFilter = filter; this.filteredEvents = this.getFilteredEvents(); + this.cd.detectChanges(); } onKeyDown(event) { @@ -279,6 +224,7 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { this.showCommand(`Unknown syntax: ${this.command}`); } this.command = ''; + this.cd.detectChanges(); } clearConsole() { @@ -301,6 +247,7 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { setTimeout( () => { this.console.nativeElement.scrollTop = this.console.nativeElement.scrollHeight; }, 100 ); + this.cd.detectChanges(); } getFilteredEvents(): LogEvent[] { @@ -375,8 +322,4 @@ export class LogConsoleComponent implements OnInit, AfterViewInit, OnDestroy { y: ${drawing.y}, z: ${drawing.z}`; } - - close() { - this.closeConsole.emit(false); - } } diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 8276557c..bc4c464a 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -51,6 +51,14 @@ developer_board Go to servers + + - - - - - -
@@ -191,7 +187,7 @@
- +
diff --git a/src/app/components/project-map/project-map.component.scss b/src/app/components/project-map/project-map.component.scss index f081293f..4be70ccc 100644 --- a/src/app/components/project-map/project-map.component.scss +++ b/src/app/components/project-map/project-map.component.scss @@ -1,3 +1,7 @@ +.wrapper { + height: 600px; +} + app-root, app-project-map, .project-map, diff --git a/src/app/components/project-map/web-console/web-console.component.html b/src/app/components/project-map/web-console/web-console.component.html new file mode 100644 index 00000000..42dda52d --- /dev/null +++ b/src/app/components/project-map/web-console/web-console.component.html @@ -0,0 +1 @@ +
diff --git a/src/app/components/project-map/web-console/web-console.component.scss b/src/app/components/project-map/web-console/web-console.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/web-console/web-console.component.spec.ts b/src/app/components/project-map/web-console/web-console.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/web-console/web-console.component.ts b/src/app/components/project-map/web-console/web-console.component.ts new file mode 100644 index 00000000..b98d24c0 --- /dev/null +++ b/src/app/components/project-map/web-console/web-console.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, Input, AfterViewInit, ViewEncapsulation, ViewChild, ElementRef } from '@angular/core'; +import { Project } from '../../../models/project'; +import { Server } from '../../../models/server'; +import { Terminal } from 'xterm'; +import { AttachAddon } from 'xterm-addon-attach'; +import { Node } from '../../../cartography/models/node'; +import { FitAddon } from 'xterm-addon-fit'; +import { NodeConsoleService } from '../../../services/nodeConsole.service'; + + +@Component({ + encapsulation: ViewEncapsulation.None, + selector: 'app-web-console', + templateUrl: './web-console.component.html', + styleUrls: ['../../../../../node_modules/xterm/css/xterm.css'] +}) +export class WebConsoleComponent implements OnInit, AfterViewInit { + @Input() server: Server; + @Input() project: Project; + @Input() node: Node; + + public term: Terminal = new Terminal(); + public fitAddon: FitAddon = new FitAddon(); + + @ViewChild('terminal', {static: false}) terminal: ElementRef; + + constructor( + private consoleService: NodeConsoleService + ) {} + + ngOnInit() { + this.consoleService.consoleResized.subscribe(ev => { + this.fitAddon.fit(); + }); + } + + ngAfterViewInit() { + this.term.open(this.terminal.nativeElement); + const socket = new WebSocket(this.getUrl()); + + socket.onerror = ((event) => { + this.term.write('Connection lost'); + }); + socket.onclose = ((event) => { + this.consoleService.closeConsoleForNode(this.node); + }); + + const attachAddon = new AttachAddon(socket); + this.term.loadAddon(attachAddon); + this.term.setOption('cursorBlink', true); + this.term.loadAddon(this.fitAddon); + this.fitAddon.activate(this.term); + this.term.focus(); + } + + getUrl() { + return `ws://${this.server.host}:${this.server.port}/v2/projects/${this.node.project_id}/nodes/${this.node.node_id}/console/ws` + } +} diff --git a/src/app/services/nodeConsole.service.ts b/src/app/services/nodeConsole.service.ts new file mode 100644 index 00000000..77467943 --- /dev/null +++ b/src/app/services/nodeConsole.service.ts @@ -0,0 +1,24 @@ +import { Injectable, EventEmitter } from '@angular/core'; +import { Node } from '../cartography/models/node'; +import { Subject } from 'rxjs'; + +@Injectable() +export class NodeConsoleService { + public nodeConsoleTrigger = new EventEmitter(); + public closeNodeConsoleTrigger = new Subject(); + public consoleResized = new Subject(); + + constructor() {} + + openConsoleForNode(node: Node) { + this.nodeConsoleTrigger.emit(node); + } + + closeConsoleForNode(node: Node) { + this.closeNodeConsoleTrigger.next(node); + } + + resizeTerminal() { + this.consoleResized.next(true); + } +} diff --git a/yarn.lock b/yarn.lock index 7b564db9..43a6d7c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10129,6 +10129,16 @@ xterm@^4.1.0: resolved "https://registry.npmjs.org/xterm/-/xterm-4.3.0.tgz#9a302efefe75172d4f7ea3afc20f9bd983f05027" integrity sha512-6dnrC4nxgnRKQzIWwC5HA0mnT9/rpDPZflUIr24gdcdSMTKM1QQcor4qQ/xz4Zerz6AIL/CuuBPypFfzsB63dQ== +xterm-addon-attach@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.5.0.tgz#e35cde4ae493ecace7d07e52ff2b9714e3c43068" + integrity sha512-L6ThPjF/fVt+gJS2+2h2rEEAXNxIRmCU8/RCM6rYR08K9GtPiHmYcnpRT7WNJf31yFLpWVA8dKcItfP3C0ZKlA== + +xterm-addon-fit@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.3.0.tgz#341710741027de9d648a9f84415a01ddfdbbe715" + integrity sha512-kvkiqHVrnMXgyCH9Xn0BOBJ7XaWC/4BgpSWQy3SueqximgW630t/QOankgqkvk11iTOCwWdAY9DTyQBXUMN3lw== + y18n@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"