diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 38a4cfb5..01bc5a61 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -188,11 +188,15 @@ import { NodeCreatedLabelStylesFixer } from './components/project-map/helpers/no 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 { LogConsoleComponent } from './components/project-map/log-console/log-console.component'; import { LogEventsDataSource } from './components/project-map/log-console/log-events-datasource'; +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'; if (environment.production) { @@ -312,11 +316,14 @@ if (environment.production) { NodesMenuComponent, AdbutlerComponent, ConsoleDeviceActionComponent, + ShowNodeActionComponent, ConsoleComponent, NodesMenuComponent, ProjectMapMenuComponent, HelpComponent, LogConsoleComponent, + TopologySummaryComponent, + InfoDialogComponent, BringToFrontActionComponent ], imports: [ @@ -394,7 +401,8 @@ if (environment.production) { NodeCreatedLabelStylesFixer, NonNegativeValidator, RotationValidator, - MapSettingService + MapSettingsService, + InfoService ], entryComponents: [ AddServerDialogComponent, @@ -410,7 +418,8 @@ if (environment.production) { SymbolsComponent, DeleteConfirmationDialogComponent, HelpDialogComponent, - StartCaptureDialogComponent + StartCaptureDialogComponent, + InfoDialogComponent ], bootstrap: [AppComponent] }) 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..867d2e40 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() { 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/context-menu.component.html b/src/app/components/project-map/context-menu/context-menu.component.html index 1824f2ad..5ae6be95 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,6 +1,10 @@
+ {{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/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..b43cd717 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,7 +1,7 @@ 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 { MapSettingsService } from '../../../services/mapsettings.service'; import { MatIconModule, MatToolbarModule, MatMenuModule, MatCheckboxModule } from '@angular/material'; import { CommonModule } from '@angular/common'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -15,7 +15,7 @@ describe('ProjectMapMenuComponent', () => { let component: ProjectMapMenuComponent; let fixture: ComponentFixture; let drawingService = new MockedDrawingService(); - let mapSettingService = new MapSettingService(); + let mapSettingService = new MapSettingsService(); beforeEach(async(() => { TestBed.configureTestingModule({ @@ -23,7 +23,7 @@ describe('ProjectMapMenuComponent', () => { providers: [ { provide: DrawingService, useValue: drawingService }, { provide: ToolsService }, - { provide: MapSettingService, useValue: mapSettingService } + { provide: MapSettingsService, useValue: mapSettingService } ], 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..49b8007a 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,7 +2,7 @@ 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'; @@ -26,7 +26,7 @@ export class ProjectMapMenuComponent implements OnInit, OnDestroy { constructor( private toolsService: ToolsService, - private mapSettingsService: MapSettingService, + private mapSettingsService: MapSettingsService, private drawingService: DrawingService ) {} diff --git a/src/app/components/project-map/project-map.component.html b/src/app/components/project-map/project-map.component.html index 27c21c67..736a8450 100644 --- a/src/app/components/project-map/project-map.component.html +++ b/src/app/components/project-map/project-map.component.html @@ -64,6 +64,9 @@ Show console + + Show topology summary +
@@ -128,5 +131,8 @@
- + +
+
+
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 3627b945..f21ec588 100644 --- a/src/app/components/project-map/project-map.component.spec.ts +++ b/src/app/components/project-map/project-map.component.spec.ts @@ -27,7 +27,7 @@ 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,7 +48,7 @@ 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'; @@ -213,6 +213,10 @@ export class MockedNodesDataSource { update() { return of({}); } + + public get changes() { + return new BehaviorSubject<[]>([]); + } } export class MockedLinksDataSource { @@ -277,7 +281,8 @@ describe('ProjectMapComponent', () => { { provide: MapNodesDataSource, useClass: MapNodesDataSource }, { provide: MapLinksDataSource, useClass: LinksDataSource }, { provide: MapDrawingsDataSource, useClass: MapDrawingsDataSource }, - { provide: MapSymbolsDataSource, useClass: MapSymbolsDataSource } + { 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 e3499d27..35d1dd21 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -52,6 +52,7 @@ import { MapLinkNodeToLinkNodeConverter } from '../../cartography/converters/map import { ProjectMapMenuComponent } from './project-map-menu/project-map-menu.component'; import { ToasterService } from '../../services/toaster.service'; import { MapNodesDataSource, MapLinksDataSource, MapDrawingsDataSource, MapSymbolsDataSource, Indexed } from '../../cartography/datasources/map-datasource'; +import { MapSettingsService } from '../../services/mapsettings.service'; @Component({ @@ -70,6 +71,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { public ws: WebSocket; public isProjectMapMenuVisible: boolean = false; public isConsoleVisible: boolean = false; + public isTopologySummaryVisible: boolean = false; tools = { selection: true, @@ -122,11 +124,13 @@ export class ProjectMapComponent implements OnInit, OnDestroy { private mapNodesDataSource: MapNodesDataSource, private mapLinksDataSource: MapLinksDataSource, private mapDrawingsDataSource: MapDrawingsDataSource, - private mapSymbolsDataSource: MapSymbolsDataSource + private mapSymbolsDataSource: MapSymbolsDataSource, + private mapSettingsService: MapSettingsService ) {} ngOnInit() { this.settings = this.settingsService.getAll(); + this.isTopologySummaryVisible = this.mapSettingsService.isTopologySummaryVisible; this.progressService.activate(); const routeSub = this.route.paramMap.subscribe((paramMap: ParamMap) => { @@ -398,6 +402,11 @@ export class ProjectMapComponent implements OnInit, OnDestroy { public toggleShowConsole(visible: boolean) { this.isConsoleVisible = visible; } + + public toggleShowTopologySummary(visible: boolean) { + this.isTopologySummaryVisible = visible; + this.mapSettingsService.toggleTopologySummary(this.isTopologySummaryVisible); + } public hideMenu() { this.projectMapMenuComponent.resetDrawToolChoice() diff --git a/src/app/components/projects/projects.component.html b/src/app/components/projects/projects.component.html index 7f785447..90d28c2e 100644 --- a/src/app/components/projects/projects.component.html +++ b/src/app/components/projects/projects.component.html @@ -40,6 +40,9 @@ + diff --git a/src/app/components/projects/projects.component.spec.ts b/src/app/components/projects/projects.component.spec.ts index 29e82cec..3f3a4d8a 100644 --- a/src/app/components/projects/projects.component.spec.ts +++ b/src/app/components/projects/projects.component.spec.ts @@ -85,6 +85,28 @@ describe('ProjectsComponent', () => { expect(mockedProjectService.delete).toHaveBeenCalled(); }); + it('should call project service after duplicate action', () => { + spyOn(mockedProjectService, 'duplicate').and.returnValue(of()); + let project = new Project(); + project.project_id = '1'; + project.status = 'closed'; + + component.duplicate(project); + + expect(mockedProjectService.duplicate).toHaveBeenCalled(); + }); + + it('should call refresh after duplicate action', () => { + spyOn(component, 'refresh'); + let project = new Project(); + project.project_id = '1'; + project.status = 'closed'; + + component.duplicate(project); + + expect(component.refresh).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..1c7fa38a 100644 --- a/src/app/components/projects/projects.component.ts +++ b/src/app/components/projects/projects.component.ts @@ -107,6 +107,12 @@ export class ProjectsComponent implements OnInit { ); } + duplicate(project: Project) { + this.projectService.duplicate(this.server, project.project_id, project.name).subscribe(() => { + this.refresh(); + }); + } + addBlankProject() { const dialogRef = this.dialog.open(AddBlankProjectDialogComponent, { width: '400px', 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/topology-summary/topology-summary.component.html b/src/app/components/topology-summary/topology-summary.component.html new file mode 100644 index 00000000..3ca33e54 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.html @@ -0,0 +1,44 @@ +
+
+ Topology summary ({{projectsStatistics.snapshots}} snapshots) + close +
+ +
+ Filter by status
+
+ Started + Suspended + Stopped +
+
+
+ Sorting
+
+ + By name ascending + By name descending + +
+
+ +
+
+
+ + + + + + + {{node.name}} +
+
+ {{node.console_type}} {{node.console_host}}:{{node.console}} +
+
+ none +
+
+
+
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..a9629c20 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.scss @@ -0,0 +1,99 @@ +.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: 24px; + 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: 240px; + overflow: auto; + scrollbar-color: darkgrey #263238; + scrollbar-width: thin; +} + +.title { + margin-left: 5px; + margin-top: 4px; +} + +.divider { + margin: 5px; + width: 290px; + height: 2px; +} + +.nodeRow { + width: 100%; + display: flex; + justify-content: space-between; + padding-right: 5px; +} + +mat-icon { + font-size: 18px; + width: 20px; + height: 20px; + margin-top: 4px; +} + +::-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; +} + +.filterBox { + display: flex; + justify-content: space-between; +} 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..3cbffe52 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.spec.ts @@ -0,0 +1,148 @@ +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 } 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'; + + +describe('TopologySummaryComponent', () => { + let component: TopologySummaryComponent; + let fixture: ComponentFixture; + let mockedProjectService: MockedProjectService = new MockedProjectService(); + let mockedNodesDataSource: MockedNodesDataSource = new MockedNodesDataSource(); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + MatTableModule, + MatTooltipModule, + MatIconModule, + MatSortModule, + MatDialogModule, + NoopAnimationsModule, + RouterTestingModule.withRoutes([]) + ], + providers: [ + { provide: ProjectService, useValue: mockedProjectService }, + { provide: NodesDataSource, useValue: mockedNodesDataSource } + ], + 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..a53402e1 --- /dev/null +++ b/src/app/components/topology-summary/topology-summary.component.ts @@ -0,0 +1,116 @@ +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'; + + +@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; + + constructor( + private nodesDataSource: NodesDataSource, + private projectService: ProjectService + ) {} + + 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; + }); + } + + 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(); + } + + 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.sortingOrder === 'asc') { + this.filteredNodes = nodes.sort(this.compareAsc); + } else { + this.filteredNodes = nodes.sort(this.compareDesc); + } + } + + close() { + this.closeTopologySummary.emit(false); + } +} 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/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..caf4a6c8 100644 --- a/src/app/services/mapsettings.service.ts +++ b/src/app/services/mapsettings.service.ts @@ -2,12 +2,17 @@ import { Injectable } from "@angular/core"; import { Subject } from 'rxjs'; @Injectable() -export class MapSettingService { +export class MapSettingsService { public isMapLocked = new Subject(); + public isTopologySummaryVisible: boolean = false; constructor() {} changeMapLockValue(value: boolean) { this.isMapLocked.next(value); } + + toggleTopologySummary(value: boolean) { + this.isTopologySummaryVisible = value; + } } 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..9bf32788 100644 --- a/src/app/services/project.service.ts +++ b/src/app/services/project.service.ts @@ -49,6 +49,14 @@ export class ProjectService { return this.httpServer.delete(server, `/projects/${project_id}`); } + 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`; }