From 6ca343384c794b4785016eac4973396849e4da15 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Fri, 17 Dec 2021 15:37:28 +0100 Subject: [PATCH 01/12] Revised permission system. Admin pages can still be accessed by url if only read --- .../organisation-detail.component.html | 13 +++- .../organisation-detail.component.ts | 12 +++- .../organisation-list.component.html | 4 +- .../organisation-list.component.ts | 22 ++++--- .../permission-detail.component.html | 8 +-- .../permission-detail.component.ts | 11 +++- .../permission-edit.component.ts | 13 ++-- .../permission-list.component.html | 4 +- .../permission-list.component.ts | 7 +- .../permission-tabel.component.ts | 5 +- src/app/admin/permission/permission.model.ts | 6 +- .../user-detail/user-detail.component.html | 4 +- .../user-detail/user-detail.component.ts | 9 ++- .../users/user-edit/user-edit.component.html | 4 +- .../users/user-edit/user-edit.component.ts | 9 ++- .../users/user-list/user-list.component.html | 4 +- .../users/user-list/user-list.component.ts | 12 ++-- .../application-detail.component.html | 5 +- .../application-detail.component.ts | 3 +- .../applications-list.component.html | 6 +- .../applications-list.component.ts | 7 +- .../applications-table.component.spec.ts | 10 +-- .../applications-table.component.ts | 3 +- .../bulk-import/bulk-import.component.html | 4 +- .../bulk-import/bulk-import.component.ts | 9 ++- .../datatarget-detail.component.html | 4 +- .../datatarget-detail.component.ts | 13 ++-- .../datatarget-edit.component.html | 65 ++++++++++--------- .../datatarget-edit.component.ts | 13 ++-- .../datatarget-list.component.html | 5 +- .../datatarget-list.component.ts | 13 ++-- .../datatarget-table.component.ts | 5 +- .../iot-device-detail.component.html | 7 +- .../iot-device-detail.component.ts | 5 +- .../iot-device-edit.component.html | 4 +- .../iot-device-edit.component.ts | 23 ++++--- .../iot-devices-table.component.ts | 5 +- .../device-model-detail.component.html | 5 +- .../device-model-detail.component.ts | 7 +- .../device-model-list.component.html | 5 +- .../device-model-list.component.ts | 7 +- .../device-model-table.component.ts | 3 +- .../gateway-detail.component.html | 4 +- .../gateway-detail.component.ts | 21 +++--- .../gateway-list/gateway-list.component.html | 5 +- .../gateway-list/gateway-list.component.ts | 5 +- .../gateway-table/gateway-table.component.ts | 3 +- .../organisation-dropdown.component.html | 4 +- .../organisation-dropdown.component.ts | 12 ++-- .../payload-decoder-detail.component.html | 5 +- .../payload-decoder-detail.component.ts | 3 +- .../payload-decoder-edit.component.html | 4 +- .../payload-decoder-edit.component.ts | 21 +++--- .../payload-decoder-list.component.html | 5 +- .../payload-decoder-list.component.ts | 11 +++- .../payload-decoder-table.component.html | 2 +- .../payload-decoder-table.component.ts | 3 +- .../device-profiles/device-profile.service.ts | 2 +- .../device-profiles-edit.component.html | 6 +- .../device-profiles-edit.component.ts | 3 +- .../device-profiles-list.component.ts | 6 +- .../profiles-list.component.html | 4 +- .../profiles-list/profiles-list.component.ts | 9 ++- .../service-profiles-edit.component.ts | 11 ++-- .../service-profiles-list.component.ts | 7 +- .../search-table/search-table.component.html | 10 +-- src/app/search/search.component.html | 4 +- src/app/search/search.component.ts | 3 + .../components/top-bar/top-bar.component.ts | 7 +- src/app/shared/enums/access-scopes.ts | 5 ++ src/app/shared/services/me.service.ts | 65 +++++++++++++------ .../shared-variable.service.ts | 21 ------ .../sigfox-device-types-edit.component.ts | 3 +- .../sigfox-groups-detail.component.html | 5 +- .../sigfox-groups-detail.component.ts | 6 +- .../sigfox-groups-edit.component.html | 4 +- .../sigfox-groups-edit.component.ts | 9 ++- .../sigfox-groups-list-item.component.ts | 3 +- .../sigfox-groups-list.component.html | 5 +- .../sigfox-groups-list.component.ts | 8 ++- src/assets/i18n/da.json | 11 ++-- 81 files changed, 432 insertions(+), 276 deletions(-) create mode 100644 src/app/shared/enums/access-scopes.ts diff --git a/src/app/admin/organisation/organisation-detail/organisation-detail.component.html b/src/app/admin/organisation/organisation-detail/organisation-detail.component.html index ced52e8af..0f7fac379 100644 --- a/src/app/admin/organisation/organisation-detail/organisation-detail.component.html +++ b/src/app/admin/organisation/organisation-detail/organisation-detail.component.html @@ -1,6 +1,13 @@
- +
@@ -34,4 +41,4 @@

- \ No newline at end of file + diff --git a/src/app/admin/organisation/organisation-detail/organisation-detail.component.ts b/src/app/admin/organisation/organisation-detail/organisation-detail.component.ts index 38cce3fba..737aa23e1 100644 --- a/src/app/admin/organisation/organisation-detail/organisation-detail.component.ts +++ b/src/app/admin/organisation/organisation-detail/organisation-detail.component.ts @@ -15,6 +15,8 @@ import { DropdownButton } from '@shared/models/dropdown-button.model'; import { ApplicationService } from '@applications/application.service'; import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-organisation-detail', @@ -41,6 +43,7 @@ export class OrganisationDetailComponent implements OnInit, OnChanges, OnDestroy id: number; subscription: Subscription; permissions: PermissionResponse[]; + canEdit: boolean; constructor( public translate: TranslateService, @@ -49,7 +52,8 @@ export class OrganisationDetailComponent implements OnInit, OnChanges, OnDestroy private permissionsService: PermissionService, private deleteDialogService: DeleteDialogService, private location: Location, - private titleService: Title + private titleService: Title, + private meService: MeService ) { } ngOnChanges(changes: SimpleChanges): void { @@ -71,8 +75,10 @@ export class OrganisationDetailComponent implements OnInit, OnChanges, OnDestroy .subscribe(translations => { this.backButton.label = translations['NAV.ORGANISATIONS']; this.dropdownButton.label = translations['ORGANISATION.DROPDOWN']; - this.titleService.setTitle(translations['TITLE.ORGANIZATION']) + this.titleService.setTitle(translations['TITLE.ORGANIZATION']); }); + + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); } ngOnDestroy() { @@ -119,6 +125,6 @@ export class OrganisationDetailComponent implements OnInit, OnChanges, OnDestroy } }); } - }) + }); } } diff --git a/src/app/admin/organisation/organisation-list/organisation-list.component.html b/src/app/admin/organisation/organisation-list/organisation-list.component.html index 9cba6082c..d1b269ec0 100644 --- a/src/app/admin/organisation/organisation-list/organisation-list.component.html +++ b/src/app/admin/organisation/organisation-list/organisation-list.component.html @@ -1,5 +1,5 @@ + [ctaLabel]="'ORGANISATION.FORM.CREATE-NEW-ORGANISATION' | translate" [ctaRouterLink]="'new-organisation'" [canEdit]="canEdit">
@@ -9,4 +9,4 @@
- \ No newline at end of file + diff --git a/src/app/admin/organisation/organisation-list/organisation-list.component.ts b/src/app/admin/organisation/organisation-list/organisation-list.component.ts index 3ce2a103b..0fe174476 100644 --- a/src/app/admin/organisation/organisation-list/organisation-list.component.ts +++ b/src/app/admin/organisation/organisation-list/organisation-list.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; -import { Sort } from '@shared/models/sort.model'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-organisation-list', @@ -9,15 +10,20 @@ import { Sort } from '@shared/models/sort.model'; styleUrls: ['./organisation-list.component.scss'], }) export class OrganisationListComponent implements OnInit { - - constructor(public translate: TranslateService, private titleService: Title) { + canEdit: boolean; + + constructor( + public translate: TranslateService, + private titleService: Title, + private meService: MeService + ) { translate.use('da'); } ngOnInit(): void { - this.translate.get(['TITLE.ORGANIZATION']) - .subscribe(translations => { - this.titleService.setTitle(translations['TITLE.ORGANIZATION']); - }); - } + this.translate.get(['TITLE.ORGANIZATION']).subscribe((translations) => { + this.titleService.setTitle(translations['TITLE.ORGANIZATION']); + }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); + } } diff --git a/src/app/admin/permission/permission-detail/permission-detail.component.html b/src/app/admin/permission/permission-detail/permission-detail.component.html index c7ae3ae5e..95b261592 100644 --- a/src/app/admin/permission/permission-detail/permission-detail.component.html +++ b/src/app/admin/permission/permission-detail/permission-detail.component.html @@ -1,6 +1,6 @@
+ [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeletePermission()" [canEdit]="canEdit">
@@ -33,13 +33,13 @@

{{ 'PERMISSION.DETAIL.APPLICATIONS' | translate }}

- + - +
- \ No newline at end of file + diff --git a/src/app/admin/permission/permission-detail/permission-detail.component.ts b/src/app/admin/permission/permission-detail/permission-detail.component.ts index 28a0ab3eb..072668ce8 100644 --- a/src/app/admin/permission/permission-detail/permission-detail.component.ts +++ b/src/app/admin/permission/permission-detail/permission-detail.component.ts @@ -11,6 +11,8 @@ import { DropdownButton } from '@shared/models/dropdown-button.model'; import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service'; import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -43,6 +45,7 @@ export class PermissionDetailComponent implements OnInit, OnChanges { subscription: Subscription; users: UserResponse[]; dropdownButton: DropdownButton; + canEdit: boolean; constructor( public translate: TranslateService, @@ -50,7 +53,8 @@ export class PermissionDetailComponent implements OnInit, OnChanges { private permissionService: PermissionService, private router: Router, private titleService: Title, - private deleteDialogService: DeleteDialogService + private deleteDialogService: DeleteDialogService, + private meService: MeService ) { } ngOnInit(): void { @@ -62,7 +66,7 @@ export class PermissionDetailComponent implements OnInit, OnChanges { label: '', editRouterLink: 'edit-permission', isErasable: true, - } + }; } this.translate.get(['NAV.PERMISSIONS', 'PERMISSION.DETAIL.DROPDOWN', 'TITLE.PERMISSION']) .subscribe(translations => { @@ -70,6 +74,7 @@ export class PermissionDetailComponent implements OnInit, OnChanges { this.dropdownButton.label = translations['PERMISSION.DETAIL.DROPDOWN']; this.titleService.setTitle(translations['TITLE.PERMISSION']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); } ngOnChanges(changes: SimpleChanges): void { @@ -95,7 +100,7 @@ export class PermissionDetailComponent implements OnInit, OnChanges { } }); } - }) + }); } onEditPermission() { diff --git a/src/app/admin/permission/permission-edit/permission-edit.component.ts b/src/app/admin/permission/permission-edit/permission-edit.component.ts index 327d85724..766c04856 100644 --- a/src/app/admin/permission/permission-edit/permission-edit.component.ts +++ b/src/app/admin/permission/permission-edit/permission-edit.component.ts @@ -213,7 +213,7 @@ export class PermissionEditComponent implements OnInit, OnDestroy { if ( response.type === PermissionType.Read || - response.type === PermissionType.Write + response.type === PermissionType.OrganizationApplicationAdmin ) { this.getApplications(this.permission.organizationId); this.permission.applicationIds = response.applications.map( @@ -252,12 +252,13 @@ export class PermissionEditComponent implements OnInit, OnDestroy { } allowedLevels() { - if (this.permission.level == PermissionType.GlobalAdmin) { + if (this.permission.level === PermissionType.GlobalAdmin) { return [PermissionType.GlobalAdmin]; } return [ - PermissionType.OrganizationAdmin, - PermissionType.Write, + PermissionType.OrganizationUserAdmin, + PermissionType.OrganizationApplicationAdmin, + PermissionType.OrganizationGatewayAdmin, PermissionType.Read, ]; } @@ -280,7 +281,7 @@ export class PermissionEditComponent implements OnInit, OnDestroy { isOrganizationApplicationPermission() { return ( - this.permission.level == + this.permission.level === PermissionType.OrganizationApplicationPermissions || this.isReadOrWrite() ); @@ -289,7 +290,7 @@ export class PermissionEditComponent implements OnInit, OnDestroy { isReadOrWrite(): boolean { return ( this.permission.level === PermissionType.Read || - this.permission.level === PermissionType.Write + this.permission.level === PermissionType.OrganizationApplicationAdmin ); } diff --git a/src/app/admin/permission/permission-list/permission-list.component.html b/src/app/admin/permission/permission-list/permission-list.component.html index e41723ac3..684ba789d 100644 --- a/src/app/admin/permission/permission-list/permission-list.component.html +++ b/src/app/admin/permission/permission-list/permission-list.component.html @@ -1,5 +1,5 @@ + [ctaLabel]="'FORM.CREATE-NEW-PERMISSION' | translate" [ctaRouterLink]="'new-permission'" [canEdit]="canEdit">
@@ -9,4 +9,4 @@
- \ No newline at end of file + diff --git a/src/app/admin/permission/permission-list/permission-list.component.ts b/src/app/admin/permission/permission-list/permission-list.component.ts index cec1432d5..4812055c0 100644 --- a/src/app/admin/permission/permission-list/permission-list.component.ts +++ b/src/app/admin/permission/permission-list/permission-list.component.ts @@ -6,6 +6,8 @@ import { PermissionService } from '../permission.service'; import { Sort } from '@shared/models/sort.model'; import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-permission-list', @@ -19,11 +21,13 @@ export class PermissionListComponent implements OnInit, OnChanges { public permissions: PermissionResponse[]; permissionSubscription: Subscription; + canEdit: boolean; constructor( public translate: TranslateService, private titleService: Title, - private permissionService: PermissionService + private permissionService: PermissionService, + private meService: MeService ) { translate.use('da'); } @@ -37,6 +41,7 @@ export class PermissionListComponent implements OnInit, OnChanges { .subscribe(translations => { this.titleService.setTitle(translations['TITLE.PERMISSION']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); } getPermissions() { diff --git a/src/app/admin/permission/permission-list/permission-tabel/permission-tabel.component.ts b/src/app/admin/permission/permission-list/permission-tabel/permission-tabel.component.ts index 0b93c8e21..79ff57d30 100644 --- a/src/app/admin/permission/permission-list/permission-tabel/permission-tabel.component.ts +++ b/src/app/admin/permission/permission-list/permission-tabel/permission-tabel.component.ts @@ -14,6 +14,7 @@ import { PermissionType, } from '../../permission.model'; import { PermissionService } from '../../permission.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-permission-tabel', @@ -110,10 +111,10 @@ export class PermissionTabelComponent implements AfterViewInit { } canAccess(element: PermissionResponse) { - if (element.type == PermissionType.GlobalAdmin) { + if (element.type === PermissionType.GlobalAdmin) { return this.meService.hasGlobalAdmin(); } - return this.meService.hasAdminAccessInTargetOrganization(element.organization.id); + return this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite, element.organization.id); } private refresh() { diff --git a/src/app/admin/permission/permission.model.ts b/src/app/admin/permission/permission.model.ts index 00ca3c2d2..b80ef7fd0 100644 --- a/src/app/admin/permission/permission.model.ts +++ b/src/app/admin/permission/permission.model.ts @@ -34,9 +34,11 @@ export interface PermissionGetManyResponse { export enum PermissionType { GlobalAdmin = 'GlobalAdmin', - OrganizationAdmin = 'OrganizationAdmin', - Write = 'Write', + OrganizationUserAdmin = 'OrganizationUserAdmin', + OrganizationGatewayAdmin = 'OrganizationGatewayAdmin', + OrganizationApplicationAdmin = 'OrganizationApplicationAdmin', Read = 'Read', + OrganizationPermission = 'OrganizationPermission', OrganizationApplicationPermissions = 'OrganizationApplicationPermissions', } diff --git a/src/app/admin/users/user-detail/user-detail.component.html b/src/app/admin/users/user-detail/user-detail.component.html index 57974557e..89edbc39e 100644 --- a/src/app/admin/users/user-detail/user-detail.component.html +++ b/src/app/admin/users/user-detail/user-detail.component.html @@ -1,6 +1,6 @@
+ [dropDownButton]="dropdownButton" [canEdit]="canEdit">
@@ -34,4 +34,4 @@

-
\ No newline at end of file + diff --git a/src/app/admin/users/user-detail/user-detail.component.ts b/src/app/admin/users/user-detail/user-detail.component.ts index a902b4767..a509f1082 100644 --- a/src/app/admin/users/user-detail/user-detail.component.ts +++ b/src/app/admin/users/user-detail/user-detail.component.ts @@ -10,6 +10,8 @@ import { Application } from '@applications/application.model'; import { OrganisationResponse } from '@app/admin/organisation/organisation.model'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { environment } from '@environments/environment'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-user-detail', @@ -43,12 +45,14 @@ export class UserDetailComponent implements OnInit, OnDestroy { dropdownButton: DropdownButton; id: number; subscription: Subscription; + canEdit: boolean; constructor( public translate: TranslateService, private route: ActivatedRoute, private userService: UserService, private router: Router, + private meService: MeService ) { } ngOnInit(): void { @@ -60,13 +64,14 @@ export class UserDetailComponent implements OnInit, OnDestroy { label: '', editRouterLink: 'edit-user', isErasable: false, - } + }; } this.translate.get(['NAV.USERS', 'USERS.DETAIL.DROPDOWN']) .subscribe(translations => { this.backButton.label = translations['NAV.USERS']; - this.dropdownButton.label = translations['USERS.DETAIL.DROPDOWN'] + this.dropdownButton.label = translations['USERS.DETAIL.DROPDOWN']; }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); } private getUser(id: number) { diff --git a/src/app/admin/users/user-edit/user-edit.component.html b/src/app/admin/users/user-edit/user-edit.component.html index 0d1b2e5dd..eed313831 100644 --- a/src/app/admin/users/user-edit/user-edit.component.html +++ b/src/app/admin/users/user-edit/user-edit.component.html @@ -1,4 +1,4 @@ - +
    @@ -53,4 +53,4 @@
-
\ No newline at end of file + diff --git a/src/app/admin/users/user-edit/user-edit.component.ts b/src/app/admin/users/user-edit/user-edit.component.ts index 5ac145bea..4f89b8739 100644 --- a/src/app/admin/users/user-edit/user-edit.component.ts +++ b/src/app/admin/users/user-edit/user-edit.component.ts @@ -10,6 +10,8 @@ import { Location } from '@angular/common'; import { PermissionType } from '@app/admin/permission/permission.model'; import { AuthService, CurrentUserInfoResponse } from '@auth/auth.service'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-user-edit', @@ -30,6 +32,7 @@ export class UserEditComponent implements OnInit { subscription: Subscription; isGlobalAdmin = false; isKombit: boolean; + canEdit: boolean; constructor( private translate: TranslateService, @@ -37,7 +40,8 @@ export class UserEditComponent implements OnInit { private userService: UserService, private location: Location, private authService: AuthService, - private sharedVariableService: SharedVariableService + private sharedVariableService: SharedVariableService, + private meService: MeService ) {} ngOnInit(): void { @@ -57,6 +61,7 @@ export class UserEditComponent implements OnInit { this.user.active = true; } this.amIGlobalAdmin(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); } private getUser(id: number) { @@ -66,7 +71,7 @@ export class UserEditComponent implements OnInit { this.user.id = response.id; this.user.active = response.active; this.user.globalAdmin = response.permissions.some( - (x) => x.type == PermissionType.GlobalAdmin + (x) => x.type === PermissionType.GlobalAdmin ); this.isKombit = response.nameId != null; // We cannot set the password. diff --git a/src/app/admin/users/user-list/user-list.component.html b/src/app/admin/users/user-list/user-list.component.html index 33c5d7767..eb93c46a4 100644 --- a/src/app/admin/users/user-list/user-list.component.html +++ b/src/app/admin/users/user-list/user-list.component.html @@ -1,5 +1,5 @@ + [ctaLabel]="'USERS.CREATE' | translate" [ctaRouterLink]="'new-user'" [canEdit]="canEdit">
@@ -8,4 +8,4 @@
- \ No newline at end of file + diff --git a/src/app/admin/users/user-list/user-list.component.ts b/src/app/admin/users/user-list/user-list.component.ts index 9563dffc7..17fc8848a 100644 --- a/src/app/admin/users/user-list/user-list.component.ts +++ b/src/app/admin/users/user-list/user-list.component.ts @@ -1,21 +1,23 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; +import { MeService } from '@shared/services/me.service'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.scss'], }) -export class UserListComponent { - constructor(private titleService: Title, private translate: TranslateService) {} +export class UserListComponent implements OnInit { + canEdit: boolean; + constructor(private titleService: Title, private translate: TranslateService, private meService: MeService) {} ngOnInit(): void { this.translate.get(['TITLE.USER']) .subscribe(translations => { this.titleService.setTitle(translations['TITLE.USER']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); } - - ngOnChanges(): void {} } diff --git a/src/app/applications/application-detail/application-detail.component.html b/src/app/applications/application-detail/application-detail.component.html index 1eac588db..cfadc3a29 100644 --- a/src/app/applications/application-detail/application-detail.component.html +++ b/src/app/applications/application-detail/application-detail.component.html @@ -1,7 +1,8 @@
+ [addDetailDowndown]="true" [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeleteApplication()" + [canEdit]="canEdit">
@@ -36,4 +37,4 @@

Detaljer

- \ No newline at end of file + diff --git a/src/app/applications/application-detail/application-detail.component.ts b/src/app/applications/application-detail/application-detail.component.ts index 009f3a63b..1c894f1ef 100644 --- a/src/app/applications/application-detail/application-detail.component.ts +++ b/src/app/applications/application-detail/application-detail.component.ts @@ -9,6 +9,7 @@ import { BackButton } from '@shared/models/back-button.model'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { MeService } from '@shared/services/me.service'; import { Subscription } from 'rxjs'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-application', @@ -55,7 +56,7 @@ export class ApplicationDetailComponent implements OnInit, OnDestroy { this.dropdownButton.label = translations['APPLICATION-TABLE-ROW.SHOW-OPTIONS']; this.titleService.setTitle(translations['TITLE.APPLICATION']); }); - this.canEdit = this.meService.canWriteInTargetOrganization(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } onDeleteApplication() { diff --git a/src/app/applications/applications-list/applications-list.component.html b/src/app/applications/applications-list/applications-list.component.html index 8e1ca4949..04da671d6 100644 --- a/src/app/applications/applications-list/applications-list.component.html +++ b/src/app/applications/applications-list/applications-list.component.html @@ -1,14 +1,12 @@ + [ctaRouterLink]="'new-application'" (updatePageLimit)="updatePageLimit($event)" [canEdit]="canEdit">
-
-
\ No newline at end of file + diff --git a/src/app/applications/applications-list/applications-list.component.ts b/src/app/applications/applications-list/applications-list.component.ts index 1e9bab84c..00b34de94 100644 --- a/src/app/applications/applications-list/applications-list.component.ts +++ b/src/app/applications/applications-list/applications-list.component.ts @@ -9,6 +9,8 @@ import { Application } from '@applications/application.model'; import { environment } from '@environments/environment'; import { TranslateService } from '@ngx-translate/core'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; +import { MeService } from '@shared/services/me.service'; @Component({ providers: [NavbarComponent], @@ -24,11 +26,13 @@ export class ApplicationsListComponent implements OnInit { public pageOffset = 0; public applications: Application[]; @Input() organizationId: number; + canEdit: boolean; constructor( public translate: TranslateService, private titleService: Title, - private globalService: SharedVariableService + private globalService: SharedVariableService, + private meService: MeService ) { translate.use('da'); } @@ -39,6 +43,7 @@ export class ApplicationsListComponent implements OnInit { this.titleService.setTitle(translations['TITLE.APPLICATION']); }); this.organizationId = this.globalService.getSelectedOrganisationId(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } updatePageLimit(limit: any) { diff --git a/src/app/applications/applications-list/applications-table/applications-table.component.spec.ts b/src/app/applications/applications-list/applications-table/applications-table.component.spec.ts index 60135d748..8406c40a4 100644 --- a/src/app/applications/applications-list/applications-table/applications-table.component.spec.ts +++ b/src/app/applications/applications-list/applications-table/applications-table.component.spec.ts @@ -1,20 +1,20 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ApplicationTableOtherComponent } from './applications-table.component +import { ApplicationsTableComponent } from './applications-table.component'; describe('ApplicationTableOtherComponent', () => { - let component: ApplicationTableOtherComponent; - let fixture: ComponentFixture; + let component: ApplicationsTableComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ApplicationTableOtherComponent ] + declarations: [ ApplicationsTableComponent ] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ApplicationTableOtherComponent); + fixture = TestBed.createComponent(ApplicationsTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/applications/applications-list/applications-table/applications-table.component.ts b/src/app/applications/applications-list/applications-table/applications-table.component.ts index 4da383044..1e132956b 100644 --- a/src/app/applications/applications-list/applications-table/applications-table.component.ts +++ b/src/app/applications/applications-list/applications-table/applications-table.component.ts @@ -10,6 +10,7 @@ import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dia import { MeService } from '@shared/services/me.service'; import { merge, Observable, of as observableOf } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; /** * @title Table retrieving data through HTTP @@ -43,7 +44,7 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit { ) { } ngOnInit() { - this.canEdit = this.meService.canWriteInTargetOrganization(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } ngAfterViewInit() { diff --git a/src/app/applications/bulk-import/bulk-import.component.html b/src/app/applications/bulk-import/bulk-import.component.html index 447b772e0..466dcaf36 100644 --- a/src/app/applications/bulk-import/bulk-import.component.html +++ b/src/app/applications/bulk-import/bulk-import.component.html @@ -1,4 +1,4 @@ - +
@@ -89,4 +89,4 @@
\ No newline at end of file +
--> diff --git a/src/app/applications/bulk-import/bulk-import.component.ts b/src/app/applications/bulk-import/bulk-import.component.ts index e28d2506f..e91e91e26 100644 --- a/src/app/applications/bulk-import/bulk-import.component.ts +++ b/src/app/applications/bulk-import/bulk-import.component.ts @@ -12,6 +12,8 @@ import { Papa } from 'ngx-papaparse'; import { Observable } from 'rxjs'; import { BulkImport } from './bulk-import.model'; import { BulkMapping } from './bulkMapping'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-bulk-import', @@ -31,11 +33,12 @@ export class BulkImportComponent implements OnInit { { name: 'generic-http-sample.csv', url: '../../../assets/docs/iotdevice_generichttp.csv' }, { name: 'lorawan-otaa-sample.csv', url: '../../../assets/docs/iotdevice_lorawan_otaa.csv' }, { name: 'lorawan-abp-sample.csv', url: '../../../assets/docs/iotdevice_lorawan_abp.csv' }, - ] + ]; download$: Observable; private bulkMapper = new BulkMapping(); public backButtonTitle: string; private applicationId; + canEdit: boolean; constructor( private papa: Papa, @@ -44,7 +47,8 @@ export class BulkImportComponent implements OnInit { private titleService: Title, private translate: TranslateService, private downloads: DownloadService, - private errorMessageService: ErrorMessageService + private errorMessageService: ErrorMessageService, + private meService: MeService ) { this.translate.use('da'); } @@ -55,6 +59,7 @@ export class BulkImportComponent implements OnInit { this.titleService.setTitle(translations['TITLE.BULKIMPORT']); }); this.applicationId = +this.route.snapshot.paramMap.get('id'); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } diff --git a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html index cc9829db2..2ac565937 100644 --- a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html +++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html @@ -1,6 +1,6 @@
+ [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeleteDatatarget()" [canEdit]="canEdit">
@@ -71,4 +71,4 @@

{{ 'DATATARGET.RELATIONS' | translate }}

-
\ No newline at end of file + diff --git a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts index 7af258890..adcb4d5f3 100644 --- a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts +++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts @@ -12,6 +12,8 @@ import { Datatarget } from '../datatarget.model'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { faArrowsAltH } from '@fortawesome/free-solid-svg-icons'; import { IotDevice } from '@applications/iot-devices/iot-device.model'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-datatarget-detail', @@ -28,6 +30,7 @@ export class DatatargetDetailComponent implements OnInit, OnDestroy { public dropdownButton: DropdownButton; arrowsAltH = faArrowsAltH; private applicationName: string; + canEdit: boolean; constructor( private route: ActivatedRoute, @@ -35,7 +38,8 @@ export class DatatargetDetailComponent implements OnInit, OnDestroy { private location: Location, private datatargetRelationServicer: PayloadDeviceDatatargetService, private datatargetService: DatatargetService, - public translate: TranslateService) { } + public translate: TranslateService, + private meService: MeService) { } ngOnInit(): void { const id: number = +this.route.snapshot.paramMap.get('datatargetId'); @@ -47,13 +51,14 @@ export class DatatargetDetailComponent implements OnInit, OnDestroy { label: '', editRouterLink: '../../datatarget-edit/' + id, isErasable: true, - } + }; } this.translate.get(['NAV.MY-DATATARGET', 'DATATARGET.SHOW-OPTIONS']) .subscribe(translations => { this.backButton.label = translations['NAV.MY-DATATARGET']; - this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS'] + this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS']; }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } getDatatarget(id: number) { @@ -65,7 +70,7 @@ export class DatatargetDetailComponent implements OnInit, OnDestroy { } private setBackButton(applicationId: number) { - this.backButton.routerLink = ['applications', applicationId.toString(), 'datatarget-list', this.applicationName ] + this.backButton.routerLink = ['applications', applicationId.toString(), 'datatarget-list', this.applicationName ]; } onDeleteDatatarget() { diff --git a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html index 4d4df069e..73725f599 100644 --- a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html +++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html @@ -1,6 +1,7 @@ + backButtonTitle="{{backButtonTitle}}" + title="{{title}}" + [canEdit]="canEdit">
@@ -11,9 +12,9 @@ - {{'DATATARGET.ADD-TO-OPENDATADK' | translate}} @@ -26,7 +27,7 @@ [ngClass]="{'is-invalid' : formFailedSubmit && errorFields.includes('name'), 'is-valid' : formFailedSubmit && !errorFields.includes('name')}"> - +
* @@ -38,17 +39,17 @@
* -
@@ -70,19 +71,19 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
{{'QUESTION.ADD-RELATIONS' | translate}} - - - - + + +
+ + + + - + @@ -125,15 +126,15 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}

{{'DATATARGET.DELETE' | translate}}

- - - - -
{{'QUESTION.DATATARGET.SELECT-DEVICES' | translate}} - -
-
+
{{'QUESTION.DATATARGET.SELECT-PAYLOADDECODER' | translate}} @@ -115,7 +116,7 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
{{payloadDecoder.name}} - +
- + +
+
- \ No newline at end of file + diff --git a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts index 968df9e23..fd669e739 100644 --- a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts +++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts @@ -21,6 +21,8 @@ import { OpendatadkDialogService } from '@shared/components/opendatadk-dialog/op import { OpendatadkService } from '@shared/services/opendatadk.service'; import { ScrollToTopService } from '@shared/services/scroll-to-top.service'; import { OpenDataDkDataset } from '../opendatadk/opendatadk-dataset.model'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-datatarget-edit', @@ -54,6 +56,7 @@ export class DatatargetEditComponent implements OnInit, OnDestroy { payloadDeviceDatatarget: PayloadDeviceDatatarget[]; newDynamic: any = {}; + canEdit: boolean; constructor( public translate: TranslateService, @@ -69,6 +72,7 @@ export class DatatargetEditComponent implements OnInit, OnDestroy { private opendatadkService: OpendatadkService, private opendatadkDialogService: OpendatadkDialogService, private scrollToTopService: ScrollToTopService, + private meService: MeService ) { translate.use('da'); } @@ -104,6 +108,7 @@ export class DatatargetEditComponent implements OnInit, OnDestroy { } this.getPayloadDecoders(); this.setDataSetExcists(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } addRow() { @@ -161,7 +166,7 @@ export class DatatargetEditComponent implements OnInit, OnDestroy { this.datatargetService.update(this.datatarget) .subscribe( (response: Datatarget) => { - this.datatarget = response; + this.datatarget = response; if (this.datatarget.openDataDkDataset != null) { this.datatarget.openDataDkDataset.acceptTerms = true; } @@ -288,7 +293,7 @@ export class DatatargetEditComponent implements OnInit, OnDestroy { } routeToDatatargets(): void { - this.router.navigate(['applications',this.applicationId.toString(),'datatarget-list', this.applicationNane]) + this.router.navigate(['applications', this.applicationId.toString(), 'datatarget-list', this.applicationNane]); } onCoordinateKey(event: any) { @@ -337,12 +342,12 @@ export class DatatargetEditComponent implements OnInit, OnDestroy { observer.next(true); } } - ) + ); } private showMailClient() { if (!this.datatarget.openDataDkDataset.url) { - this.datatarget.openDataDkDataset.url = this.datatargetService.getOpendataSharingApiUrl() + this.datatarget.openDataDkDataset.url = this.datatargetService.getOpendataSharingApiUrl(); } window.location.href = 'mailto:FG2V@kk.dk?subject=Oprettelse%20af%20datas%C3%A6t%20i%20OpenDataDK&body=K%C3%A6re%20Frans%0D%0A%0D%0AHermed%20fremsendes%20linket%20til%20DCAT%20kataloget%20%2C%20du%20bedes%20registrere%20p%C3%A5%20Open%20Data%20DK%20platformen.%0D%0A%0D%0ALink%3A ' + this.datatarget.openDataDkDataset.url; } diff --git a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html index bad1cea61..6675c9a6b 100644 --- a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html +++ b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html @@ -1,6 +1,7 @@ + [backButton]="backButton" + [canEdit]="canEdit">
@@ -10,4 +11,4 @@
-
\ No newline at end of file + diff --git a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts index 2f5236cd9..4d5c33f3e 100644 --- a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts +++ b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts @@ -5,10 +5,12 @@ import { Datatarget } from '../datatarget.model'; import { BackButton } from '@shared/models/back-button.model'; import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ - selector: 'a[app-datatarget-list]', + selector: 'app-datatarget-list', templateUrl: './datatarget-list.component.html', styleUrls: ['./datatarget-list.component.scss'] }) @@ -19,24 +21,27 @@ export class DatatargetListComponent implements OnInit { public backButton: BackButton = { label: '', routerLink: ''}; public datatarget: Datatarget; private applikationId: string; + canEdit: boolean; constructor( public translate: TranslateService, private titleService: Title, - private route: ActivatedRoute) { + private route: ActivatedRoute, + private meService: MeService) { translate.use('da'); } ngOnInit(): void { const applikationName: string = this.route.snapshot.paramMap.get('name'); this.applikationId = this.route.snapshot.paramMap.get('id'); - this.translate.get(["NAV.DATATARGET", "NAV.APPLICATIONS", "TITLE.DATATARGET"]) + this.translate.get(['NAV.DATATARGET', 'NAV.APPLICATIONS', 'TITLE.DATATARGET']) .subscribe((translate) => { this.title = translate['NAV.DATATARGET'] + ' - ' + applikationName; this.backButton.label = translate['NAV.APPLICATIONS']; this.titleService.setTitle(translate['TITLE.DATATARGET']); }); - this.setBackButton() + this.setBackButton(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } setBackButton() { diff --git a/src/app/applications/datatarget/datatarget-table/datatarget-table.component.ts b/src/app/applications/datatarget/datatarget-table/datatarget-table.component.ts index c3bbc0df6..0421e9334 100644 --- a/src/app/applications/datatarget/datatarget-table/datatarget-table.component.ts +++ b/src/app/applications/datatarget/datatarget-table/datatarget-table.component.ts @@ -11,6 +11,7 @@ import { MatTableDataSource } from '@angular/material/table'; import { environment } from '@environments/environment'; import { tableSorter } from '@shared/helpers/table-sorting.helper'; import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-datatarget-table', @@ -25,7 +26,7 @@ export class DatatargetTableComponent implements OnInit, AfterViewInit, OnDestro datatargets: Datatarget[]; resultsLength = 0; public canEdit = false; - @Input() isLoadingResults: boolean = true; + @Input() isLoadingResults = true; public pageSize = environment.tablePageSize; @Input() pageLimit: number; @@ -49,7 +50,7 @@ export class DatatargetTableComponent implements OnInit, AfterViewInit, OnDestro this.applicationId = +Number(this.route.parent.parent.snapshot.paramMap.get('id')); console.log(this.applicationId); this.getDatatarget(); - this.canEdit = this.meService.canWriteInTargetOrganization() + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } ngAfterViewInit() { 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 6e5b25b24..3ad5ef645 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 @@ -1,6 +1,7 @@
+ [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="clickDelete()" + [canEdit]="canEdit">
@@ -15,7 +16,7 @@ - +
- \ No newline at end of file + 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 0ce2b4763..e4e4cdb3b 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 @@ -15,6 +15,7 @@ import { IoTDeviceService } from '../iot-device.service'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { Title } from '@angular/platform-browser'; import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-iot-device', @@ -39,7 +40,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { public errorMessages: string[]; private deleteDialogSubscription: Subscription; public dropdownButton: DropdownButton; - public canStartDownlink = false; + public canEdit = false; // TODO: Få aktivt miljø? public baseUrl = environment.baseUrl; @@ -58,7 +59,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { ) { } ngOnInit(): void { - this.canStartDownlink = this.meService.canWriteInTargetOrganization(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); this.deviceId = +this.route.snapshot.paramMap.get('deviceId'); if (this.deviceId) { diff --git a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html index 46b96a94e..28008c978 100644 --- a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html +++ b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.html @@ -1,4 +1,4 @@ - +
@@ -226,4 +226,4 @@

{{'QUESTION.ABP' | translate}}

-
\ No newline at end of file + diff --git a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts index 831d3c6a6..d243b7455 100644 --- a/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts +++ b/src/app/applications/iot-devices/iot-device-edit/iot-device-edit.component.ts @@ -21,6 +21,8 @@ import { SharedVariableService } from '@shared/shared-variable/shared-variable.s import { Subscription } from 'rxjs'; import { IotDevice } from '../iot-device.model'; import { IoTDeviceService } from '../iot-device.service'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -49,6 +51,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { private serviceProfilesSubscription: Subscription; private deviceProfileSubscription: Subscription; private devicesProfileSubscription: Subscription; + canEdit: boolean; constructor( private route: ActivatedRoute, @@ -63,7 +66,8 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { private deviceModelService: DeviceModelService, private errorMessageService: ErrorMessageService, private scrollToTopService: ScrollToTopService, - private titleService: Title + private titleService: Title, + private meService: MeService ) { } ngOnInit(): void { @@ -86,6 +90,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { this.getServiceProfiles(); this.getDeviceProfiles(); this.getDeviceModels(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } public compare(o1: any, o2: any): boolean { @@ -95,9 +100,9 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { getDeviceModels() { this.deviceModelService.getMultiple( 1000, - 0, - "id", - "ASC", + 0, + 'id', + 'ASC', this.shareVariable.getSelectedOrganisationId() ).subscribe( (response) => { @@ -194,7 +199,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { private adjustModelBasedOnType() { if (this.iotDevice.deviceModelId === 0) { - this.iotDevice.deviceModelId = null + this.iotDevice.deviceModelId = null; } switch (this.iotDevice.type) { case DeviceType.GENERICHTTP: { @@ -253,11 +258,11 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { } handleError(error: HttpErrorResponse) { - if (error?.error?.message == "MESSAGE.OTAA-INFO-MISSING") { - this.errorFields = ["OTAAapplicationKey"]; + if (error?.error?.message === 'MESSAGE.OTAA-INFO-MISSING') { + this.errorFields = ['OTAAapplicationKey']; this.errorMessages = [error?.error?.message]; - } else if (error?.error?.message == "MESSAGE.ID-INVALID-OR-ALREADY-IN-USE") { - this.errorFields = ["devEUI"]; + } else if (error?.error?.message === 'MESSAGE.ID-INVALID-OR-ALREADY-IN-USE') { + this.errorFields = ['devEUI']; this.errorMessages = [error?.error?.message]; } else { const errorMessage: ErrorMessage = this.errorMessageService.handleErrorMessageWithFields(error); diff --git a/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts b/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts index ac550bb76..a4ad26719 100644 --- a/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts +++ b/src/app/applications/iot-devices/iot-devices-table/iot-devices-table.component.ts @@ -24,6 +24,7 @@ import { ReceivedMessageMetadata } from '@shared/models/received-message-metadat import { environment } from '@environments/environment'; import { startWith, switchMap, map, catchError } from 'rxjs/operators'; import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-iot-devices-table', @@ -65,7 +66,7 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { } ngOnInit() { - this.canEdit = this.meService.canWriteInTargetOrganization(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } ngAfterViewInit() { @@ -126,7 +127,7 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { } clickDelete(element: any) { - if (element.type == DeviceType.SIGFOX) { + if (element.type === DeviceType.SIGFOX) { this.showSigfoxDeleteDialog(); } else { this.deleteDialogService.showSimpleDialog().subscribe((response) => { diff --git a/src/app/device-model/device-model-detail/device-model-detail.component.html b/src/app/device-model/device-model-detail/device-model-detail.component.html index 0347b31cf..f43d3ab07 100644 --- a/src/app/device-model/device-model-detail/device-model-detail.component.html +++ b/src/app/device-model/device-model-detail/device-model-detail.component.html @@ -1,6 +1,7 @@
+ [addDetailDowndown]="true" [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="clickDelete()" + [canEdit]="canEdit">
@@ -28,4 +29,4 @@

{{ 'DEVICE-MODEL.HEADLINE' | translate }}

- \ No newline at end of file + diff --git a/src/app/device-model/device-model-detail/device-model-detail.component.ts b/src/app/device-model/device-model-detail/device-model-detail.component.ts index 3e238a17f..d3cdebc30 100644 --- a/src/app/device-model/device-model-detail/device-model-detail.component.ts +++ b/src/app/device-model/device-model-detail/device-model-detail.component.ts @@ -8,6 +8,8 @@ import { DropdownButton } from '@shared/models/dropdown-button.model'; import { Subscription } from 'rxjs'; import { DeviceModelService } from '../device-model.service'; import { DeviceModel } from '../device.model'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-device-model-detail', @@ -22,6 +24,7 @@ export class DeviceModelDetailComponent implements OnInit, OnDestroy { deleteDialogSubscription: Subscription; dropdownButton: DropdownButton; errorTitle: string; + canEdit: boolean; constructor( private translate: TranslateService, @@ -29,6 +32,7 @@ export class DeviceModelDetailComponent implements OnInit, OnDestroy { private deviceModelService: DeviceModelService, private deleteDialogservice: DeleteDialogService, private router: Router, + private meService: MeService ) { } ngOnInit(): void { @@ -39,7 +43,7 @@ export class DeviceModelDetailComponent implements OnInit, OnDestroy { label: '', editRouterLink: '/device-model/device-model-edit/' + deviceModelId, isErasable: true, - } + }; } this.translate.use('da'); this.translate.get(['DEVICE-MODEL.DETAIL-TITLE', 'DEVICE-MODEL.DEVICE-MODEL', 'DEVICE-MODEL.SHOW-OPTIONS', 'DEVICE-MODEL.DELETE-FAILED']) @@ -49,6 +53,7 @@ export class DeviceModelDetailComponent implements OnInit, OnDestroy { this.title = translations['DEVICE-MODEL.DETAIL-TITLE']; this.errorTitle = translations['DEVICE-MODEL.DELETE-FAILED']; }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } private getDeviceModel(id: number) { diff --git a/src/app/device-model/device-model-list/device-model-list.component.html b/src/app/device-model/device-model-list/device-model-list.component.html index 3e6f89a69..4c06fc42d 100644 --- a/src/app/device-model/device-model-list/device-model-list.component.html +++ b/src/app/device-model/device-model-list/device-model-list.component.html @@ -1,5 +1,6 @@ + [ctaRouterLink]="'device-model-edit'" + [canEdit]="canEdit">
@@ -9,4 +10,4 @@
- \ No newline at end of file + diff --git a/src/app/device-model/device-model-list/device-model-list.component.ts b/src/app/device-model/device-model-list/device-model-list.component.ts index 2d2676a9a..287897d00 100644 --- a/src/app/device-model/device-model-list/device-model-list.component.ts +++ b/src/app/device-model/device-model-list/device-model-list.component.ts @@ -2,6 +2,8 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-device-model-list', @@ -9,10 +11,12 @@ import { SharedVariableService } from '@shared/shared-variable/shared-variable.s styleUrls: ['./device-model-list.component.scss'] }) export class DeviceModelListComponent implements OnInit { + canEdit: boolean; constructor( private translate: TranslateService, - private titleService: Title + private titleService: Title, + private meService: MeService ) { translate.use('da'); } @@ -22,6 +26,7 @@ export class DeviceModelListComponent implements OnInit { .subscribe(translations => { this.titleService.setTitle(translations['TITLE.DEVICEMODEL']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } } diff --git a/src/app/device-model/device-model-table/device-model-table.component.ts b/src/app/device-model/device-model-table/device-model-table.component.ts index 62d9fac29..b4529ae10 100644 --- a/src/app/device-model/device-model-table/device-model-table.component.ts +++ b/src/app/device-model/device-model-table/device-model-table.component.ts @@ -18,6 +18,7 @@ import { merge, Observable, of as observableOf, Subscription } from 'rxjs'; import { startWith, switchMap, map, catchError } from 'rxjs/operators'; import { DeviceModelService } from '../device-model.service'; import { DeviceModel, DeviceModelResponse } from '../device.model'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-device-model-table', @@ -45,7 +46,7 @@ export class DeviceModelTableComponent implements OnInit, AfterViewInit { ) {} ngOnInit(): void { - this.canEdit = this.meService.canWriteInTargetOrganization() + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); this.translateService .get(['DEVICE-MODEL.DELETE-FAILED']) .subscribe((translations) => { diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.html b/src/app/gateway/gateway-detail/gateway-detail.component.html index f6981726c..01f4d957b 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.html +++ b/src/app/gateway/gateway-detail/gateway-detail.component.html @@ -1,6 +1,6 @@
+ [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeleteGateway()" [canEdit]="canEdit">
@@ -78,4 +78,4 @@

{{ 'GATEWAY.STATS' | translate }}

-
\ No newline at end of file + diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.ts b/src/app/gateway/gateway-detail/gateway-detail.component.ts index b9e750079..b3230041e 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.ts +++ b/src/app/gateway/gateway-detail/gateway-detail.component.ts @@ -11,6 +11,7 @@ import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dia import { MeService } from '@shared/services/me.service'; import { environment } from '@environments/environment'; import { DropdownButton } from '@shared/models/dropdown-button.model'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-gateway-detail', @@ -34,6 +35,7 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit private deleteDialogSubscription: Subscription; public dropdownButton: DropdownButton; isLoadingResults = true; + canEdit: boolean; constructor( private gatewayService: ChirpstackGatewayService, @@ -47,11 +49,10 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit ngOnInit(): void { this.translate.use('da'); this.id = this.route.snapshot.paramMap.get('id'); - this.translate.get(['NAV.LORA-GATEWAYS']) - .subscribe(translations => { - this.backButton.label = translations['NAV.LORA-GATEWAYS']; - } - ); + this.translate.get(['NAV.LORA-GATEWAYS']).subscribe((translations) => { + this.backButton.label = translations['NAV.LORA-GATEWAYS']; + }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, this.gateway.internalOrganizationId); } ngAfterViewInit() { @@ -75,7 +76,7 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit this.gatewayService.get(id).subscribe((result: any) => { result.gateway.tagsString = JSON.stringify(result.gateway.tags); this.gateway = result.gateway; - this.gateway.canEdit = this.canEdit(); + this.gateway.canEdit = this.canEdit; this.gatewayStats = result.stats; this.gatewayStats.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); this.dataSource.data = this.gatewayStats; @@ -87,22 +88,18 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit } setDropdownButton() { - this.dropdownButton = this.canEdit() ? { + this.dropdownButton = this.canEdit ? { label: 'LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS', editRouterLink: '../../gateway-edit/' + this.id, isErasable: true, } : 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']; } ); } - canEdit(): boolean { - return this.meService.canWriteInTargetOrganization(this.gateway.internalOrganizationId); - } - onDeleteGateway() { this.deleteDialogSubscription = this.deleteDialogService.showSimpleDialog().subscribe( (response) => { diff --git a/src/app/gateway/gateway-list/gateway-list.component.html b/src/app/gateway/gateway-list/gateway-list.component.html index 8c92e32f7..9dd32d932 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.html +++ b/src/app/gateway/gateway-list/gateway-list.component.html @@ -1,5 +1,6 @@ + [ctaLabel]="'FORM.CREATE-NEW-LORA-GATEWAY' | translate" [ctaRouterLink]="'gateway-edit'" + [canEdit]="canEdit">
@@ -22,4 +23,4 @@
-
\ No newline at end of file + diff --git a/src/app/gateway/gateway-list/gateway-list.component.ts b/src/app/gateway/gateway-list/gateway-list.component.ts index d022dd4fe..89ef5a088 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.ts +++ b/src/app/gateway/gateway-list/gateway-list.component.ts @@ -11,6 +11,7 @@ import { MeService } from '@shared/services/me.service'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -42,6 +43,7 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { public pageTotal: number; organisationId: number; organisationChangeSubject: Subject = new Subject(); + canEdit: boolean; constructor( public translate: TranslateService, @@ -61,6 +63,7 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { .subscribe(translations => { this.titleService.setTitle(translations['TITLE.LORAWAN-GATEWAY']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite); } ngOnChanges() { @@ -177,7 +180,7 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { setCanEdit() { this.gateways.forEach( (gateway) => { - gateway.canEdit = this.meService.canWriteInTargetOrganization(gateway.internalOrganizationId); + gateway.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, gateway.internalOrganizationId); } ); } diff --git a/src/app/gateway/gateway-table/gateway-table.component.ts b/src/app/gateway/gateway-table/gateway-table.component.ts index 20211329f..aeae7b691 100644 --- a/src/app/gateway/gateway-table/gateway-table.component.ts +++ b/src/app/gateway/gateway-table/gateway-table.component.ts @@ -16,6 +16,7 @@ import { environment } from '@environments/environment'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { tableSorter } from '@shared/helpers/table-sorting.helper'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-gateway-table', @@ -84,7 +85,7 @@ export class GatewayTableComponent implements AfterViewInit { } canEdit(internalOrganizationId: number): boolean { - return this.meService.canWriteInTargetOrganization(internalOrganizationId); + return this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, internalOrganizationId); } private getGateways(): Observable { diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html index 566b42df7..85bf40722 100644 --- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html +++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html @@ -25,7 +25,7 @@ -
+ - \ No newline at end of file + diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts index 3d73055b3..806499a75 100644 --- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts +++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts @@ -6,6 +6,8 @@ import { UserResponse } from '@app/admin/users/user.model'; import { faExchangeAlt, faLayerGroup, faUsers, faIdBadge, faToolbox, faBurn } from '@fortawesome/free-solid-svg-icons'; import { TranslateService } from '@ngx-translate/core'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-organisation-dropdown', @@ -15,7 +17,7 @@ import { SharedVariableService } from '@shared/shared-variable/shared-variable.s export class OrganisationDropdownComponent implements OnInit { public organisations: Organisation[]; public user: UserResponse; - public isOrgAdmin = false; + public isUserAdmin = false; public isGlobalAdmin = false; faExchangeAlt = faExchangeAlt; @@ -30,6 +32,7 @@ export class OrganisationDropdownComponent implements OnInit { private sharedVariableService: SharedVariableService, public translate: TranslateService, private route: Router, + private meService: MeService ) { } ngOnInit(): void { @@ -54,15 +57,16 @@ export class OrganisationDropdownComponent implements OnInit { } private setLocalPermissionCheck(orgId: number) { - this.isOrgAdmin = this.user?.permissions?.some(x => x.type == PermissionType.OrganizationAdmin && x.organization.id === +orgId); + this.isUserAdmin = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite); this.isGlobalAdmin = this.user?.permissions?.some( permission => permission.type === PermissionType.GlobalAdmin); } public onChange(organizationId: string) { this.sharedVariableService.setValue(+organizationId); this.setLocalPermissionCheck(+organizationId); - this.route.navigateByUrl('/', {skipLocationChange: true}).then(()=> - this.route.navigate(['applications'])); + this.route + .navigateByUrl('/', { skipLocationChange: true }) + .then(() => this.route.navigate(['applications'])); } setSelectedOrganisation(value) { diff --git a/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.html b/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.html index ed02e0818..c9b29cc68 100644 --- a/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.html +++ b/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.html @@ -1,6 +1,7 @@
+ [addDetailDowndown]="true" [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeletePayload()" + [canEdit]="canEdit">
@@ -32,4 +33,4 @@

{{ 'PAYLOAD-DECODER.IOT-DEVICES' | translate }}

-
\ No newline at end of file + diff --git a/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.ts b/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.ts index 55283ea7a..2bd8a0d67 100644 --- a/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.ts +++ b/src/app/payload-decoder/payload-decoder-detail/payload-decoder-detail.component.ts @@ -8,6 +8,7 @@ import { PayloadDecoder, PayloadDecoderBodyResponse } from '@payload-decoder/pay import { MeService } from '@shared/services/me.service'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -57,7 +58,7 @@ export class PayloadDecoderDetailComponent implements OnInit, OnDestroy { } canEdit() { - this.payloadDecoder.canEdit = this.meService.canWriteInTargetOrganization(this.payloadDecoder?.organizationId); + this.payloadDecoder.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite, this.payloadDecoder?.organizationId); } private getPayloadDecoder(id: number) { diff --git a/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.html b/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.html index 9fb59fb46..30df23d57 100644 --- a/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.html +++ b/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.html @@ -1,4 +1,4 @@ - +
@@ -91,4 +91,4 @@
-
\ No newline at end of file + diff --git a/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.ts b/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.ts index cf7462e0a..c18f82549 100644 --- a/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.ts +++ b/src/app/payload-decoder/payload-decoder-edit/payload-decoder-edit.component.ts @@ -22,6 +22,8 @@ import { SaveSnackService } from '@shared/services/save-snack.service'; import { ErrorMessageService } from '@shared/error-message.service'; import { ScrollToTopService } from '@shared/services/scroll-to-top.service'; import { environment } from '@environments/environment'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-payload-decoder-edit', @@ -64,6 +66,7 @@ export class PayloadDecoderEditComponent implements OnInit { public pageTotal: number; public pageOffset = 0; public deviceSubscription: Subscription; + canEdit: boolean; constructor( private translate: TranslateService, @@ -78,6 +81,7 @@ export class PayloadDecoderEditComponent implements OnInit { private saveSnackService: SaveSnackService, private errorMessageService: ErrorMessageService, private scrollToTopService: ScrollToTopService, + private meService: MeService ) { } ngOnInit(): void { @@ -116,11 +120,12 @@ export class PayloadDecoderEditComponent implements OnInit { this.sharedVariableService.getValue().subscribe((organisationId) => { this.getApplications(organisationId); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } setBackButtonLink(payloadDecoderId: number) { if (payloadDecoderId) { - this.backButton.routerLink = ['payload-decoder','payload-decoder-detail', this.id.toString()]; + this.backButton.routerLink = ['payload-decoder', 'payload-decoder-detail', this.id.toString()]; } else { this.backButton.routerLink = ['payload-decoder']; } @@ -136,25 +141,25 @@ export class PayloadDecoderEditComponent implements OnInit { } testPayloadFunction() { - this.errorMessages = null + this.errorMessages = null; this.testPayloadDecoder.code = this.payloadDecoderBody; try { this.testPayloadDecoder.iotDeviceJsonString = JSON.parse(this.metadata); } catch (err) { // Allow the empty string as a valid input if (this.isMetadataDefaultOrEmpty()) { - this.testPayloadDecoder.iotDeviceJsonString = JSON.parse("{}") + this.testPayloadDecoder.iotDeviceJsonString = JSON.parse('{}'); } else { - this.errorFields = ["metadata"]; + this.errorFields = ['metadata']; this.errorMessages = [this.metadataInvalidJSONMessage]; this.formFailedSubmit = true; - return; + return; } } try { this.testPayloadDecoder.rawPayloadJsonString = JSON.parse(this.payloadData); - }catch (err) { - this.errorFields = ["payload"]; + } catch (err) { + this.errorFields = ['payload']; this.errorMessages = [this.payloadInvalidJSONMessage]; this.formFailedSubmit = true; return; @@ -172,7 +177,7 @@ export class PayloadDecoderEditComponent implements OnInit { } private isMetadataDefaultOrEmpty() { - return this.metadata.trim() == "" || this.metadata.split("\n").every(x => x.trim().startsWith("//")); + return this.metadata.trim() === '' || this.metadata.split('\n').every(x => x.trim().startsWith('//')); } getCurrentOrganisationId(): number { diff --git a/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.html b/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.html index 5a4e84de7..02895820f 100644 --- a/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.html +++ b/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.html @@ -1,5 +1,6 @@ + [ctaRouterLink]="'payload-decoder-edit'" + [canEdit]="canEdit">
@@ -9,4 +10,4 @@
- \ No newline at end of file + diff --git a/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.ts b/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.ts index 5fba749e5..cb850a1d9 100644 --- a/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.ts +++ b/src/app/payload-decoder/payload-decoder-list/payload-decoder-list.component.ts @@ -2,6 +2,8 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; import { Sort } from '@shared/models/sort.model'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-payload-decoder-list', @@ -9,19 +11,22 @@ import { Sort } from '@shared/models/sort.model'; styleUrls: ['./payload-decoder-list.component.scss'] }) export class PayloadDecoderListComponent implements OnInit { + canEdit: boolean; constructor( public translate: TranslateService, - private titleService: Title + private titleService: Title, + private meService: MeService ) { - translate.use('da') + translate.use('da'); } ngOnInit(): void { this.translate.get(['TITLE.PAYLOADDECODER']) .subscribe(translations => { this.titleService.setTitle(translations['TITLE.PAYLOADDECODER']); - }); + }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } } diff --git a/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.html b/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.html index 075fcf627..f385794fd 100644 --- a/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.html +++ b/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.html @@ -69,4 +69,4 @@ showFirstLastButtons> - \ No newline at end of file + diff --git a/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.ts b/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.ts index 2b4c8cea2..1e1d4f071 100644 --- a/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.ts +++ b/src/app/payload-decoder/payload-decoder-list/payload-decoder-table/payload-decoder-table.component.ts @@ -18,6 +18,7 @@ import { Organisation } from '@app/admin/organisation/organisation.model'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service'; import { TranslateService } from '@ngx-translate/core'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-payload-decoder-table', @@ -95,7 +96,7 @@ export class PayloadDecoderTableComponent } getCanEdit(organizationId: number) { - return this.meService.canWriteInTargetOrganization(organizationId); + return this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite, organizationId); } public filterByOrgId(event: number) { diff --git a/src/app/profiles/device-profiles/device-profile.service.ts b/src/app/profiles/device-profiles/device-profile.service.ts index 7d69a2630..a4e10f3ee 100644 --- a/src/app/profiles/device-profiles/device-profile.service.ts +++ b/src/app/profiles/device-profiles/device-profile.service.ts @@ -42,7 +42,7 @@ export class DeviceProfileService { return response; } ) - );; + ); } getMultiple(): Observable { diff --git a/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.html b/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.html index 2c647d18f..6595db403 100644 --- a/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.html +++ b/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.html @@ -224,12 +224,12 @@

{{ 'PROFILES.DEVICE_PROFILE.DETAILS' | translate }}

- \ No newline at end of file + diff --git a/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.ts b/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.ts index c63fe56a0..2674c83ca 100644 --- a/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.ts +++ b/src/app/profiles/device-profiles/device-profiles-edit/device-profiles-edit.component.ts @@ -9,6 +9,7 @@ import { MeService } from '@shared/services/me.service'; import { Subscription } from 'rxjs'; import { DeviceProfile } from '../device-profile.model'; import { DeviceProfileService } from '../device-profile.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-device-profiles-edit', @@ -67,7 +68,7 @@ export class DeviceProfilesEditComponent implements OnInit, OnDestroy { canEdit() { if (this.deviceProfile.organizationID) { - this.deviceProfile.canEdit = this.meService.canWriteInTargetOrganization(this.deviceProfile.internalOrganizationId); + this.deviceProfile.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite, this.deviceProfile.internalOrganizationId); } else { this.deviceProfile.canEdit = true; } diff --git a/src/app/profiles/device-profiles/device-profiles-list/device-profiles-list.component.ts b/src/app/profiles/device-profiles/device-profiles-list/device-profiles-list.component.ts index 04bf5c03f..0c1576929 100644 --- a/src/app/profiles/device-profiles/device-profiles-list/device-profiles-list.component.ts +++ b/src/app/profiles/device-profiles/device-profiles-list/device-profiles-list.component.ts @@ -9,6 +9,7 @@ import { SharedVariableService } from '@shared/shared-variable/shared-variable.s import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { environment } from '@environments/environment'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-device-profiles-list', @@ -30,7 +31,6 @@ export class DeviceProfilesListComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private deviceProfileService: DeviceProfileService, private meService: MeService, - private sharedVariableService: SharedVariableService, private deleteDialogService: DeleteDialogService, private translateService: TranslateService ) { } @@ -57,13 +57,13 @@ export class DeviceProfilesListComponent implements OnInit, OnDestroy { } canCreate() { - return this.sharedVariableService.getHasWritePermission() + return this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } setCanEdit() { this.deviceProfiles.forEach( (deviceProfile) => { - deviceProfile.canEdit = this.meService.canWriteInTargetOrganization(deviceProfile.internalOrganizationId); + deviceProfile.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite, deviceProfile.internalOrganizationId); } ); } diff --git a/src/app/profiles/profiles-list/profiles-list.component.html b/src/app/profiles/profiles-list/profiles-list.component.html index 01ed1b057..f51e2b1fd 100644 --- a/src/app/profiles/profiles-list/profiles-list.component.html +++ b/src/app/profiles/profiles-list/profiles-list.component.html @@ -1,4 +1,4 @@ - +
@@ -10,4 +10,4 @@
- \ No newline at end of file + diff --git a/src/app/profiles/profiles-list/profiles-list.component.ts b/src/app/profiles/profiles-list/profiles-list.component.ts index 99dca55b7..adff80882 100644 --- a/src/app/profiles/profiles-list/profiles-list.component.ts +++ b/src/app/profiles/profiles-list/profiles-list.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-profiles-list', @@ -8,11 +10,13 @@ import { TranslateService } from '@ngx-translate/core'; styleUrls: ['./profiles-list.component.scss'] }) export class ProfilesListComponent implements OnInit { + canEdit: boolean; constructor( public translate: TranslateService, - private titleService: Title - ) { + private titleService: Title, + private meService: MeService + ) { translate.use('da'); } @@ -21,6 +25,7 @@ export class ProfilesListComponent implements OnInit { .subscribe(translations => { this.titleService.setTitle(translations['TITLE.LORAWAN-PROFILE']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } } diff --git a/src/app/profiles/service-profiles/service-profiles-edit/service-profiles-edit.component.ts b/src/app/profiles/service-profiles/service-profiles-edit/service-profiles-edit.component.ts index eef7d8999..b74458d9c 100644 --- a/src/app/profiles/service-profiles/service-profiles-edit/service-profiles-edit.component.ts +++ b/src/app/profiles/service-profiles/service-profiles-edit/service-profiles-edit.component.ts @@ -9,6 +9,8 @@ import { FormGroup } from '@angular/forms'; import { BackButton } from '@shared/models/back-button.model'; import { ServiceProfileService } from '../service-profile.service'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -41,6 +43,7 @@ export class ServiceProfilesEditComponent implements OnInit { private sharedVariableService: SharedVariableService, private serviceProfileService: ServiceProfileService, private location: Location, + private meService: MeService ) { } @@ -56,7 +59,7 @@ export class ServiceProfilesEditComponent implements OnInit { if (this.id) { this.getServiceProfile(this.id); } - this.canEdit = this.sharedVariableService.getHasAnyWritePermission(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } private getServiceProfile(id: string) { @@ -93,9 +96,9 @@ export class ServiceProfilesEditComponent implements OnInit { } private showError(error: HttpErrorResponse) { - if (error.status == 403) { - this.errorMessages = ["Forbudt"]; - this.errorFields = [] + if (error.status === 403) { + this.errorMessages = ['Forbudt']; + this.errorFields = []; } else { this.errorFields = []; this.errorMessage = ''; diff --git a/src/app/profiles/service-profiles/service-profiles-list/service-profiles-list.component.ts b/src/app/profiles/service-profiles/service-profiles-list/service-profiles-list.component.ts index 538fc8e7b..ab658c2c9 100644 --- a/src/app/profiles/service-profiles/service-profiles-list/service-profiles-list.component.ts +++ b/src/app/profiles/service-profiles/service-profiles-list/service-profiles-list.component.ts @@ -11,6 +11,8 @@ import { SharedVariableService } from '@shared/shared-variable/shared-variable.s import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { environment } from '@environments/environment'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-service-profiles-list', @@ -34,6 +36,7 @@ export class ServiceProfilesListComponent implements OnInit, OnDestroy { private sharedVariableService: SharedVariableService, private deleteDialogService: DeleteDialogService, private translateService: TranslateService, + private meService: MeService ) { } ngOnInit() { @@ -50,12 +53,12 @@ export class ServiceProfilesListComponent implements OnInit, OnDestroy { .getMultiple() .subscribe((result: ServiceProfileResponseMany) => { this.serviceProfiles = result.result; - this.setCanEdit() + this.setCanEdit(); }); } canCreate() { - return this.sharedVariableService.getHasAnyWritePermission(); + return this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } setCanEdit() { diff --git a/src/app/search/search-table/search-table.component.html b/src/app/search/search-table/search-table.component.html index 784831a1c..0549e5a85 100644 --- a/src/app/search/search-table/search-table.component.html +++ b/src/app/search/search-table/search-table.component.html @@ -88,11 +88,11 @@

- - \ No newline at end of file + diff --git a/src/app/search/search.component.html b/src/app/search/search.component.html index 684d4b5a2..aa077db6a 100644 --- a/src/app/search/search.component.html +++ b/src/app/search/search.component.html @@ -1,4 +1,4 @@ - +
@@ -8,4 +8,4 @@
- \ No newline at end of file + diff --git a/src/app/search/search.component.ts b/src/app/search/search.component.ts index 4d2d4edd1..a7ea56914 100644 --- a/src/app/search/search.component.ts +++ b/src/app/search/search.component.ts @@ -8,6 +8,9 @@ import { TranslateService } from '@ngx-translate/core'; styleUrls: ['./search.component.scss'], }) export class SearchComponent implements OnInit { + // TODO: Is search component used, and why does it even have a top bar? + canEdit = false; + constructor( public translate: TranslateService, private route: ActivatedRoute diff --git a/src/app/shared/components/top-bar/top-bar.component.ts b/src/app/shared/components/top-bar/top-bar.component.ts index f5a58fe38..fad10d8f9 100644 --- a/src/app/shared/components/top-bar/top-bar.component.ts +++ b/src/app/shared/components/top-bar/top-bar.component.ts @@ -15,7 +15,6 @@ import { PayloadDecoder } from '@payload-decoder/payload-decoder.model'; import { PermissionResponse } from '@app/admin/permission/permission.model'; import { UserResponse } from '@app/admin/users/user.model'; import { DropdownButton } from '@shared/models/dropdown-button.model'; -import { MeService } from '@shared/services/me.service'; @Component({ selector: 'app-top-bar', @@ -50,15 +49,14 @@ export class TopBarComponent implements OnInit { @Output() deleteSelectedInDropdown = new EventEmitter(); @Input() addDetailDowndown: boolean; @Input() dropDownButton: DropdownButton; - public canEdit = false; + @Input() canEdit = false; faSearch = faSearch; constructor( public translate: TranslateService, private location: Location, - private router: Router, - private meService: MeService + private router: Router ) { translate.use('da'); } @@ -68,7 +66,6 @@ export class TopBarComponent implements OnInit { if (this.data) { this.subTitle = this.data.name; } - this.canEdit = this.meService.canWriteInTargetOrganization() } diff --git a/src/app/shared/enums/access-scopes.ts b/src/app/shared/enums/access-scopes.ts new file mode 100644 index 000000000..153a64661 --- /dev/null +++ b/src/app/shared/enums/access-scopes.ts @@ -0,0 +1,5 @@ +export enum OrganizationAccessScope { + ApplicationWrite, + GatewayWrite, + UserAdministrationWrite, +} diff --git a/src/app/shared/services/me.service.ts b/src/app/shared/services/me.service.ts index 6ac54c75c..4f69151b1 100644 --- a/src/app/shared/services/me.service.ts +++ b/src/app/shared/services/me.service.ts @@ -1,7 +1,8 @@ import { Injectable } from '@angular/core'; -import { PermissionType } from '@app/admin/permission/permission.model'; +import { PermissionType, PermissionResponse } from '@app/admin/permission/permission.model'; import { UserResponse } from '@app/admin/users/user.model'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Injectable({ providedIn: 'root', @@ -11,28 +12,54 @@ export class MeService { constructor(private sharedVariableService: SharedVariableService) {} - canWriteInTargetOrganization(id: number = this.sharedVariableService.getSelectedOrganisationId()): boolean { - const userInfo = this.sharedVariableService.getUserInfo(); - return userInfo.user.permissions.some((permission) => { - return ( + hasAccessToTargetOrganization( + scope: OrganizationAccessScope, + id: number = this.sharedVariableService.getSelectedOrganisationId() + ): boolean { + const { permissions } = this.sharedVariableService.getUserInfo().user; + + if (permissions.some((p) => p.type === PermissionType.GlobalAdmin)) { + return true; + } + + let canWriteCallback: (p: PermissionResponse) => boolean; + + switch (scope) { + case OrganizationAccessScope.ApplicationWrite: + canWriteCallback = this.canWriteApplicationInTargetOrganization; + break; + case OrganizationAccessScope.GatewayWrite: + canWriteCallback = this.canWriteGatewayInTargetOrganization; + break; + case OrganizationAccessScope.UserAdministrationWrite: + canWriteCallback = this.canWriteUserInTargetOrganization; + break; + default: + // Should never happen + return false; + } + + return permissions.some( + (permission) => permission.type === PermissionType.GlobalAdmin || - (permission.organization.id === id && - (permission.type === PermissionType.Write || - permission.type === PermissionType.OrganizationAdmin)) - ); - }); + (permission.organization.id === id && canWriteCallback(permission)) + ); } - hasAdminAccessInTargetOrganization(id: number): boolean { - const userInfo = this.sharedVariableService.getUserInfo(); - return userInfo.user.permissions.some((permission) => { - return ( - permission.type === PermissionType.GlobalAdmin || - (permission.organization.id === id && - permission.type === PermissionType.OrganizationAdmin) - ); - }); + private canWriteApplicationInTargetOrganization( + permission: PermissionResponse + ): boolean { + return permission.type === PermissionType.OrganizationApplicationAdmin; + } + + private canWriteGatewayInTargetOrganization(permission: PermissionResponse): boolean { + return permission.type === PermissionType.OrganizationGatewayAdmin; } + + private canWriteUserInTargetOrganization(permission: PermissionResponse): boolean { + return permission.type === PermissionType.OrganizationUserAdmin; + } + hasGlobalAdmin() { const userInfo = this.sharedVariableService.getUserInfo(); return userInfo.user.permissions.some( diff --git a/src/app/shared/shared-variable/shared-variable.service.ts b/src/app/shared/shared-variable/shared-variable.service.ts index 9aad08090..f4e8542ba 100644 --- a/src/app/shared/shared-variable/shared-variable.service.ts +++ b/src/app/shared/shared-variable/shared-variable.service.ts @@ -82,27 +82,6 @@ export class SharedVariableService { return this.getUserInfo().user.permissions.length > 0; } - getHasWritePermission(): boolean { - const permissions = this.getUserInfo().user.permissions; - return permissions.some( - (permission) => - permission.type === PermissionType.GlobalAdmin || - (permission.organization?.id === +this.selectedOrganisationId && - (permission.type === PermissionType.OrganizationAdmin || - permission.type === PermissionType.Write)) - ); - } - - getHasAnyWritePermission(): boolean { - const permissions = this.getUserInfo().user.permissions; - return permissions.some( - (permission) => - permission.type === PermissionType.GlobalAdmin || - permission.type === PermissionType.OrganizationAdmin || - permission.type === PermissionType.Write - ); - } - isGlobalAdmin(): boolean { return this.getUserInfo().user.permissions.some( (permission) => permission.type === PermissionType.GlobalAdmin diff --git a/src/app/sigfox/sigfox-groups-detail/sigfox-device-types-edit/sigfox-device-types-edit.component.ts b/src/app/sigfox/sigfox-groups-detail/sigfox-device-types-edit/sigfox-device-types-edit.component.ts index efdc539ba..e291d3099 100644 --- a/src/app/sigfox/sigfox-groups-detail/sigfox-device-types-edit/sigfox-device-types-edit.component.ts +++ b/src/app/sigfox/sigfox-groups-detail/sigfox-device-types-edit/sigfox-device-types-edit.component.ts @@ -14,6 +14,7 @@ import { Location } from '@angular/common'; import { ErrorMessageService } from '@shared/error-message.service'; import { ErrorMessage } from '@shared/models/error-message.model'; import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -66,7 +67,7 @@ export class SigfoxDeviceTypesEditComponent implements OnInit { this.sigfoxDeviceType.groupId = this.sigfoxGroupId; } this.getContracts(this.sigfoxGroupId); - this.canEdit = this.meService.canWriteInTargetOrganization() + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } private getContracts(groupId: number) { diff --git a/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.html b/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.html index a29e61710..b120e8255 100644 --- a/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.html +++ b/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.html @@ -1,7 +1,8 @@
+ [ctaLabel]="'SIGFOX-GROUP.PROFILES.CREATE-NEW-DEVICE-TYPE' | translate" [ctaRouterLink]="'new-device-type'" + [canEdit]="canEdit">
@@ -14,4 +15,4 @@

{{'SIGFOX-GROUP.PROFILES.DEVICE-TYPE' | translate}}

- \ No newline at end of file + diff --git a/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.ts b/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.ts index f49ecbe08..4a572bd30 100644 --- a/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.ts +++ b/src/app/sigfox/sigfox-groups-detail/sigfox-groups-detail.component.ts @@ -8,6 +8,8 @@ import { SigfoxGroup } from '@shared/models/sigfox-group.model'; import { SigfoxService } from '@shared/services/sigfox.service'; import { SigfoxGroupData } from '@sigfox/sigfox-settings.model'; import { Observable } from 'rxjs'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -20,18 +22,20 @@ export class SigfoxGroupsDetailComponent implements OnInit { private sigfoxGroupId: number; sigfoxDevices: SigfoxDeviceType[]; sigfoxGroup: SigfoxGroup; + canEdit: boolean; constructor( public translate: TranslateService, private route: ActivatedRoute, private sigfoxService: SigfoxService, - + private meService: MeService ) { } ngOnInit(): void { this.sigfoxGroupId = +this.route.snapshot.paramMap.get('groupId'); this.getSigFoxGroup(this.sigfoxGroupId); this.getSigFoxDevices(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } getSigFoxDevices() { diff --git a/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.html b/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.html index 10cdaa796..2cd03675a 100644 --- a/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.html +++ b/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.html @@ -1,4 +1,4 @@ - +
@@ -34,4 +34,4 @@
- \ No newline at end of file + diff --git a/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.ts b/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.ts index b6d2e1e4a..756342293 100644 --- a/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.ts +++ b/src/app/sigfox/sigfox-groups-edit/sigfox-groups-edit.component.ts @@ -11,6 +11,8 @@ import { Subscription } from 'rxjs'; import { Location } from '@angular/common'; import { ErrorMessageService } from '@shared/error-message.service'; import { ErrorMessage } from '@shared/models/error-message.model'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -30,6 +32,7 @@ export class SigfoxGroupsEditComponent implements OnInit, OnDestroy { public formFailedSubmit = false; public title = ''; public backButton: BackButton = { label: '', routerLink: '/administration' }; + canEdit: boolean; constructor( private route: ActivatedRoute, @@ -38,12 +41,11 @@ export class SigfoxGroupsEditComponent implements OnInit, OnDestroy { private sigfoxService: SigfoxService, private location: Location, private sharedVariable: SharedVariableService, - private errorMessageService: ErrorMessageService - + private errorMessageService: ErrorMessageService, + private meService: MeService ) { } ngOnInit(): void { - this.translate.get(['SIGFOX-GROUP.SIGFOX-GROUP', 'FORM.EDIT-SIGFOX-GROUPS']) .subscribe(translations => { this.title = translations['FORM.EDIT-SIGFOX-GROUPS']; @@ -55,6 +57,7 @@ export class SigfoxGroupsEditComponent implements OnInit, OnDestroy { this.getSigfoxGroup(this.sigfoxGroupId); } this.sigfoxGroup.organizationId = this.sharedVariable.getSelectedOrganisationId(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } getSigfoxGroup(id: number) { diff --git a/src/app/sigfox/sigfox-groups-list/sigfox-groups-list-item/sigfox-groups-list-item.component.ts b/src/app/sigfox/sigfox-groups-list/sigfox-groups-list-item/sigfox-groups-list-item.component.ts index 7f3989811..436e17051 100644 --- a/src/app/sigfox/sigfox-groups-list/sigfox-groups-list-item/sigfox-groups-list-item.component.ts +++ b/src/app/sigfox/sigfox-groups-list/sigfox-groups-list-item/sigfox-groups-list-item.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { faEdit, faPen, faToolbox } from '@fortawesome/free-solid-svg-icons'; import { SigfoxGroup } from '@shared/models/sigfox-group.model'; import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-sigfox-groups-list-item', @@ -23,7 +24,7 @@ export class SigfoxGroupsListItemComponent implements OnInit { ngOnInit(): void { console.log(this.sigfoxGroup); - this.canEdit = this.meService.canWriteInTargetOrganization(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } onEditSigfoxGroup() { diff --git a/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.html b/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.html index 0ec63d3df..7628bfa09 100644 --- a/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.html +++ b/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.html @@ -1,6 +1,7 @@
+ [ctaLabel]="'SIGFOX-GROUP.ADMINISTRATION.CREATE-NEW-GROUP' | translate" + [canEdit]="canEdit">
@@ -15,4 +16,4 @@

{{'SIGFOX-GROUP.ADMINISTRATION.NAME' | translate}}

- \ No newline at end of file + diff --git a/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.ts b/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.ts index 1f7a76b98..e1a6983b6 100644 --- a/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.ts +++ b/src/app/sigfox/sigfox-groups-list/sigfox-groups-list.component.ts @@ -6,6 +6,8 @@ import { SigfoxGroup } from '@shared/models/sigfox-group.model'; import { SigfoxService } from '@shared/services/sigfox.service'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; import { Observable, Subscription } from 'rxjs'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-sigfox-groups-list', @@ -18,12 +20,15 @@ export class SigfoxGroupsListComponent implements OnInit, OnDestroy { subscription: Subscription; public sigfoxGroups: Observable; + canEdit: boolean; constructor( public translate: TranslateService, private globalService: SharedVariableService, private titleService: Title, - private sigfoxService: SigfoxService) { + private sigfoxService: SigfoxService, + private meService: MeService + ) { translate.use('da'); } @@ -33,6 +38,7 @@ export class SigfoxGroupsListComponent implements OnInit, OnDestroy { .subscribe(translations => { this.titleService.setTitle(translations['TITLE.SIGFOX']); }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } getSigFoxGroups() { diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index 96c8b754f..db20c85cf 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -150,7 +150,7 @@ "AUTHORIZATIONHEADER": "Authorization header", "NO-AUTHORIZATIONHEADER": "Ingen Authorization header angivet", "ADD-TO-OPENDATADK": "Send data til OpenDataDK", - "OPENDATA-DK": "OpenDataDK", + "OPENDATA-DK": "OpenDataDK", "NO-OPENDATA-DK": "Der er ikke oprettet nogen datadeling med Open Data DK endnu" }, "OPENDATADK": { @@ -355,9 +355,10 @@ }, "PERMISSION-TYPE": { "GlobalAdmin": "Global administrator", - "OrganizationAdmin": "Organisations administrator", - "Write": "Skrive-rettigheder", - "Read": "Læse-rettigheder" + "OrganizationUserAdmin": "Brugeradministrator administrator", + "OrganizationGatewayAdmin": "Gateway administrator", + "OrganizationApplicationAdmin": "Applikationsadministrator", + "Read": "Læserettigheder" }, "FORM": { "CREATE-NEW-APPLICATION": "Opret ny applikation", @@ -697,7 +698,7 @@ "PINGSLOTFREQ": "Class-B ping-slot frequency", "SUPPORTSCLASSC_ACTIVATE": "Device supports Class-C", "CLASSCTIMEOUT": "Class C confirmed downlink timeout", - "CANCEL": "Anuller", + "CANCEL": "Annuller", "SAVE": "Gem", "OTAA-ABP": "Join (OTAA / ABP)", "MACVERSION_PLACEHOLDER": "1.0.0", From b3863b128701fa132763e047d99bb6aea8170c20 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 22 Feb 2022 15:22:13 +0100 Subject: [PATCH 02/12] Add back "canEdit" checks after merge --- .../fiware-detail.component.html | 10 +-- .../fiware-detail/fiware-detail.component.ts | 7 +- .../fiware-edit/fiware-edit.component.html | 55 ++++++++-------- .../fiware-edit/fiware-edit.component.ts | 7 +- .../httppush-detail.component.html | 4 +- .../httppush-detail.component.ts | 7 +- .../httppush-edit.component.html | 65 ++++++++++--------- .../httppush-edit/httppush-edit.component.ts | 7 +- 8 files changed, 91 insertions(+), 71 deletions(-) diff --git a/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.html b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.html index 8e895d9c8..8ff3a9d7b 100644 --- a/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.html +++ b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.html @@ -1,6 +1,6 @@
+ [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeleteDatatarget()" [canEdit]="canEdit">
@@ -30,11 +30,11 @@

{{ 'DATATARGET.DETAILS' | translate }}

{{ 'DATATARGET.NO-AUTHORIZATIONHEADER' | translate }}

- +
- - + +
@@ -73,4 +73,4 @@

{{ 'DATATARGET.RELATIONS' | translate }}

-
\ No newline at end of file +
diff --git a/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.ts b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.ts index d56148e67..24b64bb80 100644 --- a/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.ts +++ b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.ts @@ -12,6 +12,8 @@ import { Datatarget } from '../../datatarget.model'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { faArrowsAltH } from '@fortawesome/free-solid-svg-icons'; import { DatatargetDetail } from '@applications/datatarget/datatarget-detail/datatarget-detail'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ @@ -28,6 +30,7 @@ export class FiwareDetailComponent implements DatatargetDetail, OnInit, OnDestr private deleteDialogSubscription: Subscription; public dropdownButton: DropdownButton; arrowsAltH = faArrowsAltH; + canEdit: boolean; constructor( private route: ActivatedRoute, @@ -35,7 +38,8 @@ export class FiwareDetailComponent implements DatatargetDetail, OnInit, OnDestr private location: Location, private datatargetRelationServicer: PayloadDeviceDatatargetService, private datatargetService: DatatargetService, - public translate: TranslateService) { } + public translate: TranslateService, + private meService: MeService) { } ngOnInit(): void { const id: number = +this.route.snapshot.paramMap.get('datatargetId'); @@ -54,6 +58,7 @@ export class FiwareDetailComponent implements DatatargetDetail, OnInit, OnDestr this.backButton.label = translations['NAV.MY-DATATARGET']; this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS']; }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } getDatatarget(id: number) { diff --git a/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.html b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.html index 02ec75295..190a208ab 100644 --- a/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.html +++ b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.html @@ -1,6 +1,7 @@ + backButtonTitle="{{backButtonTitle}}" + title="{{title}}" + [canEdit]="canEdit">
@@ -67,17 +68,17 @@
* -
@@ -96,19 +97,19 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
{{'QUESTION.ADD-RELATIONS' | translate}} - - - - + + +
+ + + + - + @@ -151,12 +152,12 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}

{{'DATATARGET.DELETE' | translate}}

- - - - -
{{'QUESTION.DATATARGET.SELECT-DEVICES' | translate}} - -
-
+
{{'QUESTION.DATATARGET.SELECT-PAYLOADDECODER' | translate}} @@ -141,7 +142,7 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
{{payloadDecoder.name}} - +
- + +
+
diff --git a/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.ts b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.ts index b52a823b6..92567fe66 100644 --- a/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.ts +++ b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.ts @@ -21,6 +21,8 @@ import { ScrollToTopService } from '@shared/services/scroll-to-top.service'; import { DataTargetType } from '@shared/enums/datatarget-type'; import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { DatatargetEdit } from '@applications/datatarget/datatarget-edit/datatarget-edit'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-fiware-edit', @@ -53,7 +55,7 @@ export class FiwareEditComponent implements DatatargetEdit, OnInit, OnDestroy { payloadDeviceDatatarget: PayloadDeviceDatatarget[]; newDynamic: any = {}; faQuestionCircle = faQuestionCircle; - + canEdit: boolean; constructor( public translate: TranslateService, @@ -67,6 +69,7 @@ export class FiwareEditComponent implements DatatargetEdit, OnInit, OnDestroy { private dialog: MatDialog, private errorMessageService: ErrorMessageService, private scrollToTopService: ScrollToTopService, + private meService: MeService, ) { translate.use('da'); } @@ -105,7 +108,7 @@ export class FiwareEditComponent implements DatatargetEdit, OnInit, OnDestroy { this.getDevices(); } this.getPayloadDecoders(); - + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } diff --git a/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.html b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.html index 42cf6d1f0..6ba94d0ac 100644 --- a/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.html +++ b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.html @@ -1,6 +1,6 @@
+ [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeleteDatatarget()" [canEdit]="canEdit">
@@ -71,4 +71,4 @@

{{ 'DATATARGET.RELATIONS' | translate }}

-
\ No newline at end of file +
diff --git a/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.ts b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.ts index 179aebcf7..1cad19ae3 100644 --- a/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.ts +++ b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.ts @@ -12,6 +12,8 @@ import { Datatarget } from '../../datatarget.model'; import { DropdownButton } from '@shared/models/dropdown-button.model'; import { faArrowsAltH } from '@fortawesome/free-solid-svg-icons'; import { DatatargetDetail } from '@applications/datatarget/datatarget-detail/datatarget-detail'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-httppush-detail', @@ -28,6 +30,7 @@ export class HttppushDetailComponent implements DatatargetDetail, OnInit, OnDes public dropdownButton: DropdownButton; arrowsAltH = faArrowsAltH; private applicationName: string; + canEdit: boolean; constructor( private route: ActivatedRoute, @@ -35,7 +38,8 @@ export class HttppushDetailComponent implements DatatargetDetail, OnInit, OnDes private location: Location, private datatargetRelationServicer: PayloadDeviceDatatargetService, private datatargetService: DatatargetService, - public translate: TranslateService) { } + public translate: TranslateService, + private meService: MeService) { } ngOnInit(): void { const id: number = +this.route.snapshot.paramMap.get('datatargetId'); @@ -54,6 +58,7 @@ export class HttppushDetailComponent implements DatatargetDetail, OnInit, OnDes this.backButton.label = translations['NAV.MY-DATATARGET']; this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS']; }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } getDatatarget(id: number) { diff --git a/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.html b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.html index 4d4df069e..73725f599 100644 --- a/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.html +++ b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.html @@ -1,6 +1,7 @@ + backButtonTitle="{{backButtonTitle}}" + title="{{title}}" + [canEdit]="canEdit"> @@ -11,9 +12,9 @@ - {{'DATATARGET.ADD-TO-OPENDATADK' | translate}} @@ -26,7 +27,7 @@ [ngClass]="{'is-invalid' : formFailedSubmit && errorFields.includes('name'), 'is-valid' : formFailedSubmit && !errorFields.includes('name')}"> - +
* @@ -38,17 +39,17 @@
* -
@@ -70,19 +71,19 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
{{'QUESTION.ADD-RELATIONS' | translate}} - - - - + + +
+ + + + - + @@ -125,15 +126,15 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}

{{'DATATARGET.DELETE' | translate}}

- - - - -
{{'QUESTION.DATATARGET.SELECT-DEVICES' | translate}} - -
-
+
{{'QUESTION.DATATARGET.SELECT-PAYLOADDECODER' | translate}} @@ -115,7 +116,7 @@
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
{{payloadDecoder.name}} - +
- + +
+
- \ No newline at end of file + diff --git a/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.ts b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.ts index d192c2a65..c34cf7403 100644 --- a/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.ts +++ b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.ts @@ -26,6 +26,8 @@ import { ScrollToTopService } from '@shared/services/scroll-to-top.service'; import { OpenDataDkDataset } from '../../opendatadk/opendatadk-dataset.model'; import { DataTargetType } from '@shared/enums/datatarget-type'; import { DatatargetEdit } from '@applications/datatarget/datatarget-edit/datatarget-edit'; +import { MeService } from '@shared/services/me.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-httppush-edit', @@ -60,6 +62,7 @@ export class HttppushEditComponent payloadDeviceDatatarget: PayloadDeviceDatatarget[]; newDynamic: any = {}; + canEdit: boolean; constructor( public translate: TranslateService, @@ -74,7 +77,8 @@ export class HttppushEditComponent private errorMessageService: ErrorMessageService, private opendatadkService: OpendatadkService, private opendatadkDialogService: OpendatadkDialogService, - private scrollToTopService: ScrollToTopService + private scrollToTopService: ScrollToTopService, + private meService: MeService, ) { translate.use('da'); } @@ -110,6 +114,7 @@ export class HttppushEditComponent } this.getPayloadDecoders(); this.setDataSetExcists(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } addRow() { From e38ef53081bc9ff27a39c587d233ec41e1d06cc1 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Thu, 28 Apr 2022 17:15:54 +0200 Subject: [PATCH 03/12] Init gateway status overview. Styling not done. Dummy data --- .../gateway-list/gateway-list.component.html | 7 +- .../gateway-status.component.html | 35 ++++++++ .../gateway-status.component.scss | 62 +++++++++++++ .../gateway-status.component.ts | 90 +++++++++++++++++++ src/app/gateway/gateway.model.ts | 5 ++ src/app/gateway/gateway.module.ts | 3 + src/assets/i18n/da.json | 7 +- src/styles.scss | 4 + 8 files changed, 211 insertions(+), 2 deletions(-) create mode 100644 src/app/gateway/gateway-status/gateway-status.component.html create mode 100644 src/app/gateway/gateway-status/gateway-status.component.scss create mode 100644 src/app/gateway/gateway-status/gateway-status.component.ts diff --git a/src/app/gateway/gateway-list/gateway-list.component.html b/src/app/gateway/gateway-list/gateway-list.component.html index 8c92e32f7..610e66272 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.html +++ b/src/app/gateway/gateway-list/gateway-list.component.html @@ -20,6 +20,11 @@
+ +
+ +
+
- \ No newline at end of file + diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html new file mode 100644 index 000000000..fdc1fe9fd --- /dev/null +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + +
{{element.id}}{{formatTime(col)}}
+
+
+
+ {{ "GEN.ONLINE" | translate}} +
+
+
+ {{ "GEN.OFFLINE" | translate}} +
+
diff --git a/src/app/gateway/gateway-status/gateway-status.component.scss b/src/app/gateway/gateway-status/gateway-status.component.scss new file mode 100644 index 000000000..e8fae8ea2 --- /dev/null +++ b/src/app/gateway/gateway-status/gateway-status.component.scss @@ -0,0 +1,62 @@ +$online: #449d52; +$offline: #f96772; + +.status-table { + border-collapse: separate; + border-spacing: 10px 4px; + + .online { + background-color: $online; + } + .offline { + background-color: $offline; + } + + tr { + &:hover { + background-color: unset; + } + + > td.mat-cell { + min-width: 1.2rem; + + &:not(:first-child) { + max-width: 1.2rem; + } + + &:first-child { + padding-left: 0; + min-width: 5rem; + } + + &:last-child { + max-width: 1.2rem; + padding-right: 0; + } + } + + > td.mat-footer-cell { + &:last-child { + padding-right: 0; + } + } + } +} + +.status-legend { + .legend { + width: 5rem; + height: 8px; + + &.online { + background-color: $online; + } + &.offline { + background-color: $offline; + } + } + + span { + color: rgba($color: #000, $alpha: 0.6); + } +} diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts new file mode 100644 index 000000000..ac15616c2 --- /dev/null +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -0,0 +1,90 @@ +import { Component, OnInit } from '@angular/core'; +import { MatTableDataSource } from '@angular/material/table'; +import { TranslateService } from '@ngx-translate/core'; +import * as moment from 'moment'; +import { GatewayStatus } from '../gateway.model'; + +@Component({ + selector: 'app-gateway-status', + templateUrl: './gateway-status.component.html', + styleUrls: ['./gateway-status.component.scss'], +}) +export class GatewayStatusComponent implements OnInit { + private readonly columnGatewayName = 'gatewayName'; + dataSource: MatTableDataSource; + timeColumns: string[] = []; + displayedColumns: string[] = []; + nameText = ''; + timestampText = ''; + + constructor(private translate: TranslateService) {} + + ngOnInit(): void { + this.getGatewayStatus(); + + this.translate + .get(['GEN.NAME', 'LORA-GATEWAY-STATUS.TIMESTAMP']) + .subscribe((translations) => { + this.nameText = translations['GEN.NAME']; + this.timestampText = translations['LORA-GATEWAY-STATUS.TIMESTAMP']; + }); + } + + getGatewayStatus(): void { + // TODO: Fetch from API + const response: GatewayStatus[] = [ + { id: 'aaa', onlineTimestamps: [moment().toDate()] }, + { + id: 'bbbbbbbbb', + onlineTimestamps: [moment().subtract(1, 'day').toDate()], + }, + ]; + + this.buildColumns(response); + this.dataSource = new MatTableDataSource(response); + } + + private buildColumns(response: GatewayStatus[]) { + let minDate: Date | null | undefined = response[0]?.onlineTimestamps[0]; + let maxDate: Date | null | undefined = response[0]?.onlineTimestamps[0]; + + response.forEach((gateway) => { + gateway.onlineTimestamps.forEach((timestamp) => { + if (timestamp < minDate) { + minDate = timestamp; + } else if (timestamp > maxDate) { + maxDate = timestamp; + } + }); + }); + + if (minDate && maxDate) { + for ( + const dt = new Date(minDate); + dt <= new Date(maxDate); + dt.setTime(dt.getTime() + 1000 * (60 * 60 * 1)) + ) { + this.timeColumns.push(dt.toISOString()); + } + + this.displayedColumns = [this.columnGatewayName].concat(this.timeColumns); + } + } + + getStatusClass(gateway: GatewayStatus, timestamp: string) { + return gateway.onlineTimestamps.some((gatewayTimestamp) => + moment(gatewayTimestamp).isSame(moment(timestamp), 'hour') + ) + ? 'online' + : 'offline'; + } + + formatTime(timestamp: string): string { + return moment(timestamp).format('HH:00'); + } + + formatTooltip(gatewayId: string, timestamp: string): string { + const formattedTime = moment(timestamp).format('DD-MM-YYYY HH:MM'); + return `${this.nameText}: ${gatewayId}\n${this.timestampText}: ${formattedTime}`; + } +} diff --git a/src/app/gateway/gateway.model.ts b/src/app/gateway/gateway.model.ts index dc8d09813..2e2ea438c 100644 --- a/src/app/gateway/gateway.model.ts +++ b/src/app/gateway/gateway.model.ts @@ -57,3 +57,8 @@ export interface GatewayStats { txPacketsReceived: number; txPacketsEmitted: number; } + +export interface GatewayStatus { + id: string; + onlineTimestamps: Date[]; +} diff --git a/src/app/gateway/gateway.module.ts b/src/app/gateway/gateway.module.ts index a538ce239..cf8d06dc1 100644 --- a/src/app/gateway/gateway.module.ts +++ b/src/app/gateway/gateway.module.ts @@ -13,6 +13,7 @@ import { NGMaterialModule } from '@shared/Modules/materiale.module'; 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'; const gatewayRoutes: Routes = [ { @@ -35,6 +36,7 @@ const gatewayRoutes: Routes = [ GatewayListComponent, GatewayDetailComponent, GatewayEditComponent, + GatewayStatusComponent, ], imports: [ CommonModule, @@ -53,6 +55,7 @@ const gatewayRoutes: Routes = [ GatewaysComponent, GatewayListComponent, GatewayEditComponent, + GatewayStatusComponent, RouterModule ] }) diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index 88efc9499..92597834f 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -13,7 +13,9 @@ "VALUE": "Værdi", "NAME": "Navn", "DESCRIPTION": "Beskrivelse", - "to": "til" + "to": "til", + "ONLINE": "Online", + "OFFLINE": "Offline" }, "NAV": { "DASHBOARD": "Dashboard", @@ -302,6 +304,9 @@ "STATUS": "Status", "ORGANIZATION": "Organisation" }, + "LORA-GATEWAY-STATUS": { + "TIMESTAMP": "Tidspunkt" + }, "DEVICE-MODEL": { "DELETE-FAILED":"Slet Fejlede", "HEADLINE":"Detaljer", diff --git a/src/styles.scss b/src/styles.scss index b2296fe21..f19c74962 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -89,3 +89,7 @@ body { font-size: 1rem !important; } +.status-tooltip { + background: rgba(97, 97, 97, 1.0); + white-space: pre-line; +} From fd0c288431dcf8dc1a276bad000c1455b452ab08 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 3 May 2022 16:54:01 +0200 Subject: [PATCH 04/12] Adjust gateway status for responsiveness. Added TODOs --- .../iot-device-detail.component.ts | 2 +- .../gateway-list/gateway-list.component.html | 2 +- .../gateway-list/gateway-list.component.ts | 22 ++++--- .../gateway-status.component.html | 64 +++++++++++++------ .../gateway-status.component.scss | 59 +++++++++++------ .../gateway-status.component.ts | 37 ++++++++--- .../services/chirpstack-gateway.service.ts | 5 ++ src/assets/i18n/da.json | 5 +- src/assets/scss/setup/_variables.scss | 4 +- 9 files changed, 141 insertions(+), 59 deletions(-) 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 e70572fbc..6104c3ba4 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 @@ -226,7 +226,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { this.iotDeviceService.getDeviceStats(this.deviceId).subscribe( (response) => { - if (response === null) { + if (!response) { return; } diff --git a/src/app/gateway/gateway-list/gateway-list.component.html b/src/app/gateway/gateway-list/gateway-list.component.html index 610e66272..618eb79dc 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.html +++ b/src/app/gateway/gateway-list/gateway-list.component.html @@ -7,7 +7,7 @@ [dropdownLabel]="'GATEWAY.DROPDOWNFILTER' | translate" (updateSelectedOpt)="setOrgIdFilter($event)" [dropdownDefaultOption]="'GATEWAY.DROPDOWNDEFAULT' | translate"> - +
diff --git a/src/app/gateway/gateway-list/gateway-list.component.ts b/src/app/gateway/gateway-list/gateway-list.component.ts index d022dd4fe..780d9a8cd 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.ts +++ b/src/app/gateway/gateway-list/gateway-list.component.ts @@ -11,6 +11,7 @@ import { MeService } from '@shared/services/me.service'; import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; +import { MatTabChangeEvent } from '@angular/material/tabs'; @Component({ @@ -28,6 +29,7 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { public selectedSortId = 1; public gateways: Gateway[]; private gatewaySubscription: Subscription; + private gatewayStatusSubscription: Subscription; public selectedSortObject: Sort = { id: 1, dir: 'ASC', @@ -120,14 +122,21 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { ); } - showMap(event: any) { - if (event.index === 1) { + private getGatewayStatus(): void { + // TODO: GATEWAY: Implement fetch + // this.gatewayStatusSubscription = this.chirpstackGatewayService.get + } + + selectedTabChange({index}: MatTabChangeEvent) { + if (index === 1) { if (this.selectedOrg) { this.getGatewayWith(this.selectedOrg); } else { this.getGateways(); } this.showmap = true; + } else if (index === 2) { + this.getGatewayStatus(); } } @@ -184,12 +193,7 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { ngOnDestroy() { // prevent memory leak by unsubscribing - if (this.gatewaySubscription) { - this.gatewaySubscription.unsubscribe(); - } - if (this.deleteDialogSubscription) { - this.deleteDialogSubscription.unsubscribe(); - } + this.gatewaySubscription?.unsubscribe(); + this.deleteDialogSubscription?.unsubscribe(); } - } diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index fdc1fe9fd..707b4e9b2 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -2,27 +2,51 @@ Responsiveness on smaller screens or if there's too many columns - Already has horizontal scroll on iPad portrait. - "next page" where the next/previous 6 hours is shown. 24 hours is too much on iPad portrait - - minimum width on cells + - max width on cell? + - min and max date picker? - Title similar to the mock-up? - Should the status legend be moved inside the table container so that it's on the same elevation? + + Gateway id vs gateway name + - Use name if possible. Will it work if pagination is introduced? + + __Backend__ + Fetch gateways paginated + - Shouldn't be a heavy operation if we DON'T ask fetch info for every single one. + - Sorting shouldn't be a concern. Chirpstack doesn't support this and the "listevisning" tab only sorts on response + + __Questions__ + Colors used from variables.scss instead of from grafana example. Is it okay? + --> - - - - - - - - - - - - - -
{{element.id}}{{formatTime(col)}}
+

{{ 'LORA-GATEWAY-STATUS.TITLE' | translate}}

+
+ + + + + + + + + + + + + +
{{element.id}} + {{'GEN.DATE' | translate}} +
+ {{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} +
+ {{formatFooterDate(time)}} +
+ {{formatTime(time)}} +
+ +
+
@@ -32,4 +56,8 @@
{{ "GEN.OFFLINE" | translate}}
+
+
+ {{ "GEN.NEVER-SEEN" | translate}} +
diff --git a/src/app/gateway/gateway-status/gateway-status.component.scss b/src/app/gateway/gateway-status/gateway-status.component.scss index e8fae8ea2..c5d6e3427 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.scss +++ b/src/app/gateway/gateway-status/gateway-status.component.scss @@ -1,5 +1,9 @@ -$online: #449d52; -$offline: #f96772; +@import '~src/assets/scss/setup/variables'; + +$online: $color-green-001; +$offline: $red; +$neverSeen: $color-grey-003; +$cellSize: 0.9rem; .status-table { border-collapse: separate; @@ -17,27 +21,39 @@ $offline: #f96772; background-color: unset; } - > td.mat-cell { - min-width: 1.2rem; - - &:not(:first-child) { - max-width: 1.2rem; + > td { + font-size: $cellSize; + &:hover { + transition: $transition-short; + filter: brightness(0.65); } - &:first-child { - padding-left: 0; - min-width: 5rem; - } + &.mat-cell { + min-width: 1.2rem; + + &:not(:first-child) { + max-width: 1.2rem; + } + + &:first-child { + padding-left: 0; + min-width: 5rem; + } - &:last-child { - max-width: 1.2rem; - padding-right: 0; + &:last-child { + max-width: 1.2rem; + padding-right: 0; + } } - } - > td.mat-footer-cell { - &:last-child { - padding-right: 0; + &.mat-footer-cell { + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } } } } @@ -54,9 +70,16 @@ $offline: #f96772; &.offline { background-color: $offline; } + &.never-seen { + background-color: $neverSeen; + } } span { color: rgba($color: #000, $alpha: 0.6); } } + +.margin-right--5 { + margin-right: -1.5rem; +} diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index ac15616c2..36e5ed902 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -15,7 +15,9 @@ export class GatewayStatusComponent implements OnInit { timeColumns: string[] = []; displayedColumns: string[] = []; nameText = ''; + neverSeenText = ''; timestampText = ''; + visibleFooterTimeInterval = 1; constructor(private translate: TranslateService) {} @@ -23,24 +25,29 @@ export class GatewayStatusComponent implements OnInit { this.getGatewayStatus(); this.translate - .get(['GEN.NAME', 'LORA-GATEWAY-STATUS.TIMESTAMP']) + .get(['GEN.NAME', 'GEN.NEVER-SEEN', 'LORA-GATEWAY-STATUS.TIMESTAMP']) .subscribe((translations) => { this.nameText = translations['GEN.NAME']; + this.neverSeenText = translations['GEN.NEVER-SEEN']; this.timestampText = translations['LORA-GATEWAY-STATUS.TIMESTAMP']; }); } getGatewayStatus(): void { - // TODO: Fetch from API + // TODO: Fetch from API on gateway list const response: GatewayStatus[] = [ { id: 'aaa', onlineTimestamps: [moment().toDate()] }, { id: 'bbbbbbbbb', - onlineTimestamps: [moment().subtract(1, 'day').toDate()], + onlineTimestamps: [moment().subtract(10, 'hour').toDate()], }, ]; this.buildColumns(response); + this.visibleFooterTimeInterval = Math.round( + this.clamp(this.timeColumns.length / 4, 1, 6) + ); + this.dataSource = new MatTableDataSource(response); } @@ -71,20 +78,32 @@ export class GatewayStatusComponent implements OnInit { } } + private clamp(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)); + } + getStatusClass(gateway: GatewayStatus, timestamp: string) { - return gateway.onlineTimestamps.some((gatewayTimestamp) => - moment(gatewayTimestamp).isSame(moment(timestamp), 'hour') - ) + return !gateway.onlineTimestamps.length + ? 'never-seen' + : gateway.onlineTimestamps.some((gatewayTimestamp) => + moment(gatewayTimestamp).isSame(moment(timestamp), 'hour') + ) ? 'online' : 'offline'; } + formatFooterDate(timestamp: string): string { + return moment(timestamp).format('DD-MM'); + } + formatTime(timestamp: string): string { return moment(timestamp).format('HH:00'); } - formatTooltip(gatewayId: string, timestamp: string): string { - const formattedTime = moment(timestamp).format('DD-MM-YYYY HH:MM'); - return `${this.nameText}: ${gatewayId}\n${this.timestampText}: ${formattedTime}`; + formatTooltip(gateway: GatewayStatus, timestamp: string): string { + const formattedTime = !gateway.onlineTimestamps.length + ? this.neverSeenText + : moment(timestamp).format('DD-MM-YYYY HH:00'); + return `${this.nameText}: ${gateway.id}\n${this.timestampText}: ${formattedTime}`; } } diff --git a/src/app/shared/services/chirpstack-gateway.service.ts b/src/app/shared/services/chirpstack-gateway.service.ts index fd7a13ae0..391c404a7 100644 --- a/src/app/shared/services/chirpstack-gateway.service.ts +++ b/src/app/shared/services/chirpstack-gateway.service.ts @@ -56,6 +56,11 @@ export class ChirpstackGatewayService { ); } + // TODO: GATEWAY: Implement fetch + public getStatus(id: number): Observable { + return null; + } + public post(gateway: Gateway): Observable { const gatewayRequest: GatewayRequest = new GatewayRequest; gatewayRequest.gateway = gateway; diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index 92597834f..3f742d3d4 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -15,7 +15,9 @@ "DESCRIPTION": "Beskrivelse", "to": "til", "ONLINE": "Online", - "OFFLINE": "Offline" + "OFFLINE": "Offline", + "NEVER-SEEN": "Aldrig set", + "DATE": "Dato" }, "NAV": { "DASHBOARD": "Dashboard", @@ -305,6 +307,7 @@ "ORGANIZATION": "Organisation" }, "LORA-GATEWAY-STATUS": { + "TITLE": "LoRaWAN online status historik", "TIMESTAMP": "Tidspunkt" }, "DEVICE-MODEL": { diff --git a/src/assets/scss/setup/_variables.scss b/src/assets/scss/setup/_variables.scss index 4486497fe..52f3f12a5 100644 --- a/src/assets/scss/setup/_variables.scss +++ b/src/assets/scss/setup/_variables.scss @@ -152,7 +152,7 @@ $color-blue-004: rgba(63, 192, 243, 0.164) !default; $color-green-001: rgba(86, 178, 87, 1) !default; $color-green-002: rgba(86, 178, 87, 0.75) !default; -$color-green-003: rgba(486, 178, 87, 0.5) !default; +$color-green-003: rgba(86, 178, 87, 0.5) !default; $color-green-004: rgba(86, 178, 87, 0.25) !default; $color-dark-green-001: rgba(0, 49, 39, 1) !default; @@ -381,4 +381,4 @@ $red: #dc3545; .mat-tab-body.mat-tab-body-active { overflow-y: hidden; } -} \ No newline at end of file +} From 8f779bf497025bd71a9fda965ca52d6aea2c0fe2 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Mon, 9 May 2022 18:14:37 +0200 Subject: [PATCH 05/12] Implement gateway status fetch. Update styling and TODOs --- .../enums/gateway-status-interval.enum.ts | 5 + .../gateway-list/gateway-list.component.html | 3 +- .../gateway-list/gateway-list.component.ts | 34 ++-- .../gateway-status.component.html | 96 ++++++------ .../gateway-status.component.scss | 29 +++- .../gateway-status.component.ts | 147 +++++++++++++++--- src/app/gateway/gateway.model.ts | 14 ++ src/app/graph/graph.component.html | 2 +- .../services/lorawan-gateway.service.ts | 22 +++ src/assets/i18n/da.json | 128 +++++++-------- 10 files changed, 323 insertions(+), 157 deletions(-) create mode 100644 src/app/gateway/enums/gateway-status-interval.enum.ts create mode 100644 src/app/shared/services/lorawan-gateway.service.ts diff --git a/src/app/gateway/enums/gateway-status-interval.enum.ts b/src/app/gateway/enums/gateway-status-interval.enum.ts new file mode 100644 index 000000000..2e20a3d92 --- /dev/null +++ b/src/app/gateway/enums/gateway-status-interval.enum.ts @@ -0,0 +1,5 @@ +export enum GatewayStatusInterval { + DAY = 'DAY', + WEEK = 'WEEK', + MONTH = 'MONTH', +} diff --git a/src/app/gateway/gateway-list/gateway-list.component.html b/src/app/gateway/gateway-list/gateway-list.component.html index 618eb79dc..cc9cffc72 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.html +++ b/src/app/gateway/gateway-list/gateway-list.component.html @@ -22,7 +22,8 @@
- +
diff --git a/src/app/gateway/gateway-list/gateway-list.component.ts b/src/app/gateway/gateway-list/gateway-list.component.ts index 780d9a8cd..e485d895f 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.ts +++ b/src/app/gateway/gateway-list/gateway-list.component.ts @@ -13,6 +13,7 @@ import { environment } from '@environments/environment'; import { Title } from '@angular/platform-browser'; import { MatTabChangeEvent } from '@angular/material/tabs'; +const gatewayStatusTabIndex = 2; @Component({ selector: 'app-gateway-list', @@ -29,7 +30,6 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { public selectedSortId = 1; public gateways: Gateway[]; private gatewaySubscription: Subscription; - private gatewayStatusSubscription: Subscription; public selectedSortObject: Sort = { id: 1, dir: 'ASC', @@ -43,7 +43,9 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { public pageOffset = 0; public pageTotal: number; organisationId: number; - organisationChangeSubject: Subject = new Subject(); + tabIndex = 0; + organisationChangeSubject: Subject = new Subject(); + isGatewayStatusVisibleSubject: Subject = new Subject(); constructor( public translate: TranslateService, @@ -51,7 +53,8 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { private deleteDialogService: DeleteDialogService, private meService: MeService, private titleService: Title, - private sharedVariableService: SharedVariableService) { + private sharedVariableService: SharedVariableService, + ) { translate.use('da'); moment.locale('da'); } @@ -77,10 +80,16 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { } } - setOrgIdFilter(event: number) { - this.organisationId = event; - this.organisationChangeSubject.next(event); - this.filterGatewayByOrgId(event); + setOrgIdFilter(orgId: number) { + this.organisationId = orgId; + this.organisationChangeSubject.next(orgId); + + if (this.tabIndex === gatewayStatusTabIndex) { + // TODO: Race condition with organisation change event? + this.isGatewayStatusVisibleSubject.next(); + } + + this.filterGatewayByOrgId(orgId); } private getGateways(): void { @@ -122,12 +131,9 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { ); } - private getGatewayStatus(): void { - // TODO: GATEWAY: Implement fetch - // this.gatewayStatusSubscription = this.chirpstackGatewayService.get - } - selectedTabChange({index}: MatTabChangeEvent) { + this.tabIndex = index; + if (index === 1) { if (this.selectedOrg) { this.getGatewayWith(this.selectedOrg); @@ -135,8 +141,8 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { this.getGateways(); } this.showmap = true; - } else if (index === 2) { - this.getGatewayStatus(); + } else if (index === gatewayStatusTabIndex) { + this.isGatewayStatusVisibleSubject.next(); } } diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index 707b4e9b2..fd3f848a7 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -1,52 +1,52 @@ - -

{{ 'LORA-GATEWAY-STATUS.TITLE' | translate}}

-
- - - - - - - - - - - - - -
{{element.id}} - {{'GEN.DATE' | translate}} -
- {{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} -
- {{formatFooterDate(time)}} -
- {{formatTime(time)}} -
- -
+ + +

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

+
+
+ +
+ + + {{ 'LORA-GATEWAY-STATUS.INTERVAL.' + interval | translate }} + + +
+
+
+
+ +
+ + + + + + + + + + + + + + +
{{element.name}} + {{'GEN.DATE' | translate}} +
+ {{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} +
+ {{formatFooterDate(time)}} +
+ {{formatTime(time)}} +
+
+ +
diff --git a/src/app/gateway/gateway-status/gateway-status.component.scss b/src/app/gateway/gateway-status/gateway-status.component.scss index c5d6e3427..e1db399ce 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.scss +++ b/src/app/gateway/gateway-status/gateway-status.component.scss @@ -1,9 +1,9 @@ @import '~src/assets/scss/setup/variables'; -$online: $color-green-001; -$offline: $red; -$neverSeen: $color-grey-003; -$cellSize: 0.9rem; +$online: #56b257; +$offline: #e74c3c; +$neverSeen: #acacac; +$cellFontSize: 0.9rem; .status-table { border-collapse: separate; @@ -15,6 +15,9 @@ $cellSize: 0.9rem; .offline { background-color: $offline; } + .never-seen { + background-color: $neverSeen; + } tr { &:hover { @@ -22,7 +25,7 @@ $cellSize: 0.9rem; } > td { - font-size: $cellSize; + font-size: $cellFontSize; &:hover { transition: $transition-short; filter: brightness(0.65); @@ -77,9 +80,21 @@ $cellSize: 0.9rem; span { color: rgba($color: #000, $alpha: 0.6); + font-size: $cellFontSize; } } -.margin-right--5 { - margin-right: -1.5rem; +.overflow-auto { + overflow: auto; +} + +.no-shadow { + box-shadow: unset; + -webkit-box-shadow: unset; + -moz-box-shadow: unset; +} + +.status-interval { + width: 180px; + font-size: $cellFontSize; } diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index 36e5ed902..a4489ee49 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -1,15 +1,33 @@ -import { Component, OnInit } from '@angular/core'; +import { + AfterContentInit, + Component, + Input, + OnDestroy, + ViewChild, +} from '@angular/core'; +import { MatOptionSelectionChange } from '@angular/material/core'; +import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; +import { environment } from '@environments/environment'; import { TranslateService } from '@ngx-translate/core'; +import { recordToEntries } from '@shared/helpers/record.helper'; +import { LoRaWANGatewayService } from '@shared/services/lorawan-gateway.service'; +import { SharedVariableService } from '@shared/shared-variable/shared-variable.service'; import * as moment from 'moment'; -import { GatewayStatus } from '../gateway.model'; +import { Observable, Subject, Subscription } from 'rxjs'; +import { GatewayStatusInterval } from '../enums/gateway-status-interval.enum'; +import { GatewayStatus, GatewayStatusResponse } from '../gateway.model'; @Component({ selector: 'app-gateway-status', templateUrl: './gateway-status.component.html', styleUrls: ['./gateway-status.component.scss'], }) -export class GatewayStatusComponent implements OnInit { +export class GatewayStatusComponent implements AfterContentInit, OnDestroy { + @Input() organisationChangeSubject: Subject; + @Input() isVisibleSubject: Subject; + + private gatewayStatusSubscription: Subscription; private readonly columnGatewayName = 'gatewayName'; dataSource: MatTableDataSource; timeColumns: string[] = []; @@ -18,11 +36,24 @@ export class GatewayStatusComponent implements OnInit { neverSeenText = ''; timestampText = ''; visibleFooterTimeInterval = 1; + pageSize = environment.tablePageSize; + resultsLength = 0; + organizationId: number; + isLoadingResults = false; + isDirty = true; + statusIntervals: GatewayStatusInterval[]; + selectedStatusInterval = GatewayStatusInterval.DAY; + + @ViewChild(MatPaginator) paginator: MatPaginator; - constructor(private translate: TranslateService) {} + constructor( + private translate: TranslateService, + private sharedVariableService: SharedVariableService, + private lorawanGatewayService: LoRaWANGatewayService + ) {} - ngOnInit(): void { - this.getGatewayStatus(); + ngAfterContentInit(): void { + this.organizationId = this.sharedVariableService.getSelectedOrganisationId(); this.translate .get(['GEN.NAME', 'GEN.NEVER-SEEN', 'LORA-GATEWAY-STATUS.TIMESTAMP']) @@ -31,32 +62,84 @@ export class GatewayStatusComponent implements OnInit { this.neverSeenText = translations['GEN.NEVER-SEEN']; this.timestampText = translations['LORA-GATEWAY-STATUS.TIMESTAMP']; }); + + this.organisationChangeSubject.subscribe((orgId) => { + if (orgId) { + this.organizationId = orgId; + this.isDirty = true; + } + }); + + this.isVisibleSubject.pipe().subscribe(() => { + if (!this.isDirty) { + return; + } + + this.isDirty = false; + this.subscribeToGetGatewayStatus(); + }); + + this.statusIntervals = recordToEntries(GatewayStatusInterval).map( + (interval) => interval.value + ); + } + + private getGatewayStatus( + organizationId = this.organizationId, + timeInterval = this.selectedStatusInterval + ): Observable { + return this.lorawanGatewayService.getAllStatus({ + organizationId, + timeInterval, + // Paginator is only avaiable in ngAfterViewInit + limit: this.paginator?.pageSize, + offset: this.paginator?.pageIndex * this.paginator.pageSize, + }); + } + + private subscribeToGetGatewayStatus( + organizationId = this.organizationId, + timeInterval = this.selectedStatusInterval + ): void { + this.isLoadingResults = true; + this.gatewayStatusSubscription = this.getGatewayStatus( + organizationId, + timeInterval + ).subscribe((response) => { + this.isLoadingResults = false; + this.handleStatusResponse(response); + }); } - getGatewayStatus(): void { - // TODO: Fetch from API on gateway list - const response: GatewayStatus[] = [ - { id: 'aaa', onlineTimestamps: [moment().toDate()] }, - { - id: 'bbbbbbbbb', - onlineTimestamps: [moment().subtract(10, 'hour').toDate()], - }, - ]; - - this.buildColumns(response); + private handleStatusResponse(response: GatewayStatusResponse) { + this.resultsLength = response.count; + const sortedData = response.data + .slice() + .sort((a, b) => a.name.localeCompare(b.name)); + + this.buildColumns(sortedData); this.visibleFooterTimeInterval = Math.round( this.clamp(this.timeColumns.length / 4, 1, 6) ); - this.dataSource = new MatTableDataSource(response); + this.dataSource = new MatTableDataSource(sortedData); + this.dataSource.paginator = this.paginator; } private buildColumns(response: GatewayStatus[]) { - let minDate: Date | null | undefined = response[0]?.onlineTimestamps[0]; - let maxDate: Date | null | undefined = response[0]?.onlineTimestamps[0]; + let minDate: Date | null | undefined; + let maxDate: Date | null | undefined; + this.timeColumns = []; response.forEach((gateway) => { gateway.onlineTimestamps.forEach((timestamp) => { + if (!minDate) { + minDate = timestamp; + } + if (!maxDate) { + maxDate = timestamp; + } + if (timestamp < minDate) { minDate = timestamp; } else if (timestamp > maxDate) { @@ -73,9 +156,9 @@ export class GatewayStatusComponent implements OnInit { ) { this.timeColumns.push(dt.toISOString()); } - - this.displayedColumns = [this.columnGatewayName].concat(this.timeColumns); } + + this.displayedColumns = [this.columnGatewayName].concat(this.timeColumns); } private clamp(value: number, min: number, max: number) { @@ -92,6 +175,19 @@ export class GatewayStatusComponent implements OnInit { : 'offline'; } + onSelectInterval({ + isUserInput, + source: { value: newInterval }, + }: MatOptionSelectionChange) { + if ( + isUserInput && + newInterval !== this.selectedStatusInterval && + this.dataSource?.data.length + ) { + this.subscribeToGetGatewayStatus(this.organizationId, newInterval); + } + } + formatFooterDate(timestamp: string): string { return moment(timestamp).format('DD-MM'); } @@ -104,6 +200,11 @@ export class GatewayStatusComponent implements OnInit { const formattedTime = !gateway.onlineTimestamps.length ? this.neverSeenText : moment(timestamp).format('DD-MM-YYYY HH:00'); - return `${this.nameText}: ${gateway.id}\n${this.timestampText}: ${formattedTime}`; + return `${this.nameText}: ${gateway.name}\n${this.timestampText}: ${formattedTime}`; + } + + ngOnDestroy() { + // prevent memory leak by unsubscribing + this.gatewayStatusSubscription?.unsubscribe(); } } diff --git a/src/app/gateway/gateway.model.ts b/src/app/gateway/gateway.model.ts index 2e2ea438c..d36ad5309 100644 --- a/src/app/gateway/gateway.model.ts +++ b/src/app/gateway/gateway.model.ts @@ -1,5 +1,6 @@ import { EditPermission } from '@shared/models/edit-permission.model'; import { CommonLocation } from '../shared/models/common-location.model'; +import { GatewayStatusInterval } from './enums/gateway-status-interval.enum'; export class Gateway extends EditPermission { id?: string; @@ -58,7 +59,20 @@ export interface GatewayStats { txPacketsEmitted: number; } +export interface GetGatewayStatusParameters { + limit?: number; + offset?: number; + organizationId: number; + timeInterval?: GatewayStatusInterval; +} + export interface GatewayStatus { id: string; + name: string; onlineTimestamps: Date[]; } + +export interface GatewayStatusResponse { + data: GatewayStatus[]; + count: number; +} diff --git a/src/app/graph/graph.component.html b/src/app/graph/graph.component.html index a7ac6b3ab..0e724195c 100644 --- a/src/app/graph/graph.component.html +++ b/src/app/graph/graph.component.html @@ -6,7 +6,7 @@
-

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

+

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

diff --git a/src/app/shared/services/lorawan-gateway.service.ts b/src/app/shared/services/lorawan-gateway.service.ts new file mode 100644 index 000000000..c07d193b5 --- /dev/null +++ b/src/app/shared/services/lorawan-gateway.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@angular/core'; +import { + GatewayStatusResponse, + GetGatewayStatusParameters, +} from '@app/gateway/gateway.model'; +import { Observable } from 'rxjs'; +import { RestService } from './rest.service'; + +@Injectable({ + providedIn: 'root', +}) +export class LoRaWANGatewayService { + private baseUrl = 'lorawan/gateway'; + + constructor(private restService: RestService) {} + + public getAllStatus( + params: GetGatewayStatusParameters + ): Observable { + return this.restService.get(`${this.baseUrl}/status`, params); + } +} diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index 3f742d3d4..91bb292c6 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -17,7 +17,8 @@ "ONLINE": "Online", "OFFLINE": "Offline", "NEVER-SEEN": "Aldrig set", - "DATE": "Dato" + "DATE": "Dato", + "NO-DATA": "Der er ingen data at vise" }, "NAV": { "DASHBOARD": "Dashboard", @@ -47,7 +48,7 @@ "HELP": "Hjælp", "API-KEY": "API nøgler" }, - "TOPBAR":{ + "TOPBAR": { "SEARCH": { "BUTTON": "Søg" }, @@ -151,7 +152,7 @@ } }, "GATEWAY": { - "DETAILS":"Detaljer", + "DETAILS": "Detaljer", "CREATED": "Oprettet", "UPDATED": "Senest opdateret", "DESCRIPTION": "LoRaWAN gateway beskrivelse", @@ -189,7 +190,7 @@ "TIMEOUT": "Timeout", "TYPE": "Type", "NAME": "Datatarget navn", - "SHOW-OPTIONS":"Håndter datatarget", + "SHOW-OPTIONS": "Håndter datatarget", "DELETE": "Slet", "RELATIONS": "Tilknyttede IoT enheder og payload decoders", "PAYLOADEDECODER": "Payload decoder", @@ -308,28 +309,33 @@ }, "LORA-GATEWAY-STATUS": { "TITLE": "LoRaWAN online status historik", - "TIMESTAMP": "Tidspunkt" + "TIMESTAMP": "Tidspunkt", + "INTERVAL": { + "DAY": "Seneste døgn", + "WEEK": "Seneste uge", + "MONTH": "Seneste måned" + } }, "DEVICE-MODEL": { - "DELETE-FAILED":"Slet Fejlede", - "HEADLINE":"Detaljer", - "CREATED":"Oprettet", - "UPDATED":"Senest opdateret", + "DELETE-FAILED": "Slet Fejlede", + "HEADLINE": "Detaljer", + "CREATED": "Oprettet", + "UPDATED": "Senest opdateret", "ID": "Unik identifikator", "TABLE": { "ID": "Id" }, "NAME": "Navn", - "TYPE" : "Type", - "BRANDNAME" : "Mærke", - "MODELNAME" : "Model", - "MANUFACTURERNAME" : "Producentens navn", - "CONTROLLEDPROPERTY" : "Kontrol egenskaber", - "CATEGORY" : "Kategori", - "SUPPORTEDUNITS" : "Måleenheder", - "FUNCTION" : "Enhedsfunktioner", - "ENERGYLIMITATIONCLASS" : "Energibegrænsningsklassen", - "SUPPORTEDPROTOCOL" : "Understøttede protokoller", + "TYPE": "Type", + "BRANDNAME": "Mærke", + "MODELNAME": "Model", + "MANUFACTURERNAME": "Producentens navn", + "CONTROLLEDPROPERTY": "Kontrol egenskaber", + "CATEGORY": "Kategori", + "SUPPORTEDUNITS": "Måleenheder", + "FUNCTION": "Enhedsfunktioner", + "ENERGYLIMITATIONCLASS": "Energibegrænsningsklassen", + "SUPPORTEDPROTOCOL": "Understøttede protokoller", "DETAIL-TITLE": "Device model detalje", "DEVICE-MODEL": "Device nodel", "SHOW-OPTIONS": "Håndter device model" @@ -344,7 +350,7 @@ "NAME": "Navn", "TYPE": "Type" }, - "MULTICAST-TABLE":{ + "MULTICAST-TABLE": { "NAME": "Gruppenavn", "TYPE": "Gruppetype" }, @@ -369,7 +375,7 @@ "DELETE": "Slet", "EDIT": "Redigér", "SHOW-OPTIONS": "Håndter IoT enhed", - "BATTERY-ERROR-MESSAGE":"Den valgte teknologi understøtter ikke batteristatus", + "BATTERY-ERROR-MESSAGE": "Den valgte teknologi understøtter ikke batteristatus", "RESET-API-KEY": "Nulstil API nøgle", "NOT-SUPPORTED": "Ikke understøttet", "NOT-SUPPORTED-SHORT": "-", @@ -387,9 +393,9 @@ }, "PAYLOAD-DECODER": { "DELETE-FAILED": "Slet fejlede", - "DETAILS":"Detaljer", + "DETAILS": "Detaljer", "CREATED": "Oprettet", - "UPDATED":"Senest opdateret", + "UPDATED": "Senest opdateret", "TITLE": "Payload decoder", "SAVE": "Gem payload decoder", "NAME": "Navn", @@ -409,9 +415,9 @@ "DELETE": "Slet" }, "ORGANISATION": { - "HEADLINE":"Detaljer", - "CREATED":"Oprettet", - "UPDATED":"Senest opdateret", + "HEADLINE": "Detaljer", + "CREATED": "Oprettet", + "UPDATED": "Senest opdateret", "NAME": "Navn", "APPLICATION-COUNT": "Applikationer", "SAVE": "Gem organisation", @@ -450,8 +456,8 @@ "USERS": "Brugergruppens brugere", "APPLICATIONS": "Brugergruppens applikationer", "HEADLINE": "Detaljer", - "CREATED":"Oprettet", - "UPDATED":"Senest opdateret" + "CREATED": "Oprettet", + "UPDATED": "Senest opdateret" }, "EDIT": { "NAME": "Indtast brugergruppens navn", @@ -545,7 +551,7 @@ "GIVE-PAYLOADDECODER-PAYLOAD-INVALID-JSON": "Det angivne JSON var ikke gyldigt i feltet payload", "GIVE-ORGANISATION-NAME": "Navngiv organisation", "GIVE-ORGANISATION-NAME-PLACEHOLDER": "F.eks. 'Aarhus Kommune'", - "GIVE-MULTICAST-NAME":"Navngiv multicast", + "GIVE-MULTICAST-NAME": "Navngiv multicast", "GIVE-MULTICAST-ADDRESS": "Angiv multicast adressen (mcAddr)", "GIVE-MULTICAST-NETWORK-KEY": "Angiv multicast network session key (mcNwkSKey)", "GIVE-MULTICAST-APPLICATION-KEY": "Angiv multicast application session key (mcAppSKey)", @@ -842,12 +848,12 @@ }, "PROFILES": { "NAME": "LoRaWAN profiler", - "DELETE-FAILED":"Slet fejlede", + "DELETE-FAILED": "Slet fejlede", "SERVICE_PROFILE": { - "CREATED":"Oprettet", - "UPDATED":"Senest opdateret", + "CREATED": "Oprettet", + "UPDATED": "Senest opdateret", "DETAILS": "Detaljer", - "DELETE-FROM-LIST":"Slet", + "DELETE-FROM-LIST": "Slet", "HEADLINE": "Service profiler", "DROPDOWN": "Håndter service profil", "CREATE-NEW-SERVICE-PROFILE": "Opret service profile", @@ -875,10 +881,10 @@ "ID": "Service profilens id" }, "DEVICE_PROFILE": { - "CREATED":"Oprettet", - "UPDATED":"Senest opdateret", + "CREATED": "Oprettet", + "UPDATED": "Senest opdateret", "DETAILS": "Detaljer", - "DELETE-FROM-LIST":"Slet", + "DELETE-FROM-LIST": "Slet", "HEADLINE": "Device profiler", "DROPDOWN": "Håndter device profil", "CREATE-NEW-DEVICE-PROFILE": "Opret device profile", @@ -957,11 +963,11 @@ "PERMISSIONS": "Medlem af disse brugergrupper", "APPLICATIONS": "Rettigheder til disse applikationer", "NAME-PLACEHOLDER": "Navn", - "EMAIL":"Email", - "CREATED":"Oprettet", - "UPDATED":"Senest opdateret", - "STATUS":"Status", - "HEADLINE":"Detaljer" + "EMAIL": "Email", + "CREATED": "Oprettet", + "UPDATED": "Senest opdateret", + "STATUS": "Status", + "HEADLINE": "Detaljer" }, "GLOBAL_ADMIN": { "TRUE": "Ja", @@ -978,18 +984,18 @@ "WELCOME-SUB": " Din indgang til administration af IoT enheder", "LINK-1": "OS2iot.os2.eu", "LINK-2": "løsningens produktside", - "SUB-HEADER-1":"Community", + "SUB-HEADER-1": "Community", "SUB-HEADER-2": "Om OS2IOT", - "BUTTON":"Start med at oprette en organisation", + "BUTTON": "Start med at oprette en organisation", "LIST": { "HEADLINE": "Med version 1.0 kan du:", "P1": "Håndtere dine IoT enheder ét sted", - "P2":"Få et samlet overblik over dine IoT enheder på tværs af netværksteknologier ", - "P3":"Lave bulk import af IoT enheder", - "P4":"Oprette applikationer på tværs af organisationer", - "P5":"Genbruge dekodningsfunktioner og –scripts på tværs af organisationer", - "P6":"Dele gateways og data fra IoT enheder på tværs af organisationer", - "P7":"Let offentligøre dine data og meget mere." + "P2": "Få et samlet overblik over dine IoT enheder på tværs af netværksteknologier ", + "P3": "Lave bulk import af IoT enheder", + "P4": "Oprette applikationer på tværs af organisationer", + "P5": "Genbruge dekodningsfunktioner og –scripts på tværs af organisationer", + "P6": "Dele gateways og data fra IoT enheder på tværs af organisationer", + "P7": "Let offentligøre dine data og meget mere." }, "WELCOME-MESSAGE": "Vi opfordrer dig til at blive en aktiv del af vores OS2iot community på ", "WELCOME-MESSAGE-3": " og dele dine spørgsmål, udviklingsønsker og de fejl du måtte finde. Ved fælles hjælp bliver løsningen bedre.", @@ -1035,7 +1041,6 @@ "IOTDEVICE": "OS2IoT - IoT enhed", "FRONTPAGE": "OS2IoT - Forside", "API-KEY": "OS2IoT - API nøgler" - }, "PAGINATOR": { "ITEM_PER_PAGE": "Antal pr. side", @@ -1074,18 +1079,15 @@ }, "NoUsersAdded": "Ingen brugergrupper er tilføjet", "FIWARE": { - "QUESTION": { - "GIVE-DATATARGET-CONTEXTBROKER-URL": "Angiv datatargets 'context broker' URL", - "GIVE-DATATARGET-CONTEXTBROKER-URL-PLACEHOLDER": "https://contextBroker.dk/", - "GIVE-DATATARGET-TENANT": "Angiv 'tenant'", - "GIVE-DATATARGET-TENANT-INFO": "hvis tom, vil default tenant blive brugt", - "GIVE-DATATARGET-TENANT-PLACEHOLDER": "Tenant's navn", - "GIVE-DATATARGET-CONTEXT": "Angiv 'context'", - "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" + "QUESTION": { + "GIVE-DATATARGET-CONTEXTBROKER-URL": "Angiv datatargets 'context broker' URL", + "GIVE-DATATARGET-CONTEXTBROKER-URL-PLACEHOLDER": "https://contextBroker.dk/", + "GIVE-DATATARGET-TENANT": "Angiv 'tenant'", + "GIVE-DATATARGET-TENANT-INFO": "hvis tom, vil default tenant blive brugt", + "GIVE-DATATARGET-TENANT-PLACEHOLDER": "Tenant's navn", + "GIVE-DATATARGET-CONTEXT": "Angiv 'context'", + "GIVE-DATATARGET-CONTEXT-INFO": "hvis tom, skal den angives i 'payload'", + "GIVE-DATATARGET-CONTEXT-PLACEHOLDER": "https://os2iot/context-file.json" + } } } From 0dcdcfa2bc61408cb4de6379f32eeaafb0a2b302 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 10 May 2022 11:44:24 +0200 Subject: [PATCH 06/12] Reset table page. Update gateway status logic and styling --- .../gateway-list/gateway-list.component.ts | 1 - .../gateway-status.component.html | 58 +++++++-------- .../gateway-status.component.ts | 74 +++++++++++++++---- src/app/gateway/gateway.model.ts | 7 +- .../services/chirpstack-gateway.service.ts | 5 -- 5 files changed, 92 insertions(+), 53 deletions(-) diff --git a/src/app/gateway/gateway-list/gateway-list.component.ts b/src/app/gateway/gateway-list/gateway-list.component.ts index e485d895f..061a6d81a 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.ts +++ b/src/app/gateway/gateway-list/gateway-list.component.ts @@ -85,7 +85,6 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { this.organisationChangeSubject.next(orgId); if (this.tabIndex === gatewayStatusTabIndex) { - // TODO: Race condition with organisation change event? this.isGatewayStatusVisibleSubject.next(); } diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index fd3f848a7..dd838362f 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -4,7 +4,7 @@ -
+
@@ -12,38 +12,34 @@
-
-
-
- -
- - - - - - +
+
+ +
+
{{element.name}} - {{'GEN.DATE' | translate}} -
- {{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} -
+ + + + - - - - + + + + - - -
{{element.name}} + {{'GEN.DATE' | translate}} +
+ {{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} +
- {{formatFooterDate(time)}} -
- {{formatTime(time)}} -
+ {{formatFooterDate(time)}} +
+ {{formatTime(time)}} +
-
+ + +
diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index a4489ee49..2357624a2 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -64,7 +64,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { }); this.organisationChangeSubject.subscribe((orgId) => { - if (orgId) { + if (orgId && this.organizationId !== orgId) { this.organizationId = orgId; this.isDirty = true; } @@ -102,18 +102,24 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { timeInterval = this.selectedStatusInterval ): void { this.isLoadingResults = true; + this.paginator.pageIndex = 0; this.gatewayStatusSubscription = this.getGatewayStatus( organizationId, timeInterval ).subscribe((response) => { this.isLoadingResults = false; - this.handleStatusResponse(response); + + if (response) { + this.handleStatusResponse(response); + } }); } private handleStatusResponse(response: GatewayStatusResponse) { this.resultsLength = response.count; - const sortedData = response.data + const filteredData = this.takeLatestTimestampInHour(response.data); + + const sortedData = filteredData .slice() .sort((a, b) => a.name.localeCompare(b.name)); @@ -132,7 +138,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { this.timeColumns = []; response.forEach((gateway) => { - gateway.onlineTimestamps.forEach((timestamp) => { + gateway.statusTimestamps.forEach(({ timestamp }) => { if (!minDate) { minDate = timestamp; } @@ -149,27 +155,65 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { }); if (minDate && maxDate) { - for ( - const dt = new Date(minDate); - dt <= new Date(maxDate); - dt.setTime(dt.getTime() + 1000 * (60 * 60 * 1)) - ) { - this.timeColumns.push(dt.toISOString()); - } + const currDate = moment(minDate).startOf('hour'); + const lastDate = moment(maxDate).startOf('hour'); + + do { + this.timeColumns.push(currDate.toISOString()); + } while (currDate.add(1, 'hour').startOf('hour').diff(lastDate) <= 0); } this.displayedColumns = [this.columnGatewayName].concat(this.timeColumns); } + private takeLatestTimestampInHour(data: GatewayStatus[]): typeof data { + return data.map((gateway) => { + const timestamps = gateway.statusTimestamps.reduce( + (res: typeof gateway.statusTimestamps, currentStatus) => { + // Check if we already passed a timestamp in the same hour slot as the current one and if it's older + const sameHourTimestampIndex = res.findIndex((storedStatus) => { + const storedTimestamp = moment(storedStatus.timestamp); + return storedTimestamp.isSame(currentStatus.timestamp, 'hour'); + }); + + if (sameHourTimestampIndex >= 0) { + // Only keep the latest timestamp in the same slot + if ( + res[sameHourTimestampIndex].timestamp < currentStatus.timestamp + ) { + res.splice(sameHourTimestampIndex, 1); + } else { + // Don't store the current timestamp as it's older than the stored one + return res; + } + } + + res.push(currentStatus); + return res; + }, + [] + ); + + return { + ...gateway, + statusTimestamps: timestamps, + }; + }); + } + private clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } getStatusClass(gateway: GatewayStatus, timestamp: string) { - return !gateway.onlineTimestamps.length + return !gateway.statusTimestamps.length ? 'never-seen' - : gateway.onlineTimestamps.some((gatewayTimestamp) => - moment(gatewayTimestamp).isSame(moment(timestamp), 'hour') + : gateway.statusTimestamps.some( + (gatewayTimestamp) => + moment(gatewayTimestamp.timestamp).isSame( + moment(timestamp), + 'hour' + ) && gatewayTimestamp.wasOnline ) ? 'online' : 'offline'; @@ -197,7 +241,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { } formatTooltip(gateway: GatewayStatus, timestamp: string): string { - const formattedTime = !gateway.onlineTimestamps.length + const formattedTime = !gateway.statusTimestamps.length ? this.neverSeenText : moment(timestamp).format('DD-MM-YYYY HH:00'); return `${this.nameText}: ${gateway.name}\n${this.timestampText}: ${formattedTime}`; diff --git a/src/app/gateway/gateway.model.ts b/src/app/gateway/gateway.model.ts index d36ad5309..34c7c6e89 100644 --- a/src/app/gateway/gateway.model.ts +++ b/src/app/gateway/gateway.model.ts @@ -66,10 +66,15 @@ export interface GetGatewayStatusParameters { timeInterval?: GatewayStatusInterval; } +export interface StatusTimestamp { + timestamp: Date; + wasOnline: boolean; +} + export interface GatewayStatus { id: string; name: string; - onlineTimestamps: Date[]; + statusTimestamps: StatusTimestamp[]; } export interface GatewayStatusResponse { diff --git a/src/app/shared/services/chirpstack-gateway.service.ts b/src/app/shared/services/chirpstack-gateway.service.ts index 391c404a7..fd7a13ae0 100644 --- a/src/app/shared/services/chirpstack-gateway.service.ts +++ b/src/app/shared/services/chirpstack-gateway.service.ts @@ -56,11 +56,6 @@ export class ChirpstackGatewayService { ); } - // TODO: GATEWAY: Implement fetch - public getStatus(id: number): Observable { - return null; - } - public post(gateway: Gateway): Observable { const gatewayRequest: GatewayRequest = new GatewayRequest; gatewayRequest.gateway = gateway; From d3ecd2ba4ca4bfbc7615ba5a7804f9dbe9836605 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 10 May 2022 13:05:20 +0200 Subject: [PATCH 07/12] Add parent options for paginator and title --- .../gateway-status/gateway-status.component.html | 3 ++- .../gateway-status/gateway-status.component.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index dd838362f..19954e1e5 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -5,6 +5,7 @@
+

{{title}}

@@ -41,7 +42,7 @@
- +
diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index 2357624a2..9c6f387b8 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -26,6 +26,8 @@ import { GatewayStatus, GatewayStatusResponse } from '../gateway.model'; export class GatewayStatusComponent implements AfterContentInit, OnDestroy { @Input() organisationChangeSubject: Subject; @Input() isVisibleSubject: Subject; + @Input() paginatorClass: string; + @Input() title: string; private gatewayStatusSubscription: Subscription; private readonly columnGatewayName = 'gatewayName'; @@ -63,20 +65,20 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { this.timestampText = translations['LORA-GATEWAY-STATUS.TIMESTAMP']; }); - this.organisationChangeSubject.subscribe((orgId) => { + this.organisationChangeSubject?.subscribe((orgId) => { if (orgId && this.organizationId !== orgId) { this.organizationId = orgId; this.isDirty = true; } }); - this.isVisibleSubject.pipe().subscribe(() => { + this.isVisibleSubject?.pipe().subscribe(() => { if (!this.isDirty) { return; } this.isDirty = false; - this.subscribeToGetGatewayStatus(); + this.subscribeToGetAllGatewayStatus(); }); this.statusIntervals = recordToEntries(GatewayStatusInterval).map( @@ -97,7 +99,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { }); } - private subscribeToGetGatewayStatus( + private subscribeToGetAllGatewayStatus( organizationId = this.organizationId, timeInterval = this.selectedStatusInterval ): void { @@ -228,7 +230,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { newInterval !== this.selectedStatusInterval && this.dataSource?.data.length ) { - this.subscribeToGetGatewayStatus(this.organizationId, newInterval); + this.subscribeToGetAllGatewayStatus(this.organizationId, newInterval); } } From e05ae85985110c030af32ae6d2ab1f3063d19f0c Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 10 May 2022 13:12:15 +0200 Subject: [PATCH 08/12] Fix gateway status not fetching on interval change --- .../gateway/gateway-status/gateway-status.component.html | 8 +++++--- .../gateway/gateway-status/gateway-status.component.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index 19954e1e5..73548f9c7 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -21,7 +21,7 @@

{{title}}

*ngIf="dataSource?.data.length && timeColumns.length; else noGatewayStatusData"> {{element.name}} - + {{'GEN.DATE' | translate}}
{{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} @@ -31,7 +31,8 @@

{{title}}

- + {{formatFooterDate(time)}}
{{formatTime(time)}} @@ -42,7 +43,8 @@

{{title}}

- +
diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index 9c6f387b8..959a6eaec 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -228,7 +228,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { if ( isUserInput && newInterval !== this.selectedStatusInterval && - this.dataSource?.data.length + !this.isLoadingResults ) { this.subscribeToGetAllGatewayStatus(this.organizationId, newInterval); } From f70c4d3ecf87bdd5ba88b303bc50212632b8420e Mon Sep 17 00:00:00 2001 From: nlg Date: Tue, 10 May 2022 17:11:07 +0200 Subject: [PATCH 09/12] Added null checks on gateway details page --- src/app/auth/auth-guard.service.ts | 4 ++-- .../gateway-detail/gateway-detail.component.ts | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app/auth/auth-guard.service.ts b/src/app/auth/auth-guard.service.ts index 4331ea24b..055f9685c 100644 --- a/src/app/auth/auth-guard.service.ts +++ b/src/app/auth/auth-guard.service.ts @@ -11,8 +11,8 @@ export class AuthGuardService { canActivate(route: ActivatedRouteSnapshot): boolean { // Allow KOMBIT adgangsstyring callback to work if ( - route.queryParams['jwt'] != undefined || - route.queryParams['error'] != undefined + route.queryParams['jwt'] !== undefined || + route.queryParams['error'] !== undefined ) { return true; } diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.ts b/src/app/gateway/gateway-detail/gateway-detail.component.ts index b3230041e..ca34e8e79 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.ts +++ b/src/app/gateway/gateway-detail/gateway-detail.component.ts @@ -52,7 +52,10 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit this.translate.get(['NAV.LORA-GATEWAYS']).subscribe((translations) => { this.backButton.label = translations['NAV.LORA-GATEWAYS']; }); - this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, this.gateway.internalOrganizationId); + + if (this.gateway) { + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, this.gateway.internalOrganizationId); + } } ngAfterViewInit() { @@ -93,11 +96,13 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit editRouterLink: '../../gateway-edit/' + this.id, isErasable: true, } : null; + this.translate.get(['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS']) .subscribe(translations => { - this.dropdownButton.label = translations['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS']; - } - ); + if (this.dropdownButton) { + this.dropdownButton.label = translations['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS']; + } + }); } onDeleteGateway() { From 4c43bb8bcc2d054b8f8ec8c032b49904b73c1f64 Mon Sep 17 00:00:00 2001 From: Aram Al-Sabti Date: Tue, 10 May 2022 17:27:25 +0200 Subject: [PATCH 10/12] Optimize performance on gateway status --- .../gateway-status.component.html | 18 ++-- .../gateway-status.component.scss | 1 + .../gateway-status.component.ts | 96 ++++++++++--------- src/app/gateway/gateway.model.ts | 2 +- .../gateway/gateway-status-class.pipe.ts | 27 ++++++ .../gateway/gateway-status-tooltip.pipe.ts | 23 +++++ src/app/shared/pipes/pipes.module.ts | 10 +- 7 files changed, 122 insertions(+), 55 deletions(-) create mode 100644 src/app/shared/pipes/gateway/gateway-status-class.pipe.ts create mode 100644 src/app/shared/pipes/gateway/gateway-status-tooltip.pipe.ts diff --git a/src/app/gateway/gateway-status/gateway-status.component.html b/src/app/gateway/gateway-status/gateway-status.component.html index 73548f9c7..26bf6853c 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.html +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -20,7 +20,9 @@

{{title}}

- + - - - + diff --git a/src/app/gateway/gateway-status/gateway-status.component.scss b/src/app/gateway/gateway-status/gateway-status.component.scss index e1db399ce..ed89726e2 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.scss +++ b/src/app/gateway/gateway-status/gateway-status.component.scss @@ -36,6 +36,7 @@ $cellFontSize: 0.9rem; &:not(:first-child) { max-width: 1.2rem; + border-radius: 3px; } &:first-child { diff --git a/src/app/gateway/gateway-status/gateway-status.component.ts b/src/app/gateway/gateway-status/gateway-status.component.ts index 959a6eaec..a95b0ff21 100644 --- a/src/app/gateway/gateway-status/gateway-status.component.ts +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -12,12 +12,18 @@ import { environment } from '@environments/environment'; import { TranslateService } from '@ngx-translate/core'; import { recordToEntries } from '@shared/helpers/record.helper'; import { LoRaWANGatewayService } from '@shared/services/lorawan-gateway.service'; -import { SharedVariableService } from '@shared/shared-variable/shared-variable.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'; +interface TimeColumn { + exactTimestamp: string; + tooltip: string; + datePart: string; + timePart: string; +} + @Component({ selector: 'app-gateway-status', templateUrl: './gateway-status.component.html', @@ -32,15 +38,18 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { private gatewayStatusSubscription: Subscription; private readonly columnGatewayName = 'gatewayName'; dataSource: MatTableDataSource; - timeColumns: string[] = []; - displayedColumns: string[] = []; + /** + * List of pre-processed timestamps for performance + */ + timeColumns: TimeColumn[] = []; + displayedColumns: (TimeColumn | string)[] = []; nameText = ''; neverSeenText = ''; timestampText = ''; visibleFooterTimeInterval = 1; pageSize = environment.tablePageSize; resultsLength = 0; - organizationId: number; + organizationId: number | undefined; isLoadingResults = false; isDirty = true; statusIntervals: GatewayStatusInterval[]; @@ -50,13 +59,10 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { constructor( private translate: TranslateService, - private sharedVariableService: SharedVariableService, private lorawanGatewayService: LoRaWANGatewayService ) {} ngAfterContentInit(): void { - this.organizationId = this.sharedVariableService.getSelectedOrganisationId(); - this.translate .get(['GEN.NAME', 'GEN.NEVER-SEEN', 'LORA-GATEWAY-STATUS.TIMESTAMP']) .subscribe((translations) => { @@ -66,7 +72,7 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { }); this.organisationChangeSubject?.subscribe((orgId) => { - if (orgId && this.organizationId !== orgId) { + if (this.organizationId !== orgId) { this.organizationId = orgId; this.isDirty = true; } @@ -90,13 +96,18 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { organizationId = this.organizationId, timeInterval = this.selectedStatusInterval ): Observable { - return this.lorawanGatewayService.getAllStatus({ - organizationId, + const params: Record = { timeInterval, // Paginator is only avaiable in ngAfterViewInit limit: this.paginator?.pageSize, offset: this.paginator?.pageIndex * this.paginator.pageSize, - }); + }; + + if (organizationId) { + params.organizationId = organizationId; + } + + return this.lorawanGatewayService.getAllStatus(params); } private subscribeToGetAllGatewayStatus( @@ -119,9 +130,14 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { private handleStatusResponse(response: GatewayStatusResponse) { this.resultsLength = response.count; - const filteredData = this.takeLatestTimestampInHour(response.data); + const gatewaysWithLatestTimestampsPerHour = this.takeLatestTimestampInHour( + response.data + ); + const gatewaysWithWholeHourTimestamps = this.toWholeHour( + gatewaysWithLatestTimestampsPerHour + ); - const sortedData = filteredData + const sortedData = gatewaysWithWholeHourTimestamps .slice() .sort((a, b) => a.name.localeCompare(b.name)); @@ -161,11 +177,29 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { const lastDate = moment(maxDate).startOf('hour'); do { - this.timeColumns.push(currDate.toISOString()); + this.timeColumns.push({ + exactTimestamp: currDate.toISOString(), + tooltip: currDate.format('DD-MM-YYYY HH:00'), + datePart: currDate.format('DD-MM'), + timePart: currDate.format('HH:00'), + }); } while (currDate.add(1, 'hour').startOf('hour').diff(lastDate) <= 0); } - this.displayedColumns = [this.columnGatewayName].concat(this.timeColumns); + this.displayedColumns = [ + this.columnGatewayName, + ...this.timeColumns.map((column) => column.exactTimestamp), + ]; + } + + private toWholeHour(data: GatewayStatus[]): typeof data { + return data.map((gateway) => ({ + ...gateway, + statusTimestamps: gateway.statusTimestamps.map((status) => ({ + ...status, + timestamp: moment(status.timestamp).startOf('hour').toDate(), + })), + })); } private takeLatestTimestampInHour(data: GatewayStatus[]): typeof data { @@ -173,9 +207,10 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { const timestamps = gateway.statusTimestamps.reduce( (res: typeof gateway.statusTimestamps, currentStatus) => { // Check if we already passed a timestamp in the same hour slot as the current one and if it's older + const currentTimestamp = moment(currentStatus.timestamp); const sameHourTimestampIndex = res.findIndex((storedStatus) => { const storedTimestamp = moment(storedStatus.timestamp); - return storedTimestamp.isSame(currentStatus.timestamp, 'hour'); + return storedTimestamp.isSame(currentTimestamp, 'hour'); }); if (sameHourTimestampIndex >= 0) { @@ -207,20 +242,6 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { return Math.max(min, Math.min(max, value)); } - getStatusClass(gateway: GatewayStatus, timestamp: string) { - return !gateway.statusTimestamps.length - ? 'never-seen' - : gateway.statusTimestamps.some( - (gatewayTimestamp) => - moment(gatewayTimestamp.timestamp).isSame( - moment(timestamp), - 'hour' - ) && gatewayTimestamp.wasOnline - ) - ? 'online' - : 'offline'; - } - onSelectInterval({ isUserInput, source: { value: newInterval }, @@ -234,21 +255,6 @@ export class GatewayStatusComponent implements AfterContentInit, OnDestroy { } } - formatFooterDate(timestamp: string): string { - return moment(timestamp).format('DD-MM'); - } - - formatTime(timestamp: string): string { - return moment(timestamp).format('HH:00'); - } - - formatTooltip(gateway: GatewayStatus, timestamp: string): string { - const formattedTime = !gateway.statusTimestamps.length - ? this.neverSeenText - : moment(timestamp).format('DD-MM-YYYY HH:00'); - return `${this.nameText}: ${gateway.name}\n${this.timestampText}: ${formattedTime}`; - } - ngOnDestroy() { // prevent memory leak by unsubscribing this.gatewayStatusSubscription?.unsubscribe(); diff --git a/src/app/gateway/gateway.model.ts b/src/app/gateway/gateway.model.ts index 34c7c6e89..1da46e785 100644 --- a/src/app/gateway/gateway.model.ts +++ b/src/app/gateway/gateway.model.ts @@ -62,7 +62,7 @@ export interface GatewayStats { export interface GetGatewayStatusParameters { limit?: number; offset?: number; - organizationId: number; + organizationId?: number; timeInterval?: GatewayStatusInterval; } diff --git a/src/app/shared/pipes/gateway/gateway-status-class.pipe.ts b/src/app/shared/pipes/gateway/gateway-status-class.pipe.ts new file mode 100644 index 000000000..863e62fd3 --- /dev/null +++ b/src/app/shared/pipes/gateway/gateway-status-class.pipe.ts @@ -0,0 +1,27 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { StatusTimestamp } from '@app/gateway/gateway.model'; + +@Pipe({ + name: 'gatewayStatusClass', +}) +/** + * Separate pipe to format text to avoid renders if none of the values + * have changed. + */ +export class GatewayStatusClassPipe implements PipeTransform { + transform( + statusTimestamps: StatusTimestamp[], + timestamp: string, + ..._: unknown[] + ): string { + return !statusTimestamps.length + ? 'never-seen' + : statusTimestamps.some( + (gatewayTimestamp) => + gatewayTimestamp.timestamp.toISOString() === timestamp && + gatewayTimestamp.wasOnline + ) + ? 'online' + : 'offline'; + } +} diff --git a/src/app/shared/pipes/gateway/gateway-status-tooltip.pipe.ts b/src/app/shared/pipes/gateway/gateway-status-tooltip.pipe.ts new file mode 100644 index 000000000..cbc272ffe --- /dev/null +++ b/src/app/shared/pipes/gateway/gateway-status-tooltip.pipe.ts @@ -0,0 +1,23 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'gatewayStatusTooltip', +}) +/** + * Separate pipe to format text to avoid renders if none of the values + * have changed. + */ +export class GatewayStatusTooltipPipe implements PipeTransform { + transform( + hasBeenSeen: boolean, + neverSeenText: string, + timestampLabel: string, + timestamp: string, + nameLabel: string, + name: string, + ..._: unknown[] + ): string { + const formattedTime = !hasBeenSeen ? neverSeenText : timestamp; + return `${nameLabel}: ${name}\n${timestampLabel}: ${formattedTime}`; + } +} diff --git a/src/app/shared/pipes/pipes.module.ts b/src/app/shared/pipes/pipes.module.ts index 7d9119436..ea2159ad6 100644 --- a/src/app/shared/pipes/pipes.module.ts +++ b/src/app/shared/pipes/pipes.module.ts @@ -7,6 +7,8 @@ import { CreatedUpdatedByPipe } from './created-updated-by.pipe'; import { CustomDatePipe, CustomTableDatePipe, DateOnlyPipe } from './custom-date.pipe'; import { FilterDevicesPipe } from './filter-devices.pipe'; import { SortByPipe } from './sort-by.pipe'; +import { GatewayStatusTooltipPipe } from './gateway/gateway-status-tooltip.pipe'; +import { GatewayStatusClassPipe } from './gateway/gateway-status-class.pipe'; @NgModule({ declarations: [ @@ -18,7 +20,9 @@ import { SortByPipe } from './sort-by.pipe'; DateOnlyPipe, CreatedUpdatedByPipe, FilterDevicesPipe, - SortByPipe + SortByPipe, + GatewayStatusTooltipPipe, + GatewayStatusClassPipe ], imports: [CommonModule], exports: [ @@ -30,7 +34,9 @@ import { SortByPipe } from './sort-by.pipe'; DateOnlyPipe, CreatedUpdatedByPipe, FilterDevicesPipe, - SortByPipe + SortByPipe, + GatewayStatusTooltipPipe, + GatewayStatusClassPipe ], providers: [ DateOnlyPipe From 73054c3cc2420e10500c86d5b022011228b5e224 Mon Sep 17 00:00:00 2001 From: nlg Date: Wed, 11 May 2022 08:42:01 +0200 Subject: [PATCH 11/12] Fixes for gateway admin role --- src/app/admin/permission/permission.model.ts | 1 - .../gateway-detail/gateway-detail.component.ts | 3 ++- .../organisation-dropdown.component.html | 14 ++++++++------ .../organisation-dropdown.component.ts | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/app/admin/permission/permission.model.ts b/src/app/admin/permission/permission.model.ts index b80ef7fd0..ab7690b91 100644 --- a/src/app/admin/permission/permission.model.ts +++ b/src/app/admin/permission/permission.model.ts @@ -38,7 +38,6 @@ export enum PermissionType { OrganizationGatewayAdmin = 'OrganizationGatewayAdmin', OrganizationApplicationAdmin = 'OrganizationApplicationAdmin', Read = 'Read', - OrganizationPermission = 'OrganizationPermission', OrganizationApplicationPermissions = 'OrganizationApplicationPermissions', } diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.ts b/src/app/gateway/gateway-detail/gateway-detail.component.ts index ca34e8e79..cb7ad4548 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.ts +++ b/src/app/gateway/gateway-detail/gateway-detail.component.ts @@ -35,7 +35,7 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit private deleteDialogSubscription: Subscription; public dropdownButton: DropdownButton; isLoadingResults = true; - canEdit: boolean; + canEdit: boolean; constructor( private gatewayService: ChirpstackGatewayService, @@ -79,6 +79,7 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit this.gatewayService.get(id).subscribe((result: any) => { result.gateway.tagsString = JSON.stringify(result.gateway.tags); this.gateway = result.gateway; + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, this.gateway.internalOrganizationId); this.gateway.canEdit = this.canEdit; this.gatewayStats = result.stats; this.gatewayStats.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html index b119f3b7d..7e1dccfea 100644 --- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html +++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html @@ -19,12 +19,14 @@ -
  • - - {{'NAV.DEVICE-MODEL' | translate}} - - -
  • +
    {{element.name}} + {{element.name}} + {{'GEN.DATE' | translate}}
    @@ -28,14 +30,16 @@

    {{title}}

    + - {{formatFooterDate(time)}} + {{time.datePart}}
    - {{formatTime(time)}} + {{time.timePart}}