diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..910a2a5f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules\\typescript\\lib"
+}
diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts
index fffe9c8a..e5343681 100644
--- a/src/app/admin/admin-routing.module.ts
+++ b/src/app/admin/admin-routing.module.ts
@@ -12,6 +12,9 @@ import { UserDetailComponent } from './users/user-detail/user-detail.component';
import { UserEditComponent } from './users/user-edit/user-edit.component';
import { UserListComponent } from './users/user-list/user-list.component';
import { UsersComponent } from './users/users.component';
+import { ApiKeyComponent } from './api-key/api-key.component';
+import { ApiKeyListComponent } from './api-key/api-key-list/api-key-list.component';
+import { ApiKeyEditComponent } from './api-key/api-key-edit/api-key-edit.component';
const adminRoutes: Routes = [
@@ -44,6 +47,18 @@ const adminRoutes: Routes = [
},
],
},
+ {
+ path: 'api-key',
+ component: ApiKeyComponent,
+ children: [
+ { path: '', component: ApiKeyListComponent },
+ { path: 'new-api-key', component: ApiKeyEditComponent },
+ {
+ path: ':api-key-id/edit-api-key',
+ component: ApiKeyEditComponent,
+ },
+ ],
+ },
];
diff --git a/src/app/admin/admin.module.ts b/src/app/admin/admin.module.ts
index c7c52faf..93210070 100644
--- a/src/app/admin/admin.module.ts
+++ b/src/app/admin/admin.module.ts
@@ -28,6 +28,10 @@ import { UsersComponent } from './users/users.component';
import { MatSelectModule } from '@angular/material/select';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat-select-search.module';
+import { ApiKeyComponent } from './api-key/api-key.component';
+import { ApiKeyListComponent } from './api-key/api-key-list/api-key-list.component';
+import { ApiKeyTableComponent } from './api-key/api-key-list/api-key-table/api-key-table.component';
+import { ApiKeyEditComponent } from './api-key/api-key-edit/api-key-edit.component';
@NgModule({
declarations: [
@@ -46,6 +50,10 @@ import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat-
OrganisationDetailComponent,
OrganisationEditComponent,
OrganisationListComponent,
+ ApiKeyComponent,
+ ApiKeyListComponent,
+ ApiKeyTableComponent,
+ ApiKeyEditComponent,
],
imports: [
AdminRoutingModule,
@@ -79,6 +87,10 @@ import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat-
OrganisationDetailComponent,
OrganisationEditComponent,
OrganisationListComponent,
+ ApiKeyComponent,
+ ApiKeyListComponent,
+ ApiKeyTableComponent,
+ ApiKeyEditComponent,
],
})
export class AdminModule {}
diff --git a/src/app/admin/api-key/api-key-edit/api-key-edit.component.html b/src/app/admin/api-key/api-key-edit/api-key-edit.component.html
new file mode 100644
index 00000000..9a90bf14
--- /dev/null
+++ b/src/app/admin/api-key/api-key-edit/api-key-edit.component.html
@@ -0,0 +1,66 @@
+
+
+
diff --git a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.scss b/src/app/admin/api-key/api-key-edit/api-key-edit.component.scss
similarity index 100%
rename from src/app/applications/datatarget/datatarget-list/datatarget-list.component.scss
rename to src/app/admin/api-key/api-key-edit/api-key-edit.component.scss
diff --git a/src/app/admin/api-key/api-key-edit/api-key-edit.component.ts b/src/app/admin/api-key/api-key-edit/api-key-edit.component.ts
new file mode 100644
index 00000000..9c441f75
--- /dev/null
+++ b/src/app/admin/api-key/api-key-edit/api-key-edit.component.ts
@@ -0,0 +1,128 @@
+import { Location } from '@angular/common';
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { PermissionResponse } from '@app/admin/permission/permission.model';
+import { PermissionService } from '@app/admin/permission/permission.service';
+import { TranslateService } from '@ngx-translate/core';
+import { ErrorMessageService } from '@shared/error-message.service';
+import { BackButton } from '@shared/models/back-button.model';
+import { SharedVariableService } from '@shared/shared-variable/shared-variable.service';
+import { ApiKeyRequest } from '../api-key.model';
+import { ApiKeyService } from '../api-key.service';
+
+@Component({
+ selector: 'app-api-key-edit',
+ templateUrl: './api-key-edit.component.html',
+ styleUrls: ['./api-key-edit.component.scss'],
+})
+export class ApiKeyEditComponent implements OnInit {
+ apiKeyRequest = new ApiKeyRequest();
+ public backButton: BackButton = {
+ label: '',
+ routerLink: ['admin', 'api-key'],
+ };
+ public title = '';
+ public submitButton = '';
+ public errorMessage: string;
+ public errorMessages: string[];
+ public errorFields: string[];
+ public formFailedSubmit = false;
+ public permissions: PermissionResponse[] = [];
+ private organizationId: number;
+ private id: number;
+
+ constructor(
+ private translate: TranslateService,
+ private route: ActivatedRoute,
+ private location: Location,
+ private apiKeyService: ApiKeyService,
+ private permissionService: PermissionService,
+ private errorMessageService: ErrorMessageService,
+ private sharedVariableService: SharedVariableService
+ ) {}
+
+ ngOnInit(): void {
+ this.getPermissions();
+ this.translate.use('da');
+ this.translate
+ .get(['NAV.API-KEY', 'FORM.EDIT-API-KEY', 'API-KEY.EDIT.SAVE'])
+ .subscribe((translations) => {
+ this.backButton.label = translations['NAV.API-KEY'];
+ this.title = translations['FORM.EDIT-API-KEY'];
+ this.submitButton = translations['API-KEY.EDIT.SAVE'];
+ });
+
+ this.id = +this.route.snapshot.paramMap.get('api-key-id');
+
+ if (this.id > 0) {
+ this.getApiKey(this.id);
+ }
+ this.organizationId = this.sharedVariableService.getSelectedOrganisationId();
+ }
+
+ private getPermissions() {
+ this.permissionService
+ .getPermissions(
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ this.organizationId
+ )
+ .subscribe(
+ (permissionsResponse) => {
+ this.permissions = permissionsResponse.data.filter(
+ (x) => x.organization?.id === this.organizationId
+ );
+ },
+ (error: HttpErrorResponse) => {
+ this.showError(error);
+ }
+ );
+ }
+
+ private getApiKey(id: number) {
+ this.apiKeyService.get(id).subscribe((key) => {
+ this.apiKeyRequest.id = key.id;
+ this.apiKeyRequest.name = key.name;
+ this.apiKeyRequest.permissionIds = key.permissions.map((pm) => pm.id);
+ });
+ }
+
+ onSubmit(): void {
+ this.id ? this.update() : this.create();
+ }
+
+ private create(): void {
+ this.apiKeyService.create(this.apiKeyRequest).subscribe(
+ () => this.routeBack(),
+ (err) => this.showError(err)
+ );
+ }
+
+ private update(): void {
+ this.apiKeyService.update(this.apiKeyRequest, this.id).subscribe(
+ () => this.routeBack(),
+ (err) => this.showError(err)
+ );
+ }
+
+ public compare(
+ matOptionValue: number,
+ ngModelObject: number
+ ): boolean {
+ return matOptionValue === ngModelObject;
+ }
+
+ showError(err: HttpErrorResponse) {
+ const result = this.errorMessageService.handleErrorMessageWithFields(err);
+ this.errorFields = result.errorFields;
+ this.errorMessages = result.errorMessages;
+ }
+
+ routeBack(): void {
+ this.location.back();
+ }
+}
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
new file mode 100644
index 00000000..8ecf306e
--- /dev/null
+++ b/src/app/admin/api-key/api-key-list/api-key-list.component.html
@@ -0,0 +1,17 @@
+
+
+
diff --git a/src/app/admin/api-key/api-key-list/api-key-list.component.scss b/src/app/admin/api-key/api-key-list/api-key-list.component.scss
new file mode 100644
index 00000000..e69de29b
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
new file mode 100644
index 00000000..f422eac1
--- /dev/null
+++ b/src/app/admin/api-key/api-key-list/api-key-list.component.ts
@@ -0,0 +1,28 @@
+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';
+
+@Component({
+ selector: 'app-api-key-list',
+ templateUrl: './api-key-list.component.html',
+ styleUrls: ['./api-key-list.component.scss'],
+})
+export class ApiKeyListComponent implements OnInit {
+ @Input() organisationId: number;
+
+ constructor(
+ public translate: TranslateService,
+ private titleService: Title,
+ private globalService: SharedVariableService
+ ) {
+ translate.use('da');
+ }
+
+ ngOnInit(): void {
+ this.translate.get(['TITLE.API-KEY']).subscribe((translations) => {
+ this.titleService.setTitle(translations['TITLE.API-KEY']);
+ });
+ this.organisationId = this.globalService.getSelectedOrganisationId();
+ }
+}
diff --git a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.html b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.html
new file mode 100644
index 00000000..09321fe8
--- /dev/null
+++ b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+ |
+ {{ 'API-KEY.NAME' | translate }}
+ |
+
+ {{ element.name }}
+ |
+
+
+
+
+
+ {{ 'API-KEY.PERMISSIONS' | translate }}
+ |
+
+
+
+ {{ pm.name }}
+
+
+
+ {{ 'NoUsersAdded' | translate }}
+ |
+
+
+
+
+
+ {{ 'API-KEY.KEY' | translate }}
+ |
+
+ {{ element.key }}
+ |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
diff --git a/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.scss b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.scss
new file mode 100644
index 00000000..e69de29b
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
new file mode 100644
index 00000000..208d9c8e
--- /dev/null
+++ b/src/app/admin/api-key/api-key-list/api-key-table/api-key-table.component.ts
@@ -0,0 +1,111 @@
+import { AfterViewInit, Component, Input, ViewChild } from '@angular/core';
+import { MatPaginator, PageEvent } from '@angular/material/paginator';
+import { MatSort } from '@angular/material/sort';
+import { Router } from '@angular/router';
+import { environment } from '@environments/environment';
+import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+import { MeService } from '@shared/services/me.service';
+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';
+
+@Component({
+ selector: 'app-api-key-table',
+ templateUrl: './api-key-table.component.html',
+ styleUrls: ['./api-key-table.component.scss'],
+})
+export class ApiKeyTableComponent implements AfterViewInit {
+ @Input() organisationId: number;
+ displayedColumns: string[] = [
+ 'name',
+ 'permissions',
+ 'key',
+ 'menu',
+ ];
+ data: ApiKeyResponse[] = [];
+ isLoadingResults = true;
+ @ViewChild(MatPaginator) paginator: MatPaginator;
+ @ViewChild(MatSort) sort: MatSort;
+ resultsLength = 0;
+ public pageSize = environment.tablePageSize;
+
+ constructor(
+ private meService: MeService,
+ private router: Router,
+ private apiKeyService: ApiKeyService,
+ private deleteDialogService: DeleteDialogService
+ ) {}
+
+ ngAfterViewInit(): void {
+ // If the user changes the sort order, reset back to the first page.
+ this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
+
+ merge(this.sort.sortChange, this.paginator.page)
+ .pipe(
+ startWith({}),
+ switchMap(() => {
+ this.isLoadingResults = true;
+ return this.getApiKeysByOrganisationId(
+ this.sort.active,
+ this.sort.direction
+ );
+ }),
+ map((data) => {
+ // Flip flag to show that loading has finished.
+ this.isLoadingResults = false;
+ this.resultsLength = data.count;
+
+ return data.data;
+ }),
+ catchError(() => {
+ this.isLoadingResults = false;
+ return of([]);
+ })
+ )
+ .subscribe((data) => (this.data = data));
+ }
+
+ getApiKeysByOrganisationId(
+ orderByColumn: string,
+ orderByDirection: string
+ ): Observable {
+ return this.apiKeyService.getApiKeys(
+ this.paginator.pageSize,
+ this.paginator.pageIndex * this.paginator.pageSize,
+ orderByColumn,
+ orderByDirection,
+ null,
+ this.organisationId
+ );
+ }
+
+ canAccess(_element: ApiKeyResponse) {
+ return this.meService.hasAdminAccessInTargetOrganization(
+ this.organisationId
+ );
+ }
+
+ routeToPermissions(element: any) {
+ this.router.navigate(['admin/api-key', element.id]);
+ }
+
+ deleteApiKey(id: number) {
+ this.deleteDialogService.showSimpleDialog().subscribe((response) => {
+ if (response) {
+ this.apiKeyService.delete(id).subscribe((response) => {
+ if (response.ok && response.body.affected > 0) {
+ this.refresh();
+ }
+ });
+ }
+ });
+ }
+
+ private refresh() {
+ const pageEvent = new PageEvent();
+ pageEvent.pageIndex = this.paginator.pageIndex;
+ pageEvent.pageSize = this.paginator.pageSize;
+ this.paginator.page.emit(pageEvent);
+ }
+}
diff --git a/src/app/admin/api-key/api-key.component.ts b/src/app/admin/api-key/api-key.component.ts
new file mode 100644
index 00000000..97c6b2fe
--- /dev/null
+++ b/src/app/admin/api-key/api-key.component.ts
@@ -0,0 +1,11 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-api-key',
+ template: '',
+})
+export class ApiKeyComponent implements OnInit {
+ constructor() {}
+
+ ngOnInit(): void {}
+}
diff --git a/src/app/admin/api-key/api-key.model.ts b/src/app/admin/api-key/api-key.model.ts
new file mode 100644
index 00000000..ed8380f9
--- /dev/null
+++ b/src/app/admin/api-key/api-key.model.ts
@@ -0,0 +1,23 @@
+import { PermissionResponse } from '../permission/permission.model';
+
+export class ApiKeyRequest {
+ id: number;
+ name: string;
+ permissionIds?: number[];
+}
+
+export interface ApiKeyResponse {
+ id: number;
+ name: string;
+ key: string;
+ permissions?: PermissionResponse[];
+ createdBy: number;
+ updatedBy: number;
+ createdByName: string;
+ updatedByName: string;
+}
+
+export interface ApiKeyGetManyResponse {
+ data: ApiKeyResponse[];
+ count: number;
+}
diff --git a/src/app/admin/api-key/api-key.service.ts b/src/app/admin/api-key/api-key.service.ts
new file mode 100644
index 00000000..87d1c3c8
--- /dev/null
+++ b/src/app/admin/api-key/api-key.service.ts
@@ -0,0 +1,85 @@
+import { Injectable } from '@angular/core';
+import { RestService } from '@shared/services/rest.service';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { UserMinimalService } from '../users/user-minimal.service';
+import {
+ ApiKeyGetManyResponse,
+ ApiKeyRequest,
+ ApiKeyResponse,
+} from './api-key.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ApiKeyService {
+ endpoint = 'api-key';
+ constructor(
+ private restService: RestService,
+ private userMinimalService: UserMinimalService
+ ) {}
+
+ create(body: ApiKeyRequest): Observable {
+ return this.restService.post(this.endpoint, body, {
+ observe: 'response',
+ });
+ }
+
+ update(body: ApiKeyRequest, id: number): Observable {
+ return this.restService.put(this.endpoint, body, id, {
+ observe: 'response',
+ });
+ }
+
+ get(id: number): Observable {
+ return this.restService.get(this.endpoint, {}, id).pipe(
+ map((response: ApiKeyResponse) => {
+ response.createdByName = this.userMinimalService.getUserNameFrom(
+ response.createdBy
+ );
+ response.updatedByName = this.userMinimalService.getUserNameFrom(
+ response.updatedBy
+ );
+ return response;
+ })
+ );
+ }
+
+ getApiKeys(
+ limit: number = 1000,
+ offset: number = 0,
+ orderByColumn?: string,
+ orderByDirection?: string,
+ userId?: number,
+ organizationId?: number
+ ): Observable {
+ if (userId) {
+ return this.restService.get(this.endpoint, {
+ limit,
+ offset,
+ orderOn: orderByColumn,
+ sort: orderByDirection,
+ userId,
+ });
+ } else if (organizationId) {
+ return this.restService.get(this.endpoint, {
+ limit,
+ offset,
+ orderOn: orderByColumn,
+ sort: orderByDirection,
+ organizationId,
+ });
+ } else {
+ return this.restService.get(this.endpoint, {
+ limit,
+ offset,
+ orderOn: orderByColumn,
+ sort: orderByDirection,
+ });
+ }
+ }
+
+ delete(id: number) {
+ return this.restService.delete(this.endpoint, id);
+ }
+}
diff --git a/src/app/admin/organisation/organisation.model.ts b/src/app/admin/organisation/organisation.model.ts
index d3961220..f07c0a52 100644
--- a/src/app/admin/organisation/organisation.model.ts
+++ b/src/app/admin/organisation/organisation.model.ts
@@ -1,5 +1,6 @@
import { Application } from '@applications/application.model';
import { PayloadDecoder } from '../../payload-decoder/payload-decoder.model';
+import { PermissionResponse } from '../permission/permission.model';
export class Organisation {
id?: number;
@@ -18,8 +19,7 @@ export interface OrganisationResponse {
payloadDecoders: PayloadDecoder[];
applications: Application[];
- // TODO: This.
- permissions: any[];
+ permissions: PermissionResponse[];
}
export interface OrganisationGetManyResponse {
@@ -30,4 +30,4 @@ export interface OrganisationGetManyResponse {
export interface OrganisationGetMinimalResponse {
data: Organisation[];
count: number;
-}
\ 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 cec1432d..7559e47a 100644
--- a/src/app/admin/permission/permission-list/permission-list.component.ts
+++ b/src/app/admin/permission/permission-list/permission-list.component.ts
@@ -49,7 +49,6 @@ export class PermissionListComponent implements OnInit, OnChanges {
}
deletePermission(id: number) {
- console.log("list")
this.permissionService.deletePermission(id).subscribe((response) => {
if (response.ok && response.body.affected > 0) {
this.getPermissions();
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index aab4ebc9..82d3163a 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -23,6 +23,7 @@ import { MonacoEditorModule } from 'ngx-monaco-editor';
import { MatInputModule } from '@angular/material/input';
import { MatPaginatorIntl } from '@angular/material/paginator';
import { MatPaginatorIntlDa } from '@shared/helpers/mat-paginator-intl-da';
+import { MatTooltipModule } from '@angular/material/tooltip';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
@@ -62,6 +63,7 @@ export function tokenGetter() {
SearchModule,
HttpClientModule,
MatInputModule,
+ MatTooltipModule,
JwtModule.forRoot({
config: {
tokenGetter
diff --git a/src/app/applications/application-detail/application-detail.component.html b/src/app/applications/application-detail/application-detail.component.html
index 1eac588d..7c11602f 100644
--- a/src/app/applications/application-detail/application-detail.component.html
+++ b/src/app/applications/application-detail/application-detail.component.html
@@ -1,6 +1,5 @@
@@ -18,22 +17,68 @@
Detaljer
-
\ No newline at end of file
diff --git a/src/app/applications/application-detail/application-detail.component.ts b/src/app/applications/application-detail/application-detail.component.ts
index 009f3a63..2b6dfdbe 100644
--- a/src/app/applications/application-detail/application-detail.component.ts
+++ b/src/app/applications/application-detail/application-detail.component.ts
@@ -1,103 +1,109 @@
-import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
+import {
+ Component,
+ EventEmitter,
+ OnDestroy,
+ OnInit,
+ Output,
+} from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { Application } from '@applications/application.model';
import { ApplicationService } from '@applications/application.service';
+import { environment } from '@environments/environment';
import { TranslateService } from '@ngx-translate/core';
import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+import { DeviceType } from '@shared/enums/device-type';
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';
@Component({
- selector: 'app-application',
- templateUrl: './application-detail.component.html',
- styleUrls: ['./application-detail.component.scss'],
+ selector: 'app-application',
+ templateUrl: './application-detail.component.html',
+ styleUrls: ['./application-detail.component.scss'],
})
export class ApplicationDetailComponent implements OnInit, OnDestroy {
- @Output() deleteApplication = new EventEmitter();
- public applicationsSubscription: Subscription;
- private deleteDialogSubscription: Subscription;
- public application: Application;
- public backButton: BackButton = { label: '', routerLink: '/applications' };
- public id: number;
- public dropdownButton: DropdownButton;
- public errorMessage: string;
- public canEdit = false;
+ @Output() deleteApplication = new EventEmitter();
+ public applicationsSubscription: Subscription;
+ public application: Application;
+ public backButton: BackButton = { label: '', routerLink: '/applications' };
+ public id: number;
+ public pageLimit = environment.tablePageSize;
+ public dropdownButton: DropdownButton;
+ public errorMessage: string;
+ public canEdit = false;
- constructor(
- private applicationService: ApplicationService,
- private route: ActivatedRoute,
- public translate: TranslateService,
- public router: Router,
- private meService: MeService,
- private titleService: Title,
- private deleteDialogService: DeleteDialogService
- ) { }
+ constructor(
+ private applicationService: ApplicationService,
+ private route: ActivatedRoute,
+ public translate: TranslateService,
+ public router: Router,
+ private meService: MeService,
+ private titleService: Title,
+ private deleteDialogService: DeleteDialogService
+ ) {}
- ngOnInit(): void {
- this.id = +this.route.snapshot.paramMap.get('id');
- if (this.id) {
- this.bindApplication(this.id);
- this.dropdownButton = {
- label: '',
- editRouterLink: '../../edit-application/' + this.id,
- isErasable: true,
- };
+ ngOnInit(): void {
+ this.id = +this.route.snapshot.paramMap.get('id');
+ if (this.id) {
+ this.bindApplication(this.id);
+ this.dropdownButton = {
+ label: '',
+ editRouterLink: '../../edit-application/' + this.id,
+ isErasable: true,
+ };
+ }
- console.log(this.id);
- }
+ this.translate
+ .get([
+ 'NAV.APPLICATIONS',
+ 'APPLICATION-TABLE-ROW.SHOW-OPTIONS',
+ 'TITLE.APPLICATION',
+ ])
+ .subscribe((translations) => {
+ this.backButton.label = translations['NAV.APPLICATIONS'];
+ this.dropdownButton.label =
+ translations['APPLICATION-TABLE-ROW.SHOW-OPTIONS'];
+ this.titleService.setTitle(translations['TITLE.APPLICATION']);
+ });
+ this.canEdit = this.meService.canWriteInTargetOrganization();
+ }
- this.translate.get(['NAV.APPLICATIONS', 'APPLICATION-TABLE-ROW.SHOW-OPTIONS', 'TITLE.APPLICATION'])
- .subscribe(translations => {
- this.backButton.label = translations['NAV.APPLICATIONS'];
- this.dropdownButton.label = translations['APPLICATION-TABLE-ROW.SHOW-OPTIONS'];
- this.titleService.setTitle(translations['TITLE.APPLICATION']);
+ onDeleteApplication() {
+ this.deleteDialogService
+ .showApplicationDialog(this.application)
+ .subscribe((response) => {
+ if (response) {
+ this.applicationService
+ .deleteApplication(this.application.id)
+ .subscribe((response) => {
+ if (response.ok && response.body.affected > 0) {
+ console.log(
+ 'delete application with id:' + this.application.id.toString()
+ );
+ this.router.navigate(['applications']);
+ } else {
+ this.errorMessage = response?.error?.message;
+ }
});
- this.canEdit = this.meService.canWriteInTargetOrganization();
- }
-
- onDeleteApplication() {
- let message: string;
- if (this.applicationHasDevices()) {
- message = this.translate.instant('APPLICATION.DELETE-HAS-DEVICES-PROMPT');
+ } else {
+ console.log(response);
}
+ });
+ }
- this.deleteDialogSubscription = this.deleteDialogService.showSimpleDialog(message).subscribe(
- (response) => {
- if (response) {
- this.applicationService.deleteApplication(this.application.id).subscribe((response) => {
- if (response.ok && response.body.affected > 0) {
- console.log('delete application with id:' + this.application.id.toString());
- this.router.navigate(['applications']);
- } else {
- this.errorMessage = response?.error?.message;
- }
- });
- } else {
- console.log(response);
- }
- }
- );
- }
+ bindApplication(id: number): void {
+ this.applicationsSubscription = this.applicationService
+ .getApplication(id)
+ .subscribe((application) => {
+ this.application = application;
+ });
+ }
- applicationHasDevices(): boolean {
- return this.application.iotDevices?.length > 0;
- }
-
- bindApplication(id: number): void {
- this.applicationsSubscription = this.applicationService.getApplication(id).subscribe((application) => {
- this.application = application;
- });
- }
-
- ngOnDestroy() {
- if (this.applicationsSubscription) {
- this.applicationsSubscription.unsubscribe();
- }
- if (this.deleteDialogSubscription) {
- this.deleteDialogSubscription.unsubscribe();
- }
+ ngOnDestroy() {
+ if (this.applicationsSubscription) {
+ this.applicationsSubscription.unsubscribe();
}
+ }
}
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 4da38304..a459055d 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
@@ -1,4 +1,10 @@
-import { Component, ViewChild, AfterViewInit, Input, OnInit } from '@angular/core';
+import {
+ Component,
+ ViewChild,
+ AfterViewInit,
+ Input,
+ OnInit,
+} from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { Router } from '@angular/router';
@@ -10,6 +16,7 @@ import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dia
import { MeService } from '@shared/services/me.service';
import { merge, Observable, of as observableOf } from 'rxjs';
import { catchError, map, startWith, switchMap } from 'rxjs/operators';
+import { DeviceType } from '@shared/enums/device-type';
/**
* @title Table retrieving data through HTTP
@@ -40,7 +47,7 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit {
private router: Router,
private meService: MeService,
private deleteDialogService: DeleteDialogService
- ) { }
+ ) {}
ngOnInit() {
this.canEdit = this.meService.canWriteInTargetOrganization();
@@ -87,33 +94,28 @@ export class ApplicationsTableComponent implements AfterViewInit, OnInit {
}
deleteApplication(id: number) {
- let message: string;
- if (this.applicationHasDevices(id)) {
- message = this.translate.instant('APPLICATION.DELETE-HAS-DEVICES-PROMPT');
- }
+ const applicationToDelete = this.data?.find((app) => app.id === id);
- this.deleteDialogService.showSimpleDialog(message).subscribe((response) => {
- if (response) {
- this.applicationService.deleteApplication(id).subscribe((response) => {
- if (response.ok && response.body.affected > 0) {
- this.paginator.page.emit({
- pageIndex: this.paginator.pageIndex,
- pageSize: this.paginator.pageSize,
- length: this.resultsLength,
+ this.deleteDialogService
+ .showApplicationDialog(applicationToDelete)
+ .subscribe((response) => {
+ if (response) {
+ this.applicationService
+ .deleteApplication(id)
+ .subscribe((response) => {
+ if (response.ok && response.body.affected > 0) {
+ this.paginator.page.emit({
+ pageIndex: this.paginator.pageIndex,
+ pageSize: this.paginator.pageSize,
+ length: this.resultsLength,
+ });
+ } else {
+ this.errorMessage = response?.error?.message;
+ }
});
- } else {
- this.errorMessage = response?.error?.message;
- }
- });
- }
- });
+ }
+ });
}
-
- applicationHasDevices(id: number): boolean {
- const applicationToDelete = this.data?.find(app => app.id === id);
- return applicationToDelete && applicationToDelete.iotDevices.length > 0;
- }
-
navigateToEditPage(applicationId: string) {
this.router.navigate(['applications', 'edit-application', applicationId]);
}
diff --git a/src/app/applications/applications-routing.module.ts b/src/app/applications/applications-routing.module.ts
index f563dfab..5d285e70 100644
--- a/src/app/applications/applications-routing.module.ts
+++ b/src/app/applications/applications-routing.module.ts
@@ -7,10 +7,11 @@ import { ApplicationsComponent } from './applications.component';
import { IoTDeviceDetailComponent } from './iot-devices/iot-device-detail/iot-device-detail.component';
import { IotDeviceEditComponent } from './iot-devices/iot-device-edit/iot-device-edit.component';
import { DatatargetEditComponent } from './datatarget/datatarget-edit/datatarget-edit.component';
-import { DatatargetListComponent } from './datatarget/datatarget-list/datatarget-list.component';
import { DatatargetDetailComponent } from './datatarget/datatarget-detail/datatarget-detail.component';
import { BulkImportComponent } from './bulk-import/bulk-import.component';
-
+import { MulticastEditComponent } from './multicast/multicast-edit/multicast-edit.component';
+import { MulticastDetailComponent } from './multicast/multicast-detail/multicast-detail.component';
+import { DatatargetNewComponent } from './datatarget/datatarget-new/datatarget-new.component';
const applicationRoutes: Routes = [
{
@@ -27,21 +28,19 @@ const applicationRoutes: Routes = [
{ path: 'new-iot-device', component: IotDeviceEditComponent, },
{ path: 'iot-device-edit/:deviceId', component: IotDeviceEditComponent, },
{ path: 'iot-device/:deviceId', component: IoTDeviceDetailComponent, },
- {
- path: 'datatarget-list/:name',
- children: [
- { path: '', component: DatatargetListComponent },
- { path: 'datatarget-edit', component: DatatargetEditComponent },
- { path: 'datatarget-edit/:datatargetId', component: DatatargetEditComponent },
- { path: 'datatarget/:datatargetId', component: DatatargetDetailComponent }
- ]
-
- },
- { path: 'bulk-import', component: BulkImportComponent }
+ { path: 'datatarget-new', component: DatatargetNewComponent },
+ { path: 'datatarget-edit', component: DatatargetEditComponent },
+ { path: 'datatarget-edit/:datatargetId', component: DatatargetEditComponent },
+ { path: 'datatarget/:datatargetId', component: DatatargetDetailComponent },
+ { path: 'multicast-edit', component: MulticastEditComponent},
+ { path: 'multicast-edit/:multicastId', component: MulticastEditComponent },
+ { path: 'multicast/:multicastId', component: MulticastDetailComponent },
+
+ { path: 'bulk-import', component: BulkImportComponent },
],
},
- ],
+ ],
},
];
diff --git a/src/app/applications/applications.module.ts b/src/app/applications/applications.module.ts
index 69f3114c..acc30053 100644
--- a/src/app/applications/applications.module.ts
+++ b/src/app/applications/applications.module.ts
@@ -17,34 +17,35 @@ import { NGMaterialModule } from '@shared/Modules/materiale.module';
import { BulkImportComponent } from './bulk-import/bulk-import.component';
import { PipesModule } from '@shared/pipes/pipes.module';
import { ApplicationsTableComponent } from './applications-list/applications-table/applications-table.component';
-
+import { MulticastModule } from './multicast/multicast.module';
@NgModule({
- declarations: [
- ApplicationsComponent,
- ApplicationDetailComponent,
- ApplicationEditComponent,
- ApplicationsListComponent,
- ApplicationsTableComponent,
- BulkImportComponent,
- ],
- exports: [
- ApplicaitonsRoutingModule,
- ApplicationsComponent,
- ApplicationsTableComponent,
- ],
- imports: [
- CommonModule,
- RouterModule,
- TranslateModule,
- IotDevicesModule,
- DatatargetModule,
- DirectivesModule,
- FormModule,
- SharedModule,
- FontAwesomeModule,
- NGMaterialModule,
- PipesModule,
- ],
+ declarations: [
+ ApplicationsComponent,
+ ApplicationDetailComponent,
+ ApplicationEditComponent,
+ ApplicationsListComponent,
+ ApplicationsTableComponent,
+ BulkImportComponent,
+ ],
+ exports: [
+ ApplicaitonsRoutingModule,
+ ApplicationsComponent,
+ ApplicationsTableComponent,
+ ],
+ imports: [
+ CommonModule,
+ RouterModule,
+ TranslateModule,
+ IotDevicesModule,
+ DatatargetModule,
+ DirectivesModule,
+ FormModule,
+ SharedModule,
+ FontAwesomeModule,
+ NGMaterialModule,
+ PipesModule,
+ MulticastModule,
+ ],
})
-export class ApplicationsModule { }
+export class ApplicationsModule {}
diff --git a/src/app/applications/bulk-import/bulk-import.component.ts b/src/app/applications/bulk-import/bulk-import.component.ts
index e28d2506..5d7a6293 100644
--- a/src/app/applications/bulk-import/bulk-import.component.ts
+++ b/src/app/applications/bulk-import/bulk-import.component.ts
@@ -2,24 +2,36 @@ import { HttpErrorResponse } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
+import {
+ IotDeviceImportRequest,
+ IotDevicesImportResponse,
+} from '@applications/iot-devices/iot-device.model';
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 { 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 { Papa } from 'ngx-papaparse';
-import { Observable } from 'rxjs';
+import { Observable, Subject } from 'rxjs';
+import { takeWhile } from 'rxjs/operators';
import { BulkImport } from './bulk-import.model';
import { BulkMapping } from './bulkMapping';
@Component({
selector: 'app-bulk-import',
templateUrl: './bulk-import.component.html',
- styleUrls: ['./bulk-import.component.scss']
+ styleUrls: ['./bulk-import.component.scss'],
})
export class BulkImportComponent implements OnInit {
- displayedColumns: string[] = ['name', 'type', 'importStatus', 'errorMessages'];
+ displayedColumns: string[] = [
+ 'name',
+ 'type',
+ 'importStatus',
+ 'errorMessages',
+ ];
isLoading = false;
bulkImport: BulkImport[];
bulkImportResult: BulkImport[];
@@ -28,10 +40,19 @@ export class BulkImportComponent implements OnInit {
faTrash = faTrash;
faDownload = faDownload;
samples = [
- { name: 'generic-http-sample.csv', url: '../../../assets/docs/iotdevice_generichttp.csv' },
- { name: 'lorawan-otaa-sample.csv', url: '../../../assets/docs/iotdevice_lorawan_otaa.csv' },
- { name: 'lorawan-abp-sample.csv', url: '../../../assets/docs/iotdevice_lorawan_abp.csv' },
- ]
+ {
+ name: 'generic-http-sample.csv',
+ url: '../../../assets/docs/iotdevice_generichttp.csv',
+ },
+ {
+ name: 'lorawan-otaa-sample.csv',
+ url: '../../../assets/docs/iotdevice_lorawan_otaa.csv',
+ },
+ {
+ name: 'lorawan-abp-sample.csv',
+ url: '../../../assets/docs/iotdevice_lorawan_abp.csv',
+ },
+ ];
download$: Observable;
private bulkMapper = new BulkMapping();
public backButtonTitle: string;
@@ -44,25 +65,23 @@ export class BulkImportComponent implements OnInit {
private titleService: Title,
private translate: TranslateService,
private downloads: DownloadService,
- private errorMessageService: ErrorMessageService
+ private errorMessageService: ErrorMessageService,
+ private bulkImportService: BulkImportService
) {
this.translate.use('da');
- }
+ }
ngOnInit(): void {
- this.translate.get(['TITLE.BULKIMPORT'])
- .subscribe(translations => {
- this.titleService.setTitle(translations['TITLE.BULKIMPORT']);
- });
+ this.translate.get(['TITLE.BULKIMPORT']).subscribe((translations) => {
+ this.titleService.setTitle(translations['TITLE.BULKIMPORT']);
+ });
this.applicationId = +this.route.snapshot.paramMap.get('id');
-
}
- download({ name, url }: { name: string, url: string }) {
+ download({ name, url }: { name: string; url: string }) {
this.download$ = this.downloads.download(url, name);
}
-
deleteAttachment(index) {
this.files.splice(index, 1);
}
@@ -76,13 +95,14 @@ export class BulkImportComponent implements OnInit {
}
this.bulkImport = [];
this.bulkImportResult = [];
- for (let index = 0; index < evt.length; index++) {
- const element = evt[index];
+
+ for (const element of evt) {
this.files.push(element.name);
}
+
// handle csv data
this.isLoading = true;
- const files = evt; // File List object
+ const files = evt; // File List object
const file = files[0];
const reader = new FileReader();
reader.readAsText(file);
@@ -91,7 +111,7 @@ export class BulkImportComponent implements OnInit {
this.papa.parse(csv, {
skipEmptyLines: true,
header: true,
- complete: results => {
+ complete: (results) => {
this.mapData(results.data);
// this step ensures material can read from the array - should be fixed.
this.bulkImportResult = this.bulkImport;
@@ -100,9 +120,8 @@ export class BulkImportComponent implements OnInit {
} else {
return this.bulkImport;
}
- }
- }
- );
+ },
+ });
this.isLoading = false;
};
}
@@ -118,43 +137,162 @@ export class BulkImportComponent implements OnInit {
private mapData(data: any[]) {
data.forEach((device) => {
- const mappedDevice = this.bulkMapper.dataMapper(device, this.applicationId);
+ const mappedDevice = this.bulkMapper.dataMapper(
+ device,
+ this.applicationId
+ );
if (mappedDevice) {
this.bulkImport.push(new BulkImport(mappedDevice));
} else {
- this.translate.get(['ERROR.SEMANTIC'])
- .subscribe(translations => {
- this.bulkImport.push(new BulkImport(null, [translations['ERROR.SEMANTIC']]));
- });
+ this.translate.get(['ERROR.SEMANTIC']).subscribe((translations) => {
+ this.bulkImport.push(
+ new BulkImport(null, [translations['ERROR.SEMANTIC']])
+ );
+ });
}
});
}
addIoTDevice() {
- this.bulkImportResult.forEach((requestItem) => {
- if (requestItem.device?.id) {
- this.iotDeviceService.updateIoTDevice(requestItem.device, requestItem.device.id).subscribe(
+ // Subscribe to subject in service, Emit the index of next item in the array to be previous
+ // The emit will activate the subscription which should call the updateIoTDevice
+ const { newDevices, updatedDevices } = this.splitDevices();
+
+ this.postBulkImportPayload(
+ newDevices,
+ this.bulkImportService.nextCreateIotDeviceBatchIndex$,
+ this.iotDeviceService.createIoTDevices.bind(this.iotDeviceService)
+ );
+ this.postBulkImportPayload(
+ updatedDevices,
+ this.bulkImportService.nextUpdateDeviceBatchIndex$,
+ this.iotDeviceService.updateIoTDevices.bind(this.iotDeviceService)
+ );
+ }
+
+ private postBulkImportPayload(
+ bulkDevices: BulkImport[][],
+ batchIndex$: Subject,
+ importDevices: (
+ payload: IotDeviceImportRequest
+ ) => Observable
+ ): void {
+ if (!bulkDevices.length) {
+ return;
+ }
+
+ let batchIndex = 0;
+
+ // takeWhile() will unsubscribe once the condition is false
+ batchIndex$.pipe(takeWhile(() => batchIndex in bulkDevices)).subscribe(
+ () => {
+ const requestItems = bulkDevices[batchIndex];
+ const devices: IotDeviceImportRequest = {
+ data: requestItems.map((bulkResult) => bulkResult.device),
+ };
+ importDevices(devices).subscribe(
(response) => {
- console.log(response);
- requestItem.importStatus = 'success';
+ this.onSuccessfulImport(response, requestItems);
+ ++batchIndex;
+ batchIndex$.next();
},
(error: HttpErrorResponse) => {
- requestItem.errorMessages = this.errorMessageService.handleErrorMessageWithFields(error).errorMessages;
- requestItem.importStatus = 'Failed';
- }
- );
- } else if (requestItem.device) {
- this.iotDeviceService.createIoTDevice(requestItem.device).subscribe(
- (res: any) => {
- console.log(res);
- requestItem.importStatus = 'success';
- },
- (error) => {
- requestItem.errorMessages = this.errorMessageService.handleErrorMessage(error);
- requestItem.importStatus = 'Failed';
+ requestItems.forEach((item) => {
+ item.errorMessages = this.errorMessageService.handleErrorMessageWithFields(
+ error
+ ).errorMessages;
+ item.importStatus = 'Failed';
+ });
+ // Continue processing the next batches
+ ++batchIndex;
+ batchIndex$.next();
}
);
+ },
+ (_error: HttpErrorResponse) => {
+ // Should not happen
+ },
+ () => {
+ // Process any devices whose status hasn't been set and mark them as errors.
+ this.onCompleteImport(bulkDevices);
+ }
+ );
+
+ // Trigger our listener
+ batchIndex$.next();
+ }
+
+ private onSuccessfulImport(
+ response: IotDevicesImportResponse[],
+ requestItems: BulkImport[]
+ ) {
+ response.forEach((responseItem) => {
+ const match = requestItems.find(
+ ({ device }) =>
+ device.name === responseItem.idMetadata.name &&
+ device.applicationId === responseItem.idMetadata.applicationId
+ );
+ if (!match) {
+ return;
+ }
+
+ if (responseItem.error && match) {
+ match.errorMessages = this.errorMessageService.handleErrorMessageWithFields(
+ { error: responseItem.error }
+ ).errorMessages;
+ match.importStatus = 'Failed';
+ } else {
+ match.errorMessages = [];
+ match.importStatus = 'Success';
}
});
}
+
+ private onCompleteImport(devicesBulk: BulkImport[][]) {
+ for (const bulk of devicesBulk) {
+ for (const device of bulk) {
+ if (!device.importStatus) {
+ device.importStatus = 'Failed';
+ device.errorMessages = this.errorMessageService.handleErrorMessageWithFields(
+ {
+ error: {
+ message: 'MESSAGE.FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE',
+ },
+ }
+ ).errorMessages;
+ }
+ }
+ }
+ }
+
+ private splitDevices(): {
+ newDevices: BulkImport[][];
+ updatedDevices: BulkImport[][];
+ } {
+ if (!this.bulkImportResult) {
+ return { newDevices: [], updatedDevices: [] };
+ }
+
+ const { updatedDevices, newDevices } = this.bulkImportResult.reduce(
+ (
+ res: {
+ newDevices: BulkImport[];
+ updatedDevices: BulkImport[];
+ },
+ curr
+ ) => {
+ if (curr.device.id) {
+ res.updatedDevices.push(curr);
+ } else if (curr.device) {
+ res.newDevices.push(curr);
+ }
+ return res;
+ },
+ { updatedDevices: [], newDevices: [] }
+ );
+ return {
+ newDevices: splitList(newDevices),
+ updatedDevices: splitList(updatedDevices),
+ };
+ }
}
diff --git a/src/app/applications/bulk-import/bulk-import.model.ts b/src/app/applications/bulk-import/bulk-import.model.ts
index 05d07be5..904569f7 100644
--- a/src/app/applications/bulk-import/bulk-import.model.ts
+++ b/src/app/applications/bulk-import/bulk-import.model.ts
@@ -2,7 +2,7 @@ import { IotDevice } from '@applications/iot-devices/iot-device.model';
export class BulkImport {
public device: IotDevice;
- public errorMessages = [];
+ public errorMessages: unknown[] = [];
public importStatus = '';
constructor(device: IotDevice, errorMessages = [], importStatus = '') {
this.device = device;
diff --git a/src/app/applications/datatarget/datatarget-detail/datatarget-detail-type-selector.directive.spec.ts b/src/app/applications/datatarget/datatarget-detail/datatarget-detail-type-selector.directive.spec.ts
new file mode 100644
index 00000000..3a9402c7
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail-type-selector.directive.spec.ts
@@ -0,0 +1,11 @@
+/* tslint:disable:no-unused-variable */
+
+import { ViewContainerRef } from '@angular/core';
+import { DatatargetDetailTypeSelectorDirective } from './datatarget-detail-type-selector.directive';
+let viewContainerRef: ViewContainerRef;
+describe('Directive: DatatargetDetailTypeSelector', () => {
+ it('should create an instance', () => {
+ const directive = new DatatargetDetailTypeSelectorDirective(viewContainerRef);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/app/applications/datatarget/datatarget-detail/datatarget-detail-type-selector.directive.ts b/src/app/applications/datatarget/datatarget-detail/datatarget-detail-type-selector.directive.ts
new file mode 100644
index 00000000..84a3f73a
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail-type-selector.directive.ts
@@ -0,0 +1,9 @@
+import { Directive, ViewContainerRef } from '@angular/core';
+
+@Directive({
+ selector: '[detail-component]'
+})
+export class DatatargetDetailTypeSelectorDirective {
+
+ constructor(public viewContainerRef: ViewContainerRef) { }
+}
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 cc9829db..1c0c268c 100644
--- a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html
+++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.html
@@ -1,74 +1,3 @@
-
-
-
-
-
-
-
{{ 'DATATARGET.DETAILS' | translate }}
-
-
-
{{ 'DATATARGET.URL' | translate }}{{datatarget.url}}
-
{{ 'DATATARGET.TIMEOUT' | translate }}{{datatarget.timeout}}
-
{{ 'DATATARGET.TYPE' | translate }}{{datatarget.type | translate}}
-
-
{{ 'DATATARGET.AUTHORIZATIONHEADER' | translate }}
-
{{datatarget.authorizationHeader}}
-
- {{ 'DATATARGET.NO-AUTHORIZATIONHEADER' | translate }}
-
-
-
-
-
-
-
{{ 'DATATARGET.OPENDATA-DK' | translate }}
-
-
- {{ 'DATATARGET.NO-OPENDATA-DK' | translate }}
-
-
-
-
-
-
-
+
+
\ No newline at end of file
diff --git a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts
index 7af25889..17970ff3 100644
--- a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts
+++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.component.ts
@@ -1,17 +1,11 @@
-import { Component, OnDestroy, OnInit } from '@angular/core';
-import { Subscription } from 'rxjs';
+import { Component, ComponentFactoryResolver, OnDestroy, OnInit, Type, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
-import { TranslateService } from '@ngx-translate/core';
-import { PayloadDeviceDatatargetGetByDataTarget } from '@app/payload-decoder/payload-device-data.model';
-import { PayloadDeviceDatatargetService } from '@app/payload-decoder/payload-device-datatarget.service';
-import { BackButton } from '@shared/models/back-button.model';
-import { DatatargetService } from '../datatarget.service';
-import { Location } from '@angular/common';
-import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+import { DataTargetType } from '@shared/enums/datatarget-type';
+import { DatatargetTypesService } from '../datatarget-types.service';
import { Datatarget } from '../datatarget.model';
-import { DropdownButton } from '@shared/models/dropdown-button.model';
-import { faArrowsAltH } from '@fortawesome/free-solid-svg-icons';
-import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { DatatargetService } from '../datatarget.service';
+import { DatatargetDetail } from './datatarget-detail';
+import { DatatargetDetailTypeSelectorDirective } from './datatarget-detail-type-selector.directive';
@Component({
selector: 'app-datatarget-detail',
@@ -20,79 +14,46 @@ import { IotDevice } from '@applications/iot-devices/iot-device.model';
})
export class DatatargetDetailComponent implements OnInit, OnDestroy {
- public datatargetSubscription: Subscription;
+ @ViewChild(DatatargetDetailTypeSelectorDirective, {static: true}) adHost!: DatatargetDetailTypeSelectorDirective;
+
public datatarget: Datatarget;
- public backButton: BackButton = { label: '', routerLink: '/datatarget-list' };
- public dataTargetRelations: PayloadDeviceDatatargetGetByDataTarget[];
- private deleteDialogSubscription: Subscription;
- public dropdownButton: DropdownButton;
- arrowsAltH = faArrowsAltH;
- private applicationName: string;
-
- constructor(
- private route: ActivatedRoute,
- private deleteDialogService: DeleteDialogService,
- private location: Location,
- private datatargetRelationServicer: PayloadDeviceDatatargetService,
- private datatargetService: DatatargetService,
- public translate: TranslateService) { }
+ private datatargetType: DataTargetType;
- ngOnInit(): void {
- const id: number = +this.route.snapshot.paramMap.get('datatargetId');
- this.applicationName = this.route.snapshot.paramMap.get('name');
- if (id) {
- this.getDatatarget(id);
- this.getDatatargetRelations(id);
- this.dropdownButton = {
- label: '',
- editRouterLink: '../../datatarget-edit/' + id,
- isErasable: true,
- }
- }
- this.translate.get(['NAV.MY-DATATARGET', 'DATATARGET.SHOW-OPTIONS'])
- .subscribe(translations => {
- this.backButton.label = translations['NAV.MY-DATATARGET'];
- this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS']
- });
- }
+ constructor(private componentFactoryResolver: ComponentFactoryResolver,
+ private datatargetService: DatatargetService,
+ private route: ActivatedRoute,
+ private datatargetTypesService: DatatargetTypesService
+ ) { }
- getDatatarget(id: number) {
- this.datatargetService.get(id)
- .subscribe((dataTarget: Datatarget) => {
- this.datatarget = dataTarget;
- this.setBackButton(this.datatarget.applicationId);
- });
- }
- private setBackButton(applicationId: number) {
- this.backButton.routerLink = ['applications', applicationId.toString(), 'datatarget-list', this.applicationName ]
- }
+ loadComponent(componentType: Type
) {
- onDeleteDatatarget() {
- this.deleteDialogSubscription = this.deleteDialogService.showSimpleDialog().subscribe(
- (response) => {
- if (response) {
- this.datatargetService.delete(this.datatarget.id).subscribe((response) => {
- });
- this.location.back();
- } else {
- console.log(response);
- }
- }
- );
- }
+ const viewContainerRef = this.adHost.viewContainerRef;
- getDatatargetRelations(id: number) {
- this.datatargetRelationServicer.getByDataTarget(id)
- .subscribe((response) => {
- this.dataTargetRelations = response.data;
- });
+ viewContainerRef.clear();
+ const factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
+ viewContainerRef.createComponent(factory);
}
- ngOnDestroy(): void {
- if (this.deleteDialogSubscription) {
- this.deleteDialogSubscription.unsubscribe();
- }
+ ngOnInit(): void {
+
+ const id: number = +this.route.snapshot.paramMap.get('datatargetId');
+
+ this.datatargetService.get(id)
+ .subscribe((dataTarget: Datatarget) => {
+ this.datatarget = dataTarget;
+ this.datatargetType = dataTarget.type;
+
+ const component = this.datatargetTypesService.getDetailComponent(this.datatargetType);
+
+ this.loadComponent(component);
+
+ });
+
+
}
+ ngOnDestroy() {
+
+ }
}
diff --git a/src/app/applications/datatarget/datatarget-detail/datatarget-detail.ts b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.ts
new file mode 100644
index 00000000..4d5f7bfb
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-detail/datatarget-detail.ts
@@ -0,0 +1,2 @@
+export interface DatatargetDetail {
+}
diff --git a/src/app/applications/datatarget/datatarget-edit/datatarget-edit-type-selector.directive.spec.ts b/src/app/applications/datatarget/datatarget-edit/datatarget-edit-type-selector.directive.spec.ts
new file mode 100644
index 00000000..1c28242d
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit-type-selector.directive.spec.ts
@@ -0,0 +1,11 @@
+/* tslint:disable:no-unused-variable */
+
+import { ViewContainerRef } from '@angular/core';
+import { DatatargetEditTypeSelectorDirective } from './datatarget-edit-type-selector.directive';
+let viewContainerRef: ViewContainerRef;
+describe('Directive: DatatargetEditTypeSelector', () => {
+ it('should create an instance', () => {
+ const directive = new DatatargetEditTypeSelectorDirective(viewContainerRef);
+ expect(directive).toBeTruthy();
+ });
+});
diff --git a/src/app/applications/datatarget/datatarget-edit/datatarget-edit-type-selector.directive.ts b/src/app/applications/datatarget/datatarget-edit/datatarget-edit-type-selector.directive.ts
new file mode 100644
index 00000000..03b06ddb
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit-type-selector.directive.ts
@@ -0,0 +1,10 @@
+import { Directive, ViewContainerRef } from '@angular/core';
+
+@Directive({
+ selector: '[edit-component]'
+})
+export class DatatargetEditTypeSelectorDirective {
+
+ constructor(public viewContainerRef: ViewContainerRef) { }
+
+}
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 4d4df069..bb9f14e1 100644
--- a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html
+++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.html
@@ -1,139 +1,3 @@
-
-
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts
index 968df9e2..432a5e2b 100644
--- a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts
+++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.component.ts
@@ -1,26 +1,11 @@
-import { Component, OnInit, Input, OnDestroy } from '@angular/core';
-import { TranslateService } from '@ngx-translate/core';
-import { ActivatedRoute, Router } from '@angular/router';
+import { AfterViewInit, Component, ComponentFactoryResolver, OnDestroy, OnInit, QueryList, Type, ViewChild, ViewChildren } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { DataTargetType } from '@shared/enums/datatarget-type';
+import { DatatargetTypesService } from '../datatarget-types.service';
import { Datatarget } from '../datatarget.model';
-import { Observable, Subscription } from 'rxjs';
-import { Application } from '@applications/application.model';
-import { IotDevice } from '@applications/iot-devices/iot-device.model';
-import { faTimesCircle } from '@fortawesome/free-solid-svg-icons';
-import { PayloadDeviceDatatarget, PayloadDeviceDatatargetGetByDataTargetResponse } from '@payload-decoder/payload-device-data.model';
import { DatatargetService } from '../datatarget.service';
-import { ApplicationService } from '@applications/application.service';
-import { PayloadDecoderService } from '@payload-decoder/payload-decoder.service';
-import { PayloadDeviceDatatargetService } from '@payload-decoder/payload-device-datatarget.service';
-import { SaveSnackService } from '@shared/services/save-snack.service';
-import { MatDialog } from '@angular/material/dialog';
-import { HttpErrorResponse } from '@angular/common/http';
-import { PayloadDecoderMappedResponse } from '@payload-decoder/payload-decoder.model';
-import { DeleteDialogComponent } from '@shared/components/delete-dialog/delete-dialog.component';
-import { ErrorMessageService } from '@shared/error-message.service';
-import { OpendatadkDialogService } from '@shared/components/opendatadk-dialog/opendatadk-dialog.service';
-import { OpendatadkService } from '@shared/services/opendatadk.service';
-import { ScrollToTopService } from '@shared/services/scroll-to-top.service';
-import { OpenDataDkDataset } from '../opendatadk/opendatadk-dataset.model';
+import { DatatargetEdit } from './datatarget-edit';
+import { DatatargetEditTypeSelectorDirective } from './datatarget-edit-type-selector.directive';
@Component({
selector: 'app-datatarget-edit',
@@ -28,363 +13,57 @@ import { OpenDataDkDataset } from '../opendatadk/opendatadk-dataset.model';
styleUrls: ['./datatarget-edit.component.scss']
})
export class DatatargetEditComponent implements OnInit, OnDestroy {
- public multiPage = false;
- public title = '';
- public sectionTitle = '';
- public backButtonTitle = '';
- @Input() submitButton: string;
- public datatarget: Datatarget = new Datatarget();
- faTimesCircle = faTimesCircle;
- public datatargetSubscription: Subscription;
- public relationSubscription: Subscription;
- public applicationSubscription: Subscription;
- public payloadDecoderSubscription: Subscription;
- public errorMessages: any;
- public errorFields: string[];
- public formFailedSubmit = false;
- public datatargetid: number;
- private applicationId: number;
- private applicationNane: string;
- public application: Application;
- public devices: IotDevice[];
- public payloadDecoders = [];
- private counter: number;
- private dataSetExcists = false;
- private isMailDialogAlreadyShown = false;
- payloadDeviceDatatarget: PayloadDeviceDatatarget[];
- newDynamic: any = {};
+ @ViewChild(DatatargetEditTypeSelectorDirective, {static: true}) adHost!: DatatargetEditTypeSelectorDirective;
- constructor(
- public translate: TranslateService,
- private route: ActivatedRoute,
- private router: Router,
- private datatargetService: DatatargetService,
- private applicationService: ApplicationService,
- private payloadDecoderService: PayloadDecoderService,
- private payloadDeviceDataTargetService: PayloadDeviceDatatargetService,
- private saveSnackService: SaveSnackService,
- private dialog: MatDialog,
- private errorMessageService: ErrorMessageService,
- private opendatadkService: OpendatadkService,
- private opendatadkDialogService: OpendatadkDialogService,
- private scrollToTopService: ScrollToTopService,
- ) {
- translate.use('da');
- }
-
- ngOnInit() {
- this.translate
- .get([
- 'FORM.CREATE-NEW-DATATARGET',
- 'FORM.EDIT-DATATARGET',
- 'DATATARGET.SAVE',
- 'NAV.DATATARGET',
- ])
- .subscribe((translations) => {
- const datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
- if (datatargetid !== 0) {
- this.title = translations['FORM.EDIT-DATATARGET'];
- } else {
- this.title = translations['FORM.CREATE-NEW-DATATARGET'];
- }
- this.submitButton = translations['DATATARGET.SAVE'];
- this.backButtonTitle = translations['NAV.DATATARGET'];
- });
+ public datatarget: Datatarget;
+ private datatargetType: DataTargetType;
- this.datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
- this.applicationId = +this.route.snapshot.paramMap.get('id');
- this.applicationNane = this.route.snapshot.paramMap.get('name');
- if (this.datatargetid !== 0) {
- this.getDatatarget(this.datatargetid);
- this.getPayloadDeviceDatatarget(this.datatargetid);
- }
- if (this.applicationId !== 0) {
- this.getDevices();
- }
- this.getPayloadDecoders();
- this.setDataSetExcists();
- }
+ constructor(private componentFactoryResolver: ComponentFactoryResolver,
+ private datatargetService: DatatargetService,
+ private route: ActivatedRoute,
+ private datatargetTypesService: DatatargetTypesService
+ ) { }
- addRow() {
- if (!this.payloadDeviceDatatarget) {
- this.payloadDeviceDatatarget = [];
- }
- this.payloadDeviceDatatarget.push({ id: null, iotDeviceIds: [], payloadDecoderId: null, dataTargetId: this.datatargetid });
- }
- private deleteRow(index) {
- if (this.payloadDeviceDatatarget.length === 0) {
- } else if (this.payloadDeviceDatatarget[index]?.id === null) {
- this.payloadDeviceDatatarget.splice(index, 1);
- } else {
- this.payloadDeviceDataTargetService.delete(this.payloadDeviceDatatarget[index].id)
- .subscribe((response) => {
- this.payloadDeviceDatatarget.splice(index, 1);
- });
- }
+ loadComponent(componentType: Type) {
+ const viewContainerRef = this.adHost.viewContainerRef;
+ viewContainerRef.clear();
+ const factory = this.componentFactoryResolver.resolveComponentFactory(componentType);
+ viewContainerRef.createComponent(factory);
}
- openDeleteDialog(index) {
- const dialog = this.dialog.open(DeleteDialogComponent, {
- data: {
- showAccept: true,
- showCancel: true,
- message: 'Er du sikker på at du vil slette?'
- }
- });
-
- dialog.afterClosed().subscribe((result) => {
- if (result === true) {
- this.deleteRow(index);
- }
- });
- }
-
- onSubmit(): void {
- this.counter = 0;
- if (this.datatargetid) {
- this.updateDatatarget();
- this.addPayloadDeviceDatatarget();
- } else {
- this.createDatatarget();
- }
- }
-
- public compare(o1: any, o2: any): boolean {
- return o1 === o2;
- }
+ ngOnInit(): void {
- updateDatatarget() {
- this.resetErrors();
- this.counter = 1 + (this.payloadDeviceDatatarget?.length ? this.payloadDeviceDatatarget?.length : 0);
- this.datatargetService.update(this.datatarget)
- .subscribe(
- (response: Datatarget) => {
- this.datatarget = response;
- if (this.datatarget.openDataDkDataset != null) {
- this.datatarget.openDataDkDataset.acceptTerms = true;
- }
- this.shouldShowMailDialog().subscribe(
- (response) => {
- this.countToRedirect();
- }
- );
- },
- (error: HttpErrorResponse) => {
- this.checkDataTargetModelOpendatadkdatasaet();
- this.handleError(error);
- this.formFailedSubmit = true;
- }
- );
- }
+ const id: number = +this.route.snapshot.paramMap.get('datatargetId');
- addPayloadDeviceDatatarget() {
- this.payloadDeviceDatatarget.map(
- pdd => {
- if (pdd.payloadDecoderId === 0) {
- pdd.payloadDecoderId = null;
- }
- }
- );
- this.payloadDeviceDatatarget.forEach((relation) => {
- if (relation.id) {
- this.payloadDeviceDataTargetService.put(relation).subscribe(
- (response) => {
- this.countToRedirect();
- },
- (error) => {
- this.handleError(error);
- }
- );
+ if (id > 0) {
+ this.datatargetService.get(id)
+ .subscribe((dataTarget: Datatarget) => {
+ this.datatarget = dataTarget;
+ this.datatargetType = dataTarget.type;
+ const component = this.datatargetTypesService.getEditComponent(this.datatargetType);
+ this.loadComponent(component);
+ });
} else {
- this.payloadDeviceDataTargetService.post(relation).subscribe(
- (res: any) => {
- this.countToRedirect();
- },
- (error) => {
- this.handleError(error);
- }
- );
- }
- });
- }
-
- countToRedirect() {
- this.counter -= 1;
- if (this.counter <= 0 && !this.formFailedSubmit) {
- this.showSavedSnack();
- this.routeToDatatargets();
- }
- }
-
- getPayloadDeviceDatatarget(id: number) {
- this.relationSubscription = this.payloadDeviceDataTargetService
- .getByDataTarget(id)
- .subscribe((response: PayloadDeviceDatatargetGetByDataTargetResponse) => {
- this.mapToDatatargetDevicePayload(response);
- });
- }
-
- createDatatarget() {
- this.resetErrors();
- this.datatarget.applicationId = this.applicationId;
- this.datatargetService.create(this.datatarget)
- .subscribe((response: Datatarget) => {
- this.datatargetid = response.id;
- this.datatarget = response;
- if (this.datatarget.openDataDkDataset != null) {
- this.datatarget.openDataDkDataset.acceptTerms = true;
+ let datatargetTypeParam = this.route.snapshot.paramMap.get('datatargetType');
+ this.datatargetType = this.enumFromStringValue(DataTargetType, datatargetTypeParam);
+ if (this.datatargetType) {
+ const component = this.datatargetTypesService.getEditComponent(this.datatargetType);
+ this.loadComponent(component);
}
- this.showSavedSnack();
- },
- (error: HttpErrorResponse) => {
- this.checkDataTargetModelOpendatadkdatasaet();
- this.handleError(error);
- this.formFailedSubmit = true;
- });
-
- }
-
- private resetErrors() {
- this.errorFields = [];
- this.errorMessages = undefined;
- this.formFailedSubmit = false;
- }
-
- checkDataTargetModelOpendatadkdatasaet() {
- if (!this.datatarget.openDataDkDataset) {
- this.datatarget.openDataDkDataset = new OpenDataDkDataset();
- }
- }
-
- getDevices(): void {
- this.applicationSubscription = this.applicationService.getApplication(this.applicationId)
- .subscribe((application: Application) => {
- this.devices = application.iotDevices;
- });
- }
-
- public selectAllDevices(index: number) {
- this.payloadDeviceDatatarget[index].iotDeviceIds = this.devices.map(device => device.id);
- }
-
- public deSelectAllDevices(index: number) {
- this.payloadDeviceDatatarget[index].iotDeviceIds = [];
- }
-
- getPayloadDecoders() {
- this.payloadDecoderSubscription = this.payloadDecoderService.getMultiple(1000, 0, 'id', 'ASC')
- .subscribe((response: PayloadDecoderMappedResponse) => {
- this.payloadDecoders = response.data;
- });
- }
-
- handleError(error: HttpErrorResponse) {
- const errors = this.errorMessageService.handleErrorMessageWithFields(error);
- this.errorFields = errors.errorFields;
- this.errorMessages = errors.errorMessages;
- this.scrollToTopService.scrollToTop();
- }
-
- routeToDatatargets(): void {
- this.router.navigate(['applications',this.applicationId.toString(),'datatarget-list', this.applicationNane])
- }
-
- onCoordinateKey(event: any) {
- if (event.target.value.length > event.target.maxLength) {
- event.target.value = event.target.value.slice(
- 0,
- event.target.maxLength
- );
- }
- }
-
- getDatatarget(id: number) {
- this.datatargetSubscription = this.datatargetService
- .get(id)
- .subscribe((response: Datatarget) => {
- this.datatarget = response;
- });
- }
-
- showSavedSnack() {
- this.saveSnackService.showSavedSnack();
- }
-
- private setDataSetExcists() {
- this.opendatadkService.get().subscribe(
- (response) => {
- this.dataSetExcists = response.dataset.length === 0 ? false : true;
}
- );
- }
- private shouldShowMailDialog(): Observable {
- return new Observable(
- (observer) => {
- if (!this.dataSetExcists && this.datatarget.setToOpendataDk && !this.isMailDialogAlreadyShown) {
- this.isMailDialogAlreadyShown = true;
- this.opendatadkDialogService.showDialog().subscribe(
- response => {
- if (response) {
- this.showMailClient();
- }
- observer.next(response);
- }
- );
- } else {
- observer.next(true);
- }
- }
- )
- }
- private showMailClient() {
- if (!this.datatarget.openDataDkDataset.url) {
- this.datatarget.openDataDkDataset.url = this.datatargetService.getOpendataSharingApiUrl()
- }
- window.location.href = 'mailto:FG2V@kk.dk?subject=Oprettelse%20af%20datas%C3%A6t%20i%20OpenDataDK&body=K%C3%A6re%20Frans%0D%0A%0D%0AHermed%20fremsendes%20linket%20til%20DCAT%20kataloget%20%2C%20du%20bedes%20registrere%20p%C3%A5%20Open%20Data%20DK%20platformen.%0D%0A%0D%0ALink%3A ' + this.datatarget.openDataDkDataset.url;
}
- disableSaveButton(): boolean {
- let disable = true;
- if (!this.datatarget.setToOpendataDk) {
- disable = false;
- } else if (this.datatarget.openDataDkDataset?.acceptTerms) {
- disable = false;
- } else {
- disable = true;
- }
- return disable;
+ enumFromStringValue(enm: { [s: string]: T}, value: string): T | undefined {
+ return (Object.values(enm) as unknown as string[]).includes(value)
+ ? value as unknown as T
+ : undefined;
}
- ngOnDestroy(): void {
- if (this.relationSubscription) {
- this.relationSubscription.unsubscribe();
- }
- if (this.applicationSubscription) {
- this.applicationSubscription.unsubscribe();
- }
- if (this.datatargetSubscription) {
- this.datatargetSubscription.unsubscribe();
- }
- if (this.payloadDecoderSubscription) {
- this.payloadDecoderSubscription.unsubscribe();
- }
- }
+ ngOnDestroy() {
- private mapToDatatargetDevicePayload(dto: PayloadDeviceDatatargetGetByDataTargetResponse) {
- this.payloadDeviceDatatarget = [];
- dto.data.forEach(
- (element) => {
- this.payloadDeviceDatatarget.push({
- id: element.id,
- iotDeviceIds: element.iotDevices.map((x) => x.id),
- payloadDecoderId: element.payloadDecoder?.id === undefined ? 0 : element.payloadDecoder?.id,
- dataTargetId: element.dataTarget.id
- });
- }
- );
- }
+ }
}
diff --git a/src/app/applications/datatarget/datatarget-edit/datatarget-edit.ts b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.ts
new file mode 100644
index 00000000..16def301
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-edit/datatarget-edit.ts
@@ -0,0 +1,2 @@
+export interface DatatargetEdit {
+}
diff --git a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html
deleted file mode 100644
index bad1cea6..00000000
--- a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.spec.ts b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.spec.ts
deleted file mode 100644
index 7b515efa..00000000
--- a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { async, ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { DatatargetListComponent } from './datatarget-list.component';
-
-describe('DatatargetListComponent', () => {
- let component: DatatargetListComponent;
- let fixture: ComponentFixture;
-
- beforeEach(async(() => {
- TestBed.configureTestingModule({
- declarations: [ DatatargetListComponent ]
- })
- .compileComponents();
- }));
-
- beforeEach(() => {
- fixture = TestBed.createComponent(DatatargetListComponent);
- component = fixture.componentInstance;
- fixture.detectChanges();
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
diff --git a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts b/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts
deleted file mode 100644
index 2f5236cd..00000000
--- a/src/app/applications/datatarget/datatarget-list/datatarget-list.component.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Component, OnInit, Input } from '@angular/core';
-import { TranslateService } from '@ngx-translate/core';
-import { ActivatedRoute } from '@angular/router';
-import { Datatarget } from '../datatarget.model';
-import { BackButton } from '@shared/models/back-button.model';
-import { environment } from '@environments/environment';
-import { Title } from '@angular/platform-browser';
-
-
-@Component({
- selector: 'a[app-datatarget-list]',
- templateUrl: './datatarget-list.component.html',
- styleUrls: ['./datatarget-list.component.scss']
-})
-export class DatatargetListComponent implements OnInit {
-
- public pageLimit = environment.tablePageSize;
- public title: string;
- public backButton: BackButton = { label: '', routerLink: ''};
- public datatarget: Datatarget;
- private applikationId: string;
-
- constructor(
- public translate: TranslateService,
- private titleService: Title,
- private route: ActivatedRoute) {
- translate.use('da');
- }
-
- ngOnInit(): void {
- const applikationName: string = this.route.snapshot.paramMap.get('name');
- this.applikationId = this.route.snapshot.paramMap.get('id');
- this.translate.get(["NAV.DATATARGET", "NAV.APPLICATIONS", "TITLE.DATATARGET"])
- .subscribe((translate) => {
- this.title = translate['NAV.DATATARGET'] + ' - ' + applikationName;
- this.backButton.label = translate['NAV.APPLICATIONS'];
- this.titleService.setTitle(translate['TITLE.DATATARGET']);
- });
- this.setBackButton()
- }
-
- setBackButton() {
- this.backButton.routerLink = ['applications', this.applikationId];
- }
-
- updatePageLimit(limit: any) {
- console.log(limit);
- }
-}
diff --git a/src/app/applications/datatarget/datatarget-new/datatarget-new.component.html b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.html
new file mode 100644
index 00000000..6d654898
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{dataTargetType.name}}
+
+
+ {{dataTargetType.provider}}
+
+
+
+ {{ dataTargetType.description }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/applications/datatarget/datatarget-new/datatarget-new.component.scss b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.scss
new file mode 100644
index 00000000..71485bcb
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.scss
@@ -0,0 +1,44 @@
+.data-component {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ margin-left: 10px;
+}
+
+.mat-card{
+ display:flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ width: 300px;
+ min-width: 300px;
+ max-width: 400px;
+ height: 320px;
+ margin: 10px;
+}
+
+.mat-card-header {
+ flex-shrink: 1;
+
+}
+
+.mat-card-content{
+ flex-grow: 1;
+ overflow: auto;
+ margin-left: 10px;
+}
+
+
+mat-card img{
+ object-fit: contain; /*this makes the image in src fit to the size specified below*/
+ object-position: left;
+ width: 50%; /* Here you can use wherever you want to specify the width and also the height of the
*/
+ height: 20px;
+ margin-left: 3px;
+ margin-top: 3px;
+ margin-bottom: 3px;
+}
+
+.img-placeholder{
+ height: 26px;
+ clear: both;
+}
diff --git a/src/app/applications/datatarget/datatarget-new/datatarget-new.component.spec.ts b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.spec.ts
new file mode 100644
index 00000000..4f1f6508
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.spec.ts
@@ -0,0 +1,28 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { DatatargetNewComponent } from './datatarget-new.component';
+
+describe('DatatargetNewComponent', () => {
+ let component: DatatargetNewComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ DatatargetNewComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(DatatargetNewComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/applications/datatarget/datatarget-new/datatarget-new.component.ts b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.ts
new file mode 100644
index 00000000..d3f90b12
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-new/datatarget-new.component.ts
@@ -0,0 +1,63 @@
+import { Component, OnInit } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { DatatargetTypeDescriptor } from '../datatarget.model';
+import { DatatargetTypesService } from '../datatarget-types.service';
+
+
+
+@Component({
+ selector: 'app-datatarget-new',
+ templateUrl: './datatarget-new.component.html',
+ styleUrls: ['./datatarget-new.component.scss']
+})
+export class DatatargetNewComponent implements OnInit {
+
+ public title = '';
+ public sectionTitle = '';
+ public backButtonTitle = '';
+ public submitButton = '';
+ public avaiableDataTargetTypes : DatatargetTypeDescriptor[];
+
+ constructor(
+ public translate: TranslateService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private dataTargetTypesService: DatatargetTypesService
+ ) {
+ translate.use('da');
+ }
+
+ ngOnInit() {
+
+ this.translate
+ .get([
+ 'FORM.CREATE-NEW-DATATARGET',
+ 'FORM.EDIT-DATATARGET',
+ 'DATATARGET.SAVE',
+ 'NAV.DATATARGET',
+ ])
+ .subscribe((translations) => {
+ const datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
+ if (datatargetid !== 0) {
+ this.title = translations['FORM.EDIT-DATATARGET'];
+ } else {
+ this.title = translations['FORM.CREATE-NEW-DATATARGET'];
+ }
+ this.submitButton = translations['DATATARGET.SAVE'];
+ this.backButtonTitle = translations['NAV.DATATARGET'];
+ });
+
+ this.avaiableDataTargetTypes = this.dataTargetTypesService.getAvailableDataTargetTypes();
+
+ }
+
+ public createNewOf(typeDescriptor: DatatargetTypeDescriptor)
+ {
+ this.router.navigate(['../datatarget-edit', {datatargetType: typeDescriptor.type}], {relativeTo:this. route});
+ }
+ public showReadMe(typeDescriptor: DatatargetTypeDescriptor)
+ {
+ window.open(typeDescriptor.readMoreUrl, "_blank");
+ }
+}
diff --git a/src/app/applications/datatarget/datatarget-response.model.ts b/src/app/applications/datatarget/datatarget-response.model.ts
index 1d6b809d..fcb40286 100644
--- a/src/app/applications/datatarget/datatarget-response.model.ts
+++ b/src/app/applications/datatarget/datatarget-response.model.ts
@@ -9,6 +9,8 @@ export class DatatargetResponse {
timeout: number;
type: DataTargetType;
url: string;
+ tenant: string;
+ context: string;
authorizationHeader: string;
openDataDkDataset: OpenDataDkDataset;
createdAt: string;
diff --git a/src/app/applications/datatarget/datatarget-table/datatarget-table.component.html b/src/app/applications/datatarget/datatarget-table/datatarget-table.component.html
index 43de08b9..d2b4aca8 100644
--- a/src/app/applications/datatarget/datatarget-table/datatarget-table.component.html
+++ b/src/app/applications/datatarget/datatarget-table/datatarget-table.component.html
@@ -15,13 +15,13 @@
-
+
{{ 'DATATARGET-TABLE.TYPE' | translate }} |
- {{element.type | translate}} |
+ {{'DATATARGET.' + element.type | translate}}
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 c3bbc0df..65c04906 100644
--- a/src/app/applications/datatarget/datatarget-table/datatarget-table.component.ts
+++ b/src/app/applications/datatarget/datatarget-table/datatarget-table.component.ts
@@ -46,8 +46,7 @@ export class DatatargetTableComponent implements OnInit, AfterViewInit, OnDestro
}
ngOnInit(): void {
- this.applicationId = +Number(this.route.parent.parent.snapshot.paramMap.get('id'));
- console.log(this.applicationId);
+ this.applicationId = +Number(this.route.parent.snapshot.paramMap.get('id'));
this.getDatatarget();
this.canEdit = this.meService.canWriteInTargetOrganization()
}
diff --git a/src/app/applications/datatarget/datatarget-types.service.spec.ts b/src/app/applications/datatarget/datatarget-types.service.spec.ts
new file mode 100644
index 00000000..d5ef25d5
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-types.service.spec.ts
@@ -0,0 +1,16 @@
+/* tslint:disable:no-unused-variable */
+
+import { TestBed, async, inject } from '@angular/core/testing';
+import { DatatargetTypesService } from './datatarget-types.service';
+
+describe('Service: DatatargetTypesService', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [DatatargetTypesService]
+ });
+ });
+
+ it('should ...', inject([DatatargetTypesService], (service: DatatargetTypesService) => {
+ expect(service).toBeTruthy();
+ }));
+});
diff --git a/src/app/applications/datatarget/datatarget-types.service.ts b/src/app/applications/datatarget/datatarget-types.service.ts
new file mode 100644
index 00000000..581788cf
--- /dev/null
+++ b/src/app/applications/datatarget/datatarget-types.service.ts
@@ -0,0 +1,84 @@
+import { Injectable, Type } from '@angular/core';
+import { DataTargetType } from '@shared/enums/datatarget-type';
+import { DatatargetDetail } from './datatarget-detail/datatarget-detail';
+import { DatatargetEdit } from './datatarget-edit/datatarget-edit';
+import { DatatargetTypeDescriptor } from './datatarget.model';
+import { FiwareDetailComponent } from './fiware/fiware-detail/fiware-detail.component';
+import { FiwareEditComponent } from './fiware/fiware-edit/fiware-edit.component';
+import { HttppushDetailComponent } from './httppush/httppush-detail/httppush-detail.component';
+import { HttppushEditComponent } from './httppush/httppush-edit/httppush-edit.component';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class DatatargetTypesService {
+
+constructor() { }
+
+ getAvailableDataTargetTypes() : DatatargetTypeDescriptor[]
+ {
+ return [
+ { name: 'Generisk HTTP Push',
+ type: DataTargetType.HTTPPUSH,
+ icon: null,
+ description: 'Send data med HTTP POST requests til et HTTP URL endpoint',
+ readMoreUrl: '',
+ provider: 'OS2'
+
+ },
+ { name: 'Open Data DK',
+ type: DataTargetType.OPENDATADK,
+ icon: '/assets/images/logo_opendatadk.svg',
+ description: 'Offentliggør datasæt i Open Data DK\'s åbne dataportal.',
+ readMoreUrl: 'https://www.opendata.dk/',
+ provider: 'OS2'
+
+ },
+ { name: 'FIWARE connection',
+ type: DataTargetType.FIWARE,
+ icon: '/assets/images/logo_FIWARE.png',
+ description: 'En integration til FIWARE Context Broker' ,
+ readMoreUrl: 'https://www.kmd.dk',
+ provider: 'KMD A/S'
+ }
+ ]
+ }
+
+ getDetailComponent(dataTargetType: DataTargetType): Type
+ {
+ if (dataTargetType === DataTargetType.HTTPPUSH)
+ {
+ return HttppushDetailComponent;
+ }
+
+ if (dataTargetType === DataTargetType.OPENDATADK)
+ {
+ return HttppushDetailComponent;
+ }
+
+ if (dataTargetType === DataTargetType.FIWARE)
+ {
+ return FiwareDetailComponent;
+ }
+ }
+
+ getEditComponent(dataTargetType: DataTargetType): Type
+ {
+ if (dataTargetType === DataTargetType.HTTPPUSH)
+ {
+ return HttppushEditComponent;
+ }
+
+ if (dataTargetType === DataTargetType.OPENDATADK)
+ {
+ return HttppushEditComponent;
+ }
+
+ if (dataTargetType === DataTargetType.FIWARE)
+ {
+ return FiwareEditComponent;
+ }
+ }
+
+}
+
diff --git a/src/app/applications/datatarget/datatarget.model.ts b/src/app/applications/datatarget/datatarget.model.ts
index 03fba64b..12e846c9 100644
--- a/src/app/applications/datatarget/datatarget.model.ts
+++ b/src/app/applications/datatarget/datatarget.model.ts
@@ -7,6 +7,8 @@ export class Datatarget {
applicationId: number;
type: DataTargetType = DataTargetType.HTTPPUSH;
url: string;
+ tenant: string;
+ context: string;
//default 30 sec
timeout: number = 30000;
authorizationHeader: string;
@@ -24,4 +26,14 @@ export class DatatargetData {
data: Datatarget[];
ok?: boolean;
count?: number;
+}
+
+export class DatatargetTypeDescriptor
+{
+ name: string;
+ type: DataTargetType;
+ icon: string;
+ description: string;
+ readMoreUrl: string;
+ provider: string;
}
\ No newline at end of file
diff --git a/src/app/applications/datatarget/datatarget.module.ts b/src/app/applications/datatarget/datatarget.module.ts
index 58bd4078..ac65cbea 100644
--- a/src/app/applications/datatarget/datatarget.module.ts
+++ b/src/app/applications/datatarget/datatarget.module.ts
@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DatatargetTableComponent } from './datatarget-table/datatarget-table.component';
-import { DatatargetListComponent } from './datatarget-list/datatarget-list.component';
import { DatatargetEditComponent } from './datatarget-edit/datatarget-edit.component';
import { DatatargetDetailComponent } from './datatarget-detail/datatarget-detail.component';
import { TranslateModule } from '@ngx-translate/core';
@@ -15,16 +14,29 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from '@shared/shared.module';
import { PipesModule } from '@shared/pipes/pipes.module';
+import { DatatargetNewComponent } from './datatarget-new/datatarget-new.component';
+import { FiwareEditComponent } from './fiware/fiware-edit/fiware-edit.component';
+import { FiwareDetailComponent } from './fiware/fiware-detail/fiware-detail.component';
+import { HttppushDetailComponent } from './httppush/httppush-detail/httppush-detail.component';
+import { HttppushEditComponent } from './httppush/httppush-edit/httppush-edit.component';
+import { DatatargetDetailTypeSelectorDirective } from './datatarget-detail/datatarget-detail-type-selector.directive';
+import { DatatargetEditTypeSelectorDirective } from './datatarget-edit/datatarget-edit-type-selector.directive';
@NgModule({
- declarations: [
+ declarations: [
DatatargetTableComponent,
- DatatargetListComponent,
DatatargetEditComponent,
+ DatatargetNewComponent,
DatatargetDetailComponent,
+ FiwareDetailComponent,
+ FiwareEditComponent,
+ HttppushDetailComponent,
+ HttppushEditComponent,
OpendatadkComponent,
OpendatadkEditComponent,
- OpendatadkDetailComponent],
+ OpendatadkDetailComponent,
+ DatatargetDetailTypeSelectorDirective,
+ DatatargetEditTypeSelectorDirective],
imports: [
CommonModule,
RouterModule,
@@ -39,9 +51,13 @@ import { PipesModule } from '@shared/pipes/pipes.module';
],
exports: [
DatatargetTableComponent,
- DatatargetListComponent,
DatatargetEditComponent,
- DatatargetDetailComponent,
+ DatatargetNewComponent,
+ DatatargetDetailComponent,
+ FiwareDetailComponent,
+ FiwareEditComponent,
+ HttppushDetailComponent,
+ HttppushEditComponent,
NGMaterialModule
]
})
diff --git a/src/app/applications/datatarget/datatarget.service.ts b/src/app/applications/datatarget/datatarget.service.ts
index 72abdb98..6647ed30 100644
--- a/src/app/applications/datatarget/datatarget.service.ts
+++ b/src/app/applications/datatarget/datatarget.service.ts
@@ -66,7 +66,7 @@ export class DatatargetService {
return datatarget;
}
)
- );;
+ );
}
delete(id: number) {
@@ -90,6 +90,8 @@ export class DatatargetService {
timeout: dataTargetResponse.timeout,
type: dataTargetResponse.type,
url: dataTargetResponse.url,
+ tenant: dataTargetResponse.tenant,
+ context: dataTargetResponse.context,
authorizationHeader: dataTargetResponse.authorizationHeader,
applicationId: dataTargetResponse.application.id,
setToOpendataDk: dataTargetResponse?.openDataDkDataset ? true : false,
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
new file mode 100644
index 00000000..8e895d9c
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
{{ 'DATATARGET.DETAILS' | translate }}
+
+
+
Context Broker{{datatarget.url}}
+
{{ 'DATATARGET.TIMEOUT' | translate }}{{datatarget.timeout}}
+
{{ 'DATATARGET.TYPE' | translate }}{{'DATATARGET.' + datatarget.type | translate}}
+
+
+
{{ 'DATATARGET.TENANT' | translate }}
+
{{datatarget.tenant}}
+
+ {{ 'DATATARGET.NO-TENANT' | translate }}
+
+
+
{{ 'DATATARGET.CONTEXT' | translate }}
+
{{datatarget.context}}
+
+ {{ 'DATATARGET.NO-CONTEXT' | translate }}
+
+
+
{{ 'DATATARGET.AUTHORIZATIONHEADER' | translate }}
+
{{datatarget.authorizationHeader}}
+
+ {{ 'DATATARGET.NO-AUTHORIZATIONHEADER' | translate }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.scss b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.spec.ts b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.spec.ts
new file mode 100644
index 00000000..7f5198d9
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.spec.ts
@@ -0,0 +1,28 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { FiwareDetailComponent } from './fiware-detail.component';
+
+describe('FiwareDetailComponent', () => {
+ let component: FiwareDetailComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ FiwareDetailComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FiwareDetailComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 00000000..d56148e6
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-detail/fiware-detail.component.ts
@@ -0,0 +1,98 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { ActivatedRoute } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { PayloadDeviceDatatargetGetByDataTarget } from '@app/payload-decoder/payload-device-data.model';
+import { PayloadDeviceDatatargetService } from '@app/payload-decoder/payload-device-datatarget.service';
+import { BackButton } from '@shared/models/back-button.model';
+import { DatatargetService } from '../../datatarget.service';
+import { Location } from '@angular/common';
+import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+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';
+
+
+@Component({
+ selector: 'app-fiware-detail',
+ templateUrl: './fiware-detail.component.html',
+ styleUrls: ['./fiware-detail.component.scss']
+})
+export class FiwareDetailComponent implements DatatargetDetail, OnInit, OnDestroy {
+
+ public datatargetSubscription: Subscription;
+ public datatarget: Datatarget;
+ public backButton: BackButton = { label: '', routerLink: '' };
+ public dataTargetRelations: PayloadDeviceDatatargetGetByDataTarget[];
+ private deleteDialogSubscription: Subscription;
+ public dropdownButton: DropdownButton;
+ arrowsAltH = faArrowsAltH;
+
+ constructor(
+ private route: ActivatedRoute,
+ private deleteDialogService: DeleteDialogService,
+ private location: Location,
+ private datatargetRelationServicer: PayloadDeviceDatatargetService,
+ private datatargetService: DatatargetService,
+ public translate: TranslateService) { }
+
+ ngOnInit(): void {
+ const id: number = +this.route.snapshot.paramMap.get('datatargetId');
+
+ if (id) {
+ this.getDatatarget(id);
+ this.getDatatargetRelations(id);
+ this.dropdownButton = {
+ label: '',
+ editRouterLink: '../../datatarget-edit/' + id,
+ isErasable: true,
+ };
+ }
+ this.translate.get(['NAV.MY-DATATARGET', 'DATATARGET.SHOW-OPTIONS'])
+ .subscribe(translations => {
+ this.backButton.label = translations['NAV.MY-DATATARGET'];
+ this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS'];
+ });
+ }
+
+ getDatatarget(id: number) {
+ this.datatargetService.get(id)
+ .subscribe((dataTarget: Datatarget) => {
+ this.datatarget = dataTarget;
+ this.setBackButton(this.datatarget.applicationId);
+ });
+ }
+
+ private setBackButton(applicationId: number) {
+ this.backButton.routerLink = ['applications', applicationId.toString() ];
+ }
+
+ onDeleteDatatarget() {
+ this.deleteDialogSubscription = this.deleteDialogService.showSimpleDialog().subscribe(
+ (response) => {
+ if (response) {
+ this.datatargetService.delete(this.datatarget.id).subscribe((response) => {
+ });
+ this.location.back();
+ } else {
+ console.log(response);
+ }
+ }
+ );
+ }
+
+ getDatatargetRelations(id: number) {
+ this.datatargetRelationServicer.getByDataTarget(id)
+ .subscribe((response) => {
+ this.dataTargetRelations = response.data;
+ });
+ }
+
+ ngOnDestroy(): void {
+ if (this.deleteDialogSubscription) {
+ this.deleteDialogSubscription.unsubscribe();
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..02ec7529
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.html
@@ -0,0 +1,167 @@
+
+
+
+
diff --git a/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.scss b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.scss
new file mode 100644
index 00000000..bb96f657
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.scss
@@ -0,0 +1,4 @@
+.form-info-icon {
+ margin-left: 5px;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.spec.ts b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.spec.ts
new file mode 100644
index 00000000..6a58e7f6
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.spec.ts
@@ -0,0 +1,28 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { FiwareEditComponent } from './fiware-edit.component';
+
+describe('FiwareEditComponent', () => {
+ let component: FiwareEditComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ FiwareEditComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FiwareEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 00000000..b52a823b
--- /dev/null
+++ b/src/app/applications/datatarget/fiware/fiware-edit/fiware-edit.component.ts
@@ -0,0 +1,336 @@
+import { Component, OnInit, Input, OnDestroy } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Datatarget } from '../../datatarget.model';
+import { Observable, Subscription } from 'rxjs';
+import { Application } from '@applications/application.model';
+import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { faTimesCircle } from '@fortawesome/free-solid-svg-icons';
+import { PayloadDeviceDatatarget, PayloadDeviceDatatargetGetByDataTargetResponse } from '@payload-decoder/payload-device-data.model';
+import { DatatargetService } from '../../datatarget.service';
+import { ApplicationService } from '@applications/application.service';
+import { PayloadDecoderService } from '@payload-decoder/payload-decoder.service';
+import { PayloadDeviceDatatargetService } from '@payload-decoder/payload-device-datatarget.service';
+import { SnackService } from '@shared/services/snack.service';
+import { MatDialog } from '@angular/material/dialog';
+import { HttpErrorResponse } from '@angular/common/http';
+import { PayloadDecoderMappedResponse } from '@payload-decoder/payload-decoder.model';
+import { DeleteDialogComponent } from '@shared/components/delete-dialog/delete-dialog.component';
+import { ErrorMessageService } from '@shared/error-message.service';
+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';
+
+@Component({
+ selector: 'app-fiware-edit',
+ templateUrl: './fiware-edit.component.html',
+ styleUrls: ['./fiware-edit.component.scss']
+})
+export class FiwareEditComponent implements DatatargetEdit, OnInit, OnDestroy {
+
+
+ public multiPage = false;
+ public title = '';
+ public sectionTitle = '';
+ public backButtonTitle = '';
+ @Input() submitButton: string;
+ public datatarget: Datatarget = new Datatarget();
+ faTimesCircle = faTimesCircle;
+ public datatargetSubscription: Subscription;
+ public relationSubscription: Subscription;
+ public applicationSubscription: Subscription;
+ public payloadDecoderSubscription: Subscription;
+ public errorMessages: any;
+ public errorFields: string[];
+ public formFailedSubmit = false;
+ public datatargetid: number;
+ private applicationId: number;
+ public application: Application;
+ public devices: IotDevice[];
+ public payloadDecoders = [];
+ private counter: number;
+ payloadDeviceDatatarget: PayloadDeviceDatatarget[];
+ newDynamic: any = {};
+ faQuestionCircle = faQuestionCircle;
+
+
+ constructor(
+ public translate: TranslateService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private datatargetService: DatatargetService,
+ private applicationService: ApplicationService,
+ private payloadDecoderService: PayloadDecoderService,
+ private payloadDeviceDataTargetService: PayloadDeviceDatatargetService,
+ private snackService: SnackService,
+ private dialog: MatDialog,
+ private errorMessageService: ErrorMessageService,
+ private scrollToTopService: ScrollToTopService,
+ ) {
+ translate.use('da');
+ }
+
+
+
+ ngOnInit() {
+ this.translate
+ .get([
+ 'FORM.CREATE-NEW-DATATARGET',
+ 'FORM.EDIT-DATATARGET',
+ 'DATATARGET.SAVE',
+ 'NAV.DATATARGET',
+ ])
+ .subscribe((translations) => {
+ const datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
+ if (datatargetid !== 0) {
+ this.title = translations['FORM.EDIT-DATATARGET'];
+ } else {
+ this.title = translations['FORM.CREATE-NEW-DATATARGET'];
+ }
+ this.submitButton = translations['DATATARGET.SAVE'];
+ this.backButtonTitle = translations['NAV.DATATARGET'];
+ });
+
+ this.datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
+ this.applicationId = +this.route.snapshot.paramMap.get('id');
+
+ this.datatarget.type = DataTargetType.FIWARE;
+
+ if (this.datatargetid !== 0) {
+ this.getDatatarget(this.datatargetid);
+ this.getPayloadDeviceDatatarget(this.datatargetid);
+ }
+ if (this.applicationId !== 0) {
+ this.getDevices();
+ }
+ this.getPayloadDecoders();
+
+ }
+
+
+ addRow() {
+ if (!this.payloadDeviceDatatarget) {
+ this.payloadDeviceDatatarget = [];
+ }
+ this.payloadDeviceDatatarget.push({ id: null, iotDeviceIds: [], payloadDecoderId: null, dataTargetId: this.datatargetid });
+ }
+
+ private deleteRow(index) {
+ if (this.payloadDeviceDatatarget.length === 0) {
+ } else if (this.payloadDeviceDatatarget[index]?.id === null) {
+ this.payloadDeviceDatatarget.splice(index, 1);
+ } else {
+ this.payloadDeviceDataTargetService.delete(this.payloadDeviceDatatarget[index].id)
+ .subscribe((response) => {
+ this.payloadDeviceDatatarget.splice(index, 1);
+ });
+ }
+ }
+
+ openDeleteDialog(index) {
+ const dialog = this.dialog.open(DeleteDialogComponent, {
+ data: {
+ showAccept: true,
+ showCancel: true,
+ message: 'Er du sikker på at du vil slette?'
+ }
+ });
+
+ dialog.afterClosed().subscribe((result) => {
+ if (result === true) {
+ this.deleteRow(index);
+ }
+ });
+ }
+
+ onSubmit(): void {
+ this.counter = 0;
+ if (this.datatargetid) {
+ this.updateDatatarget();
+ this.addPayloadDeviceDatatarget();
+ } else {
+ this.createDatatarget();
+ }
+ }
+
+ public compare(o1: any, o2: any): boolean {
+ return o1 === o2;
+ }
+
+ updateDatatarget() {
+ this.resetErrors();
+ this.counter = 1 + (this.payloadDeviceDatatarget?.length ? this.payloadDeviceDatatarget?.length : 0);
+ this.datatargetService.update(this.datatarget)
+ .subscribe(
+ (response: Datatarget) => {
+ this.datatarget = response;
+ this.countToRedirect();
+ },
+ (error: HttpErrorResponse) => {
+ this.handleError(error);
+ this.formFailedSubmit = true;
+ }
+ );
+ }
+
+ addPayloadDeviceDatatarget() {
+ this.payloadDeviceDatatarget.map(
+ pdd => {
+ if (pdd.payloadDecoderId === 0) {
+ pdd.payloadDecoderId = null;
+ }
+ }
+ );
+ this.payloadDeviceDatatarget.forEach((relation) => {
+ if (relation.id) {
+ this.payloadDeviceDataTargetService.put(relation).subscribe(
+ (response) => {
+ this.countToRedirect();
+ },
+ (error) => {
+ this.handleError(error);
+ }
+ );
+ } else {
+ this.payloadDeviceDataTargetService.post(relation).subscribe(
+ (res: any) => {
+ this.countToRedirect();
+ },
+ (error) => {
+ this.handleError(error);
+ }
+ );
+ }
+ });
+ }
+
+ countToRedirect() {
+ this.counter -= 1;
+ if (this.counter <= 0 && !this.formFailedSubmit) {
+ this.showSavedSnack();
+ this.routeToDatatargets();
+ }
+ }
+
+ getPayloadDeviceDatatarget(id: number) {
+ this.relationSubscription = this.payloadDeviceDataTargetService
+ .getByDataTarget(id)
+ .subscribe((response: PayloadDeviceDatatargetGetByDataTargetResponse) => {
+ this.mapToDatatargetDevicePayload(response);
+ });
+ }
+
+ createDatatarget() {
+ this.resetErrors();
+ this.datatarget.applicationId = this.applicationId;
+ this.datatargetService.create(this.datatarget)
+ .subscribe((response: Datatarget) => {
+ this.datatargetid = response.id;
+ this.datatarget = response;
+ this.showSavedSnack();
+ },
+ (error: HttpErrorResponse) => {
+ this.handleError(error);
+ this.formFailedSubmit = true;
+ });
+
+ }
+
+ private resetErrors() {
+ this.errorFields = [];
+ this.errorMessages = undefined;
+ this.formFailedSubmit = false;
+ }
+
+
+
+ getDevices(): void {
+ this.applicationSubscription = this.applicationService.getApplication(this.applicationId)
+ .subscribe((application: Application) => {
+ this.devices = application.iotDevices;
+ });
+ }
+
+ public selectAllDevices(index: number) {
+ this.payloadDeviceDatatarget[index].iotDeviceIds = this.devices.map(device => device.id);
+ }
+
+ public deSelectAllDevices(index: number) {
+ this.payloadDeviceDatatarget[index].iotDeviceIds = [];
+ }
+
+ getPayloadDecoders() {
+ this.payloadDecoderSubscription = this.payloadDecoderService.getMultiple(1000, 0, 'id', 'ASC')
+ .subscribe((response: PayloadDecoderMappedResponse) => {
+ this.payloadDecoders = response.data;
+ });
+ }
+
+ handleError(error: HttpErrorResponse) {
+ const errors = this.errorMessageService.handleErrorMessageWithFields(error);
+ this.errorFields = errors.errorFields;
+ this.errorMessages = errors.errorMessages;
+ this.scrollToTopService.scrollToTop();
+ }
+
+ routeToDatatargets(): void {
+ this.router.navigate(['applications', this.applicationId.toString()]);
+ }
+
+ onCoordinateKey(event: any) {
+ if (event.target.value.length > event.target.maxLength) {
+ event.target.value = event.target.value.slice(
+ 0,
+ event.target.maxLength
+ );
+ }
+ }
+
+ getDatatarget(id: number) {
+ this.datatargetSubscription = this.datatargetService
+ .get(id)
+ .subscribe((response: Datatarget) => {
+ this.datatarget = response;
+ });
+ }
+
+ showSavedSnack() {
+ this.snackService.showSavedSnack();
+ }
+
+ disableSaveButton(): boolean {
+ const disable = false;
+
+ return disable;
+ }
+
+ ngOnDestroy(): void {
+ if (this.relationSubscription) {
+ this.relationSubscription.unsubscribe();
+ }
+ if (this.applicationSubscription) {
+ this.applicationSubscription.unsubscribe();
+ }
+ if (this.datatargetSubscription) {
+ this.datatargetSubscription.unsubscribe();
+ }
+ if (this.payloadDecoderSubscription) {
+ this.payloadDecoderSubscription.unsubscribe();
+ }
+ }
+
+ private mapToDatatargetDevicePayload(dto: PayloadDeviceDatatargetGetByDataTargetResponse) {
+ this.payloadDeviceDatatarget = [];
+ dto.data.forEach(
+ (element) => {
+ this.payloadDeviceDatatarget.push({
+ id: element.id,
+ iotDeviceIds: element.iotDevices.map((x) => x.id),
+ payloadDecoderId: element.payloadDecoder?.id === undefined ? 0 : element.payloadDecoder?.id,
+ dataTargetId: element.dataTarget.id
+ });
+ }
+ );
+ }
+}
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
new file mode 100644
index 00000000..42cf6d1f
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
{{ 'DATATARGET.DETAILS' | translate }}
+
+
+
{{ 'DATATARGET.URL' | translate }}{{datatarget.url}}
+
{{ 'DATATARGET.TIMEOUT' | translate }}{{datatarget.timeout}}
+
{{ 'DATATARGET.TYPE' | translate }}{{'DATATARGET.' + datatarget.type | translate}}
+
+
{{ 'DATATARGET.AUTHORIZATIONHEADER' | translate }}
+
{{datatarget.authorizationHeader}}
+
+ {{ 'DATATARGET.NO-AUTHORIZATIONHEADER' | translate }}
+
+
+
+
+
+
+
{{ 'DATATARGET.OPENDATA-DK' | translate }}
+
+
+ {{ 'DATATARGET.NO-OPENDATA-DK' | translate }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.scss b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.scss
new file mode 100644
index 00000000..e5e49dff
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.scss
@@ -0,0 +1,3 @@
+pre {
+ word-wrap: break-word;
+ }
\ No newline at end of file
diff --git a/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.spec.ts b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.spec.ts
new file mode 100644
index 00000000..a212025a
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.spec.ts
@@ -0,0 +1,28 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { HttppushDetailComponent } from './httppush-detail.component';
+
+describe('HttppushDetailComponent', () => {
+ let component: HttppushDetailComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ HttppushDetailComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HttppushDetailComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 00000000..179aebcf
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-detail/httppush-detail.component.ts
@@ -0,0 +1,98 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { Subscription } from 'rxjs';
+import { ActivatedRoute } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { PayloadDeviceDatatargetGetByDataTarget } from '@app/payload-decoder/payload-device-data.model';
+import { PayloadDeviceDatatargetService } from '@app/payload-decoder/payload-device-datatarget.service';
+import { BackButton } from '@shared/models/back-button.model';
+import { DatatargetService } from '../../datatarget.service';
+import { Location } from '@angular/common';
+import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+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';
+
+@Component({
+ selector: 'app-httppush-detail',
+ templateUrl: './httppush-detail.component.html',
+ styleUrls: ['./httppush-detail.component.scss']
+})
+export class HttppushDetailComponent implements DatatargetDetail, OnInit, OnDestroy {
+
+ public datatargetSubscription: Subscription;
+ public datatarget: Datatarget;
+ public backButton: BackButton = { label: '', routerLink: '' };
+ public dataTargetRelations: PayloadDeviceDatatargetGetByDataTarget[];
+ private deleteDialogSubscription: Subscription;
+ public dropdownButton: DropdownButton;
+ arrowsAltH = faArrowsAltH;
+ private applicationName: string;
+
+ constructor(
+ private route: ActivatedRoute,
+ private deleteDialogService: DeleteDialogService,
+ private location: Location,
+ private datatargetRelationServicer: PayloadDeviceDatatargetService,
+ private datatargetService: DatatargetService,
+ public translate: TranslateService) { }
+
+ ngOnInit(): void {
+ const id: number = +this.route.snapshot.paramMap.get('datatargetId');
+ this.applicationName = this.route.snapshot.paramMap.get('name');
+ if (id) {
+ this.getDatatarget(id);
+ this.getDatatargetRelations(id);
+ this.dropdownButton = {
+ label: '',
+ editRouterLink: '../../datatarget-edit/' + id,
+ isErasable: true,
+ };
+ }
+ this.translate.get(['NAV.MY-DATATARGET', 'DATATARGET.SHOW-OPTIONS'])
+ .subscribe(translations => {
+ this.backButton.label = translations['NAV.MY-DATATARGET'];
+ this.dropdownButton.label = translations['DATATARGET.SHOW-OPTIONS'];
+ });
+ }
+
+ getDatatarget(id: number) {
+ this.datatargetService.get(id)
+ .subscribe((dataTarget: Datatarget) => {
+ this.datatarget = dataTarget;
+ this.setBackButton(this.datatarget.applicationId);
+ });
+ }
+
+ private setBackButton(applicationId: number) {
+ this.backButton.routerLink = ['applications', applicationId.toString()];
+ }
+
+ onDeleteDatatarget() {
+ this.deleteDialogSubscription = this.deleteDialogService.showSimpleDialog().subscribe(
+ (response) => {
+ if (response) {
+ this.datatargetService.delete(this.datatarget.id).subscribe((response) => {
+ });
+ this.location.back();
+ } else {
+ console.log(response);
+ }
+ }
+ );
+ }
+
+ getDatatargetRelations(id: number) {
+ this.datatargetRelationServicer.getByDataTarget(id)
+ .subscribe((response) => {
+ this.dataTargetRelations = response.data;
+ });
+ }
+
+ ngOnDestroy(): void {
+ if (this.deleteDialogSubscription) {
+ this.deleteDialogSubscription.unsubscribe();
+ }
+ }
+
+}
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
new file mode 100644
index 00000000..4d4df069
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.html
@@ -0,0 +1,139 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.scss b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.spec.ts b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.spec.ts
new file mode 100644
index 00000000..1ff864db
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.spec.ts
@@ -0,0 +1,28 @@
+/* tslint:disable:no-unused-variable */
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { DebugElement } from '@angular/core';
+
+import { HttppushEditComponent } from './httppush-edit.component';
+
+describe('HttppushEditComponent', () => {
+ let component: HttppushEditComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ HttppushEditComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(HttppushEditComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
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
new file mode 100644
index 00000000..d192c2a6
--- /dev/null
+++ b/src/app/applications/datatarget/httppush/httppush-edit/httppush-edit.component.ts
@@ -0,0 +1,406 @@
+import { Component, OnInit, Input, OnDestroy } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Datatarget } from '../../datatarget.model';
+import { Observable, Subscription } from 'rxjs';
+import { Application } from '@applications/application.model';
+import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { faTimesCircle } from '@fortawesome/free-solid-svg-icons';
+import {
+ PayloadDeviceDatatarget,
+ PayloadDeviceDatatargetGetByDataTargetResponse,
+} from '@payload-decoder/payload-device-data.model';
+import { DatatargetService } from '../../datatarget.service';
+import { ApplicationService } from '@applications/application.service';
+import { PayloadDecoderService } from '@payload-decoder/payload-decoder.service';
+import { PayloadDeviceDatatargetService } from '@payload-decoder/payload-device-datatarget.service';
+import { SnackService } from '@shared/services/snack.service';
+import { MatDialog } from '@angular/material/dialog';
+import { HttpErrorResponse } from '@angular/common/http';
+import { PayloadDecoderMappedResponse } from '@payload-decoder/payload-decoder.model';
+import { DeleteDialogComponent } from '@shared/components/delete-dialog/delete-dialog.component';
+import { ErrorMessageService } from '@shared/error-message.service';
+import { OpendatadkDialogService } from '@shared/components/opendatadk-dialog/opendatadk-dialog.service';
+import { OpendatadkService } from '@shared/services/opendatadk.service';
+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';
+
+@Component({
+ selector: 'app-httppush-edit',
+ templateUrl: './httppush-edit.component.html',
+ styleUrls: ['./httppush-edit.component.scss'],
+})
+export class HttppushEditComponent
+ implements DatatargetEdit, OnInit, OnDestroy {
+ public multiPage = false;
+ public title = '';
+ public sectionTitle = '';
+ public backButtonTitle = '';
+ @Input() submitButton: string;
+ public datatarget: Datatarget = new Datatarget();
+ faTimesCircle = faTimesCircle;
+ public datatargetSubscription: Subscription;
+ public relationSubscription: Subscription;
+ public applicationSubscription: Subscription;
+ public payloadDecoderSubscription: Subscription;
+ public errorMessages: any;
+ public errorFields: string[];
+ public formFailedSubmit = false;
+ public datatargetid: number;
+ private applicationId: number;
+ private applicationName: string;
+ public application: Application;
+ public devices: IotDevice[];
+ public payloadDecoders = [];
+ private counter: number;
+ private dataSetExcists = false;
+ private isMailDialogAlreadyShown = false;
+
+ payloadDeviceDatatarget: PayloadDeviceDatatarget[];
+ newDynamic: any = {};
+
+ constructor(
+ public translate: TranslateService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private datatargetService: DatatargetService,
+ private applicationService: ApplicationService,
+ private payloadDecoderService: PayloadDecoderService,
+ private payloadDeviceDataTargetService: PayloadDeviceDatatargetService,
+ private saveSnackService: SnackService,
+ private dialog: MatDialog,
+ private errorMessageService: ErrorMessageService,
+ private opendatadkService: OpendatadkService,
+ private opendatadkDialogService: OpendatadkDialogService,
+ private scrollToTopService: ScrollToTopService
+ ) {
+ translate.use('da');
+ }
+
+ ngOnInit() {
+ this.translate
+ .get([
+ 'FORM.CREATE-NEW-DATATARGET',
+ 'FORM.EDIT-DATATARGET',
+ 'DATATARGET.SAVE',
+ 'NAV.DATATARGET',
+ ])
+ .subscribe((translations) => {
+ const datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
+ if (datatargetid !== 0) {
+ this.title = translations['FORM.EDIT-DATATARGET'];
+ } else {
+ this.title = translations['FORM.CREATE-NEW-DATATARGET'];
+ }
+ this.submitButton = translations['DATATARGET.SAVE'];
+ this.backButtonTitle = translations['NAV.DATATARGET'];
+ });
+
+ this.datatargetid = +this.route.snapshot.paramMap.get('datatargetId');
+ this.applicationId = +this.route.snapshot.paramMap.get('id');
+ this.applicationName = this.route.snapshot.paramMap.get('name');
+ if (this.datatargetid !== 0) {
+ this.getDatatarget(this.datatargetid);
+ this.getPayloadDeviceDatatarget(this.datatargetid);
+ }
+ if (this.applicationId !== 0) {
+ this.getDevices();
+ }
+ this.getPayloadDecoders();
+ this.setDataSetExcists();
+ }
+
+ addRow() {
+ if (!this.payloadDeviceDatatarget) {
+ this.payloadDeviceDatatarget = [];
+ }
+ this.payloadDeviceDatatarget.push({
+ id: null,
+ iotDeviceIds: [],
+ payloadDecoderId: null,
+ dataTargetId: this.datatargetid,
+ });
+ }
+
+ private deleteRow(index) {
+ if (this.payloadDeviceDatatarget.length === 0) {
+ } else if (this.payloadDeviceDatatarget[index]?.id === null) {
+ this.payloadDeviceDatatarget.splice(index, 1);
+ } else {
+ this.payloadDeviceDataTargetService
+ .delete(this.payloadDeviceDatatarget[index].id)
+ .subscribe((response) => {
+ this.payloadDeviceDatatarget.splice(index, 1);
+ });
+ }
+ }
+
+ openDeleteDialog(index) {
+ const dialog = this.dialog.open(DeleteDialogComponent, {
+ data: {
+ showAccept: true,
+ showCancel: true,
+ message: 'Er du sikker på at du vil slette?',
+ },
+ });
+
+ dialog.afterClosed().subscribe((result) => {
+ if (result === true) {
+ this.deleteRow(index);
+ }
+ });
+ }
+
+ onSubmit(): void {
+ this.counter = 0;
+ if (this.datatargetid) {
+ this.updateDatatarget();
+ this.addPayloadDeviceDatatarget();
+ } else {
+ this.createDatatarget();
+ }
+ }
+
+ public compare(o1: any, o2: any): boolean {
+ return o1 === o2;
+ }
+
+ updateDatatarget() {
+ this.resetErrors();
+ this.counter =
+ 1 +
+ (this.payloadDeviceDatatarget?.length
+ ? this.payloadDeviceDatatarget?.length
+ : 0);
+ this.datatargetService.update(this.datatarget).subscribe(
+ (response: Datatarget) => {
+ this.datatarget = response;
+ if (this.datatarget.openDataDkDataset != null) {
+ this.datatarget.openDataDkDataset.acceptTerms = true;
+ }
+ this.shouldShowMailDialog().subscribe((response) => {
+ this.countToRedirect();
+ });
+ },
+ (error: HttpErrorResponse) => {
+ this.checkDataTargetModelOpendatadkdatasaet();
+ this.handleError(error);
+ this.formFailedSubmit = true;
+ }
+ );
+ }
+
+ addPayloadDeviceDatatarget() {
+ this.payloadDeviceDatatarget.map((pdd) => {
+ if (pdd.payloadDecoderId === 0) {
+ pdd.payloadDecoderId = null;
+ }
+ });
+ this.payloadDeviceDatatarget.forEach((relation) => {
+ if (relation.id) {
+ this.payloadDeviceDataTargetService.put(relation).subscribe(
+ (response) => {
+ this.countToRedirect();
+ },
+ (error) => {
+ this.handleError(error);
+ }
+ );
+ } else {
+ this.payloadDeviceDataTargetService.post(relation).subscribe(
+ (res: any) => {
+ this.countToRedirect();
+ },
+ (error) => {
+ this.handleError(error);
+ }
+ );
+ }
+ });
+ }
+
+ countToRedirect() {
+ this.counter -= 1;
+ if (this.counter <= 0 && !this.formFailedSubmit) {
+ this.showSavedSnack();
+ this.routeToDatatargets();
+ }
+ }
+
+ getPayloadDeviceDatatarget(id: number) {
+ this.relationSubscription = this.payloadDeviceDataTargetService
+ .getByDataTarget(id)
+ .subscribe((response: PayloadDeviceDatatargetGetByDataTargetResponse) => {
+ this.mapToDatatargetDevicePayload(response);
+ });
+ }
+
+ createDatatarget() {
+ this.resetErrors();
+ this.datatarget.applicationId = this.applicationId;
+ this.datatargetService.create(this.datatarget).subscribe(
+ (response: Datatarget) => {
+ this.datatargetid = response.id;
+ this.datatarget = response;
+ if (this.datatarget.openDataDkDataset != null) {
+ this.datatarget.openDataDkDataset.acceptTerms = true;
+ }
+ this.showSavedSnack();
+ this.routeToDatatargets();
+ },
+ (error: HttpErrorResponse) => {
+ this.checkDataTargetModelOpendatadkdatasaet();
+ this.handleError(error);
+ this.formFailedSubmit = true;
+ }
+ );
+ }
+
+ private resetErrors() {
+ this.errorFields = [];
+ this.errorMessages = undefined;
+ this.formFailedSubmit = false;
+ }
+
+ checkDataTargetModelOpendatadkdatasaet() {
+ if (!this.datatarget.openDataDkDataset) {
+ this.datatarget.openDataDkDataset = new OpenDataDkDataset();
+ }
+ }
+
+ getDevices(): void {
+ this.applicationSubscription = this.applicationService
+ .getApplication(this.applicationId)
+ .subscribe((application: Application) => {
+ this.devices = application.iotDevices;
+ });
+ }
+
+ public selectAllDevices(index: number) {
+ this.payloadDeviceDatatarget[index].iotDeviceIds = this.devices.map(
+ (device) => device.id
+ );
+ }
+
+ public deSelectAllDevices(index: number) {
+ this.payloadDeviceDatatarget[index].iotDeviceIds = [];
+ }
+
+ getPayloadDecoders() {
+ this.payloadDecoderSubscription = this.payloadDecoderService
+ .getMultiple(1000, 0, 'id', 'ASC')
+ .subscribe((response: PayloadDecoderMappedResponse) => {
+ this.payloadDecoders = response.data;
+ });
+ }
+
+ handleError(error: HttpErrorResponse) {
+ const errors = this.errorMessageService.handleErrorMessageWithFields(error);
+ this.errorFields = errors.errorFields;
+ this.errorMessages = errors.errorMessages;
+ this.scrollToTopService.scrollToTop();
+ }
+
+ routeToDatatargets(): void {
+ this.router.navigate(['applications', this.applicationId.toString()]);
+ }
+
+ onCoordinateKey(event: any) {
+ if (event.target.value.length > event.target.maxLength) {
+ event.target.value = event.target.value.slice(0, event.target.maxLength);
+ }
+ }
+
+ getDatatarget(id: number) {
+ this.datatargetSubscription = this.datatargetService
+ .get(id)
+ .subscribe((response: Datatarget) => {
+ this.datatarget = response;
+ });
+ }
+
+ showSavedSnack() {
+ this.saveSnackService.showSavedSnack();
+ }
+
+ private setDataSetExcists() {
+ this.opendatadkService.get().subscribe((response) => {
+ this.dataSetExcists = response.dataset.length === 0 ? false : true;
+ });
+ }
+
+ private shouldShowMailDialog(): Observable {
+ return new Observable((observer) => {
+ if (
+ !this.dataSetExcists &&
+ this.datatarget.setToOpendataDk &&
+ !this.isMailDialogAlreadyShown
+ ) {
+ this.isMailDialogAlreadyShown = true;
+ this.opendatadkDialogService.showDialog().subscribe((response) => {
+ if (response) {
+ this.showMailClient();
+ }
+ observer.next(response);
+ });
+ } else {
+ observer.next(true);
+ }
+ });
+ }
+
+ private showMailClient() {
+ if (!this.datatarget.openDataDkDataset.url) {
+ this.datatarget.openDataDkDataset.url = this.datatargetService.getOpendataSharingApiUrl();
+ }
+ window.location.href =
+ 'mailto:FG2V@kk.dk?subject=Oprettelse%20af%20datas%C3%A6t%20i%20OpenDataDK&body=K%C3%A6re%20Frans%0D%0A%0D%0AHermed%20fremsendes%20linket%20til%20DCAT%20kataloget%20%2C%20du%20bedes%20registrere%20p%C3%A5%20Open%20Data%20DK%20platformen.%0D%0A%0D%0ALink%3A ' +
+ this.datatarget.openDataDkDataset.url;
+ }
+
+ disableSaveButton(): boolean {
+ let disable = true;
+ if (!this.datatarget.setToOpendataDk) {
+ disable = false;
+ } else if (this.datatarget.openDataDkDataset?.acceptTerms) {
+ disable = false;
+ } else {
+ disable = true;
+ }
+ return disable;
+ }
+
+ ngOnDestroy(): void {
+ if (this.relationSubscription) {
+ this.relationSubscription.unsubscribe();
+ }
+ if (this.applicationSubscription) {
+ this.applicationSubscription.unsubscribe();
+ }
+ if (this.datatargetSubscription) {
+ this.datatargetSubscription.unsubscribe();
+ }
+ if (this.payloadDecoderSubscription) {
+ this.payloadDecoderSubscription.unsubscribe();
+ }
+ }
+
+ private mapToDatatargetDevicePayload(
+ dto: PayloadDeviceDatatargetGetByDataTargetResponse
+ ) {
+ this.payloadDeviceDatatarget = [];
+ dto.data.forEach((element) => {
+ this.payloadDeviceDatatarget.push({
+ id: element.id,
+ iotDeviceIds: element.iotDevices.map((x) => x.id),
+ payloadDecoderId:
+ element.payloadDecoder?.id === undefined
+ ? 0
+ : element.payloadDecoder?.id,
+ dataTargetId: element.dataTarget.id,
+ });
+ });
+ }
+}
diff --git a/src/app/applications/iot-devices/downlink.model.ts b/src/app/applications/iot-devices/downlink.model.ts
index 6c5f1b8b..b5ed5035 100644
--- a/src/app/applications/iot-devices/downlink.model.ts
+++ b/src/app/applications/iot-devices/downlink.model.ts
@@ -1,6 +1,5 @@
-
export class Downlink {
- data: string;
- port = 0;
- confirmedDownlink = false;
+ data: string;
+ port = 0;
+ confirmedDownlink? = false;
}
diff --git a/src/app/applications/iot-devices/iot-device.model.ts b/src/app/applications/iot-devices/iot-device.model.ts
index 114e3d85..039647fd 100644
--- a/src/app/applications/iot-devices/iot-device.model.ts
+++ b/src/app/applications/iot-devices/iot-device.model.ts
@@ -8,70 +8,63 @@ import { ReceivedMessageMetadata } from '@shared/models/received-message-metadat
import { LatestReceivedMessage } from './latestReceivedMessage.model';
export class IotDevice {
- name: string;
- application?: Application;
- location: JsonLocation;
- commentOnLocation: string;
- comment: string;
- type: DeviceType = DeviceType.GENERICHTTP;
- receivedMessagesMetadata: ReceivedMessageMetadata[];
- metadata?: JSON;
- apiKey?: string;
- id: number;
- createdAt: Date;
- updatedAt: Date;
- createdBy: number;
- updatedBy: number;
- createdByName: string;
- updatedByName: string;
- applicationId: number;
- longitude = 0;
- latitude = 0;
- deviceModelId?: number;
- latestReceivedMessage: LatestReceivedMessage;
- lorawanSettings = new LorawanSettings();
- sigfoxSettings = new SigfoxSettings();
- deviceModel?: DeviceModel;
+ name: string;
+ application?: Application;
+ location: JsonLocation;
+ commentOnLocation: string;
+ comment: string;
+ type: DeviceType = DeviceType.GENERICHTTP;
+ receivedMessagesMetadata: ReceivedMessageMetadata[];
+ metadata?: JSON;
+ apiKey?: string;
+ id: number;
+ createdAt: Date;
+ updatedAt: Date;
+ createdBy: number;
+ updatedBy: number;
+ createdByName: string;
+ updatedByName: string;
+ applicationId: number;
+ longitude = 0;
+ latitude = 0;
+ deviceModelId?: number;
+ latestReceivedMessage: LatestReceivedMessage;
+ lorawanSettings = new LorawanSettings();
+ sigfoxSettings = new SigfoxSettings();
+ deviceModel?: DeviceModel;
}
-export class IotDeviceResponse {
- name: string;
- application?: Application;
- location: JsonLocation;
- commentOnLocation: string;
- comment: string;
- type: DeviceType = DeviceType.GENERICHTTP;
- receivedMessagesMetadata: ReceivedMessageMetadata[];
- metadata?: JSON;
- apiKey?: string;
- id: number;
- createdAt: Date;
- updatedAt: Date;
- applicationId: number;
- longitude = 0;
- latitude = 0;
- deviceModelId?: DeviceModel;
- latestReceivedMessage: LatestReceivedMessage;
- lorawanSettings = new LorawanSettings();
- sigfoxSettings = new SigfoxSettings();
-}
+export class IotDeviceResponse extends IotDevice {}
export interface IotDevicesResponse {
- data: IotDevice[];
- ok?: boolean;
- count?: number;
+ data: IotDevice[];
+ ok?: boolean;
+ count?: number;
}
-export class IoTDeviceMinimal {
- id: number;
+export interface IotDeviceImportRequest {
+ data: IotDevice[];
+}
+
+export interface IotDevicesImportResponse {
+ data: IotDevice;
+ idMetadata: {
name: string;
- canRead: boolean;
- organizationId: number;
applicationId: number;
- lastActiveTime: Date;
+ };
+ error?: Omit;
+}
+
+export class IoTDeviceMinimal {
+ id: number;
+ name: string;
+ canRead: boolean;
+ organizationId: number;
+ applicationId: number;
+ lastActiveTime: Date;
}
export class IoTDevicesMinimalResponse {
- data: IoTDeviceMinimal[];
- count: number;
-}
\ No newline at end of file
+ data: IoTDeviceMinimal[];
+ count: number;
+}
diff --git a/src/app/applications/iot-devices/iot-device.service.ts b/src/app/applications/iot-devices/iot-device.service.ts
index bedbf529..ff2d5fb5 100644
--- a/src/app/applications/iot-devices/iot-device.service.ts
+++ b/src/app/applications/iot-devices/iot-device.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
-import { IotDevice, IoTDevicesMinimalResponse, IotDevicesResponse } from './iot-device.model';
+import { IotDevice, IoTDevicesMinimalResponse, IotDevicesImportResponse, IotDeviceImportRequest } from './iot-device.model';
import { RestService } from 'src/app/shared/services/rest.service';
import { map } from 'rxjs/operators';
import { UserMinimalService } from '@app/admin/users/user-minimal.service';
@@ -14,17 +14,25 @@ export class IoTDeviceService {
constructor(
private restService: RestService,
- private userMinimalService: UserMinimalService
+ private userMinimalService: UserMinimalService
) { }
- createIoTDevice(body: IotDevice): Observable {
+ createIoTDevice(body: IotDevice): Observable {
return this.restService.post(this.BASEURL, body);
}
- updateIoTDevice(body: IotDevice, id: number): Observable {
+ updateIoTDevice(body: IotDevice, id: number): Observable {
return this.restService.put(this.BASEURL, body, id, { observe: 'response' });
}
+ createIoTDevices(body: IotDeviceImportRequest): Observable {
+ return this.restService.post(`${this.BASEURL}/createMany`, body);
+ }
+
+ updateIoTDevices(body: IotDeviceImportRequest): Observable {
+ return this.restService.post(`${this.BASEURL}/updateMany`, body);
+ }
+
getIoTDevice(id: number): Observable {
return this.restService.get(this.BASEURL, {}, id).pipe(
map(
@@ -60,7 +68,7 @@ export class IoTDeviceService {
}
getIoTDevicesUsingPayloadDecoderMinimal(payloadDecoderId: number, limit: number, offset: number): Observable {
- return this.restService.get(`iot-device/minimalByPayloadDecoder`, {limit: limit, offset: offset}, payloadDecoderId)
+ return this.restService.get(`${this.BASEURL}/minimalByPayloadDecoder`, {limit, offset}, payloadDecoderId);
}
deleteIoTDevice(id: number) {
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 ac550bb7..c2ece359 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
@@ -20,7 +20,6 @@ import { DeleteDialogComponent } from '@shared/components/delete-dialog/delete-d
import { DeviceType } from '@shared/enums/device-type';
import { MatDialog } from '@angular/material/dialog';
import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
-import { ReceivedMessageMetadata } from '@shared/models/received-message-metadata.model';
import { environment } from '@environments/environment';
import { startWith, switchMap, map, catchError } from 'rxjs/operators';
import { MeService } from '@shared/services/me.service';
diff --git a/src/app/applications/multicast/multicast-detail/multicast-detail.component.html b/src/app/applications/multicast/multicast-detail/multicast-detail.component.html
new file mode 100644
index 00000000..919ff162
--- /dev/null
+++ b/src/app/applications/multicast/multicast-detail/multicast-detail.component.html
@@ -0,0 +1,137 @@
+
+
+
+
+
+
{{ 'MULTICAST.BASIC-DETAILS' | translate }}
+
+
+ {{ 'MULTICAST.GROUPNAME' | translate }}{{ multicast.name | translate }}
+
+
+
{{ 'MULTICAST.LORAWAN-DETAILS' | translate }}
+
+
+ {{ 'MULTICAST.ADDRESS' | translate }}{{ multicast.mcAddr | translate }}
+
+
+ {{ 'MULTICAST.NETWORK-KEY' | translate }}{{ multicast.mcNwkSKey | translate }}
+
+
+ {{ 'MULTICAST.APPLICATION-KEY' | translate }}{{ multicast.mcAppSKey | translate }}
+
+
+ {{ 'MULTICAST.FRAMECOUNTER' | translate }}{{ multicast.fCnt }}
+
+
+ {{ 'MULTICAST.DATARATE' | translate }}{{ multicast.dr }}
+
+
+ {{ 'MULTICAST.FREQUENCY' | translate }}{{ multicast.frequency }}
+
+
+ {{ 'MULTICAST.GROUPTYPE' | translate }}{{ multicast.groupType }}
+
+
+
+
+
+
+
+
{{ 'APPLICATION.ATTACHED-IOT' | translate }}
+
+
+
+
+
+
Downlink
+
+
+ -
+ {{ error | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/applications/multicast/multicast-detail/multicast-detail.component.scss b/src/app/applications/multicast/multicast-detail/multicast-detail.component.scss
new file mode 100644
index 00000000..ea3f31bc
--- /dev/null
+++ b/src/app/applications/multicast/multicast-detail/multicast-detail.component.scss
@@ -0,0 +1,3 @@
+.loraDetails{
+ margin-top: 15px;
+}
\ No newline at end of file
diff --git a/src/app/applications/multicast/multicast-detail/multicast-detail.component.ts b/src/app/applications/multicast/multicast-detail/multicast-detail.component.ts
new file mode 100644
index 00000000..a2a43859
--- /dev/null
+++ b/src/app/applications/multicast/multicast-detail/multicast-detail.component.ts
@@ -0,0 +1,179 @@
+import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+import { BackButton } from '@shared/models/back-button.model';
+import { DropdownButton } from '@shared/models/dropdown-button.model';
+import { Subscription } from 'rxjs';
+import { Multicast } from '../multicast.model';
+import { Location } from '@angular/common';
+import { MulticastService } from '../multicast.service';
+import { SnackService } from '@shared/services/snack.service';
+import { Downlink } from '@applications/iot-devices/downlink.model';
+import { HttpErrorResponse } from '@angular/common/http';
+import { ErrorMessageService } from '@shared/error-message.service';
+import { MatDialog } from '@angular/material/dialog';
+import { DownlinkDialogComponent } from '@applications/iot-devices/iot-device-detail/downlink/downlink-dialog/downlink-dialog.component';
+import { keyPressedHex } from '@shared/constants/regex-constants';
+import { DownlinkService } from '@shared/services/downlink.service';
+
+@Component({
+ selector: 'app-multicast-detail',
+ templateUrl: './multicast-detail.component.html',
+ styleUrls: ['./multicast-detail.component.scss'],
+})
+export class MulticastDetailComponent implements OnInit, OnDestroy {
+ public multicast: Multicast;
+ public backButton: BackButton = { label: '', routerLink: '/multicast-list' };
+ private deleteDialogSubscription: Subscription;
+ public dropdownButton: DropdownButton;
+ public formFailedSubmit = false;
+ private applicationId: number;
+ public downlink = new Downlink();
+ @Input() errorMessages: string[];
+
+ constructor(
+ private route: ActivatedRoute,
+ private dialog: MatDialog,
+ private deleteDialogService: DeleteDialogService,
+ private location: Location,
+ private multicastService: MulticastService,
+ private translate: TranslateService,
+ private snackService: SnackService,
+ private errorMessageService: ErrorMessageService,
+ private downlinkService: DownlinkService
+ ) {}
+
+ ngOnInit(): void {
+ this.errorMessages = [];
+ const id: number = +this.route.snapshot.paramMap.get('multicastId');
+ if (id) {
+ this.getMulticast(id);
+ this.dropdownButton = {
+ label: '',
+ editRouterLink: '../../multicast-edit/' + id,
+ isErasable: true,
+ };
+ this.applicationId = +this.route.snapshot.paramMap.get('id');
+ }
+ this.translate
+ .get(['GEN.BACK', 'MULTICAST-TABLE-ROW.SHOW-OPTIONS'])
+ .subscribe((translations) => {
+ this.backButton.label = translations['GEN.BACK'];
+ this.dropdownButton.label =
+ translations['MULTICAST-TABLE-ROW.SHOW-OPTIONS'];
+ });
+ }
+
+ getMulticast(id: number) {
+ this.multicastService.get(id).subscribe((multicast: Multicast) => {
+ this.multicast = multicast;
+ this.setBackButton(this.applicationId);
+ });
+ }
+
+ private setBackButton(applicationId: number) {
+ this.backButton.routerLink = ['applications', applicationId.toString()];
+ }
+
+ // Class-B:
+ // only if classB can be used
+ // canShowPeriodicity(): boolean {
+ // if (this.multicast.groupType === MulticastType.ClassB) {
+ // return true;
+ // } else return false;
+ // }
+
+ onDeleteMulticast() {
+ this.deleteDialogSubscription = this.deleteDialogService
+ .showSimpleDialog()
+ .subscribe((response) => {
+ if (response) {
+ this.multicastService
+ .delete(this.multicast.id)
+ .subscribe((response) => {
+ if (response.status !== 0) {
+ this.snackService.showDeletedSnack();
+ this.location.back();
+ } else {
+ this.snackService.showFailSnack();
+ }
+ });
+ } else {
+ }
+ });
+ }
+
+ keyPressHexadecimal(event) {
+ // make sure only hexadecimal can be typed in input with adresses.
+ keyPressedHex(event);
+ }
+
+ private handleError(error: HttpErrorResponse) {
+ const errors = this.errorMessageService.handleErrorMessageWithFields(error);
+ this.errorMessages = errors.errorFields;
+ this.errorMessages = errors.errorMessages;
+ }
+
+ clickDownlink() {
+ if (this.multicast.iotDevices.length > 0) {
+ if (this.validateHex(this.downlink.data)) {
+ this.multicastService
+ .multicastGet(this.multicast.id)
+ .subscribe((response: any) => {
+ if (response.totalCount > 0) {
+ this.openDownlinkDialog();
+ } else {
+ this.startDownlink();
+ }
+ });
+ }
+ } else {
+ this.downlinkService.showSendDownlinkFailNoDevices();
+ }
+ }
+ openDownlinkDialog() {
+ const dialog = this.dialog.open(DownlinkDialogComponent, {});
+
+ dialog.afterClosed().subscribe((result) => {
+ if (result === true) {
+ this.startDownlink();
+ }
+ });
+ }
+
+ private startDownlink() {
+ this.errorMessages = [];
+ this.multicastService
+ .multicastPost(this.downlink, this.multicast.id)
+ .subscribe(
+ () => {
+ this.snackService.showInQueueSnack();
+ },
+ (error) => {
+ this.handleError(error);
+ }
+ );
+ }
+
+ private validateHex(input: string): boolean {
+ const isHexinput = /^[a-fA-F\d]+$/.test(input);
+
+ if (isHexinput) {
+ return true;
+ } else {
+ this.addToErrorMessage('MULTICAST.DOWNLINK.NO-PORT-OR-PAYLOAD');
+ return false;
+ }
+ }
+
+ addToErrorMessage(text: string) {
+ this.translate.get([text]).subscribe((translations) => {
+ this.errorMessages.push(translations[text]);
+ });
+ }
+
+ ngOnDestroy(): void {
+ this.deleteDialogSubscription?.unsubscribe();
+ }
+}
diff --git a/src/app/applications/multicast/multicast-edit/multicast-edit.component.html b/src/app/applications/multicast/multicast-edit/multicast-edit.component.html
new file mode 100644
index 00000000..8a2cd4cf
--- /dev/null
+++ b/src/app/applications/multicast/multicast-edit/multicast-edit.component.html
@@ -0,0 +1,271 @@
+
+
+
diff --git a/src/app/applications/multicast/multicast-edit/multicast-edit.component.scss b/src/app/applications/multicast/multicast-edit/multicast-edit.component.scss
new file mode 100644
index 00000000..74d588da
--- /dev/null
+++ b/src/app/applications/multicast/multicast-edit/multicast-edit.component.scss
@@ -0,0 +1,5 @@
+.onlyLorawan {
+ font-weight: bold;
+ font-size: 13px;
+ margin-left: 10px;
+}
diff --git a/src/app/applications/multicast/multicast-edit/multicast-edit.component.ts b/src/app/applications/multicast/multicast-edit/multicast-edit.component.ts
new file mode 100644
index 00000000..4bdde2f0
--- /dev/null
+++ b/src/app/applications/multicast/multicast-edit/multicast-edit.component.ts
@@ -0,0 +1,207 @@
+import { HttpErrorResponse } from '@angular/common/http';
+import { Component, Input, OnDestroy, OnInit } from '@angular/core';
+import { ActivatedRoute, Router } from '@angular/router';
+import { TranslateService } from '@ngx-translate/core';
+import { MulticastType } from '@shared/enums/multicast-type';
+import { ErrorMessageService } from '@shared/error-message.service';
+import { SnackService } from '@shared/services/snack.service';
+import { ScrollToTopService } from '@shared/services/scroll-to-top.service';
+import { ReplaySubject, Subject, Subscription } from 'rxjs';
+import { Multicast } from '../multicast.model';
+import { MulticastService } from '../multicast.service';
+import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { ApplicationService } from '@applications/application.service';
+import { keyPressedHex } from '@shared/constants/regex-constants';
+import { FormControl } from '@angular/forms';
+import { takeUntil } from 'rxjs/operators';
+
+@Component({
+ selector: 'app-multicast-edit',
+ templateUrl: './multicast-edit.component.html',
+ styleUrls: ['./multicast-edit.component.scss'],
+})
+export class MulticastEditComponent implements OnInit, OnDestroy {
+ public title: string;
+ public multicastId: number;
+ public errorMessages: unknown;
+ private multicastSubscription: Subscription;
+ public searchDevices: FormControl = new FormControl();
+ public errorFields: string[];
+ public iotDevices: IotDevice[] = [];
+ @Input() submitButton: string;
+ public backButtonTitle: string;
+ public multicast: Multicast = new Multicast();
+ private applicationId: number;
+ private onDestroy = new Subject();
+ public formFailedSubmit = false;
+ public multicastTypes: string[] = Object.values(MulticastType);
+ // Class-B: { public periodicities: number[] = [2, 4, 8, 16, 32, 64, 128]; // used for classB if it has to be used in the future }
+ public deviceFilterCtrl: FormControl = new FormControl();
+ public filteredDevicesMulti: ReplaySubject = new ReplaySubject<
+ IotDevice[]
+ >(1);
+
+ constructor(
+ private translate: TranslateService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private multicastService: MulticastService,
+ private errorMessageService: ErrorMessageService,
+ private scrollToTopService: ScrollToTopService,
+ private snackService: SnackService,
+ private applicationService: ApplicationService
+ ) {}
+
+ ngOnInit(): void {
+ this.translate
+ .get([
+ 'FORM.CREATE-NEW-MULTICAST',
+ 'FORM.EDIT-MULTICAST',
+ 'MULTICAST.SAVE',
+ 'NAV.MULTICAST',
+ 'GEN.BACK',
+ ])
+ .subscribe((translations) => {
+ this.multicastId = +this.route.snapshot.paramMap.get('multicastId');
+ this.applicationId = +this.route.snapshot.paramMap.get('id');
+
+ if (this.multicastId) {
+ this.title = translations['FORM.EDIT-MULTICAST'];
+ } else {
+ this.title = translations['FORM.CREATE-NEW-MULTICAST'];
+ }
+ this.submitButton = translations['MULTICAST.SAVE'];
+ this.backButtonTitle = translations['GEN.BACK'];
+ });
+
+ this.getApplication(this.applicationId);
+
+ if (this.multicastId) {
+ // If edit is pressed, then get the specific multicast.
+ this.getMulticast(this.multicastId);
+ }
+
+ this.deviceFilterCtrl.valueChanges
+ .pipe(takeUntil(this.onDestroy))
+ .subscribe(() => {
+ this.filterDevicesMulti();
+ });
+ }
+
+ private filterDevicesMulti() {
+ if (!this.iotDevices) {
+ return;
+ }
+ // get the search keyword
+ let search = this.deviceFilterCtrl?.value?.trim();
+ if (!search) {
+ this.filteredDevicesMulti.next(this.iotDevices.slice());
+ return;
+ } else {
+ search = search.toLowerCase();
+ }
+ const filtered = this.iotDevices.filter((device) => {
+ return device.name.toLocaleLowerCase().indexOf(search) > -1;
+ });
+ this.filteredDevicesMulti.next(filtered);
+ }
+
+ onSubmit(): void {
+ if (this.multicastId) {
+ // if already created, only update
+ this.updateMulticast();
+ } else {
+ // else create new
+ this.createMulticast();
+ }
+ }
+
+ getMulticast(id: number) {
+ this.multicastSubscription = this.multicastService
+ .get(id)
+ .subscribe((response: Multicast) => {
+ this.multicast = response; // gets the multicast and set's local multicast. Used when update.
+ });
+ }
+
+ getApplication(id: number) {
+ this.applicationService.getApplication(id).subscribe((application) => {
+ this.iotDevices = application.iotDevices;
+ this.filteredDevicesMulti.next(this.iotDevices.slice());
+ });
+ }
+
+ // only if classB can be used
+ // showPeriodicity(): boolean {
+ // if (this.multicast.groupType === MulticastType.ClassB) {
+ // return true;
+ // } else return false;
+ // }
+
+ updateMulticast(): void {
+ this.resetErrors();
+ this.multicast.applicationID = this.applicationId;
+
+ this.multicastService.update(this.multicast).subscribe(
+ () => {
+ this.snackService.showUpdatedSnack();
+ this.routeBack();
+ },
+ (error: HttpErrorResponse) => {
+ this.snackService.showFailSnack();
+ this.handleError(error);
+ this.formFailedSubmit = true;
+ }
+ );
+ }
+ createMulticast(): void {
+ this.resetErrors();
+ this.multicast.applicationID = this.applicationId;
+
+ this.multicastService.create(this.multicast).subscribe(
+ () => {
+ this.snackService.showSavedSnack();
+ this.routeBack();
+ },
+ (error: HttpErrorResponse) => {
+ this.snackService.showFailSnack();
+ this.handleError(error);
+ this.formFailedSubmit = true;
+ }
+ );
+ }
+ public compare(
+ o1: IotDevice | undefined,
+ o2: IotDevice | undefined
+ ): boolean {
+ return o1?.id === o2?.id;
+ }
+
+ selectAll() {
+ this.multicast.iotDevices = this.iotDevices;
+ }
+ unSelectAll() {
+ this.multicast.iotDevices = [];
+ }
+
+ routeBack(): void {
+ this.router.navigate(['applications', this.applicationId.toString()]);
+ }
+ keyPressHexadecimal(event) {
+ keyPressedHex(event);
+ }
+ private resetErrors() {
+ this.errorFields = [];
+ this.errorMessages = undefined;
+ this.formFailedSubmit = false;
+ }
+ handleError(error: HttpErrorResponse) {
+ const errors = this.errorMessageService.handleErrorMessageWithFields(error);
+ this.errorFields = errors.errorFields;
+ this.errorMessages = errors.errorMessages;
+ this.scrollToTopService.scrollToTop();
+ }
+ ngOnDestroy(): void {
+ this.multicastSubscription?.unsubscribe();
+ }
+}
diff --git a/src/app/applications/multicast/multicast-response.model.ts b/src/app/applications/multicast/multicast-response.model.ts
new file mode 100644
index 00000000..16d385f6
--- /dev/null
+++ b/src/app/applications/multicast/multicast-response.model.ts
@@ -0,0 +1,29 @@
+import { Application } from '@applications/application.model';
+import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { MulticastType } from '@shared/enums/multicast-type';
+
+export class MulticastResponse {
+ id: number;
+ application: Application;
+ iotDevices: IotDevice[];
+ groupName: string;
+ lorawanMulticastDefinition: LorawanMulticastDefinition;
+ // periodicity: number; -> only if classB is gonna be used
+ createdAt: string;
+ updatedAt: string;
+ createdBy: number;
+ updatedBy: number;
+ createdByName: string;
+ updatedByName: string;
+}
+
+export class LorawanMulticastDefinition {
+ address: string;
+ networkSessionKey: string;
+ applicationSessionKey: string;
+ frameCounter = 0;
+ dataRate = 0;
+ frequency = 0;
+ groupType: MulticastType;
+ chirpstackGroupId?: string;
+}
diff --git a/src/app/applications/multicast/multicast-table/multicast-table.component.html b/src/app/applications/multicast/multicast-table/multicast-table.component.html
new file mode 100644
index 00000000..2b35a5f4
--- /dev/null
+++ b/src/app/applications/multicast/multicast-table/multicast-table.component.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+ |
+ {{ 'MULTICAST-TABLE.NAME' | translate }}
+ |
+
+ {{element.groupName}}
+ |
+
+
+
+
+
+ {{ 'MULTICAST-TABLE.TYPE' | translate }} |
+
+
+ {{element.lorawanMulticastDefinition.groupType}} |
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/applications/multicast/multicast-table/multicast-table.component.scss b/src/app/applications/multicast/multicast-table/multicast-table.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/app/applications/multicast/multicast-table/multicast-table.component.ts b/src/app/applications/multicast/multicast-table/multicast-table.component.ts
new file mode 100644
index 00000000..54225d5c
--- /dev/null
+++ b/src/app/applications/multicast/multicast-table/multicast-table.component.ts
@@ -0,0 +1,133 @@
+import {
+ AfterViewInit,
+ Component,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewChild,
+} from '@angular/core';
+import { MatPaginator } from '@angular/material/paginator';
+import { MatSort } from '@angular/material/sort';
+import { ActivatedRoute } from '@angular/router';
+import { environment } from '@environments/environment';
+import { TranslateService } from '@ngx-translate/core';
+import { DeleteDialogService } from '@shared/components/delete-dialog/delete-dialog.service';
+import { MeService } from '@shared/services/me.service';
+import { SnackService } from '@shared/services/snack.service';
+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';
+
+@Component({
+ selector: 'app-multicast-table',
+ templateUrl: './multicast-table.component.html',
+ styleUrls: ['./multicast-table.component.scss'],
+})
+export class MulticastTableComponent
+ implements OnInit, AfterViewInit, OnDestroy {
+ @ViewChild(MatPaginator) paginator: MatPaginator;
+ @ViewChild(MatSort) sort: MatSort;
+ displayedColumns: string[] = ['groupName', 'groupType', 'menu'];
+ multicasts: Multicast[] = [];
+ resultsLength = 0;
+ public canEdit = false;
+ @Input() isLoadingResults = true;
+ public pageSize = environment.tablePageSize;
+ public pageOffset = 0;
+ public applicationId: number;
+
+ private multicastSubscription: Subscription;
+ private deleteDialogSubscription: Subscription;
+
+ constructor(
+ private route: ActivatedRoute,
+ private deleteDialogService: DeleteDialogService,
+ private multicastService: MulticastService,
+ private meService: MeService,
+ public translate: TranslateService,
+ public snackService: SnackService
+ ) {
+ translate.use('da');
+ }
+
+ ngOnInit(): void {
+ this.applicationId = +Number(this.route.parent.snapshot.paramMap.get('id'));
+ this.canEdit = this.meService.canWriteInTargetOrganization();
+ }
+
+ ngAfterViewInit() {
+ // If the user changes the sort order, reset back to the first page.
+ this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
+
+ merge(this.sort.sortChange, this.paginator.page)
+ .pipe(
+ startWith({}),
+ switchMap(() => {
+ this.isLoadingResults = true;
+ const multicasts = this.getMulticasts(
+ this.sort.active,
+ this.sort.direction
+ );
+ return multicasts;
+ }),
+ map((data) => {
+ // Flip flag to show that loading has finished.
+ if (data.ok === false) {
+ this.snackService.showLoadFailSnack();
+ }
+ this.isLoadingResults = false;
+ this.resultsLength = data.count;
+
+ return data.data;
+ }),
+ catchError(() => {
+ this.isLoadingResults = false;
+ return observableOf([]);
+ })
+ )
+ .subscribe((data) => (this.multicasts = data));
+ }
+
+ getMulticasts(
+ orderByColumn: string,
+ orderByDirection: string
+ ): Observable {
+ if (this.applicationId) {
+ return this.multicastService.getMulticastsByApplicationId(
+ this.paginator.pageSize,
+ this.paginator.pageIndex * this.paginator.pageSize,
+ orderByDirection,
+ orderByColumn,
+ this.applicationId
+ );
+ }
+ }
+
+ deleteMulticast(multicast: Multicast) {
+ this.deleteDialogSubscription = this.deleteDialogService
+ .showSimpleDialog()
+ .subscribe((response) => {
+ if (response) {
+ // if user presses "yes, delete", then delete the multicast.
+ this.multicastService.delete(multicast.id).subscribe((response) => {
+ if (response.ok && response.body.affected > 0) {
+ // if deleted succesfully, get the new array of multicasts and show a succesful snack.
+ this.paginator.page.emit({
+ pageIndex: this.paginator.pageIndex,
+ pageSize: this.paginator.pageSize,
+ length: this.resultsLength,
+ });
+ this.snackService.showDeletedSnack();
+ } else {
+ this.snackService.showFailSnack();
+ }
+ });
+ }
+ });
+ }
+ ngOnDestroy() {
+ this.multicastSubscription?.unsubscribe();
+ this.deleteDialogSubscription?.unsubscribe();
+ }
+}
diff --git a/src/app/applications/multicast/multicast.model.ts b/src/app/applications/multicast/multicast.model.ts
new file mode 100644
index 00000000..1dec442c
--- /dev/null
+++ b/src/app/applications/multicast/multicast.model.ts
@@ -0,0 +1,29 @@
+import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { MulticastType } from '@shared/enums/multicast-type';
+
+export class Multicast {
+ id: number;
+ applicationID: number;
+ iotDevices?: IotDevice[];
+ name: string;
+ mcAddr: string;
+ mcNwkSKey: string;
+ mcAppSKey: string;
+ fCnt: number = 0;
+ dr: number = 0;
+ frequency: number = 0;
+ groupType: MulticastType;
+ // periodicity: number; -> only if classB is gonna be used
+ createdAt: string;
+ updatedAt: string;
+ createdBy: number;
+ updatedBy: number;
+ createdByName: string;
+ updatedByName: string;
+}
+
+export class MulticastData {
+ data: Multicast[];
+ ok?: boolean;
+ count?: number;
+}
diff --git a/src/app/applications/multicast/multicast.module.ts b/src/app/applications/multicast/multicast.module.ts
new file mode 100644
index 00000000..e56d5271
--- /dev/null
+++ b/src/app/applications/multicast/multicast.module.ts
@@ -0,0 +1,39 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { SharedModule } from '@shared/shared.module';
+import { MulticastDetailComponent } from './multicast-detail/multicast-detail.component';
+import { MulticastEditComponent } from './multicast-edit/multicast-edit.component';
+import { MulticastTableComponent } from './multicast-table/multicast-table.component';
+import { RouterModule } from '@angular/router';
+import { TranslateModule } from '@ngx-translate/core';
+import { NGMaterialModule } from '@shared/Modules/materiale.module';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { PipesModule } from '@shared/pipes/pipes.module';
+import { MatSelectSearchModule } from '@shared/components/mat-select-search/mat-select-search.module';
+
+@NgModule({
+ declarations: [
+ MulticastDetailComponent,
+ MulticastEditComponent,
+ MulticastTableComponent,
+ ],
+ imports: [
+ CommonModule,
+ RouterModule,
+ TranslateModule,
+ NGMaterialModule,
+ FontAwesomeModule,
+ ReactiveFormsModule,
+ FormsModule,
+ SharedModule,
+ PipesModule,
+ MatSelectSearchModule,
+ ],
+ exports: [
+ MulticastDetailComponent,
+ MulticastEditComponent,
+ MulticastTableComponent,
+ ],
+})
+export class MulticastModule {}
diff --git a/src/app/applications/multicast/multicast.service.ts b/src/app/applications/multicast/multicast.service.ts
new file mode 100644
index 00000000..8dd061de
--- /dev/null
+++ b/src/app/applications/multicast/multicast.service.ts
@@ -0,0 +1,102 @@
+import { Injectable } from '@angular/core';
+import { UserMinimalService } from '@app/admin/users/user-minimal.service';
+import { Downlink } from '@applications/iot-devices/downlink.model';
+import { RestService } from '@shared/services/rest.service';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { MulticastResponse } from './multicast-response.model';
+import { Multicast, MulticastData } from './multicast.model';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class MulticastService {
+ constructor(
+ private restService: RestService,
+ private userMinimalService: UserMinimalService
+ ) {}
+
+ private multicastURL = 'multicast';
+ private multicastDownlinkURL = 'multicast/';
+ private DOWNLINKMULTICASTURL = 'downlink-multicast';
+
+ getMulticastsByApplicationId(
+ limit: number,
+ offset: number,
+ sort: string,
+ orderOn: string,
+ applicationId: number
+ ): Observable {
+ const body = {
+ limit,
+ offset,
+ sort,
+ orderOn,
+ applicationId,
+ };
+ return this.restService.get(this.multicastURL, body);
+ }
+
+ get(id: number): Observable {
+ return this.restService.get(this.multicastURL, {}, id).pipe(
+ // bind "this" correctly by creating a new lambda function
+ map((response: MulticastResponse) => {
+ const multicast = this.mapToMulticast(response);
+ return multicast;
+ })
+ );
+ }
+
+ delete(id: number) {
+ return this.restService.delete(this.multicastURL, id);
+ }
+ update(multicast: Multicast): Observable {
+ return this.restService.put(this.multicastURL, multicast, multicast.id);
+ }
+ create(multicast: Multicast): Observable {
+ return this.restService.post(this.multicastURL, multicast);
+ }
+
+ private mapToMulticast(multicastResponse: MulticastResponse): Multicast {
+ const model: Multicast = {
+ id: multicastResponse.id,
+ name: multicastResponse.groupName,
+ groupType: multicastResponse.lorawanMulticastDefinition.groupType,
+ mcAddr: multicastResponse.lorawanMulticastDefinition.address,
+ mcAppSKey:
+ multicastResponse.lorawanMulticastDefinition.applicationSessionKey,
+ dr: multicastResponse.lorawanMulticastDefinition.dataRate,
+ fCnt: multicastResponse.lorawanMulticastDefinition.frameCounter,
+ frequency: multicastResponse.lorawanMulticastDefinition.frequency,
+ mcNwkSKey: multicastResponse.lorawanMulticastDefinition.networkSessionKey,
+ applicationID: multicastResponse.application.id,
+ iotDevices: multicastResponse.iotDevices,
+ createdAt: multicastResponse.createdAt,
+ updatedAt: multicastResponse.updatedAt,
+ createdBy: multicastResponse.createdBy,
+ updatedBy: multicastResponse.updatedBy,
+ createdByName: this.userMinimalService.getUserNameFrom(
+ multicastResponse.createdBy
+ ),
+ updatedByName: this.userMinimalService.getUserNameFrom(
+ multicastResponse.updatedBy
+ ),
+ };
+ return model;
+ }
+
+ public multicastGet(multicastId: number, params = {}): Observable {
+ const url =
+ this.multicastDownlinkURL + multicastId + '/' + this.DOWNLINKMULTICASTURL;
+ return this.restService.get(url, params);
+ }
+ public multicastPost(
+ downlink: Downlink,
+ multicastId: number,
+ params = {}
+ ): Observable {
+ const url =
+ this.multicastDownlinkURL + multicastId + '/' + this.DOWNLINKMULTICASTURL;
+ return this.restService.post(url, downlink, params);
+ }
+}
diff --git a/src/app/dashboard/dashboard.component.ts b/src/app/dashboard/dashboard.component.ts
index 1c6916ae..a071e8ca 100644
--- a/src/app/dashboard/dashboard.component.ts
+++ b/src/app/dashboard/dashboard.component.ts
@@ -48,13 +48,13 @@ export class DashboardComponent implements OnInit {
} else {
const error = params['error'];
if (error) {
- if (error == "MESSAGE.KOMBIT-LOGIN-FAILED") {
+ if (error == "MESSAGE.KOMBIT-LOGIN-FAILED" || error == "MESSAGE.API-KEY-AUTH-FAILED") {
this.router.navigate(['/not-authorized'], { state: { message: this.kombitError, code: 401 } });
} if (error == "MESSAGE.USER-INACTIVE") {
this.router.navigate(['/not-authorized'], { state: { message: this.noAccess, code: 401 } });
} else {
this.router.navigate(['/not-authorized'], { state: { message: this.unauthorizedMessage, code: 401 } });
- }
+ }
}
}
await this.sharedVariableService.setUserInfo();
diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html
index 566b42df..f6a9a123 100644
--- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html
+++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.html
@@ -40,4 +40,11 @@
-
\ No newline at end of file
+
+
+ {{'NAV.API-KEY' | translate}}
+
+
+
+
diff --git a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts
index 3d73055b..97364003 100644
--- a/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts
+++ b/src/app/navbar/organisation-dropdown/organisation-dropdown.component.ts
@@ -3,7 +3,7 @@ import { Router } from '@angular/router';
import { Organisation } from '@app/admin/organisation/organisation.model';
import { PermissionType } from '@app/admin/permission/permission.model';
import { UserResponse } from '@app/admin/users/user.model';
-import { faExchangeAlt, faLayerGroup, faUsers, faIdBadge, faToolbox, faBurn } from '@fortawesome/free-solid-svg-icons';
+import { faExchangeAlt, faLayerGroup, faUsers, faIdBadge, faToolbox, faBurn, faKey } from '@fortawesome/free-solid-svg-icons';
import { TranslateService } from '@ngx-translate/core';
import { SharedVariableService } from '@shared/shared-variable/shared-variable.service';
@@ -24,6 +24,7 @@ export class OrganisationDropdownComponent implements OnInit {
faIdBadge = faIdBadge;
faToolbox = faToolbox;
faBurn = faBurn;
+ faKey = faKey;
constructor(
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 cf7462e0..e4bb105b 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
@@ -18,7 +18,7 @@ import { DeviceModelService } from '@app/device-model/device-model.service';
import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons';
import { TestPayloadDecoder } from '@payload-decoder/test-payload-decoder.model';
import { TestPayloadDecoderService } from '@payload-decoder/test-payload-decoder.service';
-import { SaveSnackService } from '@shared/services/save-snack.service';
+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';
@@ -75,7 +75,7 @@ export class PayloadDecoderEditComponent implements OnInit {
private sharedVariableService: SharedVariableService,
private iotDeviceService: IoTDeviceService,
private deviceModelService: DeviceModelService,
- private saveSnackService: SaveSnackService,
+ private saveSnackService: SnackService,
private errorMessageService: ErrorMessageService,
private scrollToTopService: ScrollToTopService,
) { }
diff --git a/src/app/shared/components/delete-dialog/delete-dialog.service.ts b/src/app/shared/components/delete-dialog/delete-dialog.service.ts
index 6bb51bcc..90a6257a 100644
--- a/src/app/shared/components/delete-dialog/delete-dialog.service.ts
+++ b/src/app/shared/components/delete-dialog/delete-dialog.service.ts
@@ -1,33 +1,72 @@
import { Injectable } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DeleteDialogComponent } from './delete-dialog.component';
-import { Observable } from 'rxjs';
+import { Observable, Subscription } from 'rxjs';
+import { Application } from '@applications/application.model';
+import { DeviceType } from '@shared/enums/device-type';
+import { TranslateService } from '@ngx-translate/core';
@Injectable({
providedIn: 'root',
})
export class DeleteDialogService {
-
+ private deleteDialogSubscription: Subscription;
constructor(
- private dialog: MatDialog
- ) { }
+ private dialog: MatDialog,
+ public translate: TranslateService,
+ ) {}
+
+ showSimpleDialog(
+ message?: string,
+ showAccept = true,
+ showCancel = true,
+ showOk = false,
+ infoTitle = ''
+ ): Observable {
+ return new Observable((observer) => {
+ const dialog = this.dialog.open(DeleteDialogComponent, {
+ data: {
+ infoTitle,
+ showOk,
+ showAccept,
+ showCancel,
+ message: message ? message : 'Er du sikker på at du vil slette?',
+ },
+ });
+
+ dialog.afterClosed().subscribe((result) => {
+ observer.next(result);
+ });
+ });
+ }
- showSimpleDialog(message?: string, showAccept = true, showCancel = true, showOk = false, infoTitle = ''): Observable {
- return new Observable(
- (observer) => {
- const dialog = this.dialog.open(DeleteDialogComponent, {
- data: {
- infoTitle,
- showOk,
- showAccept,
- showCancel,
- message: message ? message : 'Er du sikker på at du vil slette?'
- }
- });
+ showApplicationDialog(application: Application): Observable {
+ let message: string;
+ let showAccept: boolean = true;
+ const hasSigfoxDevices: boolean = this.applicationHasSigFoxDevices(
+ application
+ );
- dialog.afterClosed().subscribe((result) => {
- observer.next(result);
- });
+ if (hasSigfoxDevices) {
+ message = this.translate.instant(
+ 'APPLICATION.DELETE-HAS-SIGFOX-DEVICES-PROMPT'
+ );
+ showAccept = false;
+ } else if (this.applicationHasDevices(application)) {
+ message = this.translate.instant(
+ 'APPLICATION.DELETE-HAS-DEVICES-PROMPT'
+ );
}
- );
+ return this.showSimpleDialog(message, showAccept);
+ }
+
+ applicationHasDevices(application: Application): boolean {
+ return application.iotDevices?.length > 0;
+ }
+
+ applicationHasSigFoxDevices(application: Application): boolean {
+ const sigfoxDevice = application.iotDevices.find((device) => {
+ return device.type === DeviceType.SIGFOX;
+ });
+ return sigfoxDevice !== undefined;
}
}
diff --git a/src/app/shared/components/forms/form-body-application/form-body-application.component.ts b/src/app/shared/components/forms/form-body-application/form-body-application.component.ts
index f1eb19a5..556e82d4 100644
--- a/src/app/shared/components/forms/form-body-application/form-body-application.component.ts
+++ b/src/app/shared/components/forms/form-body-application/form-body-application.component.ts
@@ -89,8 +89,7 @@ export class FormBodyApplicationComponent implements OnInit, OnDestroy {
this.applicationService
.createApplication(this.application)
.subscribe(
- (response) => {
- console.log(response);
+ () => {
this.router.navigateByUrl('/applications');
},
(error: HttpErrorResponse) => {
diff --git a/src/app/shared/components/snack-bar/snack-bar.component.html b/src/app/shared/components/snack-bar/snack-bar.component.html
new file mode 100644
index 00000000..aa504cd5
--- /dev/null
+++ b/src/app/shared/components/snack-bar/snack-bar.component.html
@@ -0,0 +1,9 @@
+
+ {{data.title}}
+
+
+ {{data.message}}
+
+
+
+
\ No newline at end of file
diff --git a/src/app/shared/components/snack-bar/snack-bar.component.scss b/src/app/shared/components/snack-bar/snack-bar.component.scss
new file mode 100644
index 00000000..29916f9b
--- /dev/null
+++ b/src/app/shared/components/snack-bar/snack-bar.component.scss
@@ -0,0 +1,21 @@
+.snackbar-title-style {
+ font-weight: 500;
+ letter-spacing: 0.5px;
+}
+.snackbar-content-style {
+ letter-spacing: 0.5px;
+ border: none;
+ padding: 0 10px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+.snackbar-button-div-style {
+ text-align: center;
+
+ .button-style {
+ letter-spacing: 0.5px;
+ border: 1px solid #000;
+ border-radius: 5px;
+ padding: 5px 15px;
+ }
+}
diff --git a/src/app/shared/components/snack-bar/snack-bar.component.ts b/src/app/shared/components/snack-bar/snack-bar.component.ts
new file mode 100644
index 00000000..2bf27e0b
--- /dev/null
+++ b/src/app/shared/components/snack-bar/snack-bar.component.ts
@@ -0,0 +1,18 @@
+import { Component, OnInit, Inject } from "@angular/core";
+import { MAT_SNACK_BAR_DATA, MatSnackBarRef } from "@angular/material/snack-bar";
+
+
+@Component({
+ selector: 'app-snackbar',
+ templateUrl: './snack-bar.component.html',
+ styleUrls: ['./snack-bar.component.scss']
+})
+export class SnackBarComponent implements OnInit {
+
+ constructor(
+ @Inject(MAT_SNACK_BAR_DATA) public data: any,
+ public snackBarRef: MatSnackBarRef) {}
+
+ ngOnInit(): void {
+ }
+}
diff --git a/src/app/shared/constants/regex-constants.ts b/src/app/shared/constants/regex-constants.ts
new file mode 100644
index 00000000..ef7a76a5
--- /dev/null
+++ b/src/app/shared/constants/regex-constants.ts
@@ -0,0 +1,10 @@
+export function keyPressedHex(event) {
+ var inp = String.fromCharCode(event.keyCode);
+
+ if (/[a-fA-F0-9]/.test(inp)) {
+ return true;
+ } else {
+ event.preventDefault();
+ return false;
+ }
+}
diff --git a/src/app/shared/enums/datatarget-type.ts b/src/app/shared/enums/datatarget-type.ts
index 5dea3827..795c317d 100644
--- a/src/app/shared/enums/datatarget-type.ts
+++ b/src/app/shared/enums/datatarget-type.ts
@@ -1,4 +1,5 @@
export enum DataTargetType {
HTTPPUSH = 'HTTP_PUSH',
- OPENDATADK = 'OPENDATADK'
+ OPENDATADK = 'OPENDATADK',
+ FIWARE = 'FIWARE'
}
diff --git a/src/app/shared/enums/multicast-type.ts b/src/app/shared/enums/multicast-type.ts
new file mode 100644
index 00000000..acb0283d
--- /dev/null
+++ b/src/app/shared/enums/multicast-type.ts
@@ -0,0 +1,4 @@
+export enum MulticastType {
+ // Class-B: {ClassB = 'Class-B'},
+ ClassC = 'CLASS_C',
+}
diff --git a/src/app/shared/error-message.service.ts b/src/app/shared/error-message.service.ts
index 7446ab11..0e76e396 100644
--- a/src/app/shared/error-message.service.ts
+++ b/src/app/shared/error-message.service.ts
@@ -13,8 +13,7 @@ export class ErrorMessageService {
errorMessages.push(err.error.message);
} else if (err.error.chirpstackError) {
errorMessages.push(err.error.chirpstackError.message);
- }
- else {
+ } else {
err.error.message.forEach( (err) => {
if (err.property === 'lorawanSettings') {
err.children.forEach( (element) => {
@@ -32,9 +31,13 @@ export class ErrorMessageService {
return errorMessages;
}
- public handleErrorMessageWithFields(error: HttpErrorResponse): ErrorMessage {
+ public handleErrorMessageWithFields(error: HttpErrorResponse | Pick): ErrorMessage {
const errors: ErrorMessage = {errorFields: [], errorMessages: []};
- if (typeof error.error.message === 'string') {
+ if (typeof error.error === 'string') {
+ errors.errorMessages.push(error.error);
+ } else if (typeof error.error?.error === 'string') {
+ errors.errorMessages.push(error.error.error);
+ } else if (typeof error.error?.message === 'string') {
errors.errorMessages.push(error.error.message);
} else {
error.error.message.forEach((err) => {
@@ -65,7 +68,7 @@ export class ErrorMessageService {
} else if (err.message) {
errors.errorFields.push(err.field);
errors.errorMessages.push(err.message);
- } else {
+ } else if (err.constraints) {
errors.errorFields.push(err.property);
errors.errorMessages = errors.errorMessages.concat(
Object.values(err.constraints)
diff --git a/src/app/shared/helpers/array.helper.ts b/src/app/shared/helpers/array.helper.ts
new file mode 100644
index 00000000..779ffa89
--- /dev/null
+++ b/src/app/shared/helpers/array.helper.ts
@@ -0,0 +1,11 @@
+export const splitList = (
+ data: T[],
+ batchSize = 50
+): typeof data[] => {
+ const dataBatches: typeof data[] = [];
+ for (let i = 0; i < data.length; i += batchSize) {
+ dataBatches.push(data.slice(i, i + batchSize));
+ }
+
+ return dataBatches;
+};
diff --git a/src/app/shared/pipes/filter-devices.pipe.ts b/src/app/shared/pipes/filter-devices.pipe.ts
new file mode 100644
index 00000000..47edb0bf
--- /dev/null
+++ b/src/app/shared/pipes/filter-devices.pipe.ts
@@ -0,0 +1,26 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { IotDevice } from '@applications/iot-devices/iot-device.model';
+import { DeviceType } from '@shared/enums/device-type';
+
+@Pipe({
+ name: 'filterDevices',
+})
+export class FilterDevicesPipe implements PipeTransform {
+ transform(value: IotDevice[] | undefined, ..._: unknown[]): IotDevice[] {
+ // Filter devices so only LoRaWAN devices will be shown.
+ const lorawanDevices: IotDevice[] = [];
+
+ if (!value) {
+ return lorawanDevices;
+ }
+ value.forEach((device) => {
+ if (device.type === DeviceType.LORAWAN) {
+ lorawanDevices.push(device);
+ }
+ });
+
+ lorawanDevices.sort((a, b) => a.name.localeCompare(b.name));
+
+ return lorawanDevices;
+ }
+}
diff --git a/src/app/shared/pipes/pipes.module.ts b/src/app/shared/pipes/pipes.module.ts
index a3e26445..c5aacc28 100644
--- a/src/app/shared/pipes/pipes.module.ts
+++ b/src/app/shared/pipes/pipes.module.ts
@@ -5,26 +5,27 @@ import { ActiveDeactivePipe } from './activeDeactive.pipe';
import { isGlobalAdminPipe } from './is-global-admin.pipe';
import { CreatedUpdatedByPipe } from './created-updated-by.pipe';
import { CustomDatePipe, CustomTableDatePipe } from './custom-date.pipe';
+import { FilterDevicesPipe } from './filter-devices.pipe';
@NgModule({
- declarations: [
- isGlobalAdminPipe,
- ActiveDeactivePipe,
- YesNoPipe,
- CustomDatePipe,
- CustomTableDatePipe,
- CreatedUpdatedByPipe,
- ],
- imports: [
- CommonModule
- ],
- exports: [
- isGlobalAdminPipe,
- ActiveDeactivePipe,
- YesNoPipe,
- CustomDatePipe,
- CustomTableDatePipe,
- CreatedUpdatedByPipe,
- ]
+ declarations: [
+ isGlobalAdminPipe,
+ ActiveDeactivePipe,
+ YesNoPipe,
+ CustomDatePipe,
+ CustomTableDatePipe,
+ CreatedUpdatedByPipe,
+ FilterDevicesPipe,
+ ],
+ imports: [CommonModule],
+ exports: [
+ isGlobalAdminPipe,
+ ActiveDeactivePipe,
+ YesNoPipe,
+ CustomDatePipe,
+ CustomTableDatePipe,
+ CreatedUpdatedByPipe,
+ FilterDevicesPipe,
+ ],
})
-export class PipesModule { }
+export class PipesModule {}
diff --git a/src/app/shared/services/bulk-import.service.ts b/src/app/shared/services/bulk-import.service.ts
new file mode 100644
index 00000000..b093be00
--- /dev/null
+++ b/src/app/shared/services/bulk-import.service.ts
@@ -0,0 +1,12 @@
+import { Injectable } from '@angular/core';
+import { Subject } from 'rxjs';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class BulkImportService {
+ public readonly nextCreateIotDeviceBatchIndex$: Subject = new Subject();
+ public readonly nextUpdateDeviceBatchIndex$: Subject = new Subject();
+
+ constructor() {}
+}
diff --git a/src/app/shared/services/downlink.service.ts b/src/app/shared/services/downlink.service.ts
index 0715afbe..edbf8cfe 100644
--- a/src/app/shared/services/downlink.service.ts
+++ b/src/app/shared/services/downlink.service.ts
@@ -2,24 +2,42 @@ import { Injectable } from '@angular/core';
import { RestService } from './rest.service';
import { Observable } from 'rxjs';
import { Downlink } from '@applications/iot-devices/downlink.model';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { TranslateService } from '@ngx-translate/core';
@Injectable({
- providedIn: 'root'
+ providedIn: 'root',
})
export class DownlinkService {
-
private IOTDEVICEURL = 'iot-device/';
private DOWNLINKURL = 'downlink';
- constructor(private restService: RestService) { }
+ constructor(
+ private restService: RestService,
+ private snackBar: MatSnackBar,
+ public translate: TranslateService
+ ) {}
public get(deviceId: number, params = {}): Observable {
const url = this.IOTDEVICEURL + deviceId + '/' + this.DOWNLINKURL;
return this.restService.get(url, params);
}
- public post(downlink: Downlink, deviceId: number, params = {}): Observable {
+ public post(
+ downlink: Downlink,
+ deviceId: number,
+ params = {}
+ ): Observable {
const url = this.IOTDEVICEURL + deviceId + '/' + this.DOWNLINKURL;
return this.restService.post(url, downlink, params);
}
+ public showSendDownlinkFailNoDevices() {
+ this.snackBar.open(
+ this.translate.instant('SNACK.NODEVICES'),
+ this.translate.instant('SNACK.CLOSE'),
+ {
+ duration: 10000,
+ }
+ );
+ }
}
diff --git a/src/app/shared/services/save-snack.service.ts b/src/app/shared/services/save-snack.service.ts
deleted file mode 100644
index fcee59d7..00000000
--- a/src/app/shared/services/save-snack.service.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Injectable } from '@angular/core';
-import { MatSnackBar } from '@angular/material/snack-bar';
-
-
-@Injectable({
- providedIn: 'root',
-})
-export class SaveSnackService {
- constructor(
- private snackBar: MatSnackBar) { }
-
- public showSavedSnack() {
- this.snackBar.open('Gemt Succesfuldt', 'Luk', {
- duration: 10000,
- });
- }
-}
diff --git a/src/app/shared/services/snack.service.ts b/src/app/shared/services/snack.service.ts
new file mode 100644
index 00000000..f759968f
--- /dev/null
+++ b/src/app/shared/services/snack.service.ts
@@ -0,0 +1,60 @@
+import { Injectable } from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { TranslateService } from '@ngx-translate/core';
+import { SnackBarComponent } from '@shared/components/snack-bar/snack-bar.component';
+
+
+@Injectable({
+ providedIn: 'root',
+})
+export class SnackService {
+ constructor(
+ private snackBar: MatSnackBar,
+ public translate: TranslateService
+ ) {}
+
+ public showSavedSnack() {
+ this.snackBar.open(this.translate.instant('SNACK.SAVE'), this.translate.instant('SNACK.CLOSE'), {
+ duration: 10000,
+ });
+ }
+ public showDeletedSnack() {
+ this.snackBar.open(this.translate.instant('SNACK.DELETE'), this.translate.instant('SNACK.CLOSE'), {
+ duration: 10000,
+ });
+ }
+ public showUpdatedSnack() {
+ this.snackBar.open(this.translate.instant('SNACK.UPDATE'), this.translate.instant('SNACK.CLOSE'), {
+ duration: 10000,
+ });
+ }
+ public showFailSnack() {
+ this.snackBar.open(this.translate.instant('SNACK.FAIL'), this.translate.instant('SNACK.CLOSE'), {
+ duration: 10000,
+ });
+ }
+ public showLoadFailSnack() {
+ this.snackBar.open(this.translate.instant('SNACK.LOADFAIL'), this.translate.instant('SNACK.CLOSE'), {
+ duration: 10000,
+ });
+ }
+ public showInQueueSnack() {
+ this.snackBar.open(this.translate.instant('SNACK.QUEUE'), this.translate.instant('SNACK.CLOSE'), {
+ duration: 10000,
+ });
+ }
+
+ public showSnackBar(title: string, displayMessage: string, buttonText: string) {
+ this.snackBar.openFromComponent(SnackBarComponent, {
+ data: {
+ title: title,
+ message: displayMessage,
+ buttonText: buttonText
+ },
+ duration: 5000,
+ horizontalPosition: 'right',
+ verticalPosition: 'top',
+ panelClass: 'snackBar'
+ });
+ }
+}
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 b6d2e1e4..11f9d15a 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
@@ -109,7 +109,7 @@ export class SigfoxGroupsEditComponent implements OnInit, OnDestroy {
return;
}
- this.sigfoxService.getGroup(this.sigfoxGroup.id).subscribe(
+ this.sigfoxService.getGroup(this.sigfoxGroupId).subscribe(
(response: any) => {
if (response.data.length !== 0 || response.data === undefined) {
this.update();
diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json
index 96c8b754..ff0293ac 100644
--- a/src/assets/i18n/da.json
+++ b/src/assets/i18n/da.json
@@ -17,6 +17,7 @@
"ALL-IOT-DEVICES": "Alle IoT enheder",
"LORA-GATEWAYS": "LoRaWAN gateways",
"DATATARGET": "Datatarget",
+ "MULTICAST": "Multicast",
"MY-DATATARGET": "Tilbage",
"DATATARGET-APPLIKATION": "for applikation:",
"PROFILES": "LoRaWAN profiler",
@@ -34,7 +35,8 @@
"DEVICE-MODEL": "Device model",
"BACK": "Tilbage",
"LOGOUT": "Log ud",
- "HELP": "Hjælp"
+ "HELP": "Hjælp",
+ "API-KEY": "API nøgler"
},
"TOPBAR":{
"SEARCH": {
@@ -45,6 +47,16 @@
"DELETE": "Slet"
}
},
+ "SNACK": {
+ "SAVE": "Gemt succesfuldt",
+ "DELETE": "Slettet succesfuldt",
+ "UPDATE": "Opdateret succesfuldt",
+ "FAIL": "Fejl - aktion ikke fuldført",
+ "LOADFAIL": "Fejl - kunne ikke loade",
+ "QUEUE": "Element sat i kø",
+ "CLOSE": "Luk",
+ "NODEVICES": "Kan ikke sende downlink - der er ingen devices!"
+ },
"SEARCH": {
"ICON": "",
"TYPE": "Type",
@@ -81,11 +93,15 @@
"SAVE": "Gem applikation",
"DELETE": "Slet applikation",
"DELETE-HAS-DEVICES-PROMPT": "Der er knyttet IoT-enheder til denne applikation. Disse vil også blive slettet. Slet alligevel?",
+ "DELETE-HAS-SIGFOX-DEVICES-PROMPT": "Applikationen kan ikke slettes, da der er knyttet Sigfox enheder til den",
"NAME": "Applikationens navn",
"DESCRIPTION": "Applikationens beskrivelse",
"ATTACHED-IOT": "Tilknyttede IoT enheder",
"DATATARGET-SHOW": "Tilknyttede data targets",
+ "MULTICAST-SHOW": "Tilknyttede multicast",
+ "MULTICAST-GROUPS": "Multicast-grupper",
"IMPORT-CSV": "Bulk import af IoT enheder",
+ "IOT-DEVICES": "IoT-enheder",
"BULK": {
"TEMPLATE": {
"GENERIC": "Generic HTTP sample",
@@ -135,6 +151,8 @@
"DATATARGET": {
"DETAILS": "Detaljer",
"SAVE": "Gem datatarget",
+ "CREATE": "Opret",
+ "READMORE": "Læs mere",
"DESCRIPTION": "Beskrivelse",
"URL": "URL",
"TIMEOUT": "Timeout",
@@ -147,11 +165,37 @@
"NO-PAYLOADDECODER": "Ingen payload decoder",
"IOTDEVICE": "IoT enhed(er)",
"NO-RELATIONS": "ingen tilknyttede relationer",
+ "TENANT": "Tenant",
+ "NO-TENANT": "Ingen Tenant angivet",
+ "CONTEXT": "Context",
+ "NO-CONTEXT": "Ingen Context angivet",
"AUTHORIZATIONHEADER": "Authorization header",
"NO-AUTHORIZATIONHEADER": "Ingen Authorization header angivet",
"ADD-TO-OPENDATADK": "Send data til OpenDataDK",
"OPENDATA-DK": "OpenDataDK",
- "NO-OPENDATA-DK": "Der er ikke oprettet nogen datadeling med Open Data DK endnu"
+ "NO-OPENDATA-DK": "Der er ikke oprettet nogen datadeling med Open Data DK endnu",
+ "HTTP_PUSH": "HTTP Push",
+ "FIWARE": "FIWARE"
+ },
+ "MULTICAST": {
+ "SAVE": "Gem multicast",
+ "BASIC-DETAILS": "Basale detaljer",
+ "LORAWAN-DETAILS": "LoRaWAN detailjer",
+ "GROUPNAME": "Gruppe navn",
+ "ADDRESS": "Adresse",
+ "NETWORK-KEY": "Network session key",
+ "APPLICATION-KEY": "Network application key",
+ "FRAMECOUNTER": "Frame counter",
+ "DATARATE": "Data rate",
+ "FREQUENCY": "Frekvens (Hz)",
+ "GROUPTYPE": "Gruppe type",
+ "PERIODICITY": "Periodicitet",
+ "IOTDEVICE": "IoT enheder",
+ "DOWNLINK": {
+ "PORT": "Angiv den ønskede port",
+ "PAYLOAD": "Angiv det ønskede payload",
+ "START": "Sæt downlink i kø"
+ }
},
"OPENDATADK": {
"QUESTION": {
@@ -240,6 +284,10 @@
"NAME": "Navn",
"TYPE": "Type"
},
+ "MULTICAST-TABLE":{
+ "NAME": "Gruppenavn",
+ "TYPE": "Gruppetype"
+ },
"IOT-DEVICE-TYPES": {
"GENERIC_HTTP": "Generisk HTTP",
"LORAWAN": "LoRaWAN",
@@ -264,7 +312,13 @@
},
"DATATARGET-TABLE-ROW": {
"DELETE": "Slet datatarget",
- "EDIT": "Redigér"
+ "EDIT": "Redigér",
+ "SHOW-OPTIONS": "Håndter datatarget"
+ },
+ "MULTICAST-TABLE-ROW": {
+ "DELETE": "Slet multicast",
+ "EDIT": "Redigér",
+ "SHOW-OPTIONS": "Håndter multicast"
},
"PAYLOAD-DECODER": {
"DELETE-FAILED": "Slet fejlede",
@@ -364,7 +418,9 @@
"CREATE-NEW-LORA-GATEWAY": "Opret ny LoRaWAN gateway",
"CREATE-NEW-DEVICE-MODEL": "Opret ny device model",
"CREATE-NEW-DATATARGET": "Opret nyt datatarget",
+ "CREATE-NEW-MULTICAST": "Opret nyt multicast",
"EDIT-DATATARGET": "Redigér datatarget",
+ "EDIT-MULTICAST": "Redigér multicast",
"CREATE-NEW-IOT-DEVICE": "Tilføj en IoT enhed",
"EDIT-NEW-GATEWAY": "Redigér LoRaWAN gateway",
"EDIT-NEW-APPLICATION": "Redigér applikation",
@@ -382,7 +438,8 @@
"EDIT-SIGFOX-GROUPS": "Redigér Sigfox grupper",
"EDIT-SIGFOX-GROUP": "Redigér Sigfox gruppe",
"EDIT-USERS": "Redigér bruger",
- "EDIT-DEVICE-PROFILE": "Redigér device profil"
+ "EDIT-DEVICE-PROFILE": "Redigér device profil",
+ "EDIT-API-KEY": "Redigér API nøgle"
},
"QUESTION": {
"CREATE-IOT-DEVICE": "IoT-enhed",
@@ -426,6 +483,23 @@
"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-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)",
+ "GIVE-MULTICAST-FRAMECOUNTER": "Angiv frame counter",
+ "GIVE-MULTICAST-DATARATE": "Angiv data rate",
+ "GIVE-MULTICAST-IOTDEVICES": "Angiv IoT-enheder",
+ "GIVE-MULTICAST-FREQUENCY": "Angiv frekvens (Hz)",
+ "GIVE-MULTICAST-GROUP-TYPE": "Angiv multicast gruppe type",
+ "GIVE-MULTICAST-PERIODICITY": "Angiv multicast ping-slot periodicitet",
+ "GIVE-MULTICAST-GROUPTYPE": "Angiv multicast gruppe typen",
+ "MULTICAST-NAME-PLACEHOLDER": "Multicastens navn",
+ "MULTICAST-ADDRESS-PLACEHOLDER": "Multicast adressen",
+ "MULTICAST-NETWORK-KEY-PLACEHOLDER": "Multicastens network session key",
+ "MULTICAST-APPLICATION-KEY-PLACEHOLDER": "Multicastens application session key",
+ "MULTICAST-GROUPTYPE-PLACEHOLDER": "Vælg multicast gruppe typen",
+ "MULTICAST-PERIODICITY-PLACEHOLDER": "Vælg Class-B ping periodiciteten",
"OTAAAPPLICATIONKEY": "OTAA application key (AppKey)",
"OTAAAPPLICATIONKEY-PLACEHOLDER": "Indtast OTAA application key",
"DEVADDR": "Device adress",
@@ -458,6 +532,17 @@
"DESELECTALLDEVICES": "Fravælg alle",
"RELATIONS": "*Efter oprettelse af dit data target parres dette med en/flere payload decoder(s) og IoT-devices"
},
+ "MULTICAST": {
+ "SELECT-DEVICES": "Vælg enheder",
+ "SELECTALLDEVICES": "Vælg alle",
+ "DESELECTALLDEVICES": "Fravælg alle",
+ "NO-PORT-OR-PAYLOAD": "Angiv en port og en payload",
+ "ONLY-LORAWAN": "* På nuværende tidspunkt er det kun muligt at tilknytte LoRaWAN devices.",
+ "HINT-8-HEXA": "8-tegns hexadecimal værdi",
+ "HINT-32-HEXA": "32-tegns hexadecimal værdi",
+ "HINT-DATA-RATE": "Data rate, der skal bruges ved overførsel af multicast-frames. Se venligst LoRaWAN Regional Parameters specifikationen for gyldige værdier",
+ "HINT-FREQUENCY": "Frekvensen, der skal bruges ved transmission af multicast-frames. Se venligst LoRaWAN Regional Parameters specifikationen for gyldige værdier"
+ },
"SIGFOX": {
"TITLE": "Sigfox specifikke felter",
"CONNECTTOEXISTINGDEVICEINBACKEND": "Er enheden allerede registreret i Sigfox backend?",
@@ -506,6 +591,9 @@
"SELECT-ENERGYLIMITATIONCLASS": "Vælg energibegrænsningsklassen (energyLimitationClass)",
"SELECT-SUPPORTEDPROTOCOL": "Vælg understøttede protokoller (supportedProtocol)",
"FIWARE-LINK-TEXT": "Denne data model er adopteret fra Fiware og følger ETSI standarden"
+ },
+ "PERMISSION": {
+ "SELECT-PERMISSION": "Vælg brugergruppe"
}
},
"QUESTION-LORA-GATEWAY": {
@@ -630,7 +718,14 @@
"DELETE-NOT-ALLOWED-HAS-SIGFOX-DEVICE": "Applikation kan ikke blive slettet, da den indeholder en eller flere Sigfox enheder.",
"DELETE-NOT-ALLOWED-HAS-LORAWAN-DEVICE": "Service profilen kan ikke blive slettet, da den er i brug af en eller flere LoRaWAN enheder.",
"OTAA-INFO-MISSING": "OTAA nøgle mangler eller er ikke gyldig.",
- "ABP-INFO-MISSING": "ABP nøgle mangler eller er ikke gyldig."
+ "ABP-INFO-MISSING": "ABP nøgle mangler eller er ikke gyldig.",
+ "DIFFERENT-SERVICE-PROFILE": "Dine devices har forskellige service profiles. De skal have den samme service profile!",
+ "WRONG-SERVICE-PROFILE": "Dine devices har forkert service profile. Vælg devices som har samme service profile som din multicast.",
+ "ID-DOES-NOT-EXIST": "Id'et findes ikke",
+ "APPLICATION-DOES-NOT-EXIST": "Den tilhørende applikation findes ikke",
+ "FAILED-TO-CREATE-OR-UPDATE-IOT-DEVICE": "Enheden kunne ikke oprettes eller opdateres",
+ "DEVICE-MODEL-ORGANIZATION-DOES-NOT-MATCH": "Organisationsid'et på device modellen matcher ikke den tilhørende applikation",
+ "DEVICE-MODEL-DOES-NOT-EXIST": "Device model findes ikke"
},
"PROFILES": {
"NAME": "LoRaWAN profiler",
@@ -697,7 +792,7 @@
"PINGSLOTFREQ": "Class-B ping-slot frequency",
"SUPPORTSCLASSC_ACTIVATE": "Device supports Class-C",
"CLASSCTIMEOUT": "Class C confirmed downlink timeout",
- "CANCEL": "Anuller",
+ "CANCEL": "Annuller",
"SAVE": "Gem",
"OTAA-ABP": "Join (OTAA / ABP)",
"MACVERSION_PLACEHOLDER": "1.0.0",
@@ -821,9 +916,11 @@
"PERMISSION": "OS2IoT - Brugergrupper",
"ORGANIZATION": "OS2IoT - Organisationer",
"DATATARGET": "OS2IoT - Datatarget",
+ "MULTICAST": "OS2IoT - Multicast",
"BULKIMPORT": "OS2IoT - Bulk import",
"IOTDEVICE": "OS2IoT - IoT enhed",
- "FRONTPAGE": "OS2IoT - Forside"
+ "FRONTPAGE": "OS2IoT - Forside",
+ "API-KEY": "OS2IoT - API nøgler"
},
"PAGINATOR": {
@@ -841,5 +938,39 @@
"Forbidden resource": "Du har ikke rettigheder til at foretage denne handling",
"GENERIC_HTTP": "Generisk HTTP",
"LORAWAN": "LoRaWAN",
- "SIGFOX": "Sigfox"
+ "SIGFOX": "Sigfox",
+ "API-KEY": {
+ "NAME": "Navn",
+ "ORGANIZATION": "Organisation",
+ "PERMISSIONS": "Brugergrupper",
+ "KEY": "Nøgle",
+ "CREATE-NEW-API-KEY": "Opret ny nøgle",
+ "DETAIL": {},
+ "EDIT": {
+ "NAME": "Indtast nøglens navn",
+ "NAME-PLACEHOLDER": "Indtast nøglens navn",
+ "CANCEL": "Annuller",
+ "SAVE": "Gem nøgle",
+ "CREATE-API-KEY": "Opret nøgle"
+ },
+ "TABLE-ROW": {
+ "EDIT": "Redigér",
+ "DELETE": "Slet"
+ }
+ },
+ "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"
+ }
+
+ }
}
diff --git a/src/assets/images/logo_FIWARE.png b/src/assets/images/logo_FIWARE.png
new file mode 100644
index 00000000..beb19973
Binary files /dev/null and b/src/assets/images/logo_FIWARE.png differ
diff --git a/src/assets/images/logo_opendatadk.svg b/src/assets/images/logo_opendatadk.svg
new file mode 100644
index 00000000..37964dfc
--- /dev/null
+++ b/src/assets/images/logo_opendatadk.svg
@@ -0,0 +1 @@
+
diff --git a/src/styles.scss b/src/styles.scss
index 91b964e8..4455b654 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -53,3 +53,38 @@
font-family: 'roboto-bold';
src: url('./assets/fonts/Roboto-Bold.ttf') format('truetype');
}
+
+body {
+ // For Google Chrome
+ &::-webkit-scrollbar {
+ width: 5px;
+ height: 5px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: rgba($color: #000000, $alpha: 0.5);
+ }
+
+ &::-webkit-scrollbar-track {
+ background: rgba($color: #000000, $alpha: 0.4);
+ }
+
+ // For FireFox
+ scrollbar-width: 5px;
+
+ // For Internet Explorer
+ & {
+ scrollbar-face-color: rgba($color: #000000, $alpha: 0.5);
+ scrollbar-track-color: rgba($color: #000000, $alpha: 0.4);
+ }
+}
+
+.snackBar.mat-snack-bar-container{
+ background: #f4f4f4;
+ color: #000;
+}
+
+.mat-tooltip {
+ font-size: 1rem !important;
+}
+
diff --git a/tslint.json b/tslint.json
index 4f4dff57..b35a3d4e 100644
--- a/tslint.json
+++ b/tslint.json
@@ -129,7 +129,7 @@
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true,
"variable-name": {
- "options": ["ban-keywords", "check-format", "allow-pascal-case"]
+ "options": ["ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"]
},
"whitespace": {
"options": [