mirror of
https://github.com/mempool/mempool.git
synced 2025-02-24 14:50:52 +01:00
Merge pull request #4321 from mempool/mononaut/ssr
Angular Universal SSR
This commit is contained in:
commit
b4d0b75557
94 changed files with 3390 additions and 300 deletions
|
@ -280,6 +280,56 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"builder": "@angular-devkit/build-angular:server",
|
||||
"options": {
|
||||
"outputPath": "dist/mempool/server",
|
||||
"main": "server.ts",
|
||||
"tsConfig": "tsconfig.server.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"outputHashing": "media",
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"sourceMap": false,
|
||||
"localize": true,
|
||||
"optimization": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve-ssr": {
|
||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build",
|
||||
"serverTarget": "mempool:server"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "mempool:build:production",
|
||||
"serverTarget": "mempool:server:production",
|
||||
"optimization": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"prerender": {
|
||||
"builder": "@nguniversal/builders:prerender",
|
||||
"options": {
|
||||
"browserTarget": "mempool:build:production",
|
||||
"serverTarget": "mempool:server:production",
|
||||
"routes": [
|
||||
"/"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
|
|
|
@ -71,7 +71,7 @@ const newConfig = `(function (window) {
|
|||
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
|
||||
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
|
||||
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
|
||||
}(this));`;
|
||||
}((typeof global !== 'undefined') ? global : this));`;
|
||||
|
||||
const newConfigTemplate = `(function (window) {
|
||||
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}
|
||||
|
|
2659
frontend/package-lock.json
generated
2659
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -48,6 +48,9 @@
|
|||
"prettier": "prettier --write \"src/app/**/*.{js,json,css,scss,less,md,ts,html,component.html}\"",
|
||||
"e2e": "npm run generate-config && npm run ng -- e2e",
|
||||
"e2e:ci": "npm run cypress:run:ci",
|
||||
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
|
||||
"serve:ssr": "npm run generate-config && node server.run.js",
|
||||
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
|
||||
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
|
||||
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
|
||||
|
@ -61,18 +64,18 @@
|
|||
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^16.2.0",
|
||||
"@angular/animations": "^16.2.2",
|
||||
"@angular/cli": "^16.2.0",
|
||||
"@angular/common": "^16.2.2",
|
||||
"@angular/compiler": "^16.2.2",
|
||||
"@angular/core": "^16.2.2",
|
||||
"@angular/forms": "^16.2.2",
|
||||
"@angular/localize": "^16.2.2",
|
||||
"@angular/platform-browser": "^16.2.2",
|
||||
"@angular/platform-browser-dynamic": "^16.2.2",
|
||||
"@angular/platform-server": "^16.2.2",
|
||||
"@angular/router": "^16.2.2",
|
||||
"@angular-devkit/build-angular": "^16.1.1",
|
||||
"@angular/animations": "^16.1.1",
|
||||
"@angular/cli": "^16.1.1",
|
||||
"@angular/common": "^16.1.1",
|
||||
"@angular/compiler": "^16.1.1",
|
||||
"@angular/core": "^16.1.1",
|
||||
"@angular/forms": "^16.1.1",
|
||||
"@angular/localize": "^16.1.1",
|
||||
"@angular/platform-browser": "^16.1.1",
|
||||
"@angular/platform-browser-dynamic": "^16.1.1",
|
||||
"@angular/platform-server": "^16.1.1",
|
||||
"@angular/router": "^16.1.1",
|
||||
"@fortawesome/angular-fontawesome": "~0.13.0",
|
||||
"@fortawesome/fontawesome-common-types": "~6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "~6.5.1",
|
||||
|
@ -96,14 +99,17 @@
|
|||
"zone.js": "~0.13.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^16.1.5",
|
||||
"@angular/language-service": "^16.1.5",
|
||||
"@angular/compiler-cli": "^16.1.1",
|
||||
"@angular/language-service": "^16.1.1",
|
||||
"@nguniversal/builders": "16.1.1",
|
||||
"@nguniversal/express-engine": "16.1.1",
|
||||
"@types/node": "^18.11.9",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"eslint": "^8.31.0",
|
||||
"http-proxy-middleware": "~2.0.6",
|
||||
"prettier": "^3.0.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.9.3"
|
||||
},
|
||||
|
|
105
frontend/server.run.ts
Normal file
105
frontend/server.run.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import 'zone.js/dist/zone-node';
|
||||
import './src/resources/config.js';
|
||||
|
||||
import * as domino from 'domino';
|
||||
import * as express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const {readFileSync, existsSync} = require('fs');
|
||||
const {createProxyMiddleware} = require('http-proxy-middleware');
|
||||
|
||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
||||
const win = domino.createWindow(template);
|
||||
|
||||
// @ts-ignore
|
||||
win.__env = global.__env;
|
||||
|
||||
// @ts-ignore
|
||||
win.matchMedia = () => {
|
||||
return {
|
||||
matches: true
|
||||
};
|
||||
};
|
||||
// @ts-ignore
|
||||
win.setTimeout = (fn) => { fn(); };
|
||||
win.document.body.scrollTo = (() => {});
|
||||
// @ts-ignore
|
||||
global['window'] = win;
|
||||
global['document'] = win.document;
|
||||
// @ts-ignore
|
||||
global['history'] = { state: { } };
|
||||
|
||||
global['localStorage'] = {
|
||||
getItem: () => '',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the list of supported and actually active locales
|
||||
*/
|
||||
function getActiveLocales() {
|
||||
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
|
||||
|
||||
const supportedLocales = [
|
||||
angularConfig.projects.mempool.i18n.sourceLocale,
|
||||
...Object.keys(angularConfig.projects.mempool.i18n.locales),
|
||||
];
|
||||
|
||||
return supportedLocales.filter(locale => locale === 'en-US' && existsSync(`./dist/mempool/server/${locale}`));
|
||||
// return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
|
||||
}
|
||||
|
||||
function app() {
|
||||
const server = express();
|
||||
|
||||
// proxy websocket
|
||||
server.get('/api/v1/ws', createProxyMiddleware({
|
||||
target: 'ws://localhost:4200/api/v1/ws',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
logLevel: 'debug'
|
||||
}));
|
||||
// proxy API to nginx
|
||||
server.get('/api/**', createProxyMiddleware({
|
||||
// @ts-ignore
|
||||
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
|
||||
changeOrigin: true,
|
||||
}));
|
||||
server.get('/resources/**', express.static('./src'));
|
||||
|
||||
|
||||
// map / and /en to en-US
|
||||
const defaultLocale = 'en-US';
|
||||
console.log(`serving default locale: ${defaultLocale}`);
|
||||
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
|
||||
server.use('/', appServerModule.app(defaultLocale));
|
||||
server.use('/en', appServerModule.app(defaultLocale));
|
||||
|
||||
// map each locale to its localized main.js
|
||||
getActiveLocales().forEach(locale => {
|
||||
console.log('serving locale:', locale);
|
||||
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
|
||||
|
||||
// map everything to itself
|
||||
server.use(`/${locale}`, appServerModule.app(locale));
|
||||
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function run() {
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Start up the Node server
|
||||
app().listen(port, () => {
|
||||
console.log(`Node Express server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
112
frontend/server.ts
Normal file
112
frontend/server.ts
Normal file
|
@ -0,0 +1,112 @@
|
|||
import 'zone.js/dist/zone-node';
|
||||
import './src/resources/config.js';
|
||||
|
||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||
import * as express from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as domino from 'domino';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
import { join } from 'path';
|
||||
import { AppServerModule } from './src/main.server';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
import { ResizeObserver } from './shims';
|
||||
|
||||
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
|
||||
const win = domino.createWindow(template);
|
||||
|
||||
// @ts-ignore
|
||||
win.__env = global.__env;
|
||||
|
||||
// @ts-ignore
|
||||
win.matchMedia = (media) => {
|
||||
return {
|
||||
media,
|
||||
matches: true,
|
||||
};
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
win.setTimeout = (fn) => { fn(); };
|
||||
win.document.body.scrollTo = (() => {});
|
||||
win['ResizeObserver'] = ResizeObserver;
|
||||
// @ts-ignore
|
||||
global['window'] = win;
|
||||
// @ts-ignore
|
||||
global['document'] = win.document;
|
||||
// @ts-ignore
|
||||
global['history'] = { state: { } };
|
||||
// @ts-ignore
|
||||
Object.defineProperty(global, 'navigator', {
|
||||
value: win.navigator,
|
||||
writable: true
|
||||
});
|
||||
|
||||
global['localStorage'] = {
|
||||
getItem: () => '',
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
clear: () => {},
|
||||
length: 0,
|
||||
key: () => '',
|
||||
};
|
||||
|
||||
// The Express app is exported so that it can be used by serverless Functions.
|
||||
export function app(locale: string): express.Express {
|
||||
const server = express();
|
||||
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
|
||||
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
|
||||
|
||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||
server.engine('html', ngExpressEngine({
|
||||
bootstrap: AppServerModule,
|
||||
}));
|
||||
|
||||
server.set('view engine', 'html');
|
||||
server.set('views', distFolder);
|
||||
|
||||
|
||||
// static file handler so we send HTTP 404 to nginx
|
||||
server.get('/**.(css|js|json|ico|webmanifest|png|jpg|jpeg|svg|mp4)*', express.static(distFolder, { maxAge: '1y', fallthrough: false }));
|
||||
// handle page routes
|
||||
server.get('/**', getLocalizedSSR(indexHtml));
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function getLocalizedSSR(indexHtml) {
|
||||
return (req, res) => {
|
||||
res.render(indexHtml, {
|
||||
req,
|
||||
providers: [
|
||||
{ provide: APP_BASE_HREF, useValue: req.baseUrl }
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// only used for development mode
|
||||
function run(): void {
|
||||
const port = process.env.PORT || 4000;
|
||||
|
||||
// Start up the Node server
|
||||
const server = app('en-US');
|
||||
server.listen(port, () => {
|
||||
console.log(`Node Express server listening on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Webpack will replace 'require' with '__webpack_require__'
|
||||
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||
declare const __non_webpack_require__: NodeRequire;
|
||||
const mainModule = __non_webpack_require__.main;
|
||||
const moduleFilename = mainModule && mainModule.filename || '';
|
||||
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||
run();
|
||||
}
|
||||
|
||||
export * from './src/main.server';
|
7
frontend/shims.ts
Normal file
7
frontend/shims.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export class ResizeObserver {
|
||||
constructor() {}
|
||||
|
||||
disconnect() {}
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
}
|
|
@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
|
|||
import { ModuleWithProviders, NgModule } from '@angular/core';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { ElectrsApiService } from './services/electrs-api.service';
|
||||
|
@ -13,6 +14,7 @@ import { WebsocketService } from './services/websocket.service';
|
|||
import { AudioService } from './services/audio.service';
|
||||
import { SeoService } from './services/seo.service';
|
||||
import { OpenGraphService } from './services/opengraph.service';
|
||||
import { ZoneService } from './services/zone-shim.service';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { StorageService } from './services/storage.service';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
|
@ -42,7 +44,8 @@ const providers = [
|
|||
CapAddressPipe,
|
||||
AppPreloadingStrategy,
|
||||
ServicesApiServices,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
|
||||
{ provide: ZONE_SERVICE, useClass: ZoneService },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
|
||||
import { ServerModule } from '@angular/platform-server';
|
||||
|
||||
import { ZONE_SERVICE } from './injection-tokens';
|
||||
import { AppModule } from './app.module';
|
||||
import { AppComponent } from './components/app/app.component';
|
||||
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
|
||||
import { ZoneService } from './services/zone.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
AppModule,
|
||||
ServerModule,
|
||||
ServerTransferStateModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
|
||||
{ provide: ZONE_SERVICE, useClass: ZoneService },
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppServerModule {}
|
||||
export class AppServerModule {}
|
|
@ -45,10 +45,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
|
||||
import { EChartsOption } from 'echarts';
|
||||
import { EChartsOption } from '../../../graphs/echarts';
|
||||
import { Observable, Subscription, combineLatest, fromEvent, share } from 'rxjs';
|
||||
import { startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
|
@ -11,6 +11,7 @@ import { MiningService } from '../../../services/mining.service';
|
|||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Acceleration } from '../../../interfaces/node-api.interface';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-acceleration-fees-graph',
|
||||
|
@ -59,6 +60,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
|||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
private route: ActivatedRoute,
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
|
@ -176,10 +178,10 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
|
|||
padding: [10, 0, 0, 0],
|
||||
},
|
||||
type: 'time',
|
||||
boundaryGap: false,
|
||||
boundaryGap: [0, 0],
|
||||
axisLine: { onZero: true },
|
||||
axisLabel: {
|
||||
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
|
||||
formatter: (val): string => formatterXAxisTimeCategory(this.locale, this.timespan, val),
|
||||
align: 'center',
|
||||
fontSize: 11,
|
||||
lineHeight: 12,
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
<span> </span>
|
||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||
</a>
|
||||
<div class="mempool-block-wrapper">
|
||||
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
|
||||
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -66,16 +66,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avg block fees graph -->
|
||||
<!-- <div class="col" style="margin-bottom: 1.47rem">
|
||||
<div class="card">
|
||||
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
|
||||
<app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph>
|
||||
<div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more »</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- Recent Accelerations List -->
|
||||
<div class="col">
|
||||
<div class="card list-card">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { SeoService } from '../../../services/seo.service';
|
||||
import { OpenGraphService } from '../../../services/opengraph.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
|
@ -10,6 +10,7 @@ import { hexToColor } from '../../block-overview-graph/utils';
|
|||
import TxView from '../../block-overview-graph/tx-view';
|
||||
import { feeLevels, mempoolFeeColors } from '../../../app.constants';
|
||||
import { ServicesApiServices } from '../../../services/services-api.service';
|
||||
import { detectWebGL } from '../../../shared/graphs.utils';
|
||||
|
||||
const acceleratedColor: Color = hexToColor('8F5FF6');
|
||||
const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F'));
|
||||
|
@ -30,6 +31,7 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||
pendingAccelerations$: Observable<Acceleration[]>;
|
||||
minedAccelerations$: Observable<Acceleration[]>;
|
||||
loadingBlocks: boolean = true;
|
||||
webGlEnabled = true;
|
||||
|
||||
graphHeight: number = 300;
|
||||
|
||||
|
@ -39,7 +41,9 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||
private websocketService: WebsocketService,
|
||||
private serviceApiServices: ServicesApiServices,
|
||||
private stateService: StateService,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`);
|
||||
this.ogService.setManualOgImage('accelerator.jpg');
|
||||
}
|
||||
|
@ -48,7 +52,7 @@ export class AcceleratorDashboardComponent implements OnInit {
|
|||
this.onResize();
|
||||
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
|
||||
|
||||
this.pendingAccelerations$ = interval(30000).pipe(
|
||||
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.serviceApiServices.getAccelerations$().pipe(
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
</div>
|
||||
|
||||
<ng-container *ngIf="!error">
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
|
|
|
@ -62,10 +62,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
|
||||
import { EChartsOption, graphic } from 'echarts';
|
||||
import { echarts, EChartsOption } from '../../graphs/echarts';
|
||||
import { Observable, combineLatest, of } from 'rxjs';
|
||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
@ -55,7 +55,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private route: ActivatedRoute,
|
||||
|
@ -209,7 +209,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||
|
||||
prepareChartOptions(data, weightMode) {
|
||||
this.chartOptions = {
|
||||
color: this.widget ? ['#6b6b6b', new graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
color: this.widget ? ['#6b6b6b', new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
|
||||
{ offset: 0, color: '#F4511E' },
|
||||
{ offset: 0.25, color: '#FB8C00' },
|
||||
{ offset: 0.5, color: '#FFB300' },
|
||||
|
@ -282,7 +282,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
|
|||
legend: (this.widget || data.series.length === 0) ? undefined : {
|
||||
padding: [10, 75],
|
||||
data: data.legends,
|
||||
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
|
||||
selected: JSON.parse(this.storageService.getValue('fee_rates_legend') || 'null') ?? {
|
||||
'Min': true,
|
||||
'10th': true,
|
||||
'25th': true,
|
||||
|
|
|
@ -36,10 +36,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { MiningService } from '../../services/mining.service';
|
|||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-fees-graph',
|
||||
|
@ -54,6 +55,7 @@ export class BlockFeesGraphComponent implements OnInit {
|
|||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private route: ActivatedRoute,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
|
|
|
@ -45,10 +45,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ export class BlockHealthGraphComponent implements OnInit {
|
|||
private storageService: StorageService,
|
||||
private zone: NgZone,
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
<div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
|
||||
<div class="block-overview-graph">
|
||||
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
|
||||
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
|
||||
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
|
||||
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
|
||||
|
|
|
@ -83,9 +83,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
constructor(
|
||||
readonly ngZone: NgZone,
|
||||
readonly elRef: ElementRef,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
|
||||
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
|
||||
this.searchText = text;
|
||||
|
@ -94,13 +94,15 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
|
||||
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
|
||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||
if (this.canvas) {
|
||||
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
|
||||
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
|
||||
this.gl = this.canvas.nativeElement.getContext('webgl');
|
||||
|
||||
if (this.gl) {
|
||||
this.initCanvas();
|
||||
this.resizeCanvas();
|
||||
if (this.gl) {
|
||||
this.initCanvas();
|
||||
this.resizeCanvas();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,8 +144,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
cancelAnimationFrame(this.animationFrameRequest);
|
||||
clearTimeout(this.animationHeartBeat);
|
||||
}
|
||||
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
|
||||
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
|
||||
if (this.canvas) {
|
||||
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
|
||||
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
|
||||
}
|
||||
}
|
||||
|
||||
clear(direction): void {
|
||||
|
@ -209,6 +213,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
}
|
||||
|
||||
initCanvas(): void {
|
||||
if (!this.canvas || !this.gl) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
||||
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
||||
|
||||
|
@ -262,24 +270,26 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
resizeCanvas(): void {
|
||||
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
|
||||
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
|
||||
this.displayWidth = window.devicePixelRatio * this.cssWidth;
|
||||
this.displayHeight = window.devicePixelRatio * this.cssHeight;
|
||||
this.canvas.nativeElement.width = this.displayWidth;
|
||||
this.canvas.nativeElement.height = this.displayHeight;
|
||||
if (this.gl) {
|
||||
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
||||
}
|
||||
if (this.scene) {
|
||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
|
||||
this.start();
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
|
||||
if (this.canvas) {
|
||||
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
|
||||
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
|
||||
this.displayWidth = window.devicePixelRatio * this.cssWidth;
|
||||
this.displayHeight = window.devicePixelRatio * this.cssHeight;
|
||||
this.canvas.nativeElement.width = this.displayWidth;
|
||||
this.canvas.nativeElement.height = this.displayHeight;
|
||||
if (this.gl) {
|
||||
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
|
||||
}
|
||||
if (this.scene) {
|
||||
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
|
||||
this.start();
|
||||
} else {
|
||||
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
|
||||
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
|
||||
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
|
||||
colorFunction: this.getColorFunction() });
|
||||
this.start();
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -406,6 +416,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
|
||||
@HostListener('pointerup', ['$event'])
|
||||
onClick(event) {
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') {
|
||||
this.setPreviewTx(event.offsetX, event.offsetY, true);
|
||||
} else if (event.target === this.canvas.nativeElement) {
|
||||
|
@ -417,6 +430,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
|
|||
|
||||
@HostListener('pointermove', ['$event'])
|
||||
onPointerMove(event) {
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
if (event.target === this.canvas.nativeElement) {
|
||||
this.setPreviewTx(event.offsetX, event.offsetY, false);
|
||||
} else {
|
||||
|
|
|
@ -37,10 +37,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import { StorageService } from '../../services/storage.service';
|
|||
import { ActivatedRoute } from '@angular/router';
|
||||
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
|
||||
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-rewards-graph',
|
||||
|
@ -54,6 +55,7 @@ export class BlockRewardsGraphComponent implements OnInit {
|
|||
private formBuilder: UntypedFormBuilder,
|
||||
private miningService: MiningService,
|
||||
private storageService: StorageService,
|
||||
public stateService: StateService,
|
||||
private route: ActivatedRoute,
|
||||
private fiatShortenerPipe: FiatShortenerPipe,
|
||||
private fiatCurrencyPipe: FiatCurrencyPipe,
|
||||
|
|
|
@ -44,10 +44,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { StorageService } from '../../services/storage.service';
|
|||
import { MiningService } from '../../services/mining.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { download, formatterXAxis } from '../../shared/graphs.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-block-sizes-weights-graph',
|
||||
|
@ -52,6 +53,7 @@ export class BlockSizesWeightsGraphComponent implements OnInit {
|
|||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
<div class="col-sm chart-container" *ngIf="webGlEnabled && !showAudit">
|
||||
<app-block-overview-graph
|
||||
#blockGraphActual
|
||||
[isLoading]="isLoadingOverview"
|
||||
[isLoading]="!stateService.isBrowser || isLoadingOverview"
|
||||
[resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="'top'"
|
||||
|
@ -229,7 +229,7 @@
|
|||
<div class="col-sm audit-col" [class.mobile]="isMobile">
|
||||
<h3 class="block-subtitle" *ngIf="!isMobile"><ng-container i18n="block.expected-block">Expected Block</ng-container></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
<app-block-overview-graph #blockGraphProjected [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="!isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
|
@ -244,7 +244,7 @@
|
|||
<div class="col-sm audit-col" *ngIf="!isMobile">
|
||||
<h3 class="block-subtitle actual" *ngIf="!isMobile"><ng-container i18n="block.actual-block">Actual Block</ng-container><a class="info-link" [routerLink]="['/docs/faq' | relativeUrl ]" fragment="how-do-block-audits-work"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon></a></h3>
|
||||
<div class="block-graph-wrapper">
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="isLoadingOverview" [resolution]="86"
|
||||
<app-block-overview-graph #blockGraphActual [isLoading]="!stateService.isBrowser || isLoadingOverview" [resolution]="86"
|
||||
[blockLimit]="stateService.blockVSize" [orientation]="'top'" [flip]="false" [mirrorTxid]="hoverTx" mode="mined" [auditHighlighting]="showAudit"
|
||||
(txClickEvent)="onTxClick($event)" (txHoverEvent)="onTxHover($event)" [unavailable]="isMobile && !showAudit"
|
||||
[showFilters]="true" [excludeFilters]="['replacement']"></app-block-overview-graph>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ViewChildren, QueryList, Inject, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
|
@ -108,8 +108,10 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
private priceService: PriceService,
|
||||
private cacheService: CacheService,
|
||||
private servicesApiService: ServicesApiServices,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -318,6 +320,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
this.transactions = transactions;
|
||||
this.isLoadingTransactions = false;
|
||||
this.cd.markForCheck();
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
|
@ -471,6 +474,7 @@ export class BlockComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.isLoadingOverview = false;
|
||||
this.setupBlockGraphs();
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.oobSubscription = block$.pipe(
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<div *ngIf="indexingAvailable" class="tooltip-custom">
|
||||
<a class="clear-link" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]">
|
||||
<img width="22" height="22" src="{{ block.extras.pool['logo'] }}"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">
|
||||
<span class="pool-name">{{ block.extras.pool.name }}</span>
|
||||
</a>
|
||||
<span *ngIf="!widget" class="tooltiptext badge badge-secondary scriptmessage">{{ block.extras.coinbaseRaw | hex2ascii }}</span>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Subscription, tap, timer } from 'rxjs';
|
||||
import { WebsocketService } from '../../services/websocket.service';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
|
@ -33,17 +32,20 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.timeSubscription = timer(0, 250).pipe(
|
||||
tap(() => {
|
||||
this.updateTime();
|
||||
})
|
||||
).subscribe();
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe((blocks) => {
|
||||
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
|
||||
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
||||
this.updateSegments();
|
||||
});
|
||||
if (this.stateService.isBrowser) {
|
||||
this.timeSubscription = timer(0, 250).pipe(
|
||||
tap(() => {
|
||||
console.log('face tick');
|
||||
this.updateTime();
|
||||
})
|
||||
).subscribe();
|
||||
this.blocksSubscription = this.stateService.blocks$
|
||||
.subscribe((blocks) => {
|
||||
this.blockTimes = blocks.map(block => [block.height, new Date(block.timestamp * 1000)]);
|
||||
this.blockTimes = this.blockTimes.sort((a, b) => a[1].getTime() - b[1].getTime());
|
||||
this.updateSegments();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
|
@ -54,7 +56,9 @@ export class ClockFaceComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.timeSubscription.unsubscribe();
|
||||
if (this.timeSubscription) {
|
||||
this.timeSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
updateTime(): void {
|
||||
|
|
|
@ -110,8 +110,8 @@ export class ClockComponent implements OnInit {
|
|||
|
||||
@HostListener('window:resize', ['$event'])
|
||||
resizeCanvas(): void {
|
||||
const windowWidth = this.limitWidth || window.innerWidth;
|
||||
const windowHeight = this.limitHeight || window.innerHeight;
|
||||
const windowWidth = this.limitWidth || window.innerWidth || 800;
|
||||
const windowHeight = this.limitHeight || window.innerHeight || 800;
|
||||
this.chainWidth = windowWidth;
|
||||
this.chainHeight = Math.max(60, windowHeight / 8);
|
||||
this.clockSize = Math.min(800, windowWidth, windowHeight - (1.4 * this.chainHeight));
|
||||
|
|
|
@ -95,7 +95,7 @@ export class EightBlocksComponent implements OnInit, OnDestroy {
|
|||
private apiService: ApiService,
|
||||
private bytesPipe: BytesPipe,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="fee-distribution-chart" *ngIf="mempoolVsizeFeesOptions; else loadingFees">
|
||||
<div echarts [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
|
||||
<div class="fee-distribution-chart" *ngIf="mempoolVsizeFeesOptions && stateService.isBrowser; else loadingFees">
|
||||
<div *browserOnly echarts [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingFees>
|
||||
|
|
|
@ -36,7 +36,7 @@ export class FeeDistributionGraphComponent implements OnInit, OnChanges, OnDestr
|
|||
};
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private vbytesPipe: VbytesPipe,
|
||||
) { }
|
||||
|
||||
|
|
|
@ -54,10 +54,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? ((height + 20) + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@ export class HashrateChartComponent implements OnInit {
|
|||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
private route: ActivatedRoute,
|
||||
private stateService: StateService
|
||||
public stateService: StateService
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -326,7 +326,7 @@ export class HashrateChartComponent implements OnInit {
|
|||
},
|
||||
},
|
||||
],
|
||||
selected: JSON.parse(this.storageService.getValue('hashrate_difficulty_legend')) ?? {
|
||||
selected: JSON.parse(this.storageService?.getValue('hashrate_difficulty_legend') || 'null') ?? {
|
||||
'$localize`:@@79a9dc5b1caca3cbeb1733a19515edacc5fc7920:Hashrate`': true,
|
||||
'$localize`::Difficulty`': this.network === '',
|
||||
'$localize`Hashrate (MA)`': true,
|
||||
|
|
|
@ -31,10 +31,10 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { StorageService } from '../../services/storage.service';
|
|||
import { MiningService } from '../../services/mining.service';
|
||||
import { download } from '../../shared/graphs.utils';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
interface Hashrate {
|
||||
timestamp: number;
|
||||
|
@ -60,6 +61,7 @@ export class HashrateChartPoolsComponent implements OnInit {
|
|||
private cd: ChangeDetectorRef,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="echarts" echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption" (chartRendered)="rendered()"
|
||||
<div class="echarts" *browserOnly echarts [initOpts]="mempoolStatsChartInitOption" [options]="mempoolStatsChartOption" (chartRendered)="rendered()"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
|
@ -48,7 +48,7 @@ export class IncomingTransactionsGraphComponent implements OnInit, OnChanges, On
|
|||
constructor(
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
private storageService: StorageService,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="echarts" echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="echarts" *browserOnly echarts [initOpts]="pegsChartInitOption" [options]="pegsChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
|
@ -1,6 +1,7 @@
|
|||
import { Component, Inject, LOCALE_ID, ChangeDetectionStrategy, Input, OnChanges, OnInit } from '@angular/core';
|
||||
import { formatDate, formatNumber } from '@angular/common';
|
||||
import { EChartsOption } from '../../graphs/echarts';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lbtc-pegs-graph',
|
||||
|
@ -32,6 +33,7 @@ export class LbtcPegsGraphComponent implements OnInit, OnChanges {
|
|||
};
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class="echarts" echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="echarts" *browserOnly echarts [initOpts]="ratioChartInitOptions" [options]="ratioChartOptions" (chartRendered)="rendered()"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<app-block-overview-graph
|
||||
#blockGraph
|
||||
[isLoading]="isLoading$ | async"
|
||||
[isLoading]="(isLoading$ | async) || !stateService.isBrowser"
|
||||
[resolution]="resolution"
|
||||
[blockLimit]="stateService.blockVSize"
|
||||
[orientation]="timeLtr ? 'right' : 'left'"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, Inject, PLATFORM_ID, ChangeDetectorRef } from '@angular/core';
|
||||
import { detectWebGL } from '../../shared/graphs.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { switchMap, map, tap, filter } from 'rxjs/operators';
|
||||
|
@ -29,8 +30,9 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||
private seoService: SeoService,
|
||||
private websocketService: WebsocketService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
this.webGlEnabled = detectWebGL();
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -93,9 +95,3 @@ export class MempoolBlockComponent implements OnInit, OnDestroy {
|
|||
this.previewTx = event;
|
||||
}
|
||||
}
|
||||
|
||||
function detectWebGL() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
||||
return (gl && gl instanceof WebGLRenderingContext);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div echarts class="echarts" (chartInit)="onChartReady($event)" (chartRendered)="rendered()" [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div *browserOnly echarts class="echarts" (chartInit)="onChartReady($event)" (chartRendered)="rendered()" [initOpts]="mempoolVsizeFeesInitOptions" [options]="mempoolVsizeFeesOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
|
@ -59,7 +59,7 @@ export class MempoolGraphComponent implements OnInit, OnChanges {
|
|||
private vbytesPipe: VbytesPipe,
|
||||
private wubytesPipe: WuBytesPipe,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
) { }
|
||||
|
|
|
@ -76,11 +76,11 @@
|
|||
</div>
|
||||
|
||||
<div [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
@ -102,7 +102,7 @@
|
|||
<tr *ngFor="let pool of miningStats.pools">
|
||||
<td class="d-none d-md-table-cell">{{ pool.rank }}</td>
|
||||
<td class="text-right">
|
||||
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.src = '/resources/mining-pools/default.svg'">
|
||||
<img width="25" height="25" src="{{ pool.logo }}" [alt]="pool.name + ' mining pool logo'" onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'">
|
||||
</td>
|
||||
<td class="pool-name"><a [routerLink]="[('/mining/pool/' + pool.slug) | relativeUrl]">{{ pool.name }}</a></td>
|
||||
<td class="" *ngIf="this.miningWindowPreference === '24h'">{{ pool.lastEstimatedHashrate }} {{
|
||||
|
|
|
@ -41,7 +41,7 @@ export class PoolRankingComponent implements OnInit {
|
|||
miningStatsObservable$: Observable<MiningStats>;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private storageService: StorageService,
|
||||
private formBuilder: UntypedFormBuilder,
|
||||
private miningService: MiningService,
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="row hash-chart full-width-row">
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartFinished)="onChartReady()"></div>
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartFinished)="onChartReady()"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<div *ngIf="poolStats$ | async as poolStats; else loadingMain">
|
||||
<div style="display:flex" class="mb-3">
|
||||
<img width="50" height="50" src="{{ poolStats['logo'] }}" [alt]="poolStats.pool.name + ' mining pool logo'"
|
||||
onError="this.src = '/resources/mining-pools/default.svg'" class="mr-3">
|
||||
onError="this.onerror=null; this.src = '/resources/mining-pools/default.svg'" class="mr-3">
|
||||
<h1 class="m-0 pt-1 pt-md-0">{{ poolStats.pool.name }}</h1>
|
||||
</div>
|
||||
|
||||
|
@ -168,8 +168,8 @@
|
|||
</ng-template>
|
||||
|
||||
<!-- Hashrate chart -->
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -35,4 +35,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *serverOnly>
|
||||
<!-- disgusting hack to apply an initial scroll to server-side rendered blockchain bar -->
|
||||
<img *ngIf="!stateService.isLiquid()" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="pixel" style="visibility: hidden; position: absolute;" onload="(() => { const b = document.getElementById('blockchain-container'); const d = document.getElementById('divider'); if (b && d) { b.scrollLeft = d.getBoundingClientRect().x - (window.innerWidth * (window.innerWidth >= 768 ? 0.5 : 0.95)); }})()">
|
||||
<img *ngIf="stateService.isLiquid()" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="pixel" style="visibility: hidden; position: absolute;" onload="(() => { const b = document.getElementById('blockchain-container'); const d = document.getElementById('divider'); if (b && d) { b.scrollLeft = d.getBoundingClientRect().x - (window.innerWidth >= 768 ? 420 : (window.innerWidth * 0.5)); }})()">
|
||||
</ng-container>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
|
|
@ -59,7 +59,7 @@ export class StartComponent implements OnInit, AfterViewChecked, OnDestroy {
|
|||
hasMenu = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {
|
||||
this.isiOS = ['iPhone','iPod','iPad'].includes((navigator as any)?.userAgentData?.platform || navigator.platform);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef, Inject, ChangeDetectorRef } from '@angular/core';
|
||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||
import {
|
||||
|
@ -28,6 +28,7 @@ import { Price, PriceService } from '../../services/price.service';
|
|||
import { isFeatureActive } from '../../bitcoin.utils';
|
||||
import { ServicesApiServices } from '../../services/services-api.service';
|
||||
import { EnterpriseService } from '../../services/enterprise.service';
|
||||
import { ZONE_SERVICE } from '../../injection-tokens';
|
||||
|
||||
interface Pool {
|
||||
id: number;
|
||||
|
@ -101,7 +102,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
inputIndex: number;
|
||||
outputIndex: number;
|
||||
graphExpanded: boolean = false;
|
||||
graphWidth: number = 1000;
|
||||
graphWidth: number = 1068;
|
||||
graphHeight: number = 360;
|
||||
inOutLimit: number = 150;
|
||||
maxInOut: number = 0;
|
||||
|
@ -141,6 +142,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
private priceService: PriceService,
|
||||
private storageService: StorageService,
|
||||
private enterpriseService: EnterpriseService,
|
||||
private cd: ChangeDetectorRef,
|
||||
@Inject(ZONE_SERVICE) private zoneService: any,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
|
@ -356,7 +359,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
});
|
||||
|
||||
this.subscription = this.route.paramMap
|
||||
this.subscription = this.zoneService.wrapObservable(this.route.paramMap
|
||||
.pipe(
|
||||
switchMap((params: ParamMap) => {
|
||||
const urlMatch = (params.get('id') || '').split(':');
|
||||
|
@ -430,7 +433,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}
|
||||
return of(tx);
|
||||
})
|
||||
)
|
||||
))
|
||||
.subscribe((tx: Transaction) => {
|
||||
if (!tx) {
|
||||
this.fetchCachedTx$.next(this.txId);
|
||||
|
@ -503,6 +506,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
).subscribe();
|
||||
|
||||
setTimeout(() => { this.applyFragment(); }, 0);
|
||||
|
||||
this.cd.detectChanges();
|
||||
},
|
||||
(error) => {
|
||||
this.error = error;
|
||||
|
@ -785,9 +790,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
@HostListener('window:resize', ['$event'])
|
||||
setGraphSize(): void {
|
||||
this.isMobile = window.innerWidth < 850;
|
||||
if (this.graphContainer?.nativeElement) {
|
||||
if (this.graphContainer?.nativeElement && this.stateService.isBrowser) {
|
||||
setTimeout(() => {
|
||||
if (this.graphContainer?.nativeElement) {
|
||||
if (this.graphContainer?.nativeElement?.clientWidth) {
|
||||
this.graphWidth = this.graphContainer.nativeElement.clientWidth;
|
||||
} else {
|
||||
setTimeout(() => { this.setGraphSize(); }, 1);
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<div class="bowtie-graph">
|
||||
<svg *ngIf="inputs && outputs" class="bowtie" [class.rtl]="dir === 'rtl'" [attr.height]="(height + 10) + 'px'" [attr.width]="width + 'px'">
|
||||
<svg
|
||||
*ngIf="inputs && outputs"
|
||||
class="bowtie"
|
||||
[class.rtl]="dir === 'rtl'"
|
||||
[attr.height]="(height + 10) + 'px'"
|
||||
[attr.width]="stateService.isBrowser ? (width + 'px') : '100%'"
|
||||
[attr.viewBox]="stateService.isBrowser ? null : ('0 0 ' + width + ' ' + (height + 10))"
|
||||
[attr.preserveAspectRatio]="stateService.isBrowser ? null : 'none'"
|
||||
>
|
||||
<defs>
|
||||
<marker id="input-arrow" viewBox="-5 -5 10 10"
|
||||
refX="0" refY="0"
|
||||
|
|
|
@ -101,7 +101,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
|
|||
constructor(
|
||||
private router: Router,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private electrsApiService: ElectrsApiService,
|
||||
private assetsService: AssetsService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
<div class="container-xl dashboard-container" *ngIf="(network$ | async) !== 'liquid'; else liquidDashboard">
|
||||
<div class="row row-cols-1 row-cols-md-2" *ngIf="{ value: (mempoolInfoData$ | async) } as mempoolInfoData">
|
||||
<ng-container *ngIf="(network$ | async) !== 'liquidtestnet'">
|
||||
<ng-container>
|
||||
<div class="col card-wrapper">
|
||||
<div class="main-title" i18n="fees-box.transaction-fees">Transaction Fees</div>
|
||||
<div class="card">
|
||||
|
@ -29,7 +29,7 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mempool-block-wrapper">
|
||||
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
|
||||
<app-mempool-block-overview
|
||||
[index]="0"
|
||||
[resolution]="goggleResolution"
|
||||
|
@ -45,12 +45,12 @@
|
|||
<div class="card-body">
|
||||
<ng-container *ngTemplateOutlet="mempoolTable; context: { $implicit: mempoolInfoData }"></ng-container>
|
||||
<h5 class="card-title mt-3" i18n="dashboard.incoming-transactions">Incoming Transactions</h5>
|
||||
<div class="mempool-graph" *ngIf="{ value: (mempoolStats$ | async) } as mempoolStats">
|
||||
<div class="mempool-graph" *ngIf="(mempoolStats$ | async) as mempoolStats">
|
||||
<app-incoming-transactions-graph
|
||||
[height]="incomingGraphHeight"
|
||||
[left]="50"
|
||||
[right]="20"
|
||||
[data]="mempoolStats.value?.weightPerSecond"
|
||||
[data]="mempoolStats?.weightPerSecond"
|
||||
[windowPreferenceOverride]="'2h'"
|
||||
></app-incoming-transactions-graph>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnDestroy, OnInit } from '@angular/core';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||
import { combineLatest, EMPTY, fromEvent, interval, merge, Observable, of, Subject, Subscription, timer } from 'rxjs';
|
||||
import { catchError, delayWhen, distinctUntilChanged, filter, map, scan, share, shareReplay, startWith, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||
import { AuditStatus, BlockExtended, CurrentPegs, FederationAddress, FederationUtxo, OptimizedMempoolStats, PegsVolume, RecentPeg } from '../interfaces/node-api.interface';
|
||||
|
@ -8,6 +8,7 @@ import { StateService } from '../services/state.service';
|
|||
import { WebsocketService } from '../services/websocket.service';
|
||||
import { SeoService } from '../services/seo.service';
|
||||
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils';
|
||||
import { detectWebGL } from '../shared/graphs.utils';
|
||||
|
||||
interface MempoolBlocksData {
|
||||
blocks: number;
|
||||
|
@ -67,6 +68,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
currency: string;
|
||||
incomingGraphHeight: number = 300;
|
||||
lbtcPegGraphHeight: number = 360;
|
||||
webGlEnabled = true;
|
||||
private lastPegBlockUpdate: number = 0;
|
||||
private lastPegAmount: string = '';
|
||||
private lastReservesBlockUpdate: number = 0;
|
||||
|
@ -88,8 +90,11 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
public stateService: StateService,
|
||||
private apiService: ApiService,
|
||||
private websocketService: WebsocketService,
|
||||
private seoService: SeoService
|
||||
) { }
|
||||
private seoService: SeoService,
|
||||
@Inject(PLATFORM_ID) private platformId: Object,
|
||||
) {
|
||||
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.stateService.focusSearchInputDesktop();
|
||||
|
@ -241,7 +246,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||
return null;
|
||||
}
|
||||
}),
|
||||
share(),
|
||||
shareReplay(1),
|
||||
);
|
||||
|
||||
if (this.stateService.network === 'liquid') {
|
||||
|
|
3
frontend/src/app/injection-tokens.ts
Normal file
3
frontend/src/app/injection-tokens.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { InjectionToken } from '@angular/core';
|
||||
|
||||
export const ZONE_SERVICE = new InjectionToken('ZONE_TASK');
|
|
@ -8,6 +8,7 @@ import { IChannel, INodesRanking, IOldestNodes, ITopNodesPerCapacity, ITopNodesP
|
|||
providedIn: 'root'
|
||||
})
|
||||
export class LightningApiService {
|
||||
private apiBaseUrl: string; // base URL is protocol, hostname, and port
|
||||
private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
|
||||
|
||||
private requestCache = new Map<string, { subject: BehaviorSubject<any>, expiry: number }>;
|
||||
|
@ -16,6 +17,10 @@ export class LightningApiService {
|
|||
private httpClient: HttpClient,
|
||||
private stateService: StateService,
|
||||
) {
|
||||
this.apiBaseUrl = ''; // use relative URL by default
|
||||
if (!stateService.isBrowser) { // except when inside AU SSR process
|
||||
this.apiBaseUrl = this.stateService.env.NGINX_PROTOCOL + '://' + this.stateService.env.NGINX_HOSTNAME + ':' + this.stateService.env.NGINX_PORT;
|
||||
}
|
||||
this.apiBasePath = ''; // assume mainnet by default
|
||||
this.stateService.networkChanged$.subscribe((network) => {
|
||||
if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
|
||||
|
@ -66,15 +71,15 @@ export class LightningApiService {
|
|||
}
|
||||
|
||||
getNode$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
|
||||
}
|
||||
|
||||
getNodeGroup$(name: string): Observable<any[]> {
|
||||
return this.httpClient.get<any[]>(this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/group/' + name);
|
||||
}
|
||||
|
||||
getChannel$(shortId: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
|
||||
}
|
||||
|
||||
getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
|
||||
|
@ -84,57 +89,57 @@ export class LightningApiService {
|
|||
.set('status', status)
|
||||
;
|
||||
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
|
||||
}
|
||||
|
||||
getLatestStatistics$(): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics/latest');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics/latest');
|
||||
}
|
||||
|
||||
listNodeStats$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
getNodeFeeHistogram$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/fees/histogram');
|
||||
}
|
||||
|
||||
getNodesRanking$(): Observable<INodesRanking> {
|
||||
return this.httpClient.get<INodesRanking>(this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
return this.httpClient.get<INodesRanking>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings');
|
||||
}
|
||||
|
||||
listChannelStats$(publicKey: string): Observable<any> {
|
||||
return this.httpClient.get<any>(this.apiBasePath + '/channels/' + publicKey + '/statistics');
|
||||
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/channels/' + publicKey + '/statistics');
|
||||
}
|
||||
|
||||
listStatistics$(interval: string | undefined): Observable<any> {
|
||||
return this.httpClient.get<any>(
|
||||
this.apiBasePath + '/api/v1/lightning/statistics' +
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/statistics' +
|
||||
(interval !== undefined ? `/${interval}` : ''), { observe: 'response' }
|
||||
);
|
||||
}
|
||||
|
||||
getTopNodesByCapacity$(): Observable<ITopNodesPerCapacity[]> {
|
||||
return this.httpClient.get<ITopNodesPerCapacity[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/liquidity'
|
||||
);
|
||||
}
|
||||
|
||||
getTopNodesByChannels$(): Observable<ITopNodesPerChannels[]> {
|
||||
return this.httpClient.get<ITopNodesPerChannels[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/connectivity'
|
||||
);
|
||||
}
|
||||
|
||||
getPenaltyClosedChannels$(): Observable<IChannel[]> {
|
||||
return this.httpClient.get<IChannel[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/penalties'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/penalties'
|
||||
);
|
||||
}
|
||||
|
||||
getOldestNodes$(): Observable<IOldestNodes[]> {
|
||||
return this.httpClient.get<IOldestNodes[]>(
|
||||
this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
|
||||
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/rankings/age'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, OnInit } from '@angular/core';
|
||||
import { Observable, merge } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface';
|
||||
import { SeoService } from '../../services/seo.service';
|
||||
|
@ -24,6 +24,7 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
|||
private seoService: SeoService,
|
||||
private ogService: OpenGraphService,
|
||||
private stateService: StateService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
|
@ -35,6 +36,12 @@ export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
|||
|
||||
this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
|
||||
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||
|
||||
if (!this.stateService.isBrowser) {
|
||||
merge(this.nodesRanking$, this.statistics$).subscribe(() => {
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="full-container">
|
||||
<h2 i18n="lightning.node-fee-distribution">Fee distribution</h2>
|
||||
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
.full-container {
|
||||
position: relative;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
min-height: 450px;
|
||||
}
|
|
@ -5,6 +5,7 @@ import { download } from '../../shared/graphs.utils';
|
|||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-fee-chart',
|
||||
|
@ -33,6 +34,7 @@ export class NodeFeeChartComponent implements OnInit {
|
|||
constructor(
|
||||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
public stateService: StateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="full-container">
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { StorageService } from '../../services/storage.service';
|
|||
import { download } from '../../shared/graphs.utils';
|
||||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-node-statistics-chart',
|
||||
|
@ -48,6 +49,7 @@ export class NodeStatisticsChartComponent implements OnInit {
|
|||
@Inject(LOCALE_ID) public locale: string,
|
||||
private lightningApiService: LightningApiService,
|
||||
private storageService: StorageService,
|
||||
public stateService: StateService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<div class="map-wrapper" [class]="style" *ngIf="style !== 'graph'">
|
||||
<ng-container *ngIf="channelsObservable | async">
|
||||
<div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
|
||||
<div class="chart" [class]="style" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart" [class]="style" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
|
||||
</div>
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="isLoading && !disableSpinner">
|
||||
|
||||
<div class="text-center loading-spinner" [class]="style" *ngIf="(!stateService.isBrowser || isLoading) && !disableSpinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
@ -21,8 +22,10 @@
|
|||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="channelsObservable | async" class="chart-graph" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
<ng-container *ngIf="channelsObservable | async">
|
||||
<div class="chart-graph" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
min-height: 400px;
|
||||
margin-top: 25px;
|
||||
margin-bottom: 25px;
|
||||
min-height: 100%;
|
||||
}
|
||||
.full-container.widget {
|
||||
height: 250px;
|
||||
|
|
|
@ -45,7 +45,7 @@ export class NodesChannelsMap implements OnInit {
|
|||
constructor(
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private assetsService: AssetsService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
|
||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||
<div echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
<div class="node-channels-container">
|
||||
<div *ngIf="channelsObservable$ | async" style="min-height: 455px">
|
||||
<h2 i18n="lightning.active-channels-map">Active channels map</h2>
|
||||
<div *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!stateService.isBrowser || isLoading" class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isLoading" class="text-center loading-spinner">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
.node-channels-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
min-height: 455px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export class NodeChannels implements OnChanges {
|
|||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private router: Router,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) {}
|
||||
|
||||
ngOnChanges(): void {
|
||||
|
|
|
@ -7,8 +7,13 @@
|
|||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
</div>
|
||||
|
||||
<div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
<ng-container *ngIf="observable$ | async">
|
||||
<div class="chart" [class]="widget ? 'widget' : ''" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="text-center loading-spinner" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -63,3 +63,13 @@
|
|||
.chart.widget {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: calc(50% - 15px);
|
||||
z-index: 100;
|
||||
@media (max-width: 767.98px) {
|
||||
top: 550px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ export class NodesMap implements OnInit, OnChanges {
|
|||
inputNodes$: BehaviorSubject<any>;
|
||||
nodes$: Observable<any>;
|
||||
observable$: Observable<any>;
|
||||
isLoading: boolean = true;
|
||||
|
||||
chartInstance = undefined;
|
||||
chartOptions: EChartsOption = {};
|
||||
|
@ -37,7 +38,7 @@ export class NodesMap implements OnInit, OnChanges {
|
|||
@Inject(LOCALE_ID) public locale: string,
|
||||
private seoService: SeoService,
|
||||
private apiService: ApiService,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private assetsService: AssetsService,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
|
@ -226,6 +227,7 @@ export class NodesMap implements OnInit, OnChanges {
|
|||
},
|
||||
]
|
||||
};
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
|
@ -235,6 +237,10 @@ export class NodesMap implements OnInit, OnChanges {
|
|||
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('finished', () => {
|
||||
this.isLoading = false;
|
||||
});
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
if (e.data) {
|
||||
this.zone.run(() => {
|
||||
|
|
|
@ -35,8 +35,8 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { SeoService } from '../../services/seo.service';
|
|||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-nodes-networks-chart',
|
||||
|
@ -58,6 +59,7 @@ export class NodesNetworksChartComponent implements OnInit, OnChanges {
|
|||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -12,12 +12,12 @@
|
|||
|
||||
<div class="container pb-lg-0">
|
||||
<div class="pb-lg-5">
|
||||
<div class="chart w-100" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div class="chart w-100" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export class NodesPerCountryChartComponent implements OnInit {
|
|||
private seoService: SeoService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private zone: NgZone,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
private router: Router,
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest, map, Observable, share, tap } from 'rxjs';
|
||||
import { ApiService } from '../../services/api.service';
|
||||
|
@ -27,6 +27,7 @@ export class NodesPerCountry implements OnInit {
|
|||
constructor(
|
||||
private apiService: ApiService,
|
||||
private seoService: SeoService,
|
||||
private cd: ChangeDetectorRef,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
for (let i = 0; i < this.pageSize; ++i) {
|
||||
|
@ -94,7 +95,10 @@ export class NodesPerCountry implements OnInit {
|
|||
ispCount: Object.keys(isps).length
|
||||
};
|
||||
}),
|
||||
tap(() => this.isLoading = false),
|
||||
tap(() => {
|
||||
this.isLoading = false
|
||||
this.cd.markForCheck();
|
||||
}),
|
||||
share()
|
||||
);
|
||||
|
||||
|
|
|
@ -39,14 +39,10 @@
|
|||
</div>
|
||||
|
||||
<div *ngIf="!indexingInProgress else indexing" [class]="!widget ? '' : 'pb-0'" class="container pb-lg-0">
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="widget ? 'chart-widget' : 'chart'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)">
|
||||
</div>
|
||||
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-md-end toggle" *ngIf="!widget">
|
||||
<app-toggle [textLeft]="'Sort by nodes'" [textRight]="'capacity'" [checked]="true" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
|
||||
</div>
|
||||
|
@ -74,6 +70,9 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #loadingReward>
|
||||
|
|
|
@ -44,7 +44,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
|||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
private stateService: StateService,
|
||||
public stateService: StateService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
|
@ -42,9 +42,9 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
<div [class]="!widget ? 'chart' : 'chart-widget'" *browserOnly [style]="{ height: widget ? (height + 'px') : null}" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
|
||||
(chartInit)="onChartInit($event)"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="isLoading">
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { download } from '../../shared/graphs.utils';
|
|||
import { LightningApiService } from '../lightning-api.service';
|
||||
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
|
||||
import { isMobile } from '../../shared/common.utils';
|
||||
import { StateService } from '../../services/state.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-lightning-statistics-chart',
|
||||
|
@ -55,6 +56,7 @@ export class LightningStatisticsChartComponent implements OnInit, OnChanges {
|
|||
private formBuilder: UntypedFormBuilder,
|
||||
private storageService: StorageService,
|
||||
private miningService: MiningService,
|
||||
public stateService: StateService,
|
||||
private amountShortenerPipe: AmountShortenerPipe,
|
||||
) {
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
|
||||
import { HttpInterceptor, HttpEvent, HttpRequest, HttpHandler, HttpResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
|
||||
|
@ -17,14 +17,18 @@ export class HttpCacheInterceptor implements HttpInterceptor {
|
|||
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
if (this.isBrowser && request.method === 'GET') {
|
||||
|
||||
const cachedResponse = this.transferState.get<any>(makeStateKey(request.url), null);
|
||||
if (cachedResponse) {
|
||||
const { response, headers } = this.transferState.get<any>(makeStateKey(request.url), null) || {};
|
||||
if (response) {
|
||||
const httpHeaders = new HttpHeaders();
|
||||
for (const [k,v] of Object.entries(headers)) {
|
||||
httpHeaders.set(k,v as string[]);
|
||||
}
|
||||
const modifiedResponse = new HttpResponse<any>({
|
||||
headers: cachedResponse.headers,
|
||||
body: cachedResponse.body,
|
||||
status: cachedResponse.status,
|
||||
statusText: cachedResponse.statusText,
|
||||
url: cachedResponse.url
|
||||
headers: httpHeaders,
|
||||
body: response.body,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: response.url
|
||||
});
|
||||
this.transferState.remove(makeStateKey(request.url));
|
||||
return of(modifiedResponse);
|
||||
|
@ -35,7 +39,11 @@ export class HttpCacheInterceptor implements HttpInterceptor {
|
|||
.pipe(tap((event: HttpEvent<any>) => {
|
||||
if (!this.isBrowser && event instanceof HttpResponse) {
|
||||
let keyId = request.url.split('/').slice(3).join('/');
|
||||
this.transferState.set<any>(makeStateKey('/' + keyId), event);
|
||||
const headers = {};
|
||||
for (const k of event.headers.keys()) {
|
||||
headers[k] = event.headers.getAll(k);
|
||||
}
|
||||
this.transferState.set<any>(makeStateKey('/' + keyId), { response: event, headers });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@ import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
|||
import { WebsocketResponse } from '../interfaces/websocket.interface';
|
||||
import { StateService } from './state.service';
|
||||
import { Transaction } from '../interfaces/electrs.interface';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { firstValueFrom, Subscription } from 'rxjs';
|
||||
import { ApiService } from './api.service';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TransferState, makeStateKey } from '@angular/platform-browser';
|
||||
import { TransferState, makeStateKey } from '@angular/core';
|
||||
import { CacheService } from './cache.service';
|
||||
import { uncompressDeltaChange, uncompressTx } from '../shared/common.utils';
|
||||
|
||||
|
@ -57,8 +57,12 @@ export class WebsocketService {
|
|||
this.network = this.stateService.network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND ? '' : this.stateService.network;
|
||||
this.websocketSubject = webSocket<WebsocketResponse>(this.webSocketUrl.replace('{network}', this.network ? '/' + this.network : ''));
|
||||
|
||||
const theInitData = this.transferState.get<any>(initData, null);
|
||||
const { response: theInitData } = this.transferState.get<any>(initData, null) || {};
|
||||
if (theInitData) {
|
||||
if (theInitData.body.blocks) {
|
||||
theInitData.body.blocks = theInitData.body.blocks.reverse();
|
||||
}
|
||||
this.stateService.isLoadingWebSocket$.next(false);
|
||||
this.handleResponse(theInitData.body);
|
||||
this.startSubscription(false, true);
|
||||
} else {
|
||||
|
@ -223,6 +227,7 @@ export class WebsocketService {
|
|||
}
|
||||
|
||||
startTrackRbfSummary() {
|
||||
this.initRbfSummary();
|
||||
this.websocketSubject.next({ 'track-rbf-summary': true });
|
||||
this.isTrackingRbfSummary = true;
|
||||
}
|
||||
|
@ -445,4 +450,30 @@ export class WebsocketService {
|
|||
this.websocketSubject.next({'refresh-blocks': true});
|
||||
}
|
||||
}
|
||||
|
||||
async initRbfSummary(): Promise<void> {
|
||||
if (!this.stateService.isBrowser) {
|
||||
const rbfList = await firstValueFrom(this.apiService.getRbfList$(false));
|
||||
if (rbfList) {
|
||||
const rbfSummary = rbfList.slice(0, 6).map(rbfTree => {
|
||||
let oldFee = 0;
|
||||
let oldVsize = 0;
|
||||
for (const replaced of rbfTree.replaces) {
|
||||
oldFee += replaced.tx.fee;
|
||||
oldVsize += replaced.tx.vsize;
|
||||
}
|
||||
return {
|
||||
txid: rbfTree.tx.txid,
|
||||
mined: !!rbfTree.tx.mined,
|
||||
fullRbf: !!rbfTree.tx.fullRbf,
|
||||
oldFee,
|
||||
oldVsize,
|
||||
newFee: rbfTree.tx.fee,
|
||||
newVsize: rbfTree.tx.vsize,
|
||||
};
|
||||
});
|
||||
this.stateService.rbfLatestSummary$.next(rbfSummary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
14
frontend/src/app/services/zone-shim.service.ts
Normal file
14
frontend/src/app/services/zone-shim.service.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ZoneService {
|
||||
|
||||
constructor() { }
|
||||
|
||||
wrapObservable<T>(obs: Observable<T>): Observable<T> {
|
||||
return obs;
|
||||
}
|
||||
}
|
60
frontend/src/app/services/zone.service.ts
Normal file
60
frontend/src/app/services/zone.service.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { ApplicationRef, Injectable, NgZone } from '@angular/core';
|
||||
import { Observable, Subscriber } from 'rxjs';
|
||||
|
||||
// global Zone object provided by zone.js
|
||||
declare const Zone: any;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ZoneService {
|
||||
|
||||
constructor(
|
||||
private ngZone: NgZone,
|
||||
private appRef: ApplicationRef,
|
||||
) { }
|
||||
|
||||
wrapObservable<T>(obs: Observable<T>): Observable<T> {
|
||||
return new Observable((subscriber: Subscriber<T>) => {
|
||||
let task: any;
|
||||
|
||||
this.ngZone.run(() => {
|
||||
task = Zone.current.scheduleMacroTask('wrapObservable', () => {}, {}, () => {}, () => {});
|
||||
});
|
||||
|
||||
const subscription = obs.subscribe(
|
||||
value => {
|
||||
subscriber.next(value);
|
||||
if (task) {
|
||||
this.ngZone.run(() => {
|
||||
this.appRef.tick();
|
||||
});
|
||||
task.invoke();
|
||||
}
|
||||
},
|
||||
err => {
|
||||
subscriber.error(err);
|
||||
if (task) {
|
||||
this.appRef.tick();
|
||||
task.invoke();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
subscriber.complete();
|
||||
if (task) {
|
||||
this.appRef.tick();
|
||||
task.invoke();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
if (task) {
|
||||
this.appRef.tick();
|
||||
task.invoke();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
19
frontend/src/app/shared/directives/browser-only.directive.ts
Normal file
19
frontend/src/app/shared/directives/browser-only.directive.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Directive, TemplateRef, ViewContainerRef, Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: '[browserOnly]'
|
||||
})
|
||||
export class BrowserOnlyDirective {
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object
|
||||
) {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
}
|
||||
}
|
||||
}
|
19
frontend/src/app/shared/directives/server-only.directive.ts
Normal file
19
frontend/src/app/shared/directives/server-only.directive.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Directive, TemplateRef, ViewContainerRef, Inject, PLATFORM_ID } from '@angular/core';
|
||||
import { isPlatformServer } from '@angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: '[serverOnly]'
|
||||
})
|
||||
export class ServerOnlyDirective {
|
||||
constructor(
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
@Inject(PLATFORM_ID) private platformId: Object
|
||||
) {
|
||||
if (isPlatformServer(this.platformId)) {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
} else {
|
||||
this.viewContainer.clear();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -323,7 +323,7 @@ export function hasTouchScreen(): boolean {
|
|||
// @ts-ignore
|
||||
hasTouchScreen = navigator.msMaxTouchPoints > 0;
|
||||
} else {
|
||||
const mQ = matchMedia?.('(pointer:coarse)');
|
||||
const mQ = window.matchMedia?.('(pointer:coarse)');
|
||||
if (mQ?.media === '(pointer:coarse)') {
|
||||
hasTouchScreen = !!mQ.matches;
|
||||
} else if ('orientation' in window) {
|
||||
|
|
|
@ -33,6 +33,8 @@ import { ReactiveFormsModule } from '@angular/forms';
|
|||
import { LanguageSelectorComponent } from '../components/language-selector/language-selector.component';
|
||||
import { FiatSelectorComponent } from '../components/fiat-selector/fiat-selector.component';
|
||||
import { RateUnitSelectorComponent } from '../components/rate-unit-selector/rate-unit-selector.component';
|
||||
import { BrowserOnlyDirective } from './directives/browser-only.directive';
|
||||
import { ServerOnlyDirective } from './directives/server-only.directive';
|
||||
import { ColoredPriceDirective } from './directives/colored-price.directive';
|
||||
import { NoSanitizePipe } from './pipes/no-sanitize.pipe';
|
||||
import { MempoolBlocksComponent } from '../components/mempool-blocks/mempool-blocks.component';
|
||||
|
@ -132,6 +134,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||
FeeRoundingPipe,
|
||||
FiatCurrencyPipe,
|
||||
ColoredPriceDirective,
|
||||
BrowserOnlyDirective,
|
||||
ServerOnlyDirective,
|
||||
BlockchainComponent,
|
||||
BlockViewComponent,
|
||||
EightBlocksComponent,
|
||||
|
@ -264,6 +268,8 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
|
|||
Decimal2HexPipe,
|
||||
FeeRoundingPipe,
|
||||
ColoredPriceDirective,
|
||||
BrowserOnlyDirective,
|
||||
ServerOnlyDirective,
|
||||
NoSanitizePipe,
|
||||
BlockchainComponent,
|
||||
MempoolBlocksComponent,
|
||||
|
|
11
frontend/src/main.server.ts
Normal file
11
frontend/src/main.server.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
import '@angular/localize/init';
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { environment } from './environments/environment';
|
||||
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
export { AppServerModule } from './app/app.server.module';
|
||||
export { renderModule } from '@angular/platform-server';
|
18
frontend/tsconfig.server.json
Normal file
18
frontend/tsconfig.server.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/server",
|
||||
"target": "ES2022",
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"files": [
|
||||
"src/main.server.ts",
|
||||
"server.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"entryModule": "./src/app/app.server.module#AppServerModule"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue