diff --git a/.appveyor.yml b/.appveyor.yml index d5ae64c0..792bc9b6 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -14,10 +14,11 @@ init: - git config --global core.autocrlf input install: - - ps: Install-Product node 11 x64 + - ps: Install-Product node 12 x64 - yarn install build_script: + - cmd: set NODE_OPTIONS=--max-old-space-size=8092 - yarn buildforelectron - "%PYTHON%\\python.exe -m pip install -r scripts\\requirements.txt" - "%PYTHON%\\python.exe scripts\\build.py download -a" diff --git a/package.json b/package.json index ff4897de..ffc3a719 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gns3-web-ui", - "version": "2019.2.0-alpha.6dev", + "version": "2019.2.0-alpha.8dev", "author": { "name": "GNS3 Technology Inc.", "email": "developers@gns3.com" @@ -38,25 +38,26 @@ }, "private": true, "dependencies": { - "@angular/animations": "^8.1.2", - "@angular/cdk": "^8.1.1", - "@angular/common": "^8.1.2", - "@angular/compiler": "^8.1.2", - "@angular/core": "^8.1.2", - "@angular/forms": "^8.1.2", + "@angular/animations": "^8.2.8", + "@angular/cdk": "^8.2.1", + "@angular/common": "^8.2.8", + "@angular/compiler": "^8.2.8", + "@angular/core": "^8.2.8", + "@angular/forms": "^8.2.8", "@angular/http": "^7.2.15", - "@angular/material": "^8.1.1", - "@angular/platform-browser": "^8.1.2", - "@angular/platform-browser-dynamic": "^8.1.2", - "@angular/router": "^8.1.2", + "@angular/material": "^8.2.1", + "@angular/platform-browser": "^8.2.8", + "@angular/platform-browser-dynamic": "^8.2.8", + "@angular/router": "^8.2.8", "angular-persistence": "^1.0.1", - "angular2-hotkeys": "^2.1.4", + "angular2-hotkeys": "^2.1.5", "angular2-indexeddb": "^1.2.3", "bootstrap": "4.3.1", "command-exists": "^1.2.8", - "core-js": "^3.1.4", - "css-tree": "^1.0.0-alpha.33", + "core-js": "^3.2.1", + "css-tree": "^1.0.0-alpha.34", "d3-ng2-service": "^2.1.0", + "file-saver": "^2.0.2", "hammerjs": "^2.0.8", "ini": "^1.3.5", "material-design-icons": "^3.0.1", @@ -65,24 +66,27 @@ "node-fetch": "^2.6.0", "notosans-fontface": "^1.1.0", "raven-js": "^3.27.2", - "rxjs": "^6.5.2", - "rxjs-compat": "^6.5.2", + "rxjs": "^6.5.3", + "rxjs-compat": "^6.5.3", + "save-svg-as-png": "^1.4.14", + "svg-crowbar": "^0.2.3", "tree-kill": "^1.2.1", "typeface-roboto": "^0.0.75", + "xterm": "^3.14.5", "yargs": "^13.3.0", "zone.js": "^0.9.1" }, "devDependencies": { - "@angular-devkit/build-angular": "~0.801.2", - "@angular/cli": "^8.1.2", - "@angular/compiler-cli": "^8.1.2", - "@angular/language-service": "^8.1.2", - "@sentry/cli": "^1.47.0", - "@sentry/electron": "^0.17.3", - "@types/jasmine": "~3.3.15", - "@types/jasminewd2": "~2.0.6", - "@types/node": "~12.6.8", - "codelyzer": "~5.1.0", + "@angular-devkit/build-angular": "^0.801.3", + "@angular/cli": "^8.3.6", + "@angular/compiler-cli": "^8.2.8", + "@angular/language-service": "^8.2.8", + "@sentry/cli": "^1.47.2", + "@sentry/electron": "^0.17.4", + "@types/jasmine": "^3.3.16", + "@types/jasminewd2": "^2.0.7", + "@types/node": "^12.6.9", + "codelyzer": "^5.1.2", "electron": "5.0.8", "electron-builder": "21.1.5", "jasmine-core": "~3.4.0", @@ -99,8 +103,8 @@ "popper.js": "^1.15.0", "prettier": "^1.18.2", "protractor": "~5.4.2", - "replace": "^1.1.0", - "ts-mockito": "^2.4.2", + "replace": "^1.1.1", + "ts-mockito": "^2.5.0", "ts-node": "~8.3.0", "tslint": "~5.18.0", "tslint-config-prettier": "^1.18.0", diff --git a/src/ReleaseNotes.txt b/src/ReleaseNotes.txt index 9c0fe032..e796ac23 100644 --- a/src/ReleaseNotes.txt +++ b/src/ReleaseNotes.txt @@ -3,14 +3,17 @@ GNS3 WebUI is web implementation of user interface for GNS3 software. Current version: 2019.2.0 What's New -- Help section added with information about third party components -- Showing progress when server starting -- Possibility to edit interface & node labels by using context menu -- Enhancements in moving elements on map -- Context menu extended with option to duplicate -- Main menu extended with option to lock all items on map +- Editing interface labels on double click +- Support for keyboard shortcuts +- Menu extended with option to delete currently opened project, export & import project +- Possibility to save current state of project +- Ability to duplicate project from projects page +- Node information dialog available from context menu +- Topology summary widget on map view +- Improvements in dialog styles Bug Fixes -- Removing issues with positioning interface labels while adding link between nodes on map +- Removing issues with opening console - Context menu now is correctly placed -- Entered text in text & style editor is now validated +- Text validation in dialogs +- Removing errors with creating WebSockets diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 25db77e8..1dcf6b05 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -53,6 +53,10 @@ import { CopyIouTemplateComponent } from './components/preferences/ios-on-unix/c import { ListOfSnapshotsComponent } from './components/snapshots/list-of-snapshots/list-of-snapshots.component'; import { ConsoleComponent } from './components/settings/console/console.component'; import { HelpComponent } from './components/help/help.component'; +import { TracengPreferencesComponent } from './components/preferences/traceng/traceng-preferences/traceng-preferences.component'; +import { TracengTemplatesComponent } from './components/preferences/traceng/traceng-templates/traceng-templates.component'; +import { AddTracengTemplateComponent } from './components/preferences/traceng/add-traceng/add-traceng-template.component'; +import { TracengTemplateDetailsComponent } from './components/preferences/traceng/traceng-template-details/traceng-template-details.component'; const routes: Routes = [ { @@ -111,6 +115,11 @@ const routes: Routes = [ { path: 'server/:server_id/preferences/vmware/templates/:template_id', component: VmwareTemplateDetailsComponent }, { path: 'server/:server_id/preferences/vmware/addtemplate', component: AddVmwareTemplateComponent }, + // { path: 'server/:server_id/preferences/traceng', component: TracengPreferencesComponent }, + // { path: 'server/:server_id/preferences/traceng/templates', component: TracengTemplatesComponent }, + // { path: 'server/:server_id/preferences/traceng/templates/:template_id', component: TracengTemplateDetailsComponent }, + // { path: 'server/:server_id/preferences/traceng/addtemplate', component: AddTracengTemplateComponent }, + { path: 'server/:server_id/preferences/docker/templates', component: DockerTemplatesComponent }, { path: 'server/:server_id/preferences/docker/templates/:template_id', component: DockerTemplateDetailsComponent }, { path: 'server/:server_id/preferences/docker/templates/:template_id/copy', component: CopyDockerTemplateComponent }, diff --git a/src/app/app.component.css b/src/app/app.component.css index e69de29b..1bc4310f 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -0,0 +1,3 @@ +mat-menu-panel { + min-height: 0px; +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 133be591..178309aa 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -189,9 +189,59 @@ import { NotificationBoxComponent } from './components/notification-box/notifica import { NonNegativeValidator } from './validators/non-negative-validator'; import { RotationValidator } from './validators/rotation-validator'; import { DuplicateActionComponent } from './components/project-map/context-menu/actions/duplicate-action/duplicate-action.component'; -import { MapSettingService } from './services/mapsettings.service'; +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 { ConfigEditorDialogComponent } from './components/project-map/node-editors/config-editor/config-editor.component'; +import { EditConfigActionComponent } from './components/project-map/context-menu/actions/edit-config/edit-config-action.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'; +import { InfoDialogComponent } from './components/project-map/info-dialog/info-dialog.component'; +import { InfoService } from './services/info.service'; +import { BringToFrontActionComponent } from './components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component'; +import { ExportConfigActionComponent } from './components/project-map/context-menu/actions/export-config/export-config-action.component'; +import { ImportConfigActionComponent } from './components/project-map/context-menu/actions/import-config/import-config-action.component'; +import { ConsoleDeviceActionBrowserComponent } from './components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component'; +import { ChangeSymbolDialogComponent } from './components/project-map/change-symbol-dialog/change-symbol-dialog.component'; +import { ChangeSymbolActionComponent } from './components/project-map/context-menu/actions/change-symbol/change-symbol-action.component'; +import { EditProjectDialogComponent } from './components/projects/edit-project-dialog/edit-project-dialog.component'; +import { ProjectsFilter } from './filters/projectsFilter.pipe'; +import { ComputeService } from './services/compute.service'; +import { ReloadNodeActionComponent } from './components/project-map/context-menu/actions/reload-node-action/reload-node-action.component'; +import { SuspendNodeActionComponent } from './components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component'; +import { ConfigActionComponent } from './components/project-map/context-menu/actions/config-action/config-action.component'; +import { ConfiguratorDialogVpcsComponent } from './components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component'; +import { ConfiguratorDialogEthernetHubComponent } from './components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component'; +import { ConfiguratorDialogEthernetSwitchComponent } from './components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component'; +import { PortsComponent } from './components/preferences/common/ports/ports.component'; +import { ConfiguratorDialogSwitchComponent } from './components/project-map/node-editors/configurator/switch/configurator-switch.component'; +import { ConfiguratorDialogVirtualBoxComponent } from './components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component'; +import { CustomAdaptersTableComponent } from './components/preferences/common/custom-adapters-table/custom-adapters-table.component'; +import { ConfiguratorDialogQemuComponent } from './components/project-map/node-editors/configurator/qemu/configurator-qemu.component'; +import { ConfiguratorDialogCloudComponent } from './components/project-map/node-editors/configurator/cloud/configurator-cloud.component'; +import { UdpTunnelsComponent } from './components/preferences/common/udp-tunnels/udp-tunnels.component'; +import { ConfiguratorDialogAtmSwitchComponent } from './components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component'; +import { ConfiguratorDialogVmwareComponent } from './components/project-map/node-editors/configurator/vmware/configurator-vmware.component'; +import { ConfiguratorDialogIouComponent } from './components/project-map/node-editors/configurator/iou/configurator-iou.component'; +import { ConfiguratorDialogIosComponent } from './components/project-map/node-editors/configurator/ios/configurator-ios.component'; +import { ConfiguratorDialogDockerComponent } from './components/project-map/node-editors/configurator/docker/configurator-docker.component'; +import { ConfiguratorDialogNatComponent } from './components/project-map/node-editors/configurator/nat/configurator-nat.component'; +import { ConfiguratorDialogTracengComponent } from './components/project-map/node-editors/configurator/traceng/configurator-traceng.component'; +import { AddTracengTemplateComponent } from './components/preferences/traceng/add-traceng/add-traceng-template.component'; +import { TracengPreferencesComponent } from './components/preferences/traceng/traceng-preferences/traceng-preferences.component'; +import { TracengTemplatesComponent } from './components/preferences/traceng/traceng-templates/traceng-templates.component'; +import { TracengService } from './services/traceng.service'; +import { TracengTemplateDetailsComponent } from './components/preferences/traceng/traceng-template-details/traceng-template-details.component'; +import { QemuImageCreatorComponent } from './components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component'; +import { ChooseNameDialogComponent } from './components/projects/choose-name-dialog/choose-name-dialog.component'; +import { PacketCaptureService } from './services/packet-capture.service'; +import { StartCaptureOnStartedLinkActionComponent } from './components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component'; +import { LockActionComponent } from './components/project-map/context-menu/actions/lock-action/lock-action.component'; +import { NavigationDialogComponent } from './components/projects/navigation-dialog/navigation-dialog.component'; +import { ScreenshotDialogComponent } from './components/project-map/screenshot-dialog/screenshot-dialog.component'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -305,16 +355,61 @@ if (environment.production) { SearchFilter, DateFilter, NameFilter, + ProjectsFilter, ListOfSnapshotsComponent, CustomAdaptersComponent, NodesMenuComponent, AdbutlerComponent, ConsoleDeviceActionComponent, + ShowNodeActionComponent, ConsoleComponent, NodesMenuComponent, NotificationBoxComponent, ProjectMapMenuComponent, - HelpComponent + HelpComponent, + ConfigEditorDialogComponent, + EditConfigActionComponent, + LogConsoleComponent, + SaveProjectDialogComponent, + TopologySummaryComponent, + InfoDialogComponent, + BringToFrontActionComponent, + ExportConfigActionComponent, + ImportConfigActionComponent, + ConsoleDeviceActionBrowserComponent, + ChangeSymbolDialogComponent, + ChangeSymbolActionComponent, + EditProjectDialogComponent, + ReloadNodeActionComponent, + SuspendNodeActionComponent, + ConfigActionComponent, + ConfiguratorDialogVpcsComponent, + ConfiguratorDialogEthernetHubComponent, + ConfiguratorDialogEthernetSwitchComponent, + PortsComponent, + ConfiguratorDialogSwitchComponent, + ConfiguratorDialogVirtualBoxComponent, + CustomAdaptersTableComponent, + ConfiguratorDialogQemuComponent, + ConfiguratorDialogCloudComponent, + UdpTunnelsComponent, + ConfiguratorDialogAtmSwitchComponent, + ConfiguratorDialogVmwareComponent, + ConfiguratorDialogIouComponent, + ConfiguratorDialogIosComponent, + ConfiguratorDialogDockerComponent, + ConfiguratorDialogNatComponent, + ConfiguratorDialogTracengComponent, + AddTracengTemplateComponent, + TracengPreferencesComponent, + TracengTemplatesComponent, + TracengTemplateDetailsComponent, + QemuImageCreatorComponent, + ChooseNameDialogComponent, + StartCaptureOnStartedLinkActionComponent, + LockActionComponent, + NavigationDialogComponent, + ScreenshotDialogComponent ], imports: [ BrowserModule, @@ -354,6 +449,7 @@ if (environment.production) { LinksDataSource, NodesDataSource, SymbolsDataSource, + LogEventsDataSource, SelectionManager, InRectangleHelper, DrawingsDataSource, @@ -390,7 +486,11 @@ if (environment.production) { NodeCreatedLabelStylesFixer, NonNegativeValidator, RotationValidator, - MapSettingService + MapSettingsService, + InfoService, + ComputeService, + TracengService, + PacketCaptureService ], entryComponents: [ AddServerDialogComponent, @@ -406,7 +506,30 @@ if (environment.production) { SymbolsComponent, DeleteConfirmationDialogComponent, HelpDialogComponent, - StartCaptureDialogComponent + StartCaptureDialogComponent, + ConfigEditorDialogComponent, + SaveProjectDialogComponent, + InfoDialogComponent, + ChangeSymbolDialogComponent, + EditProjectDialogComponent, + ConfiguratorDialogVpcsComponent, + ConfiguratorDialogEthernetHubComponent, + ConfiguratorDialogEthernetSwitchComponent, + ConfiguratorDialogSwitchComponent, + ConfiguratorDialogVirtualBoxComponent, + ConfiguratorDialogQemuComponent, + ConfiguratorDialogCloudComponent, + ConfiguratorDialogAtmSwitchComponent, + ConfiguratorDialogVmwareComponent, + ConfiguratorDialogIouComponent, + ConfiguratorDialogIosComponent, + ConfiguratorDialogDockerComponent, + ConfiguratorDialogNatComponent, + ConfiguratorDialogTracengComponent, + QemuImageCreatorComponent, + ChooseNameDialogComponent, + NavigationDialogComponent, + ScreenshotDialogComponent ], bootstrap: [AppComponent] }) diff --git a/src/app/cartography/cartography.module.ts b/src/app/cartography/cartography.module.ts index 226ebbae..11eff982 100644 --- a/src/app/cartography/cartography.module.ts +++ b/src/app/cartography/cartography.module.ts @@ -58,6 +58,8 @@ import { DrawingAddingComponent } from './components/drawing-adding/drawing-addi import { MovingEventSource } from './events/moving-event-source'; import { MovingCanvasDirective } from './directives/moving-canvas.directive'; import { ZoomingCanvasDirective } from './directives/zooming-canvas.directive'; +import { EthernetLinkWidget } from './widgets/links/ethernet-link'; +import { SerialLinkWidget } from './widgets/links/serial-link'; @NgModule({ imports: [CommonModule, MatMenuModule, MatIconModule], @@ -117,6 +119,8 @@ import { ZoomingCanvasDirective } from './directives/zooming-canvas.directive'; MapSettingsManager, FontBBoxCalculator, StylesToFontConverter, + EthernetLinkWidget, + SerialLinkWidget, ...D3_MAP_IMPORTS ], exports: [D3MapComponent, ExperimentalMapComponent] diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts b/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts index fb362b7e..2bb40d57 100644 --- a/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts +++ b/src/app/cartography/components/draggable-selection/draggable-selection.component.spec.ts @@ -21,7 +21,7 @@ import { MapLabel } from '../../models/map/map-label'; import { MapLinkNode } from '../../models/map/map-link-node'; import { select } from 'd3-selection'; import { MapLink } from '../../models/map/map-link'; -import { MapSettingService } from '../../../services/mapsettings.service'; +import { MapSettingsService } from '../../../services/mapsettings.service'; describe('DraggableSelectionComponent', () => { let component: DraggableSelectionComponent; @@ -123,7 +123,7 @@ describe('DraggableSelectionComponent', () => { { provide: DrawingsEventSource, useValue: drawingsEventSourceStub }, { provide: GraphDataManager, useValue: mockedGraphDataManager }, { provide: LinksEventSource, useValue: linksEventSourceStub }, - { provide: MapSettingService, useClass: MapSettingService } + { provide: MapSettingsService, useClass: MapSettingsService } ], declarations: [DraggableSelectionComponent] }).compileComponents(); diff --git a/src/app/cartography/components/draggable-selection/draggable-selection.component.ts b/src/app/cartography/components/draggable-selection/draggable-selection.component.ts index 858a1c8c..ffb73210 100644 --- a/src/app/cartography/components/draggable-selection/draggable-selection.component.ts +++ b/src/app/cartography/components/draggable-selection/draggable-selection.component.ts @@ -17,7 +17,7 @@ import { LabelWidget } from '../../widgets/label'; import { InterfaceLabelWidget } from '../../widgets/interface-label'; import { MapLinkNode } from '../../models/map/map-link-node'; import { LinksEventSource } from '../../events/links-event-source'; -import { MapSettingService } from '../../../services/mapsettings.service'; +import { MapSettingsService } from '../../../services/mapsettings.service'; @Component({ selector: 'app-draggable-selection', @@ -44,7 +44,7 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy { private drawingsEventSource: DrawingsEventSource, private graphDataManager: GraphDataManager, private linksEventSource: LinksEventSource, - private mapSettingsService: MapSettingService + private mapSettingsService: MapSettingsService ) {} ngOnInit() { @@ -94,8 +94,10 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy { ).subscribe((evt: DraggableDrag) => { if (!this.isMapLocked) { const selected = this.selectionManager.getSelected(); - const selectedNodes = selected.filter(item => item instanceof MapNode); // update nodes + let mapNodes = selected.filter(item => item instanceof MapNode); + const lockedNodes = mapNodes.filter((item: MapNode) => item.locked); + const selectedNodes = mapNodes.filter((item: MapNode) => !item.locked); selectedNodes.forEach((node: MapNode) => { node.x += evt.dx; node.y += evt.dy; @@ -116,52 +118,52 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy { }); // update drawings - selected - .filter(item => item instanceof MapDrawing) - .forEach((drawing: MapDrawing) => { - drawing.x += evt.dx; - drawing.y += evt.dy; - this.drawingsWidget.redrawDrawing(svg, drawing); - }); + let mapDrawings = selected.filter(item => item instanceof MapDrawing); + const selectedDrawings = mapDrawings.filter((item: MapDrawing) => !item.locked); + selectedDrawings.forEach((drawing: MapDrawing) => { + drawing.x += evt.dx; + drawing.y += evt.dy; + this.drawingsWidget.redrawDrawing(svg, drawing); + }); // update labels - selected - .filter(item => item instanceof MapLabel) - .forEach((label: MapLabel) => { - const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0; - if (isParentNodeSelected) { - return; - } + let mapLabels = selected.filter(item => item instanceof MapLabel); + const selectedLabels = mapLabels.filter((item: MapLabel) => lockedNodes.filter((node) => node.id === item.nodeId).length === 0); + selectedLabels.forEach((label: MapLabel) => { + const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0; + if (isParentNodeSelected) { + return; + } - const node = this.graphDataManager.getNodes().filter(node => node.id === label.nodeId)[0]; - node.label.x += evt.dx; - node.label.y += evt.dy; - this.labelWidget.redrawLabel(svg, label); - }); + const node = this.graphDataManager.getNodes().filter(node => node.id === label.nodeId)[0]; + node.label.x += evt.dx; + node.label.y += evt.dy; + this.labelWidget.redrawLabel(svg, label); + }); // update interface labels - selected - .filter(item => item instanceof MapLinkNode) - .forEach((interfaceLabel: MapLinkNode) => { - const isParentNodeSelected = selectedNodes.filter(node => node.id === interfaceLabel.nodeId).length > 0; - if (isParentNodeSelected) { - return; - } + let mapLinkNodes = selected.filter(item => item instanceof MapLinkNode); + const selectedLinkNodes = mapLinkNodes.filter((item: MapLinkNode) => lockedNodes.filter((node) => node.id === item.nodeId).length === 0); + selectedLinkNodes.forEach((interfaceLabel: MapLinkNode) => { + const isParentNodeSelected = selectedNodes.filter(node => node.id === interfaceLabel.nodeId).length > 0; + if (isParentNodeSelected) { + return; + } - const link = this.graphDataManager - .getLinks() - .filter(link => link.nodes[0].id === interfaceLabel.id || link.nodes[1].id === interfaceLabel.id)[0]; - if (link.nodes[0].id === interfaceLabel.id) { - link.nodes[0].label.x += evt.dx; - link.nodes[0].label.y += evt.dy; - } - if (link.nodes[1].id === interfaceLabel.id) { - link.nodes[1].label.x += evt.dx; - link.nodes[1].label.y += evt.dy; - } + const link = this.graphDataManager + .getLinks() + .filter(link => link.nodes[0].id === interfaceLabel.id || link.nodes[1].id === interfaceLabel.id)[0]; + if (link.nodes[0].id === interfaceLabel.id) { + link.nodes[0].label.x += evt.dx; + link.nodes[0].label.y += evt.dy; + } + if (link.nodes[1].id === interfaceLabel.id) { + link.nodes[1].label.x += evt.dx; + link.nodes[1].label.y += evt.dy; + } - this.linksWidget.redrawLink(svg, link); - }); + this.linksWidget.redrawLink(svg, link); + }); } }); @@ -173,39 +175,41 @@ export class DraggableSelectionComponent implements OnInit, OnDestroy { ).subscribe((evt: DraggableEnd) => { if (!this.isMapLocked) { const selected = this.selectionManager.getSelected(); - const selectedNodes = selected.filter(item => item instanceof MapNode); + let mapNodes = selected.filter(item => item instanceof MapNode); + const lockedNodes = mapNodes.filter((item: MapNode) => item.locked); + const selectedNodes = mapNodes.filter((item: MapNode) => !item.locked); selectedNodes.forEach((item: MapNode) => { this.nodesEventSource.dragged.emit(new DraggedDataEvent(item, evt.dx, evt.dy)); }); - selected - .filter(item => item instanceof MapDrawing) - .forEach((item: MapDrawing) => { - this.drawingsEventSource.dragged.emit(new DraggedDataEvent(item, evt.dx, evt.dy)); - }); + let mapDrawings = selected.filter(item => item instanceof MapDrawing); + const selectedDrawings = mapDrawings.filter((item: MapDrawing) => !item.locked); + selectedDrawings.forEach((item: MapDrawing) => { + this.drawingsEventSource.dragged.emit(new DraggedDataEvent(item, evt.dx, evt.dy)); + }); - selected - .filter(item => item instanceof MapLabel) - .forEach((label: MapLabel) => { - const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0; - if (isParentNodeSelected) { - return; - } + let mapLabels = selected.filter(item => item instanceof MapLabel); + const selectedLabels = mapLabels.filter((item: MapLabel) => lockedNodes.filter((node) => node.id === item.nodeId).length === 0); + selectedLabels.forEach((label: MapLabel) => { + const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0; + if (isParentNodeSelected) { + return; + } - this.nodesEventSource.labelDragged.emit(new DraggedDataEvent(label, evt.dx, evt.dy)); - }); + this.nodesEventSource.labelDragged.emit(new DraggedDataEvent(label, evt.dx, evt.dy)); + }); - selected - .filter(item => item instanceof MapLinkNode) - .forEach((label: MapLinkNode) => { - const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0; - if (isParentNodeSelected) { - return; - } - this.linksEventSource.interfaceDragged.emit(new DraggedDataEvent(label, evt.dx, evt.dy)); - }); - } + let mapLinkNodes = selected.filter(item => item instanceof MapLinkNode); + const selectedLinkNodes = mapLinkNodes.filter((item: MapLinkNode) => lockedNodes.filter((node) => node.id === item.nodeId).length === 0) + selectedLinkNodes.forEach((label: MapLinkNode) => { + const isParentNodeSelected = selectedNodes.filter(node => node.id === label.nodeId).length > 0; + if (isParentNodeSelected) { + return; + } + this.linksEventSource.interfaceDragged.emit(new DraggedDataEvent(label, evt.dx, evt.dy)); + }); + } }); } diff --git a/src/app/cartography/converters/map/drawing-to-map-drawing-converter.ts b/src/app/cartography/converters/map/drawing-to-map-drawing-converter.ts index 2c43cf80..a45ebd29 100644 --- a/src/app/cartography/converters/map/drawing-to-map-drawing-converter.ts +++ b/src/app/cartography/converters/map/drawing-to-map-drawing-converter.ts @@ -14,6 +14,7 @@ export class DrawingToMapDrawingConverter implements Converter { node.first_port_name = mapNode.firstPortName; node.height = mapNode.height; node.label = mapNode.label ? this.mapLabelToLabel.convert(mapNode.label) : undefined; + node.locked = mapNode.locked; node.name = mapNode.name; node.node_directory = mapNode.nodeDirectory; node.node_type = mapNode.nodeType; diff --git a/src/app/cartography/converters/map/node-to-map-node-converter.ts b/src/app/cartography/converters/map/node-to-map-node-converter.ts index 1a739069..b3e64b1d 100644 --- a/src/app/cartography/converters/map/node-to-map-node-converter.ts +++ b/src/app/cartography/converters/map/node-to-map-node-converter.ts @@ -28,13 +28,14 @@ export class NodeToMapNodeConverter implements Converter { mapNode.consoleHost = node.console_host; mapNode.firstPortName = node.first_port_name; mapNode.height = node.height; - mapNode.label = this.labelToMapLabel.convert(node.label, { node_id: node.node_id }); + mapNode.label = this.labelToMapLabel ? this.labelToMapLabel.convert(node.label, { node_id: node.node_id }) : undefined; + mapNode.locked = node.locked; mapNode.name = node.name; mapNode.nodeDirectory = node.node_directory; mapNode.nodeType = node.node_type; mapNode.portNameFormat = node.port_name_format; mapNode.portSegmentSize = node.port_segment_size; - mapNode.ports = node.ports.map(port => this.portToMapPort.convert(port)); + mapNode.ports = node.ports ? node.ports.map(port => this.portToMapPort.convert(port)) : []; mapNode.projectId = node.project_id; mapNode.status = node.status; mapNode.symbol = node.symbol; diff --git a/src/app/cartography/managers/graph-data-manager.ts b/src/app/cartography/managers/graph-data-manager.ts index fdf0a786..0de68ed5 100644 --- a/src/app/cartography/managers/graph-data-manager.ts +++ b/src/app/cartography/managers/graph-data-manager.ts @@ -34,31 +34,39 @@ export class GraphDataManager { ) {} public setNodes(nodes: Node[]) { - const mapNodes = nodes.map(n => this.nodeToMapNode.convert(n)); - this.mapNodesDataSource.set(mapNodes); + if (nodes) { + const mapNodes = nodes.map(n => this.nodeToMapNode.convert(n)); + this.mapNodesDataSource.set(mapNodes); - this.assignDataToLinks(); - this.onDataUpdate(); + this.assignDataToLinks(); + this.onDataUpdate(); + } } public setLinks(links: Link[]) { - const mapLinks = links.map(l => this.linkToMapLink.convert(l)); - this.mapLinksDataSource.set(mapLinks); + if (links) { + const mapLinks = links.map(l => this.linkToMapLink.convert(l)); + this.mapLinksDataSource.set(mapLinks); - this.assignDataToLinks(); - this.onDataUpdate(); + this.assignDataToLinks(); + this.onDataUpdate(); + } } public setDrawings(drawings: Drawing[]) { - const mapDrawings = drawings.map(d => this.drawingToMapDrawing.convert(d)); - this.mapDrawingsDataSource.set(mapDrawings); - - this.onDataUpdate(); + if (drawings) { + const mapDrawings = drawings.map(d => this.drawingToMapDrawing.convert(d)); + this.mapDrawingsDataSource.set(mapDrawings); + + this.onDataUpdate(); + } } public setSymbols(symbols: Symbol[]) { - const mapSymbols = symbols.map(s => this.symbolToMapSymbol.convert(s)); - this.mapSymbolsDataSource.set(mapSymbols); + if (symbols) { + const mapSymbols = symbols.map(s => this.symbolToMapSymbol.convert(s)); + this.mapSymbolsDataSource.set(mapSymbols); + } } public getNodes() { diff --git a/src/app/cartography/models/drawing.ts b/src/app/cartography/models/drawing.ts index 984617c5..e3eda3d6 100644 --- a/src/app/cartography/models/drawing.ts +++ b/src/app/cartography/models/drawing.ts @@ -5,6 +5,7 @@ export class Drawing { project_id: string; rotation: number; svg: string; + locked: boolean; x: number; y: number; z: number; diff --git a/src/app/cartography/models/map/map-drawing.ts b/src/app/cartography/models/map/map-drawing.ts index 685fee93..3510125b 100644 --- a/src/app/cartography/models/map/map-drawing.ts +++ b/src/app/cartography/models/map/map-drawing.ts @@ -6,6 +6,7 @@ export class MapDrawing implements Indexed { projectId: string; rotation: number; svg: string; + locked: boolean; x: number; y: number; z: number; diff --git a/src/app/cartography/models/map/map-node.ts b/src/app/cartography/models/map/map-node.ts index ae08ccf7..fda84783 100644 --- a/src/app/cartography/models/map/map-node.ts +++ b/src/app/cartography/models/map/map-node.ts @@ -12,6 +12,7 @@ export class MapNode implements Indexed { firstPortName: string; height: number; label: MapLabel; + locked: boolean; name: string; nodeDirectory: string; nodeType: string; diff --git a/src/app/cartography/models/node.ts b/src/app/cartography/models/node.ts index 5caf995f..c5960ae2 100644 --- a/src/app/cartography/models/node.ts +++ b/src/app/cartography/models/node.ts @@ -1,15 +1,75 @@ import { Label } from './label'; import { Port } from '../../models/port'; +import { CustomAdapter } from '../../models/qemu/qemu-custom-adapter'; + +export class PortsMapping { + name: string; + interface?: string; + port_number: number; + type?: string; +} + +export class Properties { + adapter_type: string; + adapters: number; + headless: boolean; + linked_clone: boolean; + on_close: string; + ram: number; + nvram: number; + usage: string; + use_any_adapter: boolean; + vmname: string; + ports_mapping: PortsMapping[]; + mappings: any; + bios_image: string; + bios_image_md5sum?: any; + boot_priority: string; + cdrom_image: string; + cdrom_image_md5sum?: any; + cpu_throttling: number; + cpus: number; + hda_disk_image: string; + hda_disk_image_md5sum: string; + hda_disk_interface: string; + hdb_disk_image: string; + hdb_disk_image_md5sum?: any; + hdb_disk_interface: string; + hdc_disk_image: string; + hdc_disk_image_md5sum?: any; + hdc_disk_interface: string; + hdd_disk_image: string; + hdd_disk_image_md5sum?: any; + hdd_disk_interface: string; + initrd: string; + initrd_md5sum?: any; + kernel_command_line: string; + kernel_image: string; + kernel_image_md5sum?: any; + legacy_networking: boolean; + mac_address: string; + options: string; + platform: string; + process_priority: string; + qemu_path: string; + environment: string; + extra_hosts: string; +} export class Node { command_line: string; compute_id: string; console: number; + console_auto_start: boolean; console_host: string; console_type: string; + custom_adapters?: CustomAdapter[]; + ethernet_adapters?: any; + serial_adapters?: any; first_port_name: string; height: number; label: Label; + locked: boolean; name: string; node_directory: string; node_id: string; @@ -18,6 +78,7 @@ export class Node { port_segment_size: number; ports: Port[]; project_id: string; + properties: Properties; status: string; symbol: string; symbol_url: string; // @TODO: full URL to symbol, move to MapNode once converters are moved to app module diff --git a/src/app/cartography/widgets/interface-status.ts b/src/app/cartography/widgets/interface-status.ts index 119ec6b7..1fd969cf 100644 --- a/src/app/cartography/widgets/interface-status.ts +++ b/src/app/cartography/widgets/interface-status.ts @@ -16,16 +16,17 @@ export class InterfaceStatusWidget implements Widget { const link_group = select(this); const link_path = link_group.select('path'); - const start_point: SVGPoint = link_path.node().getPointAtLength(45); - const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 45); - let statuses = []; + if (link_path.node()) { + const start_point: SVGPoint = link_path.node().getPointAtLength(45); + const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 45); - if (link_path.node().getTotalLength() > 2 * 45 + 10) { - statuses = [ - new LinkStatus(start_point.x, start_point.y, l.source.status), - new LinkStatus(end_point.x, end_point.y, l.target.status) - ]; + if (link_path.node().getTotalLength() > 2 * 45 + 10) { + statuses = [ + new LinkStatus(start_point.x, start_point.y, l.source.status), + new LinkStatus(end_point.x, end_point.y, l.target.status) + ]; + } } const status_started = link_group diff --git a/src/app/cartography/widgets/link.ts b/src/app/cartography/widgets/link.ts index 5874574a..55d2d60c 100644 --- a/src/app/cartography/widgets/link.ts +++ b/src/app/cartography/widgets/link.ts @@ -20,7 +20,9 @@ export class LinkWidget implements Widget { private multiLinkCalculatorHelper: MultiLinkCalculatorHelper, private interfaceLabelWidget: InterfaceLabelWidget, private interfaceStatusWidget: InterfaceStatusWidget, - private selectionManager: SelectionManager + private selectionManager: SelectionManager, + private ethernetLinkWidget: EthernetLinkWidget, + private serialLinkWidget: SerialLinkWidget ) {} public draw(view: SVGSelection) { @@ -88,11 +90,8 @@ export class LinkWidget implements Widget { .attr('height', '48px') .attr("xlink:href", "assets/resources/images/filter.svg"); - const serial_link_widget = new SerialLinkWidget(); - serial_link_widget.draw(link_body_merge); - - const ethernet_link_widget = new EthernetLinkWidget(); - ethernet_link_widget.draw(link_body_merge); + this.serialLinkWidget.draw(link_body_merge); + this.ethernetLinkWidget.draw(link_body_merge); link_body_merge .select('path') diff --git a/src/app/cartography/widgets/links/ethernet-link.ts b/src/app/cartography/widgets/links/ethernet-link.ts index 2638c128..d453b149 100644 --- a/src/app/cartography/widgets/links/ethernet-link.ts +++ b/src/app/cartography/widgets/links/ethernet-link.ts @@ -1,14 +1,19 @@ import { path } from 'd3-path'; - +import { EventEmitter, Injectable } from '@angular/core'; import { Widget } from '../widget'; import { SVGSelection } from '../../models/types'; import { MapLink } from '../../models/map/map-link'; +import { LinkContextMenu } from '../../events/event-source'; class EthernetLinkPath { constructor(public source: [number, number], public target: [number, number]) {} } -export class EthernetLinkWidget implements Widget { +@Injectable() export class EthernetLinkWidget implements Widget { + public onContextMenu = new EventEmitter(); + + constructor() {} + private linktoEthernetLink(link: MapLink) { return new EthernetLinkPath( [link.source.x + link.source.width / 2, link.source.y + link.source.height / 2], @@ -27,9 +32,21 @@ export class EthernetLinkWidget implements Widget { const link_enter = link .enter() .append('path') - .attr('class', 'ethernet_link'); + .attr('class', 'ethernet_link') + .on('contextmenu', (datum) => { + let link: MapLink = datum as unknown as MapLink; + const evt = event; + this.onContextMenu.emit(new LinkContextMenu(evt, link)); + }); - link_enter.attr('stroke', '#000').attr('stroke-width', '2'); + link_enter + .attr('stroke', '#000') + .attr('stroke-width', '2') + .on('contextmenu', (datum) => { + let link: MapLink = datum as unknown as MapLink; + const evt = event; + this.onContextMenu.emit(new LinkContextMenu(evt, link)); + }); const link_merge = link.merge(link_enter); diff --git a/src/app/cartography/widgets/links/serial-link.ts b/src/app/cartography/widgets/links/serial-link.ts index 768ccbc0..cd6d26c9 100644 --- a/src/app/cartography/widgets/links/serial-link.ts +++ b/src/app/cartography/widgets/links/serial-link.ts @@ -3,6 +3,8 @@ import { path } from 'd3-path'; import { Widget } from '../widget'; import { SVGSelection } from '../../models/types'; import { MapLink } from '../../models/map/map-link'; +import { Injectable, EventEmitter } from '@angular/core'; +import { LinkContextMenu } from '../../events/event-source'; class SerialLinkPath { constructor( @@ -13,7 +15,11 @@ class SerialLinkPath { ) {} } -export class SerialLinkWidget implements Widget { +@Injectable() export class SerialLinkWidget implements Widget { + public onContextMenu = new EventEmitter(); + + constructor() {} + private linkToSerialLink(link: MapLink) { const source = { x: link.source.x + link.source.width / 2, @@ -55,7 +61,12 @@ export class SerialLinkWidget implements Widget { const link_enter = link .enter() .append('path') - .attr('class', 'serial_link'); + .attr('class', 'serial_link') + .on('contextmenu', (datum) => { + let link: MapLink = datum as unknown as MapLink; + const evt = event; + this.onContextMenu.emit(new LinkContextMenu(evt, link)); + }); link_enter .attr('stroke', '#B22222') diff --git a/src/app/common/progress/progress.component.spec.ts b/src/app/common/progress/progress.component.spec.ts index e5c8f4bb..735354d2 100644 --- a/src/app/common/progress/progress.component.spec.ts +++ b/src/app/common/progress/progress.component.spec.ts @@ -7,7 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { Router } from '@angular/router'; import { BehaviorSubject, Observable } from 'rxjs'; -class MockedRouter { +export class MockedRouter { events: BehaviorSubject; constructor() { diff --git a/src/app/components/drawings-listeners/drawing-dragged/drawing-dragged.component.spec.ts b/src/app/components/drawings-listeners/drawing-dragged/drawing-dragged.component.spec.ts index 841a1b91..cd6e6684 100644 --- a/src/app/components/drawings-listeners/drawing-dragged/drawing-dragged.component.spec.ts +++ b/src/app/components/drawings-listeners/drawing-dragged/drawing-dragged.component.spec.ts @@ -48,6 +48,7 @@ describe('DrawingDraggedComponent', () => { }; const mapDrawing: MapDrawing = { id: 'sampleId', + locked: false, projectId: 'sampleprojectId', rotation: 0, svg: 'sampleSvg', diff --git a/src/app/components/drawings-listeners/drawing-resized/drawing-resized.component.spec.ts b/src/app/components/drawings-listeners/drawing-resized/drawing-resized.component.spec.ts index bd9796f0..0af53fda 100644 --- a/src/app/components/drawings-listeners/drawing-resized/drawing-resized.component.spec.ts +++ b/src/app/components/drawings-listeners/drawing-resized/drawing-resized.component.spec.ts @@ -51,6 +51,7 @@ describe('DrawingResizedComponent', () => { }; const mapDrawing: MapDrawing = { id: 'sampleId', + locked: false, projectId: 'sampleprojectId', rotation: 0, svg: 'sampleSvg', diff --git a/src/app/components/drawings-listeners/link-created/link-created.component.spec.ts b/src/app/components/drawings-listeners/link-created/link-created.component.spec.ts index 1a8e8642..06547cb8 100644 --- a/src/app/components/drawings-listeners/link-created/link-created.component.spec.ts +++ b/src/app/components/drawings-listeners/link-created/link-created.component.spec.ts @@ -72,6 +72,7 @@ describe('LinkCreatedComponent', () => { firstPortName: 'sampleFirstPortName', height: 0, label: {} as MapLabel, + locked: false, name: 'sampleName', nodeDirectory: 'sampleNodeDirectory', nodeType: 'sampleNodeType', diff --git a/src/app/components/drawings-listeners/node-dragged/node-dragged.component.spec.ts b/src/app/components/drawings-listeners/node-dragged/node-dragged.component.spec.ts index 1965cee6..82b67ff5 100644 --- a/src/app/components/drawings-listeners/node-dragged/node-dragged.component.spec.ts +++ b/src/app/components/drawings-listeners/node-dragged/node-dragged.component.spec.ts @@ -52,6 +52,7 @@ describe('NodeDraggedComponent', () => { firstPortName: 'sampleFirstPortName', height: 0, label: {} as MapLabel, + locked: false, name: 'sampleName', nodeDirectory: 'sampleNodeDirectory', nodeType: 'sampleNodeType', diff --git a/src/app/components/help/help.component.html b/src/app/components/help/help.component.html index 0eda8204..eab6611f 100644 --- a/src/app/components/help/help.component.html +++ b/src/app/components/help/help.component.html @@ -3,6 +3,19 @@
+ + + Useful shortcuts + + + ctrl + + to zoom in + ctrl + - to zoom out + ctrl + 0 to reset zoom + ctrl + a to select all items on map + ctrl + shift + a to deselect all items on map + ctrl + shift + s to go to preferences + + Third party components diff --git a/src/app/components/preferences/built-in/cloud-nodes/cloud-nodes-template-details/cloud-nodes-template-details.component.html b/src/app/components/preferences/built-in/cloud-nodes/cloud-nodes-template-details/cloud-nodes-template-details.component.html index f9aea8f0..41c50b26 100644 --- a/src/app/components/preferences/built-in/cloud-nodes/cloud-nodes-template-details/cloud-nodes-template-details.component.html +++ b/src/app/components/preferences/built-in/cloud-nodes/cloud-nodes-template-details/cloud-nodes-template-details.component.html @@ -82,7 +82,7 @@ placeholder="Ethernet interface" [ngModelOptions]="{standalone: true}" [(ngModel)]="ethernetInterface"> - + {{type}} diff --git a/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.html b/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.html index f798570b..8089383b 100644 --- a/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.html +++ b/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.html @@ -57,69 +57,13 @@ - + Port settings - - - - - - - - - - - - - - - - - - - - - - - -
Port number {{element.port_number}} VLAN {{element.vlan}} Type {{element.type}} EtherType {{element.ethertype}}

