From 3700bcaa5d7cbecd9b573b82d14c51a9060e0403 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Wed, 14 Aug 2019 19:55:24 +0300 Subject: [PATCH] Devices page implementation --- .../ThingsboardSecurityConfiguration.java | 4 +- .../thingsboard/server/config/WebConfig.java | 2 +- ui-ngx/src/app/core/http/customer.service.ts | 4 +- ui-ngx/src/app/core/http/device.service.ts | 39 ++- ui-ngx/src/app/core/http/entity.service.ts | 263 +++++++++++++++++ .../device-credentials-dialog.component.html | 79 +++++ .../device-credentials-dialog.component.ts | 138 +++++++++ .../home/pages/device/device.component.ts | 38 +++ .../home/pages/device/device.module.ts | 7 +- .../device/devices-table-config.resolver.ts | 134 ++++++++- .../home/pages/profile/profile.component.ts | 38 ++- .../entity/entities-table-config.models.ts | 1 + .../entity/entities-table.component.html | 31 +- .../entity/entity-autocomplete.component.html | 43 +++ .../entity/entity-autocomplete.component.ts | 274 ++++++++++++++++++ ui-ngx/src/app/shared/models/alarm.models.ts | 51 ++++ ui-ngx/src/app/shared/models/asset.models.ts | 34 +++ ui-ngx/src/app/shared/models/device.models.ts | 20 ++ .../app/shared/models/entity-type.models.ts | 4 + .../app/shared/models/entity-view.models.ts | 50 ++++ ui-ngx/src/app/shared/models/id/alarm-id.ts | 26 ++ ui-ngx/src/app/shared/models/id/asset-id.ts | 26 ++ .../shared/models/id/device-credentials-id.ts | 24 ++ .../app/shared/models/id/entity-view-id.ts | 26 ++ .../src/app/shared/models/id/rule-chain-id.ts | 26 ++ .../src/app/shared/models/id/rule-node-id.ts | 26 ++ .../app/shared/models/id/widget-type-id.ts | 26 ++ .../app/shared/models/id/widgets-bundle-id.ts | 26 ++ .../app/shared/models/rule-chain.models.ts | 37 +++ .../src/app/shared/models/rule-node.models.ts | 36 +++ .../app/shared/models/widget-type.models.ts | 33 +++ .../app/shared/models/widgets-bundle.model.ts | 26 ++ ui-ngx/src/app/shared/shared.module.ts | 3 + 33 files changed, 1552 insertions(+), 43 deletions(-) create mode 100644 ui-ngx/src/app/core/http/entity.service.ts create mode 100644 ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts create mode 100644 ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html create mode 100644 ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts create mode 100644 ui-ngx/src/app/shared/models/alarm.models.ts create mode 100644 ui-ngx/src/app/shared/models/asset.models.ts create mode 100644 ui-ngx/src/app/shared/models/entity-view.models.ts create mode 100644 ui-ngx/src/app/shared/models/id/alarm-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/asset-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/device-credentials-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/entity-view-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/rule-chain-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/rule-node-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/widget-type-id.ts create mode 100644 ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts create mode 100644 ui-ngx/src/app/shared/models/rule-chain.models.ts create mode 100644 ui-ngx/src/app/shared/models/rule-node.models.ts create mode 100644 ui-ngx/src/app/shared/models/widget-type.models.ts create mode 100644 ui-ngx/src/app/shared/models/widgets-bundle.model.ts diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 653ad68232..f826264c72 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -68,7 +68,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login"; public static final String PUBLIC_LOGIN_ENTRY_POINT = "/api/auth/login/public"; public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token"; - protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/static/**", "/api/noauth/**", "/webjars/**"}; + protected static final String[] NON_TOKEN_BASED_AUTH_ENTRY_POINTS = new String[] {"/index.html", "/assets/**", "/static/**", "/api/noauth/**", "/webjars/**"}; public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**"; public static final String WS_TOKEN_BASED_AUTH_ENTRY_POINT = "/api/ws/**"; @@ -155,7 +155,7 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt @Override public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/static/**"); + web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); } @Override diff --git a/application/src/main/java/org/thingsboard/server/config/WebConfig.java b/application/src/main/java/org/thingsboard/server/config/WebConfig.java index a7b5019d74..d764238fbc 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebConfig.java +++ b/application/src/main/java/org/thingsboard/server/config/WebConfig.java @@ -21,7 +21,7 @@ import org.springframework.web.bind.annotation.RequestMapping; @Controller public class WebConfig { - @RequestMapping(value = "/{path:^(?!api$)(?!static$)(?!webjars$)[^\\.]*}/**") + @RequestMapping(value = "/{path:^(?!api$)(?!assets$)(?!static$)(?!webjars$)[^\\.]*}/**") public String redirect() { return "forward:/index.html"; } diff --git a/ui-ngx/src/app/core/http/customer.service.ts b/ui-ngx/src/app/core/http/customer.service.ts index c84502cc38..f36d2dcf41 100644 --- a/ui-ngx/src/app/core/http/customer.service.ts +++ b/ui-ngx/src/app/core/http/customer.service.ts @@ -31,9 +31,9 @@ export class CustomerService { private http: HttpClient ) { } - public getCustomers(tenantId: string, pageLink: PageLink, ignoreErrors: boolean = false, + public getCustomers(pageLink: PageLink, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { - return this.http.get>(`/api/tenant/${tenantId}/customers${pageLink.toQuery()}`, + return this.http.get>(`/api/customers${pageLink.toQuery()}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); } diff --git a/ui-ngx/src/app/core/http/device.service.ts b/ui-ngx/src/app/core/http/device.service.ts index 19c34e5881..f970a90475 100644 --- a/ui-ngx/src/app/core/http/device.service.ts +++ b/ui-ngx/src/app/core/http/device.service.ts @@ -16,15 +16,16 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptions } from './http-utils'; -import { Observable } from 'rxjs/index'; +import { Observable, Subject, ReplaySubject } from 'rxjs/index'; import { HttpClient } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; import { PageData } from '@shared/models/page/page-data'; import { Tenant } from '@shared/models/tenant.model'; import {DashboardInfo, Dashboard} from '@shared/models/dashboard.models'; import {map} from 'rxjs/operators'; -import {DeviceInfo, Device} from '@app/shared/models/device.models'; +import {DeviceInfo, Device, DeviceCredentials} from '@app/shared/models/device.models'; import {EntitySubtype} from '@app/shared/models/entity-type.models'; +import {AuthService} from '../auth/auth.service'; @Injectable({ providedIn: 'root' @@ -67,6 +68,40 @@ export class DeviceService { return this.http.get>('/api/device/types', defaultHttpOptions(ignoreLoading, ignoreErrors)); } + public getDeviceCredentials(deviceId: string, sync: boolean = false, ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable { + const url = `/api/device/${deviceId}/credentials`; + if (sync) { + const responseSubject = new ReplaySubject(); + const request = new XMLHttpRequest(); + request.open('GET', url, false); + request.setRequestHeader('Accept', 'application/json, text/plain, */*'); + const jwtToken = AuthService.getJwtToken(); + if (jwtToken) { + request.setRequestHeader('X-Authorization', 'Bearer ' + jwtToken); + } + request.send(null); + if (request.status === 200) { + const credentials = JSON.parse(request.responseText) as DeviceCredentials; + responseSubject.next(credentials); + } else { + responseSubject.error(null); + } + return responseSubject.asObservable(); + } else { + return this.http.get(url, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + } + + public saveDeviceCredentials(deviceCredentials: DeviceCredentials, ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable { + return this.http.post('/api/device/credentials', deviceCredentials, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + + public makeDevicePublic(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable { + return this.http.post(`/api/customer/public/device/${deviceId}`, null, defaultHttpOptions(ignoreLoading, ignoreErrors)); + } + public unassignDeviceFromCustomer(deviceId: string, ignoreErrors: boolean = false, ignoreLoading: boolean = false) { return this.http.delete(`/api/customer/device/${deviceId}`, defaultHttpOptions(ignoreLoading, ignoreErrors)); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts new file mode 100644 index 0000000000..c7bfdcc2ae --- /dev/null +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -0,0 +1,263 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {Injectable} from '@angular/core'; +import {Observable, throwError, of, empty, EMPTY} from 'rxjs/index'; +import {HttpClient} from '@angular/common/http'; +import {PageLink} from '@shared/models/page/page-link'; +import {EntityType} from '@shared/models/entity-type.models'; +import {BaseData} from '@shared/models/base-data'; +import {EntityId} from '@shared/models/id/entity-id'; +import {DeviceService} from '@core/http/device.service'; +import {TenantService} from '@core/http/tenant.service'; +import {CustomerService} from '@core/http/customer.service'; +import {UserService} from './user.service'; +import {DashboardService} from '@core/http/dashboard.service'; +import {Direction} from '@shared/models/page/sort-order'; +import {PageData} from '@shared/models/page/page-data'; +import {getCurrentAuthUser} from '../auth/auth.selectors'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {Authority} from '@shared/models/authority.enum'; +import {Tenant} from '@shared/models/tenant.model'; +import {concatMap, expand, map, toArray} from 'rxjs/operators'; +import {Customer} from '@app/shared/models/customer.model'; + +@Injectable({ + providedIn: 'root' +}) +export class EntityService { + + constructor( + private http: HttpClient, + private store: Store, + private deviceService: DeviceService, + private tenantService: TenantService, + private customerService: CustomerService, + private userService: UserService, + private dashboardService: DashboardService + ) { } + + private getEntityObservable(entityType: EntityType, entityId: string, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + + let observable: Observable>; + switch (entityType) { + case EntityType.DEVICE: + observable = this.deviceService.getDevice(entityId, ignoreErrors, ignoreLoading); + break; + case EntityType.ASSET: + // TODO: + break; + case EntityType.ENTITY_VIEW: + // TODO: + break; + case EntityType.TENANT: + observable = this.tenantService.getTenant(entityId, ignoreErrors, ignoreLoading); + break; + case EntityType.CUSTOMER: + observable = this.customerService.getCustomer(entityId, ignoreErrors, ignoreLoading); + break; + case EntityType.DASHBOARD: + observable = this.dashboardService.getDashboardInfo(entityId, ignoreErrors, ignoreLoading); + break; + case EntityType.USER: + observable = this.userService.getUser(entityId, ignoreErrors, ignoreLoading); + break; + case EntityType.RULE_CHAIN: + // TODO: + break; + case EntityType.ALARM: + console.error('Get Alarm Entity is not implemented!'); + break; + } + return observable; + } + + public getEntity(entityType: EntityType, entityId: string, + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable> { + const entityObservable = this.getEntityObservable(entityType, entityId, ignoreErrors, ignoreLoading); + if (entityObservable) { + return entityObservable; + } else { + return throwError(null); + } + } + + private getSingleTenantByPageLinkObservable(pageLink: PageLink, + ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable> { + const authUser = getCurrentAuthUser(this.store); + const tenantId = authUser.tenantId; + return this.tenantService.getTenant(tenantId, ignoreErrors, ignoreLoading).pipe( + map((tenant) => { + const result = { + data: [], + totalPages: 0, + totalElements: 0, + hasNext: false + } as PageData; + if (tenant.title.toLowerCase().startsWith(pageLink.textSearch.toLowerCase())) { + result.data.push(tenant); + result.totalPages = 1; + result.totalElements = 1; + } + return result; + }) + ); + } + + private getSingleCustomerByPageLinkObservable(pageLink: PageLink, + ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable> { + const authUser = getCurrentAuthUser(this.store); + const customerId = authUser.customerId; + return this.customerService.getCustomer(customerId, ignoreErrors, ignoreLoading).pipe( + map((customer) => { + const result = { + data: [], + totalPages: 0, + totalElements: 0, + hasNext: false + } as PageData; + if (customer.title.toLowerCase().startsWith(pageLink.textSearch.toLowerCase())) { + result.data.push(customer); + result.totalPages = 1; + result.totalElements = 1; + } + return result; + }) + ); + } + + private getEntitiesByPageLinkObservable(entityType: EntityType, pageLink: PageLink, subType: string = '', + ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable>> { + let entitiesObservable: Observable>>; + const authUser = getCurrentAuthUser(this.store); + const customerId = authUser.customerId; + switch (entityType) { + case EntityType.DEVICE: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.deviceService.getCustomerDeviceInfos(customerId, pageLink, subType, ignoreErrors, ignoreLoading); + } else { + entitiesObservable = this.deviceService.getTenantDeviceInfos(pageLink, subType, ignoreErrors, ignoreLoading); + } + break; + case EntityType.ASSET: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + // TODO: + } else { + // TODO: + } + break; + case EntityType.ENTITY_VIEW: + pageLink.sortOrder.property = 'name'; + if (authUser.authority === Authority.CUSTOMER_USER) { + // TODO: + } else { + // TODO: + } + break; + case EntityType.TENANT: + pageLink.sortOrder.property = 'title'; + if (authUser.authority === Authority.TENANT_ADMIN) { + entitiesObservable = this.getSingleTenantByPageLinkObservable(pageLink, ignoreErrors, ignoreLoading); + } else { + entitiesObservable = this.tenantService.getTenants(pageLink, ignoreErrors, ignoreLoading); + } + break; + case EntityType.CUSTOMER: + pageLink.sortOrder.property = 'title'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.getSingleCustomerByPageLinkObservable(pageLink, ignoreErrors, ignoreLoading); + } else { + entitiesObservable = this.customerService.getCustomers(pageLink, ignoreErrors, ignoreLoading); + } + break; + case EntityType.RULE_CHAIN: + pageLink.sortOrder.property = 'name'; + // TODO: + break; + case EntityType.DASHBOARD: + pageLink.sortOrder.property = 'title'; + if (authUser.authority === Authority.CUSTOMER_USER) { + entitiesObservable = this.dashboardService.getCustomerDashboards(customerId, pageLink, ignoreErrors, ignoreLoading); + } else { + entitiesObservable = this.dashboardService.getTenantDashboards(pageLink, ignoreErrors, ignoreLoading); + } + break; + case EntityType.USER: + console.error('Get User Entities is not implemented!'); + break; + case EntityType.ALARM: + console.error('Get Alarm Entities is not implemented!'); + break; + } + return entitiesObservable; + } + + private getEntitiesByPageLink(entityType: EntityType, pageLink: PageLink, subType: string = '', + ignoreErrors: boolean = false, + ignoreLoading: boolean = false): Observable>> { + const entitiesObservable: Observable>> = + this.getEntitiesByPageLinkObservable(entityType, pageLink, subType, ignoreErrors, ignoreLoading); + if (entitiesObservable) { + return entitiesObservable.pipe( + expand((data) => { + if (data.hasNext) { + pageLink.page += 1; + return this.getEntitiesByPageLinkObservable(entityType, pageLink, subType, ignoreErrors, ignoreLoading); + } else { + return EMPTY; + } + }), + map((data) => data.data), + concatMap((data) => data), + toArray() + ); + } else { + return of(null); + } + } + + public getEntitiesByNameFilter(entityType: EntityType, entityNameFilter: string, + pageSize: number, subType: string = '', + ignoreErrors: boolean = false, ignoreLoading: boolean = false): Observable>> { + const pageLink = new PageLink(pageSize, 0, entityNameFilter, { + property: 'name', + direction: Direction.ASC + }); + if (pageSize === -1) { // all + pageLink.pageSize = 100; + return this.getEntitiesByPageLink(entityType, pageLink, subType, ignoreErrors, ignoreLoading).pipe( + map((data) => data && data.length ? data : null) + ); + } else { + const entitiesObservable: Observable>> = + this.getEntitiesByPageLinkObservable(entityType, pageLink, subType, ignoreErrors, ignoreLoading); + if (entitiesObservable) { + return entitiesObservable.pipe( + map((data) => data && data.data.length ? data.data : null) + ); + } else { + return of(null); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html new file mode 100644 index 0000000000..95975082a9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html @@ -0,0 +1,79 @@ + +
+ +

