From 0f7142cfef7f04afc69ebf5f2a63885accc0b3f3 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 31 Jan 2025 18:11:51 +0200 Subject: [PATCH 01/22] ERRORs processing DEVICE PROFILE - Added saving the DELETED action when the edge is offline. - Added synchronization of Device Profile twice for its relation with Ota Package. --- .../org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java | 1 + .../server/service/edge/rpc/processor/BaseEdgeProcessor.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 7426207a56..7fab0094b4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -87,6 +87,7 @@ public class EdgeSyncCursor { fetchers.add(new SystemWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new TenantWidgetsBundlesEdgeEventFetcher(ctx.getWidgetsBundleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); + fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); fetchers.add(new OAuth2EdgeEventFetcher(ctx.getDomainService())); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index e448fbd937..8f0f06c6bf 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -120,7 +120,7 @@ public abstract class BaseEdgeProcessor { private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) { return switch (action) { case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, - UPDATED_COMMENT -> true; + UPDATED_COMMENT, DELETED -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, ADMIN_SETTINGS, OTA_PACKAGE, QUEUE, RELATION, NOTIFICATION_TEMPLATE, NOTIFICATION_TARGET, From c3de38305f2ef8210c52f62729c5f21d8faf58d1 Mon Sep 17 00:00:00 2001 From: Illia Tsybulenko-Sihov Date: Fri, 31 Jan 2025 22:27:18 +0200 Subject: [PATCH 02/22] updated prometheus version to 3.1.0 --- docker/docker-compose.prometheus-grafana.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prometheus-grafana.yml b/docker/docker-compose.prometheus-grafana.yml index 311d96bd86..f6a5f7fb6d 100644 --- a/docker/docker-compose.prometheus-grafana.yml +++ b/docker/docker-compose.prometheus-grafana.yml @@ -23,7 +23,7 @@ volumes: services: prometheus: - image: prom/prometheus:v2.1.0 + image: prom/prometheus:v3.1.0 volumes: - ./monitoring/prometheus/:/etc/prometheus/ - prometheus_data:/prometheus From 76155aca75c3e49553700b354941779ff1472f5f Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 4 Feb 2025 21:24:24 +0100 Subject: [PATCH 03/22] Added test to verify attribute and RPC subscriptions removal on session overflow. Signed-off-by: Oleksandra Matviienko --- .../server/actors/ActorSystemContext.java | 2 +- .../device/DeviceActorMessageProcessor.java | 4 +-- .../DeviceActorMessageProcessorTest.java | 30 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index 1c18a31f68..b13cf8e490 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -489,7 +489,7 @@ public class ActorSystemContext { @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter - private long maxConcurrentSessionsPerDevice; + private int maxConcurrentSessionsPerDevice; @Value("${actors.session.sync.timeout:10000}") @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 77bf7c32fa..a29fcfc362 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -126,8 +126,8 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso final TenantId tenantId; final DeviceId deviceId; final LinkedHashMapRemoveEldest sessions; - private final Map attributeSubscriptions; - private final Map rpcSubscriptions; + final Map attributeSubscriptions; + final Map rpcSubscriptions; private final Map toDeviceRpcPendingMap; private final boolean rpcSequential; private final RpcSubmitStrategy rpcSubmitStrategy; diff --git a/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java b/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java index 35f53587e5..8408f9868f 100644 --- a/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java @@ -17,22 +17,27 @@ package org.thingsboard.server.actors.device; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import org.thingsboard.common.util.LinkedHashMapRemoveEldest; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.device.DeviceService; +import org.thingsboard.server.service.transport.TbCoreToTransportService; + +import java.util.UUID; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; public class DeviceActorMessageProcessorTest { - public static final long MAX_CONCURRENT_SESSIONS_PER_DEVICE = 10L; + public static final int MAX_CONCURRENT_SESSIONS_PER_DEVICE = 10; ActorSystemContext systemContext; DeviceService deviceService; TenantId tenantId = TenantId.SYS_TENANT_ID; @@ -47,6 +52,7 @@ public class DeviceActorMessageProcessorTest { willReturn(MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice(); willReturn(deviceService).given(systemContext).getDeviceService(); processor = new DeviceActorMessageProcessor(systemContext, tenantId, deviceId); + willReturn(mock(TbCoreToTransportService.class)).given(systemContext).getTbCoreToTransportService(); } @Test @@ -55,4 +61,26 @@ public class DeviceActorMessageProcessorTest { assertThat(processor.sessions.getMaxEntries(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); assertThat(processor.sessions.getRemovalConsumer(), notNullValue()); } + + @Test + public void givenFullSessionMap_whenSessionOverflow_thenShouldDeleteAttributeAndRPCSubscriptions() { + //givenFullSessionMap + for (int i = 0; i < MAX_CONCURRENT_SESSIONS_PER_DEVICE; i++) { + UUID sessionID = UUID.randomUUID(); + processor.sessions.put(sessionID, Mockito.mock(SessionInfoMetaData.class, RETURNS_DEEP_STUBS)); + processor.attributeSubscriptions.put(sessionID, Mockito.mock(SessionInfo.class)); + processor.rpcSubscriptions.put(sessionID, Mockito.mock(SessionInfo.class)); + } + assertThat(processor.sessions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); + assertThat(processor.attributeSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); + assertThat(processor.rpcSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); + + //add one more + processor.sessions.put(UUID.randomUUID(), Mockito.mock(SessionInfoMetaData.class)); + + assertThat(processor.sessions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); + assertThat(processor.attributeSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE-1)); + assertThat(processor.rpcSubscriptions.size(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE-1)); + + } } \ No newline at end of file From 139c0c1d0433eed08b36eacb56208edf83b31d5c Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Wed, 5 Feb 2025 13:25:00 +0100 Subject: [PATCH 04/22] Type casting was performed in the Test class Signed-off-by: Oleksandra Matviienko --- .../org/thingsboard/server/actors/ActorSystemContext.java | 2 +- .../server/actors/device/DeviceActorMessageProcessorTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index b13cf8e490..1c18a31f68 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -489,7 +489,7 @@ public class ActorSystemContext { @Value("${actors.session.max_concurrent_sessions_per_device:1}") @Getter - private int maxConcurrentSessionsPerDevice; + private long maxConcurrentSessionsPerDevice; @Value("${actors.session.sync.timeout:10000}") @Getter diff --git a/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java b/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java index 8408f9868f..4f571d4fe6 100644 --- a/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessorTest.java @@ -49,7 +49,7 @@ public class DeviceActorMessageProcessorTest { public void setUp() { systemContext = mock(ActorSystemContext.class); deviceService = mock(DeviceService.class); - willReturn(MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice(); + willReturn((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE).given(systemContext).getMaxConcurrentSessionsPerDevice(); willReturn(deviceService).given(systemContext).getDeviceService(); processor = new DeviceActorMessageProcessor(systemContext, tenantId, deviceId); willReturn(mock(TbCoreToTransportService.class)).given(systemContext).getTbCoreToTransportService(); @@ -58,7 +58,7 @@ public class DeviceActorMessageProcessorTest { @Test public void givenSystemContext_whenNewInstance_thenVerifySessionMapMaxSize() { assertThat(processor.sessions, instanceOf(LinkedHashMapRemoveEldest.class)); - assertThat(processor.sessions.getMaxEntries(), is(MAX_CONCURRENT_SESSIONS_PER_DEVICE)); + assertThat(processor.sessions.getMaxEntries(), is((long)MAX_CONCURRENT_SESSIONS_PER_DEVICE)); assertThat(processor.sessions.getRemovalConsumer(), notNullValue()); } From 3a32bf81dab73839a322fd419c93da1c6937e312 Mon Sep 17 00:00:00 2001 From: yevhenii Date: Thu, 6 Feb 2025 13:20:25 +0200 Subject: [PATCH 05/22] Fix error in device profile processing - Added and fixed test --- .../server/controller/EdgeControllerTest.java | 3 +- .../server/edge/AbstractEdgeTest.java | 10 ++--- .../server/edge/DeviceProfileEdgeTest.java | 40 +++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 4d38934dcb..0ffdbe8e60 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -897,7 +897,7 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); - edgeImitator.expectMessageAmount(26); + edgeImitator.expectMessageAmount(27); edgeImitator.connect(); waitForMessages(edgeImitator); @@ -1003,6 +1003,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity")); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "jwt")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); + Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 10db03a6c2..afc0f72081 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -144,7 +144,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(OAuth2ClientUpdateMsg.class); edgeImitator.ignoreType(OAuth2DomainUpdateMsg.class); - edgeImitator.expectMessageAmount(24); + edgeImitator.expectMessageAmount(26); edgeImitator.connect(); requestEdgeRuleChainMetadata(); @@ -265,10 +265,10 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { // 4 messages // - 1 from default profile fetcher - // - 2 from device profile fetcher (default and thermostat) + // - 4 from device profile fetcher (2 * (default and thermostat) before and after ota packages fetcher // - 1 from device fetcher - validateMsgsCnt(DeviceProfileUpdateMsg.class, 4); - validateDeviceProfiles(4); + validateMsgsCnt(DeviceProfileUpdateMsg.class, 6); + validateDeviceProfiles(6); // 3 messages // - 1 from default profile fetcher @@ -656,7 +656,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { } protected RuleChainId createEdgeRuleChainAndAssignToEdge(String ruleChainName) throws Exception { - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); RuleChain ruleChain = new RuleChain(); ruleChain.setName(ruleChainName); ruleChain.setType(RuleChainType.EDGE); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index a085f3a821..244163732f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -126,6 +126,45 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { unAssignFromEdgeAndDeleteDashboard(thermostatsDashboardId); } + @Test + public void testDeleteDeviceProfilesWhenEdgeIsOffline() throws Exception { + RuleChainId thermostatsRuleChainId = createEdgeRuleChainAndAssignToEdge("Thermostats Rule Chain"); + + // create device profile + DeviceProfile deviceProfile = this.createDeviceProfile("ONE_MORE_DEVICE_PROFILE", null); + deviceProfile.setDefaultEdgeRuleChainId(thermostatsRuleChainId); + extendDeviceProfileData(deviceProfile); + edgeImitator.expectMessageAmount(1); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + DeviceProfile deviceProfileMsg = JacksonUtil.fromString(deviceProfileUpdateMsg.getEntity(), DeviceProfile.class, true); + Assert.assertNotNull(deviceProfileMsg); + Assert.assertEquals(deviceProfile, deviceProfileMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + + // delete profile when edge is offline + edgeImitator.disconnect(); + doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) + .andExpect(status().isOk()); + edgeImitator.connect(); + // 27 sync message + // + 1 delete message + edgeImitator.expectMessageAmount(28); + Assert.assertTrue(edgeImitator.waitForMessages()); + + latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(deviceProfile.getUuidId().getMostSignificantBits(), deviceProfileUpdateMsg.getIdMSB()); + Assert.assertEquals(deviceProfile.getUuidId().getLeastSignificantBits(), deviceProfileUpdateMsg.getIdLSB()); + + unAssignFromEdgeAndDeleteRuleChain(thermostatsRuleChainId); + } + @Test public void testDeviceProfiles_snmp() throws Exception { DeviceProfile deviceProfile = createDeviceProfileAndDoBasicAssert("SNMP", createSnmpDeviceProfileTransportConfiguration()); @@ -449,4 +488,5 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { deviceProfile.setProfileData(createProfileData()); return deviceProfile; } + } From cc6f0b6c7b5a1f560b70b24efd984ec6decbda35 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 7 Feb 2025 15:59:52 +0200 Subject: [PATCH 06/22] UI: Value stepper widget --- .../json/system/widget_bundles/buttons.json | 1 + .../widget_bundles/control_widgets.json | 1 + .../system/widget_types/value_stepper.json | 51 +++ .../basic/basic-widget-config.module.ts | 5 + .../power-button-basic-config.component.html | 32 ++ .../power-button-basic-config.component.ts | 38 +- .../value-stepper-basic-config.component.html | 297 ++++++++++++++ .../value-stepper-basic-config.component.ts | 225 ++++++++++ .../lib/rpc/power-button-widget.component.ts | 7 +- .../lib/rpc/power-button-widget.models.ts | 373 +++++++++++------ .../rpc/value-stepper-widget.component.html | 43 ++ .../rpc/value-stepper-widget.component.scss | 90 ++++ .../lib/rpc/value-stepper-widget.component.ts | 388 ++++++++++++++++++ .../lib/rpc/value-stepper-widget.models.ts | 239 +++++++++++ ...lue-stepper-widget-settings.component.html | 266 ++++++++++++ ...value-stepper-widget-settings.component.ts | 191 +++++++++ .../lib/settings/widget-settings.module.ts | 5 + .../widget/widget-components.module.ts | 3 + .../assets/locale/locale.constant-en_US.json | 37 ++ .../assets/widget/value-stepper/filled.svg | 1 + .../widget/value-stepper/simplified.svg | 1 + .../assets/widget/value-stepper/volume.svg | 1 + 22 files changed, 2165 insertions(+), 130 deletions(-) create mode 100644 application/src/main/data/json/system/widget_types/value_stepper.json create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html create mode 100644 ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts create mode 100644 ui-ngx/src/assets/widget/value-stepper/filled.svg create mode 100644 ui-ngx/src/assets/widget/value-stepper/simplified.svg create mode 100644 ui-ngx/src/assets/widget/value-stepper/volume.svg diff --git a/application/src/main/data/json/system/widget_bundles/buttons.json b/application/src/main/data/json/system/widget_bundles/buttons.json index facca89336..939e583686 100644 --- a/application/src/main/data/json/system/widget_bundles/buttons.json +++ b/application/src/main/data/json/system/widget_bundles/buttons.json @@ -12,6 +12,7 @@ "command_button", "toggle_button", "two_segment_button", + "value_stepper", "power_button" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/control_widgets.json b/application/src/main/data/json/system/widget_bundles/control_widgets.json index c3dc219f5b..71a25df0c2 100644 --- a/application/src/main/data/json/system/widget_bundles/control_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/control_widgets.json @@ -12,6 +12,7 @@ "command_button", "toggle_button", "two_segment_button", + "value_stepper", "power_button", "slider", "control_widgets.switch_control", diff --git a/application/src/main/data/json/system/widget_types/value_stepper.json b/application/src/main/data/json/system/widget_types/value_stepper.json new file mode 100644 index 0000000000..0126b25470 --- /dev/null +++ b/application/src/main/data/json/system/widget_types/value_stepper.json @@ -0,0 +1,51 @@ +{ + "fqn": "slider", + "name": "Value stepper", + "deprecated": false, + "image": "tb-image;/api/images/system/value-stepper-widget.svg", + "description": "Allows users to click the buttons to send commands to devices or update attributes/time series data. Configurable settings let users define how to retrieve the initial state and specify actions for each button.", + "descriptor": { + "type": "rpc", + "sizeX": 3.5, + "sizeY": 2, + "resources": [], + "templateHtml": "\n", + "templateCss": "", + "controllerScript": "self.onInit = function() {\n self.ctx.$scope.actionWidget.onInit();\n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '230px',\n previewHeight: '110px',\n embedTitlePanel: true,\n displayRpcMessageToast: false\n };\n};\n\nself.onDestroy = function() {\n}\n", + "dataKeySettingsForm": [], + "settingsDirective": "tb-value-stepper-widget-settings", + "hasBasicMode": true, + "basicModeDirective": "tb-value-stepper-basic-config", + "defaultConfig": "{\"showTitle\":true,\"backgroundColor\":\"#ffffff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"initialState\":{\"action\":\"EXECUTE_RPC\",\"defaultValue\":0,\"executeRpc\":{\"method\":\"getState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return integer value */\\nreturn data;\"}},\"disabledState\":{\"action\":\"DO_NOTHING\",\"defaultValue\":false,\"getAttribute\":{\"key\":\"state\",\"scope\":null},\"getTimeSeries\":{\"key\":\"state\"},\"getAlarmStatus\":{\"severityList\":null,\"typeList\":null},\"dataToValue\":{\"type\":\"NONE\",\"compareToValue\":true,\"dataToValueFunction\":\"/* Should return boolean value */\\nreturn data;\"}},\"leftButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"rightButtonClick\":{\"action\":\"EXECUTE_RPC\",\"executeRpc\":{\"method\":\"setState\",\"requestTimeout\":5000,\"requestPersistent\":false,\"persistentPollingInterval\":1000},\"setAttribute\":{\"key\":\"state\",\"scope\":\"SERVER_SCOPE\"},\"putTimeSeries\":{\"key\":\"state\"},\"valueToData\":{\"type\":\"VALUE\",\"constantValue\":0,\"valueToDataFunction\":\"/* Convert input integer value to RPC parameters or attribute/time-series value */\\nreturn value;\"}},\"appearance\":{\"type\":\"simplified\",\"autoScale\":true,\"minValueRange\":-100,\"maxValueRange\":100,\"valueStep\":0.5,\"showValueBox\":true,\"valueUnits\":\"\",\"valueDecimals\":1,\"valueFont\":{\"family\":\"Roboto\",\"weight\":\"500\",\"style\":\"normal\",\"size\":16,\"sizeUnit\":\"px\",\"lineHeight\":\"24px\"},\"valueColor\":\"#000\",\"valueBoxBackground\":\"rgba(0, 0, 0, 0.04)\",\"showBorder\":true,\"borderWidth\":1,\"borderColor\":\"#305680\"},\"buttonAppearance\":{\"leftButton\":{\"showButton\":true,\"icon\":\"arrow_back_ios_new\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}},\"rightButton\":{\"showButton\":true,\"icon\":\"arrow_forward_ios\",\"iconSize\":24,\"iconSizeUnit\":\"px\",\"mainColorOn\":\"#3F52DD\",\"backgroundColorOn\":\"#FFFFFF\",\"mainColorDisabled\":\"rgba(0,0,0,0.12)\",\"backgroundColorDisabled\":\"#FFFFFF\",\"customStyle\":{\"enabled\":null,\"hovered\":null,\"pressed\":null,\"activated\":null,\"disabled\":null}}},\"background\":{\"type\":\"color\",\"color\":\"#fff\",\"overlay\":{\"enabled\":false,\"color\":\"rgba(255,255,255,0.72)\",\"blur\":3}},\"padding\":\"12px\"},\"title\":\"Value stepper\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"actions\":{},\"widgetCss\":\"\",\"noDataDisplayMessage\":\"\",\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"24px\"},\"showTitleIcon\":false,\"titleTooltip\":\"\",\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"pageSize\":1024,\"titleIcon\":\"\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"14px\",\"configMode\":\"basic\",\"titleColor\":\"rgba(0, 0, 0, 0.87)\",\"datasources\":null,\"borderRadius\":null}" + }, + "resources": [ + { + "link": "/api/images/system/value-stepper-widget.svg", + "title": "\"Value stepper\" system widget image", + "type": "IMAGE", + "subType": "IMAGE", + "fileName": "value-stepper-widget.svg", + "publicResourceKey": "s0UKoqbiMCcKVn0pD55XZzPUR89XlXAO", + "mediaType": "image/svg+xml", + "data": "PHN2ZyB3aWR0aD0iMjE0IiBoZWlnaHQ9Ijc2IiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxnIGZpbHRlcj0idXJsKCNhKSI+PHJlY3QgeD0iOC41IiB5PSI0LjUiIHdpZHRoPSIxOTciIGhlaWdodD0iNTkiIHJ4PSI0IiBmaWxsPSIjZmZmIiBzaGFwZS1yZW5kZXJpbmc9ImNyaXNwRWRnZXMiLz48cmVjdCB4PSI5IiB5PSI1IiB3aWR0aD0iMTk2IiBoZWlnaHQ9IjU4IiByeD0iMy41IiBzdHJva2U9IiMzMDU2ODAiIHNoYXBlLXJlbmRlcmluZz0iY3Jpc3BFZGdlcyIvPjxyZWN0IHg9IjIwLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0zOC41IDQwIDEuNDEtMS40MUwzNS4zMyAzNGw0LjU4LTQuNTlMMzguNSAyOGwtNiA2IDYgNloiIGZpbGw9IiNmZmYiLz48cmVjdCB4PSI2NSIgeT0iMTguNSIgd2lkdGg9Ijg0IiBoZWlnaHQ9IjMxIiByeD0iMy41IiBmaWxsPSIjMzA1NjgwIiBmaWxsLW9wYWNpdHk9Ii4wNCIvPjxyZWN0IHg9IjY1IiB5PSIxOC41IiB3aWR0aD0iODQiIGhlaWdodD0iMzEiIHJ4PSIzLjUiIHN0cm9rZT0iIzMwNTY4MCIvPjxwYXRoIGQ9Ik04OC41OSAzNy41VjM5aC03LjYzdi0xLjI5bDMuNy00LjA0Yy40MS0uNDYuNzMtLjg1Ljk3LTEuMTkuMjMtLjMzLjQtLjYzLjQ5LS45YTIuMyAyLjMgMCAwIDAtLjA2LTEuNzNjLS4xMy0uMjctLjMyLS41LS41OC0uNjVhMS43IDEuNyAwIDAgMC0uOTMtLjI0Yy0uNDIgMC0uNzcuMS0xLjA2LjI3LS4yOC4xOS0uNS40NC0uNjUuNzZhMi42IDIuNiAwIDAgMC0uMjIgMS4xaC0xLjg4YzAtLjY3LjE1LTEuMjcuNDYtMS44Mi4zLS41NS43My0uOTkgMS4zLTEuM2E0LjEgNC4xIDAgMCAxIDIuMDgtLjVjLjc2IDAgMS40LjEzIDEuOTQuMzguNTMuMjYuOTMuNjIgMS4yIDEuMDlhMy4zNiAzLjM2IDAgMCAxIC4yNSAyLjcyIDUgNSAwIDAgMS0uNDkgMS4wNCA5IDkgMCAwIDEtLjc0IDEuMDRjLS4yOC4zNS0uNi43LS45NCAxLjA1bC0yLjQ2IDIuNzFoNS4yNVptOS4yNy05Ljg4djEuMDRMOTMuMyAzOWgtMS45OGw0LjU0LTkuODhoLTUuOXYtMS41aDcuODlabTEuOTggMTAuNDRjMC0uMjkuMS0uNTMuMy0uNzMuMi0uMi40Ni0uMy44LS4zcy42LjEuOC4zYy4yLjIuMy40NC4zLjczYTEgMSAwIDAgMS0uMy43NGMtLjIuMi0uNDYuMy0uOC4zcy0uNi0uMS0uOC0uM2ExIDEgMCAwIDEtLjMtLjc0Wm02LjQ5LTQuMzUtMS41LS4zNy42MS01LjcyaDYuMTR2MS42SDEwN2wtLjMyIDIuNzlhMy42NyAzLjY3IDAgMCAxIDEuODEtLjQ2Yy41NCAwIDEuMDIuMDkgMS40NC4yNi40Mi4xNy43OS40MyAxLjA4Ljc2LjMuMzMuNTMuNzMuNjggMS4yYTUgNSAwIDAgMSAwIDMuMDcgMy4xNyAzLjE3IDAgMCAxLTEuODUgMi4wM2MtLjQ2LjE5LTEuMDEuMjktMS42NS4yOWE0LjYgNC42IDAgMCAxLTEuMzYtLjIgMy43MyAzLjczIDAgMCAxLTEuMTctLjYyIDMuMTQgMy4xNCAwIDAgMS0xLjE5LTIuNDJoMS44NWMuMDUuMzcuMTUuNjkuMy45NS4xNi4yNS4zOC40NS42NC41OGEyIDIgMCAwIDAgLjkyLjJjLjMyIDAgLjYtLjA1LjgzLS4xNi4yMy0uMTEuNDItLjI3LjU3LS40OC4xNS0uMjIuMjctLjQ3LjM0LS43NWEzLjYyIDMuNjIgMCAwIDAtLjAyLTEuODcgMS45OCAxLjk4IDAgMCAwLS4zOC0uNzJjLS4xNy0uMi0uMzgtLjM2LS42My0uNDdhMi4xMyAyLjEzIDAgMCAwLS44OC0uMTdjLS40NSAwLS44LjA3LTEuMDQuMi0uMjMuMTMtLjQ1LjI5LS42NS40OFptMTEuNzUtNC4xNmMwLS4zOC4xLS43Mi4yOC0xLjA0LjE5LS4zMi40NC0uNTcuNzUtLjc2YTEuOTYgMS45NiAwIDAgMSAyLjA1IDBjLjMxLjE5LjU2LjQ0Ljc0Ljc2LjE5LjMyLjI4LjY2LjI4IDEuMDRzLS4xLjczLS4yOCAxLjA1Yy0uMTguMzEtLjQzLjU2LS43NC43NGEyLjA0IDIuMDQgMCAwIDEtMi44LS43NCAyLjAzIDIuMDMgMCAwIDEtLjI4LTEuMDVabTEuMDUgMGExIDEgMCAwIDAgMSAxIC45Ny45NyAwIDAgMCAuOTgtMSAxIDEgMCAwIDAtLjI3LS43Mi45My45MyAwIDAgMC0uNy0uM2MtLjI4IDAtLjUxLjEtLjcxLjMtLjIuMi0uMy40My0uMy43MlptMTIuMTkgNS43NWgxLjk1YTQuNSA0LjUgMCAwIDEtLjYyIDEuOTkgMy43MiAzLjcyIDAgMCAxLTEuNSAxLjM3IDUgNSAwIDAgMS0yLjMzLjUgNC4xNSA0LjE1IDAgMCAxLTMuMzQtMS40NWMtLjQtLjQ4LS43MS0xLjA0LS45Mi0xLjctLjIxLS42Ni0uMzItMS40LS4zMi0yLjIydi0uOTVjMC0uODEuMS0xLjU1LjMyLTIuMjIuMjItLjY2LjUzLTEuMjIuOTQtMS42OS40LS40Ny45LS44NCAxLjQ2LTEuMDlhNC43OCA0Ljc4IDAgMCAxIDEuOTMtLjM3Yy45IDAgMS42Ny4xNyAyLjMuNS42Mi4zMyAxLjEuOCAxLjQ1IDEuMzguMzUuNTkuNTYgMS4yNi42NCAyLjAyaC0xLjk1Yy0uMDUtLjQ4LS4xNy0uOS0uMzUtMS4yNWExLjc3IDEuNzcgMCAwIDAtLjc2LS44IDIuNzMgMi43MyAwIDAgMC0xLjMzLS4yOGMtLjQ1IDAtLjg0LjA4LTEuMTcuMjVhMi4yIDIuMiAwIDAgMC0uODQuNzNjLS4yMi4zMy0uMzkuNzItLjUgMS4yLS4xMS40Ny0uMTcgMS0uMTcgMS42di45N2MwIC41Ny4wNSAxLjEuMTUgMS41Ni4xLjQ3LjI2Ljg2LjQ3IDEuMi4yMS4zMy40OC41OS44MS43Ny4zMy4xOC43Mi4yNyAxLjE4LjI3LjU2IDAgMS0uMDggMS4zNS0uMjYuMzUtLjE4LjYxLS40NC44LS43OC4xNy0uMzQuMy0uNzYuMzUtMS4yNVoiIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjg3Ii8+PHJlY3QgeD0iMTYxLjUiIHk9IjE4IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSIxNiIgZmlsbD0iIzMwNTY4MCIvPjxwYXRoIGQ9Im0xNzUuNSAyOC0xLjQxIDEuNDEgNC41OCA0LjU5LTQuNTggNC41OUwxNzUuNSA0MGw2LTYtNi02WiIgZmlsbD0iI2ZmZiIvPjwvZz48ZGVmcz48ZmlsdGVyIGlkPSJhIiB4PSIuNSIgeT0iLjUiIHdpZHRoPSIyMTMiIGhlaWdodD0iNzUiIGZpbHRlclVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIj48ZmVGbG9vZCBmbG9vZC1vcGFjaXR5PSIwIiByZXN1bHQ9IkJhY2tncm91bmRJbWFnZUZpeCIvPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VBbHBoYSIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIgcmVzdWx0PSJoYXJkQWxwaGEiLz48ZmVPZmZzZXQgZHk9IjQiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSI0Ii8+PGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA0IDAiLz48ZmVCbGVuZCBpbjI9IkJhY2tncm91bmRJbWFnZUZpeCIgcmVzdWx0PSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QxX2Ryb3BTaGFkb3dfNTY2OV8xNjA3MDUiIHJlc3VsdD0ic2hhcGUiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+", + "public": true + } + ], + "scada": false, + "tags": [ + "command", + "downlink", + "device configuration", + "device control", + "invocation", + "remote method", + "remote function", + "interface", + "subroutine call", + "inter-process communication", + "server request", + "update attribute", + "set attribute", + "add time-series" + ] +} \ 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 24cb04cca6..8a71e04e1e 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 @@ -146,6 +146,9 @@ import { ScadaSymbolBasicConfigComponent } from '@home/components/widget/config/ import { SegmentedButtonBasicConfigComponent } from '@home/components/widget/config/basic/button/segmented-button-basic-config.component'; +import { + ValueStepperBasicConfigComponent +} from '@home/components/widget/config/basic/rpc/value-stepper-basic-config.component'; @NgModule({ declarations: [ @@ -182,6 +185,7 @@ import { PowerButtonBasicConfigComponent, SliderBasicConfigComponent, ToggleButtonBasicConfigComponent, + ValueStepperBasicConfigComponent, TimeSeriesChartBasicConfigComponent, ComparisonKeyRowComponent, ComparisonKeysTableComponent, @@ -236,6 +240,7 @@ import { PowerButtonBasicConfigComponent, SliderBasicConfigComponent, ToggleButtonBasicConfigComponent, + ValueStepperBasicConfigComponent, TimeSeriesChartBasicConfigComponent, StatusWidgetBasicConfigComponent, PieChartBasicConfigComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html index 75fcf791b0..a2e3f90aee 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.html @@ -116,6 +116,38 @@ +
+ + {{ 'widgets.power-button.button-icon-on' | translate }} + +
+ + + + + + +
+
+
+ + {{ 'widgets.power-button.button-icon-off' | translate }} + +
+ + + + + + +
+
{{ 'widgets.power-button.power-on-colors' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts index 2c92936141..00b0a41e18 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/button/power-button-basic-config.component.ts @@ -87,6 +87,19 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent icon: [configData.config.titleIcon, []], iconColor: [configData.config.iconColor, []], + onButtonIcon: this.fb.group({ + showIcon: [settings.onButtonIcon.showIcon, []], + iconSize: [settings.onButtonIcon.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.onButtonIcon.iconSizeUnit, []], + icon: [settings.onButtonIcon.icon, []], + }), + offButtonIcon: this.fb.group({ + showIcon: [settings.offButtonIcon.showIcon, []], + iconSize: [settings.offButtonIcon.iconSize, [Validators.min(0)]], + iconSizeUnit: [settings.offButtonIcon.iconSizeUnit, []], + icon: [settings.offButtonIcon.icon, []], + }), + mainColorOn: [settings.mainColorOn, []], backgroundColorOn: [settings.backgroundColorOn, []], @@ -128,6 +141,9 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent this.widgetConfig.config.settings.layout = config.layout; + this.widgetConfig.config.settings.onButtonIcon = config.onButtonIcon; + this.widgetConfig.config.settings.offButtonIcon = config.offButtonIcon; + this.widgetConfig.config.settings.mainColorOn = config.mainColorOn; this.widgetConfig.config.settings.backgroundColorOn = config.backgroundColorOn; @@ -148,12 +164,14 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent } protected validatorTriggers(): string[] { - return ['showTitle', 'showIcon']; + return ['showTitle', 'showIcon', 'onButtonIcon.showIcon', 'offButtonIcon.showIcon']; } protected updateValidators(emitEvent: boolean, trigger?: string) { const showTitle: boolean = this.powerButtonWidgetConfigForm.get('showTitle').value; const showIcon: boolean = this.powerButtonWidgetConfigForm.get('showIcon').value; + const onButtonIcon: boolean = this.powerButtonWidgetConfigForm.get('onButtonIcon').get('showIcon').value; + const offButtonIcon: boolean = this.powerButtonWidgetConfigForm.get('offButtonIcon').get('showIcon').value; if (showTitle) { this.powerButtonWidgetConfigForm.get('title').enable(); this.powerButtonWidgetConfigForm.get('titleFont').enable(); @@ -180,6 +198,24 @@ export class PowerButtonBasicConfigComponent extends BasicWidgetConfigComponent this.powerButtonWidgetConfigForm.get('icon').disable(); this.powerButtonWidgetConfigForm.get('iconColor').disable(); } + if (onButtonIcon) { + this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSize').enable(); + this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSizeUnit').enable(); + this.powerButtonWidgetConfigForm.get('onButtonIcon').get('icon').enable(); + } else { + this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSize').disable(); + this.powerButtonWidgetConfigForm.get('onButtonIcon').get('iconSizeUnit').disable(); + this.powerButtonWidgetConfigForm.get('onButtonIcon').get('icon').disable(); + } + if (offButtonIcon) { + this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSize').enable(); + this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSizeUnit').enable(); + this.powerButtonWidgetConfigForm.get('offButtonIcon').get('icon').enable(); + } else { + this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSize').disable(); + this.powerButtonWidgetConfigForm.get('offButtonIcon').get('iconSizeUnit').disable(); + this.powerButtonWidgetConfigForm.get('offButtonIcon').get('icon').disable(); + } } private getCardButtons(config: WidgetConfig): string[] { diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html new file mode 100644 index 0000000000..748cf64c7f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.html @@ -0,0 +1,297 @@ + + + +
+
widgets.value-stepper.behavior
+
+
widgets.value-stepper.initial-state
+ +
+
+
widgets.value-stepper.left-button-click
+ +
+
+
widgets.value-stepper.right-button-click
+ +
+
+
widgets.button-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ valueStepperTypeTranslationMap.get(type) | translate }} + + +
+ + {{ 'widgets.value-stepper.auto-scale' | translate }} + +
+ +
+
{{ 'widgets.value-stepper.value-range' | translate }}
+
+
widgets.value-stepper.min-range
+ + + +
widgets.value-stepper.max-range
+ + + +
+
+
+
{{ 'widgets.value-stepper.value-increment-decrement-step' | translate }}
+ + + +
+
+ + {{ 'widgets.value-stepper.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
+
+ +
+
{{ 'widgets.value-stepper.value-box-background' | translate }}
+ + +
+
+ + {{ 'widgets.value-stepper.border' | translate }} + +
+ + +
px
+
+ + +
+
+
+
+
+
widgets.value-stepper.button-appearance
+ + {{ 'widgets.value-stepper.left' | translate }} + {{ 'widgets.value-stepper.right' | translate }} + +
+
+
+ + {{ 'widgets.value-stepper.left-button' | translate }} + +
+
+
{{ 'widgets.value-stepper.icon' | translate }}
+
+ + + + + + +
+
+
+
{{ 'widgets.value-stepper.button-on-colors' | translate }}
+
+
+
widgets.value-stepper.main
+ + +
+ +
+
widgets.value-stepper.background
+ + +
+
+
+
+
{{ 'widgets.value-stepper.disabled-colors' | translate }}
+
+
+
widgets.value-stepper.main
+ + +
+ +
+
widgets.value-stepper.background
+ + +
+
+
+
+
+
+ + {{ 'widgets.value-stepper.right-button' | translate }} + +
+
+
{{ 'widgets.value-stepper.icon' | translate }}
+
+ + + + + + +
+
+
+
{{ 'widgets.value-stepper.button-on-colors' | translate }}
+
+
+
widgets.value-stepper.main
+ + +
+ +
+
widgets.value-stepper.background
+ + +
+
+
+
+
{{ 'widgets.value-stepper.disabled-colors' | translate }}
+
+
+
widgets.value-stepper.main
+ + +
+ +
+
widgets.value-stepper.background
+ + +
+
+
+
+
+
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
widget-config.show-card-buttons
+ + {{ 'fullscreen.fullscreen' | translate }} + +
+
+
{{ 'widget-config.card-border-radius' | translate }}
+ + + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+ + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts new file mode 100644 index 0000000000..abee223b40 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/rpc/value-stepper-basic-config.component.ts @@ -0,0 +1,225 @@ +/// +/// Copyright © 2016-2024 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 { TargetDevice, WidgetConfig, } from '@shared/models/widget.models'; +import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; +import { formatValue, isUndefined } from '@core/utils'; +import { ValueType } from '@shared/models/constants'; +import { + valueStepperDefaultSettings, + valueStepperTypeImages, + valueStepperTypes, + valueStepperTypeTranslations, + ValueStepperWidgetSettings +} from '@home/components/widget/lib/rpc/value-stepper-widget.models'; + +type ButtonAppearanceType = 'left' | 'right'; + +@Component({ + selector: 'tb-value-stepper-basic-config', + templateUrl: './value-stepper-basic-config.component.html', + styleUrls: ['../basic-config.scss'] +}) +export class ValueStepperBasicConfigComponent extends BasicWidgetConfigComponent { + + get targetDevice(): TargetDevice { + return this.valueStepperWidgetConfigForm.get('targetDevice').value; + } + + valueStepperTypeTranslationMap = valueStepperTypeTranslations; + valueStepperTypes = valueStepperTypes; + valueStepperTypeImageMap = valueStepperTypeImages; + + buttonAppearanceType: ButtonAppearanceType = 'left'; + + valueType = ValueType; + + valueStepperWidgetConfigForm: UntypedFormGroup; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + protected widgetConfigComponent: WidgetConfigComponent, + private fb: UntypedFormBuilder) { + super(store, widgetConfigComponent); + } + + protected configForm(): UntypedFormGroup { + return this.valueStepperWidgetConfigForm; + } + + protected onConfigSet(configData: WidgetConfigComponentData) { + const settings: ValueStepperWidgetSettings = {...valueStepperDefaultSettings, ...(configData.config.settings || {})}; + this.valueStepperWidgetConfigForm = this.fb.group({ + targetDevice: [configData.config.targetDevice, []], + + initialState: [settings.initialState, []], + leftButtonClick: [settings.leftButtonClick, []], + rightButtonClick: [settings.rightButtonClick, []], + disabledState: [settings.disabledState, []], + + appearance: this.fb.group({ + type: [settings.appearance.type, []], + autoScale: [settings.appearance.autoScale, []], + minValueRange: [settings.appearance.minValueRange, []], + maxValueRange: [settings.appearance.maxValueRange, []], + valueStep: [settings.appearance.valueStep, [Validators.min(0)]], + showValueBox: [settings.appearance.showValueBox, []], + valueUnits: [settings.appearance.valueUnits, []], + valueDecimals: [settings.appearance.valueDecimals, []], + valueFont: [settings.appearance.valueFont, []], + valueColor: [settings.appearance.valueColor], + valueBoxBackground: [settings.appearance.valueBoxBackground, []], + showBorder: [settings.appearance.showBorder, []], + borderWidth: [settings.appearance.borderWidth, []], + borderColor: [settings.appearance.borderColor, []] + }), + + buttonAppearance: this.fb.group({ + leftButton: this.fb.group({ + showButton: [settings.buttonAppearance.leftButton.showButton], + icon: [settings.buttonAppearance.leftButton.icon], + iconSize: [settings.buttonAppearance.leftButton.iconSize], + iconSizeUnit: [settings.buttonAppearance.leftButton.iconSizeUnit], + mainColorOn: [settings.buttonAppearance.leftButton.mainColorOn, []], + backgroundColorOn: [settings.buttonAppearance.leftButton.backgroundColorOn, []], + mainColorDisabled: [settings.buttonAppearance.leftButton.mainColorDisabled, []], + backgroundColorDisabled: [settings.buttonAppearance.leftButton.backgroundColorDisabled, []] + }), + rightButton: this.fb.group({ + showButton: [settings.buttonAppearance.rightButton.showButton], + icon: [settings.buttonAppearance.rightButton.icon], + iconSize: [settings.buttonAppearance.rightButton.iconSize], + iconSizeUnit: [settings.buttonAppearance.rightButton.iconSizeUnit], + mainColorOn: [settings.buttonAppearance.rightButton.mainColorOn, []], + backgroundColorOn: [settings.buttonAppearance.rightButton.backgroundColorOn, []], + mainColorDisabled: [settings.buttonAppearance.rightButton.mainColorDisabled, []], + backgroundColorDisabled: [settings.buttonAppearance.rightButton.backgroundColorDisabled, []] + }) + }), + + background: [settings.background, []], + cardButtons: [this.getCardButtons(configData.config), []], + borderRadius: [configData.config.borderRadius, []], + padding: [settings.padding, []], + + actions: [configData.config.actions || {}, []] + }); + } + + + protected prepareOutputConfig(config: any): WidgetConfigComponentData { + this.widgetConfig.config.targetDevice = config.targetDevice; + this.widgetConfig.config.settings = this.widgetConfig.config.settings || {}; + this.widgetConfig.config.settings.initialState = config.initialState; + this.widgetConfig.config.settings.disabledState = config.disabledState; + this.widgetConfig.config.settings.leftButtonClick = config.leftButtonClick; + this.widgetConfig.config.settings.rightButtonClick = config.rightButtonClick; + this.widgetConfig.config.settings.appearance = config.appearance; + this.widgetConfig.config.settings.buttonAppearance = config.buttonAppearance; + + this.widgetConfig.config.settings.background = config.background; + this.setCardButtons(config.cardButtons, this.widgetConfig.config); + this.widgetConfig.config.borderRadius = config.borderRadius; + this.widgetConfig.config.settings.padding = config.padding; + this.widgetConfig.config.actions = config.actions; + + return this.widgetConfig; + } + + + protected validatorTriggers(): string[] { + return ['appearance.showValueBox', 'appearance.showBorder', + 'buttonAppearance.leftButton.showButton', 'buttonAppearance.rightButton.showButton']; + } + + protected updateValidators(emitEvent: boolean, trigger?: string) { + const showValueBox: boolean = this.valueStepperWidgetConfigForm.get('appearance').get('showValueBox').value; + const showBorder: boolean = this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').value; + const showLeftButton: boolean = this.valueStepperWidgetConfigForm.get('buttonAppearance').get('leftButton').get('showButton').value; + const showRightButton: boolean = this.valueStepperWidgetConfigForm.get('buttonAppearance').get('rightButton').get('showButton').value; + if (showValueBox) { + this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').enable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').enable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueFont').enable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueColor').enable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueBoxBackground').enable(); + this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').enable({emitEvent: false}); + if (showBorder) { + this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').enable(); + this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').enable(); + } else { + this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').disable(); + } + } else { + this.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueFont').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueColor').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('valueBoxBackground').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('showBorder').disable({emitEvent: false}); + this.valueStepperWidgetConfigForm.get('appearance').get('borderWidth').disable(); + this.valueStepperWidgetConfigForm.get('appearance').get('borderColor').disable(); + } + this.buttonValidators(showLeftButton, 'leftButton'); + this.buttonValidators(showRightButton, 'rightButton'); + } + + private buttonValidators(showButtonValue: boolean, button: string) { + if (showButtonValue) { + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('icon').enable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSize').enable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSizeUnit').enable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorOn').enable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorOn').enable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorDisabled').enable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').enable() + } else { + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('icon').disable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSize').disable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('iconSizeUnit').disable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorOn').disable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorOn').disable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('mainColorDisabled').disable() + this.valueStepperWidgetConfigForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').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.valueStepperWidgetConfigForm.get('appearance').get('valueUnits').value; + const decimals: number = this.valueStepperWidgetConfigForm.get('appearance').get('valueDecimals').value; + return formatValue(48, decimals, units, false); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts index 6c9072e915..262a532163 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts @@ -33,11 +33,11 @@ import { DomSanitizer } from '@angular/platform-browser'; import { ValueType } from '@shared/models/constants'; import { powerButtonDefaultSettings, - PowerButtonShape, - powerButtonShapeSize, + PowerButtonShape, powerButtonShapeSize, PowerButtonWidgetSettings } from '@home/components/widget/lib/rpc/power-button-widget.models'; import { SVG, Svg } from '@svgdotjs/svg.js'; +import { MatIconRegistry } from '@angular/material/icon'; @Component({ selector: 'tb-power-button-widget', @@ -72,6 +72,7 @@ export class PowerButtonWidgetComponent extends constructor(protected imagePipe: ImagePipe, protected sanitizer: DomSanitizer, private renderer: Renderer2, + private iconRegistry: MatIconRegistry, protected cd: ChangeDetectorRef, protected zone: NgZone) { super(cd); @@ -180,7 +181,7 @@ export class PowerButtonWidgetComponent extends this.renderer.setStyle(this.svgShape.node, 'user-select', 'none'); this.zone.run(() => { - this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape, + this.powerButtonSvgShape = PowerButtonShape.fromSettings(this.ctx, this.svgShape, this.iconRegistry, this.settings, this.value, this.disabledState, () => this.onClick()); }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts index 503347939d..540ef8f9f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.models.ts @@ -28,6 +28,11 @@ import { Circle, Effect, Element, G, Gradient, Path, Runner, Svg, Text, Timeline import '@svgdotjs/svg.filter.js'; import tinycolor from 'tinycolor2'; import { WidgetContext } from '@home/models/widget-component.models'; +import { Observable, of } from 'rxjs'; +import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; +import { catchError, map, take } from 'rxjs/operators'; +import { MatIconRegistry } from '@angular/material/icon'; +import { isDefinedAndNotNull } from '@core/utils'; export enum PowerButtonLayout { default = 'default', @@ -71,20 +76,29 @@ export const powerButtonLayoutImages = new Map( ] ); +export interface ButtonIconSettings { + showIcon: boolean; + iconSize: number; + iconSizeUnit: string; + icon: string; +} + export interface PowerButtonWidgetSettings { - initialState: GetValueSettings; - disabledState: GetValueSettings; - onUpdateState: SetValueSettings; - offUpdateState: SetValueSettings; + initialState?: GetValueSettings; + disabledState?: GetValueSettings; + onUpdateState?: SetValueSettings; + offUpdateState?: SetValueSettings; layout: PowerButtonLayout; + onButtonIcon: ButtonIconSettings, + offButtonIcon: ButtonIconSettings, mainColorOn: string; backgroundColorOn: string; mainColorOff: string; backgroundColorOff: string; mainColorDisabled: string; backgroundColorDisabled: string; - background: BackgroundSettings; - padding: string; + background?: BackgroundSettings; + padding?: string; } export const powerButtonDefaultSettings: PowerButtonWidgetSettings = { @@ -177,6 +191,18 @@ export const powerButtonDefaultSettings: PowerButtonWidgetSettings = { } }, layout: PowerButtonLayout.default, + onButtonIcon: { + showIcon: false, + iconSize: 32, + iconSizeUnit: 'px', + icon: 'power_settings_new' + }, + offButtonIcon: { + showIcon: false, + iconSize: 32, + iconSizeUnit: 'px', + icon: 'power_settings_new' + }, mainColorOn: '#3F52DD', backgroundColorOn: '#FFFFFF', mainColorOff: '#A2A2A2', @@ -207,6 +233,11 @@ interface PowerButtonColorState { backgroundColor: PowerButtonColor; } +interface ButtonsIconSettings { + onButtonIcon: ButtonIconSettings; + offButtonIcon: ButtonIconSettings; +} + type PowerButtonShapeColors = Record; const createPowerButtonShapeColors = (settings: PowerButtonWidgetSettings): PowerButtonShapeColors => { @@ -257,33 +288,35 @@ export abstract class PowerButtonShape { static fromSettings(ctx: WidgetContext, svgShape: Svg, + iconRegistry: MatIconRegistry, settings: PowerButtonWidgetSettings, value: boolean, disabled: boolean, onClick: () => void): PowerButtonShape { switch (settings.layout) { case PowerButtonLayout.default: - return new DefaultPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new DefaultPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.simplified: - return new SimplifiedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new SimplifiedPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.outlined: - return new OutlinedPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new OutlinedPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.default_volume: - return new DefaultVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new DefaultVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.simplified_volume: - return new SimplifiedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new SimplifiedVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.outlined_volume: - return new OutlinedVolumePowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new OutlinedVolumePowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.default_icon: - return new DefaultIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new DefaultIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.simplified_icon: - return new SimplifiedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new SimplifiedIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); case PowerButtonLayout.outlined_icon: - return new OutlinedIconPowerButtonShape(ctx, svgShape, settings, value, disabled, onClick); + return new OutlinedIconPowerButtonShape(ctx, svgShape, iconRegistry, settings, value, disabled, onClick); } } protected readonly colors: PowerButtonShapeColors; + protected readonly icons: ButtonsIconSettings; protected readonly onLabel: string; protected readonly offLabel: string; @@ -293,18 +326,67 @@ export abstract class PowerButtonShape { protected pressed = false; protected forcePressed = false; + protected offPowerSymbolIcon: Element; + protected onPowerSymbolIcon: Element; + protected offLabelShape: Text; + protected onLabelShape: Text; + + protected offPowerSymbolCircle: Path; + protected offPowerSymbolLine: Path; + protected onPowerSymbolCircle: Path; + protected onPowerSymbolLine: Path; + protected constructor(protected widgetContext: WidgetContext, protected svgShape: Svg, + protected iconRegistry: MatIconRegistry, protected settings: PowerButtonWidgetSettings, protected value: boolean, protected disabled: boolean, protected onClick: () => void) { this.colors = createPowerButtonShapeColors(this.settings); + this.icons = {onButtonIcon: this.settings.onButtonIcon, offButtonIcon: this.settings.offButtonIcon}; this.onLabel = this.widgetContext.translate.instant('widgets.power-button.on-label').toUpperCase(); this.offLabel = this.widgetContext.translate.instant('widgets.power-button.off-label').toUpperCase(); this._drawShape(); } + public createIconElement(icon: string, size: number): Observable { + const isSvg = isSvgIcon(icon); + if (isSvg) { + const [namespace, iconName] = splitIconName(icon); + return this.iconRegistry + .getNamedSvgIcon(iconName, namespace) + .pipe( + take(1), + map((svgElement) => { + const element = new Element(svgElement.firstChild); + const box = element.bbox(); + const scale = size / box.height; + element.scale(scale); + return element; + }), + catchError(() => of(null) + )); + } else { + const iconName = splitIconName(icon)[1]; + const textElement = this.svgShape.text(iconName); + const fontSetClasses = ( + this.iconRegistry.getDefaultFontSetClass() + ).filter(className => className.length > 0); + fontSetClasses.forEach(className => textElement.addClass(className)); + textElement.font({size: `${size}px`}); + textElement.attr({ + style: `font-size: ${size}px`, + 'text-anchor': 'start' + }); + const tspan = textElement.first(); + tspan.attr({ + 'dominant-baseline': 'hanging' + }); + return of(textElement); + } + } + public setValue(value: boolean) { if (this.value !== value) { this.value = value; @@ -330,6 +412,106 @@ export abstract class PowerButtonShape { } } + public drawOffShape(centerGroup: G, label: boolean, labelWeight?: string, circleStroke?: boolean) { + if (this.icons.offButtonIcon.showIcon) { + this.createIconElement(this.icons.offButtonIcon.icon, this.icons.offButtonIcon.iconSize).subscribe(icon => + this.offPowerSymbolIcon = icon.center(cx, cy).addTo(centerGroup)); + } else { + if (label) { + this.offLabelShape = this.createOffLabel(labelWeight).addTo(centerGroup); + } else { + this.offPowerSymbolCircle = this.svgShape.path(circleStroke ? powerCircle : powerCircleStroke).center(cx, cy).addTo(centerGroup); + this.offPowerSymbolLine = this.svgShape.path(circleStroke ? powerLine : powerLineStroke).center(cx, cy-12).addTo(centerGroup); + } + } + } + + public drawOnShape(onCenterGroup?: G, label?: boolean, labelWeight?: string, circleStroke?: boolean, mask?: Circle) { + if (this.icons.onButtonIcon.showIcon) { + this.createIconElement(this.icons.onButtonIcon.icon, this.icons.onButtonIcon.iconSize).subscribe(icon => { + this.onPowerSymbolIcon = icon.center(cx, cy); + if (isDefinedAndNotNull(onCenterGroup)) { + this.onPowerSymbolIcon.addTo(onCenterGroup); + } + if (isDefinedAndNotNull(mask)) { + this.createMask(mask, [this.onPowerSymbolIcon]); + } + }); + } else { + if (label) { + this.onLabelShape = this.createOnLabel(labelWeight); + if (isDefinedAndNotNull(onCenterGroup)) { + this.onLabelShape.addTo(onCenterGroup); + } + if (isDefinedAndNotNull(mask)) { + this.createMask(mask, [this.onLabelShape]); + } + } else { + this.onPowerSymbolCircle = this.svgShape.path(circleStroke ? powerCircle : powerCircleStroke).center(cx, cy); + this.onPowerSymbolLine = this.svgShape.path(circleStroke ? powerLine : powerLineStroke).center(cx, cy-12); + if (isDefinedAndNotNull(onCenterGroup)) { + this.onPowerSymbolCircle.addTo(onCenterGroup); + this.onPowerSymbolLine.addTo(onCenterGroup); + } + if (isDefinedAndNotNull(mask)) { + this.createMask(mask, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); + } + } + } + } + + public onCenterTimeLine(timeline: Timeline, label: boolean) { + if (this.icons.onButtonIcon.showIcon) { + this.onPowerSymbolIcon.timeline(timeline); + } else { + if (label) { + this.onLabelShape.timeline(timeline); + } else { + this.onPowerSymbolCircle.timeline(timeline); + this.onPowerSymbolLine.timeline(timeline); + } + } + } + + public offCenterColor(mainColor: PowerButtonColor, label: boolean) { + if (this.icons.offButtonIcon.showIcon) { + this.offPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + if (label) { + this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + } + } + + public onCenterColor(mainColor: PowerButtonColor, label: boolean) { + if (this.icons.onButtonIcon.showIcon) { + this.onPowerSymbolIcon.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + if (label) { + this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } else { + this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + } + } + } + + public buttonAnimation(scale: number, label: boolean) { + if (this.icons.onButtonIcon.showIcon) { + powerButtonAnimation(this.onPowerSymbolIcon).transform({scale}); + } else { + if (label) { + powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); + } else { + powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); + powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); + } + } + } + private _drawShape() { this.backgroundShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy) @@ -530,9 +712,7 @@ class DefaultPowerButtonShape extends PowerButtonShape { private outerBorder: Circle; private outerBorderMask: Circle; - private offLabelShape: Text; private onCircleShape: Circle; - private onLabelShape: Text; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; @@ -543,22 +723,20 @@ class DefaultPowerButtonShape extends PowerButtonShape { this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); this.createMask(this.outerBorder, [this.outerBorderMask]); this.centerGroup = this.svgShape.group(); - this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); - this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20) - .center(cx, cy); - this.onLabelShape = this.createOnLabel(); - this.createMask(this.onCircleShape, [this.onLabelShape]); + this.drawOffShape(this.centerGroup, true); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 20).center(cx, cy); + this.drawOnShape(null, true, '', false, this.onCircleShape); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 20, cx, cy, 0, 0); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); - this.onLabelShape.timeline(this.pressedTimeline); + this.onCenterTimeLine(this.pressedTimeline, true); this.pressedShadow.timeline(this.pressedTimeline); } protected drawColorState(mainColor: PowerButtonColor) { this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, true); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -578,14 +756,14 @@ class DefaultPowerButtonShape extends PowerButtonShape { this.pressedTimeline.finish(); const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}}); + this.buttonAnimation(pressedScale, true); this.pressedShadow.animate(6, 0.6); } protected onPressEnd() { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + this.buttonAnimation(1, true); this.pressedShadow.animateRestore(); } @@ -596,8 +774,6 @@ class SimplifiedPowerButtonShape extends PowerButtonShape { private outerBorder: Circle; private outerBorderMask: Circle; private onCircleShape: Circle; - private offLabelShape: Text; - private onLabelShape: Text; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; @@ -608,22 +784,21 @@ class SimplifiedPowerButtonShape extends PowerButtonShape { this.outerBorderMask = this.svgShape.circle(powerButtonShapeSize - 4).center(cx, cy); this.createMask(this.outerBorder, [this.outerBorderMask]); this.centerGroup = this.svgShape.group(); - this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.drawOffShape(this.centerGroup, true); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize).center(cx, cy); - this.onLabelShape = this.createOnLabel(); - this.createMask(this.onCircleShape, [this.onLabelShape]); + this.drawOnShape(null, true, '', false, this.onCircleShape) this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 4, cx, cy, 0, 0); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); - this.onLabelShape.timeline(this.pressedTimeline); + this.onCenterTimeLine(this.pressedTimeline, true); this.pressedShadow.timeline(this.pressedTimeline); } protected drawColorState(mainColor: PowerButtonColor) { this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, true); } protected drawOff() { @@ -642,14 +817,14 @@ class SimplifiedPowerButtonShape extends PowerButtonShape { this.pressedTimeline.finish(); const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale, origin: {x: cx, y: cy}}); + this.buttonAnimation(pressedScale, true); this.pressedShadow.animate(6, 0.6); } protected onPressEnd() { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + this.buttonAnimation(1, true); this.pressedShadow.animateRestore(); } } @@ -659,9 +834,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape { private outerBorderMask: Circle; private innerBorder: Circle; private innerBorderMask: Circle; - private offLabelShape: Text; private onCircleShape: Circle; - private onLabelShape: Text; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; @@ -677,25 +850,23 @@ class OutlinedPowerButtonShape extends PowerButtonShape { this.innerBorderMask = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); this.createMask(this.innerBorder, [this.innerBorderMask]); this.centerGroup = this.svgShape.group(); - this.offLabelShape = this.createOffLabel().addTo(this.centerGroup); + this.drawOffShape(this.centerGroup, true); this.onCenterGroup = this.svgShape.group(); - this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy) - .addTo(this.onCenterGroup); - this.onLabelShape = this.createOnLabel(); - this.createMask(this.onCircleShape, [this.onLabelShape]); + this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 28).center(cx, cy).addTo(this.onCenterGroup); + this.drawOnShape(null, true, '', false, this.onCircleShape) this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 0, 0); this.pressedTimeline = new Timeline(); this.centerGroup.timeline(this.pressedTimeline); this.onCenterGroup.timeline(this.pressedTimeline); - this.onLabelShape.timeline(this.pressedTimeline); + this.onCenterTimeLine(this.pressedTimeline, true); this.pressedShadow.timeline(this.pressedTimeline); } protected drawColorState(mainColor: PowerButtonColor) { this.outerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); this.innerBorder.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, true); this.onCircleShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); } @@ -714,7 +885,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape { const pressedScale = 0.75; powerButtonAnimation(this.centerGroup).transform({scale: pressedScale}); powerButtonAnimation(this.onCenterGroup).transform({scale: 0.98}); - powerButtonAnimation(this.onLabelShape).transform({scale: pressedScale / 0.98, origin: {x: cx, y: cy}}); + this.buttonAnimation(pressedScale / 0.98, true); this.pressedShadow.animate(6, 0.6); } @@ -722,7 +893,7 @@ class OutlinedPowerButtonShape extends PowerButtonShape { this.pressedTimeline.finish(); powerButtonAnimation(this.centerGroup).transform({scale: 1}); powerButtonAnimation(this.onCenterGroup).transform({scale: 1}); - powerButtonAnimation(this.onLabelShape).transform({scale: 1, origin: {x: cx, y: cy}}); + this.buttonAnimation(1, true); this.pressedShadow.animateRestore(); } } @@ -737,35 +908,29 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { private innerShadow: InnerShadowCircle; //private innerShadowGradient: Gradient; //private innerShadowGradientStop: Stop; - private offLabelShape: Text; - private onCircleShape: Circle; - private onLabelShape: Text; + protected onCircleShape: Circle; private pressedTimeline: Timeline; private centerGroup: G; protected drawOffCenter(centerGroup: G) { - this.offLabelShape = this.createOffLabel('400').addTo(centerGroup); + this.drawOffShape(centerGroup, true, '400'); } protected drawOnCenter() { - this.onLabelShape = this.createOnLabel('400'); - } - - protected addOnCenterToMask(onCircleShape: Circle) { - this.createMask(onCircleShape,[this.onLabelShape]); + this.drawOnShape(null, true, '400', false, this.onCircleShape); } protected addOnCenterTimeLine(pressedTimeline: Timeline) { - this.onLabelShape.timeline(pressedTimeline); + this.onCenterTimeLine(pressedTimeline, true); } protected drawOffCenterColor(mainColor: PowerButtonColor) { - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, true); } protected onCenterAnimation(scale: number) { - powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); + this.buttonAnimation(scale, true); } protected drawShape() { @@ -789,7 +954,6 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { this.drawOffCenter(this.centerGroup); this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 24).center(cx, cy); this.drawOnCenter(); - this.addOnCenterToMask(this.onCircleShape); this.innerShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 24, cx, cy, 3, 0.3); this.pressedTimeline = new Timeline(); @@ -857,38 +1021,25 @@ class DefaultVolumePowerButtonShape extends PowerButtonShape { } class DefaultIconPowerButtonShape extends DefaultVolumePowerButtonShape { - private offPowerSymbolCircle: Path; - private offPowerSymbolLine: Path; - private onPowerSymbolCircle: Path; - private onPowerSymbolLine: Path; protected drawOffCenter(centerGroup: G) { - this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup); - this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup); + this.drawOffShape(centerGroup, false); } protected drawOnCenter() { - this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy); - this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12); - } - - protected addOnCenterToMask(onCircleShape: Circle) { - this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); + this.drawOnShape(null, false, '', false, this.onCircleShape); } protected addOnCenterTimeLine(pressedTimeline: Timeline) { - this.onPowerSymbolCircle.timeline(pressedTimeline); - this.onPowerSymbolLine.timeline(pressedTimeline); + this.onCenterTimeLine(pressedTimeline, false); } protected drawOffCenterColor(mainColor: PowerButtonColor) { - this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, false); } protected onCenterAnimation(scale: number) { - powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); - powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); + this.buttonAnimation(scale, false); } } @@ -896,8 +1047,6 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { private outerBorder: Circle; private outerBorderMask: Circle; - private offLabelShape: Text; - private onLabelShape: Text; private innerShadow: InnerShadowCircle; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; @@ -905,8 +1054,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { private onCenterGroup: G; protected drawCenterGroup(centerGroup: G, onCenterGroup: G) { - this.offLabelShape = this.createOffLabel().addTo(centerGroup); - this.onLabelShape = this.createOnLabel().addTo(onCenterGroup); + this.drawOffShape(centerGroup, true); + this.drawOnShape(onCenterGroup, true); } protected drawShape() { @@ -926,8 +1075,8 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { } protected drawColorState(mainColor: PowerButtonColor){ - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, true); + this.onCenterColor(mainColor, true); } protected drawOff() { @@ -970,23 +1119,15 @@ class SimplifiedVolumePowerButtonShape extends PowerButtonShape { } class SimplifiedIconPowerButtonShape extends SimplifiedVolumePowerButtonShape { - private offPowerSymbolCircle: Path; - private offPowerSymbolLine: Path; - private onPowerSymbolCircle: Path; - private onPowerSymbolLine: Path; protected drawCenterGroup(centerGroup: G, onCenterGroup: G) { - this.offPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(centerGroup); - this.offPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(centerGroup); - this.onPowerSymbolCircle = this.svgShape.path(powerCircle).center(cx, cy).addTo(onCenterGroup); - this.onPowerSymbolLine = this.svgShape.path(powerLine).center(cx, cy-12).addTo(onCenterGroup); + this.drawOffShape(centerGroup, false); + this.drawOnShape(onCenterGroup, false, '', false); } protected drawColorState(mainColor: PowerButtonColor) { - this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.onPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, false); + this.onCenterColor(mainColor, false); } } @@ -996,36 +1137,30 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { private outerBorderGradient: Gradient; private innerBorder: Circle; private innerBorderMask: Circle; - private offLabelShape: Text; - private onCircleShape: Circle; - private onLabelShape: Text; + protected onCircleShape: Circle; private pressedShadow: InnerShadowCircle; private pressedTimeline: Timeline; private centerGroup: G; private onCenterGroup: G; protected drawOffCenter(centerGroup: G) { - this.offLabelShape = this.createOffLabel('800').addTo(centerGroup); + this.drawOffShape(centerGroup, true, '800'); } protected drawOnCenter() { - this.onLabelShape = this.createOnLabel('800'); - } - - protected addOnCenterToMask(onCircleShape: Circle) { - this.createMask(onCircleShape,[this.onLabelShape]); + this.drawOnShape(null, true, '800', false, this.onCircleShape); } protected addOnCenterTimeLine(pressedTimeline: Timeline) { - this.onLabelShape.timeline(pressedTimeline); + this.onCenterTimeLine(pressedTimeline, true); } protected drawOffCenterColor(mainColor: PowerButtonColor) { - this.offLabelShape.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, true); } protected onCenterAnimation(scale: number) { - powerButtonAnimation(this.onLabelShape).transform({scale, origin: {x: cx, y: cy}}); + this.buttonAnimation(scale, true); } protected drawShape() { @@ -1047,7 +1182,6 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { this.onCircleShape = this.svgShape.circle(powerButtonShapeSize - 30).center(cx, cy) .addTo(this.onCenterGroup); this.drawOnCenter(); - this.addOnCenterToMask(this.onCircleShape); this.pressedShadow = new InnerShadowCircle(this.svgShape, powerButtonShapeSize - 30, cx, cy, 0, 0); this.backgroundShape.addClass('tb-small-shadow'); @@ -1102,37 +1236,24 @@ class OutlinedVolumePowerButtonShape extends PowerButtonShape { } class OutlinedIconPowerButtonShape extends OutlinedVolumePowerButtonShape { - private offPowerSymbolCircle: Path; - private offPowerSymbolLine: Path; - private onPowerSymbolCircle: Path; - private onPowerSymbolLine: Path; protected drawOffCenter(centerGroup: G) { - this.offPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy).addTo(centerGroup); - this.offPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12).addTo(centerGroup); + this.drawOffShape(centerGroup, false, '', true); } protected drawOnCenter() { - this.onPowerSymbolCircle = this.svgShape.path(powerCircleStroke).center(cx, cy); - this.onPowerSymbolLine = this.svgShape.path(powerLineStroke).center(cx, cy-12); - } - - protected addOnCenterToMask(onCircleShape: Circle) { - this.createMask(onCircleShape, [this.onPowerSymbolCircle, this.onPowerSymbolLine]); + this.drawOnShape(null, false, '', true, this.onCircleShape); } protected addOnCenterTimeLine(pressedTimeline: Timeline) { - this.onPowerSymbolCircle.timeline(pressedTimeline); - this.onPowerSymbolLine.timeline(pressedTimeline); + this.onCenterTimeLine(pressedTimeline, false); } protected drawOffCenterColor(mainColor: PowerButtonColor) { - this.offPowerSymbolCircle.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); - this.offPowerSymbolLine.attr({ fill: mainColor.hex, 'fill-opacity': mainColor.opacity}); + this.offCenterColor(mainColor, false); } protected onCenterAnimation(scale: number) { - powerButtonAnimation(this.onPowerSymbolCircle).transform({scale, origin: {x: cx, y: cy}}); - powerButtonAnimation(this.onPowerSymbolLine).transform({scale, origin: {x: cx, y: cy}}); + this.buttonAnimation(scale, false); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html new file mode 100644 index 0000000000..6b28d44066 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html @@ -0,0 +1,43 @@ + +
+
+ +
+
+
+
+
{{ valueText }}
+
+
+
+
+ +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss new file mode 100644 index 0000000000..47f15e9dd5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.scss @@ -0,0 +1,90 @@ +/** + * Copyright © 2016-2024 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-stepper-panel { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 24px 24px 24px; + > div:not(.tb-value-stepper-overlay) { + z-index: 1; + } + .tb-value-stepper-overlay { + position: absolute; + top: 12px; + left: 12px; + bottom: 12px; + right: 12px; + } + div.tb-widget-title { + padding: 0; + } + .tb-value-stepper-content { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + min-width: 0; + min-height: 0; + height: 100%; + .tb-value-stepper-value-box { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + height: 32px; + padding: 0 12px; + border-radius: 4px; + .tb-value-stepper-value-disabled { + color: rgba(0, 0, 0, 0.38) !important; + } + &-disabled { + border-color: rgba(0, 0, 0, 0.38); + } + } + .tb-button-shape { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + &-left { + margin-right: 12px; + } + &-right { + margin-left: 12px; + } + svg { + .tb-small-shadow { + filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 0.2)); + } + .tb-shadow { + filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.15)); + } + } + &.tb-button-pointer { + svg { + .tb-hover-circle { + cursor: pointer; + } + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts new file mode 100644 index 0000000000..d419aef0d4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.ts @@ -0,0 +1,388 @@ +/// +/// Copyright © 2016-2024 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, + NgZone, + OnDestroy, + OnInit, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { BasicActionWidgetComponent, ValueSetter } from '@home/components/widget/lib/action/action-widget.models'; +import { backgroundStyle, ComponentStyle, overlayStyle, textStyle } from '@shared/models/widget-settings.models'; +import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs'; +import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ValueType } from '@shared/models/constants'; +import { + PowerButtonLayout, + PowerButtonShape, + powerButtonShapeSize, + PowerButtonWidgetSettings +} from '@home/components/widget/lib/rpc/power-button-widget.models'; +import { SVG, Svg } from '@svgdotjs/svg.js'; +import { MatIconRegistry } from '@angular/material/icon'; +import { formatValue, isDefinedAndNotNull, isNumeric } from '@core/utils'; +import { + valueStepperDefaultSettings, + ValueStepperWidgetSettings +} from '@home/components/widget/lib/rpc/value-stepper-widget.models'; +import { UtilsService } from '@core/services/utils.service'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + selector: 'tb-value-stepper-widget', + templateUrl: './value-stepper-widget.component.html', + styleUrls: ['../action/action-widget.scss', './value-stepper-widget.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ValueStepperWidgetComponent extends + BasicActionWidgetComponent implements OnInit, AfterViewInit, OnDestroy { + + @ViewChild('leftButton', {static: false}) + leftButton: ElementRef; + + @ViewChild('rightButton', {static: false}) + rightButton: ElementRef; + + @ViewChild('stepperContent', {static: false}) + stepperContent: ElementRef; + + @ViewChild('valueBoxContainer', {static: false}) + valueBox: ElementRef; + + @ViewChild('value', {static: false}) + valueElement: ElementRef; + + settings: ValueStepperWidgetSettings; + + backgroundStyle$: Observable; + overlayStyle: ComponentStyle = {}; + padding: string; + + valueStyle: ComponentStyle = {}; + value: number = null; + + autoScale = false; + + showValueBox = true; + showLeftButton = true; + showRightButton = true; + + valueText = 'N/A'; + + disabledState$ = new BehaviorSubject(false); + + private prevValue: number = null; + private shapeResize$: ResizeObserver; + private drawSvgShapePending = false; + private svgShapeLeft: Svg; + private svgShapeRight: Svg; + private powerButtonSvgShapeLeft: PowerButtonShape; + private powerButtonSvgShapeRight: PowerButtonShape; + + private disabledState = false; + public leftDisabledState = false; + public rightDisabledState = false; + + private valueSetterLeft: ValueSetter; + private valueSetterRight: ValueSetter; + + private leftDisabledState$ = new BehaviorSubject(false); + private rightDisabledState$ = new BehaviorSubject(false); + private readonly destroy$ = new Subject(); + + constructor(protected imagePipe: ImagePipe, + protected sanitizer: DomSanitizer, + private renderer: Renderer2, + private iconRegistry: MatIconRegistry, + private utils: UtilsService, + private elementRef: ElementRef, + protected cd: ChangeDetectorRef, + protected zone: NgZone) { + super(cd); + } + + ngOnInit(): void { + super.ngOnInit(); + this.settings = {...valueStepperDefaultSettings, ...this.ctx.settings}; + + this.autoScale = this.settings.appearance.autoScale; + + this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer); + this.overlayStyle = overlayStyle(this.settings.background.overlay); + this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding; + + this.showValueBox = this.settings.appearance.showValueBox; + this.showLeftButton = this.settings.buttonAppearance.leftButton.showButton; + this.showRightButton = this.settings.buttonAppearance.rightButton.showButton; + this.valueStyle = textStyle(this.settings.appearance.valueFont); + this.valueStyle.color = this.settings.appearance.valueColor; + + if (this.showValueBox) { + const valueBoxCss = `.tb-value-stepper-value-box {\n`+ + `border: ${this.settings.appearance.showBorder ? + `${this.settings.appearance.borderWidth}px solid ${this.settings.appearance.borderColor}` : + 'none'};\n`+ + `background-color: ${this.settings.appearance.valueBoxBackground}` + + `}`; + this.utils.applyCssToElement(this.renderer, this.elementRef.nativeElement, 'tb-value-stepper-value-box', valueBoxCss); + } + + const getInitialStateSettings = + {...this.settings.initialState, actionLabel: this.ctx.translate.instant('widgets.slider.initial-value')}; + this.createValueGetter(getInitialStateSettings, ValueType.INTEGER, { + next: (value) => this.onValue(value) + }); + const disabledStateSettings = + {...this.settings.disabledState, actionLabel: this.ctx.translate.instant('widgets.rpc-state.disabled-state')}; + this.createValueGetter(disabledStateSettings, ValueType.BOOLEAN, { + next: (value) => this.disabledState$.next(value) + }); + + const leftButtonClick = {...this.settings.leftButtonClick, + actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')}; + this.valueSetterLeft = this.createValueSetter(leftButtonClick); + + const rightButtonClick = {...this.settings.rightButtonClick, + actionLabel: this.ctx.translate.instant('widgets.slider.on-value-change')}; + this.valueSetterRight = this.createValueSetter(rightButtonClick); + + combineLatest([ + this.loading$, + this.disabledState$.asObservable(), + this.leftDisabledState$.asObservable() + ]).pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + const state = value.includes(true); + this.updateLeftDisabledState(state) + }); + + combineLatest([ + this.loading$, + this.disabledState$.asObservable(), + this.rightDisabledState$.asObservable() + ]).pipe( + takeUntil(this.destroy$) + ).subscribe(value => { + const state = value.includes(true); + this.updateRightDisabledState(state) + }); + } + + ngAfterViewInit(): void { + if (this.drawSvgShapePending) { + this.drawSvg(); + } + super.ngAfterViewInit(); + } + + ngOnDestroy() { + if (this.shapeResize$) { + this.shapeResize$.disconnect(); + } + this.destroy$.next(); + this.destroy$.complete(); + super.ngOnDestroy(); + } + + public onInit() { + super.onInit(); + const borderRadius = this.ctx.$widgetElement.css('borderRadius'); + this.overlayStyle = {...this.overlayStyle, ...{borderRadius}}; + if (this.leftButton || this.rightButton) { + this.drawSvg(); + } else { + this.drawSvgShapePending = true; + } + this.cd.detectChanges(); + } + + private onValue(value: number): void { + this.value = value; + this.prevValue = value; + if ((this.value + this.settings.appearance.valueStep) >= this.settings.appearance.maxValueRange) { + this.rightDisabledState$.next(true); + } else { + this.rightDisabledState$.next(false); + } + if ((this.value - this.settings.appearance.valueStep) <= this.settings.appearance.minValueRange) { + this.leftDisabledState$.next(true); + } else { + this.leftDisabledState$.next(false); + } + this.updateValueText(); + this.cd.markForCheck(); + } + + private updateValueText() { + if (isDefinedAndNotNull(this.value) && isNumeric(this.value)) { + this.valueText = formatValue(this.value, this.settings.appearance.valueDecimals, this.settings.appearance.valueUnits, false); + } else { + this.valueText = 'N/A'; + } + } + + private onClick(rightButtonClick: boolean = false) { + this.updateValueText(); + if (!this.ctx.isEdit && !this.ctx.isPreview && !this.disabledState) { + const prevValue = this.prevValue; + const targetValue = rightButtonClick ? + (this.value + this.settings.appearance.valueStep) : + (this.value - this.settings.appearance.valueStep); + this.updateValue(rightButtonClick ? this.valueSetterRight : this.valueSetterLeft, targetValue, { + next: () => this.onValue(targetValue), + error: () => this.onValue(prevValue) + }); + } + } + + private drawSvg() { + let leftButtonSetting: PowerButtonWidgetSettings; + let rightButtonSetting: PowerButtonWidgetSettings; + if (this.showLeftButton) { + this.svgShapeLeft = SVG().addTo(this.leftButton.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize); + this.renderer.setStyle(this.svgShapeLeft.node, 'overflow', 'visible'); + this.renderer.setStyle(this.svgShapeLeft.node, 'user-select', 'none'); + leftButtonSetting = { + layout: PowerButtonLayout[this.settings.appearance.type], + onButtonIcon: { + showIcon: true, + icon: this.settings.buttonAppearance.leftButton.icon, + iconSize: this.settings.buttonAppearance.leftButton.iconSize * 1.7, + iconSizeUnit: this.settings.buttonAppearance.leftButton.iconSizeUnit + }, + offButtonIcon: { + showIcon: true, + icon: this.settings.buttonAppearance.leftButton.icon, + iconSize: this.settings.buttonAppearance.leftButton.iconSize * 1.7, + iconSizeUnit: this.settings.buttonAppearance.leftButton.iconSizeUnit + }, + mainColorOn: this.settings.buttonAppearance.leftButton.mainColorOn, + backgroundColorOn: this.settings.buttonAppearance.leftButton.backgroundColorOn, + mainColorOff: this.settings.buttonAppearance.leftButton.mainColorOff, + backgroundColorOff: this.settings.buttonAppearance.leftButton.backgroundColorOff, + mainColorDisabled: this.settings.buttonAppearance.leftButton.mainColorDisabled, + backgroundColorDisabled: this.settings.buttonAppearance.leftButton.backgroundColorDisabled + }; + } + if (this.showRightButton) { + this.svgShapeRight = SVG().addTo(this.rightButton.nativeElement).size(powerButtonShapeSize, powerButtonShapeSize); + this.renderer.setStyle(this.svgShapeRight.node, 'overflow', 'visible'); + this.renderer.setStyle(this.svgShapeRight.node, 'user-select', 'none'); + + rightButtonSetting = { + layout: PowerButtonLayout[this.settings.appearance.type], + onButtonIcon: { + showIcon: true, + icon: this.settings.buttonAppearance.rightButton.icon, + iconSize: this.settings.buttonAppearance.rightButton.iconSize * 1.7, + iconSizeUnit: this.settings.buttonAppearance.rightButton.iconSizeUnit + }, + offButtonIcon: { + showIcon: true, + icon: this.settings.buttonAppearance.rightButton.icon, + iconSize: this.settings.buttonAppearance.rightButton.iconSize * 1.7, + iconSizeUnit: this.settings.buttonAppearance.rightButton.iconSizeUnit + }, + mainColorOn: this.settings.buttonAppearance.rightButton.mainColorOn, + backgroundColorOn: this.settings.buttonAppearance.rightButton.backgroundColorOn, + mainColorOff: this.settings.buttonAppearance.rightButton.mainColorOff, + backgroundColorOff: this.settings.buttonAppearance.rightButton.backgroundColorOff, + mainColorDisabled: this.settings.buttonAppearance.rightButton.mainColorDisabled, + backgroundColorDisabled: this.settings.buttonAppearance.rightButton.backgroundColorDisabled + }; + } + + this.zone.run(() => { + if (this.showLeftButton) { + this.powerButtonSvgShapeLeft = PowerButtonShape.fromSettings(this.ctx, this.svgShapeLeft, this.iconRegistry, + leftButtonSetting , true, this.leftDisabledState, () => this.onClick()); + } + if (this.showRightButton) { + this.powerButtonSvgShapeRight = PowerButtonShape.fromSettings(this.ctx, this.svgShapeRight, this.iconRegistry, + rightButtonSetting, true, this.rightDisabledState, () => this.onClick(true)); + } + }); + + this.shapeResize$ = new ResizeObserver(() => { + this.onResize(); + }); + if (this.autoScale) { + this.shapeResize$.observe(this.stepperContent.nativeElement); + } + if (this.showLeftButton) { + this.shapeResize$.observe(this.leftButton.nativeElement); + } + if (this.showRightButton) { + this.shapeResize$.observe(this.rightButton.nativeElement); + } + this.onResize(); + } + + private updateLeftDisabledState(disabled: boolean) { + this.leftDisabledState = disabled; + this.powerButtonSvgShapeLeft?.setDisabled(this.leftDisabledState); + this.cd.markForCheck(); + } + + + private updateRightDisabledState(disabled: boolean) { + this.rightDisabledState = disabled; + this.powerButtonSvgShapeRight?.setDisabled(this.rightDisabledState); + this.cd.markForCheck(); + } + + private onResize() { + const panelWidth = this.stepperContent.nativeElement.getBoundingClientRect().width; + const panelHeight = this.stepperContent.nativeElement.getBoundingClientRect().height; + + const minAspect = 0.2; + const avgContentHeight = 32; + const targetHeight = panelWidth * Math.min(panelHeight / panelWidth, minAspect); + const multiplier = targetHeight / avgContentHeight; + const size = avgContentHeight * multiplier; + + if (this.showValueBox) { + this.renderer.setStyle(this.valueBox?.nativeElement, 'height', `${size}px`); + this.renderer.setStyle(this.valueElement?.nativeElement, 'font-size', `${this.settings.appearance.valueFont.size * multiplier}px`); + } + if (this.showLeftButton) { + this.renderer.setStyle(this.leftButton?.nativeElement, 'width', `${size}px`); + this.renderer.setStyle(this.leftButton?.nativeElement, 'height', `${size}px`); + } + if (this.showRightButton) { + this.renderer.setStyle(this.rightButton?.nativeElement, 'width', `${size}px`); + this.renderer.setStyle(this.rightButton?.nativeElement, 'height', `${size}px`); + } + if (size) { + const scale = size / powerButtonShapeSize; + if (this.showLeftButton) { + this.renderer.setStyle(this.svgShapeLeft?.node, 'transform', `scale(${scale})`); + } + if (this.showRightButton) { + this.renderer.setStyle(this.svgShapeRight?.node, 'transform', `scale(${scale})`); + } + } + } + +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts new file mode 100644 index 0000000000..02471bfc05 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts @@ -0,0 +1,239 @@ +import { + DataToValueType, + GetValueAction, + GetValueSettings, SetValueAction, + SetValueSettings, ValueToDataType +} from '@shared/models/action-widget-settings.models'; +import { defaultWidgetAction, WidgetAction } from '@shared/models/widget.models'; +import { + ButtonToggleAppearance, segmentedButtonDefaultAppearance, + SegmentedButtonWidgetSettings +} from '@home/components/widget/lib/button/segmented-button-widget.models'; +import { WidgetButtonCustomStyles, WidgetButtonType } from '@shared/components/button/widget-button.models'; +import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; +import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; + + +const defaultMainColor = '#305680'; + +export enum ValueStepperType { + simplified = 'simplified', + default = 'default', + default_volume = 'default_volume' +} + +export const valueStepperTypes = Object.keys(ValueStepperType) as ValueStepperType[]; + +export const valueStepperTypeTranslations = new Map( + [ + [ValueStepperType.simplified, 'widgets.value-stepper.simplified'], + [ValueStepperType.default, 'widgets.value-stepper.filled'], + [ValueStepperType.default_volume, 'widgets.value-stepper.volume'] + ] +); + +export const valueStepperTypeImages = new Map( + [ + [ValueStepperType.simplified, 'assets/widget/value-stepper/simplified.svg'], + [ValueStepperType.default, 'assets/widget/value-stepper/filled.svg'], + [ValueStepperType.default_volume, 'assets/widget/value-stepper/volume.svg'] + ] +); + +export interface ValueStepperWidgetSettings { + initialState: GetValueSettings; + leftButtonClick: SetValueSettings; + rightButtonClick: SetValueSettings; + disabledState: GetValueSettings; + + appearance: ValueStepperAppearance; + buttonAppearance: { + leftButton: ValueStepperButtonAppearance; + rightButton: ValueStepperButtonAppearance; + } + + background: BackgroundSettings; + padding: string; +} + +export interface ValueStepperAppearance { + type: ValueStepperType; + autoScale: boolean; + minValueRange: number; + maxValueRange: number; + valueStep: number; + showValueBox: boolean; + valueUnits: string; + valueDecimals: number; + valueFont: Font; + valueColor: string; + valueBoxBackground: string; + showBorder: boolean; + borderWidth: number; + borderColor: string; +} + +export interface ValueStepperButtonAppearance { + showButton: boolean; + icon: string; + iconSize: number; + iconSizeUnit: cssUnit; + mainColorOn: string; + backgroundColorOn: string; + mainColorOff: string; + backgroundColorOff: string; + mainColorDisabled: string; + backgroundColorDisabled: string; + customStyle: WidgetButtonCustomStyles; +} + +export const valueStepperDefaultAppearance: ValueStepperAppearance = { + type: ValueStepperType.simplified, + autoScale: true, + minValueRange: -100, + maxValueRange: 100, + valueStep: 0.5, + showValueBox: true, + valueUnits: '', + valueDecimals: 1, + valueFont: { + family: 'Roboto', + weight: '500', + style: 'normal', + size: 16, + sizeUnit: 'px', + lineHeight: '24px' + }, + valueColor: '#000', + valueBoxBackground: 'rgba(0, 0, 0, 0.12)', + showBorder: true, + borderWidth: 1, + borderColor: defaultMainColor +} + +export const valueStepperButtonDefaultAppearance: ValueStepperButtonAppearance = { + showButton: true, + icon: '', + iconSize: 24, + iconSizeUnit: 'px', + + mainColorOn: '#3F52DD', + backgroundColorOn: '#FFFFFF', + mainColorOff: '#A2A2A2', + backgroundColorOff: '#FFFFFF', + mainColorDisabled: 'rgba(0,0,0,0.12)', + backgroundColorDisabled: '#FFFFFF', + customStyle: { + enabled: null, + hovered: null, + pressed: null, + activated: null, + disabled: null + } +} + +export const valueStepperDefaultSettings: ValueStepperWidgetSettings = { + initialState: { + action: GetValueAction.EXECUTE_RPC, + defaultValue: 0, + executeRpc: { + method: 'getState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + getAlarmStatus: { + severityList: null, + typeList: null + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return integer value */\nreturn data;' + } + }, + disabledState: { + action: GetValueAction.DO_NOTHING, + defaultValue: false, + getAttribute: { + key: 'state', + scope: null + }, + getTimeSeries: { + key: 'state' + }, + getAlarmStatus: { + severityList: null, + typeList: null + }, + dataToValue: { + type: DataToValueType.NONE, + compareToValue: true, + dataToValueFunction: '/* Should return boolean value */\nreturn data;' + } + }, + leftButtonClick: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SERVER_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.VALUE, + constantValue: 0, + valueToDataFunction: '/* Convert input integer value to RPC parameters or attribute/time-series value */\nreturn value;' + } + }, + rightButtonClick: { + action: SetValueAction.EXECUTE_RPC, + executeRpc: { + method: 'setState', + requestTimeout: 5000, + requestPersistent: false, + persistentPollingInterval: 1000 + }, + setAttribute: { + key: 'state', + scope: AttributeScope.SERVER_SCOPE + }, + putTimeSeries: { + key: 'state' + }, + valueToData: { + type: ValueToDataType.VALUE, + constantValue: 0, + valueToDataFunction: '/* Convert input integer value to RPC parameters or attribute/time-series value */\nreturn value;' + } + }, + appearance: valueStepperDefaultAppearance, + buttonAppearance: { + leftButton: {...valueStepperButtonDefaultAppearance, icon: 'arrow_back_ios_new'}, + rightButton: {...valueStepperButtonDefaultAppearance, icon: 'arrow_forward_ios'} + }, + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '12px' +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html new file mode 100644 index 0000000000..ebc106794e --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.html @@ -0,0 +1,266 @@ + + +
+
widgets.value-stepper.behavior
+
+
widgets.value-stepper.initial-state
+ +
+
+
widgets.value-stepper.left-button-click
+ +
+
+
widgets.value-stepper.right-button-click
+ +
+
+
widgets.button-state.disabled-state
+ +
+
+
+
widget-config.appearance
+ + + {{ valueStepperTypeTranslationMap.get(type) | translate }} + + +
+ + {{ 'widgets.value-stepper.auto-scale' | translate }} + +
+ +
+
{{ 'widgets.value-stepper.value-range' | translate }}
+
+
widgets.value-stepper.min-range
+ + + +
widgets.value-stepper.max-range
+ + + +
+
+
+
{{ 'widgets.value-stepper.value-increment-decrement-step' | translate }}
+ + + +
+
+ + {{ 'widgets.value-stepper.value' | translate }} + +
+ + + +
widget-config.decimals-suffix
+
+ + + + +
+
+ +
+
{{ 'widgets.value-stepper.value-box-background' | translate }}
+ + +
+
+ + {{ 'widgets.value-stepper.border' | translate }} + +
+ + +
px
+
+ + +
+
+
+
+
+
widgets.value-stepper.button-appearance
+ + {{ 'widgets.value-stepper.left' | translate }} + {{ 'widgets.value-stepper.right' | translate }} + +
+
+
+ + {{ 'widgets.value-stepper.left-button' | translate }} + +
+
+
{{ 'widgets.value-stepper.icon' | translate }}
+
+ + + + + + +
+
+
+
{{ 'widgets.power-button.power-on-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
{{ 'widgets.power-button.disabled-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
+
+ + {{ 'widgets.value-stepper.right-button' | translate }} + +
+
+
{{ 'widgets.value-stepper.icon' | translate }}
+
+ + + + + + +
+
+
+
{{ 'widgets.power-button.power-on-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
{{ 'widgets.power-button.disabled-colors' | translate }}
+
+
+
widgets.power-button.main
+ + +
+ +
+
widgets.power-button.background
+ + +
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts new file mode 100644 index 0000000000..119c099da0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/control/value-stepper-widget-settings.component.ts @@ -0,0 +1,191 @@ +/// +/// Copyright © 2016-2024 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 { TargetDevice, WidgetSettings, WidgetSettingsComponent, widgetType } from '@shared/models/widget.models'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { ValueType } from '@shared/models/constants'; +import { getTargetDeviceFromDatasources } from '@shared/models/widget-settings.models'; +import { + valueStepperDefaultSettings, + valueStepperTypeImages, + valueStepperTypes, + valueStepperTypeTranslations +} from '@home/components/widget/lib/rpc/value-stepper-widget.models'; +import { formatValue } from '@core/utils'; + +type ButtonAppearanceType = 'left' | 'right'; + +@Component({ + selector: 'tb-value-stepper-widget-settings', + templateUrl: './value-stepper-widget-settings.component.html', + styleUrls: ['../widget-settings.scss'] +}) +export class ValueStepperWidgetSettingsComponent extends WidgetSettingsComponent { + + get targetDevice(): TargetDevice { + const datasources = this.widgetConfig?.config?.datasources; + return getTargetDeviceFromDatasources(datasources); + } + + get widgetType(): widgetType { + return this.widgetConfig?.widgetType; + } + get borderRadius(): string { + return this.widgetConfig?.config?.borderRadius; + } + + valueType = ValueType; + + valueStepperWidgetSettingsForm: UntypedFormGroup; + + valueStepperTypeTranslationMap = valueStepperTypeTranslations; + valueStepperTypes = valueStepperTypes; + valueStepperTypeImageMap = valueStepperTypeImages; + + buttonAppearanceType: ButtonAppearanceType = 'left'; + + valuePreviewFn = this._valuePreviewFn.bind(this); + + constructor(protected store: Store, + private fb: UntypedFormBuilder) { + super(store); + } + + protected settingsForm(): UntypedFormGroup { + return this.valueStepperWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return {...valueStepperDefaultSettings}; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.valueStepperWidgetSettingsForm = this.fb.group({ + initialState: [settings.initialState, []], + leftButtonClick: [settings.leftButtonClick, []], + rightButtonClick: [settings.rightButtonClick, []], + disabledState: [settings.disabledState, []], + + appearance: this.fb.group({ + type: [settings.appearance.type, []], + autoScale: [settings.appearance.autoScale, []], + minValueRange: [settings.appearance.minValueRange, []], + maxValueRange: [settings.appearance.maxValueRange, []], + valueStep: [settings.appearance.valueStep, [Validators.min(0)]], + showValueBox: [settings.appearance.showValueBox, []], + valueUnits: [settings.appearance.valueUnits, []], + valueDecimals: [settings.appearance.valueDecimals, []], + valueFont: [settings.appearance.valueFont, []], + valueColor: [settings.appearance.valueColor], + valueBoxBackground: [settings.appearance.valueBoxBackground, []], + showBorder: [settings.appearance.showBorder, []], + borderWidth: [settings.appearance.borderWidth, []], + borderColor: [settings.appearance.borderColor, []] + }), + + buttonAppearance: this.fb.group({ + leftButton: this.fb.group({ + showButton: [settings.buttonAppearance.leftButton.showButton], + icon: [settings.buttonAppearance.leftButton.icon], + iconSize: [settings.buttonAppearance.leftButton.iconSize], + iconSizeUnit: [settings.buttonAppearance.leftButton.iconSizeUnit], + mainColorOn: [settings.buttonAppearance.leftButton.mainColorOn, []], + backgroundColorOn: [settings.buttonAppearance.leftButton.backgroundColorOn, []], + mainColorDisabled: [settings.buttonAppearance.leftButton.mainColorDisabled, []], + backgroundColorDisabled: [settings.buttonAppearance.leftButton.backgroundColorDisabled, []] + }), + rightButton: this.fb.group({ + showButton: [settings.buttonAppearance.rightButton.showButton], + icon: [settings.buttonAppearance.rightButton.icon], + iconSize: [settings.buttonAppearance.rightButton.iconSize], + iconSizeUnit: [settings.buttonAppearance.rightButton.iconSizeUnit], + mainColorOn: [settings.buttonAppearance.rightButton.mainColorOn, []], + backgroundColorOn: [settings.buttonAppearance.rightButton.backgroundColorOn, []], + mainColorDisabled: [settings.buttonAppearance.rightButton.mainColorDisabled, []], + backgroundColorDisabled: [settings.buttonAppearance.rightButton.backgroundColorDisabled, []] + }) + }) + }); + } + + + protected validatorTriggers(): string[] { + return ['appearance.showValueBox', 'appearance.showBorder', + 'buttonAppearance.leftButton.showButton', 'buttonAppearance.rightButton.showButton']; + } + + protected updateValidators(_emitEvent: boolean): void { + const showValueBox: boolean = this.valueStepperWidgetSettingsForm.get('appearance').get('showValueBox').value; + const showBorder: boolean = this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').value; + const showLeftButton: boolean = this.valueStepperWidgetSettingsForm.get('buttonAppearance').get('leftButton').get('showButton').value; + const showRightButton: boolean = this.valueStepperWidgetSettingsForm.get('buttonAppearance').get('rightButton').get('showButton').value; + if (showValueBox) { + this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').enable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').enable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueFont').enable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueColor').enable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueBoxBackground').enable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').enable({emitEvent: false}); + if (showBorder) { + this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').enable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').enable(); + } else { + this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').disable(); + } + } else { + this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueFont').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueColor').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('valueBoxBackground').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('showBorder').disable({emitEvent: false}); + this.valueStepperWidgetSettingsForm.get('appearance').get('borderWidth').disable(); + this.valueStepperWidgetSettingsForm.get('appearance').get('borderColor').disable(); + } + this.buttonValidators(showLeftButton, 'leftButton'); + this.buttonValidators(showRightButton, 'rightButton'); + } + + private buttonValidators(showButtonValue: boolean, button: string) { + if (showButtonValue) { + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('icon').enable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSize').enable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSizeUnit').enable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorOn').enable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorOn').enable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorDisabled').enable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').enable() + } else { + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('icon').disable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSize').disable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('iconSizeUnit').disable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorOn').disable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorOn').disable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('mainColorDisabled').disable() + this.valueStepperWidgetSettingsForm.get('buttonAppearance').get(button).get('backgroundColorDisabled').disable() + } + } + + private _valuePreviewFn(): string { + const units: string = this.valueStepperWidgetSettingsForm.get('appearance').get('valueUnits').value; + const decimals: number = this.valueStepperWidgetSettingsForm.get('appearance').get('valueDecimals').value; + return formatValue(48, decimals, units, false); + } +} 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 0aee57babb..e50fcb30f0 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 @@ -371,6 +371,9 @@ import { import { SegmentedButtonWidgetSettingsComponent } from '@home/components/widget/lib/settings/button/segmented-button-widget-settings.component'; +import { + ValueStepperWidgetSettingsComponent +} from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component'; @NgModule({ declarations: [ @@ -487,6 +490,7 @@ import { SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, SegmentedButtonWidgetSettingsComponent, + ValueStepperWidgetSettingsComponent, CommandButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent, SliderWidgetSettingsComponent, @@ -624,6 +628,7 @@ import { SingleSwitchWidgetSettingsComponent, ActionButtonWidgetSettingsComponent, SegmentedButtonWidgetSettingsComponent, + ValueStepperWidgetSettingsComponent, CommandButtonWidgetSettingsComponent, PowerButtonWidgetSettingsComponent, SliderWidgetSettingsComponent, diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 21f2d89dc6..fe752bb8b5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -88,6 +88,7 @@ import { import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/button/two-segment-button-widget.component'; +import { ValueStepperWidgetComponent } from '@home/components/widget/lib/rpc/value-stepper-widget.component'; @NgModule({ declarations: [ @@ -128,6 +129,7 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but TwoSegmentButtonWidgetComponent, CommandButtonWidgetComponent, PowerButtonWidgetComponent, + ValueStepperWidgetComponent, SliderWidgetComponent, ToggleButtonWidgetComponent, TimeSeriesChartWidgetComponent, @@ -190,6 +192,7 @@ import { TwoSegmentButtonWidgetComponent } from '@home/components/widget/lib/but TwoSegmentButtonWidgetComponent, CommandButtonWidgetComponent, PowerButtonWidgetComponent, + ValueStepperWidgetComponent, SliderWidgetComponent, ToggleButtonWidgetComponent, TimeSeriesChartWidgetComponent, diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 57df3865e9..5163a3c2bb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -6617,6 +6617,8 @@ "layout-outlined-icon": "Outlined.Icon", "main": "Main", "background": "Background", + "button-icon-on": "Button icon 'On'", + "button-icon-off": "Button icon 'Off'", "power-on-colors": "Power 'On' colors", "power-off-colors": "Power 'Off' colors", "disabled-colors": "Disabled colors", @@ -6669,6 +6671,41 @@ "preview": "Preview", "copy-style-from": "Copy style from" }, + "value-stepper": { + "behavior": "Behavior", + "simplified": "Simplified", + "filled": "Filled", + "outlined": "Outlined", + "volume": "Volume", + "initial-state": "Initial state", + "initial-state-hint": "Action to get the initial value.", + "disabled-state": "Disabled state", + "disabled-state-hint": "Configure condition under which the component is disabled.", + "right-button-click": "Right button click", + "right-button-click-hint": "Action while pressing on right button.", + "left-button-click": "Left button click", + "left-button-click-hint": "Action while pressing on left button.", + "auto-scale": "Auto scale", + "value-range": "Range", + "min-range": "Min", + "max-range": "Max", + "value-increment-decrement-step": "Value increment/decrement step", + "value": "Value", + "value-box-background": "Value box background", + "border": "Border", + "button-appearance": "Button appearance", + "left": "Left", + "right": "Right", + "left-button": "Left button", + "right-button": "Right button", + "icon": "Icon", + "color-palette": "Color palette", + "main": "Main", + "background": "Background", + "button-icon-on": "Button icon 'On'", + "button-on-colors": "Power 'On' colors", + "disabled-colors": "Disabled colors" + }, "button-state": { "activated-state": "Activated state", "activated-state-hint": "Configure condition under which the button is active.", diff --git a/ui-ngx/src/assets/widget/value-stepper/filled.svg b/ui-ngx/src/assets/widget/value-stepper/filled.svg new file mode 100644 index 0000000000..26c94b92cd --- /dev/null +++ b/ui-ngx/src/assets/widget/value-stepper/filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-ngx/src/assets/widget/value-stepper/simplified.svg b/ui-ngx/src/assets/widget/value-stepper/simplified.svg new file mode 100644 index 0000000000..8a71ad47c8 --- /dev/null +++ b/ui-ngx/src/assets/widget/value-stepper/simplified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui-ngx/src/assets/widget/value-stepper/volume.svg b/ui-ngx/src/assets/widget/value-stepper/volume.svg new file mode 100644 index 0000000000..96bda9fa7a --- /dev/null +++ b/ui-ngx/src/assets/widget/value-stepper/volume.svg @@ -0,0 +1 @@ + \ No newline at end of file From 1cdbd2c338ceef94e2b09f161d3cdc605e080a55 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 7 Feb 2025 16:36:06 +0200 Subject: [PATCH 07/22] UI: fixed widget fqn --- .../src/main/data/json/system/widget_types/value_stepper.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_types/value_stepper.json b/application/src/main/data/json/system/widget_types/value_stepper.json index 0126b25470..dea2406539 100644 --- a/application/src/main/data/json/system/widget_types/value_stepper.json +++ b/application/src/main/data/json/system/widget_types/value_stepper.json @@ -1,5 +1,5 @@ { - "fqn": "slider", + "fqn": "value_stepper", "name": "Value stepper", "deprecated": false, "image": "tb-image;/api/images/system/value-stepper-widget.svg", From 4cd5e0369d3312b8b2579dbef9fc8af73bf9bda2 Mon Sep 17 00:00:00 2001 From: devaskim Date: Sun, 9 Feb 2025 17:32:36 +0500 Subject: [PATCH 08/22] Encode username/passwrod pair instead of decoding it while creating request. (cherry picked from commit 0cda72e6b2847c1baee498d2971bb3431b1d1895) --- .../java/org/thingsboard/rule/engine/rest/TbHttpClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index 566430bd8d..b4f5c7f096 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -343,7 +343,7 @@ public class TbHttpClient { if (CredentialsType.BASIC == credentials.getType()) { BasicCredentials basicCredentials = (BasicCredentials) credentials; String authString = basicCredentials.getUsername() + ":" + basicCredentials.getPassword(); - String encodedAuthString = new String(Base64.getDecoder().decode(authString.getBytes(StandardCharsets.UTF_8))); + String encodedAuthString = new String(Base64.getEncoder().encode(authString.getBytes(StandardCharsets.UTF_8))); headers.add("Authorization", "Basic " + encodedAuthString); } } From 3860d79613fb3b34f186558c9e4cbd246f22c270 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 10 Feb 2025 18:42:25 +0200 Subject: [PATCH 09/22] Improved Edge handling in case connect/disconnect. Speed up initial setup --- .../instructions/upgrade/docker/upgrade_db.md | 17 ++----------- .../service/edge/rpc/EdgeGrpcService.java | 3 +++ .../service/edge/rpc/EdgeGrpcSession.java | 8 ++++-- .../dashboard/BaseDashboardProcessor.java | 5 +++- .../edge/rpc/sync/EdgeRequestsService.java | 5 ++++ .../server/edge/DashboardEdgeTest.java | 25 ++++++++++++++++--- .../provider/KafkaMonolithQueueFactory.java | 2 +- .../provider/KafkaTbCoreQueueFactory.java | 2 +- 8 files changed, 43 insertions(+), 24 deletions(-) diff --git a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md index a594ebe4e7..386c5f2743 100644 --- a/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md +++ b/application/src/main/data/json/edge/instructions/upgrade/docker/upgrade_db.md @@ -43,19 +43,6 @@ volumes: Execute the following command to start upgrade process: ```bash -docker compose -f docker-compose-upgrade.yml up +docker compose -f docker-compose-upgrade.yml up --abort-on-container-exit {:copy-code} -``` - -Once upgrade process successfully completed, exit from the docker-compose shell by this combination: - -```text -Ctrl + C -``` - -Execute the following command to stop TB Edge upgrade container: - -```bash -docker compose -f docker-compose-upgrade.yml stop -{:copy-code} -``` +``` \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 73a504245d..471c47dafc 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -404,6 +404,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void scheduleEdgeEventsCheck(EdgeGrpcSession session) { EdgeId edgeId = session.getEdge().getId(); TenantId tenantId = session.getEdge().getTenantId(); + + cancelScheduleEdgeEventsCheck(edgeId); + if (sessions.containsKey(edgeId)) { ScheduledFuture edgeEventCheckTask = edgeEventProcessingExecutorService.schedule(() -> { try { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 137c4d5bf5..b8cedf8a56 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -448,7 +448,11 @@ public abstract class EdgeGrpcSession implements Closeable { private void scheduleDownlinkMsgsPackSend(int attempt) { Runnable sendDownlinkMsgsTask = () -> { try { - if (isConnected() && !sessionState.getPendingMsgsMap().values().isEmpty()) { + if (!isConnected()) { + stopCurrentSendDownlinkMsgsTask(true); + return; + } + if (!sessionState.getPendingMsgsMap().values().isEmpty()) { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); if (attempt > 1) { String error = "Failed to deliver the batch"; @@ -525,11 +529,11 @@ public abstract class EdgeGrpcSession implements Closeable { private void onDownlinkResponse(DownlinkResponseMsg msg) { try { if (msg.getSuccess()) { - sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); } else { log.error("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); } + sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); if (sessionState.getPendingMsgsMap().isEmpty()) { log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId); stopCurrentSendDownlinkMsgsTask(false); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java index faf001fbba..39007552ad 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/BaseDashboardProcessor.java @@ -41,7 +41,10 @@ public abstract class BaseDashboardProcessor extends BaseEdgeProcessor { if (dashboard == null) { throw new RuntimeException("[{" + tenantId + "}] dashboardUpdateMsg {" + dashboardUpdateMsg + "} cannot be converted to dashboard"); } - Set newAssignedCustomers = new HashSet<>(dashboard.getAssignedCustomers()); + Set newAssignedCustomers = new HashSet<>(); + if (dashboard.getAssignedCustomers() != null && !dashboard.getAssignedCustomers().isEmpty()) { + newAssignedCustomers.addAll(dashboard.getAssignedCustomers()); + } Dashboard dashboardById = edgeCtx.getDashboardService().findDashboardById(tenantId, dashboardId); if (dashboardById == null) { created = true; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java index 6d47c16680..e04d4d01af 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java @@ -28,17 +28,22 @@ import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; public interface EdgeRequestsService { + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processRuleChainMetadataRequestMsg(TenantId tenantId, Edge edge, RuleChainMetadataRequestMsg ruleChainMetadataRequestMsg); ListenableFuture processAttributesRequestMsg(TenantId tenantId, Edge edge, AttributesRequestMsg attributesRequestMsg); ListenableFuture processRelationRequestMsg(TenantId tenantId, Edge edge, RelationRequestMsg relationRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processDeviceCredentialsRequestMsg(TenantId tenantId, Edge edge, DeviceCredentialsRequestMsg deviceCredentialsRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processUserCredentialsRequestMsg(TenantId tenantId, Edge edge, UserCredentialsRequestMsg userCredentialsRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge, WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg); + @Deprecated(since = "3.9.1", forRemoval = true) ListenableFuture processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg); } diff --git a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java index 5b09e05a09..42b75b5a74 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DashboardEdgeTest.java @@ -48,12 +48,14 @@ public class DashboardEdgeTest extends AbstractEdgeTest { private static final int MOBILE_ORDER = 5; private static final String IMAGE = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+"; + private static final String DASHBOARD_TITLE = "Edge Test Dashboard"; + @Test public void testDashboards() throws Exception { // create dashboard and assign to edge edgeImitator.expectMessageAmount(2); Dashboard dashboard = new Dashboard(); - dashboard.setTitle("Edge Test Dashboard"); + dashboard.setTitle(DASHBOARD_TITLE); dashboard.setMobileHide(true); dashboard.setImage(IMAGE); dashboard.setMobileOrder(MOBILE_ORDER); @@ -200,12 +202,27 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Dashboard foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class); Assert.assertNotNull(foundDashboard); - Assert.assertEquals("Edge Test Dashboard", foundDashboard.getName()); + Assert.assertEquals(DASHBOARD_TITLE, foundDashboard.getName()); PageData pageData = doGetTypedWithPageLink("/api/customer/" + savedCustomer.getId().toString() + "/dashboards?", new TypeReference<>() {}, new PageLink(100)); Assert.assertEquals(1, pageData.getData().size()); - Assert.assertEquals("Edge Test Dashboard", pageData.getData().get(0).getTitle()); + Assert.assertEquals(DASHBOARD_TITLE, pageData.getData().get(0).getTitle()); + + dashboard.setTitle(DASHBOARD_TITLE + " Updated"); + dashboard.setAssignedCustomers(null); + dashboardUpdateMsgBuilder.setEntity(JacksonUtil.toString(dashboard)); + dashboardUpdateMsgBuilder.setMsgType(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE); + uplinkMsgBuilder = UplinkMsg.newBuilder(); + uplinkMsgBuilder.addDashboardUpdateMsg(dashboardUpdateMsgBuilder.build()); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + + Assert.assertTrue(edgeImitator.waitForResponses()); + + foundDashboard = doGet("/api/dashboard/" + dashboard.getUuidId(), Dashboard.class); + Assert.assertEquals(DASHBOARD_TITLE + " Updated", foundDashboard.getName()); } @Test @@ -256,7 +273,7 @@ public class DashboardEdgeTest extends AbstractEdgeTest { Dashboard dashboard = new Dashboard(); dashboard.setId(new DashboardId(UUID.randomUUID())); dashboard.setTenantId(tenantId); - dashboard.setTitle("Edge Test Dashboard"); + dashboard.setTitle(DASHBOARD_TITLE); dashboard.setAssignedCustomers(Sets.newHashSet(new ShortCustomerInfo(savedCustomer.getId(), savedCustomer.getTitle(), savedCustomer.isPublic()))); return dashboard; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index dd5d61e834..cd9d577b86 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -472,7 +472,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); - consumerBuilder.clientId("monolith-to-edge-event-consumer" + serviceInfoProvider.getServiceId()); + consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(edgeEventAdmin); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index cc0e044917..dffb127c70 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -421,7 +421,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); - consumerBuilder.clientId("tb-core-edge-event-consumer" + serviceInfoProvider.getServiceId()); + consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(edgeEventAdmin); From 1b4930ebafc27bd015fa432969235a56d8dfadce Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Mon, 10 Feb 2025 18:51:13 +0200 Subject: [PATCH 10/22] Introduced separate count for edge consumers --- .../server/queue/provider/KafkaMonolithQueueFactory.java | 3 ++- .../server/queue/provider/KafkaTbCoreQueueFactory.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java index cd9d577b86..0767e5e303 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaMonolithQueueFactory.java @@ -96,6 +96,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi private final TbQueueAdmin edgeEventAdmin; private final AtomicLong consumerCount = new AtomicLong(); + private final AtomicLong edgeConsumerCount = new AtomicLong(); public KafkaMonolithQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, TbServiceInfoProvider serviceInfoProvider, @@ -472,7 +473,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); - consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); + consumerBuilder.clientId("monolith-to-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("monolith-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(edgeEventAdmin); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java index dffb127c70..06fd6147a8 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/provider/KafkaTbCoreQueueFactory.java @@ -95,6 +95,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { private final TbQueueAdmin edgeEventAdmin; private final AtomicLong consumerCount = new AtomicLong(); + private final AtomicLong edgeConsumerCount = new AtomicLong(); public KafkaTbCoreQueueFactory(TopicService topicService, TbKafkaSettings kafkaSettings, @@ -421,7 +422,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { TbKafkaConsumerTemplate.TbKafkaConsumerTemplateBuilder> consumerBuilder = TbKafkaConsumerTemplate.builder(); consumerBuilder.settings(kafkaSettings); consumerBuilder.topic(topicService.buildTopicName("tb_edge_event.notifications." + tenantId + "." + edgeId)); - consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + consumerCount.incrementAndGet()); + consumerBuilder.clientId("tb-core-edge-event-consumer-" + serviceInfoProvider.getServiceId() + "-" + edgeConsumerCount.incrementAndGet()); consumerBuilder.groupId(topicService.buildTopicName("tb-core-edge-event-consumer")); consumerBuilder.decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdgeEventNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); consumerBuilder.admin(edgeEventAdmin); From 8a662076c3c4e8927b9dd815c99d03fb7da0d4b9 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 11 Feb 2025 08:07:36 +0200 Subject: [PATCH 11/22] ack failed downlink if its not timeseries or attibutes failures --- .../server/service/edge/rpc/EdgeGrpcSession.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index b8cedf8a56..2e4d5c8aa5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -529,11 +529,16 @@ public abstract class EdgeGrpcSession implements Closeable { private void onDownlinkResponse(DownlinkResponseMsg msg) { try { if (msg.getSuccess()) { + sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); } else { log.error("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); + DownlinkMsg downlinkMsg = sessionState.getPendingMsgsMap().get(msg.getDownlinkMsgId()); + // if NOT timeseries or attributes failures - ack failed downlink + if (downlinkMsg.getEntityDataCount() == 0) { + sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); + } } - sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); if (sessionState.getPendingMsgsMap().isEmpty()) { log.debug("[{}][{}][{}] Pending msgs map is empty. Stopping current iteration", tenantId, edge.getId(), sessionId); stopCurrentSendDownlinkMsgsTask(false); From a315f18195188f7b592dce46d65e4dca90b5606b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 11 Feb 2025 08:40:40 +0200 Subject: [PATCH 12/22] Improved GeneralEdgeEventFetcher for case when seqId less maxRecordsReadSize --- .../rpc/fetch/GeneralEdgeEventFetcher.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index b2a1044981..93461f8c39 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -54,23 +54,25 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { log.trace("[{}] Finding general edge events [{}], seqIdStart = {}, pageLink = {}", tenantId, edge.getId(), seqIdStart, pageLink); PageData edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), seqIdStart, null, (TimePageLink) pageLink); - if (edgeEvents.getData().isEmpty()) { + if (!edgeEvents.getData().isEmpty()) { + return edgeEvents; + } + if (seqIdStart > this.maxReadRecordsCount) { edgeEvents = edgeEventService.findEdgeEvents(tenantId, edge.getId(), 0L, Math.max(this.maxReadRecordsCount, seqIdStart - this.maxReadRecordsCount), (TimePageLink) pageLink); if (edgeEvents.getData().stream().anyMatch(ee -> ee.getSeqId() < seqIdStart)) { log.info("[{}] seqId column of edge_event table started new cycle [{}]", tenantId, edge.getId()); this.seqIdNewCycleStarted = true; this.seqIdStart = 0L; - } else { - edgeEvents = new PageData<>(); - log.warn("[{}] unexpected edge notification message received. " + - "no new events found and seqId column of edge_event table doesn't started new cycle [{}]", tenantId, edge.getId()); + return edgeEvents; } } - return edgeEvents; + log.info("[{}] Unexpected edge notification message received. " + + "No new events found, and the seqId column of the edge_event table has not started a new cycle [{}].", tenantId, edge.getId()); + return new PageData<>(); } catch (Exception e) { - log.error("[{}] failed to find edge events [{}]", tenantId, edge.getId()); + log.error("[{}] Failed to find edge events [{}]", tenantId, edge.getId(), e); + return new PageData<>(); } - return new PageData<>(); } } From 227c7d7a1564ba802f6e05a5dcde14d67192a0a7 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 11 Feb 2025 11:44:49 +0200 Subject: [PATCH 13/22] Remove Timescale deprecation comment --- application/src/main/resources/thingsboard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 4301f0f211..507024ae0f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -212,9 +212,9 @@ ui: database: ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by a single API call to fetch telemetry records ts: - type: "${DATABASE_TS_TYPE:sql}" # cassandra or sql. timescale option is deprecated and will no longer be supported in ThingsBoard 4.0 + type: "${DATABASE_TS_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) ts_latest: - type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra or sql. timescale option is deprecated and will no longer be supported in ThingsBoard 4.0 + type: "${DATABASE_TS_LATEST_TYPE:sql}" # cassandra, sql, or timescale (for hybrid mode, DATABASE_TS_TYPE value should be cassandra, or timescale) # Cassandra driver configuration parameters cassandra: From 796a7027a7d04b5a0e2a98e3cf54a45ff179a792 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 11 Feb 2025 15:26:29 +0200 Subject: [PATCH 14/22] UI: Fixed incorrect shape layout settings in liquid level widget --- .../indicator/liquid-level-card-basic-config.component.html | 2 +- .../indicator/liquid-level-card-widget-settings.component.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html index 9808548bbe..a0776f5aa8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/indicator/liquid-level-card-basic-config.component.html @@ -63,7 +63,7 @@
-
+
widgets.liquid-level-card.shape
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html index a1aae64747..8d787116a8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/indicator/liquid-level-card-widget-settings.component.html @@ -18,7 +18,7 @@
-
+
widgets.liquid-level-card.shape
From 7d00f4ed1db0301afbb1f800ed53d1de7de2e5f2 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 11 Feb 2025 18:36:26 +0200 Subject: [PATCH 15/22] UI: Fixed apply changes button enabled by default in Device Profile Transport configuration tab --- .../device/device-profile-transport-configuration.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts index 47d782dbdb..a295ef76ab 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts @@ -103,7 +103,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu delete configuration.type; } setTimeout(() => { - this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}); + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); }, 0); } From 8d68f2bdbb619933384f3d49ce6ea9252733dc34 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 13 Feb 2025 14:44:33 +0200 Subject: [PATCH 16/22] UI: Imporove description for scada symbols --- .../main/data/json/system/scada_symbols/control-panel-hp.svg | 2 +- .../data/json/system/scada_symbols/horizontal-connector-hp.svg | 2 +- .../src/main/data/json/system/scada_symbols/leak-sensor.svg | 2 +- .../json/system/scada_symbols/left-analog-water-level-meter.svg | 2 +- .../src/main/data/json/system/scada_symbols/left-drain-pipe.svg | 2 +- .../data/json/system/scada_symbols/left-elbow-drain-pipe.svg | 2 +- .../main/data/json/system/scada_symbols/long-bottom-filter.svg | 2 +- .../json/system/scada_symbols/long-horizontal-connector-hp.svg | 2 +- .../src/main/data/json/system/scada_symbols/long-top-filter.svg | 2 +- .../json/system/scada_symbols/long-vertical-connector-hp.svg | 2 +- .../system/scada_symbols/right-analog-water-level-meter.svg | 2 +- .../main/data/json/system/scada_symbols/right-drain-pipe.svg | 2 +- .../data/json/system/scada_symbols/right-elbow-drain-pipe.svg | 2 +- .../main/data/json/system/scada_symbols/short-bottom-filter.svg | 2 +- .../data/json/system/scada_symbols/short-left-drain-pipe.svg | 2 +- .../data/json/system/scada_symbols/short-right-drain-pipe.svg | 2 +- .../main/data/json/system/scada_symbols/short-top-filter.svg | 2 +- .../data/json/system/scada_symbols/vertical-connector-hp.svg | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg b/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg index 630d0faa28..d9b0857156 100644 --- a/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/control-panel-hp.svg @@ -1,6 +1,6 @@ { "title": "HP Control panel", - "description": "Control panel", + "description": "Sends the command to the device or updates attribute/time series when the user pushes the button. Widget settings will enable you to configure behavior, how to fetch the initial state, and what to trigger when power on/off states.", "searchTags": [ "control" ], diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg index 3548d687c5..33b6c0a222 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{ "title": "HP Horizontal connector", - "description": "Horizontal connector", + "description": "Horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, "tags": [ diff --git a/application/src/main/data/json/system/scada_symbols/leak-sensor.svg b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg index e2859ea90c..0fce8be608 100644 --- a/application/src/main/data/json/system/scada_symbols/leak-sensor.svg +++ b/application/src/main/data/json/system/scada_symbols/leak-sensor.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="100" height="100" fill="none" version="1.1" viewBox="0 0 100 100"><tb:metadata xmlns=""><![CDATA[{ "title": "Leak sensor", - "description": "Leak sensor", + "description": "Leak sensor for real-time detection and warning of fluid leakage.", "searchTags": [ "leak" ], diff --git a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg index 18394d3d2c..2cdc9e587a 100644 --- a/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-analog-water-level-meter.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{ "title": "Left analog water level meter", - "description": "Left analog water level meter", + "description": "Left analog water level meter with real-time display of fluid levels.", "searchTags": [ "water meter" ], diff --git a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg index 326a263208..cb7bb26821 100644 --- a/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/left-drain-pipe.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"> <tb:metadata><![CDATA[{ "title": "Left drain pipe", - "description": "Left drain pipe", + "description": "Left drain pipe with configurable fluid and leak visualizations.", "searchTags": [ "pipe", "drain" diff --git a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg index b9a6aa6057..085830c5e4 100644 --- a/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/left-elbow-drain-pipe.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"> <tb:metadata xmlns=""><![CDATA[{ "title": "Left elbow drain pipe", - "description": "Left elbow drain pipe", + "description": "Left elbow drain pipe with configurable fluid and leak visualizations.", "searchTags": [ "pipe", "drain" diff --git a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg index d9defd33d9..689851529d 100644 --- a/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/long-bottom-filter.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600"> <tb:metadata xmlns=""><![CDATA[{ - "title": "Long bottom filter", + "title": "Long bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.", "description": "Long bottom filter", "searchTags": [ "filter" diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg index 4376bb2348..86bb07a520 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg @@ -1,7 +1,7 @@ <svg width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg"> <tb:metadata><![CDATA[{ "title": "HP Long horizontal connector", - "description": "Long horizontal connector", + "description": "Long horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 2, "widgetSizeY": 1, "tags": [ diff --git a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg index a63b679f0a..9b287cbe46 100644 --- a/application/src/main/data/json/system/scada_symbols/long-top-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/long-top-filter.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="600" fill="none" version="1.1" viewBox="0 0 200 600"> <tb:metadata xmlns=""><![CDATA[{ - "title": "Long top filter", + "title": "Long top filter with configurable click actions for custom operations, dashboard manipulation, etc.", "description": "Title\nLong top filter\n", "searchTags": [ "filter" diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index 86235ff1c1..d7a0aa8aa7 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"><tb:metadata xmlns=""><![CDATA[{ "title": "HP Long vertical connector", - "description": "Long vertical connector", + "description": "Long vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 2, "tags": [ diff --git a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg index 4bd6754f5c..1b1be4f007 100644 --- a/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-analog-water-level-meter.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{ "title": "Right analog water level meter", - "description": "Right analog water level meter", + "description": "Right analog water level meter with real-time display of fluid levels.", "searchTags": [ "water meter" ], diff --git a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg index 093078c69b..a990e023e1 100644 --- a/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/right-drain-pipe.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{ "title": "Right drain pipe", - "description": "Right drain pipe", + "description": "Right drain pipe with configurable fluid and leak visualizations.", "searchTags": [ "pipe", "drain" diff --git a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg index 63b143eeef..6f5ae5abb9 100644 --- a/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/right-elbow-drain-pipe.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"> <tb:metadata xmlns=""><![CDATA[{ "title": "Right elbow drain pipe", - "description": "Right elbow drain pipe", + "description": "Right elbow drain pipe with configurable fluid and leak visualizations.", "searchTags": [ "pipe", "drain" diff --git a/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg index b3235c5906..cf191d40fe 100644 --- a/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/short-bottom-filter.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"> <tb:metadata xmlns=""><![CDATA[{ "title": "Short bottom filter", - "description": "Short bottom filter", + "description": "Short bottom filter with configurable click actions for custom operations, dashboard manipulation, etc.", "searchTags": [ "filter" ], diff --git a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg index abf43aacb9..da2273d096 100644 --- a/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/short-left-drain-pipe.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"> <tb:metadata><![CDATA[{ "title": "Short left drain pipe", - "description": "Short left drain pipe", + "description": "Short left drain pipe with configurable fluid and leak visualizations.", "searchTags": [ "pipe", "drain" diff --git a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg index 1b2ae3934a..1e01fe8b77 100644 --- a/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg +++ b/application/src/main/data/json/system/scada_symbols/short-right-drain-pipe.svg @@ -1,7 +1,7 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"> <tb:metadata><![CDATA[{ "title": "Short right drain pipe", - "description": "Short right drain pipe", + "description": "Short right drain pipe with configurable fluid and leak visualizations.", "searchTags": [ "pipe", "drain" diff --git a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg index d50eada2aa..0a77c78f00 100644 --- a/application/src/main/data/json/system/scada_symbols/short-top-filter.svg +++ b/application/src/main/data/json/system/scada_symbols/short-top-filter.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="400" fill="none" version="1.1" viewBox="0 0 200 400"> <tb:metadata xmlns=""><![CDATA[{ - "title": "Short top filter", + "title": "Short top filter with configurable click actions for custom operations, dashboard manipulation, etc.", "description": "Short top filter", "searchTags": [ "filter" diff --git a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg index aea94b0dd2..6ce9336fee 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg @@ -1,6 +1,6 @@ <svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="200" height="200" fill="none" version="1.1" viewBox="0 0 200 200"><tb:metadata xmlns=""><![CDATA[{ "title": "HP Vertical connector", - "description": "Vertical connector", + "description": "Vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, "tags": [ From d2389817a48d69063f18fcc705b65131e09162ad Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko <Dgerik2015@gmail.com> Date: Thu, 13 Feb 2025 15:00:44 +0200 Subject: [PATCH 17/22] UI: Fixed scada bundle order and add hook to general --- .../widget_bundles/general_high_performance_scada_symbols.json | 1 + .../widget_bundles/high_performance_scada_fluid_system.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json b/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json index 33efaacc0c..97768915f7 100644 --- a/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json +++ b/application/src/main/data/json/system/widget_bundles/general_high_performance_scada_symbols.json @@ -29,6 +29,7 @@ "hp_top_tee_connector", "hp_drawwork", "hp_crane", + "hp_hook", "hp_consumers", "hp_house", "hp_apartments", diff --git a/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json b/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json index 1e41b22446..29a0571dfa 100644 --- a/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json +++ b/application/src/main/data/json/system/widget_bundles/high_performance_scada_fluid_system.json @@ -5,7 +5,7 @@ "image": "tb-image:aHBfc2NhZGFfZmx1aWRfc3lzdGVtX2J1bmRsZV9pbWFnZS5wbmc=:IkhpZ2gtcGVyZm9ybWFuY2UgU0NBREEgZmx1aWQgc3lzdGVtIiBzeXN0ZW0gYnVuZGxlIGltYWdl:SU1BR0U=;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABNVBMVEXe3t7f39/X19cAAADf39/e3t7f39/e3t7e3t7////I3/fr6+vHx8esrKzj4+OcrsHT0tKVlZWvr6/Kysrx8fF0dHT9/f24uLjBwcHc29v39/etra27u7ubm5vt7e2/v7/Dw8OysrKqqqq+1ev6+vrE2/Lh4eHV1dWoqKiQkJCmpqa1yt+2trbu7e2YmJi/1eyurq7B2O9ycXFlZWXT09Ojo6PMzMzIyMjm5eWBgYGenp78/Py0tLSTk5OLi4uwxdq6z+Wous6ioqKGhoZjY2OgoKBoaGheXl5WVlasv9SorrR/fn7R0dGlpaVSUlKhrr13d3evuseTpLWWoq+fn59sbGw+Pj6rv9Tv8veitci4uryAk7vf5O7P1+WQocRgeasvLy8hISG/yd2frsx/k7tvhrNAXZmZOz+XAAAACHRSTlPvICAAv7Cvv9cI9yEAAAzySURBVHja7JttT9pQGIaNe8lTejg9dDRqJEElwFxNN1lIZNko3RrRuoAvUaOf/P+/YkecPq0v0PY5lZZ48dUvV06vm54Ylt4tv1+CovNx+d3ScvE1JFLjAywE8R6rNcg9SzNEama36/R/raxLVmazsfrTrMFcmCYiNrdAb7VX6ht6u91e0+MhdkwGBNSLdDd0ZK/V0mOzYwIBxSLiJ+hhEc71+EBdAAGVIuKzTuIzwUSpyKoeZa/FE5oAAXUijo609D0UiQ+hE3UiTOhIvf5Jb0kSimwIoEIX2dTDIp9wfpOwCXGpHHyfwqiSXgSHNzy/mR1J5RtMYT29SE1HEseOmHMX+aIroT53kdVnToTvJRbZmbtINJE1+bl9tAooYpTCGIZbakhKSTFyJnLlHZZs2x4UXmRC8+vXRRHZXQwRyWKILMCJNO2GvRAiJds7VDy/XZqImVLEHaie35s/LCrSX59CPyrChhe5mV+LW1ZYhVUi+H4lAgNEHPWqVm5itzSN+87LybwcRd9nmmblZn4tTbJ1YCYVMc9XNEmuTuSW7WOWRIQNt6saRWTQcKXIblO5iMatIzZbBOPQNJLIlYHzq1REwg+ceCLObRxEEdt2cX7VikxSqc0WMc+3NMTKz/xqYbb32XQRti/j+E8uY9cwlSki4qjDtRC5m98wbUzlsYjTmcRRiBO5S4U9J2IOMQ6yiDvwshfRqg+poAg7xjhUiGQ4v09TQRFx5GMcKkQuB3amIgg7cFCkG42jAPMbSWXI7kTM4Q/tnkLFfg+XqVQq4tE3R2HmNwzvXYwmcRT7RKSI3+v1MhFxB/YriOAKi8lLCVcu0jyV8zuwbfs1RH4MzfvLR1n5iTQbrza/nS7+kTPKZn53s3u0MA4BYXoWL2DsVbz4Rt7fiza/ZRnHM7BRuVAnwjoOPAJTaSubX9vNVoR3MI6nCEyFKpLp/OL7O/I0FTUijUGG81s+N2EmtVE5540w34FYOCOeY5FqD+OYBZOp5HR+q9tDBnHAi28uT6SGccTFHJVJV91LT70I8/uQAsfn6UUOPeXzi3EkhVkWTytiN1XP7wXGkRz25+a1G/HgBUwg0Y0tMi49It3/R04gW2aLnJSUcAY06CLf1Dxa10CDLiK8yPy6V6lEPCBCF4Egslqul2p+AyBCF4keSVN+Usyvy4AKXQQCeiMVoEMXgbOnIjne3iki4ow4vX+BAF0EEScYRTPpF2JjTPWgiyDB+OHSnvDR8n4DGboIwoKxO9nfRPPbOAsIx6FWBLkOxieeYRinEmM243EQXMNcWIgfuL6J5JE3kX/tneeu0zAUgEEsH8mx3MY0o0naRIiEqK0Q6oDS3h8gJCRA4gcgJJaY7/8I2Bk9GaVJKKOMD6Gbyy3H/myfeKSFU+O/yKnxj4nw/uzBIjY0yY07Dx9eXWuHwHOn1cigPwt/3lWEznyNQRFhrNchfBONZMyn8BOZrjqJPPEp7CNcU9Ek4sNPZdRehJ+5Ar4J1dhhEQo/Fae1iB8LOAilv1OEthSh9xk0wW6wkxd5EkEbAu3ERbCpG2BrccIifCugNWvWKEL38QtE+Ay6ELAGkRXsQYQ/UQQ9jjCpi9D9FfnpIlvoSiBOUWTfJCiCReDOegs36E8Y1Fn/aBERjkeSMRXfLfKE1YJGwU1Odty8Y9Zf4vxIEToynbxAZ9MPv0uEbqp1dM84qdA7q6pQ2kbk7ds3r941icwnlfKk13eIONWXu0nYobudbYLI37oPku/9auy1aBZ5+eHdS3h1WOR6psEN3/dHw8wp9rqKzBiUmDkq6MRl4N2GBBb0VAh6j1WyqFkE3nyCj68PivRp0prBZumAIlxqwySW3lEkKtfuPpcaQVrkLpTJHBXELZsYorlHvrx6/flQjogg0RipUHwKKSvfT2rGuoj4rBT3hgqbu9lzSOEAgVs3oUcnO1NRaV8ANlz6NaZqeLEOIuVM36raziHHyr6ogKxuEoruInUPf1eeZUOCqX4UYys3idQzJOayro91LMgqtJTo8erUuT5SZKIG0AQkaCDh6RSgfijaiiygQECTNvfQhKfxsb94/ANF/KSqnlkRsTzYmTgtRSgtzR+E9JjKDRUQ+2JqFTLoJgNE0GNEGE3TAE0sq9huIiaE83YipfaN08kxM8HGMSBjeuMmIfehgEYVd2oiId2xWtEcBiVcQugU6g3HIWOywho2iCwAYT4hdwBKJrYuf1tYTF92CS3VpQSFDpgqm3fFoYju5e3G1bGJ2UaEG+XAONI8fRcZPTwQi3K+U7pfhF2XvyTXQwbXc+ZiUDnc8fcUhwPATAZJ3EakzwCRCXADv7PSaX2FGTJnAKpLFgKQJ3tFBtg188G4cA1ISIutfZtnItgh6mKCWXJQxAWE3ZTfr1Y611M7007Hq14qit4kfALIep/IoCBy/S6KjAdooppkONRNa5pVe5rlZC5nKiHhELJqIfKsfE9/wLIq65ZKjzS+hR5ZVtwDZNbUI3URHFkaqAzUzUTCSEXyDsk2DiuVx80ii/Kk7gKOWWt6ezq7f3b/eRpQqqXcI+SsqL+3R+4mF04ick2KCMaYkCJ3YYD7GRxZti7DexaEdNTPytFZ5k4IZ00imEjY1IhlPTWXkhtMleQVat4DRPu2yNX3N3IR9uL9C6ZEsEsoITzEOJ6hjx2RnwbYppf7qkxqFgkAmWVeaGYuN+tgubwDlr4rzzyjZMhgh0P3iVyTF/Tq1athJgLsIVPNe21QHFlDKOKNpICdDDEsz7YiQkbNIhogvcwLRZb9W7duPVk+mHhZUH1uAXMIDWv333qP5CiRDCUyqN98cQVq3LolNYYGyy24afuEOEeKnC23UuTRcnJPl8xlSmY3N0qbRK7tEykPLT1taYTC9JaEwcaTWCtT1201lI8SsT2+4jeWaxl4uwywA9qK3G0WCasiI/CUiAWxxbk1tUHSWsQAZJFuFr2VPuf6bds2by2Xs0f3zWW2uMKaDxuH1mBOM5zJtcfjnMe1oZXdf2V36w8A4lu3eri07CISlyd2Vw6hbHNzW7fZ2VKhjaGIIacb0XTXGly7Zmdcu3b32u5yUBLpA9N1k6czoe456eqXQolRq2SfAfIA76vZ7ZY9DZabO2fGFMod5wISVETQJOPu3bvXdqCHmiDo0MrLM+QVc0DiLKGIuCnDNosMAYlwFTU1IcHUYip0sOa8tCZ5AIj/DZHB3UGKvM5c7qo/Km1GJvnqx0yL1YaU+oybhZabq+mmWSQqD35OS6sR3bO8dKniWTqfZrmuykee1UWQpNpKJQcK4H1Vz8ozwEzXvNzUp7gic9osUQQgZ8mmCT2mLN1XWdM8J7m+imQ3sqa1VkVnv8hKVlFkq+p8tWvmu0RbupiWZytdret+xE/3sVMd8gYCj+3WwbiLXAMSOs0iWbdABc4J2aBHUu4K18GS2xbv4c7qoAjxK+v4e+iRDSsF6m4ooQEgwRE7xE1y17QK7QYcKg3nYhUPi7hQ4J468tMho3AOYJvl3TDy7Mit7mbXbrqHxfV3L+Etd4ikTwFhMrO2Aj0kenknuiblDmGLI0TUMHUmUOgQmNupiYHt5rM2IpWF24bjIYkBeWQ0iWllqT8mx4hwjidwlpcYlBtObXRNaCfiisqJKe9hYDxBkaGVR/XM9P5RIuAQvAUXv2RHXSFGbBbhQfUslm9FIaKBk/2aVk4rgNLjRESc/51pLmLnJoa9wmPsBpF6uuNJNa+cKwNTzxucMexN9e4imAQkYjiEbX0nqWao1RRai/CgbnLzbOSBAg+wXRV3MYEiGj1GBE10E0wsLuXxSHlc7/TESpRD3+BE/bNEIaRwO3uoiM8bcL18tAiwmEjcZ3hSgA8V/Wm3Z4g+lIkfEInj3otoyBjdLtybSSW3AkpE9FgRhUh3/X6fpuFNMR/FPBkqrOvD0GrxTMVWcOfm7jKovWpGjhFBTJ8k3BxNJuNJ5PDsIXX35+xxTT281+ME4cMtBQQf0h0tgioIPlTsKoIbDEQY99wHQ2M8HvbkGNsT1SXHiyCm5nCSwZ1gggV2EuEx7IWFNGSwl4g2iHSGjkYOpY4zDo94Uw2NoAN1j9N54xmNunqcqAihbjePkxUhfMugHWJGyQmLEOI2VQOn/tMWIdQV0EjkEHLqIqQXOE2h1i4/EGADP5XHLUVcIceNBg3vjd8eEIkF/ESE3yyC7whkgcH2h9Eo2y30u+fZ8cw3pJUIjbDhjWp9Qk2jIr/uH/wETQPumO7FeNJvgLf93/dKFTccQ9ZdoRmBpoVQYEPJd4J7pTrXV6SBBpF4lBBpddY3JFqdaJRwg3QEPZpNuoss4HsZk248mUwieoBgMknT+dRFaNvP7PwFIph93UUi7Xs5I92IaCMRac25v+UTlecukr+CS+fO/xVdIjUunL9M/nTOXTl/4St0s1h4GZ5DIgAAAABJRU5ErkJggg==", "scada": true, "description": "Bundle with high-performance SCADA symbols for fluid system", - "order": 9350, + "order": 9415, "name": "High-performance SCADA fluid system" }, "widgetTypeFqns": [ From a460570c449adf9a11e23ed9ea4701a3872f0d06 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko <Dgerik2015@gmail.com> Date: Thu, 13 Feb 2025 15:24:12 +0200 Subject: [PATCH 18/22] UI: Fixed scale symbol units --- .../json/system/scada_symbols/dynamic-vertical-scale-hp.svg | 2 +- .../data/json/system/scada_symbols/simple-vertical-scale-hp.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg index 98185cbc8d..960abed340 100644 --- a/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/dynamic-vertical-scale-hp.svg @@ -776,7 +776,7 @@ } ] } -Outdoor°C +Outdoor°C diff --git a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg index bfc356cca2..fa0c0fc4d1 100644 --- a/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/simple-vertical-scale-hp.svg @@ -704,7 +704,7 @@ } ] }]]> -Outdoor°C +Outdoor°C From b6a7c3231b0cffa07bfc55d86bbafd2fa5c392f0 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 17 Feb 2025 13:24:18 +0200 Subject: [PATCH 19/22] UI: Add Mobile bundle validators (PROD-5516). --- .../bundes/layout/custom-mobile-page.component.html | 8 ++++++++ .../mobile/bundes/layout/custom-mobile-page.component.ts | 2 +- .../mobile/bundes/mobile-bundle-dialog.component.html | 6 ++++++ .../pages/mobile/bundes/mobile-bundle-dialog.component.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 3 +++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html index 1c89c7e06b..b0a44386cd 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.html @@ -46,6 +46,14 @@ class="tb-error"> warning + + warning +
diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts index e9576f313a..cf57c90a14 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/layout/custom-mobile-page.component.ts @@ -65,7 +65,7 @@ export class CustomMobilePageComponent implements ControlValueAccessor, Validato customMobilePageForm = this.fb.group({ visible: [true], icon: ['star'], - label: ['', [Validators.required, Validators.pattern(/\S/)]], + label: ['', [Validators.required, Validators.pattern(/\S/), Validators.maxLength(255)]], type: [MobilePageType.DASHBOARD], dashboardId: this.fb.control(null, Validators.required), url: [{value:'', disabled: true}, [Validators.required, Validators.pattern(WEB_URL_REGEX)]], diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 8e029cf9f7..e3352a2eb4 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -44,6 +44,12 @@ {{ 'mobile.title-required' | translate }} + + {{ 'mobile.title-cannot-contain-only-spaces' | translate }} + + + {{ 'mobile.title-max-length' | translate }} + Date: Mon, 17 Feb 2025 13:25:53 +0200 Subject: [PATCH 20/22] UI: Remove redundant request after close mobile config dialog (PROD-5525). --- .../mobile/bundes/mobile-bundle-table-config.resolve.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts index ac9445c615..5576117460 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-table-config.resolve.ts @@ -185,11 +185,7 @@ export class MobileBundleTableConfigResolver { iosApp: data.iosApp } }).afterClosed() - .subscribe(() => { - if (afterAdd) { - this.config.updateData(); - } - }); + .subscribe(); }) } } From eb9d929ccff3767c5b3a23304d1eaae416abfd53 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Tue, 18 Feb 2025 11:09:43 +0200 Subject: [PATCH 21/22] UI: Add property decimals for flow meter symbol --- .../scada_symbols/bottom-flow-meter.svg | 108 +++++------------- .../horizontal-inline-flow-meter.svg | 108 +++++------------- .../system/scada_symbols/left-flow-meter.svg | 104 +++-------------- .../system/scada_symbols/right-flow-meter.svg | 104 +++-------------- .../system/scada_symbols/top-flow-meter.svg | 108 +++++------------- .../vertical-inline-flow-meter.svg | 108 +++++------------- .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 147 insertions(+), 494 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg index 7d22c90abf..794e6e61d9 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,16 +463,20 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", "fieldClass": "medium-width", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "valueDecimals", + "name": "{i18n:scada.symbol.decimals}", + "type": "number", + "default": 0, + "fieldClass": "medium-width", + "min": 0, + "step": 1, + "disabled": false, + "visible": true }, { "id": "defaultBorderColor", @@ -511,16 +515,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeBorderColor", @@ -559,16 +555,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningBorderColor", @@ -607,16 +595,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalBorderColor", @@ -655,16 +635,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "backgroundColor", @@ -703,48 +675,24 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg index 4ad766764a..5e0c781941 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-inline-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,16 +463,20 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", "fieldClass": "medium-width", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "valueDecimals", + "name": "{i18n:scada.symbol.decimals}", + "type": "number", + "default": 0, + "fieldClass": "medium-width", + "min": 0, + "step": 1, + "disabled": false, + "visible": true }, { "id": "defaultBorderColor", @@ -511,16 +515,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeBorderColor", @@ -559,16 +555,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningBorderColor", @@ -607,16 +595,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalBorderColor", @@ -655,16 +635,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "backgroundColor", @@ -703,48 +675,24 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg index 12ec888e66..1c29a79479 100644 --- a/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/left-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,16 +463,16 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", + "fieldClass": "medium-width" + }, + { + "id": "valueDecimals", + "name": "{i18n:scada.symbol.decimals}", + "type": "number", + "default": 0, "fieldClass": "medium-width", - "min": null, - "max": null, - "step": null + "min": 0, + "step": 1 }, { "id": "defaultBorderColor", @@ -510,17 +510,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "activeBorderColor", @@ -558,17 +548,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "warningBorderColor", @@ -606,17 +586,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "criticalBorderColor", @@ -654,17 +624,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "backgroundColor", @@ -702,49 +662,19 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#1EC1F480" }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", - "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#FFFFFF" } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg index 9be5ec5857..6a201111ef 100644 --- a/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/right-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,16 +463,16 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", + "fieldClass": "medium-width" + }, + { + "id": "valueDecimals", + "name": "{i18n:scada.symbol.decimals}", + "type": "number", + "default": 0, "fieldClass": "medium-width", - "min": null, - "max": null, - "step": null + "min": 0, + "step": 1 }, { "id": "defaultBorderColor", @@ -510,17 +510,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "activeBorderColor", @@ -558,17 +548,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "warningBorderColor", @@ -606,17 +586,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "criticalBorderColor", @@ -654,17 +624,7 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "backgroundColor", @@ -702,49 +662,19 @@ "rangeAdvanced": [] }, "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';" - }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + } }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", - "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#1EC1F480" }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", - "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "default": "#FFFFFF" } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg index 619bb48ef8..9c09a6067e 100644 --- a/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/top-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,16 +463,20 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", "fieldClass": "medium-width", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "valueDecimals", + "name": "{i18n:scada.symbol.decimals}", + "type": "number", + "default": 0, + "fieldClass": "medium-width", + "min": 0, + "step": 1, + "disabled": false, + "visible": true }, { "id": "defaultBorderColor", @@ -511,16 +515,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeBorderColor", @@ -559,16 +555,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningBorderColor", @@ -607,16 +595,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalBorderColor", @@ -655,16 +635,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "backgroundColor", @@ -703,48 +675,24 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg index fff4819e1e..8f9a30e576 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-inline-flow-meter.svg @@ -57,7 +57,7 @@ }, { "tag": "value", - "stateRenderFunction": "var value = ctx.values.value;\nctx.api.text(element, value.toFixed(0));\n", + "stateRenderFunction": "var value = ctx.api.formatValue(ctx.values.value, ctx.properties.valueDecimals, '', false);\nctx.api.text(element, value);\n", "actions": { "click": { "actionFunction": "ctx.api.callAction(event, 'displayClick');" @@ -463,16 +463,20 @@ "name": "{i18n:scada.symbol.units}", "type": "units", "default": "m³/hr", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", "fieldClass": "medium-width", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true + }, + { + "id": "valueDecimals", + "name": "{i18n:scada.symbol.decimals}", + "type": "number", + "default": 0, + "fieldClass": "medium-width", + "min": 0, + "step": 1, + "disabled": false, + "visible": true }, { "id": "defaultBorderColor", @@ -511,16 +515,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "activeBorderColor", @@ -559,16 +555,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "warningBorderColor", @@ -607,16 +595,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "criticalBorderColor", @@ -655,16 +635,8 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "backgroundColor", @@ -703,48 +675,24 @@ }, "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';" }, - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "fluidColor", "name": "{i18n:scada.symbol.fluid-color}", "type": "color", "default": "#1EC1F480", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true }, { "id": "pipeColor", "name": "{i18n:scada.symbol.pipe-color}", "type": "color", "default": "#FFFFFF", - "required": null, - "subLabel": null, - "divider": null, - "fieldSuffix": null, - "disableOnProperty": null, - "rowClass": "", - "fieldClass": "", - "min": null, - "max": null, - "step": null + "disabled": false, + "visible": true } ] }]]> diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 4ebe2c7a2f..3713bca3a6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3218,6 +3218,7 @@ "bottom-fluid-color": "Bottom fluid color", "display": "Display", "value": "Value", + "decimals": "Decimals", "units": "Units", "flow-meter-value-hint": "Double value showing on flow meter display", "value-hint": "Double value indicating the current value", From 2530c77268ff4d1da50d060fa0816320f38cff0e Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 19 Feb 2025 12:32:41 +0200 Subject: [PATCH 22/22] UI: Refactoring --- .../lib/rpc/power-button-widget.component.ts | 6 ++-- .../rpc/value-stepper-widget.component.html | 2 +- .../rpc/value-stepper-widget.component.scss | 3 -- .../lib/rpc/value-stepper-widget.component.ts | 33 ++++++++++--------- .../lib/rpc/value-stepper-widget.models.ts | 31 ++++++++++++----- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts index 262a532163..cab9b9d572 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/power-button-widget.component.ts @@ -18,7 +18,8 @@ import { AfterViewInit, ChangeDetectorRef, Component, - ElementRef, NgZone, + ElementRef, + NgZone, OnDestroy, OnInit, Renderer2, @@ -33,7 +34,8 @@ import { DomSanitizer } from '@angular/platform-browser'; import { ValueType } from '@shared/models/constants'; import { powerButtonDefaultSettings, - PowerButtonShape, powerButtonShapeSize, + PowerButtonShape, + powerButtonShapeSize, PowerButtonWidgetSettings } from '@home/components/widget/lib/rpc/power-button-widget.models'; import { SVG, Svg } from '@svgdotjs/svg.js'; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html index 6b28d44066..6ce4387c1a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.component.html @@ -30,7 +30,7 @@ [class.tb-value-stepper-value-box-disabled]="(disabledState$ | async) === true">
{{ valueText }}
; - @ViewChild('rightButton', {static: false}) + @ViewChild('rightButton', {static: true}) rightButton: ElementRef; - @ViewChild('stepperContent', {static: false}) + @ViewChild('stepperContent', {static: true}) stepperContent: ElementRef; - @ViewChild('valueBoxContainer', {static: false}) + @ViewChild('valueBoxContainer', {static: true}) valueBox: ElementRef; - @ViewChild('value', {static: false}) + @ViewChild('value', {static: true}) valueElement: ElementRef; settings: ValueStepperWidgetSettings; @@ -79,6 +81,8 @@ export class ValueStepperWidgetComponent extends padding: string; valueStyle: ComponentStyle = {}; + valueStyleColor = ''; + disabledColor = 'rgba(0, 0, 0, 0.38)'; value: number = null; autoScale = false; @@ -108,7 +112,8 @@ export class ValueStepperWidgetComponent extends private leftDisabledState$ = new BehaviorSubject(false); private rightDisabledState$ = new BehaviorSubject(false); - private readonly destroy$ = new Subject(); + + protected destroyRef = inject(DestroyRef); constructor(protected imagePipe: ImagePipe, protected sanitizer: DomSanitizer, @@ -135,7 +140,7 @@ export class ValueStepperWidgetComponent extends this.showLeftButton = this.settings.buttonAppearance.leftButton.showButton; this.showRightButton = this.settings.buttonAppearance.rightButton.showButton; this.valueStyle = textStyle(this.settings.appearance.valueFont); - this.valueStyle.color = this.settings.appearance.valueColor; + this.valueStyleColor = this.settings.appearance.valueColor; if (this.showValueBox) { const valueBoxCss = `.tb-value-stepper-value-box {\n`+ @@ -171,7 +176,7 @@ export class ValueStepperWidgetComponent extends this.disabledState$.asObservable(), this.leftDisabledState$.asObservable() ]).pipe( - takeUntil(this.destroy$) + takeUntilDestroyed(this.destroyRef) ).subscribe(value => { const state = value.includes(true); this.updateLeftDisabledState(state) @@ -182,7 +187,7 @@ export class ValueStepperWidgetComponent extends this.disabledState$.asObservable(), this.rightDisabledState$.asObservable() ]).pipe( - takeUntil(this.destroy$) + takeUntilDestroyed(this.destroyRef) ).subscribe(value => { const state = value.includes(true); this.updateRightDisabledState(state) @@ -200,8 +205,6 @@ export class ValueStepperWidgetComponent extends if (this.shapeResize$) { this.shapeResize$.disconnect(); } - this.destroy$.next(); - this.destroy$.complete(); super.ngOnDestroy(); } @@ -220,12 +223,12 @@ export class ValueStepperWidgetComponent extends private onValue(value: number): void { this.value = value; this.prevValue = value; - if ((this.value + this.settings.appearance.valueStep) >= this.settings.appearance.maxValueRange) { + if ((this.value + this.settings.appearance.valueStep) > this.settings.appearance.maxValueRange) { this.rightDisabledState$.next(true); } else { this.rightDisabledState$.next(false); } - if ((this.value - this.settings.appearance.valueStep) <= this.settings.appearance.minValueRange) { + if ((this.value - this.settings.appearance.valueStep) < this.settings.appearance.minValueRange) { this.leftDisabledState$.next(true); } else { this.leftDisabledState$.next(false); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts index 02471bfc05..9ec5ff5d65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/rpc/value-stepper-widget.models.ts @@ -1,19 +1,32 @@ +/// +/// Copyright © 2016-2024 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 { DataToValueType, GetValueAction, - GetValueSettings, SetValueAction, - SetValueSettings, ValueToDataType + GetValueSettings, + SetValueAction, + SetValueSettings, + ValueToDataType } from '@shared/models/action-widget-settings.models'; -import { defaultWidgetAction, WidgetAction } from '@shared/models/widget.models'; -import { - ButtonToggleAppearance, segmentedButtonDefaultAppearance, - SegmentedButtonWidgetSettings -} from '@home/components/widget/lib/button/segmented-button-widget.models'; -import { WidgetButtonCustomStyles, WidgetButtonType } from '@shared/components/button/widget-button.models'; +import { WidgetButtonCustomStyles } from '@shared/components/button/widget-button.models'; import { BackgroundSettings, BackgroundType, cssUnit, Font } from '@shared/models/widget-settings.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; - const defaultMainColor = '#305680'; export enum ValueStepperType {