├── .angular-cli.json ├── .editorconfig ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── app.routing.module.ts │ ├── bootstrap.ts │ ├── component │ │ ├── button │ │ │ ├── _button.danger.scss │ │ │ ├── _button.disabled.scss │ │ │ ├── _button.icon.scss │ │ │ ├── _button.neutral.scss │ │ │ ├── _button.primary.scss │ │ │ ├── _button.secondary.scss │ │ │ ├── button.component.html │ │ │ ├── button.component.scss │ │ │ └── button.component.ts │ │ ├── graph │ │ │ ├── graph-utils │ │ │ │ ├── distance.ts │ │ │ │ ├── finder.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mouse.ts │ │ │ │ ├── shape.ts │ │ │ │ └── state.ts │ │ │ ├── graph.component.scss │ │ │ └── graph.component.ts │ │ ├── header │ │ │ ├── header.component.html │ │ │ ├── header.component.scss │ │ │ └── header.component.ts │ │ ├── index.ts │ │ ├── label │ │ │ ├── label.component.scss │ │ │ └── label.component.ts │ │ ├── link-edit │ │ │ ├── link-edit.component.html │ │ │ ├── link-edit.component.scss │ │ │ └── link-edit.component.ts │ │ ├── multi-select │ │ │ ├── multi-select-option.component.scss │ │ │ ├── multi-select-option.component.ts │ │ │ ├── multi-select.component.scss │ │ │ └── multi-select.component.ts │ │ ├── node-edit │ │ │ ├── node-edit.component.html │ │ │ ├── node-edit.component.scss │ │ │ └── node-edit.component.ts │ │ ├── search │ │ │ ├── search.component.scss │ │ │ └── search.component.ts │ │ ├── switch │ │ │ ├── switch.component.scss │ │ │ └── switch.component.ts │ │ └── tutorial │ │ │ ├── tutorial.component.html │ │ │ ├── tutorial.component.scss │ │ │ └── tutorial.component.ts │ ├── core │ │ ├── array │ │ │ ├── crosscut.ts │ │ │ ├── diff.ts │ │ │ ├── distinct.ts │ │ │ ├── group.ts │ │ │ ├── index.ts │ │ │ ├── orderby.ts │ │ │ └── unique.ts │ │ ├── index.ts │ │ ├── pipe │ │ │ ├── entries.pipe.ts │ │ │ ├── index.ts │ │ │ ├── keys.pipe.ts │ │ │ └── nicedate.pipe.ts │ │ ├── property.access.ts │ │ ├── string │ │ │ └── index.ts │ │ └── uuid.ts │ ├── neo4j │ │ ├── index.ts │ │ ├── model │ │ │ ├── index.ts │ │ │ ├── label.interface.ts │ │ │ ├── link.interface.ts │ │ │ ├── link.ts │ │ │ ├── node.interface.ts │ │ │ └── node.ts │ │ ├── neo4j.repository.ts │ │ ├── neo4j.service.ts │ │ ├── orm │ │ │ ├── cypher-query.ts │ │ │ ├── index.ts │ │ │ ├── result-set.ts │ │ │ ├── simple-query.ts │ │ │ └── transaction.ts │ │ └── utils │ │ │ ├── color.ts │ │ │ ├── escape.ts │ │ │ ├── index.ts │ │ │ ├── name.ts │ │ │ ├── quote.ts │ │ │ └── truncate.ts │ ├── page │ │ ├── debug │ │ │ ├── debug.page.html │ │ │ ├── debug.page.scss │ │ │ └── debug.page.ts │ │ ├── home │ │ │ ├── home.page.html │ │ │ ├── home.page.scss │ │ │ └── home.page.ts │ │ ├── index.ts │ │ └── settings │ │ │ ├── settings.page.html │ │ │ ├── settings.page.scss │ │ │ └── settings.page.ts │ └── service │ │ ├── broadcast.service.ts │ │ ├── debug.ts │ │ ├── index.ts │ │ ├── local.storage.ts │ │ └── settings.service.ts ├── assets │ ├── .gitkeep │ ├── flaticon │ │ ├── backup.txt │ │ ├── font │ │ │ ├── Flaticon.eot │ │ │ ├── Flaticon.svg │ │ │ ├── Flaticon.ttf │ │ │ ├── Flaticon.woff │ │ │ ├── _flaticon.scss │ │ │ ├── flaticon.css │ │ │ └── flaticon.html │ │ └── license │ │ │ └── license.pdf │ ├── icomoon │ │ ├── Read Me.txt │ │ ├── demo-files │ │ │ ├── demo.css │ │ │ └── demo.js │ │ ├── demo.html │ │ ├── fonts │ │ │ ├── icomoon.eot │ │ │ ├── icomoon.svg │ │ │ ├── icomoon.ttf │ │ │ └── icomoon.woff │ │ ├── selection.json │ │ └── style.css │ ├── neo4j.settings.json.dist │ ├── plus.png │ ├── svg │ │ ├── icon8-plus.svg │ │ └── three-dots.svg │ └── tutos │ │ ├── inline-create-mode-switch.png │ │ ├── neo4j-js-tuto-01-low.gif │ │ ├── neo4j-js-tuto-02-low.gif │ │ └── neo4j-js-tuto-03-low.gif ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── theme │ ├── common.scss │ ├── flex.scss │ ├── form.scss │ ├── layout.scss │ ├── normalize.css │ ├── shared │ │ ├── _mixins.scss │ │ ├── _scrollbars.scss │ │ └── _variables.scss │ ├── styles.scss │ └── svg │ │ ├── cursors.scss │ │ ├── links.scss │ │ ├── node.scss │ │ └── svg.scss ├── tsconfig.app.json └── typings.d.ts ├── support └── neo4j-js.conf.dist ├── tsconfig.json └── tslint.json /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "neo4j-js-ng2" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "tsconfig": "tsconfig.app.json", 18 | "prefix": "app", 19 | "styles": [ 20 | "theme/styles.scss" 21 | ], 22 | "scripts": [], 23 | "environmentSource": "environments/environment.ts", 24 | "environments": { 25 | "dev": "environments/environment.ts", 26 | "prod": "environments/environment.prod.ts" 27 | } 28 | } 29 | ], 30 | "lint": [ 31 | { 32 | "project": "src/tsconfig.app.json", 33 | "exclude": "**/node_modules/**" 34 | } 35 | ], 36 | "defaults": { 37 | "styleExt": "scss", 38 | "class": { 39 | "spec": false 40 | }, 41 | "component": {} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | neo4j.settings.json 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neo4jJs (v2) 2 | 3 | A Neo4j graph database editor. Explore your **neo4j** graph, create and edit nodes and relationships 4 | 5 | [Features and bugs roadmap](https://trello.com/b/NLtaurIH/neo4j-js-https-githubcom-adadgio-neo4j-js-ng2) 6 | 7 | Table of Contents 8 | ================= 9 | 10 | * [Improvements over v1](#improvements-over-v1) 11 | * [Getting started](#getting-started) 12 | * [Pre-requisites](#pre-requisites) 13 | * [Quick configuration](#quick-configuration) 14 | * [Simple queries](#simple-queries) 15 | * [Running in production](#running-in-production) 16 | * [Running in development](#running-in-development) 17 | * [Known issues](#known-issues) 18 | * [License](#license) 19 | 20 | ## Improvements over v1 21 | 22 | **Bug improvements** 23 | 24 | - Settings can now be updated on the fly via the UI. 25 | - Better separated components thanks to Angular2. 26 | - **Much much cleaner code** for developers to build upon. 27 | - Better events handling in graph and database interaction. 28 | - Annoying bugs and annoying features fixed from v1. 29 | 30 | **New features** 31 | 32 | - Editable relationships types and properties. 33 | - Links/relationships can be created in the create mode. 34 | - Added a plain *cypher query* mode in the main search bar (*@todo will be deprecated*) 35 | - Settings are served from `neo4j.settings.json` and can be changed on the fly (stored in local storage). 36 | 37 | ![Demo gif 01](https://github.com/adadgio/neo4j-js-ng2/blob/develop/src/assets/tutos/neo4j-js-tuto-01-low.gif) 38 | 39 | ![Demo gif 02](https://github.com/adadgio/neo4j-js-ng2/blob/develop/src/assets/tutos/neo4j-js-tuto-02-low.gif) 40 | 41 | ![Demo gif 03](https://github.com/adadgio/neo4j-js-ng2/blob/develop/src/assets/tutos/neo4j-js-tuto-03-low.gif) 42 | 43 | ## Getting started 44 | 45 | - Clone or download the project 46 | - Copy `src/assets/neo4j.settings.json.dist` to `src/assets/neo4j.settings.json`. 47 | - Run `ng serve` or `npm start`. 48 | 49 | ### Pre-requisites 50 | 51 | - Neo4j must be installed [Neo4j quick install instructions here](https://www.digitalocean.com/community/tutorials/how-to-install-neo4j-on-an-ubuntu-vps) 52 | - Neo4j Basic Authentication must have been configured (by default) 53 | - Angular2 CLI is required for running with `ng serve` or building into the `dist` folder. 54 | 55 | ### Quick configuration 56 | 57 | - With Angular2: serve project with `ng serve`and navigate to `http://localhost:4200/` 58 | - Without Angular2: create a virtual host on your machine and point it to the `dist` folder 59 | - Copy `src/assets/neo4j.settings.json.dist` to `src/assets/neo4j.settings.json` and change with your settings 60 | - Change client `authBasic` value to `Basic: `. Auth string is a **base64 encode** `username:password` 61 | 62 | *Note:* various settings like node colors and default labels are customizable in the JSON or on the fly! 63 | 64 | ## Simple queries 65 | 66 | Simple queries let you pop nodes on the graph very quickly without writing cypher queries. Simple queries are types in the main exploration search bar. 67 | 68 | *Why use this instead of cypher queries?* Because it's a little bit more complicated to allow any alias such as `MATCH (myAlias) RETURN myAlias`, but that's coming in the future. Besides, for exploration, simple queries are faster user-end wise. 69 | 70 | **Examples** 71 | 72 | ``` 73 | // simple query pseudo code format 74 | :Label1:Label2 property="Value" limit,skip 75 | 76 | // numbers, limit and multiple properties (AND...) 77 | :Person name="Ben" age=12 10 78 | 79 | // limit and skip 80 | :Person name="Ben" age=34 50,0 81 | 82 | // queery and show 1st level relationships (+1 flag) 83 | :Company name="Gougle" +1 84 | ``` 85 | 86 | ## Running in production 87 | 88 | Clone the repository and point an Apache2 or Nginx virtual host to the `./dist` folder (see `./support` files for examples). 89 | 90 | ## Development server 91 | 92 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 93 | 94 | ## Build 95 | 96 | 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. 97 | 98 | ## Known issues 99 | 100 | - Chrome: **Compatibility OK** (no known issues) 101 | - In Firefox local storage is not shared between tabs so you might experience settings or debug logs inconsistent views. 102 | 103 | ## Licence 104 | 105 | You do absolutely what you want with that project (MIT Licence). 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neo4j-js-ng2", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "lint": "ng lint" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^4.2.4", 14 | "@angular/common": "^4.2.4", 15 | "@angular/compiler": "^4.2.4", 16 | "@angular/core": "^4.2.4", 17 | "@angular/forms": "^4.2.4", 18 | "@angular/http": "^4.2.4", 19 | "@angular/platform-browser": "^4.2.4", 20 | "@angular/platform-browser-dynamic": "^4.2.4", 21 | "@angular/router": "^4.2.4", 22 | "core-js": "^2.4.1", 23 | "d3": "^3.5.17", 24 | "moment": "^2.19.2", 25 | "rxjs": "^5.4.2", 26 | "tinycolor2": "^1.4.1", 27 | "zone.js": "^0.8.14" 28 | }, 29 | "devDependencies": { 30 | "@angular/cli": "1.4.9", 31 | "@angular/compiler-cli": "^4.2.4", 32 | "@angular/language-service": "^4.2.4", 33 | "@types/node": "~6.0.60", 34 | "codelyzer": "~3.2.0", 35 | "ts-node": "~3.2.0", 36 | "tslint": "~5.7.0", 37 | "typescript": "~2.3.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/app/app.component.css -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent 9 | { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { APP_INITIALIZER } from '@angular/core'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { Http, HttpModule } from '@angular/http'; 6 | 7 | import { bootstrap } from './bootstrap'; 8 | import { AppComponent } from './app.component'; 9 | import { AppRoutingModule } from './app.routing.module'; 10 | 11 | import { KeysPipe } from './core/pipe'; 12 | import { EntriesPipe } from './core/pipe'; 13 | import { NiceDate } from './core/pipe'; 14 | import { SettingsService } from './service'; 15 | 16 | import { Neo4jService } from './neo4j'; 17 | import { Neo4jRepository } from './neo4j'; 18 | 19 | import { HomePageComponent } from './page'; 20 | import { DebugPageComponent } from './page'; 21 | import { SettingsPageComponent } from './page'; 22 | 23 | import { HeaderComponent } from './component'; 24 | import { SearchComponent } from './component'; 25 | import { TutorialComponent } from './component'; 26 | import { SwitchComponent } from './component'; 27 | import { ButtonComponent } from './component'; 28 | import { LabelComponent } from './component'; 29 | 30 | import { GraphComponent } from './component'; 31 | import { NodeEditComponent } from './component'; 32 | import { LinkEditComponent } from './component'; 33 | import { MultiSelectComponent } from './component'; 34 | import { MultiSelectOptionComponent } from './component'; 35 | 36 | 37 | @NgModule({ 38 | declarations: [ 39 | AppComponent, 40 | 41 | KeysPipe, 42 | EntriesPipe, 43 | NiceDate, 44 | 45 | HeaderComponent, 46 | SearchComponent, 47 | TutorialComponent, 48 | SwitchComponent, 49 | ButtonComponent, 50 | LabelComponent, 51 | 52 | GraphComponent, 53 | NodeEditComponent, 54 | LinkEditComponent, 55 | 56 | HomePageComponent, 57 | DebugPageComponent, 58 | SettingsPageComponent, 59 | 60 | MultiSelectComponent, 61 | MultiSelectOptionComponent, 62 | ], 63 | imports: [ 64 | BrowserModule, 65 | BrowserModule, 66 | HttpModule, 67 | FormsModule, 68 | AppRoutingModule 69 | ], 70 | providers: [ 71 | { 72 | provide: APP_INITIALIZER, 73 | useFactory: bootstrap, 74 | deps: [ Http, SettingsService ], 75 | multi: true 76 | }, 77 | SettingsService, 78 | Neo4jService, 79 | Neo4jRepository, 80 | ], 81 | bootstrap: [ 82 | AppComponent 83 | ] 84 | }) 85 | export class AppModule { } 86 | -------------------------------------------------------------------------------- /src/app/app.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { PreloadAllModules } from '@angular/router'; 4 | 5 | import { HomePageComponent } from './page'; 6 | import { DebugPageComponent } from './page'; 7 | import { SettingsPageComponent } from './page'; 8 | 9 | const APP_ROUTES: Routes = [ 10 | { 11 | path: '', 12 | component: HomePageComponent, 13 | canActivate: [], 14 | }, 15 | { 16 | path: 'debug', 17 | component: DebugPageComponent, 18 | canActivate: [], 19 | }, 20 | { 21 | path: 'settings', 22 | component: SettingsPageComponent, 23 | canActivate: [], 24 | }, 25 | { path: '', redirectTo: '/', pathMatch: 'full' }, 26 | ]; 27 | 28 | @NgModule({ 29 | imports: [ 30 | RouterModule.forRoot( 31 | APP_ROUTES, 32 | { 33 | // enableTracing: true, // debugging purposes only 34 | preloadingStrategy: PreloadAllModules, 35 | } 36 | ) 37 | ], 38 | exports: [ 39 | RouterModule 40 | ] 41 | }) 42 | export class AppRoutingModule { } 43 | -------------------------------------------------------------------------------- /src/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import 'rxjs/add/operator/map'; 2 | import 'rxjs/add/operator/toPromise'; 3 | import { Headers, Http } from '@angular/http'; 4 | import { SettingsService } from './service'; 5 | 6 | /** 7 | * @return Promise 8 | */ 9 | export function bootstrap(http: Http, settings: SettingsService) 10 | { 11 | let promises: Array> = []; 12 | const headers = new Headers({ 'Content-Type': 'application/json' }) 13 | 14 | if (true === settings.areSet()) { 15 | 16 | promises[0] = Promise.resolve() 17 | 18 | } else { 19 | 20 | promises[0] = new Promise((resolve, reject) => { 21 | http.get('assets/neo4j.settings.json', { headers: headers }) 22 | .map(res => res.json()) 23 | .toPromise() 24 | .then(data => { 25 | settings.set(data) 26 | resolve(data) 27 | }).catch(err => { 28 | throw new Error(err) 29 | }) 30 | }) 31 | 32 | } 33 | 34 | return () => { return Promise.all(promises) }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/component/button/_button.danger.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host[danger] { 5 | button { 6 | 7 | color: colors(white); 8 | background-color: colors(danger); 9 | 10 | &:hover { 11 | background-color: colors(danger-hover); 12 | } 13 | &:active { 14 | background-color:colors(red); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/component/button/_button.disabled.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | button[disabled] { 6 | 7 | cursor: not-allowed; 8 | color: colors(faded); 9 | background-color: colors(disabled); 10 | 11 | &:hover { 12 | background-color: colors(disabled-hover); 13 | } 14 | 15 | &:active { 16 | background-color: #424952; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/component/button/_button.icon.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host[icon-only] { 5 | 6 | button { 7 | padding: 0px; 8 | margin: 0px; 9 | min-width: 0px; 10 | 11 | padding-left: 14px; 12 | padding-right: 14px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/component/button/_button.neutral.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host[neutral] { 5 | 6 | button { 7 | padding: 0px; 8 | margin: 0px; 9 | min-width: 0px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/component/button/_button.primary.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host[primary] { 5 | 6 | button { 7 | 8 | color: colors(white); 9 | background-color: colors(primary); 10 | 11 | &:hover { 12 | background-color: colors(primary-hover); 13 | } 14 | 15 | &:active { 16 | background-color: #315d8c; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/component/button/_button.secondary.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host[secondary] { 5 | button { 6 | 7 | color: colors(white); 8 | background-color: colors(secondary); 9 | 10 | &:hover { 11 | background-color: colors(secondary-hover); 12 | } 13 | &:active { 14 | background-color: #434d56; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/component/button/button.component.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/app/component/button/button.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | @import "./_button.neutral"; 4 | @import "./_button.primary"; 5 | @import "./_button.secondary"; 6 | @import "./_button.danger"; 7 | @import "./_button.disabled"; 8 | @import "./_button.icon"; 9 | 10 | :host { 11 | display: flex; 12 | align-items: stretch; 13 | flex-direction: row; 14 | } 15 | 16 | .visible { visibility: visible } 17 | .hidden { visibility: hidden } 18 | 19 | 20 | button { 21 | position: relative; 22 | cursor: pointer; 23 | min-width: 75px; 24 | 25 | display: flex; 26 | flex: 1; 27 | flex-direction: row; 28 | align-items: center; 29 | justify-content: center; 30 | 31 | padding: 8px 12px; 32 | line-height: 16px; 33 | border: none; 34 | @include border-radius(2px); 35 | @include transition(background-color ease 0.3s, color ease 0.3s); 36 | 37 | cursor: pointer; 38 | font-family: inherit; 39 | font-size: $fontSize; 40 | 41 | span { 42 | display: flex; 43 | flex-direction: row; 44 | align-items: center; 45 | justify-content: center; 46 | text-align: center; 47 | } 48 | 49 | span.loader { 50 | position: absolute; 51 | left: 0; 52 | right: 0; 53 | top: 0; 54 | bottom: 0; 55 | width: 100%; 56 | height: 100%; 57 | text-align: center; 58 | 59 | img.svg-loader { 60 | width: 20px; 61 | } 62 | } 63 | 64 | /deep/ .icon { 65 | margin-right: 5px; 66 | line-height: 16px; 67 | font-size: $fontSize + 3px; 68 | } 69 | } 70 | 71 | :host[dropdown-button] { 72 | flex: 1; 73 | flex-direction: column; 74 | } 75 | -------------------------------------------------------------------------------- /src/app/component/button/button.component.ts: -------------------------------------------------------------------------------- 1 | import { Input, Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-button', 5 | templateUrl: './button.component.html', 6 | styleUrls: ['./button.component.scss'] 7 | }) 8 | export class ButtonComponent 9 | { 10 | @Input('id') id: string = null; 11 | @Input('loading') loading: boolean = false; 12 | @Input('disabled') disabled: boolean = false; 13 | 14 | constructor() { 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/component/graph/graph-utils/distance.ts: -------------------------------------------------------------------------------- 1 | export function distance(pointA: [number, number], pointB: [number, number]): number { 2 | const dx = pointA[0] - pointB[0]; // delta x 3 | const dy = pointA[1] - pointB[1]; // delta y 4 | return Math.sqrt(dx * dx + dy * dy); // distance 5 | } 6 | -------------------------------------------------------------------------------- /src/app/component/graph/graph-utils/finder.ts: -------------------------------------------------------------------------------- 1 | import { Node, Link } from '../../../neo4j/model'; 2 | 3 | const LINK_TYPE = 'LINK_TYPE'; 4 | const NODE_TYPE = 'NODE_TYPE'; 5 | 6 | export class FinderSingleton 7 | { 8 | type: 'LINK_TYPE'|'NODE_TYPE'; 9 | items: Array = []; 10 | 11 | in(items: Array): FinderSingleton 12 | { 13 | if (items.length === 0) { 14 | Object.assign(this.items, []) 15 | } else { 16 | Object.assign(this.items, items) 17 | } 18 | 19 | return this; 20 | } 21 | 22 | private idOf(needle: Node|Link|number): number 23 | { 24 | if (typeof needle === 'number') { 25 | return needle; 26 | } else { 27 | return (needle instanceof Node) ? needle.getId() : needle.relationship.getId(); 28 | } 29 | } 30 | 31 | indexOf(needle: Node|Link|number) 32 | { 33 | for (let i in this.items) { 34 | if (this.idOf(this.items[i]) === this.idOf(needle)) { 35 | return parseInt(i); 36 | } 37 | } 38 | 39 | return null; 40 | } 41 | 42 | findById(id: number) 43 | { 44 | for (let i in this.items) { 45 | if (this.idOf(this.items[i]) === id) { 46 | return this.items[i]; 47 | } 48 | } 49 | 50 | return null; 51 | } 52 | 53 | findIndexById(id: number) 54 | { 55 | if (typeof id === 'undefined') { 56 | console.warn(`finder.ts Trying to find a node by id but id is undefined, did you return ID(n) in your query?`) 57 | } 58 | 59 | for (let i in this.items) { 60 | if (this.idOf(this.items[i]) === id) { 61 | return parseInt(i) 62 | } 63 | } 64 | 65 | return null; 66 | } 67 | } 68 | 69 | export let Finder = new FinderSingleton(); 70 | -------------------------------------------------------------------------------- /src/app/component/graph/graph-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './state'; 2 | export * from './mouse'; 3 | export * from './shape'; 4 | export * from './distance'; 5 | export * from './finder'; 6 | -------------------------------------------------------------------------------- /src/app/component/graph/graph-utils/mouse.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | 3 | class MouseSingleton 4 | { 5 | coords: [number, number] = [null, null]; 6 | 7 | clickTolerance: number = 5; 8 | dblClickTimeout = null; 9 | 10 | node = { 11 | clickTolerance: 5, 12 | clickWaitTimeout: null, // a timeout 13 | isMouseDown: false, 14 | } 15 | 16 | getCoords(eventTarget?: any): [number, number] 17 | { 18 | return (eventTarget) ? d3.mouse(eventTarget) : d3.mouse(document.body) 19 | } 20 | } 21 | 22 | export let Mouse = new MouseSingleton() 23 | -------------------------------------------------------------------------------- /src/app/component/graph/graph-utils/shape.ts: -------------------------------------------------------------------------------- 1 | import * as tinycolor from 'tinycolor2' 2 | import { NodeInterface } from '../../../neo4j/model'; 3 | import { name, color, truncate } from '../../../neo4j/utils'; 4 | import { Mouse } from './mouse'; 5 | 6 | class ShapeSingleton 7 | { 8 | private svg: any; 9 | 10 | attach(svg: any) 11 | { 12 | this.svg = svg; 13 | } 14 | 15 | /** 16 | * Creates the hidden drag line when linking two nodes. 17 | * Default coords are the line 18 | */ 19 | createDragline(defaultCoords: [number, number, number, number] = [5, 5, 100, 100]): any 20 | { 21 | // line displayed when dragging new nodes 22 | let dragline: any = this.svg.append('line') 23 | .attr('x1', defaultCoords[0]) 24 | .attr('y1', defaultCoords[1]) 25 | .attr('x2', defaultCoords[2]) 26 | .attr('y2', defaultCoords[3]) 27 | .attr('class', 'dragline') 28 | .attr('marker-end', 'url(#dragline-arrow)') 29 | 30 | this.svg.append('svg:defs').append('svg:marker') 31 | .attr('id', 'dragline-arrow') 32 | .attr('class', 'dragline-arrow') 33 | .attr('viewBox', '0 -5 10 10') 34 | .attr('refX', 6) 35 | .attr('markerWidth', 3) 36 | .attr('markerHeight', 3) 37 | .attr('orient', 'auto') 38 | .append('svg:path') 39 | .attr('d', 'M0,-5L10,0L0,5') 40 | 41 | dragline.currentlyBeeingDragged = false 42 | 43 | dragline.hide = () => { 44 | dragline.classed('hidden', true) 45 | } 46 | 47 | dragline.show = () => { 48 | dragline.classed('hidden', false) 49 | } 50 | 51 | dragline.beeingDragged = (value: boolean = null) => { 52 | dragline.currentlyBeeingDragged = value 53 | } 54 | 55 | dragline.isBeeingDragged = () => { 56 | return dragline.currentlyBeeingDragged 57 | } 58 | 59 | return dragline; 60 | } 61 | 62 | /** 63 | * Creates a circle that shows up when creating a new node in create mode. 64 | */ 65 | createCursor() 66 | { 67 | let cursor: any = this.svg.append('circle') 68 | .attr('r', 30) 69 | .attr('id', 'cursor') 70 | .attr('transform', 'translate(-100,-100)') 71 | .attr('class', 'cursor') 72 | 73 | cursor.resetStyle = function() { 74 | this.transition().style('stroke', '#ABB1BB').style('stroke-width', '1.4px').attr('r', 30) 75 | } 76 | 77 | cursor.animate = function () { 78 | this.transition().delay(2).duration(500) 79 | .attr('r', 15) 80 | .style('stroke', '#F2F2DC') 81 | .style('stroke-width', '3px') 82 | } 83 | 84 | cursor.hide = function () { 85 | this.classed('hidden', true) 86 | } 87 | 88 | cursor.show = function () { 89 | this.classed('hidden', false) 90 | } 91 | 92 | return cursor; 93 | } 94 | 95 | appendNodeGroupShapes(groupsRef: any, settings: any) 96 | { 97 | // find node default label shown on the ui 98 | const nameOptions = settings.get('graph.nodes.displayNameOptions') 99 | const colorOptions = settings.get('graph.nodes.displayColorOptions') 100 | let circleColor: string = ''; 101 | 102 | // then append a circle to the group 103 | groupsRef.append('circle') 104 | .attr('class', 'node') 105 | .attr('r', 20) 106 | .style('fill', (n: NodeInterface) => { 107 | circleColor = color(n, colorOptions) 108 | return circleColor 109 | }) 110 | .style('stroke', (n: NodeInterface) => { 111 | return tinycolor(circleColor).lighten(15).toString() 112 | }) 113 | 114 | groupsRef.append('circle') 115 | .attr('class', 'ring') 116 | .attr('r', 23) 117 | 118 | groupsRef.append('text') 119 | .attr('class', 'label') 120 | .text((n: NodeInterface) => { 121 | return `[${n.getId().toString()}] ` + truncate(name(n, nameOptions), 6); 122 | }) 123 | 124 | // groupsRef.append('circle') 125 | // .attr('class', 'expander') 126 | // .attr('r', 7) 127 | // .attr('cx', 22) 128 | // .attr('cy', -18) 129 | // groupsRef.append('circle') 130 | // .attr('class', 'expander expander-inner') 131 | // .attr('r', 3) 132 | // .attr('cx', 22) 133 | // .attr('cy', -18) 134 | 135 | // groupsRef.append('svg:image') 136 | // .attr('xlink:href', './assets/svg/icon8-plus.svg') 137 | // .attr('width', 17) 138 | // .attr('height', 17) 139 | // .attr('cx', 10) 140 | // .attr('cy', -5) 141 | } 142 | 143 | appendShapesToLinkGroups(group: any, settings: any) 144 | { 145 | const nameOptions = settings.get('graph.links.displayNameOptions') 146 | 147 | group 148 | .append('line') 149 | .attr('class', 'link') 150 | .attr('marker-end', 'url(#link-arrow)') 151 | 152 | group.append('line') 153 | .attr('class', 'link-overlay') 154 | 155 | group.append('text') 156 | .attr('class', 'link-text') 157 | .attr('dy', 14) 158 | .text((l, i) => { 159 | return name(l.relationship, nameOptions) + ` [${l.relationship.ID}]`; 160 | }) 161 | .attr('text-anchor', 'middle') 162 | 163 | group.append('svg:defs').append('svg:marker') 164 | .attr('id', 'link-arrow') 165 | .attr('class', 'link-arrow') 166 | .attr('viewBox', '0 -5 10 10') 167 | .attr('refX', 10) 168 | .attr('markerWidth', 5) 169 | .attr('markerHeight', 5) 170 | .attr('orient', 'auto') 171 | .append('svg:path') 172 | .attr('d', 'M0,-5L10,0L0,5') 173 | } 174 | } 175 | 176 | export let Shape = new ShapeSingleton() 177 | -------------------------------------------------------------------------------- /src/app/component/graph/graph-utils/state.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | 3 | class StateSingleton 4 | { 5 | cursor: any = null; 6 | dragline: any = null; 7 | createModeEnabled: boolean = false; 8 | pushMouseTimer: any = null; 9 | pushMouseTimerTolerance: number = 150; // the create mode node push mouse timer length 10 | 11 | currentlyDragging: boolean = false; 12 | 13 | dragEnabled: boolean = false; 14 | dragPos: [number, number] = [null, null]; 15 | dragStartPos: any 16 | dragEndPos: any 17 | 18 | // we save exact event handlers functions 19 | // to attach them and dettach them upon request 20 | savedHandlers: any = { 21 | touchStartDrag: null, 22 | mouseDownDrag: null, 23 | } 24 | 25 | savedNode: any = null; 26 | } 27 | 28 | export let State = new StateSingleton() 29 | -------------------------------------------------------------------------------- /src/app/component/graph/graph.component.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Graph component 3 | */ 4 | :host { 5 | display: flex; 6 | flex: 1; 7 | } 8 | 9 | svg { 10 | display: block; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/component/header/header.component.html: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/app/component/header/header.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | nav { 5 | position: fixed; 6 | width: 100%; 7 | height: $headerHeight; 8 | background-color: colors(nemo); 9 | z-index: 9; 10 | color: colors(white); 11 | padding-left: 8px; 12 | padding-right: 8px; 13 | @include box-sizing(); 14 | 15 | a { 16 | display: flex; 17 | align-items: center; 18 | margin: 0px; 19 | padding: 0px 16px; 20 | 21 | &:hover { 22 | background-color: colors(nemo-hover); 23 | } 24 | 25 | &.main { 26 | font-size: $fontSize + 4px; 27 | font-weight: 200; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/component/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Input, ElementRef } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { Neo4jService } from '../../neo4j'; 5 | import { Debug } from '../../service'; 6 | 7 | @Component({ 8 | selector: 'header-component', 9 | templateUrl: './header.component.html', 10 | styleUrls: ['./header.component.scss'] 11 | }) 12 | export class HeaderComponent 13 | { 14 | neo4jOk: boolean = false; 15 | tutorialIsVisible: boolean = false; 16 | criticalErrorsCount: number; 17 | 18 | constructor(private router: Router, private neo4j: Neo4jService) 19 | { 20 | this.neo4j.ping().then((yes: boolean) => { 21 | this.neo4jOk = yes 22 | }, (res: any) => { 23 | this.neo4jOk = false 24 | }).catch(err => { 25 | this.neo4jOk = false 26 | }) 27 | } 28 | 29 | ngOnInit() 30 | { 31 | this.criticalErrorsCount = Debug.countErrorsByLevel('critical') 32 | } 33 | 34 | logout(e: Event) 35 | { 36 | e.preventDefault() 37 | } 38 | 39 | showTutorial(e: any) 40 | { 41 | e.preventDefault() 42 | this.tutorialIsVisible = true; 43 | } 44 | 45 | dismissTutorial() 46 | { 47 | this.tutorialIsVisible = false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/component/index.ts: -------------------------------------------------------------------------------- 1 | export * from './label/label.component'; 2 | export * from './button/button.component'; 3 | export * from './switch/switch.component'; 4 | export * from './graph/graph.component'; 5 | export * from './header/header.component'; 6 | export * from './search/search.component'; 7 | export * from './tutorial/tutorial.component'; 8 | export * from './multi-select/multi-select.component'; 9 | export * from './multi-select/multi-select-option.component'; 10 | export * from './node-edit/node-edit.component'; 11 | export * from './link-edit/link-edit.component'; 12 | -------------------------------------------------------------------------------- /src/app/component/label/label.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | display: flex; 6 | } 7 | 8 | a.label { 9 | display: flex; 10 | flex-direction: row; 11 | align-self: flex-start; 12 | align-items: center; 13 | margin-right: auto; 14 | padding: 4px 8px; 15 | 16 | &:hover { 17 | 18 | } 19 | 20 | .count { 21 | font-size: $fontSize - 2px; 22 | margin-left: 5px; 23 | } 24 | 25 | @include box-sizing(); 26 | @include border-radius(2px); 27 | } 28 | -------------------------------------------------------------------------------- /src/app/component/label/label.component.ts: -------------------------------------------------------------------------------- 1 | import * as tinycolor from 'tinycolor2' 2 | import { Component, EventEmitter } from '@angular/core'; 3 | import { Input, Output, OnInit } from '@angular/core'; 4 | import { LabelInterface } from '../../neo4j/model'; 5 | 6 | @Component({ 7 | selector: 'label-component', 8 | template: ` 9 | ({{ label.count }}) 10 | `, 11 | styleUrls: ['./label.component.scss'] 12 | }) 13 | export class LabelComponent implements OnInit 14 | { 15 | textColor: string; 16 | @Input('label') label: LabelInterface; 17 | @Input('displayCount') displayCount: boolean = false; 18 | @Output('onClick') onClick: EventEmitter = new EventEmitter(); 19 | 20 | constructor() 21 | { 22 | 23 | } 24 | 25 | ngOnInit() 26 | { 27 | this.textColor = tinycolor(this.label.color).lighten(60).toString() 28 | } 29 | 30 | click(e: any) 31 | { 32 | e.preventDefault(); 33 | this.onClick.emit(this.label) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/component/link-edit/link-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | 9 |
10 | Properties 11 | 12 |
13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 | Save 31 | Reset 32 |
33 | -------------------------------------------------------------------------------- /src/app/component/link-edit/link-edit.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | @include hostSupportsFullHeight() 6 | } 7 | 8 | .input-group { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: space-between; 13 | margin-bottom: 8px; 14 | 15 | input[type="text"] { 16 | width: 100%; 17 | } 18 | 19 | .i-col1 { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | margin-right: 8px; 24 | flex-basis: 35%; 25 | } 26 | .i-col2 { 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | margin-right: 8px; 31 | } 32 | .i-col3 { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | text-align: right; 37 | font-size: $fontSize - 2px; 38 | margin-left: 4px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/component/link-edit/link-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef } from '@angular/core'; 2 | import { OnChanges, OnInit, AfterViewInit } from '@angular/core'; 3 | import { HostListener } from '@angular/core'; 4 | import { Input, Output, EventEmitter } from '@angular/core'; 5 | import { ViewChildren, QueryList, } from '@angular/core'; 6 | import { SimpleChanges } from '@angular/core'; 7 | import { SettingsService } from '../../service'; 8 | import { Neo4jRepository } from '../../neo4j'; 9 | import { ResultSet, CypherQuery, Transaction } from '../../neo4j/orm'; 10 | import { Node, NodeInterface } from '../../neo4j/model'; 11 | import { diff } from '../../core/array'; 12 | 13 | const randomPropNames = ['jumpy', 'flashbull', 'mourn', 'ugliest', 'furry', 'chew', 'equable', 'puzzling', 'oranges'] 14 | 15 | export type LinkUpdatedEvent = { 16 | currentValue: NodeInterface; 17 | previousValue: NodeInterface; 18 | } 19 | 20 | @Component({ 21 | selector: 'link-edit', 22 | templateUrl: './link-edit.component.html', 23 | styleUrls: ['./link-edit.component.scss'] 24 | }) 25 | export class LinkEditComponent implements OnInit, AfterViewInit, OnChanges 26 | { 27 | @Input('link') link: NodeInterface = null; 28 | @Output('onLinkEdited') onLinkEdited: EventEmitter = new EventEmitter(); 29 | 30 | @ViewChildren('propNames', { read: ElementRef }) propNames: QueryList; 31 | @ViewChildren('propValues', { read: ElementRef }) propValues: QueryList; 32 | 33 | loading: boolean = false; 34 | cancelable: boolean = false; 35 | 36 | originalLink: any; 37 | originalType: string|String = null; 38 | originalProperties: Array = []; 39 | removedProperties: Array = []; 40 | 41 | type: string|String = null; 42 | properties: Array = [] 43 | 44 | constructor(private settings: SettingsService, private repo: Neo4jRepository) 45 | { 46 | 47 | } 48 | 49 | ngOnInit() 50 | { 51 | 52 | } 53 | 54 | ngAfterViewInit() 55 | { 56 | 57 | } 58 | 59 | ngOnChanges(changes: SimpleChanges) 60 | { 61 | if (null === changes.link.currentValue) { 62 | this.type = null; 63 | this.properties = []; 64 | 65 | } 66 | 67 | // detect when a node was changed (from null->node, node->node or null->node) 68 | if (this.gracefulId(changes.link.previousValue) !== this.gracefulId(changes.link.currentValue)) { 69 | 70 | // a new node was really selected 71 | if (null !== changes.link.currentValue) { 72 | 73 | this.assignValue(this.link) 74 | this.cancelable = false; 75 | } 76 | } 77 | } 78 | 79 | assignValue(link: Node) 80 | { 81 | this.originalLink = Object.assign({}, link) 82 | this.originalType = link.getType() 83 | Object.assign(this.originalProperties, link.propertiesAsArray()) 84 | 85 | this.link = link; 86 | this.type = this.link.getType() 87 | this.properties = this.link.propertiesAsArray() 88 | } 89 | 90 | onTypeKeyup(e: any) 91 | { 92 | this.cancelable = true; 93 | } 94 | 95 | save(e?: any) 96 | { 97 | if (e) { e.preventDefault() } 98 | this.loading = true; 99 | this.cancelable = false; 100 | 101 | // if link type must be changed... 102 | let changedType = null; 103 | if (this.type !== this.originalType) { 104 | changedType = this.type; 105 | } 106 | 107 | const newProperties = this.gatherProperties(); 108 | const removedProperties = this.gatherRemovedProperties(); 109 | 110 | this.originalProperties = newProperties; 111 | 112 | this.repo.updateRelationshipById(this.link.getId(), this.originalType, changedType, newProperties, removedProperties).then((resultSets: Array) => { 113 | 114 | const link = resultSets[0].getDataset('r').first() 115 | 116 | // update current link 117 | this.loading = false; 118 | this.onLinkEdited.emit({ currentValue: link, previousValue: this.link }) 119 | this.assignValue(link) 120 | 121 | }).catch(err => { 122 | this.loading = false 123 | // @TODO show a ui error 124 | console.log(err) 125 | }) 126 | } 127 | 128 | addProperty(e: any) 129 | { 130 | e.preventDefault(); 131 | this.cancelable = true; 132 | 133 | const prop = randomPropNames[Math.floor(Math.random() * randomPropNames.length)] 134 | this.properties.push([prop, '']) 135 | } 136 | 137 | deleteProperty(e: any, index: number) 138 | { 139 | e.preventDefault() 140 | this.cancelable = true 141 | 142 | this.removedProperties.push(this.properties[index]) 143 | this.properties.splice(index, 1) 144 | } 145 | 146 | cancel(e: any) 147 | { 148 | e.preventDefault(); 149 | this.cancelable = false; 150 | 151 | this.type = this.originalType; 152 | this.properties = Object.assign([], this.originalProperties) 153 | } 154 | 155 | private gatherProperties() 156 | { 157 | let props: any = {} 158 | const propsArray = this.propNames.toArray() 159 | const valuesArray = this.propValues.toArray() 160 | 161 | propsArray.forEach((ref: ElementRef, i) => { 162 | const prop = ref.nativeElement.value 163 | const value = valuesArray[i].nativeElement.value 164 | props[prop] = value 165 | }) 166 | 167 | return props; 168 | } 169 | 170 | private gatherRemovedProperties() 171 | { 172 | let props: any = {} 173 | 174 | this.removedProperties.forEach((pair: [string, any]) => { 175 | const prop = pair[0] 176 | const value = pair[1] 177 | props[prop] = null 178 | }) 179 | 180 | return props; 181 | } 182 | 183 | private gracefulId(node: NodeInterface) 184 | { 185 | return (node !== null && typeof(node) !== 'undefined' && node.getId() != null) ? node.getId() : null; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/app/component/multi-select/multi-select-option.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | 5 | .multi-select-option { 6 | cursor: pointer; 7 | display: flex; 8 | flex-direction: column; 9 | line-height: 1em; 10 | font-size: $fontSize - 1px; 11 | @include border-radius(2px); 12 | 13 | &:hover { 14 | background-color: colors(charcoal); 15 | } 16 | 17 | &.available { 18 | flex-direction: column; 19 | padding: 8px 16px; 20 | } 21 | 22 | span.item { 23 | display: flex; 24 | flex-direction: row; 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | 29 | &.selected { 30 | padding: 4px 8px 4px 8px; 31 | flex-direction: row; 32 | align-items: center; 33 | margin-right: 4px; 34 | margin-bottom: 4px; 35 | background-color: colors(coal); 36 | 37 | a.rm { 38 | margin-left: 4px; 39 | color: colors(gray); color: colors(gray); 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; 43 | text-align: center; 44 | &:hover { color: colors(success); } 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/app/component/multi-select/multi-select-option.component.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { ElementRef, ViewChild } from '@angular/core'; 4 | import { HostListener } from '@angular/core'; 5 | import { Input, Output, EventEmitter } from '@angular/core'; 6 | 7 | @Component({ 8 | selector: 'multi-select-option', 9 | styleUrls: ['./multi-select-option.component.scss'], 10 | template: `
14 | 15 | x 16 |
`, 17 | //[ngStyle]="{ 'background-color': (value.color && type === 'selected') ? value.color : 'none' }" 18 | }) 19 | export class MultiSelectOptionComponent 20 | { 21 | @Input('value') value: any; 22 | @Input('type') type: 'selected'|'available'; 23 | @Output('onRemove') onRemove: EventEmitter = new EventEmitter(); 24 | 25 | remove(value, e?: MouseEvent) 26 | { 27 | e.preventDefault() 28 | this.onRemove.emit(this.value) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/component/multi-select/multi-select.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | 5 | .multi-select { 6 | display: flex; 7 | flex-direction: column; 8 | box-sizing: border-box; 9 | position: relative; 10 | 11 | .selection { 12 | .selected-items { 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | margin-top: 8px; 17 | } 18 | 19 | input[type="text"] { 20 | width: 100%; 21 | display: flex; 22 | } 23 | } 24 | 25 | .dropdown { 26 | position: absolute; 27 | width: 100%; 28 | top: 38px; 29 | left: 0px; 30 | z-index: 9; 31 | 32 | ul { 33 | color: gray; 34 | padding: 6px 0px; 35 | margin: 0; 36 | display: flex; 37 | flex-direction: column; 38 | list-style-type: none; 39 | background-color: colors(night); 40 | border: none; 41 | border-top: 0px; 42 | @include box-sizing(); 43 | @include border-radius(4px); 44 | @include box-shadow(0, 0, 8px, #252525); 45 | 46 | li { 47 | display: flex; 48 | flex-direction: column; 49 | 50 | .empty-results-text { 51 | padding: 8px 16px; 52 | color: gray; 53 | } 54 | } 55 | } 56 | 57 | &:after { 58 | content: ""; 59 | position: absolute; 60 | left: 8px; 61 | top: -8px; 62 | width: 0; 63 | height: 0; 64 | border-style: solid; 65 | border-width: 0 8px 8px 8px; 66 | border-color: transparent transparent colors(night) transparent; 67 | z-index: 10; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/app/component/multi-select/multi-select.component.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 2 | import { Component, OnInit } from '@angular/core'; 3 | import { ElementRef, ViewChild } from '@angular/core'; 4 | import { QueryList, HostListener } from '@angular/core'; 5 | import { Input, Output, EventEmitter } from '@angular/core'; 6 | import { ViewChildren, SimpleChange } from '@angular/core'; 7 | import { MultiSelectOptionComponent } from './multi-select-option.component'; 8 | 9 | @Component({ 10 | selector: 'multi-select', 11 | styleUrls: ['./multi-select.component.scss'], 12 | template: `
13 | 14 |
15 | 16 | 23 | 24 |
25 | 26 | 31 | 32 |
33 |
34 | 35 | 48 |
`, 49 | 50 | }) 51 | export class MultiSelectComponent implements OnInit 52 | { 53 | searchTerm: BehaviorSubject = new BehaviorSubject(''); 54 | 55 | @Input('items') items: Array = []; 56 | @Input('values') values: Array = []; 57 | @Input('dropdownVisible') dropdownVisible: boolean = false; 58 | @Output('valuesChanged') valuesChanged: EventEmitter = new EventEmitter(); 59 | @Output('valueAdded') valueAdded: EventEmitter = new EventEmitter(); 60 | @Output('valueRemoved') valueRemoved: EventEmitter = new EventEmitter(); 61 | @ViewChild('input') input: ElementRef; 62 | 63 | placeholder: string; 64 | areValuesScalar: boolean = false; 65 | 66 | constructor(private elementRef: ElementRef) 67 | { 68 | 69 | } 70 | 71 | ngOnInit() 72 | { 73 | 74 | } 75 | 76 | @HostListener('document:click', ['$event']) 77 | onClickOut(e: any) 78 | { 79 | if (!this.elementRef.nativeElement.contains(e.target)) { 80 | this.dropdownVisible = false; 81 | } 82 | } 83 | 84 | onFocus(e: any) 85 | { 86 | this.dropdownVisible = true; 87 | 88 | } 89 | 90 | onSearchKeyup(e: any) 91 | { 92 | const term = e.target.value.toLowerCase().trim() 93 | 94 | if (term !== '') { 95 | this.dropdownVisible = true 96 | this.searchTerm.next(term) 97 | } else { 98 | this.dropdownVisible = false 99 | } 100 | } 101 | 102 | filter(term: string, items: Array) 103 | { 104 | // if (null === term) { 105 | // return items; 106 | // } else { 107 | // return items.filter((item) => { 108 | // return (item.name.toLowerCase().indexOf(term) > -1) ? true : false 109 | // }) 110 | // } 111 | } 112 | 113 | addItem(value: string) 114 | { 115 | this.dropdownVisible = false; 116 | 117 | if (this.values.indexOf(value) === -1) { 118 | this.values.push(value); 119 | this.items.splice(this.items.indexOf(value), 1); 120 | this.valuesChanged.emit(this.values); 121 | this.valueAdded.emit(value) 122 | } 123 | } 124 | 125 | removeItem(value: string) 126 | { 127 | const index = this.values.indexOf(value); 128 | this.values.splice(index, 1); 129 | 130 | if (this.items.indexOf(value) === -1) { 131 | this.items.push(value); 132 | } 133 | 134 | this.valueRemoved.emit(value) 135 | this.valuesChanged.emit(this.values); 136 | } 137 | 138 | getValues() { 139 | return this.values; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/app/component/node-edit/node-edit.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | Labels 8 | 9 |
10 | 11 | 19 |
20 | 21 |
22 | Properties 23 | 24 |
25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 | Save 43 | Reset 44 |
45 | -------------------------------------------------------------------------------- /src/app/component/node-edit/node-edit.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | @include hostSupportsFullHeight() 6 | } 7 | 8 | .input-group { 9 | display: flex; 10 | flex-direction: row; 11 | align-items: center; 12 | justify-content: space-between; 13 | margin-bottom: 8px; 14 | 15 | input[type="text"] { 16 | width: 100%; 17 | } 18 | 19 | .i-col1 { 20 | display: flex; 21 | flex-direction: row; 22 | align-items: center; 23 | margin-right: 8px; 24 | flex-basis: 35%; 25 | } 26 | .i-col2 { 27 | display: flex; 28 | flex-direction: row; 29 | align-items: center; 30 | margin-right: 8px; 31 | } 32 | .i-col3 { 33 | display: flex; 34 | flex-direction: row; 35 | align-items: center; 36 | text-align: right; 37 | font-size: $fontSize - 2px; 38 | margin-left: 4px; 39 | } 40 | } 41 | 42 | .label { 43 | color: colors(white); 44 | display: flex; 45 | flex-direction: row; 46 | align-items: center; 47 | padding: 2px 4px; 48 | background-color: #b533b7; 49 | @include border-radius(1px); 50 | } 51 | -------------------------------------------------------------------------------- /src/app/component/node-edit/node-edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef, OnChanges } from '@angular/core'; 2 | import { Input, Output, EventEmitter } from '@angular/core'; 3 | import { ViewChildren, QueryList, ContentChildren } from '@angular/core'; 4 | import { SimpleChanges } from '@angular/core'; 5 | import { SettingsService } from '../../service'; 6 | import { Neo4jRepository } from '../../neo4j'; 7 | import { ResultSet, CypherQuery, Transaction } from '../../neo4j/orm'; 8 | import { Node, NodeInterface } from '../../neo4j/model'; 9 | import { diff } from '../../core/array'; 10 | 11 | const randomPropNames = ['jumpy', 'flashbull', 'mourn', 'ugliest', 'furry', 'chew', 'equable', 'puzzling', 'oranges'] 12 | 13 | @Component({ 14 | selector: 'node-edit', 15 | templateUrl: './node-edit.component.html', 16 | styleUrls: ['./node-edit.component.scss'] 17 | }) 18 | export class NodeEditComponent 19 | { 20 | @Input('node') node: NodeInterface = null; 21 | @Output('onNodeEdited') onNodeEdited: EventEmitter = new EventEmitter() 22 | 23 | @ViewChildren('propNames', { read: ElementRef }) propNames: QueryList; 24 | @ViewChildren('propValues', { read: ElementRef }) propValues: QueryList; 25 | 26 | loading: boolean = false; 27 | cancelable: boolean = false; 28 | 29 | labelsDropdownVisible: boolean = false; 30 | 31 | selectedLabels: Array = []; 32 | availableLabels: Array = []; 33 | private originalLabels: Array = []; 34 | private removedLabels: Array = []; 35 | 36 | private properties: Array<[string, any]> = []; 37 | private originalProperties: Array<[string, any]> = []; 38 | private removedProperties : Array<[string, any]> = []; 39 | 40 | constructor(private settings: SettingsService, private repo: Neo4jRepository) 41 | { 42 | 43 | } 44 | 45 | toggleLabelsDropdown(e: any) 46 | { 47 | e.preventDefault() 48 | } 49 | 50 | private parseLabels() 51 | { 52 | this.availableLabels = this.settings.get('graph.labels'); 53 | this.selectedLabels = this.node.getLabels(); 54 | } 55 | 56 | ngOnChanges(changes: SimpleChanges) 57 | { 58 | if (null !== changes.node.currentValue) { 59 | // always copy node properties to properties for the view whenever its not null 60 | // also update all labels (available and node labels) 61 | this.properties = this.node.propertiesAsArray() 62 | 63 | } else { 64 | this.properties = []; 65 | } 66 | 67 | // detect when a node was changed (from null->node, node->node or null->node) 68 | if (this.gracefulId(changes.node.previousValue) !== this.gracefulId(changes.node.currentValue)) { 69 | // a new node was really selected, changes do not only concern current node 70 | 71 | if (null !== changes.node.currentValue) { 72 | // make a separate copy of the thing 73 | this.originalProperties = Object.assign([], this.properties); 74 | this.originalLabels = Object.assign([], this.node.getLabels()); 75 | 76 | this.removedProperties = []; 77 | this.removedLabels = []; 78 | } 79 | } 80 | 81 | this.parseLabels() 82 | } 83 | 84 | onLabelsChanged(values: Array) 85 | { 86 | this.cancelable = true; 87 | } 88 | 89 | onLabelRemoved(label: string) 90 | { 91 | // add value to the labels to remove if the node original 92 | // labels did contain this value 93 | if (this.originalLabels.indexOf(label) > -1) { 94 | this.removedLabels.push(label); 95 | } 96 | } 97 | 98 | onLabelAdded(label: string) 99 | { 100 | // there is no need to update this manually because 101 | // selected labels is already two-way bounded 102 | } 103 | 104 | save(e?: any) 105 | { 106 | if (e) { e.preventDefault() } 107 | 108 | this.loading = true; 109 | this.cancelable = false; 110 | 111 | const newProperties = this.gatherProperties() 112 | const removedProperties = this.gatherRemovedProperties() 113 | 114 | this.originalProperties = newProperties; 115 | 116 | this.repo.updateNodeById(this.node.getId(), newProperties, removedProperties, this.selectedLabels, this.removedLabels).then((resultSets: Array) => { 117 | 118 | const node = resultSets[0].getDataset('n').first() 119 | this.onNodeEdited.emit(node) 120 | this.loading = false 121 | 122 | }).catch(err => { 123 | this.loading = false 124 | console.log(err) 125 | this.onNodeEdited.emit(null) 126 | }) 127 | } 128 | 129 | addProperty(e: any) 130 | { 131 | e.preventDefault() 132 | this.cancelable = true 133 | 134 | const prop = randomPropNames[Math.floor(Math.random() * randomPropNames.length)] 135 | this.properties.push([prop, '']) 136 | } 137 | 138 | deleteProperty(e: any, index: number) 139 | { 140 | e.preventDefault() 141 | this.cancelable = true 142 | 143 | this.removedProperties.push(this.properties[index]) 144 | this.properties.splice(index, 1) 145 | } 146 | 147 | cancel(e?: any) 148 | { 149 | if (e) { e.preventDefault() } 150 | 151 | this.cancelable = false; 152 | 153 | this.properties = Object.assign([], this.originalProperties) 154 | this.selectedLabels = Object.assign([], this.originalLabels) 155 | 156 | } 157 | 158 | private gatherProperties() 159 | { 160 | let props: any = {} 161 | const propsArray = this.propNames.toArray() 162 | const valuesArray = this.propValues.toArray() 163 | 164 | propsArray.forEach((ref: ElementRef, i) => { 165 | const prop = ref.nativeElement.value 166 | const value = valuesArray[i].nativeElement.value 167 | props[prop] = value 168 | }) 169 | 170 | return props; 171 | } 172 | 173 | private gatherRemovedProperties() 174 | { 175 | let props: any = {} 176 | 177 | this.removedProperties.forEach((pair: [string, any]) => { 178 | const prop = pair[0] 179 | const value = pair[1] 180 | props[prop] = null 181 | }) 182 | 183 | return props; 184 | } 185 | 186 | private gracefulId(node: NodeInterface) 187 | { 188 | return (node !== null && typeof(node) !== 'undefined' && node.getId() != null) ? node.getId() : null; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/app/component/search/search.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | 6 | } 7 | 8 | .search { 9 | display: flex; 10 | flex-direction: column; 11 | padding: 16px; 12 | 13 | input[type="text"] { 14 | flex: 1; 15 | @include border-radius(0px 1px 1px 0px); 16 | } 17 | 18 | a.info { 19 | color: colors(grayish); 20 | position: absolute; 21 | right: 10px; 22 | top: 9px; 23 | display: flex; 24 | align-items: center; 25 | line-height: 1em; 26 | font-size: $fontSize + 4px; 27 | 28 | &.info-loader { 29 | background-color: colors(charcoal); 30 | padding: 4px; 31 | border-radius: 10px; 32 | } 33 | } 34 | } 35 | 36 | .search-bar, .query-bar { 37 | display: flex; 38 | position: relative; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/component/search/search.component.ts: -------------------------------------------------------------------------------- 1 | import { Subject, Observable } from 'rxjs'; 2 | import { HostListener, ElementRef } from '@angular/core'; 3 | import { Input, Output, Component } from '@angular/core'; 4 | import { OnInit, HostBinding, EventEmitter } from '@angular/core'; 5 | import { AfterViewInit, Renderer, ViewChild } from '@angular/core'; 6 | import { CypherQuery, SimpleQuery } from '../../neo4j/orm'; 7 | 8 | @Component({ 9 | selector: 'search-component', 10 | styleUrls: ['./search.component.scss'], 11 | template: ``, 37 | providers: [], 38 | }) 39 | export class SearchComponent implements OnInit, AfterViewInit 40 | { 41 | @Input('loading') loading: boolean = false; 42 | @Input('mode') mode: 'normal'|'advanced' = 'normal'; 43 | @Output('onSearch') onSearch = new EventEmitter(); 44 | @ViewChild('searchInput') searchInput: ElementRef; 45 | 46 | normalQueryString: string = ''; // name="Planning de garde" 10,0 47 | cypherQueryString: string = ''; //'MATCH (a) RETURN a, LABELS(a), ID(a) LIMIT 10'; 48 | 49 | normalQueryLimit: number = 30; 50 | normalQuerySkip: number = null; 51 | 52 | ngKlasses: string; 53 | term: Subject = new Subject(); 54 | 55 | constructor(private renderer: Renderer, private elementRef: ElementRef) 56 | { 57 | 58 | } 59 | 60 | ngOnInit() 61 | { 62 | 63 | } 64 | 65 | ngAfterViewInit() 66 | { 67 | 68 | } 69 | 70 | toggleMode(e: any) 71 | { 72 | e.preventDefault() 73 | this.mode = (this.mode === 'normal') ? 'advanced' : 'normal'; 74 | } 75 | 76 | onSubmit(e: any) 77 | { 78 | e.preventDefault() 79 | let queryString: string; 80 | 81 | if (this.mode === 'normal') { 82 | 83 | const simple = new SimpleQuery(this.normalQueryString) 84 | queryString = simple.getQuery(); 85 | this.onSearch.emit({ mode: this.mode, queryString: queryString }) 86 | 87 | } else { 88 | 89 | this.onSearch.emit({ mode: this.mode, queryString: this.cypherQueryString }) 90 | } 91 | 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/component/switch/switch.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | 6 | } 7 | 8 | .switch { 9 | $size: $fontSize + 10px; 10 | 11 | cursor: pointer; 12 | display: flex; 13 | flex-direction: row; 14 | align-items: center; 15 | height: $size; 16 | line-height: $size; 17 | padding-left: 2px; 18 | padding-right: 2px; 19 | width: ($size * 2) - 10px; 20 | 21 | background-color: colors(danger); 22 | @include border-radius($size / 2); 23 | 24 | .knob { 25 | width: $size - 4px; 26 | height: $size - 4px; 27 | background-color: colors(coal); 28 | @include border-radius($size / 2); 29 | 30 | &.right { 31 | 32 | } 33 | } 34 | 35 | &.on { 36 | background-color: colors(success); 37 | 38 | .knob { 39 | margin-left: auto; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/component/switch/switch.component.ts: -------------------------------------------------------------------------------- 1 | import { Subject, Observable } from 'rxjs'; 2 | import { HostListener, ElementRef } from '@angular/core'; 3 | import { Input, Output, Component } from '@angular/core'; 4 | import { OnInit, HostBinding, EventEmitter } from '@angular/core'; 5 | import { AfterViewInit, Renderer, ViewChild } from '@angular/core'; 6 | 7 | @Component({ 8 | selector: 'switch-component', 9 | styleUrls: ['./switch.component.scss'], 10 | template: `
11 |
12 |
` 13 | }) 14 | export class SwitchComponent implements OnInit, AfterViewInit 15 | { 16 | @Input('value') value: boolean = false 17 | @Output('change') onChange: EventEmitter = new EventEmitter(); 18 | 19 | constructor() 20 | { 21 | 22 | 23 | } 24 | 25 | ngOnInit() 26 | { 27 | 28 | } 29 | 30 | ngAfterViewInit() 31 | { 32 | 33 | } 34 | 35 | toggle(e: any) 36 | { 37 | this.value = !this.value 38 | this.onChange.emit(this.value) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/component/tutorial/tutorial.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |

Explore: Simple queries language

7 |

Create: Create mode

8 |

Edit: Node & link edition

9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | Tuto gif 20 |
21 |

22 | Simple queries let you find nodes without typing long cypher queries. 23 |

24 | 25 | // pseudo code format
26 | :Label1:Label2 property="Value" limit,skip 27 |
28 | 29 | // using numbers, limit and multiple properties
30 | :Person name="Ben" age=12 10 31 |
32 | 33 | // using everything
34 | :Person name="Ben" age=34 50,0 35 |
36 | 37 |

38 | Display one level of relationships using the "+1" flag at the very end of the query 39 |

40 | 41 | :Company name="Gougle" +1 42 | 43 | 44 |
45 | 46 |
47 |
48 | Tuto gif 49 |
50 |

51 | Create mode lets you create new nodes and relationships. You can toggle the create mode by clicking 52 | on the create mode switch (switch). 53 |

54 |

55 | You can also toggle create mode by using the Ctrl+N shortcut. 56 |

57 |

58 | Long-click on the empty circle to create a new node, and drag a line between two nodes to create a relationship. 59 |

60 |
61 | 62 |
63 |
64 | Tuto gif 65 |
66 |

67 | Create mode lets you create new nodes and relationships. You can toggle the create mode by clicking 68 | on the create mode switch (switch). 69 |

70 |

71 | You can also toggle create mode by using the Ctrl+N shortcut. 72 |

73 |

74 | Long-click on the empty circle to create a new node, and drag a line between two nodes to create a relationship. 75 |

76 |
77 | 78 |
79 | 80 | 81 |
82 | 83 | 84 | 85 |
86 |
87 | -------------------------------------------------------------------------------- /src/app/component/tutorial/tutorial.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | .tutorial-overlay { 5 | position: absolute; 6 | top: 0px; 7 | left: 0px; 8 | right: 0px; 9 | bottom: 0px; 10 | width: 100%; 11 | height: 100%; 12 | overflow: hidden; 13 | background-color: colors(charcoal); 14 | opacity: 0.9; 15 | z-index: 9; 16 | } 17 | 18 | .tutorial-content { 19 | position: absolute; 20 | left: 50%; 21 | top: 50%; 22 | width: 100%; 23 | height: 90%; 24 | transform: translate(-50%, -50%); 25 | max-width: 600px; 26 | background-color: colors(coal); 27 | z-index: 10; 28 | 29 | .header { 30 | padding: 16px 24px; 31 | border-bottom: 1px solid colors(charcoal); 32 | 33 | .flaticon-cancel:before { 34 | font-size: 15px; 35 | } 36 | } 37 | 38 | h2.tuto-title { 39 | font-size: $fontSize + 2px; 40 | margin: 0px; 41 | font-weight: 200; 42 | text-align: center; 43 | } 44 | 45 | .sub-content { 46 | padding: 24px 32px; 47 | 48 | .tuto-gif { 49 | text-align: center; 50 | img { 51 | margin: auto; 52 | display: block; 53 | } 54 | 55 | margin-bottom: 24px; 56 | } 57 | p.tuto-text { 58 | &.center { 59 | text-align: center 60 | } 61 | line-height: 1.5em; 62 | margin-bottom: 16px 63 | } 64 | code { 65 | color: colors(warning); 66 | display: inline; 67 | } 68 | code.tuto-code { 69 | display: block; 70 | color: colors(warning); 71 | margin-bottom: 16px; 72 | padding: 8px; 73 | line-height: 1.5em;; 74 | background-color: colors(charcoal); 75 | @include border-radius(2px); 76 | } 77 | 78 | code { 79 | .comment { 80 | color: gray; 81 | } 82 | } 83 | } 84 | } 85 | 86 | .bubbles { 87 | margin: 0px auto; 88 | padding: 25px 0px; 89 | 90 | a.bubble { 91 | $bh: 12px; 92 | width: $bh; 93 | height: $bh; 94 | background-color: colors(secondary); 95 | border: 2px solid transparent; 96 | @include border-radius($bh); 97 | margin-left: 6px; 98 | margin-right: 6px; 99 | @include transition(background-color ease 0.1s); 100 | 101 | &:hover { 102 | background-color: colors(primary); 103 | } 104 | &.active { 105 | background-color: colors(primary); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/component/tutorial/tutorial.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'tutorial-component', 5 | templateUrl: './tutorial.component.html', 6 | styleUrls: ['./tutorial.component.scss'] 7 | }) 8 | export class TutorialComponent 9 | { 10 | step: number = 1; 11 | @Output('onDismiss') onDismiss: EventEmitter = new EventEmitter(); 12 | 13 | constructor() 14 | { 15 | 16 | } 17 | 18 | dismiss(e: any) 19 | { 20 | e.preventDefault() 21 | this.onDismiss.emit() 22 | } 23 | 24 | next(e: any, step: number) 25 | { 26 | e.preventDefault(); 27 | this.step = step; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/app/core/array/crosscut.ts: -------------------------------------------------------------------------------- 1 | // determine if all values of array1 are in array2 and vice versa, whatever the order though 2 | export function crosscut(array1: Array, array2: Array) { 3 | array1 = array1.map(v => { return (typeof(v) === 'string') ? v.toLowerCase() : v }) 4 | array2 = array2.map(v => { return (typeof(v) === 'string') ? v.toLowerCase() : v }) 5 | 6 | function isIn(value: any, index) { 7 | return array1.indexOf(value) > -1; 8 | } 9 | 10 | return array2.every(isIn) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/core/array/diff.ts: -------------------------------------------------------------------------------- 1 | export function diff(array1: Array, array2: Array) { 2 | let a = [], diff = []; 3 | 4 | for (let i = 0; i < array1.length; i++) { 5 | a[array1[i]] = true; 6 | } 7 | 8 | for (let i = 0; i < array2.length; i++) { 9 | if (a[array2[i]]) { 10 | delete a[array2[i]]; 11 | } else { 12 | a[array2[i]] = true; 13 | } 14 | } 15 | 16 | for (let k in a) { 17 | diff.push(k); 18 | } 19 | 20 | return diff; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/core/array/distinct.ts: -------------------------------------------------------------------------------- 1 | export function distinct(prop: string, array: Array) { 2 | const keys = array.map((obj) => { return obj[prop] }) 3 | return array.filter((obj, i) => { return keys.indexOf(obj[prop]) === i }) 4 | } 5 | -------------------------------------------------------------------------------- /src/app/core/array/group.ts: -------------------------------------------------------------------------------- 1 | export function group(criterium: string, array: Array): Array { 2 | let groups: any = {} 3 | 4 | for (let i in array) { 5 | const data = array[i] 6 | const key = array[i][criterium] 7 | 8 | if (typeof(groups[key]) === 'undefined') { 9 | groups[key] = [] 10 | } 11 | 12 | groups[key].push(data) 13 | } 14 | 15 | let groupsArray: Array = [] 16 | 17 | for (var key in groups) { 18 | if (groups.hasOwnProperty(key)) { 19 | groupsArray.push(groups[key]); 20 | } 21 | } 22 | 23 | return groupsArray 24 | } 25 | -------------------------------------------------------------------------------- /src/app/core/array/index.ts: -------------------------------------------------------------------------------- 1 | export * from './diff'; 2 | export * from './group'; 3 | export * from './crosscut'; 4 | export * from './distinct'; 5 | export * from './unique'; 6 | export * from './orderby'; 7 | -------------------------------------------------------------------------------- /src/app/core/array/orderby.ts: -------------------------------------------------------------------------------- 1 | import { PropertyAccess } from '../property.access'; 2 | 3 | export function orderBy(access: string, array: Array, order: string = 'ASC') { 4 | const accessor = new PropertyAccess() 5 | 6 | return array.sort((a, b) => { 7 | if (order === 'ASC') { 8 | return (accessor.getValue(a, access) > accessor.getValue(b, access)) ? 1 : -1; 9 | } else { 10 | return (accessor.getValue(a, access) < accessor.getValue(b, access)) ? 1 : -1; 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/app/core/array/unique.ts: -------------------------------------------------------------------------------- 1 | export function unique(array: Array) { 2 | return array.filter((item, i) => { return array.indexOf(item) === i }) 3 | } 4 | -------------------------------------------------------------------------------- /src/app/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './uuid'; 2 | export * from './property.access'; 3 | -------------------------------------------------------------------------------- /src/app/core/pipe/entries.pipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Pipe } from '@angular/core'; 2 | 3 | @Pipe({ name: 'entries', pure: false }) 4 | export class EntriesPipe implements PipeTransform { 5 | transform(object: any) : any { 6 | return Object.entries(object) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/core/pipe/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nicedate.pipe'; 2 | export * from './keys.pipe'; 3 | export * from './entries.pipe'; 4 | -------------------------------------------------------------------------------- /src/app/core/pipe/keys.pipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Pipe } from '@angular/core'; 2 | 3 | @Pipe({ name: 'keys', pure: false }) 4 | export class KeysPipe implements PipeTransform { 5 | transform(value, args:string[]) : any { 6 | let keys = []; 7 | for (let key in value) { 8 | keys.push(key); 9 | } 10 | return keys; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/core/pipe/nicedate.pipe.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import { PipeTransform, Pipe } from '@angular/core'; 3 | 4 | @Pipe({ name: 'nicedate', pure: false }) 5 | export class NiceDate implements PipeTransform { 6 | transform(value: string|number, args: string[]) : any { 7 | 8 | if (typeof(value) === 'number') { 9 | 10 | const date = moment(value); 11 | return date.format('ddd HH:mm A'); 12 | 13 | } else { 14 | return value; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/property.access.ts: -------------------------------------------------------------------------------- 1 | export class PropertyAccess 2 | { 3 | constructor() 4 | { 5 | 6 | } 7 | 8 | getValue(data: any, accessor: string) 9 | { 10 | const keys = accessor.split('.') 11 | return this.descendKeysTree(data, keys) 12 | } 13 | 14 | descendKeysTree(data: any, keys: Array) 15 | { 16 | if (keys.length === 0) { 17 | return data; 18 | } 19 | 20 | for (let key of keys) { 21 | 22 | const prop = this.findPropertyName(key) 23 | const index = this.findArrayIndex(key) 24 | 25 | if (typeof(data[prop])) { 26 | 27 | // update data to next level of data when key was found 28 | // and remove first key value to descend one level 29 | // also handles accessors featuring array access like "property[2]" 30 | 31 | if (false !== index) { 32 | data = data[prop][index] 33 | } else { 34 | data = data[prop] 35 | } 36 | 37 | keys.shift() 38 | 39 | if (keys.length === 0) { 40 | return data 41 | } else { 42 | return this.descendKeysTree(data, keys) 43 | } 44 | 45 | } else { 46 | return null; 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * Accepts key values like "property[2]" or just "property" 53 | * and always return the "property" without the array access value. 54 | */ 55 | findPropertyName(key: string): string 56 | { 57 | const parts = key.split('[') 58 | 59 | if (parts.length === 2) { 60 | return parts[0] 61 | } else { 62 | return key 63 | } 64 | } 65 | 66 | /** 67 | * Accepts key values like "property[2]" or just "property" 68 | * and always return the "2" index value (string or number) 69 | */ 70 | findArrayIndex(key: string) 71 | { 72 | const parts = key.split('[') 73 | 74 | if (parts.length === 2) { 75 | const index = parts[1].replace(']', '') 76 | return index 77 | } else { 78 | return false 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/core/string/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/app/core/string/index.ts -------------------------------------------------------------------------------- /src/app/core/uuid.ts: -------------------------------------------------------------------------------- 1 | export function uuid() { 2 | function s4() { 3 | return Math.floor((1 + Math.random()) * 0x10000) 4 | .toString(16) 5 | .substring(1); 6 | } 7 | 8 | return `${s4()}${s4()}-${s4()}${s4()}-${s4()}${s4()}-${s4()}${s4()}`; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/neo4j/index.ts: -------------------------------------------------------------------------------- 1 | export * from './neo4j.service'; 2 | export * from './neo4j.repository'; 3 | -------------------------------------------------------------------------------- /src/app/neo4j/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './node.interface'; 2 | export * from './link.interface'; 3 | export * from './label.interface'; 4 | export * from './node'; 5 | export * from './link'; 6 | -------------------------------------------------------------------------------- /src/app/neo4j/model/label.interface.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from './node.interface'; 2 | 3 | export interface LabelInterface { 4 | name: string; 5 | count?: number; 6 | color?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/neo4j/model/link.interface.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from './node.interface'; 2 | 3 | export interface LinkInterface { 4 | source: NodeInterface; 5 | target: NodeInterface; 6 | relationship: NodeInterface; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/neo4j/model/link.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from './node.interface'; 2 | import { LinkInterface } from './link.interface'; 3 | 4 | export type LinkDataType = { 5 | source: NodeInterface; 6 | target: NodeInterface; 7 | relationship: NodeInterface; 8 | } 9 | 10 | export class Link implements LinkInterface 11 | { 12 | source: NodeInterface; 13 | target: NodeInterface; 14 | relationship: NodeInterface; 15 | 16 | constructor(link: LinkDataType) 17 | { 18 | this.source = link.source; 19 | this.target = link.target; 20 | this.relationship = link.relationship; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/neo4j/model/node.interface.ts: -------------------------------------------------------------------------------- 1 | export interface NodeInterface { 2 | ID: number; 3 | LABELS: Array; 4 | META: any; 5 | TYPE?: string; 6 | props: any; 7 | fixed?: boolean; 8 | x?: number; 9 | y?: number; 10 | hydrate(data: any, allowReplace: boolean): void; 11 | properties(): any; 12 | propertiesAsArray(): any; 13 | add(prop: string, value: any): any 14 | set(prop: string, value: any, enumerable?: boolean): any; 15 | reset(props: any): any; 16 | get(prop: string): any; 17 | remove(prop: string): any; 18 | renameProperty(prop: string, newProp: string): any 19 | getId(): number; 20 | getType(): string; 21 | hasLabel(label:string): boolean; 22 | getLabels(): Array; 23 | getFirstLabel(): string; 24 | setLabels(labels: Array): any; 25 | addLabel(label: string): any; 26 | removeLabel(label: string): any; 27 | metadata(): any; 28 | [prop: string]: any; 29 | 30 | setFixed(fixed: boolean): any; 31 | setCoords(coords: [number, number]): any; 32 | getCoords(): any 33 | } 34 | -------------------------------------------------------------------------------- /src/app/neo4j/model/node.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from './node.interface'; 2 | 3 | const reserved = [ 4 | 'ID', 'LABELS', 'META', 'TYPE' 5 | ]; 6 | 7 | export class Node implements NodeInterface 8 | { 9 | ID: number; 10 | LABELS: Array = []; 11 | META: any; 12 | TYPE?: string; 13 | props: any = {}; 14 | x?: number; 15 | y?: number; 16 | fixed?: boolean; 17 | 18 | constructor(data: any = null) 19 | { 20 | if (null !== data) { 21 | this.hydrate(data) 22 | } 23 | } 24 | 25 | hydrate(data: any, allowReplace: boolean = true): void 26 | { 27 | for (let prop in data) { 28 | if (data.hasOwnProperty(prop) && typeof data[prop] !== 'function') { 29 | 30 | if (reserved.indexOf(prop) === -1) { 31 | this.props[prop] = data[prop] 32 | } else { 33 | this[prop] = data[prop] 34 | } 35 | } 36 | } 37 | } 38 | 39 | reset(data: any) 40 | { 41 | for (let prop in this.props) { 42 | if (data.hasOwnProperty(prop)) { 43 | // keep the prop ! 44 | } else { 45 | this.remove(prop) 46 | } 47 | } 48 | 49 | return this 50 | } 51 | 52 | set(prop: string, value: any, enumerable: boolean = true) 53 | { 54 | if (false === enumerable) { 55 | Object.defineProperty(this, prop, { 56 | configurable: false, 57 | enumerable: false, 58 | writable: true, 59 | value: value 60 | }) 61 | } else { 62 | 63 | if (reserved.indexOf(prop) === -1) { 64 | this.props[prop] = value; 65 | } else { 66 | this[prop] = value 67 | } 68 | 69 | } 70 | 71 | return this 72 | } 73 | 74 | add(prop: string, value: any) 75 | { 76 | if (reserved.indexOf(prop) === -1) { 77 | this.props[prop] = value; 78 | } else { 79 | this[prop] = value 80 | } 81 | return this 82 | } 83 | 84 | renameProperty(prop: string, newProp: string) 85 | { 86 | const value = this.props[prop] 87 | this.remove(prop) 88 | this.add(newProp, value) 89 | return this 90 | } 91 | 92 | remove(prop: string) 93 | { 94 | delete(this.props[prop]) 95 | return this; 96 | } 97 | 98 | properties() 99 | { 100 | return this.props; 101 | } 102 | 103 | propertiesAsArray() 104 | { 105 | return Object.entries(this.props); 106 | } 107 | 108 | get(prop: string) 109 | { 110 | return (typeof this.props[prop] === 'undefined') ? null : this.props[prop] 111 | } 112 | 113 | getId() 114 | { 115 | return this.ID; 116 | } 117 | 118 | getType() 119 | { 120 | return this.TYPE; 121 | } 122 | 123 | getLabels() 124 | { 125 | return this.LABELS; 126 | } 127 | 128 | hasLabel(label: string) 129 | { 130 | return this.LABELS.indexOf(label) > -1 ? true : false; 131 | } 132 | 133 | setLabels(labels: Array) 134 | { 135 | this.LABELS = labels 136 | return this; 137 | } 138 | 139 | addLabel(label: string) 140 | { 141 | this.LABELS.push(label) 142 | return this; 143 | } 144 | 145 | removeLabel(label: string) 146 | { 147 | const index = this.LABELS.indexOf(label); 148 | 149 | if (this.LABELS.indexOf(label) > -1) { 150 | this.LABELS.splice(index, 1); 151 | } 152 | 153 | return this; 154 | } 155 | 156 | getFirstLabel() 157 | { 158 | return (this.LABELS.length > 0) ? this.LABELS[0] : null; 159 | } 160 | 161 | metadata() 162 | { 163 | return this.META; 164 | } 165 | 166 | setFixed(fixed: boolean) 167 | { 168 | this.fixed = fixed; 169 | return this; 170 | } 171 | 172 | setCoords(coords: [number, number]) 173 | { 174 | this.x = coords[0]; 175 | this.y = coords[1]; 176 | return this; 177 | } 178 | 179 | getCoords() 180 | { 181 | return [this.x, this.y] 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/app/neo4j/neo4j.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Neo4jService } from './neo4j.service'; 3 | import { SettingsService } from '../service'; 4 | import { ResultSet, Transaction } from '../neo4j/orm'; 5 | import { CypherQuery, SimpleQuery } from '../neo4j/orm'; 6 | import { Node, NodeInterface } from '../neo4j/model'; 7 | import { Link, LinkInterface } from '../neo4j/model'; 8 | import { LabelInterface } from '../neo4j/model'; 9 | 10 | /** 11 | * Just short methods for useful top user operations. 12 | * Enhance it as you wish using the tools demonstrated bellow. 13 | */ 14 | @Injectable() 15 | export class Neo4jRepository 16 | { 17 | constructor(private neo4j: Neo4jService, private settings: SettingsService) 18 | { 19 | 20 | } 21 | 22 | findAllLabels() 23 | { 24 | const transaction = new Transaction() 25 | const colors = this.settings.get('graph.nodes.displayColorOptions') 26 | 27 | transaction.add(`MATCH (a) WITH DISTINCT LABELS(a) AS tmp, COUNT(a) AS tmpCnt UNWIND tmp AS label RETURN label, SUM(tmpCnt) AS cnt`) 28 | 29 | return new Promise((resolve, reject) => { 30 | this.neo4j.commit(transaction, true).then(rawResults => { 31 | 32 | let labels = []; 33 | 34 | for (let i in rawResults[0].data) { 35 | const row = rawResults[0].data[i].row; 36 | const name = row[0]; 37 | const color = (typeof colors[name] !== 'undefined') ? colors[name] : '#222'; 38 | 39 | const label: LabelInterface = { name: name, count: row[1], color: color }; 40 | labels.push(label) 41 | } 42 | 43 | resolve(labels) 44 | 45 | }).catch(err => { 46 | reject(err) 47 | }) 48 | }) 49 | } 50 | 51 | persistNode(node: NodeInterface): Promise 52 | { 53 | const builder = new CypherQuery() 54 | 55 | // build a query string to pass to a transaction 56 | const query = builder 57 | .create('n', node.getLabels()) 58 | .setProperties('n', node.properties()) 59 | .returns('n, ID(n), LABELS(n)') 60 | .limit(1) 61 | .getQuery() 62 | 63 | const transaction = new Transaction() 64 | transaction.add(query) 65 | 66 | return new Promise((resolve, reject) => { 67 | this.neo4j.commit(transaction).then((resultSets: Array) => { 68 | 69 | let node = resultSets[0].getDataset('n').first() 70 | resolve(node) 71 | 72 | }).catch(err => { 73 | reject(err) 74 | }) 75 | }) 76 | } 77 | 78 | updateNodeById(id: number, changedProperties: any, removedProperties: any, labels: Array = null, removedLabels: Array = null): Promise> 79 | { 80 | const builder = new CypherQuery() 81 | 82 | // build a query string to pass to a transaction 83 | const query = builder 84 | .matches('n') 85 | .andWhere('n', 'ID(?)', id) 86 | .setLabels('n', labels) 87 | .removeLabels('n', removedLabels) 88 | .setProperties('n', changedProperties) 89 | .removeProperties('n', removedProperties) 90 | .returns('n, ID(n), LABELS(n)') 91 | .skip(0) 92 | .limit(1) 93 | .getQuery(); 94 | 95 | const transaction = new Transaction() 96 | transaction.add(query) 97 | 98 | return this.neo4j.commit(transaction) 99 | } 100 | 101 | createRelationship(source: NodeInterface, target: NodeInterface, direction: '->'|'<-', type: string) 102 | { 103 | let createDir: string; 104 | 105 | if (direction === '->') { 106 | createDir = `(a)-[r:${type}]->(b)`; 107 | } else if (direction === '<-') { 108 | createDir = `(a)<-[r:${type}]-(b)`; 109 | } 110 | 111 | const transaction = new Transaction() 112 | transaction.add(`MATCH (a), (b) WHERE ID(a)=${source.getId()} AND ID(b)=${target.getId()} CREATE ${createDir} RETURN r, ID(r), TYPE(r)`) 113 | 114 | return new Promise((resolve, reject) => { 115 | this.neo4j.commit(transaction).then((resultSets: Array) => { 116 | 117 | let link: Link = null; 118 | let relationship = resultSets[0].getDataset('r').first() 119 | 120 | if (null !== relationship) { 121 | resolve(new Link({ source: source, target: target, relationship: relationship })) 122 | } else { 123 | reject('Could not creaete relationship') 124 | } 125 | 126 | }).catch(err => { 127 | reject(err) 128 | }) 129 | }) 130 | } 131 | 132 | findRelationships(node: NodeInterface, direction: '->'|'<-' = '->') 133 | { 134 | let match: string; 135 | 136 | if (direction === '->') { 137 | match = '(a)-[r]->(b)'; 138 | 139 | } else if (direction === '<-') { 140 | match = '(a)<-[r]-(b)'; 141 | 142 | } else { 143 | // cant happen 144 | match = '(a)-[r]-(b)'; 145 | } 146 | 147 | const transaction = new Transaction() 148 | transaction.add(`MATCH ${match} WHERE ID(a) = ${node.getId()} RETURN a, b, ID(a), ID(b), LABELS(a), LABELS(b), r, ID(r), TYPE(r)`) 149 | 150 | return new Promise((resolve, reject) => { 151 | this.neo4j.commit(transaction).then((resultSets: Array) => { 152 | 153 | // "r" dataset (relationship nodes) should match number of "b" nodes...) 154 | // @TODO ...what happens with multiple relationships then? 155 | let dataset1 = resultSets[0].getDataset('a') 156 | let dataset2 = resultSets[0].getDataset('r') 157 | let dataset3 = resultSets[0].getDataset('b') 158 | 159 | let links = []; 160 | 161 | dataset2.forEach((rel: NodeInterface, i) => { 162 | const sourceNode = (direction === '->') ? node : dataset3[i]; 163 | const targetNode = (direction === '->') ? dataset3[i] : node; 164 | 165 | // direction always stays the same 166 | links.push(new Link({ source: sourceNode, target: targetNode, relationship: dataset2[i] })) 167 | 168 | }) 169 | 170 | resolve(links) 171 | 172 | }).catch(err => { 173 | reject(err) 174 | }) 175 | 176 | }) 177 | 178 | } 179 | 180 | 181 | updateRelationshipById(id: number, type: string|String, changedType: string|String = null, changedProperties: any, removedProperties: any) 182 | { 183 | const transaction01 = new Transaction() 184 | 185 | // make a cypher query to update relationship type 186 | if (null != changedType) { 187 | 188 | transaction01.add(`MATCH (a)-[r1:${type}]->(b) WHERE ID(r1) = ${id} CREATE (a)-[r:${changedType}]->(b) SET r = r1 189 | WITH r1, r DELETE r1 RETURN r, TYPE(r), ID(r)`) 190 | 191 | } else if (type != null) { 192 | transaction01.add(`MATCH (a)-[r:${type}]->(b) WHERE ID(r) = ${id} RETURN r, TYPE(r), ID(r)`) 193 | 194 | } else { 195 | transaction01.add(`MATCH (a)-[r]->(b) WHERE ID(r) = ${id} RETURN r, TYPE(r), ID(r)`) 196 | } 197 | 198 | return new Promise((resolve, reject) => { 199 | this.neo4j.commit(transaction01).then((resultSets: Array) => { 200 | 201 | let dataset: Node = resultSets[0].getDataset('r').first() 202 | resolve(dataset) 203 | 204 | }).catch(err => { 205 | reject(err) 206 | }) 207 | }).then((link: Node) => { 208 | const transaction = new Transaction() 209 | const builder = new CypherQuery() 210 | 211 | builder 212 | .matches(`MATCH (n)-[r:${link.getType()}]-(b)`, CypherQuery.RAW_QUERY_PART) 213 | .andWhere('r', 'ID(?)', link.getId()) 214 | .setProperties('r', changedProperties) 215 | .removeProperties('r', removedProperties) 216 | .returns('r, ID(r), TYPE(r)') 217 | 218 | let query = builder.getQuery() 219 | transaction.add(query) 220 | 221 | return this.neo4j.commit(transaction); 222 | }) 223 | } 224 | 225 | execute(queryString: string) 226 | { 227 | const transaction = new Transaction() 228 | transaction.add(queryString) 229 | 230 | return this.neo4j.commit(transaction) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/app/neo4j/neo4j.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Response } from '@angular/http'; 3 | import { Headers, Http } from '@angular/http'; 4 | import { Debug, SettingsService } from '../service'; 5 | import { PropertyAccess } from '../core'; 6 | import { ResultSet, Transaction } from './orm'; 7 | 8 | @Injectable() 9 | export class Neo4jService 10 | { 11 | private url: string; 12 | private headers: Headers; 13 | 14 | static DEBUG = true 15 | static NO_DEBUG = false 16 | 17 | constructor(private http: Http, private settings: SettingsService) 18 | { 19 | this.headers = new Headers({ 20 | 'Content-Type': 'application/json; charset=utf-8', 21 | 'Authorization': this.settings.get('client.authBasic') 22 | }) 23 | 24 | const endpoint = this.settings.get('client.apiEndpoint'); 25 | this.url = `${endpoint}/transaction/commit`; 26 | 27 | const debugEnabled = this.settings.get('debug'); 28 | Debug.debug(debugEnabled) 29 | } 30 | 31 | commit(trans: Transaction, rawRes: boolean = false): Promise 32 | { 33 | Debug.group('neo4j.service.commit').log(trans.getStatements(), 'Statements info', 'info') 34 | 35 | return new Promise((resolve, reject) => { 36 | this.http.post(this.url, { statements: trans.getStatements() }, { headers: this.headers }) 37 | .map(res => { 38 | if (res) { 39 | return res.json() 40 | } else { 41 | reject(res) 42 | } 43 | }) 44 | .toPromise() 45 | .then((response: any) => { 46 | 47 | if (rawRes === true) { 48 | resolve(response.results) 49 | return; 50 | } 51 | 52 | const result = this.handleResults(response) 53 | 54 | if (response.errors.length > 0) { 55 | Debug.log(response.errors, 'Transaction error', 'critical') 56 | reject(response.errors) 57 | } else { 58 | Debug.log(result, 'Transaction success', 'info') 59 | resolve(result) 60 | } 61 | 62 | }).catch(err => { 63 | Debug.log(err, 'Caught error', 'critical') 64 | reject(err) 65 | }) 66 | }) 67 | 68 | } 69 | 70 | handleResults(response: any) 71 | { 72 | let resultSets = []; 73 | 74 | for (let i in response.results) { 75 | resultSets.push(new ResultSet(response.results[i])) 76 | } 77 | 78 | for (let i in response.errors) { 79 | 80 | } 81 | 82 | return resultSets; 83 | } 84 | 85 | ping(): Promise 86 | { 87 | const trans = new Transaction() 88 | trans.add('MATCH (n) RETURN ID(n) LIMIT 1') 89 | 90 | return new Promise((resolve, reject) => { 91 | this.http.post(this.url, { statements: trans.getStatements() }, { headers: this.headers }) 92 | .map(res => { 93 | if (res) { 94 | return res.json() 95 | } else { 96 | reject(res) 97 | } 98 | }) 99 | .toPromise() 100 | .then((response: Response) => { 101 | resolve(true) 102 | }).catch(err => { 103 | reject(err) 104 | }) 105 | }) 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/app/neo4j/orm/cypher-query.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeInterface } from '../model'; 2 | import { escape, quote } from '../utils'; 3 | 4 | export class CypherQuery 5 | { 6 | queryString: string = null; 7 | 8 | queryParts: Array = []; 9 | queryCreateClauses: Array = []; 10 | queryWhereClauses: Array = []; 11 | querySetLabelsClause: string = null; 12 | queryRmLabelsClause: string = null; 13 | querySetPropsClauses: Array = []; 14 | queryReturnClauses: Array = []; 15 | queryRemovePropsClauses: Array = []; 16 | 17 | queryLimit: number = null; 18 | querySkip: number = null; 19 | 20 | queryNativeParts: Array = []; 21 | 22 | public static RAW_QUERY_PART = true; 23 | 24 | constructor() 25 | { 26 | this.clear() 27 | } 28 | 29 | rawCypher(rawQueryString: string) 30 | { 31 | this.queryString = rawQueryString; 32 | return this; 33 | } 34 | 35 | clear() 36 | { 37 | this.queryString = null; 38 | 39 | this.queryParts = []; 40 | this.queryCreateClauses = []; 41 | this.queryWhereClauses = []; 42 | this.querySetPropsClauses = []; 43 | this.queryReturnClauses = []; 44 | this.queryRemovePropsClauses = []; 45 | this.querySetLabelsClause = null; 46 | this.queryRmLabelsClause = null; 47 | 48 | this.queryLimit = null; 49 | this.querySkip = null; 50 | 51 | this.queryNativeParts = []; 52 | 53 | return this 54 | } 55 | 56 | addNative(queryString: string) 57 | { 58 | this.queryNativeParts.push(queryString) 59 | return this; 60 | } 61 | 62 | create(alias: string, labels: Array = null): CypherQuery 63 | { 64 | let clause: string; 65 | 66 | if (null !== labels) { 67 | let labelStr = labels.join(':') 68 | clause = `CREATE (${alias}:${labelStr})` 69 | } else { 70 | clause = `CREATE (${alias})` 71 | } 72 | 73 | this.queryCreateClauses = [clause] 74 | return this; 75 | } 76 | 77 | matches(alias: string, raw: boolean = false): CypherQuery 78 | { 79 | this.queryParts = (raw === CypherQuery.RAW_QUERY_PART) ? [alias] : [`MATCH (${alias})`]; 80 | 81 | return this 82 | } 83 | 84 | returns(clause: string) 85 | { 86 | this.queryReturnClauses.push(clause) 87 | return this; 88 | } 89 | 90 | skip(skip: number) 91 | { 92 | this.querySkip = skip 93 | return this; 94 | } 95 | 96 | limit(limit: number) 97 | { 98 | this.queryLimit = limit 99 | return this; 100 | } 101 | 102 | getQuery(): string 103 | { 104 | if (null !== this.queryString) { 105 | return this.queryString 106 | } 107 | 108 | this.addQueryParts(null, this.queryCreateClauses) 109 | this.addQueryParts('WHERE', this.queryWhereClauses) 110 | 111 | this.addQueryParts(null, this.querySetPropsClauses) 112 | this.addQueryParts(null, this.queryRemovePropsClauses) 113 | 114 | this.addQueryParts(null, [this.querySetLabelsClause]) 115 | this.addQueryParts(null, [this.queryRmLabelsClause]) 116 | 117 | this.addQueryParts(null, this.queryNativeParts) 118 | 119 | this.addQueryParts('RETURN', this.queryReturnClauses) 120 | 121 | if (null !== this.querySkip) { 122 | this.queryParts.push(`SKIP ${this.querySkip}`) 123 | } 124 | if (null !== this.queryLimit) { 125 | this.queryParts.push(`LIMIT ${this.queryLimit}`) 126 | } 127 | 128 | this.queryString = this.queryParts.join(' ') 129 | return this.queryString; 130 | } 131 | 132 | andWhere(alias: string, prop: string, value: string|number): CypherQuery 133 | { 134 | let aliasedProp: string; 135 | 136 | if (['ID', 'ID(?)'].indexOf(prop) > -1) { 137 | aliasedProp = `ID(${alias})`; 138 | } else { 139 | aliasedProp = `${alias}.${prop}`; 140 | } 141 | 142 | this.queryWhereClauses.push(`${aliasedProp} = ${quote(escape(value))}`) 143 | return this; 144 | } 145 | 146 | setProperties(alias: string, properties: any): CypherQuery 147 | { 148 | for (let prop in properties) { 149 | 150 | let value = quote(escape(properties[prop])) 151 | this.querySetPropsClauses.push(`SET ${alias}.${prop} = ${value}`) 152 | } 153 | 154 | return this; 155 | } 156 | 157 | removeProperties(alias: string, properties: any): CypherQuery 158 | { 159 | for (let prop in properties) { 160 | this.queryRemovePropsClauses.push(`REMOVE ${alias}.${prop}`) 161 | } 162 | return this; 163 | } 164 | 165 | setLabels(alias: string, labels: Array = null) 166 | { 167 | if (null === labels || labels.length === 0) { 168 | return this; 169 | } 170 | 171 | let labelsSuite = labels.join(':'); 172 | this.querySetLabelsClause = `SET ${alias}:${labelsSuite}`; 173 | return this; 174 | } 175 | 176 | removeLabels(alias: string, labels: Array = null) 177 | { 178 | if (null === labels || labels.length === 0) { 179 | return this; 180 | } 181 | 182 | let labelsSuite = labels.join(':'); 183 | this.queryRmLabelsClause = `REMOVE ${alias}:${labelsSuite}`; 184 | return this; 185 | } 186 | 187 | setParameter() 188 | { 189 | // @TODO 190 | } 191 | 192 | addQueryParts(prefix: string = null, clauses: Array) 193 | { 194 | if (clauses.length === 0) { 195 | return; 196 | } 197 | 198 | if (null !== prefix) { 199 | this.queryParts.push(prefix) 200 | } 201 | 202 | for (let i in clauses) { 203 | if (null === clauses[i]) { continue; } 204 | this.queryParts.push(`${clauses[i]}`) 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/app/neo4j/orm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './transaction'; 2 | export * from './result-set'; 3 | export * from './simple-query'; 4 | export * from './cypher-query'; 5 | -------------------------------------------------------------------------------- /src/app/neo4j/orm/result-set.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeInterface } from '../model'; 2 | 3 | // @TODO Warn if not LABEL nor ID or event TYPE (for relationships) are not in the query 4 | export class ResultSet 5 | { 6 | datasets: any = {} 7 | 8 | constructor(results: any) 9 | { 10 | this.datasets = this.parse(results) 11 | } 12 | 13 | parse(results: Array) 14 | { 15 | let datasets = {}; 16 | const data: any = results['data']; 17 | const columns: any = results['columns']; 18 | 19 | for (let index in columns) { 20 | const col = columns[index] 21 | const nfo = this.parseColumn(col) 22 | 23 | const alias = nfo.alias 24 | const prop = nfo.property 25 | 26 | if (typeof datasets[alias] === 'undefined') { 27 | datasets[alias] = [] 28 | 29 | Object.defineProperty(datasets[alias], 'alias', { 30 | configurable: false, 31 | enumerable: false, 32 | writable: false, 33 | value: alias 34 | }) 35 | 36 | Object.defineProperty(datasets[alias], 'first', { 37 | configurable: false, 38 | enumerable: false, 39 | writable: false, 40 | value: function () { 41 | return this.length > 0 ? this[0] : null 42 | } 43 | }) 44 | } 45 | 46 | for (let i in data) { 47 | 48 | let row = data[i].row; 49 | const meta = data[i].meta; 50 | 51 | // meta data from the neo4j rest api tells us if 52 | // current row a node or a relationship 53 | 54 | if (typeof datasets[alias][i] === 'undefined') { 55 | datasets[alias][i] = new Node() 56 | } 57 | 58 | // iterate through row properties 59 | for (let k in row) { 60 | 61 | // only add value if index is current column index 62 | // otherwhise all nodes will have all other node entities properties 63 | if (k === index) { 64 | 65 | let valOrProps = row[k] 66 | const metadata = meta[k] 67 | datasets[alias][i].set('META', metadata, false) 68 | 69 | if (prop === null) { 70 | 71 | // use false to prevent overriding exisint properties 72 | datasets[alias][i].hydrate(valOrProps, false) 73 | 74 | } else if (prop === 'ID') { 75 | 76 | // use false parameter to make this property non-enumerable (hidden) 77 | datasets[alias][i].set('ID', valOrProps, false) 78 | 79 | } else if (prop === 'LABELS') { 80 | 81 | // use false parameter to make this property non-enumerable (hidden) 82 | datasets[alias][i].set('LABELS', valOrProps, false) 83 | 84 | } else if (prop === 'TYPE') { 85 | 86 | datasets[alias][i].set('TYPE', valOrProps, false) 87 | 88 | } else { 89 | 90 | // node.hydrate(valOrProps, false) 91 | } 92 | 93 | } 94 | 95 | } 96 | 97 | } 98 | } 99 | 100 | this.datasets = datasets; 101 | return datasets; 102 | } 103 | 104 | addProperties(row: any, propsAndValues: any) 105 | { 106 | for (let prop in propsAndValues) { 107 | if (propsAndValues.hasOwnProperty(prop)) { row[prop] = propsAndValues[prop] } 108 | } 109 | return row; 110 | } 111 | 112 | /** 113 | * Turns column names into: 114 | * n => { entity: "n", property: null } 115 | * n.name => { alias: "n", property: "name" } 116 | * ID(n) => { alias: "n", property: "ID" } 117 | * @return object { alias: "", property } 118 | */ 119 | parseColumn(col: string) 120 | { 121 | const partsA = col.split('.') 122 | const partsB = col.split('(') 123 | 124 | if (partsA.length === 2) { 125 | return { alias: partsA[0], property: partsA[1] } 126 | } else if (partsB.length === 2) { 127 | return { alias: partsB[1].replace(')', ''), property: partsB[0] } 128 | } else { 129 | return { alias: col, property: null } 130 | } 131 | } 132 | 133 | getDatasets() 134 | { 135 | return this.datasets; 136 | } 137 | 138 | getDataset(col: string) 139 | { 140 | return (typeof this.datasets[col] === 'undefined') ? [] : this.datasets[col]; 141 | } 142 | 143 | size() 144 | { 145 | return Object.keys(this.datasets).length; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/app/neo4j/orm/simple-query.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeInterface } from '../model'; 2 | import { escape, quote } from '../utils'; 3 | 4 | export class SimpleQuery 5 | { 6 | queryString: string; 7 | 8 | constructor(string: string) 9 | { 10 | // simple query expression looks like 11 | // ":Person id=3 name=3" 12 | let relationshipLevel = 0; 13 | let labels = ''; 14 | let where: string = null; 15 | let properties = []; 16 | let limit: number = null; 17 | let skip: number = null; 18 | 19 | // match labels filter like ":Label1:Label2:..." 20 | const labelsRegex = new RegExp(/(:[a-zA-Z0-9_:]+)\s{0,}/); 21 | const propertiesMatch = new RegExp(/((?:[a-z0-9]+)=(("[\w\s]+"){1}|([\S]+)))/gi); 22 | const limitSkipMatch = new RegExp(/([0-9,\s]+)$/i); 23 | const relLevelMatch = new RegExp(/\s\+([0-9]{0,})$/g) 24 | 25 | const labelsMatch = string.match(labelsRegex); 26 | labels = (labelsMatch) ? labelsMatch[1] : ''; 27 | 28 | const propsMatch = string.match(propertiesMatch); 29 | if (propsMatch) { 30 | const whereClauses = this.andWhereProps(propsMatch) 31 | if (whereClauses.length > 0) { 32 | where = `${whereClauses.join(' AND ')}`; 33 | } 34 | } 35 | 36 | const skipLimitMatch = string.match(limitSkipMatch); 37 | if (skipLimitMatch) { 38 | const parts = skipLimitMatch[0].split(',').map(v => { return v.trim() }); 39 | 40 | if (parts.length === 2) { 41 | limit = parseInt(parts[0]) 42 | skip = parseInt(parts[1]) 43 | } else if (parts.length === 1) { 44 | limit = parseInt(parts[0]) 45 | skip = null 46 | } 47 | } 48 | 49 | // build cypher query string 50 | let queryString = `MATCH (a${labels})`; 51 | if (null !== where) { 52 | queryString += ` WHERE ${where}`; 53 | } 54 | 55 | 56 | // test if one level of relationship is detected with a "+" at the end 57 | const plusMatch = string.match(relLevelMatch) 58 | if (plusMatch) { 59 | relationshipLevel = parseInt(plusMatch[0].trim().replace('+', '')) 60 | } 61 | 62 | // @TODO only one level of relationships is supported 63 | if (relationshipLevel > 0) { 64 | queryString += `-[r]->(b) RETURN a, b, r, ID(a), ID(b), TYPE(r), LABELS(a), LABELS(b)`; 65 | } else { 66 | queryString += ` RETURN a, ID(a), LABELS(a)`; 67 | } 68 | 69 | if (relationshipLevel > 1) { 70 | console.warn(`simple-query.ts Only one level of relationship is supported in a simple query expression`) 71 | } 72 | 73 | if (null !== skip) { 74 | queryString += ` SKIP ${skip}`; 75 | } 76 | if (null !== limit) { 77 | queryString += ` LIMIT ${limit}`; 78 | } 79 | 80 | this.queryString = queryString; 81 | } 82 | 83 | /** 84 | * Turn a string like "id=3, name=ben" into 85 | * an array of ["id=3", "name='Ben'"] 86 | */ 87 | private andWhereProps(matches: any): Array 88 | { 89 | let whereClauses = []; 90 | 91 | for (let i=0; i < matches.length; i++) { 92 | const parts = matches[i].split('=').map(v => { return v.trim() } ); 93 | let value = parts[1].replace(/"/g, ''); 94 | const prop = parts[0].trim() // { property: parts[0].trim(), value: escape(value) } 95 | whereClauses.push(`a.${prop}=${quote(escape(value))}`) 96 | } 97 | 98 | return whereClauses; 99 | } 100 | 101 | getQuery() 102 | { 103 | return this.queryString; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/app/neo4j/orm/transaction.ts: -------------------------------------------------------------------------------- 1 | import { Node, NodeInterface } from '../model'; 2 | 3 | type Statement = { 4 | statement: string; 5 | parameters: any; 6 | } 7 | 8 | export class Transaction 9 | { 10 | statements: Array = []; 11 | 12 | constructor(statements: Array = []) 13 | { 14 | this.statements = statements; 15 | } 16 | 17 | add(query: string, params: any = {}) 18 | { 19 | const statement: Statement = { statement: query, parameters: params } 20 | this.statements.push(statement) 21 | } 22 | 23 | getStatements() 24 | { 25 | return this.statements 26 | } 27 | 28 | matchByIdAndSetStatement(node: NodeInterface) 29 | { 30 | let cypher: Array = [`MATCH (n) WHERE ID(n)`]; 31 | 32 | for (let prop in node.properties()) { 33 | 34 | let value = this.escape(node.get(prop)); 35 | 36 | cypher.push(`SET n.{prop} = '${value}'`) 37 | } 38 | 39 | return cypher.join(' '); 40 | } 41 | 42 | escape(value: any) 43 | { 44 | if (typeof(value) === 'string') { 45 | return value.replace("'", "\'"); 46 | } else { 47 | console.warn(`transaction.ts: unsupported escape value ${typeof(value)}`) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/neo4j/utils/color.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from '../model'; 2 | 3 | export function color(node: NodeInterface, colorOptions: Array) { 4 | const label = node.getFirstLabel() 5 | 6 | if (typeof colorOptions[label] !== 'undefined') { 7 | return colorOptions[label] 8 | } 9 | 10 | return '#1E1F24' 11 | } 12 | -------------------------------------------------------------------------------- /src/app/neo4j/utils/escape.ts: -------------------------------------------------------------------------------- 1 | // determine if all values of array1 are in array2 and vice versa, whatever the order though 2 | export function escape(value: any) { 3 | if (typeof value === 'undefined' || value === '' || value == null) { 4 | return ''; 5 | } 6 | else if (typeof value === 'string') { 7 | value = value.replace(/'/g, "\\\'") 8 | } 9 | else if (typeof value === 'number') { 10 | value = value; 11 | 12 | } else { 13 | console.warn(`cypher-query.ts: unsupported escape value ${typeof(value)}`) 14 | } 15 | 16 | return value; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/neo4j/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './name'; 2 | export * from './color'; 3 | export * from './truncate'; 4 | export * from './escape'; 5 | export * from './quote'; 6 | -------------------------------------------------------------------------------- /src/app/neo4j/utils/name.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from '../model'; 2 | 3 | export function name(node: NodeInterface, nameOptions: Array) { 4 | 5 | for (let prop of nameOptions) { 6 | if ( 7 | typeof(node['props'][prop]) !== 'undefined' && node['props'][prop] != null 8 | ) { 9 | return node['props'][prop] 10 | } 11 | } 12 | 13 | for (let prop of nameOptions) { 14 | if ( 15 | typeof(node[prop]) !== 'undefined' && node[prop] != null 16 | ) { 17 | return node[prop] 18 | } 19 | } 20 | 21 | return '?'; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/neo4j/utils/quote.ts: -------------------------------------------------------------------------------- 1 | // determine if all values of array1 are in array2 and vice versa, whatever the order though 2 | export function quote(value: any) { 3 | if (typeof(value) === 'number') { 4 | return value; 5 | } else { 6 | if (!isNaN(value)) { 7 | return parseInt(value); 8 | } else { 9 | return `'${value}'`; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/neo4j/utils/truncate.ts: -------------------------------------------------------------------------------- 1 | import { NodeInterface } from '../model'; 2 | 3 | export function truncate(value: string|number, len: number = 10) { 4 | if (typeof(value) === 'number') { return value } 5 | 6 | let trunc = value.substring(0, len).trim() 7 | return (value.length > len) ? `${trunc}...` : value 8 | } 9 | -------------------------------------------------------------------------------- /src/app/page/debug/debug.page.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | Clear logs 5 |
6 | 7 |
8 | 9 |
10 | Debug group  11 | 12 |
13 | 14 |
15 | 16 |
17 |  »  18 | 19 | 20 |
21 | 22 |
23 |

24 |             
25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 | -------------------------------------------------------------------------------- /src/app/page/debug/debug.page.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | @import "../../../theme/shared/_scrollbars"; 4 | 5 | :host { 6 | // @include hostSupportsFullPage() 7 | @include hostSupportsPageScroll() 8 | } 9 | 10 | .debug-group { 11 | display: flex; 12 | flex-direction: column; 13 | margin-bottom: 16px; 14 | padding: 16px; 15 | overflow: hidden; 16 | background-color: colors(coal); 17 | 18 | .title { 19 | color: colors(secondary); 20 | margin: 0px; 21 | padding: 0px; 22 | font-size: $fontSize; 23 | } 24 | 25 | .group { 26 | display: flex; 27 | flex-direction: column; 28 | // padding: 16px; 29 | margin-bottom: 8px; 30 | // background-color: colors(charcoal); 31 | 32 | &:last-of-type { 33 | margin-bottom: 0px; 34 | } 35 | 36 | .infos { 37 | cursor: pointer; 38 | color: colors(fullwhite); 39 | display: flex; 40 | flex-direction: row; 41 | padding: 8px; 42 | font-weight: bold; 43 | margin-bottom: 8px; 44 | @include border-radius(2px); 45 | 46 | &.info { 47 | border: 1px solid #49a363; 48 | background-color: #49a363; 49 | // &:hover { border: 1px solid colors(white) } 50 | } 51 | &.debug { 52 | border: 1px solid #21a3c3; 53 | background-color: #21a3c3; 54 | } 55 | &.warning { 56 | border: 1px solid #efb83f; 57 | background-color: #efb83f; 58 | } 59 | &.critical { 60 | border: 1px solid #e03313; 61 | background-color: #e03313; 62 | } 63 | 64 | .level { 65 | margin-left: auto; 66 | text-transform: uppercase; 67 | } 68 | } 69 | 70 | .trace { 71 | max-height: 260px; 72 | overflow-y: auto; 73 | padding-left: 8px; 74 | @include scrollbars(.5em, #6f7f8f, #222222); 75 | color: colors(gray); 76 | 77 | pre { 78 | color: colors(white); 79 | font-size: $fontSize - 2px; 80 | margin-top: 8px; 81 | font-family: monospace, "Monospace sans", sans-serif; 82 | } 83 | 84 | &.hidden { 85 | > pre { 86 | display: none; 87 | } 88 | } 89 | &.expanded { 90 | // max-height: none; 91 | // overflow-y: hidden; 92 | > pre { 93 | display: block; 94 | } 95 | } 96 | } 97 | 98 | } 99 | 100 | 101 | } 102 | 103 | .actions { 104 | margin-bottom: 16px; 105 | } 106 | -------------------------------------------------------------------------------- /src/app/page/debug/debug.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { OnInit, AfterViewInit } from '@angular/core'; 3 | import { Router } from '@angular/router'; 4 | import { Debug } from '../../service'; 5 | import { group } from '../../core/array'; 6 | 7 | @Component({ 8 | selector: 'debug-page', 9 | templateUrl: './debug.page.html', 10 | styleUrls: ['./debug.page.scss'] 11 | }) 12 | export class DebugPageComponent implements OnInit, AfterViewInit 13 | { 14 | groups: Array = [] 15 | 16 | constructor(private router: Router) 17 | { 18 | 19 | } 20 | 21 | ngOnInit() 22 | { 23 | const messages = Debug.getMessages() 24 | this.groups = group('timestamp', messages) 25 | } 26 | 27 | ngAfterViewInit() 28 | { 29 | 30 | } 31 | 32 | toggleTrace(e: any, i: number, j: number) 33 | { 34 | e.preventDefault() 35 | 36 | let status = (typeof this.groups[i][j]['hidden'] === 'undefined') ? true : this.groups[i][j]['hidden'] 37 | this.groups[i][j]['hidden'] = !status 38 | } 39 | 40 | clear(e: any) 41 | { 42 | e.preventDefault() 43 | this.groups = [] 44 | Debug.clear() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/page/home/home.page.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | Node details 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 |

15 | Can't edit nodes in create mode. 16 |

17 |

18 | Click on a node/link to edit its details. 19 |

20 | 21 |
22 | Explore labels   23 | 24 | 25 | 26 |
    27 |
  • 28 | 29 |
  • 30 |
31 |
32 | 33 | 37 | 38 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 | 63 |
64 |
65 | 66 | 67 |
68 |
69 | Create ({{ settingsInfo.hotkeys.toggleCreateMode }}) 70 | 71 |
72 | 73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 |
85 |
86 | 87 |
88 | -------------------------------------------------------------------------------- /src/app/page/home/home.page.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | 4 | :host { 5 | @include hostSupportsFullPage() 6 | } 7 | 8 | .column-left { 9 | width: 290px; 10 | margin-right: 24px; 11 | } 12 | 13 | .column-right { 14 | width: 260px; 15 | margin-left: 24px; 16 | } 17 | 18 | a.explore-toggler { 19 | display: flex; 20 | margin: 16px 0 8px; 21 | 22 | span.caret { 23 | // position: relative; 24 | // top: -1px; 25 | font-size: $fontSize + 4px; 26 | line-height: $fontSize; 27 | vertical-align: middle; 28 | } 29 | } 30 | 31 | ul.labels-list { 32 | display: flex; 33 | flex: 1; 34 | flex-flow: row wrap; 35 | flex-direction: row; 36 | align-items: center; 37 | 38 | list-style-type: none; 39 | padding: 0px; 40 | margin: 0px; 41 | margin-top: 16px; 42 | 43 | li { 44 | display: flex; 45 | flex-direction: column; 46 | margin-bottom: 4px; 47 | margin-right: 4px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/page/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { OnInit, AfterViewInit } from '@angular/core'; 3 | import { GraphComponent } from '../../component'; 4 | import { SettingsService } from '../../service'; 5 | import { Neo4jRepository } from '../../neo4j'; 6 | import { ResultSet, Transaction } from '../../neo4j/orm'; 7 | import { Node, NodeInterface } from '../../neo4j/model'; 8 | import { Link, LinkInterface } from '../../neo4j/model'; 9 | import { LabelInterface } from '../../neo4j/model'; 10 | import { unique, crosscut } from '../../core/array'; 11 | import { LinkUpdatedEvent } from '../../component/link-edit/link-edit.component'; 12 | 13 | @Component({ 14 | selector: 'home-page', 15 | templateUrl: './home.page.html', 16 | styleUrls: ['./home.page.scss'], 17 | host: { '(window:keydown)': 'onHotkeyPressed($event)' }, 18 | }) 19 | export class HomePageComponent implements OnInit, AfterViewInit 20 | { 21 | selectedNode: Node = null; 22 | selectedLink: Node = null; 23 | 24 | createModeEnabled: boolean = false 25 | createModeDefaults: any = { 26 | label: 'Test', 27 | relationshipType: 'MY_REL' 28 | } 29 | @ViewChild(GraphComponent) graph: GraphComponent; 30 | 31 | saveErrorText: string = null; 32 | saveSuccessText: string = null; 33 | searchLoading: boolean = false; 34 | explorerToggled: boolean = false; 35 | 36 | labels: Array = []; 37 | settingsInfo: any; 38 | 39 | // @TODO general: use a setting value to distinguish nodes by variable property (defaults to ID) 40 | // and use distinct INSIDE graph.component 41 | constructor(private repo: Neo4jRepository, private settings: SettingsService) 42 | { 43 | this.settingsInfo = this.settings.all() 44 | } 45 | 46 | ngOnInit() 47 | { 48 | this.repo.findAllLabels().then((labels: Array) => { 49 | this.labels = labels; 50 | }).catch(err => { 51 | this.toastError(err) 52 | }) 53 | } 54 | 55 | ngAfterViewInit() 56 | { 57 | this.graph.start() 58 | } 59 | 60 | ngOnChanges() 61 | { 62 | 63 | } 64 | 65 | onHotkeyPressed(e: any) 66 | { 67 | // handle IE, and then Netscape/Firefox/Opera 68 | const keynum = (window.event) ? e.keyCode : e.which 69 | 70 | const letter = String.fromCharCode(keynum); 71 | const ctrlKey = (true === e.ctrlKey) ? 'ctrl' : false 72 | const inputCombination = [letter, ctrlKey] 73 | 74 | const createModeCombination = this.settings.get('hotkeys.toggleCreateMode') 75 | 76 | if (crosscut(inputCombination, createModeCombination)) { 77 | this.onCreateModeChanged(!this.createModeEnabled) 78 | } 79 | } 80 | 81 | exploreToggle(e: any) 82 | { 83 | e.preventDefault() 84 | this.explorerToggled = !this.explorerToggled; 85 | } 86 | 87 | filterByLabel(e: LabelInterface) 88 | { 89 | let label = e; 90 | this.graph.clear(); 91 | this.onSearch({ mode: 'normal', queryString: `MATCH (a:${label.name}) RETURN a, LABELS(a), ID(a)` }) 92 | } 93 | 94 | onSearch(e: any) 95 | { 96 | this.searchLoading = true; 97 | 98 | // const mode = e.mode; 99 | // const queryString = e.queryString; 100 | this.repo.execute(e.queryString).then((resultSets: Array) => { 101 | 102 | this.searchLoading = false; 103 | 104 | let links = []; 105 | let dataset1 = resultSets[0].getDataset('a') 106 | let dataset2 = resultSets[0].getDataset('r') 107 | let dataset3 = resultSets[0].getDataset('b') 108 | 109 | dataset2.forEach((rel: NodeInterface, i) => { 110 | links.push({ source: dataset3[i], target: dataset1[i], relationship: dataset2[i] }); 111 | }) 112 | 113 | this.graph.addNodes(dataset1) 114 | this.graph.addNodes(dataset3) 115 | this.graph.addLinks(links) 116 | this.graph.update() 117 | 118 | }).catch(err => { 119 | console.log(err) 120 | this.searchLoading = false; 121 | this.toastError(err) 122 | }) 123 | } 124 | 125 | onNodeCreated(node: NodeInterface) 126 | { 127 | // a new node object is createee but not yet appended to the graph 128 | // and not saved remotely. This is because we are using real neo4j IDs 129 | // to handle proper UI and be safe regarding integrity so you'll 130 | // need to make the cypher transaction and append node to graph only 131 | // if it was successfull 132 | const savedNode = node; 133 | 134 | // @TODO Set default label from create mode windows 135 | node.addLabel(this.createModeDefaults.label) 136 | 137 | this.repo.persistNode(node).then((node: NodeInterface) => { 138 | node.setFixed(true).setCoords(savedNode.getCoords()) 139 | this.graph.addNode(node) 140 | }) 141 | } 142 | 143 | onNodeSelected(node: NodeInterface) 144 | { 145 | this.selectedLink = null 146 | this.selectedNode = node 147 | } 148 | 149 | onLinkSelected(g: any) 150 | { 151 | this.selectedNode = null; 152 | 153 | if (null !== g) { 154 | this.selectedLink = g.relationship; 155 | } 156 | } 157 | 158 | onNodeDoubleClicked(node: NodeInterface) 159 | { 160 | this.findRelationships(node) 161 | } 162 | 163 | onNodeAdded(node: NodeInterface) 164 | { 165 | // very unlikely you will use this event but who knows... 166 | // this is triggered when a node is added to the graph (not created per say) 167 | } 168 | 169 | onNodeEdited(node: NodeInterface) 170 | { 171 | if (null === node) { 172 | //then an error occured 173 | this.saveSuccessText = null; 174 | this.saveErrorText = 'An error occured'; 175 | } else { 176 | this.graph.updateNode(node) 177 | this.saveSuccessText = null; 178 | this.saveSuccessText = 'Node saved'; 179 | } 180 | } 181 | 182 | /** 183 | * @param e { link: NodeInterface, previousLink: NodeInterface } 184 | */ 185 | onLinkEdited(e: LinkUpdatedEvent) 186 | { 187 | if (null === e.currentValue) { 188 | //then an error occured 189 | this.toastError('An error occured') 190 | } else { 191 | this.graph.updateLink(e.currentValue, e.previousValue) 192 | this.toastSuccess('Relationship saved') 193 | } 194 | } 195 | 196 | onLinkCreated(e: any) 197 | { 198 | this.repo.createRelationship(e.source, e.target, '->', this.createModeDefaults.relationshipType).then((link: Link) => { 199 | 200 | this.graph.addLink(link) 201 | this.toastSuccess('Relationship saved') 202 | 203 | }).catch(err => { 204 | this.toastError(err) 205 | }) 206 | } 207 | 208 | onCreateModeChanged(e: boolean) 209 | { 210 | // avoid expression (of createModeEnabled) before it was checked (just Angular2 classic view adjustement here) 211 | this.selectedNode = null; 212 | // then safely reset create mode variable 213 | this.createModeEnabled = e; 214 | } 215 | 216 | private findRelationships(sourceNode: NodeInterface) 217 | { 218 | // query relationships in both ways 219 | this.repo.findRelationships(sourceNode, '->').then((links: Array) => { 220 | 221 | links.forEach((link: LinkInterface, i) => { 222 | this.graph.addNode(link.target) 223 | this.graph.addLink(link) 224 | }) 225 | 226 | }).catch(err => { 227 | this.toastError(err) 228 | }) 229 | 230 | this.repo.findRelationships(sourceNode, '<-').then((links: Array) => { 231 | 232 | links.forEach((link: LinkInterface, i) => { 233 | this.graph.addNode(link.source) 234 | this.graph.addLink(link) 235 | }) 236 | 237 | }).catch(err => { 238 | this.toastError(err) 239 | }) 240 | } 241 | 242 | private toastError(msg: string) 243 | { 244 | this.saveSuccessText = null; 245 | this.saveErrorText = msg; 246 | } 247 | 248 | private toastSuccess(msg: string) 249 | { 250 | this.saveSuccessText = msg; 251 | this.saveErrorText = null; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/app/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home/home.page'; 2 | export * from './debug/debug.page'; 3 | export * from './settings/settings.page'; 4 | -------------------------------------------------------------------------------- /src/app/page/settings/settings.page.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | settings.js 8 | 9 | 10 | Reset 11 |
12 | 13 |

14 | 
15 |             
16 | Save settings 17 | Cancel 18 |
19 | 20 |
21 | 22 |
23 | sqd 24 |
25 | 26 |
27 | 28 |
29 | -------------------------------------------------------------------------------- /src/app/page/settings/settings.page.scss: -------------------------------------------------------------------------------- 1 | @import "../../../theme/shared/_mixins"; 2 | @import "../../../theme/shared/_variables"; 3 | @import "../../../theme/shared/_scrollbars"; 4 | 5 | :host { 6 | // @include hostSupportsPageScroll() 7 | // margin-bottom: 24px; 8 | @include hostSupportsFullPage() 9 | } 10 | 11 | .editor, .actions { 12 | 13 | } 14 | 15 | pre.editor { 16 | width: 100%; 17 | color: colors(white); 18 | overflow-y: auto; 19 | height: 480px; 20 | min-width: 500px; 21 | background-color: colors(charcoal); 22 | padding: 16px; 23 | margin: 0px; 24 | font-size: $fontSize - 2px; 25 | font-family: Monospace, "Monospace sans", sans-serif; 26 | 27 | @include box-sizing(); 28 | @include scrollbars(.5em, slategray); 29 | } 30 | 31 | .actions { 32 | app-button { 33 | // margin-right: 16px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/page/settings/settings.page.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, ElementRef } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { OnInit, AfterViewInit, OnChanges } from '@angular/core'; 4 | import { SimpleChanges } from '@angular/core'; 5 | import { SettingsService } from '../../service'; 6 | 7 | @Component({ 8 | selector: 'settings-page', 9 | templateUrl: './settings.page.html', 10 | styleUrls: ['./settings.page.scss'] 11 | }) 12 | export class SettingsPageComponent implements OnInit, AfterViewInit, OnChanges 13 | { 14 | settingsString: string; 15 | errorText: string = null; 16 | successText: string = null; 17 | 18 | @ViewChild('settingsEditor') settingsEditor: ElementRef; 19 | 20 | constructor(private elementRef: ElementRef, private settings: SettingsService, private router: Router) 21 | { 22 | const data = settings.get() 23 | this.settingsString = JSON.stringify(data, null, 2) 24 | } 25 | 26 | ngOnInit() 27 | { 28 | 29 | } 30 | 31 | ngAfterViewInit() 32 | { 33 | 34 | } 35 | 36 | ngOnChanges(changes: SimpleChanges) 37 | { 38 | 39 | } 40 | 41 | save(e?: any) 42 | { 43 | if (e) { e.preventDefault() } 44 | 45 | const data = this.settingsEditor.nativeElement.innerText 46 | 47 | try { 48 | this.errorText = null; 49 | this.successText = `Settings saved`; 50 | 51 | const json = JSON.parse(data) 52 | 53 | this.settings.set(json, true) 54 | this.settingsString = JSON.stringify(json, null, 2) 55 | 56 | } catch (e) { 57 | this.errorText = 'Invalid JSON'; 58 | this.successText = null; 59 | } 60 | } 61 | 62 | reset(e?: any) 63 | { 64 | if (e) { e.preventDefault() } 65 | this.errorText = null; 66 | this.successText = `Settings reset to defaults`; 67 | this.settings.reset() 68 | } 69 | 70 | cancel(e?: any) 71 | { 72 | if (e) { e.preventDefault() } 73 | this.router.navigateByUrl('/') 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/service/broadcast.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject } from 'rxjs/Subject'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import 'rxjs/add/operator/filter'; 5 | import 'rxjs/add/operator/map'; 6 | 7 | interface BroadcastEvent { 8 | key: any; 9 | data?: any; 10 | } 11 | 12 | export class BroadcastServiceModule 13 | { 14 | private _eventBus: Subject; 15 | 16 | constructor() { 17 | this._eventBus = new Subject(); 18 | } 19 | 20 | emit(key: any, data?: any) { 21 | this._eventBus.next({key, data}); 22 | } 23 | 24 | on(key: any): Observable { 25 | return this._eventBus.asObservable() 26 | .filter(event => event.key === key) 27 | .map(event => event.data) 28 | } 29 | } 30 | 31 | export let BroadcastService = new BroadcastServiceModule(); 32 | -------------------------------------------------------------------------------- /src/app/service/debug.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import { LocalStorage } from './local.storage'; 3 | import { uuid } from '../core'; 4 | import { orderBy } from '../core/array'; 5 | 6 | class DebugSingleton 7 | { 8 | private uuid: string; 9 | private timestamp: number; 10 | private groupName: string; 11 | 12 | private messages: Array = []; 13 | private storageKey: string = 'neo4j_debug_log'; 14 | 15 | private debugEnabled: boolean = true; 16 | 17 | constructor() 18 | { 19 | this.uuid = uuid() 20 | const localMessages = LocalStorage.get(this.storageKey, null) 21 | 22 | if (null !== localMessages) { 23 | this.messages = localMessages; 24 | } 25 | 26 | this.timestamp = moment().valueOf(); 27 | } 28 | 29 | debug(debugEnabled: boolean) 30 | { 31 | this.debugEnabled = debugEnabled; 32 | return this; 33 | } 34 | 35 | group(name: string = 'Debug group') 36 | { 37 | this.groupName = name; 38 | this.uuid = uuid(); 39 | this.timestamp = moment().valueOf(); 40 | return this; 41 | } 42 | 43 | log(msg: string|any, category?: string, level: string|'info'|'debug'|'error'|'warning' = 'debug') 44 | { 45 | if (!this.debugEnabled) { 46 | return this; 47 | } 48 | 49 | let logEntry: any = {} 50 | 51 | if (typeof(msg) === 'object') { 52 | logEntry = { 53 | level: level, 54 | timestamp: this.timestamp, 55 | trace: msg, 56 | category: category, 57 | } 58 | } else { 59 | logEntry = { 60 | level: level, 61 | timestamp: this.timestamp, 62 | trace: msg, 63 | category: category, 64 | } 65 | } 66 | 67 | this.messages.push(JSON.stringify(logEntry)) 68 | LocalStorage.set(this.storageKey, this.messages) 69 | return this; 70 | } 71 | 72 | getMessages(format: string = 'json') 73 | { 74 | const messages = this.messages.map(msg => { return JSON.parse(msg) }) 75 | return orderBy('timestamp', messages, 'DESC') 76 | } 77 | 78 | countErrorsByLevel(level: string) 79 | { 80 | let count: number = 0; 81 | const messages = this.messages.map(msg => { return JSON.parse(msg) }) 82 | 83 | for (let i in messages) { 84 | if (messages[i].level === level) { 85 | count++; 86 | } 87 | } 88 | return count; 89 | } 90 | 91 | clear() 92 | { 93 | this.messages = [] 94 | LocalStorage.set(this.storageKey, null) 95 | } 96 | } 97 | 98 | export let Debug = new DebugSingleton() 99 | -------------------------------------------------------------------------------- /src/app/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug'; 2 | export * from './local.storage'; 3 | export * from './broadcast.service'; 4 | export * from './settings.service'; 5 | -------------------------------------------------------------------------------- /src/app/service/local.storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Local storage service. 3 | */ 4 | class LocalStorageSingleton 5 | { 6 | 7 | get(key: string, defaultValue?: any) { 8 | let value = localStorage.getItem(key) 9 | 10 | if (typeof value === 'undefined' || value == null || value == 'null') { 11 | value = (defaultValue) ? defaultValue : null 12 | } 13 | 14 | return this.output(value); 15 | } 16 | 17 | set(key: string, input: any) { 18 | localStorage.setItem(key, this.transform(input)); 19 | return this; 20 | } 21 | 22 | remove(key: string) { 23 | localStorage.removeItem(key); 24 | return this; 25 | } 26 | 27 | output(data: any) { 28 | let value: any; 29 | if (data === null) { return null } 30 | 31 | try { 32 | value = JSON.parse(data); 33 | } catch (e) { 34 | value = data; 35 | } 36 | 37 | return value; 38 | } 39 | 40 | transform(data: any) { 41 | const type = typeof(data); 42 | let value: any; 43 | 44 | switch (type) { 45 | case 'string': 46 | value = data; 47 | break; 48 | case 'number': 49 | value = data; 50 | break; 51 | case 'object': 52 | value = JSON.stringify(data); 53 | break; 54 | default: 55 | value = data; 56 | break; 57 | } 58 | 59 | return value; 60 | } 61 | } 62 | 63 | export let LocalStorage = new LocalStorageSingleton(); 64 | -------------------------------------------------------------------------------- /src/app/service/settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional } from '@angular/core'; 2 | import { Headers, Http } from '@angular/http'; 3 | import { PropertyAccess } from '../core'; 4 | import { LocalStorage } from './local.storage'; 5 | 6 | @Injectable() 7 | export class SettingsService 8 | { 9 | settings: any = {} 10 | inited: boolean = false 11 | 12 | private storageKey: string = 'neo4j_settings' 13 | 14 | constructor(@Optional() private http: Http) 15 | { 16 | // read data from local storage first 17 | const localSettings = LocalStorage.get(this.storageKey) 18 | 19 | if (localSettings !== null) { 20 | this.set(localSettings) 21 | console.warn(`settings.service.ts Neo4j settings loaded from local storage`) 22 | } 23 | } 24 | 25 | areSet() 26 | { 27 | return this.inited 28 | } 29 | 30 | reset() 31 | { 32 | this.settings = {} 33 | LocalStorage.remove(this.storageKey) 34 | return this 35 | } 36 | 37 | set(settings: any, force: boolean = false) 38 | { 39 | if (true === this.areSet() && force !== true) { 40 | return this 41 | } 42 | 43 | this.settings = settings; 44 | LocalStorage.set(this.storageKey, settings) 45 | 46 | this.inited = true 47 | return this 48 | } 49 | 50 | get(access: string = null) 51 | { 52 | const accessor = new PropertyAccess() 53 | return (null === access) ? this.settings : accessor.getValue(this.settings, access) 54 | } 55 | 56 | all() 57 | { 58 | return this.get() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/flaticon/backup.txt: -------------------------------------------------------------------------------- 1 | eyIxIjp7IklEIjoxLCJuYW1lIjoiTXkgaWNvbnMgY29sbGVjdGlvbiIsImJvb2ttYXJrX2lkIjoibzJobDkxbGM0ZjkwMDAwMCIsImNyZWF0ZWQiOm51bGwsInVwZGF0ZWQiOjE1MTE3ODc2OTQsImFjdGl2ZSI6MSwic291cmNlIjoibG9jYWwiLCJvcmRlciI6MCwiY29sb3IiOiIwMDAwMDAiLCJzdGF0dXMiOjF9LCJvMmhsOTFsYzRmOTAwMDAwIjpbeyJpZCI6MTI3OTM2LCJ0ZWFtIjowLCJuYW1lIjoiY2FuY2VsIiwiY29sb3IiOiIjMDAwMDAwIiwicHJlbWl1bSI6MCwic29ydCI6MX0seyJpZCI6MTQ4NzY2LCJ0ZWFtIjowLCJuYW1lIjoiZXJyb3IiLCJjb2xvciI6IiNYWFhYWFgiLCJwcmVtaXVtIjowLCJzb3J0IjoyfSx7ImlkIjoxMzE5MTcsInRlYW0iOjAsIm5hbWUiOiJpbmZvIiwiY29sb3IiOiIjMDAwMDAwIiwicHJlbWl1bSI6MCwic29ydCI6M30seyJpZCI6MTQ5MTUwLCJ0ZWFtIjowLCJuYW1lIjoiaW5mbyIsImNvbG9yIjoiIzAwMDAwMCIsInByZW1pdW0iOjAsInNvcnQiOjR9LHsiaWQiOjE0OTE0NSwidGVhbSI6MCwibmFtZSI6InBsdXMiLCJjb2xvciI6IiMwMDAwMDAiLCJwcmVtaXVtIjowLCJzb3J0Ijo1fSx7ImlkIjoxNDkzMDksInRlYW0iOjAsIm5hbWUiOiJzZWFyY2giLCJjb2xvciI6IiMwMDAwMDAiLCJwcmVtaXVtIjowLCJzb3J0Ijo2fSx7ImlkIjoxNDk4NTIsInRlYW0iOjAsIm5hbWUiOiJzZWFyY2giLCJjb2xvciI6IiMwMDAwMDAiLCJwcmVtaXVtIjowLCJzb3J0Ijo3fSx7ImlkIjoxNDg3NjQsInRlYW0iOjAsIm5hbWUiOiJwbHVzIiwiY29sb3IiOiIjWFhYWFhYIiwicHJlbWl1bSI6MCwic29ydCI6OH0seyJpZCI6MzM0MDQ3LCJ0ZWFtIjowLCJuYW1lIjoibmVnYXRpdmUiLCJjb2xvciI6IiNYWFhYWFgiLCJwcmVtaXVtIjowLCJzb3J0Ijo5fSx7ImlkIjoxNDkxNDYsInRlYW0iOjAsIm5hbWUiOiJtaW51cyIsImNvbG9yIjoiIzAwMDAwMCIsInByZW1pdW0iOjAsInNvcnQiOjEwfSx7ImlkIjoxNDg3NjgsInRlYW0iOjAsIm5hbWUiOiJ3YXJuaW5nIiwiY29sb3IiOiIjWFhYWFhYIiwicHJlbWl1bSI6MCwic29ydCI6MTF9LHsiaWQiOjE5Njc1OSwidGVhbSI6MCwibmFtZSI6Indhcm5pbmciLCJjb2xvciI6IiNYWFhYWFgiLCJwcmVtaXVtIjowLCJzb3J0IjoxMn0seyJpZCI6NjI4Mjg5LCJ0ZWFtIjowLCJuYW1lIjoibGFkeWJ1ZyIsImNvbG9yIjoiI1hYWFhYWCIsInByZW1pdW0iOjAsInNvcnQiOjEzfSx7ImlkIjo2MjgzNzEsInRlYW0iOjAsIm5hbWUiOiJsYWR5YnVnIiwiY29sb3IiOiIjMDAwMDAwIiwicHJlbWl1bSI6MCwic29ydCI6MTR9LHsiaWQiOjEyNjQ3MiwidGVhbSI6MCwibmFtZSI6InNldHRpbmdzIiwiY29sb3IiOiIjMDAwMDAwIiwicHJlbWl1bSI6MCwic29ydCI6MTV9LHsiaWQiOjE0ODgzNiwidGVhbSI6MCwibmFtZSI6Imxpa2UiLCJjb2xvciI6IiNYWFhYWFgiLCJwcmVtaXVtIjowLCJzb3J0IjoxNn0seyJpZCI6MTQ5MDYwLCJ0ZWFtIjowLCJuYW1lIjoicGxhY2Vob2xkZXIiLCJjb2xvciI6IiNYWFhYWFgiLCJwcmVtaXVtIjowLCJzb3J0IjoxN30seyJpZCI6MTQ4NzY3LCJ0ZWFtIjowLCJuYW1lIjoic3VjY2VzcyIsImNvbG9yIjoiI1hYWFhYWCIsInByZW1pdW0iOjAsInNvcnQiOjE4fSx7ImlkIjoxNDkxNDgsInRlYW0iOjAsIm5hbWUiOiJzdWNjZXNzIiwiY29sb3IiOiIjMDAwMDAwIiwicHJlbWl1bSI6MCwic29ydCI6MTl9LHsiaWQiOjE0OTMyMiwidGVhbSI6MCwibmFtZSI6InN0b3B3YXRjaCIsImNvbG9yIjoiIzAwMDAwMCIsInByZW1pdW0iOjAsInNvcnQiOjIwfSx7ImlkIjoxNDkxNDcsInRlYW0iOjAsIm5hbWUiOiJlcnJvciIsImNvbG9yIjoiIzAwMDAwMCIsInByZW1pdW0iOjAsInNvcnQiOjIxfSx7ImlkIjoxNDkzNDMsInRlYW0iOjAsIm5hbWUiOiJnYXJiYWdlIiwiY29sb3IiOiIjMDAwMDAwIiwicHJlbWl1bSI6MCwic29ydCI6MjJ9XX0= -------------------------------------------------------------------------------- /src/assets/flaticon/font/Flaticon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/flaticon/font/Flaticon.eot -------------------------------------------------------------------------------- /src/assets/flaticon/font/Flaticon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/flaticon/font/Flaticon.ttf -------------------------------------------------------------------------------- /src/assets/flaticon/font/Flaticon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/flaticon/font/Flaticon.woff -------------------------------------------------------------------------------- /src/assets/flaticon/font/_flaticon.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Flaticon icon font: Flaticon 3 | Creation date: 27/11/2017 14:07 4 | */ 5 | 6 | @font-face { 7 | font-family: "Flaticon"; 8 | src: url("./Flaticon.eot"); 9 | src: url("./Flaticon.eot?#iefix") format("embedded-opentype"), 10 | url("./Flaticon.woff") format("woff"), 11 | url("./Flaticon.ttf") format("truetype"), 12 | url("./Flaticon.svg#Flaticon") format("svg"); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | @media screen and (-webkit-min-device-pixel-ratio:0) { 18 | @font-face { 19 | font-family: "Flaticon"; 20 | src: url("./Flaticon.svg#Flaticon") format("svg"); 21 | } 22 | } 23 | 24 | .fi:before{ 25 | display: inline-block; 26 | font-family: "Flaticon"; 27 | font-style: normal; 28 | font-weight: normal; 29 | font-variant: normal; 30 | line-height: 1; 31 | text-decoration: inherit; 32 | text-rendering: optimizeLegibility; 33 | text-transform: none; 34 | -moz-osx-font-smoothing: grayscale; 35 | -webkit-font-smoothing: antialiased; 36 | font-smoothing: antialiased; 37 | } 38 | 39 | .flaticon-garbage:before { content: "\f100"; } 40 | .flaticon-error:before { content: "\f101"; } 41 | .flaticon-stopwatch:before { content: "\f102"; } 42 | .flaticon-success:before { content: "\f103"; } 43 | .flaticon-settings:before { content: "\f104"; } 44 | .flaticon-ladybug:before { content: "\f105"; } 45 | .flaticon-minus:before { content: "\f106"; } 46 | .flaticon-search-1:before { content: "\f107"; } 47 | .flaticon-search:before { content: "\f108"; } 48 | .flaticon-plus:before { content: "\f109"; } 49 | .flaticon-info-1:before { content: "\f10a"; } 50 | .flaticon-info:before { content: "\f10b"; } 51 | .flaticon-cancel:before { content: "\f10c"; } 52 | 53 | $font-Flaticon-garbage: "\f100"; 54 | $font-Flaticon-error: "\f101"; 55 | $font-Flaticon-stopwatch: "\f102"; 56 | $font-Flaticon-success: "\f103"; 57 | $font-Flaticon-settings: "\f104"; 58 | $font-Flaticon-ladybug: "\f105"; 59 | $font-Flaticon-minus: "\f106"; 60 | $font-Flaticon-search-1: "\f107"; 61 | $font-Flaticon-search: "\f108"; 62 | $font-Flaticon-plus: "\f109"; 63 | $font-Flaticon-info-1: "\f10a"; 64 | $font-Flaticon-info: "\f10b"; 65 | $font-Flaticon-cancel: "\f10c"; -------------------------------------------------------------------------------- /src/assets/flaticon/font/flaticon.css: -------------------------------------------------------------------------------- 1 | /* 2 | Flaticon icon font: Flaticon 3 | Creation date: 27/11/2017 14:07 4 | */ 5 | 6 | @font-face { 7 | font-family: "Flaticon"; 8 | src: url("./Flaticon.eot"); 9 | src: url("./Flaticon.eot?#iefix") format("embedded-opentype"), 10 | url("./Flaticon.woff") format("woff"), 11 | url("./Flaticon.ttf") format("truetype"), 12 | url("./Flaticon.svg#Flaticon") format("svg"); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | @media screen and (-webkit-min-device-pixel-ratio:0) { 18 | @font-face { 19 | font-family: "Flaticon"; 20 | src: url("./Flaticon.svg#Flaticon") format("svg"); 21 | } 22 | } 23 | 24 | [class^="flaticon-"]:before, [class*=" flaticon-"]:before, 25 | [class^="flaticon-"]:after, [class*=" flaticon-"]:after { 26 | font-family: Flaticon; 27 | font-size: 20px; 28 | font-style: normal; 29 | } 30 | 31 | .flaticon-garbage:before { content: "\f100"; } 32 | .flaticon-error:before { content: "\f101"; } 33 | .flaticon-stopwatch:before { content: "\f102"; } 34 | .flaticon-success:before { content: "\f103"; } 35 | .flaticon-settings:before { content: "\f104"; } 36 | .flaticon-ladybug:before { content: "\f105"; } 37 | .flaticon-minus:before { content: "\f106"; } 38 | .flaticon-search-1:before { content: "\f107"; } 39 | .flaticon-search:before { content: "\f108"; } 40 | .flaticon-plus:before { content: "\f109"; } 41 | .flaticon-info-1:before { content: "\f10a"; } 42 | .flaticon-info:before { content: "\f10b"; } 43 | .flaticon-cancel:before { content: "\f10c"; } 44 | -------------------------------------------------------------------------------- /src/assets/flaticon/license/license.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/flaticon/license/license.pdf -------------------------------------------------------------------------------- /src/assets/icomoon/Read Me.txt: -------------------------------------------------------------------------------- 1 | Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. 2 | 3 | To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts 4 | 5 | You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. 6 | 7 | You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. 8 | -------------------------------------------------------------------------------- /src/assets/icomoon/demo-files/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font-family: sans-serif; 5 | font-size: 1em; 6 | line-height: 1.5; 7 | color: #555; 8 | background: #fff; 9 | } 10 | h1 { 11 | font-size: 1.5em; 12 | font-weight: normal; 13 | } 14 | small { 15 | font-size: .66666667em; 16 | } 17 | a { 18 | color: #e74c3c; 19 | text-decoration: none; 20 | } 21 | a:hover, a:focus { 22 | box-shadow: 0 1px #e74c3c; 23 | } 24 | .bshadow0, input { 25 | box-shadow: inset 0 -2px #e7e7e7; 26 | } 27 | input:hover { 28 | box-shadow: inset 0 -2px #ccc; 29 | } 30 | input, fieldset { 31 | font-family: sans-serif; 32 | font-size: 1em; 33 | margin: 0; 34 | padding: 0; 35 | border: 0; 36 | } 37 | input { 38 | color: inherit; 39 | line-height: 1.5; 40 | height: 1.5em; 41 | padding: .25em 0; 42 | } 43 | input:focus { 44 | outline: none; 45 | box-shadow: inset 0 -2px #449fdb; 46 | } 47 | .glyph { 48 | font-size: 16px; 49 | width: 15em; 50 | padding-bottom: 1em; 51 | margin-right: 4em; 52 | margin-bottom: 1em; 53 | float: left; 54 | overflow: hidden; 55 | } 56 | .liga { 57 | width: 80%; 58 | width: calc(100% - 2.5em); 59 | } 60 | .talign-right { 61 | text-align: right; 62 | } 63 | .talign-center { 64 | text-align: center; 65 | } 66 | .bgc1 { 67 | background: #f1f1f1; 68 | } 69 | .fgc1 { 70 | color: #999; 71 | } 72 | .fgc0 { 73 | color: #000; 74 | } 75 | p { 76 | margin-top: 1em; 77 | margin-bottom: 1em; 78 | } 79 | .mvm { 80 | margin-top: .75em; 81 | margin-bottom: .75em; 82 | } 83 | .mtn { 84 | margin-top: 0; 85 | } 86 | .mtl, .mal { 87 | margin-top: 1.5em; 88 | } 89 | .mbl, .mal { 90 | margin-bottom: 1.5em; 91 | } 92 | .mal, .mhl { 93 | margin-left: 1.5em; 94 | margin-right: 1.5em; 95 | } 96 | .mhmm { 97 | margin-left: 1em; 98 | margin-right: 1em; 99 | } 100 | .mls { 101 | margin-left: .25em; 102 | } 103 | .ptl { 104 | padding-top: 1.5em; 105 | } 106 | .pbs, .pvs { 107 | padding-bottom: .25em; 108 | } 109 | .pvs, .pts { 110 | padding-top: .25em; 111 | } 112 | .unit { 113 | float: left; 114 | } 115 | .unitRight { 116 | float: right; 117 | } 118 | .size1of2 { 119 | width: 50%; 120 | } 121 | .size1of1 { 122 | width: 100%; 123 | } 124 | .clearfix:before, .clearfix:after { 125 | content: " "; 126 | display: table; 127 | } 128 | .clearfix:after { 129 | clear: both; 130 | } 131 | .hidden-true { 132 | display: none; 133 | } 134 | .textbox0 { 135 | width: 3em; 136 | background: #f1f1f1; 137 | padding: .25em .5em; 138 | line-height: 1.5; 139 | height: 1.5em; 140 | } 141 | #testDrive { 142 | display: block; 143 | padding-top: 24px; 144 | line-height: 1.5; 145 | } 146 | .fs0 { 147 | font-size: 16px; 148 | } 149 | .fs1 { 150 | font-size: 32px; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/assets/icomoon/demo-files/demo.js: -------------------------------------------------------------------------------- 1 | if (!('boxShadow' in document.body.style)) { 2 | document.body.setAttribute('class', 'noBoxShadow'); 3 | } 4 | 5 | document.body.addEventListener("click", function(e) { 6 | var target = e.target; 7 | if (target.tagName === "INPUT" && 8 | target.getAttribute('class').indexOf('liga') === -1) { 9 | target.select(); 10 | } 11 | }); 12 | 13 | (function() { 14 | var fontSize = document.getElementById('fontSize'), 15 | testDrive = document.getElementById('testDrive'), 16 | testText = document.getElementById('testText'); 17 | function updateTest() { 18 | testDrive.innerHTML = testText.value || String.fromCharCode(160); 19 | if (window.icomoonLiga) { 20 | window.icomoonLiga(testDrive); 21 | } 22 | } 23 | function updateSize() { 24 | testDrive.style.fontSize = fontSize.value + 'px'; 25 | } 26 | fontSize.addEventListener('change', updateSize, false); 27 | testText.addEventListener('input', updateTest, false); 28 | testText.addEventListener('change', updateTest, false); 29 | updateSize(); 30 | }()); 31 | -------------------------------------------------------------------------------- /src/assets/icomoon/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/icomoon/fonts/icomoon.eot -------------------------------------------------------------------------------- /src/assets/icomoon/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/icomoon/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/assets/icomoon/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/icomoon/fonts/icomoon.woff -------------------------------------------------------------------------------- /src/assets/icomoon/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('fonts/icomoon.eot?nzb1of'); 4 | src: url('fonts/icomoon.eot?nzb1of#iefix') format('embedded-opentype'), 5 | url('fonts/icomoon.ttf?nzb1of') format('truetype'), 6 | url('fonts/icomoon.woff?nzb1of') format('woff'), 7 | url('fonts/icomoon.svg?nzb1of#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomoon' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .icon-quill:before { 28 | content: "\e907"; 29 | } 30 | .icon-pen:before { 31 | content: "\e908"; 32 | } 33 | .icon-droplet:before { 34 | content: "\e90b"; 35 | } 36 | .icon-podcast:before { 37 | content: "\e91c"; 38 | } 39 | .icon-feed:before { 40 | content: "\e91d"; 41 | } 42 | .icon-stack:before { 43 | content: "\e92e"; 44 | } 45 | .icon-folder:before { 46 | content: "\e92f"; 47 | } 48 | .icon-folder-plus:before { 49 | content: "\e931"; 50 | } 51 | .icon-folder-minus:before { 52 | content: "\e932"; 53 | } 54 | .icon-price-tag:before { 55 | content: "\e935"; 56 | } 57 | .icon-calculator:before { 58 | content: "\e940"; 59 | } 60 | .icon-pushpin:before { 61 | content: "\e946"; 62 | } 63 | .icon-location:before { 64 | content: "\e947"; 65 | } 66 | .icon-compass:before { 67 | content: "\e949"; 68 | } 69 | .icon-compass2:before { 70 | content: "\e94a"; 71 | } 72 | .icon-map:before { 73 | content: "\e94b"; 74 | } 75 | .icon-map2:before { 76 | content: "\e94c"; 77 | } 78 | .icon-clock2:before { 79 | content: "\e94f"; 80 | } 81 | .icon-bell:before { 82 | content: "\e951"; 83 | } 84 | .icon-drawer:before { 85 | content: "\e95c"; 86 | } 87 | .icon-drawer2:before { 88 | content: "\e95d"; 89 | } 90 | .icon-download:before { 91 | content: "\e960"; 92 | } 93 | .icon-upload:before { 94 | content: "\e961"; 95 | } 96 | .icon-database:before { 97 | content: "\e964"; 98 | } 99 | .icon-undo:before { 100 | content: "\e965"; 101 | } 102 | .icon-redo:before { 103 | content: "\e966"; 104 | } 105 | .icon-user:before { 106 | content: "\e971"; 107 | } 108 | .icon-spinner:before { 109 | content: "\e97a"; 110 | } 111 | .icon-spinner2:before { 112 | content: "\e97b"; 113 | } 114 | .icon-spinner5:before { 115 | content: "\e97e"; 116 | } 117 | .icon-spinner8:before { 118 | content: "\e981"; 119 | } 120 | .icon-search:before { 121 | content: "\e986"; 122 | } 123 | .icon-lock:before { 124 | content: "\e98f"; 125 | } 126 | .icon-unlocked:before { 127 | content: "\e990"; 128 | } 129 | .icon-equalizer:before { 130 | content: "\e992"; 131 | } 132 | .icon-equalizer2:before { 133 | content: "\e993"; 134 | } 135 | .icon-cog:before { 136 | content: "\e994"; 137 | } 138 | .icon-cogs:before { 139 | content: "\e995"; 140 | } 141 | .icon-hammer:before { 142 | content: "\e996"; 143 | } 144 | .icon-bug:before { 145 | content: "\e999"; 146 | } 147 | .icon-pie-chart:before { 148 | content: "\e99a"; 149 | } 150 | .icon-trophy:before { 151 | content: "\e99e"; 152 | } 153 | .icon-gift:before { 154 | content: "\e99f"; 155 | } 156 | .icon-fire:before { 157 | content: "\e9a9"; 158 | } 159 | .icon-lab:before { 160 | content: "\e9aa"; 161 | } 162 | .icon-bin2:before { 163 | content: "\e9ad"; 164 | } 165 | .icon-switch:before { 166 | content: "\e9b6"; 167 | } 168 | .icon-tree:before { 169 | content: "\e9bc"; 170 | } 171 | .icon-sphere:before { 172 | content: "\e9c9"; 173 | } 174 | .icon-earth:before { 175 | content: "\e9ca"; 176 | } 177 | .icon-link:before { 178 | content: "\e9cb"; 179 | } 180 | .icon-eye:before { 181 | content: "\e9ce"; 182 | } 183 | .icon-eye-plus:before { 184 | content: "\e9cf"; 185 | } 186 | .icon-eye-minus:before { 187 | content: "\e9d0"; 188 | } 189 | .icon-eye-blocked:before { 190 | content: "\e9d1"; 191 | } 192 | .icon-star-empty:before { 193 | content: "\e9d7"; 194 | } 195 | .icon-star-half:before { 196 | content: "\e9d8"; 197 | } 198 | .icon-star-full:before { 199 | content: "\e9d9"; 200 | } 201 | .icon-notification:before { 202 | content: "\ea08"; 203 | } 204 | .icon-plus:before { 205 | content: "\ea0a"; 206 | } 207 | .icon-minus:before { 208 | content: "\ea0b"; 209 | } 210 | .icon-info:before { 211 | content: "\ea0c"; 212 | } 213 | .icon-cancel-circle:before { 214 | content: "\ea0d"; 215 | } 216 | .icon-blocked:before { 217 | content: "\ea0e"; 218 | } 219 | .icon-cross:before { 220 | content: "\ea0f"; 221 | } 222 | .icon-checkmark:before { 223 | content: "\ea10"; 224 | } 225 | .icon-spell-check:before { 226 | content: "\ea12"; 227 | } 228 | .icon-play3:before { 229 | content: "\ea1c"; 230 | } 231 | .icon-pause2:before { 232 | content: "\ea1d"; 233 | } 234 | .icon-stop2:before { 235 | content: "\ea1e"; 236 | } 237 | .icon-infinite:before { 238 | content: "\ea2f"; 239 | } 240 | .icon-circle-up:before { 241 | content: "\ea41"; 242 | } 243 | .icon-circle-right:before { 244 | content: "\ea42"; 245 | } 246 | .icon-circle-down:before { 247 | content: "\ea43"; 248 | } 249 | .icon-circle-left:before { 250 | content: "\ea44"; 251 | } 252 | .icon-radio-checked:before { 253 | content: "\ea54"; 254 | } 255 | .icon-scissors:before { 256 | content: "\ea5a"; 257 | } 258 | .icon-filter:before { 259 | content: "\ea5b"; 260 | } 261 | .icon-omega:before { 262 | content: "\ea66"; 263 | } 264 | .icon-sigma:before { 265 | content: "\ea67"; 266 | } 267 | .icon-embed2:before { 268 | content: "\ea80"; 269 | } 270 | .icon-share2:before { 271 | content: "\ea82"; 272 | } 273 | .icon-github:before { 274 | content: "\eab0"; 275 | } 276 | -------------------------------------------------------------------------------- /src/assets/neo4j.settings.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "client": { 3 | "apiEndpoint": "http://127.0.0.1:7474/db/data", 4 | "authBasic": "Basic queRty==" 5 | }, 6 | "hotkeys": { 7 | "toggleCreateMode": ["CTRL", "n"] 8 | }, 9 | "graph": { 10 | "nodes": { 11 | "primaryKey": "ID", 12 | "displayNameOptions": ["name", "label", "id"], 13 | "displayColorOptions": { 14 | "Ad": "#6DCE9E", 15 | "Person": "#343434", 16 | "Test": "#e8cb3b", 17 | "Specialty": "#11b7ad", 18 | "Client": "#6DCE9E", 19 | "Company": "#1e6f98", 20 | "Officer": "#e8cb3b", 21 | "Test": "#11b7ad" 22 | } 23 | }, 24 | "links": { 25 | "displayNameOptions": ["name", "TYPE"], 26 | "displayColorOptions": { 27 | 28 | } 29 | }, 30 | "labels": ["Test1", "Test2", "Document", "Officer", "Company"] 31 | }, 32 | "debug": true 33 | } 34 | -------------------------------------------------------------------------------- /src/assets/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/plus.png -------------------------------------------------------------------------------- /src/assets/svg/icon8-plus.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg/three-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 12 | 13 | 14 | 18 | 22 | 23 | 24 | 28 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/tutos/inline-create-mode-switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/tutos/inline-create-mode-switch.png -------------------------------------------------------------------------------- /src/assets/tutos/neo4j-js-tuto-01-low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/tutos/neo4j-js-tuto-01-low.gif -------------------------------------------------------------------------------- /src/assets/tutos/neo4j-js-tuto-02-low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/tutos/neo4j-js-tuto-02-low.gif -------------------------------------------------------------------------------- /src/assets/tutos/neo4j-js-tuto-03-low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/assets/tutos/neo4j-js-tuto-03-low.gif -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adadgio/neo4j-js-ng2/422aef5d3c1f160d70364e7bb69c2fe4fda4155f/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Neo4jJs · Graph visualization and edition 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | 22 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 23 | // import 'core-js/es6/symbol'; 24 | // import 'core-js/es6/object'; 25 | // import 'core-js/es6/function'; 26 | // import 'core-js/es6/parse-int'; 27 | // import 'core-js/es6/parse-float'; 28 | // import 'core-js/es6/number'; 29 | // import 'core-js/es6/math'; 30 | // import 'core-js/es6/string'; 31 | // import 'core-js/es6/date'; 32 | // import 'core-js/es6/array'; 33 | // import 'core-js/es6/regexp'; 34 | // import 'core-js/es6/map'; 35 | // import 'core-js/es6/weak-map'; 36 | // import 'core-js/es6/set'; 37 | 38 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 39 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 40 | 41 | /** IE10 and IE11 requires the following for the Reflect API. */ 42 | // import 'core-js/es6/reflect'; 43 | 44 | 45 | /** Evergreen browsers require these. **/ 46 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 47 | import 'core-js/es7/reflect'; 48 | 49 | 50 | /** 51 | * Required to support Web Animations `@angular/platform-browser/animations`. 52 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | 57 | 58 | /*************************************************************************************************** 59 | * Zone JS is required by Angular itself. 60 | */ 61 | import 'zone.js/dist/zone'; // Included with Angular CLI. 62 | 63 | 64 | /*************************************************************************************************** 65 | * APPLICATION IMPORTS 66 | */ 67 | 68 | /** 69 | * Date, currency, decimal and percent pipes. 70 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 71 | */ 72 | // import 'intl'; // Run `npm install --save intl`. 73 | /** 74 | * Need to import at least one locale-data with intl. 75 | */ 76 | // import 'intl/locale-data/jsonp/en'; 77 | -------------------------------------------------------------------------------- /src/theme/common.scss: -------------------------------------------------------------------------------- 1 | @import "./shared/_mixins"; 2 | @import "./shared/_variables"; 3 | 4 | /** 5 | * Commpon classes and styles. 6 | */ 7 | $radius: 1px; 8 | .radiusize { 9 | @include border-radius($radius) 10 | } 11 | .radiusize-top { 12 | @include border-radius($radius $radius 0px 0px) 13 | } 14 | .radiusize-bottom { 15 | @include border-radius(0px 0px $radius $radius) 16 | } 17 | 18 | img.inline-image { 19 | display: inline-block; 20 | height: $fontSize + 4px; 21 | line-height: inherit; 22 | vertical-align: middle; 23 | } 24 | 25 | .coal { 26 | background-color: colors(coal) 27 | } 28 | .coal-border { 29 | border: 1px solid colors(coal); 30 | } 31 | 32 | .charcoal { 33 | background-color: colors(charcoal) 34 | } 35 | 36 | .gray { 37 | color: colors(gray); 38 | } 39 | .red { 40 | color: colors(red); 41 | } 42 | .box-title { 43 | font-size: $fontSize + 2px; 44 | } 45 | .small { 46 | font-size: $fontSize - 2px 47 | } 48 | .center { 49 | text-align: center; 50 | } 51 | 52 | .emarged { 53 | margin-top: 16px; 54 | margin-bottom: 16px; 55 | } 56 | .emarged-bottom { 57 | margin-bottom: 16px; 58 | } 59 | .padding-8px { 60 | padding: 8px; 61 | } 62 | .padding-16px { 63 | padding: 16px; 64 | } 65 | .padding-8-16px { 66 | padding: 16px; 67 | } 68 | .margin-right-8px { 69 | margin-right: 8px; 70 | } 71 | .margin-left-8px { 72 | margin-left: 8px; 73 | } 74 | .margin-top-8px { 75 | margin-top: 8px; 76 | } 77 | .margin-bottom-8px { 78 | margin-bottom: 8px; 79 | } 80 | .margin-bottom-16px { 81 | margin-bottom: 16px; 82 | } 83 | .relative { 84 | position: relative; 85 | } 86 | .absolute { 87 | position: absolute; 88 | } 89 | 90 | .primary { 91 | color: colors(primary); 92 | } 93 | .success { 94 | color: colors(success); 95 | } 96 | .secondary { 97 | color: colors(secondary); 98 | } 99 | a.secondary { 100 | &:hover { color: colors(white) } 101 | } 102 | .warning, a.warning { 103 | color: colors(warning); 104 | &:hover { } 105 | } 106 | .danger { 107 | color: colors(danger); 108 | } 109 | .info { 110 | color: colors(info); 111 | } 112 | 113 | .green { 114 | color: colors(green); 115 | } 116 | 117 | 118 | .near-toast { 119 | font-size: inherit; 120 | display: flex; 121 | align-items: center; 122 | padding: 2px 6px; 123 | } 124 | 125 | .toast { 126 | font-size: $fontSize; 127 | display: flex; 128 | align-items: center; 129 | padding: 2px 6px; 130 | @include border-radius(2px); 131 | 132 | &.cleared { 133 | padding: 2px 0px; 134 | } 135 | &.toast-small { 136 | font-size: $fontSize - 2px; 137 | } 138 | 139 | &.primary { 140 | color: colors(fullwhite); 141 | background-color: colors(primary); 142 | } 143 | &.secondary { 144 | color: colors(fullwhite); 145 | background-color: colors(secondary); 146 | } 147 | &.success { 148 | color: colors(fullwhite); 149 | background-color: colors(success); 150 | } 151 | &.warning { 152 | color: colors(fullwhite); 153 | background-color: colors(warning); 154 | } 155 | &.danger { 156 | color: colors(fullwhite); 157 | background-color: colors(danger); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/theme/flex.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Flex layout 3 | */ 4 | [flex] { 5 | display: flex; 6 | } 7 | 8 | [flex-rows] { 9 | display: flex; 10 | flex-direction: row; 11 | } 12 | 13 | [flex-columns] { 14 | display: flex; 15 | flex-direction: column; 16 | } 17 | 18 | [flex-fill] { 19 | display: flex; 20 | flex: 1; 21 | } 22 | 23 | [flex-center] { 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | [flex-align-center] 28 | { 29 | align-items: center; 30 | } 31 | [flex-justify-center] { 32 | justify-content: center; 33 | } 34 | [flex-justify-start] { 35 | justify-content: flex-start; 36 | } 37 | [flex-justify-end] { 38 | justify-content: flex-end; 39 | } 40 | [flex-align-end] { 41 | align-items: flex-end; 42 | } 43 | [flex-space-between] { 44 | justify-content: space-between; 45 | } 46 | [flex-space-around] { 47 | justify-content: space-around; 48 | } 49 | 50 | [pull-right] { 51 | margin-left: auto; 52 | } 53 | 54 | [pull-left] { 55 | margin-right: auto; 56 | } 57 | 58 | [flex-align-stretch] { 59 | align-items: stretch; 60 | } 61 | [flex-with-icon] { 62 | display: flex; 63 | flex-direction: row; 64 | align-items: center; 65 | } 66 | // [flex-fullpage-layout] { 67 | // display: flex; 68 | // flex-direction: column; 69 | // flex: 1; 70 | // } 71 | 72 | [margin] { margin: 8px 8px 8px 8px } 73 | [padding] { padding: 8px 8px 8px 8px } 74 | [margin-x2] { margin: 16px 16px 16px 16px } 75 | [padding-x2] { padding: 16px 16px 16px 16px } 76 | -------------------------------------------------------------------------------- /src/theme/form.scss: -------------------------------------------------------------------------------- 1 | @import "./shared/_mixins"; 2 | @import "./shared/_variables"; 3 | 4 | form { 5 | display: flex; 6 | flex-direction: column; 7 | // align-items: stretch; 8 | } 9 | 10 | .controls { 11 | display: flex; 12 | flex-direction: row; 13 | // align-items: stretch; 14 | } 15 | 16 | label { 17 | margin-bottom: 8px; 18 | margin-top: 16px; 19 | 20 | &.first { 21 | margin-top: 0px; 22 | } 23 | } 24 | 25 | input[type="text"], 26 | input[type="password"], 27 | input[type="number"], 28 | input[type="email"] { 29 | color: #313131; 30 | height: 27px; 31 | display: flex; 32 | line-height: 1em; 33 | align-items: center; 34 | border: none; 35 | font-family: inherit; 36 | font-size: $fontSize; 37 | padding: 8px 10px 8px 10px; 38 | background-color: #eaeaea; 39 | border: none; 40 | @include box-sizing(); 41 | @include border-radius(2px); 42 | 43 | &.input-small { 44 | font-size: $fontSize - 1px; 45 | padding: 6px 7px; 46 | } 47 | 48 | &.input-large { 49 | height: auto; 50 | font-size: $fontSize + 1px; 51 | padding: 10px 12px 10px 12px; 52 | } 53 | 54 | &.input-dark { 55 | color: #b4b4b4; 56 | background-color: colors(midnight); 57 | } 58 | 59 | &.input-code { 60 | font-family: Monospace, "Monospace sans", sans-serif; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/theme/layout.scss: -------------------------------------------------------------------------------- 1 | @import "./shared/_mixins"; 2 | @import "./shared/_variables"; 3 | 4 | /** 5 | * Layout 6 | */ 7 | *, html { 8 | outline: none; 9 | } 10 | 11 | html, body { 12 | width: 100%; 13 | height: 100%; 14 | } 15 | 16 | body { 17 | color: colors(white); 18 | display: flex; 19 | flex-direction: column; 20 | font-size: $fontSize; 21 | font-family: Arial, sans-serif; 22 | background-color: colors(midnight); 23 | @include text-antialias(); 24 | } 25 | 26 | app-root { 27 | display: flex; 28 | flex: 1; 29 | flex-direction: column; 30 | } 31 | 32 | router-outlet { 33 | display: none; 34 | } 35 | 36 | a { 37 | color: inherit; 38 | text-decoration: none; 39 | @include transition(background-color ease 0.2s, color ease 0.2s); 40 | 41 | &:hover { 42 | color: colors(warning); 43 | } 44 | } 45 | 46 | h1 { 47 | color: colors(gray); 48 | font-size: $fontSize + 8px; 49 | font-weight: 200; 50 | margin: 16px 0px 32px 0px; 51 | } 52 | 53 | p { 54 | margin: 0px; 55 | padding: 0px; 56 | line-height: 1.4em; 57 | } 58 | p.paragraph { 59 | padding: 0px 16px; 60 | margin: 8px 0px; 61 | } 62 | 63 | [page] { 64 | display: flex; 65 | flex: 1; 66 | flex-direction: column; 67 | margin-top: $headerHeight; 68 | padding: 16px; 69 | } 70 | -------------------------------------------------------------------------------- /src/theme/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in 9 | * IE on Windows Phone and in iOS. 10 | */ 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -ms-text-size-adjust: 100%; /* 2 */ 14 | -webkit-text-size-adjust: 100%; /* 2 */ 15 | } 16 | 17 | /* Sections 18 | ========================================================================== */ 19 | 20 | /** 21 | * Remove the margin in all browsers (opinionated). 22 | */ 23 | 24 | body { 25 | margin: 0; 26 | } 27 | 28 | /** 29 | * Add the correct display in IE 9-. 30 | */ 31 | 32 | article, 33 | aside, 34 | footer, 35 | header, 36 | nav, 37 | section { 38 | display: block; 39 | } 40 | 41 | /** 42 | * Correct the font size and margin on `h1` elements within `section` and 43 | * `article` contexts in Chrome, Firefox, and Safari. 44 | */ 45 | 46 | h1 { 47 | font-size: 2em; 48 | margin: 0.67em 0; 49 | } 50 | 51 | /* Grouping content 52 | ========================================================================== */ 53 | 54 | /** 55 | * Add the correct display in IE 9-. 56 | * 1. Add the correct display in IE. 57 | */ 58 | 59 | figcaption, 60 | figure, 61 | main { /* 1 */ 62 | display: block; 63 | } 64 | 65 | /** 66 | * Add the correct margin in IE 8. 67 | */ 68 | 69 | figure { 70 | margin: 1em 40px; 71 | } 72 | 73 | /** 74 | * 1. Add the correct box sizing in Firefox. 75 | * 2. Show the overflow in Edge and IE. 76 | */ 77 | 78 | hr { 79 | box-sizing: content-box; /* 1 */ 80 | height: 0; /* 1 */ 81 | overflow: visible; /* 2 */ 82 | } 83 | 84 | /** 85 | * 1. Correct the inheritance and scaling of font size in all browsers. 86 | * 2. Correct the odd `em` font sizing in all browsers. 87 | */ 88 | 89 | pre { 90 | font-family: monospace, monospace; /* 1 */ 91 | font-size: 1em; /* 2 */ 92 | } 93 | 94 | /* Text-level semantics 95 | ========================================================================== */ 96 | 97 | /** 98 | * 1. Remove the gray background on active links in IE 10. 99 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 100 | */ 101 | 102 | a { 103 | background-color: transparent; /* 1 */ 104 | -webkit-text-decoration-skip: objects; /* 2 */ 105 | } 106 | 107 | /** 108 | * 1. Remove the bottom border in Chrome 57- and Firefox 39-. 109 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 110 | */ 111 | 112 | abbr[title] { 113 | border-bottom: none; /* 1 */ 114 | text-decoration: underline; /* 2 */ 115 | text-decoration: underline dotted; /* 2 */ 116 | } 117 | 118 | /** 119 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 120 | */ 121 | 122 | b, 123 | strong { 124 | font-weight: inherit; 125 | } 126 | 127 | /** 128 | * Add the correct font weight in Chrome, Edge, and Safari. 129 | */ 130 | 131 | b, 132 | strong { 133 | font-weight: bolder; 134 | } 135 | 136 | /** 137 | * 1. Correct the inheritance and scaling of font size in all browsers. 138 | * 2. Correct the odd `em` font sizing in all browsers. 139 | */ 140 | 141 | code, 142 | kbd, 143 | samp { 144 | font-family: monospace, monospace; /* 1 */ 145 | font-size: 1em; /* 2 */ 146 | } 147 | 148 | /** 149 | * Add the correct font style in Android 4.3-. 150 | */ 151 | 152 | dfn { 153 | font-style: italic; 154 | } 155 | 156 | /** 157 | * Add the correct background and color in IE 9-. 158 | */ 159 | 160 | mark { 161 | background-color: #ff0; 162 | color: #000; 163 | } 164 | 165 | /** 166 | * Add the correct font size in all browsers. 167 | */ 168 | 169 | small { 170 | font-size: 80%; 171 | } 172 | 173 | /** 174 | * Prevent `sub` and `sup` elements from affecting the line height in 175 | * all browsers. 176 | */ 177 | 178 | sub, 179 | sup { 180 | font-size: 75%; 181 | line-height: 0; 182 | position: relative; 183 | vertical-align: baseline; 184 | } 185 | 186 | sub { 187 | bottom: -0.25em; 188 | } 189 | 190 | sup { 191 | top: -0.5em; 192 | } 193 | 194 | /* Embedded content 195 | ========================================================================== */ 196 | 197 | /** 198 | * Add the correct display in IE 9-. 199 | */ 200 | 201 | audio, 202 | video { 203 | display: inline-block; 204 | } 205 | 206 | /** 207 | * Add the correct display in iOS 4-7. 208 | */ 209 | 210 | audio:not([controls]) { 211 | display: none; 212 | height: 0; 213 | } 214 | 215 | /** 216 | * Remove the border on images inside links in IE 10-. 217 | */ 218 | 219 | img { 220 | border-style: none; 221 | } 222 | 223 | /** 224 | * Hide the overflow in IE. 225 | */ 226 | 227 | svg:not(:root) { 228 | overflow: hidden; 229 | } 230 | 231 | /* Forms 232 | ========================================================================== */ 233 | 234 | /** 235 | * 1. Change the font styles in all browsers (opinionated). 236 | * 2. Remove the margin in Firefox and Safari. 237 | */ 238 | 239 | button, 240 | input, 241 | optgroup, 242 | select, 243 | textarea { 244 | font-family: sans-serif; /* 1 */ 245 | font-size: 100%; /* 1 */ 246 | line-height: 1.15; /* 1 */ 247 | margin: 0; /* 2 */ 248 | } 249 | 250 | /** 251 | * Show the overflow in IE. 252 | * 1. Show the overflow in Edge. 253 | */ 254 | 255 | button, 256 | input { /* 1 */ 257 | overflow: visible; 258 | } 259 | 260 | /** 261 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 262 | * 1. Remove the inheritance of text transform in Firefox. 263 | */ 264 | 265 | button, 266 | select { /* 1 */ 267 | text-transform: none; 268 | } 269 | 270 | /** 271 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 272 | * controls in Android 4. 273 | * 2. Correct the inability to style clickable types in iOS and Safari. 274 | */ 275 | 276 | button, 277 | html [type="button"], /* 1 */ 278 | [type="reset"], 279 | [type="submit"] { 280 | -webkit-appearance: button; /* 2 */ 281 | } 282 | 283 | /** 284 | * Remove the inner border and padding in Firefox. 285 | */ 286 | 287 | button::-moz-focus-inner, 288 | [type="button"]::-moz-focus-inner, 289 | [type="reset"]::-moz-focus-inner, 290 | [type="submit"]::-moz-focus-inner { 291 | border-style: none; 292 | padding: 0; 293 | } 294 | 295 | /** 296 | * Restore the focus styles unset by the previous rule. 297 | */ 298 | 299 | button:-moz-focusring, 300 | [type="button"]:-moz-focusring, 301 | [type="reset"]:-moz-focusring, 302 | [type="submit"]:-moz-focusring { 303 | outline: 1px dotted ButtonText; 304 | } 305 | 306 | /** 307 | * Correct the padding in Firefox. 308 | */ 309 | 310 | fieldset { 311 | padding: 0.35em 0.75em 0.625em; 312 | } 313 | 314 | /** 315 | * 1. Correct the text wrapping in Edge and IE. 316 | * 2. Correct the color inheritance from `fieldset` elements in IE. 317 | * 3. Remove the padding so developers are not caught out when they zero out 318 | * `fieldset` elements in all browsers. 319 | */ 320 | 321 | legend { 322 | box-sizing: border-box; /* 1 */ 323 | color: inherit; /* 2 */ 324 | display: table; /* 1 */ 325 | max-width: 100%; /* 1 */ 326 | padding: 0; /* 3 */ 327 | white-space: normal; /* 1 */ 328 | } 329 | 330 | /** 331 | * 1. Add the correct display in IE 9-. 332 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 333 | */ 334 | 335 | progress { 336 | display: inline-block; /* 1 */ 337 | vertical-align: baseline; /* 2 */ 338 | } 339 | 340 | /** 341 | * Remove the default vertical scrollbar in IE. 342 | */ 343 | 344 | textarea { 345 | overflow: auto; 346 | } 347 | 348 | /** 349 | * 1. Add the correct box sizing in IE 10-. 350 | * 2. Remove the padding in IE 10-. 351 | */ 352 | 353 | [type="checkbox"], 354 | [type="radio"] { 355 | box-sizing: border-box; /* 1 */ 356 | padding: 0; /* 2 */ 357 | } 358 | 359 | /** 360 | * Correct the cursor style of increment and decrement buttons in Chrome. 361 | */ 362 | 363 | [type="number"]::-webkit-inner-spin-button, 364 | [type="number"]::-webkit-outer-spin-button { 365 | height: auto; 366 | } 367 | 368 | /** 369 | * 1. Correct the odd appearance in Chrome and Safari. 370 | * 2. Correct the outline style in Safari. 371 | */ 372 | 373 | [type="search"] { 374 | -webkit-appearance: textfield; /* 1 */ 375 | outline-offset: -2px; /* 2 */ 376 | } 377 | 378 | /** 379 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 380 | */ 381 | 382 | [type="search"]::-webkit-search-cancel-button, 383 | [type="search"]::-webkit-search-decoration { 384 | -webkit-appearance: none; 385 | } 386 | 387 | /** 388 | * 1. Correct the inability to style clickable types in iOS and Safari. 389 | * 2. Change font properties to `inherit` in Safari. 390 | */ 391 | 392 | ::-webkit-file-upload-button { 393 | -webkit-appearance: button; /* 1 */ 394 | font: inherit; /* 2 */ 395 | } 396 | 397 | /* Interactive 398 | ========================================================================== */ 399 | 400 | /* 401 | * Add the correct display in IE 9-. 402 | * 1. Add the correct display in Edge, IE, and Firefox. 403 | */ 404 | 405 | details, /* 1 */ 406 | menu { 407 | display: block; 408 | } 409 | 410 | /* 411 | * Add the correct display in all browsers. 412 | */ 413 | 414 | summary { 415 | display: list-item; 416 | } 417 | 418 | /* Scripting 419 | ========================================================================== */ 420 | 421 | /** 422 | * Add the correct display in IE 9-. 423 | */ 424 | 425 | canvas { 426 | display: inline-block; 427 | } 428 | 429 | /** 430 | * Add the correct display in IE. 431 | */ 432 | 433 | template { 434 | display: none; 435 | } 436 | 437 | /* Hidden 438 | ========================================================================== */ 439 | 440 | /** 441 | * Add the correct display in IE 10-. 442 | */ 443 | 444 | [hidden] { 445 | display: none; 446 | } 447 | -------------------------------------------------------------------------------- /src/theme/shared/_mixins.scss: -------------------------------------------------------------------------------- 1 | /* Mixins and function */ 2 | 3 | @mixin border-radius($size: 3px) { 4 | -webkit-border-radius: $size; 5 | -moz-border-radius: $size; 6 | border-radius: $size; 7 | } 8 | 9 | @mixin transition($args...) { 10 | -webkit-transition: $args; 11 | -moz-transition: $args; 12 | transition: $args; 13 | } 14 | @mixin rotate($degrees) { 15 | -webkit-transform: rotate($degrees); 16 | -moz-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | -o-transform: rotate($degrees); 19 | transform: rotate($degrees); 20 | } 21 | 22 | @mixin box-sizing($value: border-box) { 23 | box-sizing: $value; 24 | -moz-box-sizing: $value; 25 | -webkit-box-sizing: $value; 26 | } 27 | 28 | @mixin text-antialias() { 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -o-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | } 34 | 35 | @mixin box-shadow($top, $left, $blur, $color, $inset: false) { 36 | @if $inset { 37 | -webkit-box-shadow:inset $top $left $blur $color; 38 | -moz-box-shadow:inset $top $left $blur $color; 39 | box-shadow:inset $top $left $blur $color; 40 | } @else { 41 | -webkit-box-shadow: $top $left $blur $color; 42 | -moz-box-shadow: $top $left $blur $color; 43 | box-shadow: $top $left $blur $color; 44 | } 45 | } 46 | 47 | @mixin hostSupportsFullPage() { 48 | display: flex; 49 | flex-direction: column; 50 | flex: 1; 51 | } 52 | 53 | @mixin hostSupportsPageScroll() { 54 | display: block; 55 | flex: none; 56 | overflow-y: auto; 57 | margin-bottom: 16px; 58 | } 59 | 60 | @mixin hostSupportsFullHeight() { 61 | display: flex; 62 | flex-direction: column; 63 | flex: 1; 64 | } 65 | 66 | @mixin hostDefaults() { 67 | flex: none 68 | } 69 | -------------------------------------------------------------------------------- /src/theme/shared/_scrollbars.scss: -------------------------------------------------------------------------------- 1 | // Mixin to customize scrollbars 2 | /// Beware, this does not work in all browsers 3 | /// @author Hugo Giraudel 4 | /// @param {Length} $size - Horizontal scrollbar's height and vertical scrollbar's width 5 | /// @param {Color} $foreground-color - Scrollbar's color 6 | /// @param {Color} $background-color [mix($primary, white, 75%)] - Scrollbar's color 7 | /// @example scss - Scrollbar styling 8 | /// @include scrollbars(.5em, slategray); 9 | @mixin scrollbars($size, $foreground-color, $background-color: mix($foreground-color, white, 50%)) { 10 | &::-webkit-scrollbar { 11 | width: $size; 12 | height: $size; 13 | } 14 | 15 | &::-webkit-scrollbar-thumb { 16 | background: $foreground-color; 17 | border-radius: 6px; 18 | } 19 | 20 | &::-webkit-scrollbar-track { 21 | background: $background-color; 22 | } 23 | 24 | // For Internet Explorer 25 | // body { 26 | // scrollbar-face-color: $foreground-color; 27 | // scrollbar-track-color: $background-color; 28 | // } 29 | } 30 | -------------------------------------------------------------------------------- /src/theme/shared/_variables.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Variables, colors and theme. 3 | */ 4 | $fontSize: 13px; 5 | $fontColor: #545454; 6 | $headerHeight: 55px; 7 | 8 | $theme: ( 9 | fullwhite: #f9f9f9, 10 | gray: #8c8c8c, 11 | grayish: #868686, 12 | white: #d2d2d2, 13 | black: $fontColor, 14 | whitesmoke: whitesmoke, 15 | midnight: #222222, 16 | coal: #464545, 17 | charcoal: #303030, 18 | nemo: #375a7f, 19 | nemo-hover: #28415b, 20 | green: #148014, 21 | 22 | night: #1b1b1b, //#232323, 23 | 24 | faded: #6c7886, 25 | 26 | primary: #37597e, 27 | // secondary: #444444, 28 | secondary: #8e8e8e, 29 | success: #26bc8c, 30 | danger: #e43826, 31 | warning: #f49b13, 32 | info: #3497db, 33 | disabled: #424952, 34 | 35 | primary-hover: #29405a, 36 | success-hover: #009670, 37 | warning-hover: #d4860b, 38 | danger-hover: #e12e1c, 39 | info-hover: #2384c6, 40 | disabled-hover: #424952, 41 | ); 42 | 43 | @function colors($colorName) { 44 | @return map-get($theme, $colorName); 45 | } 46 | -------------------------------------------------------------------------------- /src/theme/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "./normalize"; 3 | @import "./layout"; 4 | @import "./flex"; 5 | @import "./common"; 6 | @import "./form"; 7 | 8 | @import "./svg/svg"; 9 | @import "./svg/node"; 10 | @import "./svg/links"; 11 | @import "./svg/cursors"; 12 | -------------------------------------------------------------------------------- /src/theme/svg/cursors.scss: -------------------------------------------------------------------------------- 1 | /* the dragline to create links between 2 nodes in create mode */ 2 | $draglineColor: #dedede; 3 | 4 | line.dragline { 5 | stroke: $draglineColor; 6 | stroke-width: 2px; 7 | } 8 | .dragline-arrow { 9 | fill: $draglineColor; 10 | } 11 | -------------------------------------------------------------------------------- /src/theme/svg/links.scss: -------------------------------------------------------------------------------- 1 | 2 | g.link-group { 3 | cursor: pointer; 4 | 5 | text.link-text { 6 | font-size: 10px; 7 | fill: #808080; 8 | text-decoration: uppercase; 9 | } 10 | 11 | 12 | line.link { 13 | stroke: #dedede; 14 | stroke-width: 1.3px; 15 | } 16 | 17 | line.link-overlay { 18 | stroke: #25a4ca; 19 | stroke-width: 6px; 20 | stroke-opacity: 0; 21 | } 22 | 23 | &:hover { 24 | line.link-overlay { 25 | stroke-opacity: 0.2; 26 | } 27 | } 28 | 29 | .link-arrow { 30 | fill: #dedede; 31 | } 32 | 33 | &.selected { 34 | line.link { 35 | stroke: #097bb5; 36 | } 37 | line.link-overlay { 38 | 39 | stroke-opacity: 0.4; 40 | } 41 | } 42 | 43 | .arrow-marker { 44 | fill: red; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/theme/svg/node.scss: -------------------------------------------------------------------------------- 1 | @import "./../shared/_variables"; 2 | 3 | g.node-group { 4 | cursor: grab; 5 | cursor: -webkit-grab; 6 | cursor:-moz-grab; 7 | 8 | &.selected { 9 | circle.ring { 10 | stroke: #565656; 11 | } 12 | } 13 | 14 | &.fixed { 15 | circle.node { 16 | stroke: #fff; 17 | } 18 | } 19 | &.droppable { 20 | cursor: pointer; 21 | } 22 | &.beeing-dragged { 23 | cursor: grabbing; 24 | cursor: -webkit-grabbing; 25 | cursor:-moz-grabbing; 26 | } 27 | 28 | &:hover { 29 | circle.expander { 30 | display: block; 31 | } 32 | } 33 | } 34 | 35 | circle { 36 | &.node { 37 | stroke-width: 3px; 38 | } 39 | 40 | &.ring { 41 | fill: transparent; 42 | stroke: transparent; 43 | stroke-width: 4px; 44 | transition: stroke 0.2s ease; 45 | } 46 | 47 | &.cursor { 48 | fill: none; 49 | stroke: #ABB1BB; 50 | stroke-width: 1.4px; 51 | 52 | &.hidden { 53 | display: none; 54 | } 55 | } 56 | 57 | &.expander { 58 | display: none; 59 | cursor: pointer; 60 | fill: colors(primary); 61 | } 62 | &.expander.expander-inner { 63 | fill: colors(info); 64 | } 65 | } 66 | 67 | text.label { 68 | fill: #808080; 69 | font-size: 11px; 70 | -webkit-font-smoothing: antialiased; 71 | -moz-osx-font-smoothing: grayscale; 72 | baseline-shift: -3px; 73 | transform: translate(0px, 30px); 74 | text-anchor: middle; 75 | } 76 | 77 | // .dragged, .beeing-dragged { 78 | // pointer-events: none; 79 | // } 80 | -------------------------------------------------------------------------------- /src/theme/svg/svg.scss: -------------------------------------------------------------------------------- 1 | svg.dragging-around-fix { 2 | cursor: pointer 3 | } 4 | 5 | svg text { 6 | -webkit-user-select: none; 7 | -moz-user-select: none; 8 | -ms-user-select: none; 9 | user-select: none; 10 | } 11 | svg text::selection { 12 | background: none; 13 | } 14 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /support/neo4j-js.conf.dist: -------------------------------------------------------------------------------- 1 | 2 | ServerAdmin webmaster@dummy-host.example.com 3 | DocumentRoot "/Users/adadgio/WebServer/projects/adadgio/neo4j-js-ng2/dist" 4 | ServerName neo4j-js-ng2.dev 5 | ServerAlias www.neo4j-js-ng2.dev 6 | 7 | 8 | AllowOverride All 9 | Require all granted 10 | Allow from All 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": false, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "eofline": true, 15 | "forin": true, 16 | "import-blacklist": [ 17 | true, 18 | "rxjs", 19 | "rxjs/Rx" 20 | ], 21 | "import-spacing": true, 22 | "indent": [ 23 | true, 24 | "spaces" 25 | ], 26 | "interface-over-type-literal": true, 27 | "label-position": true, 28 | "max-line-length": [ 29 | true, 30 | 140 31 | ], 32 | "member-access": false, 33 | "member-ordering": [ 34 | true, 35 | { 36 | "order": [ 37 | "static-field", 38 | "instance-field", 39 | "static-method", 40 | "instance-method" 41 | ] 42 | } 43 | ], 44 | "no-arg": true, 45 | "no-bitwise": true, 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-construct": true, 55 | "no-debugger": true, 56 | "no-duplicate-super": true, 57 | "no-empty": false, 58 | "no-empty-interface": true, 59 | "no-eval": true, 60 | "no-inferrable-types": [ 61 | true, 62 | "ignore-params" 63 | ], 64 | "no-misused-new": true, 65 | "no-non-null-assertion": true, 66 | "no-shadowed-variable": true, 67 | "no-string-literal": false, 68 | "no-string-throw": true, 69 | "no-switch-case-fall-through": true, 70 | "no-trailing-whitespace": true, 71 | "no-unnecessary-initializer": true, 72 | "no-unused-expression": true, 73 | "no-use-before-declare": true, 74 | "no-var-keyword": true, 75 | "object-literal-sort-keys": false, 76 | "one-line": [ 77 | true, 78 | "check-open-brace", 79 | "check-catch", 80 | "check-else", 81 | "check-whitespace" 82 | ], 83 | "prefer-const": true, 84 | "quotemark": [ 85 | true, 86 | "single" 87 | ], 88 | "radix": true, 89 | "semicolon": [ 90 | true, 91 | "always" 92 | ], 93 | "triple-equals": [ 94 | true, 95 | "allow-null-check" 96 | ], 97 | "typedef-whitespace": [ 98 | true, 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | } 106 | ], 107 | "typeof-compare": true, 108 | "unified-signatures": true, 109 | "variable-name": false, 110 | "whitespace": [ 111 | true, 112 | "check-branch", 113 | "check-decl", 114 | "check-operator", 115 | "check-separator", 116 | "check-type" 117 | ], 118 | "directive-selector": [ 119 | true, 120 | "attribute", 121 | "app", 122 | "camelCase" 123 | ], 124 | "component-selector": [ 125 | true, 126 | "element", 127 | "app", 128 | "kebab-case" 129 | ], 130 | "use-input-property-decorator": true, 131 | "use-output-property-decorator": true, 132 | "use-host-property-decorator": true, 133 | "no-input-rename": true, 134 | "no-output-rename": true, 135 | "use-life-cycle-interface": true, 136 | "use-pipe-transform-interface": true, 137 | "component-class-suffix": true, 138 | "directive-class-suffix": true, 139 | "invoke-injectable": true 140 | } 141 | } 142 | --------------------------------------------------------------------------------