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 @@ + + +
+
+ +
+ +
+
+ * + +
+
+ +
+
+ * + + + {{ permission.name }} + + +
+
+ +
+ + +
+
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 }}

-
-
-
-
- -
-
-
-
-

{{ 'DATATARGET.RELATIONS' | translate }}

-
-

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

-
-
-
-
-
-

{{'DATATARGET.PAYLOADEDECODER' | translate}} - {{relation.payloadDecoder.name}} - - {{ 'DATATARGET.NO-PAYLOADDECODER' | translate}} -

-
-
- -
-
-

{{'DATATARGET.IOTDEVICE' | translate}} - - , {{device.name}} - -

-
- -
-
-
-
-
-
-
-
+
+
\ 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 @@ - - - -
-
-
    -
  • - {{error | translate}} -
  • -
-
- - {{'DATATARGET.ADD-TO-OPENDATADK' | translate}} - -
-
- * - -
-
- -
-
- * - -
-
-
-
- * - -
-
-
-
- - -
-
- -
-
-
-
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
-
-
- {{'QUESTION.ADD-RELATIONS' | translate}} - - - - - - - - - -
-
- - {{'QUESTION.DATATARGET.SELECT-DEVICES' | translate}} - - - - - - {{device.name}} - - -
-
-
- - {{'QUESTION.DATATARGET.SELECT-PAYLOADDECODER' | translate}} - - - {{'QUESTION.DATATARGET.NO-PAYLOAD-DECODER-SELECTED' | translate}} - - - {{payloadDecoder.name}} - - - -
-
- -
- -

{{'DATATARGET.DELETE' | translate}}

-
-
-
-
-
-
- - -
-
\ 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 }}

+
+ +
+
+ + +
+
+
+
+

{{ 'DATATARGET.RELATIONS' | translate }}

+
+

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

+
+
+
+
+
+

{{'DATATARGET.PAYLOADEDECODER' | translate}} + {{relation.payloadDecoder.name}} + + {{ 'DATATARGET.NO-PAYLOADDECODER' | translate}} +

+
+
+ +
+
+

{{'DATATARGET.IOTDEVICE' | translate}} + + , {{device.name}} + +

+
+ +
+
+
+
+
+
+
+
+
\ 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 @@ + + + +
+ +
+
    +
  • + {{error | translate}} +
  • +
+
+ +
+
+ * + +
+
+ +
+
+ * + +
+
+ +
+
+ + + +
+
+ +
+
+ + + +
+
+ +
+
+ * + +
+
+ +
+
+ + +
+
+
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
+
+
+ {{'QUESTION.ADD-RELATIONS' | translate}} + + + + + + + + + +
+
+ + {{'QUESTION.DATATARGET.SELECT-DEVICES' | translate}} + + + + + + {{device.name}} + + +
+
+
+ + {{'QUESTION.DATATARGET.SELECT-PAYLOADDECODER' | translate}} + + + {{'QUESTION.DATATARGET.NO-PAYLOAD-DECODER-SELECTED' | translate}} + + + {{payloadDecoder.name}} + + + +
+
+ +
+ +

{{'DATATARGET.DELETE' | translate}}

+
+
+
+
+
+ +
+ + +
+
+
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 }}

+
+
+
+
+ +
+
+
+
+

{{ 'DATATARGET.RELATIONS' | translate }}

+
+

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

+
+
+
+
+
+

{{'DATATARGET.PAYLOADEDECODER' | translate}} + {{relation.payloadDecoder.name}} + + {{ 'DATATARGET.NO-PAYLOADDECODER' | translate}} +

+
+
+ +
+
+

{{'DATATARGET.IOTDEVICE' | translate}} + + , {{device.name}} + +

+
+ +
+
+
+
+
+
+
+
+
\ 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 @@ + + + +
+
+
    +
  • + {{error | translate}} +
  • +
+
+ + {{'DATATARGET.ADD-TO-OPENDATADK' | translate}} + +
+
+ * + +
+
+ +
+
+ * + +
+
+
+
+ * + +
+
+
+
+ + +
+
+ +
+
+
+
{{'QUESTION.DATATARGET.RELATIONS' | translate}}
+
+
+ {{'QUESTION.ADD-RELATIONS' | translate}} + + + + + + + + + +
+
+ + {{'QUESTION.DATATARGET.SELECT-DEVICES' | translate}} + + + + + + {{device.name}} + + +
+
+
+ + {{'QUESTION.DATATARGET.SELECT-PAYLOADDECODER' | translate}} + + + {{'QUESTION.DATATARGET.NO-PAYLOAD-DECODER-SELECTED' | translate}} + + + {{payloadDecoder.name}} + + + +
+
+ +
+ +

{{'DATATARGET.DELETE' | translate}}

+
+
+
+
+
+
+ + +
+
\ 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 }}

+
+

+ {{ 'MULTICAST.IOTDEVICE' | translate }}: + + , + {{ device.name }} + +

+
+
+
+
+
+

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 @@ + + +
+
+
    +
  • + {{ error | translate }} +
  • +
+
+ +
+
+ * + +
+ +
+ * + + +
+ +
+ * + + +
+ +
+ * + + +
+ +
+ * + +
+ +
+ * + + +
+ +
+ * + + +
+ +
+ * + + + {{ multicastType }} + + +
+ + + +
+ +
+ + +
+ + {{ + 'QUESTION.MULTICAST.SELECT-DEVICES' | translate + }} + + + + + + + + + {{ device.name }} + + +
+
+
+ + +
+
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 + + 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 @@ +Open Data DK 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": [