Skip to content

Commit e96a976

Browse files
Fixed routing of gateway list + fixed memory leak (#144)
* Fixed routing of gateway list + fixed memory leak by unsubscribing properly from gateway fetches * Fixed routing errors in gateway list
1 parent 9c2fb6a commit e96a976

File tree

4 files changed

+168
-148
lines changed

4 files changed

+168
-148
lines changed

src/app/gateway/gateway-overview/gateway-overview.component.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { environment } from '@environments/environment';
99
import { Title } from '@angular/platform-browser';
1010
import { OrganizationAccessScope } from '@shared/enums/access-scopes';
1111
import { GatewayService } from '@app/gateway/gateway.service';
12-
import { Router } from '@angular/router';
12+
import { NavigationEnd, Router } from '@angular/router';
1313

1414
@Component({
1515
selector: 'app-gateway-list',
@@ -40,6 +40,7 @@ export class GatewayOverviewComponent implements OnInit, OnChanges, OnDestroy {
4040
organisations: Organisation[];
4141

4242
private deleteDialogSubscription: Subscription;
43+
private routerSubscription: Subscription;
4344
canEdit: boolean;
4445

4546
constructor(
@@ -68,6 +69,12 @@ export class GatewayOverviewComponent implements OnInit, OnChanges, OnDestroy {
6869
if (this.router.url === '/gateways') {
6970
this.router.navigateByUrl('/gateways/list', { replaceUrl: true });
7071
}
72+
// Subscribe to route change to root and route to list view
73+
this.routerSubscription = this.router.events.subscribe((e) => {
74+
if (e instanceof NavigationEnd && e.url === '/gateways') {
75+
this.router.navigateByUrl('/gateways/list', { replaceUrl: true });
76+
}
77+
});
7178
}
7279

7380
ngOnChanges() {}
@@ -82,5 +89,6 @@ export class GatewayOverviewComponent implements OnInit, OnChanges, OnDestroy {
8289
// prevent memory leak by unsubscribing
8390
this.gatewaySubscription?.unsubscribe();
8491
this.deleteDialogSubscription?.unsubscribe();
92+
this.routerSubscription?.unsubscribe();
8593
}
8694
}

src/app/gateway/gateway-overview/gateway-tabs/gateway-list/gateway-list.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AfterViewInit, Component, OnInit } from '@angular/core';
1+
import { AfterViewInit, Component } from '@angular/core';
22
import { GatewayService } from '@app/gateway/gateway.service';
33

44
@Component({

src/app/gateway/gateway-overview/gateway-tabs/gateway-map/gateway-map.component.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit {
1515
public gateways: Gateway[];
1616
public coordinateList = [];
1717
private gatewaySubscription: Subscription;
18+
private organizationChangeSubscription: Subscription;
1819
isLoadingResults = true;
1920

2021
constructor(
@@ -26,13 +27,15 @@ export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit {
2627
ngOnInit(): void {}
2728

2829
ngAfterViewInit() {
29-
this.gatewayService.organisationChangeSubject.subscribe((x) => {
30-
if (x) {
31-
this.getGatewayWith(x);
32-
} else {
33-
this.getGateways();
30+
this.organizationChangeSubscription = this.gatewayService.organisationChangeSubject.subscribe(
31+
(x) => {
32+
if (x) {
33+
this.getGatewayWith(x);
34+
} else {
35+
this.getGateways();
36+
}
3437
}
35-
});
38+
);
3639
if (this.gatewayService.selectedOrg) {
3740
this.getGatewayWith(this.gatewayService.selectedOrg);
3841
} else {
@@ -110,6 +113,6 @@ export class GatewayMapComponent implements OnInit, OnDestroy, AfterViewInit {
110113
ngOnDestroy() {
111114
// prevent memory leak by unsubscribing
112115
this.gatewaySubscription?.unsubscribe();
113-
// this.deleteDialogSubscription?.unsubscribe();
116+
this.organizationChangeSubscription.unsubscribe();
114117
}
115118
}
Lines changed: 148 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,148 @@
1-
import { ChirpstackGatewayService } from 'src/app/shared/services/chirpstack-gateway.service';
2-
import { TranslateService } from '@ngx-translate/core';
3-
import { Gateway, GatewayResponseMany } from '../gateway.model';
4-
import {
5-
faExclamationTriangle,
6-
faCheckCircle,
7-
} from '@fortawesome/free-solid-svg-icons';
8-
import moment from 'moment';
9-
import { Component, ViewChild, AfterViewInit, Input } from '@angular/core';
10-
import { MatPaginator, PageEvent } from '@angular/material/paginator';
11-
import { Observable, of as observableOf, Subject } from 'rxjs';
12-
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
13-
import { MeService } from '@shared/services/me.service';
14-
import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
15-
import { environment } from '@environments/environment';
16-
import { MatSort } from '@angular/material/sort';
17-
import { MatTableDataSource } from '@angular/material/table';
18-
import { tableSorter } from '@shared/helpers/table-sorting.helper';
19-
import { OrganizationAccessScope } from '@shared/enums/access-scopes';
20-
import { DefaultPageSizeOptions } from '@shared/constants/page.constants';
21-
22-
@Component({
23-
selector: 'app-gateway-table',
24-
templateUrl: './gateway-table.component.html',
25-
styleUrls: ['./gateway-table.component.scss'],
26-
})
27-
export class GatewayTableComponent implements AfterViewInit {
28-
@Input() organisationChangeSubject: Subject<any>;
29-
organizationId?: number;
30-
displayedColumns: string[] = [
31-
'name',
32-
'gateway-id',
33-
'location',
34-
'internalOrganizationName',
35-
'last-seen',
36-
'status',
37-
'menu',
38-
];
39-
data: Gateway[] = [];
40-
dataSource: MatTableDataSource<Gateway>;
41-
public pageSize = environment.tablePageSize;
42-
public pageSizeOptions = DefaultPageSizeOptions;
43-
44-
faExclamationTriangle = faExclamationTriangle;
45-
faCheckCircle = faCheckCircle;
46-
refetchIntervalId: NodeJS.Timeout;
47-
batteryStatusColor = 'green';
48-
batteryStatusPercentage = 50;
49-
resultsLength = 0;
50-
isLoadingResults = true;
51-
52-
@ViewChild(MatPaginator) paginator: MatPaginator;
53-
@ViewChild(MatSort) sort: MatSort;
54-
55-
constructor(
56-
private chirpstackGatewayService: ChirpstackGatewayService,
57-
public translate: TranslateService,
58-
private meService: MeService,
59-
private deleteDialogService: DeleteDialogService
60-
) {
61-
this.translate.use('da');
62-
moment.locale('da');
63-
}
64-
65-
ngAfterViewInit() {
66-
this.organisationChangeSubject.subscribe((x) => {
67-
this.organizationId = x;
68-
this.refresh();
69-
});
70-
this.refetchIntervalId = setInterval(() => this.refresh(), 60 * 1000)
71-
this.refresh();
72-
}
73-
74-
ngOnDestroy() {
75-
clearInterval(this.refetchIntervalId)
76-
}
77-
78-
private refresh() {
79-
this.getGateways().subscribe((data) => {
80-
data.result.forEach((gw) => {
81-
gw.canEdit = this.canEdit(gw.internalOrganizationId);
82-
});
83-
this.data = data.result;
84-
this.resultsLength = data.totalCount;
85-
this.isLoadingResults = false;
86-
this.dataSource = new MatTableDataSource(this.data);
87-
this.dataSource.sortingDataAccessor = tableSorter;
88-
this.dataSource.paginator = this.paginator;
89-
this.dataSource.sort = this.sort;
90-
});
91-
}
92-
93-
canEdit(internalOrganizationId: number): boolean {
94-
return this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, internalOrganizationId);
95-
}
96-
97-
private getGateways(): Observable<GatewayResponseMany> {
98-
const params = {
99-
limit: this.paginator.pageSize,
100-
offset: this.paginator.pageIndex * this.paginator.pageSize,
101-
};
102-
if (this.organizationId > 0) {
103-
params['organizationId'] = this.organizationId;
104-
}
105-
return this.chirpstackGatewayService.getMultiple(params);
106-
}
107-
108-
gatewayStatus(gateway: Gateway): boolean {
109-
return this.chirpstackGatewayService.isGatewayActive(gateway);
110-
}
111-
112-
lastActive(gateway: Gateway): string {
113-
if (gateway?.lastSeenAt) {
114-
const lastSeenAtUnixTimestamp = moment(gateway?.lastSeenAt).valueOf();
115-
const now = moment(new Date()).valueOf();
116-
return moment(Math.min(lastSeenAtUnixTimestamp, now)).fromNow();
117-
} else {
118-
return this.translate.instant('ACTIVITY.NEVER');
119-
}
120-
}
121-
122-
clickDelete(element) {
123-
this.deleteGateway(element.id);
124-
}
125-
126-
deleteGateway(id: string) {
127-
this.deleteDialogService.showSimpleDialog().subscribe((response) => {
128-
if (response) {
129-
this.chirpstackGatewayService.delete(id).subscribe((response) => {
130-
if (response.ok && response.body.success === true) {
131-
this.refresh();
132-
}
133-
});
134-
} else {
135-
console.error(response);
136-
}
137-
});
138-
}
139-
}
1+
import { ChirpstackGatewayService } from 'src/app/shared/services/chirpstack-gateway.service';
2+
import { TranslateService } from '@ngx-translate/core';
3+
import { Gateway, GatewayResponseMany } from '../gateway.model';
4+
import {
5+
faExclamationTriangle,
6+
faCheckCircle,
7+
} from '@fortawesome/free-solid-svg-icons';
8+
import moment from 'moment';
9+
import {
10+
Component,
11+
ViewChild,
12+
AfterViewInit,
13+
Input,
14+
OnDestroy,
15+
} from '@angular/core';
16+
import { MatPaginator, PageEvent } from '@angular/material/paginator';
17+
import { Observable, of as observableOf, Subject, Subscription } from 'rxjs';
18+
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
19+
import { MeService } from '@shared/services/me.service';
20+
import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
21+
import { environment } from '@environments/environment';
22+
import { MatSort } from '@angular/material/sort';
23+
import { MatTableDataSource } from '@angular/material/table';
24+
import { tableSorter } from '@shared/helpers/table-sorting.helper';
25+
import { OrganizationAccessScope } from '@shared/enums/access-scopes';
26+
import { DefaultPageSizeOptions } from '@shared/constants/page.constants';
27+
28+
@Component({
29+
selector: 'app-gateway-table',
30+
templateUrl: './gateway-table.component.html',
31+
styleUrls: ['./gateway-table.component.scss'],
32+
})
33+
export class GatewayTableComponent implements AfterViewInit, OnDestroy {
34+
@Input() organisationChangeSubject: Subject<any>;
35+
organizationId?: number;
36+
displayedColumns: string[] = [
37+
'name',
38+
'gateway-id',
39+
'location',
40+
'internalOrganizationName',
41+
'last-seen',
42+
'status',
43+
'menu',
44+
];
45+
data: Gateway[] = [];
46+
dataSource: MatTableDataSource<Gateway>;
47+
public pageSize = environment.tablePageSize;
48+
public pageSizeOptions = DefaultPageSizeOptions;
49+
50+
faExclamationTriangle = faExclamationTriangle;
51+
faCheckCircle = faCheckCircle;
52+
refetchIntervalId: NodeJS.Timeout;
53+
resultsLength = 0;
54+
isLoadingResults = true;
55+
private fetchSubscription: Subscription;
56+
57+
@ViewChild(MatPaginator) paginator: MatPaginator;
58+
@ViewChild(MatSort) sort: MatSort;
59+
60+
constructor(
61+
private chirpstackGatewayService: ChirpstackGatewayService,
62+
public translate: TranslateService,
63+
private meService: MeService,
64+
private deleteDialogService: DeleteDialogService
65+
) {
66+
this.translate.use('da');
67+
moment.locale('da');
68+
}
69+
70+
ngAfterViewInit() {
71+
this.fetchSubscription = this.organisationChangeSubject.subscribe((x) => {
72+
this.organizationId = x;
73+
this.refresh();
74+
});
75+
this.refetchIntervalId = setInterval(() => this.refresh(), 60 * 1000);
76+
this.refresh();
77+
}
78+
79+
ngOnDestroy() {
80+
clearInterval(this.refetchIntervalId);
81+
this.fetchSubscription.unsubscribe();
82+
}
83+
84+
private refresh() {
85+
this.getGateways().subscribe((data) => {
86+
data.result.forEach((gw) => {
87+
gw.canEdit = this.canEdit(gw.internalOrganizationId);
88+
});
89+
this.data = data.result;
90+
this.resultsLength = data.totalCount;
91+
this.isLoadingResults = false;
92+
this.dataSource = new MatTableDataSource(this.data);
93+
this.dataSource.sortingDataAccessor = tableSorter;
94+
this.dataSource.paginator = this.paginator;
95+
this.dataSource.sort = this.sort;
96+
});
97+
}
98+
99+
canEdit(internalOrganizationId: number): boolean {
100+
return this.meService.hasAccessToTargetOrganization(
101+
OrganizationAccessScope.GatewayWrite,
102+
internalOrganizationId
103+
);
104+
}
105+
106+
private getGateways(): Observable<GatewayResponseMany> {
107+
const params = {
108+
limit: this.paginator.pageSize,
109+
offset: this.paginator.pageIndex * this.paginator.pageSize,
110+
};
111+
if (this.organizationId > 0) {
112+
params['organizationId'] = this.organizationId;
113+
}
114+
return this.chirpstackGatewayService.getMultiple(params);
115+
}
116+
117+
gatewayStatus(gateway: Gateway): boolean {
118+
return this.chirpstackGatewayService.isGatewayActive(gateway);
119+
}
120+
121+
lastActive(gateway: Gateway): string {
122+
if (gateway?.lastSeenAt) {
123+
const lastSeenAtUnixTimestamp = moment(gateway?.lastSeenAt).valueOf();
124+
const now = moment(new Date()).valueOf();
125+
return moment(Math.min(lastSeenAtUnixTimestamp, now)).fromNow();
126+
} else {
127+
return this.translate.instant('ACTIVITY.NEVER');
128+
}
129+
}
130+
131+
clickDelete(element) {
132+
this.deleteGateway(element.id);
133+
}
134+
135+
deleteGateway(id: string) {
136+
this.deleteDialogService.showSimpleDialog().subscribe((response) => {
137+
if (response) {
138+
this.chirpstackGatewayService.delete(id).subscribe((response) => {
139+
if (response.ok && response.body.success === true) {
140+
this.refresh();
141+
}
142+
});
143+
} else {
144+
console.error(response);
145+
}
146+
});
147+
}
148+
}

0 commit comments

Comments
 (0)