Merge branch 'master' into simon/node-alias-fulltext-search

This commit is contained in:
wiz 2022-08-21 22:29:18 +09:00 committed by GitHub
commit 4727b4bade
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 472 additions and 105 deletions

View File

@ -77,13 +77,19 @@
},
"LIGHTNING": {
"ENABLED": false,
"BACKEND": "lnd"
"BACKEND": "lnd",
"STATS_REFRESH_INTERVAL": 600,
"GRAPH_REFRESH_INTERVAL": 600,
"LOGGER_UPDATE_INTERVAL": 30
},
"LND": {
"TLS_CERT_PATH": "tls.cert",
"MACAROON_PATH": "readonly.macaroon",
"REST_API_URL": "https://localhost:8080"
},
"CLIGHTNING": {
"SOCKET": "lightning-rpc"
},
"SOCKS5PROXY": {
"ENABLED": false,
"USE_ONION": true,

View File

@ -88,5 +88,21 @@
"LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
"BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
"BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
},
"LIGHTNING": {
"ENABLED": "__LIGHTNING_ENABLED__",
"BACKEND": "__LIGHTNING_BACKEND__",
"TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
"STATS_REFRESH_INTERVAL": 600,
"GRAPH_REFRESH_INTERVAL": 600,
"LOGGER_UPDATE_INTERVAL": 30
},
"LND": {
"TLS_CERT_PATH": "",
"MACAROON_PATH": "",
"REST_API_URL": "https://localhost:8080"
},
"CLIGHTNING": {
"SOCKET": "__CLIGHTNING_SOCKET__"
}
}

View File

