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 d49db3581d..c1420e6549 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -12,6 +12,7 @@ "cards.horizontal_value_card", "cards.aggregated_value_card", "simple_value_and_chart_card", + "progress_bar", "cards.label_widget", "cards.dashboard_state_widget", "cards.qr_code", diff --git a/application/src/main/data/json/system/widget_bundles/status_indicators.json b/application/src/main/data/json/system/widget_bundles/status_indicators.json index e175ffa5bb..429d3fe016 100644 --- a/application/src/main/data/json/system/widget_bundles/status_indicators.json +++ b/application/src/main/data/json/system/widget_bundles/status_indicators.json @@ -9,6 +9,7 @@ }, "widgetTypeFqns": [ "battery_level", - "signal_strength" + "signal_strength", + "progress_bar" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/progress_bar.json b/application/src/main/data/json/system/widget_types/progress_bar.json new file mode 100644 index 0000000000..df3dcf6268 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/progress_bar.json @@ -0,0 +1,26 @@ +{ + "fqn": "progress_bar", + "name": "Progress bar", + "deprecated": false, + "image": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAAeFBMVEXg4ODf39/g4ODg4OAAAAD////g4OD19fUhISE/Ut3j4+OsrKzHx8eQkJB0dHQ8PDzx8fEvLy+6urrV1dWCgoJYWFienp5KSkrv7++GhoZmZmaXl5f39/epqanLy8vCwsL29vZtfORiceJ+fn7m5ubU1NTn6fve4fIbcz38AAAABXRSTlPvIL+vAC9A4IoAAASZSURBVHja7d0NT9swEIDhMji4nb/jxEk/NmCM7f//w/niQLtJyxamtdfiF6EmDkh+1Di0IojVzdWH1e25d311s7paDXD2+cy49nABDfm0gotodSmQ2woRVoVIq0KkVSHSqhBpVYi0KkRaFSKtGUijctrDv6XQwEzHgDjkXLoAiKaoAurzhxAAGGxBm2gMgE+25SHeUNE0QKbRNo/otlWQo2QNQa6xrdpD1DSqW6vyBhmtrD86RKHNGIcOyOUHZJDDEEIe1xgQdT487gGF0CNmgUXnsH+BhPzhCKDFfNh50OgQ6dgQv0HFEB2hw8QYDQo3HlqGZFf0066GDiNQFmvcMEZNEMdH2zza8WhivopwVEjX9QF7yBCVSbwFCc04YdBoy4xhkw3QYAsWjQciiNhH8OT3a8RnXo4ooRm/M3dMSN91m+QnCM+6zN9h2ShTyq4xy08X9mpcVxgswR4yfktymDsJhICbIPEA4n+GJJXTfNS48atIWQzxABKQl0YEJQDiQ/Dl1LeoDyHtONWYj41TDtDYvJMwTZA2w7ADwwNJAAQS9sqgY4NLyb1CKGCrGNeh0QY7Pq6bHvUEQaMcKh5VJkiAgAmIHfGGw7B5hQD1iMHkR4vlC5TLA+l1jeS9dlo5LdojQeYj/7rBp8xrnvz+kSOCg349WjolpES9itphBEm9BeI3OJ46omLI8qhppP2e6z28sTqvKkRaFSKtCpFWhUirQqRVIdKqEGlViLQqRFoVIq0KkVaFSKtCpFUh0qoQaVWItCpEWhUirQqR1jzER609HEbSbhT4G4g3AXOWYKrpAnYgszlIjyVXJNTl7XOEJMQ+ArXT5L3LpBQJZDYD6RGJAQED5DKoBbnN3uTfvIAAgBB7EFyBzBbQlXsTNV/EQGh/hPgWUZW7e1PglR+hJOw6PA/RfCs2Gsg5nApFYvtO1L2A8xC1n3ne6DWQRewKEfQGSM7FeB5CSrUOUY0QN13D0DPEQuzANiAlhsxHAYNnSA9ci6gZ5Jq2VRbEVCCzWcRYrl2cKRAgk6gT9NPx9xBvbTqYeofBTyz9IlTaiDm3Zp6RckoxAePIScxzGKBEPTnVK5DRDMS+vtZyLMguBbTZv1BxpAw0UpbJDMQ7LAWCXIMl52EsGVAtKCmvv2YgQNYxY0PTbs97xh9SN07KemfITFHHw4nzntA3jO/kPfsZVSHSqhBpVYi0KkRaFSKtCpHWO4B8e/y4rC/f7/5/D8shXz4u7fHuCO0WQz4u7+4YLYY8LnZ8uTtCu+VrZOm59XiMNbJ7x1etM6tCpFUh0qoQac1A7s325L+P2t1/AoBhu90O04SWQ4b1ANsTP10Pa7XOD5/u4fMWbte74WlYDnlOAM3Jn5KBIesBHp7gXmXS5+WQz5/EQOz42TQVIgWyHgDyqfVGyPD0AOkZThxD+JrznBf7NpO+LodAszZbOHEF8nW9ZYEyZvnll9vtQErDNKH3/ZP9vKoQaVWItCpEWhUirQqRVoVIq0KkVSHSqhBpVYi0LgiygovodvXhAv71NIC/Xl2txPzpxNsbMuPm6vr23Ft9uLr5AdpbpSAPQyy7AAAAAElFTkSuQmCC", + "description": "Displays any value reading as a horizontal progress bar. Allows to configure value range, bar colors, and other settings.", + "descriptor": { + "type": "latest", + "sizeX": 3, + "sizeY": 2, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.progressBarWidget.onInit();\n};\n\nself.onDataUpdated = function() {\n self.ctx.$scope.progressBarWidget.onDataUpdated();\n};\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n maxDataKeys: 1,\n singleEntity: true,\n previewWidth: '280px',\n previewHeight: '180px',\n embedTitlePanel: true\n };\n};\n\n\nself.onDestroy = function() {\n};\n", + "settingsSchema": "", + "dataKeySettingsSchema": "", + "settingsDirective": "tb-progress-bar-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-progress-bar-basic-config", + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"humidity\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.2392660816082064,\"funcBody\":\"var value = prevValue + Math.random() * 7;\\nif (value < 0) {\\n\\tvalue = 0;\\n} else if (value > 100) {\\n\\tvalue = 0;\\n}\\nreturn value;\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"rgba(0, 0, 0, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"layout\":\"default\",\"autoScale\":true,\"showValue\":true,\"valueFont\":{\"family\":\"Roboto\",\"size\":24,\"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';\"},\"tickMin\":0,\"tickMax\":100,\"showTicks\":true,\"ticksFont\":{\"family\":\"Roboto\",\"size\":11,\"sizeUnit\":\"px\",\"style\":\"normal\",\"weight\":\"400\",\"lineHeight\":\"16px\"},\"ticksColor\":\"rgba(0,0,0,0.54)\",\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}}},\"title\":\"Progress bar\",\"dropShadow\":true,\"enableFullscreen\":false,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"units\":\"%\",\"decimals\":0,\"useDashboardTimewindow\":true,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"configMode\":\"basic\",\"displayTimewindow\":true,\"margin\":\"0px\",\"borderRadius\":\"0px\",\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"lineHeight\":\"24px\"},\"titleIcon\":\"mdi:water-percent\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"18px\",\"timewindowStyle\":{\"showIcon\":true,\"iconSize\":\"14px\",\"icon\":\"query_builder\",\"iconPosition\":\"left\",\"font\":{\"size\":12,\"sizeUnit\":\"px\",\"family\":null,\"weight\":null,\"style\":null,\"lineHeight\":\"1\"},\"color\":null},\"titleColor\":\"rgba(0, 0, 0, 0.87)\"}" + }, + "externalId": null, + "tags": [ + "progress" + ] +} \ 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 fd0e48322d..c26cf43542 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 @@ -67,6 +67,9 @@ import { import { ValueChartCardBasicConfigComponent } from '@home/components/widget/config/basic/cards/value-chart-card-basic-config.component'; +import { + ProgressBarBasicConfigComponent +} from '@home/components/widget/config/basic/cards/progress-bar-basic-config.component'; @NgModule({ declarations: [ @@ -87,7 +90,8 @@ import { BatteryLevelBasicConfigComponent, WindSpeedDirectionBasicConfigComponent, SignalStrengthBasicConfigComponent, - ValueChartCardBasicConfigComponent + ValueChartCardBasicConfigComponent, + ProgressBarBasicConfigComponent ], imports: [ CommonModule, @@ -112,7 +116,8 @@ import { BatteryLevelBasicConfigComponent, WindSpeedDirectionBasicConfigComponent, SignalStrengthBasicConfigComponent, - ValueChartCardBasicConfigComponent + ValueChartCardBasicConfigComponent, + ProgressBarBasicConfigComponent ] }) export class BasicWidgetConfigModule { @@ -131,5 +136,6 @@ export const basicWidgetConfigComponentsMap: {[key: string]: Type + + + + + + + widget-config.appearance + + + {{ progressBarLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widgets.progress-bar.auto-scale' | translate }} + + + + + {{ 'widget-config.title' | translate }} + + + + + + + + + + + + + + {{ 'widgets.progress-bar.icon' | translate }} + + + + + + + + + + + + + + + {{ 'widgets.progress-bar.value' | translate }} + + + + + + widget-config.decimals-suffix + + + + + + + + + {{ 'widgets.progress-bar.range' | translate }} + + widgets.progress-bar.min + + + + widgets.progress-bar.max + + + + + + + + {{ 'widgets.progress-bar.range-ticks' | translate }} + + + + + + + + + + + widgets.progress-bar.bar + + {{ 'widgets.progress-bar.bar-color' | translate }} + + + + + {{ 'widgets.progress-bar.bar-background' | 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/progress-bar-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts new file mode 100644 index 0000000000..ef54d64861 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/cards/progress-bar-basic-config.component.ts @@ -0,0 +1,262 @@ +/// +/// 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 { + datasourcesHasAggregation, + datasourcesHasOnlyComparisonAggregation, + 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, resolveCssSize } from '@shared/models/widget-settings.models'; +import { + progressBarDefaultSettings, + ProgressBarLayout, + progressBarLayoutImages, + progressBarLayouts, + progressBarLayoutTranslations, + ProgressBarWidgetSettings +} from '@home/components/widget/lib/cards/progress-bar-widget.models'; + +@Component({ + selector: 'tb-progress-bar-basic-config', + templateUrl: './progress-bar-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class ProgressBarBasicConfigComponent extends BasicWidgetConfigComponent { + + public get displayTimewindowConfig(): boolean { + const datasources = this.progressBarWidgetConfigForm.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + + public onlyHistoryTimewindow(): boolean { + const datasources = this.progressBarWidgetConfigForm.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } + + progressBarLayout = ProgressBarLayout; + + progressBarLayouts = progressBarLayouts; + + progressBarLayoutTranslationMap = progressBarLayoutTranslations; + progressBarLayoutImageMap = progressBarLayoutImages; + + progressBarWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.progressBarWidgetConfigForm; + } + + protected setupDefaults(configData: WidgetConfigComponentData) { + this.setupDefaultDatasource(configData, [{ name: 'humidity', label: 'humidity', type: DataKeyType.timeseries }]); + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: ProgressBarWidgetSettings = {...progressBarDefaultSettings, ...(configData.config.settings || {})}; + const iconSize = resolveCssSize(configData.config.iconSize); + this.progressBarWidgetConfigForm = 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, []], + + tickMin: [settings.tickMin, []], + tickMax: [settings.tickMax, []], + + showTicks: [settings.showTicks, []], + ticksFont: [settings.ticksFont, []], + ticksColor: [settings.ticksColor, []], + + barColor: [settings.barColor, []], + barBackground: [settings.barBackground, []], + + 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; + + this.widgetConfig.config.settings.tickMin = config.tickMin; + this.widgetConfig.config.settings.tickMax = config.tickMax; + + this.widgetConfig.config.settings.showTicks = config.showTicks; + this.widgetConfig.config.settings.ticksFont = config.ticksFont; + this.widgetConfig.config.settings.ticksColor = config.ticksColor; + + this.widgetConfig.config.settings.barColor = config.barColor; + this.widgetConfig.config.settings.barBackground = config.barBackground; + + 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', 'showTicks', 'layout']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showTitle: boolean = this.progressBarWidgetConfigForm.get('showTitle').value; + const showIcon: boolean = this.progressBarWidgetConfigForm.get('showIcon').value; + const showValue: boolean = this.progressBarWidgetConfigForm.get('showValue').value; + const showTicks: boolean = this.progressBarWidgetConfigForm.get('showTicks').value; + const layout: ProgressBarLayout = this.progressBarWidgetConfigForm.get('layout').value; + + const ticksEnabled = layout === ProgressBarLayout.default; + + if (showTitle) { + this.progressBarWidgetConfigForm.get('title').enable(); + this.progressBarWidgetConfigForm.get('titleFont').enable(); + this.progressBarWidgetConfigForm.get('titleColor').enable(); + this.progressBarWidgetConfigForm.get('showIcon').enable({emitEvent: false}); + if (showIcon) { + this.progressBarWidgetConfigForm.get('iconSize').enable(); + this.progressBarWidgetConfigForm.get('iconSizeUnit').enable(); + this.progressBarWidgetConfigForm.get('icon').enable(); + this.progressBarWidgetConfigForm.get('iconColor').enable(); + } else { + this.progressBarWidgetConfigForm.get('iconSize').disable(); + this.progressBarWidgetConfigForm.get('iconSizeUnit').disable(); + this.progressBarWidgetConfigForm.get('icon').disable(); + this.progressBarWidgetConfigForm.get('iconColor').disable(); + } + } else { + this.progressBarWidgetConfigForm.get('title').disable(); + this.progressBarWidgetConfigForm.get('titleFont').disable(); + this.progressBarWidgetConfigForm.get('titleColor').disable(); + this.progressBarWidgetConfigForm.get('showIcon').disable({emitEvent: false}); + this.progressBarWidgetConfigForm.get('iconSize').disable(); + this.progressBarWidgetConfigForm.get('iconSizeUnit').disable(); + this.progressBarWidgetConfigForm.get('icon').disable(); + this.progressBarWidgetConfigForm.get('iconColor').disable(); + } + + if (showValue) { + this.progressBarWidgetConfigForm.get('units').enable(); + this.progressBarWidgetConfigForm.get('decimals').enable(); + this.progressBarWidgetConfigForm.get('valueFont').enable(); + this.progressBarWidgetConfigForm.get('valueColor').enable(); + } else { + this.progressBarWidgetConfigForm.get('units').disable(); + this.progressBarWidgetConfigForm.get('decimals').disable(); + this.progressBarWidgetConfigForm.get('valueFont').disable(); + this.progressBarWidgetConfigForm.get('valueColor').disable(); + } + + if (ticksEnabled) { + this.progressBarWidgetConfigForm.get('showTicks').enable({emitEvent: false}); + if (showTicks) { + this.progressBarWidgetConfigForm.get('ticksFont').enable(); + this.progressBarWidgetConfigForm.get('ticksColor').enable(); + } else { + this.progressBarWidgetConfigForm.get('ticksFont').disable(); + this.progressBarWidgetConfigForm.get('ticksColor').disable(); + } + } else { + this.progressBarWidgetConfigForm.get('showTicks').disable({emitEvent: false}); + this.progressBarWidgetConfigForm.get('ticksFont').disable(); + this.progressBarWidgetConfigForm.get('ticksColor').disable(); + } + } + + 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(78, decimals, units, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html new file mode 100644 index 0000000000..3f788aaf82 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + {{ valueText }} + + + + + + + {{ settings.tickMin }} + {{ settings.tickMax }} + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.scss new file mode 100644 index 0000000000..7f83997987 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.scss @@ -0,0 +1,84 @@ +/** + * 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-progress-bar-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 20px 24px 24px 24px; + > div:not(.tb-progress-bar-overlay) { + z-index: 1; + } + .tb-progress-bar-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-progress-bar-container { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-start; + &.simplified { + justify-content: center; + } + } + .tb-progress-bar-content { + display: flex; + flex-direction: column; + align-self: stretch; + gap: 4px; + .tb-progress-bar-title-value-row { + display: flex; + flex-direction: row; + justify-content: space-between; + &.flex-end { + justify-content: flex-end; + } + .tb-progress-bar-value { + white-space: nowrap; + } + } + .tb-progress-bar { + display: flex; + flex-direction: column; + .tb-progress-bar-background { + border-radius: 12px; + .tb-progress-bar-progress { + height: 5px; + border-radius: 8px; + transition: background-color .2s, width .2s; + } + } + .tb-progress-bar-ticks { + display: flex; + flex-direction: row; + justify-content: space-between; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts new file mode 100644 index 0000000000..fe1ac58ab4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.component.ts @@ -0,0 +1,221 @@ +/// +/// 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, isNumeric } from '@core/utils'; +import { DatePipe } from '@angular/common'; +import { + backgroundStyle, + ColorProcessor, + ComponentStyle, + getDataKey, + getSingleTsValue, + overlayStyle, + resolveCssSize, + textStyle +} from '@shared/models/widget-settings.models'; +import { WidgetComponent } from '@home/components/widget/widget.component'; +import { progressBarDefaultSettings, ProgressBarLayout, ProgressBarWidgetSettings } from './progress-bar-widget.models'; +import { ResizeObserver } from '@juggle/resize-observer'; + +const defaultLayoutHeight = 80; +const simplifiedLayoutHeight = 75; +const defaultAspect = defaultLayoutHeight / 150; +const simplifiedAspect = simplifiedLayoutHeight / 150; + +@Component({ + selector: 'tb-progress-bar-widget', + templateUrl: './progress-bar-widget.component.html', + styleUrls: ['./progress-bar-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ProgressBarWidgetComponent implements OnInit, OnDestroy, AfterViewInit { + + @ViewChild('progressBarPanel', {static: true}) + progressBarPanel: ElementRef; + + @ViewChild('progressBarContainer', {static: true}) + progressBarContainer: ElementRef; + + progressBarLayout = ProgressBarLayout; + + settings: ProgressBarWidgetSettings; + + @Input() + ctx: WidgetContext; + + @Input() + widgetTitlePanel: TemplateRef; + + layout: ProgressBarLayout; + + layoutClass = ''; + + showTitleValueRow = true; + titleValueRowClass = ''; + + showValue = true; + valueText = 'N/A'; + valueStyle: ComponentStyle = {}; + valueColor: ColorProcessor; + + barWidth = '0%'; + barColor: ColorProcessor; + + showTicks = true; + ticksStyle: ComponentStyle = {}; + + value: number; + + backgroundStyle: ComponentStyle = {}; + overlayStyle: ComponentStyle = {}; + + progressBarPanelResize$: ResizeObserver; + + private decimals = 0; + private units = ''; + + constructor(private date: DatePipe, + private widgetComponent: WidgetComponent, + private renderer: Renderer2, + private cd: ChangeDetectorRef) { + } + + ngOnInit(): void { + this.ctx.$scope.progressBarWidget = this; + this.settings = {...progressBarDefaultSettings, ...this.ctx.settings}; + + this.decimals = this.ctx.decimals; + this.units = this.ctx.units; + const dataKey = getDataKey(this.ctx.datasources); + if (isDefinedAndNotNull(dataKey?.decimals)) { + this.decimals = dataKey.decimals; + } + if (dataKey?.units) { + this.units = dataKey.units; + } + + this.layout = this.settings.layout; + + this.showValue = this.settings.showValue; + this.valueStyle = textStyle(this.settings.valueFont, '0.1px'); + this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor); + + this.showTitleValueRow = this.showValue || + (this.layout === ProgressBarLayout.simplified && this.widgetComponent.dashboardWidget.showWidgetTitlePanel); + + this.layoutClass = (this.layout === ProgressBarLayout.simplified || !this.widgetComponent.dashboardWidget.showWidgetTitlePanel) + ? 'simplified' : ''; + + this.titleValueRowClass = (this.layout === ProgressBarLayout.simplified && + !this.widgetComponent.dashboardWidget.showWidgetTitlePanel) ? 'flex-end' : ''; + + this.barColor = ColorProcessor.fromSettings(this.settings.barColor); + + this.showTicks = this.settings.showTicks && this.layout === ProgressBarLayout.default; + if (this.showTicks) { + this.ticksStyle = textStyle(this.settings.ticksFont, '0.1px'); + this.ticksStyle.color = this.settings.ticksColor; + } + + this.backgroundStyle = backgroundStyle(this.settings.background); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + } + + ngAfterViewInit() { + if (this.settings.autoScale) { + this.renderer.setStyle(this.progressBarContainer.nativeElement, 'overflow', 'visible'); + this.renderer.setStyle(this.progressBarContainer.nativeElement, 'position', 'absolute'); + this.progressBarPanelResize$ = new ResizeObserver(() => { + this.onResize(); + }); + this.progressBarPanelResize$.observe(this.progressBarPanel.nativeElement); + this.onResize(); + } + } + + ngOnDestroy() { + } + + public onInit() { + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + this.cd.detectChanges(); + } + + public onDataUpdated() { + const tsValue = getSingleTsValue(this.ctx.data); + this.value = 0; + if (tsValue && isDefinedAndNotNull(tsValue[1]) && isNumeric(tsValue[1])) { + this.value = tsValue[1]; + this.valueText = formatValue(this.value, this.decimals, this.units, true); + } else { + this.valueText = 'N/A'; + } + this.valueColor.update(this.value); + this.barColor.update(this.value); + const range = this.settings.tickMax - this.settings.tickMin; + this.barWidth = `${(this.value / range) * 100}%`; + this.cd.detectChanges(); + } + + private onResize() { + const paddingLeft = getComputedStyle(this.progressBarPanel.nativeElement).paddingLeft; + const paddingRight = getComputedStyle(this.progressBarPanel.nativeElement).paddingRight; + const paddingTop = getComputedStyle(this.progressBarPanel.nativeElement).paddingTop; + const paddingBottom = getComputedStyle(this.progressBarPanel.nativeElement).paddingBottom; + const pLeft = resolveCssSize(paddingLeft)[0]; + const pRight = resolveCssSize(paddingRight)[0]; + const pTop = resolveCssSize(paddingTop)[0]; + const pBottom = resolveCssSize(paddingBottom)[0]; + const panelWidth = this.progressBarPanel.nativeElement.getBoundingClientRect().width - (pLeft + pRight); + const panelHeight = this.progressBarPanel.nativeElement.getBoundingClientRect().height - (pTop + pBottom); + const defaultLayout = this.layout === ProgressBarLayout.default && this.widgetComponent.dashboardWidget.showWidgetTitlePanel; + let minAspect = defaultLayout ? defaultAspect : simplifiedAspect; + let layoutHeight = defaultLayout ? defaultLayoutHeight : simplifiedLayoutHeight; + if (!this.showTitleValueRow) { + minAspect -= (40 / 150); + layoutHeight -= 40; + } + if (!this.showTicks) { + minAspect -= (10 / 150); + layoutHeight -= 10; + } + const aspect = Math.min(panelHeight / panelWidth, minAspect); + const targetWidth = panelWidth; + const targetHeight = targetWidth * aspect; + const scale = targetHeight / layoutHeight; + const width = targetWidth / scale; + const height = panelHeight / scale; + this.renderer.setStyle(this.progressBarContainer.nativeElement, 'width', width + 'px'); + this.renderer.setStyle(this.progressBarContainer.nativeElement, 'height', height + 'px'); + this.renderer.setStyle(this.progressBarContainer.nativeElement, 'transform', `scale(${scale})`); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.models.ts new file mode 100644 index 0000000000..d0aff79ba5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/progress-bar-widget.models.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 { + BackgroundSettings, + BackgroundType, + ColorSettings, + constantColor, + Font +} from '@shared/models/widget-settings.models'; + +export enum ProgressBarLayout { + default = 'default', + simplified = 'simplified' +} + +export const progressBarLayouts = Object.keys(ProgressBarLayout) as ProgressBarLayout[]; + +export const progressBarLayoutTranslations = new Map( + [ + [ProgressBarLayout.default, 'widgets.progress-bar.layout-default'], + [ProgressBarLayout.simplified, 'widgets.progress-bar.layout-simplified'] + ] +); + +export const progressBarLayoutImages = new Map( + [ + [ProgressBarLayout.default, 'assets/widget/progress-bar/default-layout.svg'], + [ProgressBarLayout.simplified, 'assets/widget/progress-bar/simplified-layout.svg'] + ] +); + +export interface ProgressBarWidgetSettings { + layout: ProgressBarLayout; + autoScale: boolean; + showValue: boolean; + valueFont: Font; + valueColor: ColorSettings; + showTicks: boolean; + tickMin: number; + tickMax: number; + ticksFont: Font; + ticksColor: string; + barColor: ColorSettings; + barBackground: string; + background: BackgroundSettings; +} + +export const progressBarDefaultSettings: ProgressBarWidgetSettings = { + layout: ProgressBarLayout.default, + autoScale: true, + showValue: true, + valueFont: { + family: 'Roboto', + size: 24, + sizeUnit: 'px', + style: 'normal', + weight: '500', + lineHeight: '32px' + }, + valueColor: constantColor('rgba(0, 0, 0, 0.87)'), + showTicks: true, + tickMin: 0, + tickMax: 100, + ticksFont: { + family: 'Roboto', + size: 11, + sizeUnit: 'px', + style: 'normal', + weight: '400', + lineHeight: '16px' + }, + ticksColor: 'rgba(0,0,0,0.54)', + barColor: constantColor('rgba(63, 82, 221, 1)'), + barBackground: 'rgba(0, 0, 0, 0.04)', + 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/indicator/battery-level-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss index 441391d95a..fbf244a383 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss @@ -20,7 +20,7 @@ display: flex; flex-direction: column; gap: 16px; - padding: 24px; + padding: 20px 24px 24px 24px; &.tb-battery-level-pointer { cursor: pointer; } @@ -34,6 +34,9 @@ bottom: 12px; right: 12px; } + div.tb-widget-title { + padding: 0; + } .tb-battery-level-content { min-height: 0; flex: 1; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts index 9161764223..a2793234f4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts @@ -263,6 +263,7 @@ export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterView if (this.autoScaleValueSize) { const valueFontSize = ratios.valueFontSizeRatio * boxSize; const valueLineHeight = ratios.valueLineHeightRaio * boxSize; + this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'minWidth', '0'); this.setValueFontSize(valueFontSize, valueLineHeight, boxWidth); } const fontSize = parseInt(window.getComputedStyle(this.batteryLevelValue.nativeElement).fontSize, 10) || 10; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html new file mode 100644 index 0000000000..bb7089991d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.html @@ -0,0 +1,94 @@ + + + + widgets.progress-bar.progress-bar-card-style + + + {{ progressBarLayoutTranslationMap.get(layout) | translate }} + + + + + {{ 'widgets.progress-bar.auto-scale' | translate }} + + + + + {{ 'widgets.progress-bar.value' | translate }} + + + + + + + + + + {{ 'widgets.progress-bar.range' | translate }} + + widgets.progress-bar.min + + + + widgets.progress-bar.max + + + + + + + + {{ 'widgets.progress-bar.range-ticks' | translate }} + + + + + + + + + + {{ 'widgets.progress-bar.bar-color' | translate }} + + + + + {{ 'widgets.progress-bar.bar-background' | translate }} + + + + + {{ 'widgets.background.background' | translate }} + + + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts new file mode 100644 index 0000000000..b56d8bff3b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/progress-bar-widget-settings.component.ts @@ -0,0 +1,133 @@ +/// +/// 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 { + progressBarDefaultSettings, + ProgressBarLayout, + progressBarLayoutImages, + progressBarLayouts, + progressBarLayoutTranslations +} from '@home/components/widget/lib/cards/progress-bar-widget.models'; + +@Component({ + selector: 'tb-progress-bar-widget-settings', + templateUrl: './progress-bar-widget-settings.component.html', + styleUrls: [] +}) +export class ProgressBarWidgetSettingsComponent extends WidgetSettingsComponent { + + progressBarLayout = ProgressBarLayout; + + progressBarLayouts = progressBarLayouts; + + progressBarLayoutTranslationMap = progressBarLayoutTranslations; + progressBarLayoutImageMap = progressBarLayoutImages; + + progressBarWidgetSettingsForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + private $injector: Injector, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.progressBarWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...progressBarDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.progressBarWidgetSettingsForm = this.fb.group({ + layout: [settings.layout, []], + autoScale: [settings.autoScale, []], + + showValue: [settings.showValue, []], + valueFont: [settings.valueFont, []], + valueColor: [settings.valueColor, []], + + tickMin: [settings.tickMin, []], + tickMax: [settings.tickMax, []], + + showTicks: [settings.showTicks, []], + ticksFont: [settings.ticksFont, []], + ticksColor: [settings.ticksColor, []], + + barColor: [settings.barColor, []], + barBackground: [settings.barBackground, []], + + background: [settings.background, []] + }); + } + + protected validatorTriggers(): string[] { + return ['showValue', 'showTicks', 'layout']; + } + + protected updateValidators(emitEvent: boolean) { + const showValue: boolean = this.progressBarWidgetSettingsForm.get('showValue').value; + const showTicks: boolean = this.progressBarWidgetSettingsForm.get('showTicks').value; + const layout: ProgressBarLayout = this.progressBarWidgetSettingsForm.get('layout').value; + + const ticksEnabled = layout === ProgressBarLayout.default; + + if (showValue) { + this.progressBarWidgetSettingsForm.get('valueFont').enable(); + this.progressBarWidgetSettingsForm.get('valueColor').enable(); + } else { + this.progressBarWidgetSettingsForm.get('valueFont').disable(); + this.progressBarWidgetSettingsForm.get('valueColor').disable(); + } + + if (ticksEnabled) { + this.progressBarWidgetSettingsForm.get('showTicks').enable({emitEvent: false}); + if (showTicks) { + this.progressBarWidgetSettingsForm.get('ticksFont').enable(); + this.progressBarWidgetSettingsForm.get('ticksColor').enable(); + } else { + this.progressBarWidgetSettingsForm.get('ticksFont').disable(); + this.progressBarWidgetSettingsForm.get('ticksColor').disable(); + } + } else { + this.progressBarWidgetSettingsForm.get('showTicks').disable({emitEvent: false}); + this.progressBarWidgetSettingsForm.get('ticksFont').disable(); + this.progressBarWidgetSettingsForm.get('ticksColor').disable(); + } + + this.progressBarWidgetSettingsForm.get('valueFont').updateValueAndValidity({emitEvent}); + this.progressBarWidgetSettingsForm.get('valueColor').updateValueAndValidity({emitEvent}); + this.progressBarWidgetSettingsForm.get('showTicks').updateValueAndValidity({emitEvent: false}); + this.progressBarWidgetSettingsForm.get('ticksFont').updateValueAndValidity({emitEvent}); + this.progressBarWidgetSettingsForm.get('ticksColor').updateValueAndValidity({emitEvent}); + } + + private _valuePreviewFn(): string { + const units: string = this.widgetConfig.config.units; + const decimals: number = this.widgetConfig.config.decimals; + return formatValue(78, 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 c4d2a1edc5..33b4323fdf 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 @@ -297,6 +297,9 @@ import { import { ValueChartCardWidgetSettingsComponent } from '@home/components/widget/lib/settings/cards/value-chart-card-widget-settings.component'; +import { + ProgressBarWidgetSettingsComponent +} from '@home/components/widget/lib/settings/cards/progress-bar-widget-settings.component'; @NgModule({ declarations: [ @@ -406,7 +409,8 @@ import { BatteryLevelWidgetSettingsComponent, WindSpeedDirectionWidgetSettingsComponent, SignalStrengthWidgetSettingsComponent, - ValueChartCardWidgetSettingsComponent + ValueChartCardWidgetSettingsComponent, + ProgressBarWidgetSettingsComponent ], imports: [ CommonModule, @@ -521,7 +525,8 @@ import { BatteryLevelWidgetSettingsComponent, WindSpeedDirectionWidgetSettingsComponent, SignalStrengthWidgetSettingsComponent, - ValueChartCardWidgetSettingsComponent + ValueChartCardWidgetSettingsComponent, + ProgressBarWidgetSettingsComponent ] }) export class WidgetSettingsModule { @@ -601,5 +606,6 @@ export const widgetSettingsComponentsMap: {[key: string]: Type + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/assets/widget/progress-bar/simplified-layout.svg b/ui-ngx/src/assets/widget/progress-bar/simplified-layout.svg new file mode 100644 index 0000000000..2e5f60f77c --- /dev/null +++ b/ui-ngx/src/assets/widget/progress-bar/simplified-layout.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui-ngx/src/form.scss b/ui-ngx/src/form.scss index 82794cbbc5..93272af3ca 100644 --- a/ui-ngx/src/form.scss +++ b/ui-ngx/src/form.scss @@ -225,6 +225,13 @@ margin-left: 12px; } } + .tb-small-label { + height: 40px; + font-size: 14px; + line-height: 40px; + letter-spacing: 0.2px; + color: rgba(0, 0, 0, 0.38); + } } .tb-form-row:not(.tb-standard-fields) .mat-mdc-form-field:not(.tb-not-inline-field), .mat-mdc-form-field.tb-inline-field {