Commit 35f6f40ca347f2e4dba0fcfff2ed306ba325bfff

Authored by zbeacon
2 parents 93278f72 c7b57ca5

Merge branch 'master' of https://github.com/thingsboard/thingsboard into feature…

…/device-provision-3.2-onlyProfileVersion
Showing 25 changed files with 1028 additions and 216 deletions
... ... @@ -192,10 +192,11 @@ public class DeviceProfileController extends BaseController {
192 192 @RequestParam int page,
193 193 @RequestParam(required = false) String textSearch,
194 194 @RequestParam(required = false) String sortProperty,
195   - @RequestParam(required = false) String sortOrder) throws ThingsboardException {
  195 + @RequestParam(required = false) String sortOrder,
  196 + @RequestParam(required = false) String transportType) throws ThingsboardException {
196 197 try {
197 198 PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
198   - return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink));
  199 + return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink, transportType));
199 200 } catch (Exception e) {
200 201 throw handleException(e);
201 202 }
... ...
... ... @@ -37,7 +37,7 @@ public interface DeviceProfileService {
37 37
38 38 PageData<DeviceProfile> findDeviceProfiles(TenantId tenantId, PageLink pageLink);
39 39
40   - PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink);
  40 + PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
41 41
42 42 DeviceProfile findOrCreateDeviceProfile(TenantId tenantId, String profileName);
43 43
... ...
... ... @@ -32,7 +32,7 @@ public interface DeviceProfileDao extends Dao<DeviceProfile> {
32 32
33 33 PageData<DeviceProfile> findDeviceProfiles(TenantId tenantId, PageLink pageLink);
34 34
35   - PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink);
  35 + PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType);
36 36
37 37 DeviceProfile findDefaultDeviceProfile(TenantId tenantId);
38 38
... ...
... ... @@ -182,11 +182,11 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D
182 182 }
183 183
184 184 @Override
185   - public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink) {
  185 + public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) {
186 186 log.trace("Executing findDeviceProfileInfos tenantId [{}], pageLink [{}]", tenantId, pageLink);
187 187 validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
188 188 Validator.validatePageLink(pageLink);
189   - return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink);
  189 + return deviceProfileDao.findDeviceProfileInfos(tenantId, pageLink, transportType);
190 190 }
191 191
192 192 @Cacheable(cacheNames = DEVICE_PROFILE_CACHE, key = "{#tenantId.id, #name}")
... ...
... ... @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.Query;
21 21 import org.springframework.data.repository.PagingAndSortingRepository;
22 22 import org.springframework.data.repository.query.Param;
23 23 import org.thingsboard.server.common.data.DeviceProfileInfo;
  24 +import org.thingsboard.server.common.data.DeviceTransportType;
24 25 import org.thingsboard.server.dao.model.sql.DeviceProfileEntity;
25 26
26 27 import java.util.UUID;
... ... @@ -45,6 +46,14 @@ public interface DeviceProfileRepository extends PagingAndSortingRepository<Devi
45 46 @Param("textSearch") String textSearch,
46 47 Pageable pageable);
47 48
  49 + @Query("SELECT new org.thingsboard.server.common.data.DeviceProfileInfo(d.id, d.name, d.type, d.transportType) " +
  50 + "FROM DeviceProfileEntity d WHERE " +
  51 + "d.tenantId = :tenantId AND d.transportType = :transportType AND LOWER(d.searchText) LIKE LOWER(CONCAT(:textSearch, '%'))")
  52 + Page<DeviceProfileInfo> findDeviceProfileInfos(@Param("tenantId") UUID tenantId,
  53 + @Param("textSearch") String textSearch,
  54 + @Param("transportType") DeviceTransportType transportType,
  55 + Pageable pageable);
  56 +
48 57 @Query("SELECT d FROM DeviceProfileEntity d " +
49 58 "WHERE d.tenantId = :tenantId AND d.isDefault = true")
50 59 DeviceProfileEntity findByDefaultTrueAndTenantId(@Param("tenantId") UUID tenantId);
... ...
... ... @@ -15,11 +15,13 @@
15 15 */
16 16 package org.thingsboard.server.dao.sql.device;
17 17
  18 +import org.apache.commons.lang.StringUtils;
18 19 import org.springframework.beans.factory.annotation.Autowired;
19 20 import org.springframework.data.repository.CrudRepository;
20 21 import org.springframework.stereotype.Component;
21 22 import org.thingsboard.server.common.data.DeviceProfile;
22 23 import org.thingsboard.server.common.data.DeviceProfileInfo;
  24 +import org.thingsboard.server.common.data.DeviceTransportType;
23 25 import org.thingsboard.server.common.data.id.TenantId;
24 26 import org.thingsboard.server.common.data.page.PageData;
25 27 import org.thingsboard.server.common.data.page.PageLink;
... ... @@ -62,12 +64,21 @@ public class JpaDeviceProfileDao extends JpaAbstractSearchTextDao<DeviceProfileE
62 64 }
63 65
64 66 @Override
65   - public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink) {
66   - return DaoUtil.pageToPageData(
67   - deviceProfileRepository.findDeviceProfileInfos(
68   - tenantId.getId(),
69   - Objects.toString(pageLink.getTextSearch(), ""),
70   - DaoUtil.toPageable(pageLink)));
  67 + public PageData<DeviceProfileInfo> findDeviceProfileInfos(TenantId tenantId, PageLink pageLink, String transportType) {
  68 + if (StringUtils.isNotEmpty(transportType)) {
  69 + return DaoUtil.pageToPageData(
  70 + deviceProfileRepository.findDeviceProfileInfos(
  71 + tenantId.getId(),
  72 + Objects.toString(pageLink.getTextSearch(), ""),
  73 + DeviceTransportType.valueOf(transportType),
  74 + DaoUtil.toPageable(pageLink)));
  75 + } else {
  76 + return DaoUtil.pageToPageData(
  77 + deviceProfileRepository.findDeviceProfileInfos(
  78 + tenantId.getId(),
  79 + Objects.toString(pageLink.getTextSearch(), ""),
  80 + DaoUtil.toPageable(pageLink)));
  81 + }
71 82 }
72 83
73 84 @Override
... ...
... ... @@ -260,7 +260,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest {
260 260 pageLink = new PageLink(17);
261 261 PageData<DeviceProfileInfo> pageData;
262 262 do {
263   - pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink);
  263 + pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink, null);
264 264 loadedDeviceProfileInfos.addAll(pageData.getData());
265 265 if (pageData.hasNext()) {
266 266 pageLink = pageLink.nextPageLink();
... ... @@ -284,7 +284,7 @@ public class BaseDeviceProfileServiceTest extends AbstractServiceTest {
284 284 }
285 285
286 286 pageLink = new PageLink(17);
287   - pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink);
  287 + pageData = deviceProfileService.findDeviceProfileInfos(tenantId, pageLink, null);
288 288 Assert.assertFalse(pageData.hasNext());
289 289 Assert.assertEquals(1, pageData.getTotalElements());
290 290 }
... ...
... ... @@ -20,7 +20,8 @@ import { PageLink } from '@shared/models/page/page-link';
20 20 import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils';
21 21 import { Observable } from 'rxjs';
22 22 import { PageData } from '@shared/models/page/page-data';
23   -import { DeviceProfile, DeviceProfileInfo } from '@shared/models/device.models';
  23 +import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models';
  24 +import { isDefinedAndNotNull } from '@core/utils';
