diff --git a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.html b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.html index 9a1424abe..907d2f86c 100644 --- a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.html +++ b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.html @@ -22,12 +22,12 @@
+ [type]="'line'" [title]="'IOTDEVICE.HISTORY-TAB.RSSI' | translate">
+ [type]="'line'" [title]="'IOTDEVICE.HISTORY-TAB.SNR' | translate">
diff --git a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts index 6104c3ba4..aa4fb7574 100644 --- a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts +++ b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts @@ -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) @@ -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', @@ -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: [], diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.html b/src/app/gateway/gateway-detail/gateway-detail.component.html index f6981726c..0494dea12 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.html +++ b/src/app/gateway/gateway-detail/gateway-detail.component.html @@ -46,36 +46,54 @@

{{ 'GATEWAY.LOCATION' | translate }}

-

{{ 'GATEWAY.STATS' | translate }}

-
-
- -
- - - - - - - - - - + + + - - - - +
+

{{ 'GATEWAY.DATA-PACKETS' | translate }}

+
+ +
-
- -
- {{ 'GATEWAY.STATS-RXPACKETSRECEIVED' | translate }} - {{element.rxPacketsReceived}}{{ 'GATEWAY.STATS-TXPACKETSEMITTED' | translate }} - {{element.txPacketsEmitted}}{{ 'GATEWAY.STATS-TIMESTAMP' | translate }}{{element.timestamp | date}}
- - +
+
+ + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + +
+ {{ 'GATEWAY.STATS-RXPACKETSRECEIVED' | translate }} + {{element.rxPacketsReceived}}{{ 'GATEWAY.STATS-TXPACKETSEMITTED' | translate }} + {{element.txPacketsEmitted}}{{ 'GATEWAY.STATS-TIMESTAMP' | translate }}{{element.timestamp | date}}
+ +
-
\ No newline at end of file +
diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.scss b/src/app/gateway/gateway-detail/gateway-detail.component.scss index 00e4cb9fd..1922e7ffa 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.scss +++ b/src/app/gateway/gateway-detail/gateway-detail.component.scss @@ -1,3 +1,3 @@ table { - width: 100%; - } \ No newline at end of file + width: 100%; +} diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.ts b/src/app/gateway/gateway-detail/gateway-detail.component.ts index b9e750079..5195e80b6 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.ts +++ b/src/app/gateway/gateway-detail/gateway-detail.component.ts @@ -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', @@ -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(); + receivedGraphData: ChartConfiguration['data'] = { datasets: [] }; + sentGraphData: ChartConfiguration['data'] = { datasets: [] }; constructor( private gatewayService: ChirpstackGatewayService, @@ -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(); @@ -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(); }); } @@ -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); } diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index 26bf6853c..81ce42d60 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -5,7 +5,7 @@
-

{{title}}

+

{{title}}

@@ -21,7 +21,8 @@

{{title}}

