Browse Source

Merge branch 'master' of https://github.com/thingsboard/thingsboard into feature/device-provision-3.2-onlyProfileVersion

pull/3518/head
zbeacon 6 years ago
parent
commit
35f6f40ca3
  1. 5
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  2. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java
  3. 2
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java
  4. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java
  5. 9
      dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java
  6. 23
      dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java
  7. 4
      dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java
  8. 12
      ui-ngx/src/app/core/http/device-profile.service.ts
  9. 77
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.html
  10. 229
      ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts
  11. 8
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  12. 2
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html
  13. 3
      ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts
  14. 8
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
  15. 52
      ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts
  16. 2
      ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html
  17. 169
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html
  18. 64
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss
  19. 331
      ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts
  20. 3
      ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts
  21. 62
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html
  22. 111
      ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts
  23. 32
      ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts
  24. 8
      ui-ngx/src/app/shared/models/device.models.ts
  25. 22
      ui-ngx/src/assets/locale/locale.constant-en_US.json

5
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -192,10 +192,11 @@ public class DeviceProfileController extends BaseController {
@RequestParam int page,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@RequestParam(required = false) String sortOrder,
@RequestParam(required = false) String transportType) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink));
return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType));
} catch (Exception e) {
throw handleException(e);
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java

@ -37,7 +37,7 @@ public interface DeviceProfileService {
PageData<DeviceProfile> findDeviceProfiles(TenantId tenantId, PageLink pageLink);
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink);
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);

2
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileDao.java

@ -32,7 +32,7 @@ public interface DeviceProfileDao extends Dao<DeviceProfile> {
PageData<DeviceProfile> findDeviceProfiles(TenantId tenantId, PageLink pageLink);
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink);
PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
DeviceProfile findDefaultDeviceProfile(TenantId tenantId);

4
dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java

@ -182,11 +182,11 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
}
@Override
public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink) {
public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) {
log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink);
validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validatePageLink(pageLink);
return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink);
return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType);
}
@Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#tenantId.id, #name}")

9
dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java

@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
import java.util.UUID;
@ -45,6 +46,14 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository<Devi
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " +
"FROM DeviceProfileEntity d WHERE " +
"d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId,
@Param("textSearch") String textSearch,
@Param("transportType") DeviceTransportType transportType,
Pageable pageable);
@Query("SELECT d FROM DeviceProfileEntity d " +
"WHERE d.tenantId = :tenantId AND d.isDefault = true")
DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId);

23
dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java

