Merge pull request #147 from GNS3/local-server

Local server support and limit experimental features
This commit is contained in:
ziajka 2018-06-26 12:12:41 +02:00 committed by GitHub
commit 5bb5374b65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 193 additions and 38 deletions

View File

@ -8,6 +8,7 @@ import { ServersComponent } from "./servers/servers.component";
import { ProjectsComponent } from "./projects/projects.component"; import { ProjectsComponent } from "./projects/projects.component";
import { DefaultLayoutComponent } from "./default-layout/default-layout.component"; import { DefaultLayoutComponent } from "./default-layout/default-layout.component";
import { SettingsComponent } from "./settings/settings.component"; import { SettingsComponent } from "./settings/settings.component";
import { LocalServerComponent } from "./local-server/local-server.component";
const routes: Routes = [ const routes: Routes = [
@ -15,6 +16,7 @@ const routes: Routes = [
children: [ children: [
{ path: '', redirectTo: 'servers', pathMatch: 'full'}, { path: '', redirectTo: 'servers', pathMatch: 'full'},
{ path: 'servers', component: ServersComponent }, { path: 'servers', component: ServersComponent },
{ path: 'local', component: LocalServerComponent },
{ path: 'server/:server_id/projects', component: ProjectsComponent }, { path: 'server/:server_id/projects', component: ProjectsComponent },
{ path: 'settings', component: SettingsComponent }, { path: 'settings', component: SettingsComponent },
] ]

View File

@ -75,6 +75,7 @@ import { SettingsComponent } from './settings/settings.component';
import { SettingsService } from "./shared/services/settings.service"; import { SettingsService } from "./shared/services/settings.service";
import { RavenErrorHandler } from "./raven-error-handler"; import { RavenErrorHandler } from "./raven-error-handler";
import { LocalServerComponent } from './local-server/local-server.component';
Raven Raven
.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726') .config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726')
@ -102,6 +103,7 @@ Raven
MoveLayerUpActionComponent, MoveLayerUpActionComponent,
ProjectMapShortcutsComponent, ProjectMapShortcutsComponent,
SettingsComponent, SettingsComponent,
LocalServerComponent,
], ],
imports: [ imports: [
NgbModule.forRoot(), NgbModule.forRoot(),

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LocalServerComponent } from './local-server.component';
describe('LocalServerComponent', () => {
let component: LocalServerComponent;
let fixture: ComponentFixture<LocalServerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LocalServerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LocalServerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { Location } from "@angular/common";
import { Router } from "@angular/router";
import { ServerService } from "../shared/services/server.service";
import { Server } from "../shared/models/server";
@Component({
selector: 'app-local-server',
templateUrl: './local-server.component.html',
styleUrls: ['./local-server.component.scss']
})
export class LocalServerComponent implements OnInit {
constructor(private location: Location,
private router: Router,
private serverService: ServerService) { }
ngOnInit() {
this.serverService.getLocalServer(location.hostname, parseInt(location.port, 10))
.then((server: Server) => {
this.router.navigate(['/server', server.id, 'projects']);
});
}
}

View File

@ -17,6 +17,9 @@ import { SelectionManager } from "../../cartography/shared/managers/selection-ma
import { Server } from "../../shared/models/server"; import { Server } from "../../shared/models/server";
import { Node } from "../../cartography/shared/models/node"; import { Node } from "../../cartography/shared/models/node";
import { Project } from "../../shared/models/project"; import { Project } from "../../shared/models/project";
import { ProjectService } from "../../shared/services/project.service";
import { MockedProjectService } from "../../shared/services/project.service.spec";
import { SettingsService } from "../../shared/services/settings.service";
describe('ProjectMapShortcutsComponent', () => { describe('ProjectMapShortcutsComponent', () => {
@ -40,6 +43,8 @@ describe('ProjectMapShortcutsComponent', () => {
{ provide: NodeService, useFactory: () => instance(nodeServiceMock) }, { provide: NodeService, useFactory: () => instance(nodeServiceMock) },
{ provide: HotkeysService, useFactory: () => hotkeyServiceInstanceMock }, { provide: HotkeysService, useFactory: () => hotkeyServiceInstanceMock },
{ provide: ToasterService, useClass: MockedToasterService }, { provide: ToasterService, useClass: MockedToasterService },
{ provide: ProjectService, useClass: MockedProjectService },
{ provide: SettingsService, useClass: SettingsService }
], ],
declarations: [ ProjectMapShortcutsComponent ] declarations: [ ProjectMapShortcutsComponent ]
}) })

View File

@ -6,6 +6,7 @@ import { NodeService } from '../../shared/services/node.service';
import { Server } from '../../shared/models/server'; import { Server } from '../../shared/models/server';
import { ToasterService } from '../../shared/services/toaster.service'; import { ToasterService } from '../../shared/services/toaster.service';
import { Project } from "../../shared/models/project"; import { Project } from "../../shared/models/project";
import { ProjectService } from "../../shared/services/project.service";
@Component({ @Component({
@ -23,6 +24,7 @@ export class ProjectMapShortcutsComponent implements OnInit, OnDestroy {
private hotkeysService: HotkeysService, private hotkeysService: HotkeysService,
private toaster: ToasterService, private toaster: ToasterService,
private nodesService: NodeService, private nodesService: NodeService,
private projectService: ProjectService
) { } ) { }
ngOnInit() { ngOnInit() {
@ -31,7 +33,7 @@ export class ProjectMapShortcutsComponent implements OnInit, OnDestroy {
} }
onDeleteHandler(event: KeyboardEvent): boolean { onDeleteHandler(event: KeyboardEvent): boolean {
if (!this.project.readonly) { if (!this.projectService.isReadOnly(this.project)) {
const selectedNodes = this.selectionManager.getSelectedNodes(); const selectedNodes = this.selectionManager.getSelectedNodes();
if (selectedNodes) { if (selectedNodes) {
selectedNodes.forEach((node) => { selectedNodes.forEach((node) => {

View File

@ -33,7 +33,7 @@
</div> </div>
</mat-menu> </mat-menu>
<mat-toolbar-row *ngIf="!project.readonly"> <mat-toolbar-row *ngIf="!readonly">
<button mat-icon-button [color]="drawLineMode ? 'primary': 'basic'" (click)="toggleDrawLineMode()"> <button mat-icon-button [color]="drawLineMode ? 'primary': 'basic'" (click)="toggleDrawLineMode()">
<mat-icon>timeline</mat-icon> <mat-icon>timeline</mat-icon>
</button> </button>
@ -45,13 +45,13 @@
</button> </button>
</mat-toolbar-row> </mat-toolbar-row>
<mat-toolbar-row *ngIf="!project.readonly" > <mat-toolbar-row *ngIf="!readonly" >
<button mat-icon-button (click)="createSnapshotModal()"> <button mat-icon-button (click)="createSnapshotModal()">
<mat-icon>snooze</mat-icon> <mat-icon>snooze</mat-icon>
</button> </button>
</mat-toolbar-row> </mat-toolbar-row>
<mat-toolbar-row *ngIf="!project.readonly" > <mat-toolbar-row *ngIf="!readonly" >
<app-appliance [server]="server" (onNodeCreation)="onNodeCreation($event)"></app-appliance> <app-appliance [server]="server" (onNodeCreation)="onNodeCreation($event)"></app-appliance>
</mat-toolbar-row> </mat-toolbar-row>

View File

@ -40,7 +40,7 @@ import { SelectionManager } from "../cartography/shared/managers/selection-manag
import { InRectangleHelper } from "../cartography/map/helpers/in-rectangle-helper"; import { InRectangleHelper } from "../cartography/map/helpers/in-rectangle-helper";
import { DrawingsDataSource } from "../cartography/shared/datasources/drawings-datasource"; import { DrawingsDataSource } from "../cartography/shared/datasources/drawings-datasource";
import { Subscription } from "rxjs/Subscription"; import { Subscription } from "rxjs/Subscription";
import { SettingsService } from "../shared/services/settings.service";
@Component({ @Component({
@ -61,6 +61,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private ws: Subject<any>; private ws: Subject<any>;
private drawLineMode = false; private drawLineMode = false;
private movingMode = false; private movingMode = false;
private readonly = false;
protected selectionManager: SelectionManager; protected selectionManager: SelectionManager;
@ -85,6 +86,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
private progressDialogService: ProgressDialogService, private progressDialogService: ProgressDialogService,
private toaster: ToasterService, private toaster: ToasterService,
private projectWebServiceHandler: ProjectWebServiceHandler, private projectWebServiceHandler: ProjectWebServiceHandler,
private settingsService: SettingsService,
protected nodesDataSource: NodesDataSource, protected nodesDataSource: NodesDataSource,
protected linksDataSource: LinksDataSource, protected linksDataSource: LinksDataSource,
protected drawingsDataSource: DrawingsDataSource, protected drawingsDataSource: DrawingsDataSource,
@ -104,7 +106,6 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
.flatMap((server: Server) => { .flatMap((server: Server) => {
this.server = server; this.server = server;
return this.projectService.get(server, paramMap.get('project_id')).map((project) => { return this.projectService.get(server, paramMap.get('project_id')).map((project) => {
project.readonly = true;
return project; return project;
}); });
}) })
@ -162,6 +163,8 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
} }
onProjectLoad(project: Project) { onProjectLoad(project: Project) {
this.readonly = this.projectService.isReadOnly(project);
const subscription = this.symbolService const subscription = this.symbolService
.load(this.server) .load(this.server)
.flatMap(() => { .flatMap(() => {
@ -195,10 +198,11 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
} }
setUpMapCallbacks(project: Project) { setUpMapCallbacks(project: Project) {
if (this.project.readonly) { if (this.readonly) {
this.mapChild.graphLayout.getSelectionTool().deactivate(); this.mapChild.graphLayout.getSelectionTool().deactivate();
} }
this.mapChild.graphLayout.getNodesWidget().setDraggingEnabled(!this.project.readonly);
this.mapChild.graphLayout.getNodesWidget().setDraggingEnabled(!this.readonly);
this.mapChild.graphLayout.getNodesWidget().setOnContextMenuCallback((event: any, node: Node) => { this.mapChild.graphLayout.getNodesWidget().setOnContextMenuCallback((event: any, node: Node) => {
this.nodeContextMenu.open(node, event.clientY, event.clientX); this.nodeContextMenu.open(node, event.clientY, event.clientX);
@ -280,18 +284,19 @@ export class ProjectMapComponent implements OnInit, OnDestroy {
public toggleMovingMode() { public toggleMovingMode() {
this.movingMode = !this.movingMode; this.movingMode = !this.movingMode;
if (this.movingMode) { if (this.movingMode) {
if (!this.project.readonly) { if (!this.readonly) {
this.mapChild.graphLayout.getSelectionTool().deactivate(); this.mapChild.graphLayout.getSelectionTool().deactivate();
} }
this.mapChild.graphLayout.getMovingTool().activate(); this.mapChild.graphLayout.getMovingTool().activate();
} else { } else {
this.mapChild.graphLayout.getMovingTool().deactivate(); this.mapChild.graphLayout.getMovingTool().deactivate();
if (!this.project.readonly) { if (!this.readonly) {
this.mapChild.graphLayout.getSelectionTool().activate(); this.mapChild.graphLayout.getSelectionTool().activate();
} }
} }
} }
public onChooseInterface(event) { public onChooseInterface(event) {
const node: Node = event.node; const node: Node = event.node;
const port: Port = event.port; const port: Port = event.port;

View File

@ -46,7 +46,7 @@ export class ProjectsComponent implements OnInit {
this.route.paramMap this.route.paramMap
.switchMap((params: ParamMap) => { .switchMap((params: ParamMap) => {
const server_id = params.get('server_id'); const server_id = params.get('server_id');
return this.serverService.getLocalOrRemote(server_id); return this.serverService.get(parseInt(server_id, 10));
}) })
.subscribe((server: Server) => { .subscribe((server: Server) => {
this.server = server; this.server = server;

View File

@ -6,6 +6,7 @@ import { FormsModule } from "@angular/forms";
import { SettingsService } from "../shared/services/settings.service"; import { SettingsService } from "../shared/services/settings.service";
import { PersistenceModule } from "angular-persistence"; import { PersistenceModule } from "angular-persistence";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MockedToasterService, ToasterService } from "../shared/services/toaster.service";
describe('SettingsComponent', () => { describe('SettingsComponent', () => {
let component: SettingsComponent; let component: SettingsComponent;
@ -17,7 +18,10 @@ describe('SettingsComponent', () => {
imports: [ imports: [
MatExpansionModule, MatCheckboxModule, FormsModule, MatExpansionModule, MatCheckboxModule, FormsModule,
PersistenceModule, BrowserAnimationsModule ], PersistenceModule, BrowserAnimationsModule ],
providers: [ SettingsService ], providers: [
SettingsService,
{ provide: ToasterService, useClass: MockedToasterService }
],
declarations: [ SettingsComponent ] declarations: [ SettingsComponent ]
}) })
.compileComponents(); .compileComponents();
@ -37,7 +41,8 @@ describe('SettingsComponent', () => {
it('should get and save new settings', () => { it('should get and save new settings', () => {
const settings = { const settings = {
'crash_reports': true 'crash_reports': true,
'experimental_features': true
}; };
const getAll = spyOn(settingsService, 'getAll').and.returnValue(settings); const getAll = spyOn(settingsService, 'getAll').and.returnValue(settings);
const setAll = spyOn(settingsService, 'setAll'); const setAll = spyOn(settingsService, 'setAll');

View File

@ -8,4 +8,5 @@ export class Server {
authorization: ServerAuthorization; authorization: ServerAuthorization;
login: string; login: string;
password: string; password: string;
is_local: boolean;
} }

View File

@ -3,7 +3,7 @@
<mat-menu #contextMenu="matMenu"> <mat-menu #contextMenu="matMenu">
<app-start-node-action [server]="server" [node]="node"></app-start-node-action> <app-start-node-action [server]="server" [node]="node"></app-start-node-action>
<app-stop-node-action [server]="server" [node]="node"></app-stop-node-action> <app-stop-node-action [server]="server" [node]="node"></app-stop-node-action>
<app-move-layer-up-action *ngIf="!project.readonly" [server]="server" [node]="node"></app-move-layer-up-action> <app-move-layer-up-action *ngIf="!projectService.isReadOnly(project)" [server]="server" [node]="node"></app-move-layer-up-action>
<app-move-layer-down-action *ngIf="!project.readonly" [server]="server" [node]="node"></app-move-layer-down-action> <app-move-layer-down-action *ngIf="!projectService.isReadOnly(project)" [server]="server" [node]="node"></app-move-layer-down-action>
</mat-menu> </mat-menu>
</div> </div>

View File

@ -4,6 +4,7 @@ import { DomSanitizer } from "@angular/platform-browser";
import { Node } from "../../cartography/shared/models/node"; import { Node } from "../../cartography/shared/models/node";
import { Server } from "../models/server"; import { Server } from "../models/server";
import { Project } from "../models/project"; import { Project } from "../models/project";
import { ProjectService } from "../services/project.service";
@Component({ @Component({
@ -23,7 +24,8 @@ export class NodeContextMenuComponent implements OnInit {
constructor( constructor(
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private changeDetector: ChangeDetectorRef) {} private changeDetector: ChangeDetectorRef,
protected projectService: ProjectService) {}
ngOnInit() { ngOnInit() {
this.setPosition(0, 0); this.setPosition(0, 0);

View File

@ -176,4 +176,17 @@ describe('HttpServer', () => {
expect(req.request.headers.get('Authorization')).toEqual('Basic bG9naW46cGFzc3dvcmQ='); expect(req.request.headers.get('Authorization')).toEqual('Basic bG9naW46cGFzc3dvcmQ=');
expect(req.request.headers.get('CustomHeader')).toEqual('value'); expect(req.request.headers.get('CustomHeader')).toEqual('value');
}); });
it('should make local call when ip and port is not defined', () => {
server.ip = null;
server.port = null;
service.get(server, '/test', {
headers: {
'CustomHeader': 'value'
}
}).subscribe();
const req = httpTestingController.expectOne('/v2/test');
});
}); });

View File

@ -112,7 +112,11 @@ export class HttpServer {
} }
private getOptionsForServer<T extends HeadersOptions>(server: Server, url: string, options: T) { private getOptionsForServer<T extends HeadersOptions>(server: Server, url: string, options: T) {
if (server.ip && server.port) {
url = `http://${server.ip}:${server.port}/v2${url}`; url = `http://${server.ip}:${server.port}/v2${url}`;
} else {
url = `/v2${url}`;
}
if (!options.headers) { if (!options.headers) {
options.headers = {}; options.headers = {};

View File

@ -4,12 +4,21 @@ import { HttpClient } from '@angular/common/http';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpServer } from './http-server.service'; import { HttpServer } from './http-server.service';
import { Server } from '../models/server'; import { Server } from '../models/server';
import { Node } from '../../cartography/shared/models/node';
import { Port } from '../models/port';
import { getTestServer } from './testing'; import { getTestServer } from './testing';
import { Appliance } from '../models/appliance';
import { Project } from '../models/project';
import { ProjectService } from './project.service'; import { ProjectService } from './project.service';
import { SettingsService } from "./settings.service";
import { MockedSettingsService } from "./settings.service.spec";
/**
* Mocks ProjectsService so it's not based on settings
*/
export class MockedProjectService {
isReadOnly(project) {
return project.readonly;
}
}
describe('ProjectService', () => { describe('ProjectService', () => {
let httpClient: HttpClient; let httpClient: HttpClient;
@ -25,7 +34,8 @@ describe('ProjectService', () => {
], ],
providers: [ providers: [
HttpServer, HttpServer,
ProjectService ProjectService,
{ provide: SettingsService, useClass: MockedSettingsService }
] ]
}); });
@ -105,4 +115,5 @@ describe('ProjectService', () => {
const path = service.notificationsPath(server, "myproject"); const path = service.notificationsPath(server, "myproject");
expect(path).toEqual('ws://127.0.0.1:3080/v2/projects/myproject/notifications/ws') expect(path).toEqual('ws://127.0.0.1:3080/v2/projects/myproject/notifications/ws')
})); }));
}); });

View File

@ -8,11 +8,13 @@ import { Link } from "../../cartography/shared/models/link";
import { Server } from "../models/server"; import { Server } from "../models/server";
import { HttpServer } from "./http-server.service"; import { HttpServer } from "./http-server.service";
import {Drawing} from "../../cartography/shared/models/drawing"; import {Drawing} from "../../cartography/shared/models/drawing";
import { SettingsService } from "./settings.service";
@Injectable() @Injectable()
export class ProjectService { export class ProjectService {
constructor(private httpServer: HttpServer) { } constructor(private httpServer: HttpServer,
private settingsService: SettingsService) { }
get(server: Server, project_id: string) { get(server: Server, project_id: string) {
return this.httpServer return this.httpServer
@ -52,4 +54,11 @@ export class ProjectService {
notificationsPath(server: Server, project_id: string): string { notificationsPath(server: Server, project_id: string): string {
return `ws://${server.ip}:${server.port}/v2/projects/${project_id}/notifications/ws`; return `ws://${server.ip}:${server.port}/v2/projects/${project_id}/notifications/ws`;
} }
isReadOnly(project: Project) {
if (project.readonly) {
return project.readonly;
}
return !this.settingsService.isExperimentalEnabled();
}
} }

View File

@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import {IndexedDbService} from "./indexed-db.service"; import {IndexedDbService} from "./indexed-db.service";
import {Server} from "../models/server"; import {Server} from "../models/server";
import { Observable } from "rxjs/Observable";
@Injectable() @Injectable()
@ -21,13 +22,6 @@ export class ServerService {
this.indexedDbService.get().getByKey(this.tablename, id)); this.indexedDbService.get().getByKey(this.tablename, id));
} }
public getLocalOrRemote(id: string) {
if (id === 'local') {
}
return this.get(parseInt(id, 10));
}
public create(server: Server) { public create(server: Server) {
return this.onReady(() => { return this.onReady(() => {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
@ -40,6 +34,17 @@ export class ServerService {
}); });
} }
public update(server: Server) {
return this.onReady(() => {
const promise = new Promise((resolve, reject) => {
this.indexedDbService.get().update(this.tablename, server).then((updated) => {
resolve(server);
}, reject);
});
return promise;
});
}
public findAll() { public findAll() {
return this.onReady(() => return this.onReady(() =>
this.indexedDbService.get().getAll(this.tablename)); this.indexedDbService.get().getAll(this.tablename));
@ -50,6 +55,32 @@ export class ServerService {
this.indexedDbService.get().delete(this.tablename, server.id)); this.indexedDbService.get().delete(this.tablename, server.id));
} }
public getLocalServer(ip: string, port: number) {
const promise = new Promise((resolve, reject) => {
this.findAll().then((servers: Server[]) => {
const local = servers.find((server) => server.is_local);
if (local) {
local.ip = ip;
local.port = port;
this.update(local).then((updated) => {
resolve(updated);
}, reject);
} else {
const server = new Server();
server.name = 'local';
server.ip = ip;
server.port = port;
server.is_local = true;
this.create(server).then((created) => {
resolve(created);
}, reject);
}
}, reject);
});
return promise;
}
private onReady(query) { private onReady(query) {
const promise = new Promise((resolve, reject) => { const promise = new Promise((resolve, reject) => {
this.ready.then(() => { this.ready.then(() => {

View File

@ -5,6 +5,13 @@ import { Settings, SettingsService } from './settings.service';
import createSpyObj = jasmine.createSpyObj; import createSpyObj = jasmine.createSpyObj;
export class MockedSettingsService {
isExperimentalEnabled() {
return true;
}
}
describe('SettingsService', () => { describe('SettingsService', () => {
let persistenceService: PersistenceService; let persistenceService: PersistenceService;
@ -45,7 +52,8 @@ describe('SettingsService', () => {
it('should get all values', inject([SettingsService], (service: SettingsService) => { it('should get all values', inject([SettingsService], (service: SettingsService) => {
expect(service.getAll()).toEqual({ expect(service.getAll()).toEqual({
'crash_reports': true 'crash_reports': true,
'experimental_features': false
}); });
})); }));
@ -53,10 +61,11 @@ describe('SettingsService', () => {
const settings = { const settings = {
'crash_reports': false 'crash_reports': false
}; };
service.setAll(settings) service.setAll(settings);
expect(service.getAll()).toEqual({ expect(service.getAll()).toEqual({
'crash_reports': false 'crash_reports': false,
'experimental_features': false
}); });
})); }));

View File

@ -1,3 +1,5 @@
export const environment = { export const environment = {
production: true production: true,
electron: false,
githubio: false
}; };