mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2025-01-21 20:08:08 +00:00
Merge pull request #380 from GNS3/putty-bundled
PuTTY in Web UI on Windows, Fixes #368
This commit is contained in:
commit
0afa6f7e0b
65
console-executor.js
Normal file
65
console-executor.js
Normal file
@ -0,0 +1,65 @@
|
||||
const { spawn } = require('child_process');
|
||||
const { app } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
async function setPATHEnv() {
|
||||
const puttyLookup = [
|
||||
path.join(__dirname, 'dist', 'putty'),
|
||||
path.join(path.dirname(app.getPath('exe')), 'dist', 'putty')
|
||||
];
|
||||
|
||||
// prevent adding duplicates
|
||||
let extra = [
|
||||
...puttyLookup,
|
||||
].filter((dir) => {
|
||||
return process.env.PATH.indexOf(dir) < 0;
|
||||
});
|
||||
extra.push(process.env.PATH);
|
||||
process.env.PATH = extra.join(";");
|
||||
}
|
||||
|
||||
exports.openConsole = async (consoleRequest) => {
|
||||
// const genericConsoleCommand = 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"';
|
||||
const genericConsoleCommand = 'putty.exe -telnet %h %p -loghost "%d"';
|
||||
|
||||
const command = prepareCommand(genericConsoleCommand, consoleRequest);
|
||||
|
||||
console.log(`Setting up PATH`);
|
||||
await setPATHEnv();
|
||||
|
||||
console.log(`Starting console with command: '${command}'`);
|
||||
|
||||
let consoleProcess = spawn(command, [], {
|
||||
shell :true
|
||||
});
|
||||
|
||||
consoleProcess.stdout.on('data', (data) => {
|
||||
console.log(`Console stdout is producing: ${data.toString()}`);
|
||||
});
|
||||
|
||||
consoleProcess.stderr.on('data', (data) => {
|
||||
console.log(`Console stderr is producing: ${data.toString()}`);
|
||||
});
|
||||
|
||||
consoleProcess.on('close', (code) => {
|
||||
console.log(`child process exited with code ${code}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function prepareCommand(consoleCommand, consoleRequest) {
|
||||
const mapping = {
|
||||
h: consoleRequest.host,
|
||||
p: consoleRequest.port,
|
||||
d: consoleRequest.name,
|
||||
i: consoleRequest.project_id,
|
||||
n: consoleRequest.node_id,
|
||||
c: consoleRequest.server_url
|
||||
};
|
||||
|
||||
for(var key in mapping) {
|
||||
const regExp = new RegExp(`%${key}`, 'g');
|
||||
consoleCommand = consoleCommand.replace(regExp, mapping[key]);
|
||||
}
|
||||
return consoleCommand;
|
||||
}
|
@ -16,6 +16,7 @@ files:
|
||||
- sentry.js
|
||||
- installed-software.js
|
||||
- local-server.js
|
||||
- console-executor.js
|
||||
- package.json
|
||||
|
||||
extraFiles:
|
||||
|
@ -40,6 +40,7 @@ SOURCE_DESTINATION = os.path.join(WORKING_DIR, 'source')
|
||||
BINARIES_EXTENSION = platform.system() == "Windows" and ".exe" or ""
|
||||
DEPENDENCIES = {
|
||||
'ubridge': {
|
||||
'type': 'github',
|
||||
'releases': 'https://api.github.com/repos/GNS3/ubridge/releases',
|
||||
'version': 'LATEST',
|
||||
'files': {
|
||||
@ -50,6 +51,7 @@ DEPENDENCIES = {
|
||||
}
|
||||
},
|
||||
'vpcs': {
|
||||
'type': 'github',
|
||||
'releases': 'https://api.github.com/repos/GNS3/vpcs/releases',
|
||||
'version': '0.6.1',
|
||||
'files': {
|
||||
@ -60,6 +62,7 @@ DEPENDENCIES = {
|
||||
}
|
||||
},
|
||||
'dynamips': {
|
||||
'type': 'github',
|
||||
'releases': 'https://api.github.com/repos/GNS3/dynamips/releases',
|
||||
'version': '0.2.17',
|
||||
'files': {
|
||||
@ -69,6 +72,16 @@ DEPENDENCIES = {
|
||||
'nvram_export.exe'
|
||||
]
|
||||
}
|
||||
},
|
||||
'putty': {
|
||||
'type': 'http',
|
||||
'url': 'https://the.earth.li/~sgtatham/putty/{version}/w64/putty.exe',
|
||||
'version': '0.71',
|
||||
'files': {
|
||||
'windows': [
|
||||
'putty.exe',
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,31 +126,54 @@ def prepare():
|
||||
os.makedirs(WORKING_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def download_from_github(name, definition, output_directory):
|
||||
response = requests.get(definition['releases'])
|
||||
response.raise_for_status()
|
||||
releases = response.json()
|
||||
|
||||
if definition['version'] == 'LATEST':
|
||||
release = releases[0]
|
||||
else:
|
||||
release = list(filter(lambda x: x['tag_name'] == "v{}".format(definition['version']), releases))[0]
|
||||
|
||||
dependency_dir = os.path.join(output_directory, name)
|
||||
os.makedirs(dependency_dir, exist_ok=True)
|
||||
|
||||
files = []
|
||||
if platform.system() == "Windows":
|
||||
files = definition['files']['windows']
|
||||
|
||||
for filename in files:
|
||||
dependency_file = os.path.join(dependency_dir, filename)
|
||||
dependency_url = list(filter(lambda x: x['name'] == filename, release['assets']))[0]['browser_download_url']
|
||||
download(dependency_url, dependency_file)
|
||||
print('Downloaded {} to {}'.format(filename, dependency_file))
|
||||
|
||||
|
||||
def download_from_http(name, definition, output_directory):
|
||||
url = definition['url'].format(version=definition['version'])
|
||||
|
||||
dependency_dir = os.path.join(output_directory, name)
|
||||
os.makedirs(dependency_dir, exist_ok=True)
|
||||
|
||||
files = []
|
||||
if platform.system() == "Windows":
|
||||
files = definition['files']['windows']
|
||||
|
||||
for filename in files:
|
||||
dependency_file = os.path.join(dependency_dir, filename)
|
||||
download(url, dependency_file)
|
||||
print('Downloaded {} to {}'.format(filename, dependency_file))
|
||||
|
||||
|
||||
def download_dependencies_command(arguments):
|
||||
output_directory = os.path.join(os.getcwd(), arguments.b)
|
||||
|
||||
for name, definition in DEPENDENCIES.items():
|
||||
response = requests.get(definition['releases'])
|
||||
response.raise_for_status()
|
||||
releases = response.json()
|
||||
|
||||
if definition['version'] == 'LATEST':
|
||||
release = releases[0]
|
||||
else:
|
||||
release = list(filter(lambda x: x['tag_name'] == "v{}".format(definition['version']), releases))[0]
|
||||
|
||||
dependency_dir = os.path.join(output_directory, name)
|
||||
os.makedirs(dependency_dir, exist_ok=True)
|
||||
|
||||
files = []
|
||||
if platform.system() == "Windows":
|
||||
files = definition['files']['windows']
|
||||
|
||||
for filename in files:
|
||||
dependency_file = os.path.join(dependency_dir, filename)
|
||||
dependency_url = list(filter(lambda x: x['name'] == filename, release['assets']))[0]['browser_download_url']
|
||||
download(dependency_url, dependency_file)
|
||||
print('Downloaded {} to {}'.format(filename, dependency_file))
|
||||
if definition['type'] == 'github':
|
||||
download_from_github(name, definition, output_directory)
|
||||
if definition['type'] == 'http':
|
||||
download_from_http(name, definition, output_directory)
|
||||
|
||||
|
||||
|
||||
|
@ -51,6 +51,7 @@ import { CopyIosTemplateComponent } from './components/preferences/dynamips/copy
|
||||
import { CopyDockerTemplateComponent } from './components/preferences/docker/copy-docker-template/copy-docker-template.component';
|
||||
import { CopyIouTemplateComponent } from './components/preferences/ios-on-unix/copy-iou-template/copy-iou-template.component';
|
||||
import { ListOfSnapshotsComponent } from './components/snapshots/list-of-snapshots/list-of-snapshots.component';
|
||||
import { ConsoleComponent } from './components/settings/console/console.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@ -62,6 +63,7 @@ const routes: Routes = [
|
||||
{ path: 'bundled', component: BundledServerFinderComponent },
|
||||
{ path: 'server/:server_id/projects', component: ProjectsComponent },
|
||||
{ path: 'settings', component: SettingsComponent },
|
||||
{ path: 'settings/console', component: ConsoleComponent },
|
||||
{ path: 'installed-software', component: InstalledSoftwareComponent },
|
||||
{ path: 'server/:server_id/project/:project_id/snapshots', component: ListOfSnapshotsComponent },
|
||||
{ path: 'server/:server_id/preferences', component: PreferencesComponent },
|
||||
|
@ -168,6 +168,9 @@ import { ListOfSnapshotsComponent } from './components/snapshots/list-of-snapsho
|
||||
import { DateFilter } from './filters/dateFilter.pipe';
|
||||
import { NameFilter } from './filters/nameFilter.pipe';
|
||||
import { CustomAdaptersComponent } from './components/preferences/common/custom-adapters/custom-adapters.component';
|
||||
|
||||
import { ConsoleDeviceActionComponent } from './components/project-map/context-menu/actions/console-device-action/console-device-action.component';
|
||||
import { ConsoleComponent } from './components/settings/console/console.component';
|
||||
import { NodesMenuComponent } from './components/project-map/nodes-menu/nodes-menu.component';
|
||||
import { PacketFiltersActionComponent } from './components/project-map/context-menu/actions/packet-filters-action/packet-filters-action.component';
|
||||
import { PacketFiltersDialogComponent } from './components/project-map/packet-capturing/packet-filters/packet-filters.component';
|
||||
@ -177,6 +180,8 @@ import { StartCaptureDialogComponent } from './components/project-map/packet-cap
|
||||
import { SuspendLinkActionComponent } from './components/project-map/context-menu/actions/suspend-link/suspend-link-action.component';
|
||||
import { ResumeLinkActionComponent } from './components/project-map/context-menu/actions/resume-link-action/resume-link-action.component';
|
||||
import { StopCaptureActionComponent } from './components/project-map/context-menu/actions/stop-capture/stop-capture-action.component';
|
||||
import { ConsoleService } from './services/settings/console.service';
|
||||
import { DefaultConsoleService } from './services/settings/default-console.service';
|
||||
|
||||
if (environment.production) {
|
||||
Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', {
|
||||
@ -291,6 +296,8 @@ if (environment.production) {
|
||||
NameFilter,
|
||||
ListOfSnapshotsComponent,
|
||||
CustomAdaptersComponent,
|
||||
ConsoleDeviceActionComponent,
|
||||
ConsoleComponent,
|
||||
NodesMenuComponent
|
||||
],
|
||||
imports: [
|
||||
@ -360,7 +367,9 @@ if (environment.production) {
|
||||
IouService,
|
||||
IouConfigurationService,
|
||||
RecentlyOpenedProjectService,
|
||||
ServerManagementService
|
||||
ServerManagementService,
|
||||
ConsoleService,
|
||||
DefaultConsoleService
|
||||
],
|
||||
entryComponents: [
|
||||
AddServerDialogComponent,
|
||||
|
@ -0,0 +1,4 @@
|
||||
<button mat-menu-item (click)="console()">
|
||||
<mat-icon>web_asset</mat-icon>
|
||||
<span>Console</span>
|
||||
</button>
|
@ -0,0 +1,126 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatIconModule } from '@angular/material';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
|
||||
import { ConsoleDeviceActionComponent } from './console-device-action.component';
|
||||
import { ServerService } from '../../../../../services/server.service';
|
||||
import { MockedServerService } from '../../../../../services/server.service.spec';
|
||||
import { ToasterService } from '../../../../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../../../../services/toaster.service.spec';
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
import { MockedSettingsService } from '../../../../../services/settings.service.spec';
|
||||
import { Node } from '../../../../../cartography/models/node';
|
||||
import { Server } from '../../../../../models/server';
|
||||
|
||||
|
||||
describe('ConsoleDeviceActionComponent', () => {
|
||||
let component: ConsoleDeviceActionComponent;
|
||||
let fixture: ComponentFixture<ConsoleDeviceActionComponent>;
|
||||
let electronService;
|
||||
let server: Server;
|
||||
let mockedSettingsService: MockedSettingsService;
|
||||
let mockedServerService: MockedServerService;
|
||||
let mockedToaster: MockedToasterService
|
||||
|
||||
beforeEach(() => {
|
||||
electronService = {
|
||||
isElectronApp: true,
|
||||
remote: {
|
||||
require: (file) => {
|
||||
return {
|
||||
openConsole() {}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockedSettingsService = new MockedSettingsService();
|
||||
mockedServerService = new MockedServerService();
|
||||
mockedToaster = new MockedToasterService();
|
||||
|
||||
server = { host: 'localhost', 'port': 222} as Server;
|
||||
})
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ElectronService, useValue: electronService },
|
||||
{ provide: ServerService, useValue: mockedServerService },
|
||||
{ provide: SettingsService, useValue: mockedSettingsService },
|
||||
{ provide: ToasterService, useValue: mockedToaster }
|
||||
],
|
||||
imports: [
|
||||
MatIconModule
|
||||
],
|
||||
declarations: [ ConsoleDeviceActionComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConsoleDeviceActionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('console to nodes', () => {
|
||||
let nodes: Node[];
|
||||
|
||||
beforeEach(() => {
|
||||
nodes = [{
|
||||
status: 'started',
|
||||
console_type: 'telnet',
|
||||
console_host: 'host',
|
||||
console: 999,
|
||||
name: 'Node 1',
|
||||
project_id: '1111',
|
||||
node_id: '2222',
|
||||
} as Node];
|
||||
|
||||
component.nodes = nodes;
|
||||
component.server = server;
|
||||
|
||||
mockedSettingsService.set('console_command', 'command');
|
||||
spyOn(component, 'openConsole');
|
||||
});
|
||||
|
||||
it('should console to device', async () => {
|
||||
await component.console();
|
||||
|
||||
expect(component.openConsole).toHaveBeenCalledWith({
|
||||
command: 'command',
|
||||
type: 'telnet',
|
||||
host: 'host',
|
||||
port: 999,
|
||||
name: 'Node 1',
|
||||
project_id: '1111',
|
||||
node_id: '2222',
|
||||
server_url: 'localhost:222'
|
||||
});
|
||||
});
|
||||
|
||||
it('should show message when command is not defined', async () => {
|
||||
mockedSettingsService.set('console_command', undefined);
|
||||
await component.console();
|
||||
expect(component.openConsole).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show message when there is no started nodes', async () => {
|
||||
nodes[0]['status'] = 'stopped';
|
||||
await component.console();
|
||||
expect(component.openConsole).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only start running nodes', async () => {
|
||||
nodes.push({
|
||||
status: 'stopped'
|
||||
} as Node);
|
||||
await component.console();
|
||||
expect(component.openConsole).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,64 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Node } from '../../../../../cartography/models/node';
|
||||
import { Server } from '../../../../../models/server';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
import { ServerService } from '../../../../../services/server.service';
|
||||
import { SettingsService } from '../../../../../services/settings.service';
|
||||
import { ToasterService } from '../../../../../services/toaster.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-console-device-action',
|
||||
templateUrl: './console-device-action.component.html'
|
||||
})
|
||||
export class ConsoleDeviceActionComponent implements OnInit {
|
||||
@Input() server: Server;
|
||||
@Input() nodes: Node[];
|
||||
|
||||
constructor(
|
||||
private electronService: ElectronService,
|
||||
private serverService: ServerService,
|
||||
private settingsService: SettingsService,
|
||||
private toasterService: ToasterService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
async console() {
|
||||
const consoleCommand = this.settingsService.get<string>('console_command');
|
||||
|
||||
if(consoleCommand === undefined) {
|
||||
this.toasterService.error('Console command is not defined. Please change it in the Settings.');
|
||||
return;
|
||||
}
|
||||
|
||||
const startedNodes = this.nodes.filter(node => node.status === 'started');
|
||||
|
||||
if(startedNodes.length === 0) {
|
||||
this.toasterService.error('Device needs to be started in order to console to it.');
|
||||
return;
|
||||
}
|
||||
|
||||
for(var node of this.nodes) {
|
||||
if(node.status !== 'started') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const consoleRequest = {
|
||||
command: consoleCommand,
|
||||
type: node.console_type,
|
||||
host: node.console_host,
|
||||
port: node.console,
|
||||
name: node.name,
|
||||
project_id: node.project_id,
|
||||
node_id: node.node_id,
|
||||
server_url: this.serverService.getServerUrl(this.server)
|
||||
};
|
||||
await this.openConsole(consoleRequest);
|
||||
}
|
||||
}
|
||||
|
||||
async openConsole(request) {
|
||||
return await this.electronService.remote.require('./console-executor.js').openConsole(request);
|
||||
}
|
||||
}
|
@ -3,6 +3,11 @@
|
||||
<mat-menu #contextMenu="matMenu" class="context-menu-items">
|
||||
<app-start-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-start-node-action>
|
||||
<app-stop-node-action *ngIf="nodes.length" [server]="server" [nodes]="nodes"></app-stop-node-action>
|
||||
<app-console-device-action
|
||||
*ngIf="!projectService.isReadOnly(project) && nodes.length && isElectronApp"
|
||||
[server]="server"
|
||||
[nodes]="nodes"
|
||||
></app-console-device-action>
|
||||
<app-edit-style-action *ngIf="drawings.length===1 && !hasTextCapabilities"
|
||||
[server]="server"
|
||||
[project]="project"
|
||||
|
@ -9,17 +9,23 @@ import { Drawing } from '../../../cartography/models/drawing';
|
||||
import { RectElement } from '../../../cartography/models/drawings/rect-element';
|
||||
import { TextElement } from '../../../cartography/models/drawings/text-element';
|
||||
import { Server } from '../../../models/server';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
|
||||
describe('ContextMenuComponent', () => {
|
||||
let component: ContextMenuComponent;
|
||||
let fixture: ComponentFixture<ContextMenuComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
const electronMock = {
|
||||
isElectronApp: true
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatMenuModule, BrowserModule],
|
||||
providers: [
|
||||
{ provide: ChangeDetectorRef },
|
||||
{ provide: ProjectService, useClass: MockedProjectService }
|
||||
{ provide: ProjectService, useClass: MockedProjectService },
|
||||
{ provide: ElectronService, useValue: electronMock}
|
||||
],
|
||||
declarations: [ContextMenuComponent],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@ -37,6 +43,10 @@ describe('ContextMenuComponent', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should define property if running in electron ', () => {
|
||||
expect(component.isElectronApp).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should reset capabilities while opening menu for node', () => {
|
||||
component.contextMenu = { openMenu() {} } as MatMenuTrigger;
|
||||
var spy = spyOn<any>(component, 'resetCapabilities');
|
||||
|
@ -9,6 +9,7 @@ import { Drawing } from '../../../cartography/models/drawing';
|
||||
import { TextElement } from '../../../cartography/models/drawings/text-element';
|
||||
import { Label } from '../../../cartography/models/label';
|
||||
import { Link } from '../../../models/link';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
|
||||
|
||||
@Component({
|
||||
@ -30,17 +31,21 @@ export class ContextMenuComponent implements OnInit {
|
||||
labels: Label[] = [];
|
||||
links: Link[] = [];
|
||||
|
||||
hasTextCapabilities: boolean = false;
|
||||
hasTextCapabilities = false;
|
||||
isElectronApp = false;
|
||||
isBundledServer: boolean = false;
|
||||
|
||||
constructor(
|
||||
private sanitizer: DomSanitizer,
|
||||
private changeDetector: ChangeDetectorRef,
|
||||
private electronService: ElectronService,
|
||||
public projectService: ProjectService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.setPosition(0, 0);
|
||||
|
||||
this.isElectronApp = this.electronService.isElectronApp;
|
||||
this.isBundledServer = this.server.location === 'bundled';
|
||||
}
|
||||
|
||||
|
28
src/app/components/settings/console/console.component.html
Normal file
28
src/app/components/settings/console/console.component.html
Normal file
@ -0,0 +1,28 @@
|
||||
<div class="content">
|
||||
<div class="default-header">
|
||||
<div class="row">
|
||||
<h1 class="col">Console settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="default-content">
|
||||
<mat-card>
|
||||
<form [formGroup]="consoleForm">
|
||||
<mat-form-field class="form-field full-width-field">
|
||||
<textarea matInput formControlName="command" placeholder="Command"></textarea>
|
||||
</mat-form-field>
|
||||
<div class="help">The following variables are replaced by GNS3:<br />
|
||||
%h: console IP or hostname<br />
|
||||
%p: console port<br />
|
||||
%s: path of the serial connection<br />
|
||||
%d: title of the console<br />
|
||||
%i: Project UUID<br />
|
||||
%c: server URL (http://user:password@server:port)
|
||||
</div>
|
||||
</form>
|
||||
</mat-card>
|
||||
<div class="buttons-bar">
|
||||
<button class="cancel-button" (click)="goBack()" mat-button>Cancel</button>
|
||||
<button mat-raised-button color="primary" (click)="save()">Save</button><br/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,3 @@
|
||||
.help {
|
||||
font-size: 14px;
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConsoleComponent } from './console.component';
|
||||
import { MatFormFieldModule, MatCardModule, MatInputModule } from '@angular/material';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ConsoleService } from '../../../services/settings/console.service';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../../services/toaster.service.spec';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
|
||||
describe('ConsoleComponent', () => {
|
||||
let component: ConsoleComponent;
|
||||
let fixture: ComponentFixture<ConsoleComponent>;
|
||||
let consoleService;
|
||||
let router;
|
||||
let toaster: MockedToasterService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
consoleService = {
|
||||
command: 'command'
|
||||
};
|
||||
|
||||
router = {
|
||||
navigate: jasmine.createSpy('navigate')
|
||||
};
|
||||
|
||||
toaster = new MockedToasterService();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ConsoleService, useValue: consoleService },
|
||||
{ provide: ToasterService, useValue: toaster },
|
||||
{ provide: Router, useValue: router}
|
||||
],
|
||||
imports: [ FormsModule, ReactiveFormsModule, MatFormFieldModule, MatCardModule, MatInputModule, NoopAnimationsModule ],
|
||||
declarations: [ ConsoleComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConsoleComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set default command', () => {
|
||||
component.ngOnInit();
|
||||
expect(component.consoleForm.value.command).toEqual('command');
|
||||
})
|
||||
|
||||
it('should go back', () => {
|
||||
component.goBack();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/settings']);
|
||||
});
|
||||
|
||||
|
||||
it('should update console command', () => {
|
||||
component.consoleForm.get('command').setValue('newCommand');
|
||||
spyOn(component, 'goBack');
|
||||
component.save();
|
||||
expect(toaster.success.length).toEqual(1);
|
||||
expect(component.goBack).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
40
src/app/components/settings/console/console.component.ts
Normal file
40
src/app/components/settings/console/console.component.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { ConsoleService } from '../../../services/settings/console.service';
|
||||
import { ToasterService } from '../../../services/toaster.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-console',
|
||||
templateUrl: './console.component.html',
|
||||
styleUrls: ['./console.component.scss']
|
||||
})
|
||||
export class ConsoleComponent implements OnInit {
|
||||
|
||||
consoleForm = new FormGroup({
|
||||
'command': new FormControl(''),
|
||||
});
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private consoleService: ConsoleService,
|
||||
private toasterService: ToasterService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
const commandControl = this.consoleForm.get('command');
|
||||
commandControl.setValue(this.consoleService.command);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/settings']);
|
||||
}
|
||||
|
||||
save() {
|
||||
const formValue = this.consoleForm.value;
|
||||
this.consoleService.command = formValue.command;
|
||||
this.toasterService.success("Console command has been updated.");
|
||||
this.goBack();
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,21 @@
|
||||
>
|
||||
</div> -->
|
||||
</mat-expansion-panel>
|
||||
|
||||
<mat-expansion-panel [expanded]="false">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title> Console settings </mat-panel-title>
|
||||
<mat-panel-description> Customize console settings </mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<div>
|
||||
<mat-form-field class="full-width-field">
|
||||
<input matInput placeholder="console" [value]="consoleCommand" readonly="true">
|
||||
<a mat-icon-button matSuffix routerLink="/settings/console"><mat-icon>mode_edit</mat-icon></a>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatCheckboxModule, MatExpansionModule } from '@angular/material';
|
||||
import { MatCheckboxModule, MatExpansionModule, MatIconModule, MatFormFieldModule, MatInputModule } from '@angular/material';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
@ -9,16 +9,26 @@ import { SettingsComponent } from './settings.component';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
import { MockedToasterService } from '../../services/toaster.service.spec';
|
||||
import { ConsoleService } from '../../services/settings/console.service';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
let settingsService: SettingsService;
|
||||
let consoleService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
consoleService = {
|
||||
command: 'command'
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatExpansionModule, MatCheckboxModule, FormsModule, PersistenceModule, BrowserAnimationsModule],
|
||||
providers: [SettingsService, { provide: ToasterService, useClass: MockedToasterService }],
|
||||
imports: [MatExpansionModule, MatCheckboxModule, FormsModule, PersistenceModule, BrowserAnimationsModule, MatIconModule, MatFormFieldModule, MatInputModule],
|
||||
providers: [
|
||||
SettingsService,
|
||||
{ provide: ToasterService, useClass: MockedToasterService },
|
||||
{ provide: ConsoleService, useValue: consoleService}
|
||||
],
|
||||
declarations: [SettingsComponent]
|
||||
}).compileComponents();
|
||||
|
||||
@ -39,7 +49,8 @@ describe('SettingsComponent', () => {
|
||||
const settings = {
|
||||
crash_reports: true,
|
||||
experimental_features: true,
|
||||
angular_map: false
|
||||
angular_map: false,
|
||||
console_command: ''
|
||||
};
|
||||
const getAll = spyOn(settingsService, 'getAll').and.returnValue(settings);
|
||||
const setAll = spyOn(settingsService, 'setAll');
|
||||
@ -50,4 +61,5 @@ describe('SettingsComponent', () => {
|
||||
component.save();
|
||||
expect(setAll).toHaveBeenCalledWith(settings);
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { ToasterService } from '../../services/toaster.service';
|
||||
import { ConsoleService } from '../../services/settings/console.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings',
|
||||
@ -9,11 +10,16 @@ import { ToasterService } from '../../services/toaster.service';
|
||||
})
|
||||
export class SettingsComponent implements OnInit {
|
||||
settings = { ...SettingsService.DEFAULTS };
|
||||
consoleCommand: string;
|
||||
|
||||
constructor(private settingsService: SettingsService, private toaster: ToasterService) {}
|
||||
constructor(
|
||||
private settingsService: SettingsService,
|
||||
private toaster: ToasterService,
|
||||
private consoleService: ConsoleService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.settings = this.settingsService.getAll();
|
||||
this.consoleCommand = this.consoleService.command;
|
||||
}
|
||||
|
||||
save() {
|
||||
|
@ -35,6 +35,10 @@ export class MockedServerService {
|
||||
resolve(this.servers);
|
||||
});
|
||||
}
|
||||
|
||||
public getServerUrl(server: Server) {
|
||||
return `${server.host}:${server.port}`;
|
||||
}
|
||||
}
|
||||
|
||||
describe('ServerService', () => {
|
||||
|
@ -55,6 +55,10 @@ export class ServerService {
|
||||
return this.onReady(() => this.indexedDbService.get().delete(this.tablename, server.id));
|
||||
}
|
||||
|
||||
public getServerUrl(server: Server) {
|
||||
return `http://${server.host}:${server.port}/`;
|
||||
}
|
||||
|
||||
public getLocalServer(host: string, port: number) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.findAll().then((servers: Server[]) => {
|
||||
|
@ -4,10 +4,20 @@ import { PersistenceService, StorageType } from 'angular-persistence';
|
||||
import { Settings, SettingsService } from './settings.service';
|
||||
|
||||
export class MockedSettingsService {
|
||||
settings = {};
|
||||
|
||||
isExperimentalEnabled() {
|
||||
return true;
|
||||
}
|
||||
getAll() {}
|
||||
|
||||
get(key: string) {
|
||||
return this.settings[key];
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.settings[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SettingsService', () => {
|
||||
@ -50,7 +60,8 @@ describe('SettingsService', () => {
|
||||
expect(service.getAll()).toEqual({
|
||||
crash_reports: true,
|
||||
experimental_features: false,
|
||||
angular_map: false
|
||||
angular_map: false,
|
||||
console_command: undefined
|
||||
});
|
||||
}));
|
||||
|
||||
@ -63,7 +74,8 @@ describe('SettingsService', () => {
|
||||
expect(service.getAll()).toEqual({
|
||||
crash_reports: false,
|
||||
experimental_features: false,
|
||||
angular_map: false
|
||||
angular_map: false,
|
||||
console_command: undefined
|
||||
});
|
||||
}));
|
||||
|
||||
|
@ -6,6 +6,7 @@ export interface Settings {
|
||||
crash_reports: boolean;
|
||||
experimental_features: boolean;
|
||||
angular_map: boolean;
|
||||
console_command: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@ -13,7 +14,8 @@ export class SettingsService {
|
||||
static DEFAULTS: Settings = {
|
||||
crash_reports: true,
|
||||
experimental_features: false,
|
||||
angular_map: false
|
||||
angular_map: false,
|
||||
console_command: undefined
|
||||
};
|
||||
|
||||
private settingsSubject: BehaviorSubject<Settings>;
|
||||
|
33
src/app/services/settings/console.service.spec.ts
Normal file
33
src/app/services/settings/console.service.spec.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ConsoleService } from './console.service';
|
||||
import { MockedSettingsService } from '../settings.service.spec';
|
||||
import { SettingsService } from '../settings.service';
|
||||
import { DefaultConsoleService } from './default-console.service';
|
||||
|
||||
describe('ConsoleService', () => {
|
||||
let service: ConsoleService;
|
||||
let settings: MockedSettingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
let defaultConsoleService = {
|
||||
get: () => 'default'
|
||||
};
|
||||
settings = new MockedSettingsService();
|
||||
service = new ConsoleService(defaultConsoleService as any, settings as any);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should get command from settings if defined', () => {
|
||||
settings.set('console_command', 'from_settings');
|
||||
expect(service.command).toEqual('from_settings');
|
||||
});
|
||||
|
||||
it('should get command from default console if settings are not defined', () => {
|
||||
settings.set('console_command', undefined);
|
||||
expect(service.command).toEqual('default');
|
||||
});
|
||||
});
|
24
src/app/services/settings/console.service.ts
Normal file
24
src/app/services/settings/console.service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DefaultConsoleService } from './default-console.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
|
||||
@Injectable()
|
||||
export class ConsoleService {
|
||||
|
||||
constructor(
|
||||
private defaultConsoleService: DefaultConsoleService,
|
||||
private settingsService: SettingsService
|
||||
) { }
|
||||
|
||||
get command(): string {
|
||||
const command = this.settingsService.get<string>('console_command');
|
||||
if(command === undefined) {
|
||||
return this.defaultConsoleService.get();
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
set command(command: string) {
|
||||
this.settingsService.set<string>('console_command', command);
|
||||
}
|
||||
}
|
45
src/app/services/settings/default-console.service.spec.ts
Normal file
45
src/app/services/settings/default-console.service.spec.ts
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
import { DefaultConsoleService } from './default-console.service';
|
||||
|
||||
describe('DefaultConsoleService', () => {
|
||||
let electronService;
|
||||
let service: DefaultConsoleService;
|
||||
beforeEach(() => {
|
||||
electronService = {
|
||||
isElectronApp: false,
|
||||
isWindows: false,
|
||||
isLinux: false
|
||||
};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service = new DefaultConsoleService(electronService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return undefined when not running in electron', () => {
|
||||
electronService.isElectronApp = false;
|
||||
expect(service.get()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return console for windows', () => {
|
||||
electronService.isElectronApp = true;
|
||||
electronService.isWindows = true;
|
||||
expect(service.get()).toEqual('putty.exe -telnet %h %p -loghost "%d"');
|
||||
})
|
||||
|
||||
it('should return console for linux', () => {
|
||||
electronService.isElectronApp = true;
|
||||
electronService.isLinux = true;
|
||||
expect(service.get()).toEqual('xfce4-terminal --tab -T "%d" -e "telnet %h %p"');
|
||||
})
|
||||
|
||||
it('should return undefined for other platforms', () => {
|
||||
electronService.isElectronApp = true;
|
||||
expect(service.get()).toBeUndefined();
|
||||
})
|
||||
|
||||
});
|
26
src/app/services/settings/default-console.service.ts
Normal file
26
src/app/services/settings/default-console.service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ElectronService } from 'ngx-electron';
|
||||
|
||||
@Injectable()
|
||||
export class DefaultConsoleService {
|
||||
|
||||
constructor(
|
||||
private electronService: ElectronService
|
||||
) { }
|
||||
|
||||
get() {
|
||||
if(!this.electronService.isElectronApp) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(this.electronService.isLinux) {
|
||||
return 'xfce4-terminal --tab -T "%d" -e "telnet %h %p"'
|
||||
}
|
||||
|
||||
if(this.electronService.isWindows) {
|
||||
return 'putty.exe -telnet %h %p -loghost "%d"'
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -26,6 +26,10 @@ a.table-link {
|
||||
}
|
||||
}
|
||||
|
||||
.full-width-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
app-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user