@ -15,11 +15,13 @@
*/
package org.thingsboard.server.dao.sql.device;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -62,12 +64,21 @@ public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao<DeviceProfileE
}
@Override
public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink) {
return DaoUtil.pageToPageData(
deviceProfileRepository.findDeviceProfileInfos(
tenantId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) {
if (StringUtils.isNotEmpty(transportType)) {
return DaoUtil.pageToPageData(
deviceProfileRepository.findDeviceProfileInfos(
tenantId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
DeviceTransportType.valueOf(transportType),
DaoUtil.toPageable(pageLink)));
} else {
return DaoUtil.pageToPageData(
deviceProfileRepository.findDeviceProfileInfos(
tenantId.getId(),
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
}
@Override

4
dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java

@ -260,7 +260,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest {
pageLink = new PageLink(17);
PageData<DeviceProfileInfo> pageData;
do {
pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink);
pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink, null);
loadedDeviceProfileInfos.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -284,7 +284,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest {
}
pageLink = new PageLink(17);
pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink);
pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink, null);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getTotalElements());
}

12
ui-ngx/src/app/core/http/device-profile.service.ts

@ -20,7 +20,8 @@ import { PageLink } from '@shared/models/page/page-link';
import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
import { Observable } from 'rxjs';
import { PageData } from '@shared/models/page/page-data';
import { DeviceProfile, DeviceProfileInfo } from '@shared/models/device.models';
import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
import { isDefinedAndNotNull } from '@core/utils';
@Injectable({
providedIn: 'root'
@ -59,8 +60,13 @@ export class DeviceProfileService {
return this.http.get<DeviceProfileInfo>(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
}
public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable<PageData<DeviceProfileInfo>> {
return this.http.get<PageData<DeviceProfileInfo>>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
public getDeviceProfileInfos(pageLink: PageLink, transportType?: DeviceTransportType,
config?: RequestConfig): Observable<PageData<DeviceProfileInfo>> {
let url = `/api/deviceProfileInfos${pageLink.toQuery()}`;
if (isDefinedAndNotNull(transportType)) {
url += `&transportType=${transportType}`;
}
return this.http.get<PageData<DeviceProfileInfo>>(url, defaultHttpOptionsFromConfig(config));
}
}

77
ui-ngx/src/app/modules/home/components/device/device-credentials.component.html

@ -0,0 +1,77 @@
<!--
Copyright © 2016-2020 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.
-->
<section [formGroup]="deviceCredentialsFormGroup">
<mat-form-field class="mat-block">
<mat-label translate>device.credentials-type</mat-label>
<mat-select formControlName="credentialsType">
<mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
{{ credentialTypeNamesMap.get(deviceCredentialsType[credentialsType]) }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.ACCESS_TOKEN"
class="mat-block">
<mat-label translate>device.access-token</mat-label>
<input matInput formControlName="credentialsId" required>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
{{ 'device.access-token-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')">
{{ 'device.access-token-invalid' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.X509_CERTIFICATE"
class="mat-block">
<mat-label translate>device.rsa-key</mat-label>
<textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
{{ 'device.rsa-key-required' | translate }}
</mat-error>
</mat-form-field>
<section *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.MQTT_BASIC" formGroupName="credentialsBasic">
<mat-form-field class="mat-block">
<mat-label translate>device.client-id</mat-label>
<input matInput formControlName="clientId">
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.clientId').hasError('pattern')">
{{ 'device.client-id-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.user-name</mat-label>
<input matInput formControlName="userName" [required]="!!deviceCredentialsFormGroup.get('credentialsBasic.password').value">
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.userName').hasError('required')">
{{ 'device.user-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.password</mat-label>
<input matInput formControlName="password"
autocomplete="new-password"
(ngModelChange)="passwordChanged()"
[type]="hidePassword ? 'password' : 'text'">
<button mat-icon-button matSuffix type="button"
(click)="hidePassword = !hidePassword"
[attr.aria-pressed]="hidePassword">
<mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne')">
{{ 'device.client-id-or-user-name-necessary' | translate }}
</mat-error>
</section>
</section>

229
ui-ngx/src/app/modules/home/components/device/device-credentials.component.ts

@ -0,0 +1,229 @@
///
/// Copyright © 2016-2020 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, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
import {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
ValidatorFn,
Validators
} from '@angular/forms';
import {
credentialTypeNames,
DeviceCredentialMQTTBasic,
DeviceCredentials,
DeviceCredentialsType
} from '@shared/models/device.models';
import { Subscription } from 'rxjs';
import { isDefinedAndNotNull } from '@core/utils';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'tb-device-credentials',
templateUrl: './device-credentials.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DeviceCredentialsComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => DeviceCredentialsComponent),
multi: true,
}],
styleUrls: []
})
export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
deviceCredentialsFormGroup: FormGroup;
subscriptions: Subscription[] = [];
@Input()
disabled: boolean;
deviceCredentials: DeviceCredentials = null;
submitted = false;
deviceCredentialsType = DeviceCredentialsType;
credentialsTypes = Object.keys(DeviceCredentialsType);
credentialTypeNamesMap = credentialTypeNames;
hidePassword = true;
private propagateChange = (v: any) => {};
constructor(public fb: FormBuilder) {
this.deviceCredentialsFormGroup = this.fb.group({
credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
credentialsId: [null],
credentialsValue: [null],
credentialsBasic: this.fb.group({
clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]],
userName: [null],
password: [null]
}, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
});
this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
this.subscriptions.push(
this.deviceCredentialsFormGroup.valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
this.updateView();
})
);
this.subscriptions.push(
this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.subscribe(() => {
this.credentialsTypeChanged();
})
);
}
ngOnInit(): void {
if (this.disabled) {
this.deviceCredentialsFormGroup.disable({emitEvent: false});
}
}
ngOnDestroy() {
this.subscriptions.forEach(s => s.unsubscribe());
}
writeValue(value: DeviceCredentials | null): void {
if (isDefinedAndNotNull(value)) {
this.deviceCredentials = value;
let credentialsBasic = {clientId: null, userName: null, password: null};
let credentialsValue = null;
if (value.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
credentialsBasic = JSON.parse(value.credentialsValue) as DeviceCredentialMQTTBasic;
} else {
credentialsValue = value.credentialsValue;
}
this.deviceCredentialsFormGroup.patchValue({
credentialsType: value.credentialsType,
credentialsId: value.credentialsId,
credentialsValue,
credentialsBasic
}, {emitEvent: false});
this.updateValidators();
}
}
updateView() {
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
}
delete deviceCredentialsValue.credentialsBasic;
this.propagateChange(deviceCredentialsValue);
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.deviceCredentialsFormGroup.disable({emitEvent: false});
} else {
this.deviceCredentialsFormGroup.enable({emitEvent: false});
this.updateValidators();
}
}
public validate(c: FormControl) {
return this.deviceCredentialsFormGroup.valid ? null : {
deviceCredentials: {
valid: false,
},
};
}
credentialsTypeChanged(): void {
this.deviceCredentialsFormGroup.patchValue({
credentialsId: null,
credentialsValue: null,
credentialsBasic: {clientId: '', userName: '', password: ''}
});
this.updateValidators();
}
updateValidators(): void {
this.hidePassword = true;
const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
switch (crendetialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false});
break;
case DeviceCredentialsType.X509_CERTIFICATE:
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false});
break;
case DeviceCredentialsType.MQTT_BASIC:
this.deviceCredentialsFormGroup.get('credentialsBasic').enable({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
}
}
private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
passwordChanged() {
const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
if (value !== '') {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
}
} else {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
}
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity({
emitEvent: false,
onlySelf: true
});
}
}