24 25
25 26 @Injectable({
26 27 providedIn: 'root'
... ... @@ -59,8 +60,13 @@ export class DeviceProfileService {
59 60 return this.http.get<DeviceProfileInfo>(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config));
60 61 }
61 62
62   - public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable<PageData<DeviceProfileInfo>> {
63   - return this.http.get<PageData<DeviceProfileInfo>>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config));
  63 + public getDeviceProfileInfos(pageLink: PageLink, transportType?: DeviceTransportType,
  64 + config?: RequestConfig): Observable<PageData<DeviceProfileInfo>> {
  65 + let url = `/api/deviceProfileInfos${pageLink.toQuery()}`;
  66 + if (isDefinedAndNotNull(transportType)) {
  67 + url += `&transportType=${transportType}`;
  68 + }
  69 + return this.http.get<PageData<DeviceProfileInfo>>(url, defaultHttpOptionsFromConfig(config));
64 70 }
65 71
66 72 }
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<section [formGroup]="deviceCredentialsFormGroup">
  19 + <mat-form-field class="mat-block">
  20 + <mat-label translate>device.credentials-type</mat-label>
  21 + <mat-select formControlName="credentialsType">
  22 + <mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
  23 + {{ credentialTypeNamesMap.get(deviceCredentialsType[credentialsType]) }}
  24 + </mat-option>
  25 + </mat-select>
  26 + </mat-form-field>
  27 + <mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.ACCESS_TOKEN"
  28 + class="mat-block">
  29 + <mat-label translate>device.access-token</mat-label>
  30 + <input matInput formControlName="credentialsId" required>
  31 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
  32 + {{ 'device.access-token-required' | translate }}
  33 + </mat-error>
  34 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')">
  35 + {{ 'device.access-token-invalid' | translate }}
  36 + </mat-error>
  37 + </mat-form-field>
  38 + <mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.X509_CERTIFICATE"
  39 + class="mat-block">
  40 + <mat-label translate>device.rsa-key</mat-label>
  41 + <textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
  42 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
  43 + {{ 'device.rsa-key-required' | translate }}
  44 + </mat-error>
  45 + </mat-form-field>
  46 + <section *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.MQTT_BASIC" formGroupName="credentialsBasic">
  47 + <mat-form-field class="mat-block">
  48 + <mat-label translate>device.client-id</mat-label>
  49 + <input matInput formControlName="clientId">
  50 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.clientId').hasError('pattern')">
  51 + {{ 'device.client-id-pattern' | translate }}
  52 + </mat-error>
  53 + </mat-form-field>
  54 + <mat-form-field class="mat-block">
  55 + <mat-label translate>device.user-name</mat-label>
  56 + <input matInput formControlName="userName" [required]="!!deviceCredentialsFormGroup.get('credentialsBasic.password').value">
  57 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.userName').hasError('required')">
  58 + {{ 'device.user-name-required' | translate }}
  59 + </mat-error>
  60 + </mat-form-field>
  61 + <mat-form-field class="mat-block">
  62 + <mat-label translate>device.password</mat-label>
  63 + <input matInput formControlName="password"
  64 + autocomplete="new-password"
  65 + (ngModelChange)="passwordChanged()"
  66 + [type]="hidePassword ? 'password' : 'text'">
  67 + <button mat-icon-button matSuffix type="button"
  68 + (click)="hidePassword = !hidePassword"
  69 + [attr.aria-pressed]="hidePassword">
  70 + <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
  71 + </button>
  72 + </mat-form-field>
  73 + <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne')">
  74 + {{ 'device.client-id-or-user-name-necessary' | translate }}
  75 + </mat-error>
  76 + </section>
  77 +</section>
... ...
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core';
  18 +import {
  19 + ControlValueAccessor,
  20 + FormBuilder,
  21 + FormControl,
  22 + FormGroup,
  23 + NG_VALIDATORS,
  24 + NG_VALUE_ACCESSOR,
  25 + ValidationErrors,
  26 + Validator,
  27 + ValidatorFn,
  28 + Validators
  29 +} from '@angular/forms';
  30 +import {
  31 + credentialTypeNames,
  32 + DeviceCredentialMQTTBasic,
  33 + DeviceCredentials,
  34 + DeviceCredentialsType
  35 +} from '@shared/models/device.models';
  36 +import { Subscription } from 'rxjs';
  37 +import { isDefinedAndNotNull } from '@core/utils';
  38 +import { distinctUntilChanged } from 'rxjs/operators';
  39 +
  40 +@Component({
  41 + selector: 'tb-device-credentials',
  42 + templateUrl: './device-credentials.component.html',
  43 + providers: [
  44 + {
  45 + provide: NG_VALUE_ACCESSOR,
  46 + useExisting: forwardRef(() => DeviceCredentialsComponent),
  47 + multi: true
  48 + },
  49 + {
  50 + provide: NG_VALIDATORS,
  51 + useExisting: forwardRef(() => DeviceCredentialsComponent),
  52 + multi: true,
  53 + }],
  54 + styleUrls: []
  55 +})
  56 +export class DeviceCredentialsComponent implements ControlValueAccessor, OnInit, Validator, OnDestroy {
  57 +
  58 + deviceCredentialsFormGroup: FormGroup;
  59 +
  60 + subscriptions: Subscription[] = [];
  61 +
  62 + @Input()
  63 + disabled: boolean;
  64 +
  65 + deviceCredentials: DeviceCredentials = null;
  66 +
  67 + submitted = false;
  68 +
  69 + deviceCredentialsType = DeviceCredentialsType;
  70 +
  71 + credentialsTypes = Object.keys(DeviceCredentialsType);
  72 +
  73 + credentialTypeNamesMap = credentialTypeNames;
  74 +
  75 + hidePassword = true;
  76 +
  77 + private propagateChange = (v: any) => {};
  78 +
  79 + constructor(public fb: FormBuilder) {
  80 + this.deviceCredentialsFormGroup = this.fb.group({
  81 + credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
  82 + credentialsId: [null],
  83 + credentialsValue: [null],
  84 + credentialsBasic: this.fb.group({
  85 + clientId: [null, [Validators.pattern(/^[A-Za-z0-9]+$/)]],
  86 + userName: [null],
  87 + password: [null]
  88 + }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
  89 + });
  90 + this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
  91 + this.subscriptions.push(
  92 + this.deviceCredentialsFormGroup.valueChanges.pipe(distinctUntilChanged()).subscribe(() => {
  93 + this.updateView();
  94 + })
  95 + );
  96 + this.subscriptions.push(
  97 + this.deviceCredentialsFormGroup.get('credentialsType').valueChanges.subscribe(() => {
  98 + this.credentialsTypeChanged();
  99 + })
  100 + );
  101 + }
  102 +
  103 + ngOnInit(): void {
  104 + if (this.disabled) {
  105 + this.deviceCredentialsFormGroup.disable({emitEvent: false});
  106 + }
  107 + }
  108 +
  109 + ngOnDestroy() {
  110 + this.subscriptions.forEach(s => s.unsubscribe());
  111 + }
  112 +
  113 + writeValue(value: DeviceCredentials | null): void {
  114 + if (isDefinedAndNotNull(value)) {
  115 + this.deviceCredentials = value;
  116 + let credentialsBasic = {clientId: null, userName: null, password: null};
  117 + let credentialsValue = null;
  118 + if (value.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
  119 + credentialsBasic = JSON.parse(value.credentialsValue) as DeviceCredentialMQTTBasic;
  120 + } else {
  121 + credentialsValue = value.credentialsValue;
  122 + }
  123 + this.deviceCredentialsFormGroup.patchValue({
  124 + credentialsType: value.credentialsType,
  125 + credentialsId: value.credentialsId,
  126 + credentialsValue,
  127 + credentialsBasic
  128 + }, {emitEvent: false});
  129 + this.updateValidators();
  130 + }
  131 + }
  132 +
  133 + updateView() {
  134 + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
  135 + if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
  136 + deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
  137 + }
  138 + delete deviceCredentialsValue.credentialsBasic;
  139 + this.propagateChange(deviceCredentialsValue);
  140 + }
  141 +
  142 + registerOnChange(fn: any): void {
  143 + this.propagateChange = fn;
  144 + }
  145 +
  146 + registerOnTouched(fn: any): void {}
  147 +
  148 + setDisabledState(isDisabled: boolean): void {
  149 + this.disabled = isDisabled;
  150 + if (this.disabled) {
  151 + this.deviceCredentialsFormGroup.disable({emitEvent: false});
  152 + } else {
  153 + this.deviceCredentialsFormGroup.enable({emitEvent: false});
  154 + this.updateValidators();
  155 + }
  156 + }
  157 +
  158 + public validate(c: FormControl) {
  159 + return this.deviceCredentialsFormGroup.valid ? null : {
  160 + deviceCredentials: {
  161 + valid: false,
  162 + },
  163 + };
  164 + }
  165 +
  166 + credentialsTypeChanged(): void {
  167 + this.deviceCredentialsFormGroup.patchValue({
  168 + credentialsId: null,
  169 + credentialsValue: null,
  170 + credentialsBasic: {clientId: '', userName: '', password: ''}
  171 + });
  172 + this.updateValidators();
  173 + }
  174 +
  175 + updateValidators(): void {
  176 + this.hidePassword = true;
  177 + const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
  178 + switch (crendetialsType) {
  179 + case DeviceCredentialsType.ACCESS_TOKEN:
  180 + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
  181 + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
  182 + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
  183 + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
  184 + this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false});
  185 + break;
  186 + case DeviceCredentialsType.X509_CERTIFICATE:
  187 + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]);
  188 + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
  189 + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
  190 + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
  191 + this.deviceCredentialsFormGroup.get('credentialsBasic').disable({emitEvent: false});
  192 + break;
  193 + case DeviceCredentialsType.MQTT_BASIC:
  194 + this.deviceCredentialsFormGroup.get('credentialsBasic').enable({emitEvent: false});
  195 + this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity({emitEvent: false});
  196 + this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
  197 + this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity({emitEvent: false});
  198 + this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
  199 + this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity({emitEvent: false});
  200 + }
  201 + }
  202 +
  203 + private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
  204 + return (group: FormGroup): ValidationErrors | null => {
  205 + if (!controls) {
  206 + controls = Object.keys(group.controls);
  207 + }
  208 + const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
  209 +
  210 + return hasAtLeastOne ? null : {atLeastOne: true};
  211 + };
  212 + }
  213 +
  214 + passwordChanged() {
  215 + const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
  216 + if (value !== '') {
  217 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
  218 + if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
  219 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
  220 + }
  221 + } else {
  222 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
  223 + }
  224 + this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity({
  225 + emitEvent: false,
  226 + onlySelf: true
  227 + });
  228 + }
  229 +}
