Implemented visual page

This commit is contained in:
Djuri Baars 2022-01-17 12:10:57 +01:00
parent 5392848a57
commit ab6d8617ac
39 changed files with 2104 additions and 56 deletions

View file

@ -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": {

View file

@ -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,

View file

@ -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<NodeOwner[]>;
settings$!: Observable<SettingState>;
@ -53,8 +53,6 @@ export class OverviewComponent implements OnInit, OnDestroy {
});
}
ngOnInit(): void {}
ngOnDestroy(): void {
this.lnData.channelSocket.emit('unsubscribe_all');
}

View file

@ -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<fromRoot.State>) {
constructor(
private file: FileService,
private notification: NotificationService,
private store: Store<fromRoot.State>
) {
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) {}

View file

@ -1,42 +1,31 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-10">
<h3>Ring Designer
<div class="btn-group btn-small small btn-group-toggle" ngbRadioGroup name="names" [(ngModel)]="settings.viewMode"
(ngModelChange)="viewChange($event)">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" value="node"> Nodename
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" value="tg"> TG username
</label>
</div>
</h3>
<div id="graphContainer"></div>
</div>
<div class="col-md-2">
<!-- <app-ring-order></app-ring-order>
<app-file-exporter></app-file-exporter> -->
<hr />
<!-- <form class="form-inline">
<label class="my-1 mr-2" for="inlineFormCustomSelectPref">Igniter</label>
<select class="form-control" [(ngModel)]="selectedIgniter" name="selectedIgniter">
<option [value]="undefined">Select</option>
<ng-container *ngFor="let s of nodeOwners">
<option [ngValue]="s">
<ng-template [ngIf]="viewMode == 'tg'" [ngIfElse]="elseBlock">
{{ s.first_name }}
</ng-template>
<ng-template #elseBlock>
{{ s.nodename }}
</ng-template>
</option>
</ng-container>
</select>
<button type="submit" class="btn btn-primary my-1" (click)="reorderIgniter()">Reorder</button>
</form> -->
</div>
<div class="col-md-8">
<h3>Node connections
<div class="btn-group btn-small small btn-group-toggle" ngbRadioGroup name="names"
[(ngModel)]="settings.viewMode" (ngModelChange)="viewChange($event)">
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" value="node"> Nodename
</label>
<label ngbButtonLabel class="btn-primary btn-sm">
<input ngbButton type="radio" value="tg"> TG username
</label>
</div>
</h3>
<app-node-connections></app-node-connections>
</div>
<div class="col-md-4">
<app-edit-ring-order></app-edit-ring-order>
<div class="row mt-3">
<div class="col-md-6">
<app-reorder-participants></app-reorder-participants>
</div>
<div class="col-md-6">
<app-file-exporter></app-file-exporter>
</div>
</div>
<hr />
</div>
</div>
</div>
</div>

View file

@ -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<SettingState>;
nodeOwners$!: Observable<NodeOwner[]>;
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));
}
}

View file

@ -0,0 +1,6 @@
export enum ExportFile {
RingToolsPubKeysTxt = 'pubkeys.txt',
RingToolsChannelsTxt = 'channels.txt',
IgniterPubkeys = 'igniter_pubkeys.txt',
IgniterSh = 'igniter.sh'
}

View file

@ -0,0 +1,12 @@
<h3>Ring order</h3>
<ul class="list-group" dragula="PARTICIPANTS" id="participants" [(dragulaModel)]="nodeOwners">
<li class="list-group-item d-flex justify-content-between" *ngFor="let s of nodeOwners">
<ng-template [ngIf]="(settings$ | async)?.viewMode === 'tg'" [ngIfElse]="elseBlock">
{{ getCbUsername(s) }}
</ng-template>
<ng-template #elseBlock>
{{ s.nodename }}
</ng-template>
<small>{{ s.nodename }}</small>
</li>
</ul>

View file

@ -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<EditRingOrderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ EditRingOrderComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(EditRingOrderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -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<NodeOwner[]>;
nodeOwners: NodeOwner[] = [];
ringSettings$!: Observable<RingSetting[]>;
settings$: Observable<SettingState>;
subs = new Subscription();
constructor(
private store: Store<fromRoot.State>,
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');
}
}

View file

