diff --git a/package.json b/package.json index 4eb5a7dc..1a745a25 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "rxjs-compat": "^6.5.2", "tree-kill": "^1.2.1", "typeface-roboto": "^0.0.75", + "xterm": "^3.14.5", "yargs": "^13.3.0", "zone.js": "^0.9.1" }, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2df35267..d5ecc84c 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -191,6 +191,8 @@ import { DuplicateActionComponent } from './components/project-map/context-menu/ import { MapSettingsService } from './services/mapsettings.service'; import { ProjectMapMenuComponent } from './components/project-map/project-map-menu/project-map-menu.component'; import { HelpComponent } from './components/help/help.component'; +import { LogConsoleComponent } from './components/project-map/log-console/log-console.component'; +import { LogEventsDataSource } from './components/project-map/log-console/log-events-datasource'; import { SaveProjectDialogComponent } from './components/projects/save-project-dialog/save-project-dialog.component'; import { TopologySummaryComponent } from './components/topology-summary/topology-summary.component'; import { ShowNodeActionComponent } from './components/project-map/context-menu/actions/show-node-action/show-node-action.component'; @@ -321,6 +323,7 @@ if (environment.production) { NodesMenuComponent, ProjectMapMenuComponent, HelpComponent, + LogConsoleComponent, SaveProjectDialogComponent, TopologySummaryComponent, InfoDialogComponent, @@ -365,6 +368,7 @@ if (environment.production) { LinksDataSource, NodesDataSource, SymbolsDataSource, + LogEventsDataSource, SelectionManager, InRectangleHelper, DrawingsDataSource, diff --git a/src/app/components/project-map/log-console/log-console.component.html b/src/app/components/project-map/log-console/log-console.component.html new file mode 100644 index 00000000..5db1f7da --- /dev/null +++ b/src/app/components/project-map/log-console/log-console.component.html @@ -0,0 +1,38 @@ +
+
+
+
Console
+ + + + + + + + + +
+ +
+ close +
+
+ +
+ + {{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 new file mode 100644 index 00000000..e3b1ec5d --- /dev/null +++ b/src/app/components/project-map/log-console/log-console.component.scss @@ -0,0 +1,86 @@ +.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; +} + +.filterButton { + background: #263238; + color: white; + border: none; +} + +.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: #263238; + 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/log-console/log-console.component.spec.ts b/src/app/components/project-map/log-console/log-console.component.spec.ts new file mode 100644 index 00000000..3ec36f58 --- /dev/null +++ b/src/app/components/project-map/log-console/log-console.component.spec.ts @@ -0,0 +1,176 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserModule } from '@angular/platform-browser'; +import { NO_ERRORS_SCHEMA, EventEmitter, inject } from '@angular/core'; +import { MatMenuModule } from '@angular/material'; +import { Server } from '../../../models/server'; +import { LogConsoleComponent } from './log-console.component'; +import { ProjectWebServiceHandler, WebServiceMessage } from '../../../handlers/project-web-service-handler'; +import { NodeService } from '../../../services/node.service'; +import { MockedNodeService, MockedNodesDataSource } from '../project-map.component.spec'; +import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource'; +import { of } from 'rxjs'; +import { LogEventsDataSource } from './log-events-datasource'; +import { HttpServer, ServerErrorHandler } from '../../../services/http-server.service'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; + +export class MockedProjectWebServiceHandler { + public nodeNotificationEmitter = new EventEmitter(); + public linkNotificationEmitter = new EventEmitter(); + public drawingNotificationEmitter = new EventEmitter(); + public infoNotificationEmitter = new EventEmitter(); + public warningNotificationEmitter = new EventEmitter(); + public errorNotificationEmitter = new EventEmitter(); +} + +describe('LogConsoleComponent', () => { + let component: LogConsoleComponent; + let fixture: ComponentFixture; + + let mockedNodeService: MockedNodeService = new MockedNodeService(); + let mockedNodesDataSource: MockedNodesDataSource = new MockedNodesDataSource(); + let mockedProjectWebServiceHandler: MockedProjectWebServiceHandler = new MockedProjectWebServiceHandler(); + + let httpServer = new HttpServer({} as HttpClient, {} as ServerErrorHandler); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MatMenuModule, BrowserModule], + providers: [ + { provide: ProjectWebServiceHandler, useValue: mockedProjectWebServiceHandler }, + { provide: NodeService, useValue: mockedNodeService }, + { provide: NodesDataSource, useValue: mockedNodesDataSource }, + { provide: LogEventsDataSource, useClass: LogEventsDataSource }, + { provide: HttpServer, useValue: httpServer } + ], + declarations: [LogConsoleComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LogConsoleComponent); + component = fixture.componentInstance; + component.server = {location: 'local'} as Server; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call show message when help command entered', () => { + spyOn(component, 'showMessage'); + component.command = 'help'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Available commands: help, version, start all, start {node name}, stop all, stop {node name}, suspend all, suspend {node name}, reload all, reload {node name}, show {node name}.'}); + }); + + it('should call show message when version command entered', () => { + spyOn(component, 'showMessage'); + component.command = 'version'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Current version: 2019.2.0'}); + }); + + it('should call show message when unknown command entered', () => { + spyOn(component, 'showMessage'); + component.command = 'xyz'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Unknown syntax: xyz'}); + }); + + it('should call node service when start all entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'startAll').and.returnValue(of({})); + component.command = 'start all'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Starting all nodes...'}); + expect(mockedNodeService.startAll).toHaveBeenCalled(); + }); + + it('should call node service when stop all entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'stopAll').and.returnValue(of({})); + component.command = 'stop all'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Stopping all nodes...'}); + expect(mockedNodeService.stopAll).toHaveBeenCalled(); + }); + + it('should call node service when suspend all entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'suspendAll').and.returnValue(of({})); + component.command = 'suspend all'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Suspending all nodes...'}); + expect(mockedNodeService.suspendAll).toHaveBeenCalled(); + }); + + it('should call node service when reload all entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'reloadAll').and.returnValue(of({})); + component.command = 'reload all'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Reloading all nodes...'}); + expect(mockedNodeService.reloadAll).toHaveBeenCalled(); + }); + + it('should call node service when start node entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'start').and.returnValue(of({})); + component.command = 'start testNode'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Starting node testNode...'}); + expect(mockedNodeService.start).toHaveBeenCalled(); + }); + + it('should call node service when stop node entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'stop').and.returnValue(of({})); + component.command = 'stop testNode'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Stopping node testNode...'}); + expect(mockedNodeService.stop).toHaveBeenCalled(); + }); + + it('should call node service when suspend node entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'suspend').and.returnValue(of({})); + component.command = 'suspend testNode'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Suspending node testNode...'}); + expect(mockedNodeService.suspend).toHaveBeenCalled(); + }); + + it('should call node service when reload node entered', () => { + spyOn(component, 'showMessage'); + spyOn(mockedNodeService, 'reload').and.returnValue(of({})); + component.command = 'reload testNode'; + + component.handleCommand(); + + expect(component.showMessage).toHaveBeenCalledWith({type: 'command', message: 'Reloading node testNode...'}); + expect(mockedNodeService.reload).toHaveBeenCalled(); + }); +}); 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 new file mode 100644 index 00000000..26d758e8 --- /dev/null +++ b/src/app/components/project-map/log-console/log-console.component.ts @@ -0,0 +1,286 @@ +import { Component, OnInit, AfterViewInit, OnDestroy, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { ProjectWebServiceHandler } from '../../../handlers/project-web-service-handler'; +import { NodeService } from '../../../services/node.service'; +import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource'; +import { Project } from '../../../models/project'; +import { Server } from '../../../models/server'; +import { Drawing } from '../../../cartography/models/drawing'; +import { Link } from '../../../models/link'; +import { Node } from '../../../cartography/models/node'; +import { Port } from '../../../models/port'; +import { LogEventsDataSource } from './log-events-datasource'; +import { HttpServer } from '../../../services/http-server.service'; +import { LogEvent } from '../../../models/logEvent'; + + +@Component({ + 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(); + @ViewChild('console', {static: false}) console: ElementRef; + private nodeSubscription: Subscription; + private linkSubscription: Subscription; + private drawingSubscription: Subscription; + private serverRequestsSubscription: Subscription; + 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[] = []; + + private regexStart: RegExp = /^start (.*?)$/; + private regexStop: RegExp = /^stop (.*?)$/; + private regexSuspend: RegExp = /^suspend (.*?)$/; + private regexReload: RegExp = /^reload (.*?)$/; + private regexShow: RegExp = /^show (.*?)$/; + + constructor( + private projectWebServiceHandler: ProjectWebServiceHandler, + private nodeService: NodeService, + private nodesDataSource: NodesDataSource, + private logEventsDataSource: LogEventsDataSource, + private httpService: HttpServer + ) {} + + ngOnInit() { + this.nodeSubscription = this.projectWebServiceHandler.nodeNotificationEmitter.subscribe((event) => { + let node: Node = event.event as Node; + let message = `Event received: ${event.action} - ${this.printNode(node)}.` + this.showMessage({ + type: 'map update', + message: message + }); + }); + this.linkSubscription = this.projectWebServiceHandler.linkNotificationEmitter.subscribe((event) => { + let link: Link = event.event as Link; + let message = `Event received: ${event.action} - ${this.printLink(link)}.` + this.showMessage({ + type: 'map update', + message: message + }); + }); + this.drawingSubscription = this.projectWebServiceHandler.drawingNotificationEmitter.subscribe((event) => { + let drawing: Drawing = event.event as Drawing; + let message = `Event received: ${event.action} - ${this.printDrawing(drawing)}.` + this.showMessage({ + type: 'map update', + message: message + }); + }); + this.serverRequestsSubscription = this.httpService.requestsNotificationEmitter.subscribe((message) => { + this.showMessage({ + type: 'server request', + message: message + }); + }); + this.errorSubscription = this.projectWebServiceHandler.errorNotificationEmitter.subscribe((message) => { + this.showMessage({ + type: 'error', + message: message + }); + }); + this.errorSubscription = this.projectWebServiceHandler.warningNotificationEmitter.subscribe((message) => { + this.showMessage({ + type: 'warning', + message: message + }); + }); + this.errorSubscription = this.projectWebServiceHandler.infoNotificationEmitter.subscribe((message) => { + this.showMessage({ + type: 'info', + message: message + }); + }); + } + + ngAfterViewInit() { + this.console.nativeElement.scrollTop = this.console.nativeElement.scrollHeight; + } + + ngOnDestroy() { + this.nodeSubscription.unsubscribe(); + this.linkSubscription.unsubscribe(); + this.drawingSubscription.unsubscribe(); + this.serverRequestsSubscription.unsubscribe(); + this.errorSubscription.unsubscribe(); + this.warningSubscription.unsubscribe(); + this.infoSubscription.unsubscribe(); + } + + applyFilter() { + this.filteredEvents = this.getFilteredEvents(); + } + + onKeyDown(event) { + if (event.key === "Enter") { + this.handleCommand(); + } + } + + handleCommand() { + if (this.command === 'help') { + this.showCommand("Available commands: help, version, start all, start {node name}, stop all, stop {node name}, suspend all, suspend {node name}, reload all, reload {node name}, show {node name}.") + } else if (this.command === 'version') { + this.showCommand("Current version: 2019.2.0"); + } else if (this.command === 'start all') { + this.showCommand("Starting all nodes..."); + this.nodeService.startAll(this.server, this.project).subscribe(() => { + this.showCommand("All nodes started.") + }); + } else if (this.command === 'stop all') { + this.showCommand("Stopping all nodes..."); + this.nodeService.stopAll(this.server, this.project).subscribe(() => { + this.showCommand("All nodes stopped.") + }); + } else if (this.command === 'suspend all') { + this.showCommand("Suspending all nodes..."); + this.nodeService.suspendAll(this.server, this.project).subscribe(() => { + this.showCommand("All nodes suspended.") + }); + } else if (this.command === 'reload all') { + this.showCommand("Reloading all nodes..."); + this.nodeService.reloadAll(this.server, this.project).subscribe(() => { + this.showCommand("All nodes reloaded.") + }); + } else if ( + this.regexStart.test(this.command) || this.regexStop.test(this.command) || this.regexSuspend.test(this.command) || this.regexReload.test(this.command) || this.regexShow.test(this.command)) { + let splittedCommand = this.command.split(/[ ,]+/); + let node = this.nodesDataSource.getItems().find(n => n.name.valueOf() === splittedCommand[1].valueOf()); + if (node) { + if (this.regexStart.test(this.command)) { + this.showCommand(`Starting node ${splittedCommand[1]}...`); + this.nodeService.start(this.server, node).subscribe(() => this.showCommand(`Node ${node.name} started.`)); + } + else if (this.regexStop.test(this.command)) { + this.showCommand(`Stopping node ${splittedCommand[1]}...`); + this.nodeService.stop(this.server, node).subscribe(() => this.showCommand(`Node ${node.name} stopped.`)); + } + else if (this.regexSuspend.test(this.command)) { + this.showCommand(`Suspending node ${splittedCommand[1]}...`); + this.nodeService.suspend(this.server, node).subscribe(() => this.showCommand(`Node ${node.name} suspended.`)); + } + else if (this.regexReload.test(this.command)) { + this.showCommand(`Reloading node ${splittedCommand[1]}...`); + this.nodeService.reload(this.server, node).subscribe(() => this.showCommand(`Node ${node.name} reloaded.`)); + } + else if (this.regexShow.test(this.command)) { + this.showCommand(`Information about node ${node.name}:`); + this.showCommand(this.printNode(node)); + } + } else { + this.showCommand(`Node with ${splittedCommand[1]} name was not found.`); + } + } else { + this.showCommand(`Unknown syntax: ${this.command}`); + } + this.command = ''; + } + + clearConsole() { + this.filteredEvents = []; + this.console.nativeElement.scrollTop = this.console.nativeElement.scrollHeight; + } + + showCommand(message: string) { + this.showMessage({ + type: 'command', + message: message + }); + } + + showMessage(event: LogEvent) { + this.logEventsDataSource.add(event); + this.filteredEvents = this.getFilteredEvents(); + this.console.nativeElement.scrollTop = this.console.nativeElement.scrollHeight; + + setTimeout( () => { + this.console.nativeElement.scrollTop = this.console.nativeElement.scrollHeight; + }, 100 ); + } + + getFilteredEvents(): LogEvent[] { + if (this.selectedFilter === 'server requests') { + return this.logEventsDataSource.getItems().filter(n => n.type === 'server request'); + } else if (this.selectedFilter === 'errors') { + return this.logEventsDataSource.getItems().filter(n => n.type === 'error'); + } else if (this.selectedFilter === 'warnings') { + return this.logEventsDataSource.getItems().filter(n => n.type === 'warning'); + } else if (this.selectedFilter === 'info') { + return this.logEventsDataSource.getItems().filter(n => n.type === 'info'); + } else if (this.selectedFilter === 'map updates') { + return this.logEventsDataSource.getItems().filter(n => n.type === 'map update' || n.type === 'command'); + } else { + return this.logEventsDataSource.getItems(); + } + } + + printNode(node: Node): string { + return `command_line: ${node.command_line}, + compute_id: ${node.compute_id}, + console: ${node.console}, + console_host: ${node.console_host}, + console_type: ${node.console_type}, + first_port_name: ${node.first_port_name}, + height: ${node.height}, + label: ${node.label.text}, + name: ${node.name}, + node_directory: ${node.node_directory}, + node_id: ${node.node_id}, + node_type: ${node.node_type}, + port_name_format: ${node.port_name_format}, + port_segment_size: ${node.port_segment_size}, ` + + this.printPorts(node.ports) + + `project_id: ${node.project_id}, + status: ${node.status}, + symbol: ${node.symbol}, + symbol_url: ${node.symbol_url}, + width: ${node.width}, + x: ${node.x}, + y: ${node.y}, + z: ${node.z}`; + } + + printPorts(ports: Port[]): string { + let response: string = `ports: ` + ports.forEach(port => { + response = response + `adapter_number: ${port.adapter_number}, + link_type: ${port.link_type}, + name: ${port.name}, + port_number: ${port.port_number}, + short_name: ${port.short_name}, ` + }); + return response; + } + + printLink(link: Link): string { + return `capture_file_name: ${link.capture_file_name}, + capture_file_path: ${link.capture_file_path}, + capturing: ${link.capturing}, + link_id: ${link.link_id}, + link_type: ${link.link_type}, + project_id: ${link.project_id}, + suspend: ${link.suspend}, `; + } + + printDrawing(drawing: Drawing): string { + return `drawing_id: ${drawing.drawing_id}, + project_id: ${drawing.project_id}, + rotation: ${drawing.rotation}, + x: ${drawing.x}, + y: ${drawing.y}, + z: ${drawing.z}`; + } + + close() { + this.closeConsole.emit(false); + } +} diff --git a/src/app/components/project-map/log-console/log-events-datasource.ts b/src/app/components/project-map/log-console/log-events-datasource.ts new file mode 100644 index 00000000..9bc21a23 --- /dev/null +++ b/src/app/components/project-map/log-console/log-events-datasource.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core'; +import { DataSource } from '../../../cartography/datasources/datasource'; +import { LogEvent } from '../../../models/logEvent'; + +@Injectable() +export class LogEventsDataSource extends DataSource { + protected getItemKey(log: LogEvent) { + return log; + } +} diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 6635238d..c307c7f1 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -77,6 +77,9 @@ Show interface labels + + Show console + Show topology summary @@ -143,6 +146,9 @@ +
+ +
diff --git a/src/app/components/project-map/project-map.component.spec.ts b/src/app/components/project-map/project-map.component.spec.ts index f78e17a0..65ea89ad 100644 --- a/src/app/components/project-map/project-map.component.spec.ts +++ b/src/app/components/project-map/project-map.component.spec.ts @@ -93,6 +93,22 @@ export class MockedNodeService { return of(); } + start(server: Server, node: Node) { + return of(); + } + + stop(server: Server, node: Node) { + return of(); + } + + suspend(server: Server, node: Node) { + return of(); + } + + reload(server: Server, node: Node) { + return of(); + } + duplicate(server: Server, node: Node) { return of(node); } @@ -190,6 +206,10 @@ export class MockedNodesDataSource { return {status: 'started'}; } + getItems() { + return [{name: 'testNode'}]; + } + update() { return of({}); } diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index 78713a83..92061eef 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -74,6 +74,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { public server: Server; public ws: WebSocket; public isProjectMapMenuVisible: boolean = false; + public isConsoleVisible: boolean = false; public isTopologySummaryVisible: boolean = false; tools = { @@ -135,6 +136,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { ngOnInit() { this.settings = this.settingsService.getAll(); this.isTopologySummaryVisible = this.mapSettingsService.isTopologySummaryVisible; + this.isConsoleVisible = this.mapSettingsService.isLogConsoleVisible; this.progressService.activate(); const routeSub = this.route.paramMap.subscribe((paramMap: ParamMap) => { @@ -403,6 +405,11 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.project.show_interface_labels = enabled; } + public toggleShowConsole(visible: boolean) { + this.isConsoleVisible = visible; + this.mapSettingsService.toggleLogConsole(this.isConsoleVisible); + } + public toggleShowTopologySummary(visible: boolean) { this.isTopologySummaryVisible = visible; this.mapSettingsService.toggleTopologySummary(this.isTopologySummaryVisible); diff --git a/src/app/handlers/project-web-service-handler.ts b/src/app/handlers/project-web-service-handler.ts index 597553f6..e9405c62 100644 --- a/src/app/handlers/project-web-service-handler.ts +++ b/src/app/handlers/project-web-service-handler.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, EventEmitter } from '@angular/core'; import { Subject } from 'rxjs'; import { NodesDataSource } from '../cartography/datasources/nodes-datasource'; @@ -15,6 +15,14 @@ export class WebServiceMessage { @Injectable() export class ProjectWebServiceHandler { + public nodeNotificationEmitter = new EventEmitter(); + public linkNotificationEmitter = new EventEmitter(); + public drawingNotificationEmitter = new EventEmitter(); + + public infoNotificationEmitter = new EventEmitter(); + public warningNotificationEmitter = new EventEmitter(); + public errorNotificationEmitter = new EventEmitter(); + constructor( private nodesDataSource: NodesDataSource, private linksDataSource: LinksDataSource, @@ -24,30 +32,48 @@ export class ProjectWebServiceHandler { public handleMessage(message: WebServiceMessage) { if (message.action === 'node.updated') { this.nodesDataSource.update(message.event as Node); + this.nodeNotificationEmitter.emit(message); } if (message.action === 'node.created') { this.nodesDataSource.add(message.event as Node); + this.nodeNotificationEmitter.emit(message); } if (message.action === 'node.deleted') { this.nodesDataSource.remove(message.event as Node); + this.nodeNotificationEmitter.emit(message); } if (message.action === 'link.created') { this.linksDataSource.add(message.event as Link); + this.linkNotificationEmitter.emit(message); } if (message.action === 'link.updated') { this.linksDataSource.update(message.event as Link); + this.linkNotificationEmitter.emit(message); } if (message.action === 'link.deleted') { this.linksDataSource.remove(message.event as Link); + this.linkNotificationEmitter.emit(message); } if (message.action === 'drawing.created') { this.drawingsDataSource.add(message.event as Drawing); + this.drawingNotificationEmitter.emit(message); } if (message.action === 'drawing.updated') { this.drawingsDataSource.update(message.event as Drawing); + this.drawingNotificationEmitter.emit(message); } if (message.action === 'drawing.deleted') { this.drawingsDataSource.remove(message.event as Drawing); + this.drawingNotificationEmitter.emit(message); + } + if (message.action === 'log.error') { + this.errorNotificationEmitter.emit(message.event); + } + if (message.action === 'log.warning') { + this.warningNotificationEmitter.emit(message.event); + } + if (message.action === 'log.info') { + this.infoNotificationEmitter.emit(message.event); } } } diff --git a/src/app/models/logEvent.ts b/src/app/models/logEvent.ts new file mode 100644 index 00000000..4f3e7110 --- /dev/null +++ b/src/app/models/logEvent.ts @@ -0,0 +1,4 @@ +export class LogEvent { + type: string; + message: string; +} diff --git a/src/app/services/http-server.service.ts b/src/app/services/http-server.service.ts index 5758ecc5..ca9b71ce 100644 --- a/src/app/services/http-server.service.ts +++ b/src/app/services/http-server.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, EventEmitter } from '@angular/core'; import { HttpHeaders, HttpClient, HttpParams, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; @@ -79,11 +79,15 @@ export class ServerErrorHandler { @Injectable() export class HttpServer { + public requestsNotificationEmitter = new EventEmitter(); + constructor(private http: HttpClient, private errorHandler: ServerErrorHandler) {} get(server: Server, url: string, options?: JsonOptions): Observable { options = this.getJsonOptions(options); const intercepted = this.getOptionsForServer(server, url, options); + this.requestsNotificationEmitter.emit(`GET ${intercepted.url}`); + return this.http .get(intercepted.url, intercepted.options as JsonOptions) .pipe(catchError(this.errorHandler.handleError)) as Observable; @@ -92,6 +96,8 @@ export class HttpServer { getText(server: Server, url: string, options?: TextOptions): Observable { options = this.getTextOptions(options); const intercepted = this.getOptionsForServer(server, url, options); + this.requestsNotificationEmitter.emit(`GET ${intercepted.url}`); + return this.http .get(intercepted.url, intercepted.options as TextOptions) .pipe(catchError(this.errorHandler.handleError)); @@ -100,6 +106,8 @@ export class HttpServer { post(server: Server, url: string, body: any | null, options?: JsonOptions): Observable { options = this.getJsonOptions(options); const intercepted = this.getOptionsForServer(server, url, options); + this.requestsNotificationEmitter.emit(`POST ${intercepted.url}`); + return this.http .post(intercepted.url, body, intercepted.options) .pipe(catchError(this.errorHandler.handleError)) as Observable; @@ -108,6 +116,8 @@ export class HttpServer { put(server: Server, url: string, body: any, options?: JsonOptions): Observable { options = this.getJsonOptions(options); const intercepted = this.getOptionsForServer(server, url, options); + this.requestsNotificationEmitter.emit(`PUT ${intercepted.url}`); + return this.http .put(intercepted.url, body, intercepted.options) .pipe(catchError(this.errorHandler.handleError)) as Observable; @@ -116,6 +126,8 @@ export class HttpServer { delete(server: Server, url: string, options?: JsonOptions): Observable { options = this.getJsonOptions(options); const intercepted = this.getOptionsForServer(server, url, options); + this.requestsNotificationEmitter.emit(`DELETE ${intercepted.url}`); + return this.http .delete(intercepted.url, intercepted.options) .pipe(catchError(this.errorHandler.handleError)) as Observable; diff --git a/src/app/services/mapsettings.service.ts b/src/app/services/mapsettings.service.ts index caf4a6c8..2f1764e5 100644 --- a/src/app/services/mapsettings.service.ts +++ b/src/app/services/mapsettings.service.ts @@ -5,6 +5,7 @@ import { Subject } from 'rxjs'; export class MapSettingsService { public isMapLocked = new Subject(); public isTopologySummaryVisible: boolean = false; + public isLogConsoleVisible: boolean = false; constructor() {} @@ -15,4 +16,8 @@ export class MapSettingsService { toggleTopologySummary(value: boolean) { this.isTopologySummaryVisible = value; } + + toggleLogConsole(value: boolean) { + this.isLogConsoleVisible = value; + } } diff --git a/src/app/services/node.service.spec.ts b/src/app/services/node.service.spec.ts index 6559f9d2..1687c7ab 100644 --- a/src/app/services/node.service.spec.ts +++ b/src/app/services/node.service.spec.ts @@ -64,6 +64,30 @@ describe('NodeService', () => { expect(req.request.body).toEqual({}); })); + it('should suspend node', inject([NodeService], (service:NodeService) => { + const node = new Node(); + node.project_id = 'myproject'; + node.node_id = 'id'; + + service.suspend(server, node).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/projects/myproject/nodes/id/suspend'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual({}); + })); + + it('should reload node', inject([NodeService], (service:NodeService) => { + const node = new Node(); + node.project_id = 'myproject'; + node.node_id = 'id'; + + service.reload(server, node).subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/projects/myproject/nodes/id/reload'); + expect(req.request.method).toEqual('POST'); + expect(req.request.body).toEqual({}); + })); + it('should start all nodes', inject([NodeService], (service: NodeService) => { let project = { project_id: '1' diff --git a/src/app/services/node.service.ts b/src/app/services/node.service.ts index b0852956..4f894d09 100644 --- a/src/app/services/node.service.ts +++ b/src/app/services/node.service.ts @@ -29,10 +29,18 @@ export class NodeService { return this.httpServer.post(server, `/projects/${project.project_id}/nodes/stop`, {}); } + suspend(server: Server, node: Node) { + return this.httpServer.post(server, `/projects/${node.project_id}/nodes/${node.node_id}/suspend`, {}); + } + suspendAll(server: Server, project: Project) { return this.httpServer.post(server, `/projects/${project.project_id}/nodes/suspend`, {}); } + reload(server: Server, node: Node) { + return this.httpServer.post(server, `/projects/${node.project_id}/nodes/${node.node_id}/reload`, {}); + } + reloadAll(server: Server, project: Project) { return this.httpServer.post(server, `/projects/${project.project_id}/nodes/reload`, {}); } diff --git a/yarn.lock b/yarn.lock index 0d799b27..71512706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9006,6 +9006,11 @@ xtend@~2.1.1: dependencies: object-keys "~0.4.0" +xterm@^3.14.5: + version "3.14.5" + resolved "https://registry.npmjs.org/xterm/-/xterm-3.14.5.tgz#c9d14e48be6873aa46fb429f22f2165557fd2dea" + integrity sha512-DVmQ8jlEtL+WbBKUZuMxHMBgK/yeIZwkXB81bH+MGaKKnJGYwA+770hzhXPfwEIokK9On9YIFPRleVp/5G7z9g== + y18n@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"