... ...
... ... @@ -109,6 +109,8 @@ import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-di
109 109 import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component';
110 110 import { DeviceProfileProvisionConfigurationComponent } from "./profile/device-profile-provision-configuration.component";
111 111 import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component';
  112 +import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component';
  113 +import { DeviceCredentialsComponent } from './device/device-credentials.component';
112 114
113 115 @NgModule({
114 116 declarations:
... ... @@ -200,7 +202,9 @@ import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component
200 202 AddDeviceProfileDialogComponent,
201 203 RuleChainAutocompleteComponent,
202 204 DeviceProfileProvisionConfigurationComponent,
203   - AlarmScheduleComponent
  205 + AlarmScheduleComponent,
  206 + DeviceWizardDialogComponent,
  207 + DeviceCredentialsComponent
204 208 ],
205 209 imports: [
206 210 CommonModule,
... ... @@ -280,6 +284,8 @@ import { AlarmScheduleComponent } from './profile/alarm/alarm-schedule.component
280 284 DeviceProfileDialogComponent,
281 285 AddDeviceProfileDialogComponent,
282 286 RuleChainAutocompleteComponent,
  287 + DeviceWizardDialogComponent,
  288 + DeviceCredentialsComponent,
283 289 DeviceProfileProvisionConfigurationComponent,
284 290 AlarmScheduleComponent
285 291 ],
... ...
... ... @@ -85,7 +85,7 @@
85 85 </mat-step>
86 86 <mat-step [stepControl]="alarmRulesFormGroup">
87 87 <form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
88   - <ng-template matStepLabel>{{'device-profile.alarm-rules' | translate:
  88 + <ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
89 89 {count: alarmRulesFormGroup.get('alarms').value ?
90 90 alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
91 91 <tb-device-profile-alarms
... ...
... ... @@ -46,6 +46,7 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id';
46 46
47 47 export interface AddDeviceProfileDialogData {
48 48 deviceProfileName: string;
  49 + transportType: DeviceTransportType;
49 50 }
50 51
51 52 @Component({
... ... @@ -99,7 +100,7 @@ export class AddDeviceProfileDialogComponent extends
99 100 );
100 101 this.transportConfigFormGroup = this.fb.group(
101 102 {
102   - transportType: [DeviceTransportType.DEFAULT, [Validators.required]],
  103 + transportType: [data.transportType ? data.transportType : DeviceTransportType.DEFAULT, [Validators.required]],
103 104 transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT),
104 105 [Validators.required]]
105 106 }
... ...
... ... @@ -48,7 +48,7 @@
48 48 </mat-option>
49 49 <mat-option *ngIf="!(filteredDeviceProfiles | async)?.length" [value]="null" class="tb-not-found">
50 50 <div class="tb-not-found-content" (click)="$event.stopPropagation()">
51   - <div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty">
  51 + <div *ngIf="!textIsNotEmpty(searchText) || !addNewProfile; else searchNotEmpty">
52 52 <span translate>device-profile.no-device-profiles-found</span>
53 53 </div>
54 54 <ng-template #searchNotEmpty>
... ... @@ -56,10 +56,10 @@
56 56 {{ translate.get('device-profile.no-device-profiles-matching',
57 57 {entity: truncate.transform(searchText, true, 6, &apos;...&apos;)}) | async }}
58 58 </span>
  59 + <span>
  60 + <a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
  61 + </span>
59 62 </ng-template>
60   - <span>
61   - <a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
62   - </span>
63 63 </div>
64 64 </mat-option>
65 65 </mat-autocomplete>
... ...
... ... @@ -19,9 +19,10 @@ import {
19 19 ElementRef,
20 20 EventEmitter,
21 21 forwardRef,
22   - Input, NgZone,
  22 + Input,
  23 + NgZone, OnChanges,
23 24 OnInit,
24   - Output,
  25 + Output, SimpleChanges,
25 26 ViewChild
26 27 } from '@angular/core';
27 28 import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
... ... @@ -38,14 +39,7 @@ import { TruncatePipe } from '@shared//pipe/truncate.pipe';
38 39 import { ENTER } from '@angular/cdk/keycodes';
39 40 import { MatDialog } from '@angular/material/dialog';
40 41 import { DeviceProfileId } from '@shared/models/id/device-profile-id';
41   -import {
42   - createDeviceProfileConfiguration,
43   - createDeviceProfileTransportConfiguration,
44   - DeviceProfile,
45   - DeviceProfileInfo,
46   - DeviceProfileType,
47   - DeviceTransportType
48   -} from '@shared/models/device.models';
  42 +import { DeviceProfile, DeviceProfileInfo, DeviceProfileType, DeviceTransportType } from '@shared/models/device.models';
49 43 import { DeviceProfileService } from '@core/http/device-profile.service';
50 44 import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component';
51 45 import { MatAutocomplete } from '@angular/material/autocomplete';
... ... @@ -61,7 +55,7 @@ import { AddDeviceProfileDialogComponent, AddDeviceProfileDialogData } from './a
61 55 multi: true
62 56 }]
63 57 })
64   -export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit {
  58 +export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit, OnChanges {
65 59
66 60 selectDeviceProfileFormGroup: FormGroup;
67 61
... ... @@ -76,6 +70,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
76 70 @Input()
77 71 editProfileEnabled = true;
78 72
  73 + @Input()
  74 + addNewProfile = true;
  75 +
  76 + @Input()
  77 + transportType: DeviceTransportType = null;
  78 +
79 79 private requiredValue: boolean;
80 80 get required(): boolean {
81 81 return this.requiredValue;
... ... @@ -168,11 +168,22 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
168 168 );
169 169 }
170 170
  171 + ngOnChanges(changes: SimpleChanges): void {
  172 + for (const propName of Object.keys(changes)) {
  173 + const change = changes[propName];
  174 + if (!change.firstChange && change.currentValue !== change.previousValue) {
  175 + if (propName === 'transportType') {
  176 + this.writeValue(null);
  177 + }
  178 + }
  179 + }
  180 + }
  181 +
171 182 selectDefaultDeviceProfileIfNeeded(): void {
172 183 if (this.selectDefaultProfile && !this.modelValue) {
173 184 this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe(
174 185 (profile) => {
175   - if (profile) {
  186 + if (profile && !this.transportType || (profile.transportType === this.transportType)) {
176 187 this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false});
177 188 this.updateView(profile);
178 189 }
... ... @@ -183,6 +194,11 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
183 194
184 195 setDisabledState(isDisabled: boolean): void {
185 196 this.disabled = isDisabled;
  197 + if (this.disabled) {
  198 + this.selectDeviceProfileFormGroup.disable();
  199 + } else {
  200 + this.selectDeviceProfileFormGroup.enable();
  201 + }
186 202 }
187 203
188 204 writeValue(value: DeviceProfileId | null): void {
... ... @@ -244,7 +260,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
244 260 property: 'name',
245 261 direction: Direction.ASC
246 262 });
247   - return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe(
  263 + return this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).pipe(
248 264 map(pageData => {
249 265 let data = pageData.data;
250 266 if (this.displayAllOnEmpty) {
... ... @@ -280,9 +296,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
280 296 createDeviceProfile($event: Event, profileName: string) {
281 297 $event.preventDefault();
282 298 const deviceProfile: DeviceProfile = {
283   - name: profileName
  299 + name: profileName,
  300 + transportType: this.transportType
284 301 } as DeviceProfile;
285   - this.openDeviceProfileDialog(deviceProfile, true);
  302 + if (this.addNewProfile) {
  303 + this.openDeviceProfileDialog(deviceProfile, true);
  304 + }
286 305 }
287 306
288 307 editDeviceProfile($event: Event) {
... ... @@ -312,7 +331,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor,
312 331 disableClose: true,
313 332 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
314 333 data: {
315   - deviceProfileName: deviceProfile.name
  334 + deviceProfileName: deviceProfile.name,
  335 + transportType: deviceProfile.transportType
316 336 }
317 337 }).afterClosed();
318 338 }
... ...
... ... @@ -42,7 +42,7 @@
42 42 <mat-expansion-panel [expanded]="true">
43 43 <mat-expansion-panel-header>
44 44 <mat-panel-title>
45   - <div>{{'device-profile.alarm-rules' | translate:
  45 + <div>{{'device-profile.alarm-rules-with-count' | translate:
46 46 {count: deviceProfileDataFormGroup.get('alarms').value ?
47 47 deviceProfileDataFormGroup.get('alarms').value.length : 0} }}</div>
48 48 </mat-panel-title>
... ...
  1 +<!--
  2 +
  3 + Copyright © 2016-2020 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div>
  19 + <mat-toolbar color="primary">
  20 + <h2 translate>device.add-device-text</h2>
  21 + <span fxFlex></span>
  22 + <button mat-icon-button
  23 + (click)="cancel()"
  24 + type="button">
  25 + <mat-icon class="material-icons">close</mat-icon>
  26 + </button>
  27 + </mat-toolbar>
  28 + <mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
  29 + </mat-progress-bar>
  30 + <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
  31 + <div mat-dialog-content>
  32 + <mat-horizontal-stepper [linear]="true" [labelPosition]="labelPosition" #addDeviceWizardStepper (selectionChange)="changeStep($event)">
  33 + <ng-template matStepperIcon="edit">
  34 + <mat-icon>check</mat-icon>
  35 + </ng-template>
  36 + <mat-step [stepControl]="deviceWizardFormGroup">
  37 + <form [formGroup]="deviceWizardFormGroup" style="padding-bottom: 16px;">
  38 + <ng-template matStepLabel>{{ 'device.wizard.device-details' | translate}}</ng-template>
  39 + <fieldset [disabled]="isLoading$ | async">
  40 + <mat-form-field class="mat-block">
  41 + <mat-label translate>device.name</mat-label>
  42 + <input matInput formControlName="name" required>
  43 + <mat-error *ngIf="deviceWizardFormGroup.get('name').hasError('required')">
  44 + {{ 'device.name-required' | translate }}
  45 + </mat-error>
  46 + </mat-form-field>
  47 + <mat-form-field class="mat-block">
  48 + <mat-label translate>device.label</mat-label>
  49 + <input matInput formControlName="label">
  50 + </mat-form-field>
  51 + <mat-form-field class="mat-block" style="padding-bottom: 14px;">
  52 + <mat-label translate>device-profile.transport-type</mat-label>
  53 + <mat-select formControlName="transportType" required>
  54 + <mat-option *ngFor="let type of deviceTransportTypes" [value]="type">
  55 + {{deviceTransportTypeTranslations.get(type) | translate}}
  56 + </mat-option>
  57 + </mat-select>
  58 + <mat-hint *ngIf="deviceWizardFormGroup.get('transportType').value">
  59 + {{deviceTransportTypeHints.get(deviceWizardFormGroup.get('transportType').value) | translate}}
  60 + </mat-hint>
  61 + <mat-error *ngIf="deviceWizardFormGroup.get('transportType').hasError('required')">
  62 + {{ 'device-profile.transport-type-required' | translate }}
  63 + </mat-error>
  64 + </mat-form-field>
  65 + <div fxLayout="row" fxLayoutGap="16px">
  66 + <mat-radio-group fxLayout="column" formControlName="addProfileType" fxLayoutAlign="space-around">
  67 + <mat-radio-button [value]="0" color="primary">
  68 + <span translate>device.wizard.existing-device-profile</span>
  69 + </mat-radio-button>
  70 + <mat-radio-button [value]="1" color="primary">
  71 + <span translate>device.wizard.new-device-profile</span>
  72 + </mat-radio-button>
  73 + </mat-radio-group>
  74 + <div fxLayout="column">
  75 + <tb-device-profile-autocomplete
  76 + [required]="!createProfile"
  77 + [transportType]="deviceWizardFormGroup.get('transportType').value"
  78 + formControlName="deviceProfileId"
  79 + (deviceProfileChanged)="$event?.transportType ? deviceWizardFormGroup.get('transportType').patchValue($event?.transportType) : {}"
  80 + [addNewProfile]="false"
  81 + [selectDefaultProfile]="true"
  82 + [editProfileEnabled]="false">
  83 + </tb-device-profile-autocomplete>
  84 + <mat-form-field fxFlex class="mat-block">
  85 + <mat-label translate>device-profile.new-device-profile-name</mat-label>
  86 + <input matInput formControlName="newDeviceProfileTitle"
  87 + [required]="createProfile">
  88 + <mat-error *ngIf="deviceWizardFormGroup.get('newDeviceProfileTitle').hasError('required')">
  89 + {{ 'device-profile.new-device-profile-name-required' | translate }}
  90 + </mat-error>
  91 + </mat-form-field>
  92 + </div>
  93 + </div>
  94 + <mat-checkbox formControlName="gateway" style="padding-bottom: 16px;">
  95 + {{ 'device.is-gateway' | translate }}
  96 + </mat-checkbox>
  97 + <mat-form-field class="mat-block">
  98 + <mat-label translate>device.description</mat-label>
  99 + <textarea matInput formControlName="description" rows="2"></textarea>
  100 + </mat-form-field>
  101 + </fieldset>
  102 + </form>
  103 + </mat-step>
  104 + <mat-step [stepControl]="transportConfigFormGroup" *ngIf="createTransportConfiguration">
  105 + <form [formGroup]="transportConfigFormGroup" style="padding-bottom: 16px;">
  106 + <ng-template matStepLabel>{{ 'device-profile.transport-configuration' | translate }}</ng-template>
  107 + <tb-device-profile-transport-configuration
  108 + formControlName="transportConfiguration"
  109 + required>
  110 + </tb-device-profile-transport-configuration>
  111 + </form>
  112 + </mat-step>
  113 + <mat-step [stepControl]="alarmRulesFormGroup" [optional]="true" *ngIf="createProfile">
  114 + <form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;">
  115 + <ng-template matStepLabel>{{'device-profile.alarm-rules-with-count' | translate:
  116 + {count: alarmRulesFormGroup.get('alarms').value ?
  117 + alarmRulesFormGroup.get('alarms').value.length : 0} }}</ng-template>
  118 + <tb-device-profile-alarms
  119 + formControlName="alarms">
  120 + </tb-device-profile-alarms>
  121 + </form>
  122 + </mat-step>
  123 + <mat-step [stepControl]="credentialsFormGroup" [optional]="true">
  124 + <ng-template matStepLabel>{{ 'device.credentials' | translate }}</ng-template>
  125 + <form [formGroup]="credentialsFormGroup" style="padding-bottom: 16px;">
  126 + <mat-checkbox style="padding-bottom: 16px;" formControlName="setCredential">{{ 'device.wizard.add-credential' | translate }}</mat-checkbox>
  127 + <tb-device-credentials
  128 + [fxShow]="credentialsFormGroup.get('setCredential').value"
  129 + formControlName="credential">
  130 + </tb-device-credentials>
  131 + </form>
  132 + </mat-step>
  133 + <mat-step [stepControl]="customerFormGroup" [optional]="true">
  134 + <ng-template matStepLabel>{{ 'customer.customer' | translate }}</ng-template>
  135 + <form [formGroup]="customerFormGroup" style="padding-bottom: 16px;">
  136 + <tb-entity-autocomplete
  137 + formControlName="customerId"
  138 + labelText="device.wizard.customer-to-assign-device"
  139 + [entityType]="entityType.CUSTOMER">
  140 + </tb-entity-autocomplete>
  141 + </form>
  142 + </mat-step>
  143 + </mat-horizontal-stepper>
  144 + </div>
  145 + <div mat-dialog-actions fxLayout="column" fxLayoutAlign="start wrap" fxLayoutGap="8px" style="height: 100px;">
  146 + <div fxFlex fxLayout="row" fxLayoutAlign="end">
  147 + <button mat-raised-button
  148 + *ngIf="showNext"
  149 + [disabled]="(isLoading$ | async)"
  150 + (click)="nextStep()">{{ 'action.next-with-label' | translate:{label: (getFormLabel(this.selectedIndex+1) | translate)} }}</button>
  151 + </div>
  152 + <div fxFlex fxLayout="row">
  153 + <button mat-button
  154 + color="primary"
  155 + [disabled]="(isLoading$ | async)"
  156 + (click)="cancel()">{{ 'action.cancel' | translate }}</button>
  157 + <span fxFlex></span>
  158 + <div fxLayout="row wrap" fxLayoutGap="8px">
  159 + <button mat-raised-button *ngIf="selectedIndex > 0"
  160 + [disabled]="(isLoading$ | async)"
  161 + (click)="previousStep()">{{ 'action.back' | translate }}</button>
  162 + <button mat-raised-button
  163 + [disabled]="(isLoading$ | async)"
  164 + color="primary"
  165 + (click)="add()">{{ 'action.add' | translate }}</button>
  166 + </div>
  167 + </div>
  168 + </div>
  169 +</div>
... ...
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +@import "../../../../../scss/constants";
  18 +
  19 +:host-context(.tb-fullscreen-dialog .mat-dialog-container) {
  20 + @media #{$mat-lt-sm} {
  21 + .mat-dialog-content {
  22 + max-height: 75vh;
  23 + }
  24 + }
  25 +}
  26 +
  27 +:host ::ng-deep {
  28 + .mat-dialog-content {
  29 + display: flex;
  30 + flex-direction: column;
  31 + height: 100%;
  32 +
  33 + .mat-stepper-horizontal {
  34 + display: flex;
  35 + flex-direction: column;
  36 + height: 100%;
  37 + overflow: hidden;
  38 + @media #{$mat-lt-sm} {
  39 + .mat-step-label {
  40 + white-space: normal;
  41 + overflow: visible;
  42 + .mat-step-text-label {
  43 + overflow: visible;
  44 + }
  45 + }
  46 + }
  47 + .mat-horizontal-content-container {
  48 + height: 450px;
  49 + max-height: 100%;
  50 + width: 100%;;
  51 + overflow-y: auto;
  52 + @media #{$mat-gt-sm} {
  53 + min-width: 800px;
  54 + }
  55 + }
  56 + .mat-horizontal-stepper-content[aria-expanded=true] {
  57 + height: 100%;
  58 + form {
  59 + height: 100%;
  60 + }
  61 + }
  62 + }
  63 + }
  64 +}
... ...
  1 +///
  2 +/// Copyright © 2016-2020 The Thingsboard Authors
  3 +///
  4 +/// Licensed under the Apache License, Version 2.0 (the "License");
  5 +/// you may not use this file except in compliance with the License.
  6 +/// You may obtain a copy of the License at
  7 +///
  8 +/// http://www.apache.org/licenses/LICENSE-2.0
  9 +///
  10 +/// Unless required by applicable law or agreed to in writing, software
  11 +/// distributed under the License is distributed on an "AS IS" BASIS,
  12 +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 +/// See the License for the specific language governing permissions and
  14 +/// limitations under the License.
  15 +///
  16 +
  17 +import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core';
  18 +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
  19 +import { Store } from '@ngrx/store';
  20 +import { AppState } from '@core/core.state';
  21 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms';
  22 +import { DialogComponent } from '@shared/components/dialog.component';
  23 +import { Router } from '@angular/router';
  24 +import {
  25 + createDeviceProfileConfiguration,
  26 + createDeviceProfileTransportConfiguration,
  27 + DeviceProfile,
  28 + DeviceProfileType,
  29 + DeviceTransportType, deviceTransportTypeHintMap,
  30 + deviceTransportTypeTranslationMap
  31 +} from '@shared/models/device.models';
  32 +import { MatHorizontalStepper } from '@angular/material/stepper';
  33 +import { AddEntityDialogData } from '@home/models/entity/entity-component.models';
  34 +import { BaseData, HasId } from '@shared/models/base-data';
  35 +import { EntityType } from '@shared/models/entity-type.models';
  36 +import { DeviceProfileService } from '@core/http/device-profile.service';
  37 +import { EntityId } from '@shared/models/id/entity-id';
  38 +import { Observable, of, Subscription } from 'rxjs';
  39 +import { map, mergeMap, tap } from 'rxjs/operators';
  40 +import { DeviceService } from '@core/http/device.service';
  41 +import { ErrorStateMatcher } from '@angular/material/core';
  42 +import { StepperSelectionEvent } from '@angular/cdk/stepper';
  43 +import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout';
  44 +import { MediaBreakpoints } from '@shared/models/constants';
  45 +
  46 +@Component({
  47 + selector: 'tb-device-wizard',
  48 + templateUrl: './device-wizard-dialog.component.html',
  49 + providers: [],
  50 + styleUrls: ['./device-wizard-dialog.component.scss']
  51 +})
  52 +export class DeviceWizardDialogComponent extends
  53 + DialogComponent<DeviceWizardDialogComponent, boolean> implements OnDestroy, ErrorStateMatcher {
  54 +
  55 + @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper;
  56 +
  57 + selectedIndex = 0;
  58 +
  59 + showNext = true;
  60 +
  61 + createProfile = false;
  62 + createTransportConfiguration = false;
  63 +
  64 + entityType = EntityType;
  65 +
  66 + deviceTransportTypes = Object.keys(DeviceTransportType);
  67 +
  68 + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap;
  69 +
  70 + deviceTransportTypeHints = deviceTransportTypeHintMap;
  71 +
  72 + deviceWizardFormGroup: FormGroup;
  73 +
  74 + transportConfigFormGroup: FormGroup;
  75 +
  76 + alarmRulesFormGroup: FormGroup;
  77 +
  78 + credentialsFormGroup: FormGroup;
  79 +
  80 + customerFormGroup: FormGroup;
  81 +
  82 + labelPosition = 'end';
  83 +
  84 + private subscriptions: Subscription[] = [];
  85 +
  86 + constructor(protected store: Store<AppState>,
  87 + protected router: Router,
  88 + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<EntityId>>,
  89 + @SkipSelf() private errorStateMatcher: ErrorStateMatcher,
  90 + public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>,
  91 + private deviceProfileService: DeviceProfileService,
  92 + private deviceService: DeviceService,
  93 + private breakpointObserver: BreakpointObserver,
  94 + private fb: FormBuilder) {
  95 + super(store, router, dialogRef);
  96 + this.deviceWizardFormGroup = this.fb.group({
  97 + name: ['', Validators.required],
  98 + label: [''],
  99 + gateway: [false],
  100 + transportType: [DeviceTransportType.DEFAULT, Validators.required],
  101 + addProfileType: [0],
  102 + deviceProfileId: [null, Validators.required],
  103 + newDeviceProfileTitle: [{value: null, disabled: true}],
  104 + description: ['']
  105 + }
  106 + );
  107 +
  108 + this.subscriptions.push(this.deviceWizardFormGroup.get('addProfileType').valueChanges.subscribe(
  109 + (addProfileType: number) => {
  110 + if (addProfileType === 0) {
  111 + this.deviceWizardFormGroup.get('deviceProfileId').setValidators([Validators.required]);
  112 + this.deviceWizardFormGroup.get('deviceProfileId').enable();
  113 + this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators(null);
  114 + this.deviceWizardFormGroup.get('newDeviceProfileTitle').disable();
  115 + this.deviceWizardFormGroup.updateValueAndValidity();
  116 + this.createProfile = false;
  117 + this.createTransportConfiguration = false;
  118 + } else {
  119 + this.deviceWizardFormGroup.get('deviceProfileId').setValidators(null);
  120 + this.deviceWizardFormGroup.get('deviceProfileId').disable();
  121 + this.deviceWizardFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]);
  122 + this.deviceWizardFormGroup.get('newDeviceProfileTitle').enable();
  123 + this.deviceWizardFormGroup.updateValueAndValidity();
  124 + this.createProfile = true;
  125 + this.createTransportConfiguration = this.deviceWizardFormGroup.get('transportType').value &&
  126 + DeviceTransportType.DEFAULT !== this.deviceWizardFormGroup.get('transportType').value;
  127 + }
  128 + }
  129 + ));
  130 +
  131 + this.transportConfigFormGroup = this.fb.group(
  132 + {
  133 + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required]
  134 + }
  135 + );
  136 + this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => {
  137 + this.deviceProfileTransportTypeChanged(transportType);
  138 + }));
  139 +
  140 + this.alarmRulesFormGroup = this.fb.group({
  141 + alarms: [null]
  142 + }
  143 + );
  144 +
  145 + this.credentialsFormGroup = this.fb.group({
  146 + setCredential: [false],
  147 + credential: [{value: null, disabled: true}]
  148 + }
  149 + );
  150 +
  151 + this.subscriptions.push(this.credentialsFormGroup.get('setCredential').valueChanges.subscribe((value) => {
  152 + if (value) {
  153 + this.credentialsFormGroup.get('credential').enable();
  154 + } else {
  155 + this.credentialsFormGroup.get('credential').disable();
  156 + }
  157 + }));
  158 +
  159 + this.customerFormGroup = this.fb.group({
  160 + customerId: [null]
  161 + }
  162 + );
  163 +
  164 + this.labelPosition = this.breakpointObserver.isMatched(MediaBreakpoints['gt-sm']) ? 'end' : 'bottom';
  165 +
  166 + this.subscriptions.push(this.breakpointObserver
  167 + .observe(MediaBreakpoints['gt-sm'])
  168 + .subscribe((state: BreakpointState) => {
  169 + if (state.matches) {
  170 + this.labelPosition = 'end';
  171 + } else {
  172 + this.labelPosition = 'bottom';
  173 + }
  174 + }
  175 + ));
  176 + }
  177 +
  178 + ngOnDestroy() {
  179 + super.ngOnDestroy();
  180 + this.subscriptions.forEach(s => s.unsubscribe());
  181 + }
  182 +
  183 + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
  184 + const originalErrorState = this.errorStateMatcher.isErrorState(control, form);
  185 + const customErrorState = !!(control && control.invalid);
  186 + return originalErrorState || customErrorState;
  187 + }
  188 +
  189 + cancel(): void {
  190 + this.dialogRef.close(null);
  191 + }
  192 +
  193 + previousStep(): void {
  194 + this.addDeviceWizardStepper.previous();
  195 + }
  196 +
  197 + nextStep(): void {
  198 + this.addDeviceWizardStepper.next();
  199 + }
  200 +
  201 + getFormLabel(index: number): string {
  202 + if (index > 0) {
  203 + if (!this.createProfile) {
  204 + index += 2;
  205 + } else if (!this.createTransportConfiguration) {
  206 + index += 1;
  207 + }
  208 + }
  209 + switch (index) {
  210 + case 0:
  211 + return 'device.wizard.device-details';
  212 + case 1:
  213 + return 'device-profile.transport-configuration';
  214 + case 2:
  215 + return 'device-profile.alarm-rules';
  216 + case 3:
  217 + return 'device.credentials';
  218 + case 4:
  219 + return 'customer.customer';
  220 + }
  221 + }
  222 +
  223 + get maxStepperIndex(): number {
  224 + return this.addDeviceWizardStepper?._steps?.length - 1;
  225 + }
  226 +
  227 + private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void {
  228 + this.transportConfigFormGroup.patchValue(
  229 + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)});
  230 + this.createTransportConfiguration = this.createProfile && deviceTransportType &&
  231 + DeviceTransportType.DEFAULT !== deviceTransportType;
  232 + }
  233 +
  234 + add(): void {
  235 + if (this.allValid()) {
  236 + this.createDeviceProfile().pipe(
  237 + mergeMap(profileId => this.createDevice(profileId)),
  238 + mergeMap(device => this.saveCredentials(device))
  239 + ).subscribe(
  240 + (created) => {
  241 + this.dialogRef.close(created);
  242 + }
  243 + );
  244 + }
  245 + }
  246 +
  247 + private createDeviceProfile(): Observable<EntityId> {
  248 + if (this.deviceWizardFormGroup.get('addProfileType').value) {
  249 + const deviceProfile: DeviceProfile = {
  250 + name: this.deviceWizardFormGroup.get('newDeviceProfileTitle').value,
  251 + type: DeviceProfileType.DEFAULT,
  252 + transportType: this.deviceWizardFormGroup.get('transportType').value,
  253 + profileData: {
  254 + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT),
  255 + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value,
  256 + alarms: this.alarmRulesFormGroup.get('alarms').value
  257 + }
  258 + };
  259 + return this.deviceProfileService.saveDeviceProfile(deviceProfile).pipe(
  260 + map(profile => profile.id),
  261 + tap((profileId) => {
  262 + this.deviceWizardFormGroup.patchValue({
  263 + deviceProfileId: profileId,
  264 + addProfileType: 0
  265 + });
  266 + })
  267 + );
  268 + } else {
  269 + return of(null);
  270 + }
  271 + }
  272 +
  273 + private createDevice(profileId: EntityId = this.deviceWizardFormGroup.get('deviceProfileId').value): Observable<BaseData<HasId>> {
  274 + const device = {
  275 + name: this.deviceWizardFormGroup.get('name').value,
  276 + label: this.deviceWizardFormGroup.get('label').value,
  277 + deviceProfileId: profileId,
  278 + additionalInfo: {
  279 + gateway: this.deviceWizardFormGroup.get('gateway').value,
  280 + description: this.deviceWizardFormGroup.get('description').value
  281 + },
  282 + customerId: null
  283 + };
  284 + if (this.customerFormGroup.get('customerId').value) {
  285 + device.customerId = {
  286 + entityType: EntityType.CUSTOMER,
  287 + id: this.customerFormGroup.get('customerId').value
  288 + };
  289 + }
  290 + return this.data.entitiesTableConfig.saveEntity(device);
  291 + }
  292 +
  293 + private saveCredentials(device: BaseData<HasId>): Observable<boolean> {
  294 + if (this.credentialsFormGroup.get('setCredential').value) {
  295 + return this.deviceService.getDeviceCredentials(device.id.id).pipe(
  296 + mergeMap(
  297 + (deviceCredentials) => {
  298 + const deviceCredentialsValue = {...deviceCredentials, ...this.credentialsFormGroup.value.credential};
  299 + return this.deviceService.saveDeviceCredentials(deviceCredentialsValue);
  300 + }
  301 + ),
  302 + map(() => true));
  303 + }
  304 + return of(true);
  305 + }
  306 +
  307 + allValid(): boolean {
  308 + if (this.addDeviceWizardStepper.steps.find((item, index) => {
  309 + if (item.stepControl.invalid) {
  310 + item.interacted = true;
  311 + this.addDeviceWizardStepper.selectedIndex = index;
  312 + return true;
  313 + } else {
  314 + return false;
  315 + }
  316 + } )) {
  317 + return false;
  318 + } else {
  319 + return true;
  320 + }
  321 + }
  322 +
  323 + changeStep($event: StepperSelectionEvent): void {
  324 + this.selectedIndex = $event.selectedIndex;
  325 + if (this.selectedIndex === this.maxStepperIndex) {
  326 + this.showNext = false;
  327 + } else {
  328 + this.showNext = true;
  329 + }
  330 + }
  331 +}
