diff --git a/src/app/admin/permission/permission-edit/permission-edit.component.html b/src/app/admin/permission/permission-edit/permission-edit.component.html index cbf16ea1..29840e44 100644 --- a/src/app/admin/permission/permission-edit/permission-edit.component.html +++ b/src/app/admin/permission/permission-edit/permission-edit.component.html @@ -29,7 +29,7 @@ [placeholder]="'PERMISSION.EDIT.TYPE-PLACEHOLDER' | translate" [compareWith]="compareLevels" [multiple]="true"> - + {{'PERMISSION-TYPE.' + level.type | translate}} diff --git a/src/app/admin/users/user-page/user-page.component.html b/src/app/admin/users/user-page/user-page.component.html index 2b1a2a9a..efca403b 100644 --- a/src/app/admin/users/user-page/user-page.component.html +++ b/src/app/admin/users/user-page/user-page.component.html @@ -32,16 +32,10 @@
-
- - {{ 'NAV.ORGANISATIONS' | translate }} - - {{ 'USER_PAGE.NO_ORGS' | translate }} - +
> {{ org.name }} -
-
diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 080559ec..e66a1665 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -29,6 +29,7 @@ import { NGMaterialModule } from '@shared/Modules/materiale.module'; import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat-select-search.module'; import { UserPageComponent } from './admin/users/user-page/user-page.component'; import { SharedModule } from '@shared/shared.module'; +import { PipesModule } from '@shared/pipes/pipes.module'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json'); @@ -81,6 +82,7 @@ export function tokenGetter() { }), MonacoEditorModule.forRoot(), WelcomeDialogModule, + PipesModule, ], bootstrap: [AppComponent], exports: [TranslateModule], diff --git a/src/app/applications/applications-list/applications-list.component.ts b/src/app/applications/applications-list/applications-list.component.ts index 52f1545a..e92c9abc 100644 --- a/src/app/applications/applications-list/applications-list.component.ts +++ b/src/app/applications/applications-list/applications-list.component.ts @@ -14,6 +14,8 @@ import { OrganizationAccessScope } from '@shared/enums/access-scopes'; import { MeService } from '@shared/services/me.service'; import { WelcomeDialogModel } from '@shared/models/dialog.model'; +const welcomeDialogId = 'welcome-dialog'; + @Component({ providers: [NavbarComponent], selector: 'app-applications-list', @@ -121,16 +123,19 @@ export class ApplicationsListComponent implements OnInit { this.isGlobalAdmin = this.meService.hasGlobalAdmin(); const hasSeenWelcomeScreen = this.userMinimalService.getHasSeenWelcomeScreen(); this.hasSomePermission = this.sharedVariableService.getHasAnyPermission(); + const isOpen = !!this.dialog?.getDialogById(welcomeDialogId); if ( !this.hasSomePermission || (!this.isGlobalAdmin && !hasSeenWelcomeScreen && - userInfo?.user?.showWelcomeScreen) + userInfo?.user?.showWelcomeScreen && + !isOpen) ) { this.userMinimalService.setHasSeenWelcomeScreen(); this.dialog.open(WelcomeDialogComponent, { + id: welcomeDialogId, disableClose: true, closeOnNavigation: true, panelClass: 'welcome-screen', 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 fc2b418d..7033621f 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 @@ -324,7 +324,10 @@ export class IoTDeviceDetailComponent implements OnInit, OnDestroy { this.iotDeviceService .resetHttpDeviceApiKey(this.device.id) .subscribe((response) => { - this.device.apiKey = response.apiKey; + this.device = { + ...this.device, + apiKey: response.apiKey + }; }); } }); diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.html b/src/app/gateway/gateway-detail/gateway-detail.component.html index 9a9bd8b2..84430002 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.html +++ b/src/app/gateway/gateway-detail/gateway-detail.component.html @@ -1,99 +1,104 @@
- -
-
-
-
-

{{ 'GATEWAY.DETAILS' | translate }}

-

{{ 'GATEWAY.ID' | translate }}{{gateway.id}}

-

{{ 'GATEWAY.ORGANIZATION' | translate }}{{gateway.internalOrganizationName}}

- -

{{ 'GATEWAY.TAGS' | translate }}{{gateway.tagsString}}

-

- {{ 'GATEWAY.DESCRIPTION' | translate }}

-

{{gateway.description}}

-
-
-
-
-
-

{{ 'GATEWAY.LOCATION' | translate }}

- -
-
-

{{ 'GATEWAY.LONGITUDE' | translate }} - {{gateway.location?.longitude | number:'2.1-6'}}

-
-
-

{{ 'GATEWAY.LATITUDE' | translate }} - {{gateway.location.latitude | number:'2.1-6'}}

-
-
-

{{ 'GATEWAY.ALTITUDE' | translate }} - {{gateway.location.altitude | number:'2.1-6'}}

-
-
-
- -

{{ 'GATEWAY.NOLOCATION' | translate}}

-
-
+ +
+
+
+
+

{{ 'GATEWAY.DETAILS' | translate }}

+

{{ 'GATEWAY.ID' | translate }}{{gateway.id}}

+

{{ 'GATEWAY.ORGANIZATION' | translate }}{{gateway.internalOrganizationName}}

+ +

{{ 'GATEWAY.TAGS' | translate }}{{gateway.tagsString}}

+

+ {{ 'GATEWAY.DESCRIPTION' | translate }} +

+

{{gateway.description}}

+
+
+
+
+
+

{{ 'GATEWAY.LOCATION' | translate }}

+ +
+
+

{{ 'GATEWAY.LONGITUDE' | translate }} + {{gateway.location?.longitude | number:'2.1-6'}}

+
+
+

{{ 'GATEWAY.LATITUDE' | translate }} + {{gateway.location.latitude | number:'2.1-6'}}

+
+
+

{{ 'GATEWAY.ALTITUDE' | translate }} + {{gateway.location.altitude | number:'2.1-6'}}

+
+
+ +

{{ 'GATEWAY.NOLOCATION' | translate}}

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

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

-
- -
+
+

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

+
+ +
-
-
- - -
-
- - -
-
+
+
+ + +
+
+ + +
+
- - - - - +
- {{ 'GATEWAY.STATS-RXPACKETSRECEIVED' | translate }} - {{element.rxPacketsReceived}}
+ + + + - - - - + + + + - - - - + + + + - - -
+ {{ 'GATEWAY.STATS-RXPACKETSRECEIVED' | translate }} + {{element.rxPacketsReceived}}{{ 'GATEWAY.STATS-TXPACKETSEMITTED' | translate }} - {{element.txPacketsEmitted}}{{ 'GATEWAY.STATS-TXPACKETSEMITTED' | translate }} + {{element.txPacketsEmitted}}{{ 'GATEWAY.STATS-TIMESTAMP' | translate }}{{element.timestamp | date}}{{ 'GATEWAY.STATS-TIMESTAMP' | translate }}{{element.timestamp | date}}
- - -
+ + + + + +
+
diff --git a/src/app/gateway/gateway-detail/gateway-detail.component.ts b/src/app/gateway/gateway-detail/gateway-detail.component.ts index a2827796..082218c5 100644 --- a/src/app/gateway/gateway-detail/gateway-detail.component.ts +++ b/src/app/gateway/gateway-detail/gateway-detail.component.ts @@ -120,7 +120,11 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit } private buildGraphs() { - const { receivedDatasets, sentDatasets, labels } = this.gatewayStats.reduce( + const { + receivedDatasets, + sentDatasets, + labels, + } = this.gatewayStats.slice().reverse().reduce( ( res: { receivedDatasets: ChartConfiguration['data']['datasets']; @@ -139,10 +143,18 @@ export class GatewayDetailComponent implements OnInit, OnDestroy, AfterViewInit }, { receivedDatasets: [ - { data: [], borderColor: ColorGraphBlue1, backgroundColor: ColorGraphBlue1 }, + { + data: [], + borderColor: ColorGraphBlue1, + backgroundColor: ColorGraphBlue1, + }, ], sentDatasets: [ - { data: [], borderColor: ColorGraphBlue1, backgroundColor: ColorGraphBlue1 }, + { + data: [], + borderColor: ColorGraphBlue1, + backgroundColor: ColorGraphBlue1, + }, ], labels: [], } diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index ba57114f..954ae041 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -71,7 +71,7 @@ export class NavbarComponent implements OnInit { hasEmail(): string { this.userInfo = this.sharedVariableService.getUserInfo(); - return this.userInfo.user.email; + return this.userInfo?.user?.email; } public goToHelp() { 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 97696cf2..9bb15c4b 100644 --- a/src/app/shared/components/top-bar/top-bar.component.ts +++ b/src/app/shared/components/top-bar/top-bar.component.ts @@ -147,11 +147,11 @@ export class TopBarComponent implements OnInit { } hasEmail(): boolean { - if (this.sharedVariableService.getUserInfo().user.email) - { - return true - } - else return false; + if (this.sharedVariableService.getUserInfo()?.user?.email) { + return true; + } else { + return false; + } } hasAnyPermission(): boolean { diff --git a/src/app/shared/helpers/array.helper.ts b/src/app/shared/helpers/array.helper.ts index 425074d9..795c234c 100644 --- a/src/app/shared/helpers/array.helper.ts +++ b/src/app/shared/helpers/array.helper.ts @@ -1,3 +1,5 @@ +import type { Tail } from './type.helper'; + export const splitList = ( data: T[], batchSize = 50 @@ -10,19 +12,58 @@ export const splitList = ( return dataBatches; }; -export const sortBy = ( +const sortByGeneric = ( value: T[], - column: keyof T, - order: 'asc' | 'desc' = 'asc' + order: 'asc' | 'desc' = 'asc', + sortByDelegate: ( + arr: typeof value, + ...params: DelegateParams[] + ) => typeof value, + ...sortByParams: Tail> ): T[] => { if (!value?.length) { return value; } - const copy = value.slice(); - copy.sort((a, b) => - a[column] === b[column] ? 0 : a[column] > b[column] ? 1 : -1 - ); - + const copy = sortByDelegate(value, ...sortByParams); return order === 'asc' ? copy : copy.reverse(); }; + +const sortByColumnAsc = (value: T[], column: keyof T): T[] => { + return value + .slice() + .sort((a, b) => + a[column] === b[column] ? 0 : a[column] > b[column] ? 1 : -1 + ); +}; + +const sortBySelectorAsc = ( + value: T[], + valueSelector: (e: T) => string | number +): T[] => { + return value + .slice() + .sort((a, b) => + valueSelector(a) === valueSelector(b) + ? 0 + : valueSelector(a) > valueSelector(b) + ? 1 + : -1 + ); +}; + +export const sortBy = ( + value: T[], + column: keyof T, + order: 'asc' | 'desc' = 'asc' +): T[] => { + return sortByGeneric(value, order, sortByColumnAsc, column); +}; + +export const sortBySelector = ( + value: T[], + valueSelector: (e: T) => string | number, + order: 'asc' | 'desc' = 'asc' +): T[] => { + return sortByGeneric(value, order, sortBySelectorAsc, valueSelector); +}; diff --git a/src/app/shared/helpers/type.helper.ts b/src/app/shared/helpers/type.helper.ts index 132da2fb..bfc49c32 100644 --- a/src/app/shared/helpers/type.helper.ts +++ b/src/app/shared/helpers/type.helper.ts @@ -3,3 +3,17 @@ export type keyofType = { }[keyof Value]; export type keyofNumber = keyofType; export const nameof = (name: Extract): string => name; + +/** + * Tail returns a tuple with the first element removed + * so Tail<[1, 2, 3]> is [2, 3] + * (works by using rest tuples) + * + * @see https://stackoverflow.com/a/56370310 + */ +export type Tail = ((...t: T) => void) extends ( + h: unknown, + ...r: infer R +) => void + ? R + : never; diff --git a/src/app/shared/pipes/permission/translate-permissions.pipe.ts b/src/app/shared/pipes/permission/translate-permissions.pipe.ts index 4f6b5086..2e743d9b 100644 --- a/src/app/shared/pipes/permission/translate-permissions.pipe.ts +++ b/src/app/shared/pipes/permission/translate-permissions.pipe.ts @@ -11,6 +11,7 @@ export class TranslatePermissionsPipe implements PipeTransform { transform(permissions: PermissionTypes[] | undefined): string { const formattedPermissions = permissions .map(({ type }) => this.translate.instant('PERMISSION-TYPE.' + type)) + .sort() .join(', '); return formattedPermissions; diff --git a/src/app/shared/pipes/pipes.module.ts b/src/app/shared/pipes/pipes.module.ts index d0ce9cb0..c31d75fe 100644 --- a/src/app/shared/pipes/pipes.module.ts +++ b/src/app/shared/pipes/pipes.module.ts @@ -11,6 +11,7 @@ import { SortByPipe } from './sort-by.pipe'; import { GatewayStatusTooltipPipe } from './gateway/gateway-status-tooltip.pipe'; import { GatewayStatusClassPipe } from './gateway/gateway-status-class.pipe'; import { CanEditApplicationPipe } from './permission/can-edit-application.pipe'; +import { SortByTranslationPipe } from './sort-by-translation.pipe'; @NgModule({ declarations: [ @@ -27,6 +28,7 @@ import { CanEditApplicationPipe } from './permission/can-edit-application.pipe'; GatewayStatusTooltipPipe, GatewayStatusClassPipe, CanEditApplicationPipe, + SortByTranslationPipe, ], imports: [CommonModule], exports: [ @@ -43,6 +45,7 @@ import { CanEditApplicationPipe } from './permission/can-edit-application.pipe'; GatewayStatusTooltipPipe, GatewayStatusClassPipe, CanEditApplicationPipe, + SortByTranslationPipe, ], providers: [ DateOnlyPipe diff --git a/src/app/shared/pipes/sort-by-translation.pipe.ts b/src/app/shared/pipes/sort-by-translation.pipe.ts new file mode 100644 index 00000000..5bd73436 --- /dev/null +++ b/src/app/shared/pipes/sort-by-translation.pipe.ts @@ -0,0 +1,31 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { sortBySelector } from '@shared/helpers/array.helper'; + +@Pipe({ name: 'sortByTranslation' }) +export class SortByTranslationPipe implements PipeTransform { + constructor(private translate: TranslateService) {} + + /** + * Example: + * ``` + * *ngFor="let c of arrayOfObjects | sortBy::'asc'" + * ``` + */ + transform( + translationValues: T[], + column: keyof T | undefined, + order: 'asc' | 'desc' = 'asc', + prefix: string = '', + suffix: string = '' + ): T[] { + const res = sortBySelector( + translationValues, + (val) => + this.translate.instant(prefix + (column ? val[column] : val) + suffix), + order + ); + + return res; + } +} diff --git a/src/app/shared/services/me.service.ts b/src/app/shared/services/me.service.ts index f1d2d32e..e8ab46a9 100644 --- a/src/app/shared/services/me.service.ts +++ b/src/app/shared/services/me.service.ts @@ -21,7 +21,11 @@ export class MeService { organizationId: number = this.sharedVariableService.getSelectedOrganisationId(), applicationId?: number ): boolean { - const { permissions } = this.sharedVariableService.getUserInfo().user; + const { permissions } = this.sharedVariableService.getUserInfo()?.user ?? {}; + + if (!permissions) { + return false; + } if ( permissions.some((p) => diff --git a/src/app/shared/shared-variable/shared-variable.service.ts b/src/app/shared/shared-variable/shared-variable.service.ts index 5fb5e99e..13ffabc0 100644 --- a/src/app/shared/shared-variable/shared-variable.service.ts +++ b/src/app/shared/shared-variable/shared-variable.service.ts @@ -75,11 +75,11 @@ export class SharedVariableService { } getUsername(): string { - return this.getUserInfo()?.user.name; + return this.getUserInfo()?.user?.name; } getHasAnyPermission(): boolean { - return this.getUserInfo().user.permissions.length > 0; + return this.getUserInfo()?.user?.permissions.length > 0; } getSelectedOrganisationId(): number { diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json index c37643cf..1b527b46 100644 --- a/src/assets/i18n/da.json +++ b/src/assets/i18n/da.json @@ -1067,11 +1067,11 @@ "USER_PAGE": { "AWAITING_CONFIRMATION": "Din bruger er oprettet og afventer bekræftelse fra administratorer på de organisationer du har søgt tilknytning.", "APPLY_ORGANISATIONS": "Søg tilknytning til andre organisationer", - "APPLIED_ORGANISATIONS": "Der er pt søgt eller allerede eksisterende tilknytning til følgende organisationer:", + "APPLIED_ORGANISATIONS": "Der er pt. søgt eller allerede eksisterende tilknytning til følgende organisationer:", "QUESTION_APPLY_ORGANISATIONS": "Ønsker du at søge tilknytning til andre organisationer?", "USER_PAGE": "Organisationstilknytning", - "NO_APPLIED_ORGS": "Du har ikke søgt tilknytning til nogle organisationer..", - "NO_ORGS": "Der findes ikke yderligere organisationer.." + "NO_APPLIED_ORGS": "Du har ikke søgt tilknytning til nogle organisationer.", + "NO_ORGS": "Der findes ikke yderligere organisationer." }, "false": "Nej",