From f2af7d0831b9359cebf76452510901cb26ce40c4 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 29 Sep 2020 15:47:17 +0300 Subject: [PATCH] UI add device wizard --- .../app/core/http/device-profile.service.ts | 12 +- .../home/components/home-components.module.ts | 3 + .../add-device-profile-dialog.component.ts | 3 +- ...device-profile-autocomplete.component.html | 8 +- .../device-profile-autocomplete.component.ts | 35 ++- .../device-wizard-dialog.component.html | 155 ++++++++++ .../device-wizard-dialog.component.scss | 38 +++ .../wizard/device-wizard-dialog.component.ts | 286 ++++++++++++++++++ .../device-profiles-table-config.resolver.ts | 3 +- .../device/devices-table-config.resolver.ts | 36 ++- .../assets/locale/locale.constant-en_US.json | 12 +- 11 files changed, 562 insertions(+), 29 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts diff --git a/ui-ngx/src/app/core/http/device-profile.service.ts b/ui-ngx/src/app/core/http/device-profile.service.ts index 8cdc8c188b..c535878fd2 100644 --- a/ui-ngx/src/app/core/http/device-profile.service.ts +++ b/ui-ngx/src/app/core/http/device-profile.service.ts @@ -20,7 +20,8 @@ import { PageLink } from '@shared/models/page/page-link'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; import { Observable } from 'rxjs'; import { PageData } from '@shared/models/page/page-data'; -import { DeviceProfile, DeviceProfileInfo } from '@shared/models/device.models'; +import { DeviceProfile, DeviceProfileInfo, DeviceTransportType } from '@shared/models/device.models'; +import { isDefinedAndNotNull } from '@core/utils'; @Injectable({ providedIn: 'root' @@ -59,8 +60,13 @@ export class DeviceProfileService { return this.http.get(`/api/deviceProfileInfo/${deviceProfileId}`, defaultHttpOptionsFromConfig(config)); } - public getDeviceProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/deviceProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getDeviceProfileInfos(pageLink: PageLink, transportType?: DeviceTransportType, + config?: RequestConfig): Observable> { + let url = `/api/deviceProfileInfos${pageLink.toQuery()}`; + if (isDefinedAndNotNull(transportType)) { + url += `&transportType=${transportType}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } } diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 2bbf2cb01d..f3d912e16d 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -107,6 +107,7 @@ import { AlarmRuleKeyFiltersDialogComponent } from './profile/alarm/alarm-rule-k import { FilterTextComponent } from './filter/filter-text.component'; import { AddDeviceProfileDialogComponent } from './profile/add-device-profile-dialog.component'; import { RuleChainAutocompleteComponent } from './rule-chain/rule-chain-autocomplete.component'; +import { DeviceWizardDialogComponent } from './wizard/device-wizard-dialog.component'; import { DeviceCredentialsComponent } from './device/device-credentials.component'; @NgModule({ @@ -198,6 +199,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, RuleChainAutocompleteComponent, + DeviceWizardDialogComponent, DeviceCredentialsComponent ], imports: [ @@ -278,6 +280,7 @@ import { DeviceCredentialsComponent } from './device/device-credentials.componen DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, RuleChainAutocompleteComponent, + DeviceWizardDialogComponent, DeviceCredentialsComponent ], providers: [ diff --git a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts index 476c69488f..a88b250c7e 100644 --- a/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/add-device-profile-dialog.component.ts @@ -46,6 +46,7 @@ import { RuleChainId } from '@shared/models/id/rule-chain-id'; export interface AddDeviceProfileDialogData { deviceProfileName: string; + transportType: DeviceTransportType; } @Component({ @@ -97,7 +98,7 @@ export class AddDeviceProfileDialogComponent extends ); this.transportConfigFormGroup = this.fb.group( { - transportType: [DeviceTransportType.DEFAULT, [Validators.required]], + transportType: [data.transportType ? data.transportType : DeviceTransportType.DEFAULT, [Validators.required]], transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), [Validators.required]] } diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html index a8af9c2738..9941b1db94 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.html @@ -48,7 +48,7 @@
-
+
device-profile.no-device-profiles-found
@@ -56,10 +56,10 @@ {{ translate.get('device-profile.no-device-profiles-matching', {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + device-profile.create-new-device-profile + - - device-profile.create-new-device-profile -
diff --git a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts index c419b7f11c..835c31a477 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device-profile-autocomplete.component.ts @@ -19,7 +19,8 @@ import { ElementRef, EventEmitter, forwardRef, - Input, NgZone, + Input, + NgZone, OnInit, Output, ViewChild @@ -38,14 +39,7 @@ import { TruncatePipe } from '@shared//pipe/truncate.pipe'; import { ENTER } from '@angular/cdk/keycodes'; import { MatDialog } from '@angular/material/dialog'; import { DeviceProfileId } from '@shared/models/id/device-profile-id'; -import { - createDeviceProfileConfiguration, - createDeviceProfileTransportConfiguration, - DeviceProfile, - DeviceProfileInfo, - DeviceProfileType, - DeviceTransportType -} from '@shared/models/device.models'; +import { DeviceProfile, DeviceProfileInfo, DeviceProfileType, DeviceTransportType } from '@shared/models/device.models'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { DeviceProfileDialogComponent, DeviceProfileDialogData } from './device-profile-dialog.component'; import { MatAutocomplete } from '@angular/material/autocomplete'; @@ -76,6 +70,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, @Input() editProfileEnabled = true; + @Input() + addNewProfile = true; + + @Input() + transportType: DeviceTransportType = null; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -183,6 +183,11 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; + if (this.disabled) { + this.selectDeviceProfileFormGroup.disable(); + } else { + this.selectDeviceProfileFormGroup.enable(); + } } writeValue(value: DeviceProfileId | null): void { @@ -244,7 +249,7 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, property: 'name', direction: Direction.ASC }); - return this.deviceProfileService.getDeviceProfileInfos(pageLink, {ignoreLoading: true}).pipe( + return this.deviceProfileService.getDeviceProfileInfos(pageLink, this.transportType, {ignoreLoading: true}).pipe( map(pageData => { let data = pageData.data; if (this.displayAllOnEmpty) { @@ -280,9 +285,12 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, createDeviceProfile($event: Event, profileName: string) { $event.preventDefault(); const deviceProfile: DeviceProfile = { - name: profileName + name: profileName, + transportType: this.transportType } as DeviceProfile; - this.openDeviceProfileDialog(deviceProfile, true); + if (this.addNewProfile) { + this.openDeviceProfileDialog(deviceProfile, true); + } } editDeviceProfile($event: Event) { @@ -312,7 +320,8 @@ export class DeviceProfileAutocompleteComponent implements ControlValueAccessor, disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - deviceProfileName: deviceProfile.name + deviceProfileName: deviceProfile.name, + transportType: deviceProfile.transportType } }).afterClosed(); } diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html new file mode 100644 index 0000000000..c391bc848a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.html @@ -0,0 +1,155 @@ + +
+ +

device.add-device-text

+ + +
+ + +
+
+ + + check + + +
+ {{ 'device.wizard.device-details' | translate}} +
+ + device.name + + + {{ 'device.name-required' | translate }} + + + + device.label + + + + device-profile.transport-type + + + {{deviceTransportTypeTranslations.get(type) | translate}} + + + + {{ 'device-profile.transport-type-required' | translate }} + + + + {{ 'device.is-gateway' | translate }} + + + device.description + + +
+
+
+ +
+ {{ 'device.wizard.profile-configuration' | translate}} + + +
+ device.wizard.existing-device-profile + + +
+
+ +
+ device.wizard.new-device-profile + + device-profile.device-profile + + + {{ 'device-profile.device-profile-required' | translate }} + + +
+
+
+
+
+ +
+ {{ 'device-profile.transport-configuration' | translate }} + + +
+
+ +
+ {{'device-profile.alarm-rules' | translate: + {count: alarmRulesFormGroup.get('alarms').value ? + alarmRulesFormGroup.get('alarms').value.length : 0} }} + + +
+
+ + {{ 'device.wizard.specific-configuration' | translate }} +
+ + + {{ 'device.wizard.add-credential' | translate }} + + +
+
+
+
+
+ + +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss new file mode 100644 index 0000000000..cafcce74b6 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.scss @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2020 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .mat-dialog-content { + display: flex; + flex-direction: column; + overflow: hidden; + + .mat-stepper-horizontal { + display: flex; + flex-direction: column; + overflow: hidden; + } + } +} + +:host ::ng-deep { + .mat-dialog-content { + .mat-stepper-horizontal { + .mat-horizontal-content-container { + overflow: auto; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts new file mode 100644 index 0000000000..15efa868b2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/wizard/device-wizard-dialog.component.ts @@ -0,0 +1,286 @@ +/// +/// Copyright © 2016-2020 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject, OnDestroy, SkipSelf, ViewChild } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { + createDeviceProfileConfiguration, + createDeviceProfileTransportConfiguration, + DeviceProfile, + DeviceProfileType, + DeviceTransportType, + deviceTransportTypeTranslationMap +} from '@shared/models/device.models'; +import { MatHorizontalStepper } from '@angular/material/stepper'; +import { AddEntityDialogData } from '@home/models/entity/entity-component.models'; +import { BaseData, HasId } from '@shared/models/base-data'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DeviceProfileService } from '@core/http/device-profile.service'; +import { EntityId } from '@shared/models/id/entity-id'; +import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'; +import { map, mergeMap, tap } from 'rxjs/operators'; +import { DeviceService } from '@core/http/device.service'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; + +@Component({ + selector: 'tb-device-wizard', + templateUrl: './device-wizard-dialog.component.html', + providers: [], + styleUrls: ['./device-wizard-dialog.component.scss'] +}) +export class DeviceWizardDialogComponent extends + DialogComponent implements OnDestroy, ErrorStateMatcher { + + @ViewChild('addDeviceWizardStepper', {static: true}) addDeviceWizardStepper: MatHorizontalStepper; + + selectedIndex = 0; + + nextStepButtonLabel$ = new BehaviorSubject('action.continue'); + + createdProfile = false; + + entityType = EntityType; + + deviceTransportTypes = Object.keys(DeviceTransportType); + + deviceTransportTypeTranslations = deviceTransportTypeTranslationMap; + + deviceWizardFormGroup: FormGroup; + + profileConfigFormGroup: FormGroup; + + transportConfigFormGroup: FormGroup; + + alarmRulesFormGroup: FormGroup; + + specificConfigFormGroup: FormGroup; + + private subscriptions: Subscription[] = []; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AddEntityDialogData>, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + public dialogRef: MatDialogRef, + private deviceProfileService: DeviceProfileService, + private deviceService: DeviceService, + private fb: FormBuilder) { + super(store, router, dialogRef); + this.deviceWizardFormGroup = this.fb.group({ + name: ['', Validators.required], + label: [''], + gateway: [false], + transportType: [DeviceTransportType.DEFAULT, Validators.required], + description: [''] + } + ); + + this.profileConfigFormGroup = this.fb.group({ + addProfileType: [0], + deviceProfileId: [null, Validators.required], + newDeviceProfileTitle: [{value: null, disabled: true}] + } + ); + + this.subscriptions.push(this.profileConfigFormGroup.get('addProfileType').valueChanges.subscribe( + (addProfileType: number) => { + if (addProfileType === 0) { + this.profileConfigFormGroup.get('deviceProfileId').setValidators([Validators.required]); + this.profileConfigFormGroup.get('deviceProfileId').enable(); + this.profileConfigFormGroup.get('newDeviceProfileTitle').setValidators(null); + this.profileConfigFormGroup.get('newDeviceProfileTitle').disable(); + this.profileConfigFormGroup.updateValueAndValidity(); + this.createdProfile = false; + } else { + this.profileConfigFormGroup.get('deviceProfileId').setValidators(null); + this.profileConfigFormGroup.get('deviceProfileId').disable(); + this.profileConfigFormGroup.get('newDeviceProfileTitle').setValidators([Validators.required]); + this.profileConfigFormGroup.get('newDeviceProfileTitle').enable(); + this.profileConfigFormGroup.updateValueAndValidity(); + this.createdProfile = true; + } + } + )); + + this.transportConfigFormGroup = this.fb.group( + { + transportConfiguration: [createDeviceProfileTransportConfiguration(DeviceTransportType.DEFAULT), Validators.required] + } + ); + this.subscriptions.push(this.deviceWizardFormGroup.get('transportType').valueChanges.subscribe((transportType) => { + this.deviceProfileTransportTypeChanged(transportType); + })); + + this.alarmRulesFormGroup = this.fb.group({ + alarms: [null] + } + ); + + this.specificConfigFormGroup = this.fb.group({ + customerId: [null], + setCredential: [false], + credential: [{value: null, disabled: true}] + } + ); + + this.subscriptions.push(this.specificConfigFormGroup.get('setCredential').valueChanges.subscribe((value) => { + if (value) { + this.specificConfigFormGroup.get('credential').enable(); + } else { + this.specificConfigFormGroup.get('credential').disable(); + } + })); + } + + ngOnDestroy() { + super.ngOnDestroy(); + this.subscriptions.forEach(s => s.unsubscribe()); + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + previousStep(): void { + this.addDeviceWizardStepper.previous(); + } + + nextStep(): void { + if (this.selectedIndex < this.maxStepperIndex) { + this.addDeviceWizardStepper.next(); + } else { + this.add(); + } + } + + get selectedForm(): FormGroup { + const index = !this.createdProfile && this.selectedIndex === this.maxStepperIndex ? 4 : this.selectedIndex; + switch (index) { + case 0: + return this.deviceWizardFormGroup; + case 1: + return this.profileConfigFormGroup; + case 2: + return this.transportConfigFormGroup; + case 3: + return this.alarmRulesFormGroup; + case 4: + return this.specificConfigFormGroup; + } + } + + get maxStepperIndex(): number { + return this.addDeviceWizardStepper?._steps?.length - 1; + } + + private deviceProfileTransportTypeChanged(deviceTransportType: DeviceTransportType): void { + this.transportConfigFormGroup.patchValue( + {transportConfiguration: createDeviceProfileTransportConfiguration(deviceTransportType)}); + } + + private add(): void { + this.creatProfile().pipe( + mergeMap(profileId => this.createdDevice(profileId)), + mergeMap(device => this.saveCredential(device)) + ).subscribe( + (created) => { + this.dialogRef.close(created); + } + ); + } + + private creatProfile(): Observable { + if (this.profileConfigFormGroup.get('addProfileType').value) { + const deviceProfile: DeviceProfile = { + name: this.profileConfigFormGroup.get('newDeviceProfileTitle').value, + type: DeviceProfileType.DEFAULT, + transportType: this.deviceWizardFormGroup.get('transportType').value, + profileData: { + configuration: createDeviceProfileConfiguration(DeviceProfileType.DEFAULT), + transportConfiguration: this.transportConfigFormGroup.get('transportConfiguration').value, + alarms: this.alarmRulesFormGroup.get('alarms').value + } + }; + return this.deviceProfileService.saveDeviceProfile(deviceProfile).pipe( + map(profile => profile.id), + tap((profileId) => { + this.profileConfigFormGroup.patchValue({ + deviceProfileId: profileId, + addProfileType: 0 + }); + this.addDeviceWizardStepper.selectedIndex = 2; + }) + ); + } else { + return of(null); + } + } + + private createdDevice(profileId: EntityId = this.profileConfigFormGroup.get('deviceProfileId').value): Observable> { + const device = { + name: this.deviceWizardFormGroup.get('name').value, + label: this.deviceWizardFormGroup.get('label').value, + deviceProfileId: profileId, + additionalInfo: { + gateway: this.deviceWizardFormGroup.get('gateway').value, + description: this.deviceWizardFormGroup.get('description').value + }, + customerId: null + }; + if (this.specificConfigFormGroup.get('customerId').value) { + device.customerId = { + entityType: EntityType.CUSTOMER, + id: this.specificConfigFormGroup.get('customerId').value + }; + } + return this.data.entitiesTableConfig.saveEntity(device); + } + + private saveCredential(device: BaseData): Observable { + if (this.specificConfigFormGroup.get('setCredential').value) { + return this.deviceService.getDeviceCredentials(device.id.id).pipe( + mergeMap( + (deviceCredentials) => { + const deviceCredentialsValue = {...deviceCredentials, ...this.specificConfigFormGroup.value.credential}; + return this.deviceService.saveDeviceCredentials(deviceCredentialsValue); + } + ), + map(() => true)); + } + return of(true); + } + + changeStep($event: StepperSelectionEvent): void { + this.selectedIndex = $event.selectedIndex; + if (this.selectedIndex === this.maxStepperIndex) { + this.nextStepButtonLabel$.next('action.add'); + } else { + this.nextStepButtonLabel$.next('action.continue'); + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts index c4927fbe99..d59c7225c5 100644 --- a/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/device-profile/device-profiles-table-config.resolver.ts @@ -114,7 +114,8 @@ export class DeviceProfilesTableConfigResolver implements Resolve> { @@ -221,7 +223,7 @@ export class DevicesTableConfigResolver implements Resolve true, + isEnabled: () => true, onAction: ($event, entity) => this.manageCredentials($event, entity) } ); @@ -243,7 +245,7 @@ export class DevicesTableConfigResolver implements Resolve true, + isEnabled: () => true, onAction: ($event, entity) => this.manageCredentials($event, entity) } ); @@ -253,7 +255,7 @@ export class DevicesTableConfigResolver implements Resolve true, + isEnabled: () => true, onAction: ($event, entity) => this.manageCredentials($event, entity) } ); @@ -301,7 +303,13 @@ export class DevicesTableConfigResolver implements Resolve true, onAction: ($event) => this.importDevices($event) - } + }, + { + name: this.translate.instant('device.wizard.device-wizard'), + icon: 'library_add', + isEnabled: () => true, + onAction: ($event) => this.deviceWizard($event) + }, ); } if (deviceScope === 'customer') { @@ -326,6 +334,23 @@ export class DevicesTableConfigResolver implements Resolve>, + boolean>(DeviceWizardDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + entitiesTableConfig: this.config.table.entitiesTableConfig + } + }).afterClosed().subscribe( + (res) => { + if (res) { + this.config.table.updateData(); + } + } + ); + } + addDevicesToCustomer($event: Event) { if ($event) { $event.stopPropagation(); @@ -480,5 +505,4 @@ export class DevicesTableConfigResolver implements Resolve