... ...
... ... @@ -114,7 +114,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve<EntityTableCon
114 114 disableClose: true,
115 115 panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
116 116 data: {
117   - deviceProfileName: null
  117 + deviceProfileName: null,
  118 + transportType: null
118 119 }
119 120 }).afterClosed();
120 121 }
... ...
... ... @@ -30,65 +30,9 @@
30 30 <div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
31 31 <div mat-dialog-content>
32 32 <fieldset [disabled]="(isLoading$ | async) || isReadOnly">
33   - <mat-form-field class="mat-block">
34   - <mat-label translate>device.credentials-type</mat-label>
35   - <mat-select formControlName="credentialsType"
36   - (ngModelChange)="credentialsTypeChanged()">
37   - <mat-option *ngFor="let credentialsType of credentialsTypes" [value]="credentialsType">
38   - {{ credentialTypeNamesMap.get(deviceCredentialsType[credentialsType]) }}
39   - </mat-option>
40   - </mat-select>
41   - </mat-form-field>
42   - <mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.ACCESS_TOKEN"
43   - class="mat-block">
44   - <mat-label translate>device.access-token</mat-label>
45   - <input matInput formControlName="credentialsId" required>
46   - <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('required')">
47   - {{ 'device.access-token-required' | translate }}
48   - </mat-error>
49   - <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsId').hasError('pattern')">
50   - {{ 'device.access-token-invalid' | translate }}
51   - </mat-error>
52   - </mat-form-field>
53   - <mat-form-field *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.X509_CERTIFICATE"
54   - class="mat-block">
55   - <mat-label translate>device.rsa-key</mat-label>
56   - <textarea matInput formControlName="credentialsValue" cols="15" rows="5" required></textarea>
57   - <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsValue').hasError('required')">
58   - {{ 'device.rsa-key-required' | translate }}
59   - </mat-error>
60   - </mat-form-field>
61   - <section *ngIf="deviceCredentialsFormGroup.get('credentialsType').value === deviceCredentialsType.MQTT_BASIC" formGroupName="credentialsBasic">
62   - <mat-form-field class="mat-block">
63   - <mat-label translate>device.client-id</mat-label>
64   - <input matInput formControlName="clientId">
65   - <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.clientId').hasError('pattern')">
66   - {{ 'device.client-id-pattern' | translate }}
67   - </mat-error>
68   - </mat-form-field>
69   - <mat-form-field class="mat-block">
70   - <mat-label translate>device.user-name</mat-label>
71   - <input matInput formControlName="userName" [required]="!!deviceCredentialsFormGroup.get('credentialsBasic.password').value">
72   - <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic.userName').hasError('required')">
73   - {{ 'device.user-name-required' | translate }}
74   - </mat-error>
75   - </mat-form-field>
76   - <mat-form-field class="mat-block">
77   - <mat-label translate>device.password</mat-label>
78   - <input matInput formControlName="password"
79   - autocomplete="new-password"
80   - (ngModelChange)="passwordChanged()"
81   - [type]="hidePassword ? 'password' : 'text'">
82   - <button mat-icon-button matSuffix type="button"
83   - (click)="hidePassword = !hidePassword"
84   - [attr.aria-pressed]="hidePassword">
85   - <mat-icon>{{hidePassword ? 'visibility_off' : 'visibility'}}</mat-icon>
86   - </button>
87   - </mat-form-field>
88   - <mat-error *ngIf="deviceCredentialsFormGroup.get('credentialsBasic').hasError('atLeastOne')">
89   - {{ 'device.client-id-or-user-name-necessary' | translate }}
90   - </mat-error>
91   - </section>
  33 + <tb-device-credentials
  34 + formControlName="credential">
  35 + </tb-device-credentials>