8
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -109,6 +109,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di
import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";
import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
import { DeviceCredentialsComponent } from './device/device-credentials.component';
@NgModule({
declarations:
@ -200,7 +202,9 @@ import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
AlarmScheduleComponent,
DeviceWizardDialogComponent,
DeviceCredentialsComponent
],
imports: [
CommonModule,
@ -280,6 +284,8 @@ import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component
DeviceProfileDialogComponent,
AddDeviceProfileDialogComponent,
RuleChainAutocompleteComponent,
DeviceWizardDialogComponent,
DeviceCredentialsComponent,
DeviceProfileProvisionConfigurationComponent,
AlarmScheduleComponent
],

2
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.html

@ -85,7 +85,7 @@
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules' | translate:
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms

3
ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts

@ -46,6 +46,7 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
export interface AddDeviceProfileDialogData {
deviceProfileName: string;
transportType: DeviceTransportType;
}
@Component({
@ -99,7 +100,7 @@ export class AddDeviceProfileDialogComponent extends
);
this.transportConfigFormGroup = this.fb.group(
{
transportType: [DeviceTransportType.DEFAULT, [Validators.required]],
transportType: [data.transportType ? data.transportType : DeviceTransportType.DEFAULT, [Validators.required]],
transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT),
[Validators.required]]
}

8
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html

@ -48,7 +48,7 @@
</mat-option>
<mat-option *ngIf="!(filteredDeviceProfiles | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
<div *ngIf="!textIsNotEmpty(searchText) || !addNewProfile; else searchNotEmpty">
<span translate>device-profile.no-device-profiles-found</span>
</div>
<ng-template #searchNotEmpty>
@ -56,10 +56,10 @@
{{ translate.get('device-profile.no-device-profiles-matching',
{entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
</span>
<span>
<a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
</span>
</ng-template>
<span>
<a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
</span>
</div>
</mat-option>
</mat-autocomplete>

52
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts

@ -19,9 +19,10 @@ import {
ElementRef,
EventEmitter,
forwardRef,
Input, NgZone,
Input,
NgZone, OnChanges,
OnInit,
Output,
Output, SimpleChanges,
ViewChild
} from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
@ -38,14 +39,7 @@ import { TruncatePipe } from '@shared//pipe/truncate.pipe';
import { ENTER } from '@angular/cdk/keycodes';
import { MatDialog } from '@angular/material/dialog';
import { DeviceProfileId } from '@shared/models/id/device-profile-id';
import {
createDeviceProfileConfiguration,
createDeviceProfileTransportConfiguration,
DeviceProfile,
DeviceProfileInfo,
DeviceProfileType,
DeviceTransportType
} from '@shared/models/device.models';
import { DeviceProfile, DeviceProfileInfo, DeviceProfileType, DeviceTransportType } from '@shared/models/device.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component';
import { MatAutocomplete } from '@angular/material/autocomplete';
@ -61,7 +55,7 @@ import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './a
multi: true
}]
})
export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit {
export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges {
selectDeviceProfileFormGroup: FormGroup;
@ -76,6 +70,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
@Input()
editProfileEnabled = true;
@Input()
addNewProfile = true;
@Input()
transportType: DeviceTransportType = null;
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;
@ -168,11 +168,22 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
);
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue) {
if (propName === 'transportType') {
this.writeValue(null);
}
}
}
}
selectDefaultDeviceProfileIfNeeded(): void {
if (this.selectDefaultProfile && !this.modelValue) {
this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe(
(profile) => {
if (profile) {
if (profile && !this.transportType || (profile.transportType === this.transportType)) {
this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false});
this.updateView(profile);
}
@ -183,6 +194,11 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.selectDeviceProfileFormGroup.disable();
} else {
this.selectDeviceProfileFormGroup.enable();
}
}
writeValue(value: DeviceProfileId | null): void {
@ -244,7 +260,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
property: 'name',
direction: Direction.ASC
});
return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe(
return this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).pipe(
map(pageData => {
let data = pageData.data;
if (this.displayAllOnEmpty) {
@ -280,9 +296,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
createDeviceProfile($event: Event, profileName: string) {
$event.preventDefault();
const deviceProfile: DeviceProfile = {
name: profileName
name: profileName,
transportType: this.transportType
} as DeviceProfile;
this.openDeviceProfileDialog(deviceProfile, true);
if (this.addNewProfile) {
this.openDeviceProfileDialog(deviceProfile, true);
}
}
editDeviceProfile($event: Event) {
@ -312,7 +331,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceProfileName: deviceProfile.name
deviceProfileName: deviceProfile.name,
transportType: deviceProfile.transportType
}
}).afterClosed();
}

2
ui-ngx/src/app/modules/home/components/profile/device-profile-data.component.html

@ -42,7 +42,7 @@
<mat-expansion-panel [expanded]="true">
<mat-expansion-panel-header>
<mat-panel-title>
<div>{{'device-profile.alarm-rules' | translate:
<div>{{'device-profile.alarm-rules-with-count' | translate:
{count: deviceProfileDataFormGroup.get('alarms').value ?
deviceProfileDataFormGroup.get('alarms').value.length : 0} }}</div>
</mat-panel-title>

169
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html

@ -0,0 +1,169 @@
<!--
Copyright © 2016-2020 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.
-->
<div>
<mat-toolbar color="primary">
<h2 translate>device.add-device-text</h2>
<span fxFlex></span>
<button mat-icon-button
(click)="cancel()"
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
<mat-step [stepControl]="deviceWizardFormGroup">
<form [formGroup]="deviceWizardFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device.wizard.device-details' | translate}}</ng-template>
<fieldset [disabled]="isLoading$ | async">
<mat-form-field class="mat-block">
<mat-label translate>device.name</mat-label>
<input matInput formControlName="name" required>
<mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('required')">
{{ 'device.name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.label</mat-label>
<input matInput formControlName="label">
</mat-form-field>
<mat-form-field class="mat-block" style="padding-bottom: 14px;">
<mat-label translate>device-profile.transport-type</mat-label>
<mat-select formControlName="transportType" required>
<mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
{{deviceTransportTypeTranslations.get(type) | translate}}
</mat-option>
</mat-select>
<mat-hint *ngIf="deviceWizardFormGroup.get('transportType').value">
{{deviceTransportTypeHints.get(deviceWizardFormGroup.get('transportType').value) | translate}}
</mat-hint>
<mat-error *ngIf="deviceWizardFormGroup.get('transportType').hasError('required')">
{{ 'device-profile.transport-type-required' | translate }}
</mat-error>
</mat-form-field>
<div fxLayout="row" fxLayoutGap="16px">
<mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
<mat-radio-button [value]="0" color="primary">
<span translate>device.wizard.existing-device-profile</span>
</mat-radio-button>
<mat-radio-button [value]="1" color="primary">
<span translate>device.wizard.new-device-profile</span>
</mat-radio-button>
</mat-radio-group>
<div fxLayout="column">
<tb-device-profile-autocomplete
[required]="!createProfile"
[transportType]="deviceWizardFormGroup.get('transportType').value"
formControlName="deviceProfileId"
(deviceProfileChanged)="$event?.transportType ? deviceWizardFormGroup.get('transportType').patchValue($event?.transportType) : {}"
[addNewProfile]="false"
[selectDefaultProfile]="true"
[editProfileEnabled]="false">
</tb-device-profile-autocomplete>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>device-profile.new-device-profile-name</mat-label>
<input matInput formControlName="newDeviceProfileTitle"
[required]="createProfile">
<mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
{{ 'device-profile.new-device-profile-name-required' | translate }}
</mat-error>
</mat-form-field>
</div>
</div>
<mat-checkbox formControlName="gateway" style="padding-bottom: 16px;">
{{ 'device.is-gateway' | translate }}
</mat-checkbox>
<mat-form-field class="mat-block">
<mat-label translate>device.description</mat-label>
<textarea matInput formControlName="description" rows="2"></textarea>
</mat-form-field>
</fieldset>
</form>
</mat-step>
<mat-step [stepControl]="transportConfigFormGroup" *ngIf="createTransportConfiguration">
<form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
<tb-device-profile-transport-configuration
formControlName="transportConfiguration"
required>
</tb-device-profile-transport-configuration>
</form>
</mat-step>
<mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
<form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
<ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
{count: alarmRulesFormGroup.get('alarms').value ?
alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
<tb-device-profile-alarms
formControlName="alarms">
</tb-device-profile-alarms>
</form>
</mat-step>
<mat-step [stepControl]="credentialsFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
<form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
<mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credential' | translate }}</mat-checkbox>
<tb-device-credentials
[fxShow]="credentialsFormGroup.get('setCredential').value"
formControlName="credential">
</tb-device-credentials>
</form>
</mat-step>
<mat-step [stepControl]="customerFormGroup" [optional]="true">
<ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
<form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
<tb-entity-autocomplete
formControlName="customerId"
labelText="device.wizard.customer-to-assign-device"
[entityType]="entityType.CUSTOMER">
</tb-entity-autocomplete>
</form>
</mat-step>
</mat-horizontal-stepper>
</div>
<div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;">
<div fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-raised-button
*ngIf="showNext"
[disabled]="(isLoading$ | async)"
(click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
</div>
<div fxFlex fxLayout="row">
<button mat-button
color="primary"
[disabled]="(isLoading$ | async)"
(click)="cancel()">{{ 'action.cancel' | translate }}</button>
<span fxFlex></span>
<div fxLayout="row wrap" fxLayoutGap="8px">
<button mat-raised-button *ngIf="selectedIndex > 0"
[disabled]="(isLoading$ | async)"
(click)="previousStep()">{{ 'action.back' | translate }}</button>
<button mat-raised-button
[disabled]="(isLoading$ | async)"
color="primary"
(click)="add()">{{ 'action.add' | translate }}</button>
</div>
</div>
</div>
</div>

64
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2020 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 "../../../../../scss/constants";
:host-context(.tb-fullscreen-dialog .mat-dialog-container) {
@media #{$mat-lt-sm} {
.mat-dialog-content {
max-height: 75vh;
}
}
}
:host ::ng-deep {
.mat-dialog-content {
display: flex;
flex-direction: column;
height: 100%;
.mat-stepper-horizontal {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
@media #{$mat-lt-sm} {
.mat-step-label {
white-space: normal;
overflow: visible;
.mat-step-text-label {
overflow: visible;
}
}
}
.mat-horizontal-content-container {
height: 450px;
max-height: 100%;
width: 100%;;
overflow-y: auto;
@media #{$mat-gt-sm} {
min-width: 800px;
}
}
.mat-horizontal-stepper-content[aria-expanded=true] {
height: 100%;
form {
height: 100%;
}
}
}
}
}

331
ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts

@ -0,0 +1,331 @@
///
/// Copyright © 2016-2020 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, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
import {
createDeviceProfileConfiguration,
createDeviceProfileTransportConfiguration,
DeviceProfile,
DeviceProfileType,
DeviceTransportType, deviceTransportTypeHintMap,
deviceTransportTypeTranslationMap
} from '@shared/models/device.models';
import { MatHorizontalStepper } from '@angular/material/stepper';
import { AddEntityDialogData } from '@home/models/entity/entity-component.models';
import { BaseData, HasId } from '@shared/models/base-data';
import { EntityType } from '@shared/models/entity-type.models';
import { DeviceProfileService } from '@core/http/device-profile.service';
import { EntityId } from '@shared/models/id/entity-id';
import { Observable, of, Subscription } from 'rxjs';
import { map, mergeMap, tap } from 'rxjs/operators';
import { DeviceService } from '@core/http/device.service';
import { ErrorStateMatcher } from '@angular/material/core';
import { StepperSelectionEvent } from '@angular/cdk/stepper';
import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@Component({
selector: 'tb-device-wizard',
templateUrl: './device-wizard-dialog.component.html',
providers: [],
styleUrls: ['./device-wizard-dialog.component.scss']
})
export class DeviceWizardDialogComponent extends
DialogComponent<DeviceWizardDialogComponent, boolean> implements OnDestroy, ErrorStateMatcher {
@ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper;
selectedIndex = 0;
showNext = true;
createProfile = false;
createTransportConfiguration = false;
entityType = EntityType;
deviceTransportTypes = Object.keys(DeviceTransportType);
deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
deviceTransportTypeHints = deviceTransportTypeHintMap;
deviceWizardFormGroup: FormGroup;
transportConfigFormGroup: FormGroup;
alarmRulesFormGroup: FormGroup;
credentialsFormGroup: FormGroup;
customerFormGroup: FormGroup;
labelPosition = 'end';
private subscriptions: Subscription[] = [];
constructor(protected store: Store<AppState>,
protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<EntityId>>,
@SkipSelf() private errorStateMatcher: ErrorStateMatcher,
public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
private deviceProfileService: DeviceProfileService,
private deviceService: DeviceService,
private breakpointObserver: BreakpointObserver,
private fb: FormBuilder) {
super(store, router, dialogRef);
this.deviceWizardFormGroup = this.fb.group({
name: ['', Validators.required],
label: [''],
gateway: [false],
transportType: [DeviceTransportType.DEFAULT, Validators.required],
addProfileType: [0],
deviceProfileId: [null, Validators.required],
newDeviceProfileTitle: [{value: null, disabled: true}],
description: ['']
}
);
this.subscriptions.push(this.deviceWizardFormGroup.get('addProfileType').valueChanges.subscribe(
(addProfileType: number) => {
if (addProfileType === 0) {
this.deviceWizardFormGroup.get('deviceProfileId').setValidators([Validators.required]);
this.deviceWizardFormGroup.get('deviceProfileId').enable();
this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null);
this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable();
this.deviceWizardFormGroup.updateValueAndValidity();
this.createProfile = false;
this.createTransportConfiguration = false;
} else {
this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null);
this.deviceWizardFormGroup.get('deviceProfileId').disable();
this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]);
this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable();
this.deviceWizardFormGroup.updateValueAndValidity();
this.createProfile = true;
this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value &&
DeviceTransportType.DEFAULT !== this.deviceWizardFormGroup.get('transportType').value;
}
}
));
this.transportConfigFormGroup = this.fb.group(
{
transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required]
}
);
this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => {
this.deviceProfileTransportTypeChanged(transportType);
}));
this.alarmRulesFormGroup = this.fb.group({
alarms: [null]
}
);
this.credentialsFormGroup = this.fb.group({
setCredential: [false],
credential: [{value: null, disabled: true}]
}
);
this.subscriptions.push(this.credentialsFormGroup.get('setCredential').valueChanges.subscribe((value) => {
if (value) {
this.credentialsFormGroup.get('credential').enable();
} else {
this.credentialsFormGroup.get('credential').disable();
}
}));
this.customerFormGroup = this.fb.group({
customerId: [null]
}
);
this.labelPosition = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']) ? 'end' : 'bottom';
this.subscriptions.push(this.breakpointObserver
.observe(MediaBreakpoints['gt-sm'])
.subscribe((state: BreakpointState) => {
if (state.matches) {
this.labelPosition = 'end';
} else {
this.labelPosition = 'bottom';
}
}
));
}
ngOnDestroy() {
super.ngOnDestroy();
this.subscriptions.forEach(s => s.unsubscribe());
}
isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
const customErrorState = !!(control && control.invalid);
return originalErrorState || customErrorState;
}
cancel(): void {
this.dialogRef.close(null);
}
previousStep(): void {
this.addDeviceWizardStepper.previous();
}
nextStep(): void {
this.addDeviceWizardStepper.next();
}
getFormLabel(index: number): string {
if (index > 0) {
if (!this.createProfile) {
index += 2;
} else if (!this.createTransportConfiguration) {
index += 1;
}
}
switch (index) {
case 0:
return 'device.wizard.device-details';
case 1:
return 'device-profile.transport-configuration';
case 2:
return 'device-profile.alarm-rules';
case 3:
return 'device.credentials';
case 4:
return 'customer.customer';
}
}
get maxStepperIndex(): number {
return this.addDeviceWizardStepper?._steps?.length - 1;
}
private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void {
this.transportConfigFormGroup.patchValue(
{transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
this.createTransportConfiguration = this.createProfile && deviceTransportType &&
DeviceTransportType.DEFAULT !== deviceTransportType;
}
add(): void {
if (this.allValid()) {
this.createDeviceProfile().pipe(
mergeMap(profileId => this.createDevice(profileId)),
mergeMap(device => this.saveCredentials(device))
).subscribe(
(created) => {
this.dialogRef.close(created);
}
);
}
}
private createDeviceProfile(): Observable<EntityId> {
if (this.deviceWizardFormGroup.get('addProfileType').value) {
const deviceProfile: DeviceProfile = {
name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value,
type: DeviceProfileType.DEFAULT,
transportType: this.deviceWizardFormGroup.get('transportType').value,
profileData: {
configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
alarms: this.alarmRulesFormGroup.get('alarms').value
}
};
return this.deviceProfileService.saveDeviceProfile(deviceProfile).pipe(
map(profile => profile.id),
tap((profileId) => {
this.deviceWizardFormGroup.patchValue({
deviceProfileId: profileId,
addProfileType: 0
});
})
);
} else {
return of(null);
}
}
private createDevice(profileId: EntityId = this.deviceWizardFormGroup.get('deviceProfileId').value): Observable<BaseData<HasId>> {
const device = {
name: this.deviceWizardFormGroup.get('name').value,
label: this.deviceWizardFormGroup.get('label').value,
deviceProfileId: profileId,
additionalInfo: {
gateway: this.deviceWizardFormGroup.get('gateway').value,
description: this.deviceWizardFormGroup.get('description').value
},
customerId: null
};
if (this.customerFormGroup.get('customerId').value) {
device.customerId = {
entityType: EntityType.CUSTOMER,
id: this.customerFormGroup.get('customerId').value
};
}
return this.data.entitiesTableConfig.saveEntity(device);
}
private saveCredentials(device: BaseData<HasId>): Observable<boolean> {
if (this.credentialsFormGroup.get('setCredential').value) {
return this.deviceService.getDeviceCredentials(device.id.id).pipe(
mergeMap(
(deviceCredentials) => {
const deviceCredentialsValue = {...deviceCredentials, ...this.credentialsFormGroup.value.credential};
return this.deviceService.saveDeviceCredentials(deviceCredentialsValue);
}
),
map(() => true));
}
return of(true);
}
allValid(): boolean {
if (this.addDeviceWizardStepper.steps.find((item, index) => {
if (item.stepControl.invalid) {
item.interacted = true;
this.addDeviceWizardStepper.selectedIndex = index;
return true;
} else {
return false;
}
} )) {
return false;
} else {
return true;
}
}
changeStep($event: StepperSelectionEvent): void {
this.selectedIndex = $event.selectedIndex;
if (this.selectedIndex === this.maxStepperIndex) {
this.showNext = false;
} else {
this.showNext = true;
}
}
}

3
ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts

@ -114,7 +114,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
deviceProfileName: null
deviceProfileName: null,
transportType: null
}
}).afterClosed();
}

