Skip to content

Commit

Permalink
Visualize gateway traffic and status (#85)
Browse files Browse the repository at this point in the history
* Fetch single gateway

* Add empty gateway graphs.

* Visualize gateway packages

* Fix date and merge

Co-authored-by: nlg <[email protected]>
  • Loading branch information
AramAlsabti and nlg authored May 18, 2022
1 parent a809fb3 commit c3fe17d
Show file tree
Hide file tree
Showing 16 changed files with 188 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
<div class="row">
<div class="col-md-6 d-flex align-items-stretch mt-1 mb-4">
<app-graph *ngIf="device.type === 'SIGFOX' || device.type === 'LORAWAN'" [data]="rssiChartData"
[type]="'line'" [options]="rssiChartOptions" [title]="'IOTDEVICE.HISTORY-TAB.RSSI' | translate">
[type]="'line'" [title]="'IOTDEVICE.HISTORY-TAB.RSSI' | translate">
</app-graph>
</div>
<div class="col-md-6 d-flex align-items-stretch mt-1 mb-4">
<app-graph *ngIf="device.type === 'SIGFOX' || device.type === 'LORAWAN'" [data]="snrChartData"
[type]="'line'" [options]="snrChartOptions" [title]="'IOTDEVICE.HISTORY-TAB.SNR' | translate">
[type]="'line'" [title]="'IOTDEVICE.HISTORY-TAB.SNR' | translate">
</app-graph>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,7 @@ import { MatTabChangeEvent } from '@angular/material/tabs';
import { ChartConfiguration } from 'chart.js';
import * as moment from 'moment';
import { recordToEntries } from '@shared/helpers/record.helper';

const colorGraphBlue1 = '#03AEEF';

const defaultChartOptions: ChartConfiguration['options'] = {
plugins: { legend: { display: false }, },
responsive: true,
layout: {
padding: {
top: 15,
left: 10,
right: 10,
}
},
};
import { ColorGraphBlue1 } from '@shared/constants/color-constants';

/**
* Ordered from "worst" to "best" (from DR0 and up)
Expand Down Expand Up @@ -83,17 +70,13 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy {
dataRateChartData: ChartConfiguration['data'] = { datasets: [] };
rssiChartData: ChartConfiguration['data'] = { datasets: [] };
snrChartData: ChartConfiguration['data'] = { datasets: [] };
rssiChartOptions = defaultChartOptions;
snrChartOptions: typeof defaultChartOptions = defaultChartOptions;

dataRateChartOptions: typeof defaultChartOptions = {
...defaultChartOptions,
dataRateChartOptions: ChartConfiguration['options'] = {
scales: {
x: { stacked: true },
y: { stacked: true },
},
plugins: {
...defaultChartOptions,
tooltip: {
mode: 'index',
position: 'average',
Expand Down Expand Up @@ -250,10 +233,10 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy {
},
{
rssiDatasets: [
{ data: [], borderColor: colorGraphBlue1, backgroundColor: colorGraphBlue1 },
{ data: [], borderColor: ColorGraphBlue1, backgroundColor: ColorGraphBlue1 },
],
snrDatasets: [
{ data: [], borderColor: colorGraphBlue1, backgroundColor: colorGraphBlue1 },
{ data: [], borderColor: ColorGraphBlue1, backgroundColor: ColorGraphBlue1 },
],
dataRateDatasets: this.initDataRates(),
labels: [],
Expand Down
74 changes: 46 additions & 28 deletions src/app/gateway/gateway-detail/gateway-detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,54 @@ <h3>{{ 'GATEWAY.LOCATION' | translate }}</h3>
<div class="row">
<div class="col-12">
<div class="jumbotron">
<h3>{{ 'GATEWAY.STATS' | translate }}</h3>
<div class="mat-elevation-z8">
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>
<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="rxPacketsReceived">
<th mat-header-cell *matHeaderCellDef>
{{ 'GATEWAY.STATS-RXPACKETSRECEIVED' | translate }}
</th>
<td mat-cell *matCellDef="let element">{{element.rxPacketsReceived}}</td>
</ng-container>

<ng-container matColumnDef="txPacketsEmitted">
<th mat-header-cell *matHeaderCellDef>{{ 'GATEWAY.STATS-TXPACKETSEMITTED' | translate }}
</th>
<td mat-cell *matCellDef="let element">{{element.txPacketsEmitted}}</td>
</ng-container>
<app-gateway-status [isVisibleSubject]="isGatewayStatusVisibleSubject" [gatewayId]="id" paginatorClass="d-none"
[shouldLinkToDetails]="false" [title]="'GATEWAY.ONLINE-STATUS' | translate">
</app-gateway-status>
</div>

<ng-container matColumnDef="txPacketsReceived">
<th mat-header-cell *matHeaderCellDef>{{ 'GATEWAY.STATS-TIMESTAMP' | translate }}</th>
<td mat-cell *matCellDef="let element">{{element.timestamp | date}}</td>
</ng-container>
<div class="jumbotron">
<h3>{{ 'GATEWAY.DATA-PACKETS' | translate }}</h3>
<div class="loading-shade" *ngIf="isLoadingResults">
<mat-spinner *ngIf="isLoadingResults"></mat-spinner>
</div>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [length]="resultLength" [pageSizeOptions]="[5, 10, 20]" [pageSize]="pageSize" showFirstLastButtons>
</mat-paginator>
<div class="d-flex flex-row mb-4">
<div class="col-md-6 d-flex align-items-stretch mt-1 mb-4">
<app-graph [data]="receivedGraphData" [type]="'line'" [title]="'GATEWAY.STATS-RXPACKETSRECEIVED' | translate"
[graphCardClass]="'shadow-none pl-0'" [graphHeaderClass]="'mat-card-header-text-ml-0'">
</app-graph>
</div>
<div class="col-md-6 d-flex align-items-stretch mt-1 mb-4">
<app-graph [data]="sentGraphData" [type]="'line'" [title]="'GATEWAY.STATS-TXPACKETSEMITTED' | translate"
[graphCardClass]="'shadow-none pl-0'" [graphHeaderClass]="'mat-card-header-text-ml-0'">
</app-graph>
</div>
</div>

<table mat-table [dataSource]="dataSource">
<ng-container matColumnDef="rxPacketsReceived">
<th mat-header-cell *matHeaderCellDef>
{{ 'GATEWAY.STATS-RXPACKETSRECEIVED' | translate }}
</th>
<td mat-cell *matCellDef="let element">{{element.rxPacketsReceived}}</td>
</ng-container>

<ng-container matColumnDef="txPacketsEmitted">
<th mat-header-cell *matHeaderCellDef>{{ 'GATEWAY.STATS-TXPACKETSEMITTED' | translate }}
</th>
<td mat-cell *matCellDef="let element">{{element.txPacketsEmitted}}</td>
</ng-container>

<ng-container matColumnDef="txPacketsReceived">
<th mat-header-cell *matHeaderCellDef>{{ 'GATEWAY.STATS-TIMESTAMP' | translate }}</th>
<td mat-cell *matCellDef="let element">{{element.timestamp | date}}</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<mat-paginator [length]="resultLength" [pageSizeOptions]="[5, 10, 20]" [pageSize]="pageSize" showFirstLastButtons>
</mat-paginator>
</div>
</div>
</div>
</div>
4 changes: 2 additions & 2 deletions src/app/gateway/gateway-detail/gateway-detail.component.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
table {
width: 100%;
}
width: 100%;
}
52 changes: 47 additions & 5 deletions src/app/gateway/gateway-detail/gateway-detail.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { AfterViewInit, Component, EventEmitter, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subscription, Subject } from 'rxjs';
import { ChirpstackGatewayService } from 'src/app/shared/services/chirpstack-gateway.service';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator } from '@angular/material/paginator';
import { BackButton } from '@shared/models/back-button.model';
import { Gateway, GatewayStats } from '../gateway.model';
import { Gateway, GatewayStats, GatewayResponse } from '../gateway.model';
import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
import { MeService } from '@shared/services/me.service';
import { environment } from '@environments/environment';
import { DropdownButton } from '@shared/models/dropdown-button.model';
import { ChartConfiguration } from 'chart.js';
import { ColorGraphBlue1 } from '@shared/constants/color-constants';
import { formatDate } from '@angular/common';

@Component({
selector: 'app-gateway-detail',
Expand All @@ -29,11 +32,14 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit
public gatewaySubscription: Subscription;
public gateway: Gateway;
public backButton: BackButton = { label: '', routerLink: ['gateways'] };
private id: string;
id: string;
deleteGateway = new EventEmitter();
private deleteDialogSubscription: Subscription;
public dropdownButton: DropdownButton;
isLoadingResults = true;
isGatewayStatusVisibleSubject = new Subject<void>();
receivedGraphData: ChartConfiguration['data'] = { datasets: [] };
sentGraphData: ChartConfiguration['data'] = { datasets: [] };

constructor(
private gatewayService: ChirpstackGatewayService,
Expand Down Expand Up @@ -72,7 +78,7 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit
}

bindGateway(id: string): void {
this.gatewayService.get(id).subscribe((result: any) => {
this.gatewayService.get(id).subscribe((result: GatewayResponse) => {
result.gateway.tagsString = JSON.stringify(result.gateway.tags);
this.gateway = result.gateway;
this.gateway.canEdit = this.canEdit();
Expand All @@ -83,6 +89,9 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit
this.dataSource.paginator = this.paginator;
this.setDropdownButton();
this.isLoadingResults = false;

this.buildGraphs();
this.isGatewayStatusVisibleSubject.next();
});
}

Expand All @@ -94,11 +103,44 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit
} : null;
this.translate.get(['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS'])
.subscribe(translations => {
this.dropdownButton.label = translations['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS']
this.dropdownButton.label = translations['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS'];
}
);
}

private buildGraphs() {
const { receivedDatasets, sentDatasets, labels } = this.gatewayStats.reduce(
(
res: {
receivedDatasets: ChartConfiguration['data']['datasets'];
sentDatasets: ChartConfiguration['data']['datasets'];
labels: ChartConfiguration['data']['labels'];
},
data
) => {
res.receivedDatasets[0].data.push(data.rxPacketsReceived);
res.sentDatasets[0].data.push(data.txPacketsEmitted);

// Formatted to stay consistent with the corresponding table. When more languages are added,
// register and use them properly. See https://stackoverflow.com/a/54769064
res.labels.push(formatDate(data.timestamp, 'dd MMM', 'en-US'));
return res;
},
{
receivedDatasets: [
{ data: [], borderColor: ColorGraphBlue1, backgroundColor: ColorGraphBlue1 },
],
sentDatasets: [
{ data: [], borderColor: ColorGraphBlue1, backgroundColor: ColorGraphBlue1 },
],
labels: [],
}
);

this.receivedGraphData = { datasets: receivedDatasets, labels };
this.sentGraphData = { datasets: sentDatasets, labels };
}

canEdit(): boolean {
return this.meService.canWriteInTargetOrganization(this.gateway.internalOrganizationId);
}
Expand Down
5 changes: 3 additions & 2 deletions src/app/gateway/gateway-status/gateway-status.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</ng-template>

<div class="d-flex mb-3">
<h3 *ngIf="title">{{title}}</h3>
<h3 *ngIf="title" class="title">{{title}}</h3>
<mat-select class="form-control ml-auto status-interval" name="statusIntervals" [(ngModel)]="selectedStatusInterval">
<mat-option *ngFor="let interval of statusIntervals" [value]="interval"
(onSelectionChange)="onSelectInterval($event)">
Expand All @@ -21,7 +21,8 @@ <h3 *ngIf="title">{{title}}</h3>
*ngIf="dataSource?.data.length && timeColumns.length; else noGatewayStatusData">
<ng-container matColumnDef="gatewayName">
<td mat-cell *matCellDef="let element">
<a [routerLink]="'/gateways/gateway-detail/' + element.id" routerLinkActive="active">{{element.name}}</a>
<a [routerLink]="'/gateways/gateway-detail/' + element.id" routerLinkActive="active" *ngIf="shouldLinkToDetails">{{element.name}}</a>
<span class="text--semibold" *ngIf="!shouldLinkToDetails">{{element.name}}</span>
</td>
<td mat-footer-cell *matFooterCellDef class="text--semibold">
{{'GEN.DATE' | translate}}
Expand Down
4 changes: 4 additions & 0 deletions src/app/gateway/gateway-status/gateway-status.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,7 @@ $cellFontSize: 0.9rem;
width: 180px;
font-size: $cellFontSize;
}

.title {
margin-bottom: 0 !important;
}
20 changes: 16 additions & 4 deletions src/app/gateway/gateway-status/gateway-status.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { LoRaWANGatewayService } from '@shared/services/lorawan-gateway.service'
import * as moment from 'moment';
import { Observable, Subject, Subscription } from 'rxjs';
import { GatewayStatusInterval } from '../enums/gateway-status-interval.enum';
import { GatewayStatus, GatewayStatusResponse } from '../gateway.model';
import { GatewayStatus, AllGatewayStatusResponse } from '../gateway.model';
import { map } from 'rxjs/operators';

interface TimeColumn {
exactTimestamp: string;
Expand All @@ -34,6 +35,8 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy {
@Input() isVisibleSubject: Subject<void>;
@Input() paginatorClass: string;
@Input() title: string;
@Input() gatewayId: string;
@Input() shouldLinkToDetails = true;

private gatewayStatusSubscription: Subscription;
private readonly columnGatewayName = 'gatewayName';
Expand Down Expand Up @@ -95,7 +98,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy {
private getGatewayStatus(
organizationId = this.organizationId,
timeInterval = this.selectedStatusInterval
): Observable<GatewayStatusResponse> {
): Observable<AllGatewayStatusResponse> {
const params: Record<string, string | number> = {
timeInterval,
// Paginator is only avaiable in ngAfterViewInit
Expand All @@ -107,7 +110,16 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy {
params.organizationId = organizationId;
}

return this.lorawanGatewayService.getAllStatus(params);
return !this.gatewayId
? this.lorawanGatewayService.getAllStatus(params)
: this.lorawanGatewayService
.getStatus(this.gatewayId, { timeInterval })
.pipe(
map(
(response) =>
({ data: [response], count: 1 } as AllGatewayStatusResponse)
)
);
}

private subscribeToGetAllGatewayStatus(
Expand All @@ -128,7 +140,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy {
});
}

private handleStatusResponse(response: GatewayStatusResponse) {
private handleStatusResponse(response: AllGatewayStatusResponse) {
this.resultsLength = response.count;
const gatewaysWithLatestTimestampsPerHour = this.takeLatestTimestampInHour(
response.data
Expand Down
8 changes: 6 additions & 2 deletions src/app/gateway/gateway.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface GatewayStats {
txPacketsEmitted: number;
}

export interface GetGatewayStatusParameters {
export interface GetAllGatewayStatusParameters {
limit?: number;
offset?: number;
organizationId?: number;
Expand All @@ -71,13 +71,17 @@ export interface StatusTimestamp {
wasOnline: boolean;
}

export interface GetGatewayStatusParameters {
timeInterval?: GatewayStatusInterval;
}

export interface GatewayStatus {
id: string;
name: string;
statusTimestamps: StatusTimestamp[];
}

export interface GatewayStatusResponse {
export interface AllGatewayStatusResponse {
data: GatewayStatus[];
count: number;
}
2 changes: 2 additions & 0 deletions src/app/gateway/gateway.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { FormModule } from '@shared/components/forms/form.module';
import { SharedModule } from '@shared/shared.module';
import { PipesModule } from '@shared/pipes/pipes.module';
import { GatewayStatusComponent } from './gateway-status/gateway-status.component';
import { GraphModule } from '@app/graph/graph.module';

const gatewayRoutes: Routes = [
{
Expand Down Expand Up @@ -49,6 +50,7 @@ const gatewayRoutes: Routes = [
RouterModule.forChild(gatewayRoutes),
SharedModule,
PipesModule,
GraphModule,
],
exports: [
GatewayTableComponent,
Expand Down
Loading

0 comments on commit c3fe17d

Please sign in to comment.