*ngIf="dataSource?.data.length && timeColumns.length; else noGatewayStatusData"> - {{element.name}} + {{element.name}} + {{element.name}} {{'GEN.DATE' | translate}} diff --git a/src/app/gateway/gateway-status/gateway-status.component.scss b/src/app/gateway/gateway-status/gateway-status.component.scss index ed89726e2..18ded2fbf 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.scss +++ b/src/app/gateway/gateway-status/gateway-status.component.scss @@ -99,3 +99,7 @@ $cellFontSize: 0.9rem; width: 180px; font-size: $cellFontSize; } + +.title { + margin-bottom: 0 !important; +} diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index f66f0bcf3..f569027db 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -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; @@ -34,6 +35,8 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { @Input() isVisibleSubject: Subject; @Input() paginatorClass: string; @Input() title: string; + @Input() gatewayId: string; + @Input() shouldLinkToDetails = true; private gatewayStatusSubscription: Subscription; private readonly columnGatewayName = 'gatewayName'; @@ -95,7 +98,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { private getGatewayStatus( organizationId = this.organizationId, timeInterval = this.selectedStatusInterval - ): Observable { + ): Observable { const params: Record = { timeInterval, // Paginator is only avaiable in ngAfterViewInit @@ -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( @@ -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 diff --git a/src/app/gateway/gateway.model.ts b/src/app/gateway/gateway.model.ts index 1da46e785..56efa7d94 100644 --- a/src/app/gateway/gateway.model.ts +++ b/src/app/gateway/gateway.model.ts @@ -59,7 +59,7 @@ export interface GatewayStats { txPacketsEmitted: number; } -export interface GetGatewayStatusParameters { +export interface GetAllGatewayStatusParameters { limit?: number; offset?: number; organizationId?: number; @@ -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; } diff --git a/src/app/gateway/gateway.module.ts b/src/app/gateway/gateway.module.ts index cf8d06dc1..4f6c01562 100644 --- a/src/app/gateway/gateway.module.ts +++ b/src/app/gateway/gateway.module.ts @@ -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 = [ { @@ -49,6 +50,7 @@ const gatewayRoutes: Routes = [ RouterModule.forChild(gatewayRoutes), SharedModule, PipesModule, + GraphModule, ], exports: [ GatewayTableComponent, diff --git a/src/app/graph/graph.component.html b/src/app/graph/graph.component.html index 0e724195c..1c8030cf3 100644 --- a/src/app/graph/graph.component.html +++ b/src/app/graph/graph.component.html @@ -1,12 +1,12 @@ - - + + {{title}}
-

{{ 'GEN.NO-DATA' | translate }}

+ {{ 'GEN.NO-DATA' | translate }}
diff --git a/src/app/graph/graph.component.ts b/src/app/graph/graph.component.ts index 2710eca4b..e1d0d478d 100644 --- a/src/app/graph/graph.component.ts +++ b/src/app/graph/graph.component.ts @@ -19,9 +19,19 @@ export class GraphComponent implements OnChanges { @Input() data: ChartConfiguration['data']; @Input() type: ChartConfiguration['type']; @Input() options: ChartConfiguration['options'] = { + plugins: { legend: { display: false } }, responsive: true, + layout: { + padding: { + top: 15, + left: 10, + right: 10, + }, + }, }; @Input() title: string; + @Input() graphCardClass: string; + @Input() graphHeaderClass: string; chartInstance: Chart = null; isGraphEmpty: boolean; @@ -72,13 +82,13 @@ export class GraphComponent implements OnChanges { ? { ...options, scales: { - ...options.scales, + ...options?.scales, x: { - ...options.scales.x, + ...options?.scales?.x, display: false, }, y: { - ...options.scales.y, + ...options?.scales?.y, display: false, }, }, diff --git a/src/app/shared/constants/color-constants.ts b/src/app/shared/constants/color-constants.ts new file mode 100644 index 000000000..19b9b6c5a --- /dev/null +++ b/src/app/shared/constants/color-constants.ts @@ -0,0 +1 @@ +export const ColorGraphBlue1 = '#03AEEF'; diff --git a/src/app/shared/services/lorawan-gateway.service.ts b/src/app/shared/services/lorawan-gateway.service.ts index c07d193b5..21b0afb87 100644 --- a/src/app/shared/services/lorawan-gateway.service.ts +++ b/src/app/shared/services/lorawan-gateway.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@angular/core'; import { - GatewayStatusResponse, + AllGatewayStatusResponse, + GetAllGatewayStatusParameters, GetGatewayStatusParameters, + GatewayStatus, } from '@app/gateway/gateway.model'; import { Observable } from 'rxjs'; import { RestService } from './rest.service'; @@ -15,8 +17,19 @@ export class LoRaWANGatewayService { constructor(private restService: RestService) {} public getAllStatus( - params: GetGatewayStatusParameters - ): Observable { + params: GetAllGatewayStatusParameters + ): Observable { return this.restService.get(`${this.baseUrl}/status`, params); } + + public getStatus( + id: string, + params: GetGatewayStatusParameters + ): Observable { + return this.restService.get( + `${this.baseUrl}/status`, + params, + id + ); + } } diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index 273a64c6f..1a2708dc0 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -13,7 +13,12 @@ "VALUE": "Værdi", "NAME": "Navn", "DESCRIPTION": "Beskrivelse", - "to": "til" + "to": "til", + "ONLINE": "Online", + "OFFLINE": "Offline", + "NEVER-SEEN": "Aldrig set", + "DATE": "Dato", + "NO-DATA": "Der er ingen data at vise" }, "NAV": { "APPLICATIONS": "Applikationer", @@ -160,13 +165,15 @@ "NAME": "Gatewayens navn", "STATS": "Statistik", "STATS-RXPACKETSRECEIVED": "Pakker modtaget", - "STATS-TXPACKETSEMITTED": "Pakker Sendt", + "STATS-TXPACKETSEMITTED": "Pakker sendt", "STATS-TIMESTAMP": "Tidspunkt", "TABEL-TAB": "Listevisning", "DROPDOWNFILTER": "Organisationsfilter", "DROPDOWNDEFAULT": "Alle", "MAP-TAB": "Kort", - "ORGANIZATION": "Organisation" + "ORGANIZATION": "Organisation", + "ONLINE-STATUS": "Online status", + "DATA-PACKETS": "Datapakker" }, "IOT-DEVICE": { "CREATE": "Gem IoT enhed", @@ -301,6 +308,15 @@ "STATUS": "Status", "ORGANIZATION": "Organisation" }, + "LORA-GATEWAY-STATUS": { + "TITLE": "LoRaWAN online status historik", + "TIMESTAMP": "Tidspunkt", + "INTERVAL": { + "DAY": "Seneste døgn", + "WEEK": "Seneste uge", + "MONTH": "Seneste måned" + } + }, "DEVICE-MODEL": { "DELETE-FAILED":"Slet Fejlede", "HEADLINE":"Detaljer", @@ -1069,8 +1085,5 @@ "GIVE-DATATARGET-CONTEXT-INFO": "hvis tom, skal den angives i 'payload'", "GIVE-DATATARGET-CONTEXT-PLACEHOLDER": "https://os2iot/context-file.json" } - }, - "GRAPH": { - "NO-DATA": "Der er ingen data at vise" } } diff --git a/src/styles.scss b/src/styles.scss index 09fd330b2..7c28ffbd2 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -94,6 +94,10 @@ body { white-space: pre-line; } +.mat-card-header-text-ml-0 .mat-card-header-text { + margin-left: 0; +} + .welcome-screen .mat-dialog-container { background-color: #55b156; }