62
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.html

@ -30,65 +30,9 @@
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div mat-dialog-content>
<fieldset [disabled]="(isLoading$ | async) || isReadOnly">
<mat-form-field class="mat-block">
<mat-label translate>device.credentials-type</mat-label>
<mat-select formControlName="credentialsType"
(ngModelChange)="credentialsTypeChanged()">
<mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
{{ credentialTypeNamesMap.get(deviceCredentialsType[credentialsType]) }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.ACCESS_TOKEN"
class="mat-block">
<mat-label translate>device.access-token</mat-label>
<input matInput formControlName="credentialsId" required>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
{{ 'device.access-token-required' | translate }}
</mat-error>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')">
{{ 'device.access-token-invalid' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.X509_CERTIFICATE"
class="mat-block">
<mat-label translate>device.rsa-key</mat-label>
<textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
{{ 'device.rsa-key-required' | translate }}
</mat-error>
</mat-form-field>
<section *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.MQTT_BASIC" formGroupName="credentialsBasic">
<mat-form-field class="mat-block">
<mat-label translate>device.client-id</mat-label>
<input matInput formControlName="clientId">
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.clientId').hasError('pattern')">
{{ 'device.client-id-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.user-name</mat-label>
<input matInput formControlName="userName" [required]="!!deviceCredentialsFormGroup.get('credentialsBasic.password').value">
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.userName').hasError('required')">
{{ 'device.user-name-required' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>device.password</mat-label>
<input matInput formControlName="password"
autocomplete="new-password"
(ngModelChange)="passwordChanged()"
[type]="hidePassword ? 'password' : 'text'">
<button mat-icon-button matSuffix type="button"
(click)="hidePassword = !hidePassword"
[attr.aria-pressed]="hidePassword">
<mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne')">
{{ 'device.client-id-or-user-name-necessary' | translate }}
</mat-error>
</section>
<tb-device-credentials
formControlName="credential">
</tb-device-credentials>
</fieldset>
</div>
<div mat-dialog-actions fxLayoutAlign="end center">

111
ui-ngx/src/app/modules/home/pages/device/device-credentials-dialog.component.ts

@ -19,23 +19,9 @@ import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import {
FormBuilder,
FormControl,
FormGroup,
FormGroupDirective,
NgForm,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
import { DeviceService } from '@core/http/device.service';
import {
credentialTypeNames,
DeviceCredentialMQTTBasic,
DeviceCredentials,
DeviceCredentialsType
} from '@shared/models/device.models';
import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
import { DialogComponent } from '@shared/components/dialog.component';
import { Router } from '@angular/router';
@ -83,19 +69,10 @@ export class DeviceCredentialsDialogComponent extends
ngOnInit(): void {
this.deviceCredentialsFormGroup = this.fb.group({
credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
credentialsId: [''],
credentialsValue: [''],
credentialsBasic: this.fb.group({
clientId: ['', [Validators.pattern(/^[A-Za-z0-9]+$/)]],
userName: [''],
password: ['']
}, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
credential: [null]
});
if (this.isReadOnly) {
this.deviceCredentialsFormGroup.disable({emitEvent: false});
} else {
this.registerDisableOnLoadFormControl(this.deviceCredentialsFormGroup.get('credentialsType'));
}
this.loadDeviceCredentials();
}
@ -110,82 +87,20 @@ export class DeviceCredentialsDialogComponent extends
this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe(
(deviceCredentials) => {
this.deviceCredentials = deviceCredentials;
let credentialsValue = deviceCredentials.credentialsValue;
let credentialsBasic = {clientId: null, userName: null, password: null};
if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
credentialsValue = null;
credentialsBasic = JSON.parse(deviceCredentials.credentialsValue) as DeviceCredentialMQTTBasic;
}
this.deviceCredentialsFormGroup.patchValue({
credentialsType: deviceCredentials.credentialsType,
credentialsId: deviceCredentials.credentialsId,
credentialsValue,
credentialsBasic
});
this.updateValidators();
credential: deviceCredentials
}, {emitEvent: false});
}
);
}
credentialsTypeChanged(): void {
this.deviceCredentialsFormGroup.patchValue({
credentialsId: null,
credentialsValue: null,
credentialsBasic: {clientId: '', userName: '', password: ''}
}, {emitEvent: true});
this.updateValidators();
}
updateValidators(): void {
this.hidePassword = true;
const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
switch (crendetialsType) {
case DeviceCredentialsType.ACCESS_TOKEN:
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
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();
this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
break;
case DeviceCredentialsType.MQTT_BASIC:
this.deviceCredentialsFormGroup.get('credentialsBasic').enable();
this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity();
this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
}
}
private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
return (group: FormGroup): ValidationErrors | null => {
if (!controls) {
controls = Object.keys(group.controls);
}
const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
return hasAtLeastOne ? null : {atLeastOne: true};
};
}
cancel(): void {
this.dialogRef.close(null);
}
save(): void {
this.submitted = true;
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
}
delete deviceCredentialsValue.credentialsBasic;
const deviceCredentialsValue = this.deviceCredentialsFormGroup.value.credential;
this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue};
this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe(
(deviceCredentials) => {
@ -193,18 +108,4 @@ export class DeviceCredentialsDialogComponent extends
}
);
}
passwordChanged() {
const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
if (value !== '') {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
}
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
} else {
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
}
}
}

