diff --git a/src/app/admin/api-key/api-key-list/api-key-list.component.html b/src/app/admin/api-key/api-key-list/api-key-list.component.html index 8ecf306ee..4960433b4 100644 --- a/src/app/admin/api-key/api-key-list/api-key-list.component.html +++ b/src/app/admin/api-key/api-key-list/api-key-list.component.html @@ -2,6 +2,7 @@ [title]="'NAV.API-KEY' | translate" [ctaLabel]="'API-KEY.CREATE-NEW-API-KEY' | translate" [ctaRouterLink]="'new-api-key'" + [canEdit]="canEdit" >
diff --git a/src/app/admin/api-key/api-key-list/api-key-list.component.ts b/src/app/admin/api-key/api-key-list/api-key-list.component.ts index f422eac11..9fdf53d36 100644 --- a/src/app/admin/api-key/api-key-list/api-key-list.component.ts +++ b/src/app/admin/api-key/api-key-list/api-key-list.component.ts @@ -2,6 +2,8 @@ import { Component, Input, 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-api-key-list', @@ -10,11 +12,13 @@ import { SharedVariableService } from '@shared/shared-variable/shared-variable.s }) export class ApiKeyListComponent implements OnInit { @Input() organisationId: number; + canEdit: boolean; constructor( public translate: TranslateService, private titleService: Title, - private globalService: SharedVariableService + private globalService: SharedVariableService, + private meService: MeService ) { translate.use('da'); } @@ -24,5 +28,6 @@ export class ApiKeyListComponent implements OnInit { this.titleService.setTitle(translations['TITLE.API-KEY']); }); this.organisationId = this.globalService.getSelectedOrganisationId(); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.UserAdministrationWrite, this.organisationId); } } diff --git a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts index 208d9c8e9..a669a2331 100644 --- a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts +++ b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts @@ -9,6 +9,7 @@ import { merge, Observable, of } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { ApiKeyGetManyResponse, ApiKeyResponse } from '../../api-key.model'; import { ApiKeyService } from '../../api-key.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-api-key-table', @@ -81,7 +82,8 @@ export class ApiKeyTableComponent implements AfterViewInit { } canAccess(_element: ApiKeyResponse) { - return this.meService.hasAdminAccessInTargetOrganization( + return this.meService.hasAccessToTargetOrganization( + OrganizationAccessScope.UserAdministrationWrite, this.organisationId ); } 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 7559e47a4..07ba2bb4a 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..ab7690b91 100644 --- a/src/app/admin/permission/permission.model.ts +++ b/src/app/admin/permission/permission.model.ts @@ -34,8 +34,9 @@ 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 @@ - +
-
\ 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 965526ce2..637e839f2 100644 --- a/src/app/applications/application-detail/application-detail.component.html +++ b/src/app/applications/application-detail/application-detail.component.html @@ -1,6 +1,7 @@
+ [addDetailDowndown]="true" [dropDownButton]="dropdownButton" (deleteSelectedInDropdown)="onDeleteApplication()" + [canEdit]="canEdit">
diff --git a/src/app/applications/application-detail/application-detail.component.ts b/src/app/applications/application-detail/application-detail.component.ts index 2b6dfdbeb..84587a0e6 100644 --- a/src/app/applications/application-detail/application-detail.component.ts +++ b/src/app/applications/application-detail/application-detail.component.ts @@ -17,6 +17,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', @@ -67,7 +68,7 @@ export class ApplicationDetailComponent implements OnInit, OnDestroy { 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 a459055d9..057c408ba 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 @@ -17,6 +17,7 @@ import { MeService } from '@shared/services/me.service'; import { merge, Observable, of as observableOf } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { DeviceType } from '@shared/enums/device-type'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; /** * @title Table retrieving data through HTTP @@ -50,7 +51,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 5d7a6293c..c6c1dc0bb 100644 --- a/src/app/applications/bulk-import/bulk-import.component.ts +++ b/src/app/applications/bulk-import/bulk-import.component.ts @@ -9,11 +9,13 @@ import { import { IoTDeviceService } from '@applications/iot-devices/iot-device.service'; import { faDownload, faTrash } from '@fortawesome/free-solid-svg-icons'; import { TranslateService } from '@ngx-translate/core'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; import { ErrorMessageService } from '@shared/error-message.service'; import { splitList } from '@shared/helpers/array.helper'; import { Download } from '@shared/helpers/download.helper'; import { BulkImportService } from '@shared/services/bulk-import.service'; import { DownloadService } from '@shared/services/download.service'; +import { MeService } from '@shared/services/me.service'; import { Papa } from 'ngx-papaparse'; import { Observable, Subject } from 'rxjs'; import { takeWhile } from 'rxjs/operators'; @@ -57,6 +59,7 @@ export class BulkImportComponent implements OnInit { private bulkMapper = new BulkMapping(); public backButtonTitle: string; private applicationId; + canEdit: boolean; constructor( private papa: Papa, @@ -66,6 +69,7 @@ export class BulkImportComponent implements OnInit { private translate: TranslateService, private downloads: DownloadService, private errorMessageService: ErrorMessageService, + private meService: MeService, private bulkImportService: BulkImportService ) { this.translate.use('da'); @@ -76,6 +80,9 @@ 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 + ); } download({ name, url }: { name: string; url: string }) { 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 1c0c268cc..68709486e 100644 --- a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html +++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html @@ -1,3 +1,3 @@
-
\ No newline at end of file +
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 bb9f14e19..c5cc8ced7 100644 --- a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html +++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html @@ -1,3 +1,3 @@
-
\ No newline at end of file +
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 65c049068..def5822c5 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; @@ -48,7 +49,7 @@ export class DatatargetTableComponent implements OnInit, AfterViewInit, OnDestro ngOnInit(): void { this.applicationId = +Number(this.route.parent.snapshot.paramMap.get('id')); this.getDatatarget(); - this.canEdit = this.meService.canWriteInTargetOrganization() + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } ngAfterViewInit() { 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 fed1a65ea..f48f868de 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() { diff --git a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.html b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.html index 9a1424abe..af9695919 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()" (extraDropdownOptions)="clickExtraDropdownOption($event)" + [canEdit]="canEdit"> @@ -63,7 +64,7 @@
- +
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..2b445c662 100644 --- a/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts +++ b/src/app/applications/iot-devices/iot-device-detail/iot-device-detail.component.ts @@ -18,6 +18,7 @@ import { MatTabChangeEvent } from '@angular/material/tabs'; import { ChartConfiguration } from 'chart.js'; import * as moment from 'moment'; import { recordToEntries } from '@shared/helpers/record.helper'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; const colorGraphBlue1 = '#03AEEF'; @@ -70,7 +71,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { public errorMessages: string[]; private deleteDialogSubscription: Subscription; public dropdownButton: DropdownButton; - public canStartDownlink = false; + public canEdit = false; private resetApiKeyId = 'RESET-API-KEY'; private resetApiKeyOption: ExtraDropdownOption; @@ -113,7 +114,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) { @@ -163,7 +164,7 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { } if ( - this.meService.canWriteInTargetOrganization() && + this.canEdit && this.device.type === DeviceType.GENERIC_HTTP ) { this.dropdownButton.extraOptions.push(this.resetApiKeyOption); @@ -226,7 +227,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/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 a771f655c..9fbb6b98f 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 @@ - +
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 355797797..6b7d316cc 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 @@ -22,6 +22,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({ @@ -52,6 +54,7 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { private serviceProfilesSubscription: Subscription; private deviceProfileSubscription: Subscription; private devicesProfileSubscription: Subscription; + canEdit: boolean; constructor( private route: ActivatedRoute, @@ -66,7 +69,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 { @@ -89,6 +93,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 { @@ -99,8 +104,8 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { this.deviceModelService.getMultiple( 1000, 0, - "id", - "ASC", + 'id', + 'ASC', this.shareVariable.getSelectedOrganisationId() ).subscribe( (response) => { @@ -229,7 +234,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.GENERIC_HTTP: { @@ -318,11 +323,11 @@ export class IotDeviceEditComponent implements OnInit, OnDestroy { } handleError(error: Pick) { - 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 5bad6d57d..6a261cfc2 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 @@ -23,6 +23,7 @@ import { merge, Observable, of as observableOf } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { RestService } from 'src/app/shared/services/rest.service'; import { IoTDeviceService } from '../iot-device.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-iot-devices-table', @@ -52,8 +53,6 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { batteryStatusPercentage = 50; resultsLength = 0; isLoadingResults = true; - noValueText: string; - toText: string; constructor( private restService: RestService, @@ -68,12 +67,7 @@ export class IotDevicesTableComponent implements AfterViewInit, OnInit { } ngOnInit() { - this.canEdit = this.meService.canWriteInTargetOrganization(); - this.translate.get(['IOTDEVICE-TABLE-ROW.NOT-AVAILABLE', 'GEN.to']) - .subscribe(translations => { - this.noValueText = translations['IOTDEVICE-TABLE-ROW.NOT-AVAILABLE']; - this.toText = translations['GEN.to']; - }); + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.ApplicationWrite); } ngAfterViewInit() { diff --git a/src/app/applications/multicast/multicast-table/multicast-table.component.ts b/src/app/applications/multicast/multicast-table/multicast-table.component.ts index 54225d5cc..cd6b95714 100644 --- a/src/app/applications/multicast/multicast-table/multicast-table.component.ts +++ b/src/app/applications/multicast/multicast-table/multicast-table.component.ts @@ -18,6 +18,7 @@ import { merge, Observable, Subscription, of as observableOf } from 'rxjs'; import { catchError, map, startWith, switchMap } from 'rxjs/operators'; import { Multicast, MulticastData } from '../multicast.model'; import { MulticastService } from '../multicast.service'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; @Component({ selector: 'app-multicast-table', @@ -53,7 +54,9 @@ export class MulticastTableComponent ngOnInit(): void { this.applicationId = +Number(this.route.parent.snapshot.paramMap.get('id')); - this.canEdit = this.meService.canWriteInTargetOrganization(); + this.canEdit = this.meService.hasAccessToTargetOrganization( + OrganizationAccessScope.ApplicationWrite + ); } ngAfterViewInit() { 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/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/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-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..cb7ad4548 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,13 @@ 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']; + }); + + if (this.gateway) { + this.canEdit = this.meService.hasAccessToTargetOrganization(OrganizationAccessScope.GatewayWrite, this.gateway.internalOrganizationId); + } } ngAfterViewInit() { @@ -75,7 +79,8 @@ 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.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()); this.dataSource.data = this.gatewayStats; @@ -87,20 +92,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'] - } - ); - } - - canEdit(): boolean { - return this.meService.canWriteInTargetOrganization(this.gateway.internalOrganizationId); + if (this.dropdownButton) { + this.dropdownButton.label = translations['LORA-GATEWAY-TABLE-ROW.SHOW-OPTIONS']; + } + }); } onDeleteGateway() { diff --git a/src/app/gateway/gateway-list/gateway-list.component.html b/src/app/gateway/gateway-list/gateway-list.component.html index 8c92e32f7..4b7295c1e 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">
@@ -7,7 +8,7 @@ [dropdownLabel]="'GATEWAY.DROPDOWNFILTER' | translate" (updateSelectedOpt)="setOrgIdFilter($event)" [dropdownDefaultOption]="'GATEWAY.DROPDOWNDEFAULT' | translate"> - +
@@ -20,6 +21,12 @@
+ +
+ +
+
-
\ 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..7cea977a5 100644 --- a/src/app/gateway/gateway-list/gateway-list.component.ts +++ b/src/app/gateway/gateway-list/gateway-list.component.ts @@ -11,7 +11,10 @@ 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'; +import { OrganizationAccessScope } from '@shared/enums/access-scopes'; +const gatewayStatusTabIndex = 2; @Component({ selector: 'app-gateway-list', @@ -41,7 +44,10 @@ 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(); + canEdit: boolean; constructor( public translate: TranslateService, @@ -49,7 +55,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'); } @@ -61,6 +68,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() { @@ -75,10 +83,15 @@ 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) { + this.isGatewayStatusVisibleSubject.next(); + } + + this.filterGatewayByOrgId(orgId); } private getGateways(): void { @@ -120,14 +133,18 @@ export class GatewayListComponent implements OnInit, OnChanges, OnDestroy { ); } - showMap(event: any) { - if (event.index === 1) { + selectedTabChange({index}: MatTabChangeEvent) { + this.tabIndex = index; + + if (index === 1) { if (this.selectedOrg) { this.getGatewayWith(this.selectedOrg); } else { this.getGateways(); } this.showmap = true; + } else if (index === gatewayStatusTabIndex) { + this.isGatewayStatusVisibleSubject.next(); } } @@ -177,19 +194,14 @@ 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); } ); } 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 new file mode 100644 index 000000000..26bf6853c --- /dev/null +++ b/src/app/gateway/gateway-status/gateway-status.component.html @@ -0,0 +1,66 @@ + + +

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

+
+
+ +
+

{{title}}

+ + + {{ 'LORA-GATEWAY-STATUS.INTERVAL.' + interval | translate }} + + +
+
+
+ +
+ + + + + + + + + + + + + +
+ {{element.name}} + + {{'GEN.DATE' | translate}} +
+ {{'LORA-GATEWAY-STATUS.TIMESTAMP' | translate}} +
+ {{time.datePart}} +
+ {{time.timePart}} +
+
+ + +
+
+
+ {{ "GEN.ONLINE" | translate}} +
+
+
+ {{ "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 new file mode 100644 index 000000000..ed89726e2 --- /dev/null +++ b/src/app/gateway/gateway-status/gateway-status.component.scss @@ -0,0 +1,101 @@ +@import '~src/assets/scss/setup/variables'; + +$online: #56b257; +$offline: #e74c3c; +$neverSeen: #acacac; +$cellFontSize: 0.9rem; + +.status-table { + border-collapse: separate; + border-spacing: 10px 4px; + + .online { + background-color: $online; + } + .offline { + background-color: $offline; + } + .never-seen { + background-color: $neverSeen; + } + + tr { + &:hover { + background-color: unset; + } + + > td { + font-size: $cellFontSize; + &:hover { + transition: $transition-short; + filter: brightness(0.65); + } + + &.mat-cell { + min-width: 1.2rem; + + &:not(:first-child) { + max-width: 1.2rem; + border-radius: 3px; + } + + &:first-child { + padding-left: 0; + min-width: 5rem; + } + + &:last-child { + max-width: 1.2rem; + padding-right: 0; + } + } + + &.mat-footer-cell { + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } + } + } +} + +.status-legend { + .legend { + width: 5rem; + height: 8px; + + &.online { + background-color: $online; + } + &.offline { + background-color: $offline; + } + &.never-seen { + background-color: $neverSeen; + } + } + + span { + color: rgba($color: #000, $alpha: 0.6); + font-size: $cellFontSize; + } +} + +.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 new file mode 100644 index 000000000..a95b0ff21 --- /dev/null +++ b/src/app/gateway/gateway-status/gateway-status.component.ts @@ -0,0 +1,262 @@ +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 * 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', + styleUrls: ['./gateway-status.component.scss'], +}) +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'; + dataSource: MatTableDataSource; + /** + * List of pre-processed timestamps for performance + */ + timeColumns: TimeColumn[] = []; + displayedColumns: (TimeColumn | string)[] = []; + nameText = ''; + neverSeenText = ''; + timestampText = ''; + visibleFooterTimeInterval = 1; + pageSize = environment.tablePageSize; + resultsLength = 0; + organizationId: number | undefined; + isLoadingResults = false; + isDirty = true; + statusIntervals: GatewayStatusInterval[]; + selectedStatusInterval = GatewayStatusInterval.DAY; + + @ViewChild(MatPaginator) paginator: MatPaginator; + + constructor( + private translate: TranslateService, + private lorawanGatewayService: LoRaWANGatewayService + ) {} + + ngAfterContentInit(): void { + this.translate + .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']; + }); + + this.organisationChangeSubject?.subscribe((orgId) => { + if (this.organizationId !== orgId) { + this.organizationId = orgId; + this.isDirty = true; + } + }); + + this.isVisibleSubject?.pipe().subscribe(() => { + if (!this.isDirty) { + return; + } + + this.isDirty = false; + this.subscribeToGetAllGatewayStatus(); + }); + + this.statusIntervals = recordToEntries(GatewayStatusInterval).map( + (interval) => interval.value + ); + } + + private getGatewayStatus( + organizationId = this.organizationId, + timeInterval = this.selectedStatusInterval + ): Observable { + 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( + organizationId = this.organizationId, + timeInterval = this.selectedStatusInterval + ): void { + this.isLoadingResults = true; + this.paginator.pageIndex = 0; + this.gatewayStatusSubscription = this.getGatewayStatus( + organizationId, + timeInterval + ).subscribe((response) => { + this.isLoadingResults = false; + + if (response) { + this.handleStatusResponse(response); + } + }); + } + + private handleStatusResponse(response: GatewayStatusResponse) { + this.resultsLength = response.count; + const gatewaysWithLatestTimestampsPerHour = this.takeLatestTimestampInHour( + response.data + ); + const gatewaysWithWholeHourTimestamps = this.toWholeHour( + gatewaysWithLatestTimestampsPerHour + ); + + const sortedData = gatewaysWithWholeHourTimestamps + .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(sortedData); + this.dataSource.paginator = this.paginator; + } + + private buildColumns(response: GatewayStatus[]) { + let minDate: Date | null | undefined; + let maxDate: Date | null | undefined; + this.timeColumns = []; + + response.forEach((gateway) => { + gateway.statusTimestamps.forEach(({ timestamp }) => { + if (!minDate) { + minDate = timestamp; + } + if (!maxDate) { + maxDate = timestamp; + } + + if (timestamp < minDate) { + minDate = timestamp; + } else if (timestamp > maxDate) { + maxDate = timestamp; + } + }); + }); + + if (minDate && maxDate) { + const currDate = moment(minDate).startOf('hour'); + const lastDate = moment(maxDate).startOf('hour'); + + do { + 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, + ...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 { + 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 currentTimestamp = moment(currentStatus.timestamp); + const sameHourTimestampIndex = res.findIndex((storedStatus) => { + const storedTimestamp = moment(storedStatus.timestamp); + return storedTimestamp.isSame(currentTimestamp, '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)); + } + + onSelectInterval({ + isUserInput, + source: { value: newInterval }, + }: MatOptionSelectionChange) { + if ( + isUserInput && + newInterval !== this.selectedStatusInterval && + !this.isLoadingResults + ) { + this.subscribeToGetAllGatewayStatus(this.organizationId, newInterval); + } + } + + ngOnDestroy() { + // prevent memory leak by unsubscribing + this.gatewayStatusSubscription?.unsubscribe(); + } +} 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/gateway/gateway.model.ts b/src/app/gateway/gateway.model.ts index dc8d09813..1da46e785 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; @@ -57,3 +58,26 @@ export interface GatewayStats { txPacketsReceived: number; txPacketsEmitted: number; } + +export interface GetGatewayStatusParameters { + limit?: number; + offset?: number; + organizationId?: number; + timeInterval?: GatewayStatusInterval; +} + +export interface StatusTimestamp { + timestamp: Date; + wasOnline: boolean; +} + +export interface GatewayStatus { + id: string; + name: string; + statusTimestamps: StatusTimestamp[]; +} + +export interface GatewayStatusResponse { + data: GatewayStatus[]; + count: number; +} 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/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/navbar/organisation-dropdown/organisation-dropdown.component.html b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html index f6a9a1236..7e1dccfea 100644 --- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html +++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html @@ -19,13 +19,15 @@ -
  • - - {{'NAV.DEVICE-MODEL' | 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 e4bb105b3..096e69937 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 { SnackService } from '@shared/services/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: SnackService, 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 bd856163b..298e82229 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 cf691fea3..c3844d7c8 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', @@ -51,15 +50,14 @@ export class TopBarComponent implements OnInit { @Output() extraDropdownOptions = 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'); } @@ -69,7 +67,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/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 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/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 11f9d15a0..4850e2a5f 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 88efc9499..2219cffc7 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -13,7 +13,12 @@ "VALUE": "Værdi", "NAME": "Navn", "DESCRIPTION": "Beskrivelse", - "to": "til" + "to": "til", + "ONLINE": "Online", + "OFFLINE": "Offline", + "NEVER-SEEN": "Aldrig set", + "DATE": "Dato", + "NO-DATA": "Der er ingen data at vise" }, "NAV": { "DASHBOARD": "Dashboard", @@ -43,7 +48,7 @@ "HELP": "Hjælp", "API-KEY": "API nøgler" }, - "TOPBAR":{ + "TOPBAR": { "SEARCH": { "BUTTON": "Søg" }, @@ -147,7 +152,7 @@ } }, "GATEWAY": { - "DETAILS":"Detaljer", + "DETAILS": "Detaljer", "CREATED": "Oprettet", "UPDATED": "Senest opdateret", "DESCRIPTION": "LoRaWAN gateway beskrivelse", @@ -185,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", @@ -302,26 +307,35 @@ "STATUS": "Status", "ORGANIZATION": "Organisation" }, + "LORA-GATEWAY-STATUS": { + "TITLE": "LoRaWAN online status historik", + "TIMESTAMP": "Tidspunkt", + "INTERVAL": { + "DAY": "Seneste døgn", + "WEEK": "Seneste uge", + "MONTH": "Seneste måned" + } + }, "DEVICE-MODEL": { - "DELETE-FAILED":"Slet Fejlede", - "HEADLINE":"Detaljer", - "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" @@ -336,7 +350,7 @@ "NAME": "Navn", "TYPE": "Type" }, - "MULTICAST-TABLE":{ + "MULTICAST-TABLE": { "NAME": "Gruppenavn", "TYPE": "Gruppetype" }, @@ -361,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": "-", @@ -379,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", @@ -401,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", @@ -442,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", @@ -466,9 +480,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", @@ -537,7 +552,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)", @@ -834,12 +849,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", @@ -867,10 +882,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", @@ -949,11 +964,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", @@ -970,18 +985,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.", @@ -1027,7 +1042,6 @@ "IOTDEVICE": "OS2IoT - IoT enhed", "FRONTPAGE": "OS2IoT - Forside", "API-KEY": "OS2IoT - API nøgler" - }, "PAGINATOR": { "ITEM_PER_PAGE": "Antal pr. side", @@ -1066,18 +1080,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" + } } } 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 +} 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; +}