Draft of WebUI

This commit is contained in:
ziajka 2017-09-25 13:07:52 +02:00
parent eca48dd435
commit 7968368b81
79 changed files with 11391 additions and 1 deletions

61
.angular-cli.json Normal file
View File

@ -0,0 +1,61 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "gns3-web-ui"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.min.css",
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db

View File

@ -1,2 +1,56 @@
# gns3-web-ui
WebUI implementation for GNS3
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.
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.6.
## Installation for development
Please install `npm` if not present in your system.
Next step is `angular-cli` installation:
```
npm install @angular/cli
```
## Development server
### Run GNS3 server
Please run locally GNS3 server.
### Using default CORS policy.
GNS3 server contains CORS policies to run Web UI on 8080 at localhost. In order to use it, please run development server with custom port:
```
ng serve --port 8080
```
Application is accessible on `http://localhost:8080/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
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`.
## Further help
If you want to contribute to GNS3 Web UI feel free to reach us at `developers@gns3.net`.

14
e2e/app.e2e-spec.ts Normal file
View File

@ -0,0 +1,14 @@
import { Gns3WebUiPage } from './app.po';
describe('gns3-web-ui App', () => {
let page: Gns3WebUiPage;
beforeEach(() => {
page = new Gns3WebUiPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});

11
e2e/app.po.ts Normal file
View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class Gns3WebUiPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

14
e2e/tsconfig.e2e.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

33
karma.conf.js Normal file
View File

@ -0,0 +1,33 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

9192
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
package.json Normal file
View File

@ -0,0 +1,53 @@
{
"name": "gns3-web-ui",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^4.0.0",
"@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/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",
"core-js": "^2.4.1",
"d3-ng2-service": "^1.16.0",
"rxjs": "^5.4.1",
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "^1.2.6",
"@angular/compiler-cli": "^4.0.0",
"@angular/language-service": "^4.0.0",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "~3.0.1",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.0.4",
"tslint": "~5.3.2",
"typescript": "~2.3.3"
}
}

28
protractor.conf.js Normal file
View File

@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProjectMapComponent } from './project-map/project-map.component';
import { ServersComponent } from "./servers/servers.component";
import { ProjectsComponent } from "./projects/projects.component";
const routes: Routes = [
{ path: '', redirectTo: '/servers', pathMatch: 'full' },
{ path: 'servers', component: ServersComponent },
{ path: 'server/:server_id/projects', component: ProjectsComponent },
{ path: 'server/:server_id/project/:project_id', component: ProjectMapComponent },
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}

View File

View File

@ -0,0 +1,19 @@
<nav class="navbar navbar-expand-lg navbar-dark">
<a class="navbar-brand" href="/"><img src="/assets/logo-header.png"></a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" [routerLink]="['/servers']" >Servers <span class="sr-only">(current)</span></a>
</li>
</ul>
</div>
</nav>
<router-outlet></router-outlet>
<footer class="footer">
<span class="text-muted">GNS3 Web UI demo</span>
</footer>

View File

@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});

11
src/app/app.component.ts Normal file
View File

@ -0,0 +1,11 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
ngOnInit(): void {
}
}

55
src/app/app.module.ts Normal file
View File