92 36 </fieldset>
93 37 </div>
94 38 <div mat-dialog-actions fxLayoutAlign="end center">
... ...
... ... @@ -19,23 +19,9 @@ import { ErrorStateMatcher } from '@angular/material/core';
19 19 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
20 20 import { Store } from '@ngrx/store';
21 21 import { AppState } from '@core/core.state';
22   -import {
23   - FormBuilder,
24   - FormControl,
25   - FormGroup,
26   - FormGroupDirective,
27   - NgForm,
28   - ValidationErrors,
29   - ValidatorFn,
30   - Validators
31   -} from '@angular/forms';
  22 +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
32 23 import { DeviceService } from '@core/http/device.service';
33   -import {
34   - credentialTypeNames,
35   - DeviceCredentialMQTTBasic,
36   - DeviceCredentials,
37   - DeviceCredentialsType
38   -} from '@shared/models/device.models';
  24 +import { credentialTypeNames, DeviceCredentials, DeviceCredentialsType } from '@shared/models/device.models';
39 25 import { DialogComponent } from '@shared/components/dialog.component';
40 26 import { Router } from '@angular/router';
41 27
... ... @@ -83,19 +69,10 @@ export class DeviceCredentialsDialogComponent extends
83 69
84 70 ngOnInit(): void {
85 71 this.deviceCredentialsFormGroup = this.fb.group({
86   - credentialsType: [DeviceCredentialsType.ACCESS_TOKEN],
87   - credentialsId: [''],
88   - credentialsValue: [''],
89   - credentialsBasic: this.fb.group({
90   - clientId: ['', [Validators.pattern(/^[A-Za-z0-9]+$/)]],
91   - userName: [''],
92   - password: ['']
93   - }, {validators: this.atLeastOne(Validators.required, ['clientId', 'userName'])})
  72 + credential: [null]
94 73 });
95 74 if (this.isReadOnly) {
96 75 this.deviceCredentialsFormGroup.disable({emitEvent: false});
97   - } else {
98   - this.registerDisableOnLoadFormControl(this.deviceCredentialsFormGroup.get('credentialsType'));
99 76 }
100 77 this.loadDeviceCredentials();
101 78 }
... ... @@ -110,82 +87,20 @@ export class DeviceCredentialsDialogComponent extends
110 87 this.deviceService.getDeviceCredentials(this.data.deviceId).subscribe(
111 88 (deviceCredentials) => {
112 89 this.deviceCredentials = deviceCredentials;
113   - let credentialsValue = deviceCredentials.credentialsValue;
114   - let credentialsBasic = {clientId: null, userName: null, password: null};
115   - if (deviceCredentials.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
116   - credentialsValue = null;
117   - credentialsBasic = JSON.parse(deviceCredentials.credentialsValue) as DeviceCredentialMQTTBasic;
118   - }
119 90 this.deviceCredentialsFormGroup.patchValue({
120   - credentialsType: deviceCredentials.credentialsType,
121   - credentialsId: deviceCredentials.credentialsId,
122   - credentialsValue,
123   - credentialsBasic
124   - });
125   - this.updateValidators();
  91 + credential: deviceCredentials
  92 + }, {emitEvent: false});