device.device-credentials

+ + +
+ + +
+
+
+ + device.credentials-type + + + {{ credentialTypeNamesMap.get(credentialsType) }} + + + + + device.access-token + + + {{ 'device.access-token-required' | translate }} + + + {{ 'device.access-token-invalid' | translate }} + + + + device.rsa-key + + + {{ 'device.rsa-key-required' | translate }} + + +
+
+
+ + + +
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts new file mode 100644 index 0000000000..a8d9d1e7b1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts @@ -0,0 +1,138 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {Component, OnInit, SkipSelf, Inject} from '@angular/core'; +import {ErrorStateMatcher, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material'; +import { PageComponent } from '@shared/components/page.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import {FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators} from '@angular/forms'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { AuthService } from '@core/auth/auth.service'; +import {DeviceService} from '@core/http/device.service'; +import {DeviceCredentials, DeviceCredentialsType, credentialTypeNames} from '@shared/models/device.models'; + +export interface DeviceCredentialsDialogData { + isReadOnly: boolean; + deviceId: string; +} + +@Component({ + selector: 'tb-device-credentials-dialog', + templateUrl: './device-credentials-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: DeviceCredentialsDialogComponent}], + styleUrls: [] +}) +export class DeviceCredentialsDialogComponent extends PageComponent implements OnInit, ErrorStateMatcher { + + deviceCredentialsFormGroup: FormGroup; + + isReadOnly: boolean; + + deviceCredentials: DeviceCredentials; + + submitted = false; + + deviceCredentialsType = DeviceCredentialsType; + + credentialsTypes = Object.keys(DeviceCredentialsType); + + credentialTypeNamesMap = credentialTypeNames; + + constructor(protected store: Store, + @Inject(MAT_DIALOG_DATA) public data: DeviceCredentialsDialogData, + private deviceService: DeviceService, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + public fb: FormBuilder) { + super(store); + + this.isReadOnly = data.isReadOnly; + } + + ngOnInit(): void { + this.deviceCredentialsFormGroup = this.fb.group({ + credentialsType: [DeviceCredentialsType.ACCESS_TOKEN], + credentialsId: [''], + credentialsValue: [''] + }); + if (this.isReadOnly) { + this.deviceCredentialsFormGroup.disable({emitEvent: false}); + } else { + this.registerDisableOnLoadFormControl(this.deviceCredentialsFormGroup.get('credentialsType')); + } + this.loadDeviceCredentials(); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + loadDeviceCredentials() { + this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe( + (deviceCredentials) => { + this.deviceCredentials = deviceCredentials; + this.deviceCredentialsFormGroup.patchValue({ + credentialsType: deviceCredentials.credentialsType, + credentialsId: deviceCredentials.credentialsId, + credentialsValue: deviceCredentials.credentialsValue + }); + this.updateValidators(); + } + ); + } + + credentialsTypeChanged(): void { + this.deviceCredentialsFormGroup.patchValue( + {credentialsId: null, credentialsValue: null}, {emitEvent: true}); + this.updateValidators(); + } + + updateValidators(): void { + const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType; + switch (crendetialsType) { + case DeviceCredentialsType.ACCESS_TOKEN: + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.max(20), Validators.pattern(/^.{1,20}$/)]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); + break; + case DeviceCredentialsType.X509_CERTIFICATE: + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]); + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity(); + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]); + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity(); + break; + } + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + this.deviceCredentials = {...this.deviceCredentials, ...this.deviceCredentialsFormGroup.value}; + this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe( + (deviceCredentials) => { + this.dialogRef.close(deviceCredentials); + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device/device.component.ts b/ui-ngx/src/app/modules/home/pages/device/device.component.ts index 3d287c5c62..ca86fa33af 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.component.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.component.ts @@ -26,6 +26,10 @@ import { Authority } from '@shared/models/authority.enum'; import {DeviceInfo} from '@shared/models/device.models'; import {EntityType} from '@shared/models/entity-type.models'; import {NULL_UUID} from '@shared/models/id/has-uuid'; +import {ActionNotificationShow} from '@core/notification/notification.actions'; +import {TranslateService} from '@ngx-translate/core'; +import {DeviceService} from '@core/http/device.service'; +import {ClipboardService} from 'ngx-clipboard'; @Component({ selector: 'tb-device', @@ -39,6 +43,9 @@ export class DeviceComponent extends EntityComponent { deviceScope: 'tenant' | 'customer' | 'customer_user'; constructor(protected store: Store, + protected translate: TranslateService, + private deviceService: DeviceService, + private clipboardService: ClipboardService, public fb: FormBuilder) { super(store); } @@ -85,4 +92,35 @@ export class DeviceComponent extends EntityComponent { this.entityForm.patchValue({additionalInfo: {description: entity.additionalInfo ? entity.additionalInfo.description : ''}}); } + + onDeviceIdCopied($event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + + copyAccessToken($event) { + if (this.entity.id) { + this.deviceService.getDeviceCredentials(this.entity.id.id, true).subscribe( + (deviceCredentials) => { + const credentialsId = deviceCredentials.credentialsId; + if (this.clipboardService.copyFromContent(credentialsId)) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('device.accessTokenCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + } + ); + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts index de4f34eeef..50bcbb5b56 100644 --- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts +++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts @@ -20,15 +20,18 @@ import { SharedModule } from '@shared/shared.module'; import {DeviceComponent} from '@modules/home/pages/device/device.component'; import {DeviceRoutingModule} from './device-routing.module'; import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; +import {DeviceCredentialsDialogComponent} from '@modules/home/pages/device/device-credentials-dialog.component'; @NgModule({ entryComponents: [ DeviceComponent, - DeviceTableHeaderComponent + DeviceTableHeaderComponent, + DeviceCredentialsDialogComponent ], declarations: [ DeviceComponent, - DeviceTableHeaderComponent + DeviceTableHeaderComponent, + DeviceCredentialsDialogComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts index 27e5c86a84..9cc9ecb078 100644 --- a/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts @@ -24,7 +24,8 @@ import { checkBoxCell, DateEntityTableColumn, EntityTableColumn, - EntityTableConfig + EntityTableConfig, + HeaderActionDescriptor } from '@shared/components/entity/entities-table-config.models'; import { TenantService } from '@core/http/tenant.service'; import { TranslateService } from '@ngx-translate/core'; @@ -37,7 +38,7 @@ import { import { TenantComponent } from '@modules/home/pages/tenant/tenant.component'; import { EntityAction } from '@shared/components/entity/entity-component.models'; import { User } from '@shared/models/user.model'; -import {Device, DeviceInfo} from '@app/shared/models/device.models'; +import {Device, DeviceCredentials, DeviceInfo} from '@app/shared/models/device.models'; import {DeviceComponent} from '@modules/home/pages/device/device.component'; import {Observable, of} from 'rxjs'; import {select, Store} from '@ngrx/store'; @@ -51,6 +52,12 @@ import {Customer} from '@app/shared/models/customer.model'; import {NULL_UUID} from '@shared/models/id/has-uuid'; import {BroadcastService} from '@core/services/broadcast.service'; import {DeviceTableHeaderComponent} from '@modules/home/pages/device/device-table-header.component'; +import {MatDialog} from '@angular/material'; +import { + DeviceCredentialsDialogComponent, + DeviceCredentialsDialogData +} from '@modules/home/pages/device/device-credentials-dialog.component'; +import {DialogService} from '@core/services/dialog.service'; @Injectable() export class DevicesTableConfigResolver implements Resolve> { @@ -63,9 +70,11 @@ export class DevicesTableConfigResolver implements Resolve this.deviceService.getTenantDeviceInfos(pageLink, this.config.componentsData.deviceType); + this.config.entitiesFetchFunction = pageLink => + this.deviceService.getTenantDeviceInfos(pageLink, this.config.componentsData.deviceType); this.config.deleteEntity = id => this.deviceService.deleteDevice(id.id); } else { - this.config.entitiesFetchFunction = pageLink => this.deviceService.getCustomerDeviceInfos(this.customerId, pageLink, this.config.componentsData.deviceType); + this.config.entitiesFetchFunction = pageLink => + this.deviceService.getCustomerDeviceInfos(this.customerId, pageLink, this.config.componentsData.deviceType); this.config.deleteEntity = id => this.deviceService.unassignDeviceFromCustomer(id.id); } } - configureCellActions(deviceScope: string): Array> { - const actions: Array> = []; + configureCellActions(deviceScope: string): Array> { + const actions: Array> = []; if (deviceScope === 'tenant') { actions.push( { @@ -177,18 +189,122 @@ export class DevicesTableConfigResolver implements Resolve { + const actions: Array = []; + actions.push( + { + name: this.translate.instant('device.add-device-text'), + icon: 'insert_drive_file', + isEnabled: () => true, + onAction: ($event) => this.config.table.addEntity($event) + }, + { + name: this.translate.instant('device.import'), + icon: 'file_upload', + isEnabled: () => true, + onAction: ($event) => this.importDevices($event) + } + ); + return actions; + } + + importDevices($event: Event) { if ($event) { $event.stopPropagation(); } // TODO: } - onDeviceAction(action: EntityAction): boolean { + makePublic($event: Event, device: Device) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('device.make-public-device-title', {deviceName: device.name}), + this.translate.instant('device.make-public-device-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceService.makeDevicePublic(device.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + assignToCustomer($event: Event, device: Device) { + if ($event) { + $event.stopPropagation(); + } + // TODO: + } + + unassignFromCustomer($event: Event, device: DeviceInfo) { + if ($event) { + $event.stopPropagation(); + } + const isPublic = device.customerIsPublic; + let title; + let content; + if (isPublic) { + title = this.translate.instant('device.make-private-device-title', {deviceName: device.name}); + content = this.translate.instant('device.make-private-device-text'); + } else { + title = this.translate.instant('device.unassign-device-title', {deviceName: device.name}); + content = this.translate.instant('device.unassign-device-text'); + } + this.dialogService.confirm( + this.translate.instant(title), + this.translate.instant(content), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((res) => { + if (res) { + this.deviceService.unassignDeviceFromCustomer(device.id.id).subscribe( + () => { + this.config.table.updateData(); + } + ); + } + } + ); + } + + manageCredentials($event: Event, device: Device) { + if ($event) { + $event.stopPropagation(); + } + this.dialog.open(DeviceCredentialsDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + deviceId: device.id.id, + isReadOnly: this.config.componentsData.deviceScope === 'customer_user' + } + }); + } + + onDeviceAction(action: EntityAction): boolean { switch (action.action) { case 'makePublic': this.makePublic(action.event, action.entity); return true; + case 'assignToCustomer': + this.assignToCustomer(action.event, action.entity); + return true; + case 'unassignFromCustomer': + this.unassignFromCustomer(action.event, action.entity); + return true; + case 'manageCredentials': + this.manageCredentials(action.event, action.entity); + return true; } return false; } diff --git a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts index 1f2dbfd697..7bb273ae0b 100644 --- a/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts +++ b/ui-ngx/src/app/modules/home/pages/profile/profile.component.ts @@ -14,26 +14,24 @@ /// limitations under the License. /// -import { Component, OnInit } from '@angular/core'; -import { UserService } from '@core/http/user.service'; -import { User } from '@shared/models/user.model'; -import { Authority } from '@shared/models/authority.enum'; -import { PageComponent } from '@shared/components/page.component'; -import { select, Store } from '@ngrx/store'; -import { AppState } from '@core/core.state'; -import { getCurrentAuthUser, selectAuthUser } from '@core/auth/auth.selectors'; -import { mergeMap, take } from 'rxjs/operators'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; -import { ActionAuthUpdateUserDetails } from '@core/auth/auth.actions'; -import { environment as env } from '@env/environment'; -import { TranslateService } from '@ngx-translate/core'; -import { ActionSettingsChangeLanguage } from '@core/settings/settings.actions'; -import { ChangePasswordDialogComponent } from '@modules/home/pages/profile/change-password-dialog.component'; -import { MatDialog } from '@angular/material'; -import { DialogService } from '@core/services/dialog.service'; -import { AuthService } from '@core/auth/auth.service'; -import { ActivatedRoute } from '@angular/router'; +import {Component, OnInit} from '@angular/core'; +import {UserService} from '@core/http/user.service'; +import {User} from '@shared/models/user.model'; +import {Authority} from '@shared/models/authority.enum'; +import {PageComponent} from '@shared/components/page.component'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {HasConfirmForm} from '@core/guards/confirm-on-exit.guard'; +import {ActionAuthUpdateUserDetails} from '@core/auth/auth.actions'; +import {environment as env} from '@env/environment'; +import {TranslateService} from '@ngx-translate/core'; +import {ActionSettingsChangeLanguage} from '@core/settings/settings.actions'; +import {ChangePasswordDialogComponent} from '@modules/home/pages/profile/change-password-dialog.component'; +import {MatDialog} from '@angular/material'; +import {DialogService} from '@core/services/dialog.service'; +import {AuthService} from '@core/auth/auth.service'; +import {ActivatedRoute} from '@angular/router'; @Component({ selector: 'tb-profile', diff --git a/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts b/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts index 28cfcd6a18..c140636c0c 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts +++ b/ui-ngx/src/app/shared/components/entity/entities-table-config.models.ts @@ -122,6 +122,7 @@ export class EntityTableConfig, P extends PageLink = P cellActionDescriptors: Array> = []; groupActionDescriptors: Array> = []; headerActionDescriptors: Array = []; + addActionDescriptors: Array = []; headerComponent: Type>; addEntity: CreateEntityOperation = null; detailsReadonly: EntityBooleanFunction = () => false; diff --git a/ui-ngx/src/app/shared/components/entity/entities-table.component.html b/ui-ngx/src/app/shared/components/entity/entities-table.component.html index 6cbcd63490..9ddc5ed91e 100644 --- a/ui-ngx/src/app/shared/components/entity/entities-table.component.html +++ b/ui-ngx/src/app/shared/components/entity/entities-table.component.html @@ -42,11 +42,32 @@ asButton historyOnly> - +
+ + + + + + + +
+ + + + + + + {{ translate.get(noEntitiesMatchingText, {entity: searchText}) | async }} + + + + + {{ entityRequiredText | translate }} + + diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts new file mode 100644 index 0000000000..982e929071 --- /dev/null +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -0,0 +1,274 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {AfterViewInit, Component, ElementRef, forwardRef, Input, OnInit, ViewChild} from '@angular/core'; +import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {Observable} from 'rxjs'; +import {map, mergeMap, startWith, tap} from 'rxjs/operators'; +import {Store} from '@ngrx/store'; +import {AppState} from '@app/core/core.state'; +import {TranslateService} from '@ngx-translate/core'; +import {AliasEntityType, EntityType} from '@shared/models/entity-type.models'; +import {BaseData} from '@shared/models/base-data'; +import {EntityId} from '@shared/models/id/entity-id'; +import {EntityService} from '@core/http/entity.service'; + +@Component({ + selector: 'tb-entity-autocomplete', + templateUrl: './entity-autocomplete.component.html', + styleUrls: [], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => EntityAutocompleteComponent), + multi: true + }] +}) +export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit, AfterViewInit { + + selectEntityFormGroup: FormGroup; + + modelValue: string | null; + + entityTypeValue: EntityType | AliasEntityType; + + entitySubtypeValue: string; + + @Input() + set entityType(entityType: EntityType) { + if (this.entityTypeValue !== entityType) { + this.entityTypeValue = entityType; + this.load(); + } + } + + @Input() + set entitySubtype(entitySubtype: string) { + if (this.entitySubtypeValue !== entitySubtype) { + this.entitySubtypeValue = entitySubtype; + const currentEntity = this.getCurrentEntity(); + if (currentEntity) { + if ((currentEntity as any).type !== this.entitySubtypeValue) { + this.reset(); + } + } + } + } + + @Input() + excludeEntityIds: Array; + + @Input() + required: boolean; + + @Input() + disabled: boolean; + + @ViewChild('entityInput', {static: true}) entityInput: ElementRef; + + entityText: string; + noEntitiesMatchingText: string; + entityRequiredText: string; + + filteredEntities: Observable>>; + + private searchText = ''; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + private entityService: EntityService, + private fb: FormBuilder) { + this.selectEntityFormGroup = this.fb.group({ + entity: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredEntities = this.selectEntityFormGroup.get('dashboard').valueChanges + .pipe( + tap(value => { + let modelValue; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value.id.id; + } + this.updateView(modelValue); + }), + startWith>(''), + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), + mergeMap(name => this.fetchEntities(name) ) + ); + } + + ngAfterViewInit(): void {} + + load(): void { + if (this.entityTypeValue) { + switch (this.entityTypeValue) { + case EntityType.ASSET: + this.entityText = 'asset.asset'; + this.noEntitiesMatchingText = 'asset.no-assets-matching'; + this.entityRequiredText = 'asset.asset-required'; + break; + case EntityType.DEVICE: + this.entityText = 'device.device'; + this.noEntitiesMatchingText = 'device.no-devices-matching'; + this.entityRequiredText = 'device.device-required'; + break; + case EntityType.ENTITY_VIEW: + this.entityText = 'entity-view.entity-view'; + this.noEntitiesMatchingText = 'entity-view.no-entity-views-matching'; + this.entityRequiredText = 'entity-view.entity-view-required'; + break; + case EntityType.RULE_CHAIN: + this.entityText = 'rulechain.rulechain'; + this.noEntitiesMatchingText = 'rulechain.no-rulechains-matching'; + this.entityRequiredText = 'rulechain.rulechain-required'; + break; + case EntityType.TENANT: + this.entityText = 'tenant.tenant'; + this.noEntitiesMatchingText = 'tenant.no-tenants-matching'; + this.entityRequiredText = 'tenant.tenant-required'; + break; + case EntityType.CUSTOMER: + this.entityText = 'customer.customer'; + this.noEntitiesMatchingText = 'customer.no-customers-matching'; + this.entityRequiredText = 'customer.customer-required'; + break; + case EntityType.USER: + this.entityText = 'user.user'; + this.noEntitiesMatchingText = 'user.no-users-matching'; + this.entityRequiredText = 'user.user-required'; + break; + case EntityType.DASHBOARD: + this.entityText = 'dashboard.dashboard'; + this.noEntitiesMatchingText = 'dashboard.no-dashboards-matching'; + this.entityRequiredText = 'dashboard.dashboard-required'; + break; + case EntityType.ALARM: + this.entityText = 'alarm.alarm'; + this.noEntitiesMatchingText = 'alarm.no-alarms-matching'; + this.entityRequiredText = 'alarm.alarm-required'; + break; + case AliasEntityType.CURRENT_CUSTOMER: + this.entityText = 'customer.default-customer'; + this.noEntitiesMatchingText = 'customer.no-customers-matching'; + this.entityRequiredText = 'customer.default-customer-required'; + break; + } + } + const currentEntity = this.getCurrentEntity(); + if (currentEntity) { + const currentEntityType = currentEntity.id.entityType; + if (this.entityTypeValue && currentEntityType !== this.entityTypeValue) { + this.reset(); + } + } + } + + getCurrentEntity(): BaseData | null { + const currentEntity = this.selectEntityFormGroup.get('entity').value; + if (currentEntity && typeof currentEntity !== 'string') { + return currentEntity as BaseData; + } else { + return null; + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + writeValue(value: string | null): void { + this.searchText = ''; + if (value != null) { + let targetEntityType = this.entityTypeValue; + if (targetEntityType === AliasEntityType.CURRENT_CUSTOMER) { + targetEntityType = EntityType.CUSTOMER; + } + this.entityService.getEntity(targetEntityType, value).subscribe( + (entity) => { + this.modelValue = entity.id.id; + this.selectEntityFormGroup.get('entity').patchValue(entity, {emitEvent: true}); + } + ); + } else { + this.modelValue = null; + this.selectEntityFormGroup.get('entity').patchValue(null, {emitEvent: true}); + } + } + + reset() { + this.selectEntityFormGroup.get('entity').patchValue(null, {emitEvent: true}); + } + + updateView(value: string | null) { + if (this.modelValue !== value) { + this.modelValue = value; + this.propagateChange(this.modelValue); + } + } + + displayEntityFn(entity?: BaseData): string | undefined { + return entity ? entity.name : undefined; + } + + fetchEntities(searchText?: string): Observable>> { + this.searchText = searchText; + let targetEntityType = this.entityTypeValue; + if (targetEntityType === AliasEntityType.CURRENT_CUSTOMER) { + targetEntityType = EntityType.CUSTOMER; + } + return this.entityService.getEntitiesByNameFilter(targetEntityType, searchText, + 50, this.entitySubtypeValue, false, true).pipe( + map((data) => { + if (data) { + if (this.excludeEntityIds && this.excludeEntityIds.length) { + const entities: Array> = []; + data.forEach((entity) => { + if (this.excludeEntityIds.indexOf(entity.id.id) === -1) { + entities.push(entity); + } + }); + return entities; + } else { + return data; + } + } else { + return []; + } + } + )); + } + + clear() { + this.selectEntityFormGroup.get('entity').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.entityInput.nativeElement.blur(); + this.entityInput.nativeElement.focus(); + }, 0); + } + +} diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts new file mode 100644 index 0000000000..8fbd24de3b --- /dev/null +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -0,0 +1,51 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {AssetId} from '@shared/models/id/asset-id'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {CustomerId} from '@shared/models/id/customer-id'; +import {AlarmId} from '@shared/models/id/alarm-id'; +import {EntityId} from '@shared/models/id/entity-id'; + +export enum AlarmSeverity { + CRITICAL = 'CRITICAL', + MAJOR = 'MAJOR', + MINOR = 'MINOR', + WARNING = 'WARNING', + INDETERMINATE = 'INDETERMINATE' +} + +export enum AlarmStatus { + ACTIVE_UNACK = 'ACTIVE_UNACK', + ACTIVE_ACK = 'ACTIVE_ACK', + CLEARED_UNACK = 'CLEARED_UNACK', + CLEARED_ACK = 'CLEARED_ACK' +} + +export interface Alarm extends BaseData { + tenantId: TenantId; + type: string; + originator: EntityId; + severity: AlarmSeverity; + status: AlarmStatus; + startTs: number; + endTs: number; + ackTs: number; + clearTs: number; + propagate: boolean; + details?: any; +} diff --git a/ui-ngx/src/app/shared/models/asset.models.ts b/ui-ngx/src/app/shared/models/asset.models.ts new file mode 100644 index 0000000000..b048317d22 --- /dev/null +++ b/ui-ngx/src/app/shared/models/asset.models.ts @@ -0,0 +1,34 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {AssetId} from './id/asset-id'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {CustomerId} from '@shared/models/id/customer-id'; +import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; + +export interface Asset extends BaseData { + tenantId: TenantId; + customerId: CustomerId; + name: string; + type: string; + additionalInfo?: any; +} + +export interface AssetInfo extends Asset { + customerTitle: string; + customerIsPublic: boolean; +} diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index ebd3acb44a..be0d1637da 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -18,6 +18,7 @@ import {BaseData} from '@shared/models/base-data'; import {DeviceId} from './id/device-id'; import {TenantId} from '@shared/models/id/tenant-id'; import {CustomerId} from '@shared/models/id/customer-id'; +import {DeviceCredentialsId} from '@shared/models/id/device-credentials-id'; export interface Device extends BaseData { tenantId: TenantId; @@ -32,3 +33,22 @@ export interface DeviceInfo extends Device { customerTitle: string; customerIsPublic: boolean; } + +export enum DeviceCredentialsType { + ACCESS_TOKEN = 'ACCESS_TOKEN', + X509_CERTIFICATE = 'X509_CERTIFICATE' +} + +export const credentialTypeNames = new Map( + [ + [DeviceCredentialsType.ACCESS_TOKEN, 'Access token'], + [DeviceCredentialsType.X509_CERTIFICATE, 'X.509 Certificate'], + ] +); + +export interface DeviceCredentials extends BaseData { + deviceId: DeviceId; + credentialsType: DeviceCredentialsType; + credentialsId: string; + credentialsValue: string; +} diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 4b9687f019..475d24d67f 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -31,6 +31,10 @@ export enum EntityType { WIDGET_TYPE = 'WIDGET_TYPE' } +export enum AliasEntityType { + CURRENT_CUSTOMER = 'CURRENT_CUSTOMER' +} + export interface EntityTypeTranslation { type: string; typePlural: string; diff --git a/ui-ngx/src/app/shared/models/entity-view.models.ts b/ui-ngx/src/app/shared/models/entity-view.models.ts new file mode 100644 index 0000000000..88f4a1aa64 --- /dev/null +++ b/ui-ngx/src/app/shared/models/entity-view.models.ts @@ -0,0 +1,50 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {AssetId} from './id/asset-id'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {CustomerId} from '@shared/models/id/customer-id'; +import {EntityViewId} from '@shared/models/id/entity-view-id'; +import {EntityId} from '@shared/models/id/entity-id'; + +export interface AttributesEntityView { + cs: Array; + ss: Array; + sh: Array; +} + +export interface TelemetryEntityView { + timeseries: Array; + attributes: AttributesEntityView; +} + +export interface EntityView extends BaseData { + tenantId: TenantId; + customerId: CustomerId; + entityId: EntityId; + name: string; + type: string; + keys: TelemetryEntityView; + startTimeMs: number; + endTimeMs: number; + additionalInfo?: any; +} + +export interface EntityViewInfo extends EntityView { + customerTitle: string; + customerIsPublic: boolean; +} diff --git a/ui-ngx/src/app/shared/models/id/alarm-id.ts b/ui-ngx/src/app/shared/models/id/alarm-id.ts new file mode 100644 index 0000000000..8c5b80cf00 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/alarm-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class AlarmId implements EntityId { + entityType = EntityType.ALARM; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/asset-id.ts b/ui-ngx/src/app/shared/models/id/asset-id.ts new file mode 100644 index 0000000000..cfb9afa0dd --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/asset-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class AssetId implements EntityId { + entityType = EntityType.ASSET; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/device-credentials-id.ts b/ui-ngx/src/app/shared/models/id/device-credentials-id.ts new file mode 100644 index 0000000000..a76d5a5251 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/device-credentials-id.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { HasUUID } from '@shared/models/id/has-uuid'; + +export class DeviceCredentialsId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/entity-view-id.ts b/ui-ngx/src/app/shared/models/id/entity-view-id.ts new file mode 100644 index 0000000000..dbc16acddd --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/entity-view-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class EntityViewId implements EntityId { + entityType = EntityType.ENTITY_VIEW; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/rule-chain-id.ts b/ui-ngx/src/app/shared/models/id/rule-chain-id.ts new file mode 100644 index 0000000000..f34bd581d4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/rule-chain-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class RuleChainId implements EntityId { + entityType = EntityType.RULE_CHAIN; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/rule-node-id.ts b/ui-ngx/src/app/shared/models/id/rule-node-id.ts new file mode 100644 index 0000000000..83b06fa681 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/rule-node-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class RuleNodeId implements EntityId { + entityType = EntityType.RULE_NODE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/widget-type-id.ts b/ui-ngx/src/app/shared/models/id/widget-type-id.ts new file mode 100644 index 0000000000..850d279de7 --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/widget-type-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class WidgetTypeId implements EntityId { + entityType = EntityType.WIDGET_TYPE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts b/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts new file mode 100644 index 0000000000..2e560c2bfc --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/widgets-bundle-id.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { EntityId } from './entity-id'; +import { EntityType } from '@shared/models/entity-type.models'; + +export class WidgetsBundleId implements EntityId { + entityType = EntityType.WIDGETS_BUNDLE; + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/rule-chain.models.ts b/ui-ngx/src/app/shared/models/rule-chain.models.ts new file mode 100644 index 0000000000..52d396d888 --- /dev/null +++ b/ui-ngx/src/app/shared/models/rule-chain.models.ts @@ -0,0 +1,37 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {AssetId} from '@shared/models/id/asset-id'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {CustomerId} from '@shared/models/id/customer-id'; +import {RuleChainId} from '@shared/models/id/rule-chain-id'; +import {RuleNodeId} from '@shared/models/id/rule-node-id'; + +export interface RuleChainConfiguration { + todo: Array; + // TODO: +} + +export interface RuleChain extends BaseData { + tenantId: TenantId; + name: string; + firstRuleNodeId: RuleNodeId; + root: boolean; + debugMode: boolean; + configuration: RuleChainConfiguration; + additionalInfo?: any; +} diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts new file mode 100644 index 0000000000..5448bd21af --- /dev/null +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {AssetId} from '@shared/models/id/asset-id'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {CustomerId} from '@shared/models/id/customer-id'; +import {RuleChainId} from '@shared/models/id/rule-chain-id'; +import {RuleNodeId} from '@shared/models/id/rule-node-id'; + +export interface RuleNodeConfiguration { + todo: Array; + // TODO: +} + +export interface RuleNode extends BaseData { + ruleChainId: RuleChainId; + type: string; + name: string; + debugMode: boolean; + configuration: RuleNodeConfiguration; + additionalInfo?: any; +} diff --git a/ui-ngx/src/app/shared/models/widget-type.models.ts b/ui-ngx/src/app/shared/models/widget-type.models.ts new file mode 100644 index 0000000000..f081e0942f --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget-type.models.ts @@ -0,0 +1,33 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id'; +import {WidgetTypeId} from '@shared/models/id/widget-type-id'; + +export interface WidgetTypeDescriptor { + todo: Array; + // TODO: +} + +export interface WidgetType extends BaseData { + tenantId: TenantId; + bundleAlias: string; + alias: string; + name: string; + descriptor: WidgetTypeDescriptor; +} diff --git a/ui-ngx/src/app/shared/models/widgets-bundle.model.ts b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts new file mode 100644 index 0000000000..5d0454bff4 --- /dev/null +++ b/ui-ngx/src/app/shared/models/widgets-bundle.model.ts @@ -0,0 +1,26 @@ +/// +/// Copyright © 2016-2019 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import {BaseData} from '@shared/models/base-data'; +import {TenantId} from '@shared/models/id/tenant-id'; +import {WidgetsBundleId} from '@shared/models/id/widgets-bundle-id'; + +export interface WidgetsBundle extends BaseData { + tenantId: TenantId; + alias: string; + title: string; + image: string; +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 1390aefefb..d309519b1b 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -80,6 +80,7 @@ import { HighlightPipe } from '@shared/pipe/highlight.pipe'; import {DashboardAutocompleteComponent} from '@shared/components/dashboard-autocomplete.component'; import {EntitySubTypeAutocompleteComponent} from '@shared/components/entity/entity-subtype-autocomplete.component'; import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-select.component'; +import {EntityAutocompleteComponent} from './components/entity/entity-autocomplete.component'; @NgModule({ providers: [ @@ -122,6 +123,7 @@ import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-s DashboardAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, + EntityAutocompleteComponent, NospacePipe, MillisecondsToTimeStringPipe, EnumToArrayPipe, @@ -189,6 +191,7 @@ import {EntitySubTypeSelectComponent} from './components/entity/entity-subtype-s DashboardAutocompleteComponent, EntitySubTypeAutocompleteComponent, EntitySubTypeSelectComponent, + EntityAutocompleteComponent, // ValueInputComponent, MatButtonModule, MatCheckboxModule,