@ -0,0 +1,55 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { D3Service } from 'd3-ng2-service';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MapComponent } from './map/map.component';
import { ProjectMapComponent } from './project-map/project-map.component';
import { ServerCreateModalComponent, ServersComponent } from './servers/servers.component';
import { ProjectsComponent } from './projects/projects.component';
import { VersionService } from './services/version.service';
import { ProjectService } from './services/project.service';
import { SymbolService } from "./services/symbol.service";
import { ServerService } from "./services/server.service";
import { IndexedDbService } from "./services/indexed-db.service";
import { HttpServer } from "./services/http-server.service";
@NgModule({
declarations: [
AppComponent,
MapComponent,
ProjectMapComponent,
ServersComponent,
ServerCreateModalComponent,
ProjectsComponent,
],
imports: [
NgbModule.forRoot(),
BrowserModule,
HttpModule,
AppRoutingModule,
FormsModule
],
providers: [
D3Service,
VersionService,
ProjectService,
SymbolService,
ServerService,
IndexedDbService,
HttpServer,
],
entryComponents: [
ServerCreateModalComponent,
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

View File

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

After

Width:  |  Height:  |  Size: 39 B

View File

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

View File

@ -0,0 +1,129 @@
import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChange } from '@angular/core';
import { D3, D3Service } from 'd3-ng2-service';
import { Selection } from 'd3-selection';
import { Node } from "../models/node";
import { Link } from "../models/link";
import { GraphLayout } from "./models/graph-layout";
import { Context } from "./models/context";
import { Size } from "./models/size";
@Component({
selector: 'app-map',
templateUrl: './map.component.html',
styleUrls: ['./map.component.css']
})
export class MapComponent implements OnInit, OnChanges, OnDestroy {
@Input() nodes: Node[] = [];
@Input() links: Link[] = [];
@Input() width = 1500;
@Input() height = 600;
@Input() phylloRadius = 7;
@Input() pointRadius= 2;
private d3: D3;
private parentNativeElement: any;
private svg: Selection<SVGSVGElement, any, null, undefined>;
private graphLayout: GraphLayout;
private graphContext: Context;
constructor(protected element: ElementRef,
protected d3Service: D3Service
) {
this.d3 = d3Service.getD3();
this.parentNativeElement = element.nativeElement;
}
ngOnChanges(changes: { [propKey: string]: SimpleChange }) {
if (
(changes['width'] && !changes['width'].isFirstChange()) ||
(changes['height'] && !changes['height'].isFirstChange()) ||
(changes['phylloRadius'] && !changes['phylloRadius'].isFirstChange()) ||
(changes['pointRadius'] && !changes['pointRadius'].isFirstChange()) ||
(changes['nodes'] && !changes['nodes'].isFirstChange()) ||
(changes['links'] && !changes['links'].isFirstChange())
) {
if (this.svg.empty && !this.svg.empty()) {
if (changes['nodes']) {
this.onNodesChange(changes['nodes']);
}
if (changes['links']) {
this.onLinksChange(changes['links']);
}
this.changeLayout();
}
}
}
ngOnDestroy() {
if (this.svg.empty && !this.svg.empty()) {
this.svg.selectAll('*').remove();
}
}
ngOnInit() {
const d3 = this.d3;
let rootElement: Selection<HTMLElement, any, null, undefined>;
const self = this;
if (this.parentNativeElement !== null) {
rootElement = d3.select(this.parentNativeElement);
this.svg = rootElement.select<SVGSVGElement>('svg');
this.graphContext = new Context(this.svg);
this.graphContext.setSize(new Size(this.width, this.height));
this.graphLayout = new GraphLayout();
this.graphLayout.draw(this.svg, this.graphContext);
}
}
private changeLayout() {
this.svg
.attr('width', this.width)
.attr('height', this.height);
this.graphLayout.setNodes(this.nodes);
this.graphLayout.setLinks(this.links);
this.redraw();
}
private onLinksChange(change: SimpleChange) {
const nodes_by_id = {};
this.nodes.forEach((n: Node) => {
nodes_by_id[n.node_id] = n;
});
this.links.forEach((link: Link) => {
const source_id = link.nodes[0].node_id;
const target_id = link.nodes[1].node_id;
if (source_id in nodes_by_id) {
link.source = nodes_by_id[source_id];
}
if (target_id in nodes_by_id) {
link.target = nodes_by_id[target_id];
}
});
}
private onNodesChange(change: SimpleChange) {
this.onLinksChange(null);
}
public redraw() {
this.graphLayout.draw(this.svg, this.graphContext);
}
public reload() {
this.onLinksChange(null);
this.redraw();
}
}

View File

@ -0,0 +1,23 @@
import {Size} from "./size";
import {Selection} from "d3-selection";
export class Context {
private size: Size;
private root: Selection<SVGSVGElement, any, null, undefined>;
constructor(root: Selection<SVGSVGElement, any, null, undefined>) {
this.root = root;
}
public getSize(): Size {
return this.size;
}
public setSize(size: Size): void {
this.size = size;
}
public getRoot() {
return this.root;
}
}

View File

@ -0,0 +1,73 @@
import { Context } from "./context";
import { Node } from "../../models/node";
import { Link } from "../../models/link";
import { NodesWidget } from "./nodes-widget";
import { Widget } from "./widget";
import { SVGSelection } from "./types";
import { LinksWidget } from "./links-widget";
import { D3ZoomEvent, zoom } from "d3-zoom";
import { event } from "d3-selection";
export class GraphLayout implements Widget {
private nodes: Node[] = [];
private links: Link[] = [];
private nodesWidget = new NodesWidget();
private linksWidget = new LinksWidget();
private centerZeroZeroPoint = true;
public setNodes(nodes: Node[]) {
this.nodes = nodes;
}
public setLinks(links: Link[]) {
this.links = links;
}
draw(view: SVGSelection, context: Context) {
const self = this;
const drawing = view
.selectAll<SVGGElement, Context>('g.drawing')
.data([context]);
const drawingEnter = drawing.enter()
.append<SVGGElement>('g')
.attr('class', 'drawing');
if (this.centerZeroZeroPoint) {
drawing.attr(
'transform',
(ctx: Context) => `translate(${ctx.getSize().width / 2}, ${ctx.getSize().height / 2})`);
}
const links = drawingEnter.append<SVGGElement>('g')
.attr('class', 'links');
const nodes = drawingEnter.append<SVGGElement>('g')
.attr('class', 'nodes');
this.linksWidget.draw(drawing, this.links);
this.nodesWidget.draw(drawing, this.nodes);
const onZoom = function(this: SVGSVGElement) {
const e: D3ZoomEvent<SVGSVGElement, any> = event;
if (self.centerZeroZeroPoint) {
drawing.attr(
'transform',
`translate(${context.getSize().width / 2 + e.transform.x}, ` +
`${context.getSize().height / 2 + e.transform.y}) scale(${e.transform.k})`);
} else {
drawing.attr('transform', e.transform.toString());
}
};
view.call(zoom<SVGSVGElement, any>()
.scaleExtent([1 / 2, 8])
.on('zoom', onZoom));
}
}

View File

@ -0,0 +1,45 @@
import {Link} from "../../../models/link";
export class MultiLinkCalculatorHelper {
LINK_WIDTH = 2;
public linkTranslation(
distance: number,
point0: {x: number, y: number},
point1: {x: number, y: number}): {dx: number, dy: number} {
const x1_x0 = point1.x - point0.x;
const y1_y0 = point1.y - point0.y;
let x2_x0;
let y2_y0;
if (y1_y0 === 0) {
x2_x0 = 0;
y2_y0 = distance;
} else {
const angle = Math.atan((x1_x0) / (y1_y0));
x2_x0 = -distance * Math.cos(angle);
y2_y0 = distance * Math.sin(angle);
}
return {
dx: x2_x0,
dy: y2_y0
};
}
public assignDataToLinks(links: Link[]) {
const links_from_nodes = {};
links.forEach((l: Link, i: number) => {
const sid = l.nodes[0].node_id;
const tid = l.nodes[1].node_id;
const key = (sid < tid ? sid + "," + tid : tid + "," + sid);
let idx = 1;
if (!(key in links_from_nodes)) {
links_from_nodes[key] = [i];
} else {
idx = links_from_nodes[key].push(i);
}
l.distance = (idx % 2 === 0 ? idx * this.LINK_WIDTH : (-idx + 1) * this.LINK_WIDTH);
});
}
}

View File

@ -0,0 +1,104 @@
import { select } from "d3-selection";
import { line } from "d3-shape";
import { Widget } from "./widget";
import { SVGSelection } from "./types";
import { Link } from "../../models/link";
import { LinkStatus } from "../../models/link-status";
import { MultiLinkCalculatorHelper } from "./helpers/multi-link-calculator-helper";
export class LinksWidget implements Widget {
private multiLinkCalculatorHelper = new MultiLinkCalculatorHelper();
public draw(view: SVGSelection, links: Link[]) {
const self = this;
this.multiLinkCalculatorHelper.assignDataToLinks(links);
const link = view
.selectAll("g.link")
.data(links.filter((l: Link) => {
return l.target && l.source;
}));
const link_enter = link.enter()
.append<SVGGElement>('g')
.attr('class', 'link')
.attr('link_id', (l: Link) => l.link_id)
.attr('map-source', (l: Link) => l.source.node_id)
.attr('map-target', (l: Link) => l.target.node_id);
link.merge(link_enter)
.each(function (this: SVGGElement, l: Link) {
const link_data = [[
[l.source.x, l.source.y],
[l.target.x, l.target.y]
]];
const link_group = select<SVGGElement, Link>(this);
const value_line = line();
let link_path = link_group.select<SVGPathElement>('path');
if (!link_path.node()) {
link_path = link_group.append<SVGPathElement>('path');
}
const link_path_data = link_path.data(link_data);
link_path_data
.attr('d', value_line)
.attr('stroke', '#000')
.attr('stroke-width', '1');
const start_point: SVGPoint = link_path.node().getPointAtLength(50);
const end_point: SVGPoint = link_path.node().getPointAtLength(link_path.node().getTotalLength() - 50);
const statuses = [
new LinkStatus(start_point.x, start_point.y, l.source.status),
new LinkStatus(end_point.x, end_point.y, l.target.status)
];
const status_started = link_group.selectAll<SVGCircleElement, LinkStatus>('circle.status_started')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'started'));
const status_started_enter = status_started.enter().append<SVGCircleElement>('circle');
status_started.merge(status_started_enter)
.attr('class', 'status_started')
.attr('cx', (ls: LinkStatus) => ls.x)
.attr('cy', (ls: LinkStatus) => ls.y)
.attr('r', 4)
.attr('fill', '#2ecc71');
status_started.exit().remove();
const status_stopped = link_group.selectAll<SVGRectElement, LinkStatus>('rect.status_stopped')
.data(statuses.filter((link_status: LinkStatus) => link_status.status === 'stopped'));
const status_stopped_enter = status_stopped.enter().append<SVGRectElement>('rect');
const STOPPED_STATUS_RECT_WIDTH = 6;
status_stopped.merge(status_stopped_enter)
.attr('class', 'status_stopped')
.attr('x', (ls: LinkStatus) => ls.x - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('y', (ls: LinkStatus) => ls.y - STOPPED_STATUS_RECT_WIDTH / 2.)
.attr('width', STOPPED_STATUS_RECT_WIDTH)
.attr('height', STOPPED_STATUS_RECT_WIDTH)
.attr('fill', 'red');
status_stopped.exit().remove();
})
.attr('transform', function(l) {
if (l.source && l.target) {
const translation = self.multiLinkCalculatorHelper.linkTranslation(l.distance, l.source, l.target);
return `translate (${translation.dx + l.source.width / 2.}, ${translation.dy + l.source.height / 2.})`;
}
return null;
});
link.exit().remove();
}
}

View File

@ -0,0 +1,66 @@
import {Widget} from "./widget";
import {Node} from "../../models/node";
import {SVGSelection} from "./types";
import {D3DragEvent} from "d3-drag";
import {select} from "d3-selection";
export class NodesWidget implements Widget {
constructor() {}
public draw(view: SVGSelection, nodes: Node[]) {
const self = this;
// function dragged(this: SVGElement, node: Node) {
// const element = this;
// const e: D3DragEvent<SVGGElement, Node, Node> = d3.event;
//
// d3.select(this)
// .attr('transform', `translate(${e.x},${e.y})`);
//
// node.x = e.x;
// node.y = e.y;
// }
const node = view.selectAll<SVGGElement, any>('g.node')
.data(nodes);
const node_enter = node.enter()
.append<SVGGElement>('g')
.attr('class', 'node');
// .call(d3.drag<SVGGElement, Node>().on('drag', dragged))
const node_image = node_enter.append<SVGImageElement>('image')
.attr('xlink:href', (n: Node) => 'data:image/svg+xml;base64,' + btoa(n.icon.raw))
.attr('width', (n: Node) => n.width)
.attr('height', (n: Node) => n.height);
node_enter.append<SVGCircleElement>('circle')
.attr('class', 'node_point')
.attr('r', 2);
node_enter.append<SVGTextElement>('text')
.attr('class', 'label');
node_enter.append<SVGTextElement>('text')
.attr('class', 'node_point_label')
.attr('x', '-100')
.attr('y', '0');
const node_merge = node.merge(node_enter)
.attr('transform', (n: Node) => {
return `translate(${n.x},${n.y})`;
});
node_merge.select<SVGTextElement>('text.label')
.attr('x', (n: Node) => n.label.x)
.attr('y', (n: Node) => n.label.y)
.attr('style', (n: Node) => n.label.style)
.text((n: Node) => n.label.text);
node_merge.select<SVGTextElement>('text.node_point_label')
.text((n: Node) => `(${n.x}, ${n.y})`);
node.exit().remove();
}
}

View File

@ -0,0 +1,8 @@
export class Size {
width: number;
height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
}

View File

@ -0,0 +1,3 @@
import {BaseType, Selection} from "d3-selection";
export type SVGSelection = Selection<SVGElement, any, BaseType, any>;

View File

@ -0,0 +1,5 @@
export interface Widget {
draw(view: any, datum: any): void;
}

7
src/app/models/label.ts Normal file
View File

@ -0,0 +1,7 @@
export class Label {
rotation: number;
style: string;
text: string;
x: number;
y: number;
}

View File

@ -0,0 +1,11 @@
export class LinkStatus {
x: number;
y: number;
status: string;
public constructor(x: number, y: number, status: string) {
this.x = x;
this.y = y;
this.status = status;
}
}

15
src/app/models/link.ts Normal file
View File

@ -0,0 +1,15 @@
import {Node} from "./node";
export class Link {
capture_file_name: string;
capture_file_path: string;
capturing: boolean;
link_id: string;
link_type: string;
nodes: Node[];
project_id: string;
distance: number; // this is not from server
length: number; // this is not from server
source: Node; // this is not from server
target: Node; // this is not from server
}

29
src/app/models/node.ts Normal file
View File

@ -0,0 +1,29 @@
import {Label} from "./label";
import {Symbol} from "./symbol";
export class Node {
command_line: string;
compute_id: string;
console: number;
console_host: string;
console_type: string;
first_port_name: string;
height: number;
label: Label;
name: string;
node_directory: string;
node_id: string;
node_type: string;
port_name_format: string;
port_segment_size: number;
ports: number[];
project_id: string;
status: string;
symbol: string;
width: number;
x: number;
y: number;
z: number;
icon: Symbol; // not from server
}

View File

@ -0,0 +1,6 @@
import {Node} from "./node";
export class NodeLink {
source: Node;
target: Node;
}

12
src/app/models/project.ts Normal file
View File

@ -0,0 +1,12 @@
export class Project {
auto_close: boolean;
auto_open: boolean;
auto_start: boolean;
filename: string;
name: string;
path: string;
project_id: string;
scene_height: number;
scene_width: number;
status: string;
}

11
src/app/models/server.ts Normal file
View File

@ -0,0 +1,11 @@
export type ServerAuthorization = "basic" | "none";
export class Server {
id: number;
name: string;
ip: string;
port: number;
authorization: ServerAuthorization;
login: string;
password: string;
}

6
src/app/models/symbol.ts Normal file
View File

@ -0,0 +1,6 @@
export class Symbol {
builtin: boolean;
filename: string;
symbol_id: string;
raw: string;
}

View File

@ -0,0 +1,3 @@
export class Version {
version: string;
}

View File

@ -0,0 +1,6 @@
<div *ngIf="project" class="project-map">
<app-map [nodes]="nodes" [links]="links"></app-map>
</div>

View File

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

View File

@ -0,0 +1,151 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from "rxjs/Subject";
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/observable/fromPromise';
import 'rxjs/add/observable/dom/webSocket';
import { Project } from '../models/project';
import { Node } from '../models/node';
import { SymbolService } from '../services/symbol.service';
import { Link } from "../models/link";
import { MapComponent } from "../map/map.component";
import { ServerService } from "../services/server.service";
import { ProjectService } from '../services/project.service';
import { Server } from "../models/server";
@Component({
selector: 'app-project-map',
templateUrl: './project-map.component.html',
styleUrls: ['./project-map.component.css'],
})
export class ProjectMapComponent implements OnInit {
public nodes: Node[] = [];
public links: Link[] = [];
project: Project;
server: Server;
private ws: Subject<any>;
@ViewChild(MapComponent) mapChild: MapComponent;
constructor(
private route: ActivatedRoute,
private serverService: ServerService,
private projectService: ProjectService,
private symbolService: SymbolService) {
}
ngOnInit() {
this.route.paramMap.subscribe((paramMap: ParamMap) => {
const server_id = parseInt(paramMap.get('server_id'), 10);
Observable
.fromPromise(this.serverService.get(server_id))
.flatMap((server: Server) => {
this.server = server;
return this.projectService.get(server, paramMap.get('project_id'));
})
.flatMap((project: Project) => {
this.project = project;
if (this.project.status === 'opened') {
return new Observable((observer) => {
observer.next(this.project);
});
} else {
return this.projectService.open(
this.server, this.project.project_id);
}
})
.subscribe((project: Project) => {
this.onProjectLoad(project);
});
});
}
onProjectLoad(project: Project) {
this.symbolService
.load(this.server)
.flatMap(() => {
return this.projectService.links(this.server, project.project_id);
})
.flatMap((links: Link[]) => {
this.links = links;
return this.projectService.nodes(this.server, project.project_id);
})
.subscribe((nodes: Node[]) => {
this.nodes = nodes;
nodes.forEach((n: Node) => {
n.icon = this.symbolService.get(n.symbol);
});
this.setUpWS(project);
});
}
setUpWS(project: Project) {
this.ws = Observable.webSocket(
this.projectService.notificationsPath(this.server, project.project_id));
this.ws.subscribe((o: any) => {
if (o.action === 'node.updated') {
const node: Node = o.event;
const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id);
if (index >= 0) {
this.nodes[index] = node;
this.mapChild.reload(); // temporary invocation
}
}
if (o.action === 'node.created') {
const node: Node = o.event;
const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id);
if (index === -1) {
this.nodes.push(node);
this.mapChild.reload(); // temporary invocation
}
}
if (o.action === 'node.deleted') {
const node: Node = o.event;
const index = this.nodes.findIndex((n: Node) => n.node_id === node.node_id);
if (index >= 0) {
this.nodes.splice(index, 1);
this.mapChild.reload(); // temporary invocation
}
}
if (o.action === 'link.created') {
const link: Link = o.event;
const index = this.links.findIndex((l: Link) => l.link_id === link.link_id);
if (index === -1) {
this.links.push(link);
this.mapChild.reload(); // temporary invocation
}
}
if (o.action === 'link.updated') {
const link: Link = o.event;
const index = this.links.findIndex((l: Link) => l.link_id === link.link_id);
if (index >= 0) {
this.links[index] = link;
this.mapChild.reload(); // temporary invocation
}
}
if (o.action === 'link.deleted') {
const link: Link = o.event;
const index = this.links.findIndex((l: Link) => l.link_id === link.link_id);
if (index >= 0) {
this.links.splice(index, 1);
this.mapChild.reload(); // temporary invocation
}
}
});
}
}

