Commit f2af7d0831b9359cebf76452510901cb26ce40c4

Authored by Vladyslav_Prykhodko
1 parent 4f306785

UI add device wizard

... ... @@ -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, &apos;...&apos;)}) | async }}
58 58 </span>
  59 + <span>
  60 + <a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
  61 + </span>
59 62 </ng-template>
60   - <span>
61   - <a translate (click)="createDeviceProfile($event, searchText)">device-profile.create-new-device-profile</a>
62   - </span>
63 63 </div>
64 64 </mat-option>
65 65 </mat-autocomplete>
... ...
... ... @@ -19,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",
... ...