32
ui-ngx/src/app/modules/home/pages/device/devices-table-config.resolver.ts

@ -29,7 +29,7 @@ import {
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
import { EntityAction } from '@home/models/entity/entity-component.models';
import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models';
import { DeviceComponent } from '@modules/home/pages/device/device.component';
import { forkJoin, Observable, of } from 'rxjs';
@ -61,6 +61,8 @@ import {
} from '../../dialogs/add-entities-to-customer-dialog.component';
import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component';
import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
import { BaseData, HasId } from '@shared/models/base-data';
@Injectable()
export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
@ -221,7 +223,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
{
name: this.translate.instant('device.manage-credentials'),
icon: 'security',
isEnabled: (entity) => true,
isEnabled: () => true,
onAction: ($event, entity) => this.manageCredentials($event, entity)
}
);
@ -243,7 +245,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
{
name: this.translate.instant('device.manage-credentials'),
icon: 'security',
isEnabled: (entity) => true,
isEnabled: () => true,
onAction: ($event, entity) => this.manageCredentials($event, entity)
}
);
@ -253,7 +255,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
{
name: this.translate.instant('device.view-credentials'),
icon: 'security',
isEnabled: (entity) => true,
isEnabled: () => true,
onAction: ($event, entity) => this.manageCredentials($event, entity)
}
);
@ -294,14 +296,14 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
name: this.translate.instant('device.add-device-text'),
icon: 'insert_drive_file',
isEnabled: () => true,
onAction: ($event) => this.config.table.addEntity($event)
onAction: ($event) => this.deviceWizard($event)
},
{
name: this.translate.instant('device.import'),
icon: 'file_upload',
isEnabled: () => true,
onAction: ($event) => this.importDevices($event)
}
},
);
}
if (deviceScope === 'customer') {
@ -326,6 +328,23 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
});
}
deviceWizard($event: Event) {
this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>,
boolean>(DeviceWizardDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
entitiesTableConfig: this.config.table.entitiesTableConfig
}
}).afterClosed().subscribe(
(res) => {
if (res) {
this.config.table.updateData();
}
}
);
}
addDevicesToCustomer($event: Event) {
if ($event) {
$event.stopPropagation();
@ -480,5 +499,4 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
}
return false;
}
}