View File

View File

@ -0,0 +1,19 @@
<div class="container page">
<h1>Projects</h1>
<div class="row">
<table class="table table-inverse">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let project of projects">
<td><a [routerLink]="['/server', server.id, 'project', project.project_id]">{{ project.name }}</a></td>
</tr>
</tbody>
</table>
</div>
</div>

View File

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

View File

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Project } from "../models/project";
import { ProjectService } from "../services/project.service";
import { Server } from "../models/server";
import { ServerService } from "../services/server.service";
@Component({
selector: 'app-projects',
templateUrl: './projects.component.html',
styleUrls: ['./projects.component.css']
})
export class ProjectsComponent implements OnInit {
server: Server;
projects: Project[] = [];
constructor(private route: ActivatedRoute,
private serverService: ServerService,
private projectService: ProjectService) { }
ngOnInit() {
this.route.paramMap
.switchMap((params: ParamMap) => {
const server_id = parseInt(params.get('server_id'), 10);
return this.serverService.get(server_id);
})
.subscribe((server: Server) => {
this.server = server;
this.projectService
.list(server)
.subscribe((projects: Project[]) => {
this.projects = projects;
});
});
}
}

View File

@ -0,0 +1,27 @@
<div class="modal-header">
<h4 class="modal-title">Add server</h4>
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form #f="ngForm">
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" name="name" placeholder="Enter name" [(ngModel)]="server.name" required>
</div>
<div class="form-group">
<label for="ip">IP</label>
<input type="text" class="form-control" id="ip" name="ip" placeholder="Enter IP" [(ngModel)]="server.ip" required>
</div>
<div class="form-group">
<label for="port">Port</label>
<input type="number" class="form-control" id="port" name="port" placeholder="Enter Port" [(ngModel)]="server.port" required>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="activeModal.dismiss()">Close</button>
<button type="button" class="btn btn-success" (click)="add()" [disabled]="!f.valid">Add</button>
</div>

