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
|
# 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