diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index f07f1e858e..d49db3581d 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -11,6 +11,7 @@ "cards.value_card", "cards.horizontal_value_card", "cards.aggregated_value_card", + "simple_value_and_chart_card", "cards.label_widget", "cards.dashboard_state_widget", "cards.qr_code", diff --git a/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json b/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json new file mode 100644 index 0000000000..7fa97f4a52 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/simple_value_and_chart_card.json @@ -0,0 +1,27 @@ +{ + "fqn": "simple_value_and_chart_card", + "name": "Simple Value and chart card", + "deprecated": false, + "image": "data:image/png;base64,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", + "description": "Displays a single entity historical telemetry values as a simplified chart. Optionally may display the corresponding latest telemetry value.", + "descriptor": { + "type": "timeseries", + "sizeX": 4.5, + "sizeY": 2, + "resources": [], + "templateHtml": "\n\n", + "templateCss": ".legend {\n font-size: 13px;\n line-height: 10px;\n}\n\n.legend table { \n border-spacing: 0px;\n border-collapse: separate;\n}\n\n.mouse-events .flot-overlay {\n cursor: crosshair; \n}\n\n", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.valueChartCardWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.valueChartCardWidget.onDataUpdated();\n};\n\nself.onLatestDataUpdated = function() {\n self.ctx.$scope.valueChartCardWidget.onLatestDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.valueChartCardWidget.onEditModeChanged();\n}\n\nself.onDestroy = function() {\n self.ctx.$scope.valueChartCardWidget.onDestroy();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '300px',\n previewHeight: '150px',\n embedTitlePanel: true,\n hasAdditionalLatestDataKeys: true\n };\n}\n", + "settingsSchema": "{}", + "dataKeySettingsSchema": "{}", + "latestDataKeySettingsSchema": "{}", + "settingsDirective": "tb-value-chart-card-widget-settings", + "dataKeySettingsDirective": "", + "latestDataKeySettingsDirective": "", + "hasBasicMode": true, + "basicModeDirective": "tb-value-chart-card-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"latestDataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Latest\",\"color\":\"rgb(63, 82, 221)\",\"settings\":{},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}]}],\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":null,\"padding\":\"0\",\"settings\":{\"layout\":\"left\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":28,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"500\",\"lineHeight\":\"32px\"},\"valueColor\":{\"type\":\"constant\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"colorFunction\":\"var temperature = value;\\nif (typeof temperature !== undefined) {\\n var percent = (temperature + 60)/120 * 100;\\n return tinycolor.mix('blue', 'red', percent).toHexString();\\n}\\nreturn 'blue';\"},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Simple Value and chart card\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":null,\"mobileHeight\":null,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"thermostat\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"iconSize\":\"18px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"useDashboardTimewindow\":true,\"decimals\":0,\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"borderRadius\":null,\"units\":\"°C\",\"displayTimewindow\":true,\"timewindow\":{\"hideInterval\":false,\"hideLastInterval\":false,\"hideQuickInterval\":false,\"hideAggregation\":false,\"hideAggInterval\":false,\"hideTimezone\":false,\"selectedTab\":1,\"history\":{\"historyType\":2,\"timewindowMs\":60000,\"interval\":43200000,\"fixedTimewindow\":{\"startTimeMs\":1697382151041,\"endTimeMs\":1697468551041},\"quickInterval\":\"CURRENT_MONTH_SO_FAR\"},\"aggregation\":{\"type\":\"AVG\",\"limit\":25000}},\"timewindowStyle\":{\"showIcon\":false,\"iconSize\":\"24px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"400\",\"style\":\"normal\",\"lineHeight\":\"16px\"},\"color\":\"rgba(0, 0, 0, 0.38)\",\"displayTypePrefix\":true}}" + }, + "externalId": null, + "tags": null +} \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts index 639ede30f4..fd0e48322d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/basic-widget-config.module.ts @@ -64,6 +64,9 @@ import { import { SignalStrengthBasicConfigComponent } from '@home/components/widget/config/basic/indicator/signal-strength-basic-config.component'; +import { + ValueChartCardBasicConfigComponent +} from '@home/components/widget/config/basic/cards/value-chart-card-basic-config.component'; @NgModule({ declarations: [ @@ -83,7 +86,8 @@ import { EntityCountBasicConfigComponent, BatteryLevelBasicConfigComponent, WindSpeedDirectionBasicConfigComponent, - SignalStrengthBasicConfigComponent + SignalStrengthBasicConfigComponent, + ValueChartCardBasicConfigComponent ], imports: [ CommonModule, @@ -107,7 +111,8 @@ import { EntityCountBasicConfigComponent, BatteryLevelBasicConfigComponent, WindSpeedDirectionBasicConfigComponent, - SignalStrengthBasicConfigComponent + SignalStrengthBasicConfigComponent, + ValueChartCardBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -125,5 +130,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + + + + + widget-config.appearance + + + {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widgets.value-chart-card.auto-scale' | translate }} + + + + + {{ 'widget-config.title' | translate }} + + + + + + + + + + + + + + {{ 'widgets.value-chart-card.icon' | translate }} + + + + + + + + + + + + + + + {{ 'widgets.value-chart-card.value' | translate }} + + + + + + widget-config.decimals-suffix + + + + + + + + + {{ 'widgets.value-chart-card.chart' | translate }} + + + + + + widget-config.card-appearance + + {{ 'widgets.background.background' | translate }} + + + + + widget-config.show-card-buttons + + {{ 'fullscreen.fullscreen' | translate }} + + + + {{ 'widget-config.card-border-radius' | translate }} + + + + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts new file mode 100644 index 0000000000..f2c3a853a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/value-chart-card-basic-config.component.ts @@ -0,0 +1,250 @@ +/// +/// Copyright © 2016-2023 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 } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BasicWidgetConfigComponent } from '@home/components/widget/config/widget-config.component.models'; +import { WidgetConfigComponentData } from '@home/models/widget-component.models'; +import { DataKey, Datasource, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + getTimewindowConfig, + setTimewindowConfig +} from '@home/components/widget/config/timewindow-config-panel.component'; +import { formatValue, isUndefined } from '@core/utils'; +import { cssSizeToStrSize, getDataKey, resolveCssSize } from '@shared/models/widget-settings.models'; +import { + valueCartCardLayouts, + valueChartCardDefaultSettings, + valueChartCardLayoutImages, + valueChartCardLayoutTranslations, + ValueChartCardWidgetSettings +} from '@home/components/widget/lib/cards/value-chart-card-widget.models'; + +@Component({ + selector: 'tb-value-chart-card-basic-config', + templateUrl: './value-chart-card-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class ValueChartCardBasicConfigComponent extends BasicWidgetConfigComponent { + + public get datasource(): Datasource { + const datasources: Datasource[] = this.valueChartCardWidgetConfigForm.get('datasources').value; + if (datasources && datasources.length) { + return datasources[0]; + } else { + return null; + } + } + + valueChartCardLayouts = valueCartCardLayouts; + + valueChartCardLayoutTranslationMap = valueChartCardLayoutTranslations; + valueChartCardLayoutImageMap = valueChartCardLayoutImages; + + valueChartCardWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.valueChartCardWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [ + { name: 'temperature', label: 'Temperature', type: DataKeyType.timeseries, color: 'rgba(63, 82, 221, 1)'} + ], + [{ name: 'temperature', label: 'Latest', type: DataKeyType.timeseries}] + ); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: ValueChartCardWidgetSettings = {...valueChartCardDefaultSettings, ...(configData.config.settings || {})}; + const dataKey = getDataKey(configData.config.datasources); + const iconSize = resolveCssSize(configData.config.iconSize); + this.valueChartCardWidgetConfigForm = this.fb.group({ + timewindowConfig: [getTimewindowConfig(configData.config), []], + datasources: [configData.config.datasources, []], + + layout: [settings.layout, []], + autoScale: [settings.autoScale, []], + + showTitle: [configData.config.showTitle, []], + title: [configData.config.title, []], + titleFont: [configData.config.titleFont, []], + titleColor: [configData.config.titleColor, []], + + showIcon: [configData.config.showTitleIcon, []], + iconSize: [iconSize[0], [Validators.min(0)]], + iconSizeUnit: [iconSize[1], []], + icon: [configData.config.titleIcon, []], + iconColor: [configData.config.iconColor, []], + + showValue: [settings.showValue, []], + units: [configData.config.units, []], + decimals: [configData.config.decimals, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + chartColor: [dataKey?.color, []], + + background: [settings.background, []], + + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + + actions: [configData.config.actions || {}, []] + }); + } + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + setTimewindowConfig(this.widgetConfig.config, config.timewindowConfig); + this.widgetConfig.config.datasources = config.datasources; + + this.widgetConfig.config.showTitle = config.showTitle; + this.widgetConfig.config.title = config.title; + this.widgetConfig.config.titleFont = config.titleFont; + this.widgetConfig.config.titleColor = config.titleColor; + + this.widgetConfig.config.showTitleIcon = config.showIcon; + this.widgetConfig.config.iconSize = cssSizeToStrSize(config.iconSize, config.iconSizeUnit); + this.widgetConfig.config.titleIcon = config.icon; + this.widgetConfig.config.iconColor = config.iconColor; + + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + + this.widgetConfig.config.settings.layout = config.layout; + this.widgetConfig.config.settings.autoScale = config.autoScale; + + this.widgetConfig.config.settings.showValue = config.showValue; + this.widgetConfig.config.units = config.units; + this.widgetConfig.config.decimals = config.decimals; + this.widgetConfig.config.settings.valueFont = config.valueFont; + this.widgetConfig.config.settings.valueColor = config.valueColor; + + const dataKey = getDataKey(this.widgetConfig.config.datasources); + if (dataKey) { + dataKey.color = config.chartColor; + this.updateLatestValues(dataKey, this.widgetConfig.config.datasources); + } + + this.widgetConfig.config.settings.background = config.background; + + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + + this.widgetConfig.config.actions = config.actions; + return this.widgetConfig; + } + + protected validatorTriggers(): string[] { + return ['showTitle', 'showIcon', 'showValue']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.valueChartCardWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.valueChartCardWidgetConfigForm.get('showIcon').value; + const showValue: boolean = this.valueChartCardWidgetConfigForm.get('showValue').value; + + if (showTitle) { + this.valueChartCardWidgetConfigForm.get('title').enable(); + this.valueChartCardWidgetConfigForm.get('titleFont').enable(); + this.valueChartCardWidgetConfigForm.get('titleColor').enable(); + this.valueChartCardWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.valueChartCardWidgetConfigForm.get('iconSize').enable(); + this.valueChartCardWidgetConfigForm.get('iconSizeUnit').enable(); + this.valueChartCardWidgetConfigForm.get('icon').enable(); + this.valueChartCardWidgetConfigForm.get('iconColor').enable(); + } else { + this.valueChartCardWidgetConfigForm.get('iconSize').disable(); + this.valueChartCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.valueChartCardWidgetConfigForm.get('icon').disable(); + this.valueChartCardWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.valueChartCardWidgetConfigForm.get('title').disable(); + this.valueChartCardWidgetConfigForm.get('titleFont').disable(); + this.valueChartCardWidgetConfigForm.get('titleColor').disable(); + this.valueChartCardWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.valueChartCardWidgetConfigForm.get('iconSize').disable(); + this.valueChartCardWidgetConfigForm.get('iconSizeUnit').disable(); + this.valueChartCardWidgetConfigForm.get('icon').disable(); + this.valueChartCardWidgetConfigForm.get('iconColor').disable(); + } + + if (showValue) { + this.valueChartCardWidgetConfigForm.get('units').enable(); + this.valueChartCardWidgetConfigForm.get('decimals').enable(); + this.valueChartCardWidgetConfigForm.get('valueFont').enable(); + this.valueChartCardWidgetConfigForm.get('valueColor').enable(); + } else { + this.valueChartCardWidgetConfigForm.get('units').disable(); + this.valueChartCardWidgetConfigForm.get('decimals').disable(); + this.valueChartCardWidgetConfigForm.get('valueFont').disable(); + this.valueChartCardWidgetConfigForm.get('valueColor').disable(); + } + } + + private updateLatestValues(sourceDataKey: DataKey, datasources?: Datasource[]) { + if (datasources && datasources.length) { + let latestDataKeys = datasources[0].latestDataKeys; + if (!latestDataKeys) { + latestDataKeys = []; + datasources[0].latestDataKeys = latestDataKeys; + } + let dataKey: DataKey; + if (!latestDataKeys.length) { + dataKey = {...sourceDataKey}; + latestDataKeys.push(dataKey); + } else { + dataKey = latestDataKeys[0]; + dataKey = {...dataKey, ...sourceDataKey}; + latestDataKeys[0] = dataKey; + } + dataKey.label = 'Latest'; + dataKey.units = null; + dataKey.decimals = null; + } + } + + private getCardButtons(config: WidgetConfig): string[] { + const buttons: string[] = []; + if (isUndefined(config.enableFullscreen) || config.enableFullscreen) { + buttons.push('fullscreen'); + } + return buttons; + } + + private setCardButtons(buttons: string[], config: WidgetConfig) { + config.enableFullscreen = buttons.includes('fullscreen'); + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts index 93fc304b7a..556d6e283c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/timewindow-config-panel.component.ts @@ -92,7 +92,7 @@ export class TimewindowConfigPanelComponent implements ControlValueAccessor, OnI timewindowStyle: [null, []] }); this.timewindowConfig.valueChanges.subscribe( - (val) => this.propagateChange(val) + () => this.propagateChange(this.timewindowConfig.getRawValue()) ); this.timewindowConfig.get('useDashboardTimewindow').valueChanges.subscribe(() => { this.updateTimewindowConfigEnabledState(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html new file mode 100644 index 0000000000..b1789800eb --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.html @@ -0,0 +1,25 @@ + + + + + + {{ valueText }} + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss new file mode 100644 index 0000000000..3ea61fdcec --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.scss @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2023 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. + */ +.tb-value-chart-card-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 20px 24px 24px 24px; + > div:not(.tb-value-chart-card-overlay) { + z-index: 1; + } + div.tb-widget-title { + padding: 0; + } + .tb-value-chart-card-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + .tb-value-chart-card-content { + min-height: 0; + flex: 1; + display: flex; + align-items: flex-end; + gap: 16px; + position: relative; + &.left { + flex-direction: row; + } + &.right { + flex-direction: row-reverse; + } + .tb-value-chart-card-value { + white-space: nowrap; + } + .tb-value-chart-card-chart { + flex: 1; + height: 100%; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts new file mode 100644 index 0000000000..7cd58e3c13 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.component.ts @@ -0,0 +1,247 @@ +/// +/// Copyright © 2016-2023 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, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + Renderer2, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { WidgetContext } from '@home/models/widget-component.models'; +import { formatValue, isDefinedAndNotNull } from '@core/utils'; +import { + backgroundStyle, + ColorProcessor, + ComponentStyle, + getDataKey, + overlayStyle, resolveCssSize, + textStyle +} from '@shared/models/widget-settings.models'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { ResizeObserver } from '@juggle/resize-observer'; +import { + valueChartCardDefaultSettings, + ValueChartCardLayout, + ValueChartCardWidgetSettings +} from '@home/components/widget/lib/cards/value-chart-card-widget.models'; +import { TbFlot } from '@home/components/widget/lib/flot-widget'; +import { DataKey } from '@shared/models/widget.models'; +import { TbFlotKeySettings, TbFlotSettings } from '@home/components/widget/lib/flot-widget.models'; +import { getTsValueByLatestDataKey } from '@home/components/widget/lib/cards/aggregated-value-card.models'; + +const layoutWidth = 218; +const layoutValueWidth = 68; +const valueRelativeWidth = layoutValueWidth / layoutWidth; + +@Component({ + selector: 'tb-value-chart-card-widget', + templateUrl: './value-chart-card-widget.component.html', + styleUrls: ['./value-chart-card-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ValueChartCardWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('chartElement', {static: false}) + chartElement: ElementRef; + + @ViewChild('valueChartCardContent', {static: false}) + valueChartCardContent: ElementRef; + + @ViewChild('valueChartCardValue', {static: false}) + valueChartCardValue: ElementRef; + + settings: ValueChartCardWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + layout: ValueChartCardLayout; + + showValue = true; + valueText = 'N/A'; + valueStyle: ComponentStyle = {}; + valueColor: ColorProcessor; + + backgroundStyle: ComponentStyle = {}; + overlayStyle: ComponentStyle = {}; + + private flot: TbFlot; + private flotDataKey: DataKey; + + private valueKey: DataKey; + + private contentResize$: ResizeObserver; + + private decimals = 0; + private units = ''; + + constructor(private renderer: Renderer2, + private widgetComponent: WidgetComponent, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.valueChartCardWidget = this; + this.settings = {...valueChartCardDefaultSettings, ...this.ctx.settings}; + + if (this.showValue) { + this.decimals = this.ctx.decimals; + this.units = this.ctx.units; + const dataKey = getDataKey(this.ctx.datasources); + if (dataKey?.name && this.ctx.defaultSubscription.firstDatasource?.latestDataKeys?.length) { + const dataKeys = this.ctx.defaultSubscription.firstDatasource?.latestDataKeys; + this.valueKey = dataKeys?.find(k => k.name === dataKey.name); + if (isDefinedAndNotNull(this.valueKey?.decimals)) { + this.decimals = this.valueKey.decimals; + } + if (this.valueKey?.units) { + this.units = dataKey.units; + } + } + } + + this.layout = this.settings.layout; + + this.showValue = this.settings.showValue; + this.valueStyle = textStyle(this.settings.valueFont, '0.25px'); + this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); + + this.backgroundStyle = backgroundStyle(this.settings.background); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + + if (this.ctx.defaultSubscription.firstDatasource?.dataKeys?.length) { + this.flotDataKey = this.ctx.defaultSubscription.firstDatasource?.dataKeys[0]; + this.flotDataKey.settings = { + fillLines: false, + showLines: true, + lineWidth: 2 + } as TbFlotKeySettings; + } + } + + public ngAfterViewInit() { + const settings = { + shadowSize: 0, + enableSelection: false, + smoothLines: true, + grid: { + tickColor: 'rgba(0,0,0,0.12)', + horizontalLines: false, + verticalLines: false, + outlineWidth: 0, + minBorderMargin: 0, + margin: 0 + }, + yaxis: { + showLabels: false, + tickGenerator: 'return [(axis.max + axis.min) / 2];' + }, + xaxis: { + showLabels: false + } + } as TbFlotSettings; + this.flot = new TbFlot(this.ctx, 'line', $(this.chartElement.nativeElement), settings); + + this.contentResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.contentResize$.observe(this.valueChartCardContent.nativeElement); + this.onResize(); + } + + ngOnDestroy() { + if (this.contentResize$) { + this.contentResize$.disconnect(); + } + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onDataUpdated() { + this.flot.update(); + } + + public onLatestDataUpdated() { + if (this.showValue && this.valueKey) { + const tsValue = getTsValueByLatestDataKey(this.ctx.latestData, this.valueKey); + let value; + if (tsValue) { + value = tsValue[1]; + this.valueText = formatValue(value, this.decimals, this.units, true); + } else { + this.valueText = 'N/A'; + } + this.valueColor.update(value); + this.cd.detectChanges(); + setTimeout(() => { + this.onResize(false); + }, 0); + } + } + + public onEditModeChanged() { + this.flot.checkMouseEvents(); + } + + public onDestroy() { + this.flot.destroy(); + } + + private onResize(fitMaxWidth = true) { + if (this.settings.autoScale && this.showValue) { + const contentWidth = this.valueChartCardContent.nativeElement.getBoundingClientRect().width; + const contentHeight = this.valueChartCardContent.nativeElement.getBoundingClientRect().height; + const targetValueWidth = valueRelativeWidth * contentWidth; + this.setValueFontSize(targetValueWidth, contentHeight, fitMaxWidth); + } + this.flot.resize(); + } + + private setValueFontSize(maxWidth: number, maxHeight: number, fitMaxWidth = true) { + const fontSize = getComputedStyle(this.valueChartCardValue.nativeElement).fontSize; + let valueFontSize = resolveCssSize(fontSize)[0]; + this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px'); + this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'lineHeight', '1'); + let valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; + while (fitMaxWidth && valueWidth < maxWidth) { + valueFontSize++; + this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px'); + valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; + } + let valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height; + while ((valueWidth > maxWidth || valueHeight > maxHeight) && valueFontSize > 6) { + valueFontSize--; + this.renderer.setStyle(this.valueChartCardValue.nativeElement, 'fontSize', valueFontSize + 'px'); + valueWidth = this.valueChartCardValue.nativeElement.getBoundingClientRect().width; + valueHeight = this.valueChartCardValue.nativeElement.getBoundingClientRect().height; + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.models.ts new file mode 100644 index 0000000000..e7a4a6d356 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/value-chart-card-widget.models.ts @@ -0,0 +1,77 @@ +/// +/// Copyright © 2016-2023 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 { + BackgroundSettings, + BackgroundType, + ColorSettings, + constantColor, + Font +} from '@shared/models/widget-settings.models'; + +export enum ValueChartCardLayout { + left = 'left', + right = 'right' +} + +export const valueCartCardLayouts = Object.keys(ValueChartCardLayout) as ValueChartCardLayout[]; + +export const valueChartCardLayoutTranslations = new Map( + [ + [ValueChartCardLayout.left, 'widgets.value-chart-card.layout-left'], + [ValueChartCardLayout.right, 'widgets.value-chart-card.layout-right'] + ] +); + +export const valueChartCardLayoutImages = new Map( + [ + [ValueChartCardLayout.left, 'assets/widget/value-chart-card/left-layout.svg'], + [ValueChartCardLayout.right, 'assets/widget/value-chart-card/right-layout.svg'] + ] +); + +export interface ValueChartCardWidgetSettings { + layout: ValueChartCardLayout; + autoScale: boolean; + showValue: boolean; + valueFont: Font; + valueColor: ColorSettings; + background: BackgroundSettings; +} + +export const valueChartCardDefaultSettings: ValueChartCardWidgetSettings = { + layout: ValueChartCardLayout.left, + autoScale: true, + showValue: true, + valueFont: { + family: 'Roboto', + size: 28, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '32px' + }, + valueColor: constantColor('rgba(0, 0, 0, 0.87)'), + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + } +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html new file mode 100644 index 0000000000..5b4a671772 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.html @@ -0,0 +1,54 @@ + + + + widgets.value-chart-card.value-chart-card-style + + + {{ valueChartCardLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widgets.value-chart-card.auto-scale' | translate }} + + + + + {{ 'widgets.value-chart-card.value' | translate }} + + + + + + + + + + {{ 'widgets.background.background' | translate }} + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts new file mode 100644 index 0000000000..88db47b3ad --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component.ts @@ -0,0 +1,98 @@ +/// +/// Copyright © 2016-2023 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, Injector } from '@angular/core'; +import { WidgetSettings, WidgetSettingsComponent } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { formatValue } from '@core/utils'; +import { + valueCartCardLayouts, + valueChartCardDefaultSettings, + valueChartCardLayoutImages, + valueChartCardLayoutTranslations +} from '@home/components/widget/lib/cards/value-chart-card-widget.models'; + +@Component({ + selector: 'tb-value-chart-card-widget-settings', + templateUrl: './value-chart-card-widget-settings.component.html', + styleUrls: [] +}) +export class ValueChartCardWidgetSettingsComponent extends WidgetSettingsComponent { + + valueChartCardLayouts = valueCartCardLayouts; + + valueChartCardLayoutTranslationMap = valueChartCardLayoutTranslations; + valueChartCardLayoutImageMap = valueChartCardLayoutImages; + + valueChartCardWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.valueChartCardWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...valueChartCardDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.valueChartCardWidgetSettingsForm = this.fb.group({ + layout: [settings.layout, []], + autoScale: [settings.autoScale, []], + + showValue: [settings.showValue, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showValue']; + } + + protected updateValidators(emitEvent: boolean) { + const showValue: boolean = this.valueChartCardWidgetSettingsForm.get('showValue').value; + + if (showValue) { + this.valueChartCardWidgetSettingsForm.get('valueFont').enable(); + this.valueChartCardWidgetSettingsForm.get('valueColor').enable(); + } else { + this.valueChartCardWidgetSettingsForm.get('valueFont').disable(); + this.valueChartCardWidgetSettingsForm.get('valueColor').disable(); + } + + this.valueChartCardWidgetSettingsForm.get('valueFont').updateValueAndValidity({emitEvent}); + this.valueChartCardWidgetSettingsForm.get('valueColor').updateValueAndValidity({emitEvent}); + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(22, decimals, units, true); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index 595b2bdc41..c4d2a1edc5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -294,6 +294,9 @@ import { import { SignalStrengthWidgetSettingsComponent } from '@home/components/widget/lib/settings/indicator/signal-strength-widget-settings.component'; +import { + ValueChartCardWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component'; @NgModule({ declarations: [ @@ -402,7 +405,8 @@ import { EntityCountWidgetSettingsComponent, BatteryLevelWidgetSettingsComponent, WindSpeedDirectionWidgetSettingsComponent, - SignalStrengthWidgetSettingsComponent + SignalStrengthWidgetSettingsComponent, + ValueChartCardWidgetSettingsComponent ], imports: [ CommonModule, @@ -516,7 +520,8 @@ import { EntityCountWidgetSettingsComponent, BatteryLevelWidgetSettingsComponent, WindSpeedDirectionWidgetSettingsComponent, - SignalStrengthWidgetSettingsComponent + SignalStrengthWidgetSettingsComponent, + ValueChartCardWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -595,5 +600,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/value-chart-card/right-layout.svg b/ui-ngx/src/assets/widget/value-chart-card/right-layout.svg new file mode 100644 index 0000000000..662b62d47a --- /dev/null +++ b/ui-ngx/src/assets/widget/value-chart-card/right-layout.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +