mirror of
https://github.com/GNS3/gns3-web-ui.git
synced 2024-12-18 20:47:51 +00:00
Draft of WebUI
This commit is contained in:
parent
eca48dd435
commit
7968368b81
61
.angular-cli.json
Normal file
61
.angular-cli.json
Normal 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
13
.editorconfig
Normal 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
42
.gitignore
vendored
Normal 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
|
56
README.md
56
README.md
@ -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
14
e2e/app.e2e-spec.ts
Normal 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
11
e2e/app.po.ts
Normal 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
14
e2e/tsconfig.e2e.json
Normal 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
33
karma.conf.js
Normal 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
9192
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
package.json
Normal file
53
package.json
Normal 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
28
protractor.conf.js
Normal 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 } }));
|
||||
}
|
||||
};
|
19
src/app/app-routing.module.ts
Normal file
19
src/app/app-routing.module.ts
Normal 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 {}
|
0
src/app/app.component.css
Normal file
0
src/app/app.component.css
Normal file
19
src/app/app.component.html
Normal file
19
src/app/app.component.html
Normal 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>
|
32
src/app/app.component.spec.ts
Normal file
32
src/app/app.component.spec.ts
Normal 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
11
src/app/app.component.ts
Normal 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
55
src/app/app.module.ts
Normal 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 { }
|
0
src/app/map/map.component.css
Normal file
0
src/app/map/map.component.css
Normal file
1
src/app/map/map.component.html
Normal file
1
src/app/map/map.component.html
Normal file
@ -0,0 +1 @@
|
||||
<svg preserveAspectRatio="none"></svg>
|
After Width: | Height: | Size: 39 B |
25
src/app/map/map.component.spec.ts
Normal file
25
src/app/map/map.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
129
src/app/map/map.component.ts
Normal file
129
src/app/map/map.component.ts
Normal 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();
|
||||
}
|
||||
}
|
23
src/app/map/models/context.ts
Normal file
23
src/app/map/models/context.ts
Normal 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;
|
||||
}
|
||||
}
|
73
src/app/map/models/graph-layout.ts
Normal file
73
src/app/map/models/graph-layout.ts
Normal 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));
|
||||
}
|
||||
|
||||
}
|
45
src/app/map/models/helpers/multi-link-calculator-helper.ts
Normal file
45
src/app/map/models/helpers/multi-link-calculator-helper.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
104
src/app/map/models/links-widget.ts
Normal file
104
src/app/map/models/links-widget.ts
Normal 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();
|
||||
}
|
||||
}
|
66
src/app/map/models/nodes-widget.ts
Normal file
66
src/app/map/models/nodes-widget.ts
Normal 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();
|
||||
}
|
||||
}
|
8
src/app/map/models/size.ts
Normal file
8
src/app/map/models/size.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export class Size {
|
||||
width: number;
|
||||
height: number;
|
||||
constructor(width: number, height: number) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
3
src/app/map/models/types.ts
Normal file
3
src/app/map/models/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import {BaseType, Selection} from "d3-selection";
|
||||
|
||||
export type SVGSelection = Selection<SVGElement, any, BaseType, any>;
|
5
src/app/map/models/widget.ts
Normal file
5
src/app/map/models/widget.ts
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
export interface Widget {
|
||||
draw(view: any, datum: any): void;
|
||||
}
|
||||
|
7
src/app/models/label.ts
Normal file
7
src/app/models/label.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class Label {
|
||||
rotation: number;
|
||||
style: string;
|
||||
text: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
11
src/app/models/link-status.ts
Normal file
11
src/app/models/link-status.ts
Normal 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
15
src/app/models/link.ts
Normal 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
29
src/app/models/node.ts
Normal 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
|
||||
}
|
6
src/app/models/node_link.ts
Normal file
6
src/app/models/node_link.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {Node} from "./node";
|
||||
|
||||
export class NodeLink {
|
||||
source: Node;
|
||||
target: Node;
|
||||
}
|
12
src/app/models/project.ts
Normal file
12
src/app/models/project.ts
Normal 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
11
src/app/models/server.ts
Normal 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
6
src/app/models/symbol.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export class Symbol {
|
||||
builtin: boolean;
|
||||
filename: string;
|
||||
symbol_id: string;
|
||||
raw: string;
|
||||
}
|
3
src/app/models/version.ts
Normal file
3
src/app/models/version.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export class Version {
|
||||
version: string;
|
||||
}
|
0
src/app/project-map/project-map.component.css
Normal file
0
src/app/project-map/project-map.component.css
Normal file
6
src/app/project-map/project-map.component.html
Normal file
6
src/app/project-map/project-map.component.html
Normal file
@ -0,0 +1,6 @@
|
||||
<div *ngIf="project" class="project-map">
|
||||
<app-map [nodes]="nodes" [links]="links"></app-map>
|
||||
</div>
|
||||
|
||||
|
||||
|
25
src/app/project-map/project-map.component.spec.ts
Normal file
25
src/app/project-map/project-map.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
151
src/app/project-map/project-map.component.ts
Normal file
151
src/app/project-map/project-map.component.ts
Normal 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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
0
src/app/projects/projects.component.css
Normal file
0
src/app/projects/projects.component.css
Normal file
19
src/app/projects/projects.component.html
Normal file
19
src/app/projects/projects.component.html
Normal 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>
|
25
src/app/projects/projects.component.spec.ts
Normal file
25
src/app/projects/projects.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
39
src/app/projects/projects.component.ts
Normal file
39
src/app/projects/projects.component.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
27
src/app/servers/server-create-modal.component.html
Normal file
27
src/app/servers/server-create-modal.component.html
Normal 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">×</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>
|
0
src/app/servers/servers.component.css
Normal file
0
src/app/servers/servers.component.css
Normal file
29
src/app/servers/servers.component.html
Normal file
29
src/app/servers/servers.component.html
Normal 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>
|
25
src/app/servers/servers.component.spec.ts
Normal file
25
src/app/servers/servers.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
58
src/app/servers/servers.component.ts
Normal file
58
src/app/servers/servers.component.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
15
src/app/services/http-server.service.spec.ts
Normal file
15
src/app/services/http-server.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
64
src/app/services/http-server.service.ts
Normal file
64
src/app/services/http-server.service.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
15
src/app/services/indexed-db.service.spec.ts
Normal file
15
src/app/services/indexed-db.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
15
src/app/services/indexed-db.service.ts
Normal file
15
src/app/services/indexed-db.service.ts
Normal 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;
|
||||
}
|
||||
}
|
15
src/app/services/project.service.spec.ts
Normal file
15
src/app/services/project.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
51
src/app/services/project.service.ts
Normal file
51
src/app/services/project.service.ts
Normal 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`;
|
||||
}
|
||||
}
|
15
src/app/services/server.service.spec.ts
Normal file
15
src/app/services/server.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
55
src/app/services/server.service.ts
Normal file
55
src/app/services/server.service.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
15
src/app/services/symbol.service.spec.ts
Normal file
15
src/app/services/symbol.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
51
src/app/services/symbol.service.ts
Normal file
51
src/app/services/symbol.service.ts
Normal 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);
|
||||
}
|
||||
}
|
15
src/app/services/version.service.spec.ts
Normal file
15
src/app/services/version.service.spec.ts
Normal 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();
|
||||
}));
|
||||
});
|
21
src/app/services/version.service.ts
Normal file
21
src/app/services/version.service.ts
Normal 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
0
src/assets/.gitkeep
Normal file
BIN
src/assets/favicon.ico
Normal 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
BIN
src/assets/logo-header.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
3
src/environments/environment.prod.ts
Normal file
3
src/environments/environment.prod.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
8
src/environments/environment.ts
Normal file
8
src/environments/environment.ts
Normal 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
BIN
src/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
17
src/index.html
Normal file
17
src/index.html
Normal 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
11
src/main.ts
Normal 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
72
src/polyfills.ts
Normal 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
64
src/styles.css
Normal 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
32
src/test.ts
Normal 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
13
src/tsconfig.app.json
Normal 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
20
src/tsconfig.spec.json
Normal 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
5
src/typings.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/* SystemJS module definition */
|
||||
declare var module: NodeModule;
|
||||
interface NodeModule {
|
||||
id: string;
|
||||
}
|
19
tsconfig.json
Normal file
19
tsconfig.json
Normal 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
142
tslint.json
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user