Showing
11 changed files
with
562 additions
and
29 deletions
... | ... | @@ -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 | } | ... | ... |
... | ... | @@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k |
107 | 107 | import { FilterTextComponent } from './filter/filter-text.component'; |
108 | 108 | import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; |
109 | 109 | import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; |
110 | +import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; | |
110 | 111 | import { DeviceCredentialsComponent } from './device/device-credentials.component'; |
111 | 112 | |
112 | 113 | @NgModule({ |
... | ... | @@ -198,6 +199,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen |
198 | 199 | DeviceProfileDialogComponent, |
199 | 200 | AddDeviceProfileDialogComponent, |
200 | 201 | RuleChainAutocompleteComponent, |
202 | + DeviceWizardDialogComponent, | |
201 | 203 | DeviceCredentialsComponent |
202 | 204 | ], |
203 | 205 | imports: [ |
... | ... | @@ -278,6 +280,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen |
278 | 280 | DeviceProfileDialogComponent, |
279 | 281 | AddDeviceProfileDialogComponent, |
280 | 282 | RuleChainAutocompleteComponent, |
283 | + DeviceWizardDialogComponent, | |
281 | 284 | DeviceCredentialsComponent |
282 | 285 | ], |
283 | 286 | providers: [ | ... | ... |
... | ... | @@ -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({ |
... | ... | @@ -97,7 +98,7 @@ export class AddDeviceProfileDialogComponent extends |
97 | 98 | ); |
98 | 99 | this.transportConfigFormGroup = this.fb.group( |
99 | 100 | { |
100 | - transportType: [DeviceTransportType.DEFAULT, [Validators.required]], | |
101 | + transportType: [data.transportType ? data.transportType : DeviceTransportType.DEFAULT, [Validators.required]], | |
101 | 102 | transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), |
102 | 103 | [Validators.required]] |
103 | 104 | } | ... | ... |
... | ... | @@ -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,7 +19,8 @@ import { |
19 | 19 | ElementRef, |
20 | 20 | EventEmitter, |
21 | 21 | forwardRef, |
22 | - Input, NgZone, | |
22 | + Input, | |
23 | + NgZone, | |
23 | 24 | OnInit, |
24 | 25 | Output, |
25 | 26 | ViewChild |
... | ... | @@ -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'; |
... | ... | @@ -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; |
... | ... | @@ -183,6 +183,11 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, |
183 | 183 | |
184 | 184 | setDisabledState(isDisabled: boolean): void { |
185 | 185 | this.disabled = isDisabled; |
186 | + if (this.disabled) { | |
187 | + this.selectDeviceProfileFormGroup.disable(); | |
188 | + } else { | |
189 | + this.selectDeviceProfileFormGroup.enable(); | |
190 | + } | |
186 | 191 | } |
187 | 192 | |
188 | 193 | writeValue(value: DeviceProfileId | null): void { |
... | ... | @@ -244,7 +249,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, |
244 | 249 | property: 'name', |
245 | 250 | direction: Direction.ASC |
246 | 251 | }); |
247 | - return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe( | |
252 | + return this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).pipe( | |
248 | 253 | map(pageData => { |
249 | 254 | let data = pageData.data; |
250 | 255 | if (this.displayAllOnEmpty) { |
... | ... | @@ -280,9 +285,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, |
280 | 285 | createDeviceProfile($event: Event, profileName: string) { |
281 | 286 | $event.preventDefault(); |
282 | 287 | const deviceProfile: DeviceProfile = { |
283 | - name: profileName | |
288 | + name: profileName, | |
289 | + transportType: this.transportType | |
284 | 290 | } as DeviceProfile; |
285 | - this.openDeviceProfileDialog(deviceProfile, true); | |
291 | + if (this.addNewProfile) { | |
292 | + this.openDeviceProfileDialog(deviceProfile, true); | |
293 | + } | |
286 | 294 | } |
287 | 295 | |
288 | 296 | editDeviceProfile($event: Event) { |
... | ... | @@ -312,7 +320,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, |
312 | 320 | disableClose: true, |
313 | 321 | panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], |
314 | 322 | data: { |
315 | - deviceProfileName: deviceProfile.name | |
323 | + deviceProfileName: deviceProfile.name, | |
324 | + transportType: deviceProfile.transportType | |
316 | 325 | } |
317 | 326 | }).afterClosed(); |
318 | 327 | } | ... | ... |
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 style="min-width: 1000px;"> | |
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" #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"> | |
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-error *ngIf="deviceWizardFormGroup.get('transportType').hasError('required')"> | |
59 | + {{ 'device-profile.transport-type-required' | translate }} | |
60 | + </mat-error> | |
61 | + </mat-form-field> | |
62 | + <mat-checkbox formControlName="gateway" style="padding-bottom: 16px;"> | |
63 | + {{ 'device.is-gateway' | translate }} | |
64 | + </mat-checkbox> | |
65 | + <mat-form-field class="mat-block"> | |
66 | + <mat-label translate>device.description</mat-label> | |
67 | + <textarea matInput formControlName="description" rows="2"></textarea> | |
68 | + </mat-form-field> | |
69 | + </fieldset> | |
70 | + </form> | |
71 | + </mat-step> | |
72 | + <mat-step [stepControl]="profileConfigFormGroup"> | |
73 | + <form [formGroup]="profileConfigFormGroup" style="padding-bottom: 16px;"> | |
74 | + <ng-template matStepLabel>{{ 'device.wizard.profile-configuration' | translate}}</ng-template> | |
75 | + <mat-radio-group fxLayout="column" fxFlex formControlName="addProfileType"> | |
76 | + <mat-radio-button [value]="0" color="primary"> | |
77 | + <section> | |
78 | + <span translate>device.wizard.existing-device-profile</span> | |
79 | + <tb-device-profile-autocomplete | |
80 | + [required]="profileConfigFormGroup.get('addProfileType').value === 0" | |
81 | + [transportType]="deviceWizardFormGroup.get('transportType').value" | |
82 | + formControlName="deviceProfileId" | |
83 | + [addNewProfile]="false" | |
84 | + [editProfileEnabled]="false"> | |
85 | + </tb-device-profile-autocomplete> | |
86 | + </section> | |
87 | + </mat-radio-button> | |
88 | + <mat-radio-button [value]="1" color="primary"> | |
89 | + <section fxLayout="column"> | |
90 | + <span translate>device.wizard.new-device-profile</span> | |
91 | + <mat-form-field fxFlex class="mat-block"> | |
92 | + <mat-label translate>device-profile.device-profile</mat-label> | |
93 | + <input matInput formControlName="newDeviceProfileTitle" | |
94 | + [required]="profileConfigFormGroup.get('addProfileType').value === 1"> | |
95 | + <mat-error *ngIf="profileConfigFormGroup.get('newDeviceProfileTitle').hasError('required')"> | |
96 | + {{ 'device-profile.device-profile-required' | translate }} | |
97 | + </mat-error> | |
98 | + </mat-form-field> | |
99 | + </section> | |
100 | + </mat-radio-button> | |
101 | + </mat-radio-group> | |
102 | + </form> | |
103 | + </mat-step> | |
104 | + <mat-step [stepControl]="transportConfigFormGroup" *ngIf="createdProfile"> | |
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" *ngIf="createdProfile"> | |
114 | + <form [formGroup]="alarmRulesFormGroup" style="padding-bottom: 16px;"> | |
115 | + <ng-template matStepLabel>{{'device-profile.alarm-rules' | 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]="specificConfigFormGroup"> | |
124 | + <ng-template matStepLabel>{{ 'device.wizard.specific-configuration' | translate }}</ng-template> | |
125 | + <form [formGroup]="specificConfigFormGroup" style="padding-bottom: 16px;"> | |
126 | + <tb-entity-autocomplete | |
127 | + formControlName="customerId" | |
128 | + labelText="device.wizard.customer-to-assign-device" | |
129 | + [entityType]="entityType.CUSTOMER"> | |
130 | + </tb-entity-autocomplete> | |
131 | + <mat-checkbox formControlName="setCredential">{{ 'device.wizard.add-credential' | translate }}</mat-checkbox> | |
132 | + <tb-device-credentials | |
133 | + [fxShow]="specificConfigFormGroup.get('setCredential').value" | |
134 | + formControlName="credential"> | |
135 | + </tb-device-credentials> | |
136 | + </form> | |
137 | + </mat-step> | |
138 | + </mat-horizontal-stepper> | |
139 | + </div> | |
140 | + <div mat-dialog-actions fxLayout="row wrap" fxLayoutAlign="space-between center"> | |
141 | + <button mat-button *ngIf="selectedIndex > 0" | |
142 | + [disabled]="(isLoading$ | async)" | |
143 | + (click)="previousStep()">{{ 'action.back' | translate }}</button> | |
144 | + <span *ngIf="selectedIndex == 0"></span> | |
145 | + <div fxLayout="row wrap" fxLayoutGap="20px"> | |
146 | + <button mat-button | |
147 | + [disabled]="(isLoading$ | async)" | |
148 | + (click)="cancel()">{{ 'action.cancel' | translate }}</button> | |
149 | + <button mat-raised-button | |
150 | + [disabled]="(isLoading$ | async) || selectedForm.invalid" | |
151 | + color="primary" | |
152 | + (click)="nextStep()">{{ nextStepButtonLabel$ | async | translate }}</button> | |
153 | + </div> | |
154 | + </div> | |
155 | +</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 | +:host { | |
17 | + .mat-dialog-content { | |
18 | + display: flex; | |
19 | + flex-direction: column; | |
20 | + overflow: hidden; | |
21 | + | |
22 | + .mat-stepper-horizontal { | |
23 | + display: flex; | |
24 | + flex-direction: column; | |
25 | + overflow: hidden; | |
26 | + } | |
27 | + } | |
28 | +} | |
29 | + | |
30 | +:host ::ng-deep { | |
31 | + .mat-dialog-content { | |
32 | + .mat-stepper-horizontal { | |
33 | + .mat-horizontal-content-container { | |
34 | + overflow: auto; | |
35 | + } | |
36 | + } | |
37 | + } | |
38 | +} | ... | ... |
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, | |
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 { BehaviorSubject, 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 | + | |
44 | +@Component({ | |
45 | + selector: 'tb-device-wizard', | |
46 | + templateUrl: './device-wizard-dialog.component.html', | |
47 | + providers: [], | |
48 | + styleUrls: ['./device-wizard-dialog.component.scss'] | |
49 | +}) | |
50 | +export class DeviceWizardDialogComponent extends | |
51 | + DialogComponent<DeviceWizardDialogComponent, boolean> implements OnDestroy, ErrorStateMatcher { | |
52 | + | |
53 | + @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper; | |
54 | + | |
55 | + selectedIndex = 0; | |
56 | + | |
57 | + nextStepButtonLabel$ = new BehaviorSubject<string>('action.continue'); | |
58 | + | |
59 | + createdProfile = false; | |
60 | + | |
61 | + entityType = EntityType; | |
62 | + | |
63 | + deviceTransportTypes = Object.keys(DeviceTransportType); | |
64 | + | |
65 | + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; | |
66 | + | |
67 | + deviceWizardFormGroup: FormGroup; | |
68 | + | |
69 | + profileConfigFormGroup: FormGroup; | |
70 | + | |
71 | + transportConfigFormGroup: FormGroup; | |
72 | + | |
73 | + alarmRulesFormGroup: FormGroup; | |
74 | + | |
75 | + specificConfigFormGroup: FormGroup; | |
76 | + | |
77 | + private subscriptions: Subscription[] = []; | |
78 | + | |
79 | + constructor(protected store: Store<AppState>, | |
80 | + protected router: Router, | |
81 | + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData<BaseData<EntityId>>, | |
82 | + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, | |
83 | + public dialogRef: MatDialogRef<DeviceWizardDialogComponent, boolean>, | |
84 | + private deviceProfileService: DeviceProfileService, | |
85 | + private deviceService: DeviceService, | |
86 | + private fb: FormBuilder) { | |
87 | + super(store, router, dialogRef); | |
88 | + this.deviceWizardFormGroup = this.fb.group({ | |
89 | + name: ['', Validators.required], | |
90 | + label: [''], | |
91 | + gateway: [false], | |
92 | + transportType: [DeviceTransportType.DEFAULT, Validators.required], | |
93 | + description: [''] | |
94 | + } | |
95 | + ); | |
96 | + | |
97 | + this.profileConfigFormGroup = this.fb.group({ | |
98 | + addProfileType: [0], | |
99 | + deviceProfileId: [null, Validators.required], | |
100 | + newDeviceProfileTitle: [{value: null, disabled: true}] | |
101 | + } | |
102 | + ); | |
103 | + | |
104 | + this.subscriptions.push(this.profileConfigFormGroup.get('addProfileType').valueChanges.subscribe( | |
105 | + (addProfileType: number) => { | |
106 | + if (addProfileType === 0) { | |
107 | + this.profileConfigFormGroup.get('deviceProfileId').setValidators([Validators.required]); | |
108 | + this.profileConfigFormGroup.get('deviceProfileId').enable(); | |
109 | + this.profileConfigFormGroup.get('newDeviceProfileTitle').setValidators(null); | |
110 | + this.profileConfigFormGroup.get('newDeviceProfileTitle').disable(); | |
111 | + this.profileConfigFormGroup.updateValueAndValidity(); | |
112 | + this.createdProfile = false; | |
113 | + } else { | |
114 | + this.profileConfigFormGroup.get('deviceProfileId').setValidators(null); | |
115 | + this.profileConfigFormGroup.get('deviceProfileId').disable(); | |
116 | + this.profileConfigFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]); | |
117 | + this.profileConfigFormGroup.get('newDeviceProfileTitle').enable(); | |
118 | + this.profileConfigFormGroup.updateValueAndValidity(); | |
119 | + this.createdProfile = true; | |
120 | + } | |
121 | + } | |
122 | + )); | |
123 | + | |
124 | + this.transportConfigFormGroup = this.fb.group( | |
125 | + { | |
126 | + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required] | |
127 | + } | |
128 | + ); | |
129 | + this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => { | |
130 | + this.deviceProfileTransportTypeChanged(transportType); | |
131 | + })); | |
132 | + | |
133 | + this.alarmRulesFormGroup = this.fb.group({ | |
134 | + alarms: [null] | |
135 | + } | |
136 | + ); | |
137 | + | |
138 | + this.specificConfigFormGroup = this.fb.group({ | |
139 | + customerId: [null], | |
140 | + setCredential: [false], | |
141 | + credential: [{value: null, disabled: true}] | |
142 | + } | |
143 | + ); | |
144 | + | |
145 | + this.subscriptions.push(this.specificConfigFormGroup.get('setCredential').valueChanges.subscribe((value) => { | |
146 | + if (value) { | |
147 | + this.specificConfigFormGroup.get('credential').enable(); | |
148 | + } else { | |
149 | + this.specificConfigFormGroup.get('credential').disable(); | |
150 | + } | |
151 | + })); | |
152 | + } | |
153 | + | |
154 | + ngOnDestroy() { | |
155 | + super.ngOnDestroy(); | |
156 | + this.subscriptions.forEach(s => s.unsubscribe()); | |
157 | + } | |
158 | + | |
159 | + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { | |
160 | + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); | |
161 | + const customErrorState = !!(control && control.invalid); | |
162 | + return originalErrorState || customErrorState; | |
163 | + } | |
164 | + | |
165 | + cancel(): void { | |
166 | + this.dialogRef.close(null); | |
167 | + } | |
168 | + | |
169 | + previousStep(): void { | |
170 | + this.addDeviceWizardStepper.previous(); | |
171 | + } | |
172 | + | |
173 | + nextStep(): void { | |
174 | + if (this.selectedIndex < this.maxStepperIndex) { | |
175 | + this.addDeviceWizardStepper.next(); | |
176 | + } else { | |
177 | + this.add(); | |
178 | + } | |
179 | + } | |
180 | + | |
181 | + get selectedForm(): FormGroup { | |
182 | + const index = !this.createdProfile && this.selectedIndex === this.maxStepperIndex ? 4 : this.selectedIndex; | |
183 | + switch (index) { | |
184 | + case 0: | |
185 | + return this.deviceWizardFormGroup; | |
186 | + case 1: | |
187 | + return this.profileConfigFormGroup; | |
188 | + case 2: | |
189 | + return this.transportConfigFormGroup; | |
190 | + case 3: | |
191 | + return this.alarmRulesFormGroup; | |
192 | + case 4: | |
193 | + return this.specificConfigFormGroup; | |
194 | + } | |
195 | + } | |
196 | + | |
197 | + get maxStepperIndex(): number { | |
198 | + return this.addDeviceWizardStepper?._steps?.length - 1; | |
199 | + } | |
200 | + | |
201 | + private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void { | |
202 | + this.transportConfigFormGroup.patchValue( | |
203 | + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); | |
204 | + } | |
205 | + | |
206 | + private add(): void { | |
207 | + this.creatProfile().pipe( | |
208 | + mergeMap(profileId => this.createdDevice(profileId)), | |
209 | + mergeMap(device => this.saveCredential(device)) | |
210 | + ).subscribe( | |
211 | + (created) => { | |
212 | + this.dialogRef.close(created); | |
213 | + } | |
214 | + ); | |
215 | + } | |
216 | + | |
217 | + private creatProfile(): Observable<EntityId> { | |
218 | + if (this.profileConfigFormGroup.get('addProfileType').value) { | |
219 | + const deviceProfile: DeviceProfile = { | |
220 | + name: this.profileConfigFormGroup.get('newDeviceProfileTitle').value, | |
221 | + type: DeviceProfileType.DEFAULT, | |
222 | + transportType: this.deviceWizardFormGroup.get('transportType').value, | |
223 | + profileData: { | |
224 | + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), | |
225 | + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, | |
226 | + alarms: this.alarmRulesFormGroup.get('alarms').value | |
227 | + } | |
228 | + }; | |
229 | + return this.deviceProfileService.saveDeviceProfile(deviceProfile).pipe( | |
230 | + map(profile => profile.id), | |
231 | + tap((profileId) => { | |
232 | + this.profileConfigFormGroup.patchValue({ | |
233 | + deviceProfileId: profileId, | |
234 | + addProfileType: 0 | |
235 | + }); | |
236 | + this.addDeviceWizardStepper.selectedIndex = 2; | |
237 | + }) | |
238 | + ); | |
239 | + } else { | |
240 | + return of(null); | |
241 | + } | |
242 | + } | |
243 | + | |
244 | + private createdDevice(profileId: EntityId = this.profileConfigFormGroup.get('deviceProfileId').value): Observable<BaseData<HasId>> { | |
245 | + const device = { | |
246 | + name: this.deviceWizardFormGroup.get('name').value, | |
247 | + label: this.deviceWizardFormGroup.get('label').value, | |
248 | + deviceProfileId: profileId, | |
249 | + additionalInfo: { | |
250 | + gateway: this.deviceWizardFormGroup.get('gateway').value, | |
251 | + description: this.deviceWizardFormGroup.get('description').value | |
252 | + }, | |
253 | + customerId: null | |
254 | + }; | |
255 | + if (this.specificConfigFormGroup.get('customerId').value) { | |
256 | + device.customerId = { | |
257 | + entityType: EntityType.CUSTOMER, | |
258 | + id: this.specificConfigFormGroup.get('customerId').value | |
259 | + }; | |
260 | + } | |
261 | + return this.data.entitiesTableConfig.saveEntity(device); | |
262 | + } | |
263 | + | |
264 | + private saveCredential(device: BaseData<HasId>): Observable<boolean> { | |
265 | + if (this.specificConfigFormGroup.get('setCredential').value) { | |
266 | + return this.deviceService.getDeviceCredentials(device.id.id).pipe( | |
267 | + mergeMap( | |
268 | + (deviceCredentials) => { | |
269 | + const deviceCredentialsValue = {...deviceCredentials, ...this.specificConfigFormGroup.value.credential}; | |
270 | + return this.deviceService.saveDeviceCredentials(deviceCredentialsValue); | |
271 | + } | |
272 | + ), | |
273 | + map(() => true)); | |
274 | + } | |
275 | + return of(true); | |
276 | + } | |
277 | + | |
278 | + changeStep($event: StepperSelectionEvent): void { | |
279 | + this.selectedIndex = $event.selectedIndex; | |
280 | + if (this.selectedIndex === this.maxStepperIndex) { | |
281 | + this.nextStepButtonLabel$.next('action.add'); | |
282 | + } else { | |
283 | + this.nextStepButtonLabel$.next('action.continue'); | |
284 | + } | |
285 | + } | |
286 | +} | ... | ... |
... | ... | @@ -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 | } | ... | ... |
... | ... | @@ -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 | ); |
... | ... | @@ -301,7 +303,13 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
301 | 303 | icon: 'file_upload', |
302 | 304 | isEnabled: () => true, |
303 | 305 | onAction: ($event) => this.importDevices($event) |
304 | - } | |
306 | + }, | |
307 | + { | |
308 | + name: this.translate.instant('device.wizard.device-wizard'), | |
309 | + icon: 'library_add', | |
310 | + isEnabled: () => true, | |
311 | + onAction: ($event) => this.deviceWizard($event) | |
312 | + }, | |
305 | 313 | ); |
306 | 314 | } |
307 | 315 | if (deviceScope === 'customer') { |
... | ... | @@ -326,6 +334,23 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
326 | 334 | }); |
327 | 335 | } |
328 | 336 | |
337 | + deviceWizard($event: Event) { | |
338 | + this.dialog.open<DeviceWizardDialogComponent, AddEntityDialogData<BaseData<HasId>>, | |
339 | + boolean>(DeviceWizardDialogComponent, { | |
340 | + disableClose: true, | |
341 | + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], | |
342 | + data: { | |
343 | + entitiesTableConfig: this.config.table.entitiesTableConfig | |
344 | + } | |
345 | + }).afterClosed().subscribe( | |
346 | + (res) => { | |
347 | + if (res) { | |
348 | + this.config.table.updateData(); | |
349 | + } | |
350 | + } | |
351 | + ); | |
352 | + } | |
353 | + | |
329 | 354 | addDevicesToCustomer($event: Event) { |
330 | 355 | if ($event) { |
331 | 356 | $event.stopPropagation(); |
... | ... | @@ -480,5 +505,4 @@ export class DevicesTableConfigResolver implements Resolve<EntityTableConfig<Dev |
480 | 505 | } |
481 | 506 | return false; |
482 | 507 | } |
483 | - | |
484 | 508 | } | ... | ... |
... | ... | @@ -756,7 +756,17 @@ |
756 | 756 | "search": "Search devices", |
757 | 757 | "selected-devices": "{ count, plural, 1 {1 device} other {# devices} } selected", |
758 | 758 | "device-configuration": "Device configuration", |
759 | - "transport-configuration": "Transport configuration" | |
759 | + "transport-configuration": "Transport configuration", | |
760 | + "wizard": { | |
761 | + "device-wizard": "Device Wizard", | |
762 | + "device-details": "Device details", | |
763 | + "profile-configuration": "Profile configuration", | |
764 | + "new-device-profile": "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", | ... | ... |