Merge branch 'master' into selection

This commit is contained in:
ziajka 2018-03-13 09:39:16 +01:00
commit 7589c46285
67 changed files with 10471 additions and 13085 deletions

View File

@ -20,7 +20,6 @@
"prefix": "app",
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.min.css",
"../node_modules/ng2-toasty/bundles/style-material.css",
"styles.css",
"theme.scss"
],
@ -28,7 +27,9 @@
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
"prod": "environments/environment.prod.ts",
"electronProd": "environments/environment.electron.prod.ts",
"electronDev": "environments/environment.electron.ts"
}
}
],

33
.appveyor.yml Normal file
View File

@ -0,0 +1,33 @@
version: 1.0.{build}
# Do not build feature branch with open Pull Requests
skip_branch_with_pr: true
platform:
- x64
init:
- git config --global core.autocrlf input
install:
- ps: Install-Product node 8 x64
- yarn
build_script:
- yarn buildforelectron
- "%PYTHON%\\python.exe -m pip install -r scripts\\requirements.txt"
- "%PYTHON%\\python.exe scripts\\build.py download"
- "%PYTHON%\\python.exe scripts\\build.py build_exe -b dist/exe.gns3server -s"
- "%PYTHON%\\python.exe scripts\\build.py validate -b dist"
- yarn electron-builder --win --x64
test: off
artifacts:
- path: 'GNS3*.exe'
name: gns3-web-ui
environment:
GH_TOKEN:
secure: Zb0F4wfA/3zXZBQiEmEGpKIP17hD9gb/CNwxQE2N3J4Eq3z58mp0K0ey5g8Dupsb
PYTHON: "C:\\Python36-x64"

67
.circleci/config.yml Normal file
View File

@ -0,0 +1,67 @@
# iOS CircleCI 2.0 configuration file
version: 2
jobs:
build:
macos:
xcode: "9.1.0"
steps:
- checkout
- run:
name: Set timezone and check current datetime
command: |
sudo systemsetup -settimezone Europe/Warsaw
echo "Today is $(date +"%Y-%m-%d %T")"
- run:
name: Install project
# there is an issue with yarn and cache during executed on CI; for now we just run it twice, second should
# be successful. Check it later if updates fixed the issue
command: |
yarn || true
yarn || true
- run:
name: Building WebUI for distribution
command: |
yarn buildforelectron
- run:
name: Building gns3server
command: |
# brew update is required due to bug in CCI: https://discuss.circleci.com/t/homebrew-must-be-run-under-ruby-2-3-runtimeerror/17232/5
brew update
brew --env
brew --config
brew upgrade python
python -V
pip install -r scripts/requirements.txt
python scripts/build.py download
python scripts/build.py build_exe -b dist/exe.gns3server -s
python scripts/build.py validate -b dist
- run:
name: Dist project
command: |
yarn electron-builder --mac --x64
- run:
name: Gather artifacts
command: |
mkdir artifacts
cp build/*.dmg artifacts/
- store_artifacts:
path: artifacts
destination: artifacts
workflows:
version: 2
build_and_deploy:
jobs:
- build:
filters:
tags:
only: /v.*/

9
.gitignore vendored
View File

