/// /// 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, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, Inject, Injector, Input, NgZone, OnChanges, OnDestroy, OnInit, Renderer2, SimpleChanges, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core'; import { DashboardWidget } from '@home/models/dashboard-component.models'; import { defaultLegendConfig, LegendConfig, LegendData, LegendPosition, MobileActionResult, Widget, WidgetActionDescriptor, widgetActionSources, WidgetActionType, WidgetComparisonSettings, WidgetMobileActionDescriptor, WidgetMobileActionType, WidgetResource, widgetType, WidgetTypeParameters } from '@shared/models/widget.models'; import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { WidgetService } from '@core/http/widget.service'; import { UtilsService } from '@core/services/utils.service'; import { forkJoin, Observable, of, ReplaySubject, Subscription, throwError } from 'rxjs'; import { deepClone, insertVariable, isDefined, isNotEmptyStr, objToBase64, objToBase64URI, validateEntityId } from '@core/utils'; import { IDynamicWidgetComponent, ShowWidgetHeaderActionFunction, updateEntityParams, WidgetContext, WidgetHeaderAction, WidgetInfo, WidgetTypeInstance } from '@home/models/widget-component.models'; import { IWidgetSubscription, StateObject, StateParams, SubscriptionEntityInfo, SubscriptionInfo, SubscriptionMessage, WidgetSubscriptionContext, WidgetSubscriptionOptions } from '@core/api/widget-api.models'; import { EntityId } from '@shared/models/id/entity-id'; import { ActivatedRoute, Router } from '@angular/router'; import cssjs from '@core/css/css'; import { ResourcesService } from '@core/services/resources.service'; import { catchError, map, switchMap } from 'rxjs/operators'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { TimeService } from '@core/services/time.service'; import { DeviceService } from '@app/core/http/device.service'; import { ExceptionData } from '@shared/models/error.models'; import { WidgetComponentService } from './widget-component.service'; import { Timewindow } from '@shared/models/time/time.models'; import { CancelAnimationFrame, RafService } from '@core/services/raf.service'; import { DashboardService } from '@core/http/dashboard.service'; import { WidgetSubscription } from '@core/api/widget-subscription'; import { EntityService } from '@core/http/entity.service'; import { ServicesMap } from '@home/models/services.map'; import { ResizeObserver } from '@juggle/resize-observer'; import { EntityDataService } from '@core/api/entity-data.service'; import { TranslateService } from '@ngx-translate/core'; import { NotificationType } from '@core/notification/notification.models'; import { AlarmDataService } from '@core/api/alarm-data.service'; import { MatDialog } from '@angular/material/dialog'; import { ComponentType } from '@angular/cdk/portal'; import { EMBED_DASHBOARD_DIALOG_TOKEN } from '@home/components/widget/dialog/embed-dashboard-dialog-token'; import { MobileService } from '@core/services/mobile.service'; import { DialogService } from '@core/services/dialog.service'; import { PopoverPlacement } from '@shared/components/popover.models'; import { TbPopoverService } from '@shared/components/popover.service'; import { DASHBOARD_PAGE_COMPONENT_TOKEN } from '@home/components/tokens'; @Component({ selector: 'tb-widget', templateUrl: './widget.component.html', styleUrls: ['./widget.component.scss'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) export class WidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy { @Input() isEdit: boolean; @Input() isMobile: boolean; @Input() dashboardWidget: DashboardWidget; @ViewChild('widgetContent', {read: ViewContainerRef, static: true}) widgetContentContainer: ViewContainerRef; widget: Widget; widgetInfo: WidgetInfo; errorMessages: string[]; widgetContext: WidgetContext; widgetType: any; typeParameters: WidgetTypeParameters; widgetTypeInstance: WidgetTypeInstance; widgetErrorData: ExceptionData; loadingData: boolean; displayNoData = false; noDataDisplayMessageText: string; displayLegend: boolean; legendConfig: LegendConfig; legendData: LegendData; isLegendFirst: boolean; legendContainerLayoutType: string; legendStyle: {[klass: string]: any}; dynamicWidgetComponentRef: ComponentRef; dynamicWidgetComponent: IDynamicWidgetComponent; subscriptionContext: WidgetSubscriptionContext; subscriptionInited = false; destroyed = false; widgetSizeDetected = false; widgetInstanceInited = false; dataUpdatePending = false; pendingMessage: SubscriptionMessage; cafs: {[cafId: string]: CancelAnimationFrame} = {}; toastTargetId = 'widget-messages-' + this.utils.guid(); private widgetResize$: ResizeObserver; private cssParser = new cssjs(); private rxSubscriptions = new Array(); constructor(protected store: Store, private route: ActivatedRoute, private router: Router, private widgetComponentService: WidgetComponentService, private componentFactoryResolver: ComponentFactoryResolver, private elementRef: ElementRef, private injector: Injector, private dialog: MatDialog, private renderer: Renderer2, private popoverService: TbPopoverService, @Inject(EMBED_DASHBOARD_DIALOG_TOKEN) private embedDashboardDialogComponent: ComponentType, @Inject(DASHBOARD_PAGE_COMPONENT_TOKEN) private dashboardPageComponent: ComponentType, private widgetService: WidgetService, private resources: ResourcesService, private timeService: TimeService, private deviceService: DeviceService, private entityService: EntityService, private dashboardService: DashboardService, private entityDataService: EntityDataService, private alarmDataService: AlarmDataService, private translate: TranslateService, private utils: UtilsService, private mobileService: MobileService, private dialogs: DialogService, private raf: RafService, private ngZone: NgZone, private cd: ChangeDetectorRef) { super(store); this.cssParser.testMode = false; } ngOnInit(): void { this.loadingData = true; this.widget = this.dashboardWidget.widget; this.displayLegend = isDefined(this.widget.config.showLegend) ? this.widget.config.showLegend : this.widget.type === widgetType.timeseries; this.legendContainerLayoutType = 'column'; if (this.displayLegend) { this.legendConfig = this.widget.config.legendConfig || defaultLegendConfig(this.widget.type); this.legendData = { keys: [], data: [] }; if (this.legendConfig.position === LegendPosition.top || this.legendConfig.position === LegendPosition.bottom) { this.legendContainerLayoutType = 'column'; this.isLegendFirst = this.legendConfig.position === LegendPosition.top; } else { this.legendContainerLayoutType = 'row'; this.isLegendFirst = this.legendConfig.position === LegendPosition.left; } switch (this.legendConfig.position) { case LegendPosition.top: this.legendStyle = { paddingBottom: '8px', maxHeight: '50%', overflowY: 'auto' }; break; case LegendPosition.bottom: this.legendStyle = { paddingTop: '8px', maxHeight: '50%', overflowY: 'auto' }; break; case LegendPosition.left: this.legendStyle = { paddingRight: '0px', maxWidth: '50%', overflowY: 'auto' }; break; case LegendPosition.right: this.legendStyle = { paddingLeft: '0px', maxWidth: '50%', overflowY: 'auto' }; break; } } const actionDescriptorsBySourceId: {[actionSourceId: string]: Array} = {}; if (this.widget.config.actions) { for (const actionSourceId of Object.keys(this.widget.config.actions)) { const descriptors = this.widget.config.actions[actionSourceId]; const actionDescriptors: Array = []; descriptors.forEach((descriptor) => { const actionDescriptor: WidgetActionDescriptor = deepClone(descriptor); actionDescriptor.displayName = this.utils.customTranslation(descriptor.name, descriptor.name); actionDescriptors.push(actionDescriptor); }); actionDescriptorsBySourceId[actionSourceId] = actionDescriptors; } } this.widgetContext = this.dashboardWidget.widgetContext; this.widgetContext.changeDetector = this.cd; this.widgetContext.ngZone = this.ngZone; this.widgetContext.store = this.store; this.widgetContext.servicesMap = ServicesMap; this.widgetContext.isEdit = this.isEdit; this.widgetContext.isMobile = this.isMobile; this.widgetContext.toastTargetId = this.toastTargetId; this.widgetContext.subscriptionApi = { createSubscription: this.createSubscription.bind(this), createSubscriptionFromInfo: this.createSubscriptionFromInfo.bind(this), removeSubscription: (id) => { const subscription = this.widgetContext.subscriptions[id]; if (subscription) { subscription.destroy(); delete this.widgetContext.subscriptions[id]; } } }; this.widgetContext.actionsApi = { actionDescriptorsBySourceId, getActionDescriptors: this.getActionDescriptors.bind(this), handleWidgetAction: this.handleWidgetAction.bind(this), elementClick: this.elementClick.bind(this), getActiveEntityInfo: this.getActiveEntityInfo.bind(this), openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this), openDashboardStateInPopover: this.openDashboardStateInPopover.bind(this) }; this.widgetContext.customHeaderActions = []; const headerActionsDescriptors = this.getActionDescriptors(widgetActionSources.headerButton.value); headerActionsDescriptors.forEach((descriptor) => { let useShowWidgetHeaderActionFunction = descriptor.useShowWidgetActionFunction || false; let showWidgetHeaderActionFunction: ShowWidgetHeaderActionFunction = null; if (useShowWidgetHeaderActionFunction && isNotEmptyStr(descriptor.showWidgetActionFunction)) { try { showWidgetHeaderActionFunction = new Function('widgetContext', 'data', descriptor.showWidgetActionFunction) as ShowWidgetHeaderActionFunction; } catch (e) { useShowWidgetHeaderActionFunction = false; } } const headerAction: WidgetHeaderAction = { name: descriptor.name, displayName: descriptor.displayName, icon: descriptor.icon, descriptor, useShowWidgetHeaderActionFunction, showWidgetHeaderActionFunction, onAction: $event => { const entityInfo = this.getActiveEntityInfo(); const entityId = entityInfo ? entityInfo.entityId : null; const entityName = entityInfo ? entityInfo.entityName : null; const entityLabel = entityInfo ? entityInfo.entityLabel : null; this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); } }; this.widgetContext.customHeaderActions.push(headerAction); }); this.subscriptionContext = new WidgetSubscriptionContext(this.widgetContext.dashboard); this.subscriptionContext.timeService = this.timeService; this.subscriptionContext.deviceService = this.deviceService; this.subscriptionContext.translate = this.translate; this.subscriptionContext.entityDataService = this.entityDataService; this.subscriptionContext.alarmDataService = this.alarmDataService; this.subscriptionContext.utils = this.utils; this.subscriptionContext.raf = this.raf; this.subscriptionContext.widgetUtils = this.widgetContext.utils; this.subscriptionContext.getServerTimeDiff = this.dashboardService.getServerTimeDiff.bind(this.dashboardService); this.widgetComponentService.getWidgetInfo(this.widget.bundleAlias, this.widget.typeAlias, this.widget.isSystemType).subscribe( (widgetInfo) => { this.widgetInfo = widgetInfo; this.loadFromWidgetInfo(); }, (errorData) => { this.widgetInfo = errorData.widgetInfo; this.errorMessages = errorData.errorMessages; this.loadFromWidgetInfo(); } ); setTimeout(() => { this.dashboardWidget.updateWidgetParams(); }, 0); const noDataDisplayMessage = this.widget.config.noDataDisplayMessage; if (isNotEmptyStr(noDataDisplayMessage)) { this.noDataDisplayMessageText = this.utils.customTranslation(noDataDisplayMessage, noDataDisplayMessage); } else { this.noDataDisplayMessageText = this.translate.instant('widget.no-data'); } } ngAfterViewInit(): void { } ngOnChanges(changes: SimpleChanges): void { for (const propName of Object.keys(changes)) { const change = changes[propName]; if (!change.firstChange && change.currentValue !== change.previousValue) { if (propName === 'isEdit') { this.onEditModeChanged(); } else if (propName === 'isMobile') { this.onMobileModeChanged(); } } } } ngOnDestroy(): void { this.destroyed = true; this.rxSubscriptions.forEach((subscription) => { subscription.unsubscribe(); }); this.rxSubscriptions.length = 0; this.onDestroy(); } private displayWidgetInstance(): boolean { if (this.widget.type !== widgetType.static) { for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; if (subscription.isDataResolved()) { return true; } } return false; } else { return true; } } private onDestroy() { if (this.widgetContext) { const shouldDestroyWidgetInstance = this.displayWidgetInstance(); for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; subscription.destroy(); } this.subscriptionInited = false; this.dataUpdatePending = false; this.pendingMessage = null; this.widgetContext.subscriptions = {}; if (this.widgetContext.inited) { this.widgetContext.inited = false; for (const cafId of Object.keys(this.cafs)) { if (this.cafs[cafId]) { this.cafs[cafId](); this.cafs[cafId] = null; } } try { if (shouldDestroyWidgetInstance) { this.widgetTypeInstance.onDestroy(); this.widgetInstanceInited = false; } } catch (e) { this.handleWidgetException(e); } } this.widgetContext.destroyed = true; this.destroyDynamicWidgetComponent(); } } public onTimewindowChanged(timewindow: Timewindow) { for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; if (!subscription.useDashboardTimewindow) { subscription.updateTimewindowConfig(timewindow); } } } public onLegendKeyHiddenChange(index: number) { for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; subscription.updateDataVisibility(index); } } private loadFromWidgetInfo() { this.widgetContext.widgetNamespace = `widget-type-${(this.widget.isSystemType ? 'sys-' : '')}${this.widget.bundleAlias}-${this.widget.typeAlias}`; const elem = this.elementRef.nativeElement; elem.classList.add('tb-widget'); elem.classList.add(this.widgetContext.widgetNamespace); this.widgetType = this.widgetInfo.widgetTypeFunction; this.typeParameters = this.widgetInfo.typeParameters; if (!this.widgetType) { this.widgetTypeInstance = {}; } else { try { this.widgetTypeInstance = new this.widgetType(this.widgetContext); } catch (e) { this.handleWidgetException(e); this.widgetTypeInstance = {}; } } if (!this.widgetTypeInstance.onInit) { this.widgetTypeInstance.onInit = () => {}; } if (!this.widgetTypeInstance.onDataUpdated) { this.widgetTypeInstance.onDataUpdated = () => {}; } if (!this.widgetTypeInstance.onResize) { this.widgetTypeInstance.onResize = () => {}; } if (!this.widgetTypeInstance.onEditModeChanged) { this.widgetTypeInstance.onEditModeChanged = () => {}; } if (!this.widgetTypeInstance.onMobileModeChanged) { this.widgetTypeInstance.onMobileModeChanged = () => {}; } if (!this.widgetTypeInstance.onDestroy) { this.widgetTypeInstance.onDestroy = () => {}; } this.initialize().subscribe( () => { this.onInit(); }, (err) => { // console.log(err); } ); } private detectChanges(detectContainerChanges = false) { if (!this.destroyed) { try { this.cd.detectChanges(); if (detectContainerChanges) { this.widgetContext.detectContainerChanges(); } } catch (e) { // console.log(e); } } } private isReady(): boolean { return this.subscriptionInited && this.widgetSizeDetected; } private onInit(skipSizeCheck?: boolean) { if (!this.widgetContext.$containerParent || this.destroyed) { return; } if (!skipSizeCheck) { this.checkSize(); } if (!this.widgetContext.inited && this.isReady()) { this.widgetContext.inited = true; this.widgetContext.detectContainerChanges(); if (this.cafs.init) { this.cafs.init(); this.cafs.init = null; } this.cafs.init = this.raf.raf(() => { try { if (this.displayWidgetInstance()) { this.widgetTypeInstance.onInit(); this.widgetInstanceInited = true; if (this.dataUpdatePending) { this.widgetTypeInstance.onDataUpdated(); setTimeout(() => { this.dashboardWidget.updateCustomHeaderActions(true); }, 0); this.dataUpdatePending = false; } if (this.pendingMessage) { this.displayMessage(this.pendingMessage.severity, this.pendingMessage.message); this.pendingMessage = null; } } else { this.loadingData = false; this.displayNoData = true; } this.detectChanges(); } catch (e) { this.handleWidgetException(e); } }); if (!this.typeParameters.useCustomDatasources && this.widgetContext.defaultSubscription) { this.widgetContext.defaultSubscription.subscribe(); } } } private onResize() { if (this.checkSize()) { if (this.widgetContext.inited) { if (this.cafs.resize) { this.cafs.resize(); this.cafs.resize = null; } this.cafs.resize = this.raf.raf(() => { try { if (this.displayWidgetInstance()) { this.widgetTypeInstance.onResize(); } } catch (e) { this.handleWidgetException(e); } }); } else { this.onInit(true); } } } private onEditModeChanged() { if (this.widgetContext.isEdit !== this.isEdit) { this.widgetContext.isEdit = this.isEdit; if (this.widgetContext.inited) { if (this.cafs.editMode) { this.cafs.editMode(); this.cafs.editMode = null; } this.cafs.editMode = this.raf.raf(() => { try { if (this.displayWidgetInstance()) { this.widgetTypeInstance.onEditModeChanged(); } } catch (e) { this.handleWidgetException(e); } }); } } } private onMobileModeChanged() { if (this.widgetContext.isMobile !== this.isMobile) { this.widgetContext.isMobile = this.isMobile; if (this.widgetContext.inited) { if (this.cafs.mobileMode) { this.cafs.mobileMode(); this.cafs.mobileMode = null; } this.cafs.mobileMode = this.raf.raf(() => { try { if (this.displayWidgetInstance()) { this.widgetTypeInstance.onMobileModeChanged(); } } catch (e) { this.handleWidgetException(e); } }); } } } private reInit() { if (this.cafs.reinit) { this.cafs.reinit(); this.cafs.reinit = null; } this.cafs.reinit = this.raf.raf(() => { this.ngZone.run(() => { this.reInitImpl(); }); }); } private reInitImpl() { this.onDestroy(); if (!this.typeParameters.useCustomDatasources) { this.createDefaultSubscription().subscribe( () => { if (this.destroyed) { this.onDestroy(); } else { this.widgetContext.reset(); this.subscriptionInited = true; this.configureDynamicWidgetComponent(); this.onInit(); } }, () => { if (this.destroyed) { this.onDestroy(); } else { this.widgetContext.reset(); this.subscriptionInited = true; this.onInit(); } } ); } else { this.widgetContext.reset(); this.subscriptionInited = true; this.configureDynamicWidgetComponent(); this.onInit(); } } private initialize(): Observable { const initSubject = new ReplaySubject(); this.rxSubscriptions.push(this.widgetContext.aliasController.entityAliasesChanged.subscribe( (aliasIds) => { let subscriptionChanged = false; for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; subscriptionChanged = subscriptionChanged || subscription.onAliasesChanged(aliasIds); } if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { this.displayNoData = false; this.reInit(); } } )); this.rxSubscriptions.push(this.widgetContext.aliasController.filtersChanged.subscribe( (filterIds) => { let subscriptionChanged = false; for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; subscriptionChanged = subscriptionChanged || subscription.onFiltersChanged(filterIds); } if (subscriptionChanged && !this.typeParameters.useCustomDatasources) { this.displayNoData = false; this.reInit(); } } )); this.rxSubscriptions.push(this.widgetContext.dashboard.dashboardTimewindowChanged.subscribe( (dashboardTimewindow) => { for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; subscription.onDashboardTimewindowChanged(dashboardTimewindow); } } )); if (!this.typeParameters.useCustomDatasources) { this.createDefaultSubscription().subscribe( () => { this.subscriptionInited = true; this.configureDynamicWidgetComponent(); initSubject.next(); initSubject.complete(); }, (err) => { this.subscriptionInited = true; initSubject.error(err); } ); } else { this.loadingData = false; this.subscriptionInited = true; this.configureDynamicWidgetComponent(); initSubject.next(); initSubject.complete(); } return initSubject.asObservable(); } private destroyDynamicWidgetComponent() { if (this.widgetContext.$containerParent && this.widgetResize$) { this.widgetResize$.disconnect(); } if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); this.dynamicWidgetComponentRef = null; } } private handleWidgetException(e) { console.error(e); this.widgetErrorData = this.utils.processWidgetException(e); this.detectChanges(); } private displayMessage(type: NotificationType, message: string, duration?: number) { this.widgetContext.showToast(type, message, duration, 'bottom', 'right', this.toastTargetId); } private clearMessage() { this.widgetContext.hideToast(this.toastTargetId); } private configureDynamicWidgetComponent() { this.widgetContentContainer.clear(); const injector: Injector = Injector.create( { providers: [ { provide: 'widgetContext', useValue: this.widgetContext }, { provide: 'errorMessages', useValue: this.errorMessages } ], parent: this.injector } ); const containerElement = $(this.elementRef.nativeElement.querySelector('#widget-container')); this.widgetContext.$containerParent = $(containerElement); try { this.dynamicWidgetComponentRef = this.widgetContentContainer.createComponent(this.widgetInfo.componentFactory, 0, injector); this.cd.detectChanges(); } catch (e) { console.error(e); if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponentRef.destroy(); this.dynamicWidgetComponentRef = null; } this.widgetContentContainer.clear(); } if (this.dynamicWidgetComponentRef) { this.dynamicWidgetComponent = this.dynamicWidgetComponentRef.instance; this.widgetContext.$container = $(this.dynamicWidgetComponentRef.location.nativeElement); this.widgetContext.$container.css('display', 'block'); this.widgetContext.$container.attr('id', 'container'); if (this.widgetSizeDetected) { this.widgetContext.$container.css('height', this.widgetContext.height + 'px'); this.widgetContext.$container.css('width', this.widgetContext.width + 'px'); } } this.widgetResize$ = new ResizeObserver(() => { this.onResize(); }); this.widgetResize$.observe(this.widgetContext.$containerParent[0]); } private createSubscription(options: WidgetSubscriptionOptions, subscribe?: boolean): Observable { const createSubscriptionSubject = new ReplaySubject(); options.dashboardTimewindow = this.widgetContext.dashboardTimewindow; const subscription: IWidgetSubscription = new WidgetSubscription(this.subscriptionContext, options); subscription.init$.subscribe( () => { this.widgetContext.subscriptions[subscription.id] = subscription; if (subscribe) { subscription.subscribe(); } createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, (err) => { createSubscriptionSubject.error(err); } ); return createSubscriptionSubject.asObservable(); } private createSubscriptionFromInfo(type: widgetType, subscriptionsInfo: Array, options: WidgetSubscriptionOptions, useDefaultComponents: boolean, subscribe: boolean): Observable { const createSubscriptionSubject = new ReplaySubject(); options.type = type; if (useDefaultComponents) { this.defaultComponentsOptions(options); } else { if (!options.timeWindowConfig) { options.useDashboardTimewindow = true; } } if (options.type === widgetType.alarm) { options.alarmSource = this.entityService.createAlarmSourceFromSubscriptionInfo(subscriptionsInfo[0]); } else { options.datasources = this.entityService.createDatasourcesFromSubscriptionsInfo(subscriptionsInfo); } this.createSubscription(options, subscribe).subscribe( (subscription) => { if (useDefaultComponents) { this.defaultSubscriptionOptions(subscription, options); } createSubscriptionSubject.next(subscription); createSubscriptionSubject.complete(); }, (err) => { createSubscriptionSubject.error(err); } ); return createSubscriptionSubject.asObservable(); } private defaultComponentsOptions(options: WidgetSubscriptionOptions) { options.useDashboardTimewindow = isDefined(this.widget.config.useDashboardTimewindow) ? this.widget.config.useDashboardTimewindow : true; options.displayTimewindow = isDefined(this.widget.config.displayTimewindow) ? this.widget.config.displayTimewindow : !options.useDashboardTimewindow; options.timeWindowConfig = options.useDashboardTimewindow ? this.widgetContext.dashboardTimewindow : this.widget.config.timewindow; options.legendConfig = null; if (this.displayLegend) { options.legendConfig = this.legendConfig; } options.decimals = this.widgetContext.decimals; options.units = this.widgetContext.units; options.callbacks = { onDataUpdated: () => { try { if (this.displayWidgetInstance()) { if (this.widgetInstanceInited) { this.widgetTypeInstance.onDataUpdated(); setTimeout(() => { this.dashboardWidget.updateCustomHeaderActions(true); }, 0); } else { this.dataUpdatePending = true; } } } catch (e){} }, onDataUpdateError: (subscription, e) => { this.handleWidgetException(e); }, onSubscriptionMessage: (subscription, message) => { if (this.displayWidgetInstance()) { if (this.widgetInstanceInited) { this.displayMessage(message.severity, message.message); } else { this.pendingMessage = message; } } }, onInitialPageDataChanged: (subscription, nextPageData) => { this.reInit(); }, forceReInit: () => { this.reInit(); }, dataLoading: (subscription) => { if (this.loadingData !== subscription.loadingData) { this.loadingData = subscription.loadingData; this.detectChanges(); } }, legendDataUpdated: (subscription, detectChanges) => { if (detectChanges) { this.detectChanges(); } }, timeWindowUpdated: (subscription, timeWindowConfig) => { this.ngZone.run(() => { this.widget.config.timewindow = timeWindowConfig; this.detectChanges(true); }); } }; } private defaultSubscriptionOptions(subscription: IWidgetSubscription, options: WidgetSubscriptionOptions) { if (this.displayLegend) { this.legendData = subscription.legendData; } } private createDefaultSubscription(): Observable { const createSubscriptionSubject = new ReplaySubject(); let options: WidgetSubscriptionOptions; if (this.widget.type !== widgetType.rpc && this.widget.type !== widgetType.static) { const comparisonSettings: WidgetComparisonSettings = this.widgetContext.settings; options = { type: this.widget.type, stateData: this.typeParameters.stateData, datasourcesOptional: this.typeParameters.datasourcesOptional, hasDataPageLink: this.typeParameters.hasDataPageLink, singleEntity: this.typeParameters.singleEntity, warnOnPageDataOverflow: this.typeParameters.warnOnPageDataOverflow, ignoreDataUpdateOnIntervalTick: this.typeParameters.ignoreDataUpdateOnIntervalTick, comparisonEnabled: comparisonSettings.comparisonEnabled, timeForComparison: comparisonSettings.timeForComparison, comparisonCustomIntervalValue: comparisonSettings.comparisonCustomIntervalValue }; if (this.widget.type === widgetType.alarm) { options.alarmSource = deepClone(this.widget.config.alarmSource); } else { options.datasources = deepClone(this.widget.config.datasources); } this.defaultComponentsOptions(options); this.createSubscription(options).subscribe( (subscription) => { this.defaultSubscriptionOptions(subscription, options); // backward compatibility this.widgetContext.datasources = subscription.datasources; this.widgetContext.data = subscription.data; this.widgetContext.hiddenData = subscription.hiddenData; this.widgetContext.timeWindow = subscription.timeWindow; this.widgetContext.defaultSubscription = subscription; createSubscriptionSubject.next(); createSubscriptionSubject.complete(); }, (err) => { createSubscriptionSubject.error(err); } ); } else if (this.widget.type === widgetType.rpc) { this.loadingData = false; options = { type: this.widget.type, targetDeviceAliasIds: this.widget.config.targetDeviceAliasIds }; options.callbacks = { rpcStateChanged: (subscription) => { if (this.dynamicWidgetComponent) { this.dynamicWidgetComponent.rpcEnabled = subscription.rpcEnabled; this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest; this.detectChanges(); } }, onRpcSuccess: (subscription) => { if (this.dynamicWidgetComponent) { this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest; this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText; this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection; this.clearMessage(); this.detectChanges(); } }, onRpcFailed: (subscription) => { if (this.dynamicWidgetComponent) { this.dynamicWidgetComponent.executingRpcRequest = subscription.executingRpcRequest; this.dynamicWidgetComponent.rpcErrorText = subscription.rpcErrorText; this.dynamicWidgetComponent.rpcRejection = subscription.rpcRejection; if (subscription.rpcErrorText) { this.displayMessage('error', subscription.rpcErrorText); } this.detectChanges(); } }, onRpcErrorCleared: (subscription) => { if (this.dynamicWidgetComponent) { this.dynamicWidgetComponent.rpcErrorText = null; this.dynamicWidgetComponent.rpcRejection = null; this.clearMessage(); this.detectChanges(); } } }; this.createSubscription(options).subscribe( (subscription) => { this.widgetContext.defaultSubscription = subscription; createSubscriptionSubject.next(); createSubscriptionSubject.complete(); }, (err) => { createSubscriptionSubject.error(err); } ); this.detectChanges(); } else if (this.widget.type === widgetType.static) { this.loadingData = false; createSubscriptionSubject.next(); createSubscriptionSubject.complete(); this.detectChanges(); } else { createSubscriptionSubject.next(); createSubscriptionSubject.complete(); this.detectChanges(); } return createSubscriptionSubject.asObservable(); } private getActionDescriptors(actionSourceId: string): Array { let result = this.widgetContext.actionsApi.actionDescriptorsBySourceId[actionSourceId]; if (!result) { result = []; } return result; } private handleWidgetAction($event: Event, descriptor: WidgetActionDescriptor, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string): void { const type = descriptor.type; const targetEntityParamName = descriptor.stateEntityParamName; let targetEntityId: EntityId; if (descriptor.setEntityId && validateEntityId(entityId)) { targetEntityId = entityId; } switch (type) { case WidgetActionType.openDashboardState: case WidgetActionType.updateDashboardState: const params = deepClone(this.widgetContext.stateController.getStateParams()); updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); if (type === WidgetActionType.openDashboardState) { if (descriptor.openInPopover) { this.openDashboardStateInPopover($event, descriptor.targetDashboardStateId, params, descriptor.popoverHideDashboardToolbar, descriptor.popoverPreferredPlacement, descriptor.popoverHideOnClickOutside, descriptor.popoverWidth, descriptor.popoverHeight, descriptor.popoverStyle); } else if (descriptor.openInSeparateDialog && !this.mobileService.isMobileApp()) { this.openDashboardStateInSeparateDialog(descriptor.targetDashboardStateId, params, descriptor.dialogTitle, descriptor.dialogHideDashboardToolbar, descriptor.dialogWidth, descriptor.dialogHeight); } else { this.widgetContext.stateController.openState(descriptor.targetDashboardStateId, params, descriptor.openRightLayout); } } else { this.widgetContext.stateController.updateState(descriptor.targetDashboardStateId, params, descriptor.openRightLayout); } break; case WidgetActionType.openDashboard: const targetDashboardId = descriptor.targetDashboardId; const stateObject: StateObject = {}; stateObject.params = {}; updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); if (descriptor.targetDashboardStateId) { stateObject.id = descriptor.targetDashboardStateId; } const state = objToBase64URI([ stateObject ]); const isSinglePage = this.route.snapshot.data.singlePageMode; let url; if (isSinglePage) { url = `/dashboard/${targetDashboardId}?state=${state}`; } else { url = `/dashboards/${targetDashboardId}?state=${state}`; } if (descriptor.openNewBrowserTab) { window.open(url, '_blank'); } else { this.router.navigateByUrl(url); } break; case WidgetActionType.custom: const customFunction = descriptor.customFunction; if (customFunction && customFunction.length > 0) { try { if (!additionalParams) { additionalParams = {}; } const customActionFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', customFunction); customActionFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } break; case WidgetActionType.customPretty: const customPrettyFunction = descriptor.customFunction; const customHtml = descriptor.customHtml; const customCss = descriptor.customCss; const customResources = descriptor.customResources; const actionNamespace = `custom-action-pretty-${descriptor.name.toLowerCase()}`; let htmlTemplate = ''; if (isDefined(customHtml) && customHtml.length > 0) { htmlTemplate = customHtml; } this.loadCustomActionResources(actionNamespace, customCss, customResources).subscribe( () => { if (isDefined(customPrettyFunction) && customPrettyFunction.length > 0) { try { if (!additionalParams) { additionalParams = {}; } const customActionPrettyFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'htmlTemplate', 'additionalParams', 'entityLabel', customPrettyFunction); customActionPrettyFunction($event, this.widgetContext, entityId, entityName, htmlTemplate, additionalParams, entityLabel); } catch (e) { console.error(e); } } }, (errorMessages: string[]) => { this.processResourcesLoadErrors(errorMessages); } ); break; case WidgetActionType.mobileAction: const mobileAction = descriptor.mobileAction; this.handleMobileAction($event, mobileAction, entityId, entityName, additionalParams, entityLabel); break; } } private handleMobileAction($event: Event, mobileAction: WidgetMobileActionDescriptor, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { const type = mobileAction.type; let argsObservable: Observable; switch (type) { case WidgetMobileActionType.takePictureFromGallery: case WidgetMobileActionType.takePhoto: case WidgetMobileActionType.scanQrCode: case WidgetMobileActionType.getLocation: case WidgetMobileActionType.takeScreenshot: argsObservable = of([]); break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: const getLocationFunctionString = mobileAction.getLocationFunction; const getLocationFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', getLocationFunctionString); const locationArgs = getLocationFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); if (locationArgs && locationArgs instanceof Observable) { argsObservable = locationArgs; } else { argsObservable = of(locationArgs); } argsObservable = argsObservable.pipe(map(latLng => { let valid = false; if (Array.isArray(latLng) && latLng.length === 2) { if (typeof latLng[0] === 'number' && typeof latLng[1] === 'number') { valid = true; } } if (valid) { return latLng; } else { throw new Error('Location function did not return valid array of latitude/longitude!'); } })); break; case WidgetMobileActionType.makePhoneCall: const getPhoneNumberFunctionString = mobileAction.getPhoneNumberFunction; const getPhoneNumberFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', getPhoneNumberFunctionString); const phoneNumberArg = getPhoneNumberFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); if (phoneNumberArg && phoneNumberArg instanceof Observable) { argsObservable = phoneNumberArg.pipe(map(phoneNumber => [phoneNumber])); } else { argsObservable = of([phoneNumberArg]); } argsObservable = argsObservable.pipe(map(phoneNumberArr => { let valid = false; if (Array.isArray(phoneNumberArr) && phoneNumberArr.length === 1) { if (phoneNumberArr[0] !== null) { valid = true; } } if (valid) { return phoneNumberArr; } else { throw new Error('Phone number function did not return valid number!'); } })); break; } argsObservable.subscribe((args) => { this.mobileService.handleWidgetMobileAction(type, ...args).subscribe( (result) => { if (result) { if (result.hasError) { this.handleWidgetMobileActionError(result.error, $event, mobileAction, entityId, entityName, additionalParams, entityLabel); } else if (result.hasResult) { const actionResult = result.result; switch (type) { case WidgetMobileActionType.takePictureFromGallery: case WidgetMobileActionType.takePhoto: case WidgetMobileActionType.takeScreenshot: const imageUrl = actionResult.imageUrl; if (mobileAction.processImageFunction && mobileAction.processImageFunction.length) { try { const processImageFunction = new Function('imageUrl', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', mobileAction.processImageFunction); processImageFunction(imageUrl, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } break; case WidgetMobileActionType.scanQrCode: const code = actionResult.code; const format = actionResult.format; if (mobileAction.processQrCodeFunction && mobileAction.processQrCodeFunction.length) { try { const processQrCodeFunction = new Function('code', 'format', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', mobileAction.processQrCodeFunction); processQrCodeFunction(code, format, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } break; case WidgetMobileActionType.getLocation: const latitude = actionResult.latitude; const longitude = actionResult.longitude; if (mobileAction.processLocationFunction && mobileAction.processLocationFunction.length) { try { const processLocationFunction = new Function('latitude', 'longitude', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', mobileAction.processLocationFunction); processLocationFunction(latitude, longitude, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } break; case WidgetMobileActionType.mapDirection: case WidgetMobileActionType.mapLocation: case WidgetMobileActionType.makePhoneCall: const launched = actionResult.launched; if (mobileAction.processLaunchResultFunction && mobileAction.processLaunchResultFunction.length) { try { const processLaunchResultFunction = new Function('launched', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', mobileAction.processLaunchResultFunction); processLaunchResultFunction(launched, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } break; } } else { if (mobileAction.handleEmptyResultFunction && mobileAction.handleEmptyResultFunction.length) { try { const handleEmptyResultFunction = new Function('$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', mobileAction.handleEmptyResultFunction); handleEmptyResultFunction($event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } } } } ); }, (err) => { let errorMessage; if (err && typeof err === 'string') { errorMessage = err; } else if (err && err.message) { errorMessage = err.message; } errorMessage = `Failed to get mobile action arguments${errorMessage ? `: ${errorMessage}` : '!'}`; this.handleWidgetMobileActionError(errorMessage, $event, mobileAction, entityId, entityName, additionalParams, entityLabel); }); } private handleWidgetMobileActionError(error: string, $event: Event, mobileAction: WidgetMobileActionDescriptor, entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { if (mobileAction.handleErrorFunction && mobileAction.handleErrorFunction.length) { try { const handleErrorFunction = new Function('error', '$event', 'widgetContext', 'entityId', 'entityName', 'additionalParams', 'entityLabel', mobileAction.handleErrorFunction); handleErrorFunction(error, $event, this.widgetContext, entityId, entityName, additionalParams, entityLabel); } catch (e) { console.error(e); } } } private openDashboardStateInPopover($event: Event, targetDashboardStateId: string, params?: StateParams, hideDashboardToolbar = true, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, popoverWidth = '25vw', popoverHeight = '25vh', popoverStyle: { [klass: string]: any } = {}) { const trigger = ($event.target || $event.srcElement || $event.currentTarget) as Element; if (this.popoverService.hasPopover(trigger)) { this.popoverService.hidePopover(trigger); } else { const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); const stateObject: StateObject = {}; stateObject.params = params; if (targetDashboardStateId) { stateObject.id = targetDashboardStateId; } const injector = Injector.create({ parent: this.widgetContentContainer.injector, providers: [ { provide: 'embeddedValue', useValue: true } ] }); const component = this.popoverService.displayPopover(trigger, this.renderer, this.widgetContentContainer, this.dashboardPageComponent, preferredPlacement, hideOnClickOutside, injector, { embedded: true, syncStateWithQueryParam: false, hideToolbar: hideDashboardToolbar, currentState: objToBase64([stateObject]), dashboard, parentDashboard: this.widgetContext.parentDashboard ? this.widgetContext.parentDashboard : this.widgetContext.dashboard }, {width: popoverWidth, height: popoverHeight}, popoverStyle, {} ); this.widgetContext.registerPopoverComponent(component); } } private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) { const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); const stateObject: StateObject = {}; stateObject.params = params; if (targetDashboardStateId) { stateObject.id = targetDashboardStateId; } let title = dialogTitle; if (!title) { if (targetDashboardStateId && dashboard.configuration.states) { const dashboardState = dashboard.configuration.states[targetDashboardStateId]; if (dashboardState) { title = dashboardState.name; } } } if (!title) { title = dashboard.title; } title = this.utils.customTranslation(title, title); const paramsEntityName = params && params.entityName ? params.entityName : ''; const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; title = insertVariable(title, 'entityName', paramsEntityName); title = insertVariable(title, 'entityLabel', paramsEntityLabel); for (const prop of Object.keys(params)) { if (params[prop] && params[prop].entityName) { title = insertVariable(title, prop + ':entityName', params[prop].entityName); } if (params[prop] && params[prop].entityLabel) { title = insertVariable(title, prop + ':entityLabel', params[prop].entityLabel); } } this.dialog.open(this.embedDashboardDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], viewContainerRef: this.widgetContentContainer, data: { dashboard, state: objToBase64([ stateObject ]), title, hideToolbar: hideDashboardToolbar, width: dialogWidth, height: dialogHeight } }); this.cd.markForCheck(); } private elementClick($event: Event) { const elementClicked = ($event.target || $event.srcElement) as Element; const descriptors = this.getActionDescriptors('elementClick'); if (descriptors.length) { const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]); if (targetElement.length && targetElement[0].id) { $event.stopPropagation(); const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id); const entityInfo = this.getActiveEntityInfo(); const entityId = entityInfo ? entityInfo.entityId : null; const entityName = entityInfo ? entityInfo.entityName : null; const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); } } } private loadCustomActionResources(actionNamespace: string, customCss: string, customResources: Array): Observable { if (isDefined(customCss) && customCss.length > 0) { this.cssParser.cssPreviewNamespace = actionNamespace; this.cssParser.createStyleElement(actionNamespace, customCss, 'nonamespace'); } const resourceTasks: Observable[] = []; if (isDefined(customResources) && customResources.length > 0) { customResources.forEach((resource) => { resourceTasks.push( this.resources.loadResource(resource.url).pipe( catchError(e => of(`Failed to load custom action resource: '${resource.url}'`)) ) ); }); return forkJoin(resourceTasks).pipe( switchMap(msgs => { let errors: string[]; if (msgs && msgs.length) { errors = msgs.filter(msg => msg && msg.length > 0); } if (errors && errors.length) { return throwError(errors); } else { return of(null); } } )); } else { return of(null); } } private processResourcesLoadErrors(errorMessages: string[]) { let messageToShow = ''; errorMessages.forEach(error => { messageToShow += `
${error}
`; }); this.store.dispatch(new ActionNotificationShow({message: messageToShow, type: 'error'})); } private getActiveEntityInfo(): SubscriptionEntityInfo { let entityInfo = this.widgetContext.activeEntityInfo; if (!entityInfo) { for (const id of Object.keys(this.widgetContext.subscriptions)) { const subscription = this.widgetContext.subscriptions[id]; entityInfo = subscription.getFirstEntityInfo(); if (entityInfo) { break; } } } return entityInfo; } private checkSize(): boolean { const width = this.widgetContext.$containerParent.width(); const height = this.widgetContext.$containerParent.height(); let sizeChanged = false; if (!this.widgetContext.width || this.widgetContext.width !== width || !this.widgetContext.height || this.widgetContext.height !== height) { if (width > 0 && height > 0) { if (this.widgetContext.$container) { this.widgetContext.$container.css('height', height + 'px'); this.widgetContext.$container.css('width', width + 'px'); } this.widgetContext.width = width; this.widgetContext.height = height; sizeChanged = true; this.widgetSizeDetected = true; } } return sizeChanged; } }