Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualize gateway traffic and status #85

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, 'MM 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