Merge branch 'master' into nymkappa/bugfix/channels-update-status
This commit is contained in:
commit
48496abbf4
@ -12,13 +12,13 @@ class NodesApi {
|
||||
WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count,
|
||||
(SELECT Count(*)
|
||||
FROM channels
|
||||
WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count,
|
||||
WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count,
|
||||
(SELECT Sum(capacity)
|
||||
FROM channels
|
||||
WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity,
|
||||
WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity,
|
||||
(SELECT Avg(capacity)
|
||||
FROM channels
|
||||
WHERE status < 2 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg
|
||||
WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg
|
||||
FROM nodes
|
||||
LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
|
||||
LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
|
||||
@ -98,29 +98,59 @@ class NodesApi {
|
||||
}
|
||||
}
|
||||
|
||||
public async $getNodesISP() {
|
||||
public async $getNodesISP(groupBy: string, showTor: boolean) {
|
||||
try {
|
||||
let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
|
||||
const orderBy = groupBy === 'capacity' ? `CAST(SUM(capacity) as INT)` : `COUNT(DISTINCT nodes.public_key)`;
|
||||
|
||||
// Clearnet
|
||||
let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names,
|
||||
COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
|
||||
FROM nodes
|
||||
JOIN geo_names ON geo_names.id = nodes.as_number
|
||||
JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
|
||||
GROUP BY geo_names.names
|
||||
ORDER BY COUNT(DISTINCT nodes.public_key) DESC
|
||||
`;
|
||||
ORDER BY ${orderBy} DESC
|
||||
`;
|
||||
const [nodesCountPerAS]: any = await DB.query(query);
|
||||
|
||||
query = `SELECT COUNT(*) as total FROM nodes WHERE as_number IS NOT NULL`;
|
||||
const [nodesWithAS]: any = await DB.query(query);
|
||||
|
||||
let total = 0;
|
||||
const nodesPerAs: any[] = [];
|
||||
|
||||
for (const asGroup of nodesCountPerAS) {
|
||||
if (groupBy === 'capacity') {
|
||||
total += asGroup.capacity;
|
||||
} else {
|
||||
total += asGroup.nodesCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Tor
|
||||
if (showTor) {
|
||||
query = `SELECT COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
|
||||
FROM nodes
|
||||
JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
|
||||
ORDER BY ${orderBy} DESC
|
||||
`;
|
||||
const [nodesCountTor]: any = await DB.query(query);
|
||||
|
||||
total += groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount;
|
||||
nodesPerAs.push({
|
||||
ispId: null,
|
||||
name: 'Tor',
|
||||
count: nodesCountTor[0].nodesCount,
|
||||
share: Math.floor((groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount) / total * 10000) / 100,
|
||||
capacity: nodesCountTor[0].capacity,
|
||||
});
|
||||
}
|
||||
|
||||
for (const as of nodesCountPerAS) {
|
||||
nodesPerAs.push({
|
||||
ispId: as.ispId,
|
||||
name: JSON.parse(as.names),
|
||||
count: as.nodesCount,
|
||||
share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100,
|
||||
share: Math.floor((groupBy === 'capacity' ? as.capacity : as.nodesCount) / total * 10000) / 100,
|
||||
capacity: as.capacity,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return nodesPerAs;
|
||||
|
@ -9,10 +9,10 @@ class NodesRoutes {
|
||||
public initRoutes(app: Application) {
|
||||
app
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp', this.$getNodesISP)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
|
||||
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
|
||||
@ -63,9 +63,18 @@ class NodesRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
private async $getNodesISP(req: Request, res: Response) {
|
||||
private async $getISPRanking(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const nodesPerAs = await nodesApi.$getNodesISP();
|
||||
const groupBy = req.query.groupBy as string;
|
||||
const showTor = req.query.showTor as string === 'true' ? true : false;
|
||||
|
||||
if (!['capacity', 'node-count'].includes(groupBy)) {
|
||||
res.status(400).send(`groupBy must be one of 'capacity' or 'node-count'`);
|
||||
return;
|
||||
}
|
||||
|
||||
const nodesPerAs = await nodesApi.$getNodesISP(groupBy, showTor);
|
||||
|
||||
res.header('Pragma', 'public');
|
||||
res.header('Cache-control', 'public');
|
||||
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
|
||||
|
@ -141,7 +141,22 @@ class LightningStatsUpdater {
|
||||
try {
|
||||
logger.info(`Running daily node stats update...`);
|
||||
|
||||
const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 2 GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels WHERE channels.status < 2 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`;
|
||||
const query = `
|
||||
SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left,
|
||||
c2.channels_capacity_right
|
||||
FROM nodes
|
||||
LEFT JOIN (
|
||||
SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left
|
||||
FROM channels
|
||||
WHERE channels.status = 1
|
||||
GROUP BY node1_public_key
|
||||
) c1 ON c1.node1_public_key = nodes.public_key
|
||||
LEFT JOIN (
|
||||
SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right
|
||||
FROM channels WHERE channels.status = 1 GROUP BY node2_public_key
|
||||
) c2 ON c2.node2_public_key = nodes.public_key
|
||||
`;
|
||||
|
||||
const [nodes]: any = await DB.query(query);
|
||||
|
||||
for (const node of nodes) {
|
||||
|
@ -140,13 +140,8 @@
|
||||
|
||||
<div class="d-flex justify-content-between" *ngIf="!error">
|
||||
<h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
|
||||
<div class="d-flex align-items-center justify-content-end">
|
||||
<span style="margin-bottom: 0.5rem">List</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" (change)="channelsListModeChange($event)">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<span style="margin-bottom: 0.5rem">Map</span>
|
||||
<div class="d-flex justify-content-end">
|
||||
<app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -56,67 +56,4 @@ app-fiat {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(13px);
|
||||
-ms-transform: translateX(13px);
|
||||
transform: translateX(13px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
@ -75,8 +75,8 @@ export class NodeComponent implements OnInit {
|
||||
this.selectedSocketIndex = index;
|
||||
}
|
||||
|
||||
channelsListModeChange(e) {
|
||||
if (e.target.checked === true) {
|
||||
channelsListModeChange(toggle) {
|
||||
if (toggle === true) {
|
||||
this.channelsListMode = 'map';
|
||||
} else {
|
||||
this.channelsListMode = 'list';
|
||||
|
@ -7,7 +7,9 @@
|
||||
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
|
||||
<small class="d-block" style="color: #ffffff66; min-height: 25px" i18n="lightning.tor-nodes-excluded">
|
||||
<span *ngIf="!(showTorObservable$ | async)">(Tor nodes excluded)</span>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="container pb-lg-0 bottom-padding">
|
||||
@ -21,6 +23,11 @@
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex toggle">
|
||||
<app-toggle [textLeft]="'Show Tor'" [textRight]="" (toggleStatusChanged)="onTorToggleStatusChanged($event)"></app-toggle>
|
||||
<app-toggle [textLeft]="'Nodes'" [textRight]="'Capacity'" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
|
||||
</div>
|
||||
|
||||
<table class="table table-borderless text-center m-auto" style="max-width: 900px">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -34,8 +41,9 @@
|
||||
<tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
|
||||
<tr *ngFor="let asEntry of asList">
|
||||
<td class="rank text-left pl-0">{{ asEntry.rank }}</td>
|
||||
<td class="name text-left text-truncate" style="max-width: 250px">
|
||||
<a [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
|
||||
<td class="name text-left text-truncate">
|
||||
<a *ngIf="asEntry.ispId" [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
|
||||
<span *ngIf="!asEntry.ispId">{{ asEntry.name }}</span>
|
||||
</td>
|
||||
<td class="share text-right">{{ asEntry.share }}%</td>
|
||||
<td class="nodes text-right">{{ asEntry.count }}</td>
|
||||
|
@ -45,7 +45,7 @@
|
||||
.name {
|
||||
width: 25%;
|
||||
@media (max-width: 576px) {
|
||||
width: 80%;
|
||||
width: 70%;
|
||||
max-width: 150px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
@ -69,7 +69,17 @@
|
||||
.capacity {
|
||||
width: 20%;
|
||||
@media (max-width: 576px) {
|
||||
width: 10%;
|
||||
width: 20%;
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
justify-content: space-between;
|
||||
padding-top: 15px;
|
||||
@media (min-width: 576px) {
|
||||
padding-bottom: 15px;
|
||||
padding-left: 105px;
|
||||
padding-right: 105px;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { EChartsOption, PieSeriesOption } from 'echarts';
|
||||
import { map, Observable, share, tap } from 'rxjs';
|
||||
import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs';
|
||||
import { chartColors } from 'src/app/app.constants';
|
||||
import { ApiService } from 'src/app/services/api.service';
|
||||
import { SeoService } from 'src/app/services/seo.service';
|
||||
@ -17,19 +17,20 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class NodesPerISPChartComponent implements OnInit {
|
||||
miningWindowPreference: string;
|
||||
|
||||
isLoading = true;
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
timespan = '';
|
||||
chartInstance: any = undefined;
|
||||
chartInstance = undefined;
|
||||
|
||||
@HostBinding('attr.dir') dir = 'ltr';
|
||||
|
||||
nodesPerAsObservable$: Observable<any>;
|
||||
showTorObservable$: Observable<boolean>;
|
||||
groupBySubject = new Subject<boolean>();
|
||||
showTorSubject = new Subject<boolean>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@ -44,23 +45,32 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
ngOnInit(): void {
|
||||
this.seoService.setTitle($localize`Lightning nodes per ISP`);
|
||||
|
||||
this.nodesPerAsObservable$ = this.apiService.getNodesPerAs()
|
||||
this.showTorObservable$ = this.showTorSubject.asObservable();
|
||||
this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
|
||||
.pipe(
|
||||
tap(data => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data);
|
||||
}),
|
||||
map(data => {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
data[i].rank = i + 1;
|
||||
}
|
||||
return data.slice(0, 100);
|
||||
switchMap((selectedFilters) => {
|
||||
return this.apiService.getNodesPerAs(
|
||||
selectedFilters[0] ? 'capacity' : 'node-count',
|
||||
selectedFilters[1] // Show Tor nodes
|
||||
)
|
||||
.pipe(
|
||||
tap(data => {
|
||||
this.isLoading = false;
|
||||
this.prepareChartOptions(data);
|
||||
}),
|
||||
map(data => {
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
data[i].rank = i + 1;
|
||||
}
|
||||
return data.slice(0, 100);
|
||||
})
|
||||
);
|
||||
}),
|
||||
share()
|
||||
);
|
||||
}
|
||||
|
||||
generateChartSerieData(as) {
|
||||
generateChartSerieData(as): PieSeriesOption[] {
|
||||
const shareThreshold = this.isMobile() ? 2 : 0.5;
|
||||
const data: object[] = [];
|
||||
let totalShareOther = 0;
|
||||
@ -78,6 +88,9 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
data.push({
|
||||
itemStyle: {
|
||||
color: as.ispId === null ? '#7D4698' : undefined,
|
||||
},
|
||||
value: as.share,
|
||||
name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
|
||||
label: {
|
||||
@ -138,14 +151,14 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
return data;
|
||||
}
|
||||
|
||||
prepareChartOptions(as) {
|
||||
prepareChartOptions(as): void {
|
||||
let pieSize = ['20%', '80%']; // Desktop
|
||||
if (this.isMobile()) {
|
||||
pieSize = ['15%', '60%'];
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
color: chartColors,
|
||||
color: chartColors.slice(3),
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
textStyle: {
|
||||
@ -191,18 +204,18 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
};
|
||||
}
|
||||
|
||||
isMobile() {
|
||||
isMobile(): boolean {
|
||||
return (window.innerWidth <= 767.98);
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
onChartInit(ec): void {
|
||||
if (this.chartInstance !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.chartInstance = ec;
|
||||
|
||||
this.chartInstance.on('click', (e) => {
|
||||
if (e.data.data === 9999) { // "Other"
|
||||
if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor
|
||||
return;
|
||||
}
|
||||
this.zone.run(() => {
|
||||
@ -212,7 +225,7 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
onSaveChart() {
|
||||
onSaveChart(): void {
|
||||
const now = new Date();
|
||||
this.chartOptions.backgroundColor = '#11131f';
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
@ -224,8 +237,12 @@ export class NodesPerISPChartComponent implements OnInit {
|
||||
this.chartInstance.setOption(this.chartOptions);
|
||||
}
|
||||
|
||||
isEllipsisActive(e) {
|
||||
return (e.offsetWidth < e.scrollWidth);
|
||||
onTorToggleStatusChanged(e): void {
|
||||
this.showTorSubject.next(e);
|
||||
}
|
||||
|
||||
onGroupToggleStatusChanged(e): void {
|
||||
this.groupBySubject.next(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,8 +255,9 @@ export class ApiService {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
|
||||
}
|
||||
|
||||
getNodesPerAs(): Observable<any> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp');
|
||||
getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable<any> {
|
||||
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking'
|
||||
+ `?groupBy=${groupBy}&showTor=${showTorNodes}`);
|
||||
}
|
||||
|
||||
getNodeForCountry$(country: string): Observable<any> {
|
||||
|
@ -0,0 +1,8 @@
|
||||
<div class="d-flex align-items-center">
|
||||
<span style="margin-bottom: 0.5rem">{{ textLeft }}</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox" (change)="onToggleStatusChanged($event)">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<span style="margin-bottom: 0.5rem">{{ textRight }}</span>
|
||||
</div>
|
@ -0,0 +1,62 @@
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
left: 2px;
|
||||
bottom: 2px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked+.slider {
|
||||
background-color: #2196F3;
|
||||
}
|
||||
|
||||
input:focus+.slider {
|
||||
box-shadow: 0 0 1px #2196F3;
|
||||
}
|
||||
|
||||
input:checked+.slider:before {
|
||||
-webkit-transform: translateX(13px);
|
||||
-ms-transform: translateX(13px);
|
||||
transform: translateX(13px);
|
||||
}
|
||||
|
||||
/* Rounded sliders */
|
||||
.slider.round {
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, AfterViewInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle',
|
||||
templateUrl: './toggle.component.html',
|
||||
styleUrls: ['./toggle.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ToggleComponent implements AfterViewInit {
|
||||
@Output() toggleStatusChanged = new EventEmitter<boolean>();
|
||||
@Input() textLeft: string;
|
||||
@Input() textRight: string;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.toggleStatusChanged.emit(false);
|
||||
}
|
||||
|
||||
onToggleStatusChanged(e): void {
|
||||
this.toggleStatusChanged.emit(e.target.checked);
|
||||
}
|
||||
}
|
@ -80,6 +80,7 @@ import { ChangeComponent } from '../components/change/change.component';
|
||||
import { SatsComponent } from './components/sats/sats.component';
|
||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
|
||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
import { ToggleComponent } from './components/toggle/toggle.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -154,6 +155,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
SatsComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -255,6 +257,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
|
||||
SatsComponent,
|
||||
SearchResultsComponent,
|
||||
TimestampComponent,
|
||||
ToggleComponent,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
Loading…
x
Reference in New Issue
Block a user