From ab6d8617ac73a072836a1c571874e4061848bfca Mon Sep 17 00:00:00 2001 From: Djuri Baars Date: Mon, 17 Jan 2022 12:10:57 +0100 Subject: [PATCH] Implemented visual page --- package.json | 7 + src/app/app.module.ts | 3 + .../components/overview/overview.component.ts | 4 +- .../components/settings/settings.component.ts | 20 +- .../components/visual/visual.component.html | 67 +- src/app/components/visual/visual.component.ts | 10 +- src/app/models/export_file.enum.ts | 6 + .../edit-ring-order.component.html | 12 + .../edit-ring-order.component.scss | 0 .../edit-ring-order.component.spec.ts | 25 + .../edit-ring-order.component.ts | 48 + .../file-exporter.component.html | 8 + .../file-exporter.component.scss | 0 .../file-exporter.component.spec.ts | 25 + .../file-exporter/file-exporter.component.ts | 31 + .../node-connections.component.html | 2 + .../node-connections.component.scss | 5 + .../node-connections.component.spec.ts | 25 + .../node-connections.component.ts | 120 ++ src/app/partials/partials.module.ts | 37 + .../reorder-participants.component.html | 18 + .../reorder-participants.component.scss | 0 .../reorder-participants.component.spec.ts | 25 + .../reorder-participants.component.ts | 57 + src/app/reducers/node-owner.reducer.ts | 13 +- src/app/services/file.service.ts | 88 +- src/app/services/ln-data.service.ts | 10 +- src/app/services/notification.service.ts | 15 +- src/app/shared/shared.module.ts | 2 + src/app/utils/utils.ts | 31 +- .../vis/network/vis-network.directive.spec.ts | 8 + src/app/vis/network/vis-network.directive.ts | 135 ++ .../vis/network/vis-network.service.spec.ts | 16 + src/app/vis/network/vis-network.service.ts | 1134 +++++++++++++++++ src/app/vis/vis.module.ts | 33 + src/polyfills.ts | 6 +- src/style/dragula.scss | 26 + src/styles.scss | 3 + yarn.lock | 85 +- 39 files changed, 2104 insertions(+), 56 deletions(-) create mode 100644 src/app/models/export_file.enum.ts create mode 100644 src/app/partials/edit-ring-order/edit-ring-order.component.html create mode 100644 src/app/partials/edit-ring-order/edit-ring-order.component.scss create mode 100644 src/app/partials/edit-ring-order/edit-ring-order.component.spec.ts create mode 100644 src/app/partials/edit-ring-order/edit-ring-order.component.ts create mode 100644 src/app/partials/file-exporter/file-exporter.component.html create mode 100644 src/app/partials/file-exporter/file-exporter.component.scss create mode 100644 src/app/partials/file-exporter/file-exporter.component.spec.ts create mode 100644 src/app/partials/file-exporter/file-exporter.component.ts create mode 100644 src/app/partials/node-connections/node-connections.component.html create mode 100644 src/app/partials/node-connections/node-connections.component.scss create mode 100644 src/app/partials/node-connections/node-connections.component.spec.ts create mode 100644 src/app/partials/node-connections/node-connections.component.ts create mode 100644 src/app/partials/partials.module.ts create mode 100644 src/app/partials/reorder-participants/reorder-participants.component.html create mode 100644 src/app/partials/reorder-participants/reorder-participants.component.scss create mode 100644 src/app/partials/reorder-participants/reorder-participants.component.spec.ts create mode 100644 src/app/partials/reorder-participants/reorder-participants.component.ts create mode 100644 src/app/vis/network/vis-network.directive.spec.ts create mode 100644 src/app/vis/network/vis-network.directive.ts create mode 100644 src/app/vis/network/vis-network.service.spec.ts create mode 100644 src/app/vis/network/vis-network.service.ts create mode 100644 src/app/vis/vis.module.ts create mode 100644 src/style/dragula.scss diff --git a/package.json b/package.json index 1b48912..0ea5e84 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser": "~13.1.0", "@angular/platform-browser-dynamic": "~13.1.0", "@angular/router": "~13.1.0", + "@egjs/hammerjs": "^2.0.17", "@ng-bootstrap/ng-bootstrap": "11.0.0", "@ngrx/effects": "^13.0.2", "@ngrx/entity": "^13.0.2", @@ -29,10 +30,16 @@ "bootstrap": "^4.6.0", "bootstrap-icons": "^1.7.2", "d3": "^7.3.0", + "keycharm": "^0.4.0", + "ng2-dragula": "^2.1.1", "ngrx-store-localstorage": "^12.0.1", "rxjs": "7.5.2", "socket.io-client": "^4.4.1", + "timsort": "^0.3.0", "tslib": "^2.3.0", + "vis-data": "^7.1.2", + "vis-network": "^9.1.0", + "vis-util": "^5.0.2", "zone.js": "~0.11.4" }, "devDependencies": { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index ca37c70..a74ac75 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -19,6 +19,8 @@ import { SharedModule } from './shared/shared.module'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ParticipantTableComponent } from './components/participant-table/participant-table.component'; import { ParticipantRingComponent } from './components/participant-ring/participant-ring.component'; +import { PartialsModule } from './partials/partials.module'; +import { VisModule } from './vis/vis.module'; @NgModule({ declarations: [ @@ -33,6 +35,7 @@ import { ParticipantRingComponent } from './components/participant-ring/particip imports: [ SharedModule, LayoutModule, + PartialsModule, BrowserModule, AppRoutingModule, FormsModule, diff --git a/src/app/components/overview/overview.component.ts b/src/app/components/overview/overview.component.ts index 9874b0a..7a6f63e 100644 --- a/src/app/components/overview/overview.component.ts +++ b/src/app/components/overview/overview.component.ts @@ -18,7 +18,7 @@ import * as fromRoot from '../../reducers'; templateUrl: './overview.component.html', styleUrls: ['./overview.component.scss'], }) -export class OverviewComponent implements OnInit, OnDestroy { +export class OverviewComponent implements OnDestroy { viewMode!: string; nodeOwners$!: Observable; settings$!: Observable; @@ -53,8 +53,6 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } - ngOnInit(): void {} - ngOnDestroy(): void { this.lnData.channelSocket.emit('unsubscribe_all'); } diff --git a/src/app/components/settings/settings.component.ts b/src/app/components/settings/settings.component.ts index 650da2c..caf30c3 100644 --- a/src/app/components/settings/settings.component.ts +++ b/src/app/components/settings/settings.component.ts @@ -1,12 +1,15 @@ import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { loadNodeOwners } from 'src/app/actions/node-owner.actions'; import { NodeOwner } from 'src/app/models/node-owner.model'; import { RingSetting } from 'src/app/models/ring-setting.model'; import { SettingState } from 'src/app/reducers/setting.reducer'; import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors'; import { selectRingSettings } from 'src/app/selectors/ring-setting.selectors'; import { selectSettings } from 'src/app/selectors/setting.selectors'; +import { FileService } from 'src/app/services/file.service'; +import { NotificationService } from 'src/app/services/notification.service'; import { environment } from 'src/environments/environment'; import * as fromRoot from '../../reducers'; @@ -33,7 +36,11 @@ export class SettingsComponent { ringName: any = ''; ringSize!: number; - constructor(private store: Store) { + constructor( + private file: FileService, + private notification: NotificationService, + private store: Store + ) { this.store.select(selectSettings).subscribe((settings: SettingState) => { this.settings = settings; }); @@ -42,7 +49,16 @@ export class SettingsComponent { this.nodeOwners$ = this.store.select(selectNodeOwners); } - loadSettings(item: any) {} + loadSettings(item: RingSetting) { + console.log('load', item); + + this.pubkeysText = this.file.convertToCsv(item.ringParticipants); + + this.store.dispatch(loadNodeOwners({ nodeOwners: item.ringParticipants })); + + + this.notification.showSuccess(`Ring load ${item.cleanRingName} successful`); + } removeSettings(item: any) {} diff --git a/src/app/components/visual/visual.component.html b/src/app/components/visual/visual.component.html index 82f8688..a5b6f4e 100644 --- a/src/app/components/visual/visual.component.html +++ b/src/app/components/visual/visual.component.html @@ -1,42 +1,31 @@
-
-

