Commit 9f9e613320f435ada5142ee048afb881fbad5540
Merge remote-tracking branch 'upstream/master' into feature/input-password/toggle
Showing
70 changed files
with
895 additions
and
294 deletions
... | ... | @@ -290,6 +290,11 @@ |
290 | 290 | <scope>test</scope> |
291 | 291 | </dependency> |
292 | 292 | <dependency> |
293 | + <groupId>org.awaitility</groupId> | |
294 | + <artifactId>awaitility</artifactId> | |
295 | + <scope>test</scope> | |
296 | + </dependency> | |
297 | + <dependency> | |
293 | 298 | <groupId>org.mockito</groupId> |
294 | 299 | <artifactId>mockito-core</artifactId> |
295 | 300 | <scope>test</scope> | ... | ... |
... | ... | @@ -381,11 +381,11 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { |
381 | 381 | } |
382 | 382 | |
383 | 383 | private void reportSessionOpen() { |
384 | - systemContext.getDeviceStateService().onDeviceConnect(deviceId); | |
384 | + systemContext.getDeviceStateService().onDeviceConnect(tenantId, deviceId); | |
385 | 385 | } |
386 | 386 | |
387 | 387 | private void reportSessionClose() { |
388 | - systemContext.getDeviceStateService().onDeviceDisconnect(deviceId); | |
388 | + systemContext.getDeviceStateService().onDeviceDisconnect(tenantId, deviceId); | |
389 | 389 | } |
390 | 390 | |
391 | 391 | private void handleGetAttributesRequest(TbActorCtx context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) { |
... | ... | @@ -590,7 +590,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { |
590 | 590 | if (sessions.size() == 1) { |
591 | 591 | reportSessionOpen(); |
592 | 592 | } |
593 | - systemContext.getDeviceStateService().onDeviceActivity(deviceId, System.currentTimeMillis()); | |
593 | + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, System.currentTimeMillis()); | |
594 | 594 | dumpSessions(); |
595 | 595 | } else if (msg.getEvent() == SessionEvent.CLOSED) { |
596 | 596 | log.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId); |
... | ... | @@ -620,7 +620,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { |
620 | 620 | if (subscriptionInfo.getRpcSubscription()) { |
621 | 621 | rpcSubscriptions.putIfAbsent(sessionId, sessionMD.getSessionInfo()); |
622 | 622 | } |
623 | - systemContext.getDeviceStateService().onDeviceActivity(deviceId, subscriptionInfo.getLastActivityTime()); | |
623 | + systemContext.getDeviceStateService().onDeviceActivity(tenantId, deviceId, subscriptionInfo.getLastActivityTime()); | |
624 | 624 | dumpSessions(); |
625 | 625 | } |
626 | 626 | ... | ... |
... | ... | @@ -26,12 +26,12 @@ import org.springframework.web.bind.annotation.ResponseBody; |
26 | 26 | import org.springframework.web.bind.annotation.RestController; |
27 | 27 | import org.thingsboard.rule.engine.api.MailService; |
28 | 28 | import org.thingsboard.rule.engine.api.SmsService; |
29 | -import org.thingsboard.server.common.data.sms.config.TestSmsRequest; | |
30 | 29 | import org.thingsboard.server.common.data.AdminSettings; |
31 | 30 | import org.thingsboard.server.common.data.UpdateMessage; |
32 | 31 | import org.thingsboard.server.common.data.exception.ThingsboardException; |
33 | 32 | import org.thingsboard.server.common.data.id.TenantId; |
34 | 33 | import org.thingsboard.server.common.data.security.model.SecuritySettings; |
34 | +import org.thingsboard.server.common.data.sms.config.TestSmsRequest; | |
35 | 35 | import org.thingsboard.server.dao.settings.AdminSettingsService; |
36 | 36 | import org.thingsboard.server.queue.util.TbCoreComponent; |
37 | 37 | import org.thingsboard.server.service.security.permission.Operation; |
... | ... | @@ -67,7 +67,7 @@ public class AdminController extends BaseController { |
67 | 67 | accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); |
68 | 68 | AdminSettings adminSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, key)); |
69 | 69 | if (adminSettings.getKey().equals("mail")) { |
70 | - ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); | |
70 | + ((ObjectNode) adminSettings.getJsonValue()).remove("password"); | |
71 | 71 | } |
72 | 72 | return adminSettings; |
73 | 73 | } catch (Exception e) { |
... | ... | @@ -84,7 +84,7 @@ public class AdminController extends BaseController { |
84 | 84 | adminSettings = checkNotNull(adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings)); |
85 | 85 | if (adminSettings.getKey().equals("mail")) { |
86 | 86 | mailService.updateMailConfiguration(); |
87 | - ((ObjectNode) adminSettings.getJsonValue()).put("password", ""); | |
87 | + ((ObjectNode) adminSettings.getJsonValue()).remove("password"); | |
88 | 88 | } else if (adminSettings.getKey().equals("sms")) { |
89 | 89 | smsService.updateSmsConfiguration(); |
90 | 90 | } |
... | ... | @@ -126,6 +126,10 @@ public class AdminController extends BaseController { |
126 | 126 | accessControlService.checkPermission(getCurrentUser(), Resource.ADMIN_SETTINGS, Operation.READ); |
127 | 127 | adminSettings = checkNotNull(adminSettings); |
128 | 128 | if (adminSettings.getKey().equals("mail")) { |
129 | + if(!adminSettings.getJsonValue().has("password")) { | |
130 | + AdminSettings mailSettings = checkNotNull(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail")); | |
131 | + ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); | |
132 | + } | |
129 | 133 | String email = getCurrentUser().getEmail(); |
130 | 134 | mailService.sendTestMail(adminSettings.getJsonValue(), email); |
131 | 135 | } | ... | ... |
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
... | ... | @@ -215,6 +215,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { |
215 | 215 | node.put("password", ""); |
216 | 216 | node.put("tlsVersion", "TLSv1.2");//NOSONAR, key used to identify password field (not password value itself) |
217 | 217 | node.put("enableProxy", false); |
218 | + node.put("showChangePassword", false); | |
218 | 219 | mailSettings.setJsonValue(node); |
219 | 220 | adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, mailSettings); |
220 | 221 | } | ... | ... |
... | ... | @@ -17,6 +17,7 @@ package org.thingsboard.server.service.security.auth.oauth2; |
17 | 17 | |
18 | 18 | import com.fasterxml.jackson.core.JsonProcessingException; |
19 | 19 | import com.fasterxml.jackson.databind.ObjectMapper; |
20 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | |
20 | 21 | import lombok.extern.slf4j.Slf4j; |
21 | 22 | import org.springframework.boot.web.client.RestTemplateBuilder; |
22 | 23 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
... | ... | @@ -29,6 +30,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2Registration; |
29 | 30 | import org.thingsboard.server.dao.oauth2.OAuth2User; |
30 | 31 | import org.thingsboard.server.service.security.model.SecurityUser; |
31 | 32 | |
33 | +import javax.annotation.PostConstruct; | |
32 | 34 | import javax.servlet.http.HttpServletRequest; |
33 | 35 | |
34 | 36 | @Service(value = "customOAuth2ClientMapper") |
... | ... | @@ -40,6 +42,15 @@ public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper impleme |
40 | 42 | |
41 | 43 | private RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder(); |
42 | 44 | |
45 | + @PostConstruct | |
46 | + public void init() { | |
47 | + // Register time module to parse Instant objects. | |
48 | + // com.fasterxml.jackson.databind.exc.InvalidDefinitionException: | |
49 | + // Java 8 date/time type `java.time.Instant` not supported by default: | |
50 | + // add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling | |
51 | + json.registerModule(new JavaTimeModule()); | |
52 | + } | |
53 | + | |
43 | 54 | @Override |
44 | 55 | public SecurityUser getOrCreateUserByClientPrincipal(HttpServletRequest request, OAuth2AuthenticationToken token, String providerAccessToken, OAuth2Registration registration) { |
45 | 56 | OAuth2MapperConfig config = registration.getMapperConfig(); | ... | ... |
... | ... | @@ -15,6 +15,7 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.service.security.auth.oauth2; |
17 | 17 | |
18 | +import lombok.extern.slf4j.Slf4j; | |
18 | 19 | import org.springframework.beans.factory.annotation.Autowired; |
19 | 20 | import org.springframework.security.core.Authentication; |
20 | 21 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; |
... | ... | @@ -42,6 +43,7 @@ import java.net.URLEncoder; |
42 | 43 | import java.nio.charset.StandardCharsets; |
43 | 44 | import java.util.UUID; |
44 | 45 | |
46 | +@Slf4j | |
45 | 47 | @Component(value = "oauth2AuthenticationSuccessHandler") |
46 | 48 | public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { |
47 | 49 | |
... | ... | @@ -99,6 +101,8 @@ public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationS |
99 | 101 | clearAuthenticationAttributes(request, response); |
100 | 102 | getRedirectStrategy().sendRedirect(request, response, baseUrl + "/?accessToken=" + accessToken.getToken() + "&refreshToken=" + refreshToken.getToken()); |
101 | 103 | } catch (Exception e) { |
104 | + log.debug("Error occurred during processing authentication success result. " + | |
105 | + "request [{}], response [{}], authentication [{}]", request, response, authentication, e); | |
102 | 106 | clearAuthenticationAttributes(request, response); |
103 | 107 | String errorPrefix; |
104 | 108 | if (!StringUtils.isEmpty(callbackUrlScheme)) { | ... | ... |
... | ... | @@ -137,7 +137,6 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
137 | 137 | private ExecutorService deviceStateExecutor; |
138 | 138 | private final ConcurrentMap<TopicPartitionInfo, Set<DeviceId>> partitionedDevices = new ConcurrentHashMap<>(); |
139 | 139 | final ConcurrentMap<DeviceId, DeviceStateData> deviceStates = new ConcurrentHashMap<>(); |
140 | - private final ConcurrentMap<DeviceId, Long> deviceLastSavedActivity = new ConcurrentHashMap<>(); | |
141 | 140 | |
142 | 141 | final Queue<Set<TopicPartitionInfo>> subscribeQueue = new ConcurrentLinkedQueue<>(); |
143 | 142 | |
... | ... | @@ -192,7 +191,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
192 | 191 | } |
193 | 192 | |
194 | 193 | @Override |
195 | - public void onDeviceConnect(DeviceId deviceId) { | |
194 | + public void onDeviceConnect(TenantId tenantId, DeviceId deviceId) { | |
196 | 195 | log.trace("on Device Connect [{}]", deviceId.getId()); |
197 | 196 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
198 | 197 | long ts = System.currentTimeMillis(); |
... | ... | @@ -200,23 +199,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
200 | 199 | save(deviceId, LAST_CONNECT_TIME, ts); |
201 | 200 | pushRuleEngineMessage(stateData, CONNECT_EVENT); |
202 | 201 | checkAndUpdateState(deviceId, stateData); |
202 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | |
203 | 203 | } |
204 | 204 | |
205 | 205 | @Override |
206 | - public void onDeviceActivity(DeviceId deviceId, long lastReportedActivity) { | |
206 | + public void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivity) { | |
207 | 207 | log.trace("on Device Activity [{}], lastReportedActivity [{}]", deviceId.getId(), lastReportedActivity); |
208 | - long lastSavedActivity = deviceLastSavedActivity.getOrDefault(deviceId, 0L); | |
209 | - if (lastReportedActivity > 0 && lastReportedActivity > lastSavedActivity) { | |
210 | - final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); | |
208 | + final DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); | |
209 | + if (lastReportedActivity > 0 && lastReportedActivity > stateData.getState().getLastActivityTime()) { | |
211 | 210 | updateActivityState(deviceId, stateData, lastReportedActivity); |
212 | 211 | } |
212 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | |
213 | 213 | } |
214 | 214 | |
215 | 215 | void updateActivityState(DeviceId deviceId, DeviceStateData stateData, long lastReportedActivity) { |
216 | 216 | log.trace("updateActivityState - fetched state {} for device {}, lastReportedActivity {}", stateData, deviceId, lastReportedActivity); |
217 | 217 | if (stateData != null) { |
218 | 218 | save(deviceId, LAST_ACTIVITY_TIME, lastReportedActivity); |
219 | - deviceLastSavedActivity.put(deviceId, lastReportedActivity); | |
220 | 219 | DeviceState state = stateData.getState(); |
221 | 220 | state.setLastActivityTime(lastReportedActivity); |
222 | 221 | if (!state.isActive()) { |
... | ... | @@ -225,21 +224,23 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
225 | 224 | pushRuleEngineMessage(stateData, ACTIVITY_EVENT); |
226 | 225 | } |
227 | 226 | } else { |
228 | - log.warn("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); | |
227 | + log.debug("updateActivityState - fetched state IN NULL for device {}, lastReportedActivity {}", deviceId, lastReportedActivity); | |
228 | + cleanUpDeviceStateMap(deviceId); | |
229 | 229 | } |
230 | 230 | } |
231 | 231 | |
232 | 232 | @Override |
233 | - public void onDeviceDisconnect(DeviceId deviceId) { | |
233 | + public void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId) { | |
234 | 234 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
235 | 235 | long ts = System.currentTimeMillis(); |
236 | 236 | stateData.getState().setLastDisconnectTime(ts); |
237 | 237 | save(deviceId, LAST_DISCONNECT_TIME, ts); |
238 | 238 | pushRuleEngineMessage(stateData, DISCONNECT_EVENT); |
239 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | |
239 | 240 | } |
240 | 241 | |
241 | 242 | @Override |
242 | - public void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) { | |
243 | + public void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout) { | |
243 | 244 | if (inactivityTimeout <= 0L) { |
244 | 245 | return; |
245 | 246 | } |
... | ... | @@ -247,6 +248,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
247 | 248 | DeviceStateData stateData = getOrFetchDeviceStateData(deviceId); |
248 | 249 | stateData.getState().setInactivityTimeout(inactivityTimeout); |
249 | 250 | checkAndUpdateState(deviceId, stateData); |
251 | + cleanDeviceStateIfBelongsExternalPartition(tenantId, deviceId); | |
250 | 252 | } |
251 | 253 | |
252 | 254 | @Override |
... | ... | @@ -283,12 +285,10 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
283 | 285 | }, deviceStateExecutor); |
284 | 286 | } else if (proto.getUpdated()) { |
285 | 287 | DeviceStateData stateData = getOrFetchDeviceStateData(device.getId()); |
286 | - if (stateData != null) { | |
287 | - TbMsgMetaData md = new TbMsgMetaData(); | |
288 | - md.putValue("deviceName", device.getName()); | |
289 | - md.putValue("deviceType", device.getType()); | |
290 | - stateData.setMetaData(md); | |
291 | - } | |
288 | + TbMsgMetaData md = new TbMsgMetaData(); | |
289 | + md.putValue("deviceName", device.getName()); | |
290 | + md.putValue("deviceType", device.getType()); | |
291 | + stateData.setMetaData(md); | |
292 | 292 | } |
293 | 293 | } else { |
294 | 294 | //Device was probably deleted while message was in queue; |
... | ... | @@ -356,10 +356,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
356 | 356 | // We no longer manage current partition of devices; |
357 | 357 | removedPartitions.forEach(partition -> { |
358 | 358 | Set<DeviceId> devices = partitionedDevices.remove(partition); |
359 | - devices.forEach(deviceId -> { | |
360 | - deviceStates.remove(deviceId); | |
361 | - deviceLastSavedActivity.remove(deviceId); | |
362 | - }); | |
359 | + devices.forEach(this::cleanUpDeviceStateMap); | |
363 | 360 | }); |
364 | 361 | |
365 | 362 | addedPartitions.forEach(tpi -> partitionedDevices.computeIfAbsent(tpi, key -> ConcurrentHashMap.newKeySet())); |
... | ... | @@ -463,11 +460,12 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
463 | 460 | |
464 | 461 | void updateInactivityStateIfExpired() { |
465 | 462 | final long ts = System.currentTimeMillis(); |
466 | - log.debug("Calculating state updates for {} devices", deviceStates.size()); | |
467 | - Set<DeviceId> deviceIds = new HashSet<>(deviceStates.keySet()); | |
468 | - for (DeviceId deviceId : deviceIds) { | |
469 | - updateInactivityStateIfExpired(ts, deviceId); | |
470 | - } | |
463 | + partitionedDevices.forEach((tpi, deviceIds) -> { | |
464 | + log.debug("Calculating state updates. tpi {} for {} devices", tpi.getFullTopicName(), deviceIds.size()); | |
465 | + for (DeviceId deviceId : deviceIds) { | |
466 | + updateInactivityStateIfExpired(ts, deviceId); | |
467 | + } | |
468 | + }); | |
471 | 469 | } |
472 | 470 | |
473 | 471 | void updateInactivityStateIfExpired(long ts, DeviceId deviceId) { |
... | ... | @@ -488,8 +486,7 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
488 | 486 | } |
489 | 487 | } else { |
490 | 488 | log.debug("[{}] Device that belongs to other server is detected and removed.", deviceId); |
491 | - deviceStates.remove(deviceId); | |
492 | - deviceLastSavedActivity.remove(deviceId); | |
489 | + cleanUpDeviceStateMap(deviceId); | |
493 | 490 | } |
494 | 491 | } |
495 | 492 | |
... | ... | @@ -522,6 +519,15 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
522 | 519 | } |
523 | 520 | } |
524 | 521 | |
522 | + private void cleanDeviceStateIfBelongsExternalPartition(TenantId tenantId, final DeviceId deviceId) { | |
523 | + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); | |
524 | + if (!partitionedDevices.containsKey(tpi)) { | |
525 | + cleanUpDeviceStateMap(deviceId); | |
526 | + log.debug("[{}][{}] device belongs to external partition. Probably rebalancing is in progress. Topic: {}" | |
527 | + , tenantId, deviceId, tpi.getFullTopicName()); | |
528 | + } | |
529 | + } | |
530 | + | |
525 | 531 | private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, boolean added, boolean updated, boolean deleted) { |
526 | 532 | TransportProtos.DeviceStateServiceMsgProto.Builder builder = TransportProtos.DeviceStateServiceMsgProto.newBuilder(); |
527 | 533 | builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); |
... | ... | @@ -536,13 +542,16 @@ public class DefaultDeviceStateService extends TbApplicationEventListener<Partit |
536 | 542 | } |
537 | 543 | |
538 | 544 | private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) { |
539 | - deviceStates.remove(deviceId); | |
540 | - deviceLastSavedActivity.remove(deviceId); | |
545 | + cleanUpDeviceStateMap(deviceId); | |
541 | 546 | TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); |
542 | 547 | Set<DeviceId> deviceIdSet = partitionedDevices.get(tpi); |
543 | 548 | deviceIdSet.remove(deviceId); |
544 | 549 | } |
545 | 550 | |
551 | + private void cleanUpDeviceStateMap(DeviceId deviceId) { | |
552 | + deviceStates.remove(deviceId); | |
553 | + } | |
554 | + | |
546 | 555 | private ListenableFuture<DeviceStateData> fetchDeviceState(Device device) { |
547 | 556 | ListenableFuture<DeviceStateData> future; |
548 | 557 | if (persistToTelemetry) { | ... | ... |
... | ... | @@ -18,6 +18,7 @@ package org.thingsboard.server.service.state; |
18 | 18 | import org.springframework.context.ApplicationListener; |
19 | 19 | import org.thingsboard.server.common.data.Device; |
20 | 20 | import org.thingsboard.server.common.data.id.DeviceId; |
21 | +import org.thingsboard.server.common.data.id.TenantId; | |
21 | 22 | import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; |
22 | 23 | import org.thingsboard.server.gen.transport.TransportProtos; |
23 | 24 | import org.thingsboard.server.common.msg.queue.TbCallback; |
... | ... | @@ -33,13 +34,13 @@ public interface DeviceStateService extends ApplicationListener<PartitionChangeE |
33 | 34 | |
34 | 35 | void onDeviceDeleted(Device device); |
35 | 36 | |
36 | - void onDeviceConnect(DeviceId deviceId); | |
37 | + void onDeviceConnect(TenantId tenantId, DeviceId deviceId); | |
37 | 38 | |
38 | - void onDeviceActivity(DeviceId deviceId, long lastReportedActivityTime); | |
39 | + void onDeviceActivity(TenantId tenantId, DeviceId deviceId, long lastReportedActivityTime); | |
39 | 40 | |
40 | - void onDeviceDisconnect(DeviceId deviceId); | |
41 | + void onDeviceDisconnect(TenantId tenantId, DeviceId deviceId); | |
41 | 42 | |
42 | - void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout); | |
43 | + void onDeviceInactivityTimeoutUpdate(TenantId tenantId, DeviceId deviceId, long inactivityTimeout); | |
43 | 44 | |
44 | 45 | void onQueueMsg(TransportProtos.DeviceStateServiceMsgProto proto, TbCallback bytes); |
45 | 46 | ... | ... |
... | ... | @@ -224,7 +224,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene |
224 | 224 | return subscriptionUpdate; |
225 | 225 | }); |
226 | 226 | if (entityId.getEntityType() == EntityType.DEVICE) { |
227 | - updateDeviceInactivityTimeout(entityId, ts); | |
227 | + updateDeviceInactivityTimeout(tenantId, entityId, ts); | |
228 | 228 | } |
229 | 229 | callback.onSuccess(); |
230 | 230 | } |
... | ... | @@ -259,7 +259,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene |
259 | 259 | }); |
260 | 260 | if (entityId.getEntityType() == EntityType.DEVICE) { |
261 | 261 | if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { |
262 | - updateDeviceInactivityTimeout(entityId, attributes); | |
262 | + updateDeviceInactivityTimeout(tenantId, entityId, attributes); | |
263 | 263 | } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { |
264 | 264 | clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, |
265 | 265 | new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) |
... | ... | @@ -269,10 +269,10 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene |
269 | 269 | callback.onSuccess(); |
270 | 270 | } |
271 | 271 | |
272 | - private void updateDeviceInactivityTimeout(EntityId entityId, List<? extends KvEntry> kvEntries) { | |
272 | + private void updateDeviceInactivityTimeout(TenantId tenantId, EntityId entityId, List<? extends KvEntry> kvEntries) { | |
273 | 273 | for (KvEntry kvEntry : kvEntries) { |
274 | 274 | if (kvEntry.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { |
275 | - deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L)); | |
275 | + deviceStateService.onDeviceInactivityTimeoutUpdate(tenantId, new DeviceId(entityId.getId()), kvEntry.getLongValue().orElse(0L)); | |
276 | 276 | } |
277 | 277 | } |
278 | 278 | } | ... | ... |
... | ... | @@ -588,6 +588,7 @@ transport: |
588 | 588 | bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" |
589 | 589 | bind_port: "${MQTT_BIND_PORT:1883}" |
590 | 590 | timeout: "${MQTT_TIMEOUT:10000}" |
591 | + msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism | |
591 | 592 | netty: |
592 | 593 | leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" |
593 | 594 | boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" | ... | ... |
... | ... | @@ -27,6 +27,7 @@ import org.springframework.mock.web.MockMultipartFile; |
27 | 27 | import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; |
28 | 28 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; |
29 | 29 | import org.thingsboard.common.util.JacksonUtil; |
30 | +import org.thingsboard.common.util.ThingsBoardThreadFactory; | |
30 | 31 | import org.thingsboard.server.common.data.Device; |
31 | 32 | import org.thingsboard.server.common.data.DeviceProfile; |
32 | 33 | import org.thingsboard.server.common.data.DeviceProfileProvisionType; |
... | ... | @@ -261,7 +262,7 @@ public class AbstractLwM2MIntegrationTest extends AbstractWebsocketTest { |
261 | 262 | |
262 | 263 | @Before |
263 | 264 | public void beforeTest() throws Exception { |
264 | - executor = Executors.newScheduledThreadPool(10); | |
265 | + executor = Executors.newScheduledThreadPool(10, ThingsBoardThreadFactory.forName("test-lwm2m-scheduled")); | |
265 | 266 | loginTenantAdmin(); |
266 | 267 | |
267 | 268 | String[] resources = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; | ... | ... |
... | ... | @@ -16,6 +16,8 @@ |
16 | 16 | package org.thingsboard.server.transport.lwm2m; |
17 | 17 | |
18 | 18 | import com.fasterxml.jackson.core.type.TypeReference; |
19 | +import lombok.extern.slf4j.Slf4j; | |
20 | +import org.junit.After; | |
19 | 21 | import org.junit.Assert; |
20 | 22 | import org.junit.Test; |
21 | 23 | import org.thingsboard.server.common.data.Device; |
... | ... | @@ -23,16 +25,18 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCr |
23 | 25 | import org.thingsboard.server.common.data.kv.KvEntry; |
24 | 26 | import org.thingsboard.server.common.data.kv.TsKvEntry; |
25 | 27 | import org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus; |
26 | -import org.thingsboard.server.common.data.query.EntityKey; | |
27 | -import org.thingsboard.server.common.data.query.EntityKeyType; | |
28 | 28 | import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; |
29 | 29 | |
30 | 30 | import java.util.Arrays; |
31 | 31 | import java.util.Collections; |
32 | 32 | import java.util.Comparator; |
33 | 33 | import java.util.List; |
34 | +import java.util.UUID; | |
35 | +import java.util.concurrent.TimeUnit; | |
34 | 36 | import java.util.stream.Collectors; |
35 | 37 | |
38 | +import static org.awaitility.Awaitility.await; | |
39 | +import static org.hamcrest.Matchers.is; | |
36 | 40 | import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; |
37 | 41 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADED; |
38 | 42 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.DOWNLOADING; |
... | ... | @@ -43,8 +47,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDA |
43 | 47 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; |
44 | 48 | import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; |
45 | 49 | |
50 | +@Slf4j | |
46 | 51 | public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { |
47 | 52 | |
53 | + public static final int TIMEOUT = 30; | |
48 | 54 | private final String OTA_TRANSPORT_CONFIGURATION = "{\n" + |
49 | 55 | " \"observeAttr\": {\n" + |
50 | 56 | " \"keyName\": {\n" + |
... | ... | @@ -122,6 +128,15 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { |
122 | 128 | " \"type\": \"LWM2M\"\n" + |
123 | 129 | "}"; |
124 | 130 | |
131 | + LwM2MTestClient client = null; | |
132 | + | |
133 | + @After | |
134 | + public void tearDown() { | |
135 | + if (client != null) { | |
136 | + client.destroy(); | |
137 | + } | |
138 | + } | |
139 | + | |
125 | 140 | @Test |
126 | 141 | public void testConnectAndObserveTelemetry() throws Exception { |
127 | 142 | NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); |
... | ... | @@ -196,37 +211,68 @@ public class NoSecLwM2MIntegrationTest extends AbstractLwM2MIntegrationTest { |
196 | 211 | } |
197 | 212 | } |
198 | 213 | |
214 | + /** | |
215 | + * This is the example how to use the AWAITILITY instead Thread.sleep() | |
216 | + * Test will finish as fast as possible, but will await until TIMEOUT if a build machine is busy or slow | |
217 | + * Check the detailed log output to learn how Awaitility polling the API and when exactly expected result appears | |
218 | + * */ | |
199 | 219 | @Test |
200 | 220 | public void testSoftwareUpdateByObject9() throws Exception { |
201 | - LwM2MTestClient client = null; | |
202 | - try { | |
203 | - createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); | |
204 | - NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); | |
205 | - clientCredentials.setEndpoint("OTA_" + ENDPOINT); | |
206 | - Device device = createDevice(clientCredentials); | |
207 | - | |
208 | - device.setSoftwareId(createSoftware().getId()); | |
209 | - device = doPost("/api/device", device, Device.class); | |
221 | + //given | |
222 | + final List<OtaPackageUpdateStatus> expectedStatuses = Collections.unmodifiableList(Arrays.asList( | |
223 | + QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED)); | |
210 | 224 | |
211 | - Thread.sleep(1000); | |
212 | - | |
213 | - client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT); | |
214 | - client.init(SECURITY, COAP_CONFIG); | |
215 | - | |
216 | - Thread.sleep(3000); | |
217 | - | |
218 | - List<TsKvEntry> ts = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { | |
219 | - })); | |
220 | - | |
221 | - List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList()); | |
225 | + createDeviceProfile(OTA_TRANSPORT_CONFIGURATION); | |
226 | + NoSecClientCredentials clientCredentials = new NoSecClientCredentials(); | |
227 | + clientCredentials.setEndpoint("OTA_" + ENDPOINT); | |
228 | + final Device device = createDevice(clientCredentials); | |
229 | + device.setSoftwareId(createSoftware().getId()); | |
230 | + | |
231 | + log.warn("Saving by API " + device); | |
232 | + final Device savedDevice = doPost("/api/device", device, Device.class); | |
233 | + Assert.assertNotNull(savedDevice); | |
234 | + log.warn("Device saved by API {}", savedDevice); | |
235 | + | |
236 | + log.warn("AWAIT atMost {} SECONDS on get device by API...", TIMEOUT); | |
237 | + await() | |
238 | + .atMost(TIMEOUT, TimeUnit.SECONDS) | |
239 | + .until(() -> getDeviceFromAPI(device.getId().getId()), is(savedDevice)); | |
240 | + log.warn("Got device by API."); | |
241 | + | |
242 | + //when | |
243 | + log.warn("Init the client..."); | |
244 | + client = new LwM2MTestClient(executor, "OTA_" + ENDPOINT); | |
245 | + client.init(SECURITY, COAP_CONFIG); | |
246 | + log.warn("Init done"); | |
247 | + | |
248 | + log.warn("AWAIT atMost {} SECONDS on timeseries List<TsKvEntry> by API with list size {}...", TIMEOUT, expectedStatuses.size()); | |
249 | + await() | |
250 | + .atMost(30, TimeUnit.SECONDS) | |
251 | + .until(() -> getSwStateTelemetryFromAPI(device.getId().getId()) | |
252 | + .size(), is(expectedStatuses.size())); | |
253 | + log.warn("Got an expected await condition!"); | |
254 | + | |
255 | + //then | |
256 | + log.warn("Fetching ts for the final asserts"); | |
257 | + List<TsKvEntry> ts = getSwStateTelemetryFromAPI(device.getId().getId()); | |
258 | + log.warn("Got an ts {}", ts); | |
259 | + | |
260 | + List<OtaPackageUpdateStatus> statuses = ts.stream().sorted(Comparator.comparingLong(TsKvEntry::getTs)).map(KvEntry::getValueAsString).map(OtaPackageUpdateStatus::valueOf).collect(Collectors.toList()); | |
261 | + log.warn("Converted ts to statuses {}", statuses); | |
262 | + | |
263 | + Assert.assertEquals(expectedStatuses, statuses); | |
264 | + } | |
222 | 265 | |
223 | - List<OtaPackageUpdateStatus> expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); | |
266 | + private Device getDeviceFromAPI(UUID deviceId) throws Exception { | |
267 | + final Device device = doGet("/api/device/" + deviceId, Device.class); | |
268 | + log.warn("Fetched device by API for deviceId {}, device is {}", deviceId, device); | |
269 | + return device; | |
270 | + } | |
224 | 271 | |
225 | - Assert.assertEquals(expectedStatuses, statuses); | |
226 | - } finally { | |
227 | - if (client != null) { | |
228 | - client.destroy(); | |
229 | - } | |
230 | - } | |
272 | + private List<TsKvEntry> getSwStateTelemetryFromAPI(UUID deviceId) throws Exception { | |
273 | + final List<TsKvEntry> tsKvEntries = toTimeseries(doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?orderBy=ASC&keys=sw_state&startTs=0&endTs=" + System.currentTimeMillis(), new TypeReference<>() { | |
274 | + })); | |
275 | + log.warn("Fetched telemetry by API for deviceId {}, list size {}, tsKvEntries {}", deviceId, tsKvEntries.size(), tsKvEntries); | |
276 | + return tsKvEntries; | |
231 | 277 | } |
232 | 278 | } | ... | ... |
... | ... | @@ -77,25 +77,34 @@ public class TbKafkaProducerTemplate<T extends TbQueueMsg> implements TbQueuePro |
77 | 77 | |
78 | 78 | @Override |
79 | 79 | public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { |
80 | - createTopicIfNotExist(tpi); | |
81 | - String key = msg.getKey().toString(); | |
82 | - byte[] data = msg.getData(); | |
83 | - ProducerRecord<String, byte[]> record; | |
84 | - Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); | |
85 | - record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); | |
86 | - producer.send(record, (metadata, exception) -> { | |
87 | - if (exception == null) { | |
88 | - if (callback != null) { | |
89 | - callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata)); | |
90 | - } | |
91 | - } else { | |
92 | - if (callback != null) { | |
93 | - callback.onFailure(exception); | |
80 | + try { | |
81 | + createTopicIfNotExist(tpi); | |
82 | + String key = msg.getKey().toString(); | |
83 | + byte[] data = msg.getData(); | |
84 | + ProducerRecord<String, byte[]> record; | |
85 | + Iterable<Header> headers = msg.getHeaders().getData().entrySet().stream().map(e -> new RecordHeader(e.getKey(), e.getValue())).collect(Collectors.toList()); | |
86 | + record = new ProducerRecord<>(tpi.getFullTopicName(), null, key, data, headers); | |
87 | + producer.send(record, (metadata, exception) -> { | |
88 | + if (exception == null) { | |
89 | + if (callback != null) { | |
90 | + callback.onSuccess(new KafkaTbQueueMsgMetadata(metadata)); | |
91 | + } | |
94 | 92 | } else { |
95 | - log.warn("Producer template failure: {}", exception.getMessage(), exception); | |
93 | + if (callback != null) { | |
94 | + callback.onFailure(exception); | |
95 | + } else { | |
96 | + log.warn("Producer template failure: {}", exception.getMessage(), exception); | |
97 | + } | |
96 | 98 | } |
99 | + }); | |
100 | + } catch (Exception e) { | |
101 | + if (callback != null) { | |
102 | + callback.onFailure(e); | |
103 | + } else { | |
104 | + log.warn("Producer template failure (send method wrapper): {}", e.getMessage(), e); | |
97 | 105 | } |
98 | - }); | |
106 | + throw e; | |
107 | + } | |
99 | 108 | } |
100 | 109 | |
101 | 110 | private void createTopicIfNotExist(TopicPartitionInfo tpi) { | ... | ... |
... | ... | @@ -19,6 +19,9 @@ import lombok.RequiredArgsConstructor; |
19 | 19 | import lombok.extern.slf4j.Slf4j; |
20 | 20 | import org.eclipse.californium.elements.util.SslContextUtil; |
21 | 21 | import org.eclipse.californium.scandium.config.DtlsConnectorConfig; |
22 | +import org.eclipse.leshan.core.model.ObjectLoader; | |
23 | +import org.eclipse.leshan.core.model.ObjectModel; | |
24 | +import org.eclipse.leshan.core.model.StaticModel; | |
22 | 25 | import org.eclipse.leshan.server.bootstrap.BootstrapSessionManager; |
23 | 26 | import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer; |
24 | 27 | import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder; |
... | ... | @@ -26,6 +29,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
26 | 29 | import org.springframework.stereotype.Component; |
27 | 30 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore; |
28 | 31 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore; |
32 | +import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter; | |
29 | 33 | import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2mDefaultBootstrapSessionManager; |
30 | 34 | import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig; |
31 | 35 | import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; |
... | ... | @@ -38,6 +42,7 @@ import java.security.KeyStoreException; |
38 | 42 | import java.security.PrivateKey; |
39 | 43 | import java.security.PublicKey; |
40 | 44 | import java.security.cert.X509Certificate; |
45 | +import java.util.List; | |
41 | 46 | |
42 | 47 | import static org.thingsboard.server.transport.lwm2m.server.LwM2mNetworkConfig.getCoapConfig; |
43 | 48 | |
... | ... | @@ -79,12 +84,14 @@ public class LwM2MTransportBootstrapService { |
79 | 84 | builder.setCoapConfig(getCoapConfig(bootstrapConfig.getPort(), bootstrapConfig.getSecurePort(), serverConfig)); |
80 | 85 | |
81 | 86 | /* Define model provider (Create Models )*/ |
87 | + List<ObjectModel> models = ObjectLoader.loadDefault(); | |
88 | + builder.setModel(new StaticModel(models)); | |
82 | 89 | |
83 | 90 | /* Create credentials */ |
84 | 91 | this.setServerWithCredentials(builder); |
85 | 92 | |
86 | -// /** Set securityStore with new ConfigStore */ | |
87 | -// builder.setConfigStore(lwM2MInMemoryBootstrapConfigStore); | |
93 | + /* Set securityStore with new ConfigStore */ | |
94 | + builder.setConfigStore(new LwM2MInMemoryBootstrapConfigurationAdapter(lwM2MInMemoryBootstrapConfigStore)); | |
88 | 95 | |
89 | 96 | /* SecurityStore */ |
90 | 97 | builder.setSecurityStore(lwM2MBootstrapSecurityStore); | ... | ... |
... | ... | @@ -74,15 +74,19 @@ public class LwM2MBootstrapConfig implements Serializable { |
74 | 74 | configBs.servers.put(0, server0); |
75 | 75 | /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Bootstrap instance = 0 */ |
76 | 76 | this.bootstrapServer.setBootstrapServerIs(true); |
77 | - configBs.security.put(0, setServerSecurity(this.bootstrapServer.getHost(), this.bootstrapServer.getPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); | |
77 | + configBs.security.put(0, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.getSecurityHost(), this.lwm2mServer.getSecurityPort(), this.bootstrapServer.isBootstrapServerIs(), this.bootstrapServer.getSecurityMode(), this.bootstrapServer.getClientPublicKeyOrId(), this.bootstrapServer.getServerPublicKey(), this.bootstrapServer.getClientSecretKey(), this.bootstrapServer.getServerId())); | |
78 | 78 | /* Security Configuration (object 0) as defined in LWM2M 1.0.x TS. Server instance = 1 */ |
79 | - configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); | |
79 | + configBs.security.put(1, setServerSecurity(this.lwm2mServer.getHost(), this.lwm2mServer.getPort(), this.lwm2mServer.getSecurityHost(), this.lwm2mServer.getSecurityPort(), this.lwm2mServer.isBootstrapServerIs(), this.lwm2mServer.getSecurityMode(), this.lwm2mServer.getClientPublicKeyOrId(), this.lwm2mServer.getServerPublicKey(), this.lwm2mServer.getClientSecretKey(), this.lwm2mServer.getServerId())); | |
80 | 80 | return configBs; |
81 | 81 | } |
82 | 82 | |
83 | - private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { | |
83 | + private BootstrapConfig.ServerSecurity setServerSecurity(String host, Integer port, String securityHost, Integer securityPort, boolean bootstrapServer, SecurityMode securityMode, String clientPublicKey, String serverPublicKey, String secretKey, int serverId) { | |
84 | 84 | BootstrapConfig.ServerSecurity serverSecurity = new BootstrapConfig.ServerSecurity(); |
85 | - serverSecurity.uri = "coaps://" + host + ":" + Integer.toString(port); | |
85 | + if (securityMode.equals(SecurityMode.NO_SEC)) { | |
86 | + serverSecurity.uri = "coap://" + host + ":" + Integer.toString(port); | |
87 | + } else { | |
88 | + serverSecurity.uri = "coaps://" + securityHost + ":" + Integer.toString(securityPort); | |
89 | + } | |
86 | 90 | serverSecurity.bootstrapServer = bootstrapServer; |
87 | 91 | serverSecurity.securityMode = securityMode; |
88 | 92 | serverSecurity.publicKeyOrId = setPublicKeyOrId(clientPublicKey, securityMode); | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.transport.lwm2m.bootstrap.secure; | |
17 | + | |
18 | +import org.eclipse.leshan.server.bootstrap.BootstrapConfigStore; | |
19 | +import org.eclipse.leshan.server.bootstrap.BootstrapConfigurationStoreAdapter; | |
20 | + | |
21 | +public class LwM2MInMemoryBootstrapConfigurationAdapter extends BootstrapConfigurationStoreAdapter { | |
22 | + | |
23 | + public LwM2MInMemoryBootstrapConfigurationAdapter(BootstrapConfigStore store) { | |
24 | + super(store); | |
25 | + } | |
26 | + | |
27 | +} | ... | ... |
... | ... | @@ -31,24 +31,31 @@ public class LwM2MServerBootstrap { |
31 | 31 | |
32 | 32 | String host = "0.0.0.0"; |
33 | 33 | Integer port = 0; |
34 | + String securityHost = "0.0.0.0"; | |
35 | + Integer securityPort = 0; | |
34 | 36 | |
35 | 37 | SecurityMode securityMode = SecurityMode.NO_SEC; |
36 | 38 | |
37 | 39 | Integer serverId = 123; |
38 | 40 | boolean bootstrapServerIs = false; |
39 | 41 | |
40 | - public LwM2MServerBootstrap(){}; | |
42 | + public LwM2MServerBootstrap() { | |
43 | + } | |
44 | + | |
45 | + ; | |
41 | 46 | |
42 | 47 | public LwM2MServerBootstrap(LwM2MServerBootstrap bootstrapFromCredential, LwM2MServerBootstrap profileServerBootstrap) { |
43 | - this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId(); | |
44 | - this.clientSecretKey = bootstrapFromCredential.getClientSecretKey(); | |
45 | - this.serverPublicKey = profileServerBootstrap.getServerPublicKey(); | |
46 | - this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime(); | |
47 | - this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout(); | |
48 | - this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost(); | |
49 | - this.port = profileServerBootstrap.getPort(); | |
50 | - this.securityMode = profileServerBootstrap.getSecurityMode(); | |
51 | - this.serverId = profileServerBootstrap.getServerId(); | |
52 | - this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; | |
48 | + this.clientPublicKeyOrId = bootstrapFromCredential.getClientPublicKeyOrId(); | |
49 | + this.clientSecretKey = bootstrapFromCredential.getClientSecretKey(); | |
50 | + this.serverPublicKey = profileServerBootstrap.getServerPublicKey(); | |
51 | + this.clientHoldOffTime = profileServerBootstrap.getClientHoldOffTime(); | |
52 | + this.bootstrapServerAccountTimeout = profileServerBootstrap.getBootstrapServerAccountTimeout(); | |
53 | + this.host = (profileServerBootstrap.getHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getHost(); | |
54 | + this.port = profileServerBootstrap.getPort(); | |
55 | + this.securityHost = (profileServerBootstrap.getSecurityHost().equals("0.0.0.0")) ? "localhost" : profileServerBootstrap.getSecurityHost(); | |
56 | + this.securityPort = profileServerBootstrap.getSecurityPort(); | |
57 | + this.securityMode = profileServerBootstrap.getSecurityMode(); | |
58 | + this.serverId = profileServerBootstrap.getServerId(); | |
59 | + this.bootstrapServerIs = profileServerBootstrap.bootstrapServerIs; | |
53 | 60 | } |
54 | 61 | } | ... | ... |
... | ... | @@ -93,7 +93,12 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { |
93 | 93 | log.debug("Fetched clients from store: {}", fetchedClients); |
94 | 94 | fetchedClients.forEach(client -> { |
95 | 95 | lwM2mClientsByEndpoint.put(client.getEndpoint(), client); |
96 | - updateFetchedClient(nodeId, client); | |
96 | + try { | |
97 | + client.lock(); | |
98 | + updateFetchedClient(nodeId, client); | |
99 | + } finally { | |
100 | + client.unlock(); | |
101 | + } | |
97 | 102 | }); |
98 | 103 | } |
99 | 104 | |
... | ... | @@ -161,7 +166,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { |
161 | 166 | this.lwM2mClientsByRegistrationId.put(registration.getId(), client); |
162 | 167 | client.setState(LwM2MClientState.REGISTERED); |
163 | 168 | onUplink(client); |
164 | - if(!compareAndSetSleepFlag(client, false)){ | |
169 | + if (!compareAndSetSleepFlag(client, false)) { | |
165 | 170 | clientStore.put(client); |
166 | 171 | } |
167 | 172 | } finally { |
... | ... | @@ -311,7 +316,11 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { |
311 | 316 | public void update(LwM2mClient client) { |
312 | 317 | client.lock(); |
313 | 318 | try { |
314 | - clientStore.put(client); | |
319 | + if (client.getState().equals(LwM2MClientState.REGISTERED)) { | |
320 | + clientStore.put(client); | |
321 | + } else { | |
322 | + log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState()); | |
323 | + } | |
315 | 324 | } finally { |
316 | 325 | client.unlock(); |
317 | 326 | } | ... | ... |
... | ... | @@ -15,11 +15,13 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.transport.lwm2m.server.store; |
17 | 17 | |
18 | +import lombok.extern.slf4j.Slf4j; | |
18 | 19 | import org.nustaq.serialization.FSTConfiguration; |
19 | 20 | import org.springframework.data.redis.connection.RedisClusterConnection; |
20 | 21 | import org.springframework.data.redis.connection.RedisConnectionFactory; |
21 | 22 | import org.springframework.data.redis.core.Cursor; |
22 | 23 | import org.springframework.data.redis.core.ScanOptions; |
24 | +import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; | |
23 | 25 | import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; |
24 | 26 | |
25 | 27 | import java.util.ArrayList; |
... | ... | @@ -27,6 +29,7 @@ import java.util.HashSet; |
27 | 29 | import java.util.List; |
28 | 30 | import java.util.Set; |
29 | 31 | |
32 | +@Slf4j | |
30 | 33 | public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { |
31 | 34 | |
32 | 35 | private static final String CLIENT_EP = "CLIENT#EP#"; |
... | ... | @@ -76,9 +79,13 @@ public class TbRedisLwM2MClientStore implements TbLwM2MClientStore { |
76 | 79 | |
77 | 80 | @Override |
78 | 81 | public void put(LwM2mClient client) { |
79 | - byte[] clientSerialized = serializer.asByteArray(client); | |
80 | - try (var connection = connectionFactory.getConnection()) { | |
81 | - connection.getSet(getKey(client.getEndpoint()), clientSerialized); | |
82 | + if (client.getState().equals(LwM2MClientState.UNREGISTERED)) { | |
83 | + log.error("[{}] Client is in invalid state: {}!", client.getEndpoint(), client.getState(), new Exception()); | |
84 | + } else { | |
85 | + byte[] clientSerialized = serializer.asByteArray(client); | |
86 | + try (var connection = connectionFactory.getConnection()) { | |
87 | + connection.getSet(getKey(client.getEndpoint()), clientSerialized); | |
88 | + } | |
82 | 89 | } |
83 | 90 | } |
84 | 91 | ... | ... |
... | ... | @@ -89,6 +89,11 @@ |
89 | 89 | <scope>test</scope> |
90 | 90 | </dependency> |
91 | 91 | <dependency> |
92 | + <groupId>org.awaitility</groupId> | |
93 | + <artifactId>awaitility</artifactId> | |
94 | + <scope>test</scope> | |
95 | + </dependency> | |
96 | + <dependency> | |
92 | 97 | <groupId>org.mockito</groupId> |
93 | 98 | <artifactId>mockito-core</artifactId> |
94 | 99 | <scope>test</scope> | ... | ... |
... | ... | @@ -23,10 +23,15 @@ import org.springframework.beans.factory.annotation.Autowired; |
23 | 23 | import org.springframework.beans.factory.annotation.Value; |
24 | 24 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
25 | 25 | import org.springframework.stereotype.Component; |
26 | +import org.thingsboard.common.util.ThingsBoardExecutors; | |
26 | 27 | import org.thingsboard.server.common.transport.TransportContext; |
27 | 28 | import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; |
28 | 29 | import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; |
29 | 30 | |
31 | +import javax.annotation.PostConstruct; | |
32 | +import javax.annotation.PreDestroy; | |
33 | +import java.util.concurrent.ExecutorService; | |
34 | + | |
30 | 35 | /** |
31 | 36 | * Created by ashvayka on 04.10.18. |
32 | 37 | */ |
... | ... | @@ -59,4 +64,8 @@ public class MqttTransportContext extends TransportContext { |
59 | 64 | @Setter |
60 | 65 | private SslHandler sslHandler; |
61 | 66 | |
67 | + @Getter | |
68 | + @Value("${transport.mqtt.msg_queue_size_per_device_limit:100}") | |
69 | + private int messageQueueSizePerDeviceLimit; | |
70 | + | |
62 | 71 | } | ... | ... |
... | ... | @@ -123,9 +123,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
123 | 123 | private final SslHandler sslHandler; |
124 | 124 | private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; |
125 | 125 | |
126 | - private final DeviceSessionCtx deviceSessionCtx; | |
127 | - private volatile InetSocketAddress address; | |
128 | - private volatile GatewaySessionHandler gatewaySessionHandler; | |
126 | + final DeviceSessionCtx deviceSessionCtx; | |
127 | + volatile InetSocketAddress address; | |
128 | + volatile GatewaySessionHandler gatewaySessionHandler; | |
129 | 129 | |
130 | 130 | private final ConcurrentHashMap<String, String> otaPackSessions; |
131 | 131 | private final ConcurrentHashMap<String, Integer> chunkSizes; |
... | ... | @@ -164,8 +164,8 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
164 | 164 | } |
165 | 165 | } |
166 | 166 | |
167 | - private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { | |
168 | - address = (InetSocketAddress) ctx.channel().remoteAddress(); | |
167 | + void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) { | |
168 | + address = getAddress(ctx); | |
169 | 169 | if (msg.fixedHeader() == null) { |
170 | 170 | log.info("[{}:{}] Invalid message received", address.getHostName(), address.getPort()); |
171 | 171 | processDisconnect(ctx); |
... | ... | @@ -177,10 +177,14 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
177 | 177 | } else if (deviceSessionCtx.isProvisionOnly()) { |
178 | 178 | processProvisionSessionMsg(ctx, msg); |
179 | 179 | } else { |
180 | - processRegularSessionMsg(ctx, msg); | |
180 | + enqueueRegularSessionMsg(ctx, msg); | |
181 | 181 | } |
182 | 182 | } |
183 | 183 | |
184 | + InetSocketAddress getAddress(ChannelHandlerContext ctx) { | |
185 | + return (InetSocketAddress) ctx.channel().remoteAddress(); | |
186 | + } | |
187 | + | |
184 | 188 | private void processProvisionSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { |
185 | 189 | switch (msg.fixedHeader().messageType()) { |
186 | 190 | case PUBLISH: |
... | ... | @@ -223,7 +227,42 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
223 | 227 | } |
224 | 228 | } |
225 | 229 | |
226 | - private void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | |
230 | + void enqueueRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | |
231 | + final int queueSize = deviceSessionCtx.getMsgQueueSize().incrementAndGet(); | |
232 | + if (queueSize > context.getMessageQueueSizePerDeviceLimit()) { | |
233 | + log.warn("Closing current session because msq queue size for device {} exceed limit {} with msgQueueSize counter {} and actual queue size {}", | |
234 | + deviceSessionCtx.getDeviceId(), context.getMessageQueueSizePerDeviceLimit(), queueSize, deviceSessionCtx.getMsgQueue().size()); | |
235 | + ctx.close(); | |
236 | + return; | |
237 | + } | |
238 | + | |
239 | + deviceSessionCtx.getMsgQueue().add(msg); | |
240 | + processMsgQueue(ctx); //Under the normal conditions the msg queue will contain 0 messages. Many messages will be processed on device connect event in separate thread pool | |
241 | + } | |
242 | + | |
243 | + void processMsgQueue(ChannelHandlerContext ctx) { | |
244 | + if (!deviceSessionCtx.isConnected()) { | |
245 | + log.trace("[{}][{}] Postpone processing msg due to device is not connected. Msg queue size is {}", sessionId, deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size()); | |
246 | + return; | |
247 | + } | |
248 | + while (!deviceSessionCtx.getMsgQueue().isEmpty()) { | |
249 | + if (deviceSessionCtx.getMsgQueueProcessorLock().tryLock()) { | |
250 | + try { | |
251 | + MqttMessage msg; | |
252 | + while ((msg = deviceSessionCtx.getMsgQueue().poll()) != null) { | |
253 | + deviceSessionCtx.getMsgQueueSize().decrementAndGet(); | |
254 | + processRegularSessionMsg(ctx, msg); | |
255 | + } | |
256 | + } finally { | |
257 | + deviceSessionCtx.getMsgQueueProcessorLock().unlock(); | |
258 | + } | |
259 | + } else { | |
260 | + return; | |
261 | + } | |
262 | + } | |
263 | + } | |
264 | + | |
265 | + void processRegularSessionMsg(ChannelHandlerContext ctx, MqttMessage msg) { | |
227 | 266 | switch (msg.fixedHeader().messageType()) { |
228 | 267 | case PUBLISH: |
229 | 268 | processPublish(ctx, (MqttPublishMessage) msg); |
... | ... | @@ -304,6 +343,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
304 | 343 | } |
305 | 344 | } catch (RuntimeException | AdaptorException e) { |
306 | 345 | log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); |
346 | + ctx.close(); | |
307 | 347 | } |
308 | 348 | } |
309 | 349 | |
... | ... | @@ -588,7 +628,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
588 | 628 | return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader); |
589 | 629 | } |
590 | 630 | |
591 | - private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { | |
631 | + void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) { | |
592 | 632 | log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier()); |
593 | 633 | String userName = msg.payload().userName(); |
594 | 634 | String clientId = msg.payload().clientIdentifier(); |
... | ... | @@ -674,7 +714,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
674 | 714 | return null; |
675 | 715 | } |
676 | 716 | |
677 | - private void processDisconnect(ChannelHandlerContext ctx) { | |
717 | + void processDisconnect(ChannelHandlerContext ctx) { | |
678 | 718 | ctx.close(); |
679 | 719 | log.info("[{}] Client disconnected!", sessionId); |
680 | 720 | doDisconnect(); |
... | ... | @@ -761,6 +801,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
761 | 801 | } |
762 | 802 | deviceSessionCtx.setDisconnected(); |
763 | 803 | } |
804 | + | |
805 | + if (!deviceSessionCtx.getMsgQueue().isEmpty()) { | |
806 | + log.warn("doDisconnect for device {} but unprocessed messages {} left in the msg queue", deviceSessionCtx.getDeviceId(), deviceSessionCtx.getMsgQueue().size()); | |
807 | + deviceSessionCtx.getMsgQueue().clear(); | |
808 | + } | |
764 | 809 | } |
765 | 810 | |
766 | 811 | |
... | ... | @@ -778,7 +823,9 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement |
778 | 823 | SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); |
779 | 824 | checkGatewaySession(sessionMetaData); |
780 | 825 | ctx.writeAndFlush(createMqttConnAckMsg(CONNECTION_ACCEPTED, connectMessage)); |
826 | + deviceSessionCtx.setConnected(true); | |
781 | 827 | log.info("[{}] Client connected!", sessionId); |
828 | + transportService.getCallbackExecutor().execute(() -> processMsgQueue(ctx)); //this callback will execute in Producer worker thread and hard or blocking work have to be submitted to the separate thread. | |
782 | 829 | } |
783 | 830 | |
784 | 831 | @Override | ... | ... |
... | ... | @@ -18,6 +18,7 @@ package org.thingsboard.server.transport.mqtt.session; |
18 | 18 | import com.google.protobuf.Descriptors; |
19 | 19 | import com.google.protobuf.DynamicMessage; |
20 | 20 | import io.netty.channel.ChannelHandlerContext; |
21 | +import io.netty.handler.codec.mqtt.MqttMessage; | |
21 | 22 | import lombok.Getter; |
22 | 23 | import lombok.Setter; |
23 | 24 | import lombok.extern.slf4j.Slf4j; |
... | ... | @@ -35,8 +36,11 @@ import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; |
35 | 36 | import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; |
36 | 37 | |
37 | 38 | import java.util.UUID; |
39 | +import java.util.concurrent.ConcurrentLinkedQueue; | |
38 | 40 | import java.util.concurrent.ConcurrentMap; |
39 | 41 | import java.util.concurrent.atomic.AtomicInteger; |
42 | +import java.util.concurrent.locks.Lock; | |
43 | +import java.util.concurrent.locks.ReentrantLock; | |
40 | 44 | |
41 | 45 | /** |
42 | 46 | * @author Andrew Shvayka |
... | ... | @@ -45,14 +49,24 @@ import java.util.concurrent.atomic.AtomicInteger; |
45 | 49 | public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { |
46 | 50 | |
47 | 51 | @Getter |
52 | + @Setter | |
48 | 53 | private ChannelHandlerContext channel; |
49 | 54 | |
50 | 55 | @Getter |
51 | - private MqttTransportContext context; | |
56 | + private final MqttTransportContext context; | |
52 | 57 | |
53 | 58 | private final AtomicInteger msgIdSeq = new AtomicInteger(0); |
54 | 59 | |
55 | 60 | @Getter |
61 | + private final ConcurrentLinkedQueue<MqttMessage> msgQueue = new ConcurrentLinkedQueue<>(); | |
62 | + | |
63 | + @Getter | |
64 | + private final Lock msgQueueProcessorLock = new ReentrantLock(); | |
65 | + | |
66 | + @Getter | |
67 | + private final AtomicInteger msgQueueSize = new AtomicInteger(0); | |
68 | + | |
69 | + @Getter | |
56 | 70 | @Setter |
57 | 71 | private boolean provisionOnly = false; |
58 | 72 | |
... | ... | @@ -73,10 +87,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { |
73 | 87 | this.context = context; |
74 | 88 | } |
75 | 89 | |
76 | - public void setChannel(ChannelHandlerContext channel) { | |
77 | - this.channel = channel; | |
78 | - } | |
79 | - | |
80 | 90 | public int nextMsgId() { |
81 | 91 | return msgIdSeq.incrementAndGet(); |
82 | 92 | } | ... | ... |
... | ... | @@ -60,6 +60,7 @@ public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext imple |
60 | 60 | .setDeviceProfileIdLSB(deviceInfo.getDeviceProfileId().getId().getLeastSignificantBits()) |
61 | 61 | .build()); |
62 | 62 | setDeviceInfo(deviceInfo); |
63 | + setConnected(true); | |
63 | 64 | setDeviceProfile(deviceProfile); |
64 | 65 | this.transportService = transportService; |
65 | 66 | } | ... | ... |
... | ... | @@ -34,6 +34,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; |
34 | 34 | import io.netty.handler.codec.mqtt.MqttPublishMessage; |
35 | 35 | import lombok.extern.slf4j.Slf4j; |
36 | 36 | import org.springframework.util.CollectionUtils; |
37 | +import org.springframework.util.ConcurrentReferenceHashMap; | |
37 | 38 | import org.springframework.util.StringUtils; |
38 | 39 | import org.thingsboard.server.common.data.id.DeviceId; |
39 | 40 | import org.thingsboard.server.common.transport.TransportService; |
... | ... | @@ -66,6 +67,8 @@ import java.util.concurrent.ConcurrentMap; |
66 | 67 | import java.util.concurrent.locks.Lock; |
67 | 68 | import java.util.concurrent.locks.ReentrantLock; |
68 | 69 | |
70 | +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; | |
71 | + | |
69 | 72 | /** |
70 | 73 | * Created by ashvayka on 19.01.17. |
71 | 74 | */ |
... | ... | @@ -82,7 +85,7 @@ public class GatewaySessionHandler { |
82 | 85 | private final UUID sessionId; |
83 | 86 | private final ConcurrentMap<String, Lock> deviceCreationLockMap; |
84 | 87 | private final ConcurrentMap<String, GatewayDeviceSessionCtx> devices; |
85 | - private final ConcurrentMap<String, SettableFuture<GatewayDeviceSessionCtx>> deviceFutures; | |
88 | + private final ConcurrentMap<String, ListenableFuture<GatewayDeviceSessionCtx>> deviceFutures; | |
86 | 89 | private final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; |
87 | 90 | private final ChannelHandlerContext channel; |
88 | 91 | private final DeviceSessionCtx deviceSessionCtx; |
... | ... | @@ -95,11 +98,15 @@ public class GatewaySessionHandler { |
95 | 98 | this.sessionId = sessionId; |
96 | 99 | this.devices = new ConcurrentHashMap<>(); |
97 | 100 | this.deviceFutures = new ConcurrentHashMap<>(); |
98 | - this.deviceCreationLockMap = new ConcurrentHashMap<>(); | |
101 | + this.deviceCreationLockMap = createWeakMap(); | |
99 | 102 | this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); |
100 | 103 | this.channel = deviceSessionCtx.getChannel(); |
101 | 104 | } |
102 | 105 | |
106 | + ConcurrentReferenceHashMap<String, Lock> createWeakMap() { | |
107 | + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); | |
108 | + } | |
109 | + | |
103 | 110 | public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { |
104 | 111 | if (isJsonPayloadType()) { |
105 | 112 | onDeviceConnectJson(mqttMsg); |
... | ... | @@ -228,21 +235,22 @@ public class GatewaySessionHandler { |
228 | 235 | if (result == null) { |
229 | 236 | return getDeviceCreationFuture(deviceName, deviceType); |
230 | 237 | } else { |
231 | - return toCompletedFuture(result); | |
238 | + return Futures.immediateFuture(result); | |
232 | 239 | } |
233 | 240 | } finally { |
234 | 241 | deviceCreationLock.unlock(); |
235 | 242 | } |
236 | 243 | } else { |
237 | - return toCompletedFuture(result); | |
244 | + return Futures.immediateFuture(result); | |
238 | 245 | } |
239 | 246 | } |
240 | 247 | |
241 | 248 | private ListenableFuture<GatewayDeviceSessionCtx> getDeviceCreationFuture(String deviceName, String deviceType) { |
242 | - SettableFuture<GatewayDeviceSessionCtx> future = deviceFutures.get(deviceName); | |
243 | - if (future == null) { | |
244 | - final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create(); | |
245 | - deviceFutures.put(deviceName, futureToSet); | |
249 | + final SettableFuture<GatewayDeviceSessionCtx> futureToSet = SettableFuture.create(); | |
250 | + ListenableFuture<GatewayDeviceSessionCtx> future = deviceFutures.putIfAbsent(deviceName, futureToSet); | |
251 | + if (future != null) { | |
252 | + return future; | |
253 | + } | |
246 | 254 | try { |
247 | 255 | transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() |
248 | 256 | .setDeviceName(deviceName) |
... | ... | @@ -282,15 +290,6 @@ public class GatewaySessionHandler { |
282 | 290 | deviceFutures.remove(deviceName); |
283 | 291 | throw e; |
284 | 292 | } |
285 | - } else { | |
286 | - return future; | |
287 | - } | |
288 | - } | |
289 | - | |
290 | - private ListenableFuture<GatewayDeviceSessionCtx> toCompletedFuture(GatewayDeviceSessionCtx result) { | |
291 | - SettableFuture<GatewayDeviceSessionCtx> future = SettableFuture.create(); | |
292 | - future.set(result); | |
293 | - return future; | |
294 | 293 | } |
295 | 294 | |
296 | 295 | private int getMsgId(MqttPublishMessage mqttMsg) { |
... | ... | @@ -353,6 +352,7 @@ public class GatewaySessionHandler { |
353 | 352 | processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); |
354 | 353 | } catch (Throwable e) { |
355 | 354 | log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); |
355 | + channel.close(); | |
356 | 356 | } |
357 | 357 | } |
358 | 358 | |
... | ... | @@ -384,6 +384,7 @@ public class GatewaySessionHandler { |
384 | 384 | processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); |
385 | 385 | } catch (Throwable e) { |
386 | 386 | log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); |
387 | + channel.close(); | |
387 | 388 | } |
388 | 389 | } |
389 | 390 | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.transport.mqtt; | |
17 | + | |
18 | +import io.netty.buffer.ByteBuf; | |
19 | +import io.netty.buffer.EmptyByteBuf; | |
20 | +import io.netty.buffer.PooledByteBufAllocator; | |
21 | +import io.netty.channel.ChannelHandlerContext; | |
22 | +import io.netty.handler.codec.mqtt.MqttConnectMessage; | |
23 | +import io.netty.handler.codec.mqtt.MqttConnectPayload; | |
24 | +import io.netty.handler.codec.mqtt.MqttConnectVariableHeader; | |
25 | +import io.netty.handler.codec.mqtt.MqttFixedHeader; | |
26 | +import io.netty.handler.codec.mqtt.MqttMessage; | |
27 | +import io.netty.handler.codec.mqtt.MqttMessageType; | |
28 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | |
29 | +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; | |
30 | +import io.netty.handler.codec.mqtt.MqttQoS; | |
31 | +import io.netty.handler.ssl.SslHandler; | |
32 | +import lombok.extern.slf4j.Slf4j; | |
33 | +import org.junit.After; | |
34 | +import org.junit.Before; | |
35 | +import org.junit.Test; | |
36 | +import org.junit.runner.RunWith; | |
37 | +import org.mockito.Mock; | |
38 | +import org.mockito.junit.MockitoJUnitRunner; | |
39 | +import org.thingsboard.common.util.ThingsBoardThreadFactory; | |
40 | + | |
41 | +import java.net.InetSocketAddress; | |
42 | +import java.nio.charset.StandardCharsets; | |
43 | +import java.util.List; | |
44 | +import java.util.concurrent.CountDownLatch; | |
45 | +import java.util.concurrent.ExecutorService; | |
46 | +import java.util.concurrent.Executors; | |
47 | +import java.util.concurrent.TimeUnit; | |
48 | +import java.util.concurrent.atomic.AtomicInteger; | |
49 | +import java.util.stream.Collectors; | |
50 | +import java.util.stream.Stream; | |
51 | + | |
52 | +import static org.hamcrest.MatcherAssert.assertThat; | |
53 | +import static org.hamcrest.Matchers.contains; | |
54 | +import static org.hamcrest.Matchers.empty; | |
55 | +import static org.hamcrest.Matchers.greaterThan; | |
56 | +import static org.hamcrest.Matchers.is; | |
57 | +import static org.junit.Assert.fail; | |
58 | +import static org.mockito.ArgumentMatchers.any; | |
59 | +import static org.mockito.BDDMockito.willDoNothing; | |
60 | +import static org.mockito.BDDMockito.willReturn; | |
61 | +import static org.mockito.Mockito.never; | |
62 | +import static org.mockito.Mockito.spy; | |
63 | +import static org.mockito.Mockito.times; | |
64 | +import static org.mockito.Mockito.verify; | |
65 | + | |
66 | +@Slf4j | |
67 | +@RunWith(MockitoJUnitRunner.class) | |
68 | +public class MqttTransportHandlerTest { | |
69 | + | |
70 | + public static final int MSG_QUEUE_LIMIT = 10; | |
71 | + public static final InetSocketAddress IP_ADDR = new InetSocketAddress("127.0.0.1", 9876); | |
72 | + public static final int TIMEOUT = 30; | |
73 | + | |
74 | + @Mock | |
75 | + MqttTransportContext context; | |
76 | + @Mock | |
77 | + SslHandler sslHandler; | |
78 | + @Mock | |
79 | + ChannelHandlerContext ctx; | |
80 | + | |
81 | + AtomicInteger packedId = new AtomicInteger(); | |
82 | + ExecutorService executor; | |
83 | + MqttTransportHandler handler; | |
84 | + | |
85 | + @Before | |
86 | + public void setUp() throws Exception { | |
87 | + willReturn(MSG_QUEUE_LIMIT).given(context).getMessageQueueSizePerDeviceLimit(); | |
88 | + | |
89 | + handler = spy(new MqttTransportHandler(context, sslHandler)); | |
90 | + willReturn(IP_ADDR).given(handler).getAddress(any()); | |
91 | + } | |
92 | + | |
93 | + @After | |
94 | + public void tearDown() { | |
95 | + if (executor != null) { | |
96 | + executor.shutdownNow(); | |
97 | + } | |
98 | + } | |
99 | + | |
100 | + MqttConnectMessage getMqttConnectMessage() { | |
101 | + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, true, MqttQoS.AT_LEAST_ONCE, false, 123); | |
102 | + MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader("device", packedId.incrementAndGet(), true, true, true, 1, true, false, 60); | |
103 | + MqttConnectPayload payload = new MqttConnectPayload("clientId", "topic", "message".getBytes(StandardCharsets.UTF_8), "username", "password".getBytes(StandardCharsets.UTF_8)); | |
104 | + return new MqttConnectMessage(mqttFixedHeader, variableHeader, payload); | |
105 | + } | |
106 | + | |
107 | + MqttPublishMessage getMqttPublishMessage() { | |
108 | + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, MqttQoS.AT_LEAST_ONCE, false, 123); | |
109 | + MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader("v1/gateway/telemetry", packedId.incrementAndGet()); | |
110 | + ByteBuf payload = new EmptyByteBuf(new PooledByteBufAllocator()); | |
111 | + return new MqttPublishMessage(mqttFixedHeader, variableHeader, payload); | |
112 | + } | |
113 | + | |
114 | + @Test | |
115 | + public void givenMessageWithoutFixedHeader_whenProcessMqttMsg_thenProcessDisconnect() { | |
116 | + MqttFixedHeader mqttFixedHeader = null; | |
117 | + MqttMessage msg = new MqttMessage(mqttFixedHeader); | |
118 | + willDoNothing().given(handler).processDisconnect(ctx); | |
119 | + | |
120 | + handler.processMqttMsg(ctx, msg); | |
121 | + | |
122 | + assertThat(handler.address, is(IP_ADDR)); | |
123 | + verify(handler, times(1)).processDisconnect(ctx); | |
124 | + } | |
125 | + | |
126 | + @Test | |
127 | + public void givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect() { | |
128 | + MqttConnectMessage msg = getMqttConnectMessage(); | |
129 | + willDoNothing().given(handler).processConnect(ctx, msg); | |
130 | + | |
131 | + handler.processMqttMsg(ctx, msg); | |
132 | + | |
133 | + assertThat(handler.address, is(IP_ADDR)); | |
134 | + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); | |
135 | + verify(handler, never()).processDisconnect(any()); | |
136 | + verify(handler, times(1)).processConnect(ctx, msg); | |
137 | + } | |
138 | + | |
139 | + @Test | |
140 | + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenOK() { | |
141 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | |
142 | + messages.forEach(msg -> handler.enqueueRegularSessionMsg(ctx, msg)); | |
143 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT)); | |
144 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray())); | |
145 | + } | |
146 | + | |
147 | + @Test | |
148 | + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenCtxClose() { | |
149 | + final int limit = MSG_QUEUE_LIMIT + 1; | |
150 | + willDoNothing().given(handler).processMsgQueue(ctx); | |
151 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(limit).collect(Collectors.toList()); | |
152 | + | |
153 | + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg)); | |
154 | + | |
155 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(limit)); | |
156 | + verify(handler, times(limit)).enqueueRegularSessionMsg(any(), any()); | |
157 | + verify(handler, times(MSG_QUEUE_LIMIT)).processMsgQueue(any()); | |
158 | + verify(ctx, times(1)).close(); | |
159 | + } | |
160 | + | |
161 | + @Test | |
162 | + public void givenMqttConnectMessageAndPublishImmediately_whenProcessMqttMsg_thenEnqueueRegularSessionMsg() { | |
163 | + givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect(); | |
164 | + | |
165 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | |
166 | + | |
167 | + messages.forEach((msg) -> handler.processMqttMsg(ctx, msg)); | |
168 | + | |
169 | + assertThat(handler.address, is(IP_ADDR)); | |
170 | + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); | |
171 | + assertThat(handler.deviceSessionCtx.isConnected(), is(false)); | |
172 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT)); | |
173 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray())); | |
174 | + verify(handler, never()).processDisconnect(any()); | |
175 | + verify(handler, times(1)).processConnect(any(), any()); | |
176 | + verify(handler, times(MSG_QUEUE_LIMIT)).enqueueRegularSessionMsg(any(), any()); | |
177 | + verify(handler, never()).processRegularSessionMsg(any(), any()); | |
178 | + messages.forEach((msg) -> verify(handler, times(1)).enqueueRegularSessionMsg(ctx, msg)); | |
179 | + } | |
180 | + | |
181 | + @Test | |
182 | + public void givenMessageQueue_whenProcessMqttMsgConcurrently_thenEnqueueRegularSessionMsg() throws InterruptedException { | |
183 | + //given | |
184 | + assertThat(handler.deviceSessionCtx.isConnected(), is(false)); | |
185 | + assertThat(MSG_QUEUE_LIMIT, greaterThan(2)); | |
186 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | |
187 | + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg)); | |
188 | + willDoNothing().given(handler).processRegularSessionMsg(any(), any()); | |
189 | + executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getName())); | |
190 | + | |
191 | + CountDownLatch readyLatch = new CountDownLatch(MSG_QUEUE_LIMIT); | |
192 | + CountDownLatch startLatch = new CountDownLatch(1); | |
193 | + CountDownLatch finishLatch = new CountDownLatch(MSG_QUEUE_LIMIT); | |
194 | + | |
195 | + Stream.iterate(0, i -> i + 1).limit(MSG_QUEUE_LIMIT).forEach(x -> | |
196 | + executor.submit(() -> { | |
197 | + try { | |
198 | + readyLatch.countDown(); | |
199 | + assertThat(startLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | |
200 | + handler.processMsgQueue(ctx); | |
201 | + finishLatch.countDown(); | |
202 | + } catch (Exception e) { | |
203 | + log.error("Failed to run processMsgQueue", e); | |
204 | + fail("Failed to run processMsgQueue"); | |
205 | + } | |
206 | + })); | |
207 | + | |
208 | + //when | |
209 | + assertThat(readyLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | |
210 | + handler.deviceSessionCtx.setConnected(true); | |
211 | + startLatch.countDown(); | |
212 | + assertThat(finishLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | |
213 | + | |
214 | + //then | |
215 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(0)); | |
216 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), empty()); | |
217 | + verify(handler, times(MSG_QUEUE_LIMIT)).processRegularSessionMsg(any(), any()); | |
218 | + messages.forEach((msg) -> verify(handler, times(1)).processRegularSessionMsg(ctx, msg)); | |
219 | + } | |
220 | + | |
221 | +} | |
\ No newline at end of file | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.transport.mqtt.session; | |
17 | + | |
18 | +import org.junit.Test; | |
19 | + | |
20 | +import java.util.WeakHashMap; | |
21 | +import java.util.concurrent.ConcurrentMap; | |
22 | +import java.util.concurrent.TimeUnit; | |
23 | +import java.util.concurrent.locks.Lock; | |
24 | +import java.util.concurrent.locks.ReentrantLock; | |
25 | + | |
26 | +import static org.awaitility.Awaitility.await; | |
27 | +import static org.junit.Assert.assertTrue; | |
28 | +import static org.mockito.BDDMockito.willCallRealMethod; | |
29 | +import static org.mockito.Mockito.mock; | |
30 | + | |
31 | +public class GatewaySessionHandlerTest { | |
32 | + | |
33 | + @Test | |
34 | + public void givenWeakHashMap_WhenGC_thenMapIsEmpty() { | |
35 | + WeakHashMap<String, Lock> map = new WeakHashMap<>(); | |
36 | + | |
37 | + String deviceName = new String("device"); //constants are static and doesn't affected by GC, so use new instead | |
38 | + map.put(deviceName, new ReentrantLock()); | |
39 | + assertTrue(map.containsKey(deviceName)); | |
40 | + | |
41 | + deviceName = null; | |
42 | + System.gc(); | |
43 | + | |
44 | + await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device")); | |
45 | + } | |
46 | + | |
47 | + @Test | |
48 | + public void givenConcurrentReferenceHashMap_WhenGC_thenMapIsEmpty() { | |
49 | + GatewaySessionHandler gsh = mock(GatewaySessionHandler.class); | |
50 | + willCallRealMethod().given(gsh).createWeakMap(); | |
51 | + | |
52 | + ConcurrentMap<String, Lock> map = gsh.createWeakMap(); | |
53 | + map.put("device", new ReentrantLock()); | |
54 | + assertTrue(map.containsKey("device")); | |
55 | + | |
56 | + System.gc(); | |
57 | + | |
58 | + await().atMost(10, TimeUnit.SECONDS).until(() -> !map.containsKey("device")); | |
59 | + } | |
60 | + | |
61 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -188,6 +188,7 @@ public class SnmpTransportContext extends TransportContext { |
188 | 188 | |
189 | 189 | deviceSessionContext.setSessionInfo(sessionInfo); |
190 | 190 | deviceSessionContext.setDeviceInfo(msg.getDeviceInfo()); |
191 | + deviceSessionContext.setConnected(true); | |
191 | 192 | } else { |
192 | 193 | log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId()); |
193 | 194 | } | ... | ... |
... | ... | @@ -56,6 +56,8 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC |
56 | 56 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; |
57 | 57 | import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; |
58 | 58 | |
59 | +import java.util.concurrent.ExecutorService; | |
60 | + | |
59 | 61 | /** |
60 | 62 | * Created by ashvayka on 04.10.18. |
61 | 63 | */ |
... | ... | @@ -131,4 +133,6 @@ public interface TransportService { |
131 | 133 | void log(SessionInfoProto sessionInfo, String msg); |
132 | 134 | |
133 | 135 | void notifyAboutUplink(SessionInfoProto sessionInfo, TransportProtos.UplinkNotificationMsg build, TransportServiceCallback<Void> empty); |
136 | + | |
137 | + ExecutorService getCallbackExecutor(); | |
134 | 138 | } | ... | ... |
... | ... | @@ -46,6 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext { |
46 | 46 | @Setter |
47 | 47 | private volatile TransportProtos.SessionInfoProto sessionInfo; |
48 | 48 | |
49 | + @Setter | |
49 | 50 | private volatile boolean connected; |
50 | 51 | |
51 | 52 | public DeviceId getDeviceId() { |
... | ... | @@ -54,7 +55,6 @@ public abstract class DeviceAwareSessionContext implements SessionContext { |
54 | 55 | |
55 | 56 | public void setDeviceInfo(TransportDeviceInfo deviceInfo) { |
56 | 57 | this.deviceInfo = deviceInfo; |
57 | - this.connected = true; | |
58 | 58 | this.deviceId = deviceInfo.getDeviceId(); |
59 | 59 | } |
60 | 60 | ... | ... |
... | ... | @@ -52,7 +52,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { |
52 | 52 | public AdminSettings saveAdminSettings(TenantId tenantId, AdminSettings adminSettings) { |
53 | 53 | log.trace("Executing saveAdminSettings [{}]", adminSettings); |
54 | 54 | adminSettingsValidator.validate(adminSettings, data -> tenantId); |
55 | - if (adminSettings.getKey().equals("mail") && "".equals(adminSettings.getJsonValue().get("password").asText())) { | |
55 | + if(adminSettings.getKey().equals("mail") && !adminSettings.getJsonValue().has("password")) { | |
56 | 56 | AdminSettings mailSettings = findAdminSettingsByKey(tenantId, "mail"); |
57 | 57 | if (mailSettings != null) { |
58 | 58 | ((ObjectNode) adminSettings.getJsonValue()).put("password", mailSettings.getJsonValue().get("password").asText()); |
... | ... | @@ -61,7 +61,7 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { |
61 | 61 | |
62 | 62 | return adminSettingsDao.save(tenantId, adminSettings); |
63 | 63 | } |
64 | - | |
64 | + | |
65 | 65 | private DataValidator<AdminSettings> adminSettingsValidator = |
66 | 66 | new DataValidator<AdminSettings>() { |
67 | 67 | ... | ... |
... | ... | @@ -100,7 +100,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq |
100 | 100 | } |
101 | 101 | |
102 | 102 | @Override |
103 | - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { | |
103 | + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) { | |
104 | 104 | return Futures.immediateFuture(null); |
105 | 105 | } |
106 | 106 | ... | ... |
... | ... | @@ -124,7 +124,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements |
124 | 124 | } |
125 | 125 | |
126 | 126 | @Override |
127 | - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { | |
127 | + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) { | |
128 | 128 | return Futures.immediateFuture(0); |
129 | 129 | } |
130 | 130 | ... | ... |
... | ... | @@ -170,7 +170,7 @@ public class BaseTimeseriesService implements TimeseriesService { |
170 | 170 | if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { |
171 | 171 | throw new IncorrectParameterException("Telemetry data can't be stored for entity view. Read only"); |
172 | 172 | } |
173 | - futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey(), ttl)); | |
173 | + futures.add(timeseriesDao.savePartition(tenantId, entityId, tsKvEntry.getTs(), tsKvEntry.getKey())); | |
174 | 174 | futures.add(Futures.transform(timeseriesLatestDao.saveLatest(tenantId, entityId, tsKvEntry), v -> 0, MoreExecutors.directExecutor())); |
175 | 175 | futures.add(timeseriesDao.save(tenantId, entityId, tsKvEntry, ttl)); |
176 | 176 | } | ... | ... |
... | ... | @@ -181,11 +181,14 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD |
181 | 181 | } |
182 | 182 | |
183 | 183 | @Override |
184 | - public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl) { | |
184 | + public ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key) { | |
185 | 185 | if (isFixedPartitioning()) { |
186 | 186 | return Futures.immediateFuture(null); |
187 | 187 | } |
188 | - ttl = computeTtl(ttl); | |
188 | + // DO NOT apply custom TTL to partition, otherwise, short TTL will remove partition too early | |
189 | + // partitions must remain in the DB forever or be removed only by systemTtl | |
190 | + // removal of empty partition is too expensive (we need to scan all data keys for these partitions with ALLOW FILTERING) | |
191 | + long ttl = computeTtl(0); | |
189 | 192 | long partition = toPartitionTs(tsKvEntryTs); |
190 | 193 | if (cassandraTsPartitionsCache == null) { |
191 | 194 | return doSavePartition(tenantId, entityId, key, ttl, partition); | ... | ... |
... | ... | @@ -33,7 +33,7 @@ public interface TimeseriesDao { |
33 | 33 | |
34 | 34 | ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); |
35 | 35 | |
36 | - ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key, long ttl); | |
36 | + ListenableFuture<Integer> savePartition(TenantId tenantId, EntityId entityId, long tsKvEntryTs, String key); | |
37 | 37 | |
38 | 38 | ListenableFuture<Void> remove(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query); |
39 | 39 | ... | ... |
... | ... | @@ -100,10 +100,10 @@ public class CassandraPartitionsCacheTest { |
100 | 100 | long tsKvEntryTs = System.currentTimeMillis(); |
101 | 101 | |
102 | 102 | for (int i = 0; i < 50000; i++) { |
103 | - cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); | |
103 | + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i); | |
104 | 104 | } |
105 | 105 | for (int i = 0; i < 60000; i++) { |
106 | - cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i, 0); | |
106 | + cassandraBaseTimeseriesDao.savePartition(tenantId, tenantId, tsKvEntryTs, "test" + i); | |
107 | 107 | } |
108 | 108 | verify(cassandraBaseTimeseriesDao, times(60000)).executeAsyncWrite(any(TenantId.class), any(Statement.class)); |
109 | 109 | } | ... | ... |
... | ... | @@ -42,13 +42,14 @@ |
42 | 42 | <spring-boot.version>2.3.12.RELEASE</spring-boot.version> |
43 | 43 | <spring.version>5.2.16.RELEASE</spring.version> |
44 | 44 | <spring-redis.version>5.2.11.RELEASE</spring-redis.version> |
45 | - <spring-security.version>5.4.1</spring-security.version> | |
45 | + <spring-security.version>5.4.4</spring-security.version> | |
46 | 46 | <spring-data-redis.version>2.4.3</spring-data-redis.version> |
47 | 47 | <jedis.version>3.3.0</jedis.version> |
48 | 48 | <jjwt.version>0.7.0</jjwt.version> |
49 | 49 | <json-path.version>2.2.0</json-path.version> |
50 | 50 | <junit.version>4.12</junit.version> |
51 | 51 | <jupiter.version>5.7.1</jupiter.version> |
52 | + <awaitility.version>4.1.0</awaitility.version> | |
52 | 53 | <hamcrest.version>2.2</hamcrest.version> |
53 | 54 | <slf4j.version>1.7.7</slf4j.version> |
54 | 55 | <logback.version>1.2.3</logback.version> |
... | ... | @@ -1382,6 +1383,12 @@ |
1382 | 1383 | <groupId>io.grpc</groupId> |
1383 | 1384 | <artifactId>grpc-netty</artifactId> |
1384 | 1385 | <version>${grpc.version}</version> |
1386 | + <exclusions> | |
1387 | + <exclusion> | |
1388 | + <groupId>io.netty</groupId> | |
1389 | + <artifactId>*</artifactId> | |
1390 | + </exclusion> | |
1391 | + </exclusions> | |
1385 | 1392 | </dependency> |
1386 | 1393 | <dependency> |
1387 | 1394 | <groupId>io.grpc</groupId> |
... | ... | @@ -1438,6 +1445,12 @@ |
1438 | 1445 | <scope>test</scope> |
1439 | 1446 | </dependency> |
1440 | 1447 | <dependency> |
1448 | + <groupId>org.awaitility</groupId> | |
1449 | + <artifactId>awaitility</artifactId> | |
1450 | + <version>${awaitility.version}</version> | |
1451 | + <scope>test</scope> | |
1452 | + </dependency> | |
1453 | + <dependency> | |
1441 | 1454 | <groupId>org.hamcrest</groupId> |
1442 | 1455 | <artifactId>hamcrest</artifactId> |
1443 | 1456 | <version>${hamcrest.version}</version> |
... | ... | @@ -1580,6 +1593,12 @@ |
1580 | 1593 | <groupId>com.microsoft.azure</groupId> |
1581 | 1594 | <artifactId>azure-servicebus</artifactId> |
1582 | 1595 | <version>${azure-servicebus.version}</version> |
1596 | + <exclusions> | |
1597 | + <exclusion> | |
1598 | + <groupId>io.netty</groupId> | |
1599 | + <artifactId>*</artifactId> | |
1600 | + </exclusion> | |
1601 | + </exclusions> | |
1583 | 1602 | </dependency> |
1584 | 1603 | <dependency> |
1585 | 1604 | <groupId>org.passay</groupId> | ... | ... |
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java
... | ... | @@ -188,7 +188,7 @@ class AlarmState { |
188 | 188 | setAlarmConditionMetadata(ruleState, metaData); |
189 | 189 | TbMsg newMsg = ctx.newMsg(lastMsgQueueName != null ? lastMsgQueueName : ServiceQueue.MAIN, "ALARM", |
190 | 190 | originator, msg != null ? msg.getCustomerId() : null, metaData, data); |
191 | - ctx.tellNext(newMsg, relationType); | |
191 | + ctx.enqueueForTellNext(newMsg, relationType); | |
192 | 192 | } |
193 | 193 | |
194 | 194 | protected void setAlarmConditionMetadata(AlarmRuleState ruleState, TbMsgMetaData metaData) { | ... | ... |
... | ... | @@ -120,7 +120,7 @@ public class TbSendRPCRequestNode implements TbNode { |
120 | 120 | ctx.enqueueForTellNext(next, TbRelationTypes.SUCCESS); |
121 | 121 | } else { |
122 | 122 | TbMsg next = ctx.newMsg(msg.getQueueName(), msg.getType(), msg.getOriginator(), msg.getCustomerId(), msg.getMetaData(), wrap("error", ruleEngineDeviceRpcResponse.getError().get().name())); |
123 | - ctx.tellFailure(next, new RuntimeException(ruleEngineDeviceRpcResponse.getError().get().name())); | |
123 | + ctx.enqueueForTellFailure(next, ruleEngineDeviceRpcResponse.getError().get().name()); | |
124 | 124 | } |
125 | 125 | }); |
126 | 126 | ctx.ack(msg); | ... | ... |
... | ... | @@ -196,7 +196,7 @@ public class TbDeviceProfileNodeTest { |
196 | 196 | TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
197 | 197 | node.onMsg(ctx, msg); |
198 | 198 | verify(ctx).tellSuccess(msg); |
199 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
199 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
200 | 200 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
201 | 201 | |
202 | 202 | TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); |
... | ... | @@ -207,7 +207,7 @@ public class TbDeviceProfileNodeTest { |
207 | 207 | TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); |
208 | 208 | node.onMsg(ctx, msg2); |
209 | 209 | verify(ctx).tellSuccess(msg2); |
210 | - verify(ctx).tellNext(theMsg2, "Alarm Updated"); | |
210 | + verify(ctx).enqueueForTellNext(theMsg2, "Alarm Updated"); | |
211 | 211 | |
212 | 212 | } |
213 | 213 | |
... | ... | @@ -289,7 +289,7 @@ public class TbDeviceProfileNodeTest { |
289 | 289 | |
290 | 290 | node.onMsg(ctx, msg); |
291 | 291 | verify(ctx).tellSuccess(msg); |
292 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
292 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
293 | 293 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
294 | 294 | } |
295 | 295 | |
... | ... | @@ -376,7 +376,7 @@ public class TbDeviceProfileNodeTest { |
376 | 376 | |
377 | 377 | node.onMsg(ctx, msg); |
378 | 378 | verify(ctx).tellSuccess(msg); |
379 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
379 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
380 | 380 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
381 | 381 | } |
382 | 382 | |
... | ... | @@ -445,7 +445,7 @@ public class TbDeviceProfileNodeTest { |
445 | 445 | |
446 | 446 | node.onMsg(ctx, msg); |
447 | 447 | verify(ctx).tellSuccess(msg); |
448 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
448 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
449 | 449 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
450 | 450 | } |
451 | 451 | |
... | ... | @@ -554,7 +554,7 @@ public class TbDeviceProfileNodeTest { |
554 | 554 | |
555 | 555 | node.onMsg(ctx, msg2); |
556 | 556 | verify(ctx).tellSuccess(msg2); |
557 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
557 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
558 | 558 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
559 | 559 | } |
560 | 560 | |
... | ... | @@ -678,7 +678,7 @@ public class TbDeviceProfileNodeTest { |
678 | 678 | |
679 | 679 | node.onMsg(ctx, msg2); |
680 | 680 | verify(ctx).tellSuccess(msg2); |
681 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
681 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
682 | 682 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
683 | 683 | } |
684 | 684 | |
... | ... | @@ -781,7 +781,7 @@ public class TbDeviceProfileNodeTest { |
781 | 781 | |
782 | 782 | node.onMsg(ctx, msg2); |
783 | 783 | verify(ctx).tellSuccess(msg2); |
784 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
784 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
785 | 785 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
786 | 786 | } |
787 | 787 | |
... | ... | @@ -897,7 +897,7 @@ public class TbDeviceProfileNodeTest { |
897 | 897 | |
898 | 898 | node.onMsg(ctx, msg2); |
899 | 899 | verify(ctx).tellSuccess(msg2); |
900 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
900 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
901 | 901 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
902 | 902 | } |
903 | 903 | |
... | ... | @@ -999,7 +999,7 @@ public class TbDeviceProfileNodeTest { |
999 | 999 | |
1000 | 1000 | node.onMsg(ctx, msg2); |
1001 | 1001 | verify(ctx).tellSuccess(msg2); |
1002 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
1002 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
1003 | 1003 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1004 | 1004 | } |
1005 | 1005 | |
... | ... | @@ -1082,7 +1082,7 @@ public class TbDeviceProfileNodeTest { |
1082 | 1082 | |
1083 | 1083 | node.onMsg(ctx, msg); |
1084 | 1084 | verify(ctx).tellSuccess(msg); |
1085 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
1085 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
1086 | 1086 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1087 | 1087 | } |
1088 | 1088 | |
... | ... | @@ -1163,7 +1163,7 @@ public class TbDeviceProfileNodeTest { |
1163 | 1163 | |
1164 | 1164 | node.onMsg(ctx, msg); |
1165 | 1165 | verify(ctx).tellSuccess(msg); |
1166 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
1166 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
1167 | 1167 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1168 | 1168 | } |
1169 | 1169 | |
... | ... | @@ -1237,7 +1237,7 @@ public class TbDeviceProfileNodeTest { |
1237 | 1237 | |
1238 | 1238 | node.onMsg(ctx, msg); |
1239 | 1239 | verify(ctx).tellSuccess(msg); |
1240 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
1240 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
1241 | 1241 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1242 | 1242 | } |
1243 | 1243 | |
... | ... | @@ -1321,7 +1321,7 @@ public class TbDeviceProfileNodeTest { |
1321 | 1321 | |
1322 | 1322 | node.onMsg(ctx, msg); |
1323 | 1323 | verify(ctx).tellSuccess(msg); |
1324 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
1324 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
1325 | 1325 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1326 | 1326 | |
1327 | 1327 | } |
... | ... | @@ -1407,7 +1407,7 @@ public class TbDeviceProfileNodeTest { |
1407 | 1407 | |
1408 | 1408 | node.onMsg(ctx, msg); |
1409 | 1409 | verify(ctx).tellSuccess(msg); |
1410 | - verify(ctx).tellNext(theMsg, "Alarm Created"); | |
1410 | + verify(ctx).enqueueForTellNext(theMsg, "Alarm Created"); | |
1411 | 1411 | verify(ctx, Mockito.never()).tellFailure(Mockito.any(), Mockito.any()); |
1412 | 1412 | } |
1413 | 1413 | ... | ... |
... | ... | @@ -89,6 +89,7 @@ transport: |
89 | 89 | bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}" |
90 | 90 | bind_port: "${MQTT_BIND_PORT:1883}" |
91 | 91 | timeout: "${MQTT_TIMEOUT:10000}" |
92 | + msg_queue_size_per_device_limit: "${MQTT_MSG_QUEUE_SIZE_PER_DEVICE_LIMIT:100}" # messages await in the queue before device connected state. This limit works on low level before TenantProfileLimits mechanism | |
92 | 93 | netty: |
93 | 94 | leak_detector_level: "${NETTY_LEAK_DETECTOR_LVL:DISABLED}" |
94 | 95 | boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" | ... | ... |
... | ... | @@ -84,6 +84,8 @@ export interface WidgetActionsApi { |
84 | 84 | entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) => void; |
85 | 85 | elementClick: ($event: Event) => void; |
86 | 86 | getActiveEntityInfo: () => SubscriptionEntityInfo; |
87 | + openDashboardStateInSeparateDialog: (targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, | |
88 | + hideDashboardToolbar?: boolean, dialogWidth?: number, dialogHeight?: number) => void; | |
87 | 89 | } |
88 | 90 | |
89 | 91 | export interface AliasInfo { | ... | ... |
... | ... | @@ -50,11 +50,17 @@ export class AttributeService { |
50 | 50 | } |
51 | 51 | |
52 | 52 | public deleteEntityTimeseries(entityId: EntityId, timeseries: Array<AttributeData>, deleteAllDataForKeys = false, |
53 | - config?: RequestConfig): Observable<any> { | |
53 | + startTs?: number, endTs?: number, config?: RequestConfig): Observable<any> { | |
54 | 54 | const keys = timeseries.map(attribute => encodeURI(attribute.key)).join(','); |
55 | - return this.http.delete(`/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + | |
56 | - `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`, | |
57 | - defaultHttpOptionsFromConfig(config)); | |
55 | + let url = `/api/plugins/telemetry/${entityId.entityType}/${entityId.id}/timeseries/delete` + | |
56 | + `?keys=${keys}&deleteAllDataForKeys=${deleteAllDataForKeys}`; | |
57 | + if (isDefinedAndNotNull(startTs)) { | |
58 | + url += `&startTs=${startTs}`; | |
59 | + } | |
60 | + if (isDefinedAndNotNull(endTs)) { | |
61 | + url += `&endTs=${endTs}`; | |
62 | + } | |
63 | + return this.http.delete(url, defaultHttpOptionsFromConfig(config)); | |
58 | 64 | } |
59 | 65 | |
60 | 66 | public saveEntityAttributes(entityId: EntityId, attributeScope: AttributeScope, attributes: Array<AttributeData>, |
... | ... | @@ -97,7 +103,7 @@ export class AttributeService { |
97 | 103 | }); |
98 | 104 | let deleteEntityTimeseriesObservable: Observable<any>; |
99 | 105 | if (deleteTimeseries.length) { |
100 | - deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, config); | |
106 | + deleteEntityTimeseriesObservable = this.deleteEntityTimeseries(entityId, deleteTimeseries, true, null, null, config); | |
101 | 107 | } else { |
102 | 108 | deleteEntityTimeseriesObservable = of(null); |
103 | 109 | } | ... | ... |
... | ... | @@ -18,7 +18,7 @@ import { Store } from '@ngrx/store'; |
18 | 18 | import { AppState } from '@core/core.state'; |
19 | 19 | import { FormBuilder, FormGroup, ValidatorFn, Validators } from '@angular/forms'; |
20 | 20 | import { ContactBased } from '@shared/models/contact-based.model'; |
21 | -import { AfterViewInit, Directive } from '@angular/core'; | |
21 | +import { AfterViewInit, ChangeDetectorRef, Directive } from '@angular/core'; | |
22 | 22 | import { POSTAL_CODE_PATTERNS } from '@home/models/contact.models'; |
23 | 23 | import { HasId } from '@shared/models/base-data'; |
24 | 24 | import { EntityComponent } from './entity.component'; |
... | ... | @@ -30,8 +30,9 @@ export abstract class ContactBasedComponent<T extends ContactBased<HasId>> exten |
30 | 30 | protected constructor(protected store: Store<AppState>, |
31 | 31 | protected fb: FormBuilder, |
32 | 32 | protected entityValue: T, |
33 | - protected entitiesTableConfigValue: EntityTableConfig<T>) { | |
34 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
33 | + protected entitiesTableConfigValue: EntityTableConfig<T>, | |
34 | + protected cd: ChangeDetectorRef) { | |
35 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
35 | 36 | } |
36 | 37 | |
37 | 38 | buildForm(entity: T): FormGroup { | ... | ... |
... | ... | @@ -17,7 +17,7 @@ |
17 | 17 | import { BaseData, HasId } from '@shared/models/base-data'; |
18 | 18 | import { FormBuilder, FormGroup } from '@angular/forms'; |
19 | 19 | import { PageComponent } from '@shared/components/page.component'; |
20 | -import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; | |
20 | +import { ChangeDetectorRef, Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; | |
21 | 21 | import { Store } from '@ngrx/store'; |
22 | 22 | import { AppState } from '@core/core.state'; |
23 | 23 | import { EntityAction } from '@home/models/entity/entity-component.models'; |
... | ... | @@ -50,6 +50,7 @@ export abstract class EntityComponent<T extends BaseData<HasId>, |
50 | 50 | @Input() |
51 | 51 | set isEdit(isEdit: boolean) { |
52 | 52 | this.isEditValue = isEdit; |
53 | + this.cd.markForCheck(); | |
53 | 54 | this.updateFormState(); |
54 | 55 | } |
55 | 56 | |
... | ... | @@ -80,7 +81,8 @@ export abstract class EntityComponent<T extends BaseData<HasId>, |
80 | 81 | protected constructor(protected store: Store<AppState>, |
81 | 82 | protected fb: FormBuilder, |
82 | 83 | protected entityValue: T, |
83 | - protected entitiesTableConfigValue: C) { | |
84 | + protected entitiesTableConfigValue: C, | |
85 | + protected cd: ChangeDetectorRef) { | |
84 | 86 | super(store); |
85 | 87 | this.entityForm = this.buildForm(this.entityValue); |
86 | 88 | } | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject, Input, Optional } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
... | ... | @@ -77,8 +77,9 @@ export class DeviceProfileComponent extends EntityComponent<DeviceProfile> { |
77 | 77 | protected translate: TranslateService, |
78 | 78 | @Optional() @Inject('entity') protected entityValue: DeviceProfile, |
79 | 79 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceProfile>, |
80 | - protected fb: FormBuilder) { | |
81 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
80 | + protected fb: FormBuilder, | |
81 | + protected cd: ChangeDetectorRef) { | |
82 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
82 | 83 | } |
83 | 84 | |
84 | 85 | hideDelete() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject, Input, Optional } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject, Input, Optional } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
... | ... | @@ -43,8 +43,9 @@ export class TenantProfileComponent extends EntityComponent<TenantProfile> { |
43 | 43 | protected translate: TranslateService, |
44 | 44 | @Optional() @Inject('entity') protected entityValue: TenantProfile, |
45 | 45 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantProfile>, |
46 | - protected fb: FormBuilder) { | |
47 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
46 | + protected fb: FormBuilder, | |
47 | + protected cd: ChangeDetectorRef) { | |
48 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
48 | 49 | } |
49 | 50 | |
50 | 51 | hideDelete() { | ... | ... |
... | ... | @@ -284,7 +284,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
284 | 284 | getActionDescriptors: this.getActionDescriptors.bind(this), |
285 | 285 | handleWidgetAction: this.handleWidgetAction.bind(this), |
286 | 286 | elementClick: this.elementClick.bind(this), |
287 | - getActiveEntityInfo: this.getActiveEntityInfo.bind(this) | |
287 | + getActiveEntityInfo: this.getActiveEntityInfo.bind(this), | |
288 | + openDashboardStateInSeparateDialog: this.openDashboardStateInSeparateDialog.bind(this) | |
288 | 289 | }; |
289 | 290 | |
290 | 291 | this.widgetContext.customHeaderActions = []; |
... | ... | @@ -1025,7 +1026,8 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
1025 | 1026 | this.updateEntityParams(params, targetEntityParamName, targetEntityId, entityName, entityLabel); |
1026 | 1027 | if (type === WidgetActionType.openDashboardState) { |
1027 | 1028 | if (descriptor.openInSeparateDialog) { |
1028 | - this.openDashboardStateInDialog(descriptor, entityId, entityName, additionalParams, entityLabel); | |
1029 | + this.openDashboardStateInSeparateDialog(descriptor.targetDashboardStateId, params, descriptor.dialogTitle, | |
1030 | + descriptor.dialogHideDashboardToolbar, descriptor.dialogWidth, descriptor.dialogHeight); | |
1029 | 1031 | } else { |
1030 | 1032 | this.widgetContext.stateController.openState(targetDashboardStateId, params, descriptor.openRightLayout); |
1031 | 1033 | } |
... | ... | @@ -1276,22 +1278,15 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
1276 | 1278 | } |
1277 | 1279 | } |
1278 | 1280 | |
1279 | - private openDashboardStateInDialog(descriptor: WidgetActionDescriptor, | |
1280 | - entityId?: EntityId, entityName?: string, additionalParams?: any, entityLabel?: string) { | |
1281 | + private openDashboardStateInSeparateDialog(targetDashboardStateId: string, params?: StateParams, dialogTitle?: string, | |
1282 | + hideDashboardToolbar = true, dialogWidth?: number, dialogHeight?: number) { | |
1281 | 1283 | const dashboard = deepClone(this.widgetContext.stateController.dashboardCtrl.dashboardCtx.getDashboard()); |
1282 | 1284 | const stateObject: StateObject = {}; |
1283 | - stateObject.params = {}; | |
1284 | - const targetEntityParamName = descriptor.stateEntityParamName; | |
1285 | - const targetDashboardStateId = descriptor.targetDashboardStateId; | |
1286 | - let targetEntityId: EntityId; | |
1287 | - if (descriptor.setEntityId) { | |
1288 | - targetEntityId = entityId; | |
1289 | - } | |
1290 | - this.updateEntityParams(stateObject.params, targetEntityParamName, targetEntityId, entityName, entityLabel); | |
1285 | + stateObject.params = params; | |
1291 | 1286 | if (targetDashboardStateId) { |
1292 | 1287 | stateObject.id = targetDashboardStateId; |
1293 | 1288 | } |
1294 | - let title = descriptor.dialogTitle; | |
1289 | + let title = dialogTitle; | |
1295 | 1290 | if (!title) { |
1296 | 1291 | if (targetDashboardStateId && dashboard.configuration.states) { |
1297 | 1292 | const dashboardState = dashboard.configuration.states[targetDashboardStateId]; |
... | ... | @@ -1304,7 +1299,6 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
1304 | 1299 | title = dashboard.title; |
1305 | 1300 | } |
1306 | 1301 | title = this.utils.customTranslation(title, title); |
1307 | - const params = stateObject.params; | |
1308 | 1302 | const paramsEntityName = params && params.entityName ? params.entityName : ''; |
1309 | 1303 | const paramsEntityLabel = params && params.entityLabel ? params.entityLabel : ''; |
1310 | 1304 | title = insertVariable(title, 'entityName', paramsEntityName); |
... | ... | @@ -1324,28 +1318,27 @@ export class WidgetComponent extends PageComponent implements OnInit, AfterViewI |
1324 | 1318 | dashboard, |
1325 | 1319 | state: objToBase64([ stateObject ]), |
1326 | 1320 | title, |
1327 | - hideToolbar: descriptor.dialogHideDashboardToolbar, | |
1328 | - width: descriptor.dialogWidth, | |
1329 | - height: descriptor.dialogHeight | |
1321 | + hideToolbar: hideDashboardToolbar, | |
1322 | + width: dialogWidth, | |
1323 | + height: dialogHeight | |
1330 | 1324 | } |
1331 | 1325 | }); |
1332 | 1326 | } |
1333 | 1327 | |
1334 | 1328 | private elementClick($event: Event) { |
1335 | - const e = ($event.target || $event.srcElement) as Element; | |
1336 | - if (e.id) { | |
1337 | - const descriptors = this.getActionDescriptors('elementClick'); | |
1338 | - if (descriptors.length) { | |
1339 | - descriptors.forEach((descriptor) => { | |
1340 | - if (descriptor.name === e.id) { | |
1341 | - $event.stopPropagation(); | |
1342 | - const entityInfo = this.getActiveEntityInfo(); | |
1343 | - const entityId = entityInfo ? entityInfo.entityId : null; | |
1344 | - const entityName = entityInfo ? entityInfo.entityName : null; | |
1345 | - const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; | |
1346 | - this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); | |
1347 | - } | |
1348 | - }); | |
1329 | + const elementClicked = ($event.target || $event.srcElement) as Element; | |
1330 | + const descriptors = this.getActionDescriptors('elementClick'); | |
1331 | + if (descriptors.length) { | |
1332 | + const idsList = descriptors.map(descriptor => `#${descriptor.name}`).join(','); | |
1333 | + const targetElement = $(elementClicked).closest(idsList, this.widgetContext.$container[0]); | |
1334 | + if (targetElement.length && targetElement[0].id) { | |
1335 | + $event.stopPropagation(); | |
1336 | + const descriptor = descriptors.find(descriptorInfo => descriptorInfo.name === targetElement[0].id); | |
1337 | + const entityInfo = this.getActiveEntityInfo(); | |
1338 | + const entityId = entityInfo ? entityInfo.entityId : null; | |
1339 | + const entityName = entityInfo ? entityInfo.entityName : null; | |
1340 | + const entityLabel = entityInfo && entityInfo.entityLabel ? entityInfo.entityLabel : null; | |
1341 | + this.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel); | |
1349 | 1342 | } |
1350 | 1343 | } |
1351 | 1344 | } | ... | ... |
... | ... | @@ -39,7 +39,7 @@ |
39 | 39 | </mat-form-field> |
40 | 40 | <mat-form-field class="mat-block"> |
41 | 41 | <mat-label translate>admin.smtp-protocol</mat-label> |
42 | - <mat-select matInput formControlName="smtpProtocol"> | |
42 | + <mat-select formControlName="smtpProtocol"> | |
43 | 43 | <mat-option *ngFor="let protocol of smtpProtocols" [value]="protocol"> |
44 | 44 | {{protocol.toUpperCase()}} |
45 | 45 | </mat-option> |
... | ... | @@ -127,7 +127,10 @@ |
127 | 127 | <input matInput formControlName="username" placeholder="{{ 'common.enter-username' | translate }}" |
128 | 128 | autocomplete="new-username"/> |
129 | 129 | </mat-form-field> |
130 | - <mat-form-field class="mat-block"> | |
130 | + <mat-checkbox *ngIf="showChangePassword" formControlName="changePassword" style="padding-bottom: 16px;"> | |
131 | + {{ 'admin.change-password' | translate }} | |
132 | + </mat-checkbox> | |
133 | + <mat-form-field class="mat-block" *ngIf="mailSettings.get('changePassword').value || !showChangePassword"> | |
131 | 134 | <mat-label translate>common.password</mat-label> |
132 | 135 | <input matInput formControlName="password" type="password" |
133 | 136 | placeholder="{{ 'common.enter-password' | translate }}" autocomplete="new-password"/> | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, OnInit } from '@angular/core'; | |
17 | +import { Component, OnDestroy, OnInit } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { PageComponent } from '@shared/components/page.component'; |
... | ... | @@ -25,21 +25,26 @@ import { AdminService } from '@core/http/admin.service'; |
25 | 25 | import { ActionNotificationShow } from '@core/notification/notification.actions'; |
26 | 26 | import { TranslateService } from '@ngx-translate/core'; |
27 | 27 | import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard'; |
28 | -import { isString } from '@core/utils'; | |
28 | +import { isDefinedAndNotNull, isString } from '@core/utils'; | |
29 | +import { Subject } from 'rxjs'; | |
30 | +import { takeUntil } from 'rxjs/operators'; | |
29 | 31 | |
30 | 32 | @Component({ |
31 | 33 | selector: 'tb-mail-server', |
32 | 34 | templateUrl: './mail-server.component.html', |
33 | 35 | styleUrls: ['./mail-server.component.scss', './settings-card.scss'] |
34 | 36 | }) |
35 | -export class MailServerComponent extends PageComponent implements OnInit, HasConfirmForm { | |
37 | +export class MailServerComponent extends PageComponent implements OnInit, OnDestroy, HasConfirmForm { | |
36 | 38 | |
37 | 39 | mailSettings: FormGroup; |
38 | 40 | adminSettings: AdminSettings<MailServerSettings>; |
39 | 41 | smtpProtocols = ['smtp', 'smtps']; |
42 | + showChangePassword = false; | |
40 | 43 | |
41 | 44 | tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; |
42 | 45 | |
46 | + private destroy$ = new Subject(); | |
47 | + | |
43 | 48 | constructor(protected store: Store<AppState>, |
44 | 49 | private router: Router, |
45 | 50 | private adminService: AdminService, |
... | ... | @@ -56,12 +61,22 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon |
56 | 61 | if (this.adminSettings.jsonValue && isString(this.adminSettings.jsonValue.enableTls)) { |
57 | 62 | this.adminSettings.jsonValue.enableTls = (this.adminSettings.jsonValue.enableTls as any) === 'true'; |
58 | 63 | } |
64 | + this.showChangePassword = | |
65 | + isDefinedAndNotNull(this.adminSettings.jsonValue.showChangePassword) ? this.adminSettings.jsonValue.showChangePassword : true ; | |
66 | + delete this.adminSettings.jsonValue.showChangePassword; | |
59 | 67 | this.mailSettings.reset(this.adminSettings.jsonValue); |
68 | + this.enableMailPassword(!this.showChangePassword); | |
60 | 69 | this.enableProxyChanged(); |
61 | 70 | } |
62 | 71 | ); |
63 | 72 | } |
64 | 73 | |
74 | + ngOnDestroy() { | |
75 | + this.destroy$.next(); | |
76 | + this.destroy$.complete(); | |
77 | + super.ngOnDestroy(); | |
78 | + } | |
79 | + | |
65 | 80 | buildMailServerSettingsForm() { |
66 | 81 | this.mailSettings = this.fb.group({ |
67 | 82 | mailFrom: ['', [Validators.required]], |
... | ... | @@ -81,14 +96,23 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon |
81 | 96 | proxyUser: [''], |
82 | 97 | proxyPassword: [''], |
83 | 98 | username: [''], |
99 | + changePassword: [false], | |
84 | 100 | password: [''] |
85 | 101 | }); |
86 | 102 | this.registerDisableOnLoadFormControl(this.mailSettings.get('smtpProtocol')); |
87 | 103 | this.registerDisableOnLoadFormControl(this.mailSettings.get('enableTls')); |
88 | 104 | this.registerDisableOnLoadFormControl(this.mailSettings.get('enableProxy')); |
89 | - this.mailSettings.get('enableProxy').valueChanges.subscribe(() => { | |
105 | + this.registerDisableOnLoadFormControl(this.mailSettings.get('changePassword')); | |
106 | + this.mailSettings.get('enableProxy').valueChanges.pipe( | |
107 | + takeUntil(this.destroy$) | |
108 | + ).subscribe(() => { | |
90 | 109 | this.enableProxyChanged(); |
91 | 110 | }); |
111 | + this.mailSettings.get('changePassword').valueChanges.pipe( | |
112 | + takeUntil(this.destroy$) | |
113 | + ).subscribe((value) => { | |
114 | + this.enableMailPassword(value); | |
115 | + }); | |
92 | 116 | } |
93 | 117 | |
94 | 118 | enableProxyChanged(): void { |
... | ... | @@ -102,8 +126,16 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon |
102 | 126 | } |
103 | 127 | } |
104 | 128 | |
129 | + enableMailPassword(enable: boolean) { | |
130 | + if (enable) { | |
131 | + this.mailSettings.get('password').enable({emitEvent: false}); | |
132 | + } else { | |
133 | + this.mailSettings.get('password').disable({emitEvent: false}); | |
134 | + } | |
135 | + } | |
136 | + | |
105 | 137 | sendTestMail(): void { |
106 | - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; | |
138 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue}; | |
107 | 139 | this.adminService.sendTestMail(this.adminSettings).subscribe( |
108 | 140 | () => { |
109 | 141 | this.store.dispatch(new ActionNotificationShow({ message: this.translate.instant('admin.test-mail-sent'), |
... | ... | @@ -113,13 +145,11 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon |
113 | 145 | } |
114 | 146 | |
115 | 147 | save(): void { |
116 | - this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettings.value}; | |
148 | + this.adminSettings.jsonValue = {...this.adminSettings.jsonValue, ...this.mailSettingsFormValue}; | |
117 | 149 | this.adminService.saveAdminSettings(this.adminSettings).subscribe( |
118 | 150 | (adminSettings) => { |
119 | - if (!adminSettings.jsonValue.password) { | |
120 | - adminSettings.jsonValue.password = this.mailSettings.value.password; | |
121 | - } | |
122 | 151 | this.adminSettings = adminSettings; |
152 | + this.showChangePassword = true; | |
123 | 153 | this.mailSettings.reset(this.adminSettings.jsonValue); |
124 | 154 | } |
125 | 155 | ); |
... | ... | @@ -129,4 +159,9 @@ export class MailServerComponent extends PageComponent implements OnInit, HasCon |
129 | 159 | return this.mailSettings; |
130 | 160 | } |
131 | 161 | |
162 | + private get mailSettingsFormValue(): MailServerSettings { | |
163 | + const formValue = this.mailSettings.value; | |
164 | + delete formValue.changePassword; | |
165 | + return formValue; | |
166 | + } | |
132 | 167 | } | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; | |
18 | 18 | import { Subject } from 'rxjs'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
... | ... | @@ -30,7 +30,7 @@ import { |
30 | 30 | ResourceTypeTranslationMap |
31 | 31 | } from '@shared/models/resource.models'; |
32 | 32 | import { pairwise, startWith, takeUntil } from 'rxjs/operators'; |
33 | -import { ActionNotificationShow } from "@core/notification/notification.actions"; | |
33 | +import { ActionNotificationShow } from '@core/notification/notification.actions'; | |
34 | 34 | |
35 | 35 | @Component({ |
36 | 36 | selector: 'tb-resources-library', |
... | ... | @@ -48,8 +48,9 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme |
48 | 48 | protected translate: TranslateService, |
49 | 49 | @Inject('entity') protected entityValue: Resource, |
50 | 50 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Resource>, |
51 | - public fb: FormBuilder) { | |
52 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
51 | + public fb: FormBuilder, | |
52 | + protected cd: ChangeDetectorRef) { | |
53 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
53 | 54 | } |
54 | 55 | |
55 | 56 | ngOnInit() { |
... | ... | @@ -102,7 +103,7 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme |
102 | 103 | if (this.isAdd) { |
103 | 104 | form.addControl('data', this.fb.control(null, Validators.required)); |
104 | 105 | } |
105 | - return form | |
106 | + return form; | |
106 | 107 | } |
107 | 108 | |
108 | 109 | updateForm(entity: Resource) { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -41,8 +41,9 @@ export class AssetComponent extends EntityComponent<AssetInfo> { |
41 | 41 | protected translate: TranslateService, |
42 | 42 | @Inject('entity') protected entityValue: AssetInfo, |
43 | 43 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<AssetInfo>, |
44 | - public fb: FormBuilder) { | |
45 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
44 | + public fb: FormBuilder, | |
45 | + protected cd: ChangeDetectorRef) { | |
46 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
46 | 47 | } |
47 | 48 | |
48 | 49 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
... | ... | @@ -42,8 +42,9 @@ export class CustomerComponent extends ContactBasedComponent<Customer> { |
42 | 42 | protected translate: TranslateService, |
43 | 43 | @Inject('entity') protected entityValue: Customer, |
44 | 44 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Customer>, |
45 | - protected fb: FormBuilder) { | |
46 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
45 | + protected fb: FormBuilder, | |
46 | + protected cd: ChangeDetectorRef) { | |
47 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
47 | 48 | } |
48 | 49 | |
49 | 50 | hideDelete() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -49,8 +49,9 @@ export class DashboardFormComponent extends EntityComponent<Dashboard> { |
49 | 49 | private dashboardService: DashboardService, |
50 | 50 | @Inject('entity') protected entityValue: Dashboard, |
51 | 51 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<Dashboard>, |
52 | - public fb: FormBuilder) { | |
53 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
52 | + public fb: FormBuilder, | |
53 | + protected cd: ChangeDetectorRef) { | |
54 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
54 | 55 | } |
55 | 56 | |
56 | 57 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -56,8 +56,9 @@ export class DeviceComponent extends EntityComponent<DeviceInfo> { |
56 | 56 | protected translate: TranslateService, |
57 | 57 | @Inject('entity') protected entityValue: DeviceInfo, |
58 | 58 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<DeviceInfo>, |
59 | - public fb: FormBuilder) { | |
60 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
59 | + public fb: FormBuilder, | |
60 | + protected cd: ChangeDetectorRef) { | |
61 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
61 | 62 | } |
62 | 63 | |
63 | 64 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '@home/components/entity/entity.component'; |
... | ... | @@ -42,8 +42,9 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> { |
42 | 42 | protected translate: TranslateService, |
43 | 43 | @Inject('entity') protected entityValue: EdgeInfo, |
44 | 44 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EdgeInfo>, |
45 | - public fb: FormBuilder) { | |
46 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
45 | + public fb: FormBuilder, | |
46 | + protected cd: ChangeDetectorRef) { | |
47 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
47 | 48 | } |
48 | 49 | |
49 | 50 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -53,8 +53,9 @@ export class EntityViewComponent extends EntityComponent<EntityViewInfo> { |
53 | 53 | protected translate: TranslateService, |
54 | 54 | @Inject('entity') protected entityValue: EntityViewInfo, |
55 | 55 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<EntityViewInfo>, |
56 | - public fb: FormBuilder) { | |
57 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
56 | + public fb: FormBuilder, | |
57 | + protected cd: ChangeDetectorRef) { | |
58 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
58 | 59 | } |
59 | 60 | |
60 | 61 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; | |
18 | 18 | import { combineLatest, Subject } from 'rxjs'; |
19 | 19 | import { Store } from '@ngrx/store'; |
20 | 20 | import { AppState } from '@core/core.state'; |
... | ... | @@ -50,8 +50,9 @@ export class OtaUpdateComponent extends EntityComponent<OtaPackage> implements O |
50 | 50 | protected translate: TranslateService, |
51 | 51 | @Inject('entity') protected entityValue: OtaPackage, |
52 | 52 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<OtaPackage>, |
53 | - public fb: FormBuilder) { | |
54 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
53 | + public fb: FormBuilder, | |
54 | + protected cd: ChangeDetectorRef) { | |
55 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
55 | 56 | } |
56 | 57 | |
57 | 58 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -37,8 +37,9 @@ export class RuleChainComponent extends EntityComponent<RuleChain> { |
37 | 37 | protected translate: TranslateService, |
38 | 38 | @Inject('entity') protected entityValue: RuleChain, |
39 | 39 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<RuleChain>, |
40 | - public fb: FormBuilder) { | |
41 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
40 | + public fb: FormBuilder, | |
41 | + protected cd: ChangeDetectorRef) { | |
42 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
42 | 43 | } |
43 | 44 | |
44 | 45 | ngOnInit() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; |
... | ... | @@ -36,8 +36,9 @@ export class TenantComponent extends ContactBasedComponent<TenantInfo> { |
36 | 36 | protected translate: TranslateService, |
37 | 37 | @Inject('entity') protected entityValue: TenantInfo, |
38 | 38 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<TenantInfo>, |
39 | - protected fb: FormBuilder) { | |
40 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
39 | + protected fb: FormBuilder, | |
40 | + protected cd: ChangeDetectorRef) { | |
41 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
41 | 42 | } |
42 | 43 | |
43 | 44 | hideDelete() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject, Optional } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject, Optional } from '@angular/core'; | |
18 | 18 | import { select, Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -43,8 +43,9 @@ export class UserComponent extends EntityComponent<User> { |
43 | 43 | constructor(protected store: Store<AppState>, |
44 | 44 | @Optional() @Inject('entity') protected entityValue: User, |
45 | 45 | @Optional() @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<User>, |
46 | - public fb: FormBuilder) { | |
47 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
46 | + public fb: FormBuilder, | |
47 | + protected cd: ChangeDetectorRef) { | |
48 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
48 | 49 | } |
49 | 50 | |
50 | 51 | hideDelete() { | ... | ... |
... | ... | @@ -14,7 +14,7 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import { Component, Inject } from '@angular/core'; | |
17 | +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; | |
18 | 18 | import { Store } from '@ngrx/store'; |
19 | 19 | import { AppState } from '@core/core.state'; |
20 | 20 | import { EntityComponent } from '../../components/entity/entity.component'; |
... | ... | @@ -32,8 +32,9 @@ export class WidgetsBundleComponent extends EntityComponent<WidgetsBundle> { |
32 | 32 | constructor(protected store: Store<AppState>, |
33 | 33 | @Inject('entity') protected entityValue: WidgetsBundle, |
34 | 34 | @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig<WidgetsBundle>, |
35 | - public fb: FormBuilder) { | |
36 | - super(store, fb, entityValue, entitiesTableConfigValue); | |
35 | + public fb: FormBuilder, | |
36 | + protected cd: ChangeDetectorRef) { | |
37 | + super(store, fb, entityValue, entitiesTableConfigValue, cd); | |
37 | 38 | } |
38 | 39 | |
39 | 40 | hideDelete() { | ... | ... |
... | ... | @@ -27,6 +27,7 @@ export interface AdminSettings<T> { |
27 | 27 | export declare type SmtpProtocol = 'smtp' | 'smtps'; |
28 | 28 | |
29 | 29 | export interface MailServerSettings { |
30 | + showChangePassword: boolean; | |
30 | 31 | mailFrom: string; |
31 | 32 | smtpProtocol: SmtpProtocol; |
32 | 33 | smtpHost: string; |
... | ... | @@ -34,7 +35,8 @@ export interface MailServerSettings { |
34 | 35 | timeout: number; |
35 | 36 | enableTls: boolean; |
36 | 37 | username: string; |
37 | - password: string; | |
38 | + changePassword?: boolean; | |
39 | + password?: string; | |
38 | 40 | enableProxy: boolean; |
39 | 41 | proxyHost: string; |
40 | 42 | proxyPort: number; | ... | ... |
... | ... | @@ -803,7 +803,7 @@ |
803 | 803 | "rulechain-templates": "Regelkettenvorlagen", |
804 | 804 | "rulechains": "Rand Regelketten", |
805 | 805 | "search": "Kanten durchsuchen", |
806 | - "selected-edges": "{Anzahl, Plural, 1 {1 Kante} andere {# Kanten}} ausgewählt", | |
806 | + "selected-edges": "{count, plural, 1 {1 Rand} other {# Rand} } ausgewählt", | |
807 | 807 | "any-edge": "Beliebige Kante", |
808 | 808 | "no-edge-types-matching": "Es wurden keine Kantentypen gefunden, die mit '{{entitySubtype}}' übereinstimmen.", |
809 | 809 | "edge-type-list-empty": "Keine Kantentypen ausgewählt.", |
... | ... | @@ -1452,7 +1452,7 @@ |
1452 | 1452 | "unset-auto-assign-to-edge-text": "Nach der Bestätigung wird die Kantenregelkette bei der Erstellung nicht mehr automatisch den Kanten zugewiesen.", |
1453 | 1453 | "edge-template-root": "Vorlagenstamm", |
1454 | 1454 | "search": "Suchen Sie nach Regelketten", |
1455 | - "selected-rulechains": "{count, plural, 1 {1 Regelkette} andere {# Regelketten}} ausgewählt", | |
1455 | + "selected-rulechains": "{count, plural, 1 {1 Regelkette} other {# Regelketten} } ausgewählt", | |
1456 | 1456 | "open-rulechain": "Regelkette öffnen", |
1457 | 1457 | "assign-to-edge": "Rand zuweisen", |
1458 | 1458 | "edge-rulechain": "Kantenregelkette" | ... | ... |
... | ... | @@ -104,6 +104,7 @@ |
104 | 104 | "proxy-port-range": "Proxy port should be in a range from 1 to 65535.", |
105 | 105 | "proxy-user": "Proxy user", |
106 | 106 | "proxy-password": "Proxy password", |
107 | + "change-password": "Change password", | |
107 | 108 | "send-test-mail": "Send test mail", |
108 | 109 | "sms-provider": "SMS provider", |
109 | 110 | "sms-provider-settings": "SMS provider settings", | ... | ... |
... | ... | @@ -413,8 +413,8 @@ |
413 | 413 | "unassign-asset-from-edge": "Anular activo de bodre", |
414 | 414 | "unassign-asset-from-edge-title": "¿Está seguro de que desea desasignar el activo '{{assetName}}'?", |
415 | 415 | "unassign-asset-from-edge-text": "Después de la confirmación, el activo no será asignado y el borde no podrá acceder a él", |
416 | - "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos}} desde el borde", | |
417 | - "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos}}?", | |
416 | + "unassign-assets-from-edge-action-title": "Anular asignación {count, plural, 1 {1 activo} other {# activos} } desde el borde", | |
417 | + "unassign-assets-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 activo} other {# activos} }?", | |
418 | 418 | "unassign-assets-from-edge-text": "Después de la confirmación, todos los activos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." |
419 | 419 | }, |
420 | 420 | "attribute": { |
... | ... | @@ -950,7 +950,7 @@ |
950 | 950 | "assign-device-to-edge-text": "Seleccione los dispositivos para asignar al borde", |
951 | 951 | "unassign-device-from-edge-title": "¿Está seguro de que desea desasignar el dispositivo '{{deviceName}}'?", |
952 | 952 | "unassign-device-from-edge-text": "Después de la confirmación, el dispositivo no será asignado y el borde no podrá acceder a él", |
953 | - "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos}}?", | |
953 | + "unassign-devices-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 dispositivo} other {# dispositivos} }?", | |
954 | 954 | "unassign-devices-from-edge-text": "Después de la confirmación, todos los dispositivos seleccionados quedarán sin asignar y el borde no podrá acceder a ellos." |
955 | 955 | }, |
956 | 956 | "device-profile": { |
... | ... | @@ -1123,7 +1123,7 @@ |
1123 | 1123 | "delete": "Eliminar borde", |
1124 | 1124 | "delete-edge-title": "¿Está seguro de que desea eliminar el borde '{{edgeName}}'?", |
1125 | 1125 | "delete-edge-text": "Tenga cuidado, después de la confirmación, el borde y todos los datos relacionados serán irrecuperables", |
1126 | - "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes}}?", | |
1126 | + "delete-edges-title": "¿Está seguro de que desea edge {count, plural, 1 {1 borde} other {# bordes} }?", | |
1127 | 1127 | "delete-edges-text": "Tenga cuidado, después de la confirmación se eliminarán todos los bordes seleccionados y todos los datos relacionados se volverán irrecuperables", |
1128 | 1128 | "name": "Nombre", |
1129 | 1129 | "name-starts-with": "Edge name starts with", |
... | ... | @@ -1156,7 +1156,7 @@ |
1156 | 1156 | "unassign-from-customer": "Anular asignación del cliente", |
1157 | 1157 | "unassign-edge-title": "¿Está seguro de que desea desasignar el borde '{{edgeName}}'?", |
1158 | 1158 | "unassign-edge-text": "Después de la confirmación, el borde quedará sin asignar y el cliente no podrá acceder a él", |
1159 | - "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes}}?", | |
1159 | + "unassign-edges-title": "¿Está seguro de que desea anular la asignación de {count, plural, 1 {1 borde} other {# bordes} }?", | |
1160 | 1160 | "unassign-edges-text": "Después de la confirmación de todos los bordes seleccionados, se anulará la asignación y el cliente no podrá acceder a ellos.", |
1161 | 1161 | "make-public": "Hacer público el borde", |
1162 | 1162 | "make-public-edge-title": "¿Estás seguro de que quieres hacer público el edge '{{edgeName}}'?", |
... | ... | @@ -1189,14 +1189,14 @@ |
1189 | 1189 | "rulechain-templates": "Plantillas, de cadena de reglas", |
1190 | 1190 | "rulechains": "Cadenas de regla de borde", |
1191 | 1191 | "search": "Bordes de búsqueda", |
1192 | - "selected-edges": "{count, plural, 1 {1 borde} other {# bordes}} seleccionados", | |
1192 | + "selected-edges": "{count, plural, 1 {1 borde} other {# bordes} } seleccionadas", | |
1193 | 1193 | "any-edge": "Cualquier bordee", |
1194 | 1194 | "no-edge-types-matching": "No se encontraron tipos de aristas que coincidan con '{{entitySubtype}}'.", |
1195 | 1195 | "edge-type-list-empty": "No se seleccionó ningún tipo de borde.", |
1196 | 1196 | "edge-types": "Tipos de bordes", |
1197 | 1197 | "enter-edge-type": "Ingrese el tipo de borde", |
1198 | 1198 | "deployed": "Desplegada", |
1199 | - "pending": "Pending", | |
1199 | + "pending": "Pendiente", | |
1200 | 1200 | "downlinks": "Enlaces descendentes", |
1201 | 1201 | "no-downlinks-prompt": "No se encontraron enlaces descendentes", |
1202 | 1202 | "sync-process-started-successfully": "¡El proceso de sincronización se inició correctamente!", |
... | ... | @@ -1356,7 +1356,7 @@ |
1356 | 1356 | "type-api-usage-state": "Estado de uso de la API", |
1357 | 1357 | "type-edge": "Borde", |
1358 | 1358 | "type-edges": "Bordes", |
1359 | - "list-of-edges": "{cuenta, plural, 1 {Un borde} other {Lista de # bordes}}", | |
1359 | + "list-of-edges": "{count, plural, 1 {Un borde} other {Lista de # bordes} }", | |
1360 | 1360 | "edge-name-starts-with": "Bordes cuyos nombres comienzan con '{{prefijo}}'" |
1361 | 1361 | }, |
1362 | 1362 | "entity-field": { |
... | ... | @@ -1481,9 +1481,9 @@ |
1481 | 1481 | "assign-entity-view-to-edge-text": "Seleccione las vistas de entidad para asignar al borde", |
1482 | 1482 | "unassign-entity-view-from-edge-title": "¿Está seguro de que desea anular la asignación de la vista de entidad '{{entityViewName}}'?", |
1483 | 1483 | "unassign-entity-view-from-edge-text": "Después de la confirmación, la vista de entidad quedará sin asignar y el borde no podrá acceder a ella", |
1484 | - "unassign-entity-views-from-edge-action-title": "Anular asignación {recuento, plural, 1 {1 vista de entidad} otras {# vistas de entidad}} del borde", | |
1484 | + "unassign-entity-views-from-edge-action-title": "Anular asignación {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } del borde", | |
1485 | 1485 | "unassign-entity-view-from-edge": "Anular asignación de vista de entidad", |
1486 | - "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad}}?", | |
1486 | + "unassign-entity-views-from-edge-title": "¿Está seguro de que desea desasignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", | |
1487 | 1487 | "unassign-entity-views-from-edge-text": "Después de la confirmación, todas las vistas de entidad seleccionadas no serán asignadas y el borde no podrá acceder a ellas" |
1488 | 1488 | }, |
1489 | 1489 | "event": { |
... | ... | @@ -2074,9 +2074,9 @@ |
2074 | 2074 | "delete-rulechains": "Eliminar cadenas de reglas", |
2075 | 2075 | "unassign-rulechain": "Anular asignación de cadena de reglas", |
2076 | 2076 | "unassign-rulechains": "Anular asignación de cadenas de reglas", |
2077 | - "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainTitle}}'?", | |
2077 | + "unassign-rulechain-title": "¿Está seguro de que desea desasignar la cadena de reglas '{{ruleChainName}}'?", | |
2078 | 2078 | "unassign-rulechain-from-edge-text": "Después de la confirmación, la cadena de reglas quedará sin asignar y el borde no podrá acceder a ella", |
2079 | - "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas}} des bordes", | |
2079 | + "unassign-rulechains-from-edge-action-title": "Anular asignación {count, plural, 1 {1 cadena de reglas} other {# cadenas de reglas} } des bordes", | |
2080 | 2080 | "unassign-rulechains-from-edge-text": "Después de la confirmación, todas las cadenas de reglas seleccionadas quedarán sin asignar y el borde no podrá acceder a ellas", |
2081 | 2081 | "assign-rulechain-to-edge-title": "Asignar cadena (s) de reglas a borde", |
2082 | 2082 | "assign-rulechain-to-edge-text": "Seleccione las cadenas de reglas para asignar al borde", | ... | ... |
... | ... | @@ -736,7 +736,7 @@ |
736 | 736 | "assign-device-to-edge-text":"Veuillez sélectionner la bordure pour attribuer le ou les dispositifs", |
737 | 737 | "unassign-device-from-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{deviceName}} '?", |
738 | 738 | "unassign-device-from-edge-text": "Après la confirmation, dispositif sera non attribué et ne sera pas accessible a la bordure.", |
739 | - "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices}}?", | |
739 | + "unassign-devices-from-edge-title": "Voulez-vous vraiment annuler l'affectation de {count, plural, 1 {1 device} other {# devices} }?", | |
740 | 740 | "unassign-devices-from-edge-text": "Après la confirmation, tous les dispositifs sélectionnés ne seront pas attribues et ne seront pas accessibles par la bordure." |
741 | 741 | }, |
742 | 742 | "dialog": { |
... | ... | @@ -755,7 +755,7 @@ |
755 | 755 | "delete": "Supprimer la bordure", |
756 | 756 | "delete-edge-title": "Êtes-vous sûr de vouloir supprimer la bordure '{{edgeName}}'?", |
757 | 757 | "delete-edge-text": "Faites attention, après la confirmation, la bordure et toutes les données associées deviendront irrécupérables", |
758 | - "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure}}?", | |
758 | + "delete-edges-title": "Êtes-vous sûr de vouloir supprimer {count, plural, 1 {1 bordure} other {# bordure} }?", | |
759 | 759 | "delete-edges-text": "Faites attention, après la confirmation, tous les bordures sélectionnés seront supprimés et toutes les données associées deviendront irrécupérables.", |
760 | 760 | "name": "Nom", |
761 | 761 | "name-starts-with": "Le nom du bord commence par", |
... | ... | @@ -788,7 +788,7 @@ |
788 | 788 | "unassign-from-customer": "Retirer du client", |
789 | 789 | "unassign-edge-title": "Êtes-vous sûr de vouloir annuler l'affection du dispositif {{edgeName}}", |
790 | 790 | "unassign-edge-text": "Après la confirmation, le dispositif ne sera pas attribué et ne sera pas accessible au client", |
791 | - "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures}}?", | |
791 | + "unassign-edges-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 bordure} other {# bordures} }?", | |
792 | 792 | "unassign-edges-text": "Après la confirmation, tous les bordures sélectionnés ne seront plus attribués et ne seront pas accessibles par le client.", |
793 | 793 | "make-public": "Make edge public", |
794 | 794 | "make-public-edge-title": "Are you sure you want to make the edge '{{edgeName}}' public?", |
... | ... | @@ -821,7 +821,7 @@ |
821 | 821 | "rulechain-templates": "Modèles de chaîne de règles", |
822 | 822 | "rulechains": "Chaînes de règles de la bordure", |
823 | 823 | "search": "Rechercher les bords", |
824 | - "selected-edges": "{count, plural, 1 {1 edge} other {# bords}} sélectionné", | |
824 | + "selected-edges": "{count, plural, 1 {1 bordure} other {# bords} } sélectionné", | |
825 | 825 | "any-edge": "Tout bord", |
826 | 826 | "no-edge-types-matching": "Aucun type d'arête correspondant à \"{{entitySubtype}}\" n'a été trouvé.", |
827 | 827 | "edge-type-list-empty": "Aucun type d'arête sélectionné.", |
... | ... | @@ -1479,9 +1479,9 @@ |
1479 | 1479 | "delete-rulechains": "Supprimer une chaînes de règles", |
1480 | 1480 | "unassign-rulechain": "Retirer chaîne de règles", |
1481 | 1481 | "unassign-rulechains": "Retirer chaînes de règles", |
1482 | - "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainTitle}}'?", | |
1482 | + "unassign-rulechain-title": "AÊtes-vous sûr de vouloir retirer l'attribution de chaînes de règles '{{ruleChainName}}'?", | |
1483 | 1483 | "unassign-rulechain-from-edge-text": "Après la confirmation, l'actif sera non attribué et ne sera pas accessible a la bordure.", |
1484 | - "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles}} de la bordure", | |
1484 | + "unassign-rulechains-from-edge-action-title": "Retirer {count, plural, 1 {1 chaîne de règles} other {# chaînes de règles} } de la bordure", | |
1485 | 1485 | "unassign-rulechains-from-edge-text": "Après la confirmation, tous les chaînes de règles sélectionnés ne seront pas attribués et ne seront pas accessibles a la bordure.", |
1486 | 1486 | "assign-rulechain-to-edge-title": "Attribuer les chaînes de règles a la bordure", |
1487 | 1487 | "assign-rulechain-to-edge-text": "Veuillez sélectionner la bordure pour attribuer le ou les chaînes de règles", |
... | ... | @@ -1497,11 +1497,11 @@ |
1497 | 1497 | "unset-auto-assign-to-edge-text": "Après la confirmation, la chaîne de règles d'arêtes ne sera plus automatiquement affectée aux arêtes lors de la création.", |
1498 | 1498 | "edge-template-root": "Racine du modèle", |
1499 | 1499 | "search": "Rechercher des chaînes de règles", |
1500 | - "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains}} sélectionné", | |
1500 | + "selected-rulechains": "{count, plural, 1 {1 rule chain} other {# rule chains} } sélectionné", | |
1501 | 1501 | "open-rulechain": "Chaîne de règles ouverte", |
1502 | - "assign-to-edge": "Attribuer à Edge", | |
1503 | - "edge-rulechain": "Chaîne de règles Edge", | |
1504 | - "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains}}?" | |
1502 | + "assign-to-edge": "Attribuer à Bordure", | |
1503 | + "edge-rulechain": "Chaîne de règles Bordure", | |
1504 | + "unassign-rulechains-from-edge-title": "Voulez-vous vraiment annuler l'attribution de {count, plural, 1 {1 rulechain} other {# rulechains} }?" | |
1505 | 1505 | }, |
1506 | 1506 | "rulenode": { |
1507 | 1507 | "add": "Ajouter un noeud de règle", | ... | ... |