diff --git a/.gitignore b/.gitignore index 91fd7fb7..ad1e4d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ Thumbs.db # Licenses file licenses.csv + +# Temps +.temp-var-file.ts diff --git a/.travis.yml b/.travis.yml index 6d2671cd..ddf8de2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,34 +11,27 @@ cache: directories: - node_modules +dist: xenial + addons: apt: sources: - google-chrome - ubuntu-toolchain-r-test packages: - - google-chrome-stable - - google-chrome-beta + - google_chrome - g++-4.8 +services: + - xvfb + before_install: - - export CHROME_BIN=chromium-browser - - export DISPLAY=:99.0 - - sh -e /etc/init.d/xvfb start # greenkeeper-lockfile support - yarn global add greenkeeper-lockfile@1 - -# Ubuntu trusty supports max python3.4, cx_freeze-5.1.1 requires min 3.5 -# Remove when goes to xenial - | - curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash - export PATH="~/.pyenv/bin:$PATH" - eval "$(pyenv init -)" - eval "$(pyenv virtualenv-init -)" - pyenv virtualenv 3.6.3 general - pyenv global general python3 -V - pip3 -V + wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 + python3 -m pip -V before_script: @@ -70,9 +63,8 @@ after_script: # publish - yarn buildforelectron - - python3 -m venv env - | - pip3 install -r scripts/requirements.txt + python3 -m pip install -r scripts/requirements.txt python3 scripts/build.py download python3 scripts/build.py build_exe -b dist/exe.gns3server -s python3 scripts/build.py validate -b dist diff --git a/electron-builder.yml b/electron-builder.yml index a09192e4..32a97b09 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -13,6 +13,7 @@ files: - main.js - renderer.js - sentry.js + - installed-software.js - package.json mac: diff --git a/installed-software.js b/installed-software.js new file mode 100644 index 00000000..7bd27075 --- /dev/null +++ b/installed-software.js @@ -0,0 +1,88 @@ +var commandExistsSync = require('command-exists').sync; +var app = require('electron').app; +var fs = require('fs'); +var util = require('util'); +var fetch = require('node-fetch') +var stream = require('stream'); +var path = require('path'); +const { spawn } = require('child_process'); +const { ipcMain } = require('electron') + +var pipeline = util.promisify(stream.pipeline); + +exports.getInstalledSoftware = (softwareList) => { + const installed = {}; + for(var software of softwareList) { + var name = software.name; + var locations = software.locations; + + installed[name] = []; + + for(var location of locations) { + // var exists = commandExistsSync(command); + var exists = fs.existsSync(location); + if(exists) { + installed[name].push(location); + } + } + } + return installed; +} + +async function downloadFile(resource, softwarePath) { + var response = await fetch(resource); + if (response.status != 200) { + throw new Error(`Cannot download file ${resource}, response status = ${response.status}`); + } + await pipeline( + response.body, + fs.createWriteStream(softwarePath) + ); +} + +ipcMain.on('installed-software-install', async function (event, software) { + const softwarePath = path.join(app.getPath('temp'), software.binary); + const responseChannel = `installed-software-installed-${software.name}`; + + if (software.type == 'web') { + const exists = fs.existsSync(softwarePath); + if (exists) { + console.log(`Skipping downloading file due to '${softwarePath}' path exists`); + } + else { + console.log(`File '${softwarePath}' doesn't exist. Downloading file.`); + try { + await downloadFile(software.resource, softwarePath); + } catch(error) { + event.sender.send(responseChannel, { + success: false, + message: error.message + }); + } + } + } + + let child; + + if (software.sudo) { + child = spawn('powershell.exe', ['Start-Process', '-FilePath', `"${softwarePath}"`]); + } + else { + child = spawn(softwarePath, software.installation_arguments); + } + + child.on('exit', () => { + event.sender.send(responseChannel, { + success: true + }); + }); + + child.on('error', (err) => { + event.sender.send(responseChannel, { + success: false, + message: err.message + }); + }); + + child.stdin.end(); +}); diff --git a/main.js b/main.js index 172bb28c..74f42f20 100644 --- a/main.js +++ b/main.js @@ -6,6 +6,8 @@ const path = require('path'); const url = require('url'); const yargs = require('yargs'); +const { ipcMain } = require('electron') + // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow; @@ -138,7 +140,6 @@ app.on('activate', function () { } }); + // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. - - diff --git a/package.json b/package.json index 18a24b70..e036c159 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "prettier:base": "prettier", "prettier:check": "yarn prettier:base -- --list-different \"src/**/*.{ts,js,html,scss}\"", "prettier:write": "yarn prettier:base -- --write \"src/**/*.{ts,js,html,scss}\"", - "generate-licenses-file": "yarn license-checker --production --csv --out licenses.csv" + "generate-licenses-file": "yarn license-checker --production --csv --out licenses.csv", + "prebuild": "node set-variables-in-env.js --set src/environments/environment.prod.ts", + "postbuild": "node set-variables-in-env.js --unset src/environments/environment.prod.ts" }, "private": true, "dependencies": { @@ -51,6 +53,7 @@ "angular2-hotkeys": "^2.1.4", "angular2-indexeddb": "^1.2.3", "bootstrap": "4.2.1", + "command-exists": "^1.2.8", "core-js": "^2.6.1", "css-tree": "^1.0.0-alpha.29", "d3-ng2-service": "^2.1.0", @@ -88,10 +91,12 @@ "karma-jasmine": "~2.0.1", "karma-jasmine-html-reporter": "^1.4.0", "license-checker": "^24.1.0", + "node-fetch": "^2.3.0", "node-sass": "^4.11.0", "popper.js": "^1.14.6", "prettier": "^1.15.2", "protractor": "~5.4.2", + "replace": "^1.0.1", "ts-mockito": "^2.3.1", "ts-node": "~7.0.1", "tslint": "~5.12.0", diff --git a/set-variables-in-env.js b/set-variables-in-env.js new file mode 100644 index 00000000..04f4a067 --- /dev/null +++ b/set-variables-in-env.js @@ -0,0 +1,25 @@ +const yargs = require('yargs'); +const fs = require('fs'); + +const argv = yargs.argv; +const tempFile = `.temp-var-file.ts`; + +if(argv.set) { + const envFile = argv.set; + console.log(`Backuping up '${envFile}' into '${tempFile}'.`); + fs.copyFileSync(envFile, tempFile); + let content = fs.readFileSync(envFile, "utf8"); + + if(process.env.SOLARPUTTY_DOWNLOAD_URL) { + const variables = `solarputty_download_url: '${process.env.SOLARPUTTY_DOWNLOAD_URL}',` + content = content.replace('solarputty_download_url: "",', variables); + } + + fs.writeFileSync(envFile, content); +} + +if(argv.unset) { + const envFile = argv.unset; + console.log(`Restoring '${tempFile}' into '${envFile}'.`); + fs.copyFileSync(tempFile, envFile); +} \ No newline at end of file diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index b0eebb3b..7310c969 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -33,6 +33,7 @@ import { EthernetSwitchesAddTemplateComponent } from './components/preferences/b import { EthernetSwitchesTemplateDetailsComponent } from './components/preferences/built-in/ethernet-switches/ethernet-switches-template-details/ethernet-switches-template-details.component'; import { DynamipsPreferencesComponent } from './components/preferences/dynamips/dynamips-preferences/dynamips-preferences.component'; import { IosTemplatesComponent } from './components/preferences/dynamips/ios-templates/ios-templates.component'; +import { InstalledSoftwareComponent } from './components/installed-software/installed-software.component'; const routes: Routes = [ { @@ -44,6 +45,7 @@ const routes: Routes = [ { path: 'local', component: LocalServerComponent }, { path: 'server/:server_id/projects', component: ProjectsComponent }, { path: 'settings', component: SettingsComponent }, + { path: 'installed-software', component: InstalledSoftwareComponent }, { path: 'server/:server_id/preferences', component: PreferencesComponent }, // { path: 'server/:server_id/preferences/general', component: GeneralPreferencesComponent }, { path: 'server/:server_id/preferences/builtin', component: BuiltInPreferencesComponent}, diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bfad6e47..3a51eea7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -76,6 +76,8 @@ import { ProjectNameValidator } from './components/projects/models/projectNameVa import { MatSidenavModule } from '@angular/material'; import { NodeSelectInterfaceComponent } from './components/project-map/node-select-interface/node-select-interface.component'; import { DrawLinkToolComponent } from './components/project-map/draw-link-tool/draw-link-tool.component'; + +import { InstalledSoftwareComponent } from './components/installed-software/installed-software.component'; import { DrawingResizedComponent } from './components/drawings-listeners/drawing-resized/drawing-resized.component'; import { TextEditedComponent } from './components/drawings-listeners/text-edited/text-edited.component'; import { NodeDraggedComponent } from './components/drawings-listeners/node-dragged/node-dragged.component'; @@ -86,6 +88,8 @@ import { InterfaceLabelDraggedComponent } from './components/drawings-listeners/ import { ToolsService } from './services/tools.service'; import { TextAddedComponent } from './components/drawings-listeners/text-added/text-added.component'; import { DrawingAddedComponent } from './components/drawings-listeners/drawing-added/drawing-added.component'; +import { InstallSoftwareComponent } from './components/installed-software/install-software/install-software.component'; + import { StyleEditorDialogComponent } from './components/project-map/drawings-editors/style-editor/style-editor.component'; import { EditTextActionComponent } from './components/project-map/context-menu/actions/edit-text-action/edit-text-action.component'; import { TextEditorDialogComponent } from './components/project-map/drawings-editors/text-editor/text-editor.component'; @@ -123,6 +127,9 @@ import { DynamipsPreferencesComponent } from './components/preferences/dynamips/ import { IosTemplatesComponent } from './components/preferences/dynamips/ios-templates/ios-templates.component'; import { IosService } from './services/ios.service'; import { SymbolsComponent } from './components/preferences/common/symbols/symbols.component'; +import { InstalledSoftwareService } from './services/installed-software.service'; +import { ExternalSoftwareDefinitionService } from './services/external-software-definition.service'; +import { PlatformService } from './services/platform.service'; if (environment.production) { Raven.config('https://b2b1cfd9b043491eb6b566fd8acee358@sentry.io/842726', { @@ -165,6 +172,7 @@ if (environment.production) { ServerDiscoveryComponent, NodeSelectInterfaceComponent, DrawLinkToolComponent, + InstalledSoftwareComponent, DrawingAddedComponent, DrawingResizedComponent, TextAddedComponent, @@ -174,6 +182,7 @@ if (environment.production) { DrawingDraggedComponent, LinkCreatedComponent, InterfaceLabelDraggedComponent, + InstallSoftwareComponent, StyleEditorDialogComponent, TextEditorDialogComponent, QemuPreferencesComponent, @@ -254,7 +263,10 @@ if (environment.production) { TemplateMocksService, VirtualBoxService, BuiltInTemplatesService, - IosService + IosService, + InstalledSoftwareService, + ExternalSoftwareDefinitionService, + PlatformService ], entryComponents: [ AddServerDialogComponent, diff --git a/src/app/cartography/components/text-editor/text-editor.component.ts b/src/app/cartography/components/text-editor/text-editor.component.ts index 3de27fa4..f8b9fa8e 100644 --- a/src/app/cartography/components/text-editor/text-editor.component.ts +++ b/src/app/cartography/components/text-editor/text-editor.component.ts @@ -60,6 +60,7 @@ export class TextEditorComponent implements OnInit, OnDestroy { ); this.deactivateTextAdding(); this.innerText = ''; + this.temporaryTextElement.nativeElement.innerText = ''; this.temporaryTextElement.nativeElement.removeEventListener('focusout', this.textListener); this.renderer.setStyle(this.temporaryTextElement.nativeElement, 'display', 'none'); }; diff --git a/src/app/components/installed-software/install-software/install-software.component.html b/src/app/components/installed-software/install-software/install-software.component.html new file mode 100644 index 00000000..dba60bbf --- /dev/null +++ b/src/app/components/installed-software/install-software/install-software.component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/app/components/installed-software/install-software/install-software.component.scss b/src/app/components/installed-software/install-software/install-software.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/installed-software/install-software/install-software.component.spec.ts b/src/app/components/installed-software/install-software/install-software.component.spec.ts new file mode 100644 index 00000000..33f9e4df --- /dev/null +++ b/src/app/components/installed-software/install-software/install-software.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstallSoftwareComponent } from './install-software.component'; + +describe('InstallSoftwareComponent', () => { + let component: InstallSoftwareComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ InstallSoftwareComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(InstallSoftwareComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/components/installed-software/install-software/install-software.component.ts b/src/app/components/installed-software/install-software/install-software.component.ts new file mode 100644 index 00000000..88733ada --- /dev/null +++ b/src/app/components/installed-software/install-software/install-software.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, Output, EventEmitter, Input, OnDestroy, OnChanges } from '@angular/core'; +import { ElectronService } from 'ngx-electron'; + +@Component({ + selector: 'app-install-software', + templateUrl: './install-software.component.html', + styleUrls: ['./install-software.component.scss'] +}) +export class InstallSoftwareComponent implements OnInit, OnDestroy, OnChanges { + @Input('software') + software: any; + + @Output() + installedChanged = new EventEmitter(); + + public disabled = false; + public readyToInstall = true; + public buttonText: string; + + constructor( + private electronService: ElectronService + ) { } + + ngOnInit() { + this.electronService.ipcRenderer.on(this.responseChannel, (event, data) => { + this.updateButton(); + this.installedChanged.emit(data); + }); + } + + ngOnDestroy() { + this.electronService.ipcRenderer.removeAllListeners(this.responseChannel); + } + + ngOnChanges() { + this.updateButton(); + } + + install() { + this.disabled = true; + this.buttonText = "Installing"; + this.electronService.ipcRenderer.send('installed-software-install', this.software); + } + + private get responseChannel() { + return `installed-software-installed-${this.software.name}`; + } + + private updateButton() { + this.disabled = this.software.installed; + + if (this.software.installed) { + this.buttonText = "Installed"; + } + else { + this.buttonText = "Install"; + } + } +} diff --git a/src/app/components/installed-software/installed-software.component.html b/src/app/components/installed-software/installed-software.component.html new file mode 100644 index 00000000..6f7b2e7d --- /dev/null +++ b/src/app/components/installed-software/installed-software.component.html @@ -0,0 +1,28 @@ +
+
+