@ -0,0 +1,8 @@
<button type="button" class="btn btn-warning btn-lg btn-block btn-sm" (click)="persistOrder()">Persist channel
order</button>
<button type="button" class="btn btn-primary btn-lg btn-block btn-sm" (click)="downloadChannelsTxt()">Download
channels.txt</button>
<button type="button" class="btn btn-success btn-lg btn-block btn-sm" (click)="downloadPubKeysTxt()">Download
pubkeys.txt</button>
<button type="button" class="btn btn-secondary btn-lg btn-block btn-sm" (click)="downloadIgniterPubkeys()">Download
igniter.sh pubkeys</button>

View file

@ -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<FileExporterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FileExporterComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileExporterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -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);
}
}

View file

@ -0,0 +1,2 @@
<div class="network-canvas" [visNetwork]="visNetwork" [visNetworkData]="visNetworkData"
[visNetworkOptions]="visNetworkOptions"></div>

View file

@ -0,0 +1,5 @@
.network-canvas {
width: 100%;
height: 600px;
border: 1px solid lightgray;
}

View file

@ -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<NodeConnectionsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NodeConnectionsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NodeConnectionsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -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<Node>;
public edges!: DataSet<Edge>;
public visNetworkOptions!: Options;
nodeOwners: NodeOwner[] = [];
nodeOwners$: Observable<NodeOwner[]>;
constructor(
private store: Store<fromRoot.State>,
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<Node>();
this.edges = new DataSet<Edge>([]);
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;
}
}
});
}
}
}

View file

@ -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 { }

View file

@ -0,0 +1,18 @@
<form class="form-inline">
<label class="my-1 mr-2" for="inlineFormCustomSelectPref">Ringleader</label>
<select class="form-control" [(ngModel)]="selectedIgniter" name="selectedIgniter">
<option [value]="undefined">Select</option>
<ng-container *ngFor="let s of nodeOwners">
<option [ngValue]="s">
<ng-template [ngIf]="(settings$ | async)?.viewMode === 'tg'" [ngIfElse]="elseBlock">
{{ s.first_name }}
</ng-template>
<ng-template #elseBlock>
{{ s.nodename }}
</ng-template>
</option>
</ng-container>
</select>
<button type="submit" class="btn btn-primary my-1" (click)="reorderIgniter()">Reorder</button>
</form>

View file

@ -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<ReorderParticipantsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ReorderParticipantsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ReorderParticipantsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -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<NodeOwner[]>;
nodeOwners: NodeOwner[] = [];
ringSettings$!: Observable<RingSetting[]>;
settings$: Observable<SettingState>;
selectedIgniter: any;
constructor(
private store: Store<fromRoot.State>,
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' });
}
}
}

View file

@ -7,12 +7,21 @@ export const nodeOwnersFeatureKey = 'nodeOwners';
export interface NodeOwnersState extends EntityState<NodeOwner> {
// additional entities state properties
pub_key: string | null
}
export const adapter: EntityAdapter<NodeOwner> = createEntityAdapter<NodeOwner>();
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<NodeOwner> = createEntityAdapter<NodeOwner>({
selectId: selectNodeOwner,
});
export const initialState: NodeOwnersState = adapter.getInitialState({
// additional entity state properties
pub_key: ''
});
export const nodeOwnersReducer = createReducer(

View file

@ -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) {
}
}

View file

@ -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<any, any>;
channelSocket: Socket<any, any>;
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<NodeInfo>(
`http://localhost:7464/node/${pubKey}`
);
}
}

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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 };

View file

@ -0,0 +1,8 @@
import { VisNetworkDirective } from './vis-network.directive';
describe('VisNetworkDirective', () => {
it('should create an instance', () => {
const directive = new VisNetworkDirective();
expect(directive).toBeTruthy();
});
});

View file

@ -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<any>}
* @memberOf VisNetworkDirective
*/
@Output()
public initialized: EventEmitter<any> = new EventEmitter<any>();
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);
}
}

View file

@ -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();
});
});

File diff suppressed because it is too large Load diff

33
src/app/vis/vis.module.ts Normal file
View file

@ -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 { }

View file

@ -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;

26
src/style/dragula.scss Normal file
View file

@ -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);
}

View file

@ -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';

View file

@ -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"