- - - - - - - - - - {{type}} - - - - - - - {{type}} - - - - +
diff --git a/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.spec.ts b/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.spec.ts index 108d2f91..3eeef4f5 100644 --- a/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.spec.ts +++ b/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.spec.ts @@ -17,6 +17,7 @@ import { BuiltInTemplatesService } from '../../../../../services/built-in-templa import { EthernetSwitchTemplate } from '../../../../../models/templates/ethernet-switch-template'; import { EthernetSwitchesTemplateDetailsComponent } from './ethernet-switches-template-details.component'; import { BuiltInTemplatesConfigurationService } from '../../../../../services/built-in-templates-configuration.service'; +import { PortsComponent } from '../../../common/ports/ports.component'; export class MockedBuiltInTemplatesService { public getTemplate(server: Server, template_id: string) { @@ -68,6 +69,7 @@ describe('EthernetSwitchesTemplateDetailsComponent', () => { it('should call save template', () => { spyOn(mockedBuiltInTemplatesService, 'saveTemplate').and.returnValue(of({} as EthernetSwitchTemplate)); + component.portsComponent = {ethernetPorts: []} as PortsComponent; component.inputForm.controls['templateName'].setValue('template name'); component.inputForm.controls['defaultName'].setValue('default name'); component.inputForm.controls['symbol'].setValue('symbol'); @@ -102,7 +104,7 @@ describe('EthernetSwitchesTemplateDetailsComponent', () => { expect(mockedBuiltInTemplatesService.saveTemplate).not.toHaveBeenCalled(); }); - it('should call save template when symbol path is empty', () => { + it('should not call save template when symbol path is empty', () => { spyOn(mockedBuiltInTemplatesService, 'saveTemplate').and.returnValue(of({} as EthernetSwitchTemplate)); component.inputForm.controls['templateName'].setValue('template name'); component.inputForm.controls['defaultName'].setValue('default name'); diff --git a/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.ts b/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.ts index f3a33a97..6ffd3415 100644 --- a/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.ts +++ b/src/app/components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnInit, ViewChild } from "@angular/core"; import { ActivatedRoute, Router } from '@angular/router'; import { ServerService } from '../../../../../services/server.service'; import { Server } from '../../../../../models/server'; @@ -6,8 +6,8 @@ import { ToasterService } from '../../../../../services/toaster.service'; import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; import { BuiltInTemplatesService } from '../../../../../services/built-in-templates.service'; import { EthernetSwitchTemplate } from '../../../../../models/templates/ethernet-switch-template'; -import { PortsMappingEntity } from '../../../../../models/ethernetHub/ports-mapping-enity'; import { BuiltInTemplatesConfigurationService } from '../../../../../services/built-in-templates-configuration.service'; +import { PortsComponent } from '../../../common/ports/ports.component'; @Component({ @@ -16,20 +16,13 @@ import { BuiltInTemplatesConfigurationService } from '../../../../../services/bu styleUrls: ['./ethernet-switches-template-details.component.scss', '../../../preferences.component.scss'] }) export class EthernetSwitchesTemplateDetailsComponent implements OnInit { + @ViewChild(PortsComponent, {static: false}) portsComponent: PortsComponent; server: Server; ethernetSwitchTemplate: EthernetSwitchTemplate; inputForm: FormGroup; - ethernetPorts: PortsMappingEntity[] = []; - dataSource: PortsMappingEntity[] = []; - newPort: PortsMappingEntity; - isSymbolSelectionOpened: boolean = false; - categories = []; consoleTypes: string[] = []; - portTypes: string[] = []; - etherTypes: string[] = []; - displayedColumns: string[] = ['port_number', 'vlan', 'type', 'ethertype']; constructor( private route: ActivatedRoute, @@ -45,11 +38,6 @@ export class EthernetSwitchesTemplateDetailsComponent implements OnInit { defaultName: new FormControl('', Validators.required), symbol: new FormControl('', Validators.required) }); - - this.newPort = { - name: '', - port_number: 0, - }; } ngOnInit() { @@ -61,8 +49,6 @@ export class EthernetSwitchesTemplateDetailsComponent implements OnInit { this.getConfiguration(); this.builtInTemplatesService.getTemplate(this.server, template_id).subscribe((ethernetSwitchTemplate: EthernetSwitchTemplate) => { this.ethernetSwitchTemplate = ethernetSwitchTemplate; - this.ethernetPorts = this.ethernetSwitchTemplate.ports_mapping; - this.dataSource = this.ethernetSwitchTemplate.ports_mapping; }); }); } @@ -70,18 +56,6 @@ export class EthernetSwitchesTemplateDetailsComponent implements OnInit { getConfiguration() { this.categories = this.builtInTemplatesConfigurationService.getCategoriesForEthernetSwitches(); this.consoleTypes = this.builtInTemplatesConfigurationService.getConsoleTypesForEthernetSwitches(); - this.portTypes = this.builtInTemplatesConfigurationService.getPortTypesForEthernetSwitches(); - this.etherTypes = this.builtInTemplatesConfigurationService.getEtherTypesForEthernetSwitches(); - } - - onAdd() { - this.ethernetPorts.push(this.newPort); - this.dataSource = [...this.ethernetPorts]; - - this.newPort = { - name: '', - port_number: 0, - }; } goBack() { @@ -92,6 +66,7 @@ export class EthernetSwitchesTemplateDetailsComponent implements OnInit { if (this.inputForm.invalid) { this.toasterService.error(`Fill all required fields`); } else { + this.ethernetSwitchTemplate.ports_mapping = this.portsComponent.ethernetPorts; this.builtInTemplatesService.saveTemplate(this.server, this.ethernetSwitchTemplate).subscribe((ethernetSwitchTemplate: EthernetSwitchTemplate) => { this.toasterService.success("Changes saved"); }); diff --git a/src/app/components/preferences/common/custom-adapters-table/custom-adapters-table.component.html b/src/app/components/preferences/common/custom-adapters-table/custom-adapters-table.component.html new file mode 100644 index 00000000..67e35ebe --- /dev/null +++ b/src/app/components/preferences/common/custom-adapters-table/custom-adapters-table.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + +
Adapter number Adapter {{element.adapter_number}} Port name Ethernet {{element.adapter_number}} Adapter type + + + {{type}} + + + Actions + +
+ diff --git a/src/app/components/preferences/common/custom-adapters-table/custom-adapters-table.component.ts b/src/app/components/preferences/common/custom-adapters-table/custom-adapters-table.component.ts new file mode 100644 index 00000000..f9ad5452 --- /dev/null +++ b/src/app/components/preferences/common/custom-adapters-table/custom-adapters-table.component.ts @@ -0,0 +1,28 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { CustomAdapter } from '../../../../models/qemu/qemu-custom-adapter'; + + +@Component({ + selector: 'app-custom-adapters-table', + templateUrl: './custom-adapters-table.component.html', + styleUrls: ['../../preferences.component.scss'] +}) +export class CustomAdaptersTableComponent { + @Input() networkTypes = []; + @Input() displayedColumns: string[] = []; + @Input() adapters: CustomAdapter[] = []; + + public numberOfAdapters: number; + + onAdd() { + let adapter: CustomAdapter = { + adapter_number: this.adapters.length, + adapter_type: this.networkTypes[0] + } + this.adapters = this.adapters.concat([adapter]); + } + + delete(adapter: CustomAdapter) { + this.adapters = this.adapters.filter(elem => elem!== adapter); + } +} diff --git a/src/app/components/preferences/common/custom-adapters/custom-adapters.component.html b/src/app/components/preferences/common/custom-adapters/custom-adapters.component.html index 29b52942..53e1855b 100644 --- a/src/app/components/preferences/common/custom-adapters/custom-adapters.component.html +++ b/src/app/components/preferences/common/custom-adapters/custom-adapters.component.html @@ -6,31 +6,12 @@
- - - - - - - - - - - - - - - - - - -
Adapter number Adapter {{element.adapter_number}} Port name Ethernet {{element.adapter_number}} Adapter type - - - {{type[1]}} ({{type[0]}}) - - -
+
diff --git a/src/app/components/preferences/common/custom-adapters/custom-adapters.component.spec.ts b/src/app/components/preferences/common/custom-adapters/custom-adapters.component.spec.ts index 2ea2b4ca..08551de4 100644 --- a/src/app/components/preferences/common/custom-adapters/custom-adapters.component.spec.ts +++ b/src/app/components/preferences/common/custom-adapters/custom-adapters.component.spec.ts @@ -4,6 +4,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { CustomAdaptersComponent } from './custom-adapters.component'; +import { CustomAdaptersTableComponent } from '../custom-adapters-table/custom-adapters-table.component'; describe('Custom adapters component', () => { let component: CustomAdaptersComponent; @@ -31,6 +32,7 @@ describe('Custom adapters component', () => { it('should emit event when apply clicked', () => { spyOn(component.saveConfigurationEmitter, 'emit'); + component.customAdapters = {adapters: []} as CustomAdaptersTableComponent; component.configureCustomAdapters(); diff --git a/src/app/components/preferences/common/custom-adapters/custom-adapters.component.ts b/src/app/components/preferences/common/custom-adapters/custom-adapters.component.ts index 75aeb750..cdc00db9 100644 --- a/src/app/components/preferences/common/custom-adapters/custom-adapters.component.ts +++ b/src/app/components/preferences/common/custom-adapters/custom-adapters.component.ts @@ -1,5 +1,6 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { Component, Input, Output, EventEmitter, ViewChild } from '@angular/core'; import { CustomAdapter } from '../../../../models/qemu/qemu-custom-adapter'; +import { CustomAdaptersTableComponent } from '../custom-adapters-table/custom-adapters-table.component'; @Component({ @@ -13,14 +14,30 @@ export class CustomAdaptersComponent { @Output() closeConfiguratorEmitter = new EventEmitter(); @Output() saveConfigurationEmitter = new EventEmitter(); + @ViewChild("customAdapters", {static: false}) customAdapters: CustomAdaptersTableComponent; + public adapters: CustomAdapter[]; public numberOfAdapters: number; + constructor() { + console.log(this.networkTypes); + } + cancelConfigureCustomAdapters(){ this.closeConfiguratorEmitter.emit(false); } configureCustomAdapters(){ + this.adapters = []; + console.log(this.customAdapters); + + this.customAdapters.adapters.forEach(n => { + this.adapters.push({ + adapter_number: n.adapter_number, + adapter_type: n.adapter_type + }) + }); + this.saveConfigurationEmitter.emit(this.adapters); } } diff --git a/src/app/components/preferences/common/ports/ports.component.html b/src/app/components/preferences/common/ports/ports.component.html new file mode 100644 index 00000000..2fcc2f00 --- /dev/null +++ b/src/app/components/preferences/common/ports/ports.component.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Port number {{element.port_number}} VLAN {{element.vlan}} Type {{element.type}} EtherType {{element.ethertype}} Actions + +

+ + + + + + + + + + {{type}} + + + + + + + {{type}} + + + + diff --git a/src/app/components/preferences/common/ports/ports.component.scss b/src/app/components/preferences/common/ports/ports.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/preferences/common/ports/ports.component.spec.ts b/src/app/components/preferences/common/ports/ports.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/preferences/common/ports/ports.component.ts b/src/app/components/preferences/common/ports/ports.component.ts new file mode 100644 index 00000000..efc2d393 --- /dev/null +++ b/src/app/components/preferences/common/ports/ports.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; +import { Server } from '../../../../models/server'; +import { PortsMappingEntity } from '../../../../models/ethernetHub/ports-mapping-enity'; +import { BuiltInTemplatesConfigurationService } from '../../../../services/built-in-templates-configuration.service'; + + +@Component({ + selector: 'app-ports', + templateUrl: './ports.component.html', + styleUrls: ['../../preferences.component.scss'] +}) +export class PortsComponent implements OnInit { + @Input() ethernetPorts: PortsMappingEntity[] = []; + newPort: PortsMappingEntity = { + name: '', + port_number: 0, + }; + + portTypes: string[] = []; + etherTypes: string[] = []; + displayedColumns: string[] = ['port_number', 'vlan', 'type', 'ethertype', 'action']; + + constructor( + private builtInTemplatesConfigurationService: BuiltInTemplatesConfigurationService + ) {} + + ngOnInit() { + this.getConfiguration(); + } + + getConfiguration() { + this.etherTypes = this.builtInTemplatesConfigurationService.getEtherTypesForEthernetSwitches(); + this.portTypes = this.builtInTemplatesConfigurationService.getPortTypesForEthernetSwitches(); + } + + onAdd() { + this.ethernetPorts.push(this.newPort); + + this.newPort = { + name: '', + port_number: 0, + }; + } + + delete(port: PortsMappingEntity) { + this.ethernetPorts = this.ethernetPorts.filter(n => n !== port); + } +} diff --git a/src/app/components/preferences/common/symbols/symbols.component.html b/src/app/components/preferences/common/symbols/symbols.component.html index 0b2858b7..01c65e22 100644 --- a/src/app/components/preferences/common/symbols/symbols.component.html +++ b/src/app/components/preferences/common/symbols/symbols.component.html @@ -1,7 +1,31 @@ -

+ +
+ + + +
-
+
diff --git a/src/app/components/preferences/common/symbols/symbols.component.scss b/src/app/components/preferences/common/symbols/symbols.component.scss index 432de757..6a9cb194 100644 --- a/src/app/components/preferences/common/symbols/symbols.component.scss +++ b/src/app/components/preferences/common/symbols/symbols.component.scss @@ -8,6 +8,11 @@ outline: none; } +.menu { + display: flex; + justify-content: space-between; +} + .button { background: border-box; border-width: 0px; @@ -36,3 +41,19 @@ grid-row-gap: 3em; grid-column-gap: 1em; } + +.radio-selection { + width: 90%; +} + +.mat-radio-button ~ .mat-radio-button { + margin-left: 16px; +} + +.non-visible { + display: none; +} + +.example-full-width { + width: 100%; +} diff --git a/src/app/components/preferences/common/symbols/symbols.component.spec.ts b/src/app/components/preferences/common/symbols/symbols.component.spec.ts index 5646d491..a5ab9986 100644 --- a/src/app/components/preferences/common/symbols/symbols.component.spec.ts +++ b/src/app/components/preferences/common/symbols/symbols.component.spec.ts @@ -14,6 +14,10 @@ export class MockedSymbolService { public list() { return of([]); } + + public raw() { + return of('') + } } describe('Symbols component', () => { diff --git a/src/app/components/preferences/common/symbols/symbols.component.ts b/src/app/components/preferences/common/symbols/symbols.component.ts index f33407d3..c6932a09 100644 --- a/src/app/components/preferences/common/symbols/symbols.component.ts +++ b/src/app/components/preferences/common/symbols/symbols.component.ts @@ -15,6 +15,7 @@ export class SymbolsComponent implements OnInit { @Output() symbolChanged = new EventEmitter(); symbols: Symbol[] = []; + filteredSymbols: Symbol[] = []; isSelected: string = ''; searchText: string = ''; @@ -24,14 +25,55 @@ export class SymbolsComponent implements OnInit { ngOnInit() { this.isSelected = this.symbol; + this.loadSymbols(); + } - this.symbolService.list(this.server).subscribe((symbols: Symbol[]) => { - this.symbols = symbols; - }); + setFilter(filter: string) { + if (filter === 'all') { + this.filteredSymbols = this.symbols; + } else if (filter === 'builtin') { + this.filteredSymbols = this.symbols.filter(elem => elem.builtin); + } else { + this.filteredSymbols = this.symbols.filter(elem => !elem.builtin); + } } setSelected(symbol_id: string) { this.isSelected = symbol_id; this.symbolChanged.emit(this.isSelected); } + + loadSymbols() { + this.symbolService.list(this.server).subscribe((symbols: Symbol[]) => { + this.symbols = symbols; + this.filteredSymbols = symbols; + }); + } + + public uploadSymbolFile(event) { + this.readSymbolFile(event.target); + } + + private readSymbolFile(symbolInput) { + let file: File = symbolInput.files[0]; + let fileName = symbolInput.files[0].name; + let fileReader: FileReader = new FileReader(); + let imageToUpload = new Image(); + + fileReader.onloadend = () => { + let image = fileReader.result; + let svg = this.createSvgFileForImage(image, imageToUpload); + this.symbolService.add(this.server, fileName, svg).subscribe(() => { + this.loadSymbols(); + }); + } + + imageToUpload.onload = () => { fileReader.readAsDataURL(file) }; + imageToUpload.src = window.URL.createObjectURL(file); + } + + private createSvgFileForImage(image: string|ArrayBuffer, imageToUpload: HTMLImageElement) { + return `\n\n` + } } diff --git a/src/app/components/preferences/common/udp-tunnels/udp-tunnels.component.html b/src/app/components/preferences/common/udp-tunnels/udp-tunnels.component.html new file mode 100644 index 00000000..ac5a49be --- /dev/null +++ b/src/app/components/preferences/common/udp-tunnels/udp-tunnels.component.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name {{element.name}} Local port {{element.rport}} Type {{element.rhost}} Remote port {{element.lport}} Actions + +
+
+ + + + + + + + + + + + + diff --git a/src/app/components/preferences/common/udp-tunnels/udp-tunnels.component.ts b/src/app/components/preferences/common/udp-tunnels/udp-tunnels.component.ts new file mode 100644 index 00000000..6052b28c --- /dev/null +++ b/src/app/components/preferences/common/udp-tunnels/udp-tunnels.component.ts @@ -0,0 +1,47 @@ +import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; +import { Server } from '../../../../models/server'; +import { PortsMappingEntity } from '../../../../models/ethernetHub/ports-mapping-enity'; +import { BuiltInTemplatesConfigurationService } from '../../../../services/built-in-templates-configuration.service'; + + +@Component({ + selector: 'app-udp-tunnels', + templateUrl: './udp-tunnels.component.html', + styleUrls: ['../../preferences.component.scss'] +}) +export class UdpTunnelsComponent implements OnInit { + @Input() dataSourceUdp: PortsMappingEntity[] = []; + displayedColumns: string[] = ['name', 'lport', 'rhost', 'rport', 'action']; + newPort: PortsMappingEntity = { + name: '', + port_number: 0, + }; + portTypes: string[] = []; + etherTypes: string[] = []; + + constructor( + private builtInTemplatesConfigurationService: BuiltInTemplatesConfigurationService + ) {} + + ngOnInit() { + this.getConfiguration(); + } + + getConfiguration() { + this.etherTypes = this.builtInTemplatesConfigurationService.getEtherTypesForEthernetSwitches(); + this.portTypes = this.builtInTemplatesConfigurationService.getPortTypesForEthernetSwitches(); + } + + onAddUdpInterface() { + this.dataSourceUdp = this.dataSourceUdp.concat([this.newPort]); + + this.newPort = { + name: '', + port_number: 0, + }; + } + + delete(port: PortsMappingEntity) { + this.dataSourceUdp = this.dataSourceUdp.filter(n => n !== port); + } +} diff --git a/src/app/components/preferences/preferences.component.html b/src/app/components/preferences/preferences.component.html index 7a23d808..3f4246a7 100644 --- a/src/app/components/preferences/preferences.component.html +++ b/src/app/components/preferences/preferences.component.html @@ -31,6 +31,9 @@ Docker +
diff --git a/src/app/components/preferences/qemu/qemu-vm-template-details/qemu-vm-template-details.component.ts b/src/app/components/preferences/qemu/qemu-vm-template-details/qemu-vm-template-details.component.ts index 0d26ab07..32caa4d9 100644 --- a/src/app/components/preferences/qemu/qemu-vm-template-details/qemu-vm-template-details.component.ts +++ b/src/app/components/preferences/qemu/qemu-vm-template-details/qemu-vm-template-details.component.ts @@ -31,7 +31,7 @@ export class QemuVmTemplateDetailsComponent implements OnInit { binaries: QemuBinary[] = []; activateCpuThrottling: boolean = true; isConfiguratorOpened: boolean = false; - displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type']; + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; generalSettingsForm: FormGroup; @ViewChild("customAdaptersConfigurator", {static: false}) diff --git a/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.html b/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.html new file mode 100644 index 00000000..96359baa --- /dev/null +++ b/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.html @@ -0,0 +1,23 @@ +
+
+
+

New VPCS node template

+
+
+
+ +
+ + + + + + +
+
+
+ + +
+
+
diff --git a/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.scss b/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.ts b/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.ts new file mode 100644 index 00000000..70fd2a10 --- /dev/null +++ b/src/app/components/preferences/traceng/add-traceng/add-traceng-template.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit } from "@angular/core"; +import { Server } from '../../../../models/server'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ServerService } from '../../../../services/server.service'; +import { ToasterService } from '../../../../services/toaster.service'; +import { v4 as uuid } from 'uuid'; +import { TemplateMocksService } from '../../../../services/template-mocks.service'; +import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; +import { TracengService } from '../../../../services/traceng.service'; +import { TracengTemplate } from '../../../../models/templates/traceng-template'; + + +@Component({ + selector: 'app-add-traceng-template', + templateUrl: './add-traceng-template.component.html', + styleUrls: ['./add-traceng-template.component.scss', '../../preferences.component.scss'] +}) +export class AddTracengTemplateComponent implements OnInit { + server: Server; + templateName: string = ''; + ipAddress: string = ''; + templateNameForm: FormGroup + + constructor( + private route: ActivatedRoute, + private serverService: ServerService, + private tracengService: TracengService, + private router: Router, + private toasterService: ToasterService, + private templateMocksService: TemplateMocksService, + private formBuilder: FormBuilder + ) { + this.templateNameForm = this.formBuilder.group({ + templateName: new FormControl(null, [Validators.required]), + ipAddress: new FormControl(null, [Validators.required]) + }); + } + + ngOnInit() { + const server_id = this.route.snapshot.paramMap.get("server_id"); + this.serverService.get(parseInt(server_id, 10)).then((server: Server) => { + this.server = server; + }); + } + + goBack() { + this.router.navigate(['/server', this.server.id, 'preferences', 'traceng', 'templates']); + } + + addTemplate() { + if (!this.templateNameForm.invalid) { + this.templateName = this.templateNameForm.get('templateName').value; + this.ipAddress = this.templateNameForm.get('ipAddress').value; + let tracengTemplate: TracengTemplate = this.templateMocksService.getTracengTemplate(); + + tracengTemplate.template_id = uuid(); + tracengTemplate.name = this.templateName; + tracengTemplate.ip_address = this.ipAddress; + + this.tracengService.addTemplate(this.server, tracengTemplate).subscribe(() => { + this.goBack(); + }); + } else { + this.toasterService.error(`Fill all required fields`); + } + } +} diff --git a/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.html b/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.html new file mode 100644 index 00000000..4e94dbb2 --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.html @@ -0,0 +1,12 @@ +
+
+
+

TraceNG preferences

+
+
+
+ + + +
+
diff --git a/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.scss b/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.scss new file mode 100644 index 00000000..9c2173c2 --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.scss @@ -0,0 +1,3 @@ +.form-field { + width: 100%; +} diff --git a/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.ts b/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.ts new file mode 100644 index 00000000..a698d8af --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-preferences/traceng-preferences.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute } from '@angular/router'; +import { Server } from '../../../../models/server'; +import { ServerService } from '../../../../services/server.service'; + + +@Component({ + selector: 'app-traceng-preferences', + templateUrl: './traceng-preferences.component.html', + styleUrls: ['./traceng-preferences.component.scss'] +}) +export class TracengPreferencesComponent implements OnInit { + server: Server; + tracengExecutable: string; + + constructor( + private route: ActivatedRoute, + private serverService: ServerService + ) {} + + ngOnInit() { + const server_id = this.route.snapshot.paramMap.get("server_id"); + + this.serverService.get(parseInt(server_id, 10)).then((server: Server) => { + this.server = server; + }); + } + + restoreDefaults(){ + this.tracengExecutable = ''; + } +} diff --git a/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.html b/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.html new file mode 100644 index 00000000..01e1acdc --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.html @@ -0,0 +1,40 @@ +
+
+
+

TraceNG device configuration

+
+
+
+ +
+ + + + + + + + + +

+
+
+
+ + +
+
+
+ diff --git a/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.scss b/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.ts b/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.ts new file mode 100644 index 00000000..3042bfce --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-template-details/traceng-template-details.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from '@angular/router'; +import { ServerService } from '../../../../services/server.service'; +import { Server } from '../../../../models/server'; +import { ToasterService } from '../../../../services/toaster.service'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { TracengService } from '../../../../services/traceng.service'; +import { TracengTemplate } from '../../../../models/templates/traceng-template'; + + +@Component({ + selector: 'app-traceng-template-details', + templateUrl: './traceng-template-details.component.html', + styleUrls: ['./traceng-template-details.component.scss', '../../preferences.component.scss'] +}) +export class TracengTemplateDetailsComponent implements OnInit { + server: Server; + tracengTemplate: TracengTemplate; + inputForm: FormGroup; + isSymbolSelectionOpened: boolean = false; + + constructor( + private route: ActivatedRoute, + private serverService: ServerService, + private tracengService: TracengService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private router: Router + ) { + this.inputForm = this.formBuilder.group({ + templateName: new FormControl('', Validators.required), + defaultName: new FormControl('', Validators.required), + symbol: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + const server_id = this.route.snapshot.paramMap.get("server_id"); + const template_id = this.route.snapshot.paramMap.get("template_id"); + this.serverService.get(parseInt(server_id, 10)).then((server: Server) => { + this.server = server; + + this.tracengService.getTemplate(this.server, template_id).subscribe((tracengTemplate: TracengTemplate) => { + this.tracengTemplate = tracengTemplate; + }); + }); + } + + goBack() { + this.router.navigate(['/server', this.server.id, 'preferences', 'traceng', 'templates']); + } + + onSave() { + if (this.inputForm.invalid) { + this.toasterService.error(`Fill all required fields`); + } else { + this.tracengService.saveTemplate(this.server, this.tracengTemplate).subscribe((tracengTemplate: TracengTemplate) => { + this.toasterService.success("Changes saved"); + }); + } + } + + chooseSymbol() { + this.isSymbolSelectionOpened = !this.isSymbolSelectionOpened; + } + + symbolChanged(chosenSymbol: string) { + this.isSymbolSelectionOpened = !this.isSymbolSelectionOpened; + this.tracengTemplate.symbol = chosenSymbol; + } +} diff --git a/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.html b/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.html new file mode 100644 index 00000000..af922d9f --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.html @@ -0,0 +1,32 @@ +
+
+
+

TraceNG node templates

+ + +
+
+ +
+
+ +
+ {{template.name}} + + + + +
+
+
+
+
+ + diff --git a/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.scss b/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.ts b/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.ts new file mode 100644 index 00000000..b640193c --- /dev/null +++ b/src/app/components/preferences/traceng/traceng-templates/traceng-templates.component.ts @@ -0,0 +1,46 @@ +import { Component, OnInit, ViewChild } from "@angular/core"; +import { Server } from '../../../../models/server'; +import { ActivatedRoute } from '@angular/router'; +import { ServerService } from '../../../../services/server.service'; +import { DeleteTemplateComponent } from '../../common/delete-template-component/delete-template.component'; +import { TracengTemplate } from '../../../../models/templates/traceng-template'; +import { TracengService } from '../../../../services/traceng.service'; + +@Component({ + selector: 'app-traceng-templates', + templateUrl: './traceng-templates.component.html', + styleUrls: ['./traceng-templates.component.scss', '../../preferences.component.scss'] +}) +export class TracengTemplatesComponent implements OnInit { + server: Server; + tracengTemplates: TracengTemplate[] = []; + @ViewChild(DeleteTemplateComponent, {static: false}) deleteComponent: DeleteTemplateComponent; + + constructor( + private route: ActivatedRoute, + private serverService: ServerService, + private tracengService: TracengService + ) {} + + ngOnInit() { + const server_id = this.route.snapshot.paramMap.get("server_id"); + this.serverService.get(parseInt(server_id, 10)).then((server: Server) => { + this.server = server; + this.getTemplates(); + }); + } + + getTemplates() { + this.tracengService.getTemplates(this.server).subscribe((tracengTemplates: TracengTemplate[]) => { + this.tracengTemplates = tracengTemplates.filter((elem) => elem.template_type === 'traceng' && !elem.builtin); + }); + } + + deleteTemplate(template: TracengTemplate) { + this.deleteComponent.deleteItem(template.name, template.template_id); + } + + onDeleteEvent() { + this.getTemplates(); + } +} diff --git a/src/app/components/preferences/virtual-box/virtual-box-template-details/virtual-box-template-details.component.ts b/src/app/components/preferences/virtual-box/virtual-box-template-details/virtual-box-template-details.component.ts index 38c06674..d4469e15 100644 --- a/src/app/components/preferences/virtual-box/virtual-box-template-details/virtual-box-template-details.component.ts +++ b/src/app/components/preferences/virtual-box/virtual-box-template-details/virtual-box-template-details.component.ts @@ -24,7 +24,7 @@ export class VirtualBoxTemplateDetailsComponent implements OnInit { onCloseOptions = []; categories = []; networkTypes = []; - displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type']; + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; isConfiguratorOpened: boolean = false; generalSettingsForm: FormGroup; networkForm: FormGroup diff --git a/src/app/components/preferences/vmware/vmware-template-details/vmware-template-details.component.ts b/src/app/components/preferences/vmware/vmware-template-details/vmware-template-details.component.ts index 724331dd..00bd2215 100644 --- a/src/app/components/preferences/vmware/vmware-template-details/vmware-template-details.component.ts +++ b/src/app/components/preferences/vmware/vmware-template-details/vmware-template-details.component.ts @@ -20,7 +20,7 @@ export class VmwareTemplateDetailsComponent implements OnInit { server: Server; vmwareTemplate: VmwareTemplate; generalSettingsForm: FormGroup; - displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type']; + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; isConfiguratorOpened: boolean = false; isSymbolSelectionOpened: boolean = false; consoleTypes: string[] = []; diff --git a/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.html b/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.html new file mode 100644 index 00000000..8f1b40bf --- /dev/null +++ b/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.html @@ -0,0 +1,12 @@ +

Change symbol for node: {{node.name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.scss b/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.scss new file mode 100644 index 00000000..6deef2c4 --- /dev/null +++ b/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.scss @@ -0,0 +1,19 @@ +.symbolsWrapper { + height: 350px; + overflow-y: scroll; + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +::-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; +} diff --git a/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.ts b/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.ts new file mode 100644 index 00000000..6fbd4bdb --- /dev/null +++ b/src/app/components/project-map/change-symbol-dialog/change-symbol-dialog.component.ts @@ -0,0 +1,41 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; +import { Message } from '../../../models/message'; +import { Server } from '../../../models/server'; +import { Node } from '../../../cartography/models/node'; +import { Symbol } from '../../../models/symbol'; +import { NodeService } from '../../../services/node.service'; + +@Component({ + selector: 'app-change-symbol-dialog', + templateUrl: './change-symbol-dialog.component.html', + styleUrls: ['./change-symbol-dialog.component.scss'] +}) +export class ChangeSymbolDialogComponent implements OnInit { + @Input() server: Server; + @Input() node: Node; + symbol: string; + + constructor( + public dialogRef: MatDialogRef, + private nodeService: NodeService + ) {} + + ngOnInit() { + this.symbol = this.node.symbol; + } + + symbolChanged(chosenSymbol: string) { + this.symbol = chosenSymbol; + } + + onCloseClick() { + this.dialogRef.close(); + } + + onSelectClick() { + this.nodeService.updateSymbol(this.server, this.node, this.symbol).subscribe(() => { + this.onCloseClick() + }); + } +} diff --git a/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.html b/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.html new file mode 100644 index 00000000..2a3518ab --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.spec.ts new file mode 100644 index 00000000..74a264f5 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.spec.ts @@ -0,0 +1,66 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { BringToFrontActionComponent } from './bring-to-front-action.component'; +import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule } from '@angular/material'; +import { CommonModule } from '@angular/common'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { MockedDrawingService, MockedDrawingsDataSource, MockedNodesDataSource, MockedNodeService } from '../../../project-map.component.spec'; +import { DrawingService } from '../../../../../services/drawing.service'; +import { NodesDataSource } from '../../../../../cartography/datasources/nodes-datasource'; +import { DrawingsDataSource } from '../../../../../cartography/datasources/drawings-datasource'; +import { NodeService } from '../../../../../services/node.service'; +import { Node } from '../../../../../cartography/models/node'; +import { of } from 'rxjs'; +import { ComponentFactoryResolver } from '@angular/core'; +import { Drawing } from '../../../../../cartography/models/drawing'; + +describe('BringToFrontActionComponent', () => { + let component: BringToFrontActionComponent; + let fixture: ComponentFixture; + let drawingService = new MockedDrawingService(); + let drawingsDataSource = new MockedDrawingsDataSource(); + let nodeService = new MockedNodeService(); + let nodesDataSource = new MockedNodesDataSource(); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, CommonModule, NoopAnimationsModule], + providers: [ + { provide: DrawingService, useValue: drawingService }, + { provide: DrawingsDataSource, useValue: drawingsDataSource }, + { provide: NodeService, useValue: nodeService }, + { provide: NodesDataSource, useValue: nodesDataSource }, + ], + declarations: [BringToFrontActionComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BringToFrontActionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call node service when bring to front action called', () => { + spyOn(nodeService, 'update').and.returnValue(of()); + component.nodes = [{z: 0} as Node]; + component.drawings = []; + + component.bringToFront(); + + expect(nodeService.update).toHaveBeenCalled(); + }); + + it('should call drawing service when bring to front action called', () => { + spyOn(drawingService, 'update').and.returnValue(of()); + component.nodes = []; + component.drawings = [{z: 0} as Drawing]; + + component.bringToFront(); + + expect(drawingService.update).toHaveBeenCalled(); + }); +}); diff --git a/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.ts b/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.ts new file mode 100644 index 00000000..b72848fb --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/bring-to-front-action/bring-to-front-action.component.ts @@ -0,0 +1,43 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { Node } from '../../../../../cartography/models/node'; +import { NodesDataSource } from '../../../../../cartography/datasources/nodes-datasource'; +import { NodeService } from '../../../../../services/node.service'; +import { Drawing } from '../../../../../cartography/models/drawing'; +import { DrawingsDataSource } from '../../../../../cartography/datasources/drawings-datasource'; +import { DrawingService } from '../../../../../services/drawing.service'; + +@Component({ + selector: 'app-bring-to-front-action', + templateUrl: './bring-to-front-action.component.html' +}) +export class BringToFrontActionComponent implements OnInit { + @Input() server: Server; + @Input() nodes: Node[]; + @Input() drawings: Drawing[]; + + constructor( + private nodesDataSource: NodesDataSource, + private drawingsDataSource: DrawingsDataSource, + private nodeService: NodeService, + private drawingService: DrawingService + ) {} + + ngOnInit() {} + + bringToFront() { + this.nodes.forEach((node) => { + node.z = 100; + this.nodesDataSource.update(node); + + this.nodeService.update(this.server, node).subscribe((node: Node) => {}); + }); + + this.drawings.forEach((drawing) => { + drawing.z = 100; + this.drawingsDataSource.update(drawing); + + this.drawingService.update(this.server, drawing).subscribe((drawing: Drawing) => {}); + }); + } +} diff --git a/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.html b/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.html new file mode 100644 index 00000000..71a7851e --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.ts b/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.ts new file mode 100644 index 00000000..af4f8d67 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/change-symbol/change-symbol-action.component.ts @@ -0,0 +1,29 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { Node } from '../../../../../cartography/models/node'; +import { MatDialog } from '@angular/material'; +import { ChangeSymbolDialogComponent } from '../../../change-symbol-dialog/change-symbol-dialog.component'; + +@Component({ + selector: 'app-change-symbol-action', + templateUrl: './change-symbol-action.component.html' +}) +export class ChangeSymbolActionComponent implements OnInit { + @Input() server: Server; + @Input() node: Node; + + constructor(private dialog: MatDialog) {} + + ngOnInit() {} + + changeSymbol() { + const dialogRef = this.dialog.open(ChangeSymbolDialogComponent, { + width: '1000px', + height: '500px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + instance.node = this.node; + } +} diff --git a/src/app/components/project-map/context-menu/actions/config-action/config-action.component.html b/src/app/components/project-map/context-menu/actions/config-action/config-action.component.html new file mode 100644 index 00000000..3290ba3a --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/config-action/config-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/config-action/config-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/config-action/config-action.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/context-menu/actions/config-action/config-action.component.ts b/src/app/components/project-map/context-menu/actions/config-action/config-action.component.ts new file mode 100644 index 00000000..f732f445 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/config-action/config-action.component.ts @@ -0,0 +1,71 @@ +import { Component, Input, OnInit, OnChanges } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { Node } from '../../../../../cartography/models/node'; +import { MatDialog, MatDialogRef } from '@angular/material'; +import { ConfiguratorDialogVpcsComponent } from '../../../node-editors/configurator/vpcs/configurator-vpcs.component'; +import { ConfiguratorDialogEthernetHubComponent } from '../../../node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component'; +import { ConfiguratorDialogEthernetSwitchComponent } from '../../../node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component'; +import { ConfiguratorDialogSwitchComponent } from '../../../node-editors/configurator/switch/configurator-switch.component'; +import { ConfiguratorDialogVirtualBoxComponent } from '../../../node-editors/configurator/virtualbox/configurator-virtualbox.component'; +import { ConfiguratorDialogQemuComponent } from '../../../node-editors/configurator/qemu/configurator-qemu.component'; +import { ConfiguratorDialogCloudComponent } from '../../../node-editors/configurator/cloud/configurator-cloud.component'; +import { ConfiguratorDialogAtmSwitchComponent } from '../../../node-editors/configurator/atm_switch/configurator-atm-switch.component'; +import { ConfiguratorDialogVmwareComponent } from '../../../node-editors/configurator/vmware/configurator-vmware.component'; +import { ConfiguratorDialogIouComponent } from '../../../node-editors/configurator/iou/configurator-iou.component'; +import { ConfiguratorDialogIosComponent } from '../../../node-editors/configurator/ios/configurator-ios.component'; +import { ConfiguratorDialogDockerComponent } from '../../../node-editors/configurator/docker/configurator-docker.component'; +import { ConfiguratorDialogNatComponent } from '../../../node-editors/configurator/nat/configurator-nat.component'; +import { ConfiguratorDialogTracengComponent } from '../../../node-editors/configurator/traceng/configurator-traceng.component'; + + +@Component({ + selector: 'app-config-node-action', + templateUrl: './config-action.component.html' +}) +export class ConfigActionComponent { + @Input() server: Server; + @Input() node: Node; + private conf = { + autoFocus: false, + width: '800px' + }; + dialogRef; + + constructor(private dialog: MatDialog) {} + + configureNode() { + if (this.node.node_type === 'vpcs') { + this.dialogRef = this.dialog.open(ConfiguratorDialogVpcsComponent, this.conf); + } else if (this.node.node_type === 'ethernet_hub') { + this.dialogRef = this.dialog.open(ConfiguratorDialogEthernetHubComponent, this.conf); + } else if (this.node.node_type === 'ethernet_switch') { + this.dialogRef = this.dialog.open(ConfiguratorDialogEthernetSwitchComponent, this.conf); + } else if (this.node.node_type === 'cloud') { + this.dialogRef = this.dialog.open(ConfiguratorDialogCloudComponent, this.conf); + } else if (this.node.node_type === 'dynamips') { + this.dialogRef = this.dialog.open(ConfiguratorDialogIosComponent, this.conf); + } else if (this.node.node_type === 'iou') { + this.dialogRef = this.dialog.open(ConfiguratorDialogIouComponent, this.conf); + } else if (this.node.node_type === 'qemu') { + this.dialogRef = this.dialog.open(ConfiguratorDialogQemuComponent, this.conf); + } else if (this.node.node_type === 'virtualbox') { + this.dialogRef = this.dialog.open(ConfiguratorDialogVirtualBoxComponent, this.conf); + } else if (this.node.node_type === 'vmware') { + this.dialogRef = this.dialog.open(ConfiguratorDialogVmwareComponent, this.conf); + } else if (this.node.node_type === 'docker') { + this.dialogRef = this.dialog.open(ConfiguratorDialogDockerComponent, this.conf); + } else if (this.node.node_type === 'nat') { + this.dialogRef = this.dialog.open(ConfiguratorDialogNatComponent, this.conf); + } else if (this.node.node_type === 'frame_relay_switch') { + this.dialogRef = this.dialog.open(ConfiguratorDialogSwitchComponent, this.conf); + } else if (this.node.node_type === 'atm_switch') { + this.dialogRef = this.dialog.open(ConfiguratorDialogAtmSwitchComponent, this.conf); + } else if (this.node.node_type === 'traceng') { + this.dialogRef = this.dialog.open(ConfiguratorDialogTracengComponent, this.conf); + } + + let instance = this.dialogRef.componentInstance; + instance.server = this.server; + instance.node = this.node; + } +} diff --git a/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.html b/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.html new file mode 100644 index 00000000..48f51b4f --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.spec.ts b/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.ts b/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.ts new file mode 100644 index 00000000..62bd7420 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/console-device-action-browser/console-device-action-browser.component.ts @@ -0,0 +1,43 @@ +import { Component, Input } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { NodeService } from '../../../../../services/node.service'; +import { Server } from '../../../../../models/server'; + + +@Component({ + selector: 'app-console-device-action-browser', + templateUrl: './console-device-action-browser.component.html' +}) +export class ConsoleDeviceActionBrowserComponent { + @Input() server: Server; + @Input() node: Node; + + constructor( + private toasterService: ToasterService, + private nodeService: NodeService + ) {} + + openConsole() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.startConsole(); + }); + } + + startConsole() { + if (this.node.status !== "started") { + this.toasterService.error("This node must be started before a console can be opened"); + } else { + if (this.node.console_type === "telnet") { + location.assign(`gns3+telnet://${this.node.console_host}:${this.node.console}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`); + } else if (this.node.console_type === "vnc") { + location.assign(`gns3+vnc://${this.node.console_host}:${this.node.console}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`); + } else if(this.node.console_type === "spice") { + location.assign(`gns3+spice://${this.node.console_host}:${this.node.console}?name=${this.node.name}&project_id=${this.node.project_id}&node_id=${this.node.node_id}`); + } else { + this.toasterService.error("Supported console types: telnet, vnc, spice."); + } + } + } +} diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts index 5176c979..0a9a52ba 100644 --- a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.spec.ts @@ -11,6 +11,8 @@ import { SettingsService } from '../../../../../services/settings.service'; import { MockedSettingsService } from '../../../../../services/settings.service.spec'; import { Node } from '../../../../../cartography/models/node'; import { Server } from '../../../../../models/server'; +import { MockedNodeService } from '../../../project-map.component.spec'; +import { NodeService } from '../../../../../services/node.service'; describe('ConsoleDeviceActionComponent', () => { @@ -20,7 +22,8 @@ describe('ConsoleDeviceActionComponent', () => { let server: Server; let mockedSettingsService: MockedSettingsService; let mockedServerService: MockedServerService; - let mockedToaster: MockedToasterService + let mockedToaster: MockedToasterService; + let mockedNodeService: MockedNodeService = new MockedNodeService(); beforeEach(() => { electronService = { @@ -47,7 +50,8 @@ describe('ConsoleDeviceActionComponent', () => { { provide: ElectronService, useValue: electronService }, { provide: ServerService, useValue: mockedServerService }, { provide: SettingsService, useValue: mockedSettingsService }, - { provide: ToasterService, useValue: mockedToaster } + { provide: ToasterService, useValue: mockedToaster }, + { provide: NodeService, useValue: mockedNodeService } ], imports: [ MatIconModule diff --git a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts index 03496aec..b4c0a39b 100644 --- a/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/console-device-action/console-device-action.component.ts @@ -5,6 +5,7 @@ import { ElectronService } from 'ngx-electron'; import { ServerService } from '../../../../../services/server.service'; import { SettingsService } from '../../../../../services/settings.service'; import { ToasterService } from '../../../../../services/toaster.service'; +import { NodeService } from '../../../../../services/node.service'; @Component({ selector: 'app-console-device-action', @@ -18,19 +19,14 @@ export class ConsoleDeviceActionComponent implements OnInit { private electronService: ElectronService, private serverService: ServerService, private settingsService: SettingsService, - private toasterService: ToasterService + private toasterService: ToasterService, + private nodeService: NodeService ) { } - ngOnInit() { - } + ngOnInit() {} async console() { - let consoleCommand = this.settingsService.get('console_command'); - - if(consoleCommand === undefined) { - consoleCommand = `putty.exe -telnet \%h \%p -wt \"\%d\" -gns3 5 -skin 4`; - } - + let consoleCommand = this.settingsService.get('console_command') ? this.settingsService.get('console_command') : this.nodeService.getDefaultCommand(); const startedNodes = this.nodes.filter(node => node.status === 'started'); if(startedNodes.length === 0) { diff --git a/src/app/components/project-map/context-menu/actions/duplicate-action/duplicate-action.component.ts b/src/app/components/project-map/context-menu/actions/duplicate-action/duplicate-action.component.ts index 9abedd3f..ac055ffe 100644 --- a/src/app/components/project-map/context-menu/actions/duplicate-action/duplicate-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/duplicate-action/duplicate-action.component.ts @@ -45,7 +45,9 @@ export class DuplicateActionComponent { }) } - runningNodes = runningNodes.substring(0, runningNodes.length-2); - this.toasterService.error(`Cannot duplicate node data for nodes: ${runningNodes}`); + if (runningNodes.length > 0) { + runningNodes = runningNodes.substring(0, runningNodes.length-2); + this.toasterService.error(`Cannot duplicate node data for nodes: ${runningNodes}`); + } } } diff --git a/src/app/components/project-map/context-menu/actions/edit-config/edit-config-action.component.html b/src/app/components/project-map/context-menu/actions/edit-config/edit-config-action.component.html new file mode 100644 index 00000000..d3dc8ef0 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/edit-config/edit-config-action.component.html @@ -0,0 +1,6 @@ + diff --git a/src/app/components/project-map/context-menu/actions/edit-config/edit-config-action.component.ts b/src/app/components/project-map/context-menu/actions/edit-config/edit-config-action.component.ts new file mode 100644 index 00000000..de87c525 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/edit-config/edit-config-action.component.ts @@ -0,0 +1,30 @@ +import { Component, Input } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { Project } from '../../../../../models/project'; +import { Server } from '../../../../../models/server'; +import { ConfigEditorDialogComponent } from '../../../node-editors/config-editor/config-editor.component'; +import { MatDialog } from '@angular/material'; + +@Component({ + selector: 'app-edit-config-action', + templateUrl: './edit-config-action.component.html' +}) +export class EditConfigActionComponent { + @Input() server: Server; + @Input() project: Project; + @Input() node: Node; + + constructor(private dialog: MatDialog) {} + + editConfig() { + const dialogRef = this.dialog.open(ConfigEditorDialogComponent, { + width: '600px', + height: '500px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + instance.project = this.project; + instance.node = this.node; + } +} diff --git a/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts b/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts index ea7e8cdf..9a6d04a2 100644 --- a/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts @@ -20,7 +20,7 @@ export class EditStyleActionComponent implements OnInit { editStyle() { const dialogRef = this.dialog.open(StyleEditorDialogComponent, { - width: '300px', + width: '800px', autoFocus: false }); let instance = dialogRef.componentInstance; diff --git a/src/app/components/project-map/context-menu/actions/export-config/export-config-action.component.html b/src/app/components/project-map/context-menu/actions/export-config/export-config-action.component.html new file mode 100644 index 00000000..70843579 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/export-config/export-config-action.component.html @@ -0,0 +1,6 @@ + diff --git a/src/app/components/project-map/context-menu/actions/export-config/export-config-action.component.ts b/src/app/components/project-map/context-menu/actions/export-config/export-config-action.component.ts new file mode 100644 index 00000000..204a6ee1 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/export-config/export-config-action.component.ts @@ -0,0 +1,37 @@ +import { Component, Input } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { NodeService } from '../../../../../services/node.service'; +import { Server } from '../../../../../models/server'; + +@Component({ + selector: 'app-export-config-action', + templateUrl: './export-config-action.component.html' +}) +export class ExportConfigActionComponent { + @Input() server: Server; + @Input() node: Node; + + constructor( + private nodeService: NodeService + ) {} + + exportConfig() { + this.nodeService.getStartupConfiguration(this.server, this.node).subscribe((config: any) => { + this.downloadByHtmlTag(config); + }); + } + + private downloadByHtmlTag(config: string) { + const element = document.createElement('a'); + const fileType = 'text/plain'; + element.setAttribute('href', `data:${fileType};charset=utf-8,${encodeURIComponent(config)}`); + if (this.node.node_type === 'vpcs') { + element.setAttribute('download', `${this.node.name}_startup.vpc`); + } else if (this.node.node_type === 'iou' || this.node.node_type === 'dynamips') { + element.setAttribute('download', `${this.node.name}_startup.cfg`); + } + + var event = new MouseEvent("click"); + element.dispatchEvent(event); + } +} diff --git a/src/app/components/project-map/context-menu/actions/import-config/import-config-action.component.html b/src/app/components/project-map/context-menu/actions/import-config/import-config-action.component.html new file mode 100644 index 00000000..a8cc1358 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/import-config/import-config-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/import-config/import-config-action.component.ts b/src/app/components/project-map/context-menu/actions/import-config/import-config-action.component.ts new file mode 100644 index 00000000..5adba173 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/import-config/import-config-action.component.ts @@ -0,0 +1,19 @@ +import { Component, Input } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { NodeService } from '../../../../../services/node.service'; +import { Server } from '../../../../../models/server'; + +@Component({ + selector: 'app-import-config-action', + templateUrl: './import-config-action.component.html' +}) +export class ImportConfigActionComponent { + @Input() server: Server; + @Input() node: Node; + + constructor() {} + + importConfig() { + //needs implementation + } +} diff --git a/src/app/components/project-map/context-menu/actions/lock-action/lock-action.component.html b/src/app/components/project-map/context-menu/actions/lock-action/lock-action.component.html new file mode 100644 index 00000000..451af511 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/lock-action/lock-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/lock-action/lock-action.component.ts b/src/app/components/project-map/context-menu/actions/lock-action/lock-action.component.ts new file mode 100644 index 00000000..8557917e --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/lock-action/lock-action.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, Input, OnDestroy, OnChanges } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { Node } from '../../../../../cartography/models/node'; +import { NodesDataSource } from '../../../../../cartography/datasources/nodes-datasource'; +import { NodeService } from '../../../../../services/node.service'; +import { Drawing } from '../../../../../cartography/models/drawing'; +import { DrawingsDataSource } from '../../../../../cartography/datasources/drawings-datasource'; +import { DrawingService } from '../../../../../services/drawing.service'; + +@Component({ + selector: 'app-lock-action', + templateUrl: './lock-action.component.html' +}) +export class LockActionComponent implements OnChanges { + @Input() server: Server; + @Input() nodes: Node[]; + @Input() drawings: Drawing[]; + command: string; + + constructor( + private nodesDataSource: NodesDataSource, + private drawingsDataSource: DrawingsDataSource, + private nodeService: NodeService, + private drawingService: DrawingService + ) {} + + ngOnChanges() { + if (this.nodes.length === 1 && this.drawings.length === 0) { + this.command = this.nodes[0].locked ? 'Unlock item' : 'Lock item'; + } else if (this.nodes.length === 0 && this.drawings.length === 1) { + this.command = this.drawings[0].locked ? 'Unlock item' : 'Lock item'; + } else { + this.command = 'Lock/unlock items'; + } + } + + lock() { + this.nodes.forEach((node) => { + node.locked = !node.locked; + this.nodeService.updateNode(this.server, node).subscribe((node) => { this.nodesDataSource.update(node) }); + }); + + this.drawings.forEach((drawing) => { + drawing.locked = ! drawing.locked; + this.drawingService.update(this.server, drawing).subscribe((drawing) => { this.drawingsDataSource.update(drawing) }); + }); + } +} diff --git a/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.html b/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.html new file mode 100644 index 00000000..f8884c90 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.ts b/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.ts new file mode 100644 index 00000000..7cee91cc --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/reload-node-action/reload-node-action.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, OnInit, OnChanges } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { Node } from '../../../../../cartography/models/node'; + +@Component({ + selector: 'app-reload-node-action', + templateUrl: './reload-node-action.component.html' +}) +export class ReloadNodeActionComponent implements OnInit { + @Input() server: Server; + @Input() nodes: Node[]; + + filteredNodes: Node[] = []; + + constructor(private nodeService: NodeService) {} + + ngOnInit() { + this.nodes.forEach((node) => { + if (node.node_type === 'vpcs' || node.node_type === 'qemu' || node.node_type === 'virtualbox' || node.node_type === 'vmware') { + this.filteredNodes.push(node); + } + }); + } + + reloadNodes() { + this.filteredNodes.forEach((node) => { + this.nodeService.reload(this.server, node).subscribe((n: Node) => {}); + }); + } +} diff --git a/src/app/components/project-map/context-menu/actions/show-node-action/show-node-action.component.html b/src/app/components/project-map/context-menu/actions/show-node-action/show-node-action.component.html new file mode 100644 index 00000000..b92a0071 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/show-node-action/show-node-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/show-node-action/show-node-action.component.ts b/src/app/components/project-map/context-menu/actions/show-node-action/show-node-action.component.ts new file mode 100644 index 00000000..3f70d051 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/show-node-action/show-node-action.component.ts @@ -0,0 +1,26 @@ +import { Component, Input, OnInit, OnChanges } from '@angular/core'; +import { Node } from '../../../../../cartography/models/node'; +import { MatDialog } from '@angular/material'; +import { InfoDialogComponent } from '../../../info-dialog/info-dialog.component'; +import { Server } from '../../../../../models/server'; + +@Component({ + selector: 'app-show-node-action', + templateUrl: './show-node-action.component.html' +}) +export class ShowNodeActionComponent { + @Input() node: Node; + @Input() server: Server + + constructor(private dialog: MatDialog) {} + + showNode() { + const dialogRef = this.dialog.open(InfoDialogComponent, { + width: '600px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.node = this.node; + instance.server = this.server; + } +} diff --git a/src/app/components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component.html b/src/app/components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component.html new file mode 100644 index 00000000..532b38ab --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component.ts b/src/app/components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component.ts new file mode 100644 index 00000000..b7fcd6a3 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/start-capture-on-started-link/start-capture-on-started-link.component.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { Link } from '../../../../../models/link'; +import { Project } from '../../../../../models/project'; +import { PacketCaptureService } from '../../../../../services/packet-capture.service'; + +@Component({ + selector: 'app-start-capture-on-started-link-action', + templateUrl: './start-capture-on-started-link.component.html' +}) +export class StartCaptureOnStartedLinkActionComponent { + @Input() server: Server; + @Input() project: Project; + @Input() link: Link; + + constructor( + private packetCaptureService: PacketCaptureService + ) {} + + startCapture() { + var splittedFileName = this.link.capture_file_name.split('.'); + this.packetCaptureService.startCapture(this.server, this.project, this.link, splittedFileName[0]); + } +} diff --git a/src/app/components/project-map/context-menu/actions/start-capture/start-capture-action.component.ts b/src/app/components/project-map/context-menu/actions/start-capture/start-capture-action.component.ts index 726e42bf..091b86cb 100644 --- a/src/app/components/project-map/context-menu/actions/start-capture/start-capture-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/start-capture/start-capture-action.component.ts @@ -3,6 +3,7 @@ import { Server } from '../../../../../models/server'; import { Link } from '../../../../../models/link'; import { MatDialog } from '@angular/material'; import { StartCaptureDialogComponent } from '../../../packet-capturing/start-capture/start-capture.component'; +import { Project } from '../../../../../models/project'; @Component({ selector: 'app-start-capture-action', @@ -10,6 +11,7 @@ import { StartCaptureDialogComponent } from '../../../packet-capturing/start-cap }) export class StartCaptureActionComponent { @Input() server: Server; + @Input() project: Project; @Input() link: Link; constructor(private dialog: MatDialog) {} @@ -21,6 +23,7 @@ export class StartCaptureActionComponent { }); let instance = dialogRef.componentInstance; instance.server = this.server; + instance.project = this.project; instance.link = this.link; } } diff --git a/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.html b/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.html new file mode 100644 index 00000000..de0b01c3 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.html @@ -0,0 +1,4 @@ + diff --git a/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.spec.ts b/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.ts b/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.ts new file mode 100644 index 00000000..17688b62 --- /dev/null +++ b/src/app/components/project-map/context-menu/actions/suspend-node-action/suspend-node-action.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, OnInit, OnChanges } from '@angular/core'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { Node } from '../../../../../cartography/models/node'; + +@Component({ + selector: 'app-suspend-node-action', + templateUrl: './suspend-node-action.component.html' +}) +export class SuspendNodeActionComponent implements OnInit, OnChanges { + @Input() server: Server; + @Input() nodes: Node[]; + isNodeWithStartedStatus: boolean; + + constructor(private nodeService: NodeService) {} + + ngOnInit() { + } + + ngOnChanges(changes) { + if(changes.nodes) { + this.isNodeWithStartedStatus = false; + this.nodes.forEach((node) => { + if (node.status === 'started') { + this.isNodeWithStartedStatus = true; + } + }); + } + } + + suspendNodes() { + this.nodes.forEach((node) => { + this.nodeService.suspend(this.server, node).subscribe((n: 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 2ebfeb6a..49904db0 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 @@ -1,13 +1,35 @@
+ + + + + + + + + + + + {{node.name}} + + + +
+ +
diff --git a/src/app/components/project-map/info-dialog/info-dialog.component.scss b/src/app/components/project-map/info-dialog/info-dialog.component.scss new file mode 100644 index 00000000..e28aaee8 --- /dev/null +++ b/src/app/components/project-map/info-dialog/info-dialog.component.scss @@ -0,0 +1,3 @@ +.textBox { + margin-top: 10px; +} diff --git a/src/app/components/project-map/info-dialog/info-dialog.component.ts b/src/app/components/project-map/info-dialog/info-dialog.component.ts new file mode 100644 index 00000000..11968f47 --- /dev/null +++ b/src/app/components/project-map/info-dialog/info-dialog.component.ts @@ -0,0 +1,31 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; +import { Node } from '../../../cartography/models/node'; +import { InfoService } from '../../../services/info.service'; +import { Server } from '../../../models/server'; + +@Component({ + selector: 'app-info-dialog', + templateUrl: './info-dialog.component.html', + styleUrls: ['./info-dialog.component.scss'] +}) +export class InfoDialogComponent implements OnInit { + @Input() server: Server; + @Input() node: Node; + infoList: string[] = []; + commandLine: string = ''; + + constructor( + public dialogRef: MatDialogRef, + private infoService: InfoService + ) {} + + ngOnInit() { + this.infoList = this.infoService.getInfoAboutNode(this.node, this.server); + this.commandLine = this.infoService.getCommandLine(this.node); + } + + onCloseClick() { + this.dialogRef.close(); + } +} 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..0d3b16f8 --- /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..faf2ce52 --- /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, console {node name}, 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..22c9091f --- /dev/null +++ b/src/app/components/project-map/log-console/log-console.component.ts @@ -0,0 +1,311 @@ +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 (.*?)$/; + private regexConsole: RegExp = /^console (.*?)$/; + + 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: string = ''; + + if (node.label) { + message = `Event received: ${event.action} - ${this.printNode(node)}.`; + } else { + message = `Event received: ${event.action} - ${node.name}.`; + } + + 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.warningSubscription = this.projectWebServiceHandler.warningNotificationEmitter.subscribe((message) => { + this.showMessage({ + type: 'warning', + message: message + }); + }); + this.infoSubscription = 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(filter: string) { + this.selectedFilter = filter; + this.filteredEvents = this.getFilteredEvents(); + } + + onKeyDown(event) { + if (event.key === "Enter") { + this.handleCommand(); + } + } + + handleCommand() { + if (this.command === 'help' || this.command === '') { + this.showCommand("Available commands: help, version, console {node name}, 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) || this.regexConsole.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.regexConsole.test(this.command)) { + if (node.status === 'started') { + this.showCommand(`Launching console for node ${splittedCommand[1]}...`); + if (node.console_type === "telnet") { + location.assign(`gns3+telnet://${node.console_host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}`); + } else if (node.console_type === "vnc") { + location.assign(`gns3+vnc://${node.console_host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}`); + } else if(node.console_type === "spice") { + location.assign(`gns3+spice://${node.console_host}:${node.console}?name=${node.name}&project_id=${node.project_id}&node_id=${node.node_id}`); + } else { + this.showCommand("Supported console types: telnet, vnc, spice."); + } + } else { + this.showCommand(`This node must be started before a console can be opened.`); + } + } + 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/node-editors/config-editor/config-editor.component.html b/src/app/components/project-map/node-editors/config-editor/config-editor.component.html new file mode 100644 index 00000000..d4795b4e --- /dev/null +++ b/src/app/components/project-map/node-editors/config-editor/config-editor.component.html @@ -0,0 +1,19 @@ +

Configuration for node {{node.name}}

+ + + + + + + + + + + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/config-editor/config-editor.component.scss b/src/app/components/project-map/node-editors/config-editor/config-editor.component.scss new file mode 100644 index 00000000..6010a550 --- /dev/null +++ b/src/app/components/project-map/node-editors/config-editor/config-editor.component.scss @@ -0,0 +1,9 @@ +.textArea { + width: 100%; + height: 350px; +} + +.textAreaTab { + width: 100%; + height: 300px; +} diff --git a/src/app/components/project-map/node-editors/config-editor/config-editor.component.spec.ts b/src/app/components/project-map/node-editors/config-editor/config-editor.component.spec.ts new file mode 100644 index 00000000..146fa465 --- /dev/null +++ b/src/app/components/project-map/node-editors/config-editor/config-editor.component.spec.ts @@ -0,0 +1,88 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Server } from '../../../../models/server'; +import { + MatDialogModule, + MatFormFieldModule, + MatDialogRef, + MAT_DIALOG_DATA, + MatSnackBarModule, + MatTabsModule +} from '@angular/material'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { ToasterService } from '../../../../services/toaster.service'; +import { of } from 'rxjs/internal/observable/of'; +import { ConfigEditorDialogComponent } from './config-editor.component'; +import { NodeService } from '../../../../services/node.service'; +import { FormsModule } from '@angular/forms'; +import { MockedNodeService } from '../../project-map.component.spec'; +import { Node } from '../../../../cartography/models/node'; + +describe('ConfigEditorDialogComponent', () => { + let component: ConfigEditorDialogComponent; + let fixture: ComponentFixture; + let server: Server; + let node: Node; + let toaster = { + success: jasmine.createSpy('success') + }; + let dialogRef = { + close: jasmine.createSpy('close') + }; + let mockedNodeService: MockedNodeService = new MockedNodeService(); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatDialogModule, + MatFormFieldModule, + NoopAnimationsModule, + MatSnackBarModule, + FormsModule, + MatTabsModule + ], + providers: [ + { provide: MatDialogRef, useValue: dialogRef }, + { provide: MAT_DIALOG_DATA }, + { provide: NodeService, useValue: mockedNodeService }, + { provide: ToasterService, useValue: toaster } + ], + declarations: [ConfigEditorDialogComponent] + }).compileComponents(); + + server = new Server(); + server.host = 'localhost'; + server.port = 80; + + node = new Node(); + node.name = 'sample name'; + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfigEditorDialogComponent); + component = fixture.componentInstance; + component.server = server; + component.node = node; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(fixture).toBeDefined(); + expect(component).toBeTruthy(); + }); + + it('should call node service when save configuration chosen', () => { + spyOn(mockedNodeService, 'saveConfiguration').and.returnValue(of('sample config')); + + component.onSaveClick(); + + expect(mockedNodeService.saveConfiguration).toHaveBeenCalled(); + }); + + it('should not call node service when save configuration chosen', () => { + spyOn(mockedNodeService, 'saveConfiguration').and.returnValue(of('sample config')); + + component.onCancelClick(); + + expect(mockedNodeService.saveConfiguration).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/components/project-map/node-editors/config-editor/config-editor.component.ts b/src/app/components/project-map/node-editors/config-editor/config-editor.component.ts new file mode 100644 index 00000000..2a5d25d2 --- /dev/null +++ b/src/app/components/project-map/node-editors/config-editor/config-editor.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from '@angular/core'; +import { Node } from '../../../../cartography/models/node'; +import { Project } from '../../../../models/project'; +import { Server } from '../../../../models/server'; +import { MatDialogRef } from '@angular/material'; +import { NodeService } from '../../../../services/node.service'; +import { ToasterService } from '../../../../services/toaster.service'; + +@Component({ + selector: 'app-config-editor', + templateUrl: './config-editor.component.html', + styleUrls: ['./config-editor.component.scss'] +}) +export class ConfigEditorDialogComponent implements OnInit { + server: Server; + project: Project; + node: Node; + + config: any; + privateConfig: any; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService + ) {} + + ngOnInit() { + this.nodeService.getStartupConfiguration(this.server, this.node).subscribe((config: any) => { + this.config = config; + }); + + if (this.node.node_type === 'iou' || this.node.node_type === 'dynamips') { + this.nodeService.getPrivateConfiguration(this.server, this.node).subscribe((privateConfig: any) => { + this.privateConfig = privateConfig; + }); + } + } + + onSaveClick() { + this.nodeService.saveConfiguration(this.server, this.node, this.config).subscribe((response) => { + if (this.node.node_type === 'iou' || this.node.node_type === 'dynamips') { + this.nodeService.savePrivateConfiguration(this.server, this.node, this.privateConfig).subscribe((resp) => { + this.dialogRef.close(); + this.toasterService.success(`Configuration for node ${this.node.name} saved.`); + }); + } else { + this.dialogRef.close(); + this.toasterService.success(`Configuration for node ${this.node.name} saved.`); + } + }); + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component.html b/src/app/components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component.html new file mode 100644 index 00000000..26836351 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component.html @@ -0,0 +1,109 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component.ts b/src/app/components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component.ts new file mode 100644 index 00000000..98114c56 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/atm_switch/configurator-atm-switch.component.ts @@ -0,0 +1,164 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; + + +@Component({ + selector: 'app-configurator-atm-switch', + templateUrl: './configurator-atm-switch.component.html', + styleUrls: ['../configurator.component.scss', '../../../../preferences/preferences.component.scss'] +}) +export class ConfiguratorDialogAtmSwitchComponent implements OnInit { + server: Server; + node: Node; + name: string; + nameForm: FormGroup; + inputForm: FormGroup; + abstractForm: FormGroup; + consoleTypes: string[] = []; + + nodeMappings = new Map(); + nodeMappingsDataSource: NodeMapping[] = []; + dataSource = []; + displayedColumns = ['portIn', 'portOut', 'actions'] + + sourcePort: string = ''; + sourceVpi: string = ''; + sourceVci: string = ''; + destinationPort: string = ''; + destinationVpi: string = ''; + destinationVci: string = ''; + + useVpiOnly: boolean = false; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder + ) { + this.nameForm = this.formBuilder.group({ + name: new FormControl('', Validators.required), + }); + + this.inputForm = this.formBuilder.group({ + sourcePort: new FormControl('', Validators.required), + sourceVci: new FormControl('', Validators.required), + destinationPort: new FormControl('', Validators.required), + destinationVci: new FormControl('', Validators.required), + }); + + this.abstractForm = this.formBuilder.group({ + sourceVpi: new FormControl('', Validators.required), + destinationVpi: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + + let mappings = node.properties.mappings; + Object.keys(mappings).forEach(key => { + this.nodeMappings.set(key, mappings[key]); + }); + + this.nodeMappings.forEach((value: string, key: string) => { + this.nodeMappingsDataSource.push({ + portIn: key, + portOut: value + }); + }); + }); + } + + delete(elem: NodeMapping) { + this.nodeMappingsDataSource = this.nodeMappingsDataSource.filter(n => n !== elem); + } + + add() { + if (this.inputForm.valid) { + let nodeMapping: NodeMapping; + if (!this.useVpiOnly) { + if (this.abstractForm.valid) { + nodeMapping = { + portIn: `${this.sourcePort}:${this.sourceVpi}:${this.sourceVci}`, + portOut: `${this.destinationPort}:${this.destinationVpi}:${this.destinationVci}` + }; + + if (this.nodeMappingsDataSource.filter(n => n.portIn === nodeMapping.portIn).length > 0) { + this.toasterService.error('Mapping already defined.'); + } else { + this.nodeMappingsDataSource = this.nodeMappingsDataSource.concat([nodeMapping]); + this.clearUserInput(); + } + } else { + this.toasterService.error('Fill all required fields.'); + } + } else { + nodeMapping = { + portIn: `${this.sourcePort}:${this.sourceVci}`, + portOut: `${this.destinationPort}:${this.destinationVci}` + }; + + if (this.nodeMappingsDataSource.filter(n => n.portIn === nodeMapping.portIn).length > 0) { + this.toasterService.error('Mapping already defined.'); + } else { + this.nodeMappingsDataSource = this.nodeMappingsDataSource.concat([nodeMapping]); + this.clearUserInput(); + } + } + } else { + this.toasterService.error('Fill all required fields.'); + } + } + + clearUserInput() { + this.sourcePort = '0'; + this.sourceVpi = '0'; + this.sourceVci = '0'; + this.destinationPort = '0'; + this.destinationVpi = '0'; + this.sourceVci = '0'; + } + + strMapToObj(strMap) { + let obj = Object.create(null); + for (let [k,v] of strMap) { + obj[k] = v; + } + return obj; + } + + onSaveClick() { + if (this.nameForm.valid) { + this.nodeMappings.clear(); + this.nodeMappingsDataSource.forEach(elem => { + this.nodeMappings.set(elem.portIn, elem.portOut); + }); + + this.node.properties.mappings = Array.from(this.nodeMappings).reduce((obj, [key, value]) => (Object.assign(obj, { [key]: value })), {}); + + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} + +export interface NodeMapping { + portIn: string, + portOut: string +} diff --git a/src/app/components/project-map/node-editors/configurator/cloud/configurator-cloud.component.html b/src/app/components/project-map/node-editors/configurator/cloud/configurator-cloud.component.html new file mode 100644 index 00000000..d7618fda --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/cloud/configurator-cloud.component.html @@ -0,0 +1,89 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/cloud/configurator-cloud.component.ts b/src/app/components/project-map/node-editors/configurator/cloud/configurator-cloud.component.ts new file mode 100644 index 00000000..7b30aaca --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/cloud/configurator-cloud.component.ts @@ -0,0 +1,105 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { CustomAdaptersTableComponent } from '../../../../../components/preferences/common/custom-adapters-table/custom-adapters-table.component'; +import { QemuBinary } from '../../../../../models/qemu/qemu-binary'; +import { BuiltInTemplatesConfigurationService } from '../../../../../services/built-in-templates-configuration.service'; +import { PortsMappingEntity } from '../../../../../models/ethernetHub/ports-mapping-enity'; +import { UdpTunnelsComponent } from '../../../../../components/preferences/common/udp-tunnels/udp-tunnels.component'; + + +@Component({ + selector: 'app-configurator-cloud', + templateUrl: './configurator-cloud.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogCloudComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + consoleTypes: string[] = []; + binaries: QemuBinary[] = []; + onCloseOptions = []; + bootPriorities = []; + diskInterfaces: string[] = []; + + portsMappingEthernet: PortsMappingEntity[] = []; + portsMappingTap: PortsMappingEntity[] = []; + portsMappingUdp: PortsMappingEntity[] = []; + + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; + networkTypes = []; + tapInterface: string = ''; + ethernetInterface: string = ''; + ethernetInterfaces: string[] = ['Ethernet 2', 'Ethernet 3']; + + @ViewChild("udpTunnels", {static: false}) udpTunnels: UdpTunnelsComponent; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private builtInTemplatesConfigurationService: BuiltInTemplatesConfigurationService, + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + + this.portsMappingEthernet = this.node.properties.ports_mapping + .filter((elem) => elem.type === 'ethernet'); + + this.portsMappingTap = this.node.properties.ports_mapping + .filter((elem) => elem.type === 'tap'); + + this.portsMappingUdp = this.node.properties.ports_mapping + .filter((elem) => elem.type === 'udp'); + }) + } + + getConfiguration() { + this.consoleTypes = this.builtInTemplatesConfigurationService.getConsoleTypesForCloudNodes(); + } + + onAddTapInterface() { + if (this.tapInterface) { + this.portsMappingTap.push({ + interface: this.tapInterface, + name: this.tapInterface, + port_number: 0, + type: "tap" + }); + } + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.portsMappingUdp = this.udpTunnels.dataSourceUdp; + + this.node.properties.ports_mapping = this.portsMappingUdp.concat(this.portsMappingEthernet).concat(this.portsMappingTap); + + this.nodeService. updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/configurator.component.scss b/src/app/components/project-map/node-editors/configurator/configurator.component.scss new file mode 100644 index 00000000..7bae870f --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/configurator.component.scss @@ -0,0 +1,52 @@ +.form-field { + width: 100%; +} + +.configButton { + width: 100%; +} + +.select { + width: 100%; +} + +.default-content { + max-height: 400px; + overflow-y: scroll; + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +.file-button { + width: 18%; +} + +.create-button { + width: 100%; +} + +.file-name-form-field { + padding-left: 2%; + width: 80%; +} + +.nonvisible { + display: none; +} + +mat-radio-button { + margin-right: 10px; +} + +::-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; +} diff --git a/src/app/components/project-map/node-editors/configurator/docker/configurator-docker.component.html b/src/app/components/project-map/node-editors/configurator/docker/configurator-docker.component.html new file mode 100644 index 00000000..296cee21 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/docker/configurator-docker.component.html @@ -0,0 +1,54 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/docker/configurator-docker.component.ts b/src/app/components/project-map/node-editors/configurator/docker/configurator-docker.component.ts new file mode 100644 index 00000000..07f25d73 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/docker/configurator-docker.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { DockerConfigurationService } from '../../../../../services/docker-configuration.service'; + + +@Component({ + selector: 'app-configurator-docker', + templateUrl: './configurator-docker.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogDockerComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + consoleTypes: string[] = []; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private dockerConfigurationService: DockerConfigurationService + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required), + adapter: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }) + } + + getConfiguration() { + this.consoleTypes = this.dockerConfigurationService.getConsoleTypes(); + + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component.html b/src/app/components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component.html new file mode 100644 index 00000000..826a0154 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component.html @@ -0,0 +1,35 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component.ts b/src/app/components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component.ts new file mode 100644 index 00000000..484f6138 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/ethernet-switch/configurator-ethernet-switch.component.ts @@ -0,0 +1,64 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { BuiltInTemplatesConfigurationService } from '../../../../../services/built-in-templates-configuration.service'; +import { PortsComponent } from '../../../../../components/preferences/common/ports/ports.component'; + + +@Component({ + selector: 'app-configurator-ethernet-switch', + templateUrl: './configurator-ethernet-switch.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogEthernetSwitchComponent implements OnInit { + @ViewChild(PortsComponent, {static: false}) portsComponent: PortsComponent; + server: Server; + node: Node; + name: string; + inputForm: FormGroup; + consoleTypes: string[] = []; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private ethernetSwitchesConfigurationService: BuiltInTemplatesConfigurationService + ) { + this.inputForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = this.node.name; + this.getConfiguration(); + }) + } + + getConfiguration() { + this.consoleTypes = this.ethernetSwitchesConfigurationService.getConsoleTypesForEthernetSwitches(); + } + + onSaveClick() { + if (this.inputForm.valid) { + this.node.properties.ports_mapping = this.portsComponent.ethernetPorts; + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component.html b/src/app/components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component.html new file mode 100644 index 00000000..13d7838c --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component.html @@ -0,0 +1,31 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component.ts b/src/app/components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component.ts new file mode 100644 index 00000000..fe6d969b --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/ethernet_hub/configurator-ethernet-hub.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { VpcsConfigurationService } from '../../../../../services/vpcs-configuration.service'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; + + +@Component({ + selector: 'app-configurator-ethernet-hub', + templateUrl: './configurator-ethernet-hub.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogEthernetHubComponent implements OnInit { + server: Server; + node: Node; + numberOfPorts: number; + inputForm: FormGroup; + consoleTypes: string[] = []; + categories = []; + name: string; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private vpcsConfigurationService: VpcsConfigurationService + ) { + this.inputForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = this.node.name; + this.numberOfPorts = this.node.ports.length; + this.getConfiguration(); + }) + } + + getConfiguration() { + this.consoleTypes = this.vpcsConfigurationService.getConsoleTypes(); + this.categories = this.vpcsConfigurationService.getCategories(); + } + + onSaveClick() { + if (this.inputForm.valid) { + this.node.properties.ports_mapping = []; + for(let i=0; i { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`) + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/ios/configurator-ios.component.html b/src/app/components/project-map/node-editors/configurator/ios/configurator-ios.component.html new file mode 100644 index 00000000..089da3c8 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/ios/configurator-ios.component.html @@ -0,0 +1,51 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/ios/configurator-ios.component.ts b/src/app/components/project-map/node-editors/configurator/ios/configurator-ios.component.ts new file mode 100644 index 00000000..4af94677 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/ios/configurator-ios.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { IosConfigurationService } from '../../../../../services/ios-configuration.service'; + + +@Component({ + selector: 'app-configurator-ios', + templateUrl: './configurator-ios.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogIosComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + memoryForm: FormGroup; + consoleTypes: string[] = []; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private configurationService: IosConfigurationService + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + + this.memoryForm = this.formBuilder.group({ + ram: new FormControl('', Validators.required), + nvram: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }); + } + + getConfiguration() { + this.consoleTypes = this.configurationService.getConsoleTypes(); + } + + onSaveClick() { + if (this.generalSettingsForm.valid && this.memoryForm.valid) { + this.nodeService. updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/iou/configurator-iou.component.html b/src/app/components/project-map/node-editors/configurator/iou/configurator-iou.component.html new file mode 100644 index 00000000..5f44167e --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/iou/configurator-iou.component.html @@ -0,0 +1,57 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/iou/configurator-iou.component.ts b/src/app/components/project-map/node-editors/configurator/iou/configurator-iou.component.ts new file mode 100644 index 00000000..5f0421c0 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/iou/configurator-iou.component.ts @@ -0,0 +1,67 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { IouConfigurationService } from '../../../../../services/iou-configuration.service'; + + +@Component({ + selector: 'app-configurator-iou', + templateUrl: './configurator-iou.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogIouComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + networkForm: FormGroup; + consoleTypes: string[] = []; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private configurationService: IouConfigurationService + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + + this.networkForm = this.formBuilder.group({ + ethernetAdapters: new FormControl('', Validators.required), + serialAdapters: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }); + } + + getConfiguration() { + this.consoleTypes = this.configurationService.getConsoleTypes(); + } + + onSaveClick() { + if (this.generalSettingsForm.valid && this.networkForm.valid) { + this.nodeService. updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/nat/configurator-nat.component.html b/src/app/components/project-map/node-editors/configurator/nat/configurator-nat.component.html new file mode 100644 index 00000000..ff1164d0 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/nat/configurator-nat.component.html @@ -0,0 +1,24 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/nat/configurator-nat.component.ts b/src/app/components/project-map/node-editors/configurator/nat/configurator-nat.component.ts new file mode 100644 index 00000000..ea7b2ed7 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/nat/configurator-nat.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; + + +@Component({ + selector: 'app-configurator-nat', + templateUrl: './configurator-nat.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogNatComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + }) + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/qemu/configurator-qemu.component.html b/src/app/components/project-map/node-editors/configurator/qemu/configurator-qemu.component.html new file mode 100644 index 00000000..3a90ccf1 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/qemu/configurator-qemu.component.html @@ -0,0 +1,276 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/qemu/configurator-qemu.component.ts b/src/app/components/project-map/node-editors/configurator/qemu/configurator-qemu.component.ts new file mode 100644 index 00000000..939e2ad0 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/qemu/configurator-qemu.component.ts @@ -0,0 +1,131 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef, MatDialog } from '@angular/material'; +import { CustomAdaptersTableComponent } from '../../../../../components/preferences/common/custom-adapters-table/custom-adapters-table.component'; +import { QemuService } from '../../../../../services/qemu.service'; +import { QemuConfigurationService } from '../../../../../services/qemu-configuration.service'; +import { QemuBinary } from '../../../../../models/qemu/qemu-binary'; +import { QemuImageCreatorComponent } from './qemu-image-creator/qemu-image-creator.component'; +import { QemuImage } from '../../../../../models/qemu/qemu-image'; + + +@Component({ + selector: 'app-configurator-qemu', + templateUrl: './configurator-qemu.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogQemuComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + consoleTypes: string[] = []; + binaries: QemuBinary[] = []; + onCloseOptions = []; + bootPriorities = []; + diskInterfaces: string[] = []; + + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; + networkTypes = []; + qemuImages: QemuImage[] = []; + + private conf = { + autoFocus: false, + width: '800px' + }; + dialogRefQemuImageCreator; + + @ViewChild("customAdapters", {static: false}) customAdapters: CustomAdaptersTableComponent; + + constructor( + private dialog: MatDialog, + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private qemuService: QemuService, + private qemuConfigurationService: QemuConfigurationService + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required), + ram: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }) + + this.qemuService.getBinaries(this.server).subscribe((qemuBinaries: QemuBinary[]) => { + this.binaries = qemuBinaries; + }); + + this.qemuService.getImages(this.server).subscribe((qemuImages: QemuImage[]) => { + this.qemuImages = qemuImages; + }); + } + + openQemuImageCreator() { + this.dialogRefQemuImageCreator = this.dialog.open(QemuImageCreatorComponent, this.conf); + let instance = this.dialogRefQemuImageCreator.componentInstance; + instance.server = this.server; + } + + uploadCdromImageFile(event){ + this.node.properties.cdrom_image = event.target.files[0].name; + } + + uploadInitrdFile(event){ + this.node.properties.initrd = event.target.files[0].name; + } + + uploadKernelImageFile(event){ + this.node.properties.kernel_image = event.target.files[0].name; + } + + uploadBiosFile(event){ + this.node.properties.bios_image = event.target.files[0].name; + } + + getConfiguration() { + this.consoleTypes = this.qemuConfigurationService.getConsoleTypes(); + this.onCloseOptions = this.qemuConfigurationService.getOnCloseOptions(); + this.qemuConfigurationService.getNetworkTypes().forEach(n => { + this.networkTypes.push(n[0]); + }); + this.bootPriorities = this.qemuConfigurationService.getBootPriorities(); + this.diskInterfaces = this.qemuConfigurationService.getDiskInterfaces(); + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.node.custom_adapters = []; + this.customAdapters.adapters.forEach(n => { + this.node.custom_adapters.push({ + adapter_number: n.adapter_number, + adapter_type: n.adapter_type + }) + }); + + this.node.properties.adapters = this.node.custom_adapters.length; + + this.nodeService. updateNodeWithCustomAdapters(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component.html b/src/app/components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component.html new file mode 100644 index 00000000..aec8fdd5 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component.html @@ -0,0 +1,107 @@ +

Qemu image configurator

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component.ts b/src/app/components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component.ts new file mode 100644 index 00000000..d5e59ecd --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/qemu/qemu-image-creator/qemu-image-creator.component.ts @@ -0,0 +1,121 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Server } from '../../../../../../models/server'; +import { NodeService } from '../../../../../../services/node.service'; +import { ToasterService } from '../../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { QemuService } from '../../../../../../services/qemu.service'; +import { QemuImg } from '../../../../../../models/qemu/qemu-img'; + + +@Component({ + selector: 'app-qemu-image-creator', + templateUrl: './qemu-image-creator.component.html', + styleUrls: ['../../configurator.component.scss'] +}) +export class QemuImageCreatorComponent implements OnInit { + server: Server; + qemuImg: QemuImg; + + formatOptions: string[] = ['qcow2', 'qcow', 'vhd', 'vdi', 'vmdk', 'raw']; + preallocationsOptions: string[] = ['off', 'metadata', 'falloc', 'full']; + clusterSizeOptions: ClusterSize[] = [ + { + name: '512', + value: 512 + }, + { + name: '1k', + value: 1024 + }, + { + name: '2k', + value: 2048 + }, + { + name: '4k', + value: 4096 + }, + { + name: '8k', + value: 8192 + }, + { + name: '16k', + value: 16384 + }, + { + name: '32k', + value: 32768 + }, + { + name: '64k', + value: 65536 + }, + { + name: '128k', + value: 131072 + }, + { + name: '256k', + value: 262144 + }, + { + name: '512k', + value: 524288 + }, + { + name: '1024k', + value: 1048576 + }, + { + name: '2048k', + value: 2097152 + } + ]; + lazyRefcountsOptions: string[] = ['off', 'on']; + refcountBitsOptions: number[] = [1,2,4,8,16,32,64]; + zeroedGrainOptions: string[] = ['on', 'off']; + inputForm: FormGroup; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private qemuService: QemuService + ) { + this.inputForm = this.formBuilder.group({ + qemu_img: new FormControl('', Validators.required), + path: new FormControl('', Validators.required), + size: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.qemuImg = {} as QemuImg; + } + + setSubformat(subformat: string) { + this.qemuImg.subformat = subformat; + } + + onSaveClick() { + if (this.inputForm.valid && this.qemuImg.format) { + this.qemuService.addImage(this.server, this.qemuImg).subscribe(() => { + this.dialogRef.close(); + }); + } else { + this.toasterService.error('Fill all required fields.') + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} + +export interface ClusterSize { + name: string; + value: number; +} diff --git a/src/app/components/project-map/node-editors/configurator/switch/configurator-switch.component.html b/src/app/components/project-map/node-editors/configurator/switch/configurator-switch.component.html new file mode 100644 index 00000000..dcdb4b61 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/switch/configurator-switch.component.html @@ -0,0 +1,83 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/switch/configurator-switch.component.ts b/src/app/components/project-map/node-editors/configurator/switch/configurator-switch.component.ts new file mode 100644 index 00000000..744b423d --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/switch/configurator-switch.component.ts @@ -0,0 +1,133 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; + + +@Component({ + selector: 'app-configurator-switch', + templateUrl: './configurator-switch.component.html', + styleUrls: ['../configurator.component.scss', '../../../../preferences/preferences.component.scss'] +}) +export class ConfiguratorDialogSwitchComponent implements OnInit { + server: Server; + node: Node; + name: string; + nameForm: FormGroup; + inputForm: FormGroup; + consoleTypes: string[] = []; + + nodeMappings = new Map(); + nodeMappingsDataSource: NodeMapping[] = []; + dataSource = []; + displayedColumns = ['portIn', 'portOut', 'actions'] + + sourcePort: string = ''; + sourceDlci: string = ''; + destinationPort: string = ''; + destinationDlci: string = ''; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder + ) { + this.nameForm = this.formBuilder.group({ + name: new FormControl('', Validators.required), + }); + + this.inputForm = this.formBuilder.group({ + sourcePort: new FormControl('', Validators.required), + sourceDlci: new FormControl('', Validators.required), + destinationPort: new FormControl('', Validators.required), + destinationDlci: new FormControl('', Validators.required), + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + + let mappings = node.properties.mappings; + Object.keys(mappings).forEach(key => { + this.nodeMappings.set(key, mappings[key]); + }); + + this.nodeMappings.forEach((value: string, key: string) => { + this.nodeMappingsDataSource.push({ + portIn: key, + portOut: value + }); + }); + }); + } + + delete(elem: NodeMapping) { + this.nodeMappingsDataSource = this.nodeMappingsDataSource.filter(n => n !== elem); + } + + add() { + if (this.inputForm.valid) { + let nodeMapping: NodeMapping = { + portIn: `${this.sourcePort}:${this.sourceDlci}`, + portOut: `${this.destinationPort}:${this.destinationDlci}` + }; + + if (this.nodeMappingsDataSource.filter(n => n.portIn === nodeMapping.portIn).length > 0) { + this.toasterService.error('Mapping already defined.'); + } else { + this.nodeMappingsDataSource = this.nodeMappingsDataSource.concat([nodeMapping]); + this.clearUserInput(); + } + } else { + this.toasterService.error('Fill all required fields.'); + } + } + + clearUserInput() { + this.sourcePort = '0'; + this.sourceDlci = '0'; + this.destinationPort = '0'; + this.destinationDlci = '0'; + } + + strMapToObj(strMap) { + let obj = Object.create(null); + for (let [k,v] of strMap) { + obj[k] = v; + } + return obj; + } + + onSaveClick() { + if (this.nameForm.valid) { + this.nodeMappings.clear(); + this.nodeMappingsDataSource.forEach(elem => { + this.nodeMappings.set(elem.portIn, elem.portOut); + }); + + this.node.properties.mappings = Array.from(this.nodeMappings).reduce((obj, [key, value]) => (Object.assign(obj, { [key]: value })), {}); + + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} + +export interface NodeMapping { + portIn: string, + portOut: string +} diff --git a/src/app/components/project-map/node-editors/configurator/traceng/configurator-traceng.component.html b/src/app/components/project-map/node-editors/configurator/traceng/configurator-traceng.component.html new file mode 100644 index 00000000..ff1164d0 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/traceng/configurator-traceng.component.html @@ -0,0 +1,24 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/traceng/configurator-traceng.component.ts b/src/app/components/project-map/node-editors/configurator/traceng/configurator-traceng.component.ts new file mode 100644 index 00000000..47811847 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/traceng/configurator-traceng.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; + + +@Component({ + selector: 'app-configurator-traceng', + templateUrl: './configurator-traceng.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogTracengComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + }) + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component.html b/src/app/components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component.html new file mode 100644 index 00000000..991be342 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component.html @@ -0,0 +1,63 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component.ts b/src/app/components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component.ts new file mode 100644 index 00000000..83ffeeed --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/virtualbox/configurator-virtualbox.component.ts @@ -0,0 +1,81 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { VirtualBoxConfigurationService } from '../../../../../services/virtual-box-configuration.service'; +import { CustomAdaptersTableComponent } from '../../../../../components/preferences/common/custom-adapters-table/custom-adapters-table.component'; + + +@Component({ + selector: 'app-configurator-virtualbox', + templateUrl: './configurator-virtualbox.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogVirtualBoxComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + consoleTypes: string[] = []; + onCloseOptions = []; + + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; + networkTypes = []; + + @ViewChild("customAdapters", {static: false}) customAdapters: CustomAdaptersTableComponent; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private virtualBoxConfigurationService: VirtualBoxConfigurationService + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required), + ram: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }) + } + + getConfiguration() { + this.consoleTypes = this.virtualBoxConfigurationService.getConsoleTypes(); + this.onCloseOptions = this.virtualBoxConfigurationService.getOnCloseoptions(); + this.networkTypes = this.virtualBoxConfigurationService.getNetworkTypes(); + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.node.custom_adapters = []; + this.customAdapters.adapters.forEach(n => { + this.node.custom_adapters.push({ + adapter_number: n.adapter_number, + adapter_type: n.adapter_type + }) + }); + + this.node.properties.adapters = this.node.custom_adapters.length; + + this.nodeService.updateNodeWithCustomAdapters(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/vmware/configurator-vmware.component.html b/src/app/components/project-map/node-editors/configurator/vmware/configurator-vmware.component.html new file mode 100644 index 00000000..6750e2b5 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/vmware/configurator-vmware.component.html @@ -0,0 +1,63 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/vmware/configurator-vmware.component.ts b/src/app/components/project-map/node-editors/configurator/vmware/configurator-vmware.component.ts new file mode 100644 index 00000000..707e36ff --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/vmware/configurator-vmware.component.ts @@ -0,0 +1,80 @@ +import { Component, OnInit, Input, ViewChild } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; +import { CustomAdaptersTableComponent } from '../../../../../components/preferences/common/custom-adapters-table/custom-adapters-table.component'; +import { VmwareConfigurationService } from '../../../../../services/vmware-configuration.service'; + + +@Component({ + selector: 'app-configurator-vmware', + templateUrl: './configurator-vmware.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogVmwareComponent implements OnInit { + server: Server; + node: Node; + name: string; + generalSettingsForm: FormGroup; + consoleTypes: string[] = []; + onCloseOptions = []; + + displayedColumns: string[] = ['adapter_number', 'port_name', 'adapter_type', 'actions']; + networkTypes = []; + + @ViewChild("customAdapters", {static: false}) customAdapters: CustomAdaptersTableComponent; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private vmwareConfigurationService: VmwareConfigurationService + ) { + this.generalSettingsForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }) + } + + getConfiguration() { + this.consoleTypes = this.vmwareConfigurationService.getConsoleTypes(); + this.onCloseOptions = this.vmwareConfigurationService.getOnCloseoptions(); + this.networkTypes = this.vmwareConfigurationService.getNetworkTypes(); + } + + onSaveClick() { + if (this.generalSettingsForm.valid) { + this.node.custom_adapters = []; + this.customAdapters.adapters.forEach(n => { + this.node.custom_adapters.push({ + adapter_number: n.adapter_number, + adapter_type: n.adapter_type + }) + }); + + this.node.properties.adapters = this.node.custom_adapters.length; + + this.nodeService.updateNodeWithCustomAdapters(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component.html b/src/app/components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component.html new file mode 100644 index 00000000..c431fe58 --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component.html @@ -0,0 +1,39 @@ +

Configurator for node {{name}}

+ + + +
+ + +
diff --git a/src/app/components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component.ts b/src/app/components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component.ts new file mode 100644 index 00000000..9c802eed --- /dev/null +++ b/src/app/components/project-map/node-editors/configurator/vpcs/configurator-vpcs.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit, Input } from "@angular/core"; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { VpcsConfigurationService } from '../../../../../services/vpcs-configuration.service'; +import { Node } from '../../../../../cartography/models/node'; +import { Server } from '../../../../../models/server'; +import { NodeService } from '../../../../../services/node.service'; +import { ToasterService } from '../../../../../services/toaster.service'; +import { MatDialogRef } from '@angular/material'; + + +@Component({ + selector: 'app-configurator-vpcs', + templateUrl: './configurator-vpcs.component.html', + styleUrls: ['../configurator.component.scss'] +}) +export class ConfiguratorDialogVpcsComponent implements OnInit { + server: Server; + node: Node; + name: string; + inputForm: FormGroup; + consoleTypes: string[] = []; + + constructor( + public dialogRef: MatDialogRef, + public nodeService: NodeService, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private vpcsConfigurationService: VpcsConfigurationService + ) { + this.inputForm = this.formBuilder.group({ + name: new FormControl('', Validators.required) + }); + } + + ngOnInit() { + this.nodeService.getNode(this.server, this.node).subscribe((node: Node) => { + this.node = node; + this.name = node.name; + this.getConfiguration(); + }) + } + + getConfiguration() { + this.consoleTypes = this.vpcsConfigurationService.getConsoleTypes(); + } + + onSaveClick() { + if (this.inputForm.valid) { + this.nodeService.updateNode(this.server, this.node).subscribe(() => { + this.toasterService.success(`Node ${this.node.name} updated.`); + this.onCancelClick(); + }); + } else { + this.toasterService.error(`Fill all required fields.`); + } + } + + onCancelClick() { + this.dialogRef.close(); + } +} diff --git a/src/app/components/project-map/node-select-interface/node-select-interface.component.html b/src/app/components/project-map/node-select-interface/node-select-interface.component.html index 33d07113..23c5bab7 100644 --- a/src/app/components/project-map/node-select-interface/node-select-interface.component.html +++ b/src/app/components/project-map/node-select-interface/node-select-interface.component.html @@ -1,6 +1,6 @@
- +
diff --git a/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.spec.ts b/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.spec.ts index 945caaee..86c35714 100644 --- a/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.spec.ts +++ b/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.spec.ts @@ -12,6 +12,7 @@ import { MockedLinkService, MockedNodesDataSource } from '../../project-map.comp import { Link } from '../../../../models/link'; import { of } from 'rxjs'; import { NodesDataSource } from '../../../../cartography/datasources/nodes-datasource'; +import { PacketCaptureService } from '../../../../services/packet-capture.service'; describe('StartCaptureDialogComponent', () => { let component: StartCaptureDialogComponent; @@ -32,7 +33,8 @@ describe('StartCaptureDialogComponent', () => { { provide: MAT_DIALOG_DATA, useValue: [] }, { provide: ToasterService, useValue: mockedToasterService }, { provide: LinkService, useValue: mockedLinkService }, - { provide: NodesDataSource, useValue: mockedNodesDataSource } + { provide: NodesDataSource, useValue: mockedNodesDataSource }, + { provide: PacketCaptureService } ], declarations: [ StartCaptureDialogComponent diff --git a/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.ts b/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.ts index b9ef34c7..bb49e46b 100644 --- a/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.ts +++ b/src/app/components/project-map/packet-capturing/start-capture/start-capture.component.ts @@ -9,6 +9,8 @@ import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms' import { ToasterService } from '../../../../services/toaster.service'; import { LinkNode } from '../../../../models/link-node'; import { NodesDataSource } from '../../../../cartography/datasources/nodes-datasource'; +import { PacketCaptureService } from '../../../../services/packet-capture.service'; +import { Project } from '../../../../models/project'; @Component({ selector: 'app-start-capture', @@ -17,6 +19,7 @@ import { NodesDataSource } from '../../../../cartography/datasources/nodes-datas }) export class StartCaptureDialogComponent implements OnInit { server: Server; + project: Project; link: Link; linkTypes = []; inputForm: FormGroup; @@ -27,7 +30,8 @@ export class StartCaptureDialogComponent implements OnInit { private linkService: LinkService, private formBuilder: FormBuilder, private toasterService: ToasterService, - private nodesDataSource: NodesDataSource + private nodesDataSource: NodesDataSource, + private packetCaptureService: PacketCaptureService ) { this.inputForm = this.formBuilder.group({ linkType: new FormControl('', Validators.required), @@ -73,6 +77,10 @@ export class StartCaptureDialogComponent implements OnInit { data_link_type: this.inputForm.get('linkType').value }; + if (this.startProgram) { + this.packetCaptureService.startCapture(this.server, this.project, this.link, captureSettings.capture_file_name); + } + this.linkService.startCaptureOnLink(this.server, this.link, captureSettings).subscribe(() => { this.dialogRef.close(); }); diff --git a/src/app/components/project-map/project-map-menu/project-map-menu.component.html b/src/app/components/project-map/project-map-menu/project-map-menu.component.html index b0b20e55..4d099f56 100644 --- a/src/app/components/project-map/project-map-menu/project-map-menu.component.html +++ b/src/app/components/project-map/project-map-menu/project-map-menu.component.html @@ -57,9 +57,20 @@ (click)="changeLockValue()"> lock + + +
+
diff --git a/src/app/components/project-map/project-map-menu/project-map-menu.component.spec.ts b/src/app/components/project-map/project-map-menu/project-map-menu.component.spec.ts index f998feac..01403278 100644 --- a/src/app/components/project-map/project-map-menu/project-map-menu.component.spec.ts +++ b/src/app/components/project-map/project-map-menu/project-map-menu.component.spec.ts @@ -1,8 +1,8 @@ import { ProjectMapMenuComponent } from "./project-map-menu.component"; import { ComponentFixture, async, TestBed } from '@angular/core/testing'; import { MockedDrawingService } from '../project-map.component.spec'; -import { MapSettingService } from '../../../services/mapsettings.service'; -import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule } from '@angular/material'; +import { MapSettingsService } from '../../../services/mapsettings.service'; +import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, MatDialogModule } from '@angular/material'; import { CommonModule } from '@angular/common'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { DrawingService } from '../../../services/drawing.service'; @@ -10,20 +10,26 @@ import { ToolsService } from '../../../services/tools.service'; import { D3MapComponent } from '../../../cartography/components/d3-map/d3-map.component'; import { ANGULAR_MAP_DECLARATIONS } from '../../../cartography/angular-map.imports'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SymbolService } from '../../../services/symbol.service'; +import { MockedSymbolService } from '../../preferences/common/symbols/symbols.component.spec'; +import { ElectronService } from 'ngx-electron'; describe('ProjectMapMenuComponent', () => { let component: ProjectMapMenuComponent; let fixture: ComponentFixture; let drawingService = new MockedDrawingService(); - let mapSettingService = new MapSettingService(); + let mapSettingService = new MapSettingsService(); + let mockedSymbolService = new MockedSymbolService; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, CommonModule, NoopAnimationsModule], + imports: [MatIconModule, MatDialogModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, CommonModule, NoopAnimationsModule], providers: [ { provide: DrawingService, useValue: drawingService }, { provide: ToolsService }, - { provide: MapSettingService, useValue: mapSettingService } + { provide: MapSettingsService, useValue: mapSettingService }, + { provide: SymbolService, useValue: mockedSymbolService}, + { provide: ElectronService } ], declarations: [ProjectMapMenuComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/components/project-map/project-map-menu/project-map-menu.component.ts b/src/app/components/project-map/project-map-menu/project-map-menu.component.ts index f7edbc45..49964269 100644 --- a/src/app/components/project-map/project-map-menu/project-map-menu.component.ts +++ b/src/app/components/project-map/project-map-menu/project-map-menu.component.ts @@ -2,8 +2,15 @@ import { Component, OnInit, OnDestroy, Input } from '@angular/core'; import { Project } from '../../../models/project'; import { Server } from '../../../models/server'; import { ToolsService } from '../../../services/tools.service'; -import { MapSettingService } from '../../../services/mapsettings.service'; +import { MapSettingsService } from '../../../services/mapsettings.service'; import { DrawingService } from '../../../services/drawing.service'; +import * as svg from 'save-svg-as-png'; +import { SymbolService } from '../../../services/symbol.service'; +import { select } from 'd3-selection'; +import downloadSvg from 'svg-crowbar'; +import { ElectronService } from 'ngx-electron'; +import { MatDialog } from '@angular/material'; +import { ScreenshotDialogComponent, Screenshot } from '../screenshot-dialog/screenshot-dialog.component'; @Component({ @@ -26,12 +33,54 @@ export class ProjectMapMenuComponent implements OnInit, OnDestroy { constructor( private toolsService: ToolsService, - private mapSettingsService: MapSettingService, - private drawingService: DrawingService + private mapSettingsService: MapSettingsService, + private drawingService: DrawingService, + private symbolService: SymbolService, + private dialog: MatDialog ) {} ngOnInit() {} + public takeScreenshot() { + const dialogRef = this.dialog.open(ScreenshotDialogComponent, { + width: '400px', + autoFocus: false + }); + dialogRef.afterClosed().subscribe((result: Screenshot) => { + if (result) this.saveImage(result); + }); + } + + private async saveImage(screenshotProperties: Screenshot) { + if (screenshotProperties.filetype === 'png') { + let splittedSvg = document.getElementsByTagName("svg")[0].outerHTML.split('image'); + let i = 1; + + while (i < splittedSvg.length) { + let splittedImage = splittedSvg[i].split("\""); + let splittedUrl = splittedImage[1].split("/"); + + let elem = await this.symbolService.raw(this.server, splittedUrl[7]).toPromise(); + let splittedElement = elem.split('-->'); + splittedSvg[i] = splittedElement[1].substring(2); + i += 2; + } + let svgString = splittedSvg.join(); + + let placeholder = document.createElement('div'); + placeholder.innerHTML = svgString; + let element = placeholder.firstChild; + + svg.saveSvgAsPng(element, `${screenshotProperties.name}.png`); + } else { + var svg_el = select("svg") + .attr("version", 1.1) + .attr("xmlns", "http://www.w3.org/2000/svg") + .node(); + downloadSvg(select("svg").node(), `${screenshotProperties.name}`); + } + } + public addDrawing(selectedObject: string) { switch (selectedObject) { case 'rectangle': @@ -66,7 +115,7 @@ export class ProjectMapMenuComponent implements OnInit, OnDestroy { public onDrawingSaved() { this.resetDrawToolChoice(); - } + } public resetDrawToolChoice() { this.drawTools.isRectangleChosen = false; diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index bd758805..3a21ddfd 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -8,7 +8,7 @@ [drawings]="drawings" [width]="project.scene_width" [height]="project.scene_height" - [show-interface-labels]="project.show_interface_labels" + [show-interface-labels]="isInterfaceLabelVisible" [readonly]="inReadOnlyMode" (nodeDragged)="onNodeDragged($event)" (drawingDragged)="onDrawingDragged($event)" @@ -24,7 +24,7 @@ [drawings]="drawings" [width]="project.scene_width" [height]="project.scene_height" - [show-interface-labels]="project.show_interface_labels" + [show-interface-labels]="isInterfaceLabelVisible" [selection-tool]="tools.selection" [moving-tool]="tools.moving" [draw-link-tool]="tools.draw_link" @@ -40,23 +40,53 @@ + + + + + + - +
- + Show interface labels + + Show console + + + Show topology/servers summary +
@@ -85,6 +115,12 @@ settings_applications + + + +
@@ -120,3 +156,9 @@ +
+ +
+
+ +
diff --git a/src/app/components/project-map/project-map.component.scss b/src/app/components/project-map/project-map.component.scss index 49f1556a..118c8972 100644 --- a/src/app/components/project-map/project-map.component.scss +++ b/src/app/components/project-map/project-map.component.scss @@ -80,7 +80,7 @@ g.node:hover { } .extended { - width: 700px !important; + width: 830px !important; height: 100%; overflow: hidden; } @@ -230,3 +230,11 @@ g.node text, .context-menu-items .mat-menu-item:focus { background: none; } + +.visible { + display: none; +} + +mat-menu-panel { + min-height: 0px; +} 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 8daeda92..ef7f3009 100644 --- a/src/app/components/project-map/project-map.component.spec.ts +++ b/src/app/components/project-map/project-map.component.spec.ts @@ -1,7 +1,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ProjectMapComponent } from './project-map.component'; -import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule } from '@angular/material'; +import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, MatDialogModule, MatBottomSheetModule } from '@angular/material'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ServerService } from '../../services/server.service'; import { ProjectService } from '../../services/project.service'; @@ -20,14 +20,14 @@ import { DrawingsDataSource } from '../../cartography/datasources/drawings-datas import { CommonModule } from '@angular/common'; import { ANGULAR_MAP_DECLARATIONS } from '../../cartography/angular-map.imports'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { MockedSettingsService } from '../../services/settings.service.spec'; import { MockedServerService } from '../../services/server.service.spec'; import { MockedProjectService } from '../../services/project.service.spec'; import { Observable } from 'rxjs/Rx'; import { Drawing } from '../../cartography/models/drawing'; import { D3MapComponent } from '../../cartography/components/d3-map/d3-map.component'; -import { of } from 'rxjs'; +import { of, BehaviorSubject } from 'rxjs'; import { Server } from '../../models/server'; import { Node } from '../../cartography/models/node'; import { ToolsService } from '../../services/tools.service'; @@ -48,10 +48,14 @@ import { NodeCreatedLabelStylesFixer } from './helpers/node-created-label-styles import { LabelWidget } from '../../cartography/widgets/label'; import { InterfaceLabelWidget } from '../../cartography/widgets/interface-label'; import { MapLinkNodeToLinkNodeConverter } from '../../cartography/converters/map/map-link-node-to-link-node-converter'; -import { MapSettingService } from '../../services/mapsettings.service'; +import { MapSettingsService } from '../../services/mapsettings.service'; import { ProjectMapMenuComponent } from './project-map-menu/project-map-menu.component'; import { MockedToasterService } from '../../services/toaster.service.spec'; import { ToasterService } from '../../services/toaster.service'; +import { MockedActivatedRoute } from '../snapshots/list-of-snapshots/list-of-snaphshots.component.spec'; +import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource } from '../../cartography/datasources/map-datasource'; +import { EthernetLinkWidget } from '../../cartography/widgets/links/ethernet-link'; +import { SerialLinkWidget } from '../../cartography/widgets/links/serial-link'; export class MockedProgressService { public activate() {} @@ -62,6 +66,10 @@ export class MockedProgressService { export class MockedNodeService { public node = { label: {} } as Node; constructor() {} + + getDefaultCommand(): string { + return `putty.exe -telnet \%h \%p -wt \"\%d\" -gns3 5 -skin 4`; + } updateLabel(): Observable { return of(this.node); @@ -91,9 +99,37 @@ 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); } + + getStartupConfiguration(server: Server, node: Node) { + return of('sample config'); + } + + saveConfiguration(server: Server, node: Node, configuration: string) { + return of(configuration); + } + + update(server: Server, node: Node) { + return of(node); + } } export class MockedDrawingService { @@ -184,9 +220,17 @@ export class MockedNodesDataSource { return {status: 'started'}; } + getItems() { + return [{name: 'testNode'}]; + } + update() { return of({}); } + + public get changes() { + return new BehaviorSubject<[]>([]); + } } export class MockedLinksDataSource { @@ -202,6 +246,7 @@ describe('ProjectMapComponent', () => { let linksDataSource = new MockedLinksDataSource(); let mockedToasterService = new MockedToasterService(); let nodeCreatedLabelStylesFixer; + let mockedRouter = new MockedActivatedRoute; beforeEach(async(() => { nodeCreatedLabelStylesFixer = { @@ -209,7 +254,7 @@ describe('ProjectMapComponent', () => { }; TestBed.configureTestingModule({ - imports: [MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, CommonModule, NoopAnimationsModule], + imports: [MatBottomSheetModule, MatIconModule, MatDialogModule, MatToolbarModule, MatMenuModule, MatCheckboxModule, CommonModule, NoopAnimationsModule], providers: [ { provide: ActivatedRoute }, { provide: ServerService, useClass: MockedServerService }, @@ -222,6 +267,8 @@ describe('ProjectMapComponent', () => { { provide: MapChangeDetectorRef }, { provide: NodeWidget }, { provide: LinkWidget }, + { provide: EthernetLinkWidget }, + { provide: SerialLinkWidget }, { provide: DrawingsWidget }, { provide: LabelWidget }, { provide: InterfaceLabelWidget }, @@ -245,7 +292,13 @@ describe('ProjectMapComponent', () => { { provide: NodeCreatedLabelStylesFixer, useValue: nodeCreatedLabelStylesFixer}, { provide: MapScaleService }, { provide: NodeCreatedLabelStylesFixer, useValue: nodeCreatedLabelStylesFixer}, - { provide: ToasterService, useValue: mockedToasterService } + { provide: ToasterService, useValue: mockedToasterService }, + { provide: Router, useValue: mockedRouter }, + { provide: MapNodesDataSource, useClass: MapNodesDataSource }, + { provide: MapLinksDataSource, useClass: LinksDataSource }, + { provide: MapDrawingsDataSource, useClass: MapDrawingsDataSource }, + { provide: MapSymbolsDataSource, useClass: MapSymbolsDataSource }, + { provide: MapSettingsService, useClass: MapSettingsService } ], declarations: [ProjectMapComponent, ProjectMapMenuComponent, D3MapComponent, ...ANGULAR_MAP_DECLARATIONS], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index 002d167b..fbe54469 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -1,5 +1,5 @@ -import { Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { ActivatedRoute, ParamMap } from '@angular/router'; +import { Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation, ElementRef } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Observable, Subject, Subscription, from } from 'rxjs'; import { webSocket } from 'rxjs/webSocket'; @@ -43,6 +43,7 @@ import { RecentlyOpenedProjectService } from '../../services/recentlyOpenedProje import { MapLink } from '../../cartography/models/map/map-link'; import { MapLinkToLinkConverter } from '../../cartography/converters/map/map-link-to-link-converter'; import { MovingEventSource } from '../../cartography/events/moving-event-source'; +import { log } from 'util'; import { LinkWidget } from '../../cartography/widgets/link'; import { MapScaleService } from '../../services/mapScale.service'; import { NodeCreatedLabelStylesFixer } from './helpers/node-created-label-styles-fixer'; @@ -51,6 +52,16 @@ import { LabelWidget } from '../../cartography/widgets/label'; import { MapLinkNodeToLinkNodeConverter } from '../../cartography/converters/map/map-link-node-to-link-node-converter'; import { ProjectMapMenuComponent } from './project-map-menu/project-map-menu.component'; import { ToasterService } from '../../services/toaster.service'; +import { ImportProjectDialogComponent } from '../projects/import-project-dialog/import-project-dialog.component'; +import { MatDialog, MatBottomSheet } from '@angular/material'; +import { AddBlankProjectDialogComponent } from '../projects/add-blank-project-dialog/add-blank-project-dialog.component'; +import { SaveProjectDialogComponent } from '../projects/save-project-dialog/save-project-dialog.component'; +import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource, Indexed } from '../../cartography/datasources/map-datasource'; +import { MapSettingsService } from '../../services/mapsettings.service'; +import { EditProjectDialogComponent } from '../projects/edit-project-dialog/edit-project-dialog.component'; +import { EthernetLinkWidget } from '../../cartography/widgets/links/ethernet-link'; +import { SerialLinkWidget } from '../../cartography/widgets/links/serial-link'; +import { NavigationDialogComponent } from '../projects/navigation-dialog/navigation-dialog.component'; @Component({ @@ -68,6 +79,9 @@ export class ProjectMapComponent implements OnInit, OnDestroy { public server: Server; public ws: WebSocket; public isProjectMapMenuVisible: boolean = false; + public isConsoleVisible: boolean = false; + public isTopologySummaryVisible: boolean = false; + public isInterfaceLabelVisible: boolean = false; tools = { selection: true, @@ -115,11 +129,23 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private movingEventSource: MovingEventSource, private mapScaleService: MapScaleService, private nodeCreatedLabelStylesFixer: NodeCreatedLabelStylesFixer, - private toasterService: ToasterService + private toasterService: ToasterService, + private dialog: MatDialog, + private router: Router, + private mapNodesDataSource: MapNodesDataSource, + private mapLinksDataSource: MapLinksDataSource, + private mapDrawingsDataSource: MapDrawingsDataSource, + private mapSymbolsDataSource: MapSymbolsDataSource, + private mapSettingsService: MapSettingsService, + private ethernetLinkWidget: EthernetLinkWidget, + private serialLinkWidget: SerialLinkWidget, + private bottomSheet: MatBottomSheet ) {} 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) => { @@ -137,6 +163,12 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }), mergeMap((project: Project) => { this.project = project; + + if (this.mapSettingsService.interfaceLabels.has(project.project_id)) { + this.isInterfaceLabelVisible = this.mapSettingsService.interfaceLabels.get(project.project_id); + } else { + this.isInterfaceLabelVisible = this.project.show_interface_labels; + } this.recentlyOpenedProjectService.setServerId(this.server.id.toString()); this.recentlyOpenedProjectService.setProjectId(this.project.project_id); @@ -196,11 +228,37 @@ export class ProjectMapComponent implements OnInit, OnDestroy { addKeyboardListeners() { Mousetrap.bind('ctrl++', (event: Event) => { event.preventDefault(); + this.zoomIn(); }); Mousetrap.bind('ctrl+-', (event: Event) => { event.preventDefault(); - });; + this.zoomOut(); + }); + + Mousetrap.bind('ctrl+0', (event: Event) => { + event.preventDefault(); + this.resetZoom(); + }); + + Mousetrap.bind('ctrl+a', (event: Event) => { + event.preventDefault(); + let allNodes: Indexed[] = this.mapNodesDataSource.getItems(); + let allDrawings: Indexed[] = this.mapDrawingsDataSource.getItems(); + let allLinks: Indexed[] = this.mapLinksDataSource.getItems(); + let allSymbols: Indexed[] = this.mapSymbolsDataSource.getItems(); + this.selectionManager.setSelected(allNodes.concat(allDrawings).concat(allLinks).concat(allSymbols)); + }); + + Mousetrap.bind('ctrl+shift+a', (event: Event) => { + event.preventDefault(); + this.selectionManager.setSelected([]); + }); + + Mousetrap.bind('ctrl+shift+s', (event: Event) => { + event.preventDefault(); + this.router.navigate(['/server', this.server.id, 'preferences']); + }); } onProjectLoad(project: Project) { @@ -251,6 +309,16 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.pageY, eventLink.event.pageX); }); + const onEthernetLinkContextMenu = this.ethernetLinkWidget.onContextMenu.subscribe((eventLink: LinkContextMenu) => { + const link = this.mapLinkToLink.convert(eventLink.link); + this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.pageY, eventLink.event.pageX); + }); + + const onSerialLinkContextMenu = this.serialLinkWidget.onContextMenu.subscribe((eventLink: LinkContextMenu) => { + const link = this.mapLinkToLink.convert(eventLink.link); + this.contextMenu.openMenuForListOfElements([], [], [], [link], eventLink.event.pageY, eventLink.event.pageX); + }); + const onNodeContextMenu = this.nodeWidget.onContextMenu.subscribe((eventNode: NodeContextMenu) => { const node = this.mapNodeToNode.convert(eventNode.node); this.contextMenu.openMenuForNode(node, eventNode.event.pageY, eventNode.event.pageX); @@ -298,6 +366,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }); this.subscriptions.push(onLinkContextMenu); + this.subscriptions.push(onEthernetLinkContextMenu); + this.subscriptions.push(onSerialLinkContextMenu); this.subscriptions.push(onNodeContextMenu); this.subscriptions.push(onDrawingContextMenu); this.subscriptions.push(onContextMenu); @@ -324,6 +394,17 @@ export class ProjectMapComponent implements OnInit, OnDestroy { }); } + public centerView() { + if (this.project) { + let scrollX: number = (this.project.scene_width - document.documentElement.clientWidth) > 0 ? (this.project.scene_width - document.documentElement.clientWidth)/2 : 0; + let scrollY: number = (this.project.scene_height - document.documentElement.clientHeight) > 0 ? (this.project.scene_height - document.documentElement.clientHeight)/2 : 0; + + window.scrollTo(scrollX, scrollY); + } else { + this.toasterService.error('Please wait until all components are loaded.'); + } + } + public onDrawingSaved() { this.projectMapMenuComponent.resetDrawToolChoice(); } @@ -358,8 +439,19 @@ export class ProjectMapComponent implements OnInit, OnDestroy { this.toolsService.drawLinkToolActivation(this.tools.draw_link); } - public toggleShowInterfaceLabels(enabled: boolean) { - this.project.show_interface_labels = enabled; + public toggleShowInterfaceLabels(visible: boolean) { + this.isInterfaceLabelVisible = visible; + this.mapSettingsService.toggleShowInterfaceLabels(this.project.project_id, this.isInterfaceLabelVisible); + } + + public toggleShowConsole(visible: boolean) { + this.isConsoleVisible = visible; + this.mapSettingsService.toggleLogConsole(this.isConsoleVisible); + } + + public toggleShowTopologySummary(visible: boolean) { + this.isTopologySummaryVisible = visible; + this.mapSettingsService.toggleTopologySummary(this.isTopologySummaryVisible); } public hideMenu() { @@ -386,6 +478,78 @@ export class ProjectMapComponent implements OnInit, OnDestroy { resetZoom() { this.mapScaleService.resetToDefault(); } + + addNewProject() { + const dialogRef = this.dialog.open(AddBlankProjectDialogComponent, { + width: '400px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + } + + saveProject() { + const dialogRef = this.dialog.open(SaveProjectDialogComponent, { + width: '400px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + instance.project = this.project; + } + + editProject() { + const dialogRef = this.dialog.open(EditProjectDialogComponent, { + width: '500px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + instance.project = this.project; + } + + importProject() { + let uuid: string = ''; + const dialogRef = this.dialog.open(ImportProjectDialogComponent, { + width: '400px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + const subscription = dialogRef.componentInstance.onImportProject.subscribe((projectId: string) => { + uuid = projectId; + }); + + dialogRef.afterClosed().subscribe(() => { + subscription.unsubscribe(); + if (uuid) { + this.bottomSheet.open(NavigationDialogComponent); + let bottomSheetRef = this.bottomSheet._openedBottomSheetRef; + bottomSheetRef.instance.projectMessage = 'imported project'; + + const bottomSheetSubscription = bottomSheetRef.afterDismissed().subscribe((result: boolean) => { + if (result) { + this.projectService.open(this.server, uuid).subscribe(() => { + this.router.navigate(['/server', this.server.id, 'project', uuid]); + }); + } + }); + } + }); + } + + exportProject() { + if (this.nodes.filter(node => node.node_type === 'virtualbox').length > 0) { + this.toasterService.error('Map with VirtualBox machines cannot be exported.') + } else if (this.nodes.filter(node => + (node.status === 'started' && node.node_type==='vpcs') || + (node.status === 'started' && node.node_type==='virtualbox') || + (node.status === 'started' && node.node_type==='vmware')).length > 0) { + this.toasterService.error('Project with running nodes cannot be exported.') + } else { + location.assign(this.projectService.getExportPath(this.server, this.project)); + } + } public uploadImageFile(event) { this.readImageFile(event.target); @@ -408,6 +572,12 @@ export class ProjectMapComponent implements OnInit, OnDestroy { imageToUpload.src = window.URL.createObjectURL(file); } + public deleteProject() { + this.projectService.delete(this.server, this.project.project_id).subscribe(() => { + this.router.navigate(['/server', this.server.id, 'projects']); + }); + } + public ngOnDestroy() { this.drawingsDataSource.clear(); this.nodesDataSource.clear(); diff --git a/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.html b/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.html new file mode 100644 index 00000000..388459e0 --- /dev/null +++ b/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.html @@ -0,0 +1,30 @@ +

Take a screenshot

+ + + +
+ + + Name for screenshot is required + Entered name is incorrect + + +
+ + +
+
diff --git a/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.scss b/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.scss new file mode 100644 index 00000000..bde5436e --- /dev/null +++ b/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.scss @@ -0,0 +1,8 @@ +.name-form { + width: 100%; +} + +.radio-group { + display: flex; + justify-content: space-between; +} diff --git a/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.ts b/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.ts new file mode 100644 index 00000000..aeb181cc --- /dev/null +++ b/src/app/components/project-map/screenshot-dialog/screenshot-dialog.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { ToasterService } from '../../../services/toaster.service'; +import { ElectronService } from 'ngx-electron'; + + +@Component({ + selector: 'app-screenshot-dialog', + templateUrl: './screenshot-dialog.component.html', + styleUrls: ['./screenshot-dialog.component.scss'] +}) +export class ScreenshotDialogComponent implements OnInit { + nameForm: FormGroup; + isPngAvailable: boolean; + filetype: string = 'svg'; + + constructor( + public dialogRef: MatDialogRef, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private electronService: ElectronService + ) { + this.nameForm = this.formBuilder.group({ + screenshotName: new FormControl(null, [Validators.required]) + }); + + this.isPngAvailable = this.electronService.isWindows; + } + + ngOnInit() {} + + get form() { + return this.nameForm.controls; + } + + onAddClick(): void { + if (this.nameForm.invalid) { + return; + } + + let screenshotProperties: Screenshot = { + name: this.nameForm.get('screenshotName').value, + filetype: this.filetype + }; + this.dialogRef.close(screenshotProperties); + } + + onNoClick(): void { + this.dialogRef.close(); + } + + onKeyDown(event) { + if (event.key === "Enter") { + this.onAddClick(); + } + } + + setFiletype(type: string) { + if (this.isPngAvailable) { + this.filetype = type; + } + } +} + +export class Screenshot { + name: string; + filetype: string; +} diff --git a/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts b/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts index 446bd081..4177e8f6 100644 --- a/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts +++ b/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.spec.ts @@ -23,6 +23,8 @@ export class MockedProjectService { auto_close: false, auto_open: false, auto_start: false, + drawing_grid_size: 10, + grid_size: 10, filename: 'blank', name: 'blank', path: '', diff --git a/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.ts b/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.ts index 523860fe..c7121539 100644 --- a/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.ts +++ b/src/app/components/projects/add-blank-project-dialog/add-blank-project-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, EventEmitter } from '@angular/core'; import { Router } from '@angular/router'; import { MatDialog, MatDialogRef } from '@angular/material'; import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; @@ -20,6 +20,8 @@ import { ToasterService } from '../../../services/toaster.service'; export class AddBlankProjectDialogComponent implements OnInit { server: Server; projectNameForm: FormGroup; + uuid: string; + onAddProject = new EventEmitter(); constructor( public dialogRef: MatDialogRef, @@ -62,8 +64,9 @@ export class AddBlankProjectDialogComponent implements OnInit { } addProject(): void { + this.uuid = uuid(); this.projectService - .add(this.server, this.projectNameForm.controls['projectName'].value, uuid()) + .add(this.server, this.projectNameForm.controls['projectName'].value, this.uuid) .subscribe((project: Project) => { this.dialogRef.close(); this.toasterService.success(`Project ${project.name} added`); diff --git a/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.html b/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.html new file mode 100644 index 00000000..8d9b0d73 --- /dev/null +++ b/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.html @@ -0,0 +1,12 @@ +

Please choose name for exporting project

+ + + +
+ + +
diff --git a/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.scss b/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.scss new file mode 100644 index 00000000..9c2173c2 --- /dev/null +++ b/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.scss @@ -0,0 +1,3 @@ +.form-field { + width: 100%; +} diff --git a/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.ts b/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.ts new file mode 100644 index 00000000..06cc3df3 --- /dev/null +++ b/src/app/components/projects/choose-name-dialog/choose-name-dialog.component.ts @@ -0,0 +1,36 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; +import { Server } from '../../../models/server'; +import { ProjectService } from '../../../services/project.service'; +import { Project } from '../../../models/project'; + + +@Component({ + selector: 'app-choose-name-dialog', + templateUrl: './choose-name-dialog.component.html', + styleUrls: ['./choose-name-dialog.component.scss'] +}) +export class ChooseNameDialogComponent implements OnInit { + @Input() server: Server; + @Input() project: Project + name: string; + + constructor( + public dialogRef: MatDialogRef, + private projectService: ProjectService + ) {} + + ngOnInit() { + this.name = this.project.name; + } + + onCloseClick() { + this.dialogRef.close(); + } + + onSaveClick() { + this.projectService.duplicate(this.server, this.project.project_id, this.name).subscribe(() => { + this.dialogRef.close(); + }); + } +} diff --git a/src/app/components/projects/confirmation-dialog/confirmation-dialog.component.spec.ts b/src/app/components/projects/confirmation-dialog/confirmation-dialog.component.spec.ts index c319c34b..739f54f5 100644 --- a/src/app/components/projects/confirmation-dialog/confirmation-dialog.component.spec.ts +++ b/src/app/components/projects/confirmation-dialog/confirmation-dialog.component.spec.ts @@ -15,6 +15,8 @@ describe('ConfirmationDialogComponent', () => { auto_close: false, auto_open: false, auto_start: false, + drawing_grid_size: 10, + grid_size: 10, filename: 'blank', name: 'blank', path: '', diff --git a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html new file mode 100644 index 00000000..8539641a --- /dev/null +++ b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.html @@ -0,0 +1,44 @@ +

Edit project

+ +
+ + + + + + + + + + + + + + + + + + + +
+ + + Open this project in the background when GNS3 server starts + + + + Start all nodes when this project is opened + + + + Leave this project running in the background after closing + + + + Show interface labels at start + + +
+ + +
diff --git a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.scss b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.scss new file mode 100644 index 00000000..9c2173c2 --- /dev/null +++ b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.scss @@ -0,0 +1,3 @@ +.form-field { + width: 100%; +} diff --git a/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts new file mode 100644 index 00000000..baaac059 --- /dev/null +++ b/src/app/components/projects/edit-project-dialog/edit-project-dialog.component.ts @@ -0,0 +1,64 @@ +import { Component, OnInit, Injectable } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; +import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms'; +import { Server } from '../../../models/server'; +import { Project } from '../../../models/project'; +import { ToasterService } from '../../../services/toaster.service'; +import { NonNegativeValidator } from '../../../validators/non-negative-validator'; +import { ProjectService } from '../../../services/project.service'; + +@Component({ + selector: 'app-edit-project-dialog', + templateUrl: './edit-project-dialog.component.html', + styleUrls: ['./edit-project-dialog.component.scss'] +}) +export class EditProjectDialogComponent implements OnInit { + server: Server; + project: Project; + formGroup: FormGroup; + + constructor( + public dialogRef: MatDialogRef, + private formBuilder: FormBuilder, + private projectService: ProjectService, + private toasterService: ToasterService, + private nonNegativeValidator: NonNegativeValidator + ) { + this.formGroup = this.formBuilder.group({ + projectName: new FormControl('', [Validators.required]), + width: new FormControl('', [Validators.required, nonNegativeValidator.get]), + height: new FormControl('', [Validators.required, nonNegativeValidator.get]), + nodeGridSize: new FormControl('', [Validators.required, nonNegativeValidator.get]), + drawingGridSize: new FormControl('', [Validators.required, nonNegativeValidator.get]) + }); + } + + ngOnInit() { + this.formGroup.controls['projectName'].setValue(this.project.name); + this.formGroup.controls['width'].setValue(this.project.scene_width); + this.formGroup.controls['height'].setValue(this.project.scene_height); + this.formGroup.controls['nodeGridSize'].setValue(this.project.grid_size); + this.formGroup.controls['drawingGridSize'].setValue(this.project.drawing_grid_size); + } + + onNoClick() { + this.dialogRef.close(); + } + + onYesClick() { + if (this.formGroup.valid) { + this.project.name = this.formGroup.get('projectName').value; + this.project.scene_width = this.formGroup.get('width').value; + this.project.scene_height = this.formGroup.get('height').value; + this.project.drawing_grid_size = this.formGroup.get('drawingGridSize').value; + this.project.grid_size = this.formGroup.get('nodeGridSize').value; + + this.projectService.update(this.server, this.project).subscribe((project: Project) => { + this.toasterService.success(`Project ${project.name} updated.`); + this.onNoClick(); + }) + } else { + this.toasterService.error(`Fill all required fields with correct values.`); + } + } +} diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css index 5485afaa..24be550b 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css @@ -15,6 +15,10 @@ } .file-name-form-field { + width: 90%; +} + +.empty { width: 100%; } diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html index f06b11fa..277ed2de 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.html +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.html @@ -11,7 +11,7 @@ [uploader]="uploader" /> - + { @@ -112,12 +122,11 @@ describe('ImportProjectDialogComponent', () => { fileSelectDirective.onChange(); - const expectedArguments = [ + expect(fileSelectDirective.uploader.addToQueue).toHaveBeenCalledWith( debugElement.nativeElement.files, fileSelectDirective.getOptions(), fileSelectDirective.getFilters() - ]; - expect(fileSelectDirective.uploader.addToQueue).toHaveBeenCalledWith(...expectedArguments); + ); }); it('should call uploading item', () => { diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts index 07a76027..cb996165 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Inject } from '@angular/core'; +import { Component, OnInit, Inject, EventEmitter } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material'; import { FileUploader, ParsedResponseHeaders, FileItem } from 'ng2-file-upload'; import { Server } from '../../../models/server'; @@ -26,6 +26,8 @@ export class ImportProjectDialogComponent implements OnInit { projectNameForm: FormGroup; submitted: boolean = false; isFirstStepCompleted: boolean = false; + uuid: string; + onImportProject = new EventEmitter(); constructor( private dialog: MatDialog, @@ -58,6 +60,7 @@ export class ImportProjectDialogComponent implements OnInit { status: number, headers: ParsedResponseHeaders ) => { + this.onImportProject.emit(this.uuid); this.resultMessage = 'Project was imported succesfully!'; this.isFinishEnabled = true; }; @@ -138,7 +141,8 @@ export class ImportProjectDialogComponent implements OnInit { } prepareUploadPath(): string { + this.uuid = uuid(); const projectName = this.projectNameForm.controls['projectName'].value; - return `http://${this.server.host}:${this.server.port}/v2/projects/${uuid()}/import?name=${projectName}`; + return this.projectService.getUploadPath(this.server, this.uuid, projectName); } } diff --git a/src/app/components/projects/navigation-dialog/navigation-dialog.component.html b/src/app/components/projects/navigation-dialog/navigation-dialog.component.html new file mode 100644 index 00000000..93169d50 --- /dev/null +++ b/src/app/components/projects/navigation-dialog/navigation-dialog.component.html @@ -0,0 +1,7 @@ +
+
Do you want to navigate to {{projectMessage}}?
+
+ + +
+
diff --git a/src/app/components/projects/navigation-dialog/navigation-dialog.component.scss b/src/app/components/projects/navigation-dialog/navigation-dialog.component.scss new file mode 100644 index 00000000..c8a87a35 --- /dev/null +++ b/src/app/components/projects/navigation-dialog/navigation-dialog.component.scss @@ -0,0 +1,17 @@ +.dialogWrapper { + background-color: #263238; + padding: 10px 20px; + margin-bottom: -8px; + display: flex; + justify-content: space-between; + align-items: center; +} + +mat-bottom-sheet-container { + background: #263238; +} + +.title { + margin-right: 10px; + margin-left: 10px; +} diff --git a/src/app/components/projects/navigation-dialog/navigation-dialog.component.ts b/src/app/components/projects/navigation-dialog/navigation-dialog.component.ts new file mode 100644 index 00000000..52264ab0 --- /dev/null +++ b/src/app/components/projects/navigation-dialog/navigation-dialog.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA, MatBottomSheetRef } from '@angular/material'; + +@Component({ + selector: 'app-navigation-dialog', + templateUrl: 'navigation-dialog.component.html', + styleUrls: ['navigation-dialog.component.scss'] +}) +export class NavigationDialogComponent implements OnInit { + projectMessage: string = ''; + + constructor(private bottomSheetRef: MatBottomSheetRef) {} + + ngOnInit() {} + + onNoClick(): void { + this.bottomSheetRef.dismiss(false); + } + + onYesClick(): void { + this.bottomSheetRef.dismiss(true); + } +} diff --git a/src/app/components/projects/projects.component.css b/src/app/components/projects/projects.component.css index d7d423ad..5ed0ed20 100644 --- a/src/app/components/projects/projects.component.css +++ b/src/app/components/projects/projects.component.css @@ -7,3 +7,9 @@ height: 40px; margin: 20px; } + +.full-width { + width: 940px; + margin-left: -470px; + left: 50%; +} diff --git a/src/app/components/projects/projects.component.html b/src/app/components/projects/projects.component.html index 7f785447..f53b130f 100644 --- a/src/app/components/projects/projects.component.html +++ b/src/app/components/projects/projects.component.html @@ -24,9 +24,19 @@
+ +
+ + + +
+
- + Name @@ -40,6 +50,9 @@ + diff --git a/src/app/components/projects/projects.component.spec.ts b/src/app/components/projects/projects.component.spec.ts index 29e82cec..c32b23e4 100644 --- a/src/app/components/projects/projects.component.spec.ts +++ b/src/app/components/projects/projects.component.spec.ts @@ -1,5 +1,5 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule } from '@angular/material'; +import { MatIconModule, MatSortModule, MatTableModule, MatTooltipModule, MatDialogModule, MatFormFieldModule, MatInputModule, MatDialogRef, MatDialogContainer, MatBottomSheetModule } from '@angular/material'; import { RouterTestingModule } from '@angular/router/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -16,6 +16,12 @@ import { ProgressService } from '../../common/progress/progress.service'; import { Server } from '../../models/server'; import { Settings } from '../../services/settings.service'; import { Project } from '../../models/project'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ProjectsFilter } from '../../filters/projectsFilter.pipe'; +import { ChooseNameDialogComponent } from './choose-name-dialog/choose-name-dialog.component'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { OverlayRef } from '@angular/cdk/overlay'; describe('ProjectsComponent', () => { let component: ProjectsComponent; @@ -36,6 +42,11 @@ describe('ProjectsComponent', () => { MatSortModule, MatDialogModule, NoopAnimationsModule, + MatFormFieldModule, + MatInputModule, + MatBottomSheetModule, + FormsModule, + ReactiveFormsModule, RouterTestingModule.withRoutes([]) ], providers: [ @@ -44,8 +55,11 @@ describe('ProjectsComponent', () => { { provide: SettingsService, useClass: MockedSettingsService }, ProgressService ], - declarations: [ProjectsComponent] - }).compileComponents(); + declarations: [ProjectsComponent, ChooseNameDialogComponent, ProjectsFilter], + schemas: [NO_ERRORS_SCHEMA] + }) + .overrideModule(BrowserDynamicTestingModule, { set: { entryComponents: [ChooseNameDialogComponent] } }) + .compileComponents(); serverService = TestBed.get(ServerService); settingsService = TestBed.get(SettingsService); @@ -85,6 +99,14 @@ describe('ProjectsComponent', () => { expect(mockedProjectService.delete).toHaveBeenCalled(); }); + it('should call list on refresh', () => { + mockedProjectService.list = jasmine.createSpy().and.returnValue(of([])); + + component.refresh(); + + expect(mockedProjectService.list).toHaveBeenCalled(); + }); + describe('ProjectComponent open', () => { let project: Project; diff --git a/src/app/components/projects/projects.component.ts b/src/app/components/projects/projects.component.ts index ab50f90f..06b035a8 100644 --- a/src/app/components/projects/projects.component.ts +++ b/src/app/components/projects/projects.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, ViewChild } from '@angular/core'; -import { ActivatedRoute, ParamMap } from '@angular/router'; -import { MatSort, MatSortable, MatDialog } from '@angular/material'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; +import { MatSort, MatSortable, MatDialog, MatBottomSheet } from '@angular/material'; import { DataSource } from '@angular/cdk/collections'; @@ -16,6 +16,8 @@ import { ProgressService } from '../../common/progress/progress.service'; import { ImportProjectDialogComponent } from './import-project-dialog/import-project-dialog.component'; import { AddBlankProjectDialogComponent } from './add-blank-project-dialog/add-blank-project-dialog.component'; +import { ChooseNameDialogComponent } from './choose-name-dialog/choose-name-dialog.component'; +import { NavigationDialogComponent } from './navigation-dialog/navigation-dialog.component'; @Component({ selector: 'app-projects', @@ -29,6 +31,8 @@ export class ProjectsComponent implements OnInit { displayedColumns = ['name', 'actions']; settings: Settings; + searchText: string = ''; + @ViewChild(MatSort, {static: true}) sort: MatSort; constructor( @@ -37,7 +41,9 @@ export class ProjectsComponent implements OnInit { private projectService: ProjectService, private settingsService: SettingsService, private progressService: ProgressService, - private dialog: MatDialog + public dialog: MatDialog, + private router: Router, + private bottomSheet: MatBottomSheet ) {} ngOnInit() { @@ -107,6 +113,19 @@ export class ProjectsComponent implements OnInit { ); } + duplicate(project: Project) { + const dialogRef = this.dialog.open(ChooseNameDialogComponent, { + width: '400px', + autoFocus: false + }); + let instance = dialogRef.componentInstance; + instance.server = this.server; + instance.project = project; + dialogRef.afterClosed().subscribe(() => { + this.refresh(); + }); + } + addBlankProject() { const dialogRef = this.dialog.open(AddBlankProjectDialogComponent, { width: '400px', @@ -117,15 +136,33 @@ export class ProjectsComponent implements OnInit { } importProject() { + let uuid: string = ''; const dialogRef = this.dialog.open(ImportProjectDialogComponent, { width: '400px', autoFocus: false }); let instance = dialogRef.componentInstance; instance.server = this.server; + const subscription = dialogRef.componentInstance.onImportProject.subscribe((projectId: string) => { + uuid = projectId; + }); dialogRef.afterClosed().subscribe(() => { this.refresh(); + subscription.unsubscribe(); + if (uuid) { + this.bottomSheet.open(NavigationDialogComponent); + let bottomSheetRef = this.bottomSheet._openedBottomSheetRef; + bottomSheetRef.instance.projectMessage = 'imported project'; + + const bottomSheetSubscription = bottomSheetRef.afterDismissed().subscribe((result: boolean) => { + if (result) { + this.projectService.open(this.server, uuid).subscribe(() => { + this.router.navigate(['/server', this.server.id, 'project', uuid]); + }); + } + }); + } }); } } @@ -151,7 +188,7 @@ export class ProjectDatabase { } export class ProjectDataSource extends DataSource { - constructor(private projectDatabase: ProjectDatabase, private sort: MatSort) { + constructor(public projectDatabase: ProjectDatabase, private sort: MatSort) { super(); } diff --git a/src/app/components/projects/save-project-dialog/save-project-dialog.component.css b/src/app/components/projects/save-project-dialog/save-project-dialog.component.css new file mode 100644 index 00000000..acf081db --- /dev/null +++ b/src/app/components/projects/save-project-dialog/save-project-dialog.component.css @@ -0,0 +1,7 @@ +.file-name-form-field { + width: 100%; +} + +.project-snackbar { + background: #2196F3; +} diff --git a/src/app/components/projects/save-project-dialog/save-project-dialog.component.html b/src/app/components/projects/save-project-dialog/save-project-dialog.component.html new file mode 100644 index 00000000..c772cb96 --- /dev/null +++ b/src/app/components/projects/save-project-dialog/save-project-dialog.component.html @@ -0,0 +1,23 @@ +

Save project as

+
+ + + Project name is required + Project name is incorrect + +
+ + +
+
diff --git a/src/app/components/projects/save-project-dialog/save-project-dialog.component.ts b/src/app/components/projects/save-project-dialog/save-project-dialog.component.ts new file mode 100644 index 00000000..97473c6e --- /dev/null +++ b/src/app/components/projects/save-project-dialog/save-project-dialog.component.ts @@ -0,0 +1,82 @@ +import { Component, OnInit, EventEmitter } from '@angular/core'; +import { Router } from '@angular/router'; +import { MatDialog, MatDialogRef } from '@angular/material'; +import { FormBuilder, FormGroup, Validators, FormControl } from '@angular/forms'; +import { Project } from '../../../models/project'; +import { Server } from '../../../models/server'; +import { ProjectService } from '../../../services/project.service'; +import { v4 as uuid } from 'uuid'; +import { ProjectNameValidator } from '../models/projectNameValidator'; +import { ToasterService } from '../../../services/toaster.service'; +import { NodesDataSource } from '../../../cartography/datasources/nodes-datasource'; + + +@Component({ + selector: 'app-save-project-dialog', + templateUrl: './save-project-dialog.component.html', + styleUrls: ['./save-project-dialog.component.css'], + providers: [ProjectNameValidator] +}) +export class SaveProjectDialogComponent implements OnInit { + server: Server; + project: Project; + projectNameForm: FormGroup; + onAddProject = new EventEmitter(); + + constructor( + public dialogRef: MatDialogRef, + private projectService: ProjectService, + private nodesDataSource: NodesDataSource, + private toasterService: ToasterService, + private formBuilder: FormBuilder, + private projectNameValidator: ProjectNameValidator + ) { + this.projectNameForm = this.formBuilder.group({ + projectName: new FormControl(null, [Validators.required, projectNameValidator.get]) + }); + } + + ngOnInit() {} + + get form() { + return this.projectNameForm.controls; + } + + onAddClick(): void { + if (this.projectNameForm.invalid) { + return; + } + this.projectService.list(this.server).subscribe((projects: Project[]) => { + const projectName = this.projectNameForm.controls['projectName'].value; + let existingProject = projects.find(project => project.name === projectName); + + if (existingProject) { + this.toasterService.error(`Project with this name already exists.`); + } else if (this.nodesDataSource.getItems().filter(node => + (node.status === 'started' && node.node_type==='vpcs') || + (node.status === 'started' && node.node_type==='virtualbox') || + (node.status === 'started' && node.node_type==='vmware')).length > 0) { + this.toasterService.error('Please stop all nodes in order to save project.') + } else { + this.addProject(); + } + }); + } + + onNoClick(): void { + this.dialogRef.close(); + } + + addProject(): void { + this.projectService.duplicate(this.server, this.project.project_id, this.projectNameForm.controls['projectName'].value).subscribe((project: Project) => { + this.dialogRef.close(); + this.toasterService.success(`Project ${project.name} added`); + }); + } + + onKeyDown(event) { + if (event.key === "Enter") { + this.onAddClick(); + } + } +} diff --git a/src/app/components/template/template-list-dialog/template-list-dialog.component.html b/src/app/components/template/template-list-dialog/template-list-dialog.component.html index 346b52d8..3eea74e5 100644 --- a/src/app/components/template/template-list-dialog/template-list-dialog.component.html +++ b/src/app/components/template/template-list-dialog/template-list-dialog.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/app/components/template/template-list-dialog/template-list-dialog.component.scss b/src/app/components/template/template-list-dialog/template-list-dialog.component.scss index 6c455eef..6b2a2818 100644 --- a/src/app/components/template/template-list-dialog/template-list-dialog.component.scss +++ b/src/app/components/template/template-list-dialog/template-list-dialog.component.scss @@ -16,3 +16,26 @@ font-size: 16px; flex-grow: 1; } + +div { + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +mat-table { + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +::-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; +} diff --git a/src/app/components/template/template-list-dialog/template-list-dialog.component.ts b/src/app/components/template/template-list-dialog/template-list-dialog.component.ts index dce539e3..471305f3 100644 --- a/src/app/components/template/template-list-dialog/template-list-dialog.component.ts +++ b/src/app/components/template/template-list-dialog/template-list-dialog.component.ts @@ -20,7 +20,7 @@ export class TemplateListDialogComponent implements OnInit { dataSource: TemplateDataSource; displayedColumns = ['name']; - @ViewChild('filter', {static: false}) filter: ElementRef; + @ViewChild('filter', {static: true}) filter: ElementRef; constructor( public dialogRef: MatDialogRef, diff --git a/src/app/components/topology-summary/topology-summary.component.html b/src/app/components/topology-summary/topology-summary.component.html new file mode 100644 index 00000000..83500e4f --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.html @@ -0,0 +1,68 @@ +
+
+ + + close +
+
+ +
+ Filter by status
+
+ started + suspended + stopped +
+ Show devices with
+
+ active capture(s) + active packet filters +
+
+
+ Sorting
+
+ + By name ascending + By name descending + +
+
+ +
+
+
+ + + + + + + {{node.name}} +
+
+ {{node.console_type}} {{node.console_host}}:{{node.console}} +
+
+ none +
+
+
+
+
+ +
+
+
+ {{compute.name}} +
+
+ {{compute.host}} +
+
+ {{server.location}} +
+
+
+
+
diff --git a/src/app/components/topology-summary/topology-summary.component.scss b/src/app/components/topology-summary/topology-summary.component.scss new file mode 100644 index 00000000..87512793 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.scss @@ -0,0 +1,113 @@ +.summaryWrapper { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); + position: fixed; + top: 20px; + right: 20px; + height: 400px; + width: 300px; + background: #263238; + color: white; + overflow: hidden; + font-size: 12px; +} + +.summaryHeaderMenu { + height: 24px; +} + +.summaryHeader { + width: 100%; + height: 34px; + display: flex; + justify-content: space-between; + margin-right: 5px; +} + +.summaryFilters { + margin-left: 5px; + margin-right: 5px; +} + +.summarySorting { + margin-left: 5px; + margin-right: 5px; +} + +.summaryContent { + margin-left: 5px; + margin-right: 5px; + max-height: 180px; + overflow: auto; + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +.summaryContentServers { + margin-left: 5px; + margin-right: 5px; + max-height: 350px; + overflow: auto; + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +.titleButton { + margin-left: 5px; + margin-top: 4px; + outline: none; +} + +.marked { + color: #0097a7; +} + +.divider { + margin: 5px; + width: 290px; + height: 2px; +} + +.nodeRow { + width: 100%; + display: flex; + justify-content: space-between; + padding-right: 5px; +} + +::-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; +} + +.radio-group-wrapper { + margin-top: 5px; +} + +.radio-group { + display: flex; + justify-content: space-between; +} + +.closeButton { + cursor: pointer; + font-size: 24px; + margin-top: 8px; + margin-right: 5px; +} + +.filterBox { + display: flex; + justify-content: space-between; +} + +.notvisible { + display: none; +} diff --git a/src/app/components/topology-summary/topology-summary.component.spec.ts b/src/app/components/topology-summary/topology-summary.component.spec.ts new file mode 100644 index 00000000..07369e75 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.spec.ts @@ -0,0 +1,160 @@ +import { TopologySummaryComponent } from "./topology-summary.component"; +import { ComponentFixture, async, TestBed } from '@angular/core/testing'; +import { ProjectService } from '../../services/project.service'; +import { MockedProjectService } from '../../services/project.service.spec'; +import { MatTableModule, MatTooltipModule, MatIconModule, MatSortModule, MatDialogModule } from '@angular/material'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { of } from 'rxjs'; +import { MockedNodesDataSource, MockedLinksDataSource } from '../project-map/project-map.component.spec'; +import { NodesDataSource } from '../../cartography/datasources/nodes-datasource'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Project } from '../../models/project'; +import { Node } from '../../cartography/models/node'; +import { Server } from '../../models/server'; +import { ComputeService } from '../../services/compute.service'; +import { LinksDataSource } from '../../cartography/datasources/links-datasource'; + +export class MockedComputeService { + getComputes(server: Server) { + return of([]); + } +} + +describe('TopologySummaryComponent', () => { + let component: TopologySummaryComponent; + let fixture: ComponentFixture; + let mockedProjectService: MockedProjectService = new MockedProjectService(); + let mockedNodesDataSource: MockedNodesDataSource = new MockedNodesDataSource(); + let mockedComputeService: MockedComputeService = new MockedComputeService(); + let mockedLinksDataSource: MockedLinksDataSource = new MockedLinksDataSource(); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatTableModule, + MatTooltipModule, + MatIconModule, + MatSortModule, + MatDialogModule, + NoopAnimationsModule, + RouterTestingModule.withRoutes([]) + ], + providers: [ + { provide: ProjectService, useValue: mockedProjectService }, + { provide: NodesDataSource, useValue: mockedNodesDataSource }, + { provide: ComputeService, useValue: mockedComputeService}, + { provide: LinksDataSource, useValue: mockedLinksDataSource } + ], + declarations: [TopologySummaryComponent], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TopologySummaryComponent); + component = fixture.componentInstance; + component.project = { project_id: 1 } as unknown as Project; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should show only running nodes when filter started applied', () => { + component.nodes = [ + { status: 'started' } as Node, + { status: 'stopped' } as Node + ]; + + component.applyStatusFilter(true, 'started'); + + expect(component.filteredNodes.length).toBe(1); + expect(component.filteredNodes[0].status).toBe('started'); + }); + + it('should show only stopped nodes when filter stopped applied', () => { + component.nodes = [ + { status: 'started' } as Node, + { status: 'stopped' } as Node + ]; + + component.applyStatusFilter(true, 'stopped'); + + expect(component.filteredNodes.length).toBe(1); + expect(component.filteredNodes[0].status).toBe('stopped'); + }); + + it('should show only suspended nodes when filter suspended applied', () => { + component.nodes = [ + { status: 'started' } as Node, + { status: 'stopped' } as Node + ]; + + component.applyStatusFilter(true, 'suspended'); + + expect(component.filteredNodes.length).toBe(0); + }); + + it('should show all nodes when all filters applied', () => { + component.nodes = [ + { status: 'started' } as Node, + { status: 'stopped' } as Node + ]; + + component.applyStatusFilter(true, 'suspended'); + component.applyStatusFilter(true, 'started'); + component.applyStatusFilter(true, 'stopped'); + + expect(component.filteredNodes.length).toBe(2); + }); + + it('should show all nodes when no filters applied', () => { + component.nodes = [ + { status: 'started' } as Node, + { status: 'stopped' } as Node + ]; + + component.applyStatusFilter(true, 'suspended'); + component.applyStatusFilter(true, 'started'); + component.applyStatusFilter(true, 'stopped'); + + expect(component.filteredNodes.length).toBe(2); + + component.applyStatusFilter(false, 'stopped'); + + expect(component.filteredNodes.length).toBe(1); + + component.applyStatusFilter(false, 'suspended'); + component.applyStatusFilter(false, 'started'); + + expect(component.filteredNodes.length).toBe(2); + }); + + it('should sort nodes in correct order', () => { + component.nodes = [ + { status: 'started', name: 'A' } as Node, + { status: 'stopped', name: 'B' } as Node, + { status: 'stopped', name: 'D' } as Node, + ]; + + component.applyFilters(); + component.setSortingOrder('asc'); + + expect(component.filteredNodes[0].name).toBe('A'); + }); + + it('should sort filtered nodes in correct order', () => { + component.nodes = [ + { status: 'started', name: 'A' } as Node, + { status: 'stopped', name: 'B' } as Node, + { status: 'stopped', name: 'D' } as Node, + ]; + + component.applyStatusFilter(true, 'stopped'); + component.setSortingOrder('desc'); + + expect(component.filteredNodes[0].name).toBe('D'); + }); +}); diff --git a/src/app/components/topology-summary/topology-summary.component.ts b/src/app/components/topology-summary/topology-summary.component.ts new file mode 100644 index 00000000..29405692 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.ts @@ -0,0 +1,192 @@ +import { Component, OnInit, OnDestroy, Input, AfterViewInit, Output, EventEmitter } from '@angular/core'; +import { Project } from '../../models/project'; +import { Server } from '../../models/server'; +import { NodesDataSource } from '../../cartography/datasources/nodes-datasource'; +import { Node } from '../../cartography/models/node'; +import { Subscription } from 'rxjs'; +import { ProjectService } from '../../services/project.service'; +import { ProjectStatistics } from '../../models/project-statistics'; +import { Compute } from '../../models/compute'; +import { ComputeService } from '../../services/compute.service'; +import { LinksDataSource } from '../../cartography/datasources/links-datasource'; + + +@Component({ + selector: 'app-topology-summary', + templateUrl: './topology-summary.component.html', + styleUrls: ['./topology-summary.component.scss'] +}) +export class TopologySummaryComponent implements OnInit, OnDestroy { + @Input() server: Server; + @Input() project: Project; + + @Output() closeTopologySummary = new EventEmitter(); + + private subscriptions: Subscription[] = []; + projectsStatistics: ProjectStatistics; + nodes: Node[] = []; + filteredNodes: Node[] = []; + sortingOrder: string = 'asc'; + startedStatusFilterEnabled: boolean = false; + suspendedStatusFilterEnabled: boolean = false; + stoppedStatusFilterEnabled: boolean = false; + captureFilterEnabled: boolean = false; + packetFilterEnabled: boolean = false; + computes: Compute[] = []; + isTopologyVisible: boolean = true; + + constructor( + private nodesDataSource: NodesDataSource, + private projectService: ProjectService, + private computeService: ComputeService, + private linksDataSource: LinksDataSource + ) {} + + ngOnInit() { + this.subscriptions.push( + this.nodesDataSource.changes.subscribe((nodes: Node[]) => { + this.nodes = nodes; + if (this.sortingOrder === 'asc') { + this.filteredNodes = nodes.sort(this.compareAsc); + } else { + this.filteredNodes = nodes.sort(this.compareDesc); + } + }) + ); + + this.projectService.getStatistics(this.server, this.project.project_id).subscribe((stats) => { + this.projectsStatistics = stats; + }); + + this.computeService.getComputes(this.server).subscribe((computes) => { + this.computes = computes; + }); + } + + toogleTopologyVisibility(value: boolean) { + this.isTopologyVisible = value; + } + + compareAsc(first: Node, second: Node) { + if (first.name < second.name) return -1; + return 1; + } + + compareDesc(first: Node, second: Node) { + if (first.name < second.name) return 1; + return -1; + } + + ngOnDestroy() { + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); + } + + setSortingOrder(order: string) { + this.sortingOrder = order; + if (this.sortingOrder === 'asc') { + this.filteredNodes = this.filteredNodes.sort(this.compareAsc); + } else { + this.filteredNodes = this.filteredNodes.sort(this.compareDesc); + } + } + + applyStatusFilter(value: boolean, filter: string) { + if (filter === 'started') { + this.startedStatusFilterEnabled = value; + } else if (filter === 'stopped') { + this.stoppedStatusFilterEnabled = value; + } else if (filter === 'suspended') { + this.suspendedStatusFilterEnabled = value; + } + this.applyFilters(); + } + + applyCaptureFilter(value: boolean, filter: string) { + if (filter === 'capture') { + this.captureFilterEnabled = value; + } else if (filter === 'packet') { + this.packetFilterEnabled = value; + } + this.applyFilters(); + } + + applyFilters() { + let nodes: Node[] = []; + + if (this.startedStatusFilterEnabled) { + nodes = nodes.concat(this.nodes.filter(n => n.status === 'started')); + } + + if (this.stoppedStatusFilterEnabled) { + nodes = nodes.concat(this.nodes.filter(n => n.status === 'stopped')); + } + + if (this.suspendedStatusFilterEnabled) { + nodes = nodes.concat(this.nodes.filter(n => n.status === 'suspended')); + } + + if (!this.startedStatusFilterEnabled && !this.stoppedStatusFilterEnabled && !this.suspendedStatusFilterEnabled) { + nodes = nodes.concat(this.nodes); + } + + if (this.captureFilterEnabled) { + nodes = this.checkCapturing(nodes); + } + + if(this.packetFilterEnabled) { + nodes = this.checkPacketFilters(nodes); + } + + if (this.sortingOrder === 'asc') { + this.filteredNodes = nodes.sort(this.compareAsc); + } else { + this.filteredNodes = nodes.sort(this.compareDesc); + } + } + + checkCapturing(nodes: Node[]): Node[] { + let links = this.linksDataSource.getItems(); + let nodesWithCapturing: string[] = []; + + links.forEach(link => { + if (link.capturing) { + link.nodes.forEach(node => { + nodesWithCapturing.push(node.node_id); + }); + } + }); + + let filteredNodes: Node[] = []; + nodes.forEach(node => { + if (nodesWithCapturing.includes(node.node_id)) { + filteredNodes.push(node); + } + }); + return filteredNodes; + } + + checkPacketFilters(nodes: Node[]): Node[] { + let links = this.linksDataSource.getItems(); + let nodesWithPacketFilters: string[] = []; + + links.forEach(link => { + if (link.filters.bpf || link.filters.corrupt || link.filters.corrupt || link.filters.packet_loss || link.filters.frequency_drop) { + link.nodes.forEach(node => { + nodesWithPacketFilters.push(node.node_id); + }); + } + }); + + let filteredNodes: Node[] = []; + nodes.forEach(node => { + if (nodesWithPacketFilters.includes(node.node_id)) { + filteredNodes.push(node); + } + }); + return filteredNodes; + } + + close() { + this.closeTopologySummary.emit(false); + } +} diff --git a/src/app/filters/projectsFilter.pipe.ts b/src/app/filters/projectsFilter.pipe.ts new file mode 100644 index 00000000..a0d637f4 --- /dev/null +++ b/src/app/filters/projectsFilter.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { ProjectDataSource } from '../components/projects/projects.component'; + + +@Pipe({ + name: 'projectsfilter' +}) +export class ProjectsFilter implements PipeTransform { + transform(items: ProjectDataSource, searchText: string) { + if(!items) return []; + if(!searchText) return items; + + searchText = searchText.toLowerCase(); + return items.projectDatabase.data.filter( item => { + return item.filename.toLowerCase().includes(searchText); + }); + } +} 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/material.imports.ts b/src/app/material.imports.ts index fdc33412..267561d2 100644 --- a/src/app/material.imports.ts +++ b/src/app/material.imports.ts @@ -20,7 +20,9 @@ import { MatStepperModule, MatRadioModule, MatGridListModule, - MatTabsModule + MatTabsModule, + MatTreeModule, + MatBottomSheetModule } from '@angular/material'; export const MATERIAL_IMPORTS = [ @@ -45,5 +47,7 @@ export const MATERIAL_IMPORTS = [ MatStepperModule, MatRadioModule, MatGridListModule, - MatTabsModule + MatTabsModule, + MatTreeModule, + MatBottomSheetModule ]; diff --git a/src/app/models/compute.ts b/src/app/models/compute.ts new file mode 100644 index 00000000..83c16bf2 --- /dev/null +++ b/src/app/models/compute.ts @@ -0,0 +1,19 @@ +export interface Capabilities { + node_types: string[]; + platform: string; + version: string; +} + +export interface Compute { + capabilities: Capabilities; + compute_id: string; + connected: boolean; + cpu_usage_percent: number; + host: string; + last_error?: any; + memory_usage_percent: number; + name: string; + port: number; + protocol: string; + user: string; +} 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/models/port.ts b/src/app/models/port.ts index 946fc56c..6b8c4429 100644 --- a/src/app/models/port.ts +++ b/src/app/models/port.ts @@ -1,5 +1,6 @@ export class Port { adapter_number: number; + adapter_type: string; link_type: string; name: string; port_number: number; diff --git a/src/app/models/project-statistics.ts b/src/app/models/project-statistics.ts new file mode 100644 index 00000000..c26566ce --- /dev/null +++ b/src/app/models/project-statistics.ts @@ -0,0 +1,6 @@ +export class ProjectStatistics { + drawings: number; + links: number; + nodes: number; + snapshots: number +} diff --git a/src/app/models/project.ts b/src/app/models/project.ts index 03760b35..d9e5d809 100644 --- a/src/app/models/project.ts +++ b/src/app/models/project.ts @@ -2,7 +2,9 @@ export class Project { auto_close: boolean; auto_open: boolean; auto_start: boolean; + drawing_grid_size: number; filename: string; + grid_size: number; name: string; path: string; project_id: string; diff --git a/src/app/models/qemu/qemu-img.ts b/src/app/models/qemu/qemu-img.ts new file mode 100644 index 00000000..c8d8d7d2 --- /dev/null +++ b/src/app/models/qemu/qemu-img.ts @@ -0,0 +1,12 @@ +export class QemuImg { + cluster_size: number; + format: string; + lazy_refcounts: string; + path: string; + preallocation: string; + qemu_img: string; + refcount_bits: number; + size: number; + subformat: string; + zeroed_grain: string; +} diff --git a/src/app/models/templates/traceng-template.ts b/src/app/models/templates/traceng-template.ts new file mode 100644 index 00000000..00eeb3c6 --- /dev/null +++ b/src/app/models/templates/traceng-template.ts @@ -0,0 +1,12 @@ +export interface TracengTemplate { + builtin: boolean; + category: string; + compute_id: string; + console_type: string; + default_name_format: string; + ip_address: string; + name: string; + symbol: string; + template_id: string; + template_type: string; +} diff --git a/src/app/services/compute.service.ts b/src/app/services/compute.service.ts new file mode 100644 index 00000000..6cc49de7 --- /dev/null +++ b/src/app/services/compute.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { HttpServer } from './http-server.service'; +import { Server } from '../models/server'; +import { Compute } from '../models/compute'; +import { Observable } from 'rxjs'; + +@Injectable() +export class ComputeService { + constructor(private httpServer: HttpServer) {} + + getComputes(server: Server): Observable { + return this.httpServer.get(server, '/computes') as Observable; + } +} diff --git a/src/app/services/drawing.service.spec.ts b/src/app/services/drawing.service.spec.ts index 4349c94e..2377de7f 100644 --- a/src/app/services/drawing.service.spec.ts +++ b/src/app/services/drawing.service.spec.ts @@ -111,6 +111,7 @@ describe('DrawingService', () => { drawing.z = 30; drawing.rotation = 0; drawing.svg = ''; + drawing.locked = false; service.update(server, drawing).subscribe(); @@ -121,7 +122,8 @@ describe('DrawingService', () => { y: 20, z: 30, rotation: 0, - svg: '' + svg: '', + locked: false }); })); diff --git a/src/app/services/drawing.service.ts b/src/app/services/drawing.service.ts index d883260b..5542fbd9 100644 --- a/src/app/services/drawing.service.ts +++ b/src/app/services/drawing.service.ts @@ -55,6 +55,7 @@ export class DrawingService { update(server: Server, drawing: Drawing): Observable { return this.httpServer.put(server, `/projects/${drawing.project_id}/drawings/${drawing.drawing_id}`, { + locked: drawing.locked, svg: drawing.svg, rotation: drawing.rotation, x: Math.round(drawing.x), 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/info.service.ts b/src/app/services/info.service.ts new file mode 100644 index 00000000..ed3860f4 --- /dev/null +++ b/src/app/services/info.service.ts @@ -0,0 +1,77 @@ +import { Injectable } from "@angular/core"; +import { Node } from '../cartography/models/node'; +import { Port } from '../models/port'; +import { Server } from '../models/server'; + +@Injectable() +export class InfoService { + getInfoAboutNode(node: Node, server: Server): string[] { + let infoList: string[] = []; + if (node.node_type === 'cloud') { + infoList.push(`Cloud ${node.name} is always on.`); + } else if (node.node_type === 'nat') { + infoList.push(`NAT ${node.name} is always on.`); + } else if (node.node_type === 'ethernet-hub') { + infoList.push(`Ethernet hub ${node.name} is always on.`); + } else if (node.node_type === 'ethernet_switch') { + infoList.push(`Ethernet switch ${node.name} is always on.`); + } else if (node.node_type === 'frame_relay_switch') { + infoList.push(`Frame relay switch ${node.name} is always on.`); + } else if (node.node_type === 'atm_switch') { + infoList.push(`ATM switch ${node.name} is always on.`); + } else if (node.node_type === 'docker') { + infoList.push(`Docker ${node.name} is ${node.status}.`); + } else if (node.node_type === 'dynamips') { + infoList.push(`Dynamips ${node.name} is always on.`); + } else if (node.node_type === 'traceng') { + infoList.push(`TraceNG ${node.name} is always on.`); + } else if (node.node_type === 'virtualbox') { + infoList.push(`VirtualBox VM ${node.name} is ${node.status}.`); + } else if (node.node_type === 'vmware') { + infoList.push(`VMware VM ${node.name} is ${node.status}.`); + } else if (node.node_type === 'qemu') { + infoList.push(`QEMU VM ${node.name} is ${node.status}.`); + } else if (node.node_type === 'iou') { + infoList.push(`IOU ${node.name} is always on.`); + } else if (node.node_type === 'vpcs') { + infoList.push(`Node ${node.name} is ${node.status}.`); + } + infoList.push(`Running on server ${server.name} with port ${server.port}.`); + infoList.push(`Server ID is ${server.id}.`); + if (node.console_type !== 'none' && node.console_type !== 'null') { + infoList.push(`Console is on port ${node.console} and type is ${node.console_type}.`); + } + infoList = infoList.concat(this.getInfoAboutPorts(node.ports)); + return infoList; + } + + getInfoAboutPorts(ports: Port[]): string { + let response: string = `Ports: ` + ports.forEach(port => { + response += `link_type: ${port.link_type}, + name: ${port.name}; ` + }); + response = response.substring(0, response.length - 2); + return response; + } + + getCommandLine(node: Node): string { + if (node.node_type === "cloud" || + node.node_type === "nat" || + node.node_type === "ethernet_hub" || + node.node_type === "ethernet_switch" || + node.node_type === "frame_relay_switch" || + node.node_type === "atm_switch" || + node.node_type === "dynamips" || + node.node_type === "traceng" || + node.node_type === "iou") { + return 'Command line information is not supported for this type of node.'; + } else { + if (node.status === 'started') { + return node.command_line; + } else { + return 'Please start the node in order to get the command line information.'; + } + } + } +} diff --git a/src/app/services/mapsettings.service.ts b/src/app/services/mapsettings.service.ts index 640c5671..ec9f8e54 100644 --- a/src/app/services/mapsettings.service.ts +++ b/src/app/services/mapsettings.service.ts @@ -2,12 +2,27 @@ import { Injectable } from "@angular/core"; import { Subject } from 'rxjs'; @Injectable() -export class MapSettingService { +export class MapSettingsService { public isMapLocked = new Subject(); + public isTopologySummaryVisible: boolean = false; + public isLogConsoleVisible: boolean = false; + public interfaceLabels: Map = new Map(); constructor() {} changeMapLockValue(value: boolean) { this.isMapLocked.next(value); } + + toggleTopologySummary(value: boolean) { + this.isTopologySummaryVisible = value; + } + + toggleLogConsole(value: boolean) { + this.isLogConsoleVisible = value; + } + + toggleShowInterfaceLabels(projectId: string, value: boolean) { + this.interfaceLabels.set(projectId, 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..8de3b672 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`, {}); } @@ -64,6 +72,12 @@ export class NodeService { }); } + updateSymbol(server: Server, node: Node, changedSymbol: string): Observable { + return this.httpServer.put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, { + symbol: changedSymbol + }); + } + update(server: Server, node: Node): Observable { return this.httpServer.put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, { x: Math.round(node.x), @@ -72,6 +86,26 @@ export class NodeService { }); } + updateNode(server: Server, node: Node): Observable { + return this.httpServer.put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, { + console_type: node.console_type, + console_auto_start: node.console_auto_start, + locked: node.locked, + name: node.name, + properties: node.properties + }); + } + + updateNodeWithCustomAdapters(server: Server, node: Node): Observable { + return this.httpServer.put(server, `/projects/${node.project_id}/nodes/${node.node_id}`, { + console_type: node.console_type, + console_auto_start: node.console_auto_start, + custom_adapters: node.custom_adapters, + name: node.name, + properties: node.properties + }); + } + delete(server: Server, node: Node) { return this.httpServer.delete(server, `/projects/${node.project_id}/nodes/${node.node_id}`); } @@ -84,4 +118,64 @@ export class NodeService { "z": node.z }); } + + getNode(server: Server, node: Node) { + return this.httpServer.get(server, `/projects/${node.project_id}/nodes/${node.node_id}`) + } + + getDefaultCommand(): string { + return `putty.exe -telnet \%h \%p -wt \"\%d\" -gns3 5 -skin 4`; + } + + getStartupConfiguration(server: Server, node: Node) { + let urlPath: string = `/projects/${node.project_id}/nodes/${node.node_id}`; + + if (node.node_type === 'vpcs') { + urlPath += '/files/startup.vpc'; + } else if (node.node_type === 'iou') { + urlPath += '/files/startup-config.cfg'; + } else if (node.node_type === 'dynamips') { + urlPath += `/files/configs/i${node.node_id}_startup-config.cfg`; + } + + return this.httpServer.get(server, urlPath, { responseType: 'text' as 'json'}); + } + + getPrivateConfiguration(server: Server, node: Node) { + let urlPath: string = `/projects/${node.project_id}/nodes/${node.node_id}`; + + if (node.node_type === 'iou') { + urlPath += '/files/private-config.cfg'; + } else if (node.node_type === 'dynamips') { + urlPath += `/files/configs/i${node.node_id}_private-config.cfg`; + } + + return this.httpServer.get(server, urlPath, { responseType: 'text' as 'json'}); + } + + saveConfiguration(server: Server, node: Node, configuration: string) { + let urlPath: string = `/projects/${node.project_id}/nodes/${node.node_id}` + + if (node.node_type === 'vpcs') { + urlPath += '/files/startup.vpc'; + } else if (node.node_type === 'iou') { + urlPath += '/files/startup-config.cfg'; + } else if (node.node_type === 'dynamips') { + urlPath += `/files/configs/i${node.node_id}_startup-config.cfg`; + } + + return this.httpServer.post(server, urlPath, configuration); + } + + savePrivateConfiguration(server: Server, node: Node, configuration: string) { + let urlPath: string = `/projects/${node.project_id}/nodes/${node.node_id}` + + if (node.node_type === 'iou') { + urlPath += '/files/private-config.cfg'; + } else if (node.node_type === 'dynamips') { + urlPath += `/files/configs/i${node.node_id}_private-config.cfg`; + } + + return this.httpServer.post(server, urlPath, configuration); + } } diff --git a/src/app/services/packet-capture.service.ts b/src/app/services/packet-capture.service.ts new file mode 100644 index 00000000..b6f36153 --- /dev/null +++ b/src/app/services/packet-capture.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from "@angular/core"; +import { Server } from '../models/server'; +import { Project } from '../models/project'; +import { Link } from '../models/link'; + +@Injectable() +export class PacketCaptureService { + constructor() {} + + startCapture(server: Server, project: Project, link: Link, name: string) { + location.assign(`gns3+pcap://${server.host}:${server.port}?project_id=${project.project_id}&link_id=${link.link_id}&name=${name}`); + } +} diff --git a/src/app/services/project.service.spec.ts b/src/app/services/project.service.spec.ts index 2752923a..0bcb1230 100644 --- a/src/app/services/project.service.spec.ts +++ b/src/app/services/project.service.spec.ts @@ -41,6 +41,14 @@ export class MockedProjectService { delete(server: Server, project_id: string) { return of(project_id); } + + duplicate(server: Server, project_id: string) { + return of(project_id); + } + + getStatistics(server: Server, project_id: string) { + return of({}); + } } describe('ProjectService', () => { @@ -132,6 +140,13 @@ describe('ProjectService', () => { expect(req.request.method).toEqual('DELETE'); }); + it('should duplicate the project', () => { + service.duplicate(server, 'projectId', 'projectName').subscribe(); + + const req = httpTestingController.expectOne('http://127.0.0.1:3080/v2/projects/projectId/duplicate'); + expect(req.request.method).toEqual('POST'); + }); + it('should get notifications path of project', () => { const path = service.notificationsPath(server, 'myproject'); expect(path).toEqual('ws://127.0.0.1:3080/v2/projects/myproject/notifications/ws'); diff --git a/src/app/services/project.service.ts b/src/app/services/project.service.ts index 4ca87f98..878a7f4e 100644 --- a/src/app/services/project.service.ts +++ b/src/app/services/project.service.ts @@ -45,10 +45,44 @@ export class ProjectService { return this.httpServer.post(server, `/projects`, { name: project_name, project_id: project_id }); } + update(server: Server, project: Project) : Observable { + return this.httpServer.put(server, `/projects/${project.project_id}`, { + auto_close: project.auto_close, + auto_open: project.auto_open, + auto_start: project.auto_start, + drawing_grid_size: project.drawing_grid_size, + grid_size: project.grid_size, + name: project.name, + scene_width: project.scene_width, + scene_height: project.scene_height, + show_interface_labels: project.show_interface_labels + }); + } + delete(server: Server, project_id: string): Observable { return this.httpServer.delete(server, `/projects/${project_id}`); } + getUploadPath(server: Server, uuid: string, project_name: string) { + return `http://${server.host}:${server.port}/v2/projects/${uuid}/import?name=${project_name}`; + } + + getExportPath(server: Server, project: Project) { + return `http://${server.host}:${server.port}/v2/projects/${project.project_id}/export`; + } + + export(server: Server, project_id: string): Observable { + return this.httpServer.get(server, `/projects/${project_id}/export`) + } + + getStatistics(server: Server, project_id: string): Observable { + return this.httpServer.get(server, `/projects/${project_id}/stats`); + } + + duplicate(server: Server, project_id: string, project_name): Observable { + return this.httpServer.post(server, `/projects/${project_id}/duplicate`, { name: project_name }); + } + notificationsPath(server: Server, project_id: string): string { return `ws://${server.host}:${server.port}/v2/projects/${project_id}/notifications/ws`; } diff --git a/src/app/services/qemu-configuration.service.ts b/src/app/services/qemu-configuration.service.ts index 8c410031..3e73903d 100644 --- a/src/app/services/qemu-configuration.service.ts +++ b/src/app/services/qemu-configuration.service.ts @@ -11,26 +11,48 @@ export class QemuConfigurationService { } getNetworkTypes() { - let networkTypes = [["e1000", "Intel Gigabit Ethernet"], - ["i82550", "Intel i82550 Ethernet"], - ["i82551", "Intel i82551 Ethernet"], - ["i82557a", "Intel i82557A Ethernet"], - ["i82557b", "Intel i82557B Ethernet"], - ["i82557c", "Intel i82557C Ethernet"], - ["i82558a", "Intel i82558A Ethernet"], - ["i82558b", "Intel i82558B Ethernet"], - ["i82559a", "Intel i82559A Ethernet"], - ["i82559b", "Intel i82559B Ethernet"], - ["i82559c", "Intel i82559C Ethernet"], - ["i82559er", "Intel i82559ER Ethernet"], - ["i82562", "Intel i82562 Ethernet"], - ["i82801", "Intel i82801 Ethernet"], - ["ne2k_pci", "NE2000 Ethernet"], - ["pcnet", "AMD PCNet Ethernet"], - ["rtl8139", "Realtek 8139 Ethernet"], - ["virtio", "Legacy paravirtualized Network I/O"], - ["virtio-net-pci", "Paravirtualized Network I/O"], - ["vmxnet3", "VMWare Paravirtualized Ethernet v3"]]; + // needs extending of custom adapter component + // let networkTypes = [["e1000", "Intel Gigabit Ethernet"], + // ["i82550", "Intel i82550 Ethernet"], + // ["i82551", "Intel i82551 Ethernet"], + // ["i82557a", "Intel i82557A Ethernet"], + // ["i82557b", "Intel i82557B Ethernet"], + // ["i82557c", "Intel i82557C Ethernet"], + // ["i82558a", "Intel i82558A Ethernet"], + // ["i82558b", "Intel i82558B Ethernet"], + // ["i82559a", "Intel i82559A Ethernet"], + // ["i82559b", "Intel i82559B Ethernet"], + // ["i82559c", "Intel i82559C Ethernet"], + // ["i82559er", "Intel i82559ER Ethernet"], + // ["i82562", "Intel i82562 Ethernet"], + // ["i82801", "Intel i82801 Ethernet"], + // ["ne2k_pci", "NE2000 Ethernet"], + // ["pcnet", "AMD PCNet Ethernet"], + // ["rtl8139", "Realtek 8139 Ethernet"], + // ["virtio", "Legacy paravirtualized Network I/O"], + // ["virtio-net-pci", "Paravirtualized Network I/O"], + // ["vmxnet3", "VMWare Paravirtualized Ethernet v3"]]; + + let networkTypes = ["e1000", "Intel Gigabit Ethernet", + "i82550", + "i82551", + "i82557a", + "i82557b", + "i82557c", + "i82558a", + "i82558b", + "i82559a", + "i82559b", + "i82559c", + "i82559er", + "i82562", + "i82801", + "ne2k_pci", + "pcnet", + "rtl8139", + "virtio", + "virtio-net-pci", + "vmxnet3"]; return networkTypes; } diff --git a/src/app/services/qemu.service.ts b/src/app/services/qemu.service.ts index d987016f..f82c0d50 100644 --- a/src/app/services/qemu.service.ts +++ b/src/app/services/qemu.service.ts @@ -5,6 +5,7 @@ import { QemuTemplate } from '../models/templates/qemu-template'; import { Server } from '../models/server'; import { QemuBinary } from '../models/qemu/qemu-binary'; import { QemuImage } from '../models/qemu/qemu-image'; +import { QemuImg } from '../models/qemu/qemu-img'; @Injectable() export class QemuService { @@ -26,6 +27,10 @@ export class QemuService { return this.httpServer.get(server, '/compute/qemu/images') as Observable; } + addImage(server: Server, qemuImg: QemuImg): Observable { + return this.httpServer.post(server, '/compute/qemu/img', qemuImg) as Observable; + } + addTemplate(server: Server, qemuTemplate: QemuTemplate): Observable { return this.httpServer.post(server, `/templates`, qemuTemplate) as Observable; } diff --git a/src/app/services/server.service.spec.ts b/src/app/services/server.service.spec.ts index 64d98b18..98eee8fe 100644 --- a/src/app/services/server.service.spec.ts +++ b/src/app/services/server.service.spec.ts @@ -79,10 +79,7 @@ describe('ServerService', () => { const upgradeCallback = openDatabaseSpy.calls.first().args[1]; upgradeCallback(evnt); - expect(evnt.currentTarget.result.createObjectStore).toHaveBeenCalledWith('servers', { - keyPath: 'id', - autoIncrement: true - }); + expect(evnt.currentTarget.result.createObjectStore).toHaveBeenCalled(); }); describe('operations on records', () => { diff --git a/src/app/services/symbol.service.ts b/src/app/services/symbol.service.ts index 3292645f..fba64d13 100644 --- a/src/app/services/symbol.service.ts +++ b/src/app/services/symbol.service.ts @@ -15,6 +15,10 @@ export class SymbolService { return this.symbols.getValue().find((symbol: Symbol) => symbol.symbol_id === symbol_id); } + add(server: Server, symbolName: string, symbol: string) { + return this.httpServer.post(server, `/symbols/${symbolName}/raw`, symbol) + } + load(server: Server): Observable { const subscription = this.list(server).subscribe((symbols: Symbol[]) => { const streams = symbols.map(symbol => this.raw(server, symbol.symbol_id)); diff --git a/src/app/services/template-mocks.service.ts b/src/app/services/template-mocks.service.ts index c61a0c02..e746ef0f 100644 --- a/src/app/services/template-mocks.service.ts +++ b/src/app/services/template-mocks.service.ts @@ -11,9 +11,27 @@ import { VmwareTemplate } from '../models/templates/vmware-template'; import { DockerTemplate } from '../models/templates/docker-template'; import { CustomAdapter } from '../models/qemu/qemu-custom-adapter'; import { IouTemplate } from '../models/templates/iou-template'; +import { TracengTemplate } from '../models/templates/traceng-template'; @Injectable() export class TemplateMocksService { + getTracengTemplate() : TracengTemplate { + let template: TracengTemplate = { + builtin: false, + category: 'guest', + compute_id: 'local', + console_type: 'none', + default_name_format: 'TraceNG{0}', + ip_address: '', + name: '', + symbol: ':/symbols/classic/traceng.svg', + template_id: '', + template_type: 'traceng' + }; + + return template; + } + getQemuTemplate() : Observable { let template : QemuTemplate = { adapter_type: 'e1000', diff --git a/src/app/services/traceng.service.spec.ts b/src/app/services/traceng.service.spec.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/services/traceng.service.ts b/src/app/services/traceng.service.ts new file mode 100644 index 00000000..2c33f12e --- /dev/null +++ b/src/app/services/traceng.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@angular/core"; +import { HttpServer } from './http-server.service'; +import { Server } from '../models/server'; +import { Observable } from 'rxjs'; +import { HttpHeaders } from '@angular/common/http'; +import { TracengTemplate } from '../models/templates/traceng-template'; + +@Injectable() +export class TracengService { + constructor(private httpServer: HttpServer) {} + + getTemplates(server: Server): Observable { + return this.httpServer.get(server, '/templates') as Observable; + } + + getTemplate(server: Server, template_id: string): Observable { + return this.httpServer.get(server, `/templates/${template_id}`) as Observable; + } + + addTemplate(server: Server, vpcsTemplate: TracengTemplate): Observable { + return this.httpServer.post(server, `/templates`, vpcsTemplate) as Observable; + } + + saveTemplate(server: Server, vpcsTemplate: TracengTemplate): Observable { + return this.httpServer.put(server, `/templates/${vpcsTemplate.template_id}`, vpcsTemplate) as Observable; + } +} diff --git a/src/styles.css b/src/styles.css index a10f8c16..4c521464 100644 --- a/src/styles.css +++ b/src/styles.css @@ -33,3 +33,7 @@ a.table-link { app-root { width: 100%; } + +mat-menu-panel { + min-height: 0px; +} diff --git a/yarn.lock b/yarn.lock index 0d799b27..8792bf00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,24 +7,32 @@ resolved "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.0.3.tgz#bc5b5532ecafd923a61f2fb097e3b108c0106a3f" integrity sha512-GLyWIFBbGvpKPGo55JyRZAo4lVbnBiD52cKlw/0Vt+wnmKvWJkpZvsjVoaIolyBXDeAQKSicRtqFNPem9w0WYA== -"@angular-devkit/architect@0.801.2": - version "0.801.2" - resolved "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.801.2.tgz#f3928e980dc9f3124da95291c810ebc6bfc46c13" - integrity sha512-gdPdT6y3TDA3hzTAlI3Ym8QB8Zj8kqAMzDwP1JSXxekF6md0qc+NK7WCu6Y+pj1Bbo5mXpxHBov4Xwv1l4STQA== +"@angular-devkit/architect@0.801.3": + version "0.801.3" + resolved "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.801.3.tgz#7afce7623baefb896367e7038f4c4922cc9c2ec5" + integrity sha512-gg6ZyJMiRYuzzmbpuDszrsE/hpwzoUnlOVoLNNzbACGBSDiqelC1mvGHb9JQM56Sy8gSjZn6RT0K2/Og79GoSg== dependencies: - "@angular-devkit/core" "8.1.2" + "@angular-devkit/core" "8.1.3" rxjs "6.4.0" -"@angular-devkit/build-angular@~0.801.2": - version "0.801.2" - resolved "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.801.2.tgz#1d1313877c15690cbcc12c33cb5b5cedf8989cde" - integrity sha512-PXwqvogl/brFjWhQMJoBTif5cGs5w1O/dahNaW3s9qbPGOg0E0nta+K8F/lL8x2pksslQi0jui6CeP1Yv+i1aA== +"@angular-devkit/architect@0.803.6": + version "0.803.6" + resolved "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.803.6.tgz#d933ac8c6599d589ba22172223b4c2309ca53cbb" + integrity sha512-8KWQa9xSG3wgNilFSa9zohpBw/phrn5Nv1Eq/jj1xoD5VH8+kYsHHD8YLbNrwwW6QujGdA+kgXtyWzJD30EG7A== dependencies: - "@angular-devkit/architect" "0.801.2" - "@angular-devkit/build-optimizer" "0.801.2" - "@angular-devkit/build-webpack" "0.801.2" - "@angular-devkit/core" "8.1.2" - "@ngtools/webpack" "8.1.2" + "@angular-devkit/core" "8.3.6" + rxjs "6.4.0" + +"@angular-devkit/build-angular@^0.801.3": + version "0.801.3" + resolved "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-0.801.3.tgz#357d16846a87bed5b2f6c1f91e48ebf8476f1025" + integrity sha512-BmN48xepRzQN2h2L7k9MhEWSAmEJj8celtD9Tu9Gn2DeM0yh2TFW9OKFKJlqyF3vGd3at24bqHOXI4MtDeltQQ== + dependencies: + "@angular-devkit/architect" "0.801.3" + "@angular-devkit/build-optimizer" "0.801.3" + "@angular-devkit/build-webpack" "0.801.3" + "@angular-devkit/core" "8.1.3" + "@ngtools/webpack" "8.1.3" ajv "6.10.0" autoprefixer "9.6.0" browserslist "4.6.3" @@ -69,30 +77,30 @@ webpack-subresource-integrity "1.1.0-rc.6" worker-plugin "3.1.0" -"@angular-devkit/build-optimizer@0.801.2": - version "0.801.2" - resolved "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.801.2.tgz#71b3b68bc2e09b4a9753fb7602cb7ff3be3da58d" - integrity sha512-BwbRn+11MpR4XjVLoFZZY1DAPCnft/5z6g6kfbTmoJNm6TD7+KypCEXw3MzdGt9vp085XJibc405R1QmmrOF+g== +"@angular-devkit/build-optimizer@0.801.3": + version "0.801.3" + resolved "https://registry.npmjs.org/@angular-devkit/build-optimizer/-/build-optimizer-0.801.3.tgz#f4620bd8a5a5db269a336e2e572770806ffb2e16" + integrity sha512-5wyzek1Ls7T9bh24yGDC/3Ss1YePpnOyBu0D8mJkByjPJjQr0xXR6UPx/7Idq6Y8BMeGO/+MiMOLZoUTPTIa0w== dependencies: loader-utils "1.2.3" source-map "0.5.6" typescript "3.4.5" webpack-sources "1.3.0" -"@angular-devkit/build-webpack@0.801.2": - version "0.801.2" - resolved "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.801.2.tgz#142ca0e7732a480001cc318369e2930e2307bee7" - integrity sha512-xy0MHLaXw4pz0NEg7fNyPjXdKkjeLAI6T2fnzYbLw3TJOqVe9y7p5uDLWa2/wp66mk34gcM/7A0ILqaIJ/ytGg== +"@angular-devkit/build-webpack@0.801.3": + version "0.801.3" + resolved "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.801.3.tgz#5d7f2cc43d13a36c3634d039dc27da1205f66302" + integrity sha512-IuR1WKldZwrAQWlKCLv+MnNeR1tWFCSJ9wXAgKqvlAPiYHEvTvazRVbWerxgVFvL4MCOt2wYVz/AqQWVYAhwlQ== dependencies: - "@angular-devkit/architect" "0.801.2" - "@angular-devkit/core" "8.1.2" + "@angular-devkit/architect" "0.801.3" + "@angular-devkit/core" "8.1.3" rxjs "6.4.0" webpack-merge "4.2.1" -"@angular-devkit/core@8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-8.1.2.tgz#8220725a2251a415e0588e65febb5bf2a931f471" - integrity sha512-sNkqXbkHE9+ObtLOYmDKJL1bOf1zY0AwGVKemgDqCmu1mRUNqhb7CmF13DRscfU3MEcuiJYDjXqBQDjIszrFiw== +"@angular-devkit/core@8.1.3": + version "8.1.3" + resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-8.1.3.tgz#2de64de07aebb1ff879abec976f2e698a30da712" + integrity sha512-Vj5fowuz27J+S74U1+MrSrJ7vI+OZC5HBOp4m7rrh/GcYlujcX3BUu0Bxi7LI1v90yDsr0s/iEAKsff05ByXiw== dependencies: ajv "6.10.0" fast-json-stable-stringify "2.0.0" @@ -100,65 +108,77 @@ rxjs "6.4.0" source-map "0.7.3" -"@angular-devkit/schematics@8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.1.2.tgz#2b8926d4af7992d6f99ddecd86cd81cb1a619d69" - integrity sha512-Di/3vPR4jwdYcMAk13t19sAF0qQUH8KSkFcmO/5E/gECTL1tXNvV690K1Vhn6zpeE17Z1MLB5HwRNcb6nJkD+Q== +"@angular-devkit/core@8.3.6": + version "8.3.6" + resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-8.3.6.tgz#6ad4787e3cb8b03234a194dd53e12cf054a4169c" + integrity sha512-kf4ViwjxERlyAnnrbenaUzPr0muixCyupzyiJ2RIuenK3ob9t1fnAsaugZt+Gfo54i3NgfBMKu1xNwnTR7HnAw== dependencies: - "@angular-devkit/core" "8.1.2" + ajv "6.10.2" + fast-json-stable-stringify "2.0.0" + magic-string "0.25.3" + rxjs "6.4.0" + source-map "0.7.3" + +"@angular-devkit/schematics@8.3.6": + version "8.3.6" + resolved "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-8.3.6.tgz#9a21a090208398a70e87d24a66e8d147f14713e0" + integrity sha512-5I4WDIMHw8zuajhXdy2xjtJLglMWE2Bo1Ri4wFR8cmj8nXUQ1fdPMWg3CqiepcNls2c8xXXMBMHZb/FhC32sBw== + dependencies: + "@angular-devkit/core" "8.3.6" rxjs "6.4.0" -"@angular/animations@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/animations/-/animations-8.1.2.tgz#2e4fec78a9345d9f1d93e9d464911c71f8c80046" - integrity sha512-szR5qzRe6vS1qrPhV2p5fMp5vQxT2SaljXGs3Xgt2Tl23om0XVNcqK0I8NNuK/ehuJ5LXQ1fJHniGcmN2aUw0g== +"@angular/animations@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/animations/-/animations-8.2.8.tgz#5ad0f0db0825b8a6fd46a240f45e3213e32ff732" + integrity sha512-Ye4umCQ82PbzbslqgxgI2Dhhg5VEEbDCgHbMx8x/gPEBkW5VED0CLxQknNtqNk1DPHZ656gdhOP9iygRGajFxA== dependencies: tslib "^1.9.0" -"@angular/cdk@^8.1.1": - version "8.1.1" - resolved "https://registry.npmjs.org/@angular/cdk/-/cdk-8.1.1.tgz#11b11bbab7316b3fa1f9eb380211bfde0a335cc7" - integrity sha512-5hBmhrHf9+WjGVIT8gbhT0Nh37BAjgI2TGRkt1o4qX8cG+1B6gU2MxM+CDJ7PhxSJi9lW93lq2AMuWwnRSllyg== +"@angular/cdk@^8.2.1": + version "8.2.2" + resolved "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.2.tgz#e7898a66dbf479edaed4e6c71cf37ddd83daa1fa" + integrity sha512-e+BtFab0Vd1q/ZVu6l850Q4vvgyVYiugSX31oMRlp86fKHPowlAO7jL3z5JcAG7TybpLIqd7oqF8XQBR/yw83w== dependencies: tslib "^1.7.1" optionalDependencies: parse5 "^5.0.0" -"@angular/cli@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/cli/-/cli-8.1.2.tgz#ac94203e89578d5edef1b860756aa0c33ae52abc" - integrity sha512-Zz9WGqPgr+w9SfpDeKLPzGFLZaX7uu2kU7/r6vxvnESJcnoxKOJBf+ipXu42TY7D3FtSiPBO27GBTSVgPCseoQ== +"@angular/cli@^8.3.6": + version "8.3.6" + resolved "https://registry.npmjs.org/@angular/cli/-/cli-8.3.6.tgz#dbbfef581b10bdc9a255c71d304676743b947cd4" + integrity sha512-MFMx+NEoN9QIiZlOCoGMWkh7tqVmZUrJ3SbuWvuXFZFyRQp7Y7+9CLhGKl7oUHtsA+MSs1rTRiwsi7ZnPKRDaQ== dependencies: - "@angular-devkit/architect" "0.801.2" - "@angular-devkit/core" "8.1.2" - "@angular-devkit/schematics" "8.1.2" - "@schematics/angular" "8.1.2" - "@schematics/update" "0.801.2" + "@angular-devkit/architect" "0.803.6" + "@angular-devkit/core" "8.3.6" + "@angular-devkit/schematics" "8.3.6" + "@schematics/angular" "8.3.6" + "@schematics/update" "0.803.6" "@yarnpkg/lockfile" "1.1.0" - ansi-colors "4.1.0" + ansi-colors "4.1.1" debug "^4.1.1" ini "1.3.5" - inquirer "6.4.1" + inquirer "6.5.1" npm-package-arg "6.1.0" + npm-pick-manifest "3.0.2" open "6.4.0" - pacote "9.5.1" + pacote "9.5.5" read-package-tree "5.3.1" - semver "6.2.0" + semver "6.3.0" symbol-observable "1.2.0" universal-analytics "^0.4.20" uuid "^3.3.2" -"@angular/common@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/common/-/common-8.1.2.tgz#43a7fd1179d179cae9142a3e6ae60a26a34dd062" - integrity sha512-bywFofN5RjcvygYEC/3eo+bfUnYBmARA6DPau8fm6D2ZGpXrWXJ3Thd99ZesuuffvpniaIHlAjbHGI83XSnixQ== +"@angular/common@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/common/-/common-8.2.8.tgz#96961a74b2d90ad5d30d82d3bafcfd26ed34a3a6" + integrity sha512-kfwf/NBWrHCusOb9JKlkAURlbeOSy3wfr2Hhj2SanudTbNpR1aInnwNYl1ZOHKSVHHvZOrpm2iuUEhDdN5DQgg== dependencies: tslib "^1.9.0" -"@angular/compiler-cli@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.1.2.tgz#1b20c3e53520bf8bbece4ba88171dec79eb78e9c" - integrity sha512-Dxm99iuv265AlUf3aX3nRl+Iqrj3RvlQgPOYLsV1EEVnA2+4Mjj52zbKgdOOOfhCF48imVbaU45Sh8p2l1xdOw== +"@angular/compiler-cli@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-8.2.8.tgz#3e235004b9fc1acc7923fe01445374e32d6fd6c6" + integrity sha512-x2szJSLOArrkpl75tAnPyCGAY1wMt6IfeSxpTNf798IDnVwSeTbq0BiZ/4Phj2k+r+EfmKelj6BDzr+nwEe9wg== dependencies: canonical-path "1.0.0" chokidar "^2.1.1" @@ -171,24 +191,31 @@ tslib "^1.9.0" yargs "13.1.0" -"@angular/compiler@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/compiler/-/compiler-8.1.2.tgz#db91a652a5c725b553ef946976446121c024bd0b" - integrity sha512-oRkHrstOV6imbb4mGf6q20d4N4iYfBbI6WfxtPL4dz08GipGg4Zvekn4e3R01vzhFBxssGcgmeEtFQJh/UzI8g== +"@angular/compiler@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/compiler/-/compiler-8.2.8.tgz#625ba8680f7cef6fa74d68e43e2b83197d61c017" + integrity sha512-+cSkx7Gd5srOUtj0VYVxM06LGqzZI4QPmkuu350+PLxbJke8o9bJBplCBYqf4E9riCrYLSsCFXcAQrTOL0mdtA== dependencies: tslib "^1.9.0" -"@angular/core@>=4.3.1", "@angular/core@^8.1.2": +"@angular/core@>=4.3.1": version "8.1.2" resolved "https://registry.npmjs.org/@angular/core/-/core-8.1.2.tgz#d05a4965093a9ce7e7776088dc2b9e7e885e8d9f" integrity sha512-Gm/UIUnIkeah39vxi4enVH/CUcPZOgGDyw4RNagw4pH8dTP8V0RUz8uteOr3DS+Eh49BcHkrT2oU5MBZSZ3lvw== dependencies: tslib "^1.9.0" -"@angular/forms@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/forms/-/forms-8.1.2.tgz#49e1327b431adb7533a31435fc41bfea5cf502bf" - integrity sha512-DHqbWt6AGnLkNajLZUAH4yQrxZdgUkjzEW6oxwvS2PxmLIrppz4TYWizfAVQndZ1Ddl7Eo1zRoRzqqHT90XyGA== +"@angular/core@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/core/-/core-8.2.8.tgz#ed51b122ba62348f1277f4168ab5ea4263e7fe5a" + integrity sha512-LlHgqlDCt+vO/B6LBZtDfSrzTqaB5w/gp3ZWUVrPjeY5GDky6P0ZSVTnRL/uUi49wBFJehMaNoGEoORqnQBOLA== + dependencies: + tslib "^1.9.0" + +"@angular/forms@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/forms/-/forms-8.2.8.tgz#74821a051892146967ec27022f67ecb7fa9ecedf" + integrity sha512-m7pHD8rv7koUCkSr+NannjzuN+fqAn2QSWEz77OciUV4weRafN0woqgazGvyw2s/f7yUYREqqUHH3WD1wsm34g== dependencies: tslib "^1.9.0" @@ -199,36 +226,36 @@ dependencies: tslib "^1.9.0" -"@angular/language-service@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/language-service/-/language-service-8.1.2.tgz#bdc7a9664ebef837ebaaeb583f66704e06fbeabe" - integrity sha512-9DR5TclsEpMIzCmagLHKYDTAqcZUkZKPjkngqIAUJg5R4IUjsuYn8NZX+agoOrS4ky6Dy9FXGYUC+QB0iEiycg== +"@angular/language-service@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/language-service/-/language-service-8.2.8.tgz#0fb464962423f420b7dbbb442e9458cfdbae2236" + integrity sha512-wXcj5eMz72fviqwwEGrks1zLT/5bRelFmwzqt3i7TX8gClWrtiHJtKZsMADpINkP1IVNn+d96ZVxxC6+0R+w5g== -"@angular/material@^8.1.1": - version "8.1.1" - resolved "https://registry.npmjs.org/@angular/material/-/material-8.1.1.tgz#87e105fb657fa6e139ddcbd6b9c373936604f6d7" - integrity sha512-45aaxKuLTrthzhAhG2+OY86wafuRBteZcRjDG7rKZ3Cc3KteUp5QwAi+QbhHzs4O3WXLWTAmuLYJelRqRqqw7g== +"@angular/material@^8.2.1": + version "8.2.2" + resolved "https://registry.npmjs.org/@angular/material/-/material-8.2.2.tgz#c2a1773d604304f614f20bdb1c6eba1cce351f3f" + integrity sha512-mR2ppE+Z1S5As2SUFK8wUH76Fj7YgrefhrwVGaeCLcAen//RHPw043+KL2apPAUaltdIFlGFtUuA6yJN6av0nQ== dependencies: tslib "^1.7.1" -"@angular/platform-browser-dynamic@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.1.2.tgz#10297054900daf8728e9ad8531d7ea40d1270bc7" - integrity sha512-NmbGMwKPbYq3ZFt6nOqRslJsQNRS2E94cjkSLseEb5wauUmdUBX9stoHu8BOhvd+EIEcYhD7uxPB+L/qPsH46g== +"@angular/platform-browser-dynamic@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.2.8.tgz#3d0c8e0c7bd227134e5cd533a569c4b6f8196f59" + integrity sha512-ytFRw5CVupIqg0tQPjTY7Qj3Ablvhoq5ilUEbHXmf+/3ce8e4eLQetmu5Oc8XeL3MQRByYPeFlMmAyReFauJnA== dependencies: tslib "^1.9.0" -"@angular/platform-browser@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.1.2.tgz#5e136f10656a950d5a8e65da68a7270fadc83875" - integrity sha512-n61OtH3B0e+LTHCfHPjB7hiuo0ZxKxZvNWigczGyLZf2abga5jac2bNrdZnU8zXC44AUfasUD2qDS2IPIhNbqA== +"@angular/platform-browser@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-8.2.8.tgz#5abf66fdfb7b25dcfe9834192980ad297b355df7" + integrity sha512-BeKRlysfBuLar8q98soHdy/SK9lOocwWJtwnyjzun/Gl9RrWYl1SkiBrGvnY9NUdt5LxpdJcBQBfNk0yET45QQ== dependencies: tslib "^1.9.0" -"@angular/router@^8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@angular/router/-/router-8.1.2.tgz#83dbab106918225da9a3e03945380483f244085e" - integrity sha512-+SWoYZHyDBBUydDTbIu+hyoGzWtSA4VUsriUPWEOCplzQiabFhWxVvcT00mO0cim4XfupL1tmiPjE66sivLYBw== +"@angular/router@^8.2.8": + version "8.2.8" + resolved "https://registry.npmjs.org/@angular/router/-/router-8.2.8.tgz#4058e239c2682845bd06b69be77e7b920c1ad87e" + integrity sha512-61ypQFV5UGPlpD09Dwr39YAYvAUkEQTDBXay69HaNRpyuzGyOJoygTuF06908uGmEOpFtZaxI+DH7OVdhD9ecA== dependencies: tslib "^1.9.0" @@ -328,37 +355,37 @@ ajv "^6.1.0" ajv-keywords "^3.1.0" -"@ngtools/webpack@8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.1.2.tgz#5f96371d51a7f338a29990916b5b77f55bbf1ca6" - integrity sha512-xA1SZI6BiEqmfxyaxw2TobF3gkZdfyEeCP1zhPm38PjQ1zU7Xx3TbvxTeMKGV3EOTMxvUETDiS65YJrIvGnaNA== +"@ngtools/webpack@8.1.3": + version "8.1.3" + resolved "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.1.3.tgz#9df049b14539109b60590a1ca64cbd80b6e49909" + integrity sha512-gSsLMdCJsQp7ZKF5Tl/yfns1eMtmc89J+yoWiOLoSHb8cupP2G4o7DL8wGkylsyALu5eomF/RSnivC9SGvdxYQ== dependencies: - "@angular-devkit/core" "8.1.2" + "@angular-devkit/core" "8.1.3" enhanced-resolve "4.1.0" rxjs "6.4.0" tree-kill "1.2.1" webpack-sources "1.3.0" -"@schematics/angular@8.1.2": - version "8.1.2" - resolved "https://registry.npmjs.org/@schematics/angular/-/angular-8.1.2.tgz#bd81b6f1131697d16dfbb6b275c24dad9cc41c83" - integrity sha512-BeEzuS0s4j+BPboUhl97VMfhj7V+HpNbbY3PkD3TLd0cnSEoaLmtX+YjxbxZgwk6vhDp+l6YtpWt//5H/+0rFQ== +"@schematics/angular@8.3.6": + version "8.3.6" + resolved "https://registry.npmjs.org/@schematics/angular/-/angular-8.3.6.tgz#409873ef29affac0a1851f88db312fedc7528e57" + integrity sha512-2M2UPWw5YscOu1qw48qjef02akwzQh1SHO0L9LnAcPJrpXG2Q/7QoPnjIN6J95JsF+ZbpmFXK0i5rGlnvOEXpA== dependencies: - "@angular-devkit/core" "8.1.2" - "@angular-devkit/schematics" "8.1.2" + "@angular-devkit/core" "8.3.6" + "@angular-devkit/schematics" "8.3.6" -"@schematics/update@0.801.2": - version "0.801.2" - resolved "https://registry.npmjs.org/@schematics/update/-/update-0.801.2.tgz#cb6466f8eeb7644ad725dbe47e8c4446669db0d9" - integrity sha512-xb54QXvII1JLdqgEqsh6mWu5qTt5UezmOWTZayRegsj0vNlzWFzoLXpiPFCWVEKUODa6aV4O5XW5CiQuVYPVuQ== +"@schematics/update@0.803.6": + version "0.803.6" + resolved "https://registry.npmjs.org/@schematics/update/-/update-0.803.6.tgz#8cb39b8a9385bedff9be6b651244accf7e603084" + integrity sha512-iIg2nrT3CsC85NAxtfb6daSPaQzthn2uKKeq6ifaiwHIIm9mmd2MbHcV8AWjf9DZ/XXa5nZREX47V1yanUZ3sg== dependencies: - "@angular-devkit/core" "8.1.2" - "@angular-devkit/schematics" "8.1.2" + "@angular-devkit/core" "8.3.6" + "@angular-devkit/schematics" "8.3.6" "@yarnpkg/lockfile" "1.1.0" ini "1.3.5" - pacote "9.5.1" + pacote "9.5.5" rxjs "6.4.0" - semver "6.2.0" + semver "6.3.0" semver-intersect "1.4.0" "@sentry/browser@4.6.2 || ~4.6.4": @@ -371,10 +398,10 @@ "@sentry/utils" "4.6.5" tslib "^1.9.3" -"@sentry/cli@^1.47.0": - version "1.47.0" - resolved "https://registry.npmjs.org/@sentry/cli/-/cli-1.47.0.tgz#7e29b06400511f2948429f7da89f7744216c5ecb" - integrity sha512-QTl2mqHIW5Q9JWofFkNG0aPCo4qnyZdaMatVYxrDAKiqapX6Q1Y84S9qofjBuo87ELjd9hD7UeDcbLEqqGuIKA== +"@sentry/cli@^1.47.2": + version "1.47.2" + resolved "https://registry.npmjs.org/@sentry/cli/-/cli-1.47.2.tgz#75a23d2816300b333d4e34163c85feeef93bc062" + integrity sha512-bUJGyxZQzB8mqnCL3RoeLl169XleoLnn2a1w/xqc8IOBt70IEtpVQQ86jgpZfpY1HqPZlczqEkm1U5kfjCaqLw== dependencies: fs-copy-file-sync "^1.1.1" https-proxy-agent "^2.2.1" @@ -394,10 +421,10 @@ "@sentry/utils" "4.6.5" tslib "^1.9.3" -"@sentry/electron@^0.17.3": - version "0.17.3" - resolved "https://registry.npmjs.org/@sentry/electron/-/electron-0.17.3.tgz#64c860d81602cdfe83aaa6555ea5aa40defc12a2" - integrity sha512-9V3j+5tS8PPk4LQEfs8EoyjZSCs23R2fq9ImG6afBm7+shF/XwbT8ZIyALPIGdfOKSNoTqD8j6N11XMMl32hOQ== +"@sentry/electron@^0.17.4": + version "0.17.4" + resolved "https://registry.npmjs.org/@sentry/electron/-/electron-0.17.4.tgz#0684588d2b2aaed1098c6ff7789dfc4266ed1162" + integrity sha512-1IU0o+E8eY5Lrthj6Pqf+Dh8MptddHsFFmcOwKlft/bbZ+6RTKEefLtFOclKUMLR64C7GTqa80Yddq0ssjOv5w== dependencies: "@sentry/browser" "4.6.2 || ~4.6.4" "@sentry/core" "4.6.2 || ~4.6.4" @@ -695,15 +722,20 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/jasmine@*", "@types/jasmine@~3.3.15": +"@types/jasmine@*": version "3.3.15" resolved "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.3.15.tgz#545be0670e828ac570566c45be570bbffcbc66d8" integrity sha512-MljubxUVLT9wh/0NiyRWlcxSu/0axK+UYheZ04N5yaQclQkE8JuFYMsmwltPap2LEsJrvSf5p49/1514IY+d1Q== -"@types/jasminewd2@~2.0.6": - version "2.0.6" - resolved "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.6.tgz#2f57a8d9875a6c9ef328a14bd070ba14a055ac39" - integrity sha512-2ZOKrxb8bKRmP/po5ObYnRDgFE4i+lQiEB27bAMmtMWLgJSqlIDqlLx6S0IRorpOmOPRQ6O80NujTmQAtBkeNw== +"@types/jasmine@^3.3.16": + version "3.4.2" + resolved "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.2.tgz#49f672de24043b3c1fb919901fd3cd36f027bc93" + integrity sha512-SaSSGOzwUnBEn64c+HTyVTJhRf8F1CXZLnxYx2ww3UrgGBmEEw38RSux2l3fYiT9brVLP67DU5omWA6V9OHI5Q== + +"@types/jasminewd2@^2.0.7": + version "2.0.8" + resolved "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.8.tgz#67afe5098d5ef2386073a7b7384b69a840dfe93b" + integrity sha512-d9p31r7Nxk0ZH0U39PTH0hiDlJ+qNVGjlt1ucOoTUptxb2v+Y5VMnsxfwN+i3hK4yQnqBi3FMmoMFcd1JHDxdg== dependencies: "@types/jasmine" "*" @@ -717,7 +749,7 @@ resolved "https://registry.npmjs.org/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d" integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew== -"@types/node@*", "@types/node@~12.6.8": +"@types/node@*": version "12.6.8" resolved "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz#e469b4bf9d1c9832aee4907ba8a051494357c12c" integrity sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg== @@ -727,6 +759,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.14.13.tgz#ac786d623860adf39a3f51d629480aacd6a6eec7" integrity sha512-yN/FNNW1UYsRR1wwAoyOwqvDuLDtVXnaJTZ898XIw/Q5cCaeVAlVwvsmXLX5PuiScBYwZsZU4JYSHB3TvfdwvQ== +"@types/node@^12.6.9": + version "12.7.9" + resolved "https://registry.npmjs.org/@types/node/-/node-12.7.9.tgz#da0210f91096aa67138cf5afd04c4d629f8a406a" + integrity sha512-P57oKTJ/vYivL2BCfxCC5tQjlS8qW31pbOL6qt99Yrjm95YdHgNZwjrTTjMBh+C2/y6PXIX4oz253+jUzxKKfQ== + "@types/q@^0.0.32": version "0.0.32" resolved "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" @@ -999,6 +1036,16 @@ ajv@6.10.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@6.10.2, ajv@^6.1.0, ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + ajv@^5.0.0: version "5.5.2" resolved "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" @@ -1009,16 +1056,6 @@ ajv@^5.0.0: fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.3.0" -ajv@^6.1.0, ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -1029,10 +1066,10 @@ angular-persistence@^1.0.1: resolved "https://registry.npmjs.org/angular-persistence/-/angular-persistence-1.0.1.tgz#79ffe7317f1f7aed88e69f07705f0ac32ccdb9da" integrity sha1-ef/nMX8feu2I5p8HcF8KwyzNudo= -angular2-hotkeys@^2.1.4: - version "2.1.4" - resolved "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.4.tgz#7411601aea425fada77a6f1274018cb6b8961afe" - integrity sha512-/KzgsrFjodoeZosXqsx1IvUo3rWBalSJ3QyVz2EALj1C0Woz84iNtXPZnlzuPNHrCmHcfOu28BNvIGBa+9Ving== +angular2-hotkeys@^2.1.5: + version "2.1.5" + resolved "https://registry.npmjs.org/angular2-hotkeys/-/angular2-hotkeys-2.1.5.tgz#d4d5df7cecd231d556089832609283f37674fdea" + integrity sha512-HiAnK1pW7lns5LpxtRsdkRRb5iVa7fv8Cf69Jye6l9gI6/IyvaVDptRtsWmdIG7VAr2Ngz6Yeehkym39O/LdgA== dependencies: "@types/mousetrap" "^1.6.0" mousetrap "^1.6.0" @@ -1051,20 +1088,22 @@ ansi-align@^3.0.0: dependencies: string-width "^3.0.0" -ansi-colors@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.0.tgz#e1674bd61027c8c219b9edec929b6b57641b0c09" - integrity sha512-3NkLpm6I6kEgC8J0I9EZ0fouXc/mm5J9zqJFCgA2jGqmsL0O64I7Uvi3AmUMnnRqc6u7uLgVVnY4pyBQ03nCiw== +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" + integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q== + dependencies: + type-fest "^0.5.2" ansi-html@0.0.7: version "0.0.7" @@ -1813,7 +1852,7 @@ bytes@3.1.0: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@^11.3.2, cacache@^11.3.3: +cacache@^11.3.2: version "11.3.3" resolved "https://registry.npmjs.org/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc" integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA== @@ -1833,6 +1872,27 @@ cacache@^11.3.2, cacache@^11.3.3: unique-filename "^1.1.1" y18n "^4.0.0" +cacache@^12.0.0, cacache@^12.0.2: + version "12.0.3" + resolved "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" + integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2044,12 +2104,12 @@ cli-boxes@^2.2.0: resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: - restore-cursor "^2.0.0" + restore-cursor "^3.1.0" cli-width@^2.0.0: version "2.2.0" @@ -2115,10 +2175,10 @@ code-point-at@^1.0.0: resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codelyzer@~5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/codelyzer/-/codelyzer-5.1.0.tgz#a0eb89497622679ea4c53b6974aa45598f06a4b5" - integrity sha512-QiyY2/oDQnYx4mAVEDqr+z9MwrOto18tQFjExiuRChXCy0yvngS5fQpWIxvAGpbOmZFiR1PRTRLbEI71u10maA== +codelyzer@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/codelyzer/-/codelyzer-5.1.2.tgz#e6c08269f8796483e57e6d9b7c29723572472b1d" + integrity sha512-1z7mtpwxcz5uUqq0HLO0ifj/tz2dWEmeaK+8c5TEZXAwwVxrjjg0118ODCOCCOcpfYaaEHxStNCaWVYo9FUPXw== dependencies: app-root-path "^2.2.1" aria-query "^3.0.0" @@ -2367,7 +2427,7 @@ copy-webpack-plugin@5.0.3: serialize-javascript "^1.7.0" webpack-log "^2.0.0" -core-js@3.1.4, core-js@^3.1.3, core-js@^3.1.4: +core-js@3.1.4, core-js@^3.1.3: version "3.1.4" resolved "https://registry.npmjs.org/core-js/-/core-js-3.1.4.tgz#3a2837fc48e582e1ae25907afcd6cf03b0cc7a07" integrity sha512-YNZN8lt82XIMLnLirj9MhKDFZHalwzzrL9YLt6eb0T5D0EDl4IQ90IGkua8mHbnxNrkj1d8hbdizMc0Qmg1WnQ== @@ -2377,6 +2437,11 @@ core-js@^2.4.0: resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== +core-js@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" + integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2487,10 +2552,10 @@ css-selector-tokenizer@^0.7.1: fastparse "^1.1.1" regexpu-core "^1.0.0" -css-tree@^1.0.0-alpha.33: - version "1.0.0-alpha.33" - resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.33.tgz#970e20e5a91f7a378ddd0fc58d0b6c8d4f3be93e" - integrity sha512-SPt57bh5nQnpsTBsx/IXbO14sRc9xXu5MtMAVuo0BaQQmyf0NupNPPSoMaqiAF5tDFafYsTkfeH4Q/HCKXkg4w== +css-tree@^1.0.0-alpha.34: + version "1.0.0-alpha.34" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.34.tgz#9b3a774cce553391604e62276670518e670c0b27" + integrity sha512-JMKJi4h8WkQ+HPjsCUvFnIhGF0I7Jr+J4a+NcHOApyGIBjvx4/hbhk+oKMXydv+OCmVyKBp0hqhHpj5Z61tyMg== dependencies: mdn-data "2.0.4" source-map "^0.5.3" @@ -3268,6 +3333,11 @@ emoji-regex@^7.0.1: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -3662,10 +3732,10 @@ figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= +figures@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz#756275c964646163cc6f9197c7a0295dbfd04de9" + integrity sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g== dependencies: escape-string-regexp "^1.0.5" @@ -3677,6 +3747,11 @@ file-loader@4.0.0: loader-utils "^1.2.2" schema-utils "^1.0.0" +file-saver@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a" + integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw== + fileset@^2.0.3: version "2.0.3" resolved "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" @@ -4475,6 +4550,11 @@ indexof@0.0.1: resolved "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= +infer-owner@^1.0.3, infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4503,22 +4583,22 @@ ini@1.3.5, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@6.4.1: - version "6.4.1" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz#7bd9e5ab0567cd23b41b0180b68e0cfa82fc3c0b" - integrity sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw== +inquirer@6.5.1: + version "6.5.1" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.1.tgz#8bfb7a5ac02dac6ff641ac4c5ff17da112fcdb42" + integrity sha512-uxNHBeQhRXIoHWTSNYUFhQVrHYFThIt6IVo2fFmSe8aBwdR3/w6b58hJpiL/fMukFkvGzjg+hSxFtwvVmKZmXw== dependencies: - ansi-escapes "^3.2.0" + ansi-escapes "^4.2.1" chalk "^2.4.2" - cli-cursor "^2.1.0" + cli-cursor "^3.1.0" cli-width "^2.0.0" external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.11" - mute-stream "0.0.7" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" run-async "^2.2.0" rxjs "^6.4.0" - string-width "^2.1.0" + string-width "^4.1.0" strip-ansi "^5.1.0" through "^2.3.6" @@ -4695,6 +4775,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -5381,7 +5466,7 @@ lodash.tail@^4.1.1: resolved "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= -lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: version "4.17.15" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -5479,13 +5564,13 @@ make-error@^1.1.1: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== -make-fetch-happen@^4.0.1, make-fetch-happen@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-4.0.2.tgz#2d156b11696fb32bffbafe1ac1bc085dd6c78a79" - integrity sha512-YMJrAjHSb/BordlsDEcVcPyTbiJKkzqMf48N8dAJZT9Zjctrkb6Yg4TY9Sq2AwSIQJFn5qBBKVTYt3vP5FMIHA== +make-fetch-happen@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.0.tgz#a8e3fe41d3415dd656fe7b8e8172e1fb4458b38d" + integrity sha512-nFr/vpL1Jc60etMVKeaLOqfGjMMb3tAHFVJWxHOFCFS04Zmd7kGlMxo0l1tzfhoQje0/UPnd0X8OeGUiXXnfPA== dependencies: agentkeepalive "^3.4.1" - cacache "^11.3.3" + cacache "^12.0.0" http-cache-semantics "^3.8.1" http-proxy-agent "^2.1.0" https-proxy-agent "^2.2.1" @@ -5641,12 +5726,7 @@ mime@^2.3.1, mime@^2.4.2, mime@^2.4.4: resolved "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -mimic-fn@^2.0.0: +mimic-fn@^2.0.0, mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== @@ -5797,10 +5877,10 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== nan@^2.12.1, nan@^2.13.2: version "2.14.0" @@ -6057,6 +6137,15 @@ npm-packlist@^1.1.12, npm-packlist@^1.1.6: ignore-walk "^3.0.1" npm-bundled "^1.0.1" +npm-pick-manifest@3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" + integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw== + dependencies: + figgy-pudding "^3.5.1" + npm-package-arg "^6.0.0" + semver "^5.4.1" + npm-pick-manifest@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40" @@ -6066,17 +6155,18 @@ npm-pick-manifest@^2.2.3: npm-package-arg "^6.0.0" semver "^5.4.1" -npm-registry-fetch@^3.8.0: - version "3.9.1" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.9.1.tgz#00ff6e4e35d3f75a172b332440b53e93f4cb67de" - integrity sha512-VQCEZlydXw4AwLROAXWUR7QDfe2Y8Id/vpAgp6TI1/H78a4SiQ1kQrKZALm5/zxM5n4HIi+aYb+idUAV/RuY0Q== +npm-registry-fetch@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-4.0.1.tgz#4ec0964dce6f29d253801a47cd381a7d6ad13a5e" + integrity sha512-1ZQ+yjnxc698R5h9Yje9CASapzAZr7aYDkJDdERg9xg2hOEY0vRJwskOaJAXq8N/eLavzvW4g564YAfq6zMn/A== dependencies: JSONStream "^1.3.4" bluebird "^3.5.1" figgy-pudding "^3.4.1" lru-cache "^5.1.1" - make-fetch-happen "^4.0.2" + make-fetch-happen "^5.0.0" npm-package-arg "^6.1.0" + safe-buffer "^5.2.0" npm-run-path@^2.0.0: version "2.0.2" @@ -6198,12 +6288,12 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== dependencies: - mimic-fn "^1.0.0" + mimic-fn "^2.1.0" open@6.4.0: version "6.4.0" @@ -6334,18 +6424,19 @@ package-json@^6.3.0: registry-url "^5.0.0" semver "^6.2.0" -pacote@9.5.1: - version "9.5.1" - resolved "https://registry.npmjs.org/pacote/-/pacote-9.5.1.tgz#adb0d23daeef6d0b813ab5891d0c6459ccec998d" - integrity sha512-Zqvczvf/zZ7QNosdE9uTC7SRuvSs9tFqRkF6cJl+2HH7COBnx4BRAGpeXJlrbN+mM0CMHpbi620xdEHhCflghA== +pacote@9.5.5: + version "9.5.5" + resolved "https://registry.npmjs.org/pacote/-/pacote-9.5.5.tgz#63355a393614c3424e735820c3731e2cbbedaeeb" + integrity sha512-jAEP+Nqj4kyMWyNpfTU/Whx1jA7jEc5cCOlurm0/0oL+v8TAp1QSsK83N7bYe+2bEdFzMAtPG5TBebjzzGV0cA== dependencies: bluebird "^3.5.3" - cacache "^11.3.2" + cacache "^12.0.2" figgy-pudding "^3.5.1" get-stream "^4.1.0" glob "^7.1.3" + infer-owner "^1.0.4" lru-cache "^5.1.1" - make-fetch-happen "^4.0.1" + make-fetch-happen "^5.0.0" minimatch "^3.0.4" minipass "^2.3.5" mississippi "^3.0.0" @@ -6354,7 +6445,7 @@ pacote@9.5.1: npm-package-arg "^6.1.0" npm-packlist "^1.1.12" npm-pick-manifest "^2.2.3" - npm-registry-fetch "^3.8.0" + npm-registry-fetch "^4.0.0" osenv "^0.1.5" promise-inflight "^1.0.1" promise-retry "^1.1.1" @@ -7119,10 +7210,10 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -replace@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/replace/-/replace-1.1.0.tgz#4cb04f138d14f37c47b9f2d214eb4a057bd94b22" - integrity sha512-0k9rtPG0MUDfJj77XtMCSJKOPdzSwVwM79ZQ6lZuFjqqXrQAMKIMp0g7/8GDAzeERxdktV/LzqbMtJ3yxB23lg== +replace@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/replace/-/replace-1.1.1.tgz#3d1b5e5896bd55c365a42a8d6e220b07a6f5d635" + integrity sha512-RTLcKzfKEc8YPX+WbxZ5nQK921qOCpmMGWuKFWHWf727o7Ap84ydbhv8A/ipANXXXxFxI2M2PW+FaEhDsdZCdQ== dependencies: colors "1.2.4" minimatch "3.0.4" @@ -7205,12 +7296,12 @@ responselike@^1.0.2: dependencies: lowercase-keys "^1.0.0" -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== dependencies: - onetime "^2.0.0" + onetime "^5.1.0" signal-exit "^3.0.2" ret@~0.1.10: @@ -7267,10 +7358,10 @@ rw@1: resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q= -rxjs-compat@^6.5.2: - version "6.5.2" - resolved "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.5.2.tgz#e469070adf6260bdad195e9d4a39f444ae28b458" - integrity sha512-TRMkTp4FgSxE2HtGvxmgRukh3JqdFM7ejAj1Ti/VdodbPGfWvZR5+KdLKRV9jVDFyu2SknM8RD+PR54KGnoLjg== +rxjs-compat@^6.5.3: + version "6.5.3" + resolved "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.5.3.tgz#18440949b2678bf87a78a754009676b2c49183dc" + integrity sha512-BIJX2yovz3TBpjJoAZyls2QYuU6ZiCaZ+U96SmxQpuSP/qDUfiXPKOVLbThBB2WZijNHkdTTJXKRwvv5Y48H7g== rxjs@6.4.0: version "6.4.0" @@ -7279,19 +7370,26 @@ rxjs@6.4.0: dependencies: tslib "^1.9.0" -rxjs@^6.4.0, rxjs@^6.5.2: +rxjs@^6.4.0: version "6.5.2" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== dependencies: tslib "^1.9.0" +rxjs@^6.5.3: + version "6.5.3" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2: +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -7351,6 +7449,11 @@ saucelabs@^1.5.0: dependencies: https-proxy-agent "^2.2.1" +save-svg-as-png@^1.4.14: + version "1.4.14" + resolved "https://registry.npmjs.org/save-svg-as-png/-/save-svg-as-png-1.4.14.tgz#d5017bb9746adf00c146a17e63ed4badd1e10b40" + integrity sha512-hJqOFSdRvhBVD2pQSM+mJStvQGfnvQCCF6ULtAxdjF4lDwXYfWZ9Eug0fcRl05YyPL2yknCDBEOpbO4Fkw5qmg== + sax@0.5.x: version "0.5.8" resolved "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" @@ -7438,7 +7541,7 @@ semver@6.2.0: resolved "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== -semver@^6.0.0, semver@^6.1.1, semver@^6.2.0, semver@^6.3.0: +semver@6.3.0, semver@^6.0.0, semver@^6.1.1, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -8003,7 +8106,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -8020,6 +8123,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" + integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^5.2.0" + string_decoder@^1.0.0, string_decoder@^1.1.1: version "1.2.0" resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" @@ -8144,6 +8256,11 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +svg-crowbar@^0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/svg-crowbar/-/svg-crowbar-0.2.3.tgz#2b68812a1fc118757d80ccf18a41e4cc675cb2dc" + integrity sha512-35CKMmoj3HY/5Q9HU0JJmpY0Oi+YU24mFPbR3piihOeddBabv/Un06KWXcWMiR5kJN2kwogUbmB0hem2vT7GjA== + symbol-observable@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -8372,10 +8489,10 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-mockito@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.4.2.tgz#e3b383a3cbfbf5225dff7365d98ddc32af75b846" - integrity sha512-3AqLVXxjfdwlo2eC+xrzFsc5rsPtKBBhJZAnxWmyBmgT/PC+K26RIxiT2QLKcqjcJqZnuGZkwfPMx2gN31lFnw== +ts-mockito@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.5.0.tgz#ad853051f2d116dfcaf6de6b0a1df2c82eda2d1f" + integrity sha512-b3qUeMfghRq5k5jw3xNJcnU9RKhqKnRn0k9v9QkN+YpuawrFuMIiGwzFZCpdi5MHy26o7YPnK8gag2awURl3nA== dependencies: lodash "^4.17.5" @@ -8448,6 +8565,11 @@ type-fest@^0.3.0: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -9006,6 +9128,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"