From 012c8e91de49172089e9a41aab5a1723b5ce387c Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Mon, 26 Sep 2022 18:10:12 +0300 Subject: [PATCH] Implement Assets Profile UI --- .../server/controller/AssetController.java | 16 +- .../controller/AssetProfileController.java | 3 - .../profile/DefaultTbAssetProfileService.java | 2 +- .../app/core/http/asset-profile.service.ts | 67 ++++ ui-ngx/src/app/core/http/asset.service.ts | 13 + ui-ngx/src/app/core/http/entity.service.ts | 14 + ui-ngx/src/app/core/services/menu.service.ts | 36 +- ui-ngx/src/app/modules/common/modules-map.ts | 6 + .../home/components/home-components.module.ts | 9 + .../import-export/import-export.service.ts | 45 ++- .../asset-profile-autocomplete.component.html | 74 ++++ .../asset-profile-autocomplete.component.scss | 21 + .../asset-profile-autocomplete.component.ts | 369 ++++++++++++++++++ .../asset-profile-dialog.component.html | 53 +++ .../profile/asset-profile-dialog.component.ts | 100 +++++ .../profile/asset-profile.component.html | 91 +++++ .../profile/asset-profile.component.ts | 111 ++++++ ...device-profile-autocomplete.component.html | 2 +- .../asset-profile-routing.module.ts | 74 ++++ .../asset-profile-tabs.component.html | 27 ++ .../asset-profile-tabs.component.ts | 38 ++ .../asset-profile/asset-profile.module.ts | 35 ++ .../asset-profiles-table-config.resolver.ts | 189 +++++++++ .../asset/asset-table-header.component.html | 12 +- .../asset/asset-table-header.component.scss | 2 +- .../asset/asset-table-header.component.ts | 5 +- .../home/pages/asset/asset.component.html | 13 +- .../home/pages/asset/asset.component.ts | 8 +- .../asset/assets-table-config.resolver.ts | 11 +- .../device-profile-routing.module.ts | 6 +- .../device-profiles-table-config.resolver.ts | 2 +- .../modules/home/pages/home-pages.module.ts | 4 + .../pages/profiles/profiles-routing.module.ts | 51 +++ .../home/pages/profiles/profiles.module.ts | 30 ++ ui-ngx/src/app/shared/models/asset.models.ts | 22 ++ ui-ngx/src/app/shared/models/constants.ts | 1 + ui-ngx/src/app/shared/models/edge.models.ts | 76 ++-- .../app/shared/models/entity-type.models.ts | 24 +- .../app/shared/models/id/asset-profile-id.ts | 26 ++ ui-ngx/src/app/shared/models/id/public-api.ts | 1 + ui-ngx/src/app/shared/models/vc.models.ts | 1 + .../assets/locale/locale.constant-en_US.json | 51 +++ 42 files changed, 1663 insertions(+), 78 deletions(-) create mode 100644 ui-ngx/src/app/core/http/asset-profile.service.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html create mode 100644 ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile-tabs.component.ts create mode 100644 ui-ngx/src/app/modules/home/pages/asset-profile/asset-profile.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/asset-profile/asset-profiles-table-config.resolver.ts create mode 100644 ui-ngx/src/app/modules/home/pages/profiles/profiles-routing.module.ts create mode 100644 ui-ngx/src/app/modules/home/pages/profiles/profiles.module.ts create mode 100644 ui-ngx/src/app/shared/models/id/asset-profile-id.ts diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetController.java b/application/src/main/java/org/thingsboard/server/controller/AssetController.java index 75e77d4719..8450c67ea3 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetController.java @@ -41,18 +41,19 @@ import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.asset.AssetBulkImportService; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.service.entitiy.asset.TbAssetService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; @@ -65,6 +66,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_NAME_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_SORT_PROPERTY_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.ASSET_TEXT_SEARCH_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ASSET_TYPE_DESCRIPTION; @@ -257,6 +259,8 @@ public class AssetController extends BaseController { @RequestParam int page, @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) + @RequestParam(required = false) String assetProfileId, @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -268,6 +272,9 @@ public class AssetController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink)); + } else if (assetProfileId != null && assetProfileId.length() > 0) { + AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId)); + return checkNotNull(assetService.findAssetInfosByTenantIdAndAssetProfileId(tenantId, profileId, pageLink)); } else { return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink)); } @@ -345,6 +352,8 @@ public class AssetController extends BaseController { @RequestParam int page, @ApiParam(value = ASSET_TYPE_DESCRIPTION) @RequestParam(required = false) String type, + @ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION) + @RequestParam(required = false) String assetProfileId, @ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION) @RequestParam(required = false) String textSearch, @ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES) @@ -359,6 +368,9 @@ public class AssetController extends BaseController { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); if (type != null && type.trim().length() > 0) { return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink)); + } else if (assetProfileId != null && assetProfileId.length() > 0) { + AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId)); + return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId, customerId, profileId, pageLink)); } else { return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink)); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java index a87ae9227c..2e84dd6634 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java @@ -67,9 +67,6 @@ public class AssetProfileController extends BaseController { private final TbAssetProfileService tbAssetProfileService; - @Autowired - private TimeseriesService timeseriesService; - @ApiOperation(value = "Get Asset Profile (getAssetProfileById)", notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " + "The server checks that the asset profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH, diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java index b964bfcd1a..7bf356a618 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java @@ -44,7 +44,7 @@ public class DefaultTbAssetProfileService extends AbstractTbEntityService implem TenantId tenantId = assetProfile.getTenantId(); try { AssetProfile savedAssetProfile = checkNotNull(assetProfileService.saveAssetProfile(assetProfile)); - autoCommit(user, assetProfile.getId()); + autoCommit(user, savedAssetProfile.getId()); tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAssetProfile.getId(), actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED); diff --git a/ui-ngx/src/app/core/http/asset-profile.service.ts b/ui-ngx/src/app/core/http/asset-profile.service.ts new file mode 100644 index 0000000000..fc0fc13e78 --- /dev/null +++ b/ui-ngx/src/app/core/http/asset-profile.service.ts @@ -0,0 +1,67 @@ +/// +/// Copyright © 2016-2022 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 { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +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 { AssetProfile, AssetProfileInfo } from '@shared/models/asset.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AssetProfileService { + + constructor( + private http: HttpClient + ) { + } + + public getAssetProfiles(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/assetProfiles${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public getAssetProfile(assetProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public saveAssetProfile(assetProfile: AssetProfile, config?: RequestConfig): Observable { + return this.http.post('/api/assetProfile', assetProfile, defaultHttpOptionsFromConfig(config)); + } + + public deleteAssetProfile(assetProfileId: string, config?: RequestConfig) { + return this.http.delete(`/api/assetProfile/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public setDefaultAssetProfile(assetProfileId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/assetProfile/${assetProfileId}/default`, defaultHttpOptionsFromConfig(config)); + } + + public getDefaultAssetProfileInfo(config?: RequestConfig): Observable { + return this.http.get('/api/assetProfileInfo/default', defaultHttpOptionsFromConfig(config)); + } + + public getAssetProfileInfo(assetProfileId: string, config?: RequestConfig): Observable { + return this.http.get(`/api/assetProfileInfo/${assetProfileId}`, defaultHttpOptionsFromConfig(config)); + } + + public getAssetProfileInfos(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/assetProfileInfos${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/asset.service.ts b/ui-ngx/src/app/core/http/asset.service.ts index 82e6bba2a1..ae62d8befb 100644 --- a/ui-ngx/src/app/core/http/asset.service.ts +++ b/ui-ngx/src/app/core/http/asset.service.ts @@ -38,12 +38,25 @@ export class AssetService { defaultHttpOptionsFromConfig(config)); } + public getTenantAssetInfosByAssetProfileId(pageLink: PageLink, assetProfileId: string = '', + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/assetInfos${pageLink.toQuery()}&assetProfileId=${assetProfileId}`, + defaultHttpOptionsFromConfig(config)); + } + public getCustomerAssetInfos(customerId: string, pageLink: PageLink, type: string = '', config?: RequestConfig): Observable> { return this.http.get>(`/api/customer/${customerId}/assetInfos${pageLink.toQuery()}&type=${type}`, defaultHttpOptionsFromConfig(config)); } + public getCustomerAssetInfosByAssetProfileId(customerId: string, pageLink: PageLink, assetProfileId: string = '', + config?: RequestConfig): Observable> { + return this.http.get> + (`/api/customer/${customerId}/assetInfos${pageLink.toQuery()}&assetProfileId=${assetProfileId}`, + defaultHttpOptionsFromConfig(config)); + } + public getAsset(assetId: string, config?: RequestConfig): Observable { return this.http.get(`/api/asset/${assetId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index b1c395c2ba..96e6e057fc 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -87,6 +87,7 @@ import { RuleChainMetaData, RuleChainType } from '@shared/models/rule-chain.mode import { WidgetService } from '@core/http/widget.service'; import { DeviceProfileService } from '@core/http/device-profile.service'; import { QueueService } from '@core/http/queue.service'; +import { AssetProfileService } from '@core/http/asset-profile.service'; @Injectable({ providedIn: 'root' @@ -110,6 +111,7 @@ export class EntityService { private otaPackageService: OtaPackageService, private widgetService: WidgetService, private deviceProfileService: DeviceProfileService, + private assetProfileService: AssetProfileService, private utils: UtilsService, private queueService: QueueService ) { } @@ -237,6 +239,11 @@ export class EntityService { (id) => this.deviceProfileService.getDeviceProfileInfo(id, config), entityIds); break; + case EntityType.ASSET_PROFILE: + observable = this.getEntitiesByIdsObservable( + (id) => this.assetProfileService.getAssetProfileInfo(id, config), + entityIds); + break; } return observable; } @@ -382,6 +389,10 @@ export class EntityService { pageLink.sortOrder.property = 'name'; entitiesObservable = this.deviceProfileService.getDeviceProfileInfos(pageLink, null, config); break; + case EntityType.ASSET_PROFILE: + pageLink.sortOrder.property = 'name'; + entitiesObservable = this.assetProfileService.getAssetProfileInfos(pageLink, config); + break; } return entitiesObservable; } @@ -1409,6 +1420,9 @@ export class EntityService { case EdgeEventType.DEVICE_PROFILE: entityObservable = this.deviceProfileService.getDeviceProfile(entityId); break; + case EdgeEventType.ASSET_PROFILE: + entityObservable = this.assetProfileService.getAssetProfile(entityId); + break; case EdgeEventType.RELATION: entityObservable = of(entity.body); break; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index ffc4884b7f..bf2da1ebbd 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -293,11 +293,29 @@ export class MenuService { }, { id: guid(), - name: 'device-profile.device-profiles', - type: 'link', - path: '/deviceProfiles', - icon: 'mdi:alpha-d-box', - isMdiIcon: true + name: 'profiles.profiles', + type: 'toggle', + path: '/profiles', + height: '80px', + icon: 'badge', + pages: [ + { + id: guid(), + name: 'device-profile.device-profiles', + type: 'link', + path: '/profiles/deviceProfiles', + icon: 'mdi:alpha-d-box', + isMdiIcon: true + }, + { + id: guid(), + name: 'asset-profile.asset-profiles', + type: 'link', + path: '/profiles/assetProfiles', + icon: 'mdi:alpha-a-box', + isMdiIcon: true + } + ] }, { id: guid(), @@ -450,6 +468,12 @@ export class MenuService { name: 'asset.assets', icon: 'domain', path: '/assets' + }, + { + name: 'asset-profile.asset-profiles', + icon: 'mdi:alpha-a-box', + isMdiIcon: true, + path: '/profiles/assetProfiles' } ] }, @@ -465,7 +489,7 @@ export class MenuService { name: 'device-profile.device-profiles', icon: 'mdi:alpha-d-box', isMdiIcon: true, - path: '/deviceProfiles' + path: '/profiles/deviceProfiles' }, { name: 'ota-update.ota-updates', diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index 5278645965..ce86939a01 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -290,6 +290,9 @@ import * as DashboardImageDialogComponent from '@home/components/dashboard-page/ import * as WidgetContainerComponent from '@home/components/widget/widget-container.component'; import * as TenantProfileQueuesComponent from '@home/components/profile/queue/tenant-profile-queues.component'; import * as QueueFormComponent from '@home/components/queue/queue-form.component'; +import * as AssetProfileComponent from '@home/components/profile/asset-profile.component'; +import * as AssetProfileDialogComponent from '@home/components/profile/asset-profile-dialog.component'; +import * as AssetProfileAutocompleteComponent from '@home/components/profile/asset-profile-autocomplete.component'; import { IModulesMap } from '@modules/common/modules-map.models'; @@ -537,6 +540,9 @@ class ModulesMap implements IModulesMap { MqttDeviceProfileTransportConfigurationComponent, '@home/components/profile/device/coap-device-profile-transport-configuration.component': CoapDeviceProfileTransportConfigurationComponent, + '@home/components/profile/asset-profile.component': AssetProfileComponent, + '@home/components/profile/asset-profile-dialog.component': AssetProfileDialogComponent, + '@home/components/profile/asset-profile-autocomplete.component': AssetProfileAutocompleteComponent, '@home/components/profile/alarm/device-profile-alarms.component': DeviceProfileAlarmsComponent, '@home/components/profile/alarm/device-profile-alarm.component': DeviceProfileAlarmComponent, '@home/components/profile/alarm/create-alarm-rules.component': CreateAlarmRulesComponent, 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 9468289df8..7cfcf269de 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 @@ -170,6 +170,9 @@ import { RateLimitsListComponent } from '@home/components/profile/tenant/rate-li import { RateLimitsComponent } from '@home/components/profile/tenant/rate-limits/rate-limits.component'; import { RateLimitsTextComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-text.component'; import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenant/rate-limits/rate-limits-details-dialog.component'; +import { AssetProfileComponent } from '@home/components/profile/asset-profile.component'; +import { AssetProfileDialogComponent } from '@home/components/profile/asset-profile-dialog.component'; +import { AssetProfileAutocompleteComponent } from '@home/components/profile/asset-profile-autocomplete.component'; @NgModule({ declarations: @@ -266,6 +269,9 @@ import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenan DeviceProfileComponent, DeviceProfileDialogComponent, AddDeviceProfileDialogComponent, + AssetProfileComponent, + AssetProfileDialogComponent, + AssetProfileAutocompleteComponent, RuleChainAutocompleteComponent, AlarmScheduleInfoComponent, DeviceProfileProvisionConfigurationComponent, @@ -402,6 +408,9 @@ import { RateLimitsDetailsDialogComponent } from '@home/components/profile/tenan AddDeviceProfileDialogComponent, RuleChainAutocompleteComponent, DeviceWizardDialogComponent, + AssetProfileComponent, + AssetProfileDialogComponent, + AssetProfileAutocompleteComponent, AlarmScheduleInfoComponent, AlarmScheduleComponent, AlarmDynamicValue, diff --git a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts index 50f7e61d6f..4f67158c7f 100644 --- a/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts +++ b/ui-ngx/src/app/modules/home/components/import-export/import-export.service.ts @@ -72,6 +72,8 @@ import { DeviceService } from '@core/http/device.service'; import { AssetService } from '@core/http/asset.service'; import { EdgeService } from '@core/http/edge.service'; import { RuleNode } from '@shared/models/rule-node.models'; +import { AssetProfileService } from '@core/http/asset-profile.service'; +import { AssetProfile } from '@shared/models/asset.models'; // @dynamic @Injectable() @@ -85,6 +87,7 @@ export class ImportExportService { private dashboardUtils: DashboardUtilsService, private widgetService: WidgetService, private deviceProfileService: DeviceProfileService, + private assetProfileService: AssetProfileService, private tenantProfileService: TenantProfileService, private entityService: EntityService, private ruleChainService: RuleChainService, @@ -532,6 +535,37 @@ export class ImportExportService { ); } + public exportAssetProfile(assetProfileId: string) { + this.assetProfileService.getAssetProfile(assetProfileId).subscribe( + (assetProfile) => { + let name = assetProfile.name; + name = name.toLowerCase().replace(/\W/g, '_'); + this.exportToPc(this.prepareProfileExport(assetProfile), name); + }, + (e) => { + this.handleExportError(e, 'asset-profile.export-failed-error'); + } + ); + } + + public importAssetProfile(): Observable { + return this.openImportDialog('asset-profile.import', 'asset-profile.asset-profile-file').pipe( + mergeMap((assetProfile: AssetProfile) => { + if (!this.validateImportedAssetProfile(assetProfile)) { + this.store.dispatch(new ActionNotificationShow( + {message: this.translate.instant('asset-profile.invalid-asset-profile-file-error'), + type: 'error'})); + throw new Error('Invalid asset profile file'); + } else { + return this.assetProfileService.saveAssetProfile(assetProfile); + } + }), + catchError((err) => { + return of(null); + }) + ); + } + public exportTenantProfile(tenantProfileId: string) { this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe( (tenantProfile) => { @@ -628,6 +662,13 @@ export class ImportExportService { return true; } + private validateImportedAssetProfile(assetProfile: AssetProfile): boolean { + if (isUndefined(assetProfile.name)) { + return false; + } + return true; + } + private validateImportedTenantProfile(tenantProfile: TenantProfile): boolean { return isDefined(tenantProfile.name) && isDefined(tenantProfile.profileData) @@ -892,7 +933,9 @@ export class ImportExportService { } filename += '.' + fileType.extension; const blob = new Blob([data], {type: fileType.mimeType}); + // @ts-ignore if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { + // @ts-ignore this.window.navigator.msSaveOrOpenBlob(blob, filename); } else { const e = this.document.createEvent('MouseEvents'); @@ -913,7 +956,7 @@ export class ImportExportService { return dashboard; } - private prepareProfileExport(profile: T): T { + private prepareProfileExport(profile: T): T { profile = this.prepareExport(profile); profile.default = false; return profile; diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html new file mode 100644 index 0000000000..2d3e7a6d94 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.html @@ -0,0 +1,74 @@ + + + + + {{ displayAssetProfileFn(selectAssetProfileFormGroup.get('assetProfile').value) }} + + + + + + + + +
+
+ asset-profile.no-asset-profiles-found +
+ + + {{ translate.get('asset-profile.no-asset-profiles-matching', + {entity: truncate.transform(searchText, true, 6, '...')}) | async }} + + + asset-profile.create-new-asset-profile + + +
+
+
+ + {{ 'asset-profile.asset-profile-required' | translate }} + + {{ hint | translate }} +
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.scss b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.scss new file mode 100644 index 0000000000..edcc791381 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2022 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-icon-button a { + border-bottom: none; + color: inherit; + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts new file mode 100644 index 0000000000..05a4678f1c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-autocomplete.component.ts @@ -0,0 +1,369 @@ +/// +/// Copyright © 2016-2022 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, + ElementRef, + EventEmitter, + forwardRef, + Input, + NgZone, + OnInit, + Output, + ViewChild +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { catchError, debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import { AppState } from '@app/core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { coerceBooleanProperty } from '@angular/cdk/coercion'; +import { entityIdEquals } from '@shared/models/id/entity-id'; +import { TruncatePipe } from '@shared//pipe/truncate.pipe'; +import { ENTER } from '@angular/cdk/keycodes'; +import { MatDialog } from '@angular/material/dialog'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { getEntityDetailsPageURL } from '@core/utils'; +import { AssetProfileId } from '@shared/models/id/asset-profile-id'; +import { AssetProfile, AssetProfileInfo } from '@shared/models/asset.models'; +import { AssetProfileService } from '@core/http/asset-profile.service'; +import { AssetProfileDialogComponent, AssetProfileDialogData } from './asset-profile-dialog.component'; + +@Component({ + selector: 'tb-asset-profile-autocomplete', + templateUrl: './asset-profile-autocomplete.component.html', + styleUrls: ['./asset-profile-autocomplete.component.scss'], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => AssetProfileAutocompleteComponent), + multi: true + }] +}) +export class AssetProfileAutocompleteComponent implements ControlValueAccessor, OnInit { + + selectAssetProfileFormGroup: FormGroup; + + modelValue: AssetProfileId | null; + + @Input() + selectDefaultProfile = false; + + @Input() + selectFirstProfile = false; + + @Input() + displayAllOnEmpty = false; + + @Input() + editProfileEnabled = true; + + @Input() + addNewProfile = true; + + @Input() + showDetailsPageLink = false; + + private requiredValue: boolean; + get required(): boolean { + return this.requiredValue; + } + @Input() + set required(value: boolean) { + this.requiredValue = coerceBooleanProperty(value); + } + + @Input() + disabled: boolean; + + @Input() + hint: string; + + @Output() + assetProfileUpdated = new EventEmitter(); + + @Output() + assetProfileChanged = new EventEmitter(); + + @ViewChild('assetProfileInput', {static: true}) assetProfileInput: ElementRef; + + @ViewChild('assetProfileAutocomplete', {static: true}) assetProfileAutocomplete: MatAutocomplete; + + filteredAssetProfiles: Observable>; + + searchText = ''; + assetProfileURL: string; + + private dirty = false; + + private ignoreClosedPanel = false; + + private allAssetProfile: AssetProfileInfo = { + name: this.translate.instant('asset-profile.all-asset-profiles'), + id: null + }; + + private propagateChange = (v: any) => { }; + + constructor(private store: Store, + public translate: TranslateService, + public truncate: TruncatePipe, + private assetProfileService: AssetProfileService, + private fb: FormBuilder, + private zone: NgZone, + private dialog: MatDialog) { + this.selectAssetProfileFormGroup = this.fb.group({ + assetProfile: [null] + }); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(fn: any): void { + } + + ngOnInit() { + this.filteredAssetProfiles = this.selectAssetProfileFormGroup.get('assetProfile').valueChanges + .pipe( + tap((value: AssetProfileInfo | string) => { + let modelValue: AssetProfileInfo | null; + if (typeof value === 'string' || !value) { + modelValue = null; + } else { + modelValue = value; + } + if (!this.displayAllOnEmpty || modelValue) { + this.updateView(modelValue); + } + }), + map(value => { + if (value) { + if (typeof value === 'string') { + return value; + } else { + if (this.displayAllOnEmpty && value === this.allAssetProfile) { + return ''; + } else { + return value.name; + } + } + } else { + return ''; + } + }), + debounceTime(150), + distinctUntilChanged(), + switchMap(name => this.fetchAssetProfiles(name)), + share() + ); + } + + selectDefaultAssetProfileIfNeeded(): void { + if (this.selectDefaultProfile && !this.modelValue) { + this.assetProfileService.getDefaultAssetProfileInfo().subscribe( + (profile) => { + if (profile) { + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(profile, {emitEvent: false}); + this.updateView(profile); + } else { + this.selectFirstAssetProfileIfNeeded(); + } + } + ); + } else { + this.selectFirstAssetProfileIfNeeded(); + } + } + + selectFirstAssetProfileIfNeeded(): void { + if (this.selectFirstProfile && !this.modelValue) { + const pageLink = new PageLink(1, 0, null, { + property: 'createdTime', + direction: Direction.DESC + }); + this.assetProfileService.getAssetProfileInfos(pageLink, {ignoreLoading: true}).subscribe( + (pageData => { + const data = pageData.data; + if (data.length) { + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(data[0], {emitEvent: false}); + this.updateView(data[0]); + } + }) + ); + } + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.selectAssetProfileFormGroup.disable({emitEvent: false}); + } else { + this.selectAssetProfileFormGroup.enable({emitEvent: false}); + } + } + + writeValue(value: AssetProfileId | null): void { + this.searchText = ''; + if (value != null) { + this.assetProfileService.getAssetProfileInfo(value.id).subscribe( + (profile) => { + this.modelValue = new AssetProfileId(profile.id.id); + this.assetProfileURL = getEntityDetailsPageURL(this.modelValue.id, this.modelValue.entityType); + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(profile, {emitEvent: false}); + this.assetProfileChanged.emit(profile); + } + ); + } else if (this.displayAllOnEmpty) { + this.modelValue = null; + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(this.allAssetProfile, {emitEvent: false}); + } else { + this.modelValue = null; + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(null, {emitEvent: false}); + this.selectDefaultAssetProfileIfNeeded(); + } + this.dirty = true; + } + + onFocus() { + if (this.dirty) { + this.selectAssetProfileFormGroup.get('assetProfile').updateValueAndValidity({onlySelf: true, emitEvent: true}); + this.dirty = false; + } + } + + onPanelClosed() { + if (this.ignoreClosedPanel) { + this.ignoreClosedPanel = false; + } else { + if (this.displayAllOnEmpty && !this.selectAssetProfileFormGroup.get('assetProfile').value) { + this.zone.run(() => { + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(this.allAssetProfile, {emitEvent: true}); + }, 0); + } + } + } + + updateView(assetProfile: AssetProfileInfo | null) { + const idValue = assetProfile && assetProfile.id ? new AssetProfileId(assetProfile.id.id) : null; + if (!entityIdEquals(this.modelValue, idValue)) { + this.modelValue = idValue; + this.propagateChange(this.modelValue); + this.assetProfileChanged.emit(assetProfile); + } + } + + displayAssetProfileFn(profile?: AssetProfileInfo): string | undefined { + return profile ? profile.name : undefined; + } + + fetchAssetProfiles(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(10, 0, searchText, { + property: 'name', + direction: Direction.ASC + }); + return this.assetProfileService.getAssetProfileInfos(pageLink, {ignoreLoading: true}).pipe( + catchError(() => of(emptyPageData())), + map(pageData => { + let data = pageData.data; + if (this.displayAllOnEmpty) { + data = [this.allAssetProfile, ...data]; + } + return data; + }) + ); + } + + clear() { + this.ignoreClosedPanel = true; + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(null, {emitEvent: true}); + setTimeout(() => { + this.assetProfileInput.nativeElement.blur(); + this.assetProfileInput.nativeElement.focus(); + }, 0); + } + + textIsNotEmpty(text: string): boolean { + return (text && text.length > 0); + } + + assetProfileEnter($event: KeyboardEvent) { + if (this.editProfileEnabled && $event.keyCode === ENTER) { + $event.preventDefault(); + if (!this.modelValue) { + this.createAssetProfile($event, this.searchText); + } + } + } + + createAssetProfile($event: Event, profileName: string) { + $event.preventDefault(); + const assetProfile: AssetProfile = { + name: profileName + } as AssetProfile; + if (this.addNewProfile) { + this.openAssetProfileDialog(assetProfile, true); + } + } + + editAssetProfile($event: Event) { + $event.preventDefault(); + this.assetProfileService.getAssetProfile(this.modelValue.id).subscribe( + (assetProfile) => { + this.openAssetProfileDialog(assetProfile, false); + } + ); + } + + openAssetProfileDialog(assetProfile: AssetProfile, isAdd: boolean) { + this.dialog.open(AssetProfileDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + isAdd, + assetProfile + } + }).afterClosed().subscribe( + (savedAssetProfile) => { + if (!savedAssetProfile) { + setTimeout(() => { + this.assetProfileInput.nativeElement.blur(); + this.assetProfileInput.nativeElement.focus(); + }, 0); + } else { + this.assetProfileService.getAssetProfileInfo(savedAssetProfile.id.id).subscribe( + (profile) => { + this.modelValue = new AssetProfileId(profile.id.id); + this.selectAssetProfileFormGroup.get('assetProfile').patchValue(profile, {emitEvent: true}); + if (isAdd) { + this.propagateChange(this.modelValue); + } else { + this.assetProfileUpdated.next(savedAssetProfile.id); + } + this.assetProfileChanged.emit(profile); + } + ); + } + } + ); + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html new file mode 100644 index 0000000000..a406275dc4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.html @@ -0,0 +1,53 @@ + +
+ +

{{ (isAdd ? 'asset-profile.add' : 'asset-profile.edit' ) | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts new file mode 100644 index 0000000000..2adbfd3153 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile-dialog.component.ts @@ -0,0 +1,100 @@ +/// +/// Copyright © 2016-2022 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 { + AfterViewInit, + Component, + ComponentFactoryResolver, + Inject, + Injector, + SkipSelf, + ViewChild +} from '@angular/core'; +import { ErrorStateMatcher } from '@angular/material/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Router } from '@angular/router'; +import { AssetProfile } from '@shared/models/asset.models'; +import { AssetProfileComponent } from '@home/components/profile/asset-profile.component'; +import { AssetProfileService } from '@core/http/asset-profile.service'; + +export interface AssetProfileDialogData { + assetProfile: AssetProfile; + isAdd: boolean; +} + +@Component({ + selector: 'tb-asset-profile-dialog', + templateUrl: './asset-profile-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: AssetProfileDialogComponent}], + styleUrls: [] +}) +export class AssetProfileDialogComponent extends + DialogComponent implements ErrorStateMatcher, AfterViewInit { + + isAdd: boolean; + assetProfile: AssetProfile; + + submitted = false; + + @ViewChild('assetProfileComponent', {static: true}) assetProfileComponent: AssetProfileComponent; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogData, + public dialogRef: MatDialogRef, + private componentFactoryResolver: ComponentFactoryResolver, + private injector: Injector, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private assetProfileService: AssetProfileService) { + super(store, router, dialogRef); + this.isAdd = this.data.isAdd; + this.assetProfile = this.data.assetProfile; + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.assetProfileComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.assetProfileComponent.entityForm.valid) { + this.assetProfile = {...this.assetProfile, ...this.assetProfileComponent.entityFormValue()}; + this.assetProfileService.saveAssetProfile(this.assetProfile).subscribe( + (assetProfile) => { + this.dialogRef.close(assetProfile); + } + ); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html new file mode 100644 index 0000000000..a78d7b8acb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.html @@ -0,0 +1,91 @@ + +
+ + + + +
+ +
+
+
+
+
+ + asset-profile.name + + + {{ 'asset-profile.name-required' | translate }} + + + {{ 'asset-profile.name-max-length' | translate }} + + + + + +
{{'asset-profile.mobile-dashboard-hint' | translate}}
+
+ + + + + + asset-profile.description + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts new file mode 100644 index 0000000000..fdc1a42339 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/profile/asset-profile.component.ts @@ -0,0 +1,111 @@ +/// +/// Copyright © 2016-2022 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 { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { ActionNotificationShow } from '@app/core/notification/notification.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import { EntityComponent } from '../entity/entity.component'; +import { EntityType } from '@shared/models/entity-type.models'; +import { RuleChainId } from '@shared/models/id/rule-chain-id'; +import { ServiceType } from '@shared/models/queue.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { DashboardId } from '@shared/models/id/dashboard-id'; +import { AssetProfile } from '@shared/models/asset.models'; + +@Component({ + selector: 'tb-asset-profile', + templateUrl: './asset-profile.component.html', + styleUrls: [] +}) +export class AssetProfileComponent extends EntityComponent { + + @Input() + standalone = false; + + entityType = EntityType; + + serviceType = ServiceType.TB_RULE_ENGINE; + + assetProfileId: EntityId; + + constructor(protected store: Store, + protected translate: TranslateService, + @Optional() @Inject('entity') protected entityValue: AssetProfile, + @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected fb: FormBuilder, + protected cd: ChangeDetectorRef) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + buildForm(entity: AssetProfile): FormGroup { + this.assetProfileId = entity?.id ? entity.id : null; + const form = this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], + image: [entity ? entity.image : null], + defaultRuleChainId: [entity && entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null, []], + defaultDashboardId: [entity && entity.defaultDashboardId ? entity.defaultDashboardId.id : null, []], + defaultQueueName: [entity ? entity.defaultQueueName : null, []], + description: [entity ? entity.description : '', []], + } + ); + return form; + } + + updateForm(entity: AssetProfile) { + this.assetProfileId = entity.id; + this.entityForm.patchValue({name: entity.name}); + this.entityForm.patchValue({image: entity.image}, {emitEvent: false}); + this.entityForm.patchValue({defaultRuleChainId: entity.defaultRuleChainId ? entity.defaultRuleChainId.id : null}, {emitEvent: false}); + this.entityForm.patchValue({defaultDashboardId: entity.defaultDashboardId ? entity.defaultDashboardId.id : null}, {emitEvent: false}); + this.entityForm.patchValue({defaultQueueName: entity.defaultQueueName}, {emitEvent: false}); + this.entityForm.patchValue({description: entity.description}, {emitEvent: false}); + } + + prepareFormValue(formValue: any): any { + if (formValue.defaultRuleChainId) { + formValue.defaultRuleChainId = new RuleChainId(formValue.defaultRuleChainId); + } + if (formValue.defaultDashboardId) { + formValue.defaultDashboardId = new DashboardId(formValue.defaultDashboardId); + } + return super.prepareFormValue(formValue); + } + + onAssetProfileIdCopied(event) { + this.store.dispatch(new ActionNotificationShow( + { + message: this.translate.instant('asset-profile.idCopiedMessage'), + type: 'success', + duration: 750, + verticalPosition: 'bottom', + horizontalPosition: 'right' + })); + } + +} 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 1b9ba6c6b4..0bc0751249 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 @@ -25,7 +25,7 @@ (keypress)="deviceProfileEnter($event)" [matAutocomplete]="deviceProfileAutocomplete" [fxShow]="!showDetailsPageLink || !disabled || !selectDeviceProfileFormGroup.get('deviceProfile').value"> - + {{ displayDeviceProfileFn(selectDeviceProfileFormGroup.get('deviceProfile').value) }}