@ -4,6 +4,13 @@
/dist
/tmp
/out-tsc
/ng-dist
/build
/scripts/tmp
/scripts/env
/scripts/build
/scripts/dist
/env
# dependencies
/node_modules
@ -32,6 +39,8 @@
npm-debug.log
testem.log
/typings
/yarn-error.log
package-lock.json
# e2e
/e2e/*.js

View File

@ -3,26 +3,55 @@ language: node_js
node_js:
- node
# Issue with Travis: https://github.com/travis-ci/travis-ci/issues/8836#issuecomment-356362524
sudo: required
addons:
apt:
sources:
- google-chrome
- ubuntu-toolchain-r-test
packages:
- google-chrome-stable
- google-chrome-beta
- g++-4.8
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
before_script:
- npm install -g karma
- yarn
# greenkeeper-lockfile support
- greenkeeper-lockfile-update
- npm install -g codecov
script: ng test --watch=false
script: yarn ng test --watch=false --code-coverage
after_success:
- codecov
after_script:
- ng build --base-href /${TRAVIS_BRANCH}/
# greenkeeper-lockfile support
- greenkeeper-lockfile-upload
# publish on gns3.github.io
- yarn ng build --base-href /${TRAVIS_BRANCH}/
- export GIT_LAST_LOG="$(git log -1 --pretty=%B)"
- git clone https://${GITHUB_CREDENTIALS}@github.com/GNS3/gns3.github.io.git github-pages
- mkdir -p github-pages/${TRAVIS_BRANCH}
@ -33,4 +62,14 @@ after_script:
- git add -A
- git commit -m "Deploy - $GIT_LAST_LOG"
- git push origin master
- cd $TRAVIS_BUILD_DIR
# publish
- yarn buildforelectron
- python3 -m venv env
- |
pip3 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
- yarn electron-builder --linux --x64

40
.yarnclean Normal file
View File

@ -0,0 +1,40 @@
# test directories
__tests__
node_modules/*/test
node_modules/*/tests
powered-test
e2e
# asset directories
docs
doc
website
images
# examples
example
examples
# code coverage directories
coverage
.nyc_output
# build scripts
Makefile
Gulpfile.js
Gruntfile.js
# configs
.tern-project
.gitattributes
.editorconfig
.*ignore
.eslintrc
.jshintrc
.flowconfig
.documentup.json
.yarn-metadata.json
# misc
*.gz
*.md

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# Dockerfile for GNS3 Web-ui development
FROM node:carbon
# Create user
RUN useradd --user-group --create-home --shell /bin/false gns3-web-ui
# Create app directory
ENV HOME /home/gns3-web-ui
WORKDIR $HOME
# Copy source
COPY . .
RUN chown -R gns3-web-ui:gns3-web-ui $HOME
# Switch to gns3-web-ui user
USER gns3-web-ui
# Install dependencies
RUN yarn global add @angular/cli
RUN yarn install --pure-lockfile
ENV PATH /home/gns3-web-ui/.yarn/bin:$PATH
EXPOSE 8080
CMD ng serve --host 0.0.0.0 --port 8080

View File

@ -1,5 +1,12 @@
# gns3-web-ui
[![Greenkeeper badge](https://badges.greenkeeper.io/GNS3/gns3-web-ui.svg)](https://greenkeeper.io/)
[![Travis CI](https://api.travis-ci.org/GNS3/gns3-web-ui.svg?branch=master)](https://travis-ci.org)
[![AppVeyor](https://ci.appveyor.com/api/projects/status/github/GNS3/gns3-web-ui?branch=master&svg=true)](https://www.appveyor.com/)
[![CircleCI](https://circleci.com/gh/GNS3/gns3-web-ui/tree/master.png)](https://circleci.com/gh/GNS3/gns3-web-ui/tree/master.png)
[![codecov](https://codecov.io/gh/GNS3/gns3-web-ui/branch/master/graph/badge.svg)](https://codecov.io/gh/GNS3/gns3-web-ui)
Test WebUI implementation for GNS3.
This is not production ready version. It has been made to evaluate possibility of creation Web User Interface for GNS3 application.
@ -32,7 +39,13 @@ ng serve --port 8080
Application is accessible on `http://localhost:8080/`. The app will automatically reload if you change any of the source files.
### Docker container
For development you can run the GNS3 Web UI in a container
.. code:: bash
bash scripts/docker_dev_webui.sh
## Code scaffolding
@ -51,6 +64,55 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Before running the tests make sure you are serving the app via `ng serve`.
# How to build gns3server for WebUI?
python3 scripts/build.py build -b dist
# Releasing
## Bumping releases
We're using [version-bump-prompt](https://www.npmjs.com/package/version-bump-prompt) for increasing version.
Intall `bump` via:
npm install -g version-bump-prompt
If you would like to bump prepatch just type:
bump --prepatch --tag --push
## Final release
We have got configured CircleCI, TravisCI and AppVeyor for distributing application for particular platform. In order to release you need to tag your code nad push it.
Using `bump`:
bump --patch --tag --push
Or manually:
git tag v0.0.1
git push origin v0.0.1
When artifacts are made you can see draft release here: [gns3-web-ui releases](https://github.com/GNS3/gns3-web-ui/releases) which is waiting to be published.
After release please change current version in `package.json` to `X.X.X-beta.0`'. Otherwise artifacts will be overwritten during the next commit.
You may use `bump` to achieve that:
bump --prepatch
## Staging release
In case you would like to create a new staging release. Please create draft release on github, like `0.0.1-dev1`. After successful build you can find there artifacts.
# Development
## How to upgrade package.json?
yarn upgrade --latest
## Further help
If you want to contribute to GNS3 Web UI feel free to reach us at `developers@gns3.net`.

View File

@ -7,8 +7,8 @@ describe('gns3-web-ui App', () => {
page = new Gns3WebUiPage();
});
it('should display welcome message', () => {
it('should display title', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
expect(page.getTitleText()).toEqual('GNS3 Web UI Demo');
});
});

View File

@ -5,6 +5,10 @@ export class Gns3WebUiPage {
return browser.get('/');
}
getTitleText() {
return browser.getTitle();
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}

52
electron-builder.yml Normal file
View File

@ -0,0 +1,52 @@
appId: com.gns3.web-ui
copyright: "Copyright © 2018 GNS3"
productName: "GNS3 Web UI Prototype"
#forceCodeSigning: true
artifactName: "${productName}-${os}-${arch}-${version}.${ext}"
asar: true
compression: normal
directories:
output: build
files:
- dist
- main.js
- package.json
mac:
category: public.app-category.developer-tools
# publish: github
target:
- dmg
dmg:
# background: "build/appdmg.png"
icon: "dist/assets/icons/mac/icon.icns"
iconSize: 128
contents:
- x: 380
y: 240
type: link
path: /Applications
- x: 122
y: 240
type: file
linux:
# publish: github
icon: "dist/assets/icons/png"
category: "Network"
packageCategory: "Network"
description: "GNS3 Web Ui Prototype application. Please don't use it as long as it's not officially announced."
target:
- deb
- AppImage
maintainer: "Dominik Ziajka <dominik@gns3.net>"
win:
publish:
provider: "github"
owner: "GNS3"
icon: "dist/assets/icons/win/icon.ico"
nsis:
perMachine: true

100
main.js Normal file
View File

@ -0,0 +1,100 @@
const electron = require('electron');
var fs = require('fs');
// Module to control application life.
const app = electron.app;
// Module to create native browser window.
const BrowserWindow = electron.BrowserWindow;
const path = require('path');
const url = require('url');
// 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;
let serverProc = null;
let isWin = /^win/.test(process.platform);
const createServerProc = () => {
fs.readdir(path.join(__dirname, 'dist'), (err, files) => {
var serverPath = null;
files.forEach((filename) => {
if(filename.startsWith('exe.')) {
if (isWin) {
serverPath = path.join(__dirname, 'dist', filename, 'gns3server.exe');
}
else {
serverPath = path.join(__dirname, 'dist', filename, 'gns3server');
}
}
});
if (serverPath != null) {
serverProc = require('child_process').execFile(serverPath, []);
if (serverProc != null) {
console.log('gns3server started from path: ' + serverPath);
}
}
});
}
const exitServerProc = () => {
serverProc.kill();
serverProc = null;
}
function createWindow () {
// Create the browser window.
mainWindow = new BrowserWindow({width: 800, height: 600});
// and load the index.html of the app.
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, 'dist/index.html'),
protocol: 'file:',
slashes: true
}));
// Open the DevTools.
// mainWindow.webContents.openDevTools();
// Emitted when the window is closed.
mainWindow.on('closed', function () {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
mainWindow = null
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
app.on('ready', createServerProc);
app.on('will-quit', exitServerProc);
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
});
app.on('activate', function () {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) {
createWindow()
}
});
// 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.

12934
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,58 +1,81 @@
{
"name": "gns3-web-ui",
"version": "0.0.0",
"license": "MIT",
"version": "0.0.1-beta.0",
"license": "GPLv3",
"main": "main.js",
"repository": {
"type": "git",
"url": "https://github.com/GNS3/gns3-web-ui.git"
},
"scripts": {
"ng": "ng",
"start": "ng serve",
"startforelectron": "ng serve -e electronDev",
"build": "ng build",
"buildforelectron": "ng build -e electronProd",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
"e2e": "ng e2e",
"electrondev": "concurrently -k \"yarn startforelectron\" \"electron .\"",
"distlinux": "yarn buildforelectron && electron-builder --linux --x64",
"distwin": "yarn buildforelectron && electron-builder --win --x64",
"distmac": "yarn buildforelectron && electron-builder --mac --x64",
"release": "build"
},
"private": true,
"dependencies": {
"@angular/animations": "^4.4.6",
"@angular/cdk": "^2.0.0-beta.12",
"@angular/common": "^4.0.0",
"@angular/compiler": "^4.0.0",
"@angular/core": "^4.0.0",
"@angular/forms": "^4.0.0",
"@angular/http": "^4.0.0",
"@angular/material": "^2.0.0-beta.12",
"@angular/platform-browser": "^4.0.0",
"@angular/platform-browser-dynamic": "^4.0.0",
"@angular/router": "^4.0.0",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.4",
"angular2-indexeddb": "^1.0.11",
"bootstrap": "^4.0.0-beta",
"@angular/animations": "^5.2.1",
"@angular/cdk": "^5.1.0",
"@angular/common": "^5.2.1",
"@angular/compiler": "^5.2.1",
"@angular/core": "^5.2.1",
"@angular/forms": "^5.2.1",
"@angular/http": "^5.2.1",
"@angular/material": "^5.1.0",
"@angular/platform-browser": "^5.2.1",
"@angular/platform-browser-dynamic": "^5.2.1",
"@angular/router": "^5.2.1",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.9",
"angular2-indexeddb": "^1.2.2",
"bootstrap": "4.0.0",
"core-js": "^2.4.1",
"d3-ng2-service": "^1.16.0",
"ng2-toasty": "^4.0.3",
"d3-ng2-service": "^1.23.3",
"npm-check-updates": "^2.13.0",
"rxjs": "^5.4.1",
"zone.js": "^0.8.14"
"zone.js": "^0.8.20"
},
"devDependencies": {
"@angular/cli": "^1.4.7",
"@angular/compiler-cli": "^4.0.0",
"@angular/language-service": "^4.0.0",
"@types/jasmine": "~2.6.0",
"@angular/cli": "^6.0.0-beta.5",
"@angular/compiler-cli": "^5.2.1",
"@angular/language-service": "^5.2.1",
"@types/jasmine": "~2.8.5",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~8.0.44",
"codelyzer": "~3.2.1",
"jasmine-core": "~2.8.0",
"@types/node": "~9.4.0",
"codelyzer": "~4.1.0",
"electron": "1.8.3",
"electron-builder": "^20.0.4",
"jasmine-core": "~2.99.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.0",
"jquery": "^3.3.1",
"karma": "~2.0.0",
"karma-chrome-launcher": "~2.2.0",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"node-sass": "^4.5.3",
"protractor": "~5.2.0",
"ts-node": "~3.3.0",
"tslint": "~5.7.0",
"typescript": "^2.3.4"
}
"popper.js": "^1.12.3",
"protractor": "~5.3.0",
"ts-node": "~5.0.0",
"tslint": "~5.9.1",
"typescript": ">=2.4.0 <2.6.0"
},
"greenkeeper": {
"ignore": [
"typescript"
]
},
"comments": [
"Typescript should remain below 2.6.0, @todo: check later if packages were adjusted"
]
}

3
renderer.js Normal file
View File

@ -0,0 +1,3 @@
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// All of the Node.js APIs are available in this process.

258
scripts/build.py Normal file
View File

@ -0,0 +1,258 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import re
import pip
import sys
import shutil
import psutil
import zipfile
import requests
import platform
import argparse
import subprocess
from multiprocessing import Process, Queue
from multiprocessing.queues import Empty
from cx_Freeze import setup, Executable
FILE_DIR = os.path.dirname(os.path.realpath(__file__))
WORKING_DIR = os.path.join(FILE_DIR, 'tmp')
SOURCE_ZIP = os.path.join(WORKING_DIR, 'gns3-server.source.zip')
SOURCE_DESTINATION = os.path.join(WORKING_DIR, 'source')
BINARIES_EXTENSION = platform.system() == "Windows" and ".exe" or ""
def download(url, output):
print("Downloading {} to {}".format(url, output))
if os.path.exists(output):
print("{} already exist skip downloading".format(output))
return
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(output, 'wb') as f:
for chunk in r.iter_content():
f.write(chunk)
else:
print("Download error for {} status {}".format(url, r.status_code))
sys.exit(1)
def unzip(filename, directory):
zip = zipfile.ZipFile(filename, 'r')
zip.extractall(directory)
zip.close()
return zip.filelist
def getversion(version):
match = re.search("^(\d+\.\d+(.\d+)?).*$", version)
assert match
return match.group(1)
def getsource_directory():
files = os.listdir(SOURCE_DESTINATION)
if len(files) > 0:
return os.path.join(SOURCE_DESTINATION, files[0])
raise Exception("Cannot find sources for gns3server")
def prepare():
os.makedirs(WORKING_DIR, exist_ok=True)
def download_command(arguments):
shutil.rmtree(SOURCE_DESTINATION, ignore_errors=True)
os.makedirs(SOURCE_DESTINATION)
download("https://github.com/GNS3/gns3-server/archive/2.1.zip", SOURCE_ZIP)
files = unzip(SOURCE_ZIP, SOURCE_DESTINATION)
source_directory = os.path.join(SOURCE_DESTINATION, files[0].filename)
if platform.system() == "Windows":
requirements = 'win-requirements.txt'
else:
requirements = 'requirements.txt'
pip.main(['install', '-r', os.path.join(source_directory, requirements)])
def build_command(arguments):
source_directory = getsource_directory()
sys.path.append(source_directory)
if platform.system() == 'Darwin':
# this fixes cx_freeze bug for OSX and python 3.6, see:
# https://bitbucket.org/ronaldoussoren/pyobjc/issues/185/python-36-modulenotfounderror-no-module
with open(os.path.join(source_directory, 'gns3server', 'main.py')) as f:
main_content = f.read()
if 'cx_freeze_and_python_3_6_missing_library' not in main_content:
main_content += "\ndef cx_freeze_and_python_3_6_missing_library():\n import _sysconfigdata_m_darwin_darwin\n\n"
with open(os.path.join(source_directory, 'gns3server', 'main.py'), 'w') as f:
f.write(main_content)
from gns3server.version import __version__
# cx_Freeze on Windows requires version to be in format a.b.c.d
server_version = getversion(__version__)
executables = [
Executable(
os.path.join(source_directory, "gns3server/main.py"),
targetName="gns3server{}".format(BINARIES_EXTENSION)
),
Executable(
os.path.join(source_directory, "gns3server/utils/vmnet.py"),
targetName="gns3vmnet{}".format(BINARIES_EXTENSION)
)
]
excludes = [
"raven.deprecation", # reported problem in raven package (6.4.0)
"distutils", # issue on macOS
"tkinter", # issue on Windows
]
packages = [
"raven",
"psutil",
"asyncio",
"packaging", # needed for linux
"appdirs",
"idna", # required by aiohttp >= 2.3, cannot be found by cx_Freeze
]
include_files = [
("gns3server/configs", "configs"),
("gns3server/appliances", "appliances"),
("gns3server/templates", "templates"),
("gns3server/symbols", "symbols"),
]
include_files = [(os.path.join(source_directory, x), y) for x, y in include_files]
setup(
name="GNS3",
version=server_version,
description="GNS3 Network simulator",
executables=executables,
options={
"build_exe": {
"includes": [],
"excludes": excludes,
"packages": packages,
"include_files": include_files
},
}
)
def execute(exe, queue, pid_q):
binary_process = subprocess.Popen(
[exe],
bufsize=1, shell=False,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
pid_q.put(binary_process.pid)
while True:
out = binary_process.stdout.read(1)
if out == b'' and binary_process.poll() is not None:
break
queue.put(out)
def validate_command(arguments):
output_directory = os.path.join(os.getcwd(), arguments.b)
files = os.listdir(output_directory)
matching = [f for f in files if f.startswith('exe.')]
if len(matching) == 0:
raise Exception("Cannot find binaries of gns3server")
binary = os.path.join(output_directory, matching[0], 'gns3server{}'.format(BINARIES_EXTENSION))
print("Validating: {}".format(binary))
pid_queue = Queue()
output_queue = Queue()
process = Process(target=execute, args=(binary, output_queue, pid_queue))
process.start()
pid = pid_queue.get()
print("Process is running on pid: " + str(pid))
output = ""
while True:
try:
char = output_queue.get(timeout=3)
output += char.decode()
except Empty:
break
process.terminate()
print("Output of process:")
print(output)
parent = psutil.Process(pid)
parent.kill()
result = "GNS3 Technologies Inc" not in output and 1 or 0
sys.exit(result)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Building gns3server for distribution')
subparsers = parser.add_subparsers(
dest='command', help='Command which needs to be executed')
parser_download = subparsers.add_parser(
'download', help='Downloads source code of gns3server')
parser_build = subparsers.add_parser('build_exe', help='Build gns3server')
parser_build.add_argument('-b', help='Output directory')
parser_build.add_argument('-s', action='store_true', help='Silient building')
parser_validate = subparsers.add_parser('validate', help='Validate build')
parser_validate.add_argument('-b', help='Output directory')
args = parser.parse_args()
if args.command == 'build_exe':
prepare()
build_command(args)
elif args.command == 'download':
prepare()
download_command(args)
elif args.command == 'validate':
prepare()
validate_command(args)

22
scripts/docker_dev_webui.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/sh
#
# Copyright (C) 2018 GNS3 Technologies Inc.
# Copyright (C) 2018 Nabil Bendafi <nabil@bendafi.fr>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# A docker web UI use for localy test
docker build -t gns3-web-ui .
docker run -i -h gns3UIvm -p 8080:8080/tcp -t gns3-web-ui

6
scripts/requirements.txt Normal file
View File

@ -0,0 +1,6 @@
setuptools==38.4
cx_Freeze==5.1.1
requests==2.18.4
packaging==16.8
appdirs==1.4.3
psutil==5.4.0

View File

@ -1,3 +1,5 @@
import { environment } from "../environments/environment";
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@ -6,6 +8,7 @@ import { ServersComponent } from "./servers/servers.component";
import { ProjectsComponent } from "./projects/projects.component";
import { DefaultLayoutComponent } from "./default-layout/default-layout.component";
const routes: Routes = [
{ path: '', component: DefaultLayoutComponent,
children: [
@ -17,8 +20,17 @@ const routes: Routes = [
{ path: 'server/:server_id/project/:project_id', component: ProjectMapComponent },
];
let routerModule;
if (environment.electron) {
// angular in electron has problem with base-href and links separated by slashes, because of that
// we use simply hashes
routerModule = RouterModule.forRoot(routes, {useHash: true});
} else {
routerModule = RouterModule.forRoot(routes);
}
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
imports: [ routerModule ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}

View File

@ -1,2 +1 @@
<router-outlet></router-outlet>
<ng2-toasty></ng2-toasty>
<router-outlet></router-outlet>

View File

@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core';
import {Http} from "@angular/http";
import {MatIconRegistry} from "@angular/material";
import {DomSanitizer} from "@angular/platform-browser";
import {ToastyConfig} from "ng2-toasty";
@Component({
selector: 'app-root',
@ -12,9 +11,7 @@ import {ToastyConfig} from "ng2-toasty";
]
})
export class AppComponent implements OnInit {
constructor(http: Http, iconReg: MatIconRegistry, sanitizer: DomSanitizer, toastyConfig: ToastyConfig) {
toastyConfig.theme = 'material';
constructor(http: Http, iconReg: MatIconRegistry, sanitizer: DomSanitizer) {
iconReg.addSvgIcon('gns3', sanitizer.bypassSecurityTrustResourceUrl('./assets/gns3_icon.svg'));
}

View File

@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { CdkTableModule } from "@angular/cdk/table";
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
@ -16,12 +17,12 @@ import {
MatTableModule,
MatDialogModule,
MatProgressBarModule,
MatProgressSpinnerModule
MatProgressSpinnerModule,
MatSnackBarModule
} from '@angular/material';
import { D3Service } from 'd3-ng2-service';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { ToastyModule } from 'ng2-toasty';
import { AppRoutingModule } from './app-routing.module';
@ -41,7 +42,7 @@ import { ProjectsComponent } from './projects/projects.component';
import { DefaultLayoutComponent } from './default-layout/default-layout.component';
import { ProgressDialogComponent } from './shared/progress-dialog/progress-dialog.component';
import { AppComponent } from './app.component';
import { MapComponent } from './cartography/map/map.component';
import { CreateSnapshotDialogComponent, ProjectMapComponent } from './project-map/project-map.component';
import { ServersComponent, AddServerDialogComponent } from './servers/servers.component';
import { NodeContextMenuComponent } from './shared/node-context-menu/node-context-menu.component';
@ -50,13 +51,13 @@ import { StopNodeActionComponent } from './shared/node-context-menu/actions/stop
import { ApplianceComponent } from './appliance/appliance.component';
import { ApplianceListDialogComponent } from './appliance/appliance-list-dialog/appliance-list-dialog.component';
import { NodeSelectInterfaceComponent } from './shared/node-select-interface/node-select-interface.component';
import { CartographyModule } from './cartography/cartography.module';
import { ToasterService } from './shared/services/toaster.service';
@NgModule({
declarations: [
AppComponent,
MapComponent,
ProjectMapComponent,
ServersComponent,
AddServerDialogComponent,
@ -73,9 +74,9 @@ import { NodeSelectInterfaceComponent } from './shared/node-select-interface/nod
],
imports: [
NgbModule.forRoot(),
ToastyModule.forRoot(),
BrowserModule,
HttpModule,
HttpClientModule,
AppRoutingModule,
FormsModule,
BrowserAnimationsModule,
@ -90,7 +91,9 @@ import { NodeSelectInterfaceComponent } from './shared/node-select-interface/nod
MatDialogModule,
MatProgressBarModule,
MatProgressSpinnerModule,
CdkTableModule
MatSnackBarModule,
CdkTableModule,
CartographyModule
],
providers: [
D3Service,
@ -104,7 +107,8 @@ import { NodeSelectInterfaceComponent } from './shared/node-select-interface/nod
IndexedDbService,
HttpServer,
SnapshotService,
ProgressDialogService
ProgressDialogService,
ToasterService
],
entryComponents: [
AddServerDialogComponent,

View File

@ -19,7 +19,7 @@ describe('ApplianceListDialogComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -19,7 +19,7 @@ describe('ApplianceComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -6,6 +6,7 @@ import { MapComponent } from './map/map.component';
imports: [
CommonModule
],
declarations: [MapComponent]
declarations: [MapComponent],
exports: [MapComponent]
})
export class CartographyModule { }

View File

@ -1 +1,2 @@
<svg preserveAspectRatio="none"></svg>
<svg preserveAspectRatio="none">
</svg>

Before

Width:  |  Height:  |  Size: 39 B

After

Width:  |  Height:  |  Size: 40 B

View File

@ -1,5 +1,5 @@
import {
Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange
Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChange
} from '@angular/core';
import { D3, D3Service } from 'd3-ng2-service';
import {select, Selection} from 'd3-selection';
@ -95,7 +95,7 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
this.graphLayout = new GraphLayout();
this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((n: Node) => {
this.graphLayout.getNodesWidget().addOnNodeDraggingCallback((event: any, n: Node) => {
const linksWidget = this.graphLayout.getLinksWidget();
linksWidget.select(this.svg).each(function(this: SVGGElement, link: Link) {
if (link.target.node_id === n.node_id || link.source.node_id === n.node_id) {
@ -106,7 +106,6 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
});
this.graphLayout.draw(this.svg, this.graphContext);
}
}
@ -117,13 +116,6 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
}
private changeLayout() {
if (this.graphContext != null) {
this.svg
.attr('width', this.graphContext.getSize().width)
.attr('height', this.graphContext.getSize().height);
}
if (this.windowFullSize) {
if (this.parentNativeElement != null) {
this.graphContext.setSize(this.getSize());
@ -133,6 +125,14 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
}
if (this.graphContext != null) {
this.svg
.attr('width', this.graphContext.getSize().width)
.attr('height', this.graphContext.getSize().height);
}
this.graphLayout.setNodes(this.nodes);
this.graphLayout.setLinks(this.links);
this.graphLayout.setDrawings(this.drawings);
@ -174,4 +174,9 @@ export class MapComponent implements OnInit, OnChanges, OnDestroy {
this.onLinksChange(null);
this.redraw();
}
@HostListener('window:resize', ['$event'])
onResize(event) {
this.changeLayout();
}
}

View File

@ -23,7 +23,7 @@ export class LinksWidget implements Widget {
return view.selectAll<SVGGElement, Link>("g.link");
}
public revise(selection: Selection<BaseType, Link, SVGElement, any>) {
public revise(selection: SVGSelection) {
const self = this;
selection
@ -120,7 +120,9 @@ export class LinksWidget implements Widget {
.attr('map-source', (l: Link) => l.source.node_id)
.attr('map-target', (l: Link) => l.target.node_id)
this.revise(link.merge(link_enter));
const merge = link.merge(link_enter);
this.revise(merge);
link
.exit()

View File

@ -30,7 +30,7 @@ export class NodesWidget implements Widget {
this.onNodeDraggedCallback = onNodeDraggedCallback;
}
public addOnNodeDraggingCallback(onNodeDraggingCallback: (n: Node) => void) {
public addOnNodeDraggingCallback(onNodeDraggingCallback: (event: any, n: Node) => void) {
this.onNodeDraggingCallbacks.push(onNodeDraggingCallback);
}
@ -38,9 +38,9 @@ export class NodesWidget implements Widget {
this.symbols = symbols;
}
private executeOnNodeDraggingCallback(n: Node) {
this.onNodeDraggingCallbacks.forEach((callback: (n: Node) => void) => {
callback(n);
private executeOnNodeDraggingCallback(callback_event: any, node: Node) {
this.onNodeDraggingCallbacks.forEach((callback: (e: any, n: Node) => void) => {
callback(callback_event, node);
});
}
@ -105,16 +105,17 @@ export class NodesWidget implements Widget {
.attr('width', (n: Node) => n.width)
.attr('height', (n: Node) => n.height)
.attr('x', (n: Node) => -n.width / 2.)
.attr('y', (n: Node) => -n.height / 2.);
.attr('y', (n: Node) => -n.height / 2.)
.on('mouseover', function (this, n: Node) {
select(this).attr("class", "over");
})
.on('mouseout', function (this, n: Node) {
select(this).attr("class", "");
});
// .attr('width', (n: Node) => n.width)
// .attr('height', (n: Node) => n.height);
// .on('mouseover', function (this, n: Node) {
// select(this).attr("class", "over");
// })
// .on('mouseout', function (this, n: Node) {
// select(this).attr("class", "");
// });
node_enter
.append<SVGTextElement>('text')
@ -157,7 +158,7 @@ export class NodesWidget implements Widget {
n.y = e.y;
self.revise(select(this));
self.executeOnNodeDraggingCallback(n);
self.executeOnNodeDraggingCallback(event, n);
};
const dragging = () => {

View File

@ -16,8 +16,4 @@ export class Context {
public setSize(size: Size): void {
this.size = size;
}
public getRoot() {
return this.root;
}
}

View File

@ -42,3 +42,7 @@ g.node text {
font-family: Roboto !important;
}
svg image:hover, svg image.chosen {
filter: grayscale(100%);
}

View File

@ -4,10 +4,12 @@
<div class="project-toolbar">
<mat-toolbar color="primary" class="project-toolbar">
<button mat-icon-button [matMenuTriggerFor]="mainMenu">
<mat-icon svgIcon="gns3"></mat-icon>
</button>
<mat-toolbar-row>
<button mat-icon-button [matMenuTriggerFor]="mainMenu">
<mat-icon svgIcon="gns3"></mat-icon>
</button>
</mat-toolbar-row>
<mat-menu #mainMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item [routerLink]="['/server', server.id, 'projects']">
<mat-icon>work</mat-icon>

View File

@ -19,20 +19,20 @@ import { MapComponent } from "../cartography/map/map.component";
import { ServerService } from "../shared/services/server.service";
import { ProjectService } from '../shared/services/project.service';
import { Server } from "../shared/models/server";
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material";
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef, MatSnackBar } from "@angular/material";
import { SnapshotService } from "../shared/services/snapshot.service";
import { Snapshot } from "../shared/models/snapshot";
import { ProgressDialogService } from "../shared/progress-dialog/progress-dialog.service";
import { ProgressDialogComponent } from "../shared/progress-dialog/progress-dialog.component";
import { ToastyService } from "ng2-toasty";
import { Drawing } from "../cartography/shared/models/drawing.model";
import { NodeContextMenuComponent } from "../shared/node-context-menu/node-context-menu.component";
import {Appliance} from "../shared/models/appliance";
import {NodeService} from "../shared/services/node.service";
import {Symbol} from "../shared/models/symbol";
import {NodeSelectInterfaceComponent} from "../shared/node-select-interface/node-select-interface.component";
import {Port} from "../shared/models/port";
import {LinkService} from "../shared/services/link.service";
import { Appliance } from "../shared/models/appliance";
import { NodeService } from "../shared/services/node.service";
import { Symbol } from "../shared/models/symbol";
import { NodeSelectInterfaceComponent } from "../shared/node-select-interface/node-select-interface.component";
import { Port } from "../shared/models/port";
import { LinkService } from "../shared/services/link.service";
import { ToasterService } from '../shared/services/toaster.service';
@Component({
@ -69,7 +69,7 @@ export class ProjectMapComponent implements OnInit {
private linkService: LinkService,
private dialog: MatDialog,
private progressDialogService: ProgressDialogService,
private toastyService: ToastyService) {
private toaster: ToasterService) {
}
ngOnInit() {
@ -238,17 +238,11 @@ export class ProjectMapComponent implements OnInit {
const progress = this.progressDialogService.open();
const subscription = creation.subscribe((created_snapshot: Snapshot) => {
this.toastyService.success({
'title': 'Snapshot created',
'msg': `Snapshot '${snapshot.name}' has been created.`
});
this.toaster.success(`Snapshot '${snapshot.name}' has been created.`);
progress.close();
}, (response) => {
const error = response.json();
this.toastyService.error({
'title': 'Cannot create snapshot',
'msg': error.message
});
this.toaster.error(`Cannot create snapshot: ${error.message}`);
progress.close();
});

View File

@ -19,7 +19,7 @@ describe('StartNodeActionComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -19,7 +19,7 @@ describe('StopNodeActionComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -19,7 +19,7 @@ describe('NodeContextMenuComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -17,7 +17,7 @@ export class NodeContextMenuComponent implements OnInit {
private topPosition;
private leftPosition;
private node: Node;
public node: Node;
constructor(
private sanitizer: DomSanitizer,

View File

@ -19,7 +19,7 @@ describe('NodeSelectInterfaceComponent', () => {
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
// it('should create', () => {
// expect(component).toBeTruthy();
// });
});

View File

@ -17,7 +17,7 @@ export class NodeSelectInterfaceComponent implements OnInit {
private topPosition;
private leftPosition;
private node: Node;
public node: Node;
constructor(
private sanitizer: DomSanitizer,

View File

@ -9,7 +9,7 @@ describe('ProgressDialogService', () => {
});
});
it('should be created', inject([ProgressDialogService], (service: ProgressDialogService) => {
expect(service).toBeTruthy();
}));
// it('should be created', inject([ProgressDialogService], (service: ProgressDialogService) => {
// expect(service).toBeTruthy();
// }));
});

View File

@ -9,7 +9,7 @@ describe('ApplianceService', () => {
});
});
it('should be created', inject([ApplianceService], (service: ApplianceService) => {
expect(service).toBeTruthy();
}));
// it('should be created', inject([ApplianceService], (service: ApplianceService) => {
// expect(service).toBeTruthy();
// }));
});

View File

@ -9,7 +9,7 @@ describe('LinkService', () => {
});
});
it('should be created', inject([LinkService], (service: LinkService) => {
expect(service).toBeTruthy();
}));
// it('should be created', inject([LinkService], (service: LinkService) => {
// expect(service).toBeTruthy();
// }));
});

View File

@ -9,7 +9,7 @@ describe('NodeService', () => {
});
});
it('should be created', inject([NodeService], (service: NodeService) => {
expect(service).toBeTruthy();
}));
// it('should be created', inject([NodeService], (service: NodeService) => {
// expect(service).toBeTruthy();
// }));
});

View File

@ -10,7 +10,7 @@ export class ServerService {
private ready: Promise<any>;
constructor(private indexedDbService: IndexedDbService) {
this.ready = indexedDbService.get().createStore(1, (evt) => {
this.ready = indexedDbService.get().openDatabase(1, (evt) => {
const store = evt.currentTarget.result.createObjectStore(
this.tablename, { keyPath: "id", autoIncrement: true });
});

View File

@ -1,15 +1,26 @@
import { TestBed, inject } from '@angular/core/testing';
import { SnapshotService } from './snapshot.service';
import { HttpServer } from './http-server.service';
class MockedHttpServer {
post(server: any, url: string, data: any) {}
get(server: any, url: string, data: any) {}
}
describe('SnapshotService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SnapshotService]
providers: [
SnapshotService,
{provide: HttpServer, useClass: MockedHttpServer}
],
});
});
// it('should be created', inject([SnapshotService], (service: SnapshotService) => {
// expect(service).toBeTruthy();
// }));
it('should be created', inject([SnapshotService], (service: SnapshotService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,32 @@
import { TestBed, inject } from '@angular/core/testing';
import { ToasterService } from './toaster.service';
import { MatSnackBar } from '@angular/material';
class MockedSnackBar {
public open(message: string, ignored: any, options: any) {}
}
describe('ToasterService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ToasterService, {provide: MatSnackBar, useClass: MockedSnackBar}]
});
});
it('should be created', inject([ToasterService], (service: ToasterService) => {
expect(service).toBeTruthy();
}));
it('should open when success', inject([ToasterService, MatSnackBar], (service: ToasterService, snackBar: MatSnackBar) => {
const spy = spyOn(snackBar, 'open');
service.success("message");
expect(snackBar.open).toHaveBeenCalledWith("message", null, { duration: 2000 });
}));
it('should open when error', inject([ToasterService, MatSnackBar], (service: ToasterService, snackBar: MatSnackBar) => {
const spy = spyOn(snackBar, 'open');
service.error("message");
expect(snackBar.open).toHaveBeenCalledWith("message", null, { duration: 2000 });
}));
});

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material';
@Injectable()
export class ToasterService {
constructor(private snackbar: MatSnackBar) { }
public error(message: string) {
this.snackbar.open(message, null, { duration: 2000 });
}
public success(message: string) {
this.snackbar.open(message, null, { duration: 2000 });
}
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

View File

@ -0,0 +1,4 @@
export const environment = {
production: true,
electron: true
};

View File

@ -0,0 +1,4 @@
export const environment = {
production: false,
electron: true
};

View File

@ -4,5 +4,6 @@
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
production: false
production: false,
electron: false
};

View File

@ -3,11 +3,28 @@
<head>
<meta charset="utf-8">
<title>GNS3 Web UI Demo</title>
<base href="/">
<script>
var userAgent = navigator.userAgent.toLowerCase();
// in case we're running in electron because we need it for resources
if (userAgent.indexOf(' electron/') > -1) {
document.write('<base href="' + document.location + '" />');
}
// in case base href is not set, when not defined during the build time (for instance github.io pages)
else if (document.getElementsByTagName('base').length == 0) {
document.write('<base href="/" />');
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
<!--
angular/angular-cli has an issue with handling imports with leading ~ (like: @import '~https://url.url';)
#Ref. https://github.com/angular/angular-cli/issues/9181
/* @TODO: make Material Icons self hosted */
//-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,400,500,700" type="text/css">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Slab:400,700" type="text/css">

View File

@ -1,6 +1,3 @@
/* @TODO: make icons self hosted */
@import '~https://fonts.googleapis.com/icon?family=Material+Icons';
img.logo-header {
width: 50px;
}

View File

@ -9,5 +9,9 @@
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"include": [
"../src/**/*",
"../node_modules/angular2-indexeddb/*"
]
}

View File

@ -15,6 +15,7 @@
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
"**/*.d.ts",
"../node_modules/angular2-indexeddb/*"
]
}

View File

@ -14,6 +14,9 @@
"lib": [
"es2016",
"dom"
]
}
],
},
"include": [
"./src/**/*",
]
}

9468
yarn.lock Normal file

File diff suppressed because it is too large Load Diff