126 93 }
127 94 );
128 95 }
129 96
130   - credentialsTypeChanged(): void {
131   - this.deviceCredentialsFormGroup.patchValue({
132   - credentialsId: null,
133   - credentialsValue: null,
134   - credentialsBasic: {clientId: '', userName: '', password: ''}
135   - }, {emitEvent: true});
136   - this.updateValidators();
137   - }
138   -
139   - updateValidators(): void {
140   - this.hidePassword = true;
141   - const crendetialsType = this.deviceCredentialsFormGroup.get('credentialsType').value as DeviceCredentialsType;
142   - switch (crendetialsType) {
143   - case DeviceCredentialsType.ACCESS_TOKEN:
144   - this.deviceCredentialsFormGroup.get('credentialsId').setValidators([Validators.required, Validators.pattern(/^.{1,20}$/)]);
145   - this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
146   - this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
147   - this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
148   - this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
149   - break;
150   - case DeviceCredentialsType.X509_CERTIFICATE:
151   - this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([Validators.required]);
152   - this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
153   - this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
154   - this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
155   - this.deviceCredentialsFormGroup.get('credentialsBasic').disable();
156   - break;
157   - case DeviceCredentialsType.MQTT_BASIC:
158   - this.deviceCredentialsFormGroup.get('credentialsBasic').enable();
159   - this.deviceCredentialsFormGroup.get('credentialsBasic').updateValueAndValidity();
160   - this.deviceCredentialsFormGroup.get('credentialsId').setValidators([]);
161   - this.deviceCredentialsFormGroup.get('credentialsId').updateValueAndValidity();
162   - this.deviceCredentialsFormGroup.get('credentialsValue').setValidators([]);
163   - this.deviceCredentialsFormGroup.get('credentialsValue').updateValueAndValidity();
164   - }
165   - }
166   -
167   - private atLeastOne(validator: ValidatorFn, controls: string[] = null) {
168   - return (group: FormGroup): ValidationErrors | null => {
169   - if (!controls) {
170   - controls = Object.keys(group.controls);
171   - }
172   - const hasAtLeastOne = group?.controls && controls.some(k => !validator(group.controls[k]));
173   -
174   - return hasAtLeastOne ? null : {atLeastOne: true};
175   - };
176   - }
177   -
178 97 cancel(): void {
179 98 this.dialogRef.close(null);
180 99 }
181 100
182 101 save(): void {
183 102 this.submitted = true;
184   - const deviceCredentialsValue = this.deviceCredentialsFormGroup.value;
185   - if (deviceCredentialsValue.credentialsType === DeviceCredentialsType.MQTT_BASIC) {
186   - deviceCredentialsValue.credentialsValue = JSON.stringify(deviceCredentialsValue.credentialsBasic);
187   - }
188   - delete deviceCredentialsValue.credentialsBasic;
  103 + const deviceCredentialsValue = this.deviceCredentialsFormGroup.value.credential;
189 104 this.deviceCredentials = {...this.deviceCredentials, ...deviceCredentialsValue};
190 105 this.deviceService.saveDeviceCredentials(this.deviceCredentials).subscribe(
191 106 (deviceCredentials) => {
... ... @@ -193,18 +108,4 @@ export class DeviceCredentialsDialogComponent extends
193 108 }
194 109 );
195 110 }
196   -
197   - passwordChanged() {
198   - const value = this.deviceCredentialsFormGroup.get('credentialsBasic.password').value;
199   - if (value !== '') {
200   - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([Validators.required]);
201   - if (this.deviceCredentialsFormGroup.get('credentialsBasic.userName').untouched) {
202   - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').markAsTouched({onlySelf: true});
203   - }
204   - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
205   - } else {
206   - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').setValidators([]);
207   - this.deviceCredentialsFormGroup.get('credentialsBasic.userName').updateValueAndValidity();
208   - }
209   - }
210 111 }
... ...
... ... @@ -29,7 +29,7 @@ import {
29 29 import { TranslateService } from '@ngx-translate/core';
30 30 import { DatePipe } from '@angular/common';
31 31 import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models';
32   -import { EntityAction } from '@home/models/entity/entity-component.models';
  32 +import { AddEntityDialogData, EntityAction } from '@home/models/entity/entity-component.models';
33 33 import { Device, DeviceCredentials, DeviceInfo } from '@app/shared/models/device.models';
34 34 import { DeviceComponent } from '@modules/home/pages/device/device.component';
35 35 import { forkJoin, Observable, of } from 'rxjs';
... ... @@ -61,6 +61,8 @@ import {
61 61 } from '../../dialogs/add-entities-to-customer-dialog.component';
62 62 import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component';
63 63 import { HomeDialogsService } from '@home/dialogs/home-dialogs.service';
  64 +import { DeviceWizardDialogComponent } from '@home/components/wizard/device-wizard-dialog.component';
  65 +import { BaseData, HasId } from '@shared/models/base-data';
64 66
65 67 @Injectable()
66 68 export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<DeviceInfo>> {
... ... @@ -221,7 +223,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
221 223 {
222 224 name: this.translate.instant('device.manage-credentials'),
223 225 icon: 'security',
224   - isEnabled: (entity) => true,
  226 + isEnabled: () => true,
225 227 onAction: ($event, entity) => this.manageCredentials($event, entity)
226 228 }
227 229 );
... ... @@ -243,7 +245,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
243 245 {
244 246 name: this.translate.instant('device.manage-credentials'),
245 247 icon: 'security',
246   - isEnabled: (entity) => true,
  248 + isEnabled: () => true,
247 249 onAction: ($event, entity) => this.manageCredentials($event, entity)
248 250 }
249 251 );
... ... @@ -253,7 +255,7 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
253 255 {
254 256 name: this.translate.instant('device.view-credentials'),
255 257 icon: 'security',
256   - isEnabled: (entity) => true,
  258 + isEnabled: () => true,
257 259 onAction: ($event, entity) => this.manageCredentials($event, entity)
258 260 }
259 261 );
... ... @@ -294,14 +296,14 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
294 296 name: this.translate.instant('device.add-device-text'),
295 297 icon: 'insert_drive_file',
296 298 isEnabled: () => true,
297   - onAction: ($event) => this.config.table.addEntity($event)
  299 + onAction: ($event) => this.deviceWizard($event)