Installed software

+
+
+
+ + + + Name + {{ row.name }} + + + + + + + + + + + + +
+ +
+
+ \ No newline at end of file diff --git a/src/app/components/installed-software/installed-software.component.scss b/src/app/components/installed-software/installed-software.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/installed-software/installed-software.component.spec.ts b/src/app/components/installed-software/installed-software.component.spec.ts new file mode 100644 index 00000000..c52bc58a --- /dev/null +++ b/src/app/components/installed-software/installed-software.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InstalledSoftwareComponent } from './installed-software.component'; + +describe('InstalledSoftwareComponent', () => { + let component: InstalledSoftwareComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ InstalledSoftwareComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(InstalledSoftwareComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + // it('should create', () => { + // expect(component).toBeTruthy(); + // }); +}); diff --git a/src/app/components/installed-software/installed-software.component.ts b/src/app/components/installed-software/installed-software.component.ts new file mode 100644 index 00000000..2d8e098b --- /dev/null +++ b/src/app/components/installed-software/installed-software.component.ts @@ -0,0 +1,53 @@ +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; +import { InstalledSoftwareService } from '../../services/installed-software.service'; +import { DataSource } from '@angular/cdk/table'; +import { Observable, of, BehaviorSubject } from 'rxjs'; + +@Component({ + selector: 'app-installed-software', + templateUrl: './installed-software.component.html', + styleUrls: ['./installed-software.component.scss'] +}) +export class InstalledSoftwareComponent implements OnInit { + dataSource: InstalledSoftwareDataSource; + displayedColumns = ['name', 'actions']; + + constructor( + private installedSoftwareService: InstalledSoftwareService, + private changeDetectorRef: ChangeDetectorRef + ) { } + + ngOnInit() { + this.dataSource = new InstalledSoftwareDataSource(this.installedSoftwareService); + } + + onInstalled(event) { + this.dataSource.refresh(); + /** + * During software installation we are not performing any user action + * in browser hence Angular doesn't know something suppose to change. + * Here we ask to detect changes manually. + */ + this.changeDetectorRef.detectChanges(); + } +} + +export class InstalledSoftwareDataSource extends DataSource { + installed = new BehaviorSubject([]); + + constructor(private installedSoftwareService: InstalledSoftwareService) { + super(); + } + + connect(): Observable { + this.refresh(); + return this.installed; + } + + disconnect() {} + + refresh() { + this.installed.next(this.installedSoftwareService.list()); + } + +} diff --git a/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts b/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts index 2eefb6a9..5234c05b 100644 --- a/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/edit-style-action/edit-style-action.component.ts @@ -20,7 +20,7 @@ export class EditStyleActionComponent implements OnInit { editStyle() { const dialogRef = this.dialog.open(StyleEditorDialogComponent, { - width: '450px' + width: '300px' }); let instance = dialogRef.componentInstance; instance.server = this.server; diff --git a/src/app/components/project-map/context-menu/actions/edit-text-action/edit-text-action.component.ts b/src/app/components/project-map/context-menu/actions/edit-text-action/edit-text-action.component.ts index 435733ff..cc2f0720 100644 --- a/src/app/components/project-map/context-menu/actions/edit-text-action/edit-text-action.component.ts +++ b/src/app/components/project-map/context-menu/actions/edit-text-action/edit-text-action.component.ts @@ -20,7 +20,7 @@ export class EditTextActionComponent implements OnInit { editText() { const dialogRef = this.dialog.open(TextEditorDialogComponent, { - width: '450px' + width: '300px' }); let instance = dialogRef.componentInstance; instance.server = this.server; diff --git a/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.html b/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.html index e4ee1bc5..62f53a10 100644 --- a/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.html +++ b/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.html @@ -1,24 +1,27 @@

Style editor

- -
Fill color:
- -
- -
Border color:
- -
- -
Border width:
- -
- -
Border style:
- -
- -
Rotation:
- -
+ + +
diff --git a/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.scss b/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.scss index 72ccb7a7..cc9d349b 100644 --- a/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.scss +++ b/src/app/components/project-map/drawings-editors/style-editor/style-editor.component.scss @@ -1,25 +1,22 @@ .item { - display: flex; height: 25px; font-size: 10pt; margin-bottom: 10px; } .item-name { - width: 30%; + margin-bottom: 10px; } .item-value { - width: 70%; - margin: 3px; - margin-right: 4px; - margin-left: 4px; + width: 100%; + margin-bottom: 10px; } .input-color { padding: 0px; border-width: 0px; - width: 70%; + width: 100%; background-color: transparent; outline: none; } @@ -27,3 +24,26 @@ input:focus { outline: none; } + +input[type="color"] { + -webkit-appearance: none; + border: none; + height: 25px; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: none; +} + +.modal-form-container { + display: flex; + flex-direction: column; +} + +.modal-form-container > * { + width: 100%; +} diff --git a/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.html b/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.html index 63f8daf9..97348473 100644 --- a/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.html +++ b/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.html @@ -1,13 +1,17 @@

Text editor

- -
Fill color:
- -
- -
Rotation:
- -
- + + +
diff --git a/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.scss b/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.scss index 24fd9cad..fa95b1f1 100644 --- a/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.scss +++ b/src/app/components/project-map/drawings-editors/text-editor/text-editor.component.scss @@ -1,24 +1,22 @@ .item { - display: flex; height: 25px; font-size: 10pt; margin-bottom: 10px; } .item-name { - width: 30%; + margin-bottom: 10px; } .item-value { - width: 70%; - margin-top: 3px; - margin-bottom: 3px; + width: 100%; + margin-bottom: 10px; } .input-color { padding: 0px; border-width: 0px; - width: 70%; + width: 100%; background-color: transparent; outline: none; } @@ -27,7 +25,30 @@ input:focus { outline: none; } +input[type="color"] { + -webkit-appearance: none; + border: none; + height: 25px; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: none; +} + .text { width: 100%; height: 150px; } + +.modal-form-container { + display: flex; + flex-direction: column; +} + +.modal-form-container > * { + width: 100%; +} diff --git a/src/app/components/project-map/project-map.component.ts b/src/app/components/project-map/project-map.component.ts index 9c4bd4f5..8dffbcf4 100644 --- a/src/app/components/project-map/project-map.component.ts +++ b/src/app/components/project-map/project-map.component.ts @@ -207,6 +207,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy { } setUpMapCallbacks() { + if (!this.readonly) { + this.toolsService.selectionToolActivation(true); + } + const onNodeContextMenu = this.nodeWidget.onContextMenu.subscribe((eventNode: NodeContextMenu) => { const node = this.mapNodeToNode.convert(eventNode.node); this.contextMenu.openMenuForNode(node, eventNode.event.clientY, eventNode.event.clientX); @@ -219,7 +223,7 @@ export class ProjectMapComponent implements OnInit, OnDestroy { const onContextMenu = this.selectionTool.contextMenuOpened.subscribe((event) => { const selectedItems = this.selectionManager.getSelected(); - if (selectedItems.length === 0) return; + if (selectedItems.length === 0 || !(event instanceof MouseEvent)) return; let drawings: Drawing[] = []; let nodes: Node[] = []; @@ -245,6 +249,10 @@ export class ProjectMapComponent implements OnInit, OnDestroy { } onNodeCreation(template: Template) { + if(!template) { + return; + } + this.nodeService.createFromTemplate(this.server, this.project, template, 0, 0, 'local').subscribe(() => { this.projectService.nodes(this.server, this.project.project_id).subscribe((nodes: Node[]) => { this.nodesDataSource.set(nodes); diff --git a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css index 6d26582e..5485afaa 100644 --- a/src/app/components/projects/import-project-dialog/import-project-dialog.component.css +++ b/src/app/components/projects/import-project-dialog/import-project-dialog.component.css @@ -3,9 +3,9 @@ } .file-button{ - height: 50px; - width: 20%; - margin-top: 10px; + width: 100%; + margin-top: 5px; + margin-bottom: 10px; padding: 0px; } @@ -15,8 +15,7 @@ } .file-name-form-field { - margin-left: 5%; - width: 65%; + width: 100%; } .delete-button { diff --git a/src/app/components/projects/projects.component.ts b/src/app/components/projects/projects.component.ts index 819a9c99..f6ae4fbb 100644 --- a/src/app/components/projects/projects.component.ts +++ b/src/app/components/projects/projects.component.ts @@ -109,7 +109,7 @@ export class ProjectsComponent implements OnInit { addBlankProject() { const dialogRef = this.dialog.open(AddBlankProjectDialogComponent, { - width: '550px' + width: '400px' }); let instance = dialogRef.componentInstance; instance.server = this.server; @@ -117,7 +117,7 @@ export class ProjectsComponent implements OnInit { importProject() { const dialogRef = this.dialog.open(ImportProjectDialogComponent, { - width: '550px' + width: '400px' }); let instance = dialogRef.componentInstance; instance.server = this.server; diff --git a/src/app/components/settings/settings.component.html b/src/app/components/settings/settings.component.html index 983a5c1f..f47b7e75 100644 --- a/src/app/components/settings/settings.component.html +++ b/src/app/components/settings/settings.component.html @@ -3,7 +3,7 @@
- + Local settings Customize your local settings @@ -32,4 +32,5 @@
+
diff --git a/src/app/layouts/default-layout/default-layout.component.html b/src/app/layouts/default-layout/default-layout.component.html index 27e36111..83fd5860 100644 --- a/src/app/layouts/default-layout/default-layout.component.html +++ b/src/app/layouts/default-layout/default-layout.component.html @@ -6,7 +6,22 @@ - + + + + + + + + @@ -14,4 +29,7 @@ -
GNS3 Web UI demo © 2018
+
+ GNS3 Web UI © 2019 +
+ diff --git a/src/app/layouts/default-layout/default-layout.component.spec.ts b/src/app/layouts/default-layout/default-layout.component.spec.ts index bd44ca4e..47f00002 100644 --- a/src/app/layouts/default-layout/default-layout.component.spec.ts +++ b/src/app/layouts/default-layout/default-layout.component.spec.ts @@ -1,14 +1,35 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { DefaultLayoutComponent } from './default-layout.component'; +import { ElectronService } from 'ngx-electron'; +import { MatIconModule, MatMenuModule, MatToolbarModule, MatProgressSpinnerModule } from '@angular/material'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ProgressComponent } from '../../common/progress/progress.component'; +import { ProgressService } from '../../common/progress/progress.service'; + + +class ElectronServiceMock { + public isElectronApp: boolean; +} describe('DefaultLayoutComponent', () => { let component: DefaultLayoutComponent; let fixture: ComponentFixture; + let electronServiceMock: ElectronServiceMock; beforeEach(async(() => { + electronServiceMock = new ElectronServiceMock(); + TestBed.configureTestingModule({ - declarations: [DefaultLayoutComponent] + declarations: [DefaultLayoutComponent, ProgressComponent], + imports: [MatIconModule, MatMenuModule, MatToolbarModule, RouterTestingModule, MatProgressSpinnerModule], + providers: [ + { + provide: ElectronService, + useValue: electronServiceMock + }, + ProgressService + ] }).compileComponents(); })); @@ -18,7 +39,19 @@ describe('DefaultLayoutComponent', () => { fixture.detectChanges(); }); - // it('should create', () => { - // expect(component).toBeTruthy(); - // }); + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should installed software be available', () => { + electronServiceMock.isElectronApp = true; + component.ngOnInit(); + expect(component.isInstalledSoftwareAvailable).toBeTruthy(); + }); + + it('should installed software be not available', () => { + electronServiceMock.isElectronApp = false; + component.ngOnInit(); + expect(component.isInstalledSoftwareAvailable).toBeFalsy(); + }); }); diff --git a/src/app/layouts/default-layout/default-layout.component.ts b/src/app/layouts/default-layout/default-layout.component.ts index 9a34e65e..162f83c6 100644 --- a/src/app/layouts/default-layout/default-layout.component.ts +++ b/src/app/layouts/default-layout/default-layout.component.ts @@ -1,3 +1,4 @@ +import { ElectronService } from 'ngx-electron'; import { Component, OnInit, ViewEncapsulation } from '@angular/core'; @Component({ @@ -7,7 +8,14 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core'; styleUrls: ['./default-layout.component.css'] }) export class DefaultLayoutComponent implements OnInit { - constructor() {} + public isInstalledSoftwareAvailable = false; + + constructor( + private electronService: ElectronService + ) {} + + ngOnInit() { + this.isInstalledSoftwareAvailable = this.electronService.isElectronApp; + } - ngOnInit() {} } diff --git a/src/app/models/software.ts b/src/app/models/software.ts new file mode 100644 index 00000000..6b294480 --- /dev/null +++ b/src/app/models/software.ts @@ -0,0 +1,12 @@ +export type SoftwareType = "web"; + +export class Software { + name: string; + locations: string[]; + type: SoftwareType; + resource: string; + binary: string; + sudo: boolean; + installation_arguments: string[]; + installed: boolean; +} \ No newline at end of file diff --git a/src/app/services/external-software-definition.service.spec.ts b/src/app/services/external-software-definition.service.spec.ts new file mode 100644 index 00000000..de4b073d --- /dev/null +++ b/src/app/services/external-software-definition.service.spec.ts @@ -0,0 +1,97 @@ +import { TestBed } from '@angular/core/testing'; + +import { ExternalSoftwareDefinitionService } from './external-software-definition.service'; +import { PlatformService } from './platform.service'; + +import Spy = jasmine.Spy; + +class PlatformServiceMock { + platform: string; + isWindows() { + return this.platform == 'windows'; + } + isLinux() { + return this.platform == 'linux'; + } + isDarwin() { + return this.platform == 'darwin'; + } +} + + +describe('ExternalSoftwareDefinitionService', () => { + let platformServiceMock: PlatformServiceMock; + let service: ExternalSoftwareDefinitionService; + + beforeEach(() => { + platformServiceMock = new PlatformServiceMock(); + }); + + beforeEach(() => TestBed.configureTestingModule({ + providers: [ + ExternalSoftwareDefinitionService, + { provide: PlatformService, useValue: platformServiceMock} + ] + })); + + beforeEach(() => { + service = TestBed.get(ExternalSoftwareDefinitionService); + }) + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return list for windows', () => { + let software = service.getForWindows(); + expect(software.length).toEqual(1); + }); + + it('should return list for linux', () => { + let software = service.getForLinux(); + expect(software.length).toEqual(0); + }); + + it('should return list for darwin', () => { + let software = service.getForDarwin(); + expect(software.length).toEqual(0); + }); + + describe('ExternalSoftwareDefinitionService.get', () => { + let windowsSpy: Spy; + let darwinSpy: Spy; + let linuxSpy: Spy; + + beforeEach(() => { + windowsSpy = spyOn(service, 'getForWindows').and.callThrough(); + darwinSpy = spyOn(service, 'getForDarwin').and.callThrough(); + linuxSpy = spyOn(service, 'getForLinux').and.callThrough(); + }); + + it('should return list when on windows', () => { + platformServiceMock.platform = 'windows'; + expect(service.get()).toBeDefined(); + expect(windowsSpy).toHaveBeenCalled(); + expect(darwinSpy).not.toHaveBeenCalled(); + expect(linuxSpy).not.toHaveBeenCalled(); + }); + + it('should return list when on linux', () => { + platformServiceMock.platform = 'linux'; + expect(service.get()).toBeDefined(); + expect(windowsSpy).not.toHaveBeenCalled(); + expect(darwinSpy).not.toHaveBeenCalled(); + expect(linuxSpy).toHaveBeenCalled(); + }); + + it('should return list when on darwin', () => { + platformServiceMock.platform = 'darwin'; + expect(service.get()).toBeDefined(); + expect(windowsSpy).not.toHaveBeenCalled(); + expect(darwinSpy).toHaveBeenCalled(); + expect(linuxSpy).not.toHaveBeenCalled(); + }); + + }) + +}); diff --git a/src/app/services/external-software-definition.service.ts b/src/app/services/external-software-definition.service.ts new file mode 100644 index 00000000..da205e8b --- /dev/null +++ b/src/app/services/external-software-definition.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from '@angular/core'; +import { PlatformService } from './platform.service'; +import { environment } from '../../environments/environment'; + +@Injectable() +export class ExternalSoftwareDefinitionService { + + constructor( + private platformService: PlatformService + ) { } + + get() { + if(this.platformService.isWindows()) { + return this.getForWindows(); + } + if(this.platformService.isDarwin()) { + return this.getForDarwin(); + } + return this.getForLinux(); + } + + getForWindows() { + let software = [{ + name: 'Wireshark', + locations: [ + 'C:\\Program Files\\Wireshark\\Wireshark.exe' + ], + type: 'web', + resource: 'https://1.na.dl.wireshark.org/win64/all-versions/Wireshark-win64-2.6.3.exe', + binary: 'Wireshark.exe', + sudo: true, + installation_arguments: [], + installed: false + } + ]; + + const solarPutty = { + name: 'SolarPuTTY', + locations: [ + 'SolarPuTTY.exe' + ], + type: 'web', + resource: '', + binary: 'SolarPuTTY.exe', + sudo: false, + installation_arguments: ['--only-ask'], + installed: false + }; + + if(environment.solarputty_download_url) { + solarPutty.resource = environment.solarputty_download_url; + software.push(solarPutty); + } + + return software; + } + + getForLinux() { + return []; + } + + getForDarwin() { + return []; + } +} diff --git a/src/app/services/installed-software.service.spec.ts b/src/app/services/installed-software.service.spec.ts new file mode 100644 index 00000000..25c35daa --- /dev/null +++ b/src/app/services/installed-software.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { InstalledSoftwareService } from './installed-software.service'; + +describe('InstalledSoftwareService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + // it('should be created', () => { + // const service: InstalledSoftwareService = TestBed.get(InstalledSoftwareService); + // expect(service).toBeTruthy(); + // }); +}); diff --git a/src/app/services/installed-software.service.ts b/src/app/services/installed-software.service.ts new file mode 100644 index 00000000..1e64eef5 --- /dev/null +++ b/src/app/services/installed-software.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { ElectronService } from 'ngx-electron'; +import { ExternalSoftwareDefinitionService } from './external-software-definition.service'; + +@Injectable() +export class InstalledSoftwareService { + constructor( + private electronService: ElectronService, + private externalSoftwareDefinition: ExternalSoftwareDefinitionService + ) { } + + list() { + const softwareDefinition = this.externalSoftwareDefinition.get(); + + const installedSoftware = this.electronService.remote.require('./installed-software.js') + .getInstalledSoftware(softwareDefinition); + + return softwareDefinition.map((software) => { + software.installed = installedSoftware[software.name].length > 0; + return software; + }); + } + +} diff --git a/src/app/services/platform.service.spec.ts b/src/app/services/platform.service.spec.ts new file mode 100644 index 00000000..7a49f5de --- /dev/null +++ b/src/app/services/platform.service.spec.ts @@ -0,0 +1,57 @@ +import { TestBed } from '@angular/core/testing'; + +import { PlatformService } from './platform.service'; +import { ElectronService } from 'ngx-electron'; + +class ElectronServiceMock { + process = { + platform: 'unknown' + }; +} + +describe('PlatformService', () => { + let electronServiceMock: ElectronServiceMock; + let service: PlatformService; + + beforeEach(() => { + electronServiceMock = new ElectronServiceMock(); + }); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + PlatformService, + {provide: ElectronService, useValue: electronServiceMock} + ] + }); + }); + + beforeEach(() => { + service = TestBed.get(PlatformService); + }) + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should be on windows platform', () => { + electronServiceMock.process.platform = 'win32'; + expect(service.isWindows()).toBeTruthy(); + expect(service.isDarwin()).toBeFalsy(); + expect(service.isLinux()).toBeFalsy(); + }); + + it('should be on linux platform', () => { + electronServiceMock.process.platform = 'linux'; + expect(service.isWindows()).toBeFalsy(); + expect(service.isDarwin()).toBeFalsy(); + expect(service.isLinux()).toBeTruthy(); + }); + + it('should be on darwin platform', () => { + electronServiceMock.process.platform = 'darwin'; + expect(service.isWindows()).toBeFalsy(); + expect(service.isDarwin()).toBeTruthy(); + expect(service.isLinux()).toBeFalsy(); + }); +}); diff --git a/src/app/services/platform.service.ts b/src/app/services/platform.service.ts new file mode 100644 index 00000000..58f6b5df --- /dev/null +++ b/src/app/services/platform.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { ElectronService } from 'ngx-electron'; + +@Injectable() +export class PlatformService { + + constructor( + private electronService: ElectronService + ) { } + + isWindows() { + return this.electronService.process.platform === 'win32'; + } + + isLinux() { + return this.electronService.process.platform === 'linux'; + } + + isDarwin() { + return this.electronService.process.platform === 'darwin'; + } +} diff --git a/src/environments/environment.electron.prod.ts b/src/environments/environment.electron.prod.ts index b7954d59..c8f57a95 100644 --- a/src/environments/environment.electron.prod.ts +++ b/src/environments/environment.electron.prod.ts @@ -1,4 +1,5 @@ export const environment = { production: true, - electron: true + electron: true, + solarputty_download_url: "" }; diff --git a/src/environments/environment.electron.ts b/src/environments/environment.electron.ts index 2a4b4a8f..bfe95058 100644 --- a/src/environments/environment.electron.ts +++ b/src/environments/environment.electron.ts @@ -1,4 +1,5 @@ export const environment = { production: false, - electron: true + electron: true, + solarputty_download_url: "" }; diff --git a/src/environments/environment.github.prod.ts b/src/environments/environment.github.prod.ts index bca92dff..2bf4ad5f 100644 --- a/src/environments/environment.github.prod.ts +++ b/src/environments/environment.github.prod.ts @@ -1,5 +1,6 @@ export const environment = { production: true, electron: false, - githubio: true + githubio: true, + solarputty_download_url: "" }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index cbe10f98..cd119730 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,5 +1,6 @@ export const environment = { production: true, electron: false, - githubio: false + githubio: false, + solarputty_download_url: "", }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 02712c06..b4dfd930 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -5,7 +5,8 @@ export const environment = { production: false, electron: false, - githubio: false + githubio: false, + solarputty_download_url: '' }; /* diff --git a/src/styles.css b/src/styles.css index ff884be0..fc243b2c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -16,6 +16,10 @@ a.table-link { color: white!important; } +.mat-dialog-actions { + margin-bottom: -12px!important; +} + @-moz-document url-prefix() { .temporaryElement{ line-height: 1.4em; diff --git a/yarn.lock b/yarn.lock index 972a0cfc..ac114b3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2035,6 +2035,11 @@ colors@1.1.2: resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= +colors@1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.4.tgz#e0cb41d3e4b20806b3bfc27f4559f01b94bc2f7c" + integrity sha512-6Y+iBnWmXL+AWtlOp2Vr6R2w5MUlNJRwR0ShVFaAb1CqWzhPOpQg4L0jxD+xpw/Nc8QJwaq3KM79QUCriY8CWQ== + colors@^1.1.0: version "1.3.3" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" @@ -2061,6 +2066,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +command-exists@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.8.tgz#715acefdd1223b9c9b37110a149c6392c2852291" + integrity sha512-PM54PkseWbiiD/mMsbvW351/u+dafwTJ0ye2qB60G1aGQP9j3xK2gmMDc+R34L3nDtx4qMCitXT75mkbkGJDLw== + commander@2, commander@^2.12.1: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" @@ -5941,6 +5951,11 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" +node-fetch@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" + integrity sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA== + node-forge@0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" @@ -7300,6 +7315,15 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" +replace@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace/-/replace-1.0.1.tgz#a7838aaf5977d443e903aea4ba52b2c7ac432190" + integrity sha512-Qh0XcLMb3LYa6fs7V30zQHACbJTQJUERl22lVjaq0dJp6B5q1t/vARXDauS1ywpIs3ZkT3APj4EA6aOoHoaHDA== + dependencies: + colors "1.2.4" + minimatch "3.0.4" + yargs "12.0.5" + request@^2.45.0, request@^2.83.0, request@^2.87.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" @@ -9240,6 +9264,24 @@ yargs@12.0.2: y18n "^3.2.1 || ^4.0.0" yargs-parser "^10.1.0" +yargs@12.0.5, yargs@^12.0.5: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + yargs@9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" @@ -9259,24 +9301,6 @@ yargs@9.0.1: y18n "^3.2.1" yargs-parser "^7.0.0" -yargs@^12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - yargs@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"