From 0f7142cfef7f04afc69ebf5f2a63885accc0b3f3 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Fri, 31 Jan 2025 18:11:51 +0200 Subject: [PATCH 01/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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 { From ded6daf2b3b01a463973218a2406fc8be86f722b Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Feb 2025 15:44:05 +0200 Subject: [PATCH 23/29] Kafka states restore improvements --- .../TbRuleEngineQueueConsumerManager.java | 10 +-- .../AbstractTbQueueConsumerTemplate.java | 3 + .../consumer/MainQueueConsumerManager.java | 59 ++++++++++---- .../PartitionedQueueConsumerManager.java | 80 +++++++++++++++++++ .../common/consumer/QueueStateService.java | 76 ++++++++++++++++++ .../{QueueEvent.java => QueueTaskType.java} | 5 +- .../consumer/TbQueueConsumerManagerTask.java | 49 ++++++++---- .../common/consumer/TbQueueConsumerTask.java | 5 +- 8 files changed, 245 insertions(+), 42 deletions(-) create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java create mode 100644 common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java rename common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/{QueueEvent.java => QueueTaskType.java} (83%) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 1f421974be..241aec25bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -33,14 +33,14 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.QueueEvent; +import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.DeleteQueueTask; import org.thingsboard.server.queue.common.consumer.TbQueueConsumerTask; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.service.queue.TbMsgPackCallback; import org.thingsboard.server.service.queue.TbMsgPackProcessingContext; import org.thingsboard.server.service.queue.TbRuleEngineConsumerStats; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingResult; import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingStrategy; @@ -78,13 +78,13 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager i @Override public void commit() { if (consumerLock.isLocked()) { + if (stopped) { + return; + } log.error("commit. consumerLock is locked. will wait with no timeout. it looks like a race conditions or deadlock topic " + topic, new RuntimeException("stacktrace")); } consumerLock.lock(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 9d45b168ee..7bd8076ce9 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdateConfigTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.UpdatePartitionsTask; import org.thingsboard.server.queue.discovery.QueueKey; import java.util.Collection; @@ -31,6 +33,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutorService; @@ -87,20 +90,24 @@ public class MainQueueConsumerManager createConsumerWrapper(C config) { if (config.isConsumerPerPartition()) { - this.consumerWrapper = new ConsumerPerPartitionWrapper(); + return new ConsumerPerPartitionWrapper(); } else { - this.consumerWrapper = new SingleConsumerWrapper(); + return new SingleConsumerWrapper(); } - log.debug("[{}] Initialized consumer for queue: {}", queueKey, config); } public void update(C config) { - addTask(TbQueueConsumerManagerTask.configUpdate(config)); + addTask(new UpdateConfigTask(config)); } public void update(Set partitions) { - addTask(TbQueueConsumerManagerTask.partitionChange(partitions)); + addTask(new UpdatePartitionsTask(partitions)); } protected void addTask(TbQueueConsumerManagerTask todo) { @@ -125,10 +132,10 @@ public class MainQueueConsumerManager partitions) { + private void doUpdate(Set partitions) { this.partitions = partitions; consumerWrapper.updatePartitions(partitions); } @@ -195,6 +202,15 @@ public class MainQueueConsumerManager consumerTask.awaitCompletion(timeoutSec)); log.debug("[{}] Unsubscribed and stopped consumers", queueKey); } - private static String partitionsToString(Collection partitions) { + static String partitionsToString(Collection partitions) { return partitions.stream().map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? "[" + tpi.getPartition().orElse(-1) + "]" : "")) .collect(Collectors.joining(", ", "[", "]")); @@ -279,15 +295,24 @@ public class MainQueueConsumerManager removedPartitions = new HashSet<>(consumers.keySet()); removedPartitions.removeAll(partitions); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + removePartitions(removedPartitions); + addPartitions(addedPartitions, null); + } - removedPartitions.forEach((tpi) -> consumers.get(tpi).initiateStop()); - removedPartitions.forEach((tpi) -> consumers.remove(tpi).awaitCompletion()); + protected void removePartitions(Set removedPartitions) { + removedPartitions.forEach((tpi) -> Optional.ofNullable(consumers.get(tpi)).ifPresent(TbQueueConsumerTask::initiateStop)); + removedPartitions.forEach((tpi) -> Optional.ofNullable(consumers.remove(tpi)).ifPresent(TbQueueConsumerTask::awaitCompletion)); + } - addedPartitions.forEach((tpi) -> { + protected void addPartitions(Set partitions, Consumer onStop) { + partitions.forEach(tpi -> { Integer partitionId = tpi.getPartition().orElse(-1); String key = queueKey + "-" + partitionId; - TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId)); + Runnable callback = onStop != null ? () -> onStop.accept(tpi) : null; + + TbQueueConsumerTask consumer = new TbQueueConsumerTask<>(key, () -> consumerCreator.apply(config, partitionId), callback); consumers.put(tpi, consumer); consumer.subscribe(Set.of(tpi)); launchConsumer(consumer); @@ -316,7 +341,7 @@ public class MainQueueConsumerManager(queueKey, () -> consumerCreator.apply(config, null)); // no partitionId passed + consumer = new TbQueueConsumerTask<>(queueKey, () -> consumerCreator.apply(config, null), null); // no partitionId passed } consumer.subscribe(partitions); if (!consumer.isRunning()) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java new file mode 100644 index 0000000000..57f0950cdb --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -0,0 +1,80 @@ +/** + * 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. + */ +package org.thingsboard.server.queue.common.consumer; + +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.data.queue.QueueConfig; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueConsumer; +import org.thingsboard.server.queue.TbQueueMsg; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.AddPartitionsTask; +import org.thingsboard.server.queue.common.consumer.TbQueueConsumerManagerTask.RemovePartitionsTask; +import org.thingsboard.server.queue.discovery.QueueKey; + +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +@Slf4j +public class PartitionedQueueConsumerManager extends MainQueueConsumerManager { + + private final ConsumerPerPartitionWrapper consumerWrapper; + @Getter + private final String topic; + + @Builder(builderMethodName = "create") // not to conflict with super.builder() + public PartitionedQueueConsumerManager(QueueKey queueKey, String topic, long pollInterval, MsgPackProcessor msgPackProcessor, + BiFunction> consumerCreator, + ExecutorService consumerExecutor, ScheduledExecutorService scheduler, + ExecutorService taskExecutor, Consumer uncaughtErrorHandler) { + super(queueKey, QueueConfig.of(true, pollInterval), msgPackProcessor, consumerCreator, consumerExecutor, scheduler, taskExecutor, uncaughtErrorHandler); + this.topic = topic; + this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper; + } + + @Override + public void update(Set partitions) { + throw new UnsupportedOperationException("Use manual addPartitions and removePartitions"); + } + + @Override + protected void processTask(TbQueueConsumerManagerTask task) { + if (task instanceof AddPartitionsTask addPartitionsTask) { + log.info("[{}] Added partitions: {}", queueKey, partitionsToString(addPartitionsTask.partitions())); + consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop()); + } else if (task instanceof RemovePartitionsTask removePartitionsTask) { + log.info("[{}] Removed partitions: {}", queueKey, partitionsToString(removePartitionsTask.partitions())); + consumerWrapper.removePartitions(removePartitionsTask.partitions()); + } + } + + public void addPartitions(Set partitions) { + addPartitions(partitions, null); + } + + public void addPartitions(Set partitions, Consumer onStop) { + addTask(new AddPartitionsTask(partitions, onStop)); + } + + public void removePartitions(Set partitions) { + addTask(new RemovePartitionsTask(partitions)); + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java new file mode 100644 index 0000000000..bffe441f7c --- /dev/null +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -0,0 +1,76 @@ +/** + * 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. + */ +package org.thingsboard.server.queue.common.consumer; + +import lombok.Getter; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.queue.TbQueueMsg; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +public class QueueStateService { + + private PartitionedQueueConsumerManager stateConsumer; + private PartitionedQueueConsumerManager eventConsumer; + + @Getter + private Set partitions; + private final Lock lock = new ReentrantLock(); + + public void init(PartitionedQueueConsumerManager stateConsumer, PartitionedQueueConsumerManager eventConsumer) { + this.stateConsumer = stateConsumer; + this.eventConsumer = eventConsumer; + } + + public void update(Set newPartitions) { + lock.lock(); + Set oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet(); + Set addedPartitions; + Set removedPartitions; + try { + addedPartitions = new HashSet<>(newPartitions); + addedPartitions.removeAll(oldPartitions); + removedPartitions = new HashSet<>(oldPartitions); + removedPartitions.removeAll(newPartitions); + this.partitions = newPartitions; + } finally { + lock.unlock(); + } + if (!removedPartitions.isEmpty()) { + stateConsumer.removePartitions(removedPartitions); + eventConsumer.removePartitions(removedPartitions.stream().map(tpi -> tpi.withTopic(eventConsumer.getTopic())).collect(Collectors.toSet())); + } + + if (!addedPartitions.isEmpty()) { + stateConsumer.addPartitions(addedPartitions, partition -> { + lock.lock(); + try { + if (this.partitions.contains(partition)) { + eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic()))); + } + } finally { + lock.unlock(); + } + }); + } + } + +} diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java similarity index 83% rename from common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java rename to common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java index 1a78cfc2ba..4e218cd268 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueEvent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueTaskType.java @@ -17,8 +17,9 @@ package org.thingsboard.server.queue.common.consumer; import java.io.Serializable; -public enum QueueEvent implements Serializable { +public enum QueueTaskType implements Serializable { - PARTITION_CHANGE, CONFIG_UPDATE, DELETE + UPDATE_PARTITIONS, UPDATE_CONFIG, DELETE, + ADD_PARTITIONS, REMOVE_PARTITIONS } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java index 67bf370db0..7e2c848a66 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerManagerTask.java @@ -15,34 +15,49 @@ */ package org.thingsboard.server.queue.common.consumer; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.ToString; import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import java.util.Set; +import java.util.function.Consumer; -@Getter -@ToString -@AllArgsConstructor -public class TbQueueConsumerManagerTask { +public interface TbQueueConsumerManagerTask { - private final QueueEvent event; - private QueueConfig config; - private Set partitions; - private boolean drainQueue; + QueueTaskType getType(); - public static TbQueueConsumerManagerTask delete(boolean drainQueue) { - return new TbQueueConsumerManagerTask(QueueEvent.DELETE, null, null, drainQueue); + record DeleteQueueTask(boolean drainQueue) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.DELETE; + } } - public static TbQueueConsumerManagerTask configUpdate(QueueConfig config) { - return new TbQueueConsumerManagerTask(QueueEvent.CONFIG_UPDATE, config, null, false); + record UpdateConfigTask(QueueConfig config) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.UPDATE_CONFIG; + } } - public static TbQueueConsumerManagerTask partitionChange(Set partitions) { - return new TbQueueConsumerManagerTask(QueueEvent.PARTITION_CHANGE, null, partitions, false); + record UpdatePartitionsTask(Set partitions) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.UPDATE_PARTITIONS; + } + } + + record AddPartitionsTask(Set partitions, Consumer onStop) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.ADD_PARTITIONS; + } + } + + record RemovePartitionsTask(Set partitions) implements TbQueueConsumerManagerTask { + @Override + public QueueTaskType getType() { + return QueueTaskType.REMOVE_PARTITIONS; + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java index 4ed0ffa497..96085bf49a 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/TbQueueConsumerTask.java @@ -35,14 +35,17 @@ public class TbQueueConsumerTask { private final Object key; private volatile TbQueueConsumer consumer; private volatile Supplier> consumerSupplier; + @Getter + private final Runnable callback; @Setter private Future task; - public TbQueueConsumerTask(Object key, Supplier> consumerSupplier) { + public TbQueueConsumerTask(Object key, Supplier> consumerSupplier, Runnable callback) { this.key = key; this.consumer = null; this.consumerSupplier = consumerSupplier; + this.callback = callback; } public TbQueueConsumer getConsumer() { From 6bf56c8dc24d7e888e35d1a83069e07e862d728f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 19 Feb 2025 15:54:18 +0200 Subject: [PATCH 24/29] EDQS repartitioning improvements --- .../server/edqs/processor/EdqsProcessor.java | 64 ++++++----- .../server/edqs/state/EdqsStateService.java | 2 +- .../edqs/state/KafkaEdqsStateService.java | 101 ++++++++---------- .../edqs/state/LocalEdqsStateService.java | 39 +++---- 4 files changed, 94 insertions(+), 112 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 7e9d60d09a..fa1309f932 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -43,14 +43,13 @@ import org.thingsboard.server.common.data.edqs.query.QueryResult; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.edqs.repo.EdqRepository; +import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsConverter; -import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; @@ -59,7 +58,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueHandler; import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.edqs.EdqsComponent; @@ -93,7 +92,8 @@ public class EdqsProcessor implements TbQueueHandler, @Autowired @Lazy private EdqsStateService stateService; - private MainQueueConsumerManager, QueueConfig> eventsConsumer; + @Getter + private PartitionedQueueConsumerManager> eventsConsumer; private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; private ExecutorService consumersExecutor; @@ -125,11 +125,15 @@ public class EdqsProcessor implements TbQueueHandler, } }; - eventsConsumer = MainQueueConsumerManager., QueueConfig>builder() + eventsConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) - .config(QueueConfig.of(true, config.getPollInterval())) + .topic(EdqsQueue.EVENTS.getTopic()) + .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { + if (consumer.isStopped()) { + return; + } try { ToEdqsMsg msg = queueMsg.getValue(); log.trace("Processing message: {}", msg); @@ -159,37 +163,31 @@ public class EdqsProcessor implements TbQueueHandler, if (event.getServiceType() != ServiceType.EDQS) { return; } - repartitionExecutor.submit(() -> { // todo: maybe cancel the task if new event comes - try { - Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); - Set partitions = newPartitions.stream() - .map(tpi -> tpi.withUseInternalPartition(true)) - .collect(Collectors.toSet()); + try { + Set newPartitions = event.getNewPartitions().get(new QueueKey(ServiceType.EDQS)); + Set partitions = newPartitions.stream() + .map(tpi -> tpi.withUseInternalPartition(true)) + .collect(Collectors.toSet()); - try { - stateService.restore(withTopic(partitions, EdqsQueue.STATE.getTopic())); // blocks until restored - } catch (Exception e) { - log.error("Failed to process restore for partitions {}", partitions, e); - } - eventsConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic())); - responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); + stateService.process(withTopic(partitions, EdqsQueue.STATE.getTopic())); + // eventsConsumer's partitions are updated by stateService + responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); - Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); - if (CollectionsUtil.isNotEmpty(oldPartitions)) { - Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() - .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); - if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { - log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); - } - repository.clearIf(tenantId -> { - Integer partition = partitionService.resolvePartition(tenantId); - return partition != null && removedPartitions.contains(partition); - }); + Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); + if (CollectionsUtil.isNotEmpty(oldPartitions)) { + Set removedPartitions = Sets.difference(oldPartitions, newPartitions).stream() + .map(tpi -> tpi.getPartition().orElse(-1)).collect(Collectors.toSet()); + if (config.getPartitioningStrategy() != EdqsPartitioningStrategy.TENANT && !removedPartitions.isEmpty()) { + log.warn("Partitions {} were removed but shouldn't be (due to NONE partitioning strategy)", removedPartitions); } - } catch (Throwable t) { - log.error("Failed to handle partition change event {}", event, t); + repository.clearIf(tenantId -> { + Integer partition = partitionService.resolvePartition(tenantId); + return partition != null && removedPartitions.contains(partition); + }); } - }); + } catch (Throwable t) { + log.error("Failed to handle partition change event {}", event, t); + } } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index 06d080ac64..d45cc0de14 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -25,7 +25,7 @@ import java.util.Set; public interface EdqsStateService { - void restore(Set partitions); + void process(Set partitions); void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index 6ab1fb9e1f..fc7f8d40d9 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -25,7 +25,6 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.queue.QueueConfig; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.edqs.processor.EdqsProcessor; @@ -34,8 +33,9 @@ import org.thingsboard.server.edqs.util.VersionsStore; import org.thingsboard.server.gen.transport.TransportProtos.EdqsEventMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; -import org.thingsboard.server.queue.common.consumer.MainQueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; +import org.thingsboard.server.queue.common.consumer.QueueStateService; import org.thingsboard.server.queue.discovery.QueueKey; import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsQueue; @@ -44,7 +44,6 @@ import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; import java.util.Set; import java.util.UUID; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -54,19 +53,17 @@ import java.util.concurrent.atomic.AtomicInteger; @RequiredArgsConstructor @KafkaEdqsComponent @Slf4j -public class KafkaEdqsStateService implements EdqsStateService { +public class KafkaEdqsStateService extends QueueStateService, TbProtoQueueMsg> implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; private final EdqsProcessor edqsProcessor; - private MainQueueConsumerManager, QueueConfig> stateConsumer; - private QueueConsumerManager> eventsConsumer; + private PartitionedQueueConsumerManager> stateConsumer; + private QueueConsumerManager> eventsToBackupConsumer; private EdqsProducer stateProducer; - private boolean initialRestoreDone; - private ExecutorService consumersExecutor; private ExecutorService mgmtExecutor; private ScheduledExecutorService scheduler; @@ -81,11 +78,14 @@ public class KafkaEdqsStateService implements EdqsStateService { mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); - stateConsumer = MainQueueConsumerManager., QueueConfig>builder() // FIXME Slavik: if topic is empty + stateConsumer = PartitionedQueueConsumerManager.>create() // FIXME Slavik: if topic is empty .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) - .config(QueueConfig.of(true, config.getPollInterval())) + .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { + if (consumer.isStopped()) { + return; + } try { ToEdqsMsg msg = queueMsg.getValue(); log.trace("Processing message: {}", msg); @@ -94,7 +94,7 @@ public class KafkaEdqsStateService implements EdqsStateService { log.info("[state] Processed {} msgs", stateReadCount.get()); } } catch (Exception e) { - log.error("Failed to process message: {}", queueMsg, e); // TODO: do something about the error - e.g. reprocess + log.error("Failed to process message: {}", queueMsg, e); } } consumer.commit(); @@ -105,46 +105,43 @@ public class KafkaEdqsStateService implements EdqsStateService { .scheduler(scheduler) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); + super.init(stateConsumer, edqsProcessor.getEventsConsumer()); - ExecutorService backupExecutor = ThingsBoardExecutors.newLimitedTasksExecutor(12, 1000, "events-to-backup-executor"); - eventsConsumer = QueueConsumerManager.>builder() // FIXME Slavik writes to the state while we read it, slows down the start. maybe start backup consumer after restore is finished + eventsToBackupConsumer = QueueConsumerManager.>builder() .name("edqs-events-to-backup-consumer") .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer) -> { - CountDownLatch resultLatch = new CountDownLatch(msgs.size()); for (TbProtoQueueMsg queueMsg : msgs) { - backupExecutor.submit(() -> { - try { - ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); - - if (msg.hasEventMsg()) { - EdqsEventMsg eventMsg = msg.getEventMsg(); - String key = eventMsg.getKey(); - int count = eventsReadCount.incrementAndGet(); - if (count % 100000 == 0) { - log.info("[events-to-backup] Processed {} msgs", count); - } - if (eventMsg.hasVersion()) { - if (!versionsStore.isNew(key, eventMsg.getVersion())) { - return; - } - } + if (consumer.isStopped()) { + return; + } + try { + ToEdqsMsg msg = queueMsg.getValue(); + log.trace("Processing message: {}", msg); - TenantId tenantId = getTenantId(msg); - ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); - EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); - log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); - stateProducer.send(tenantId, objectType, key, msg); + if (msg.hasEventMsg()) { + EdqsEventMsg eventMsg = msg.getEventMsg(); + String key = eventMsg.getKey(); + int count = eventsReadCount.incrementAndGet(); + if (count % 100000 == 0) { + log.info("[events-to-backup] Processed {} msgs", count); + } + if (eventMsg.hasVersion()) { + if (!versionsStore.isNew(key, eventMsg.getVersion())) { + continue; + } } - } catch (Throwable t) { - log.error("Failed to process message: {}", queueMsg, t); - } finally { - resultLatch.countDown(); + + TenantId tenantId = getTenantId(msg); + ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); + EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); + log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); + stateProducer.send(tenantId, objectType, key, msg); } - }); + } catch (Throwable t) { + log.error("Failed to process message: {}", queueMsg, t); + } } - resultLatch.await(); consumer.commit(); }) .consumerCreator(() -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group @@ -160,20 +157,12 @@ public class KafkaEdqsStateService implements EdqsStateService { } @Override - public void restore(Set partitions) { - stateReadCount.set(0); //TODO Slavik: do not support remote mode in monolith setup - long startTs = System.currentTimeMillis(); - log.info("Restore started for partitions {}", partitions.stream().map(tpi -> tpi.getPartition().orElse(-1)).sorted().toList()); - stateConsumer.doUpdate(partitions); // calling blocking doUpdate instead of update - stateConsumer.awaitStop(0); // consumers should stop on their own because EdqsQueue.STATE.stopWhenRead is true, we just need to wait - log.info("Restore finished in {} ms. Processed {} msgs", (System.currentTimeMillis() - startTs), stateReadCount.get()); - - if (!initialRestoreDone) { - initialRestoreDone = true; - - eventsConsumer.subscribe(); - eventsConsumer.launch(); + public void process(Set partitions) { + if (getPartitions() == null) { + eventsToBackupConsumer.subscribe(); + eventsToBackupConsumer.launch(); } + super.update(partitions); } @Override @@ -194,7 +183,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private void preDestroy() { stateConsumer.stop(); stateConsumer.awaitStop(); - eventsConsumer.stop(); + eventsToBackupConsumer.stop(); stateProducer.stop(); consumersExecutor.shutdownNow(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index 6a61488182..32f4159f9f 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -17,8 +17,6 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -38,29 +36,26 @@ import java.util.Set; @Slf4j public class LocalEdqsStateService implements EdqsStateService { - @Autowired @Lazy - private EdqsProcessor processor; - @Autowired - private EdqsRocksDb db; + private final EdqsProcessor processor; + private final EdqsRocksDb db; - private boolean restoreDone; + private Set partitions; @Override - public void restore(Set partitions) { - if (restoreDone) { - return; + public void process(Set partitions) { + if (this.partitions != null) { + db.forEach((key, value) -> { + try { + ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); + log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg); + processor.process(edqsMsg, EdqsQueue.STATE); + } catch (Exception e) { + log.error("[{}] Failed to restore value", key, e); + } + }); } - - db.forEach((key, value) -> { - try { - ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); - log.trace("[{}] Restored msg from RocksDB: {}", key, edqsMsg); - processor.process(edqsMsg, EdqsQueue.STATE); - } catch (Exception e) { - log.error("[{}] Failed to restore value", key, e); - } - }); - restoreDone = true; + processor.getEventsConsumer().update(partitions); + this.partitions = partitions; } @Override @@ -79,7 +74,7 @@ public class LocalEdqsStateService implements EdqsStateService { @Override public boolean isReady() { - return restoreDone; + return partitions != null; } } From 7519889c1aef0044c49b7d186615bdb0aab4c8da Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 20 Feb 2025 12:25:08 +0200 Subject: [PATCH 25/29] Refactoring for EDQS repartitioning --- .../EdqsEntityQueryControllerTest.java | 4 +- .../server/edqs/processor/EdqsProcessor.java | 22 ++++---- .../server/edqs/state/EdqsController.java | 42 --------------- .../server/edqs/state/EdqsStateService.java | 6 ++- .../edqs/state/KafkaEdqsStateService.java | 53 ++++++------------- .../edqs/state/LocalEdqsStateService.java | 15 ++++-- .../common/msg/queue/TopicPartitionInfo.java | 2 +- .../consumer/MainQueueConsumerManager.java | 3 ++ .../PartitionedQueueConsumerManager.java | 5 -- .../common/consumer/QueueStateService.java | 8 ++- 10 files changed, 57 insertions(+), 103 deletions(-) delete mode 100644 common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 1add0dae37..6833381f2d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -33,8 +33,8 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { -// "queue.type=kafka", // uncomment to use Kafka -// "queue.kafka.bootstrap.servers=10.7.1.254:9092", + "queue.type=kafka", // uncomment to use Kafka + "queue.kafka.bootstrap.servers=192.168.0.105:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index fa1309f932..968e637a3d 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -92,12 +92,11 @@ public class EdqsProcessor implements TbQueueHandler, @Autowired @Lazy private EdqsStateService stateService; - @Getter - private PartitionedQueueConsumerManager> eventsConsumer; + private PartitionedQueueConsumerManager> eventConsumer; private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; private ExecutorService consumersExecutor; - private ExecutorService mgmtExecutor; + private ExecutorService taskExecutor; private ScheduledExecutorService scheduler; private ListeningExecutorService requestExecutor; private ExecutorService repartitionExecutor; @@ -112,7 +111,7 @@ public class EdqsProcessor implements TbQueueHandler, @PostConstruct private void init() { consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); - mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-mgmt"); + taskExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-task-executor"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler"); requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests")); repartitionExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-repartition")); @@ -125,7 +124,7 @@ public class EdqsProcessor implements TbQueueHandler, } }; - eventsConsumer = PartitionedQueueConsumerManager.>create() + eventConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.EVENTS.getTopic())) .topic(EdqsQueue.EVENTS.getTopic()) .pollInterval(config.getPollInterval()) @@ -146,10 +145,12 @@ public class EdqsProcessor implements TbQueueHandler, }) .consumerCreator((config, partitionId) -> queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS)) .consumerExecutor(consumersExecutor) - .taskExecutor(mgmtExecutor) + .taskExecutor(taskExecutor) .scheduler(scheduler) .uncaughtErrorHandler(errorHandler) .build(); + stateService.init(eventConsumer); + responseTemplate = queueFactory.createEdqsResponseTemplate(); } @@ -171,7 +172,7 @@ public class EdqsProcessor implements TbQueueHandler, stateService.process(withTopic(partitions, EdqsQueue.STATE.getTopic())); // eventsConsumer's partitions are updated by stateService - responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); + responseTemplate.subscribe(withTopic(partitions, config.getRequestsTopic())); // FIXME: we subscribe to partitions before we are ready. implement consumer-per-partition version for request template Set oldPartitions = event.getOldPartitions().get(new QueueKey(ServiceType.EDQS)); if (CollectionsUtil.isNotEmpty(oldPartitions)) { @@ -280,12 +281,13 @@ public class EdqsProcessor implements TbQueueHandler, @PreDestroy public void destroy() throws InterruptedException { - eventsConsumer.stop(); - eventsConsumer.awaitStop(); + eventConsumer.stop(); + eventConsumer.awaitStop(); responseTemplate.stop(); + stateService.stop(); consumersExecutor.shutdownNow(); - mgmtExecutor.shutdownNow(); + taskExecutor.shutdownNow(); scheduler.shutdownNow(); requestExecutor.shutdownNow(); repartitionExecutor.shutdownNow(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java deleted file mode 100644 index 06d5f89aa9..0000000000 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsController.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.edqs.state; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequiredArgsConstructor -@ConditionalOnExpression("'${service.type:null}'=='edqs'") -@RequestMapping("/api/edqs") -public class EdqsController { - - private final EdqsStateService edqsStateService; - - @GetMapping("/ready") - public ResponseEntity isReady() { - if (edqsStateService.isReady()) { - return ResponseEntity.ok().build(); - } else { - return ResponseEntity.badRequest().build(); - } - } - -} diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index d45cc0de14..1966618d4b 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -20,15 +20,19 @@ import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; +import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerManager; import java.util.Set; public interface EdqsStateService { + void init(PartitionedQueueConsumerManager> eventConsumer); + void process(Set partitions); void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); - boolean isReady(); + void stop(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index fc7f8d40d9..f7382c9f15 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -15,13 +15,9 @@ */ package org.thingsboard.server.edqs.state; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; import org.thingsboard.server.common.data.id.TenantId; @@ -44,16 +40,13 @@ import org.thingsboard.server.queue.edqs.KafkaEdqsComponent; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.atomic.AtomicInteger; @Service @RequiredArgsConstructor @KafkaEdqsComponent @Slf4j -public class KafkaEdqsStateService extends QueueStateService, TbProtoQueueMsg> implements EdqsStateService { +public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; @@ -61,25 +54,19 @@ public class KafkaEdqsStateService extends QueueStateService> stateConsumer; + private QueueStateService, TbProtoQueueMsg> queueStateService; private QueueConsumerManager> eventsToBackupConsumer; private EdqsProducer stateProducer; - private ExecutorService consumersExecutor; - private ExecutorService mgmtExecutor; - private ScheduledExecutorService scheduler; - private final VersionsStore versionsStore = new VersionsStore(); private final AtomicInteger stateReadCount = new AtomicInteger(); private final AtomicInteger eventsReadCount = new AtomicInteger(); - @PostConstruct - private void init() { - consumersExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("edqs-consumer")); - mgmtExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-backup-consumer-mgmt"); - scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-backup-scheduler"); - - stateConsumer = PartitionedQueueConsumerManager.>create() // FIXME Slavik: if topic is empty + @Override + public void init(PartitionedQueueConsumerManager> eventConsumer) { + stateConsumer = PartitionedQueueConsumerManager.>create() .queueKey(new QueueKey(ServiceType.EDQS, EdqsQueue.STATE.getTopic())) + .topic(EdqsQueue.STATE.getTopic()) .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { @@ -100,12 +87,13 @@ public class KafkaEdqsStateService extends QueueStateService queueFactory.createEdqsMsgConsumer(EdqsQueue.STATE)) - .consumerExecutor(consumersExecutor) - .taskExecutor(mgmtExecutor) - .scheduler(scheduler) + .consumerExecutor(eventConsumer.getConsumerExecutor()) + .taskExecutor(eventConsumer.getTaskExecutor()) + .scheduler(eventConsumer.getScheduler()) .uncaughtErrorHandler(edqsProcessor.getErrorHandler()) .build(); - super.init(stateConsumer, edqsProcessor.getEventsConsumer()); + queueStateService = new QueueStateService<>(); + queueStateService.init(stateConsumer, eventConsumer); eventsToBackupConsumer = QueueConsumerManager.>builder() .name("edqs-events-to-backup-consumer") @@ -145,7 +133,7 @@ public class KafkaEdqsStateService extends QueueStateService queueFactory.createEdqsMsgConsumer(EdqsQueue.EVENTS, "events-to-backup-consumer-group")) // shared by all instances consumer group - .consumerExecutor(consumersExecutor) + .consumerExecutor(eventConsumer.getConsumerExecutor()) .threadPrefix("edqs-events-to-backup") .build(); @@ -158,11 +146,11 @@ public class KafkaEdqsStateService extends QueueStateService partitions) { - if (getPartitions() == null) { + if (queueStateService.getPartitions() == null) { eventsToBackupConsumer.subscribe(); eventsToBackupConsumer.launch(); } - super.update(partitions); + queueStateService.update(partitions); } @Override @@ -170,25 +158,16 @@ public class KafkaEdqsStateService extends QueueStateService> eventConsumer; private Set partitions; + @Override + public void init(PartitionedQueueConsumerManager> eventConsumer) { + this.eventConsumer = eventConsumer; + } + @Override public void process(Set partitions) { - if (this.partitions != null) { + if (this.partitions == null) { db.forEach((key, value) -> { try { ToEdqsMsg edqsMsg = ToEdqsMsg.parseFrom(value); @@ -54,7 +62,7 @@ public class LocalEdqsStateService implements EdqsStateService { } }); } - processor.getEventsConsumer().update(partitions); + eventConsumer.update(partitions); this.partitions = partitions; } @@ -73,8 +81,7 @@ public class LocalEdqsStateService implements EdqsStateService { } @Override - public boolean isReady() { - return partitions != null; + public void stop() { } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index 552bdf50d6..3bb489f209 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -58,7 +58,7 @@ public class TopicPartitionInfo { } public TopicPartitionInfo newByTopic(String topic) { - return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.myPartition); + return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition); } public String getTopic() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index 7bd8076ce9..df683b94cd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -54,8 +54,11 @@ public class MainQueueConsumerManager msgPackProcessor; protected final BiFunction> consumerCreator; + @Getter protected final ExecutorService consumerExecutor; + @Getter protected final ScheduledExecutorService scheduler; + @Getter protected final ExecutorService taskExecutor; protected final Consumer uncaughtErrorHandler; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java index 57f0950cdb..1b47d4c073 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -49,11 +49,6 @@ public class PartitionedQueueConsumerManager extends MainQ this.consumerWrapper = (ConsumerPerPartitionWrapper) super.consumerWrapper; } - @Override - public void update(Set partitions) { - throw new UnsupportedOperationException("Use manual addPartitions and removePartitions"); - } - @Override protected void processTask(TbQueueConsumerManagerTask task) { if (task instanceof AddPartitionsTask addPartitionsTask) { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java index bffe441f7c..d022ad14de 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -41,6 +41,7 @@ public class QueueStateService { } public void update(Set newPartitions) { + newPartitions = withTopic(newPartitions, stateConsumer.getTopic()); lock.lock(); Set oldPartitions = this.partitions != null ? this.partitions : Collections.emptySet(); Set addedPartitions; @@ -54,9 +55,10 @@ public class QueueStateService { } finally { lock.unlock(); } + if (!removedPartitions.isEmpty()) { stateConsumer.removePartitions(removedPartitions); - eventConsumer.removePartitions(removedPartitions.stream().map(tpi -> tpi.withTopic(eventConsumer.getTopic())).collect(Collectors.toSet())); + eventConsumer.removePartitions(withTopic(removedPartitions, eventConsumer.getTopic())); } if (!addedPartitions.isEmpty()) { @@ -73,4 +75,8 @@ public class QueueStateService { } } + private Set withTopic(Set partitions, String topic) { + return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); + } + } From a48144d2bb6e49dbc69c5de0e852216423993a49 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Thu, 20 Feb 2025 17:34:28 +0200 Subject: [PATCH 26/29] EDQS code cleanup --- .../service/edqs/DefaultEdqsService.java | 1 - .../service/edqs/KafkaEdqsSyncService.java | 13 +- .../sync/tenant/TenantExportService.java | 224 ------------------ application/src/main/resources/logback.xml | 1 - .../src/main/resources/thingsboard.yml | 31 ++- .../EdqsEntityQueryControllerTest.java | 4 +- .../resources/application-test.properties | 2 +- .../src/test/resources/logback-test.xml | 2 +- .../server/edqs/util/EdqsRocksDb.java | 2 +- .../server/queue/edqs/EdqsConfig.java | 2 +- edqs/src/main/resources/edqs.yml | 22 +- 11 files changed, 48 insertions(+), 256 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index 5f30bc291a..d5a65d248c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -125,7 +125,6 @@ public class DefaultEdqsService implements EdqsService { executor.submit(() -> { try { EdqsSyncState syncState = getSyncState(); - // FIXME: Slavik smart events check if (edqsSyncService.isSyncNeeded() || syncState == null || syncState.getStatus() != EdqsSyncStatus.FINISHED) { if (hashPartitionService.isSystemPartitionMine(ServiceType.TB_CORE)) { processSystemRequest(ToCoreEdqsRequest.builder() diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index 0c6f948770..8dd4dbe4cb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edqs; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; @@ -30,18 +29,16 @@ import java.util.Collections; @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") public class KafkaEdqsSyncService extends EdqsSyncService { - private final TbKafkaSettings kafkaSettings; - private TbKafkaAdmin kafkaAdmin; + private final boolean syncNeeded; - @PostConstruct - private void init() { - kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); + public KafkaEdqsSyncService(TbKafkaSettings kafkaSettings) { + TbKafkaAdmin kafkaAdmin = new TbKafkaAdmin(kafkaSettings, Collections.emptyMap()); + this.syncNeeded = kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic()); } @Override public boolean isSyncNeeded() { - return kafkaAdmin.isTopicEmpty(EdqsQueue.EVENTS.getTopic()); + return syncNeeded; } - } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java b/application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java deleted file mode 100644 index 25ef73a081..0000000000 --- a/application/src/main/java/org/thingsboard/server/service/sync/tenant/TenantExportService.java +++ /dev/null @@ -1,224 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.service.sync.tenant; - -import jakarta.annotation.PostConstruct; -import lombok.Data; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.AttributeScope; -import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.Tenant; -import org.thingsboard.server.common.data.audit.AuditLog; -import org.thingsboard.server.common.data.event.Event; -import org.thingsboard.server.common.data.event.EventType; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.edqs.AttributeKv; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.edqs.LatestTsKv; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.page.TimePageLink; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.dao.TenantEntityDao; -import org.thingsboard.server.dao.attributes.AttributesDao; -import org.thingsboard.server.dao.audit.AuditLogDao; -import org.thingsboard.server.dao.entity.EntityDaoRegistry; -import org.thingsboard.server.dao.event.EventDao; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.relation.RelationDao; -import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository; -import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.dao.timeseries.TimeseriesLatestDao; - -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.function.BiConsumer; - -import static org.thingsboard.server.common.data.ObjectType.ATTRIBUTE_KV; -import static org.thingsboard.server.common.data.ObjectType.AUDIT_LOG; -import static org.thingsboard.server.common.data.ObjectType.EVENT; -import static org.thingsboard.server.common.data.ObjectType.LATEST_TS_KV; -import static org.thingsboard.server.common.data.ObjectType.RELATION; -import static org.thingsboard.server.common.data.ObjectType.TENANT; - -@Service -@RequiredArgsConstructor -@Slf4j -public class TenantExportService { - - private final EntityDaoRegistry entityDaoRegistry; - private final TenantDao tenantDao; - private final EventDao eventDao; - private final AuditLogDao auditLogDao; - private final AttributesDao attributesDao; - private final RelationDao relationDao; - private final TimeseriesLatestDao timeseriesLatestDao; - private final SqlPartitioningRepository partitioningRepository; - - private Map>> customExporters; - private Map relatedEntitiesExporters; - - private static final Set RELATED = EnumSet.of(EVENT, RELATION, ATTRIBUTE_KV, LATEST_TS_KV); - - @PostConstruct - private void init() { - relatedEntitiesExporters = Map.of( - RELATION, this::exportRelations, - EVENT, this::exportEvents, // todo: query by tenant - ATTRIBUTE_KV, this::exportAttributes, - LATEST_TS_KV, this::exportLatestTelemetry - ); - customExporters = Map.of( - AUDIT_LOG, this::exportAuditLogs - ); - } - - public void exportTenant(TenantId tenantId, ExportConfig config, BiConsumer processor) { - log.info("[{}] Exporting tenant", tenantId); - Tenant tenant = tenantDao.findById(TenantId.SYS_TENANT_ID, tenantId.getId()); - if (tenant == null) { - throw new IllegalArgumentException("Tenant with id " + tenantId + " not found"); - } - - Set objectTypes = config.getIncludedObjectTypes(); - if (objectTypes.contains(TENANT)) { - exportEntity(tenantId, TENANT, tenant, config, processor); - } - - for (ObjectType type : objectTypes) { - if (RELATED.contains(type) || type == TENANT) { - continue; - } - log.debug("[{}] Exporting {} entities", tenantId, type); - if (!customExporters.containsKey(type)) { - TenantEntityDao dao = entityDaoRegistry.getTenantEntityDao(type); - var entities = new PageDataIterable<>(pageLink -> dao.findAllByTenantId(tenantId, pageLink), 100); - for (Object entity : entities) { - exportEntity(tenantId, type, entity, config, processor); - } - } else { - customExporters.get(type).accept(tenantId, processor); - } - } - } - - private void exportEntity(TenantId tenantId, ObjectType type, Object entity, ExportConfig config, BiConsumer processor) { - processor.accept(type, entity); - if (entity instanceof HasId hasId && hasId.getId() instanceof EntityId entityId) { - relatedEntitiesExporters.forEach((relatedEntityType, exporter) -> { - if (config.getIncludedObjectTypes().contains(relatedEntityType)) { - exporter.export(tenantId, entityId, processor); - } - }); - } - } - - private Map getPartitions(String table) { - List partitionsStartTime = partitioningRepository.fetchPartitions(table).stream().sorted().toList(); - if (partitionsStartTime.isEmpty()) { - return Collections.emptyMap(); - } - - Map partitions = new HashMap<>(); - for (int i = 0; i < partitionsStartTime.size(); i++) { - Long startTime = partitionsStartTime.get(i); - Long endTime; - if (partitionsStartTime.size() - 1 == i) { - endTime = System.currentTimeMillis(); - } else { - endTime = partitionsStartTime.get(i + 1) - 1; - } - partitions.put(startTime, endTime); - } - return partitions; - } - - private void exportAuditLogs(TenantId tenantId, BiConsumer processor) { - Map partitions = getPartitions(ModelConstants.AUDIT_LOG_TABLE_NAME); - partitions.forEach((startTime, endTime) -> { - PageDataIterable auditLogs = new PageDataIterable<>(pageLink -> { - return auditLogDao.findAuditLogsByTenantId(tenantId.getId(), null, new TimePageLink(pageLink, startTime, endTime)); - }, 512); - for (AuditLog auditLog : auditLogs) { - processor.accept(AUDIT_LOG, auditLog); - } - }); - } - - private void exportAttributes(TenantId tenantId, EntityId entityId, BiConsumer processor) { - for (AttributeScope attributeScope : AttributeScope.values()) { - List attributes = attributesDao.findAll(tenantId, entityId, attributeScope); - for (AttributeKvEntry entry : attributes) { - AttributeKv attributeKv = new AttributeKv(entityId, attributeScope, entry, entry.getVersion()); - processor.accept(ATTRIBUTE_KV, attributeKv); - } - } - } - - private void exportRelations(TenantId tenantId, EntityId entityId, BiConsumer processor) { - List relations = relationDao.findAllByFrom(tenantId, entityId); - for (EntityRelation relation : relations) { - processor.accept(RELATION, relation); - } - } - - @SneakyThrows - private void exportLatestTelemetry(TenantId tenantId, EntityId entityId, BiConsumer processor) { - List latestTelemetry = timeseriesLatestDao.findAllLatest(tenantId, entityId).get(30, TimeUnit.SECONDS); - for (TsKvEntry tsKvEntry : latestTelemetry) { - LatestTsKv latestTsKv = new LatestTsKv(entityId, tsKvEntry, tsKvEntry.getVersion()); - processor.accept(LATEST_TS_KV, latestTsKv); - } - } - - private void exportEvents(TenantId tenantId, EntityId entityId, BiConsumer processor) { - for (EventType eventType : EventType.values()) { - Map partitions = getPartitions(eventType.getTable()); - partitions.forEach((startTime, endTime) -> { - PageDataIterable events = new PageDataIterable<>(pageLink -> { - return eventDao.findEvents(tenantId.getId(), entityId.getId(), eventType, new TimePageLink(pageLink, startTime, endTime)); - }, 512); - for (Event event : events) { - processor.accept(EVENT, event); - } - }); - } - } - - private interface Exporter { - - void export(TenantId tenantId, EntityId entityId, BiConsumer processor); - - } - - @Data - public static class ExportConfig { - - private Set includedObjectTypes; - - } - -} diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 66d30777cd..7b71c1a272 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -28,7 +28,6 @@ - diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index b20d6f11c3..21459814b7 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1696,22 +1696,35 @@ queue: print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" edqs: sync: - enabled: "${TB_EDQS_SYNC_ENABLED:true}" # Enable/disable EDQS synchronization with postgres db FIXME: disable by default before release - entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # batch size of entities being synced with EDQS - ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" # batch size of timeseries data being synced with EDQS - api_enabled: "${TB_EDQS_API_ENABLED:true}" # FIXME: disable by default before release - mode: "${TB_EDQS_MODE:local}" # local or remote + # Enable/disable EDQS synchronization FIXME: disable by default before release + enabled: "${TB_EDQS_SYNC_ENABLED:true}" + # Batch size of entities being synced with EDQS + entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" + # Batch size of timeseries data being synced with EDQS + ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" + # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) FIXME: disable by default before release + api_enabled: "${TB_EDQS_API_ENABLED:true}" + # Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices) + mode: "${TB_EDQS_MODE:local}" local: - rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:/tmp/edqs-backup}" + # Path to RocksDB for EDQS backup when running in local mode + rocksdb_path: "${TB_EDQS_ROCKSDB_PATH:${user.home}/.rocksdb/edqs}" + # Number of partitions for EDQS topics partitions: "${TB_EDQS_PARTITIONS:12}" - partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data + # EDQS partitioning strategy: tenant (partition is resolved by tenant id) or none (no specific strategy, resolving by message key) + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" + # EDQS requests topic requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" + # EDQS responses topic responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" + # Poll interval for EDQS topics poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" + # Maximum amount of pending requests to EDQS max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" - max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + # Maximum timeout for requests to EDQS + max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" stats: - # Enable/disable statistics for EDQS service + # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index 6833381f2d..efc290d3c5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -33,8 +33,8 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { - "queue.type=kafka", // uncomment to use Kafka - "queue.kafka.bootstrap.servers=192.168.0.105:9092", +// "queue.type=kafka", // uncomment to use Kafka +// "queue.kafka.bootstrap.servers=192.168.0.105:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index 8933c36db5..bf90333c52 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -53,7 +53,7 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 -server.log_controller_error_stack_trace=true +server.log_controller_error_stack_trace=false transport.gateway.dashboard.sync.enabled=false diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index a0efcf52c1..13c93da411 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -9,7 +9,7 @@ - + diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java index 9b91b21c0d..eb3ef42f66 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsRocksDb.java @@ -33,7 +33,7 @@ public class EdqsRocksDb extends TbRocksDb { @Getter private boolean isNew; - public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${java.io.tmpdir}/edqs-backup}") String path) { + public EdqsRocksDb(@Value("${queue.edqs.local.rocksdb_path:${user.home}/.rocksdb/edqs}") String path) { super(path, new Options().setCreateIfMissing(true)); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java index f35827a8ce..8e3bd3b261 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsConfig.java @@ -38,7 +38,7 @@ public class EdqsConfig { private long pollInterval; @Value("${queue.edqs.max_pending_requests:10000}") private int maxPendingRequests; - @Value("${queue.edqs.max_request_timeout:10000}") + @Value("${queue.edqs.max_request_timeout:20000}") private int maxRequestTimeout; public String getLabel() { diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index b67f2f5f9e..fa09da1cdd 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -47,19 +47,27 @@ spring: queue: type: "${TB_QUEUE_TYPE:kafka}" # kafka (Apache Kafka) prefix: "${TB_QUEUE_PREFIX:}" # Global queue prefix. If specified, prefix is added before default topic name: 'prefix.default_topic_name'. Prefix is applied to all topics (and consumer groups for kafka). - in_memory: - stats: - # For debug level - print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}" edqs: - mode: "${TB_EDQS_MODE:local}" + # Number of partitions for EDQS topics partitions: "${TB_EDQS_PARTITIONS:12}" - partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" # tenant or none. For 'none', each instance handles all partitions and duplicates all the data + # EDQS partitioning strategy: tenant (partitions are resolved and distributed by tenant id) or none (partitions are resolved by message key; each instance has all the partitions) + partitioning_strategy: "${TB_EDQS_PARTITIONING_STRATEGY:tenant}" + # EDQS requests topic requests_topic: "${TB_EDQS_REQUESTS_TOPIC:edqs.requests}" + # EDQS responses topic responses_topic: "${TB_EDQS_RESPONSES_TOPIC:edqs.responses}" + # Poll interval for EDQS topics poll_interval: "${TB_EDQS_POLL_INTERVAL_MS:125}" + # Maximum amount of pending requests to EDQS max_pending_requests: "${TB_EDQS_MAX_PENDING_REQUESTS:10000}" - max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:10000}" + # Maximum timeout for requests to EDQS + max_request_timeout: "${TB_EDQS_MAX_REQUEST_TIMEOUT:20000}" + stats: + # Enable/disable statistics for EDQS + enabled: "${TB_EDQS_STATS_ENABLED:true}" + # Statistics printing interval for EDQS + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" + kafka: # Kafka Bootstrap nodes in "host:port" format bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" From 347cb5bc368e344433a5474fde5f5c65205bc89c Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 21 Feb 2025 11:48:21 +0200 Subject: [PATCH 27/29] EDQS code cleanup --- .../service/edqs/DefaultEdqsService.java | 3 + .../service/edqs/KafkaEdqsSyncService.java | 2 - .../server/common/data/ObjectType.java | 9 ++- .../server/common/data/alarm/AlarmType.java | 27 -------- .../data/edqs/fields/GenericFields.java | 16 +---- .../common/data/edqs/fields/TenantFields.java | 3 - .../data/page/BasePageDataIterable.java | 9 +-- .../common/data/page/PageDataIterable.java | 5 -- .../common/data/query/EntityFilter.java | 3 +- .../data/dp/CompressedStringDataPoint.java | 3 +- .../server/edqs/data/dp/JsonDataPoint.java | 2 +- .../server/edqs/data/dp/StringDataPoint.java | 2 +- .../server/edqs/processor/EdqsProcessor.java | 63 ++++++++++-------- .../server/edqs/processor/EdqsProducer.java | 10 +-- ...sitory.java => DefaultEdqsRepository.java} | 16 ++--- ...EdqRepository.java => EdqsRepository.java} | 5 +- .../server/edqs/repo/TenantRepo.java | 1 + .../edqs/state/KafkaEdqsStateService.java | 4 +- .../server/edqs/util/EdqsConverter.java | 5 +- .../edqs/{repo => util}/TbBytePool.java | 2 +- .../edqs/{repo => util}/TbStringPool.java | 2 +- .../queue/edqs/KafkaEdqsQueueFactory.java | 15 +++-- .../provider/KafkaMonolithQueueFactory.java | 2 +- .../provider/KafkaTbCoreQueueFactory.java | 2 +- .../common/util/ExceptionUtil.java | 10 +++ .../server/dao/TenantEntityDao.java | 5 -- .../server/dao/entity/EntityDaoRegistry.java | 18 +---- .../server/dao/model/sql/AlarmTypeEntity.java | 65 ------------------- .../dao/sql/alarm/AlarmTypeRepository.java | 30 --------- .../dao/sql/alarm/JpaAlarmCommentDao.java | 6 -- .../server/dao/sql/alarm/JpaAlarmDao.java | 6 -- .../server/dao/sql/alarm/JpaAlarmTypeDao.java | 46 ------------- .../dao/sql/alarm/JpaEntityAlarmDao.java | 6 -- .../server/dao/sql/asset/JpaAssetDao.java | 6 -- .../dao/sql/asset/JpaAssetProfileDao.java | 6 -- .../dao/sql/customer/JpaCustomerDao.java | 6 -- .../dao/sql/dashboard/JpaDashboardDao.java | 6 -- .../sql/device/DeviceProfileRepository.java | 1 - .../sql/device/JpaDeviceCredentialsDao.java | 6 -- .../server/dao/sql/device/JpaDeviceDao.java | 6 -- .../dao/sql/device/JpaDeviceProfileDao.java | 6 -- .../server/dao/sql/edge/JpaEdgeDao.java | 6 -- .../sql/entityview/EntityViewRepository.java | 1 - .../dao/sql/entityview/JpaEntityViewDao.java | 6 -- .../notification/JpaNotificationRuleDao.java | 6 -- .../JpaNotificationTargetDao.java | 6 -- .../JpaNotificationTemplateDao.java | 6 -- .../server/dao/sql/ota/JpaOtaPackageDao.java | 6 -- .../server/dao/sql/queue/JpaQueueDao.java | 6 -- .../dao/sql/queue/JpaQueueStatsDao.java | 6 -- .../dao/sql/resource/JpaTbResourceDao.java | 6 -- .../server/dao/sql/rpc/JpaRpcDao.java | 6 -- .../server/dao/sql/rule/JpaRuleChainDao.java | 6 -- .../server/dao/sql/rule/JpaRuleNodeDao.java | 6 -- .../dao/sql/settings/JpaAdminSettingsDao.java | 6 -- .../sql/usagerecord/JpaApiUsageStateDao.java | 6 -- .../dao/sql/user/JpaUserAuthSettingsDao.java | 6 -- .../dao/sql/user/JpaUserCredentialsDao.java | 6 -- .../server/dao/sql/user/JpaUserDao.java | 6 -- .../dao/sql/user/JpaUserSettingsDao.java | 6 -- .../dao/sql/widget/JpaWidgetTypeDao.java | 6 -- .../dao/sql/widget/JpaWidgetsBundleDao.java | 6 -- .../dao/service/EntityDaoRegistryTest.java | 31 --------- edqs/src/main/resources/logback.xml | 1 - .../server/edqs/repo/AbstractEDQTest.java | 4 +- ...q-test.properties => edqs-test.properties} | 0 66 files changed, 99 insertions(+), 504 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java rename common/edqs/src/main/java/org/thingsboard/server/edqs/repo/{InMemoryEdqRepository.java => DefaultEdqsRepository.java} (89%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/repo/{EdqRepository.java => EdqsRepository.java} (92%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/{repo => util}/TbBytePool.java (96%) rename common/edqs/src/main/java/org/thingsboard/server/edqs/{repo => util}/TbStringPool.java (96%) delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java delete mode 100644 dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java rename edqs/src/test/resources/{edq-test.properties => edqs-test.properties} (100%) diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java index d5a65d248c..a6a9b9bb23 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/DefaultEdqsService.java @@ -68,6 +68,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueRequestTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.HashPartitionService; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.environment.DistributedLock; import org.thingsboard.server.queue.environment.DistributedLockService; @@ -90,6 +91,7 @@ public class DefaultEdqsService implements EdqsService { private final DistributedLockService distributedLockService; private final AttributesService attributesService; private final EdqsPartitionService edqsPartitionService; + private final TopicService topicService; @Autowired @Lazy private TbClusterService clusterService; @Autowired @Lazy @@ -109,6 +111,7 @@ public class DefaultEdqsService implements EdqsService { eventsProducer = EdqsProducer.builder() .queue(EdqsQueue.EVENTS) .partitionService(edqsPartitionService) + .topicService(topicService) .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.EVENTS)) .build(); if (apiEnabled) { diff --git a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java index 8dd4dbe4cb..7dc25b92c8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java +++ b/application/src/main/java/org/thingsboard/server/service/edqs/KafkaEdqsSyncService.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.service.edqs; -import lombok.RequiredArgsConstructor; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; import org.thingsboard.server.queue.edqs.EdqsQueue; @@ -25,7 +24,6 @@ import org.thingsboard.server.queue.kafka.TbKafkaSettings; import java.util.Collections; @Service -@RequiredArgsConstructor @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}' == 'true' && '${queue.type:null}' == 'kafka'") public class KafkaEdqsSyncService extends EdqsSyncService { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java index 86252fb7f8..8fa08d38e9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ObjectType.java @@ -23,7 +23,6 @@ public enum ObjectType { TENANT, TENANT_PROFILE, CUSTOMER, - ADMIN_SETTINGS, QUEUE, RPC, RULE_CHAIN, @@ -32,8 +31,6 @@ public enum ObjectType { EVENT, RULE_NODE, USER, - USER_CREDENTIALS, - USER_AUTH_SETTINGS, EDGE, WIDGETS_BUNDLE, WIDGET_TYPE, @@ -54,7 +51,6 @@ public enum ObjectType { NOTIFICATION_TEMPLATE, NOTIFICATION_RULE, ALARM_COMMENT, - ALARM_TYPE, API_USAGE_STATE, QUEUE_STATS, @@ -70,7 +66,10 @@ public enum ObjectType { public static final Set edqsTypes = EnumSet.copyOf(edqsTenantTypes); public static final Set edqsSystemTypes = EnumSet.of(TENANT, TENANT_PROFILE, USER, DASHBOARD, API_USAGE_STATE, ATTRIBUTE_KV, LATEST_TS_KV); - public static final Set unversionedTypes = EnumSet.of(QUEUE_STATS); + public static final Set unversionedTypes = EnumSet.of( + QUEUE_STATS, // created once, never updated + TENANT_PROFILE // only for total count calculation + ); static { edqsTypes.addAll(List.of(RELATION, ATTRIBUTE_KV, LATEST_TS_KV)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java deleted file mode 100644 index 0813e72123..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmType.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.common.data.alarm; - -import lombok.Data; -import org.thingsboard.server.common.data.id.TenantId; - -@Data -public class AlarmType { - - private TenantId tenantId; - private String type; - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java index ff3af4ee65..94e8e35dcc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/GenericFields.java @@ -15,27 +15,15 @@ */ package org.thingsboard.server.common.data.edqs.fields; -import com.fasterxml.jackson.databind.JsonNode; -import lombok.Data; import lombok.NoArgsConstructor; -import lombok.experimental.SuperBuilder; -import java.util.UUID; -import static org.thingsboard.server.common.data.edqs.fields.FieldsUtil.getText; +import java.util.UUID; -@Data @NoArgsConstructor -@SuperBuilder public class GenericFields extends AbstractEntityFields { - private String additionalInfo; - - public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version, JsonNode additionalInfo) { - super(id, createdTime, tenantId, name, version); - this.additionalInfo = getText(additionalInfo); - } - public GenericFields(UUID id, long createdTime, UUID tenantId, String name, Long version) { super(id, createdTime, tenantId, name, version); } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java index 6942d5ea7b..c11c221a9b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/TenantFields.java @@ -56,7 +56,4 @@ public class TenantFields extends AbstractEntityFields { this.region = region; } - public TenantFields(UUID id, Long version) { - super(id, 0L, null, version); - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java index 625a1d417c..8d46a46781 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageDataIterable.java @@ -23,7 +23,6 @@ import java.util.NoSuchElementException; public abstract class BasePageDataIterable implements Iterable, Iterator { private final int fetchSize; - private SortOrder sortOrder; private List currentItems; private int currentIdx; @@ -36,12 +35,6 @@ public abstract class BasePageDataIterable implements Iterable, Iterator iterator() { return this; @@ -50,7 +43,7 @@ public abstract class BasePageDataIterable implements Iterable, Iterator extends BasePageDataIterable { this.function = function; } - public PageDataIterable(FetchFunction function, int fetchSize, SortOrder sortOrder) { - super(fetchSize, sortOrder); - this.function = function; - } - @Override PageData fetchPageData(PageLink link) { return function.fetch(link); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java index f881ed15c3..8e45e20efa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityFilter.java @@ -39,7 +39,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = AssetSearchQueryFilter.class, name = "assetSearchQuery"), @JsonSubTypes.Type(value = DeviceSearchQueryFilter.class, name = "deviceSearchQuery"), @JsonSubTypes.Type(value = EntityViewSearchQueryFilter.class, name = "entityViewSearchQuery"), - @JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery")}) + @JsonSubTypes.Type(value = EdgeSearchQueryFilter.class, name = "edgeSearchQuery") +}) public interface EntityFilter { @JsonIgnore diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java index 0502bbccda..0ef614de06 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/CompressedStringDataPoint.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import lombok.SneakyThrows; import org.thingsboard.server.common.data.kv.DataType; +import org.thingsboard.server.edqs.util.TbBytePool; import org.xerial.snappy.Snappy; public class CompressedStringDataPoint extends AbstractDataPoint { @@ -29,7 +30,7 @@ public class CompressedStringDataPoint extends AbstractDataPoint { @SneakyThrows public CompressedStringDataPoint(long ts, byte[] compressedValue) { super(ts); - this.compressedValue = compressedValue; + this.compressedValue = TbBytePool.intern(compressedValue); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java index 05cafa3edb..739cd1902e 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/JsonDataPoint.java @@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.edqs.repo.TbStringPool; +import org.thingsboard.server.edqs.util.TbStringPool; public class JsonDataPoint extends AbstractDataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java index fabf15a359..281c467c31 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/dp/StringDataPoint.java @@ -17,7 +17,7 @@ package org.thingsboard.server.edqs.data.dp; import lombok.Getter; import org.thingsboard.server.common.data.kv.DataType; -import org.thingsboard.server.edqs.repo.TbStringPool; +import org.thingsboard.server.edqs.util.TbStringPool; public class StringDataPoint extends AbstractDataPoint { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 968e637a3d..42e00be7fc 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -24,12 +24,12 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.ExceptionUtil; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.util.CollectionsUtil; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; -import org.thingsboard.server.edqs.repo.EdqRepository; +import org.thingsboard.server.edqs.repo.EdqsRepository; import org.thingsboard.server.edqs.state.EdqsPartitionService; import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsConverter; @@ -68,6 +68,7 @@ import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.EdqsQueueFactory; import org.thingsboard.server.queue.util.AfterStartUp; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutorService; @@ -85,7 +86,7 @@ public class EdqsProcessor implements TbQueueHandler, private final EdqsQueueFactory queueFactory; private final EdqsConverter converter; - private final EdqRepository repository; + private final EdqsRepository repository; private final EdqsConfig config; private final EdqsPartitionService partitionService; private final ConfigurableApplicationContext applicationContext; @@ -99,11 +100,10 @@ public class EdqsProcessor implements TbQueueHandler, private ExecutorService taskExecutor; private ScheduledExecutorService scheduler; private ListeningExecutorService requestExecutor; - private ExecutorService repartitionExecutor; private final VersionsStore versionsStore = new VersionsStore(); - private final AtomicInteger counter = new AtomicInteger(); // FIXME: TMP + private final AtomicInteger counter = new AtomicInteger(); @Getter private Consumer errorHandler; @@ -114,7 +114,6 @@ public class EdqsProcessor implements TbQueueHandler, taskExecutor = ThingsBoardExecutors.newWorkStealingPool(4, "edqs-consumer-task-executor"); scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edqs-scheduler"); requestExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(12, "edqs-requests")); - repartitionExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("edqs-repartition")); errorHandler = error -> { if (error instanceof OutOfMemoryError) { log.error("OOM detected, shutting down"); @@ -135,7 +134,6 @@ public class EdqsProcessor implements TbQueueHandler, } try { ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); process(msg, EdqsQueue.EVENTS); } catch (Exception t) { log.error("Failed to process message: {}", queueMsg, t); @@ -195,27 +193,19 @@ public class EdqsProcessor implements TbQueueHandler, public ListenableFuture> handle(TbProtoQueueMsg queueMsg) { ToEdqsMsg toEdqsMsg = queueMsg.getValue(); return requestExecutor.submit(() -> { - EdqsResponse response = new EdqsResponse(); + EdqsRequest request; + TenantId tenantId; + CustomerId customerId; try { - EdqsRequest request = JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class); - TenantId tenantId = getTenantId(toEdqsMsg); - CustomerId customerId = getCustomerId(toEdqsMsg); - log.info("[{}] Handling request: {}", tenantId, request); - - if (request.getEntityDataQuery() != null) { - PageData result = repository.findEntityDataByQuery(tenantId, customerId, - request.getEntityDataQuery(), false); - response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData)); - } else if (request.getEntityCountQuery() != null) { - long result = repository.countEntitiesByQuery(tenantId, customerId, request.getEntityCountQuery(), tenantId.isSysTenantId()); - response.setEntityCountQueryResult(result); - } - - log.info("Answering with response: {}", response); - } catch (Throwable e) { - response.setError(ExceptionUtils.getStackTrace(e)); // TODO: return only the message - log.info("Answering with error", e); + request = Objects.requireNonNull(JacksonUtil.fromString(toEdqsMsg.getRequestMsg().getValue(), EdqsRequest.class)); + tenantId = getTenantId(toEdqsMsg); + customerId = getCustomerId(toEdqsMsg); + } catch (Exception e) { + log.error("Failed to parse request msg: {}", toEdqsMsg, e); + throw e; } + + EdqsResponse response = processRequest(tenantId, customerId, request); return new TbProtoQueueMsg<>(queueMsg.getKey(), FromEdqsMsg.newBuilder() .setResponseMsg(TransportProtos.EdqsResponseMsg.newBuilder() .setValue(JacksonUtil.toString(response)) @@ -224,7 +214,27 @@ public class EdqsProcessor implements TbQueueHandler, }); } + private EdqsResponse processRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { + EdqsResponse response = new EdqsResponse(); + try { + if (request.getEntityDataQuery() != null) { + PageData result = repository.findEntityDataByQuery(tenantId, customerId, + request.getEntityDataQuery(), false); + response.setEntityDataQueryResult(result.mapData(QueryResult::toOldEntityData)); + } else if (request.getEntityCountQuery() != null) { + long result = repository.countEntitiesByQuery(tenantId, customerId, request.getEntityCountQuery(), tenantId.isSysTenantId()); + response.setEntityCountQueryResult(result); + } + log.trace("[{}] Request: {}, response: {}", tenantId, request, response); + } catch (Throwable e) { + log.error("[{}] Failed to process request: {}", tenantId, request, e); + response.setError(ExceptionUtil.getMessage(e)); + } + return response; + } + public void process(ToEdqsMsg edqsMsg, EdqsQueue queue) { + log.trace("Processing message: {}", edqsMsg); if (edqsMsg.hasEventMsg()) { EdqsEventMsg eventMsg = edqsMsg.getEventMsg(); TenantId tenantId = getTenantId(edqsMsg); @@ -290,7 +300,6 @@ public class EdqsProcessor implements TbQueueHandler, taskExecutor.shutdownNow(); scheduler.shutdownNow(); requestExecutor.shutdownNow(); - repartitionExecutor.shutdownNow(); } } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java index bfd0d9df59..9ea027ec10 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProducer.java @@ -27,6 +27,7 @@ import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; import org.thingsboard.server.queue.TbQueueProducer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.kafka.TbKafkaProducerTemplate; @@ -35,26 +36,27 @@ public class EdqsProducer { private final EdqsQueue queue; private final EdqsPartitionService partitionService; + private final TopicService topicService; private final TbQueueProducer> producer; @Builder public EdqsProducer(EdqsQueue queue, EdqsPartitionService partitionService, + TopicService topicService, TbQueueProducer> producer) { this.queue = queue; this.partitionService = partitionService; + this.topicService = topicService; this.producer = producer; } - // TODO: queue prefix! - public void send(TenantId tenantId, ObjectType type, String key, ToEdqsMsg msg) { - String topic = queue.getTopic(); + String topic = topicService.buildTopicName(queue.getTopic()); TbQueueCallback callback = new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { - log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); // fixme log levels + log.trace("[{}][{}][{}] Published msg to {}: {}", tenantId, type, key, topic, msg); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java similarity index 89% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java index 7736f303ac..bdfe76552a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/InMemoryEdqRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/DefaultEdqsRepository.java @@ -39,7 +39,7 @@ import java.util.function.Predicate; @AllArgsConstructor @Service @Slf4j -public class InMemoryEdqRepository implements EdqRepository { +public class DefaultEdqsRepository implements EdqsRepository { private final static ConcurrentMap repos = new ConcurrentHashMap<>(); private final Optional statsService; @@ -51,7 +51,7 @@ public class InMemoryEdqRepository implements EdqRepository { @Override public void processEvent(EdqsEvent event) { if (event.getEventType() == EdqsEventType.DELETED && event.getObjectType() == ObjectType.TENANT) { - log.info("Deleting tenant repo: {}", event); + log.info("Tenant {} deleted", event.getTenantId()); repos.remove(event.getTenantId()); } else { get(event.getTenantId()).processEvent(event); @@ -62,25 +62,25 @@ public class InMemoryEdqRepository implements EdqRepository { public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck) { long startNs = System.nanoTime(); long result = 0; - if (TenantId.SYS_TENANT_ID.equals(tenantId)) { + if (!tenantId.isSysTenantId()) { + result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck); + } else { for (TenantRepo repo : repos.values()) { result += repo.countEntitiesByQuery(customerId, query, ignorePermissionCheck); } - } else { - result = get(tenantId).countEntitiesByQuery(customerId, query, ignorePermissionCheck); } double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; - log.info("countEntitiesByQuery: {} ms", timingMs); + log.info("countEntitiesByQuery done in {} ms", timingMs); return result; } @Override public PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, - EntityDataQuery query, boolean ignorePermissionCheck) { + EntityDataQuery query, boolean ignorePermissionCheck) { long startNs = System.nanoTime(); var result = get(tenantId).findEntityDataByQuery(customerId, query, ignorePermissionCheck); double timingMs = (double) (System.nanoTime() - startNs) / 1000_000; - log.info("findEntityDataByQuery: {} ms", timingMs); + log.info("findEntityDataByQuery done in {} ms", timingMs); return result; } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java similarity index 92% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java index 231efd9a81..b1c72b1483 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqRepository.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/EdqsRepository.java @@ -25,13 +25,10 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import java.util.function.Predicate; -public interface EdqRepository { +public interface EdqsRepository { void processEvent(EdqsEvent event); - @Deprecated - default void addOrUpdate(TenantId tenantId, Object object) {} - long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query, boolean ignorePermissionCheck); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query, boolean ignorePermissionCheck); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 9d6a4ab135..043c0bd6c3 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -57,6 +57,7 @@ import org.thingsboard.server.edqs.query.processor.EntityQueryProcessor; import org.thingsboard.server.edqs.query.processor.EntityQueryProcessorFactory; import org.thingsboard.server.edqs.stats.EdqsStatsService; import org.thingsboard.server.edqs.util.RepositoryUtils; +import org.thingsboard.server.edqs.util.TbStringPool; import java.util.ArrayList; import java.util.Collections; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index f7382c9f15..f5ba2ba841 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -33,6 +33,7 @@ import org.thingsboard.server.queue.common.consumer.PartitionedQueueConsumerMana import org.thingsboard.server.queue.common.consumer.QueueConsumerManager; import org.thingsboard.server.queue.common.consumer.QueueStateService; import org.thingsboard.server.queue.discovery.QueueKey; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.edqs.EdqsConfig; import org.thingsboard.server.queue.edqs.EdqsQueue; import org.thingsboard.server.queue.edqs.EdqsQueueFactory; @@ -52,6 +53,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; private final EdqsProcessor edqsProcessor; + private final TopicService topicService; private PartitionedQueueConsumerManager> stateConsumer; private QueueStateService, TbProtoQueueMsg> queueStateService; @@ -75,7 +77,6 @@ public class KafkaEdqsStateService implements EdqsStateService { } try { ToEdqsMsg msg = queueMsg.getValue(); - log.trace("Processing message: {}", msg); edqsProcessor.process(msg, EdqsQueue.STATE); if (stateReadCount.incrementAndGet() % 100000 == 0) { log.info("[state] Processed {} msgs", stateReadCount.get()); @@ -140,6 +141,7 @@ public class KafkaEdqsStateService implements EdqsStateService { stateProducer = EdqsProducer.builder() .queue(EdqsQueue.STATE) .partitionService(partitionService) + .topicService(topicService) .producer(queueFactory.createEdqsMsgProducer(EdqsQueue.STATE)) .build(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java index b7419451b9..817b0df4f1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/EdqsConverter.java @@ -42,7 +42,6 @@ import org.thingsboard.server.edqs.data.dp.DoubleDataPoint; import org.thingsboard.server.edqs.data.dp.JsonDataPoint; import org.thingsboard.server.edqs.data.dp.LongDataPoint; import org.thingsboard.server.edqs.data.dp.StringDataPoint; -import org.thingsboard.server.edqs.repo.TbBytePool; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DataPointProto; import org.xerial.snappy.Snappy; @@ -161,11 +160,11 @@ public class EdqsConverter { } else if (proto.hasStringV()) { return new StringDataPoint(ts, proto.getStringV()); } else if (proto.hasCompressedStringV()) { - return new CompressedStringDataPoint(ts, TbBytePool.intern(proto.getCompressedStringV().toByteArray())); + return new CompressedStringDataPoint(ts, proto.getCompressedStringV().toByteArray()); } else if (proto.hasJsonV()) { return new JsonDataPoint(ts, proto.getJsonV()); } else if (proto.hasCompressedJsonV()) { - return new CompressedJsonDataPoint(ts, TbBytePool.intern(proto.getCompressedJsonV().toByteArray())); + return new CompressedJsonDataPoint(ts, proto.getCompressedJsonV().toByteArray()); } else { throw new IllegalArgumentException("Unsupported data point proto: " + proto); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java similarity index 96% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java index a284590161..a5a5394910 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbBytePool.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbBytePool.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.repo; +package org.thingsboard.server.edqs.util; import com.google.common.hash.Hashing; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java similarity index 96% rename from common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java rename to common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java index ceb15affdf..151d6bb1b4 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TbStringPool.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/util/TbStringPool.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.edqs.repo; +package org.thingsboard.server.edqs.util; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java index 4b599670a1..9f973ce3f1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -27,6 +27,7 @@ import org.thingsboard.server.queue.TbQueueResponseTemplate; import org.thingsboard.server.queue.common.DefaultTbQueueResponseTemplate; import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; +import org.thingsboard.server.queue.discovery.TopicService; import org.thingsboard.server.queue.kafka.TbKafkaAdmin; import org.thingsboard.server.queue.kafka.TbKafkaConsumerStatsService; import org.thingsboard.server.queue.kafka.TbKafkaConsumerTemplate; @@ -47,12 +48,13 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { private final EdqsConfig edqsConfig; private final TbServiceInfoProvider serviceInfoProvider; private final TbKafkaConsumerStatsService consumerStatsService; + private final TopicService topicService; private final AtomicInteger consumerCounter = new AtomicInteger(); public KafkaEdqsQueueFactory(TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs topicConfigs, EdqsConfig edqsConfig, TbServiceInfoProvider serviceInfoProvider, - TbKafkaConsumerStatsService consumerStatsService) { + TbKafkaConsumerStatsService consumerStatsService, TopicService topicService) { this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsEventsConfigs()); this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsRequestsConfigs()); this.edqsStateAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsStateConfigs()); @@ -60,6 +62,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { this.edqsConfig = edqsConfig; this.serviceInfoProvider = serviceInfoProvider; this.consumerStatsService = consumerStatsService; + this.topicService = topicService; } @Override @@ -72,11 +75,11 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue, String group) { return TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(queue.getTopic()) + .topic(topicService.buildTopicName(queue.getTopic())) .readFromBeginning(queue.isReadFromBeginning()) .stopWhenRead(queue.isStopWhenRead()) .clientId("edqs-" + queue.name().toLowerCase() + "-" + consumerCounter.getAndIncrement() + "-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(group) + .groupId(topicService.buildTopicName(group)) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(queue == EdqsQueue.STATE ? edqsStateAdmin : edqsEventsAdmin) .statsService(consumerStatsService) @@ -97,16 +100,16 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { String requestsConsumerGroup = "edqs-requests-consumer-group-" + edqsConfig.getLabel(); var requestConsumer = TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) - .topic(edqsConfig.getRequestsTopic()) + .topic(topicService.buildTopicName(edqsConfig.getRequestsTopic())) .clientId("edqs-requests-consumer-" + serviceInfoProvider.getServiceId()) - .groupId(requestsConsumerGroup) + .groupId(topicService.buildTopicName(requestsConsumerGroup)) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(edqsRequestsAdmin) .statsService(consumerStatsService); var responseProducer = TbKafkaProducerTemplate.>builder() .settings(kafkaSettings) .clientId("edqs-response-producer-" + serviceInfoProvider.getServiceId()) - .defaultTopic(edqsConfig.getResponsesTopic()) + .defaultTopic(topicService.buildTopicName(edqsConfig.getResponsesTopic())) .admin(edqsRequestsAdmin); return DefaultTbQueueResponseTemplate., TbProtoQueueMsg>builder() .requestTemplate(requestConsumer.build()) 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 e5b981bf5d..4acb5d94ae 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 @@ -522,7 +522,7 @@ public class KafkaMonolithQueueFactory implements TbCoreQueueFactory, TbRuleEngi var responseConsumer = TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) .topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId())) - .clientId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .clientId("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("monolith-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(edqsRequestsAdmin) 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 4e12696d3e..646cdc676a 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 @@ -471,7 +471,7 @@ public class KafkaTbCoreQueueFactory implements TbCoreQueueFactory { var responseConsumer = TbKafkaConsumerTemplate.>builder() .settings(kafkaSettings) .topic(topicService.buildTopicName(edqsConfig.getResponsesTopic() + "." + serviceInfoProvider.getServiceId())) - .clientId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) + .clientId("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId()) .groupId(topicService.buildTopicName("tb-core-edqs-response-consumer-" + serviceInfoProvider.getServiceId())) .decoder(msg -> new TbProtoQueueMsg<>(msg.getKey(), FromEdqsMsg.parseFrom(msg.getData()), msg.getHeaders())) .admin(edqsRequestsAdmin) diff --git a/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java b/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java index ace7d1540b..21b20fec7b 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ExceptionUtil.java @@ -64,4 +64,14 @@ public class ExceptionUtil { } } } + + public static String getMessage(Throwable t) { + String message = t.getMessage(); + if (StringUtils.isNotEmpty(message)) { + return message; + } else { + return t.getClass().getSimpleName(); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java b/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java index 19fe45ed88..f6391fd26f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/TenantEntityDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -30,8 +29,4 @@ public interface TenantEntityDao { throw new UnsupportedOperationException(); } - default ObjectType getType() { - throw new UnsupportedOperationException(); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java index 24f8aa4d77..86887e94ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/EntityDaoRegistry.java @@ -18,9 +18,7 @@ package org.thingsboard.server.dao.entity; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.dao.Dao; -import org.thingsboard.server.dao.TenantEntityDao; import java.util.EnumMap; import java.util.List; @@ -31,20 +29,14 @@ import java.util.Map; @SuppressWarnings({"unchecked"}) public class EntityDaoRegistry { - private final Map> tenantEntityDaos = new EnumMap<>(ObjectType.class); private final Map> entityDaos = new EnumMap<>(EntityType.class); - private EntityDaoRegistry(List> entityDaos, List> tenantEntityDaos) { + private EntityDaoRegistry(List> entityDaos) { entityDaos.forEach(dao -> { if (dao.getEntityType() != null) { this.entityDaos.put(dao.getEntityType(), dao); } }); - tenantEntityDaos.forEach(dao -> { - if (dao.getType() != null) { - this.tenantEntityDaos.put(dao.getType(), dao); - } - }); } public Dao getDao(EntityType entityType) { @@ -55,12 +47,4 @@ public class EntityDaoRegistry { return dao; } - public TenantEntityDao getTenantEntityDao(ObjectType objectType) { - TenantEntityDao dao = tenantEntityDaos.get(objectType); - if (dao == null) { - throw new IllegalArgumentException("Missing tenant entity dao for entity type " + objectType); - } - return (TenantEntityDao) dao; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java deleted file mode 100644 index 729d7a44f0..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmTypeEntity.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.dao.model.sql; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.IdClass; -import jakarta.persistence.Table; -import lombok.Data; -import org.thingsboard.server.common.data.alarm.AlarmType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.model.ModelConstants; -import org.thingsboard.server.dao.model.ToData; - -import java.util.UUID; - -@Data -@Entity -@Table(name = ModelConstants.ALARM_TYPES_TABLE_NAME) -@IdClass(AlarmTypeCompositeKey.class) -public class AlarmTypeEntity implements ToData { - - @Id - @Column(name = ModelConstants.TENANT_ID_PROPERTY, nullable = false) - private UUID tenantId; - - @Id - @Column(name = ModelConstants.ALARM_TYPE_PROPERTY, nullable = false) - private String type; - - public AlarmTypeEntity() {} - - public AlarmTypeEntity(AlarmType alarmType) { - setTenantId(alarmType.getTenantId().getId()); - setType(alarmType.getType()); - } - - public AlarmTypeEntity(UUID tenantId, String type) { - this.tenantId = tenantId; - this.type = type; - } - - @Override - public AlarmType toData() { - AlarmType alarmType = new AlarmType(); - alarmType.setTenantId(TenantId.fromUUID(tenantId)); - alarmType.setType(type); - return alarmType; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java deleted file mode 100644 index b84542e3c3..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmTypeRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.dao.sql.alarm; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.thingsboard.server.dao.model.sql.AlarmTypeCompositeKey; -import org.thingsboard.server.dao.model.sql.AlarmTypeEntity; - -import java.util.UUID; - -public interface AlarmTypeRepository extends JpaRepository { - - Page findByTenantId(UUID tenantId, Pageable pageable); - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java index 9d74b9bf8e..f6d85fc762 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDao.java @@ -22,7 +22,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentInfo; import org.thingsboard.server.common.data.id.AlarmId; @@ -93,9 +92,4 @@ public class JpaAlarmCommentDao extends JpaPartitionedAbstractDao implements A return EntityType.ALARM; } - @Override - public ObjectType getType() { - return ObjectType.ALARM; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java deleted file mode 100644 index 532f06eb03..0000000000 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmTypeDao.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * 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. - */ -package org.thingsboard.server.dao.sql.alarm; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; -import org.thingsboard.server.common.data.alarm.AlarmType; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.dao.DaoUtil; -import org.thingsboard.server.dao.TenantEntityDao; -import org.thingsboard.server.dao.util.SqlDao; - -@Component -@SqlDao -public class JpaAlarmTypeDao implements TenantEntityDao { - - @Autowired - private AlarmTypeRepository alarmTypeRepository; - - @Override - public PageData findAllByTenantId(TenantId tenantId, PageLink pageLink) { - return DaoUtil.toPageData(alarmTypeRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink, "tenantId", "type"))); - } - - @Override - public ObjectType getType() { - return ObjectType.ALARM_TYPE; - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java index 6bbc59559a..ebffd50656 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaEntityAlarmDao.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -38,9 +37,4 @@ public class JpaEntityAlarmDao implements TenantEntityDao { return DaoUtil.toPageData(entityAlarmRepository.findByTenantId(tenantId.getId(), DaoUtil.toPageable(pageLink, "entityId", "alarmId"))); } - @Override - public ObjectType getType() { - return ObjectType.ENTITY_ALARM; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java index 176eb0856a..721f46901a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetDao.java @@ -23,7 +23,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.edqs.fields.AssetFields; @@ -285,9 +284,4 @@ public class JpaAssetDao extends JpaAbstractDao implements A return EntityType.ASSET; } - @Override - public ObjectType getType() { - return ObjectType.ASSET; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java index d43871823e..55f72d9682 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/asset/JpaAssetProfileDao.java @@ -22,7 +22,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.asset.AssetProfileInfo; import org.thingsboard.server.common.data.edqs.fields.AssetProfileFields; @@ -157,9 +156,4 @@ public class JpaAssetProfileDao extends JpaAbstractDao imp return EntityType.CUSTOMER; } - @Override - public ObjectType getType() { - return ObjectType.CUSTOMER; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java index 9422f38aae..1431837864 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardDao.java @@ -21,7 +21,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.DashboardFields; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; @@ -108,9 +107,4 @@ public class JpaDashboardDao extends JpaAbstractDao return EntityType.DASHBOARD; } - @Override - public ObjectType getType() { - return ObjectType.DASHBOARD; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java index 18f7f35710..4b87f0274d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/DeviceProfileRepository.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; -import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.DeviceProfileEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java index 31d2aa6cb9..25b996eb57 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceCredentialsDao.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -76,9 +75,4 @@ public class JpaDeviceCredentialsDao extends JpaAbstractDao implement return EntityType.DEVICE; } - @Override - public ObjectType getType() { - return ObjectType.DEVICE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index c4d48dff35..8f7dcfb916 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.DeviceProfileFields; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -175,9 +174,4 @@ public class JpaDeviceProfileDao extends JpaAbstractDao implements Edge return EntityType.EDGE; } - @Override - public ObjectType getType() { - return ObjectType.EDGE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java index b18fd58248..3cc212497b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/EntityViewRepository.java @@ -22,7 +22,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; -import org.thingsboard.server.common.data.edqs.fields.GenericFields; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.EntityViewEntity; import org.thingsboard.server.dao.model.sql.EntityViewInfoEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java index 4c30e26f05..4172af056f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/entityview/JpaEntityViewDao.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.EntityViewFields; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; @@ -236,9 +235,4 @@ public class JpaEntityViewDao extends JpaAbstractDao implements Q return EntityType.QUEUE; } - @Override - public ObjectType getType() { - return ObjectType.QUEUE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java index 41e2de02a6..3e8f2c11ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueStatsDao.java @@ -21,7 +21,6 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.QueueStatsFields; import org.thingsboard.server.common.data.id.QueueStatsId; import org.thingsboard.server.common.data.id.TenantId; @@ -67,11 +66,6 @@ public class JpaQueueStatsDao extends JpaAbstractDao implements RpcDao, return EntityType.RPC; } - @Override - public ObjectType getType() { - return ObjectType.RPC; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index ad0d596474..d5cd046e98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -21,7 +21,6 @@ import org.springframework.data.domain.Limit; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; @@ -152,9 +151,4 @@ public class JpaRuleChainDao extends JpaAbstractDao return EntityType.RULE_CHAIN; } - @Override - public ObjectType getType() { - return ObjectType.RULE_CHAIN; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java index 307f15c7f4..43087b8652 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java @@ -20,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -118,9 +117,4 @@ public class JpaRuleNodeDao extends JpaAbstractDao imp return EntityType.RULE_NODE; } - @Override - public ObjectType getType() { - return ObjectType.RULE_NODE; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java index 9e0f6a70cb..6a4ae62b48 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/settings/JpaAdminSettingsDao.java @@ -21,7 +21,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.common.data.AdminSettings; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -78,9 +77,4 @@ public class JpaAdminSettingsDao extends JpaAbstractDao implements User return EntityType.USER; } - @Override - public ObjectType getType() { - return ObjectType.USER; - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java index b41bfa7728..34f496e0a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserSettingsDao.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.sql.user; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; @@ -74,9 +73,4 @@ public class JpaUserSettingsDao implements UserSettingsDao, TenantEntityDao ignored = EnumSet.of(TENANT, TENANT_PROFILE, RELATION, EVENT, ATTRIBUTE_KV, LATEST_TS_KV, AUDIT_LOG, - OAUTH2_CLIENT, OAUTH2_DOMAIN, OAUTH2_MOBILE); - for (ObjectType type : ObjectType.values()) { - if (ignored.contains(type)) { - continue; - } - - TenantEntityDao dao = assertDoesNotThrow(() -> entityDaoRegistry.getTenantEntityDao(type)); - assertDoesNotThrow(() -> { - dao.findAllByTenantId(tenantId, new PageLink(100)); - }); - } - } - } diff --git a/edqs/src/main/resources/logback.xml b/edqs/src/main/resources/logback.xml index 8680457ab1..d96cb1a7d3 100644 --- a/edqs/src/main/resources/logback.xml +++ b/edqs/src/main/resources/logback.xml @@ -28,7 +28,6 @@ - diff --git a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java index 1f22f9d5ae..6f38e7b6d6 100644 --- a/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java +++ b/edqs/src/test/java/org/thingsboard/server/edqs/repo/AbstractEDQTest.java @@ -69,14 +69,14 @@ import java.util.UUID; @Configuration @ComponentScan({"org.thingsboard.server.edqs.repo", "org.thingsboard.server.edqs.util"}) @EntityScan("org.thingsboard.server.edqs") -@TestPropertySource(locations = {"classpath:edq-test.properties"}) +@TestPropertySource(locations = {"classpath:edqs-test.properties"}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class}) public abstract class AbstractEDQTest { @Autowired - protected InMemoryEdqRepository repository; + protected DefaultEdqsRepository repository; @Autowired protected EdqsConverter edqsConverter; diff --git a/edqs/src/test/resources/edq-test.properties b/edqs/src/test/resources/edqs-test.properties similarity index 100% rename from edqs/src/test/resources/edq-test.properties rename to edqs/src/test/resources/edqs-test.properties From 965210f17b58b0ca42f698cf58178c247f7cca44 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 21 Feb 2025 14:39:26 +0200 Subject: [PATCH 28/29] Multiple improvements for EDQS --- .../src/main/resources/thingsboard.yml | 10 +- .../EdqsEntityQueryControllerTest.java | 8 +- .../server/edqs/processor/EdqsProcessor.java | 5 +- .../server/edqs/repo/TenantRepo.java | 2 +- .../server/edqs/state/EdqsStateService.java | 2 + .../edqs/state/KafkaEdqsStateService.java | 23 ++- .../edqs/state/LocalEdqsStateService.java | 5 + .../server/edqs/stats/EdqsStatsService.java | 7 +- .../common/msg/queue/TopicPartitionInfo.java | 11 +- .../consumer/MainQueueConsumerManager.java | 11 +- .../PartitionedQueueConsumerManager.java | 4 +- .../common/consumer/QueueStateService.java | 17 ++ .../queue/discovery/HashPartitionService.java | 2 +- .../server/queue/edqs/EdqsComponent.java | 1 - .../queue/kafka/TbKafkaConsumerTemplate.java | 4 +- .../common/stats/DefaultStatsFactory.java | 2 +- .../server/dao/entity/BaseEntityService.java | 4 +- .../dao/sql/query/DummyEdqsService.java | 4 +- .../server/edqs/EdqsController.java | 41 +++++ .../edqs/ThingsboardEdqsApplication.java | 67 +------- edqs/src/main/resources/edqs.yml | 147 ++---------------- edqs/src/main/resources/logback.xml | 1 + 22 files changed, 139 insertions(+), 239 deletions(-) create mode 100644 edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 21459814b7..46ab8f25de 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1696,14 +1696,14 @@ queue: print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" edqs: sync: - # Enable/disable EDQS synchronization FIXME: disable by default before release - enabled: "${TB_EDQS_SYNC_ENABLED:true}" + # Enable/disable EDQS synchronization + enabled: "${TB_EDQS_SYNC_ENABLED:false}" # Batch size of entities being synced with EDQS entity_batch_size: "${TB_EDQS_SYNC_ENTITY_BATCH_SIZE:10000}" # Batch size of timeseries data being synced with EDQS ts_batch_size: "${TB_EDQS_SYNC_TS_BATCH_SIZE:10000}" - # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) FIXME: disable by default before release - api_enabled: "${TB_EDQS_API_ENABLED:true}" + # Whether to forward entity data query requests to EDQS (otherwise use PostgreSQL implementation) + api_enabled: "${TB_EDQS_API_ENABLED:false}" # Mode of EDQS: local (for monolith) or remote (with separate EDQS microservices) mode: "${TB_EDQS_MODE:local}" local: @@ -1727,7 +1727,7 @@ queue: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" vc: # Default topic name diff --git a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java index efc290d3c5..e4948330a5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdqsEntityQueryControllerTest.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.msg.edqs.EdqsService; import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.edqs.state.EdqsStateService; import org.thingsboard.server.edqs.util.EdqsRocksDb; import java.util.concurrent.TimeUnit; @@ -34,7 +35,7 @@ import static org.awaitility.Awaitility.await; @DaoSqlTest @TestPropertySource(properties = { // "queue.type=kafka", // uncomment to use Kafka -// "queue.kafka.bootstrap.servers=192.168.0.105:9092", +// "queue.kafka.bootstrap.servers=10.7.1.254:9092", "queue.edqs.sync.enabled=true", "queue.edqs.api_enabled=true", "queue.edqs.mode=local" @@ -44,12 +45,15 @@ public class EdqsEntityQueryControllerTest extends EntityQueryControllerTest { @Autowired private EdqsService edqsService; + @Autowired(required = false) + private EdqsStateService edqsStateService; + @MockBean // so that we don't do backup for tests private EdqsRocksDb edqsRocksDb; @Before public void before() { - await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled()); + await().atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> edqsService.isApiEnabled() && edqsStateService.isReady()); } @Override diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 42e00be7fc..9349946a80 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -24,9 +24,7 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ExceptionUtil; @@ -90,8 +88,7 @@ public class EdqsProcessor implements TbQueueHandler, private final EdqsConfig config; private final EdqsPartitionService partitionService; private final ConfigurableApplicationContext applicationContext; - @Autowired @Lazy - private EdqsStateService stateService; + private final EdqsStateService stateService; private PartitionedQueueConsumerManager> eventConsumer; private TbQueueResponseTemplate, TbProtoQueueMsg> responseTemplate; diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java index 043c0bd6c3..9003eb4da2 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/repo/TenantRepo.java @@ -105,7 +105,7 @@ public class TenantRepo { public void processEvent(EdqsEvent event) { EdqsObject edqsObject = event.getObject(); - log.debug("[{}] Processing event: {}", tenantId, event); + log.trace("[{}] Processing event: {}", tenantId, event); if (event.getEventType() == EdqsEventType.UPDATED) { addOrUpdate(edqsObject); } else if (event.getEventType() == EdqsEventType.DELETED) { diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java index 1966618d4b..bb0aa67239 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/EdqsStateService.java @@ -33,6 +33,8 @@ public interface EdqsStateService { void save(TenantId tenantId, ObjectType type, String key, EdqsEventType eventType, ToEdqsMsg msg); + boolean isReady(); + void stop(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java index f5ba2ba841..05eebb188c 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -52,8 +54,9 @@ public class KafkaEdqsStateService implements EdqsStateService { private final EdqsConfig config; private final EdqsPartitionService partitionService; private final EdqsQueueFactory queueFactory; - private final EdqsProcessor edqsProcessor; private final TopicService topicService; + @Autowired @Lazy + private EdqsProcessor edqsProcessor; private PartitionedQueueConsumerManager> stateConsumer; private QueueStateService, TbProtoQueueMsg> queueStateService; @@ -63,6 +66,7 @@ public class KafkaEdqsStateService implements EdqsStateService { private final VersionsStore versionsStore = new VersionsStore(); private final AtomicInteger stateReadCount = new AtomicInteger(); private final AtomicInteger eventsReadCount = new AtomicInteger(); + private Boolean ready; @Override public void init(PartitionedQueueConsumerManager> eventConsumer) { @@ -72,9 +76,6 @@ public class KafkaEdqsStateService implements EdqsStateService { .pollInterval(config.getPollInterval()) .msgPackProcessor((msgs, consumer, config) -> { for (TbProtoQueueMsg queueMsg : msgs) { - if (consumer.isStopped()) { - return; - } try { ToEdqsMsg msg = queueMsg.getValue(); edqsProcessor.process(msg, EdqsQueue.STATE); @@ -124,7 +125,7 @@ public class KafkaEdqsStateService implements EdqsStateService { TenantId tenantId = getTenantId(msg); ObjectType objectType = ObjectType.valueOf(eventMsg.getObjectType()); EdqsEventType eventType = EdqsEventType.valueOf(eventMsg.getEventType()); - log.debug("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); + log.trace("[{}] Saving to backup [{}] [{}] [{}]", tenantId, objectType, eventType, key); stateProducer.send(tenantId, objectType, key, msg); } } catch (Throwable t) { @@ -160,6 +161,18 @@ public class KafkaEdqsStateService implements EdqsStateService { // do nothing here, backup is done by events consumer } + @Override + public boolean isReady() { + if (ready == null) { + Set partitionsInProgress = queueStateService.getPartitionsInProgress(); + if (partitionsInProgress != null && partitionsInProgress.isEmpty()) { + ready = true; // once true - always true, not to change readiness status on each repartitioning + } + } + log.error("ready: {}", ready); + return ready != null && ready; + } + private TenantId getTenantId(ToEdqsMsg edqsMsg) { return TenantId.fromUUID(new UUID(edqsMsg.getTenantIdMSB(), edqsMsg.getTenantIdLSB())); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index 6869f14b74..c1620e5ec7 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -80,6 +80,11 @@ public class LocalEdqsStateService implements EdqsStateService { } } + @Override + public boolean isReady() { + return partitions != null; + } + @Override public void stop() { } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java index 9619d086ad..d28334c5d9 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/stats/EdqsStatsService.java @@ -43,9 +43,12 @@ public class EdqsStatsService { private final ConcurrentHashMap statsMap = new ConcurrentHashMap<>(); private final StatsFactory statsFactory; - @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:60000}", - fixedDelayString = "${queue.edqs.stats.print-interval-ms:60000}") + @Scheduled(initialDelayString = "${queue.edqs.stats.print-interval-ms:300000}", + fixedDelayString = "${queue.edqs.stats.print-interval-ms:300000}") private void reportStats() { + if (statsMap.isEmpty()) { + return; + } String values = statsMap.entrySet().stream() .map(kv -> "TenantId [" + kv.getKey() + "] stats [" + kv.getValue() + "]") .collect(Collectors.joining(System.lineSeparator())); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index 3bb489f209..da7f16ec0b 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -17,13 +17,11 @@ package org.thingsboard.server.common.msg.queue; import lombok.Builder; import lombok.Getter; -import lombok.ToString; import org.thingsboard.server.common.data.id.TenantId; import java.util.Objects; import java.util.Optional; -@ToString public class TopicPartitionInfo { private final String topic; @@ -97,4 +95,13 @@ public class TopicPartitionInfo { return Objects.hash(fullTopicName, partition); } + @Override + public String toString() { + String str = fullTopicName; + if (useInternalPartition) { + str += "[" + partition + "]"; + } + return str; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java index df683b94cd..a73ee17bd2 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/MainQueueConsumerManager.java @@ -43,7 +43,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.stream.Collectors; @Slf4j public class MainQueueConsumerManager { @@ -270,12 +269,6 @@ public class MainQueueConsumerManager partitions) { - return partitions.stream().map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? - "[" + tpi.getPartition().orElse(-1) + "]" : "")) - .collect(Collectors.joining(", ", "[", "]")); - } - public interface MsgPackProcessor { void process(List msgs, TbQueueConsumer consumer, C config) throws Exception; } @@ -299,7 +292,7 @@ public class MainQueueConsumerManager removedPartitions = new HashSet<>(consumers.keySet()); removedPartitions.removeAll(partitions); - log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, partitionsToString(addedPartitions), partitionsToString(removedPartitions)); + log.info("[{}] Added partitions: {}, removed partitions: {}", queueKey, addedPartitions, removedPartitions); removePartitions(removedPartitions); addPartitions(addedPartitions, null); } @@ -333,7 +326,7 @@ public class MainQueueConsumerManager partitions) { - log.info("[{}] New partitions: {}", queueKey, partitionsToString(partitions)); + log.info("[{}] New partitions: {}", queueKey, partitions); if (partitions.isEmpty()) { if (consumer != null && consumer.isRunning()) { consumer.initiateStop(); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java index 1b47d4c073..6ab0668f30 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/PartitionedQueueConsumerManager.java @@ -52,10 +52,10 @@ public class PartitionedQueueConsumerManager extends MainQ @Override protected void processTask(TbQueueConsumerManagerTask task) { if (task instanceof AddPartitionsTask addPartitionsTask) { - log.info("[{}] Added partitions: {}", queueKey, partitionsToString(addPartitionsTask.partitions())); + log.info("[{}] Added partitions: {}", queueKey, addPartitionsTask.partitions()); consumerWrapper.addPartitions(addPartitionsTask.partitions(), addPartitionsTask.onStop()); } else if (task instanceof RemovePartitionsTask removePartitionsTask) { - log.info("[{}] Removed partitions: {}", queueKey, partitionsToString(removePartitionsTask.partitions())); + log.info("[{}] Removed partitions: {}", queueKey, removePartitionsTask.partitions()); consumerWrapper.removePartitions(removePartitionsTask.partitions()); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java index d022ad14de..00015457c1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -16,16 +16,19 @@ package org.thingsboard.server.queue.common.consumer; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueMsg; import java.util.Collections; import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; +@Slf4j public class QueueStateService { private PartitionedQueueConsumerManager stateConsumer; @@ -33,6 +36,9 @@ public class QueueStateService { @Getter private Set partitions; + private final Set partitionsInProgress = ConcurrentHashMap.newKeySet(); + private boolean initialized; + private final Lock lock = new ReentrantLock(); public void init(PartitionedQueueConsumerManager stateConsumer, PartitionedQueueConsumerManager eventConsumer) { @@ -62,9 +68,15 @@ public class QueueStateService { } if (!addedPartitions.isEmpty()) { + partitionsInProgress.addAll(addedPartitions); stateConsumer.addPartitions(addedPartitions, partition -> { lock.lock(); try { + partitionsInProgress.remove(partition); + log.info("Finished partition {} (still in progress: {})", partition, partitionsInProgress); + if (partitionsInProgress.isEmpty()) { + log.info("All partitions processed"); + } if (this.partitions.contains(partition)) { eventConsumer.addPartitions(Set.of(partition.withTopic(eventConsumer.getTopic()))); } @@ -73,10 +85,15 @@ public class QueueStateService { } }); } + initialized = true; } private Set withTopic(Set partitions, String topic) { return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); } + public Set getPartitionsInProgress() { + return initialized ? partitionsInProgress : null; + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index ea9ba3f245..5787516b25 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -59,7 +59,7 @@ import static org.thingsboard.server.common.data.DataConstants.MAIN_QUEUE_NAME; @Slf4j public class HashPartitionService implements PartitionService { - @Value("${queue.core.topic}") + @Value("${queue.core.topic:tb_core}") private String coreTopic; @Value("${queue.core.partitions:10}") private Integer corePartitions; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java index 27a1e09f79..ba113495d1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/EdqsComponent.java @@ -21,7 +21,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) -// TODO: tb-core ? @ConditionalOnExpression("'${queue.edqs.sync.enabled:true}'=='true' && ('${service.type:null}'=='edqs' || " + "(('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core') && " + "'${queue.edqs.mode:null}'=='local'))") diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java index 47c776c742..9b580c762e 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerTemplate.java @@ -54,6 +54,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue private final boolean readFromBeginning; // reset offset to beginning private final boolean stopWhenRead; // stop consuming when reached an empty msg pack + private int readCount; private Map endOffsets; // needed if stopWhenRead is true private boolean partitionsAssigned = false; @@ -152,6 +153,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue records.forEach(record -> { recordList.add(record); if (stopWhenRead) { + readCount++; int partition = record.partition(); Long endOffset = endOffsets.get(partition); if (endOffset == null) { @@ -166,7 +168,7 @@ public class TbKafkaConsumerTemplate extends AbstractTbQue }); } if (stopWhenRead && endOffsets.isEmpty()) { - log.info("Reached end offset for {}, stopping consumer", consumer.assignment()); + log.info("Finished reading {}, processed {} messages", partitions, readCount); stop(); } return recordList; diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index 2fc7970fc9..e1940e55fe 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -38,7 +38,7 @@ public class DefaultStatsFactory implements StatsFactory { private static final Counter STUB_COUNTER = new StubCounter(); - @Autowired(required = false) // FIXME Slavik !!! + @Autowired // FIXME Slavik !!! private MeterRegistry meterRegistry; @Value("${metrics.enabled:false}") diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index 3f941b776c..1a0f3be1e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -142,12 +142,12 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe private EdqsResponse processEdqsRequest(TenantId tenantId, CustomerId customerId, EdqsRequest request) { EdqsResponse response; try { - log.info("Sending request to EDQS: {}", request); + log.debug("[{}] Sending request to EDQS: {}", tenantId, request); response = edqsService.processRequest(tenantId, customerId, request).get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } - log.info("Received response from EDQS: {}", response); + log.debug("[{}] Received response from EDQS: {}", tenantId, response); if (response.getError() != null) { throw new RuntimeException(response.getError()); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java index b69aa52ec5..9586cb62f8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DummyEdqsService.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.sql.query; import com.google.common.util.concurrent.ListenableFuture; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsObject; @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.edqs.EdqsService; @Service -@ConditionalOnProperty(value = "queue.edqs.sync.enabled", havingValue = "false", matchIfMissing = true) +@ConditionalOnMissingBean(value = EdqsService.class, ignored = DummyEdqsService.class) public class DummyEdqsService implements EdqsService { @Override diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java b/edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java new file mode 100644 index 0000000000..f2e686a84c --- /dev/null +++ b/edqs/src/main/java/org/thingsboard/server/edqs/EdqsController.java @@ -0,0 +1,41 @@ +/** + * 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. + */ +package org.thingsboard.server.edqs; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.edqs.state.EdqsStateService; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/edqs") +public class EdqsController { + + private final EdqsStateService edqsStateService; + + @GetMapping("/ready") + public ResponseEntity isReady() { + if (edqsStateService.isReady()) { + return ResponseEntity.ok().build(); + } else { + return ResponseEntity.badRequest().build(); + } + } + +} diff --git a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java index 044020023d..2f5185728a 100644 --- a/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java +++ b/edqs/src/main/java/org/thingsboard/server/edqs/ThingsboardEdqsApplication.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @@ -27,6 +28,7 @@ import java.util.Arrays; @SpringBootConfiguration @EnableAsync @EnableScheduling +@EnableAutoConfiguration @ComponentScan({"org.thingsboard.server.edqs", "org.thingsboard.server.queue.edqs", "org.thingsboard.server.queue.discovery", "org.thingsboard.server.queue.kafka", "org.thingsboard.server.queue.settings", "org.thingsboard.server.queue.environment", "org.thingsboard.server.common.stats"}) @Slf4j @@ -39,71 +41,6 @@ public class ThingsboardEdqsApplication { SpringApplication.run(ThingsboardEdqsApplication.class, updateArguments(args)); } - // @Bean -// public ApplicationRunner runner(CSVLoader loader, EdqRepository edqRepository) { -// return args -> { -// long startTs = System.currentTimeMillis(); -// var loader = new TenantRepoLoader(new TenantRepo(TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")))); -// loader.load(); -// log.info("Loaded all in {} ms", System.currentTimeMillis() - startTs); - - - -// log.info("Compressed {} strings/json, Before: {}, After: {}", -// CompressedStringDataPoint.cnt.get(), -// CompressedStringDataPoint.uncompressedLength.get(), -// CompressedStringDataPoint.compressedLength.get()); -// -// log.info("Deduplicated {} short and {} long strings", -// TbStringPool.size(), TbBytePool.size()); -// -// var tenantId = TenantId.fromUUID(UUID.fromString("2a209df0-c7ff-11ea-a3e0-f321b0429d60")); -// var customerId = new CustomerId(UUID.fromString("fcbf2f50-d0d9-11ea-bea3-177755191a6e")); -// System.gc(); -// -// while (true) { -// EntityTypeFilter filter = new EntityTypeFilter(); -// filter.setEntityType(EntityType.DEVICE); -// var pageLink = new EntityDataPageLink(20, 0, null, new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.DESC), false); -// -// var entityFields = Arrays.asList(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "createdTime")); -// var latestValues = Arrays.asList(new EntityKey(EntityKeyType.TIME_SERIES, "state")); -// KeyFilter nameFilter = new KeyFilter(); -// nameFilter.setKey(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); -// var predicate = new StringFilterPredicate(); -// predicate.setIgnoreCase(false); -// predicate.setOperation(StringFilterPredicate.StringOperation.CONTAINS); -// predicate.setValue(new FilterPredicateValue<>("LoRa-")); -// nameFilter.setPredicate(predicate); -// nameFilter.setValueType(EntityKeyValueType.STRING); -// -// EntityDataQuery edq = new EntityDataQuery(filter, pageLink, entityFields, latestValues, Arrays.asList(nameFilter)); -// var result = edqRepository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, edq, false); -// log.info("Device count: {}", result.getTotalElements()); -// log.info("First: {}", result.getData().get(0).getEntityId()); -// log.info("Last: {}", result.getData().get(19).getEntityId()); -// -// pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.TIME_SERIES, "state"), EntityDataSortOrder.Direction.ASC)); -// result = edqRepository.findEntityDataByQuery(tenantId, customerId, RepositoryUtils.ALL_READ_PERMISSIONS, edq, false); -// log.info("Device count: {}", result.getTotalElements()); -// log.info("First: {}", result.getData().get(0).getEntityId()); -// log.info("Last: {}", result.getData().get(19).getEntityId()); -// -// result.getData().forEach(data -> { -// System.err.println(data.getEntityId() + ":"); -// data.getLatest().forEach((type, values) -> { -// System.err.println(type); -// values.forEach((key, tsValue) -> { -// System.err.println(key + " = " + tsValue.getValue()); -// }); -// }); -// System.err.println(); -// }); -// Thread.sleep(5000); -// } -// }; -// } - private static String[] updateArguments(String[] args) { if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) { String[] modifiedArgs = new String[args.length + 1]; diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index fa09da1cdd..3d30c5c6fa 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -14,6 +14,12 @@ # limitations under the License. # +server: + # Server bind-address + address: "${HTTP_BIND_ADDRESS:0.0.0.0}" + # Server bind port + port: "${HTTP_BIND_PORT:8080}" + # Application info parameters app: # Application version @@ -38,10 +44,9 @@ zk: # The delay is recommended because the initialization of rule chain actors is time-consuming. Avoiding unnecessary recalculations during a restart can enhance system performance and stability. recalculate_delay: "${ZOOKEEPER_RECALCULATE_DELAY_MS:0}" -# SQL DAO Configuration parameters spring: main: - web-application-type: "none" + allow-circular-references: "true" # Spring Boot configuration property that controls whether circular dependencies between beans are allowed. # Queue configuration parameters queue: @@ -66,7 +71,7 @@ queue: # Enable/disable statistics for EDQS enabled: "${TB_EDQS_STATS_ENABLED:true}" # Statistics printing interval for EDQS - print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:60000}" + print-interval-ms: "${TB_EDQS_STATS_PRINT_INTERVAL_MS:300000}" kafka: # Kafka Bootstrap nodes in "host:port" format @@ -137,9 +142,9 @@ queue: - key: max.poll.interval.ms # Example of specific consumer properties value per topic for VC value: "${TB_QUEUE_KAFKA_VC_MAX_POLL_INTERVAL_MS:600000}" - # tb_rule_engine.sq: - # - key: max.poll.records - # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" + # tb_rule_engine.sq: + # - key: max.poll.records + # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" tb_housekeeper: # Consumer properties for Housekeeper tasks topic - key: max.poll.records @@ -209,133 +214,6 @@ queue: request_poll_interval: "${TB_QUEUE_TRANSPORT_REQUEST_POLL_INTERVAL_MS:25}" # Interval in milliseconds to poll api response from transport microservices response_poll_interval: "${TB_QUEUE_TRANSPORT_RESPONSE_POLL_INTERVAL_MS:25}" - core: - # Default topic name of Kafka, RabbitMQ, etc. queue - topic: "${TB_QUEUE_CORE_TOPIC:tb_core}" - # Interval in milliseconds to poll messages by Core microservices - poll-interval: "${TB_QUEUE_CORE_POLL_INTERVAL_MS:25}" - # Amount of partitions used by Core microservices - partitions: "${TB_QUEUE_CORE_PARTITIONS:10}" - # Timeout for processing a message pack by Core microservices - pack-processing-timeout: "${TB_QUEUE_CORE_PACK_PROCESSING_TIMEOUT_MS:2000}" - # Enable/disable a separate consumer per partition for Core queue - consumer-per-partition: "${TB_QUEUE_CORE_CONSUMER_PER_PARTITION:true}" - ota: - # Default topic name for OTA updates - topic: "${TB_QUEUE_CORE_OTA_TOPIC:tb_ota_package}" - # The interval of processing the OTA updates for devices. Used to avoid any harm to the network due to many parallel OTA updates - pack-interval-ms: "${TB_QUEUE_CORE_OTA_PACK_INTERVAL_MS:60000}" - # The size of OTA updates notifications fetched from the queue. The queue stores pairs of firmware and device ids - pack-size: "${TB_QUEUE_CORE_OTA_PACK_SIZE:100}" - # Stats topic name for queue Kafka, RabbitMQ, etc. - usage-stats-topic: "${TB_QUEUE_US_TOPIC:tb_usage_stats}" - stats: - # Enable/disable statistics for Core microservices - enabled: "${TB_QUEUE_CORE_STATS_ENABLED:true}" - # Statistics printing interval for Core microservices - print-interval-ms: "${TB_QUEUE_CORE_STATS_PRINT_INTERVAL_MS:60000}" - housekeeper: - # Topic name for Housekeeper tasks - topic: "${TB_HOUSEKEEPER_TOPIC:tb_housekeeper}" - # Topic name for Housekeeper tasks to be reprocessed - reprocessing-topic: "${TB_HOUSEKEEPER_REPROCESSING_TOPIC:tb_housekeeper.reprocessing}" - # Poll interval for topics related to Housekeeper - poll-interval-ms: "${TB_HOUSEKEEPER_POLL_INTERVAL_MS:500}" - # Timeout in milliseconds for task processing. Tasks that fail to finish on time will be submitted for reprocessing - task-processing-timeout-ms: "${TB_HOUSEKEEPER_TASK_PROCESSING_TIMEOUT_MS:120000}" - # Comma-separated list of task types that shouldn't be processed. Available task types: - # DELETE_ATTRIBUTES, DELETE_TELEMETRY (both DELETE_LATEST_TS and DELETE_TS_HISTORY will be disabled), - # DELETE_LATEST_TS, DELETE_TS_HISTORY, DELETE_EVENTS, DELETE_ALARMS, UNASSIGN_ALARMS - disabled-task-types: "${TB_HOUSEKEEPER_DISABLED_TASK_TYPES:}" - # Delay in milliseconds between tasks reprocessing - task-reprocessing-delay-ms: "${TB_HOUSEKEEPER_TASK_REPROCESSING_DELAY_MS:3000}" - # Maximum amount of task reprocessing attempts. After exceeding, the task will be dropped - max-reprocessing-attempts: "${TB_HOUSEKEEPER_MAX_REPROCESSING_ATTEMPTS:10}" - stats: - # Enable/disable statistics for Housekeeper - enabled: "${TB_HOUSEKEEPER_STATS_ENABLED:true}" - # Statistics printing interval for Housekeeper - print-interval-ms: "${TB_HOUSEKEEPER_STATS_PRINT_INTERVAL_MS:60000}" - - vc: - # Default topic name for Kafka, RabbitMQ, etc. - topic: "${TB_QUEUE_VC_TOPIC:tb_version_control}" - # Number of partitions to associate with this queue. Used for scaling the number of messages that can be processed in parallel - partitions: "${TB_QUEUE_VC_PARTITIONS:10}" - # Interval in milliseconds between polling of the messages if no new messages arrive - poll-interval: "${TB_QUEUE_VC_INTERVAL_MS:25}" - # Timeout before retrying all failed and timed-out messages from the processing pack - pack-processing-timeout: "${TB_QUEUE_VC_PACK_PROCESSING_TIMEOUT_MS:180000}" - # Timeout for a request to VC-executor (for a request for the version of the entity, for a commit charge, etc.) - request-timeout: "${TB_QUEUE_VC_REQUEST_TIMEOUT:180000}" - # Queue settings for Kafka, RabbitMQ, etc. Limit for single message size - msg-chunk-size: "${TB_QUEUE_VC_MSG_CHUNK_SIZE:250000}" - js: - # JS Eval request topic - request_topic: "${REMOTE_JS_EVAL_REQUEST_TOPIC:js_eval.requests}" - # JS Eval responses topic prefix that is combined with node id - response_topic_prefix: "${REMOTE_JS_EVAL_RESPONSE_TOPIC:js_eval.responses}" - # JS Eval max pending requests - max_pending_requests: "${REMOTE_JS_MAX_PENDING_REQUESTS:10000}" - # JS Eval max request timeout - max_eval_requests_timeout: "${REMOTE_JS_MAX_EVAL_REQUEST_TIMEOUT:60000}" - # JS max request timeout - max_requests_timeout: "${REMOTE_JS_MAX_REQUEST_TIMEOUT:10000}" - # JS execution max request timeout - max_exec_requests_timeout: "${REMOTE_JS_MAX_EXEC_REQUEST_TIMEOUT:2000}" - # JS response poll interval - response_poll_interval: "${REMOTE_JS_RESPONSE_POLL_INTERVAL_MS:25}" - rule-engine: - # Deprecated. It will be removed in the nearest releases - topic: "${TB_QUEUE_RULE_ENGINE_TOPIC:tb_rule_engine}" - # Interval in milliseconds to poll messages by Rule Engine - poll-interval: "${TB_QUEUE_RULE_ENGINE_POLL_INTERVAL_MS:25}" - # Timeout for processing a message pack of Rule Engine - pack-processing-timeout: "${TB_QUEUE_RULE_ENGINE_PACK_PROCESSING_TIMEOUT_MS:2000}" - stats: - # Enable/disable statistics for Rule Engine - enabled: "${TB_QUEUE_RULE_ENGINE_STATS_ENABLED:true}" - # Statistics printing interval for Rule Engine - print-interval-ms: "${TB_QUEUE_RULE_ENGINE_STATS_PRINT_INTERVAL_MS:60000}" - # Max length of the error message that is printed by statistics - max-error-message-length: "${TB_QUEUE_RULE_ENGINE_MAX_ERROR_MESSAGE_LENGTH:4096}" - # After a queue is deleted (or the profile's isolation option was disabled), Rule Engine will continue reading related topics during this period before deleting the actual topics - topic-deletion-delay: "${TB_QUEUE_RULE_ENGINE_TOPIC_DELETION_DELAY_SEC:15}" - # Size of the thread pool that handles such operations as partition changes, config updates, queue deletion - management-thread-pool-size: "${TB_QUEUE_RULE_ENGINE_MGMT_THREAD_POOL_SIZE:12}" - transport: - # For high-priority notifications that require minimum latency and processing time - notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}" - # Interval in milliseconds to poll messages - poll_interval: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_POLL_INTERVAL_MS:25}" - integration: - # Name of hash function used for consistent hash ring in Cluster Mode. See architecture docs for more details. Valid values - murmur3_32, murmur3_128 or sha256 - partitions: "${TB_QUEUE_INTEGRATION_PARTITIONS:3}" - # Default notification topic name used by queue - notifications_topic: "${TB_QUEUE_INTEGRATION_NOTIFICATIONS_TOPIC:tb_ie.notifications}" - # Default downlink topic name used by queue - downlink_topic: "${TB_QUEUE_INTEGRATION_DOWNLINK_TOPIC:tb_ie.downlink}" - # Default uplink topic name used by queue - uplink_topic: "${TB_QUEUE_INTEGRATION_UPLINK_TOPIC:tb_ie.uplink}" - # Interval in milliseconds to poll messages by integrations - poll_interval: "${TB_QUEUE_INTEGRATION_POLL_INTERVAL_MS:25}" - # Timeout for processing a message pack by integrations - pack-processing-timeout: "${TB_QUEUE_INTEGRATION_PACK_PROCESSING_TIMEOUT_MS:10000}" - integration_api: - # Default Integration Api request topic name used by queue - requests_topic: "${TB_QUEUE_INTEGRATION_EXECUTOR_API_REQUEST_TOPIC:tb_ie.api.requests}" - # Default Integration Api response topic name used by queue - responses_topic: "${TB_QUEUE_INTEGRATION_EXECUTOR_API_RESPONSE_TOPIC:tb_ie.api.responses}" - # Maximum pending api requests from integration executor to be handled by server< - max_pending_requests: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_PENDING_REQUESTS:10000}" - # Maximum timeout in milliseconds to handle api request from integration executor microservice by server - max_requests_timeout: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_REQUEST_TIMEOUT:10000}" - # Amount of threads used to invoke callbacks - max_callback_threads: "${TB_QUEUE_INTEGRATION_EXECUTOR_MAX_CALLBACK_THREADS:10}" - # Interval in milliseconds to poll api requests from integration executor microservices - request_poll_interval: "${TB_QUEUE_INTEGRATION_EXECUTOR_REQUEST_POLL_INTERVAL_MS:25}" - # Interval in milliseconds to poll api response from integration executor microservices - response_poll_interval: "${TB_QUEUE_INTEGRATION_EXECUTOR_RESPONSE_POLL_INTERVAL_MS:25}" # General service parameters service: @@ -343,7 +221,8 @@ service: # Unique id for this service (autogenerated if empty) id: "${TB_SERVICE_ID:}" edqs: - label: "${TB_EDQS_LABEL:}" # services with the same label will share the list of partitions + # EDQS instances with the same label will share the same list of partitions + label: "${TB_EDQS_LABEL:}" # Metrics parameters metrics: diff --git a/edqs/src/main/resources/logback.xml b/edqs/src/main/resources/logback.xml index d96cb1a7d3..0aedb9d092 100644 --- a/edqs/src/main/resources/logback.xml +++ b/edqs/src/main/resources/logback.xml @@ -26,6 +26,7 @@ + From 278ec36f376cd707562ec2b6b650f6eaf273832e Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 21 Feb 2025 15:48:33 +0200 Subject: [PATCH 29/29] Fixes for EDQS --- .../server/edqs/processor/EdqsProcessor.java | 8 ++------ .../server/edqs/state/LocalEdqsStateService.java | 10 ++++++++-- .../server/common/msg/queue/TopicPartitionInfo.java | 6 ++++++ .../queue/common/AbstractTbQueueConsumerTemplate.java | 10 ++++------ .../queue/common/consumer/QueueStateService.java | 7 ++----- .../server/queue/edqs/InMemoryEdqsQueueFactory.java | 6 ++++-- .../server/queue/edqs/KafkaEdqsQueueFactory.java | 10 +++++++--- .../server/common/stats/DefaultStatsFactory.java | 2 +- 8 files changed, 34 insertions(+), 25 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java index 9349946a80..ac7af4691d 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java @@ -76,6 +76,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + @EdqsComponent @Service @RequiredArgsConstructor @@ -280,12 +282,6 @@ public class EdqsProcessor implements TbQueueHandler, } } - private Set withTopic(Set partitions, String topic) { - return partitions.stream() - .map(tpi -> tpi.withTopic(topic)) - .collect(Collectors.toSet()); - } - @PreDestroy public void destroy() throws InterruptedException { eventConsumer.stop(); diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java index c1620e5ec7..67724dd8b1 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/state/LocalEdqsStateService.java @@ -17,6 +17,8 @@ package org.thingsboard.server.edqs.state; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.ObjectType; import org.thingsboard.server.common.data.edqs.EdqsEventType; @@ -32,14 +34,17 @@ import org.thingsboard.server.queue.edqs.InMemoryEdqsComponent; import java.util.Set; +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; + @Service @RequiredArgsConstructor @InMemoryEdqsComponent @Slf4j public class LocalEdqsStateService implements EdqsStateService { - private final EdqsProcessor processor; private final EdqsRocksDb db; + @Autowired @Lazy + private EdqsProcessor processor; private PartitionedQueueConsumerManager> eventConsumer; private Set partitions; @@ -61,8 +66,9 @@ public class LocalEdqsStateService implements EdqsStateService { log.error("[{}] Failed to restore value", key, e); } }); + log.info("Restore completed"); } - eventConsumer.update(partitions); + eventConsumer.update(withTopic(partitions, EdqsQueue.EVENTS.getTopic())); this.partitions = partitions; } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java index da7f16ec0b..c7a23451d1 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/queue/TopicPartitionInfo.java @@ -21,6 +21,8 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; public class TopicPartitionInfo { @@ -75,6 +77,10 @@ public class TopicPartitionInfo { return new TopicPartitionInfo(topic, this.tenantId, this.partition, this.useInternalPartition, this.myPartition); } + public static Set withTopic(Set partitions, String topic) { + return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); + } + public TopicPartitionInfo withUseInternalPartition(boolean useInternalPartition) { return new TopicPartitionInfo(this.topic, this.tenantId, this.partition, useInternalPartition, this.myPartition); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java index 0e399929f4..88e8f60dee 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/AbstractTbQueueConsumerTemplate.java @@ -31,7 +31,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; import static java.util.Collections.emptyList; @@ -95,7 +94,7 @@ public abstract class AbstractTbQueueConsumerTemplate i partitions = subscribeQueue.poll(); } if (!subscribed) { - log.info("Subscribing to topics {}", getFullTopicNames()); + log.info("Subscribing to {}", partitions); doSubscribe(partitions); subscribed = true; } @@ -168,7 +167,7 @@ public abstract class AbstractTbQueueConsumerTemplate i @Override public void unsubscribe() { - log.info("Unsubscribing and stopping consumer for topics {}", getFullTopicNames()); + log.info("Unsubscribing and stopping consumer for {}", partitions); stopped = true; consumerLock.lock(); try { @@ -201,9 +200,8 @@ public abstract class AbstractTbQueueConsumerTemplate i return Collections.emptyList(); } return partitions.stream() - .map(tpi -> tpi.getFullTopicName() + (tpi.isUseInternalPartition() ? - "[" + tpi.getPartition().orElse(-1) + "]" : "")) - .collect(Collectors.toList()); + .map(TopicPartitionInfo::getFullTopicName) + .toList(); } protected boolean isLongPollingSupported() { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java index 00015457c1..2671f03b95 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/consumer/QueueStateService.java @@ -26,7 +26,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; + +import static org.thingsboard.server.common.msg.queue.TopicPartitionInfo.withTopic; @Slf4j public class QueueStateService { @@ -88,10 +89,6 @@ public class QueueStateService { initialized = true; } - private Set withTopic(Set partitions, String topic) { - return partitions.stream().map(tpi -> tpi.withTopic(topic)).collect(Collectors.toSet()); - } - public Set getPartitionsInProgress() { return initialized ? partitionsInProgress : null; } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java index 20b4beadcf..dc6e9f4791 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/InMemoryEdqsQueueFactory.java @@ -18,7 +18,8 @@ package org.thingsboard.server.queue.edqs; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.stats.DummyMessagesStats; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; import org.thingsboard.server.queue.TbQueueConsumer; @@ -37,6 +38,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { private final InMemoryStorage storage; private final EdqsConfig edqsConfig; + private final StatsFactory statsFactory; @Override public TbQueueConsumer> createEdqsMsgConsumer(EdqsQueue queue) { @@ -69,7 +71,7 @@ public class InMemoryEdqsQueueFactory implements EdqsQueueFactory { .maxPendingRequests(edqsConfig.getMaxPendingRequests()) .requestTimeout(edqsConfig.getMaxRequestTimeout()) .pollInterval(edqsConfig.getPollInterval()) - .stats(new DummyMessagesStats()) // FIXME + .stats(statsFactory.createMessagesStats(StatsType.EDQS.getName())) .executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs")) .build(); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java index 9f973ce3f1..9561849dd1 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/edqs/KafkaEdqsQueueFactory.java @@ -17,7 +17,8 @@ package org.thingsboard.server.queue.edqs; import org.springframework.stereotype.Component; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.server.common.stats.DummyMessagesStats; +import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.stats.StatsType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromEdqsMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToEdqsMsg; @@ -49,12 +50,14 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { private final TbServiceInfoProvider serviceInfoProvider; private final TbKafkaConsumerStatsService consumerStatsService; private final TopicService topicService; + private final StatsFactory statsFactory; private final AtomicInteger consumerCounter = new AtomicInteger(); public KafkaEdqsQueueFactory(TbKafkaSettings kafkaSettings, TbKafkaTopicConfigs topicConfigs, EdqsConfig edqsConfig, TbServiceInfoProvider serviceInfoProvider, - TbKafkaConsumerStatsService consumerStatsService, TopicService topicService) { + TbKafkaConsumerStatsService consumerStatsService, TopicService topicService, + StatsFactory statsFactory) { this.edqsEventsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsEventsConfigs()); this.edqsRequestsAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsRequestsConfigs()); this.edqsStateAdmin = new TbKafkaAdmin(kafkaSettings, topicConfigs.getEdqsStateConfigs()); @@ -63,6 +66,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { this.serviceInfoProvider = serviceInfoProvider; this.consumerStatsService = consumerStatsService; this.topicService = topicService; + this.statsFactory = statsFactory; } @Override @@ -117,7 +121,7 @@ public class KafkaEdqsQueueFactory implements EdqsQueueFactory { .maxPendingRequests(edqsConfig.getMaxPendingRequests()) .requestTimeout(edqsConfig.getMaxRequestTimeout()) .pollInterval(edqsConfig.getPollInterval()) - .stats(new DummyMessagesStats()) // FIXME + .stats(statsFactory.createMessagesStats(StatsType.EDQS.getName())) .executor(ThingsBoardExecutors.newWorkStealingPool(5, "edqs")) .build(); } diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index e1940e55fe..ca97792186 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -38,7 +38,7 @@ public class DefaultStatsFactory implements StatsFactory { private static final Counter STUB_COUNTER = new StubCounter(); - @Autowired // FIXME Slavik !!! + @Autowired private MeterRegistry meterRegistry; @Value("${metrics.enabled:false}")