diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts
index 8a8974c8bd..1f617958e8 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/count/count-widget.component.ts
@@ -77,6 +77,8 @@ export class CountWidgetComponent implements OnInit {
showChevron = false;
chevronStyle: ComponentStyle = {};
+ hasCardClickAction = false;
+
constructor(private widgetComponent: WidgetComponent,
private cd: ChangeDetectorRef) {
}
@@ -116,6 +118,8 @@ export class CountWidgetComponent implements OnInit {
this.showChevron = this.settings.showChevron;
this.chevronStyle = iconStyle(this.settings.chevronSize, this.settings.chevronSizeUnit);
this.chevronStyle.color = this.settings.chevronColor;
+
+ this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
}
public onInit() {
@@ -138,11 +142,6 @@ export class CountWidgetComponent implements OnInit {
}
public cardClick($event: Event) {
- const descriptors = this.ctx.actionsApi.getActionDescriptors('cardClick');
- if (descriptors.length) {
- $event.stopPropagation();
- const descriptor = descriptors[0];
- this.ctx.actionsApi.handleWidgetAction($event, descriptor);
- }
+ this.ctx.actionsApi.cardClick($event);
}
}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html
new file mode 100644
index 0000000000..89ca327b71
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.html
@@ -0,0 +1,44 @@
+
+
+
+
+
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
new file mode 100644
index 0000000000..5dba13ec07
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.scss
@@ -0,0 +1,158 @@
+/**
+ * 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.
+ */
+:host {
+ .tb-battery-level-panel {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ padding: 20px 24px 24px 24px;
+ &.tb-battery-level-pointer {
+ cursor: pointer;
+ }
+ > div:not(.tb-battery-level-overlay) {
+ z-index: 1;
+ }
+ .tb-battery-level-overlay {
+ position: absolute;
+ top: 12px;
+ left: 12px;
+ bottom: 12px;
+ right: 12px;
+ }
+ .tb-battery-level-content {
+ min-height: 0;
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ &.vertical {
+ flex-direction: row;
+ gap: 16px;
+ .tb-battery-level-value-box {
+ align-items: center;
+ .tb-battery-level-value {
+ padding: 8px 12px;
+ }
+ }
+ }
+ &.horizontal {
+ flex-direction: column-reverse;
+ gap: 8px;
+ align-items: center;
+ .tb-battery-level-value-box {
+ .tb-battery-level-value {
+ padding: 4px 6px;
+ }
+ }
+ }
+ .tb-battery-level-box {
+ display: flex;
+ align-items: center;
+ .tb-battery-level-rectangle {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ .tb-battery-level-shape {
+ position: absolute;
+ inset: 0;
+ mask-repeat: no-repeat;
+ mask-size: cover;
+ mask-position: center;
+ }
+ .tb-battery-level-container {
+ position: absolute;
+ display: flex;
+ gap: 3%;
+ }
+ .tb-battery-level-indicator-box {
+ width: 100%;
+ height: 100%;
+ &.solid {
+ background-repeat: no-repeat;
+ transition: background 0.2s ease-out;
+ }
+ &.divided {
+ transition: opacity 0.2s ease-out;
+ }
+ }
+ &.vertical {
+ .tb-battery-level-shape {
+ mask-image: url(/assets/widget/battery-level/battery-shape-vertical.svg);
+ }
+ .tb-battery-level-container {
+ flex-direction: column-reverse;
+ }
+ &.solid {
+ .tb-battery-level-container {
+ inset: 8.85% 6.25% 3.54% 6.25%;
+ }
+ }
+ &.divided {
+ .tb-battery-level-container {
+ inset: 9.73% 7.81% 4.42% 7.81%;
+ }
+ }
+ .tb-battery-level-indicator-box {
+ &.solid {
+ border-radius: 10.7% / 6%;
+ background-position: 0 101%;
+ }
+ &.divided {
+ border-radius: 7.14% / 17.8%;
+ }
+ }
+ }
+ &.horizontal {
+ .tb-battery-level-shape {
+ mask-image: url(/assets/widget/battery-level/battery-shape-horizontal.svg);
+ }
+ .tb-battery-level-container {
+ inset: 6.25% 8.85% 6.25% 3.54%;
+ flex-direction: row;
+ }
+ &.solid {
+ .tb-battery-level-container {
+ inset: 6.25% 8.85% 6.25% 3.54%;
+ }
+ }
+ &.divided {
+ .tb-battery-level-container {
+ inset: 7.81% 9.73% 7.81% 4.42%;
+ }
+ }
+ .tb-battery-level-indicator-box {
+ &.solid {
+ border-radius: 6% / 10.7%;
+ background-position: -1% 0%;
+ }
+ &.divided {
+ border-radius: 17.8% / 7.14%;
+ }
+ }
+ }
+ }
+ }
+ .tb-battery-level-value-box {
+ display: flex;
+ .tb-battery-level-value {
+ white-space: nowrap;
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000000..442d207a5e
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.component.ts
@@ -0,0 +1,293 @@
+///
+/// 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
+} 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,
+ textStyle
+} from '@shared/models/widget-settings.models';
+import { WidgetComponent } from '@home/components/widget/widget.component';
+import {
+ batteryLevelDefaultSettings,
+ BatteryLevelLayout,
+ BatteryLevelWidgetSettings
+} from '@home/components/widget/lib/indicator/battery-level-widget.models';
+import { ResizeObserver } from '@juggle/resize-observer';
+
+const verticalBatteryDimensions = {
+ shapeAspectRatio: 64 / 113,
+ widthRatio: {
+ valueTopBottomPaddingRatio: 8 / 64,
+ valueLeftRightPaddingRatio: 12 / 64,
+ valueFontSizeRatio: 20 / 64,
+ valueLineHeightRaio: 24 / 64
+ },
+ heightRatio: {
+ valueTopBottomPaddingRatio: 8 / 113,
+ valueLeftRightPaddingRatio: 12 / 113,
+ valueFontSizeRatio: 20 / 113,
+ valueLineHeightRaio: 24 / 113
+ }
+};
+
+const horizontalBatteryDimensions = {
+ shapeAspectRatio: 113 / 64,
+ heightRatio: {
+ valueTopBottomPaddingRatio: 4 / 64,
+ valueFontSizeRatio: 20 / 64,
+ valueLineHeightRatio: 24 / 64
+ }
+};
+
+@Component({
+ selector: 'tb-battery-level-widget',
+ templateUrl: './battery-level-widget.component.html',
+ styleUrls: ['./battery-level-widget.component.scss']
+})
+export class BatteryLevelWidgetComponent implements OnInit, OnDestroy, AfterViewInit {
+
+ @ViewChild('batteryLevelContent', {static: true})
+ batteryLevelContent: ElementRef
;
+
+ @ViewChild('batteryLevelBox', {static: true})
+ batteryLevelBox: ElementRef;
+
+ @ViewChild('batteryLevelRectangle', {static: true})
+ batteryLevelRectangle: ElementRef;
+
+ @ViewChild('batteryLevelValueBox', {static: false})
+ batteryLevelValueBox: ElementRef;
+
+ @ViewChild('batteryLevelValue', {static: false})
+ batteryLevelValue: ElementRef;
+
+ settings: BatteryLevelWidgetSettings;
+
+ @Input()
+ ctx: WidgetContext;
+
+ @Input()
+ widgetTitlePanel: TemplateRef;
+
+ layout: BatteryLevelLayout;
+ layoutClass = 'vertical';
+
+ vertical = true;
+ solid = true;
+
+ showValue = true;
+ autoScaleValueSize = true;
+ valueText = 'N/A';
+ valueStyle: ComponentStyle = {};
+ valueColor: ColorProcessor;
+
+ value: number;
+
+ batterySections: boolean[] = [false, false, false, false];
+
+ batteryLevelColor: ColorProcessor;
+
+ batteryShapeColor: ColorProcessor;
+
+ backgroundStyle: ComponentStyle = {};
+ overlayStyle: ComponentStyle = {};
+
+ batteryBoxResize$: ResizeObserver;
+
+ hasCardClickAction = false;
+
+ private decimals = 0;
+ private units = '';
+
+ constructor(private date: DatePipe,
+ private widgetComponent: WidgetComponent,
+ private renderer: Renderer2,
+ private cd: ChangeDetectorRef) {
+ }
+
+ ngOnInit(): void {
+ this.ctx.$scope.batteryLevelWidget = this;
+ this.settings = {...batteryLevelDefaultSettings, ...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.vertical = [BatteryLevelLayout.vertical_solid, BatteryLevelLayout.vertical_divided].includes(this.layout);
+ this.layoutClass = this.vertical ? 'vertical' : 'horizontal';
+ this.solid = [BatteryLevelLayout.vertical_solid, BatteryLevelLayout.horizontal_solid].includes(this.layout);
+
+ this.showValue = this.settings.showValue;
+ this.autoScaleValueSize = this.showValue && this.settings.autoScaleValueSize;
+ this.valueStyle = textStyle(this.settings.valueFont, '0.1px');
+ this.valueColor = ColorProcessor.fromSettings(this.settings.valueColor);
+
+ this.batteryLevelColor = ColorProcessor.fromSettings(this.settings.batteryLevelColor);
+
+ this.batteryShapeColor = ColorProcessor.fromSettings(this.settings.batteryShapeColor);
+
+ this.backgroundStyle = backgroundStyle(this.settings.background);
+ this.overlayStyle = overlayStyle(this.settings.background.overlay);
+
+ this.hasCardClickAction = this.ctx.actionsApi.getActionDescriptors('cardClick').length > 0;
+ }
+
+ ngAfterViewInit() {
+ this.batteryBoxResize$ = new ResizeObserver(() => {
+ this.onResize();
+ });
+ this.batteryBoxResize$.observe(this.batteryLevelContent.nativeElement);
+ if (this.showValue) {
+ this.batteryBoxResize$.observe(this.batteryLevelValueBox.nativeElement);
+ }
+ this.onResize();
+ }
+
+ ngOnDestroy() {
+ if (this.batteryBoxResize$) {
+ this.batteryBoxResize$.disconnect();
+ }
+ }
+
+ 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';
+ }
+ if (!this.solid) {
+ const sectionSize = 100 / this.batterySections.length;
+ for (let i=0; i sectionSize * i;
+ }
+ }
+ this.valueColor.update(this.value);
+ this.batteryLevelColor.update(this.value);
+ this.batteryShapeColor.update(this.value);
+ this.cd.detectChanges();
+ }
+
+ public trackBySection(index: number): number {
+ return index;
+ }
+
+ public cardClick($event: Event) {
+ this.ctx.actionsApi.cardClick($event);
+ }
+
+ private onResize() {
+ if (this.vertical) {
+ if (this.batteryLevelValue) {
+ const contentWidth = this.batteryLevelContent.nativeElement.getBoundingClientRect().width;
+ const boxWidth = (contentWidth - 16) / 2;
+ const boxHeight = this.batteryLevelContent.nativeElement.getBoundingClientRect().height;
+ const ratios = contentWidth > boxHeight ? verticalBatteryDimensions.heightRatio : verticalBatteryDimensions.widthRatio;
+ const boxSize = contentWidth > boxHeight ? boxHeight : boxWidth;
+ const topBottomValuePadding = ratios.valueTopBottomPaddingRatio * boxSize;
+ const leftRightValuePadding = ratios.valueLeftRightPaddingRatio * boxSize;
+ const valuePadding = `${topBottomValuePadding}px ${leftRightValuePadding}px`;
+ this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'padding', valuePadding);
+ if (this.autoScaleValueSize) {
+ const valueFontSize = ratios.valueFontSizeRatio * boxSize;
+ const valueLineHeight = ratios.valueLineHeightRaio * boxSize;
+ this.setValueFontSize(valueFontSize, valueLineHeight, boxWidth);
+ }
+ }
+ let height = this.batteryLevelContent.nativeElement.getBoundingClientRect().height;
+ const width = height * verticalBatteryDimensions.shapeAspectRatio;
+ this.renderer.setStyle(this.batteryLevelBox.nativeElement, 'width', width + 'px');
+ const realWidth = this.batteryLevelBox.nativeElement.getBoundingClientRect().width;
+ if (realWidth < width) {
+ height = realWidth / verticalBatteryDimensions.shapeAspectRatio;
+ this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'height', height + 'px');
+ } else {
+ this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'height', null);
+ }
+ } else {
+ const width = this.batteryLevelContent.nativeElement.getBoundingClientRect().width;
+ let height = width / horizontalBatteryDimensions.shapeAspectRatio;
+ this.renderer.setStyle(this.batteryLevelBox.nativeElement, 'height', height + 'px');
+ const realHeight = this.batteryLevelBox.nativeElement.getBoundingClientRect().height;
+ if (realHeight < height) {
+ height = realHeight;
+ const newWidth = height * horizontalBatteryDimensions.shapeAspectRatio;
+ this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', newWidth + 'px');
+ } else {
+ this.renderer.setStyle(this.batteryLevelRectangle.nativeElement, 'width', null);
+ }
+ if (this.batteryLevelValue) {
+ const ratios = horizontalBatteryDimensions.heightRatio;
+ const valuePadding = `${(ratios.valueTopBottomPaddingRatio * height)}px 6px`;
+ this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'padding', valuePadding);
+ if (this.autoScaleValueSize) {
+ const valueFontSize = ratios.valueFontSizeRatio * height;
+ const valueLineHeight = ratios.valueLineHeightRatio * height;
+ const boxWidth = this.batteryLevelContent.nativeElement.getBoundingClientRect().width;
+ this.setValueFontSize(valueFontSize, valueLineHeight, boxWidth);
+ }
+ }
+ }
+ }
+
+ private setValueFontSize(valueFontSize: number, valueLineHeight: number, maxWidth: number) {
+ this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'fontSize', valueFontSize + 'px');
+ this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'lineHeight', valueLineHeight + 'px');
+ let valueWidth = this.batteryLevelValue.nativeElement.getBoundingClientRect().width;
+ while (valueWidth > maxWidth && valueFontSize > 6) {
+ valueFontSize--;
+ valueLineHeight--;
+ this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'fontSize', valueFontSize + 'px');
+ this.renderer.setStyle(this.batteryLevelValue.nativeElement, 'lineHeight', valueLineHeight + 'px');
+ valueWidth = this.batteryLevelValue.nativeElement.getBoundingClientRect().width;
+ }
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts
new file mode 100644
index 0000000000..8d8724f901
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/battery-level-widget.models.ts
@@ -0,0 +1,108 @@
+///
+/// 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,
+ ColorType,
+ constantColor,
+ defaultColorFunction,
+ Font
+} from '@shared/models/widget-settings.models';
+
+export enum BatteryLevelLayout {
+ vertical_solid = 'vertical_solid',
+ horizontal_solid = 'horizontal_solid',
+ vertical_divided = 'vertical_divided',
+ horizontal_divided = 'horizontal_divided'
+}
+
+export const batteryLevelLayouts = Object.keys(BatteryLevelLayout) as BatteryLevelLayout[];
+
+export const batteryLevelLayoutTranslations = new Map(
+ [
+ [BatteryLevelLayout.vertical_solid, 'widgets.battery-level.layout-vertical-solid'],
+ [BatteryLevelLayout.horizontal_solid, 'widgets.battery-level.layout-horizontal-solid'],
+ [BatteryLevelLayout.vertical_divided, 'widgets.battery-level.layout-vertical-divided'],
+ [BatteryLevelLayout.horizontal_divided, 'widgets.battery-level.layout-horizontal-divided']
+ ]
+);
+
+export const batteryLevelLayoutImages = new Map(
+ [
+ [BatteryLevelLayout.vertical_solid, 'assets/widget/battery-level/vertical-solid-layout.svg'],
+ [BatteryLevelLayout.horizontal_solid, 'assets/widget/battery-level/horizontal-solid-layout.svg'],
+ [BatteryLevelLayout.vertical_divided, 'assets/widget/battery-level/vertical-divided-layout.svg'],
+ [BatteryLevelLayout.horizontal_divided, 'assets/widget/battery-level/horizontal-divided-layout.svg']
+ ]
+);
+
+export interface BatteryLevelWidgetSettings {
+ layout: BatteryLevelLayout;
+ showValue: boolean;
+ autoScaleValueSize: boolean;
+ valueFont: Font;
+ valueColor: ColorSettings;
+ batteryLevelColor: ColorSettings;
+ batteryShapeColor: ColorSettings;
+ background: BackgroundSettings;
+}
+
+export const batteryLevelDefaultSettings: BatteryLevelWidgetSettings = {
+ layout: BatteryLevelLayout.vertical_solid,
+ showValue: true,
+ autoScaleValueSize: true,
+ valueFont: {
+ family: 'Roboto',
+ size: 20,
+ sizeUnit: 'px',
+ style: 'normal',
+ weight: '500',
+ lineHeight: '24px'
+ },
+ valueColor: constantColor('rgba(0, 0, 0, 0.87)'),
+ batteryLevelColor: {
+ color: 'rgba(92, 223, 144, 1)',
+ type: ColorType.range,
+ rangeList: [
+ {from: 0, to: 25, color: 'rgba(227, 71, 71, 1)'},
+ {from: 25, to: 50, color: 'rgba(246, 206, 67, 1)'},
+ {from: 50, to: 100, color: 'rgba(92, 223, 144, 1)'}
+ ],
+ colorFunction: defaultColorFunction
+ },
+ batteryShapeColor: {
+ color: 'rgba(92, 223, 144, 0.32)',
+ type: ColorType.range,
+ rangeList: [
+ {from: 0, to: 25, color: 'rgba(227, 71, 71, 0.32)'},
+ {from: 25, to: 50, color: 'rgba(246, 206, 67, 0.32)'},
+ {from: 50, to: 100, color: 'rgba(92, 223, 144, 0.32)'}
+ ],
+ colorFunction: defaultColorFunction
+ },
+ 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/common/font-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
index 9fe3273f74..ac103fe395 100644
--- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
+++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/font-settings-panel.component.html
@@ -19,12 +19,13 @@
widgets.widget-font.font-settings