298 300 },
299 301 {
300 302 name: this.translate.instant('device.import'),
301 303 icon: 'file_upload',
302 304 isEnabled: () => true,
303 305 onAction: ($event) => this.importDevices($event)
304   - }
  306 + },
305 307 );
306 308 }
307 309 if (deviceScope === 'customer') {
... ... @@ -326,6 +328,23 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
326 328 });
327 329 }
328 330
  331 + deviceWizard($event: Event) {
  332 + this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>,
  333 + boolean>(DeviceWizardDialogComponent, {
  334 + disableClose: true,
  335 + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
  336 + data: {
  337 + entitiesTableConfig: this.config.table.entitiesTableConfig
  338 + }
  339 + }).afterClosed().subscribe(
  340 + (res) => {
  341 + if (res) {
  342 + this.config.table.updateData();
  343 + }
  344 + }
  345 + );
  346 + }
  347 +
329 348 addDevicesToCustomer($event: Event) {
330 349 if ($event) {
331 350 $event.stopPropagation();
... ... @@ -480,5 +499,4 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev
480 499 }
481 500 return false;
482 501 }
483   -
484 502 }
... ...
... ... @@ -87,6 +87,14 @@ export const deviceProvisionTypeTranslationMap = new Map<DeviceProvisionType, st
87 87 ]
88 88 )
89 89
  90 +export const deviceTransportTypeHintMap = new Map<DeviceTransportType, string>(
  91 + [
  92 + [DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'],
  93 + [DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'],
  94 + [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint']
  95 + ]
  96 +);
  97 +
90 98 export const mqttTransportPayloadTypeTranslationMap = new Map<MqttTransportPayloadType, string>(
91 99 [
92 100 [MqttTransportPayloadType.JSON, 'device-profile.mqtt-device-payload-type-json'],
... ...
... ... @@ -54,7 +54,8 @@
54 54 "share-via": "Share via {{provider}}",
55 55 "continue": "Continue",
56 56 "discard-changes": "Discard Changes",
57   - "download": "Download"
  57 + "download": "Download",
  58 + "next-with-label": "Next: {{label}}"
58 59 },
59 60 "aggregation": {
60 61 "aggregation": "Aggregation",
... ... @@ -756,7 +757,16 @@
756 757 "search": "Search devices",
757 758 "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected",
758 759 "device-configuration": "Device configuration",
759   - "transport-configuration": "Transport configuration"
  760 + "transport-configuration": "Transport configuration",
  761 + "wizard": {
  762 + "device-wizard": "Device Wizard",
  763 + "device-details": "Device details",
  764 + "new-device-profile": "Create new device profile",
  765 + "existing-device-profile": "Select existing device profile",
  766 + "specific-configuration": "Specific configuration",
  767 + "customer-to-assign-device": "Customer to assign the device",
  768 + "add-credential": "Add credential"
  769 + }
760 770 },
761 771 "device-profile": {
762 772 "device-profile": "Device profile",
... ... @@ -774,6 +784,8 @@
774 784 "set-default": "Make device profile default",
775 785 "delete": "Delete device profile",
776 786 "copyId": "Copy device profile Id",
  787 + "new-device-profile-name": "Device profile name",
  788 + "new-device-profile-name-required": "Device profile name is required.",
777 789 "name": "Name",
778 790 "name-required": "Name is required.",
779 791 "type": "Profile type",
... ... @@ -782,8 +794,11 @@
782 794 "transport-type": "Transport type",
783 795 "transport-type-required": "Transport type is required.",
784 796 "transport-type-default": "Default",
  797 + "transport-type-default-hint": "Default transport type",
785 798 "transport-type-mqtt": "MQTT",
  799 + "transport-type-mqtt-hint": "MQTT transport type",
786 800 "transport-type-lwm2m": "LWM2M",
  801 + "transport-type-lwm2m-hint": "LWM2M transport type",
787 802 "description": "Description",
788 803 "default": "Default",
789 804 "profile-configuration": "Profile configuration",
... ... @@ -814,7 +829,8 @@
814 829 "not-valid-multi-character": "Invalid use of a multi-level wildcard character",
815 830 "single-level-wildcards-hint": "<code>[+]</code> is suitable for any topic filter level. Ex.: <b>v1/devices/+/telemetry</b> or <b>+/devices/+/attributes</b>.",
816 831 "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>.",
817   - "alarm-rules": "Alarm rules ({{count}})",
  832 + "alarm-rules": "Alarm rules",
  833 + "alarm-rules-with-count": "Alarm rules ({{count}})",
818 834 "no-alarm-rules": "No alarm rules configured",
819 835 "add-alarm-rule": "Add alarm rule",
820 836 "edit-alarm-rule": "Edit alarm rule",
... ...