View File

View File

@ -0,0 +1,29 @@
<div class="container page">
<h1>Servers</h1>
<div class="row">
<table class="table table-inverse">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>IP:Port</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let server of servers">
<th scope="row">{{ server.id }}</th>
<td><a [routerLink]="['/server', server.id, 'projects']">{{ server.name }}</a></td>
<td>{{ server.ip }}:{{ server.port }}</td>
<td><button class="btn btn-outline-danger btn-sm " (click)="deleteServer(server)">Remove</button></td>
</tr>
</tbody>
</table>
</div>
<div class="row">
<button class="btn btn-primary btn-lg active" (click)="createModal()">Add server</button>
</div>
</div>

View File

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

View File

@ -0,0 +1,58 @@
import { Component, OnInit } from '@angular/core';
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Server } from "../models/server";
import { ServerService } from "../services/server.service";
@Component({
selector: 'app-server-create-modal',
templateUrl: './server-create-modal.component.html'
})
export class ServerCreateModalComponent {
public server = new Server();
constructor(public activeModal: NgbActiveModal) {}
add() {
this.activeModal.close(this.server);
}
}
@Component({
selector: 'app-server-list',
templateUrl: './servers.component.html',
styleUrls: ['./servers.component.css']
})
export class ServersComponent implements OnInit {
servers: Server[] = [];
constructor(private modalService: NgbModal, private serverService: ServerService) { }
ngOnInit() {
this.loadServers();
}
loadServers() {
this.serverService.findAll().then((servers: Server[]) => {
this.servers = servers;
});
}
createModal() {
this.modalService.open(ServerCreateModalComponent).result.then((server: Server) => {
this.serverService.create(server).then((created: Server) => {
this.loadServers();
});
}, (rejection) => {
});
}
deleteServer(server: Server) {
this.serverService.delete(server).then(() => {
this.loadServers();
});
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { HttpServer } from './http-server.service';
describe('HttpServer', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [HttpServer]
});
});
it('should be created', inject([HttpServer], (service: HttpServer) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,64 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import {Headers, Http, RequestOptions, RequestOptionsArgs, Response} from "@angular/http";
import {Server} from "../models/server";
@Injectable()
export class HttpServer {
constructor(private http: Http) { }
get(server: Server, url: string, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.get(url, options);
}
post(server: Server, url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.post(url, body, options);
}
put(server: Server, url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.put(url, body, options);
}
delete(server: Server, url: string, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.delete(url, options);
}
patch(server: Server, url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.patch(url, body, options);
}
head(server: Server, url: string, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.patch(url, options);
}
options(server: Server, url: string, options?: RequestOptionsArgs): Observable<Response> {
options = this.getOptionsForServer(server, url, options);
return this.http.options(url, options);
}
private getOptionsForServer(server: Server, url: string, options) {
if (options === undefined) {
options = new RequestOptions();
}
options.url = `http://${server.ip}:${server.port}/v2${url}`;
if (options.headers === null) {
options.headers = new Headers();
}
if (server.authorization === "basic") {
const credentials = btoa(`${server.login}:${server.password}`);
options.headers.append('Authorization', `Basic ${credentials}`);
}
return options;
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { IndexedDbService } from './indexed-db.service';
describe('IndexedDbService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [IndexedDbService]
});
});
it('should be created', inject([IndexedDbService], (service: IndexedDbService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { AngularIndexedDB } from 'angular2-indexeddb';
@Injectable()
export class IndexedDbService {
private db: AngularIndexedDB;
constructor() {
this.db = new AngularIndexedDB('gns3-web-ui', 1);
}
public get() {
return this.db;
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { ProjectService } from './project.service';
describe('ProjectService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ProjectService]
});
});
it('should be created', inject([ProjectService], (service: ProjectService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { Project } from '../models/project';
import { Node } from '../models/node';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Link } from "../models/link";
import { Server } from "../models/server";
import { ServerService } from "./server.service";
import { HttpServer } from "./http-server.service";
@Injectable()
export class ProjectService {
constructor(private httpServer: HttpServer) { }
get(server: Server, project_id: string): Observable<Project> {
return this.httpServer
.get(server, `/projects/${project_id}`)
.map(response => response.json() as Project);
}
open(server: Server, project_id: string): Observable<Project> {
return this.httpServer
.post(server, `/projects/${project_id}/open`, {})
.map(response => response.json() as Project);
}
list(server: Server): Observable<Project[]> {
return this.httpServer
.get(server, '/projects')
.map(response => response.json() as Project[]);
}
nodes(server: Server, project_id: string): Observable<Node[]> {
return this.httpServer
.get(server, `/projects/${project_id}/nodes`)
.map(response => response.json() as Node[]);
}
links(server: Server, project_id: string): Observable<Link[]> {
return this.httpServer
.get(server, `/projects/${project_id}/links`)
.map(response => response.json() as Link[]);
}
notificationsPath(server: Server, project_id: string): string {
return `ws://${server.ip}:${server.port}/v2/projects/${project_id}/notifications/ws`;
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { ServerService } from './server.service';
describe('ServerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ServerService]
});
});
it('should be created', inject([ServerService], (service: ServerService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import {IndexedDbService} from "./indexed-db.service";
import {Server} from "../models/server";
@Injectable()
export class ServerService {
private tablename = "servers";
private ready: Promise<any>;
constructor(private indexedDbService: IndexedDbService) {
this.ready = indexedDbService.get().createStore(1, (evt) => {
const store = evt.currentTarget.result.createObjectStore(
this.tablename, { keyPath: "id", autoIncrement: true });
});
}
public get(id: number) {
return this.onReady(() =>
this.indexedDbService.get().getByKey(this.tablename, id));
}
public create(server: Server) {
return this.onReady(() =>
this.indexedDbService.get().add(this.tablename, server));
}
public findAll() {
return this.onReady(() =>
this.indexedDbService.get().getAll(this.tablename));
}
public delete(server: Server) {
return this.onReady(() =>
this.indexedDbService.get().delete(this.tablename, server.id));
}
private onReady(query) {
const promise = new Promise((resolve, reject) => {
this.ready.then(() => {
query()
.then((result) => {
resolve(result);
}, (error) => {
reject(error);
});
}, (error) => {
reject(error);
});
});
return promise;
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { SymbolService } from './symbol.service';
describe('SymbolService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SymbolService]
});
});
it('should be created', inject([SymbolService], (service: SymbolService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,51 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from "rxjs/BehaviorSubject";
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/observable/of';
import { Symbol } from '../models/symbol';
import { Server } from "../models/server";
import { HttpServer } from "./http-server.service";
@Injectable()
export class SymbolService {
private symbols: BehaviorSubject<Symbol[]> = new BehaviorSubject<Symbol[]>([]);
constructor(private httpServer: HttpServer) { }
get(symbol_id: string): Symbol {
return this.symbols
.getValue()
.find((symbol: Symbol) => symbol.symbol_id === symbol_id);
}
load(server: Server): Observable<Symbol[]> {
this.list(server).subscribe((symbols: Symbol[]) => {
const streams = symbols.map(symbol => this.raw(server, symbol.symbol_id));
Observable.forkJoin(streams).subscribe((results: string[]) => {
symbols.forEach((symbol: Symbol, i: number) => {
symbol.raw = results[i];
});
this.symbols.next(symbols);
});
});
return this.symbols.asObservable();
}
list(server: Server): Observable<Symbol[]> {
return this.httpServer
.get(server, '/symbols')
.map(response => response.json() as Symbol[]);
}
raw(server: Server, symbol_id: string): Observable<string> {
const encoded_uri = encodeURI(symbol_id);
return this.httpServer
.get(server, `/symbols/${encoded_uri}/raw`)
.map(response => response.text() as string);
}
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { VersionService } from './version.service';
describe('VersionService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [VersionService]
});
});
it('should be created', inject([VersionService], (service: VersionService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Version} from '../models/version';
import { HttpServer } from "./http-server.service";
import { Server } from "../models/server";
@Injectable()
export class VersionService {
constructor(private httpServer: HttpServer) { }
get(server: Server): Observable<Version> {
return this.httpServer
.get(server, '/version')
.map(response => response.json() as Version);
}
}

0
src/assets/.gitkeep Normal file
View File

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/logo-header.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

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

View File

@ -0,0 +1,8 @@
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
production: false
};

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

17
src/index.html Normal file
View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GNS3 Web UI Demo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
<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">
</head>
<body>
<app-root></app-root>
</body>
</html>

11
src/main.ts Normal file
View File

@ -0,0 +1,11 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

72
src/polyfills.ts Normal file
View File

@ -0,0 +1,72 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
/**
* Required to support Web Animations `@angular/animation`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/**
* Need to import at least one locale-data with intl.
*/
// import 'intl/locale-data/jsonp/en';

64
src/styles.css Normal file
View File

@ -0,0 +1,64 @@
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
background-color: #26353f;
font-family: Roboto;
height: 100%;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
height: 60px;
line-height: 60px;
}
.navbar-brand > img {
width: 50px;
}
.page h1 {
margin-top: 20px;
margin-bottom: 20px;
color: white;
}
.footer {
padding-right: 15px;
padding-left: 15px;
}
.table-inverse {
background-color: #26353f;
}
.table-inverse {
background-color: #26353f;
}
.table-inverse thead th {
border: none;
}
.table thead th {
border-bottom: 2px solid white;
}
.table td, .table th {
border-top: 1px solid white;
}
.project-map {
text-align: center;
}
.project-map svg {
background-color: lightgray;
-moz-box-shadow: inset 0 0 10px #808080;
-webkit-box-shadow: inset 0 0 10px #808080;
box-shadow: inset 0 0 10px #808080;
}

32
src/test.ts Normal file
View File

@ -0,0 +1,32 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy.js';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
declare const __karma__: any;
declare const require: any;
// Prevent Karma from running prematurely.
__karma__.loaded = function () {};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
// Finally, start Karma to run the tests.
__karma__.start();

13
src/tsconfig.app.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "es2015",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

20
src/tsconfig.spec.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

5
src/typings.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/* SystemJS module definition */
declare var module: NodeModule;
interface NodeModule {
id: string;
}

19
tsconfig.json Normal file
View File

@ -0,0 +1,19 @@
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2016",
"dom"
]
}
}

142
tslint.json Normal file
View File

@ -0,0 +1,142 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": false,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
false,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"typeof-compare": true,
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true,
"no-access-missing-member": true,
"templates-use-public": true,
"invoke-injectable": true
}
}