8
ui-ngx/src/app/shared/models/device.models.ts

@ -87,6 +87,14 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
]
)
export const deviceTransportTypeHintMap = new Map<DeviceTransportType, string>(
[
[DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'],
[DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'],
[DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint']
]
);
export const mqttTransportPayloadTypeTranslationMap = new Map<MqttTransportPayloadType, string>(
[
[MqttTransportPayloadType.JSON, 'device-profile.mqtt-device-payload-type-json'],

22
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -54,7 +54,8 @@
"share-via": "Share via {{provider}}",
"continue": "Continue",
"discard-changes": "Discard Changes",
"download": "Download"
"download": "Download",
"next-with-label": "Next: {{label}}"
},
"aggregation": {
"aggregation": "Aggregation",
@ -756,7 +757,16 @@
"search": "Search devices",
"selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected",
"device-configuration": "Device configuration",
"transport-configuration": "Transport configuration"
"transport-configuration": "Transport configuration",
"wizard": {
"device-wizard": "Device Wizard",
"device-details": "Device details",
"new-device-profile": "Create new device profile",
"existing-device-profile": "Select existing device profile",
"specific-configuration": "Specific configuration",
"customer-to-assign-device": "Customer to assign the device",
"add-credential": "Add credential"
}
},
"device-profile": {
"device-profile": "Device profile",
@ -774,6 +784,8 @@
"set-default": "Make device profile default",
"delete": "Delete device profile",
"copyId": "Copy device profile Id",
"new-device-profile-name": "Device profile name",
"new-device-profile-name-required": "Device profile name is required.",
"name": "Name",
"name-required": "Name is required.",
"type": "Profile type",
@ -782,8 +794,11 @@
"transport-type": "Transport type",
"transport-type-required": "Transport type is required.",
"transport-type-default": "Default",
"transport-type-default-hint": "Default transport type",
"transport-type-mqtt": "MQTT",
"transport-type-mqtt-hint": "MQTT transport type",
"transport-type-lwm2m": "LWM2M",
"transport-type-lwm2m-hint": "LWM2M transport type",
"description": "Description",
"default": "Default",
"profile-configuration": "Profile configuration",
@ -814,7 +829,8 @@
"not-valid-multi-character": "Invalid use of a multi-level wildcard character",
"single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic filter level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
"multi-level-wildcards-hint": "<code>[#]</code> can replace the topic filter itself and must be the last symbol of the topic. Ex.: <b>#</b> or <b>v1/devices/me/#</b>.",
"alarm-rules": "Alarm rules ({{count}})",
"alarm-rules": "Alarm rules",
"alarm-rules-with-count": "Alarm rules ({{count}})",
"no-alarm-rules": "No alarm rules configured",
"add-alarm-rule": "Add alarm rule",
"edit-alarm-rule": "Edit alarm rule",

Loading…
Cancel
Save