Commit 1e87c728181c94d586e389a8485be8812af47ed2
1 parent
3adbb481
UI: Device profile autocomplete. Device profile data.
Showing
22 changed files
with
1237 additions
and
7 deletions
@@ -94,6 +94,8 @@ import { DeviceProfileDataComponent } from './profile/device-profile-data.compon | @@ -94,6 +94,8 @@ import { DeviceProfileDataComponent } from './profile/device-profile-data.compon | ||
94 | import { DeviceProfileComponent } from './profile/device-profile.component'; | 94 | import { DeviceProfileComponent } from './profile/device-profile.component'; |
95 | import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component'; | 95 | import { DefaultDeviceProfileTransportConfigurationComponent } from './profile/device/default-device-profile-transport-configuration.component'; |
96 | import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component'; | 96 | import { DeviceProfileTransportConfigurationComponent } from './profile/device/device-profile-transport-configuration.component'; |
97 | +import { DeviceProfileDialogComponent } from './profile/device-profile-dialog.component'; | ||
98 | +import { DeviceProfileAutocompleteComponent } from './profile/device-profile-autocomplete.component'; | ||
97 | 99 | ||
98 | @NgModule({ | 100 | @NgModule({ |
99 | declarations: | 101 | declarations: |
@@ -165,12 +167,14 @@ import { DeviceProfileTransportConfigurationComponent } from './profile/device/d | @@ -165,12 +167,14 @@ import { DeviceProfileTransportConfigurationComponent } from './profile/device/d | ||
165 | TenantProfileDataComponent, | 167 | TenantProfileDataComponent, |
166 | TenantProfileComponent, | 168 | TenantProfileComponent, |
167 | TenantProfileDialogComponent, | 169 | TenantProfileDialogComponent, |
170 | + DeviceProfileAutocompleteComponent, | ||
168 | DefaultDeviceProfileConfigurationComponent, | 171 | DefaultDeviceProfileConfigurationComponent, |
169 | DeviceProfileConfigurationComponent, | 172 | DeviceProfileConfigurationComponent, |
170 | DefaultDeviceProfileTransportConfigurationComponent, | 173 | DefaultDeviceProfileTransportConfigurationComponent, |
171 | DeviceProfileTransportConfigurationComponent, | 174 | DeviceProfileTransportConfigurationComponent, |
172 | DeviceProfileDataComponent, | 175 | DeviceProfileDataComponent, |
173 | - DeviceProfileComponent | 176 | + DeviceProfileComponent, |
177 | + DeviceProfileDialogComponent | ||
174 | ], | 178 | ], |
175 | imports: [ | 179 | imports: [ |
176 | CommonModule, | 180 | CommonModule, |
@@ -231,12 +235,14 @@ import { DeviceProfileTransportConfigurationComponent } from './profile/device/d | @@ -231,12 +235,14 @@ import { DeviceProfileTransportConfigurationComponent } from './profile/device/d | ||
231 | TenantProfileDataComponent, | 235 | TenantProfileDataComponent, |
232 | TenantProfileComponent, | 236 | TenantProfileComponent, |
233 | TenantProfileDialogComponent, | 237 | TenantProfileDialogComponent, |
238 | + DeviceProfileAutocompleteComponent, | ||
234 | DefaultDeviceProfileConfigurationComponent, | 239 | DefaultDeviceProfileConfigurationComponent, |
235 | DeviceProfileConfigurationComponent, | 240 | DeviceProfileConfigurationComponent, |
236 | DefaultDeviceProfileTransportConfigurationComponent, | 241 | DefaultDeviceProfileTransportConfigurationComponent, |
237 | DeviceProfileTransportConfigurationComponent, | 242 | DeviceProfileTransportConfigurationComponent, |
238 | DeviceProfileDataComponent, | 243 | DeviceProfileDataComponent, |
239 | - DeviceProfileComponent | 244 | + DeviceProfileComponent, |
245 | + DeviceProfileDialogComponent | ||
240 | ], | 246 | ], |
241 | providers: [ | 247 | providers: [ |
242 | WidgetComponentService, | 248 | WidgetComponentService, |
ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html
0 → 100644
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 | +<mat-form-field [formGroup]="selectDeviceProfileFormGroup" class="mat-block"> | ||
19 | + <input matInput type="text" placeholder="{{ 'device-profile.device-profile' | translate }}" | ||
20 | + #deviceProfileInput | ||
21 | + formControlName="deviceProfile" | ||
22 | + [required]="required" | ||
23 | + (keydown)="deviceProfileEnter($event)" | ||
24 | + (keypress)="deviceProfileEnter($event)" | ||
25 | + [matAutocomplete]="deviceProfileAutocomplete"> | ||
26 | + <button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled" | ||
27 | + type="button" | ||
28 | + matSuffix mat-button mat-icon-button aria-label="Clear" | ||
29 | + (click)="clear()"> | ||
30 | + <mat-icon class="material-icons">close</mat-icon> | ||
31 | + </button> | ||
32 | + <button *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').value && !disabled" | ||
33 | + type="button" | ||
34 | + matSuffix mat-button mat-icon-button aria-label="Edit" | ||
35 | + matTooltip="{{ 'device-profile.edit' | translate }}" | ||
36 | + matTooltipPosition="above" | ||
37 | + (click)="editDeviceProfile($event)"> | ||
38 | + <mat-icon class="material-icons">edit</mat-icon> | ||
39 | + </button> | ||
40 | + <mat-autocomplete | ||
41 | + class="tb-autocomplete" | ||
42 | + #deviceProfileAutocomplete="matAutocomplete" | ||
43 | + [displayWith]="displayDeviceProfileFn"> | ||
44 | + <mat-option *ngFor="let deviceProfile of filteredDeviceProfiles | async" [value]="deviceProfile"> | ||
45 | + <span [innerHTML]="deviceProfile.name | highlight:searchText"></span> | ||
46 | + </mat-option> | ||
47 | + <mat-option *ngIf="!(filteredDeviceProfiles | async)?.length" [value]="null" class="tb-not-found"> | ||
48 | + <div class="tb-not-found-content" (click)="$event.stopPropagation()"> | ||
49 | + <div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> | ||
50 | + <span translate>device-profile.no-device-profiles-found</span> | ||
51 | + </div> | ||
52 | + <ng-template #searchNotEmpty> | ||
53 | + <span> | ||
54 | + {{ translate.get('device-profile.no-device-profiles-matching', | ||
55 | + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} | ||
56 | + </span> | ||
57 | + </ng-template> | ||
58 | + <span> | ||
59 | + <a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a> | ||
60 | + </span> | ||
61 | + </div> | ||
62 | + </mat-option> | ||
63 | + </mat-autocomplete> | ||
64 | + <mat-error *ngIf="selectDeviceProfileFormGroup.get('deviceProfile').hasError('required')"> | ||
65 | + {{ 'device-profile.device-profile-required' | translate }} | ||
66 | + </mat-error> | ||
67 | +</mat-form-field> |
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, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||
19 | +import { Observable } from 'rxjs'; | ||
20 | +import { PageLink } from '@shared/models/page/page-link'; | ||
21 | +import { Direction } from '@shared/models/page/sort-order'; | ||
22 | +import { map, mergeMap, startWith, tap } from 'rxjs/operators'; | ||
23 | +import { Store } from '@ngrx/store'; | ||
24 | +import { AppState } from '@app/core/core.state'; | ||
25 | +import { TranslateService } from '@ngx-translate/core'; | ||
26 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
27 | +import { entityIdEquals } from '@shared/models/id/entity-id'; | ||
28 | +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; | ||
29 | +import { ENTER } from '@angular/cdk/keycodes'; | ||
30 | +import { MatDialog } from '@angular/material/dialog'; | ||
31 | +import { DeviceProfileId } from '@shared/models/id/device-profile-id'; | ||
32 | +import { | ||
33 | + createDeviceProfileConfiguration, | ||
34 | + createDeviceProfileTransportConfiguration, | ||
35 | + DeviceProfile, | ||
36 | + DeviceProfileInfo, | ||
37 | + DeviceProfileType, | ||
38 | + DeviceTransportType | ||
39 | +} from '@shared/models/device.models'; | ||
40 | +import { DeviceProfileService } from '@core/http/device-profile.service'; | ||
41 | +import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; | ||
42 | + | ||
43 | +@Component({ | ||
44 | + selector: 'tb-device-profile-autocomplete', | ||
45 | + templateUrl: './device-profile-autocomplete.component.html', | ||
46 | + styleUrls: [], | ||
47 | + providers: [{ | ||
48 | + provide: NG_VALUE_ACCESSOR, | ||
49 | + useExisting: forwardRef(() => DeviceProfileAutocompleteComponent), | ||
50 | + multi: true | ||
51 | + }] | ||
52 | +}) | ||
53 | +export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, OnInit { | ||
54 | + | ||
55 | + selectDeviceProfileFormGroup: FormGroup; | ||
56 | + | ||
57 | + modelValue: DeviceProfileId | null; | ||
58 | + | ||
59 | + @Input() | ||
60 | + selectDefaultProfile = false; | ||
61 | + | ||
62 | + private requiredValue: boolean; | ||
63 | + get required(): boolean { | ||
64 | + return this.requiredValue; | ||
65 | + } | ||
66 | + @Input() | ||
67 | + set required(value: boolean) { | ||
68 | + this.requiredValue = coerceBooleanProperty(value); | ||
69 | + } | ||
70 | + | ||
71 | + @Input() | ||
72 | + disabled: boolean; | ||
73 | + | ||
74 | + @Output() | ||
75 | + deviceProfileUpdated = new EventEmitter<DeviceProfileId>(); | ||
76 | + | ||
77 | + @Output() | ||
78 | + deviceProfileChanged = new EventEmitter<DeviceProfileInfo>(); | ||
79 | + | ||
80 | + @ViewChild('deviceProfileInput', {static: true}) deviceProfileInput: ElementRef; | ||
81 | + | ||
82 | + filteredDeviceProfiles: Observable<Array<DeviceProfileInfo>>; | ||
83 | + | ||
84 | + searchText = ''; | ||
85 | + | ||
86 | + private propagateChange = (v: any) => { }; | ||
87 | + | ||
88 | + constructor(private store: Store<AppState>, | ||
89 | + public translate: TranslateService, | ||
90 | + public truncate: TruncatePipe, | ||
91 | + private deviceProfileService: DeviceProfileService, | ||
92 | + private fb: FormBuilder, | ||
93 | + private dialog: MatDialog) { | ||
94 | + this.selectDeviceProfileFormGroup = this.fb.group({ | ||
95 | + deviceProfile: [null] | ||
96 | + }); | ||
97 | + } | ||
98 | + | ||
99 | + registerOnChange(fn: any): void { | ||
100 | + this.propagateChange = fn; | ||
101 | + } | ||
102 | + | ||
103 | + registerOnTouched(fn: any): void { | ||
104 | + } | ||
105 | + | ||
106 | + ngOnInit() { | ||
107 | + this.filteredDeviceProfiles = this.selectDeviceProfileFormGroup.get('deviceProfile').valueChanges | ||
108 | + .pipe( | ||
109 | + tap((value: DeviceProfileInfo | string) => { | ||
110 | + let modelValue: DeviceProfileInfo | null; | ||
111 | + if (typeof value === 'string' || !value) { | ||
112 | + modelValue = null; | ||
113 | + } else { | ||
114 | + modelValue = value; | ||
115 | + } | ||
116 | + this.updateView(modelValue); | ||
117 | + }), | ||
118 | + startWith<string | DeviceProfileInfo>(''), | ||
119 | + map(value => value ? (typeof value === 'string' ? value : value.name) : ''), | ||
120 | + mergeMap(name => this.fetchDeviceProfiles(name) ) | ||
121 | + ); | ||
122 | + } | ||
123 | + | ||
124 | + selectDefaultDeviceProfileIfNeeded(): void { | ||
125 | + if (this.selectDefaultProfile && !this.modelValue) { | ||
126 | + this.deviceProfileService.getDefaultDeviceProfileInfo().subscribe( | ||
127 | + (profile) => { | ||
128 | + if (profile) { | ||
129 | + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: false}); | ||
130 | + this.updateView(profile); | ||
131 | + } | ||
132 | + } | ||
133 | + ); | ||
134 | + } | ||
135 | + } | ||
136 | + | ||
137 | + setDisabledState(isDisabled: boolean): void { | ||
138 | + this.disabled = isDisabled; | ||
139 | + } | ||
140 | + | ||
141 | + writeValue(value: DeviceProfileId | null): void { | ||
142 | + this.searchText = ''; | ||
143 | + if (value != null) { | ||
144 | + this.deviceProfileService.getDeviceProfileInfo(value.id).subscribe( | ||
145 | + (profile) => { | ||
146 | + this.modelValue = new DeviceProfileId(profile.id.id); | ||
147 | + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); | ||
148 | + } | ||
149 | + ); | ||
150 | + } else { | ||
151 | + this.modelValue = null; | ||
152 | + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); | ||
153 | + this.selectDefaultDeviceProfileIfNeeded(); | ||
154 | + } | ||
155 | + } | ||
156 | + | ||
157 | + updateView(deviceProfile: DeviceProfileInfo | null) { | ||
158 | + const idValue = deviceProfile ? new DeviceProfileId(deviceProfile.id.id) : null; | ||
159 | + if (!entityIdEquals(this.modelValue, idValue)) { | ||
160 | + this.modelValue = idValue; | ||
161 | + this.propagateChange(this.modelValue); | ||
162 | + this.deviceProfileChanged.emit(deviceProfile); | ||
163 | + } | ||
164 | + } | ||
165 | + | ||
166 | + displayDeviceProfileFn(profile?: DeviceProfileInfo): string | undefined { | ||
167 | + return profile ? profile.name : undefined; | ||
168 | + } | ||
169 | + | ||
170 | + fetchDeviceProfiles(searchText?: string): Observable<Array<DeviceProfileInfo>> { | ||
171 | + this.searchText = searchText; | ||
172 | + const pageLink = new PageLink(10, 0, searchText, { | ||
173 | + property: 'name', | ||
174 | + direction: Direction.ASC | ||
175 | + }); | ||
176 | + return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe( | ||
177 | + map(pageData => { | ||
178 | + return pageData.data; | ||
179 | + }) | ||
180 | + ); | ||
181 | + } | ||
182 | + | ||
183 | + clear() { | ||
184 | + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(null, {emitEvent: true}); | ||
185 | + setTimeout(() => { | ||
186 | + this.deviceProfileInput.nativeElement.blur(); | ||
187 | + this.deviceProfileInput.nativeElement.focus(); | ||
188 | + }, 0); | ||
189 | + } | ||
190 | + | ||
191 | + textIsNotEmpty(text: string): boolean { | ||
192 | + return (text && text.length > 0); | ||
193 | + } | ||
194 | + | ||
195 | + deviceProfileEnter($event: KeyboardEvent) { | ||
196 | + if ($event.keyCode === ENTER) { | ||
197 | + $event.preventDefault(); | ||
198 | + if (!this.modelValue) { | ||
199 | + this.createDeviceProfile($event, this.searchText); | ||
200 | + } | ||
201 | + } | ||
202 | + } | ||
203 | + | ||
204 | + createDeviceProfile($event: Event, profileName: string) { | ||
205 | + $event.preventDefault(); | ||
206 | + const deviceProfile: DeviceProfile = { | ||
207 | + id: null, | ||
208 | + name: profileName, | ||
209 | + type: DeviceProfileType.DEFAULT, | ||
210 | + transportType: DeviceTransportType.DEFAULT, | ||
211 | + profileData: { | ||
212 | + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), | ||
213 | + transportConfiguration: createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT) | ||
214 | + } | ||
215 | + }; | ||
216 | + this.openDeviceProfileDialog(deviceProfile, true); | ||
217 | + } | ||
218 | + | ||
219 | + editDeviceProfile($event: Event) { | ||
220 | + $event.preventDefault(); | ||
221 | + this.deviceProfileService.getDeviceProfile(this.modelValue.id).subscribe( | ||
222 | + (deviceProfile) => { | ||
223 | + this.openDeviceProfileDialog(deviceProfile, false); | ||
224 | + } | ||
225 | + ); | ||
226 | + } | ||
227 | + | ||
228 | + openDeviceProfileDialog(deviceProfile: DeviceProfile, isAdd: boolean) { | ||
229 | + this.dialog.open<DeviceProfileDialogComponent, DeviceProfileDialogData, | ||
230 | + DeviceProfile>(DeviceProfileDialogComponent, { | ||
231 | + disableClose: true, | ||
232 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | ||
233 | + data: { | ||
234 | + isAdd, | ||
235 | + deviceProfile | ||
236 | + } | ||
237 | + }).afterClosed().subscribe( | ||
238 | + (savedDeviceProfile) => { | ||
239 | + if (!savedDeviceProfile) { | ||
240 | + setTimeout(() => { | ||
241 | + this.deviceProfileInput.nativeElement.blur(); | ||
242 | + this.deviceProfileInput.nativeElement.focus(); | ||
243 | + }, 0); | ||
244 | + } else { | ||
245 | + this.deviceProfileService.getDeviceProfileInfo(savedDeviceProfile.id.id).subscribe( | ||
246 | + (profile) => { | ||
247 | + this.modelValue = new DeviceProfileId(profile.id.id); | ||
248 | + this.selectDeviceProfileFormGroup.get('deviceProfile').patchValue(profile, {emitEvent: true}); | ||
249 | + if (isAdd) { | ||
250 | + this.propagateChange(this.modelValue); | ||
251 | + } else { | ||
252 | + this.deviceProfileUpdated.next(savedDeviceProfile.id); | ||
253 | + } | ||
254 | + this.deviceProfileChanged.emit(profile); | ||
255 | + } | ||
256 | + ); | ||
257 | + } | ||
258 | + } | ||
259 | + ); | ||
260 | + } | ||
261 | +} |
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 | +<form (ngSubmit)="save()" style="min-width: 600px;"> | ||
19 | + <mat-toolbar color="primary"> | ||
20 | + <h2>{{ (isAdd ? 'device-profile.add' : 'device-profile.edit' ) | translate }}</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 | + <tb-device-profile | ||
33 | + #deviceProfileComponent | ||
34 | + [standalone]="true" | ||
35 | + [entity]="deviceProfile" | ||
36 | + [isEdit]="true"> | ||
37 | + </tb-device-profile> | ||
38 | + </div> | ||
39 | + <div mat-dialog-actions fxLayoutAlign="end center"> | ||
40 | + <button mat-raised-button color="primary" | ||
41 | + type="submit" | ||
42 | + [disabled]="(isLoading$ | async) || deviceProfileComponent.entityForm?.invalid || !deviceProfileComponent.entityForm?.dirty"> | ||
43 | + {{ (isAdd ? 'action.add' : 'action.save') | translate }} | ||
44 | + </button> | ||
45 | + <button mat-button color="primary" | ||
46 | + type="button" | ||
47 | + cdkFocusInitial | ||
48 | + [disabled]="(isLoading$ | async)" | ||
49 | + (click)="cancel()"> | ||
50 | + {{ 'action.cancel' | translate }} | ||
51 | + </button> | ||
52 | + </div> | ||
53 | +</form> |
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 { | ||
18 | + AfterViewInit, | ||
19 | + Component, | ||
20 | + ComponentFactoryResolver, | ||
21 | + Inject, | ||
22 | + Injector, | ||
23 | + SkipSelf, | ||
24 | + ViewChild | ||
25 | +} from '@angular/core'; | ||
26 | +import { ErrorStateMatcher } from '@angular/material/core'; | ||
27 | +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; | ||
28 | +import { Store } from '@ngrx/store'; | ||
29 | +import { AppState } from '@core/core.state'; | ||
30 | +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; | ||
31 | +import { DialogComponent } from '@shared/components/dialog.component'; | ||
32 | +import { Router } from '@angular/router'; | ||
33 | +import { DeviceProfile } from '@shared/models/device.models'; | ||
34 | +import { DeviceProfileComponent } from './device-profile.component'; | ||
35 | +import { DeviceProfileService } from '@core/http/device-profile.service'; | ||
36 | + | ||
37 | +export interface DeviceProfileDialogData { | ||
38 | + deviceProfile: DeviceProfile; | ||
39 | + isAdd: boolean; | ||
40 | +} | ||
41 | + | ||
42 | +@Component({ | ||
43 | + selector: 'tb-device-profile-dialog', | ||
44 | + templateUrl: './device-profile-dialog.component.html', | ||
45 | + providers: [{provide: ErrorStateMatcher, useExisting: DeviceProfileDialogComponent}], | ||
46 | + styleUrls: [] | ||
47 | +}) | ||
48 | +export class DeviceProfileDialogComponent extends | ||
49 | + DialogComponent<DeviceProfileDialogComponent, DeviceProfile> implements ErrorStateMatcher, AfterViewInit { | ||
50 | + | ||
51 | + isAdd: boolean; | ||
52 | + deviceProfile: DeviceProfile; | ||
53 | + | ||
54 | + submitted = false; | ||
55 | + | ||
56 | + @ViewChild('deviceProfileComponent', {static: true}) deviceProfileComponent: DeviceProfileComponent; | ||
57 | + | ||
58 | + constructor(protected store: Store<AppState>, | ||
59 | + protected router: Router, | ||
60 | + @Inject(MAT_DIALOG_DATA) public data: DeviceProfileDialogData, | ||
61 | + public dialogRef: MatDialogRef<DeviceProfileDialogComponent, DeviceProfile>, | ||
62 | + private componentFactoryResolver: ComponentFactoryResolver, | ||
63 | + private injector: Injector, | ||
64 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | ||
65 | + private deviceProfileService: DeviceProfileService) { | ||
66 | + super(store, router, dialogRef); | ||
67 | + this.isAdd = this.data.isAdd; | ||
68 | + this.deviceProfile = this.data.deviceProfile; | ||
69 | + } | ||
70 | + | ||
71 | + ngAfterViewInit(): void { | ||
72 | + if (this.isAdd) { | ||
73 | + setTimeout(() => { | ||
74 | + this.deviceProfileComponent.entityForm.markAsDirty(); | ||
75 | + }, 0); | ||
76 | + } | ||
77 | + } | ||
78 | + | ||
79 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | ||
80 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | ||
81 | + const customErrorState = !!(control && control.invalid && this.submitted); | ||
82 | + return originalErrorState || customErrorState; | ||
83 | + } | ||
84 | + | ||
85 | + cancel(): void { | ||
86 | + this.dialogRef.close(null); | ||
87 | + } | ||
88 | + | ||
89 | + save(): void { | ||
90 | + this.submitted = true; | ||
91 | + if (this.deviceProfileComponent.entityForm.valid) { | ||
92 | + this.deviceProfile = {...this.deviceProfile, ...this.deviceProfileComponent.entityFormValue()}; | ||
93 | + this.deviceProfileService.saveDeviceProfile(this.deviceProfile).subscribe( | ||
94 | + (deviceProfile) => { | ||
95 | + this.dialogRef.close(deviceProfile); | ||
96 | + } | ||
97 | + ); | ||
98 | + } | ||
99 | + } | ||
100 | + | ||
101 | +} |
@@ -18,10 +18,10 @@ | @@ -18,10 +18,10 @@ | ||
18 | <div [formGroup]="deviceProfileTransportConfigurationFormGroup"> | 18 | <div [formGroup]="deviceProfileTransportConfigurationFormGroup"> |
19 | <div [ngSwitch]="transportType"> | 19 | <div [ngSwitch]="transportType"> |
20 | <ng-template [ngSwitchCase]="deviceTransportType.DEFAULT"> | 20 | <ng-template [ngSwitchCase]="deviceTransportType.DEFAULT"> |
21 | - <tb-default-device-profile-configuration | 21 | + <tb-default-device-profile-transport-configuration |
22 | [required]="required" | 22 | [required]="required" |
23 | formControlName="configuration"> | 23 | formControlName="configuration"> |
24 | - </tb-default-device-profile-configuration> | 24 | + </tb-default-device-profile-transport-configuration> |
25 | </ng-template> | 25 | </ng-template> |
26 | </div> | 26 | </div> |
27 | </div> | 27 | </div> |
ui-ngx/src/app/modules/home/pages/device/data/default-device-configuration.component.html
0 → 100644
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 | +<form [formGroup]="defaultDeviceConfigurationFormGroup" style="padding-bottom: 16px;"> | ||
19 | + <tb-json-object-edit | ||
20 | + [required]="required" | ||
21 | + label="{{ 'device-profile.type-default' | translate }}" | ||
22 | + formControlName="configuration"> | ||
23 | + </tb-json-object-edit> | ||
24 | +</form> |
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, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@app/core/core.state'; | ||
21 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
22 | +import { | ||
23 | + DefaultDeviceConfiguration, | ||
24 | + DeviceConfiguration, | ||
25 | + DeviceProfileType | ||
26 | +} from '@shared/models/device.models'; | ||
27 | + | ||
28 | +@Component({ | ||
29 | + selector: 'tb-default-device-configuration', | ||
30 | + templateUrl: './default-device-configuration.component.html', | ||
31 | + styleUrls: [], | ||
32 | + providers: [{ | ||
33 | + provide: NG_VALUE_ACCESSOR, | ||
34 | + useExisting: forwardRef(() => DefaultDeviceConfigurationComponent), | ||
35 | + multi: true | ||
36 | + }] | ||
37 | +}) | ||
38 | +export class DefaultDeviceConfigurationComponent implements ControlValueAccessor, OnInit { | ||
39 | + | ||
40 | + defaultDeviceConfigurationFormGroup: FormGroup; | ||
41 | + | ||
42 | + private requiredValue: boolean; | ||
43 | + get required(): boolean { | ||
44 | + return this.requiredValue; | ||
45 | + } | ||
46 | + @Input() | ||
47 | + set required(value: boolean) { | ||
48 | + this.requiredValue = coerceBooleanProperty(value); | ||
49 | + } | ||
50 | + | ||
51 | + @Input() | ||
52 | + disabled: boolean; | ||
53 | + | ||
54 | + private propagateChange = (v: any) => { }; | ||
55 | + | ||
56 | + constructor(private store: Store<AppState>, | ||
57 | + private fb: FormBuilder) { | ||
58 | + } | ||
59 | + | ||
60 | + registerOnChange(fn: any): void { | ||
61 | + this.propagateChange = fn; | ||
62 | + } | ||
63 | + | ||
64 | + registerOnTouched(fn: any): void { | ||
65 | + } | ||
66 | + | ||
67 | + ngOnInit() { | ||
68 | + this.defaultDeviceConfigurationFormGroup = this.fb.group({ | ||
69 | + configuration: [null, Validators.required] | ||
70 | + }); | ||
71 | + this.defaultDeviceConfigurationFormGroup.valueChanges.subscribe(() => { | ||
72 | + this.updateModel(); | ||
73 | + }); | ||
74 | + } | ||
75 | + | ||
76 | + setDisabledState(isDisabled: boolean): void { | ||
77 | + this.disabled = isDisabled; | ||
78 | + if (this.disabled) { | ||
79 | + this.defaultDeviceConfigurationFormGroup.disable({emitEvent: false}); | ||
80 | + } else { | ||
81 | + this.defaultDeviceConfigurationFormGroup.enable({emitEvent: false}); | ||
82 | + } | ||
83 | + } | ||
84 | + | ||
85 | + writeValue(value: DefaultDeviceConfiguration | null): void { | ||
86 | + this.defaultDeviceConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); | ||
87 | + } | ||
88 | + | ||
89 | + private updateModel() { | ||
90 | + let configuration: DeviceConfiguration = null; | ||
91 | + if (this.defaultDeviceConfigurationFormGroup.valid) { | ||
92 | + configuration = this.defaultDeviceConfigurationFormGroup.getRawValue().configuration; | ||
93 | + configuration.type = DeviceProfileType.DEFAULT; | ||
94 | + } | ||
95 | + this.propagateChange(configuration); | ||
96 | + } | ||
97 | +} |
ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.html
0 → 100644
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 | +<form [formGroup]="defaultDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;"> | ||
19 | + <tb-json-object-edit | ||
20 | + [required]="required" | ||
21 | + label="{{ 'device-profile.transport-type-default' | translate }}" | ||
22 | + formControlName="configuration"> | ||
23 | + </tb-json-object-edit> | ||
24 | +</form> |
ui-ngx/src/app/modules/home/pages/device/data/default-device-transport-configuration.component.ts
0 → 100644
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, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@app/core/core.state'; | ||
21 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
22 | +import { | ||
23 | + DefaultDeviceTransportConfiguration, | ||
24 | + DeviceTransportConfiguration, | ||
25 | + DeviceTransportType | ||
26 | +} from '@shared/models/device.models'; | ||
27 | + | ||
28 | +@Component({ | ||
29 | + selector: 'tb-default-device-transport-configuration', | ||
30 | + templateUrl: './default-device-transport-configuration.component.html', | ||
31 | + styleUrls: [], | ||
32 | + providers: [{ | ||
33 | + provide: NG_VALUE_ACCESSOR, | ||
34 | + useExisting: forwardRef(() => DefaultDeviceTransportConfigurationComponent), | ||
35 | + multi: true | ||
36 | + }] | ||
37 | +}) | ||
38 | +export class DefaultDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { | ||
39 | + | ||
40 | + defaultDeviceTransportConfigurationFormGroup: FormGroup; | ||
41 | + | ||
42 | + private requiredValue: boolean; | ||
43 | + get required(): boolean { | ||
44 | + return this.requiredValue; | ||
45 | + } | ||
46 | + @Input() | ||
47 | + set required(value: boolean) { | ||
48 | + this.requiredValue = coerceBooleanProperty(value); | ||
49 | + } | ||
50 | + | ||
51 | + @Input() | ||
52 | + disabled: boolean; | ||
53 | + | ||
54 | + private propagateChange = (v: any) => { }; | ||
55 | + | ||
56 | + constructor(private store: Store<AppState>, | ||
57 | + private fb: FormBuilder) { | ||
58 | + } | ||
59 | + | ||
60 | + registerOnChange(fn: any): void { | ||
61 | + this.propagateChange = fn; | ||
62 | + } | ||
63 | + | ||
64 | + registerOnTouched(fn: any): void { | ||
65 | + } | ||
66 | + | ||
67 | + ngOnInit() { | ||
68 | + this.defaultDeviceTransportConfigurationFormGroup = this.fb.group({ | ||
69 | + configuration: [null, Validators.required] | ||
70 | + }); | ||
71 | + this.defaultDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { | ||
72 | + this.updateModel(); | ||
73 | + }); | ||
74 | + } | ||
75 | + | ||
76 | + setDisabledState(isDisabled: boolean): void { | ||
77 | + this.disabled = isDisabled; | ||
78 | + if (this.disabled) { | ||
79 | + this.defaultDeviceTransportConfigurationFormGroup.disable({emitEvent: false}); | ||
80 | + } else { | ||
81 | + this.defaultDeviceTransportConfigurationFormGroup.enable({emitEvent: false}); | ||
82 | + } | ||
83 | + } | ||
84 | + | ||
85 | + writeValue(value: DefaultDeviceTransportConfiguration | null): void { | ||
86 | + this.defaultDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); | ||
87 | + } | ||
88 | + | ||
89 | + private updateModel() { | ||
90 | + let configuration: DeviceTransportConfiguration = null; | ||
91 | + if (this.defaultDeviceTransportConfigurationFormGroup.valid) { | ||
92 | + configuration = this.defaultDeviceTransportConfigurationFormGroup.getRawValue().configuration; | ||
93 | + configuration.type = DeviceTransportType.DEFAULT; | ||
94 | + } | ||
95 | + this.propagateChange(configuration); | ||
96 | + } | ||
97 | +} |
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 [formGroup]="deviceConfigurationFormGroup"> | ||
19 | + <div [ngSwitch]="type"> | ||
20 | + <ng-template [ngSwitchCase]="deviceProfileType.DEFAULT"> | ||
21 | + <tb-default-device-configuration | ||
22 | + [required]="required" | ||
23 | + formControlName="configuration"> | ||
24 | + </tb-default-device-configuration> | ||
25 | + </ng-template> | ||
26 | + </div> | ||
27 | +</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 { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@app/core/core.state'; | ||
21 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
22 | +import { DeviceConfiguration, DeviceProfileType } from '@shared/models/device.models'; | ||
23 | +import { deepClone } from '@core/utils'; | ||
24 | + | ||
25 | +@Component({ | ||
26 | + selector: 'tb-device-configuration', | ||
27 | + templateUrl: './device-configuration.component.html', | ||
28 | + styleUrls: [], | ||
29 | + providers: [{ | ||
30 | + provide: NG_VALUE_ACCESSOR, | ||
31 | + useExisting: forwardRef(() => DeviceConfigurationComponent), | ||
32 | + multi: true | ||
33 | + }] | ||
34 | +}) | ||
35 | +export class DeviceConfigurationComponent implements ControlValueAccessor, OnInit { | ||
36 | + | ||
37 | + deviceProfileType = DeviceProfileType; | ||
38 | + | ||
39 | + deviceConfigurationFormGroup: FormGroup; | ||
40 | + | ||
41 | + private requiredValue: boolean; | ||
42 | + get required(): boolean { | ||
43 | + return this.requiredValue; | ||
44 | + } | ||
45 | + @Input() | ||
46 | + set required(value: boolean) { | ||
47 | + this.requiredValue = coerceBooleanProperty(value); | ||
48 | + } | ||
49 | + | ||
50 | + @Input() | ||
51 | + disabled: boolean; | ||
52 | + | ||
53 | + type: DeviceProfileType; | ||
54 | + | ||
55 | + private propagateChange = (v: any) => { }; | ||
56 | + | ||
57 | + constructor(private store: Store<AppState>, | ||
58 | + private fb: FormBuilder) { | ||
59 | + } | ||
60 | + | ||
61 | + registerOnChange(fn: any): void { | ||
62 | + this.propagateChange = fn; | ||
63 | + } | ||
64 | + | ||
65 | + registerOnTouched(fn: any): void { | ||
66 | + } | ||
67 | + | ||
68 | + ngOnInit() { | ||
69 | + this.deviceConfigurationFormGroup = this.fb.group({ | ||
70 | + configuration: [null, Validators.required] | ||
71 | + }); | ||
72 | + this.deviceConfigurationFormGroup.valueChanges.subscribe(() => { | ||
73 | + this.updateModel(); | ||
74 | + }); | ||
75 | + } | ||
76 | + | ||
77 | + setDisabledState(isDisabled: boolean): void { | ||
78 | + this.disabled = isDisabled; | ||
79 | + if (this.disabled) { | ||
80 | + this.deviceConfigurationFormGroup.disable({emitEvent: false}); | ||
81 | + } else { | ||
82 | + this.deviceConfigurationFormGroup.enable({emitEvent: false}); | ||
83 | + } | ||
84 | + } | ||
85 | + | ||
86 | + writeValue(value: DeviceConfiguration | null): void { | ||
87 | + this.type = value?.type; | ||
88 | + const configuration = deepClone(value); | ||
89 | + if (configuration) { | ||
90 | + delete configuration.type; | ||
91 | + } | ||
92 | + this.deviceConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); | ||
93 | + } | ||
94 | + | ||
95 | + private updateModel() { | ||
96 | + let configuration: DeviceConfiguration = null; | ||
97 | + if (this.deviceConfigurationFormGroup.valid) { | ||
98 | + configuration = this.deviceConfigurationFormGroup.getRawValue().configuration; | ||
99 | + configuration.type = this.type; | ||
100 | + } | ||
101 | + this.propagateChange(configuration); | ||
102 | + } | ||
103 | +} |
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 [formGroup]="deviceDataFormGroup" style="padding-bottom: 16px;"> | ||
19 | + <mat-accordion multi="true"> | ||
20 | + <mat-expansion-panel [expanded]="true"> | ||
21 | + <mat-expansion-panel-header> | ||
22 | + <mat-panel-title> | ||
23 | + <div translate>device.device-configuration</div> | ||
24 | + </mat-panel-title> | ||
25 | + </mat-expansion-panel-header> | ||
26 | + <tb-device-configuration | ||
27 | + formControlName="configuration" | ||
28 | + required> | ||
29 | + </tb-device-configuration> | ||
30 | + </mat-expansion-panel> | ||
31 | + <mat-expansion-panel [expanded]="true"> | ||
32 | + <mat-expansion-panel-header> | ||
33 | + <mat-panel-title> | ||
34 | + <div translate>device.transport-configuration</div> | ||
35 | + </mat-panel-title> | ||
36 | + </mat-expansion-panel-header> | ||
37 | + <tb-device-transport-configuration | ||
38 | + formControlName="transportConfiguration" | ||
39 | + required> | ||
40 | + </tb-device-transport-configuration> | ||
41 | + </mat-expansion-panel> | ||
42 | + </mat-accordion> | ||
43 | +</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 { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@app/core/core.state'; | ||
21 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
22 | +import { DeviceData } from '@shared/models/device.models'; | ||
23 | + | ||
24 | +@Component({ | ||
25 | + selector: 'tb-device-data', | ||
26 | + templateUrl: './device-data.component.html', | ||
27 | + styleUrls: [], | ||
28 | + providers: [{ | ||
29 | + provide: NG_VALUE_ACCESSOR, | ||
30 | + useExisting: forwardRef(() => DeviceDataComponent), | ||
31 | + multi: true | ||
32 | + }] | ||
33 | +}) | ||
34 | +export class DeviceDataComponent implements ControlValueAccessor, OnInit { | ||
35 | + | ||
36 | + deviceDataFormGroup: FormGroup; | ||
37 | + | ||
38 | + private requiredValue: boolean; | ||
39 | + get required(): boolean { | ||
40 | + return this.requiredValue; | ||
41 | + } | ||
42 | + @Input() | ||
43 | + set required(value: boolean) { | ||
44 | + this.requiredValue = coerceBooleanProperty(value); | ||
45 | + } | ||
46 | + | ||
47 | + @Input() | ||
48 | + disabled: boolean; | ||
49 | + | ||
50 | + private propagateChange = (v: any) => { }; | ||
51 | + | ||
52 | + constructor(private store: Store<AppState>, | ||
53 | + private fb: FormBuilder) { | ||
54 | + } | ||
55 | + | ||
56 | + registerOnChange(fn: any): void { | ||
57 | + this.propagateChange = fn; | ||
58 | + } | ||
59 | + | ||
60 | + registerOnTouched(fn: any): void { | ||
61 | + } | ||
62 | + | ||
63 | + ngOnInit() { | ||
64 | + this.deviceDataFormGroup = this.fb.group({ | ||
65 | + configuration: [null, Validators.required], | ||
66 | + transportConfiguration: [null, Validators.required] | ||
67 | + }); | ||
68 | + this.deviceDataFormGroup.valueChanges.subscribe(() => { | ||
69 | + this.updateModel(); | ||
70 | + }); | ||
71 | + } | ||
72 | + | ||
73 | + setDisabledState(isDisabled: boolean): void { | ||
74 | + this.disabled = isDisabled; | ||
75 | + if (this.disabled) { | ||
76 | + this.deviceDataFormGroup.disable({emitEvent: false}); | ||
77 | + } else { | ||
78 | + this.deviceDataFormGroup.enable({emitEvent: false}); | ||
79 | + } | ||
80 | + } | ||
81 | + | ||
82 | + writeValue(value: DeviceData | null): void { | ||
83 | + this.deviceDataFormGroup.patchValue({configuration: value?.configuration}, {emitEvent: false}); | ||
84 | + this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); | ||
85 | + } | ||
86 | + | ||
87 | + private updateModel() { | ||
88 | + let deviceData: DeviceData = null; | ||
89 | + if (this.deviceDataFormGroup.valid) { | ||
90 | + deviceData = this.deviceDataFormGroup.getRawValue(); | ||
91 | + } | ||
92 | + this.propagateChange(deviceData); | ||
93 | + } | ||
94 | +} |
ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
0 → 100644
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 [formGroup]="deviceTransportConfigurationFormGroup"> | ||
19 | + <div [ngSwitch]="transportType"> | ||
20 | + <ng-template [ngSwitchCase]="deviceTransportType.DEFAULT"> | ||
21 | + <tb-default-device-transport-configuration | ||
22 | + [required]="required" | ||
23 | + formControlName="configuration"> | ||
24 | + </tb-default-device-transport-configuration> | ||
25 | + </ng-template> | ||
26 | + </div> | ||
27 | +</div> |
ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.ts
0 → 100644
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, OnInit } from '@angular/core'; | ||
18 | +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | ||
19 | +import { Store } from '@ngrx/store'; | ||
20 | +import { AppState } from '@app/core/core.state'; | ||
21 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
22 | +import { | ||
23 | + DeviceTransportConfiguration, | ||
24 | + DeviceTransportType | ||
25 | +} from '@shared/models/device.models'; | ||
26 | +import { deepClone } from '@core/utils'; | ||
27 | + | ||
28 | +@Component({ | ||
29 | + selector: 'tb-device-transport-configuration', | ||
30 | + templateUrl: './device-transport-configuration.component.html', | ||
31 | + styleUrls: [], | ||
32 | + providers: [{ | ||
33 | + provide: NG_VALUE_ACCESSOR, | ||
34 | + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), | ||
35 | + multi: true | ||
36 | + }] | ||
37 | +}) | ||
38 | +export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { | ||
39 | + | ||
40 | + deviceTransportType = DeviceTransportType; | ||
41 | + | ||
42 | + deviceTransportConfigurationFormGroup: FormGroup; | ||
43 | + | ||
44 | + private requiredValue: boolean; | ||
45 | + get required(): boolean { | ||
46 | + return this.requiredValue; | ||
47 | + } | ||
48 | + @Input() | ||
49 | + set required(value: boolean) { | ||
50 | + this.requiredValue = coerceBooleanProperty(value); | ||
51 | + } | ||
52 | + | ||
53 | + @Input() | ||
54 | + disabled: boolean; | ||
55 | + | ||
56 | + transportType: DeviceTransportType; | ||
57 | + | ||
58 | + private propagateChange = (v: any) => { }; | ||
59 | + | ||
60 | + constructor(private store: Store<AppState>, | ||
61 | + private fb: FormBuilder) { | ||
62 | + } | ||
63 | + | ||
64 | + registerOnChange(fn: any): void { | ||
65 | + this.propagateChange = fn; | ||
66 | + } | ||
67 | + | ||
68 | + registerOnTouched(fn: any): void { | ||
69 | + } | ||
70 | + | ||
71 | + ngOnInit() { | ||
72 | + this.deviceTransportConfigurationFormGroup = this.fb.group({ | ||
73 | + configuration: [null, Validators.required] | ||
74 | + }); | ||
75 | + this.deviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { | ||
76 | + this.updateModel(); | ||
77 | + }); | ||
78 | + } | ||
79 | + | ||
80 | + setDisabledState(isDisabled: boolean): void { | ||
81 | + this.disabled = isDisabled; | ||
82 | + if (this.disabled) { | ||
83 | + this.deviceTransportConfigurationFormGroup.disable({emitEvent: false}); | ||
84 | + } else { | ||
85 | + this.deviceTransportConfigurationFormGroup.enable({emitEvent: false}); | ||
86 | + } | ||
87 | + } | ||
88 | + | ||
89 | + writeValue(value: DeviceTransportConfiguration | null): void { | ||
90 | + this.transportType = value?.type; | ||
91 | + const configuration = deepClone(value); | ||
92 | + if (configuration) { | ||
93 | + delete configuration.type; | ||
94 | + } | ||
95 | + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); | ||
96 | + } | ||
97 | + | ||
98 | + private updateModel() { | ||
99 | + let configuration: DeviceTransportConfiguration = null; | ||
100 | + if (this.deviceTransportConfigurationFormGroup.valid) { | ||
101 | + configuration = this.deviceTransportConfigurationFormGroup.getRawValue().configuration; | ||
102 | + configuration.type = this.transportType; | ||
103 | + } | ||
104 | + this.propagateChange(configuration); | ||
105 | + } | ||
106 | +} |
@@ -83,6 +83,13 @@ | @@ -83,6 +83,13 @@ | ||
83 | {{ 'device.name-required' | translate }} | 83 | {{ 'device.name-required' | translate }} |
84 | </mat-error> | 84 | </mat-error> |
85 | </mat-form-field> | 85 | </mat-form-field> |
86 | + <tb-device-profile-autocomplete | ||
87 | + [selectDefaultProfile]="isAdd" | ||
88 | + required | ||
89 | + formControlName="deviceProfileId" | ||
90 | + (deviceProfileUpdated)="onDeviceProfileUpdated()" | ||
91 | + (deviceProfileChanged)="onDeviceProfileChanged($event)"> | ||
92 | + </tb-device-profile-autocomplete> | ||
86 | <tb-entity-subtype-autocomplete | 93 | <tb-entity-subtype-autocomplete |
87 | formControlName="type" | 94 | formControlName="type" |
88 | [required]="true" | 95 | [required]="true" |
@@ -93,6 +100,10 @@ | @@ -93,6 +100,10 @@ | ||
93 | <mat-label translate>device.label</mat-label> | 100 | <mat-label translate>device.label</mat-label> |
94 | <input matInput formControlName="label"> | 101 | <input matInput formControlName="label"> |
95 | </mat-form-field> | 102 | </mat-form-field> |
103 | + <tb-device-data | ||
104 | + formControlName="deviceData" | ||
105 | + required> | ||
106 | + </tb-device-data> | ||
96 | <div formGroupName="additionalInfo" fxLayout="column"> | 107 | <div formGroupName="additionalInfo" fxLayout="column"> |
97 | <mat-checkbox fxFlex formControlName="gateway" style="padding-bottom: 16px;"> | 108 | <mat-checkbox fxFlex formControlName="gateway" style="padding-bottom: 16px;"> |
98 | {{ 'device.is-gateway' | translate }} | 109 | {{ 'device.is-gateway' | translate }} |
@@ -19,7 +19,16 @@ import { Store } from '@ngrx/store'; | @@ -19,7 +19,16 @@ import { Store } from '@ngrx/store'; | ||
19 | import { AppState } from '@core/core.state'; | 19 | import { AppState } from '@core/core.state'; |
20 | import { EntityComponent } from '../../components/entity/entity.component'; | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
21 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | 21 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
22 | -import { DeviceInfo } from '@shared/models/device.models'; | 22 | +import { |
23 | + createDeviceConfiguration, | ||
24 | + createDeviceProfileConfiguration, createDeviceTransportConfiguration, | ||
25 | + DeviceData, | ||
26 | + DeviceInfo, | ||
27 | + DeviceProfileData, | ||
28 | + DeviceProfileInfo, | ||
29 | + DeviceProfileType, | ||
30 | + DeviceTransportType | ||
31 | +} from '@shared/models/device.models'; | ||
23 | import { EntityType } from '@shared/models/entity-type.models'; | 32 | import { EntityType } from '@shared/models/entity-type.models'; |
24 | import { NULL_UUID } from '@shared/models/id/has-uuid'; | 33 | import { NULL_UUID } from '@shared/models/id/has-uuid'; |
25 | import { ActionNotificationShow } from '@core/notification/notification.actions'; | 34 | import { ActionNotificationShow } from '@core/notification/notification.actions'; |
@@ -126,4 +135,36 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> { | @@ -126,4 +135,36 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> { | ||
126 | ); | 135 | ); |
127 | } | 136 | } |
128 | } | 137 | } |
138 | + | ||
139 | + onDeviceProfileUpdated() { | ||
140 | + this.entitiesTableConfig.table.updateData(false); | ||
141 | + } | ||
142 | + | ||
143 | + onDeviceProfileChanged(deviceProfile: DeviceProfileInfo) { | ||
144 | + if (deviceProfile) { | ||
145 | + const deviceProfileType: DeviceProfileType = deviceProfile.type; | ||
146 | + const deviceTransportType: DeviceTransportType = deviceProfile.transportType; | ||
147 | + let deviceData: DeviceData = this.entityForm.getRawValue().deviceData; | ||
148 | + if (!deviceData) { | ||
149 | + deviceData = { | ||
150 | + configuration: createDeviceConfiguration(deviceProfileType), | ||
151 | + transportConfiguration: createDeviceTransportConfiguration(deviceTransportType) | ||
152 | + }; | ||
153 | + this.entityForm.patchValue({deviceData}); | ||
154 | + } else { | ||
155 | + let changed = false; | ||
156 | + if (deviceData.configuration.type !== deviceProfileType) { | ||
157 | + deviceData.configuration = createDeviceConfiguration(deviceProfileType); | ||
158 | + changed = true; | ||
159 | + } | ||
160 | + if (deviceData.transportConfiguration.type !== deviceTransportType) { | ||
161 | + deviceData.transportConfiguration = createDeviceTransportConfiguration(deviceTransportType); | ||
162 | + changed = true; | ||
163 | + } | ||
164 | + if (changed) { | ||
165 | + this.entityForm.patchValue({deviceData}); | ||
166 | + } | ||
167 | + } | ||
168 | + } | ||
169 | + } | ||
129 | } | 170 | } |
@@ -24,9 +24,19 @@ import { DeviceCredentialsDialogComponent } from '@modules/home/pages/device/dev | @@ -24,9 +24,19 @@ import { DeviceCredentialsDialogComponent } from '@modules/home/pages/device/dev | ||
24 | import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; | 24 | import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; |
25 | import { HomeComponentsModule } from '@modules/home/components/home-components.module'; | 25 | import { HomeComponentsModule } from '@modules/home/components/home-components.module'; |
26 | import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; | 26 | import { DeviceTabsComponent } from '@home/pages/device/device-tabs.component'; |
27 | +import { DefaultDeviceConfigurationComponent } from './data/default-device-configuration.component'; | ||
28 | +import { DeviceConfigurationComponent } from './data/device-configuration.component'; | ||
29 | +import { DeviceDataComponent } from './data/device-data.component'; | ||
30 | +import { DefaultDeviceTransportConfigurationComponent } from './data/default-device-transport-configuration.component'; | ||
31 | +import { DeviceTransportConfigurationComponent } from './data/device-transport-configuration.component'; | ||
27 | 32 | ||
28 | @NgModule({ | 33 | @NgModule({ |
29 | declarations: [ | 34 | declarations: [ |
35 | + DefaultDeviceConfigurationComponent, | ||
36 | + DeviceConfigurationComponent, | ||
37 | + DefaultDeviceTransportConfigurationComponent, | ||
38 | + DeviceTransportConfigurationComponent, | ||
39 | + DeviceDataComponent, | ||
30 | DeviceComponent, | 40 | DeviceComponent, |
31 | DeviceTabsComponent, | 41 | DeviceTabsComponent, |
32 | DeviceTableHeaderComponent, | 42 | DeviceTableHeaderComponent, |
@@ -86,6 +86,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | @@ -86,6 +86,8 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev | ||
86 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); | 86 | this.config.entityTranslations = entityTypeTranslations.get(EntityType.DEVICE); |
87 | this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); | 87 | this.config.entityResources = entityTypeResources.get(EntityType.DEVICE); |
88 | 88 | ||
89 | + this.config.addDialogStyle = {width: '600px'}; | ||
90 | + | ||
89 | this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', { deviceName: device.name }); | 91 | this.config.deleteEntityTitle = device => this.translate.instant('device.delete-device-title', { deviceName: device.name }); |
90 | this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); | 92 | this.config.deleteEntityContent = () => this.translate.instant('device.delete-device-text'); |
91 | this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); | 93 | this.config.deleteEntitiesTitle = count => this.translate.instant('device.delete-devices-title', {count}); |
@@ -91,6 +91,19 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic | @@ -91,6 +91,19 @@ export function createDeviceProfileConfiguration(type: DeviceProfileType): Devic | ||
91 | return configuration; | 91 | return configuration; |
92 | } | 92 | } |
93 | 93 | ||
94 | +export function createDeviceConfiguration(type: DeviceProfileType): DeviceConfiguration { | ||
95 | + let configuration: DeviceConfiguration = null; | ||
96 | + if (type) { | ||
97 | + switch (type) { | ||
98 | + case DeviceProfileType.DEFAULT: | ||
99 | + const defaultConfiguration: DefaultDeviceConfiguration = {}; | ||
100 | + configuration = {...defaultConfiguration, type: DeviceProfileType.DEFAULT}; | ||
101 | + break; | ||
102 | + } | ||
103 | + } | ||
104 | + return configuration; | ||
105 | +} | ||
106 | + | ||
94 | export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { | 107 | export function createDeviceProfileTransportConfiguration(type: DeviceTransportType): DeviceProfileTransportConfiguration { |
95 | let transportConfiguration: DeviceProfileTransportConfiguration = null; | 108 | let transportConfiguration: DeviceProfileTransportConfiguration = null; |
96 | if (type) { | 109 | if (type) { |
@@ -112,6 +125,27 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT | @@ -112,6 +125,27 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT | ||
112 | return transportConfiguration; | 125 | return transportConfiguration; |
113 | } | 126 | } |
114 | 127 | ||
128 | +export function createDeviceTransportConfiguration(type: DeviceTransportType): DeviceTransportConfiguration { | ||
129 | + let transportConfiguration: DeviceTransportConfiguration = null; | ||
130 | + if (type) { | ||
131 | + switch (type) { | ||
132 | + case DeviceTransportType.DEFAULT: | ||
133 | + const defaultTransportConfiguration: DefaultDeviceTransportConfiguration = {}; | ||
134 | + transportConfiguration = {...defaultTransportConfiguration, type: DeviceTransportType.DEFAULT}; | ||
135 | + break; | ||
136 | + case DeviceTransportType.MQTT: | ||
137 | + const mqttTransportConfiguration: MqttDeviceTransportConfiguration = {}; | ||
138 | + transportConfiguration = {...mqttTransportConfiguration, type: DeviceTransportType.MQTT}; | ||
139 | + break; | ||
140 | + case DeviceTransportType.LWM2M: | ||
141 | + const lwm2mTransportConfiguration: Lwm2mDeviceTransportConfiguration = {}; | ||
142 | + transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; | ||
143 | + break; | ||
144 | + } | ||
145 | + } | ||
146 | + return transportConfiguration; | ||
147 | +} | ||
148 | + | ||
115 | export interface DeviceProfileData { | 149 | export interface DeviceProfileData { |
116 | configuration: DeviceProfileConfiguration; | 150 | configuration: DeviceProfileConfiguration; |
117 | transportConfiguration: DeviceProfileTransportConfiguration; | 151 | transportConfiguration: DeviceProfileTransportConfiguration; |
@@ -121,7 +155,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId> { | @@ -121,7 +155,7 @@ export interface DeviceProfile extends BaseData<DeviceProfileId> { | ||
121 | tenantId?: TenantId; | 155 | tenantId?: TenantId; |
122 | name: string; | 156 | name: string; |
123 | description?: string; | 157 | description?: string; |
124 | - default: boolean; | 158 | + default?: boolean; |
125 | type: DeviceProfileType; | 159 | type: DeviceProfileType; |
126 | transportType: DeviceTransportType; | 160 | transportType: DeviceTransportType; |
127 | defaultRuleChainId?: RuleChainId; | 161 | defaultRuleChainId?: RuleChainId; |
@@ -748,7 +748,9 @@ | @@ -748,7 +748,9 @@ | ||
748 | "import": "Import device", | 748 | "import": "Import device", |
749 | "device-file": "Device file", | 749 | "device-file": "Device file", |
750 | "search": "Search devices", | 750 | "search": "Search devices", |
751 | - "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected" | 751 | + "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected", |
752 | + "device-configuration": "Device configuration", | ||
753 | + "transport-configuration": "Transport configuration" | ||
752 | }, | 754 | }, |
753 | "device-profile": { | 755 | "device-profile": { |
754 | "device-profile": "Device profile", | 756 | "device-profile": "Device profile", |