Ring Designer -
- - -
-

-
-
-
- -
- -
+
+

Node connections +
+ + +
+

+ +
+
+ + +
+
+ +
+
+ +
+
+
+
-
- \ No newline at end of file + \ No newline at end of file diff --git a/src/app/components/visual/visual.component.ts b/src/app/components/visual/visual.component.ts index 9d1453d..e8c1e7d 100644 --- a/src/app/components/visual/visual.component.ts +++ b/src/app/components/visual/visual.component.ts @@ -1,7 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { setViewMode } from 'src/app/actions/setting.actions'; +import { NodeOwner } from 'src/app/models/node-owner.model'; import { SettingState } from 'src/app/reducers/setting.reducer'; +import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors'; import { selectSettings } from 'src/app/selectors/setting.selectors'; import * as fromRoot from '../../reducers'; @@ -12,6 +15,8 @@ import * as fromRoot from '../../reducers'; }) export class VisualComponent { settings$!: Observable; + nodeOwners$!: Observable; + settings!: SettingState; constructor( @@ -19,13 +24,14 @@ export class VisualComponent { ) { this.settings$ = this.store.select(selectSettings); + this.nodeOwners$ = this.store.select(selectNodeOwners); this.settings$.subscribe((settings: SettingState) => { this.settings = settings; }) } - viewChange($event: any) { - + viewChange($event: string) { + this.store.dispatch(setViewMode($event)); } } diff --git a/src/app/models/export_file.enum.ts b/src/app/models/export_file.enum.ts new file mode 100644 index 0000000..d5c4d1d --- /dev/null +++ b/src/app/models/export_file.enum.ts @@ -0,0 +1,6 @@ +export enum ExportFile { + RingToolsPubKeysTxt = 'pubkeys.txt', + RingToolsChannelsTxt = 'channels.txt', + IgniterPubkeys = 'igniter_pubkeys.txt', + IgniterSh = 'igniter.sh' +} \ No newline at end of file diff --git a/src/app/partials/edit-ring-order/edit-ring-order.component.html b/src/app/partials/edit-ring-order/edit-ring-order.component.html new file mode 100644 index 0000000..89e9e24 --- /dev/null +++ b/src/app/partials/edit-ring-order/edit-ring-order.component.html @@ -0,0 +1,12 @@ +

Ring order

+
    +
  • + + {{ getCbUsername(s) }} + + + {{ s.nodename }} + + {{ s.nodename }} +
  • +
