Commit 35f6f40ca347f2e4dba0fcfff2ed306ba325bfff
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, '...')}) | 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", | ... | ... |