@ -297,19 +297,24 @@ class NodesApi {
if (!ispList[isp1]) {
ispList[isp1] = {
id: channel.isp1ID,
id: channel.isp1ID.toString(),
capacity: 0,
channels: 0,
nodes: {},
};
} else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
ispList[isp1].id += ',' + channel.isp1ID.toString();
}
if (!ispList[isp2]) {
ispList[isp2] = {
id: channel.isp2ID,
id: channel.isp2ID.toString(),
capacity: 0,
channels: 0,
nodes: {},
};
} else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
ispList[isp2].id += ',' + channel.isp2ID.toString();
}
ispList[isp1].capacity += channel.capacity;
@ -388,7 +393,8 @@ class NodesApi {
const query = `
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
geo_names_city.names as city
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM node_stats
JOIN (
SELECT public_key, MAX(added) as last_added
@ -396,15 +402,19 @@ class NodesApi {
GROUP BY public_key
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
WHERE geo_names_country.id = ?
ORDER BY capacity DESC
`;
const [rows]: any = await DB.query(query, [countryId]);
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
rows[i].subdivision = JSON.parse(rows[i].subdivision);
}
return rows;
} catch (e) {
@ -418,7 +428,8 @@ class NodesApi {
const query = `
SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
geo_names_city.names as city, geo_names_country.names as country
geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM node_stats
JOIN (
SELECT public_key, MAX(added) as last_added
@ -426,8 +437,10 @@ class NodesApi {
GROUP BY public_key
) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
WHERE nodes.as_number IN (?)
ORDER BY capacity DESC
`;
@ -436,6 +449,7 @@ class NodesApi {
for (let i = 0; i < rows.length; ++i) {
rows[i].country = JSON.parse(rows[i].country);
rows[i].city = JSON.parse(rows[i].city);
rows[i].subdivision = JSON.parse(rows[i].subdivision);
}
return rows;
} catch (e) {

View File

@ -36,6 +36,7 @@ interface IConfig {
TOPOLOGY_FOLDER: string;
STATS_REFRESH_INTERVAL: number;
GRAPH_REFRESH_INTERVAL: number;
LOGGER_UPDATE_INTERVAL: number;
};
LND: {
TLS_CERT_PATH: string;
@ -191,6 +192,7 @@ const defaults: IConfig = {
'TOPOLOGY_FOLDER': '',
'STATS_REFRESH_INTERVAL': 600,
'GRAPH_REFRESH_INTERVAL': 600,
'LOGGER_UPDATE_INTERVAL': 30,
},
'LND': {
'TLS_CERT_PATH': '',

View File

@ -95,11 +95,19 @@ class NetworkSyncService {
*/
private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
try {
const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
const closedChannels = {};
for (const closedChannel of closedChannelsRaw) {
closedChannels[Common.channelShortIdToIntegerId(closedChannel.id)] = true;
}
let progress = 0;
const graphChannelsIds: string[] = [];
for (const channel of channels) {
if (!closedChannels[channel.channel_id]) {
await channelsApi.$saveChannel(channel);
}
graphChannelsIds.push(channel.channel_id);
++progress;

View File

@ -350,3 +350,68 @@ Corresponding `docker-compose.yml` overrides:
PRICE_DATA_SERVER_CLEARNET_URL: ""
...
```
<br/>
`mempool-config.json`:
```
"LIGHTNING": {
"ENABLED": false
"BACKEND": "lnd"
"TOPOLOGY_FOLDER": ""
"STATS_REFRESH_INTERVAL": 600
"GRAPH_REFRESH_INTERVAL": 600
"LOGGER_UPDATE_INTERVAL": 30
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
LIGHTNING_ENABLED: false
LIGHTNING_BACKEND: "lnd"
LIGHTNING_TOPOLOGY_FOLDER: ""
LIGHTNING_STATS_REFRESH_INTERVAL: 600
LIGHTNING_GRAPH_REFRESH_INTERVAL: 600
LIGHTNING_LOGGER_UPDATE_INTERVAL: 30
...
```
<br/>
`mempool-config.json`:
```
"LND": {
"TLS_CERT_PATH": ""
"MACAROON_PATH": ""
"REST_API_URL": "https://localhost:8080"
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
LND_TLS_CERT_PATH: ""
LND_MACAROON_PATH: ""
LND_REST_API_URL: "https://localhost:8080"
...
```
<br/>
`mempool-config.json`:
```
"CLN": {
"SOCKET": ""
}
```
Corresponding `docker-compose.yml` overrides:
```
api:
environment:
CLN_SOCKET: ""
...
```

View File

@ -91,6 +91,22 @@ __EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http:
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
# LIGHTNING
__LIGHTNING_ENABLED__=${LIGHTNING_ENABLED:=false}
__LIGHTNING_BACKEND__=${LIGHTNING_BACKEND:="lnd"}
__LIGHTNING_TOPOLOGY_FOLDER__=${LIGHTNING_TOPOLOGY_FOLDER:=""}
__LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600}
__LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600}
__LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
# LND
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
# CLN
__CLN_SOCKET__=${CLN_SOCKET:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
@ -173,4 +189,20 @@ sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_URL__!${__EXTERNAL_DATA_SERVER_BISQ_URL__}!g" mempool-config.json
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
# LIGHTNING
sed -i "s!__LIGHTNING_ENABLED__!${__LIGHTNING_ENABLED__}!g" mempool-config.json
sed -i "s!__LIGHTNING_BACKEND__!${__LIGHTNING_BACKEND__}!g" mempool-config.json
sed -i "s!__LIGHTNING_TOPOLOGY_FOLDER__!${__LIGHTNING_TOPOLOGY_FOLDER__}!g" mempool-config.json
sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json
sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json
sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json
# LND
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
# CLN
sed -i "s!__CLN_SOCKET__!${__CLN_SOCKET__}!g" mempool-config.json
node /backend/dist/index.js

View File

@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { OpenGraphService } from 'src/app/services/opengraph.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { getFlagEmoji } from 'src/app/shared/common.utils';
import { LightningApiService } from '../lightning-api.service';
import { isMobile } from '../../shared/common.utils';

View File

@ -42,24 +42,10 @@
<app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
</td>
</tr>
<tr *ngIf="node.country && node.city && node.subdivision">
<tr *ngIf="node.geolocation">
<td i18n="location">Location</td>
<td>
<span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
<br>
<a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
<span class="link">{{ node.country.en }}</span>
&nbsp;
<span class="flag">{{ node.flag }}</span>
</a>
</td>
</tr>
<tr *ngIf="node.country && !node.city">
<td i18n="location">Location</td>
<td>
<a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
{{ node.country.en }} {{ node.flag }}
</a>
<app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
</td>
</tr>
</tbody>

View File

@ -3,9 +3,9 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';
import { catchError, map, switchMap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { isMobile } from '../../shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
selector: 'app-node',
@ -58,7 +58,6 @@ export class NodeComponent implements OnInit {
} else if (socket.indexOf('onion') > -1) {
label = 'Tor';
}
node.flag = getFlagEmoji(node.iso_code);
socketsObject.push({
label: label,
socket: node.public_key + '@' + socket,
@ -66,6 +65,19 @@ export class NodeComponent implements OnInit {
}
node.socketsObject = socketsObject;
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
if (!node?.country && !node?.city &&
!node?.subdivision && !node?.iso) {
node.geolocation = null;
} else {
node.geolocation = <GeolocationData>{
country: node.country?.en,
city: node.city?.en,
subdivision: node.subdivision?.en,
iso: node.iso_code,
};
}
return node;
}),
catchError(err => {

View File

@ -1,5 +1,5 @@
<div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div *ngIf="channelsObservable | async">
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
<div *ngIf="style === 'graph'" class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
<span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
@ -7,8 +7,10 @@
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div>
<div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
(chartFinished)="onChartFinished($event)">
</div>
</div>
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
</div>

View File

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
import { SeoService } from 'src/app/services/seo.service';
import { ApiService } from 'src/app/services/api.service';
import { Observable, switchMap, tap, zip } from 'rxjs';
@ -16,14 +16,14 @@ import { isMobile } from 'src/app/shared/common.utils';
styleUrls: ['./nodes-channels-map.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NodesChannelsMap implements OnInit, OnDestroy {
export class NodesChannelsMap implements OnInit {
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
@Input() publicKey: string | undefined;
@Input() channel: any[] = [];
@Input() fitContainer = false;
@Output() readyEvent = new EventEmitter();
observable$: Observable<any>;
channelsObservable: Observable<any>;
center: number[] | undefined;
zoom: number | undefined;
@ -31,6 +31,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
channelOpacity = 0.1;
channelColor = '#466d9d';
channelCurve = 0;
nodeSize = 4;
chartInstance = undefined;
chartOptions: EChartsOption = {};
@ -49,8 +50,6 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
) {
}
ngOnDestroy(): void {}
ngOnInit(): void {
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
this.zoom = 1.3;
@ -66,7 +65,11 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
this.seoService.setTitle($localize`Lightning nodes channels world map`);
}
this.observable$ = this.activatedRoute.paramMap
if (['nodepage', 'channelpage'].includes(this.style)) {
this.nodeSize = 8;
}
this.channelsObservable = this.activatedRoute.paramMap
.pipe(
switchMap((params: ParamMap) => {
return zip(
@ -170,15 +173,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
prepareChartOptions(nodes, channels) {
let title: object;
if (channels.length === 0) {
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: $localize`No geolocation data available`,
left: 'center',
top: 'center'
};
this.chartOptions = null;
return;
}
this.chartOptions = {
@ -214,7 +210,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
data: nodes,
coordinateSystem: 'geo',
geoIndex: 0,
symbolSize: 4,
symbolSize: this.nodeSize,
tooltip: {
show: true,
backgroundColor: 'rgba(17, 19, 31, 1)',

View File

@ -10,6 +10,7 @@ import { download } from 'src/app/shared/graphs.utils';
import { SeoService } from 'src/app/services/seo.service';
import { LightningApiService } from '../lightning-api.service';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { isMobile } from 'src/app/shared/common.utils';
@Component({
selector: 'app-nodes-networks-chart',
@ -108,19 +109,19 @@ export class NodesNetworksChartComponent implements OnInit {
);
}
prepareChartOptions(data, maxYAxis) {
prepareChartOptions(data, maxYAxis): void {
let title: object;
if (data.tor_nodes.length === 0) {
if (!this.widget && data.tor_nodes.length === 0) {
title = {
textStyle: {
color: 'grey',
fontSize: 15
},
text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
text: $localize`Indexing in progess`,
left: 'center',
top: 'top',
top: 'center',
};
} else if (this.widget) {
} else if (data.tor_nodes.length > 0) {
title = {
textStyle: {
color: 'grey',
@ -140,11 +141,11 @@ export class NodesNetworksChartComponent implements OnInit {
height: this.widget ? 100 : undefined,
top: this.widget ? 10 : 40,
bottom: this.widget ? 0 : 70,
right: (this.isMobile() && this.widget) ? 35 : this.right,
left: (this.isMobile() && this.widget) ? 40 :this.left,
right: (isMobile() && this.widget) ? 35 : this.right,
left: (isMobile() && this.widget) ? 40 :this.left,
},
tooltip: {
show: !this.isMobile() || !this.widget,
show: !isMobile() || !this.widget,
trigger: 'axis',
axisPointer: {
type: 'line'
@ -157,7 +158,7 @@ export class NodesNetworksChartComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
formatter: (ticks): string => {
let total = 0;
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
@ -180,7 +181,7 @@ export class NodesNetworksChartComponent implements OnInit {
},
xAxis: data.tor_nodes.length === 0 ? undefined : {
type: 'time',
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
splitNumber: (isMobile() || this.widget) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
@ -372,7 +373,7 @@ export class NodesNetworksChartComponent implements OnInit {
};
}
onChartInit(ec) {
onChartInit(ec): void {
if (this.chartInstance !== undefined) {
return;
}
@ -384,11 +385,7 @@ export class NodesNetworksChartComponent implements OnInit {
});
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
onSaveChart(): void {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();

View File

@ -9,7 +9,7 @@ import { StateService } from 'src/app/services/state.service';
import { download } from 'src/app/shared/graphs.utils';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { getFlagEmoji } from 'src/app/shared/common.utils';
@Component({
selector: 'app-nodes-per-country-chart',

View File

@ -36,7 +36,7 @@
{{ node.channels }}
</td>
<td class="city text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
</td>
</tbody>
</table>

View File

@ -3,7 +3,8 @@ import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
import { getFlagEmoji } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
selector: 'app-nodes-per-country',
@ -29,6 +30,16 @@ export class NodesPerCountry implements OnInit {
name: response.country.en,
flag: getFlagEmoji(this.route.snapshot.params.country)
};
for (const i in response.nodes) {
response.nodes[i].geolocation = <GeolocationData>{
country: response.nodes[i].country?.en,
city: response.nodes[i].city?.en,
subdivision: response.nodes[i].subdivision?.en,
iso: response.nodes[i].iso_code,
};
}
this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
return response.nodes;
})

View File

@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
},
borderColor: '#000',
formatter: () => {
return `<b style="color: white">${isp[1]} (${isp[6]}%)</b><br>` +
return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
$localize`${isp[4].toString()} nodes<br>` +
$localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
;

View File

@ -33,7 +33,7 @@
{{ node.channels }}
</td>
<td class="city text-right text-truncate">
{{ node?.city?.en ?? '-' }}
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td>
</tbody>
</table>

View File

@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { map, Observable } from 'rxjs';
import { ApiService } from 'src/app/services/api.service';
import { SeoService } from 'src/app/services/seo.service';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
@Component({
selector: 'app-nodes-per-isp',
@ -29,6 +30,16 @@ export class NodesPerISP implements OnInit {
id: this.route.snapshot.params.isp
};
this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
for (const i in response.nodes) {
response.nodes[i].geolocation = <GeolocationData>{
country: response.nodes[i].country?.en,
city: response.nodes[i].city?.en,
subdivision: response.nodes[i].subdivision?.en,
iso: response.nodes[i].iso_code,
};
}
return response.nodes;
})
);

View File

@ -48,4 +48,10 @@
<div class="spinner-border text-light"></div>
</div>
<div *ngIf="widget && (capacityObservable$ | async) as stats">
<div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
Indexing in progress
</div>
</div>
</div>

View File

@ -132,3 +132,12 @@
max-width: 80px;
margin: 15px auto 3px;
}
.indexing-message {
position: absolute;
font-size: 15px;
color: grey;
font-weight: bold;
margin-left: calc(50% - 85px);
margin-top: -10px;
}

View File

@ -1,7 +1,7 @@
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { Observable } from 'rxjs';
import { map, startWith, switchMap, tap } from 'rxjs/operators';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from 'src/app/services/seo.service';
import { formatNumber } from '@angular/common';
import { FormBuilder, FormGroup } from '@angular/forms';
@ -10,6 +10,7 @@ import { MiningService } from 'src/app/services/mining.service';
import { download } from 'src/app/shared/graphs.utils';
import { LightningApiService } from '../lightning-api.service';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { isMobile } from 'src/app/shared/common.utils';
@Component({
selector: 'app-lightning-statistics-chart',
@ -96,12 +97,13 @@ export class LightningStatisticsChartComponent implements OnInit {
}),
);
}),
)
share(),
);
}
prepareChartOptions(data) {
prepareChartOptions(data): void {
let title: object;
if (data.channel_count.length === 0) {
if (!this.widget && data.channel_count.length === 0) {
title = {
textStyle: {
color: 'grey',
@ -111,7 +113,7 @@ export class LightningStatisticsChartComponent implements OnInit {
left: 'center',
top: 'center'
};
} else if (this.widget) {
} else if (data.channel_count.length > 0) {
title = {
textStyle: {
color: 'grey',
@ -138,11 +140,11 @@ export class LightningStatisticsChartComponent implements OnInit {
height: this.widget ? 100 : undefined,
top: this.widget ? 10 : 40,
bottom: this.widget ? 0 : 70,
right: (this.isMobile() && this.widget) ? 35 : this.right,
left: (this.isMobile() && this.widget) ? 40 :this.left,
right: (isMobile() && this.widget) ? 35 : this.right,
left: (isMobile() && this.widget) ? 40 :this.left,
},
tooltip: {
show: !this.isMobile(),
show: !isMobile(),
trigger: 'axis',
axisPointer: {
type: 'line'
@ -155,7 +157,7 @@ export class LightningStatisticsChartComponent implements OnInit {
align: 'left',
},
borderColor: '#000',
formatter: (ticks) => {
formatter: (ticks): string => {
let sizeString = '';
let weightString = '';
@ -169,16 +171,18 @@ export class LightningStatisticsChartComponent implements OnInit {
const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
let tooltip = `<b style="color: white; margin-left: 18px">${date}</b><br>
const tooltip = `
<b style="color: white; margin-left: 18px">${date}</b><br>
<span>${sizeString}</span><br>
<span>${weightString}</span>`;
<span>${weightString}</span>
`;
return tooltip;
}
},
xAxis: data.channel_count.length === 0 ? undefined : {
type: 'time',
splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
splitNumber: (isMobile() || this.widget) ? 5 : 10,
axisLabel: {
hideOverlap: true,
}
@ -315,7 +319,7 @@ export class LightningStatisticsChartComponent implements OnInit {
};
}
onChartInit(ec) {
onChartInit(ec): void {
if (this.chartInstance !== undefined) {
return;
}
@ -327,11 +331,7 @@ export class LightningStatisticsChartComponent implements OnInit {
});
}
isMobile() {
return (window.innerWidth <= 767.98);
}
onSaveChart() {
onSaveChart(): void {
// @ts-ignore
const prevBottom = this.chartOptions.grid.bottom;
const now = new Date();

View File

@ -1,3 +1,120 @@
export function isMobile() {
export function isMobile(): boolean {
return (window.innerWidth <= 767.98);
}
export function getFlagEmoji(countryCode): string {
if (!countryCode) {
return '';
}
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}
// https://gist.github.com/calebgrove/c285a9510948b633aa47
export function convertRegion(input, to: 'name' | 'abbreviated'): string {
if (!input) {
return '';
}
const states = [
['Alabama', 'AL'],
['Alaska', 'AK'],
['American Samoa', 'AS'],
['Arizona', 'AZ'],
['Arkansas', 'AR'],
['Armed Forces Americas', 'AA'],
['Armed Forces Europe', 'AE'],
['Armed Forces Pacific', 'AP'],
['California', 'CA'],
['Colorado', 'CO'],
['Connecticut', 'CT'],
['Delaware', 'DE'],
['District Of Columbia', 'DC'],
['Florida', 'FL'],
['Georgia', 'GA'],
['Guam', 'GU'],
['Hawaii', 'HI'],
['Idaho', 'ID'],
['Illinois', 'IL'],
['Indiana', 'IN'],
['Iowa', 'IA'],
['Kansas', 'KS'],
['Kentucky', 'KY'],
['Louisiana', 'LA'],
['Maine', 'ME'],
['Marshall Islands', 'MH'],
['Maryland', 'MD'],
['Massachusetts', 'MA'],
['Michigan', 'MI'],
['Minnesota', 'MN'],
['Mississippi', 'MS'],
['Missouri', 'MO'],
['Montana', 'MT'],
['Nebraska', 'NE'],
['Nevada', 'NV'],
['New Hampshire', 'NH'],
['New Jersey', 'NJ'],
['New Mexico', 'NM'],
['New York', 'NY'],
['North Carolina', 'NC'],
['North Dakota', 'ND'],
['Northern Mariana Islands', 'NP'],
['Ohio', 'OH'],
['Oklahoma', 'OK'],
['Oregon', 'OR'],
['Pennsylvania', 'PA'],
['Puerto Rico', 'PR'],
['Rhode Island', 'RI'],
['South Carolina', 'SC'],
['South Dakota', 'SD'],
['Tennessee', 'TN'],
['Texas', 'TX'],
['US Virgin Islands', 'VI'],
['Utah', 'UT'],
['Vermont', 'VT'],
['Virginia', 'VA'],
['Washington', 'WA'],
['West Virginia', 'WV'],
['Wisconsin', 'WI'],
['Wyoming', 'WY'],
];
// So happy that Canada and the US have distinct abbreviations
const provinces = [
['Alberta', 'AB'],
['British Columbia', 'BC'],
['Manitoba', 'MB'],
['New Brunswick', 'NB'],
['Newfoundland', 'NF'],
['Northwest Territory', 'NT'],
['Nova Scotia', 'NS'],
['Nunavut', 'NU'],
['Ontario', 'ON'],
['Prince Edward Island', 'PE'],
['Quebec', 'QC'],
['Saskatchewan', 'SK'],
['Yukon', 'YT'],
];
const regions = states.concat(provinces);
let i; // Reusable loop variable
if (to == 'abbreviated') {
input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
for (i = 0; i < regions.length; i++) {
if (regions[i][0] == input) {
return (regions[i][1]);
}
}
} else if (to == 'name') {
input = input.toUpperCase();
for (i = 0; i < regions.length; i++) {
if (regions[i][1] == input) {
return (regions[i][0]);
}
}
}
}

View File

@ -0,0 +1 @@
<span [innerHTML]="formattedLocation"></span>

View File

@ -0,0 +1,83 @@
import { Component, Input, OnChanges } from '@angular/core';
import { convertRegion, getFlagEmoji } from '../../common.utils';
export interface GeolocationData {
country: string;
city: string;
subdivision: string;
iso: string;
}
@Component({
selector: 'app-geolocation',
templateUrl: './geolocation.component.html',
styleUrls: ['./geolocation.component.scss']
})
export class GeolocationComponent implements OnChanges {
@Input() data: GeolocationData;
@Input() type: 'node' | 'list-isp' | 'list-country';
formattedLocation: string = '';
ngOnChanges(): void {
const city = this.data.city ? this.data.city : '';
const subdivisionLikeCity = this.data.city === this.data.subdivision;
let subdivision = this.data.subdivision;
if (['US', 'CA'].includes(this.data.iso) === false || (this.type === 'node' && subdivisionLikeCity)) {
this.data.subdivision = undefined;
} else if (['list-isp', 'list-country'].includes(this.type) === true) {
subdivision = convertRegion(this.data.subdivision, 'abbreviated');
}
if (this.type === 'list-country') {
if (this.data.city) {
this.formattedLocation += ' ' + city;
if (this.data.subdivision) {
this.formattedLocation += ', ' + subdivision;
}
} else {
this.formattedLocation += '-';
}
}
if (this.type === 'list-isp') {
this.formattedLocation = getFlagEmoji(this.data.iso);
if (this.data.city) {
this.formattedLocation += ' ' + city;
if (this.data.subdivision) {
this.formattedLocation += ', ' + subdivision;
}
} else {
this.formattedLocation += ' ' + this.data.country;
}
}
if (this.type === 'node') {
const city = this.data.city ? this.data.city : '';
// City
this.formattedLocation = `${city}`;
// ,Subdivision
if (this.formattedLocation.length > 0 && !subdivisionLikeCity) {
this.formattedLocation += ', ';
}
if (!subdivisionLikeCity) {
this.formattedLocation += `${subdivision}`;
}
// <br>[flag] County
if (this.data?.country.length ?? 0 > 0) {
if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) {
this.formattedLocation += '<br>';
} else if (this.data.city) {
this.formattedLocation += ', ';
}
this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`;
}
return;
}
}
}

View File

@ -91,13 +91,3 @@ export function detectWebGL() {
return (gl && gl instanceof WebGLRenderingContext);
}
export function getFlagEmoji(countryCode) {
if (!countryCode) {
return '';
}
const codePoints = countryCode
.toUpperCase()
.split('')
.map(char => 127397 + char.charCodeAt());
return String.fromCodePoint(...codePoints);
}

View File

@ -82,6 +82,7 @@ import { SatsComponent } from './components/sats/sats.component';
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
import { TimestampComponent } from './components/timestamp/timestamp.component';
import { ToggleComponent } from './components/toggle/toggle.component';
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
@NgModule({
declarations: [
@ -158,6 +159,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
SearchResultsComponent,
TimestampComponent,
ToggleComponent,
GeolocationComponent,
],
imports: [
CommonModule,
@ -261,6 +263,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
SearchResultsComponent,
TimestampComponent,
ToggleComponent,
GeolocationComponent,
]
})
export class SharedModule {