\ No newline at end of file diff --git a/src/app/partials/edit-ring-order/edit-ring-order.component.scss b/src/app/partials/edit-ring-order/edit-ring-order.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/partials/edit-ring-order/edit-ring-order.component.spec.ts b/src/app/partials/edit-ring-order/edit-ring-order.component.spec.ts new file mode 100644 index 0000000..98e0c70 --- /dev/null +++ b/src/app/partials/edit-ring-order/edit-ring-order.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditRingOrderComponent } from './edit-ring-order.component'; + +describe('EditRingOrderComponent', () => { + let component: EditRingOrderComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditRingOrderComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EditRingOrderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/partials/edit-ring-order/edit-ring-order.component.ts b/src/app/partials/edit-ring-order/edit-ring-order.component.ts new file mode 100644 index 0000000..7f11c7e --- /dev/null +++ b/src/app/partials/edit-ring-order/edit-ring-order.component.ts @@ -0,0 +1,48 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { DragulaService } from 'ng2-dragula'; +import { Observable, Subscription } from 'rxjs'; +import { NodeOwner } from 'src/app/models/node-owner.model'; +import { RingSetting } from 'src/app/models/ring-setting.model'; +import { SettingState } from 'src/app/reducers/setting.reducer'; +import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors'; +import { selectSettings } from 'src/app/selectors/setting.selectors'; +import * as fromRoot from '../../reducers'; + +@Component({ + selector: 'app-edit-ring-order', + templateUrl: './edit-ring-order.component.html', + styleUrls: ['./edit-ring-order.component.scss'], +}) +export class EditRingOrderComponent implements OnDestroy { + nodeOwners$: Observable; + nodeOwners: NodeOwner[] = []; + ringSettings$!: Observable; + settings$: Observable; + + subs = new Subscription(); + + constructor( + private store: Store, + private dragulaService: DragulaService + ) { + this.nodeOwners$ = this.store.select(selectNodeOwners); + this.settings$ = this.store.select(selectSettings) + + this.nodeOwners$.subscribe((data) => { + this.nodeOwners = data; + }) + } + + getCbUsername(nodeOwner: NodeOwner) { + if (nodeOwner.username == 'None' || nodeOwner.username == 'undefined') { + return nodeOwner.first_name; + } + return `${nodeOwner.first_name} (@${nodeOwner.username})`; + } + + ngOnDestroy(): void { + this.subs.unsubscribe(); + this.dragulaService.destroy('PARTICIPANTS'); + } +} diff --git a/src/app/partials/file-exporter/file-exporter.component.html b/src/app/partials/file-exporter/file-exporter.component.html new file mode 100644 index 0000000..0cffd08 --- /dev/null +++ b/src/app/partials/file-exporter/file-exporter.component.html @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/app/partials/file-exporter/file-exporter.component.scss b/src/app/partials/file-exporter/file-exporter.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/partials/file-exporter/file-exporter.component.spec.ts b/src/app/partials/file-exporter/file-exporter.component.spec.ts new file mode 100644 index 0000000..af8fe1e --- /dev/null +++ b/src/app/partials/file-exporter/file-exporter.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FileExporterComponent } from './file-exporter.component'; + +describe('FileExporterComponent', () => { + let component: FileExporterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FileExporterComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FileExporterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/partials/file-exporter/file-exporter.component.ts b/src/app/partials/file-exporter/file-exporter.component.ts new file mode 100644 index 0000000..766d919 --- /dev/null +++ b/src/app/partials/file-exporter/file-exporter.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { ExportFile } from 'src/app/models/export_file.enum'; +import { FileService } from 'src/app/services/file.service'; + +@Component({ + selector: 'app-file-exporter', + templateUrl: './file-exporter.component.html', + styleUrls: ['./file-exporter.component.scss'] +}) +export class FileExporterComponent { + + constructor( + private file: FileService + ) { } + + persistOrder() { + console.log('Method not implemented'); +} + + downloadChannelsTxt() { + this.file.generateAndDownload(ExportFile.RingToolsChannelsTxt); + } + + downloadPubKeysTxt() { + this.file.generateAndDownload(ExportFile.RingToolsPubKeysTxt); + } + + downloadIgniterPubkeys() { + this.file.generateAndDownload(ExportFile.IgniterPubkeys); + } +} diff --git a/src/app/partials/node-connections/node-connections.component.html b/src/app/partials/node-connections/node-connections.component.html new file mode 100644 index 0000000..0d85644 --- /dev/null +++ b/src/app/partials/node-connections/node-connections.component.html @@ -0,0 +1,2 @@ +
\ No newline at end of file diff --git a/src/app/partials/node-connections/node-connections.component.scss b/src/app/partials/node-connections/node-connections.component.scss new file mode 100644 index 0000000..558f227 --- /dev/null +++ b/src/app/partials/node-connections/node-connections.component.scss @@ -0,0 +1,5 @@ +.network-canvas { + width: 100%; + height: 600px; + border: 1px solid lightgray; + } \ No newline at end of file diff --git a/src/app/partials/node-connections/node-connections.component.spec.ts b/src/app/partials/node-connections/node-connections.component.spec.ts new file mode 100644 index 0000000..2b00e81 --- /dev/null +++ b/src/app/partials/node-connections/node-connections.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NodeConnectionsComponent } from './node-connections.component'; + +describe('NodeConnectionsComponent', () => { + let component: NodeConnectionsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NodeConnectionsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NodeConnectionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/partials/node-connections/node-connections.component.ts b/src/app/partials/node-connections/node-connections.component.ts new file mode 100644 index 0000000..a75aae9 --- /dev/null +++ b/src/app/partials/node-connections/node-connections.component.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { LightningNode } from 'src/app/models/lightning_node.model'; +import { NodeInfo } from 'src/app/models/node-info.model'; +import { NodeOwner } from 'src/app/models/node-owner.model'; +import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors'; +import { LnDataService } from 'src/app/services/ln-data.service'; +import { + Data, + DataSet, + Edge, + Node, + Options, + VisNetworkService, +} from 'src/app/vis/vis.module'; +import * as fromRoot from '../../reducers'; + +@Component({ + selector: 'app-node-connections', + templateUrl: './node-connections.component.html', + styleUrls: ['./node-connections.component.scss'], +}) +export class NodeConnectionsComponent implements OnInit { + public visNetwork: string = 'networkId1'; + public visNetworkData!: Data; + public nodes!: DataSet; + public edges!: DataSet; + public visNetworkOptions!: Options; + + nodeOwners: NodeOwner[] = []; + nodeOwners$: Observable; + + constructor( + private store: Store, + private lnData: LnDataService, + private visNetworkService: VisNetworkService + ) { + this.nodeOwners$ = this.store.select(selectNodeOwners); + + this.nodeOwners$.subscribe((data) => { + this.nodeOwners = data; + }); + } + + ngOnInit(): void { + this.nodes = new DataSet(); + this.edges = new DataSet([]); + this.visNetworkData = { + nodes: this.nodes, + edges: this.edges, + }; + + this.visNetworkOptions = { + interaction: { hover: true }, + manipulation: { + enabled: true, + }, + layout: { + randomSeed: 681154853, + }, + edges: {}, + }; + + // if (this.ringData.isLoaded) { + this.buildNodes(); + // } else { + // this.ringData.isReady$.subscribe(() => { + // this.buildNodes(); + // }); + //} + } + + public bestFit() { + this.visNetworkService.bestFit(this.visNetwork, this.nodes); + } + + buildNodes() { + for (let node of this.nodeOwners) { + let data = this.lnData + .getNodeInfo(node.pub_key) + .subscribe((data: NodeInfo) => { + let label; + + //if (this.viewMode == 'node') { + label = node.nodename; + //} else { + // label = node.username_or_name + // } + + let nodeInfo: Node = { + id: data.node.pub_key, + color: data.node.color, + label: data.node.alias, + }; + + this.nodes.add(nodeInfo); + + channelloop: for (let edge of data.channels) { + if (!this.edges.get(edge.channel_id)) { + let e: any = { + id: edge.channel_id, + from: edge.node1_pub, + to: edge.node2_pub, + dashes: true, + }; + + if (!edge.node1_policy || !edge.node2_policy) { + e.label = 'no info'; + e.color = '#ffcc00'; + } + + this.edges.add(e); + continue channelloop; + } + } + }); + } + } +} diff --git a/src/app/partials/partials.module.ts b/src/app/partials/partials.module.ts new file mode 100644 index 0000000..c23dd7f --- /dev/null +++ b/src/app/partials/partials.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FileExporterComponent } from './file-exporter/file-exporter.component'; +import { EditRingOrderComponent } from './edit-ring-order/edit-ring-order.component'; +import { DragulaModule } from 'ng2-dragula'; +import { ReorderParticipantsComponent } from './reorder-participants/reorder-participants.component'; +import { SharedModule } from '../shared/shared.module'; +import { FormsModule } from '@angular/forms'; +import { NodeConnectionsComponent } from './node-connections/node-connections.component'; +import { VisModule } from '../vis/vis.module'; +import { HttpClientModule } from '@angular/common/http'; + + + +@NgModule({ + declarations: [ + FileExporterComponent, + EditRingOrderComponent, + ReorderParticipantsComponent, + NodeConnectionsComponent + ], + imports: [ + HttpClientModule, + VisModule, + FormsModule, + SharedModule, + DragulaModule.forRoot(), + CommonModule + ], + exports: [ + FileExporterComponent, + EditRingOrderComponent, + ReorderParticipantsComponent, + NodeConnectionsComponent + ] +}) +export class PartialsModule { } diff --git a/src/app/partials/reorder-participants/reorder-participants.component.html b/src/app/partials/reorder-participants/reorder-participants.component.html new file mode 100644 index 0000000..5c857ed --- /dev/null +++ b/src/app/partials/reorder-participants/reorder-participants.component.html @@ -0,0 +1,18 @@ +
+ + + + +
\ No newline at end of file diff --git a/src/app/partials/reorder-participants/reorder-participants.component.scss b/src/app/partials/reorder-participants/reorder-participants.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/partials/reorder-participants/reorder-participants.component.spec.ts b/src/app/partials/reorder-participants/reorder-participants.component.spec.ts new file mode 100644 index 0000000..7445580 --- /dev/null +++ b/src/app/partials/reorder-participants/reorder-participants.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReorderParticipantsComponent } from './reorder-participants.component'; + +describe('ReorderParticipantsComponent', () => { + let component: ReorderParticipantsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ReorderParticipantsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReorderParticipantsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/partials/reorder-participants/reorder-participants.component.ts b/src/app/partials/reorder-participants/reorder-participants.component.ts new file mode 100644 index 0000000..2150d67 --- /dev/null +++ b/src/app/partials/reorder-participants/reorder-participants.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { loadNodeOwners } from 'src/app/actions/node-owner.actions'; +import { NodeOwner } from 'src/app/models/node-owner.model'; +import { RingSetting } from 'src/app/models/ring-setting.model'; +import { SettingState } from 'src/app/reducers/setting.reducer'; +import { selectNodeOwners } from 'src/app/selectors/node-owner.selectors'; +import { selectSettings } from 'src/app/selectors/setting.selectors'; +import { NotificationService } from 'src/app/services/notification.service'; +import * as fromRoot from '../../reducers'; + +@Component({ + selector: 'app-reorder-participants', + templateUrl: './reorder-participants.component.html', + styleUrls: ['./reorder-participants.component.scss'] +}) +export class ReorderParticipantsComponent{ + nodeOwners$: Observable; + nodeOwners: NodeOwner[] = []; + ringSettings$!: Observable; + settings$: Observable; + selectedIgniter: any; + + + constructor( + private store: Store, + private notificiation: NotificationService, + ) { + this.nodeOwners$ = this.store.select(selectNodeOwners); + this.settings$ = this.store.select(selectSettings) + + this.nodeOwners$.subscribe((data) => { + this.nodeOwners = data; + }) + } + + reorderIgniter() { + let idx = this.nodeOwners.indexOf(this.selectedIgniter); + if (idx == -1) { + this.notificiation.show('No igniter selected', { classname: 'bg-danger' }); + return; + } + + let partsUntilIgniter = this.nodeOwners.slice(0, (idx + 1)); + let partsFromIgniter = this.nodeOwners.slice((idx+1)); + let newOrder = partsFromIgniter.concat(partsUntilIgniter); + try { + this.store.dispatch(loadNodeOwners({ nodeOwners: newOrder })) +// this.ringData.saveRingSettings(this.nodeOwners); + this.notificiation.show('Node reorder persisted', { classname: 'bg-success' }); + } catch (e) { + this.notificiation.show('Error reordering', { classname: 'bg-danger' }); + } + } + +} diff --git a/src/app/reducers/node-owner.reducer.ts b/src/app/reducers/node-owner.reducer.ts index ddf7c2e..da3d7f4 100644 --- a/src/app/reducers/node-owner.reducer.ts +++ b/src/app/reducers/node-owner.reducer.ts @@ -7,12 +7,21 @@ export const nodeOwnersFeatureKey = 'nodeOwners'; export interface NodeOwnersState extends EntityState { // additional entities state properties + pub_key: string | null + } -export const adapter: EntityAdapter = createEntityAdapter(); +export function selectNodeOwner(a: NodeOwner): string { + //In this case this would be optional since primary key is id + return a.pub_key; +} + +export const adapter: EntityAdapter = createEntityAdapter({ + selectId: selectNodeOwner, +}); export const initialState: NodeOwnersState = adapter.getInitialState({ - // additional entity state properties + pub_key: '' }); export const nodeOwnersReducer = createReducer( diff --git a/src/app/services/file.service.ts b/src/app/services/file.service.ts index 3be737a..b03fdfc 100644 --- a/src/app/services/file.service.ts +++ b/src/app/services/file.service.ts @@ -1,9 +1,93 @@ import { Injectable } from '@angular/core'; +import { ExportFile } from '../models/export_file.enum'; +import { NodeOwner } from '../models/node-owner.model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class FileService { + constructor() {} - constructor() { } + convertToCsv(ringParticipants:NodeOwner[], header: boolean = true) { + let pkContents = ''; + + if (header) + pkContents = 'user_name,nodename,pub_key,new,handle,capacity_sat\r\n'; + + for (let p of ringParticipants) { + pkContents += `${p.first_name},${p.nodename},${p.pub_key},false,${p.username},0\r\n`; + } + + return pkContents; + } + + /** + * CSV + * @returns + */ + parseCsvToType(contents: string) { + let segmentLines = contents.split('\n'); + let segments: NodeOwner[] = []; + for (let line of segmentLines.slice(1)) { + let parts = line.split(','); + if (parts.length > 1) { + let nodeOwner: NodeOwner = { + first_name: parts[0], + nodename: parts[1], + pub_key: parts[2], + username: parts[4], + username_or_name: undefined + }; + segments.push(nodeOwner); + } + } + return segments; + } + + doDownloadFileWithData(data: string, filename: string) { + let element = document.createElement('a'); + element.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(data) + ); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + } + + convertToExportFormat(ringParticipants: NodeOwner[]) { + let pkContents = ''; + let add = ''; + for (let p of ringParticipants) { + pkContents += `${add}${p.first_name},${p.username},${p.pub_key},${p.nodename}`; + add = "|"; + } + + return pkContents; + } + + parseNewExportFormat(data: string) { + let segmentLines = data.split('|'); + let segments: NodeOwner[] = []; + for (let line of segmentLines) { + let parts = line.split(','); + if (parts.length > 1) { + let nodeOwner:NodeOwner = { + first_name: parts[0], + username: parts[1], + pub_key: parts[2], + nodename: parts[3], + username_or_name: undefined + }; + segments.push(nodeOwner); + } + } + return segments; + } + + generateAndDownload(file_template: ExportFile) { + + } } diff --git a/src/app/services/ln-data.service.ts b/src/app/services/ln-data.service.ts index 71f37cf..77810ca 100644 --- a/src/app/services/ln-data.service.ts +++ b/src/app/services/ln-data.service.ts @@ -1,5 +1,7 @@ import { Injectable } from '@angular/core'; import { io, Socket } from 'socket.io-client'; +import { NodeInfo } from '../models/node-info.model'; +import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' @@ -9,10 +11,16 @@ export class LnDataService { nodeSocket: Socket; channelSocket: Socket; - constructor() { + constructor(private http: HttpClient) { const url = 'http://localhost:7464'; this.socket = io(url) this.nodeSocket = io(`${url}/node`) this.channelSocket = io(`${url}/channel`) } + + getNodeInfo(pubKey: string) { + return this.http.get( + `http://localhost:7464/node/${pubKey}` + ); + } } diff --git a/src/app/services/notification.service.ts b/src/app/services/notification.service.ts index add74f5..0531972 100644 --- a/src/app/services/notification.service.ts +++ b/src/app/services/notification.service.ts @@ -1,9 +1,22 @@ import { Injectable } from '@angular/core'; +import { ToastService } from '../shared/notification/toast/toast.service'; @Injectable({ providedIn: 'root' }) export class NotificationService { - constructor() { } + constructor(private toast: ToastService) { + + } + + showSuccess(message: string) { + this.toast.show(`${message}`, { + classname: 'bg-success', + }); + } + + show(message: string, options: any) { + this.toast.show(`${message}`, options); + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index cb1b569..a2133c1 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -2,6 +2,8 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ToastComponent } from './notification/toast/toast.component'; import { NgbToastModule } from '@ng-bootstrap/ng-bootstrap'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; diff --git a/src/app/utils/utils.ts b/src/app/utils/utils.ts index 4b9b1e3..97cbcaf 100644 --- a/src/app/utils/utils.ts +++ b/src/app/utils/utils.ts @@ -8,4 +8,33 @@ const colorScale = d3 // @ts-ignore .interpolate(d3.interpolateHcl); - export { colorScale }; +const copyToClipboard = (data: string) => { + const listener = (e: ClipboardEvent) => { + if (!e.clipboardData) return; + e.clipboardData.setData('text/plain', data); + e.preventDefault(); + document.removeEventListener('copy', listener); + }; + document.addEventListener('copy', listener); + document.execCommand('copy'); +}; + +const stringToBoolean = (string: string) => { + switch (string.toLowerCase().trim()) { + case 'true': + case 'yes': + case '1': + return true; + + case 'false': + case 'no': + case '0': + case null: + return false; + + default: + return Boolean(string); + } +}; + +export { colorScale, copyToClipboard, stringToBoolean }; diff --git a/src/app/vis/network/vis-network.directive.spec.ts b/src/app/vis/network/vis-network.directive.spec.ts new file mode 100644 index 0000000..73e4b14 --- /dev/null +++ b/src/app/vis/network/vis-network.directive.spec.ts @@ -0,0 +1,8 @@ +import { VisNetworkDirective } from './vis-network.directive'; + +describe('VisNetworkDirective', () => { + it('should create an instance', () => { + const directive = new VisNetworkDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/vis/network/vis-network.directive.ts b/src/app/vis/network/vis-network.directive.ts new file mode 100644 index 0000000..acf751f --- /dev/null +++ b/src/app/vis/network/vis-network.directive.ts @@ -0,0 +1,135 @@ +import { Directive, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChange } from '@angular/core'; +import { Data, Options } from 'vis-network/esnext'; + +import { VisNetworkService } from './vis-network.service'; + +/** + * Use this directive with a div container to show network data. + * + * @export + * @class VisNetworkDirective + * @implements {OnInit} + * @implements {OnDestroy} + * @implements {OnChanges} + */ +@Directive({ + selector: '[visNetwork]', +}) +export class VisNetworkDirective implements OnInit, OnDestroy, OnChanges { + /** + * The name or identifier of the network (must be unique in your application). + * This property is used once on init and must not be changed. + * + * @type {string} + * @memberOf VisNetworkDirective + */ + @Input('visNetwork') + public visNetwork!: string; + + /** + * The data that will be used to create the network. + * Changes to the nodes or edges property won't be detected but + * changes to the reference of this object. + * Changes lead to a call to setData of this network instance. + * + * @type {Data} + * @memberOf VisNetworkDirective + */ + @Input() + public visNetworkData!: Data; + + /** + * The options that will be used with this network instance. + * Only reference changes to the whole options object will be detected + * but not changes to properties. + * Changes lead to a call to setOptions of the network instance. + * + * @type {VisOptions} + * @memberOf VisNetworkDirective + */ + @Input() + public visNetworkOptions!: Options; + + /** + * This event will be raised when the network is initialized. + * At this point of time the network is successfully registered + * with the VisNetworkService and you can register to events. + * The event data is the name of the network as a string. + * + * @type {EventEmitter} + * @memberOf VisNetworkDirective + */ + @Output() + public initialized: EventEmitter = new EventEmitter(); + + private visNetworkContainer: any; + private isInitialized: boolean = false; + + /** + * Creates an instance of VisNetworkDirective. + * + * @param {ElementRef} elementRef The HTML element reference. + * @param {VisNetworkService} visNetworkService The VisNetworkService. + * + * @memberOf VisNetworkDirective + */ + public constructor(private elementRef: ElementRef, private visNetworkService: VisNetworkService) { + this.visNetworkContainer = elementRef.nativeElement; + } + + /** + * Create the network when at least visNetwork and visData + * are defined. + * + * @memberOf VisNetworkDirective + */ + public ngOnInit(): void { + if (!this.isInitialized && this.visNetwork && this.visNetworkData) { + this.createNetwork(); + } + } + + /** + * Update the network data or options on reference changes to + * the visData or visOptions properties. + * + * @param {{[propName: string]: SimpleChange}} changes + * + * @memberOf VisNetworkDirective + */ + public ngOnChanges(changes: { [propName: string]: SimpleChange }): void { + if (!this.isInitialized && this.visNetwork && this.visNetworkData) { + this.createNetwork(); + } + + for (const propertyName in changes) { + if (changes.hasOwnProperty(propertyName)) { + const change = changes[propertyName]; + if (!change.isFirstChange()) { + if (propertyName === 'visData') { + this.visNetworkService.setData(this.visNetwork, changes[propertyName].currentValue); + } + if (propertyName === 'visOptions') { + this.visNetworkService.setOptions(this.visNetwork, changes[propertyName].currentValue); + } + } + } + } + } + + /** + * Calls the destroy function for this network instance. + * + * @memberOf VisNetworkDirective + */ + public ngOnDestroy(): void { + this.isInitialized = false; + this.visNetworkService.destroy(this.visNetwork); + } + + private createNetwork(): void { + this.visNetworkService.create(this.visNetwork, this.visNetworkContainer, this.visNetworkData, this.visNetworkOptions); + this.isInitialized = true; + this.initialized.emit(this.visNetwork); + } +} \ No newline at end of file diff --git a/src/app/vis/network/vis-network.service.spec.ts b/src/app/vis/network/vis-network.service.spec.ts new file mode 100644 index 0000000..a837b81 --- /dev/null +++ b/src/app/vis/network/vis-network.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { VisNetworkService } from './vis-network.service'; + +describe('VisNetworkService', () => { + let service: VisNetworkService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VisNetworkService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/vis/network/vis-network.service.ts b/src/app/vis/network/vis-network.service.ts new file mode 100644 index 0000000..bad42a6 --- /dev/null +++ b/src/app/vis/network/vis-network.service.ts @@ -0,0 +1,1134 @@ +/* tslint:disable */ +import { EventEmitter, Injectable } from '@angular/core'; +import { DataSet, Node } from 'vis-network'; +import { + BoundingBox, + ClusterOptions, + Data, + EdgeOptions, + FitOptions, + IdType, + MoveToOptions, + Network, + NetworkEvents, + NodeOptions, + OpenClusterOptions, + Options, + Position, +} from 'vis-network/esnext'; + +/** + * A service to create, manage and control Network instances. + * + * @export + * @class NetworkService + */ +@Injectable() +export class VisNetworkService { + /** + * Fired when the user clicks the mouse or taps on a touchscreen device. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public click: EventEmitter = new EventEmitter(); + + /** + * Fired when the user double clicks the mouse or double taps on a touchscreen device. + * Since a double click is in fact 2 clicks, 2 click events are fired, followed by a double click event. + * If you do not want to use the click events if a double click event is fired, + * just check the time between click events before processing them. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public doubleClick: EventEmitter = new EventEmitter(); + + /** + * Fired when the user click on the canvas with the right mouse button. + * The right mouse button does not select by default. + * You can use the method getNodeAt to select the node if you want. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public oncontext: EventEmitter = new EventEmitter(); + + /** + * Fired when the user clicks and holds the mouse or taps and holds on a touchscreen device. + * A click event is also fired in this case. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public hold: EventEmitter = new EventEmitter(); + + /** + * Fired after drawing on the canvas has been completed. + * Can be used to draw on top of the network. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public release: EventEmitter = new EventEmitter(); + + /** + * Fired when the selection has changed by user action. + * This means a node or edge has been selected, added to the selection or deselected. + * All select events are only triggered on click and hold. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public select: EventEmitter = new EventEmitter(); + + /** + * Fired when a node has been selected by the user. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public selectNode: EventEmitter = new EventEmitter(); + + /** + * Fired when a edge has been selected by the user. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public selectEdge: EventEmitter = new EventEmitter(); + + /** + * Fired when a node (or nodes) has (or have) been deselected by the user. + * The previous selection is the list of nodes and edges that were selected before the last user event. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public deselectNode: EventEmitter = new EventEmitter(); + + /** + * Fired when a edge (or edges) has (or have) been deselected by the user. + * The previous selection is the list of nodes and edges that were selected before the last user event. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public deselectEdge: EventEmitter = new EventEmitter(); + + /** + * Fired when starting a drag. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public dragStart: EventEmitter = new EventEmitter(); + + /** + * Fired when dragging node(s) or the view. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public dragging: EventEmitter = new EventEmitter(); + + /** + * Fired when the drag has finished. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public dragEnd: EventEmitter = new EventEmitter(); + + /** + * Fired if the option interaction:{hover:true} is enabled and the mouse hovers over a node. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public hoverNode: EventEmitter = new EventEmitter(); + + /** + * Fired if the option interaction:{hover:true} is enabled and + * the mouse moved away from a node it was hovering over before. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public blurNode: EventEmitter = new EventEmitter(); + + /** + * Fired if the option interaction:{hover:true} is enabled and the mouse hovers over an edge. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public hoverEdge: EventEmitter = new EventEmitter(); + + /** + * Fired if the option interaction:{hover:true} is enabled and + * the mouse moved away from an edge it was hovering over before. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public blurEdge: EventEmitter = new EventEmitter(); + + /** + * Fired when the user zooms in or out. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public zoom: EventEmitter = new EventEmitter(); + + /** + * Fired when the popup (tooltip) is shown. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public showPopup: EventEmitter = new EventEmitter(); + + /** + * Fired when the popup (tooltip) is hidden. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public hidePopup: EventEmitter = new EventEmitter(); + + /** + * Fired when stabilization starts. + * This is also the case when you drag a node and the physics + * simulation restarts to stabilize again. + * Stabilization does not neccesarily imply 'without showing'. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public startStabilizing: EventEmitter = new EventEmitter(); + + /** + * Fired when a multiple of the updateInterval number of iterations is reached. + * This only occurs in the 'hidden' stabilization. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public stabilizationProgress: EventEmitter = new EventEmitter(); + + /** + * Fired when the 'hidden' stabilization finishes. + * This does not necessarily mean the network is stabilized; + * it could also mean that the amount of iterations defined in the options has been reached. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public stabilizationIterationsDone: EventEmitter = new EventEmitter(); + + /** + * Fired when the 'hidden' stabilization finishes. + * This does not necessarily mean the network is stabilized; + * it could also mean that the amount of iterations defined in the options has been reached. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public stabilized: EventEmitter = new EventEmitter(); + + /** + * Fired when the size of the canvas has been resized, + * either by a redraw call when the container div has changed in size, + * a setSize() call with new values or a setOptions() with new width and/or height values. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public resize: EventEmitter = new EventEmitter(); + + /** + * Fired before the redrawing begins. + * The simulation step has completed at this point. + * Can be used to move custom elements before starting drawing the new frame. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public initRedraw: EventEmitter = new EventEmitter(); + + /** + * Fired after the canvas has been cleared, scaled and translated to + * the viewing position but before all edges and nodes are drawn. + * Can be used to draw behind the network. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public beforeDrawing: EventEmitter = new EventEmitter(); + + /** + * Fired after drawing on the canvas has been completed. + * Can be used to draw on top of the network. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public afterDrawing: EventEmitter = new EventEmitter(); + + /** + * Fired when an animation is finished. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public animationFinished: EventEmitter = new EventEmitter(); + + /** + * Fired when a user changes any option in the configurator. + * The options object can be used with the setOptions method or stringified using JSON.stringify(). + * You do not have to manually put the options into the network: this is done automatically. + * You can use the event to store user options in the database. + * + * @type {EventEmitter} + * @memberOf NetworkService + */ + public configChange: EventEmitter = new EventEmitter(); + + private networks: { [id: string]: Network } = {}; + + /** + * Creates a new network instance. + * + * @param {string} visNetwork The network name/identifier. + * @param {HTMLElement} container The HTML element that contains the network view. + * @param {Data} data The initial network nodes and edges. + * @param {Options} [options] The network options. + * + * @throws {Error} Thrown when a network with the same name already exists. + * + * @memberOf NetworkService + */ + public create(visNetwork: string, container: HTMLElement, data: Data, options?: Options): void { + if (this.networks[visNetwork]) { + throw new Error(`Network with id ${visNetwork} already exists.`); + } + + this.networks[visNetwork] = new Network(container, data, options); + } + + /** + * Remove the network from the DOM and remove all Hammer bindings and references. + * + * @param {string} visNetwork The network name/identifier. + * + * @memberOf NetworkService + */ + public destroy(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].destroy(); + delete this.networks[visNetwork]; + } + } + + /** + * Activates an event. + * + * @param {string} visNetwork The network name/identifier. + * @param {NetworkEvents} eventName The event name. + * @param {boolean} preventDefault Stops the default behavior of the event. + * @returns {boolean} Returns true when the event was activated. + * + * @memberOf NetworkService + */ + public on(visNetwork: string, eventName: NetworkEvents, preventDefault?: boolean): boolean { + if (this.networks[visNetwork]) { + /* tslint:disable */ + const that: { [index: string]: any } = this; + /* tslint:enable */ + this.networks[visNetwork].on(eventName, (params: any) => { + const emitter = that[eventName] as EventEmitter; + if (emitter) { + emitter.emit(params ? [visNetwork].concat(params) : visNetwork); + } + if (preventDefault && params.event) { + params.event.preventDefault(); + } + }); + + return true; + } + + return false; + } + + /** + * Deactivates an event. + * + * @param {string} visNetwork The network name/identifier. + * @param {NetworkEvents} eventName The event name. + * + * @memberOf NetworkService + */ + public off(visNetwork: string, eventName: NetworkEvents): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].off(eventName); + } + } + + /** + * Activates an event listener only once. + * After it has taken place, the event listener will be removed. + * + * @param {string} visNetwork The network name/identifier. + * @param {NetworkEvents} eventName The event name. + * @returns {boolean} Returns true when the event was activated. + * + * @memberOf NetworkService + */ + public once(visNetwork: string, eventName: NetworkEvents): boolean { + if (this.networks[visNetwork]) { + /* tslint:disable */ + const that: { [index: string]: any } = this; + /* tslint:disable */ + this.networks[visNetwork].on(eventName, (params: any) => { + const emitter = that[eventName] as EventEmitter; + if (emitter) { + emitter.emit(params ? [visNetwork].concat(params) : visNetwork); + this.off(visNetwork, eventName); + } + }); + + return true; + } + + return false; + } + + /** + * Override all the data in the network. + * If stabilization is enabled in the physics module, + * the network will stabilize again. + * This method is also performed when first initializing the network. + * + * @param {string} visNetwork The network name/identifier. + * @param {Data} data The network data. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public setData(visNetwork: string, data: Data): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].setData(data); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Set the options. + * + * @param {string} visNetwork The network name/identifier. + * @param {Options} options The network options. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public setOptions(visNetwork: string, options: Options): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].setOptions(options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Selects the nodes corresponding to the id's in the input array. + * This method unselects all other objects before selecting its own objects. + * Does not fire events. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType[]} nodeIds The node ids that should be selected. + * @param {boolean} [highlightEdges] If highlightEdges is true or undefined, + * the neighbouring edges will also be selected. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public selectNodes(visNetwork: string, nodeIds: IdType[], highlightEdges?: boolean): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].selectNodes(nodeIds, highlightEdges); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Selects the nodes and edges corresponding to the id's in the input arrays. + * Does not fire events. + * + * @param {string} visNetwork The network name/identifier. + * @param { nodes: IdType[], edges: IdType[] } selection The node and edge ids that should be selected. + * @param { unselectAll?: boolean, highlightEdges?: boolean } [options] + * If unselectAll is true or undefined, the other objects will be deselected. + * If highlightEdges is true or undefined, the neighbouring edges will also be selected. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public setSelection( + visNetwork: string, + selection: { nodes: IdType[]; edges: IdType[] }, + options: { unselectAll?: boolean; highlightEdges?: boolean } = {} + ): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].setSelection(selection, options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Returns an object with selected nodes and edges ids. + * + * @param {string} visNetwork The network name/identifier. + * @returns {{ nodes: IdType[], edges: IdType[] }} + * The selected node and edge ids or undefined when the network does not exist. + * + * @memberOf NetworkService + */ + public getSelection(visNetwork: string): { nodes: IdType[]; edges: IdType[] } | undefined { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getSelection(); + } + return undefined; + } + + /** + * Returns an array of selected node ids. + * + * @param {string} visNetwork The network name/identifier. + * @returns {IdType[]} The selected node ids or undefined when the network does not exist. + * + * @memberOf NetworkService + */ + public getSelectedNodes(visNetwork: string): IdType[] | undefined { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getSelectedNodes(); + } + return undefined; + } + + /** + * Returns an array of selected edge ids. + * + * @param {string} visNetwork The network name/identifier. + * @returns {IdType[]} The selected edge ids or undefined when the network does not exist. + * + * @memberOf NetworkService + */ + public getSelectedEdges(visNetwork: string): IdType[] | undefined { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getSelectedEdges(); + } + return undefined; + } + + /** + * Unselect all objects. + * Does not fire events. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public unselectAll(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].unselectAll(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Zooms out so all nodes fit on the canvas. + * + * @param {string} visNetwork The network name/identifier. + * @param {VisFitOptions} [options] Options to customize. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public fit(visNetwork: string, options?: FitOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].fit(options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + public bestFit(visNetwork: string, nodes: DataSet) { + + this.networks[visNetwork].moveTo({scale:1}); + this.networks[visNetwork].stopSimulation(); + + var bigBB = { top: Infinity, left: Infinity, right: -Infinity, bottom: -Infinity } + nodes.getIds().forEach( (i) => { + var bb = this.networks[visNetwork].getBoundingBox(i); + if (bb.top < bigBB.top) bigBB.top = bb.top; + if (bb.left < bigBB.left) bigBB.left = bb.left; + if (bb.right > bigBB.right) bigBB.right = bb.right; + if (bb.bottom > bigBB.bottom) bigBB.bottom = bb.bottom; + }) + + // @ts-ignore + var canvasWidth = this.networks[visNetwork].canvas.body.container.clientWidth; + // @ts-ignore + var canvasHeight = this.networks[visNetwork].canvas.body.container.clientHeight; + + var scaleX = canvasWidth/(bigBB.right - bigBB.left); + var scaleY = canvasHeight/(bigBB.bottom - bigBB.top); + var scale = scaleX; + if (scale * (bigBB.bottom - bigBB.top) > canvasHeight ) scale = scaleY; + + if (scale>1) scale = 0.9*scale; + + this.networks[visNetwork].moveTo({ + scale: scale, + position: { + x: (bigBB.right + bigBB.left)/2, + y: (bigBB.bottom + bigBB.top)/2 + } + }) + + } + + /** + * Redraw the network. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public redraw(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].redraw(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Go into addNode mode. Having edit mode or manipulation enabled is not required. + * To get out of this mode, call disableEditMode(). The callback functions defined in handlerFunctions still apply. + * To use these methods without having the manipulation GUI, make sure you set enabled to false. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public addNodeMode(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].addNodeMode(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Programatically enable the edit mode. + * Similar effect to pressing the edit button. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public enableEditMode(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].enableEditMode(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Go into addEdge mode. + * The explaination from addNodeMode applies here as well. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public addEdgeMode(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].addEdgeMode(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Programatically disable the edit mode. + * Similar effect to pressing the close icon + * (small cross in the corner of the toolbar). + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public disableEditMode(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].disableEditMode(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Delete selected. + * Having edit mode or manipulation enabled is not required. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public deleteSelected(visNetwork: string): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].deleteSelected(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Makes a cluster. + * + * @param {string} visNetwork The network name/identifier. + * @param {ClusterOptions} [options] The joinCondition function is presented with all nodes. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public cluster(visNetwork: string, options?: ClusterOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].cluster(options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * This method looks at the provided node and makes a cluster of it and all it's connected nodes. + * The behaviour can be customized by proving the options object. + * All options of this object are explained below. + * The joinCondition is only presented with the connected nodes. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} nodeId the id of the node + * @param {ClusterOptions} [options] the cluster options + * + * @memberOf NetworkService + */ + public clusterByConnection(visNetwork: string, nodeId: IdType, options?: ClusterOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].clusterByConnection(nodeId as any, options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * This method checks all nodes in the network and those with a equal or higher + * amount of edges than specified with the hubsize qualify. + * If a hubsize is not defined, the hubsize will be determined as the average + * value plus two standard deviations. + * For all qualifying nodes, clusterByConnection is performed on each of them. + * The options object is described for clusterByConnection and does the same here. + * + * @param {string} visNetwork The network name/identifier. + * @param {number} [hubsize] optional hubsize + * @param {ClusterOptions} [options] optional cluster options + * + * @memberOf NetworkService + */ + public clusterByHubsize(visNetwork: string, hubsize?: number, options?: ClusterOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].clusterByHubsize(hubsize, options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * This method will cluster all nodes with 1 edge with their respective connected node. + * + * @param {string} visNetwork The network name/identifier. + * @param {ClusterOptions} [options] optional cluster options + * + * @memberOf NetworkService + */ + public clusterOutliers(visNetwork: string, options?: ClusterOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].clusterOutliers(options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Nodes can be in clusters. + * Clusters can also be in clusters. + * This function returns an array of nodeIds showing where the node is. + * + * Example: + * cluster 'A' contains cluster 'B', cluster 'B' contains cluster 'C', + * cluster 'C' contains node 'fred'. + * + * network.clustering.findNode('fred') will return ['A','B','C','fred']. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} nodeId the node id. + * @returns {IdType[]} an array of nodeIds showing where the node is + * + * @memberOf NetworkService + */ + public findNode(visNetwork: string, nodeId: IdType): IdType[] { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].findNode(nodeId); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Similar to findNode in that it returns all the edge ids that were + * created from the provided edge during clustering. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} baseEdgeId the base edge id + * @returns {IdType[]} an array of edgeIds + * + * @memberOf NetworkService + */ + public getClusteredEdges(visNetwork: string, baseEdgeId: IdType): IdType[] { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getClusteredEdges(baseEdgeId); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * When a clusteredEdgeId is available, this method will return the original + * baseEdgeId provided in data.edges ie. + * After clustering the 'SelectEdge' event is fired but provides only the clustered edge. + * This method can then be used to return the baseEdgeId. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} clusteredEdgeId + * @returns {IdType} + * + * @memberOf NetworkService + * + */ + public getBaseEdge(visNetwork: string, clusteredEdgeId: IdType): IdType { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getBaseEdge(clusteredEdgeId); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Visible edges between clustered nodes are not the same edge as the ones provided + * in data.edges passed on network creation. With each layer of clustering, copies of + * the edges between clusters are created and the previous edges are hidden, + * until the cluster is opened. This method takes an edgeId (ie. a base edgeId from data.edges) + * and applys the options to it and any edges that were created from it while clustering. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} startEdgeId + * @param {VisEdgeOptions} [options] + * + * @memberOf NetworkService + * + */ + public updateEdge(visNetwork: string, startEdgeId: IdType, options?: EdgeOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].updateEdge(startEdgeId, options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Clustered Nodes when created are not contained in the original data.nodes + * passed on network creation. This method updates the cluster node. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} clusteredNodeId + * @param {NodeOptions} options + * + * @memberOf NetworkService + */ + public updateClusteredNode(visNetwork: string, clusteredNodeId: IdType, options?: NodeOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].updateClusteredNode(clusteredNodeId, options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Returns an array of all nodeIds of the nodes that + * would be released if you open the cluster. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} clusterNodeId the id of the cluster node + * @returns {IdType[]} + * + * @memberOf NetworkService + */ + public getNodesInCluster(visNetwork: string, clusterNodeId: IdType): IdType[] { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getNodesInCluster(clusterNodeId); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Opens the cluster, releases the contained nodes and edges, + * removing the cluster node and cluster edges. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} nodeId The node id that represents the cluster. + * @param {OpenClusterOptions} [options] Cluster options. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public openCluster(visNetwork: string, nodeId: IdType, options?: OpenClusterOptions): void { + if (this.networks[visNetwork]) { + this.networks[visNetwork].openCluster(nodeId, options); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } + + /** + * Returns true if the node whose ID has been supplied is a cluster. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} nodeId The associated node id. + * @returns {boolean} True if the node whose ID has been supplied is a cluster. + * + * @memberOf NetworkService + */ + public isCluster(visNetwork: string, nodeId: IdType): boolean { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].isCluster(nodeId); + } + + return false; + } + + /** + * If you like the layout of your network and would like it to start in the same way next time, + * ask for the seed using this method and put it in the layout.randomSeed option. + * + * @param {string} visNetwork The network name/identifier. + * @returns {number} The seed of the current network or -1 when the network is not defined. + * + * @memberOf NetworkService + */ + public getSeed(visNetwork: string): number | string { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getSeed(); + } + + return -1; + } + + /** + * This function converts canvas coordinates to coordinates on the DOM. + * Input and output are in the form of {x:Number,y:Number}. + * The DOM values are relative to the network container. + * + * @param {string} visNetwork The network name/identifier. + * @param {Position} position The canvas position. + * @returns {Position} The DOM position. + * + * @memberOf NetworkService + */ + public canvasToDOM(visNetwork: string, position: Position) { + return this.networks[visNetwork].canvasToDOM(position); + } + + /** + * This function converts DOM coordinates to coordinates on the canvas. + * Input and output are in the form of {x:Number,y:Number}. + * The DOM values are relative to the network container. + * + * @param {string} visNetwork The network name/identifier. + * @param {Position} position The DOM position. + * @returns {Position} The canvas position. + * + * @memberOf NetworkService + */ + public DOMtoCanvas(visNetwork: string, position: Position) { + return this.networks[visNetwork].DOMtoCanvas(position); + } + + /** + * This function looks up the node at the given DOM coordinates on the canvas. + * Input and output are in the form of {x:Number,y:Number}. + * The DOM values are relative to the network container -> DOM not Canvas coords. + * + * @param {string} visNetwork The network name/identifier. + * @param {Position} position The DOM position. + * @returns {IdType} nodeId The associated node id. + * + * @memberOf NetworkService + */ + public getNodeAt(visNetwork: string, position: Position) { + return this.networks[visNetwork].getNodeAt(position); + } + + /** + * This function looks up the edge at the given DOM coordinates on the canvas. + * Input and output are in the form of {x:Number,y:Number}. + * The DOM values are relative to the network container -> DOM not Canvas coords. + * + * @param {string} visNetwork The network name/identifier. + * @param {Position} position The DOM position. + * @returns {IdType} edgeId The associated edge id. + * + * @memberOf NetworkService + */ + public getEdgeAt(visNetwork: string, position: Position) { + return this.networks[visNetwork].getEdgeAt(position); + } + + /** + * This function looks up the edges for a given nodeId. + * The DOM values are relative to the network container -> DOM not Canvas coords. + * + * @param {string} visNetwork The network name/identifier. + * @param {IdType} nodeId The associated node id. + * @returns {IdType[]} Return array of edge ids + * + * @memberOf NetworkService + */ + public getConnectedEdges(visNetwork: string, nodeId: IdType) { + return this.networks[visNetwork].getConnectedEdges(nodeId); + } + + /** + * Returns an array of nodeIds of the all the nodes that are directly connected to this node. + * If you supply an edgeId, vis will first match the id to nodes. + * If no match is found, it will search in the edgelist and return an array: [fromId, toId]. + * + * @param {string} visNetwork The network name/identifier. + * @param nodeOrEdgeId a node or edge id + * @returns {IdType[]} Return array of node ids + */ + public getConnectedNodes(visNetwork: string, nodeOrEdgeId: IdType) { + return this.networks[visNetwork].getConnectedNodes(nodeOrEdgeId); + } + + /** + * Returns the positions of the nodes. + * @param {string} visNetwork The network name/identifier. + * @param {Array.|String} [ids] --> optional, can be array of nodeIds, can be string + * @returns {{}} + */ + public getPositions(visNetwork: string, nodeIds?: IdType[]) { + return this.networks[visNetwork].getPositions(nodeIds); + } + + /** + * Returns the positions of the nodes. + * @param {string} visNetwork The network name/identifier. + */ + public getBoundingBox(visNetwork: string, nodeId: IdType): BoundingBox { + return this.networks[visNetwork].getBoundingBox(nodeId); + } + + /** + * Returns the positions of the nodes. + * @param {string} visNetwork The network name/identifier. + */ + public storePositions(visNetwork: string): void { + return this.networks[visNetwork].storePositions(); + } + + /** + * You can animate or move the camera using the moveTo method. + * + * @param {string} visNetwork The network name/identifier. + * @param {MoveToOptions} options Options for moveTo function. + */ + public moveTo(visNetwork: string, moveToOptions: MoveToOptions) { + return this.networks[visNetwork].moveTo(moveToOptions); + } + + /** + * Start the physics simulation. + * This is normally done whenever needed and is only really useful + * if you stop the simulation yourself and wish to continue it afterwards. + * @param {string} visNetwork The network name/identifier. + */ + public startSimulation(visNetwork: string) { + return this.networks[visNetwork].startSimulation(); + } + + /** + * This stops the physics simulation and triggers a stabilized event. + * Tt can be restarted by dragging a node, + * altering the dataset or calling startSimulation(). + * @param {string} visNetwork The network name/identifier. + */ + public stopSimulation(visNetwork: string) { + return this.networks[visNetwork].stopSimulation(); + } + + /** + * Returns the current scale of the network. 1.0 is comparible to 100%, 0 is zoomed out infinitely. + * + * @param {string} visNetwork The network name/identifier. + * + * @throws {Error} Thrown when the network does not exist. + * + * @memberOf NetworkService + */ + public getScale(visNetwork: string): number { + if (this.networks[visNetwork]) { + return this.networks[visNetwork].getScale(); + } else { + throw new Error(`Network with id ${visNetwork} not found.`); + } + } +} \ No newline at end of file diff --git a/src/app/vis/vis.module.ts b/src/app/vis/vis.module.ts new file mode 100644 index 0000000..ae30b2e --- /dev/null +++ b/src/app/vis/vis.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; + +import { DataSet } from 'vis-data/esnext'; + +import { Data, Edge, Node, Options } from 'vis-network/esnext'; +import { VisNetworkDirective } from './network/vis-network.directive'; +import { VisNetworkService } from './network/vis-network.service'; + +export { + VisNetworkDirective, + VisNetworkService, + Data, + DataSet, + Edge, + Options, + Node, +}; + + +@NgModule({ + declarations: [ + VisNetworkDirective, + ], + imports: [ + CommonModule + ], + exports: [VisNetworkDirective], + providers: [VisNetworkService], + schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA], +}) +export class VisModule { } diff --git a/src/polyfills.ts b/src/polyfills.ts index 9194e82..57702c2 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -49,9 +49,11 @@ import '@angular/localize/init'; /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js'; // Included with Angular CLI. - +import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS */ + +// Required for Dragula: https://github.com/valor-software/ng2-dragula#1-important-add-the-following-line-to-your-polyfillsts +(window as any).global = window; diff --git a/src/style/dragula.scss b/src/style/dragula.scss new file mode 100644 index 0000000..0b0b626 --- /dev/null +++ b/src/style/dragula.scss @@ -0,0 +1,26 @@ +.gu-mirror { + position: fixed !important; + margin: 0 !important; + z-index: 9999 !important; + opacity: 0.8; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; + filter: alpha(opacity=80); + pointer-events: none; + } + /* high-performance display:none; helper */ + .gu-hide { + left: -9999px !important; + } + /* added to mirrorContainer (default = body) while dragging */ + .gu-unselectable { + -webkit-user-select: none !important; + -moz-user-select: none !important; + -ms-user-select: none !important; + user-select: none !important; + } + /* added to the source element while its mirror is dragged */ + .gu-transit { + opacity: 0.2; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)"; + filter: alpha(opacity=20); + } \ No newline at end of file diff --git a/src/styles.scss b/src/styles.scss index 2ad1ec7..ada426c 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -5,3 +5,6 @@ @import '~bootstrap/scss/bootstrap'; @import 'style/bootswatch.scss'; @import '~bootstrap-icons/font/bootstrap-icons.scss'; + +/* Dragula Drag & Drop */ +@import 'style/dragula.scss'; diff --git a/yarn.lock b/yarn.lock index 3a15d06..407ee07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1298,6 +1298,13 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" integrity sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA== +"@egjs/hammerjs@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" + integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A== + dependencies: + "@types/hammerjs" "^2.0.36" + "@eslint/eslintrc@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" @@ -1783,6 +1790,11 @@ "@types/d3-transition" "*" "@types/d3-zoom" "*" +"@types/dragula@^2.1.34": + version "2.1.36" + resolved "https://registry.yarnpkg.com/@types/dragula/-/dragula-2.1.36.tgz#a58236ac095e26cd271b128f3c42df7fa41bd210" + integrity sha512-K1GIMqdiviBIvUsLJPO1xkjpDFS308nU2l57zmV7LEO+znF3gtZGnWQ+c/ef78r6Ngb0cniQk8pnNkObeNlXQQ== + "@types/eslint-scope@^3.7.0": version "3.7.3" resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224" @@ -1809,6 +1821,11 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca" integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA== +"@types/hammerjs@^2.0.36": + version "2.0.41" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.41.tgz#f6ecf57d1b12d2befcce00e928a6a097c22980aa" + integrity sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA== + "@types/http-proxy@^1.17.5": version "1.17.8" resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.8.tgz#968c66903e7e42b483608030ee85800f22d03f55" @@ -2336,6 +2353,11 @@ at-least-node@^1.0.0: resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== +atoa@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49" + integrity sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk= + atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -2844,6 +2866,14 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +contra@1.9.4: + version "1.9.4" + resolved "https://registry.yarnpkg.com/contra/-/contra-1.9.4.tgz#f53bde42d7e5b5985cae4d99a8d610526de8f28d" + integrity sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0= + dependencies: + atoa "1.0.0" + ticky "1.0.1" + convert-source-map@^1.5.1, convert-source-map@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" @@ -2943,6 +2973,13 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crossvent@1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.5.tgz#ad20878e4921e9be73d9d6976f8b2ecd0f71a0b1" + integrity sha1-rSCHjkkh6b5z2daXb4suzQ9xoLE= + dependencies: + custom-event "^1.0.0" + css-blank-pseudo@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5" @@ -3019,7 +3056,7 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -custom-event@~1.0.0: +custom-event@^1.0.0, custom-event@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= @@ -3489,6 +3526,14 @@ domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +dragula@^3.7.2: + version "3.7.3" + resolved "https://registry.yarnpkg.com/dragula/-/dragula-3.7.3.tgz#909460fd0b4acba5409c6dbb1b64d24f5bc9efb6" + integrity sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ== + dependencies: + contra "1.9.4" + crossvent "1.5.5" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -4995,6 +5040,11 @@ karma@~6.3.0: ua-parser-js "^0.7.30" yargs "^16.1.1" +keycharm@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/keycharm/-/keycharm-0.4.0.tgz#8d684ea9cc01379a07fbddee33ff32d97f5ae2a7" + integrity sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ== + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -5396,6 +5446,14 @@ neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +ng2-dragula@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ng2-dragula/-/ng2-dragula-2.1.1.tgz#350e78978b6f7e1ea0b16c61ba78161c84ec6386" + integrity sha512-PSo6N2Ja894KDogVLLBI0Hzpylikay7L1hWqp+qQmW+qsNsNT9J/6J2Qim9XwGzK4VQZjAwBJaJjgJ/TijRkLQ== + dependencies: + "@types/dragula" "^2.1.34" + dragula "^3.7.2" + ngrx-store-localstorage@^12.0.1: version "12.0.1" resolved "https://registry.yarnpkg.com/ngrx-store-localstorage/-/ngrx-store-localstorage-12.0.1.tgz#282683b135011cc643e19ad883f2fb424fc81559" @@ -7147,6 +7205,16 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +ticky@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ticky/-/ticky-1.0.1.tgz#b7cfa71e768f1c9000c497b9151b30947c50e46d" + integrity sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0= + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + tmp@0.2.1, tmp@^0.2.1, tmp@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" @@ -7344,6 +7412,21 @@ vary@^1, vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= +vis-data@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/vis-data/-/vis-data-7.1.2.tgz#b7d076ac79cb54f7c5e9c80f5b03b93cc8cc1fda" + integrity sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw== + +vis-network@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-9.1.0.tgz#511db833b68060f279bedc4a852671261d40204e" + integrity sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw== + +vis-util@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/vis-util/-/vis-util-5.0.2.tgz#47e8a31580c0805680c43d253ac7da21501990b9" + integrity sha512-oPDmPc4o0uQLoKpKai2XD1DjrhYsA7MRz75Wx9KmfX84e9LLgsbno7jVL5tR0K9eNVQkD6jf0Ei8NtbBHDkF1A== + void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"