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 @@
+
+
+
+
+
+ {{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"