dashboard incomplete

dashboard incomplete
pull/260/head
Shahana Farooqui 5 years ago
parent b7bbf94abf
commit 835dca0557

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -128,6 +128,8 @@ THE SOFTWARE.
@angular/material/form-field @angular/material/form-field
@angular/material/grid-list
@angular/material/icon @angular/material/icon
@angular/material/list @angular/material/list
@ -211,6 +213,44 @@ Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.** to represent the company, product, or service to which they refer.**
@fortawesome/free-regular-svg-icons
(CC-BY-4.0 AND MIT)
Font Awesome Free License
-------------------------
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
In the Font Awesome Free download, the CC BY 4.0 license applies to all icons
packaged as SVG and JS file types.
# Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL)
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**
@fortawesome/free-solid-svg-icons @fortawesome/free-solid-svg-icons
(CC-BY-4.0 AND MIT) (CC-BY-4.0 AND MIT)
Font Awesome Free License Font Awesome Free License

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -9,8 +9,8 @@
<link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="assets/images/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="assets/images/favicon/favicon-16x16.png">
<link rel="manifest" href="assets/images/favicon/site.webmanifest"> <link rel="manifest" href="assets/images/favicon/site.webmanifest">
<link rel="stylesheet" href="styles.c08e367434232493455f.css"></head> <link rel="stylesheet" href="styles.905fb133af8ca5e83162.css"></head>
<body> <body>
<rtl-app></rtl-app> <rtl-app></rtl-app>
<script src="runtime.d75168f7793ce675c2a6.js"></script><script src="polyfills-es5.92f4069201c83f4833ef.js" nomodule></script><script src="polyfills.5ddcccdb990eb395f306.js"></script><script src="main.a5b80ff2c34bfc900dc4.js"></script></body> <script src="runtime.f4f82038f2bdab157558.js"></script><script src="polyfills-es5.92f4069201c83f4833ef.js" nomodule></script><script src="polyfills.5ddcccdb990eb395f306.js"></script><script src="main.d9079634ba953d139364.js"></script></body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(e){function r(r){for(var n,a,i=r[0],c=r[1],f=r[2],p=0,s=[];p<i.length;p++)o[a=i[p]]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(l&&l(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"50859d501eab13ab41b8",6:"ad453b6e8bc53c913101",7:"fdc70b63fa397b86320a"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(f);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var f=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var f=0;f<i.length;f++)r(i[f]);var l=c;t()}([]); !function(e){function r(r){for(var n,a,i=r[0],c=r[1],f=r[2],p=0,s=[];p<i.length;p++)o[a=i[p]]&&s.push(o[a][0]),o[a]=0;for(n in c)Object.prototype.hasOwnProperty.call(c,n)&&(e[n]=c[n]);for(l&&l(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++)0!==o[t[i]]&&(n=!1);n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={0:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.e=function(e){var r=[],t=o[e];if(0!==t)if(t)r.push(t[2]);else{var n=new Promise((function(r,n){t=o[e]=[r,n]}));r.push(t[2]=n);var u,i=document.createElement("script");i.charset="utf-8",i.timeout=120,a.nc&&i.setAttribute("nonce",a.nc),i.src=function(e){return a.p+""+({}[e]||e)+"."+{1:"69ceab2c54f4d2bf4227",6:"c9f217da6011a463bec0",7:"55a06eb6028cdaf8fef4"}[e]+".js"}(e);var c=new Error;u=function(r){i.onerror=i.onload=null,clearTimeout(f);var t=o[e];if(0!==t){if(t){var n=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+e+" failed.\n("+n+": "+u+")",c.name="ChunkLoadError",c.type=n,c.request=u,t[1](c)}o[e]=void 0}};var f=setTimeout((function(){u({type:"timeout",target:i})}),12e4);i.onerror=i.onload=u,document.head.appendChild(i)}return Promise.all(r)},a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,(function(r){return e[r]}).bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="",a.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],c=i.push.bind(i);i.push=r,i=i.slice();for(var f=0;f<i.length;f++)r(i[f]);var l=c;t()}([]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

8
package-lock.json generated

@ -1466,6 +1466,14 @@
"@fortawesome/fontawesome-common-types": "^0.2.25" "@fortawesome/fontawesome-common-types": "^0.2.25"
} }
}, },
"@fortawesome/free-regular-svg-icons": {
"version": "5.11.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.11.2.tgz",
"integrity": "sha512-k0vbThRv9AvnXYBWi1gn1rFW4X7co/aFkbm0ZNmAR5PoWb9vY9EDDDobg8Ay4ISaXtCPypvJ0W1FWkSpLQwZ6w==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.25"
}
},
"@fortawesome/free-solid-svg-icons": { "@fortawesome/free-solid-svg-icons": {
"version": "5.11.2", "version": "5.11.2",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.11.2.tgz",

@ -29,6 +29,7 @@
"@angular/router": "~8.1.2", "@angular/router": "~8.1.2",
"@fortawesome/angular-fontawesome": "^0.5.0", "@fortawesome/angular-fontawesome": "^0.5.0",
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-regular-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.11.2",
"@ngrx/effects": "^8.4.0", "@ngrx/effects": "^8.4.0",
"@ngrx/router-store": "^8.4.0", "@ngrx/router-store": "^8.4.0",

@ -41,6 +41,7 @@ export class CLEffects implements OnDestroy {
.pipe( .pipe(
map((info) => { map((info) => {
this.logger.info(info); this.logger.info(info);
info.lnImplementation = 'C-Lightning';
this.initializeRemainingData(info, action.payload.loadPage); this.initializeRemainingData(info, action.payload.loadPage);
return { return {
type: RTLActions.SET_INFO_CL, type: RTLActions.SET_INFO_CL,

@ -0,0 +1,13 @@
<div fxLayout="column" fxFlex="50" fxLayoutAlign="center start">
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">On-chain</h4>
<div class="foreground-secondary-text">{{balances.onchain | number}} (Sats)</div>
</div>
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Lightning</h4>
<div class="foreground-secondary-text">{{balances.lightning | number}} (Sats)</div>
</div>
<div fxFlex="34">
</div>
</div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="center start" style="background-color: bisque;height:67%;border-radius: 500px;"></div>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BalancesInfoComponent } from './balances-info.component';
describe('BalancesInfoComponent', () => {
let component: BalancesInfoComponent;
let fixture: ComponentFixture<BalancesInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BalancesInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BalancesInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,15 @@
import { Component, OnChanges, Input } from '@angular/core';
@Component({
selector: 'rtl-balances-info',
templateUrl: './balances-info.component.html',
styleUrls: ['./balances-info.component.scss']
})
export class BalancesInfoComponent implements OnChanges {
@Input() balances = { onchain: 0, lightning: 0 };
constructor() {}
ngOnChanges() {}
}

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChannelCapacityInfoComponent } from './channel-capacity-info.component';
describe('ChannelCapacityInfoComponent', () => {
let component: ChannelCapacityInfoComponent;
let fixture: ComponentFixture<ChannelCapacityInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelCapacityInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelCapacityInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'rtl-channel-capacity-info',
templateUrl: './channel-capacity-info.component.html',
styleUrls: ['./channel-capacity-info.component.scss']
})
export class ChannelCapacityInfoComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

@ -0,0 +1,36 @@
<div fxLayout="column" fxFlex="30" fxLayoutAlign="center start">
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Active</h4>
<div class="foreground-secondary-text"><span class="dot tiny-dot green"></span>{{(channelsStatus.active.channels || 0) | number}}</div>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Pending</h4>
<div class="foreground-secondary-text"><span class="dot tiny-dot yellow"></span>{{(channelsStatus.pending.channels || 0) | number}}</div>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Inactive</h4>
<div class="foreground-secondary-text"><span class="dot tiny-dot red"></span>{{(channelsStatus.inactive.channels || 0) | number}}</div>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Closed</h4>
<div class="foreground-secondary-text"><span class="dot tiny-dot grey"></span>{{(channelsStatus.closed.channels || 0) | number}}</div>
</div>
</div>
<div fxLayout="column" fxFlex="70" fxLayoutAlign="center start">
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Capacity</h4>
<div class="foreground-secondary-text">{{(channelsStatus.active.capacity || 0) | number}} (Sats)</div>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Capacity</h4>
<div class="foreground-secondary-text">{{(channelsStatus.pending.capacity || 0) | number}} (Sats)</div>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Capacity</h4>
<div class="foreground-secondary-text">{{(channelsStatus.inactive.capacity || 0) | number}} (Sats)</div>
</div>
<div fxFlex="25">
<h4 fxLayoutAlign="start" class="font-bold-500">Capacity</h4>
<div class="foreground-secondary-text">{{(channelsStatus.closed.capacity || 0) | number}} (Sats)</div>
</div>
</div>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChannelStatusInfoComponent } from './channel-status-info.component';
describe('ChannelStatusInfoComponent', () => {
let component: ChannelStatusInfoComponent;
let fixture: ComponentFixture<ChannelStatusInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ChannelStatusInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ChannelStatusInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,16 @@
import { Component, OnChanges, Input } from '@angular/core';
import { ChannelsStatus } from '../../../shared/models/lndModels';
@Component({
selector: 'rtl-channel-status-info',
templateUrl: './channel-status-info.component.html',
styleUrls: ['./channel-status-info.component.scss']
})
export class ChannelStatusInfoComponent implements OnChanges {
@Input() channelsStatus: ChannelsStatus = {};
constructor() {}
ngOnChanges() {}
}

@ -0,0 +1,15 @@
<div fxLayout="column" fxFlex="50" fxLayoutAlign="center start">
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Daily</h4>
<div class="foreground-secondary-text">{{fees?.day_fee_sum | number}} (Sats)</div>
</div>
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Weekly</h4>
<div class="foreground-secondary-text">{{fees?.week_fee_sum | number}} (Sats)</div>
</div>
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Monthly</h4>
<div class="foreground-secondary-text">{{fees?.month_fee_sum | number}} (Sats)</div>
</div>
</div>
<div fxLayout="column" fxFlex="50" fxLayoutAlign="center start" style="background-color:cadetblue;height:67%;border-radius: 500px;"></div>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FeeInfoComponent } from './fee-info.component';
describe('FeeInfoComponent', () => {
let component: FeeInfoComponent;
let fixture: ComponentFixture<FeeInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FeeInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FeeInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,16 @@
import { Component, OnChanges, Input } from '@angular/core';
import { Fees } from '../../../shared/models/lndModels';
@Component({
selector: 'rtl-fee-info',
templateUrl: './fee-info.component.html',
styleUrls: ['./fee-info.component.scss']
})
export class FeeInfoComponent implements OnChanges {
@Input() fees: Fees;
constructor() {}
ngOnChanges() {}
}

@ -1,4 +1,39 @@
<div fxLayout="column" fxLayout.gt-sm="row wrap"> <div fxLayout="column">
<div fxLayout="row" fxLayoutAlign="start end" class="padding-gap-x page-title-container mb-0">
<fa-icon [icon]="faSmile" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Welcome! Your node is up and running.</span>
</div>
<mat-grid-list cols="10" rowHeight="330px">
<mat-grid-tile *ngFor="let card of cards | async" [colspan]="card.cols" [rowspan]="card.rows">
<mat-card fxLayout="column" fxLayoutAlign="start start" class="dashboard-card p-16">
<mat-card-header>
<mat-card-title>
{{card.title}}
<button mat-icon-button class="more-button" [matMenuTriggerFor]="menu" aria-label="Toggle menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu" xPosition="before">
<button mat-menu-item>Expand</button>
<button mat-menu-item>Remove</button>
</mat-menu>
</mat-card-title>
</mat-card-header>
<mat-card-content class="dashboard-card-content w-100" fxFlex="100">
<div [ngSwitch]="card.id" fxLayout="column" fxFlex="100">
<rtl-node-info fxFlex="100" *ngSwitchCase="'node'" [information]="information"></rtl-node-info>
<rtl-balances-info fxFlex="100" *ngSwitchCase="'balance'" [balances]="balances"></rtl-balances-info>
<rtl-channel-capacity-info fxFlex="100" *ngSwitchCase="'capacity'"></rtl-channel-capacity-info>
<rtl-fee-info fxFlex="100" *ngSwitchCase="'fee'" [fees]="fees"></rtl-fee-info>
<rtl-channel-status-info fxFlex="100" *ngSwitchCase="'status'" [channelsStatus]="channelsStatus"></rtl-channel-status-info>
<h3 *ngSwitchDefault>Error! Unable to find information!</h3>
</div>
</mat-card-content>
</mat-card>
</mat-grid-tile>
</mat-grid-list>
</div>
<!-- <div fxLayout="column" fxLayout.gt-sm="row wrap">
<div fxFlex="25" class="padding-gap"> <div fxFlex="25" class="padding-gap">
<mat-card [ngClass]="{'custom-card error-border': flgLoading[2]==='error','custom-card': true}"> <mat-card [ngClass]="{'custom-card error-border': flgLoading[2]==='error','custom-card': true}">
<mat-card-header class="bg-primary" fxLayoutAlign="center end"> <mat-card-header class="bg-primary" fxLayoutAlign="center end">
@ -234,4 +269,4 @@
</mat-card> </mat-card>
</div> </div>
</div> </div>
<ng-template #withoutData><h3>Sats</h3></ng-template> <ng-template #withoutData><h3>Sats</h3></ng-template> -->

@ -1,12 +1,30 @@
.network-info-list .mat-list-item { .dashboard-card {
height: 44px; position: absolute;
top: 1rem;
left: 1rem;
right: 1rem;
bottom: 1rem;
} }
.mat-column-bytes_sent, .mat-column-bytes_recv, .mat-column-sat_sent, .mat-column-sat_recv, .mat-column-inbound, .mat-column-ping_time { .more-button {
flex: 0 0 8%; position: absolute;
min-width: 80px; top: 7px;
right: 7px;
} }
.card-chnl-balances { .dashboard-card-content {
min-height: 354px; text-align: left;
} }
// .network-info-list .mat-list-item {
// height: 44px;
// }
// .mat-column-bytes_sent, .mat-column-bytes_recv, .mat-column-sat_sent, .mat-column-sat_recv, .mat-column-inbound, .mat-column-ping_time {
// flex: 0 0 8%;
// min-width: 80px;
// }
// .card-chnl-balances {
// min-height: 354px;
// }

@ -1,11 +1,14 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs'; import { map } from 'rxjs/operators';
import { Breakpoints, BreakpointObserver } from '@angular/cdk/layout';
import { Subject, of } from 'rxjs';
import { takeUntil, filter } from 'rxjs/operators'; import { takeUntil, filter } from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects'; import { Actions } from '@ngrx/effects';
import { faSmile } from '@fortawesome/free-regular-svg-icons';
import { LoggerService } from '../../shared/services/logger.service'; import { LoggerService } from '../../shared/services/logger.service';
import { GetInfo, NetworkInfo, Fees, Peer } from '../../shared/models/lndModels'; import { ChannelsStatus, GetInfo, NetworkInfo, Fees, Peer } from '../../shared/models/lndModels';
import { SelNodeChild } from '../../shared/models/RTLconfig'; import { SelNodeChild } from '../../shared/models/RTLconfig';
import * as RTLActions from '../../store/rtl.actions'; import * as RTLActions from '../../store/rtl.actions';
@ -17,9 +20,11 @@ import * as fromRTLReducer from '../../store/rtl.reducers';
styleUrls: ['./home.component.scss'] styleUrls: ['./home.component.scss']
}) })
export class HomeComponent implements OnInit, OnDestroy { export class HomeComponent implements OnInit, OnDestroy {
public faSmile = faSmile;
public selNode: SelNodeChild = {}; public selNode: SelNodeChild = {};
public fees: Fees; public fees: Fees;
public information: GetInfo = {}; public information: GetInfo = {};
public balances = { onchain: -1, lightning: -1 };
public remainder = 0; public remainder = 0;
public totalPeers = -1; public totalPeers = -1;
public totalBalance = 0; public totalBalance = 0;
@ -34,6 +39,7 @@ export class HomeComponent implements OnInit, OnDestroy {
public activeChannels = 0; public activeChannels = 0;
public inactiveChannels = 0; public inactiveChannels = 0;
public pendingChannels = 0; public pendingChannels = 0;
public channelsStatus: ChannelsStatus = {};
public peers: Peer[] = []; public peers: Peer[] = [];
barPadding = 0; barPadding = 0;
maxBalanceValue = 0; maxBalanceValue = 0;
@ -42,8 +48,30 @@ export class HomeComponent implements OnInit, OnDestroy {
view = []; view = [];
yAxisLabel = 'Balance'; yAxisLabel = 'Balance';
colorScheme = {domain: ['#FFFFFF']}; colorScheme = {domain: ['#FFFFFF']};
cards = this.breakpointObserver.observe(Breakpoints.Handset).pipe(
map(({ matches }) => {
if (matches) {
return [
{ id: 'node', title: 'Node Details', cols: 10, rows: 1 },
{ id: 'balance', title: 'Balances', cols: 10, rows: 1 },
{ id: 'fee', title: 'Routing Fee Earned', cols: 10, rows: 1 },
{ id: 'status', title: 'Channel Status', cols: 10, rows: 1 },
{ id: 'capacity', title: 'Channel Capacity', cols: 10, rows: 1 }
];
}
return [
{ id: 'node', title: 'Node Details', cols: 3, rows: 1 },
{ id: 'balance', title: 'Balances', cols: 3, rows: 1 },
{ id: 'capacity', title: 'Channel Capacity', cols: 4, rows: 2 },
{ id: 'fee', title: 'Routing Fee Earned', cols: 3, rows: 1 },
{ id: 'status', title: 'Channel Status', cols: 3, rows: 1 }
];
})
);
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions) {
constructor(private logger: LoggerService, private store: Store<fromRTLReducer.RTLState>, private actions$: Actions, private breakpointObserver: BreakpointObserver) {
switch (true) { switch (true) {
case (window.innerWidth <= 730): case (window.innerWidth <= 730):
this.view = [250, 352]; this.view = [250, 352];
@ -104,7 +132,7 @@ export class HomeComponent implements OnInit, OnDestroy {
if (this.flgLoading[1] !== 'error') { if (this.flgLoading[1] !== 'error') {
this.flgLoading[1] = (undefined !== this.fees.day_fee_sum) ? false : true; this.flgLoading[1] = (undefined !== this.fees.day_fee_sum) ? false : true;
} }
this.balances.onchain = rtlStore.blockchainBalance.total_balance;
this.totalBalance = rtlStore.blockchainBalance.total_balance; this.totalBalance = rtlStore.blockchainBalance.total_balance;
this.BTCtotalBalance = rtlStore.blockchainBalance.btc_total_balance; this.BTCtotalBalance = rtlStore.blockchainBalance.btc_total_balance;
if (this.flgLoading[2] !== 'error') { if (this.flgLoading[2] !== 'error') {
@ -123,6 +151,7 @@ export class HomeComponent implements OnInit, OnDestroy {
} }
if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0) { if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0) {
this.balances.lightning = rtlStore.totalLocalBalance;
this.totalBalances = [{'name': 'Local Balance', 'value': rtlStore.totalLocalBalance}, {'name': 'Remote Balance', 'value': rtlStore.totalRemoteBalance}]; this.totalBalances = [{'name': 'Local Balance', 'value': rtlStore.totalLocalBalance}, {'name': 'Remote Balance', 'value': rtlStore.totalRemoteBalance}];
this.maxBalanceValue = (rtlStore.totalLocalBalance > rtlStore.totalRemoteBalance) ? rtlStore.totalLocalBalance : rtlStore.totalRemoteBalance; this.maxBalanceValue = (rtlStore.totalLocalBalance > rtlStore.totalRemoteBalance) ? rtlStore.totalLocalBalance : rtlStore.totalRemoteBalance;
this.flgTotalCalculated = true; this.flgTotalCalculated = true;
@ -130,10 +159,19 @@ export class HomeComponent implements OnInit, OnDestroy {
this.flgLoading[5] = false; this.flgLoading[5] = false;
} }
} }
this.activeChannels = rtlStore.numberOfActiveChannels; this.activeChannels = rtlStore.numberOfActiveChannels;
this.inactiveChannels = rtlStore.numberOfInactiveChannels; this.inactiveChannels = rtlStore.numberOfInactiveChannels;
this.pendingChannels = (undefined !== rtlStore.pendingChannels.pending_open_channels) ? rtlStore.pendingChannels.pending_open_channels.length : 0; this.pendingChannels = (undefined !== rtlStore.pendingChannels.pending_open_channels) ? rtlStore.pendingChannels.pending_open_channels.length : 0;
this.pendingChannels = this.pendingChannels + ((undefined !== rtlStore.pendingChannels.waiting_close_channels) ? rtlStore.pendingChannels.waiting_close_channels.length : 0);
this.pendingChannels = this.pendingChannels + ((undefined !== rtlStore.pendingChannels.pending_closing_channels) ? rtlStore.pendingChannels.pending_closing_channels.length : 0);
this.pendingChannels = this.pendingChannels + ((undefined !== rtlStore.pendingChannels.pending_force_closing_channels) ? rtlStore.pendingChannels.pending_force_closing_channels.length : 0);
console.warn(rtlStore.pendingChannels.total_limbo_balance);
this.channelsStatus = {
active: { channels: rtlStore.numberOfActiveChannels, capacity: rtlStore.totalCapacityActive },
inactive: { channels: rtlStore.numberOfInactiveChannels, capacity: rtlStore.totalCapacityInactive },
pending: { channels: this.pendingChannels, capacity: rtlStore.pendingChannels.total_limbo_balance },
closed: { channels: (rtlStore.closedChannels && rtlStore.closedChannels.length) ? rtlStore.closedChannels.length : 0, capacity: 0 }
};
if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0 && this.flgLoading[6] !== 'error') { if (rtlStore.totalLocalBalance >= 0 && rtlStore.totalRemoteBalance >= 0 && this.flgLoading[6] !== 'error') {
this.flgLoading[6] = false; this.flgLoading[6] = false;
} }
@ -144,6 +182,30 @@ export class HomeComponent implements OnInit, OnDestroy {
}); });
} }
initializeCards() {
this.breakpointObserver.observe(Breakpoints.Handset).pipe(
map(({ matches }) => {
if (matches) {
return [
{ title: 'Card 1', cols: 1, rows: 1 },
{ title: 'Card 2', cols: 1, rows: 1 },
{ title: 'Card 3', cols: 1, rows: 1 },
{ title: 'Card 4', cols: 1, rows: 1 },
{ title: 'Card 4', cols: 1, rows: 1 }
];
}
return [
{ title: 'Card 1', cols: 3, rows: 1 },
{ title: 'Card 2', cols: 3, rows: 1 },
{ title: 'Card 3', cols: 4, rows: 2 },
{ title: 'Card 4', cols: 3, rows: 1 },
{ title: 'Card 4', cols: 3, rows: 1 }
];
})
);
}
ngOnDestroy() { ngOnDestroy() {
this.unsub.forEach(completeSub => { this.unsub.forEach(completeSub => {
completeSub.next(); completeSub.next();

@ -0,0 +1,14 @@
<div fxLayout="column" fxFlex="100" fxLayoutAlign="center start">
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Alias</h4>
<div class="foreground-secondary-text" [ngStyle]="{'backgroundColor': information.color}">{{information.alias}}</div>
</div>
<div fxFlex="33">
<h4 fxLayoutAlign="start" class="font-bold-500">Implementation</h4>
<div class="foreground-secondary-text">{{information.lnImplementation + ' v' + information.version}}</div>
</div>
<div fxFlex="34">
<h4 fxLayoutAlign="start" class="font-bold-500">Chain</h4>
<span class="overflow-wrap foreground-secondary-text" *ngFor="let chain of chains">{{chain}}</span>
</div>
</div>

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NodeInfoComponent } from './node-info.component';
describe('NodeInfoComponent', () => {
let component: NodeInfoComponent;
let fixture: ComponentFixture<NodeInfoComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ NodeInfoComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NodeInfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,25 @@
import { Component, OnChanges, Input } from '@angular/core';
import { GetInfo } from '../../../shared/models/lndModels';
import { CommonService } from '../../../shared/services/common.service';
@Component({
selector: 'rtl-node-info',
templateUrl: './node-info.component.html',
styleUrls: ['./node-info.component.scss']
})
export class NodeInfoComponent implements OnChanges {
@Input() information: GetInfo;
public chains: Array<string> = [''];
constructor(private commonService: CommonService) { }
ngOnChanges() {
if(this.information && this.information.chains && this.information.chains.length > 0) {
this.chains = [''];
this.information.chains.forEach(chain => {
this.chains.push(this.commonService.titleCase(chain.chain) + ' ' + this.commonService.titleCase(chain.network));
});
}
}
}

@ -35,12 +35,29 @@ import { LNDUnlockedGuard } from '../shared/services/auth.guard';
import { ChannelOpenTableComponent } from './peers-channels/channels/channels-tables/channel-open-table/channel-open-table.component'; import { ChannelOpenTableComponent } from './peers-channels/channels/channels-tables/channel-open-table/channel-open-table.component';
import { UnlockWalletComponent } from './wallet/unlock/unlock.component'; import { UnlockWalletComponent } from './wallet/unlock/unlock.component';
import { InitializeWalletComponent } from './wallet/initialize/initialize.component'; import { InitializeWalletComponent } from './wallet/initialize/initialize.component';
import { NodeInfoComponent } from './home/node-info/node-info.component';
import { BalancesInfoComponent } from './home/balances-info/balances-info.component';
import { FeeInfoComponent } from './home/fee-info/fee-info.component';
import { ChannelStatusInfoComponent } from './home/channel-status-info/channel-status-info.component';
import { ChannelCapacityInfoComponent } from './home/channel-capacity-info/channel-capacity-info.component';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { LayoutModule } from '@angular/cdk/layout';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
LNDRouting LNDRouting,
MatGridListModule,
MatCardModule,
MatMenuModule,
MatIconModule,
MatButtonModule,
LayoutModule
], ],
declarations: [ declarations: [
LNDRootComponent, LNDRootComponent,
@ -70,7 +87,12 @@ import { InitializeWalletComponent } from './wallet/initialize/initialize.compon
ChannelsTablesComponent, ChannelsTablesComponent,
ChannelOpenTableComponent, ChannelOpenTableComponent,
UnlockWalletComponent, UnlockWalletComponent,
InitializeWalletComponent InitializeWalletComponent,
NodeInfoComponent,
BalancesInfoComponent,
FeeInfoComponent,
ChannelStatusInfoComponent,
ChannelCapacityInfoComponent
], ],
providers: [ providers: [
{ provide: LoggerService, useClass: ConsoleLoggerService }, { provide: LoggerService, useClass: ConsoleLoggerService },

@ -58,7 +58,6 @@ export class ChannelClosedTableComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed'}));
this.actions$.pipe(takeUntil(this.unsub[2]), filter((action) => action.type === RTLActions.RESET_LND_STORE)).subscribe((resetLndStore: RTLActions.ResetLNDStore) => { this.actions$.pipe(takeUntil(this.unsub[2]), filter((action) => action.type === RTLActions.RESET_LND_STORE)).subscribe((resetLndStore: RTLActions.ResetLNDStore) => {
this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed'})); this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed'}));
}); });

@ -12,8 +12,8 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header> Peer </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Peer </th>
<td mat-cell *matCellDef="let channel"> <td mat-cell *matCellDef="let channel">
<div class="ellipsis-parent"> <div class="ellipsis-parent">
<span *ngIf="channel.active" class="green-dot"></span> <span *ngIf="channel.active" class="dot green"></span>
<span *ngIf="!channel.active" class="yellow-dot"></span> <span *ngIf="!channel.active" class="dot yellow"></span>
<span class="ellipsis-child">{{channel.remote_alias || channel.remote_pubkey}}</span> <span class="ellipsis-child">{{channel.remote_alias || channel.remote_pubkey}}</span>
</div> </div>
</td> </td>

@ -58,6 +58,7 @@ export class LNDEffects implements OnDestroy {
payload: {} payload: {}
}; };
} else { } else {
info.lnImplementation = 'LND';
this.initializeRemainingData(info, action.payload.loadPage); this.initializeRemainingData(info, action.payload.loadPage);
return { return {
type: RTLActions.SET_INFO, type: RTLActions.SET_INFO,
@ -1048,6 +1049,7 @@ export class LNDEffects implements OnDestroy {
this.store.dispatch(new RTLActions.FetchNetwork()); this.store.dispatch(new RTLActions.FetchNetwork());
this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all'})); this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'all'}));
this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'pending'})); this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'pending'}));
this.store.dispatch(new RTLActions.FetchChannels({routeParam: 'closed'}));
this.store.dispatch(new RTLActions.FetchInvoices({num_max_invoices: 10, reversed: true})); this.store.dispatch(new RTLActions.FetchInvoices({num_max_invoices: 10, reversed: true}));
this.store.dispatch(new RTLActions.FetchPayments()); this.store.dispatch(new RTLActions.FetchPayments());
let newRoute = this.location.path(); let newRoute = this.location.path();

@ -1,7 +1,7 @@
import { SelNodeChild } from '../../shared/models/RTLconfig'; import { SelNodeChild } from '../../shared/models/RTLconfig';
import { ErrorPayload } from '../../shared/models/errorPayload'; import { ErrorPayload } from '../../shared/models/errorPayload';
import { import {
GetInfo, Peer, AddressType, Fees, NetworkInfo, Balance, Channel, Payment, ListInvoices, PendingChannels, ClosedChannel, Transaction, SwitchRes, QueryRoutes GetInfo, Peer, Fees, NetworkInfo, Balance, Channel, Payment, ListInvoices, PendingChannels, ClosedChannel, Transaction, SwitchRes, QueryRoutes
} from '../../shared/models/lndModels'; } from '../../shared/models/lndModels';
import * as RTLActions from '../../store/rtl.actions'; import * as RTLActions from '../../store/rtl.actions';
@ -20,6 +20,8 @@ export interface LNDState {
numberOfActiveChannels: number; numberOfActiveChannels: number;
numberOfInactiveChannels: number; numberOfInactiveChannels: number;
numberOfPendingChannels: number; numberOfPendingChannels: number;
totalCapacityActive: number;
totalCapacityInactive: number;
totalLocalBalance: number; totalLocalBalance: number;
totalRemoteBalance: number; totalRemoteBalance: number;
totalInvoices: number; totalInvoices: number;
@ -44,6 +46,8 @@ export const initLNDState: LNDState = {
numberOfActiveChannels: 0, numberOfActiveChannels: 0,
numberOfInactiveChannels: 0, numberOfInactiveChannels: 0,
numberOfPendingChannels: -1, numberOfPendingChannels: -1,
totalCapacityActive: -1,
totalCapacityInactive: -1,
totalLocalBalance: -1, totalLocalBalance: -1,
totalRemoteBalance: -1, totalRemoteBalance: -1,
totalInvoices: -1, totalInvoices: -1,
@ -133,7 +137,7 @@ export function LNDReducer(state = initLNDState, action: RTLActions.RTLActions)
numberOfPendingChannels: action.payload.pendingChannels, numberOfPendingChannels: action.payload.pendingChannels,
}; };
case RTLActions.SET_CHANNELS: case RTLActions.SET_CHANNELS:
let localBal = 0, remoteBal = 0, activeChannels = 0, inactiveChannels = 0; let localBal = 0, remoteBal = 0, activeChannels = 0, inactiveChannels = 0, totalCapacityActive = 0, totalCapacityInactive = 0;
if (action.payload) { if (action.payload) {
action.payload.filter(channel => { action.payload.filter(channel => {
if (undefined !== channel.local_balance) { if (undefined !== channel.local_balance) {
@ -143,8 +147,10 @@ export function LNDReducer(state = initLNDState, action: RTLActions.RTLActions)
remoteBal = +remoteBal + +channel.remote_balance; remoteBal = +remoteBal + +channel.remote_balance;
} }
if (channel.active === true) { if (channel.active === true) {
totalCapacityActive = totalCapacityActive + +channel.capacity;
activeChannels = activeChannels + 1; activeChannels = activeChannels + 1;
} else { } else {
totalCapacityInactive = totalCapacityInactive + +channel.capacity;
inactiveChannels = inactiveChannels + 1; inactiveChannels = inactiveChannels + 1;
} }
}); });
@ -154,6 +160,8 @@ export function LNDReducer(state = initLNDState, action: RTLActions.RTLActions)
allChannels: action.payload, allChannels: action.payload,
numberOfActiveChannels: activeChannels, numberOfActiveChannels: activeChannels,
numberOfInactiveChannels: inactiveChannels, numberOfInactiveChannels: inactiveChannels,
totalCapacityActive: totalCapacityActive,
totalCapacityInactive: totalCapacityInactive,
totalLocalBalance: localBal, totalLocalBalance: localBal,
totalRemoteBalance: remoteBal totalRemoteBalance: remoteBal
}; };

@ -47,8 +47,8 @@
<ng-container matColumnDef="creation_date"> <ng-container matColumnDef="creation_date">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Date Created </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> Date Created </th>
<td mat-cell *matCellDef="let invoice"> <td mat-cell *matCellDef="let invoice">
<span *ngIf="invoice.settled" class="green-dot"></span> <span *ngIf="invoice.settled" class="dot green"></span>
<span *ngIf="!invoice.settled" class="yellow-dot"></span> <span *ngIf="!invoice.settled" class="dot yellow"></span>
{{invoice.creation_date_str}}</td> {{invoice.creation_date_str}}</td>
</ng-container> </ng-container>
<ng-container matColumnDef="memo"> <ng-container matColumnDef="memo">

@ -26,6 +26,7 @@ export interface GetInfoCL {
fees_collected_msat?: string; fees_collected_msat?: string;
currency_unit?: string; currency_unit?: string;
smaller_currency_unit?: string; smaller_currency_unit?: string;
lnImplementation?: string;
} }
export interface FeesCL { export interface FeesCL {

@ -1,3 +1,15 @@
export interface ChannelStatus {
channels?: number;
capacity?:number;
}
export interface ChannelsStatus {
active?: ChannelStatus;
inactive?: ChannelStatus;
pending?: ChannelStatus;
closed?: ChannelStatus;
}
export interface AddressType { export interface AddressType {
addressId?: string; addressId?: string;
addressTp?: string; addressTp?: string;
@ -142,6 +154,7 @@ export interface GetInfoChain {
export interface GetInfo { export interface GetInfo {
identity_pubkey?: string; identity_pubkey?: string;
alias?: string; alias?: string;
color?: string;
num_pending_channels?: number; num_pending_channels?: number;
num_active_channels?: number; num_active_channels?: number;
num_peers?: number; num_peers?: number;
@ -155,6 +168,7 @@ export interface GetInfo {
version?: string; version?: string;
currency_unit?: string; currency_unit?: string;
smaller_currency_unit?: string; smaller_currency_unit?: string;
lnImplementation?: string;
} }
export interface GraphNode { export interface GraphNode {

@ -23,4 +23,7 @@ $fa-icon-regular-size: 4rem;
$yellow-color: #ffbd2e; $yellow-color: #ffbd2e;
$green-color: #28ca43; $green-color: #28ca43;
$red-color: #c62828;
$grey-color: #AAAAAA;
$tiny-dot-size: 0.8rem;
$dot-size: 1.2rem; $dot-size: 1.2rem;

@ -229,12 +229,16 @@ body {
color: #388e3c !important; color: #388e3c !important;
} }
.yellow {
color: #ffd740 !important;
}
.red { .red {
color: #c62828 !important; color: #c62828 !important;
} }
.yellow { .grey {
color: #ffd740 !important; color: #CCCCCC !important;
} }
.mat-dialog-container { .mat-dialog-container {
@ -417,6 +421,10 @@ body {
padding: 1rem !important; padding: 1rem !important;
} }
.p-16 {
padding: 1.6rem !important;
}
.pt-2 { .pt-2 {
padding-top: 2rem !important; padding-top: 2rem !important;
} }
@ -793,22 +801,31 @@ table {
width:100%; width:100%;
} }
.green-dot { .dot {
display: inline-flex;
width: $dot-size;
height: $dot-size;
border-radius: $dot-size;
margin-right: 1rem;
background-color: $green-color;
}
.yellow-dot {
display: inline-flex; display: inline-flex;
width: $dot-size; width: $dot-size;
height: $dot-size; height: $dot-size;
border-radius: $dot-size; border-radius: $dot-size;
margin-right: 1rem; margin-right: 1rem;
background-color: $yellow-color; &.tiny-dot {
width: $tiny-dot-size;
height: $tiny-dot-size;
border-radius: $tiny-dot-size;
margin-right: 0.6rem;
margin-bottom: 0.2rem;
}
&.green {
background-color: $green-color;
}
&.yellow {
background-color: $yellow-color;
}
&.red {
background-color: $red-color;
}
&.grey {
background-color: $grey-color;
}
} }
.font-size-80 { .font-size-80 {

Loading…
Cancel
Save