Commit 37407c4214bdc0462288976cf07e1623a1e2cda6
Merge branch 'master' of github.com:thingsboard/thingsboard into fix/coap-transport/obesrve-sessions
Showing
73 changed files
with
2550 additions
and
488 deletions
... | ... | @@ -782,15 +782,17 @@ public class DeviceController extends BaseController { |
782 | 782 | } |
783 | 783 | |
784 | 784 | @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") |
785 | - @RequestMapping(value = "/devices/count/{otaPackageType}", method = RequestMethod.GET) | |
785 | + @RequestMapping(value = "/devices/count/{otaPackageType}/{deviceProfileId}", method = RequestMethod.GET) | |
786 | 786 | @ResponseBody |
787 | - public Long countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType, | |
788 | - @RequestParam String deviceProfileId) throws ThingsboardException { | |
787 | + public Long countByDeviceProfileAndEmptyOtaPackage(@PathVariable("otaPackageType") String otaPackageType, | |
788 | + @PathVariable("deviceProfileId") String deviceProfileId) throws ThingsboardException { | |
789 | 789 | checkParameter("OtaPackageType", otaPackageType); |
790 | 790 | checkParameter("DeviceProfileId", deviceProfileId); |
791 | 791 | try { |
792 | 792 | return deviceService.countDevicesByTenantIdAndDeviceProfileIdAndEmptyOtaPackage( |
793 | - getCurrentUser().getTenantId(), new DeviceProfileId(UUID.fromString(deviceProfileId)), OtaPackageType.valueOf(otaPackageType)); | |
793 | + getTenantId(), | |
794 | + new DeviceProfileId(UUID.fromString(deviceProfileId)), | |
795 | + OtaPackageType.valueOf(otaPackageType)); | |
794 | 796 | } catch (Exception e) { |
795 | 797 | throw handleException(e); |
796 | 798 | } | ... | ... |
... | ... | @@ -74,6 +74,7 @@ import java.util.List; |
74 | 74 | import java.util.Map; |
75 | 75 | import java.util.Set; |
76 | 76 | import java.util.concurrent.ConcurrentMap; |
77 | +import java.util.concurrent.TimeUnit; | |
77 | 78 | import java.util.stream.Collectors; |
78 | 79 | |
79 | 80 | @Slf4j |
... | ... | @@ -86,6 +87,7 @@ public class RuleChainController extends BaseController { |
86 | 87 | public static final String RULE_NODE_ID = "ruleNodeId"; |
87 | 88 | |
88 | 89 | private static final ObjectMapper objectMapper = new ObjectMapper(); |
90 | + public static final int TIMEOUT = 20; | |
89 | 91 | |
90 | 92 | @Autowired |
91 | 93 | private InstallScripts installScripts; |
... | ... | @@ -388,25 +390,25 @@ public class RuleChainController extends BaseController { |
388 | 390 | TbMsg inMsg = TbMsg.newMsg(msgType, null, new TbMsgMetaData(metadata), TbMsgDataType.JSON, data); |
389 | 391 | switch (scriptType) { |
390 | 392 | case "update": |
391 | - output = msgToOutput(engine.executeUpdate(inMsg)); | |
393 | + output = msgToOutput(engine.executeUpdateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); | |
392 | 394 | break; |
393 | 395 | case "generate": |
394 | - output = msgToOutput(engine.executeGenerate(inMsg)); | |
396 | + output = msgToOutput(engine.executeGenerateAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS)); | |
395 | 397 | break; |
396 | 398 | case "filter": |
397 | - boolean result = engine.executeFilter(inMsg); | |
399 | + boolean result = engine.executeFilterAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); | |
398 | 400 | output = Boolean.toString(result); |
399 | 401 | break; |
400 | 402 | case "switch": |
401 | - Set<String> states = engine.executeSwitch(inMsg); | |
403 | + Set<String> states = engine.executeSwitchAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); | |
402 | 404 | output = objectMapper.writeValueAsString(states); |
403 | 405 | break; |
404 | 406 | case "json": |
405 | - JsonNode json = engine.executeJson(inMsg); | |
407 | + JsonNode json = engine.executeJsonAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); | |
406 | 408 | output = objectMapper.writeValueAsString(json); |
407 | 409 | break; |
408 | 410 | case "string": |
409 | - output = engine.executeToString(inMsg); | |
411 | + output = engine.executeToStringAsync(inMsg).get(TIMEOUT, TimeUnit.SECONDS); | |
410 | 412 | break; |
411 | 413 | default: |
412 | 414 | throw new IllegalArgumentException("Unsupported script type: " + scriptType); | ... | ... |
... | ... | @@ -30,6 +30,7 @@ import java.util.UUID; |
30 | 30 | import java.util.concurrent.ConcurrentHashMap; |
31 | 31 | import java.util.concurrent.Executors; |
32 | 32 | import java.util.concurrent.ScheduledExecutorService; |
33 | +import java.util.concurrent.TimeoutException; | |
33 | 34 | import java.util.concurrent.atomic.AtomicInteger; |
34 | 35 | |
35 | 36 | /** |
... | ... | @@ -84,8 +85,10 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { |
84 | 85 | apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1); |
85 | 86 | return doInvokeFunction(scriptId, functionName, args); |
86 | 87 | } else { |
87 | - return Futures.immediateFailedFuture( | |
88 | - new RuntimeException("Script invocation is blocked due to maximum error count " + getMaxErrors() + "!")); | |
88 | + String message = "Script invocation is blocked due to maximum error count " | |
89 | + + getMaxErrors() + ", scriptId " + scriptId + "!"; | |
90 | + log.warn(message); | |
91 | + return Futures.immediateFailedFuture(new RuntimeException(message)); | |
89 | 92 | } |
90 | 93 | } else { |
91 | 94 | return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!")); |
... | ... | @@ -117,8 +120,11 @@ public abstract class AbstractJsInvokeService implements JsInvokeService { |
117 | 120 | |
118 | 121 | protected abstract long getMaxBlacklistDuration(); |
119 | 122 | |
120 | - protected void onScriptExecutionError(UUID scriptId) { | |
121 | - disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()).incrementAndGet(); | |
123 | + protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) { | |
124 | + DisableListInfo disableListInfo = disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo()); | |
125 | + log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}", | |
126 | + disableListInfo.get(), scriptId, t, t.getCause(), scriptBody); | |
127 | + disableListInfo.incrementAndGet(); | |
122 | 128 | } |
123 | 129 | |
124 | 130 | private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) { | ... | ... |
... | ... | @@ -160,7 +160,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer |
160 | 160 | return ((Invocable) engine).invokeFunction(functionName, args); |
161 | 161 | } |
162 | 162 | } catch (Exception e) { |
163 | - onScriptExecutionError(scriptId); | |
163 | + onScriptExecutionError(scriptId, e, functionName); | |
164 | 164 | throw new ExecutionException(e); |
165 | 165 | } |
166 | 166 | }); | ... | ... |
... | ... | @@ -18,7 +18,6 @@ package org.thingsboard.server.service.script; |
18 | 18 | import com.google.common.util.concurrent.FutureCallback; |
19 | 19 | import com.google.common.util.concurrent.Futures; |
20 | 20 | import com.google.common.util.concurrent.ListenableFuture; |
21 | -import com.google.common.util.concurrent.MoreExecutors; | |
22 | 21 | import lombok.Getter; |
23 | 22 | import lombok.extern.slf4j.Slf4j; |
24 | 23 | import org.springframework.beans.factory.annotation.Autowired; |
... | ... | @@ -26,6 +25,7 @@ import org.springframework.beans.factory.annotation.Value; |
26 | 25 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
27 | 26 | import org.springframework.scheduling.annotation.Scheduled; |
28 | 27 | import org.springframework.stereotype.Service; |
28 | +import org.springframework.util.StopWatch; | |
29 | 29 | import org.thingsboard.common.util.ThingsBoardThreadFactory; |
30 | 30 | import org.thingsboard.server.gen.js.JsInvokeProtos; |
31 | 31 | import org.thingsboard.server.queue.TbQueueRequestTemplate; |
... | ... | @@ -161,7 +161,8 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { |
161 | 161 | |
162 | 162 | @Override |
163 | 163 | protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) { |
164 | - String scriptBody = scriptIdToBodysMap.get(scriptId); | |
164 | + log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptId, maxRequestsTimeout); | |
165 | + final String scriptBody = scriptIdToBodysMap.get(scriptId); | |
165 | 166 | if (scriptBody == null) { |
166 | 167 | return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!")); |
167 | 168 | } |
... | ... | @@ -170,7 +171,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { |
170 | 171 | .setScriptIdLSB(scriptId.getLeastSignificantBits()) |
171 | 172 | .setFunctionName(functionName) |
172 | 173 | .setTimeout((int) maxRequestsTimeout) |
173 | - .setScriptBody(scriptIdToBodysMap.get(scriptId)); | |
174 | + .setScriptBody(scriptBody); | |
174 | 175 | |
175 | 176 | for (Object arg : args) { |
176 | 177 | jsRequestBuilder.addArgs(arg.toString()); |
... | ... | @@ -180,6 +181,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { |
180 | 181 | .setInvokeRequest(jsRequestBuilder.build()) |
181 | 182 | .build(); |
182 | 183 | |
184 | + StopWatch stopWatch = new StopWatch(); | |
185 | + stopWatch.start(); | |
186 | + | |
183 | 187 | ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper)); |
184 | 188 | if (maxRequestsTimeout > 0) { |
185 | 189 | future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService); |
... | ... | @@ -193,7 +197,7 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { |
193 | 197 | |
194 | 198 | @Override |
195 | 199 | public void onFailure(Throwable t) { |
196 | - onScriptExecutionError(scriptId); | |
200 | + onScriptExecutionError(scriptId, t, scriptBody); | |
197 | 201 | if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) { |
198 | 202 | queueTimeoutMsgs.incrementAndGet(); |
199 | 203 | } |
... | ... | @@ -201,13 +205,16 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService { |
201 | 205 | } |
202 | 206 | }, callbackExecutor); |
203 | 207 | return Futures.transform(future, response -> { |
208 | + stopWatch.stop(); | |
209 | + log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey()); | |
204 | 210 | JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse(); |
205 | 211 | if (invokeResult.getSuccess()) { |
206 | 212 | return invokeResult.getResult(); |
207 | 213 | } else { |
208 | - onScriptExecutionError(scriptId); | |
214 | + final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails()); | |
215 | + onScriptExecutionError(scriptId, e, scriptBody); | |
209 | 216 | log.debug("[{}] Failed to compile script due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails()); |
210 | - throw new RuntimeException(invokeResult.getErrorDetails()); | |
217 | + throw e; | |
211 | 218 | } |
212 | 219 | }, callbackExecutor); |
213 | 220 | } | ... | ... |
... | ... | @@ -18,12 +18,12 @@ package org.thingsboard.server.service.script; |
18 | 18 | import com.fasterxml.jackson.core.type.TypeReference; |
19 | 19 | import com.fasterxml.jackson.databind.JsonNode; |
20 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; |
21 | -import com.google.common.collect.Sets; | |
22 | 21 | import com.google.common.util.concurrent.Futures; |
23 | 22 | import com.google.common.util.concurrent.ListenableFuture; |
24 | 23 | import com.google.common.util.concurrent.MoreExecutors; |
25 | 24 | import lombok.extern.slf4j.Slf4j; |
26 | 25 | import org.apache.commons.lang3.StringUtils; |
26 | +import org.thingsboard.server.common.data.id.CustomerId; | |
27 | 27 | import org.thingsboard.server.common.data.id.EntityId; |
28 | 28 | import org.thingsboard.server.common.data.id.TenantId; |
29 | 29 | import org.thingsboard.server.common.msg.TbMsg; |
... | ... | @@ -32,6 +32,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; |
32 | 32 | import javax.script.ScriptException; |
33 | 33 | import java.util.ArrayList; |
34 | 34 | import java.util.Collections; |
35 | +import java.util.HashSet; | |
35 | 36 | import java.util.List; |
36 | 37 | import java.util.Map; |
37 | 38 | import java.util.Set; |
... | ... | @@ -102,140 +103,115 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S |
102 | 103 | String newMessageType = !StringUtils.isEmpty(messageType) ? messageType : msg.getType(); |
103 | 104 | return TbMsg.transformMsg(msg, newMessageType, msg.getOriginator(), newMetadata, newData); |
104 | 105 | } catch (Throwable th) { |
105 | - th.printStackTrace(); | |
106 | 106 | throw new RuntimeException("Failed to unbind message data from javascript result", th); |
107 | 107 | } |
108 | 108 | } |
109 | 109 | |
110 | 110 | @Override |
111 | - public List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException { | |
112 | - JsonNode result = executeScript(msg); | |
113 | - if (result.isObject()) { | |
114 | - return Collections.singletonList(unbindMsg(result, msg)); | |
115 | - } else if (result.isArray()){ | |
116 | - List<TbMsg> res = new ArrayList<>(result.size()); | |
117 | - result.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); | |
118 | - return res; | |
119 | - } else { | |
120 | - log.warn("Wrong result type: {}", result.getNodeType()); | |
121 | - throw new ScriptException("Wrong result type: " + result.getNodeType()); | |
111 | + public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) { | |
112 | + ListenableFuture<JsonNode> result = executeScriptAsync(msg); | |
113 | + return Futures.transformAsync(result, | |
114 | + json -> executeUpdateTransform(msg, json), | |
115 | + MoreExecutors.directExecutor()); | |
116 | + } | |
117 | + | |
118 | + ListenableFuture<List<TbMsg>> executeUpdateTransform(TbMsg msg, JsonNode json) { | |
119 | + if (json.isObject()) { | |
120 | + return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); | |
121 | + } else if (json.isArray()) { | |
122 | + List<TbMsg> res = new ArrayList<>(json.size()); | |
123 | + json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); | |
124 | + return Futures.immediateFuture(res); | |
122 | 125 | } |
126 | + log.warn("Wrong result type: {}", json.getNodeType()); | |
127 | + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); | |
123 | 128 | } |
124 | 129 | |
125 | 130 | @Override |
126 | - public ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg) { | |
127 | - ListenableFuture<JsonNode> result = executeScriptAsync(msg); | |
128 | - return Futures.transformAsync(result, json -> { | |
129 | - if (json.isObject()) { | |
130 | - return Futures.immediateFuture(Collections.singletonList(unbindMsg(json, msg))); | |
131 | - } else if (json.isArray()){ | |
132 | - List<TbMsg> res = new ArrayList<>(json.size()); | |
133 | - json.forEach(jsonObject -> res.add(unbindMsg(jsonObject, msg))); | |
134 | - return Futures.immediateFuture(res); | |
135 | - } | |
136 | - else{ | |
137 | - log.warn("Wrong result type: {}", json.getNodeType()); | |
138 | - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); | |
139 | - } | |
140 | - }, MoreExecutors.directExecutor()); | |
131 | + public ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg) { | |
132 | + return Futures.transformAsync(executeScriptAsync(prevMsg), | |
133 | + result -> executeGenerateTransform(prevMsg, result), | |
134 | + MoreExecutors.directExecutor()); | |
141 | 135 | } |
142 | 136 | |
143 | - @Override | |
144 | - public TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException { | |
145 | - JsonNode result = executeScript(prevMsg); | |
137 | + ListenableFuture<TbMsg> executeGenerateTransform(TbMsg prevMsg, JsonNode result) { | |
146 | 138 | if (!result.isObject()) { |
147 | 139 | log.warn("Wrong result type: {}", result.getNodeType()); |
148 | - throw new ScriptException("Wrong result type: " + result.getNodeType()); | |
140 | + Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); | |
149 | 141 | } |
150 | - return unbindMsg(result, prevMsg); | |
142 | + return Futures.immediateFuture(unbindMsg(result, prevMsg)); | |
151 | 143 | } |
152 | 144 | |
153 | 145 | @Override |
154 | - public JsonNode executeJson(TbMsg msg) throws ScriptException { | |
155 | - return executeScript(msg); | |
156 | - } | |
157 | - | |
158 | - @Override | |
159 | - public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException { | |
146 | + public ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) { | |
160 | 147 | return executeScriptAsync(msg); |
161 | 148 | } |
162 | 149 | |
163 | 150 | @Override |
164 | - public String executeToString(TbMsg msg) throws ScriptException { | |
165 | - JsonNode result = executeScript(msg); | |
166 | - if (!result.isTextual()) { | |
167 | - log.warn("Wrong result type: {}", result.getNodeType()); | |
168 | - throw new ScriptException("Wrong result type: " + result.getNodeType()); | |
169 | - } | |
170 | - return result.asText(); | |
151 | + public ListenableFuture<String> executeToStringAsync(TbMsg msg) { | |
152 | + return Futures.transformAsync(executeScriptAsync(msg), | |
153 | + this::executeToStringTransform, | |
154 | + MoreExecutors.directExecutor()); | |
171 | 155 | } |
172 | 156 | |
173 | - @Override | |
174 | - public boolean executeFilter(TbMsg msg) throws ScriptException { | |
175 | - JsonNode result = executeScript(msg); | |
176 | - if (!result.isBoolean()) { | |
177 | - log.warn("Wrong result type: {}", result.getNodeType()); | |
178 | - throw new ScriptException("Wrong result type: " + result.getNodeType()); | |
157 | + ListenableFuture<String> executeToStringTransform(JsonNode result) { | |
158 | + if (result.isTextual()) { | |
159 | + return Futures.immediateFuture(result.asText()); | |
179 | 160 | } |
180 | - return result.asBoolean(); | |
161 | + log.warn("Wrong result type: {}", result.getNodeType()); | |
162 | + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); | |
181 | 163 | } |
182 | 164 | |
183 | 165 | @Override |
184 | 166 | public ListenableFuture<Boolean> executeFilterAsync(TbMsg msg) { |
185 | - ListenableFuture<JsonNode> result = executeScriptAsync(msg); | |
186 | - return Futures.transformAsync(result, json -> { | |
187 | - if (!json.isBoolean()) { | |
188 | - log.warn("Wrong result type: {}", json.getNodeType()); | |
189 | - return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); | |
190 | - } else { | |
191 | - return Futures.immediateFuture(json.asBoolean()); | |
192 | - } | |
193 | - }, MoreExecutors.directExecutor()); | |
167 | + return Futures.transformAsync(executeScriptAsync(msg), | |
168 | + this::executeFilterTransform, | |
169 | + MoreExecutors.directExecutor()); | |
194 | 170 | } |
195 | 171 | |
196 | - @Override | |
197 | - public Set<String> executeSwitch(TbMsg msg) throws ScriptException { | |
198 | - JsonNode result = executeScript(msg); | |
172 | + ListenableFuture<Boolean> executeFilterTransform(JsonNode json) { | |
173 | + if (json.isBoolean()) { | |
174 | + return Futures.immediateFuture(json.asBoolean()); | |
175 | + } | |
176 | + log.warn("Wrong result type: {}", json.getNodeType()); | |
177 | + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + json.getNodeType())); | |
178 | + } | |
179 | + | |
180 | + ListenableFuture<Set<String>> executeSwitchTransform(JsonNode result) { | |
199 | 181 | if (result.isTextual()) { |
200 | - return Collections.singleton(result.asText()); | |
201 | - } else if (result.isArray()) { | |
202 | - Set<String> nextStates = Sets.newHashSet(); | |
182 | + return Futures.immediateFuture(Collections.singleton(result.asText())); | |
183 | + } | |
184 | + if (result.isArray()) { | |
185 | + Set<String> nextStates = new HashSet<>(); | |
203 | 186 | for (JsonNode val : result) { |
204 | 187 | if (!val.isTextual()) { |
205 | 188 | log.warn("Wrong result type: {}", val.getNodeType()); |
206 | - throw new ScriptException("Wrong result type: " + val.getNodeType()); | |
189 | + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + val.getNodeType())); | |
207 | 190 | } else { |
208 | 191 | nextStates.add(val.asText()); |
209 | 192 | } |
210 | 193 | } |
211 | - return nextStates; | |
212 | - } else { | |
213 | - log.warn("Wrong result type: {}", result.getNodeType()); | |
214 | - throw new ScriptException("Wrong result type: " + result.getNodeType()); | |
194 | + return Futures.immediateFuture(nextStates); | |
215 | 195 | } |
196 | + log.warn("Wrong result type: {}", result.getNodeType()); | |
197 | + return Futures.immediateFailedFuture(new ScriptException("Wrong result type: " + result.getNodeType())); | |
216 | 198 | } |
217 | 199 | |
218 | - private JsonNode executeScript(TbMsg msg) throws ScriptException { | |
219 | - try { | |
220 | - String[] inArgs = prepareArgs(msg); | |
221 | - String eval = sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]).get().toString(); | |
222 | - return mapper.readTree(eval); | |
223 | - } catch (ExecutionException e) { | |
224 | - if (e.getCause() instanceof ScriptException) { | |
225 | - throw (ScriptException) e.getCause(); | |
226 | - } else if (e.getCause() instanceof RuntimeException) { | |
227 | - throw new ScriptException(e.getCause().getMessage()); | |
228 | - } else { | |
229 | - throw new ScriptException(e); | |
230 | - } | |
231 | - } catch (Exception e) { | |
232 | - throw new ScriptException(e); | |
233 | - } | |
200 | + @Override | |
201 | + public ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg) { | |
202 | + return Futures.transformAsync(executeScriptAsync(msg), | |
203 | + this::executeSwitchTransform, | |
204 | + MoreExecutors.directExecutor()); //usually runs in a callbackExecutor | |
234 | 205 | } |
235 | 206 | |
236 | - private ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) { | |
207 | + ListenableFuture<JsonNode> executeScriptAsync(TbMsg msg) { | |
208 | + log.trace("execute script async, msg {}", msg); | |
237 | 209 | String[] inArgs = prepareArgs(msg); |
238 | - return Futures.transformAsync(sandboxService.invokeFunction(tenantId, msg.getCustomerId(), this.scriptId, inArgs[0], inArgs[1], inArgs[2]), | |
210 | + return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]); | |
211 | + } | |
212 | + | |
213 | + ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, Object... args) { | |
214 | + return Futures.transformAsync(sandboxService.invokeFunction(tenantId, customerId, this.scriptId, args), | |
239 | 215 | o -> { |
240 | 216 | try { |
241 | 217 | return Futures.immediateFuture(mapper.readTree(o.toString())); | ... | ... |
... | ... | @@ -332,6 +332,7 @@ actors: |
332 | 332 | cache: |
333 | 333 | # caffeine or redis |
334 | 334 | type: "${CACHE_TYPE:caffeine}" |
335 | + maximumPoolSize: "${CACHE_MAXIMUM_POOL_SIZE:16}" # max pool size to process futures that calls the external cache | |
335 | 336 | attributes: |
336 | 337 | # make sure that if cache.type is 'redis' and cache.attributes.enabled is 'true' that you change 'maxmemory-policy' Redis config property to 'allkeys-lru', 'allkeys-lfu' or 'allkeys-random' |
337 | 338 | enabled: "${CACHE_ATTRIBUTES_ENABLED:true}" | ... | ... |
... | ... | @@ -51,6 +51,13 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur |
51 | 51 | private String privacyPassphrase; |
52 | 52 | private String engineId; |
53 | 53 | |
54 | + public SnmpDeviceTransportConfiguration() { | |
55 | + this.host = "localhost"; | |
56 | + this.port = 161; | |
57 | + this.protocolVersion = SnmpProtocolVersion.V2C; | |
58 | + this.community = "public"; | |
59 | + } | |
60 | + | |
54 | 61 | @Override |
55 | 62 | public DeviceTransportType getType() { |
56 | 63 | return DeviceTransportType.SNMP; |
... | ... | @@ -76,7 +83,7 @@ public class SnmpDeviceTransportConfiguration implements DeviceTransportConfigur |
76 | 83 | isValid = StringUtils.isNotBlank(username) && StringUtils.isNotBlank(securityName) |
77 | 84 | && contextName != null && authenticationProtocol != null |
78 | 85 | && StringUtils.isNotBlank(authenticationPassphrase) |
79 | - && privacyProtocol != null && privacyPassphrase != null && engineId != null; | |
86 | + && privacyProtocol != null && StringUtils.isNotBlank(privacyPassphrase) && engineId != null; | |
80 | 87 | break; |
81 | 88 | } |
82 | 89 | } | ... | ... |
... | ... | @@ -24,6 +24,8 @@ public interface TbQueueRequestTemplate<Request extends TbQueueMsg, Response ext |
24 | 24 | |
25 | 25 | ListenableFuture<Response> send(Request request); |
26 | 26 | |
27 | + ListenableFuture<Response> send(Request request, long timeoutNs); | |
28 | + | |
27 | 29 | void stop(); |
28 | 30 | |
29 | 31 | void setMessagesStats(MessagesStats messagesStats); | ... | ... |
... | ... | @@ -19,7 +19,9 @@ import com.google.common.util.concurrent.Futures; |
19 | 19 | import com.google.common.util.concurrent.ListenableFuture; |
20 | 20 | import com.google.common.util.concurrent.SettableFuture; |
21 | 21 | import lombok.Builder; |
22 | +import lombok.Getter; | |
22 | 23 | import lombok.extern.slf4j.Slf4j; |
24 | +import org.thingsboard.common.util.TbStopWatch; | |
23 | 25 | import org.thingsboard.common.util.ThingsBoardThreadFactory; |
24 | 26 | import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
25 | 27 | import org.thingsboard.server.queue.TbQueueAdmin; |
... | ... | @@ -31,13 +33,17 @@ import org.thingsboard.server.queue.TbQueueProducer; |
31 | 33 | import org.thingsboard.server.queue.TbQueueRequestTemplate; |
32 | 34 | import org.thingsboard.server.common.stats.MessagesStats; |
33 | 35 | |
36 | +import javax.annotation.Nullable; | |
34 | 37 | import java.util.List; |
35 | 38 | import java.util.UUID; |
36 | 39 | import java.util.concurrent.ConcurrentHashMap; |
37 | -import java.util.concurrent.ConcurrentMap; | |
38 | 40 | import java.util.concurrent.ExecutorService; |
39 | 41 | import java.util.concurrent.Executors; |
42 | +import java.util.concurrent.TimeUnit; | |
40 | 43 | import java.util.concurrent.TimeoutException; |
44 | +import java.util.concurrent.locks.Lock; | |
45 | +import java.util.concurrent.locks.LockSupport; | |
46 | +import java.util.concurrent.locks.ReentrantLock; | |
41 | 47 | |
42 | 48 | @Slf4j |
43 | 49 | public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response extends TbQueueMsg> extends AbstractTbQueueTemplate |
... | ... | @@ -46,15 +52,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
46 | 52 | private final TbQueueAdmin queueAdmin; |
47 | 53 | private final TbQueueProducer<Request> requestTemplate; |
48 | 54 | private final TbQueueConsumer<Response> responseTemplate; |
49 | - private final ConcurrentMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests; | |
50 | - private final boolean internalExecutor; | |
51 | - private final ExecutorService executor; | |
52 | - private final long maxRequestTimeout; | |
53 | - private final long maxPendingRequests; | |
54 | - private final long pollInterval; | |
55 | - private volatile long tickTs = 0L; | |
56 | - private volatile long tickSize = 0L; | |
57 | - private volatile boolean stopped = false; | |
55 | + final ConcurrentHashMap<UUID, DefaultTbQueueRequestTemplate.ResponseMetaData<Response>> pendingRequests = new ConcurrentHashMap<>(); | |
56 | + final boolean internalExecutor; | |
57 | + final ExecutorService executor; | |
58 | + final long maxRequestTimeoutNs; | |
59 | + final long maxPendingRequests; | |
60 | + final long pollInterval; | |
61 | + volatile boolean stopped = false; | |
62 | + long nextCleanupNs = 0L; | |
63 | + private final Lock cleanerLock = new ReentrantLock(); | |
58 | 64 | |
59 | 65 | private MessagesStats messagesStats; |
60 | 66 | |
... | ... | @@ -65,79 +71,113 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
65 | 71 | long maxRequestTimeout, |
66 | 72 | long maxPendingRequests, |
67 | 73 | long pollInterval, |
68 | - ExecutorService executor) { | |
74 | + @Nullable ExecutorService executor) { | |
69 | 75 | this.queueAdmin = queueAdmin; |
70 | 76 | this.requestTemplate = requestTemplate; |
71 | 77 | this.responseTemplate = responseTemplate; |
72 | - this.pendingRequests = new ConcurrentHashMap<>(); | |
73 | - this.maxRequestTimeout = maxRequestTimeout; | |
78 | + this.maxRequestTimeoutNs = TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout); | |
74 | 79 | this.maxPendingRequests = maxPendingRequests; |
75 | 80 | this.pollInterval = pollInterval; |
76 | - if (executor != null) { | |
77 | - internalExecutor = false; | |
78 | - this.executor = executor; | |
79 | - } else { | |
80 | - internalExecutor = true; | |
81 | - this.executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic())); | |
82 | - } | |
81 | + this.internalExecutor = (executor == null); | |
82 | + this.executor = internalExecutor ? createExecutor() : executor; | |
83 | + } | |
84 | + | |
85 | + ExecutorService createExecutor() { | |
86 | + return Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("tb-queue-request-template-" + responseTemplate.getTopic())); | |
83 | 87 | } |
84 | 88 | |
85 | 89 | @Override |
86 | 90 | public void init() { |
87 | 91 | queueAdmin.createTopicIfNotExists(responseTemplate.getTopic()); |
88 | - this.requestTemplate.init(); | |
89 | - tickTs = System.currentTimeMillis(); | |
92 | + requestTemplate.init(); | |
90 | 93 | responseTemplate.subscribe(); |
91 | - executor.submit(() -> { | |
92 | - long nextCleanupMs = 0L; | |
93 | - while (!stopped) { | |
94 | - try { | |
95 | - List<Response> responses = responseTemplate.poll(pollInterval); | |
96 | - if (responses.size() > 0) { | |
97 | - log.trace("Polling responses completed, consumer records count [{}]", responses.size()); | |
98 | - } | |
99 | - responses.forEach(response -> { | |
100 | - byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER); | |
101 | - UUID requestId; | |
102 | - if (requestIdHeader == null) { | |
103 | - log.error("[{}] Missing requestId in header and body", response); | |
104 | - } else { | |
105 | - requestId = bytesToUuid(requestIdHeader); | |
106 | - log.trace("[{}] Response received: {}", requestId, response); | |
107 | - ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId); | |
108 | - if (expectedResponse == null) { | |
109 | - log.trace("[{}] Invalid or stale request", requestId); | |
110 | - } else { | |
111 | - expectedResponse.future.set(response); | |
112 | - } | |
94 | + executor.submit(this::mainLoop); | |
95 | + } | |
96 | + | |
97 | + void mainLoop() { | |
98 | + while (!stopped) { | |
99 | + TbStopWatch sw = TbStopWatch.startNew(); | |
100 | + try { | |
101 | + fetchAndProcessResponses(); | |
102 | + } catch (Throwable e) { | |
103 | + long sleepNanos = TimeUnit.MILLISECONDS.toNanos(this.pollInterval) - sw.stopAndGetTotalTimeNanos(); | |
104 | + log.warn("Failed to obtain and process responses from queue. Going to sleep " + sleepNanos + "ns", e); | |
105 | + sleep(sleepNanos); | |
106 | + } | |
107 | + } | |
108 | + } | |
109 | + | |
110 | + void fetchAndProcessResponses() { | |
111 | + final long pendingRequestsCount = pendingRequests.mappingCount(); | |
112 | + log.trace("Starting template pool topic {}, for pendingRequests {}", responseTemplate.getTopic(), pendingRequestsCount); | |
113 | + List<Response> responses = doPoll(); //poll js responses | |
114 | + log.trace("Completed template poll topic {}, for pendingRequests [{}], received [{}] responses", responseTemplate.getTopic(), pendingRequestsCount, responses.size()); | |
115 | + responses.forEach(this::processResponse); //this can take a long time | |
116 | + responseTemplate.commit(); | |
117 | + tryCleanStaleRequests(); | |
118 | + } | |
119 | + | |
120 | + private boolean tryCleanStaleRequests() { | |
121 | + if (!cleanerLock.tryLock()) { | |
122 | + return false; | |
123 | + } | |
124 | + try { | |
125 | + log.trace("tryCleanStaleRequest..."); | |
126 | + final long currentNs = getCurrentClockNs(); | |
127 | + if (nextCleanupNs < currentNs) { | |
128 | + pendingRequests.forEach((key, value) -> { | |
129 | + if (value.expTime < currentNs) { | |
130 | + ResponseMetaData<Response> staleRequest = pendingRequests.remove(key); | |
131 | + if (staleRequest != null) { | |
132 | + setTimeoutException(key, staleRequest, currentNs); | |
113 | 133 | } |
114 | - }); | |
115 | - responseTemplate.commit(); | |
116 | - tickTs = System.currentTimeMillis(); | |
117 | - tickSize = pendingRequests.size(); | |
118 | - if (nextCleanupMs < tickTs) { | |
119 | - //cleanup; | |
120 | - pendingRequests.forEach((key, value) -> { | |
121 | - if (value.expTime < tickTs) { | |
122 | - ResponseMetaData<Response> staleRequest = pendingRequests.remove(key); | |
123 | - if (staleRequest != null) { | |
124 | - log.trace("[{}] Request timeout detected, expTime [{}], tickTs [{}]", key, staleRequest.expTime, tickTs); | |
125 | - staleRequest.future.setException(new TimeoutException()); | |
126 | - } | |
127 | - } | |
128 | - }); | |
129 | - nextCleanupMs = tickTs + maxRequestTimeout; | |
130 | - } | |
131 | - } catch (Throwable e) { | |
132 | - log.warn("Failed to obtain responses from queue.", e); | |
133 | - try { | |
134 | - Thread.sleep(pollInterval); | |
135 | - } catch (InterruptedException e2) { | |
136 | - log.trace("Failed to wait until the server has capacity to handle new responses", e2); | |
137 | 134 | } |
138 | - } | |
135 | + }); | |
136 | + setupNextCleanup(); | |
139 | 137 | } |
140 | - }); | |
138 | + } finally { | |
139 | + cleanerLock.unlock(); | |
140 | + } | |
141 | + return true; | |
142 | + } | |
143 | + | |
144 | + void setupNextCleanup() { | |
145 | + nextCleanupNs = getCurrentClockNs() + maxRequestTimeoutNs; | |
146 | + log.trace("setupNextCleanup {}", nextCleanupNs); | |
147 | + } | |
148 | + | |
149 | + List<Response> doPoll() { | |
150 | + return responseTemplate.poll(pollInterval); | |
151 | + } | |
152 | + | |
153 | + void sleep(long nanos) { | |
154 | + LockSupport.parkNanos(nanos); | |
155 | + } | |
156 | + | |
157 | + void setTimeoutException(UUID key, ResponseMetaData<Response> staleRequest, long currentNs) { | |
158 | + if (currentNs >= staleRequest.getSubmitTime() + staleRequest.getTimeout()) { | |
159 | + log.warn("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key); | |
160 | + } else { | |
161 | + log.error("Request timeout detected, currentNs [{}], {}, key [{}]", currentNs, staleRequest, key); | |
162 | + } | |
163 | + staleRequest.future.setException(new TimeoutException()); | |
164 | + } | |
165 | + | |
166 | + void processResponse(Response response) { | |
167 | + byte[] requestIdHeader = response.getHeaders().get(REQUEST_ID_HEADER); | |
168 | + UUID requestId; | |
169 | + if (requestIdHeader == null) { | |
170 | + log.error("[{}] Missing requestId in header and body", response); | |
171 | + } else { | |
172 | + requestId = bytesToUuid(requestIdHeader); | |
173 | + log.trace("[{}] Response received: {}", requestId, String.valueOf(response).replace("\n", " ")); //TODO remove overhead | |
174 | + ResponseMetaData<Response> expectedResponse = pendingRequests.remove(requestId); | |
175 | + if (expectedResponse == null) { | |
176 | + log.warn("[{}] Invalid or stale request, response: {}", requestId, String.valueOf(response).replace("\n", " ")); | |
177 | + } else { | |
178 | + expectedResponse.future.set(response); | |
179 | + } | |
180 | + } | |
141 | 181 | } |
142 | 182 | |
143 | 183 | @Override |
... | ... | @@ -164,17 +204,48 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
164 | 204 | |
165 | 205 | @Override |
166 | 206 | public ListenableFuture<Response> send(Request request) { |
167 | - if (tickSize > maxPendingRequests) { | |
207 | + return send(request, this.maxRequestTimeoutNs); | |
208 | + } | |
209 | + | |
210 | + @Override | |
211 | + public ListenableFuture<Response> send(Request request, long requestTimeoutNs) { | |
212 | + if (pendingRequests.mappingCount() >= maxPendingRequests) { | |
213 | + log.warn("Pending request map is full [{}]! Consider to increase maxPendingRequests or increase processing performance", maxPendingRequests); | |
168 | 214 | return Futures.immediateFailedFuture(new RuntimeException("Pending request map is full!")); |
169 | 215 | } |
170 | 216 | UUID requestId = UUID.randomUUID(); |
171 | 217 | request.getHeaders().put(REQUEST_ID_HEADER, uuidToBytes(requestId)); |
172 | 218 | request.getHeaders().put(RESPONSE_TOPIC_HEADER, stringToBytes(responseTemplate.getTopic())); |
173 | - request.getHeaders().put(REQUEST_TIME, longToBytes(System.currentTimeMillis())); | |
219 | + request.getHeaders().put(REQUEST_TIME, longToBytes(getCurrentTimeMs())); | |
220 | + long currentClockNs = getCurrentClockNs(); | |
174 | 221 | SettableFuture<Response> future = SettableFuture.create(); |
175 | - ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(tickTs + maxRequestTimeout, future); | |
176 | - pendingRequests.putIfAbsent(requestId, responseMetaData); | |
177 | - log.trace("[{}] Sending request, key [{}], expTime [{}]", requestId, request.getKey(), responseMetaData.expTime); | |
222 | + ResponseMetaData<Response> responseMetaData = new ResponseMetaData<>(currentClockNs + requestTimeoutNs, future, currentClockNs, requestTimeoutNs); | |
223 | + log.trace("pending {}", responseMetaData); | |
224 | + if (pendingRequests.putIfAbsent(requestId, responseMetaData) != null) { | |
225 | + log.warn("Pending request already exists [{}]!", maxPendingRequests); | |
226 | + return Futures.immediateFailedFuture(new RuntimeException("Pending request already exists !" + requestId)); | |
227 | + } | |
228 | + sendToRequestTemplate(request, requestId, future, responseMetaData); | |
229 | + return future; | |
230 | + } | |
231 | + | |
232 | + /** | |
233 | + * MONOTONIC clock instead jumping wall clock. | |
234 | + * Wrapped into the method for the test purposes to travel through the time | |
235 | + * */ | |
236 | + long getCurrentClockNs() { | |
237 | + return System.nanoTime(); | |
238 | + } | |
239 | + | |
240 | + /** | |
241 | + * Wall clock to send timestamp to an external service | |
242 | + * */ | |
243 | + long getCurrentTimeMs() { | |
244 | + return System.currentTimeMillis(); | |
245 | + } | |
246 | + | |
247 | + void sendToRequestTemplate(Request request, UUID requestId, SettableFuture<Response> future, ResponseMetaData<Response> responseMetaData) { | |
248 | + log.trace("[{}] Sending request, key [{}], expTime [{}], request {}", requestId, request.getKey(), responseMetaData.expTime, request); | |
178 | 249 | if (messagesStats != null) { |
179 | 250 | messagesStats.incrementTotal(); |
180 | 251 | } |
... | ... | @@ -184,7 +255,7 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
184 | 255 | if (messagesStats != null) { |
185 | 256 | messagesStats.incrementSuccessful(); |
186 | 257 | } |
187 | - log.trace("[{}] Request sent: {}", requestId, metadata); | |
258 | + log.trace("[{}] Request sent: {}, request {}", requestId, metadata, request); | |
188 | 259 | } |
189 | 260 | |
190 | 261 | @Override |
... | ... | @@ -196,17 +267,32 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
196 | 267 | future.setException(t); |
197 | 268 | } |
198 | 269 | }); |
199 | - return future; | |
200 | 270 | } |
201 | 271 | |
202 | - private static class ResponseMetaData<T> { | |
272 | + @Getter | |
273 | + static class ResponseMetaData<T> { | |
274 | + private final long submitTime; | |
275 | + private final long timeout; | |
203 | 276 | private final long expTime; |
204 | 277 | private final SettableFuture<T> future; |
205 | 278 | |
206 | - ResponseMetaData(long ts, SettableFuture<T> future) { | |
279 | + ResponseMetaData(long ts, SettableFuture<T> future, long submitTime, long timeout) { | |
280 | + this.submitTime = submitTime; | |
281 | + this.timeout = timeout; | |
207 | 282 | this.expTime = ts; |
208 | 283 | this.future = future; |
209 | 284 | } |
285 | + | |
286 | + @Override | |
287 | + public String toString() { | |
288 | + return "ResponseMetaData{" + | |
289 | + "submitTime=" + submitTime + | |
290 | + ", calculatedExpTime=" + (submitTime + timeout) + | |
291 | + ", deltaMs=" + (expTime - submitTime) + | |
292 | + ", expTime=" + expTime + | |
293 | + ", future=" + future + | |
294 | + '}'; | |
295 | + } | |
210 | 296 | } |
211 | 297 | |
212 | 298 | } | ... | ... |
... | ... | @@ -21,6 +21,7 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; |
21 | 21 | import org.apache.kafka.clients.consumer.ConsumerRecord; |
22 | 22 | import org.apache.kafka.clients.consumer.ConsumerRecords; |
23 | 23 | import org.apache.kafka.clients.consumer.KafkaConsumer; |
24 | +import org.springframework.util.StopWatch; | |
24 | 25 | import org.thingsboard.server.queue.TbQueueAdmin; |
25 | 26 | import org.thingsboard.server.queue.TbQueueMsg; |
26 | 27 | import org.thingsboard.server.queue.common.AbstractTbQueueConsumerTemplate; |
... | ... | @@ -82,7 +83,16 @@ public class TbKafkaConsumerTemplate<T extends TbQueueMsg> extends AbstractTbQue |
82 | 83 | |
83 | 84 | @Override |
84 | 85 | protected List<ConsumerRecord<String, byte[]>> doPoll(long durationInMillis) { |
86 | + StopWatch stopWatch = new StopWatch(); | |
87 | + stopWatch.start(); | |
88 | + | |
89 | + log.trace("poll topic {} maxDuration {}", getTopic(), durationInMillis); | |
90 | + | |
85 | 91 | ConsumerRecords<String, byte[]> records = consumer.poll(Duration.ofMillis(durationInMillis)); |
92 | + | |
93 | + stopWatch.stop(); | |
94 | + log.trace("poll topic {} took {}ms", getTopic(), stopWatch.getTotalTimeMillis()); | |
95 | + | |
86 | 96 | if (records.isEmpty()) { |
87 | 97 | return Collections.emptyList(); |
88 | 98 | } else { | ... | ... |
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.queue.common; | |
17 | + | |
18 | +import lombok.extern.slf4j.Slf4j; | |
19 | +import org.junit.After; | |
20 | +import org.junit.Before; | |
21 | +import org.junit.Test; | |
22 | +import org.junit.runner.RunWith; | |
23 | +import org.mockito.ArgumentCaptor; | |
24 | +import org.mockito.Mock; | |
25 | +import org.mockito.junit.MockitoJUnitRunner; | |
26 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
27 | +import org.thingsboard.server.queue.TbQueueConsumer; | |
28 | +import org.thingsboard.server.queue.TbQueueMsg; | |
29 | +import org.thingsboard.server.queue.TbQueueProducer; | |
30 | + | |
31 | +import java.util.Collections; | |
32 | +import java.util.List; | |
33 | +import java.util.UUID; | |
34 | +import java.util.concurrent.CountDownLatch; | |
35 | +import java.util.concurrent.ExecutorService; | |
36 | +import java.util.concurrent.TimeUnit; | |
37 | +import java.util.concurrent.atomic.AtomicLong; | |
38 | + | |
39 | +import static org.hamcrest.Matchers.equalTo; | |
40 | +import static org.hamcrest.Matchers.greaterThanOrEqualTo; | |
41 | +import static org.hamcrest.Matchers.is; | |
42 | +import static org.hamcrest.Matchers.lessThan; | |
43 | +import static org.mockito.ArgumentMatchers.any; | |
44 | +import static org.mockito.ArgumentMatchers.anyLong; | |
45 | +import static org.mockito.BDDMockito.willAnswer; | |
46 | +import static org.mockito.BDDMockito.willDoNothing; | |
47 | +import static org.mockito.BDDMockito.willReturn; | |
48 | +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; | |
49 | +import static org.mockito.Mockito.atLeastOnce; | |
50 | +import static org.mockito.Mockito.mock; | |
51 | +import static org.mockito.Mockito.never; | |
52 | +import static org.mockito.Mockito.spy; | |
53 | +import static org.mockito.Mockito.times; | |
54 | +import static org.mockito.Mockito.verify; | |
55 | + | |
56 | +import static org.hamcrest.MatcherAssert.assertThat; | |
57 | +import static org.mockito.hamcrest.MockitoHamcrest.longThat; | |
58 | + | |
59 | +@Slf4j | |
60 | +@RunWith(MockitoJUnitRunner.class) | |
61 | +public class DefaultTbQueueRequestTemplateTest { | |
62 | + | |
63 | + @Mock | |
64 | + TbQueueAdmin queueAdmin; | |
65 | + @Mock | |
66 | + TbQueueProducer<TbQueueMsg> requestTemplate; | |
67 | + @Mock | |
68 | + TbQueueConsumer<TbQueueMsg> responseTemplate; | |
69 | + @Mock | |
70 | + ExecutorService executorMock; | |
71 | + | |
72 | + ExecutorService executor; | |
73 | + String topic = "js-responses-tb-node-0"; | |
74 | + long maxRequestTimeout = 10; | |
75 | + long maxPendingRequests = 32; | |
76 | + long pollInterval = 5; | |
77 | + | |
78 | + DefaultTbQueueRequestTemplate inst; | |
79 | + | |
80 | + @Before | |
81 | + public void setUp() throws Exception { | |
82 | + willReturn(topic).given(responseTemplate).getTopic(); | |
83 | + inst = spy(new DefaultTbQueueRequestTemplate( | |
84 | + queueAdmin, requestTemplate, responseTemplate, | |
85 | + maxRequestTimeout, maxPendingRequests, pollInterval, executorMock)); | |
86 | + | |
87 | + } | |
88 | + | |
89 | + @After | |
90 | + public void tearDown() throws Exception { | |
91 | + if (executor != null) { | |
92 | + executor.shutdownNow(); | |
93 | + } | |
94 | + } | |
95 | + | |
96 | + @Test | |
97 | + public void givenInstance_whenVerifyInitialParameters_thenOK() { | |
98 | + assertThat(inst.maxPendingRequests, equalTo(maxPendingRequests)); | |
99 | + assertThat(inst.maxRequestTimeoutNs, equalTo(TimeUnit.MILLISECONDS.toNanos(maxRequestTimeout))); | |
100 | + assertThat(inst.pollInterval, equalTo(pollInterval)); | |
101 | + assertThat(inst.executor, is(executorMock)); | |
102 | + assertThat(inst.stopped, is(false)); | |
103 | + assertThat(inst.internalExecutor, is(false)); | |
104 | + } | |
105 | + | |
106 | + @Test | |
107 | + public void givenExternalExecutor_whenInitStop_thenOK() { | |
108 | + inst.init(); | |
109 | + assertThat(inst.nextCleanupNs, equalTo(0L)); | |
110 | + verify(queueAdmin, times(1)).createTopicIfNotExists(topic); | |
111 | + verify(requestTemplate, times(1)).init(); | |
112 | + verify(responseTemplate, times(1)).subscribe(); | |
113 | + verify(executorMock, times(1)).submit(any(Runnable.class)); | |
114 | + | |
115 | + inst.stop(); | |
116 | + assertThat(inst.stopped, is(true)); | |
117 | + verify(responseTemplate, times(1)).unsubscribe(); | |
118 | + verify(requestTemplate, times(1)).stop(); | |
119 | + verify(executorMock, never()).shutdownNow(); | |
120 | + } | |
121 | + | |
122 | + @Test | |
123 | + public void givenMainLoop_whenLoopFewTimes_thenVerifyInvocationCount() throws InterruptedException { | |
124 | + executor = inst.createExecutor(); | |
125 | + CountDownLatch latch = new CountDownLatch(5); | |
126 | + willDoNothing().given(inst).sleep(anyLong()); | |
127 | + willAnswer(invocation -> { | |
128 | + if (latch.getCount() == 1) { | |
129 | + inst.stop(); //stop the loop in natural way | |
130 | + } | |
131 | + if (latch.getCount() == 3 || latch.getCount() == 4) { | |
132 | + latch.countDown(); | |
133 | + throw new RuntimeException("test catch block"); | |
134 | + } | |
135 | + latch.countDown(); | |
136 | + return null; | |
137 | + }).given(inst).fetchAndProcessResponses(); | |
138 | + | |
139 | + executor.submit(inst::mainLoop); | |
140 | + latch.await(10, TimeUnit.SECONDS); | |
141 | + | |
142 | + verify(inst, times(5)).fetchAndProcessResponses(); | |
143 | + verify(inst, times(2)).sleep(longThat(lessThan(TimeUnit.MILLISECONDS.toNanos(inst.pollInterval)))); | |
144 | + } | |
145 | + | |
146 | + @Test | |
147 | + public void givenMessages_whenSend_thenOK() { | |
148 | + willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any()); | |
149 | + inst.init(); | |
150 | + final int msgCount = 10; | |
151 | + for (int i = 0; i < msgCount; i++) { | |
152 | + inst.send(getRequestMsgMock()); | |
153 | + } | |
154 | + assertThat(inst.pendingRequests.mappingCount(), equalTo((long) msgCount)); | |
155 | + verify(inst, times(msgCount)).sendToRequestTemplate(any(), any(), any(), any()); | |
156 | + } | |
157 | + | |
158 | + @Test | |
159 | + public void givenMessagesOverMaxPendingRequests_whenSend_thenImmediateFailedFutureForTheOfRequests() { | |
160 | + willDoNothing().given(inst).sendToRequestTemplate(any(), any(), any(), any()); | |
161 | + inst.init(); | |
162 | + int msgOverflowCount = 10; | |
163 | + for (int i = 0; i < inst.maxPendingRequests; i++) { | |
164 | + assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only | |
165 | + } | |
166 | + for (int i = 0; i < msgOverflowCount; i++) { | |
167 | + assertThat("max pending requests overflow", inst.send(getRequestMsgMock()).isDone(), is(true)); //overflow, immediate failed future | |
168 | + } | |
169 | + assertThat(inst.pendingRequests.mappingCount(), equalTo(inst.maxPendingRequests)); | |
170 | + verify(inst, times((int) inst.maxPendingRequests)).sendToRequestTemplate(any(), any(), any(), any()); | |
171 | + } | |
172 | + | |
173 | + @Test | |
174 | + public void givenNothing_whenSendAndFetchAndProcessResponsesWithTimeout_thenFail() { | |
175 | + //given | |
176 | + AtomicLong currentTime = new AtomicLong(); | |
177 | + willAnswer(x -> { | |
178 | + log.info("currentTime={}", currentTime.get()); | |
179 | + return currentTime.get(); | |
180 | + }).given(inst).getCurrentClockNs(); | |
181 | + inst.init(); | |
182 | + inst.setupNextCleanup(); | |
183 | + willReturn(Collections.emptyList()).given(inst).doPoll(); | |
184 | + | |
185 | + //when | |
186 | + long stepNs = TimeUnit.MILLISECONDS.toNanos(1); | |
187 | + for (long i = 0; i <= inst.maxRequestTimeoutNs * 2; i = i + stepNs) { | |
188 | + currentTime.addAndGet(stepNs); | |
189 | + assertThat(inst.send(getRequestMsgMock()).isDone(), is(false)); //SettableFuture future - pending only | |
190 | + if (i % (inst.maxRequestTimeoutNs * 3 / 2) == 0) { | |
191 | + inst.fetchAndProcessResponses(); | |
192 | + } | |
193 | + } | |
194 | + | |
195 | + //then | |
196 | + ArgumentCaptor<DefaultTbQueueRequestTemplate.ResponseMetaData> argumentCaptorResp = ArgumentCaptor.forClass(DefaultTbQueueRequestTemplate.ResponseMetaData.class); | |
197 | + ArgumentCaptor<UUID> argumentCaptorUUID = ArgumentCaptor.forClass(UUID.class); | |
198 | + ArgumentCaptor<Long> argumentCaptorLong = ArgumentCaptor.forClass(Long.class); | |
199 | + verify(inst, atLeastOnce()).setTimeoutException(argumentCaptorUUID.capture(), argumentCaptorResp.capture(), argumentCaptorLong.capture()); | |
200 | + | |
201 | + List<DefaultTbQueueRequestTemplate.ResponseMetaData> responseMetaDataList = argumentCaptorResp.getAllValues(); | |
202 | + List<Long> tickTsList = argumentCaptorLong.getAllValues(); | |
203 | + for (int i = 0; i < responseMetaDataList.size(); i++) { | |
204 | + assertThat("tickTs >= calculatedExpTime", tickTsList.get(i), greaterThanOrEqualTo(responseMetaDataList.get(i).getSubmitTime() + responseMetaDataList.get(i).getTimeout())); | |
205 | + } | |
206 | + } | |
207 | + | |
208 | + TbQueueMsg getRequestMsgMock() { | |
209 | + return mock(TbQueueMsg.class, RETURNS_DEEP_STUBS); | |
210 | + } | |
211 | +} | |
\ No newline at end of file | ... | ... |
... | ... | @@ -37,6 +37,10 @@ |
37 | 37 | |
38 | 38 | <dependencies> |
39 | 39 | <dependency> |
40 | + <groupId>org.springframework</groupId> | |
41 | + <artifactId>spring-core</artifactId> | |
42 | + </dependency> | |
43 | + <dependency> | |
40 | 44 | <groupId>com.google.guava</groupId> |
41 | 45 | <artifactId>guava</artifactId> |
42 | 46 | <scope>provided</scope> | ... | ... |
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.common.util; | |
17 | + | |
18 | +import org.springframework.util.StopWatch; | |
19 | + | |
20 | +/** | |
21 | + * Utility method that extends Spring Framework StopWatch | |
22 | + * It is a MONOTONIC time stopwatch. | |
23 | + * It is a replacement for any measurements with a wall-clock like System.currentTimeMillis() | |
24 | + * It is not affected by leap second, day-light saving and wall-clock adjustments by manual or network time synchronization | |
25 | + * The main features is a single call for common use cases: | |
26 | + * - create and start: TbStopWatch sw = TbStopWatch.startNew() | |
27 | + * - stop and get: sw.stopAndGetTotalTimeMillis() or sw.stopAndGetLastTaskTimeMillis() | |
28 | + * */ | |
29 | +public class TbStopWatch extends StopWatch { | |
30 | + | |
31 | + public static TbStopWatch startNew(){ | |
32 | + TbStopWatch stopWatch = new TbStopWatch(); | |
33 | + stopWatch.start(); | |
34 | + return stopWatch; | |
35 | + } | |
36 | + | |
37 | + public long stopAndGetTotalTimeMillis(){ | |
38 | + stop(); | |
39 | + return getTotalTimeMillis(); | |
40 | + } | |
41 | + | |
42 | + public long stopAndGetTotalTimeNanos(){ | |
43 | + stop(); | |
44 | + return getLastTaskTimeNanos(); | |
45 | + } | |
46 | + | |
47 | + public long stopAndGetLastTaskTimeMillis(){ | |
48 | + stop(); | |
49 | + return getLastTaskTimeMillis(); | |
50 | + } | |
51 | + | |
52 | + public long stopAndGetLastTaskTimeNanos(){ | |
53 | + stop(); | |
54 | + return getLastTaskTimeNanos(); | |
55 | + } | |
56 | + | |
57 | +} | ... | ... |
... | ... | @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.MoreExecutors; |
21 | 21 | import lombok.extern.slf4j.Slf4j; |
22 | 22 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
23 | 23 | import org.springframework.cache.Cache; |
24 | -import org.springframework.cache.CacheManager; | |
25 | 24 | import org.springframework.context.annotation.Primary; |
26 | 25 | import org.springframework.stereotype.Service; |
27 | 26 | import org.thingsboard.server.common.data.EntityType; |
... | ... | @@ -32,6 +31,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
32 | 31 | import org.thingsboard.server.common.data.kv.KvEntry; |
33 | 32 | import org.thingsboard.server.common.stats.DefaultCounter; |
34 | 33 | import org.thingsboard.server.common.stats.StatsFactory; |
34 | +import org.thingsboard.server.dao.cache.CacheExecutorService; | |
35 | 35 | import org.thingsboard.server.dao.service.Validator; |
36 | 36 | |
37 | 37 | import java.util.ArrayList; |
... | ... | @@ -45,7 +45,6 @@ import java.util.Optional; |
45 | 45 | import java.util.Set; |
46 | 46 | import java.util.stream.Collectors; |
47 | 47 | |
48 | -import static org.thingsboard.server.common.data.CacheConstants.ATTRIBUTES_CACHE; | |
49 | 48 | import static org.thingsboard.server.dao.attributes.AttributeUtils.validate; |
50 | 49 | |
51 | 50 | @Service |
... | ... | @@ -59,12 +58,15 @@ public class CachedAttributesService implements AttributesService { |
59 | 58 | private final AttributesCacheWrapper cacheWrapper; |
60 | 59 | private final DefaultCounter hitCounter; |
61 | 60 | private final DefaultCounter missCounter; |
61 | + private final CacheExecutorService cacheExecutorService; | |
62 | 62 | |
63 | 63 | public CachedAttributesService(AttributesDao attributesDao, |
64 | 64 | AttributesCacheWrapper cacheWrapper, |
65 | - StatsFactory statsFactory) { | |
65 | + StatsFactory statsFactory, | |
66 | + CacheExecutorService cacheExecutorService) { | |
66 | 67 | this.attributesDao = attributesDao; |
67 | 68 | this.cacheWrapper = cacheWrapper; |
69 | + this.cacheExecutorService = cacheExecutorService; | |
68 | 70 | |
69 | 71 | this.hitCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "hit"); |
70 | 72 | this.missCounter = statsFactory.createDefaultCounter(STATS_NAME, "result", "miss"); |
... | ... | @@ -88,7 +90,7 @@ public class CachedAttributesService implements AttributesService { |
88 | 90 | // TODO: think if it's a good idea to store 'empty' attributes |
89 | 91 | cacheWrapper.put(attributeCacheKey, foundAttrKvEntry.orElse(null)); |
90 | 92 | return foundAttrKvEntry; |
91 | - }, MoreExecutors.directExecutor()); | |
93 | + }, cacheExecutorService); | |
92 | 94 | } |
93 | 95 | } |
94 | 96 | |
... | ... | @@ -111,7 +113,7 @@ public class CachedAttributesService implements AttributesService { |
111 | 113 | notFoundAttributeKeys.removeAll(wrappedCachedAttributes.keySet()); |
112 | 114 | |
113 | 115 | ListenableFuture<List<AttributeKvEntry>> result = attributesDao.find(tenantId, entityId, scope, notFoundAttributeKeys); |
114 | - return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), MoreExecutors.directExecutor()); | |
116 | + return Futures.transform(result, foundInDbAttributes -> mergeDbAndCacheAttributes(entityId, scope, cachedAttributes, notFoundAttributeKeys, foundInDbAttributes), cacheExecutorService); | |
115 | 117 | |
116 | 118 | } |
117 | 119 | |
... | ... | @@ -169,7 +171,7 @@ public class CachedAttributesService implements AttributesService { |
169 | 171 | |
170 | 172 | // TODO: can do if (attributesCache.get() != null) attributesCache.put() instead, but will be more twice more requests to cache |
171 | 173 | List<String> attributeKeys = attributes.stream().map(KvEntry::getKey).collect(Collectors.toList()); |
172 | - future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor()); | |
174 | + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutorService); | |
173 | 175 | return future; |
174 | 176 | } |
175 | 177 | |
... | ... | @@ -177,7 +179,7 @@ public class CachedAttributesService implements AttributesService { |
177 | 179 | public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) { |
178 | 180 | validate(entityId, scope); |
179 | 181 | ListenableFuture<List<Void>> future = attributesDao.removeAll(tenantId, entityId, scope, attributeKeys); |
180 | - future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), MoreExecutors.directExecutor()); | |
182 | + future.addListener(() -> evictAttributesFromCache(tenantId, entityId, scope, attributeKeys), cacheExecutorService); | |
181 | 183 | return future; |
182 | 184 | } |
183 | 185 | ... | ... |
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.dao.cache; | |
17 | + | |
18 | +import org.springframework.beans.factory.annotation.Value; | |
19 | +import org.springframework.stereotype.Component; | |
20 | +import org.thingsboard.common.util.AbstractListeningExecutor; | |
21 | + | |
22 | +@Component | |
23 | +public class CacheExecutorService extends AbstractListeningExecutor { | |
24 | + | |
25 | + @Value("${cache.maximumPoolSize}") | |
26 | + private int poolSize; | |
27 | + | |
28 | + @Override | |
29 | + protected int getThreadPollSize() { | |
30 | + return poolSize; | |
31 | + } | |
32 | + | |
33 | +} | ... | ... |
msa/js-executor/api/httpServer.js
0 → 100644
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 | +const config = require('config'), | |
17 | + logger = require('../config/logger')._logger('httpServer'), | |
18 | + express = require('express'); | |
19 | + | |
20 | +const httpPort = Number(config.get('http_port')); | |
21 | + | |
22 | +const app = express(); | |
23 | + | |
24 | +app.get('/livenessProbe', async (req, res) => { | |
25 | + const date = new Date(); | |
26 | + const message = { now: date.toISOString() }; | |
27 | + res.send(message); | |
28 | +}) | |
29 | + | |
30 | +app.listen(httpPort, () => logger.info(`Started http endpoint on port ${httpPort}. Please, use /livenessProbe !`)) | |
\ No newline at end of file | ... | ... |
... | ... | @@ -25,6 +25,7 @@ const config = require('config'), |
25 | 25 | Utils = require('./utils'), |
26 | 26 | JsExecutor = require('./jsExecutor'); |
27 | 27 | |
28 | +const statFrequency = Number(config.get('script.stat_print_frequency')); | |
28 | 29 | const scriptBodyTraceFrequency = Number(config.get('script.script_body_trace_frequency')); |
29 | 30 | const useSandbox = config.get('script.use_sandbox') === 'true'; |
30 | 31 | const maxActiveScripts = Number(config.get('script.max_active_scripts')); |
... | ... | @@ -34,15 +35,15 @@ const slowQueryLogBody = config.get('script.slow_query_log_body') === 'true'; |
34 | 35 | const {performance} = require('perf_hooks'); |
35 | 36 | |
36 | 37 | function JsInvokeMessageProcessor(producer) { |
37 | - console.log("Producer:", producer); | |
38 | 38 | this.producer = producer; |
39 | 39 | this.executor = new JsExecutor(useSandbox); |
40 | - this.scriptMap = {}; | |
40 | + this.scriptMap = new Map(); | |
41 | 41 | this.scriptIds = []; |
42 | 42 | this.executedScriptsCounter = 0; |
43 | + this.lastStatTime = performance.now(); | |
43 | 44 | } |
44 | 45 | |
45 | -JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { | |
46 | +JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function (message) { | |
46 | 47 | var tStart = performance.now(); |
47 | 48 | let requestId; |
48 | 49 | let responseTopic; |
... | ... | @@ -77,13 +78,13 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { |
77 | 78 | var tFinish = performance.now(); |
78 | 79 | var tTook = tFinish - tStart; |
79 | 80 | |
80 | - if ( tTook > slowQueryLogMs ) { | |
81 | + if (tTook > slowQueryLogMs) { | |
81 | 82 | let functionName; |
82 | 83 | if (request.invokeRequest) { |
83 | 84 | try { |
84 | 85 | buf = Buffer.from(request.invokeRequest['functionName']); |
85 | 86 | functionName = buf.toString('utf8'); |
86 | - } catch (err){ | |
87 | + } catch (err) { | |
87 | 88 | logger.error('[%s] Failed to read functionName from message header: %s', requestId, err.message); |
88 | 89 | logger.error(err.stack); |
89 | 90 | } |
... | ... | @@ -96,7 +97,7 @@ JsInvokeMessageProcessor.prototype.onJsInvokeMessage = function(message) { |
96 | 97 | |
97 | 98 | } |
98 | 99 | |
99 | -JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, responseTopic, headers, compileRequest) { | |
100 | +JsInvokeMessageProcessor.prototype.processCompileRequest = function (requestId, responseTopic, headers, compileRequest) { | |
100 | 101 | var scriptId = getScriptId(compileRequest); |
101 | 102 | logger.debug('[%s] Processing compile request, scriptId: [%s]', requestId, scriptId); |
102 | 103 | |
... | ... | @@ -115,15 +116,20 @@ JsInvokeMessageProcessor.prototype.processCompileRequest = function(requestId, r |
115 | 116 | ); |
116 | 117 | } |
117 | 118 | |
118 | -JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, responseTopic, headers, invokeRequest) { | |
119 | +JsInvokeMessageProcessor.prototype.processInvokeRequest = function (requestId, responseTopic, headers, invokeRequest) { | |
119 | 120 | var scriptId = getScriptId(invokeRequest); |
120 | 121 | logger.debug('[%s] Processing invoke request, scriptId: [%s]', requestId, scriptId); |
121 | 122 | this.executedScriptsCounter++; |
122 | - if ( this.executedScriptsCounter >= scriptBodyTraceFrequency ) { | |
123 | - this.executedScriptsCounter = 0; | |
124 | - if (logger.levels[logger.level] >= logger.levels['debug']) { | |
125 | - logger.debug('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody); | |
126 | - } | |
123 | + if (this.executedScriptsCounter % statFrequency == 0) { | |
124 | + const nowMs = performance.now(); | |
125 | + const msSinceLastStat = nowMs - this.lastStatTime; | |
126 | + const requestsPerSec = msSinceLastStat == 0 ? statFrequency : statFrequency / msSinceLastStat * 1000; | |
127 | + this.lastStatTime = nowMs; | |
128 | + logger.info('STAT[%s]: requests [%s], took [%s]ms, request/s [%s]', this.executedScriptsCounter, statFrequency, msSinceLastStat, requestsPerSec); | |
129 | + } | |
130 | + | |
131 | + if (this.executedScriptsCounter % scriptBodyTraceFrequency == 0) { | |
132 | + logger.info('[%s] Executing script body: [%s]', scriptId, invokeRequest.scriptBody); | |
127 | 133 | } |
128 | 134 | this.getOrCompileScript(scriptId, invokeRequest.scriptBody).then( |
129 | 135 | (script) => { |
... | ... | @@ -154,15 +160,15 @@ JsInvokeMessageProcessor.prototype.processInvokeRequest = function(requestId, re |
154 | 160 | ); |
155 | 161 | } |
156 | 162 | |
157 | -JsInvokeMessageProcessor.prototype.processReleaseRequest = function(requestId, responseTopic, headers, releaseRequest) { | |
163 | +JsInvokeMessageProcessor.prototype.processReleaseRequest = function (requestId, responseTopic, headers, releaseRequest) { | |
158 | 164 | var scriptId = getScriptId(releaseRequest); |
159 | 165 | logger.debug('[%s] Processing release request, scriptId: [%s]', requestId, scriptId); |
160 | - if (this.scriptMap[scriptId]) { | |
166 | + if (this.scriptMap.has(scriptId)) { | |
161 | 167 | var index = this.scriptIds.indexOf(scriptId); |
162 | 168 | if (index > -1) { |
163 | 169 | this.scriptIds.splice(index, 1); |
164 | 170 | } |
165 | - delete this.scriptMap[scriptId]; | |
171 | + this.scriptMap.delete(scriptId); | |
166 | 172 | } |
167 | 173 | var releaseResponse = createReleaseResponse(scriptId, true); |
168 | 174 | logger.debug('[%s] Sending success release response, scriptId: [%s]', requestId, scriptId); |
... | ... | @@ -173,6 +179,7 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT |
173 | 179 | var tStartSending = performance.now(); |
174 | 180 | var remoteResponse = createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse); |
175 | 181 | var rawResponse = Buffer.from(JSON.stringify(remoteResponse), 'utf8'); |
182 | + logger.debug('[%s] Sending response to queue, scriptId: [%s]', requestId, scriptId); | |
176 | 183 | this.producer.send(responseTopic, scriptId, rawResponse, headers).then( |
177 | 184 | () => { |
178 | 185 | logger.debug('[%s] Response sent to queue, took [%s]ms, scriptId: [%s]', requestId, (performance.now() - tStartSending), scriptId); |
... | ... | @@ -186,16 +193,17 @@ JsInvokeMessageProcessor.prototype.sendResponse = function (requestId, responseT |
186 | 193 | ); |
187 | 194 | } |
188 | 195 | |
189 | -JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scriptBody) { | |
196 | +JsInvokeMessageProcessor.prototype.getOrCompileScript = function (scriptId, scriptBody) { | |
190 | 197 | var self = this; |
191 | - return new Promise(function(resolve, reject) { | |
192 | - if (self.scriptMap[scriptId]) { | |
193 | - resolve(self.scriptMap[scriptId]); | |
198 | + return new Promise(function (resolve, reject) { | |
199 | + const script = self.scriptMap.get(scriptId); | |
200 | + if (script) { | |
201 | + resolve(script); | |
194 | 202 | } else { |
195 | 203 | self.executor.compileScript(scriptBody).then( |
196 | - (script) => { | |
197 | - self.cacheScript(scriptId, script); | |
198 | - resolve(script); | |
204 | + (compiledScript) => { | |
205 | + self.cacheScript(scriptId, compiledScript); | |
206 | + resolve(compiledScript); | |
199 | 207 | }, |
200 | 208 | (err) => { |
201 | 209 | reject(err); |
... | ... | @@ -205,56 +213,57 @@ JsInvokeMessageProcessor.prototype.getOrCompileScript = function(scriptId, scrip |
205 | 213 | }); |
206 | 214 | } |
207 | 215 | |
208 | -JsInvokeMessageProcessor.prototype.cacheScript = function(scriptId, script) { | |
209 | - if (!this.scriptMap[scriptId]) { | |
216 | +JsInvokeMessageProcessor.prototype.cacheScript = function (scriptId, script) { | |
217 | + if (!this.scriptMap.has(scriptId)) { | |
210 | 218 | this.scriptIds.push(scriptId); |
211 | 219 | while (this.scriptIds.length > maxActiveScripts) { |
212 | 220 | logger.info('Active scripts count [%s] exceeds maximum limit [%s]', this.scriptIds.length, maxActiveScripts); |
213 | 221 | const prevScriptId = this.scriptIds.shift(); |
214 | 222 | logger.info('Removing active script with id [%s]', prevScriptId); |
215 | - delete this.scriptMap[prevScriptId]; | |
223 | + this.scriptMap.delete(prevScriptId); | |
216 | 224 | } |
217 | 225 | } |
218 | - this.scriptMap[scriptId] = script; | |
226 | + this.scriptMap.set(scriptId, script); | |
227 | + logger.info("scriptMap size is [%s]", this.scriptMap.size); | |
219 | 228 | } |
220 | 229 | |
221 | 230 | function createRemoteResponse(requestId, compileResponse, invokeResponse, releaseResponse) { |
222 | 231 | const requestIdBits = Utils.UUIDToBits(requestId); |
223 | 232 | return { |
224 | - requestIdMSB: requestIdBits[0], | |
225 | - requestIdLSB: requestIdBits[1], | |
226 | - compileResponse: compileResponse, | |
227 | - invokeResponse: invokeResponse, | |
228 | - releaseResponse: releaseResponse | |
233 | + requestIdMSB: requestIdBits[0], | |
234 | + requestIdLSB: requestIdBits[1], | |
235 | + compileResponse: compileResponse, | |
236 | + invokeResponse: invokeResponse, | |
237 | + releaseResponse: releaseResponse | |
229 | 238 | }; |
230 | 239 | } |
231 | 240 | |
232 | 241 | function createCompileResponse(scriptId, success, errorCode, err) { |
233 | 242 | const scriptIdBits = Utils.UUIDToBits(scriptId); |
234 | - return { | |
235 | - errorCode: errorCode, | |
236 | - success: success, | |
237 | - errorDetails: parseJsErrorDetails(err), | |
238 | - scriptIdMSB: scriptIdBits[0], | |
239 | - scriptIdLSB: scriptIdBits[1] | |
243 | + return { | |
244 | + errorCode: errorCode, | |
245 | + success: success, | |
246 | + errorDetails: parseJsErrorDetails(err), | |
247 | + scriptIdMSB: scriptIdBits[0], | |
248 | + scriptIdLSB: scriptIdBits[1] | |
240 | 249 | }; |
241 | 250 | } |
242 | 251 | |
243 | 252 | function createInvokeResponse(result, success, errorCode, err) { |
244 | - return { | |
245 | - errorCode: errorCode, | |
246 | - success: success, | |
247 | - errorDetails: parseJsErrorDetails(err), | |
248 | - result: result | |
253 | + return { | |
254 | + errorCode: errorCode, | |
255 | + success: success, | |
256 | + errorDetails: parseJsErrorDetails(err), | |
257 | + result: result | |
249 | 258 | }; |
250 | 259 | } |
251 | 260 | |
252 | 261 | function createReleaseResponse(scriptId, success) { |
253 | 262 | const scriptIdBits = Utils.UUIDToBits(scriptId); |
254 | 263 | return { |
255 | - success: success, | |
256 | - scriptIdMSB: scriptIdBits[0], | |
257 | - scriptIdLSB: scriptIdBits[1] | |
264 | + success: success, | |
265 | + scriptIdMSB: scriptIdBits[0], | |
266 | + scriptIdLSB: scriptIdBits[1] | |
258 | 267 | }; |
259 | 268 | } |
260 | 269 | ... | ... |
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | |
17 | 17 | queue_type: "TB_QUEUE_TYPE" #kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ) |
18 | 18 | request_topic: "REMOTE_JS_EVAL_REQUEST_TOPIC" |
19 | +http_port: "HTTP_PORT" # /livenessProbe | |
19 | 20 | |
20 | 21 | js: |
21 | 22 | response_poll_interval: "REMOTE_JS_RESPONSE_POLL_INTERVAL_MS" |
... | ... | @@ -26,6 +27,9 @@ kafka: |
26 | 27 | servers: "TB_KAFKA_SERVERS" |
27 | 28 | replication_factor: "TB_QUEUE_KAFKA_REPLICATION_FACTOR" |
28 | 29 | acks: "TB_KAFKA_ACKS" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge |
30 | + batch_size: "TB_KAFKA_BATCH_SIZE" # for producer | |
31 | + linger_ms: "TB_KAFKA_LINGER_MS" # for producer | |
32 | + partitions_consumed_concurrently: "TB_KAFKA_PARTITIONS_CONSUMED_CONCURRENTLY" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency | |
29 | 33 | requestTimeout: "TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS" |
30 | 34 | compression: "TB_QUEUE_KAFKA_COMPRESSION" # gzip or uncompressed |
31 | 35 | topic_properties: "TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES" |
... | ... | @@ -70,6 +74,7 @@ logger: |
70 | 74 | |
71 | 75 | script: |
72 | 76 | use_sandbox: "SCRIPT_USE_SANDBOX" |
77 | + stat_print_frequency: "SCRIPT_STAT_PRINT_FREQUENCY" | |
73 | 78 | script_body_trace_frequency: "SCRIPT_BODY_TRACE_FREQUENCY" |
74 | 79 | max_active_scripts: "MAX_ACTIVE_SCRIPTS" |
75 | 80 | slow_query_log_ms: "SLOW_QUERY_LOG_MS" #1.123456 | ... | ... |
... | ... | @@ -16,6 +16,7 @@ |
16 | 16 | |
17 | 17 | queue_type: "kafka" |
18 | 18 | request_topic: "js_eval.requests" |
19 | +http_port: "8888" # /livenessProbe | |
19 | 20 | |
20 | 21 | js: |
21 | 22 | response_poll_interval: "25" |
... | ... | @@ -26,6 +27,9 @@ kafka: |
26 | 27 | servers: "localhost:9092" |
27 | 28 | replication_factor: "1" |
28 | 29 | acks: "1" # -1 = all; 0 = no acknowledgments; 1 = only waits for the leader to acknowledge |
30 | + batch_size: "128" # for producer | |
31 | + linger_ms: "1" # for producer | |
32 | + partitions_consumed_concurrently: "1" # (EXPERIMENTAL) increase this value if you are planning to handle more than one partition (scale up, scale down) - this will decrease the latency | |
29 | 33 | requestTimeout: "30000" # The default value in kafkajs is: 30000 |
30 | 34 | compression: "gzip" # gzip or uncompressed |
31 | 35 | topic_properties: "retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100;min.insync.replicas:1" |
... | ... | @@ -59,7 +63,8 @@ logger: |
59 | 63 | |
60 | 64 | script: |
61 | 65 | use_sandbox: "true" |
62 | - script_body_trace_frequency: "1000" | |
66 | + script_body_trace_frequency: "10000" | |
67 | + stat_print_frequency: "10000" | |
63 | 68 | max_active_scripts: "1000" |
64 | - slow_query_log_ms: "1.000000" #millis | |
69 | + slow_query_log_ms: "5.000000" #millis | |
65 | 70 | slow_query_log_body: "false" | ... | ... |
... | ... | @@ -23,8 +23,11 @@ const replicationFactor = Number(config.get('kafka.replication_factor')); |
23 | 23 | const topicProperties = config.get('kafka.topic_properties'); |
24 | 24 | const kafkaClientId = config.get('kafka.client_id'); |
25 | 25 | const acks = Number(config.get('kafka.acks')); |
26 | +const maxBatchSize = Number(config.get('kafka.batch_size')); | |
27 | +const linger = Number(config.get('kafka.linger_ms')); | |
26 | 28 | const requestTimeout = Number(config.get('kafka.requestTimeout')); |
27 | 29 | const compressionType = (config.get('kafka.compression') === "gzip") ? CompressionTypes.GZIP : CompressionTypes.None; |
30 | +const partitionsConsumedConcurrently = Number(config.get('kafka.partitions_consumed_concurrently')); | |
28 | 31 | |
29 | 32 | let kafkaClient; |
30 | 33 | let kafkaAdmin; |
... | ... | @@ -33,22 +36,65 @@ let producer; |
33 | 36 | |
34 | 37 | const configEntries = []; |
35 | 38 | |
39 | +let batchMessages = []; | |
40 | +let sendLoopInstance; | |
41 | + | |
36 | 42 | function KafkaProducer() { |
37 | 43 | this.send = async (responseTopic, scriptId, rawResponse, headers) => { |
38 | - return producer.send( | |
39 | - { | |
40 | - topic: responseTopic, | |
44 | + logger.debug('Pending queue response, scriptId: [%s]', scriptId); | |
45 | + const message = { | |
46 | + topic: responseTopic, | |
47 | + messages: [{ | |
48 | + key: scriptId, | |
49 | + value: rawResponse, | |
50 | + headers: headers.data | |
51 | + }] | |
52 | + }; | |
53 | + | |
54 | + await pushMessageToSendLater(message); | |
55 | + } | |
56 | +} | |
57 | + | |
58 | +async function pushMessageToSendLater(message) { | |
59 | + batchMessages.push(message); | |
60 | + if (batchMessages.length >= maxBatchSize) { | |
61 | + await sendMessagesAsBatch(true); | |
62 | + } | |
63 | +} | |
64 | + | |
65 | +function sendLoopWithLinger() { | |
66 | + if (sendLoopInstance) { | |
67 | + clearTimeout(sendLoopInstance); | |
68 | + } else { | |
69 | + logger.debug("Starting new send loop with linger [%s]", linger) | |
70 | + } | |
71 | + sendLoopInstance = setTimeout(sendMessagesAsBatch, linger); | |
72 | +} | |
73 | + | |
74 | +async function sendMessagesAsBatch(isImmediately) { | |
75 | + if (sendLoopInstance) { | |
76 | + logger.debug("sendMessagesAsBatch: Clear sendLoop scheduler. Starting new send loop with linger [%s]", linger); | |
77 | + clearTimeout(sendLoopInstance); | |
78 | + } | |
79 | + sendLoopInstance = null; | |
80 | + if (batchMessages.length > 0) { | |
81 | + logger.debug('sendMessagesAsBatch, length: [%s], %s', batchMessages.length, isImmediately ? 'immediately' : ''); | |
82 | + const messagesToSend = batchMessages; | |
83 | + batchMessages = []; | |
84 | + try { | |
85 | + await producer.sendBatch({ | |
86 | + topicMessages: messagesToSend, | |
41 | 87 | acks: acks, |
42 | - compression: compressionType, | |
43 | - messages: [ | |
44 | - { | |
45 | - key: scriptId, | |
46 | - value: rawResponse, | |
47 | - headers: headers.data | |
48 | - } | |
49 | - ] | |
50 | - }); | |
88 | + compression: compressionType | |
89 | + }) | |
90 | + logger.debug('Response batch sent to kafka, length: [%s]', messagesToSend.length); | |
91 | + } catch(err) { | |
92 | + logger.error('Failed batch send to kafka, length: [%s], pending to reprocess msgs', messagesToSend.length); | |
93 | + logger.error(err.stack); | |
94 | + batchMessages = messagesToSend.concat(batchMessages); | |
95 | + } | |
51 | 96 | } |
97 | + sendLoopWithLinger(); | |
52 | 98 | } |
53 | 99 | |
54 | 100 | (async () => { |
... | ... | @@ -64,8 +110,8 @@ function KafkaProducer() { |
64 | 110 | |
65 | 111 | let kafkaConfig = { |
66 | 112 | brokers: kafkaBootstrapServers.split(','), |
67 | - logLevel: logLevel.INFO, | |
68 | - logCreator: KafkaJsWinstonLogCreator | |
113 | + logLevel: logLevel.INFO, | |
114 | + logCreator: KafkaJsWinstonLogCreator | |
69 | 115 | }; |
70 | 116 | |
71 | 117 | if (kafkaClientId) { |
... | ... | @@ -114,14 +160,45 @@ function KafkaProducer() { |
114 | 160 | |
115 | 161 | consumer = kafkaClient.consumer({groupId: 'js-executor-group'}); |
116 | 162 | producer = kafkaClient.producer(); |
163 | + | |
164 | +/* | |
165 | + //producer event instrumentation to debug | |
166 | + const { CONNECT } = producer.events; | |
167 | + const removeListenerC = producer.on(CONNECT, e => logger.info(`producer CONNECT`)); | |
168 | + const { DISCONNECT } = producer.events; | |
169 | + const removeListenerD = producer.on(DISCONNECT, e => logger.info(`producer DISCONNECT`)); | |
170 | + const { REQUEST } = producer.events; | |
171 | + const removeListenerR = producer.on(REQUEST, e => logger.info(`producer REQUEST ${e.payload.broker}`)); | |
172 | + const { REQUEST_TIMEOUT } = producer.events; | |
173 | + const removeListenerRT = producer.on(REQUEST_TIMEOUT, e => logger.info(`producer REQUEST_TIMEOUT ${e.payload.broker}`)); | |
174 | + const { REQUEST_QUEUE_SIZE } = producer.events; | |
175 | + const removeListenerRQS = producer.on(REQUEST_QUEUE_SIZE, e => logger.info(`producer REQUEST_QUEUE_SIZE ${e.payload.broker} size ${e.queueSize}`)); | |
176 | +*/ | |
177 | + | |
178 | +/* | |
179 | + //consumer event instrumentation to debug | |
180 | + const removeListeners = {} | |
181 | + const { FETCH_START } = consumer.events; | |
182 | + removeListeners[FETCH_START] = consumer.on(FETCH_START, e => logger.info(`consumer FETCH_START`)); | |
183 | + const { FETCH } = consumer.events; | |
184 | + removeListeners[FETCH] = consumer.on(FETCH, e => logger.info(`consumer FETCH numberOfBatches ${e.payload.numberOfBatches} duration ${e.payload.duration}`)); | |
185 | + const { START_BATCH_PROCESS } = consumer.events; | |
186 | + removeListeners[START_BATCH_PROCESS] = consumer.on(START_BATCH_PROCESS, e => logger.info(`consumer START_BATCH_PROCESS topic ${e.payload.topic} batchSize ${e.payload.batchSize}`)); | |
187 | + const { END_BATCH_PROCESS } = consumer.events; | |
188 | + removeListeners[END_BATCH_PROCESS] = consumer.on(END_BATCH_PROCESS, e => logger.info(`consumer END_BATCH_PROCESS topic ${e.payload.topic} batchSize ${e.payload.batchSize}`)); | |
189 | + const { COMMIT_OFFSETS } = consumer.events; | |
190 | + removeListeners[COMMIT_OFFSETS] = consumer.on(COMMIT_OFFSETS, e => logger.info(`consumer COMMIT_OFFSETS topics ${e.payload.topics}`)); | |
191 | +*/ | |
192 | + | |
117 | 193 | const messageProcessor = new JsInvokeMessageProcessor(new KafkaProducer()); |
118 | 194 | await consumer.connect(); |
119 | 195 | await producer.connect(); |
196 | + sendLoopWithLinger(); | |
120 | 197 | await consumer.subscribe({topic: requestTopic}); |
121 | 198 | |
122 | 199 | logger.info('Started ThingsBoard JavaScript Executor Microservice.'); |
123 | 200 | await consumer.run({ |
124 | - //partitionsConsumedConcurrently: 1, // Default: 1 | |
201 | + partitionsConsumedConcurrently: partitionsConsumedConcurrently, | |
125 | 202 | eachMessage: async ({topic, partition, message}) => { |
126 | 203 | let headers = message.headers; |
127 | 204 | let key = message.key; |
... | ... | @@ -197,6 +274,9 @@ async function disconnectProducer() { |
197 | 274 | var _producer = producer; |
198 | 275 | producer = null; |
199 | 276 | try { |
277 | + logger.info('Stopping loop...'); | |
278 | + clearTimeout(sendLoopInstance); | |
279 | + await sendMessagesAsBatch(); | |
200 | 280 | await _producer.disconnect(); |
201 | 281 | logger.info('Kafka Producer stopped.'); |
202 | 282 | } catch (e) { | ... | ... |
... | ... | @@ -418,6 +418,14 @@ abort-controller@^3.0.0: |
418 | 418 | dependencies: |
419 | 419 | event-target-shim "^5.0.0" |
420 | 420 | |
421 | +accepts@~1.3.7: | |
422 | + version "1.3.7" | |
423 | + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" | |
424 | + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== | |
425 | + dependencies: | |
426 | + mime-types "~2.1.24" | |
427 | + negotiator "0.6.2" | |
428 | + | |
421 | 429 | agent-base@6: |
422 | 430 | version "6.0.1" |
423 | 431 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" |
... | ... | @@ -487,6 +495,11 @@ argparse@^1.0.7: |
487 | 495 | dependencies: |
488 | 496 | sprintf-js "~1.0.2" |
489 | 497 | |
498 | +array-flatten@1.1.1: | |
499 | + version "1.1.1" | |
500 | + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" | |
501 | + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= | |
502 | + | |
490 | 503 | array-union@^2.1.0: |
491 | 504 | version "2.1.0" |
492 | 505 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" |
... | ... | @@ -621,6 +634,22 @@ bluebird@^3.5.2: |
621 | 634 | resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" |
622 | 635 | integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== |
623 | 636 | |
637 | +body-parser@1.19.0: | |
638 | + version "1.19.0" | |
639 | + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" | |
640 | + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== | |
641 | + dependencies: | |
642 | + bytes "3.1.0" | |
643 | + content-type "~1.0.4" | |
644 | + debug "2.6.9" | |
645 | + depd "~1.1.2" | |
646 | + http-errors "1.7.2" | |
647 | + iconv-lite "0.4.24" | |
648 | + on-finished "~2.3.0" | |
649 | + qs "6.7.0" | |
650 | + raw-body "2.4.0" | |
651 | + type-is "~1.6.17" | |
652 | + | |
624 | 653 | boxen@^4.2.0: |
625 | 654 | version "4.2.0" |
626 | 655 | resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" |
... | ... | @@ -682,6 +711,11 @@ byline@^5.0.0: |
682 | 711 | resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" |
683 | 712 | integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= |
684 | 713 | |
714 | +bytes@3.1.0: | |
715 | + version "3.1.0" | |
716 | + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" | |
717 | + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== | |
718 | + | |
685 | 719 | cacheable-request@^6.0.0: |
686 | 720 | version "6.1.0" |
687 | 721 | resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" |
... | ... | @@ -838,6 +872,28 @@ configstore@^5.0.1: |
838 | 872 | write-file-atomic "^3.0.0" |
839 | 873 | xdg-basedir "^4.0.0" |
840 | 874 | |
875 | +content-disposition@0.5.3: | |
876 | + version "0.5.3" | |
877 | + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" | |
878 | + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== | |
879 | + dependencies: | |
880 | + safe-buffer "5.1.2" | |
881 | + | |
882 | +content-type@~1.0.4: | |
883 | + version "1.0.4" | |
884 | + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" | |
885 | + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== | |
886 | + | |
887 | +cookie-signature@1.0.6: | |
888 | + version "1.0.6" | |
889 | + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" | |
890 | + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= | |
891 | + | |
892 | +cookie@0.4.0: | |
893 | + version "0.4.0" | |
894 | + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" | |
895 | + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== | |
896 | + | |
841 | 897 | core-util-is@1.0.2, core-util-is@~1.0.0: |
842 | 898 | version "1.0.2" |
843 | 899 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" |
... | ... | @@ -867,6 +923,13 @@ dateformat@1.0.2-1.2.3: |
867 | 923 | dependencies: |
868 | 924 | ms "^2.1.1" |
869 | 925 | |
926 | +debug@2.6.9, debug@^2.2.0, debug@~2.6.9: | |
927 | + version "2.6.9" | |
928 | + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | |
929 | + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== | |
930 | + dependencies: | |
931 | + ms "2.0.0" | |
932 | + | |
870 | 933 | debug@4, debug@^4.1.1: |
871 | 934 | version "4.1.1" |
872 | 935 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" |
... | ... | @@ -874,13 +937,6 @@ debug@4, debug@^4.1.1: |
874 | 937 | dependencies: |
875 | 938 | ms "^2.1.1" |
876 | 939 | |
877 | -debug@^2.2.0, debug@~2.6.9: | |
878 | - version "2.6.9" | |
879 | - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | |
880 | - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== | |
881 | - dependencies: | |
882 | - ms "2.0.0" | |
883 | - | |
884 | 940 | decamelize@^1.2.0: |
885 | 941 | version "1.2.0" |
886 | 942 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" |
... | ... | @@ -913,6 +969,16 @@ delayed-stream@~1.0.0: |
913 | 969 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" |
914 | 970 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= |
915 | 971 | |
972 | +depd@~1.1.2: | |
973 | + version "1.1.2" | |
974 | + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" | |
975 | + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= | |
976 | + | |
977 | +destroy@~1.0.4: | |
978 | + version "1.0.4" | |
979 | + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" | |
980 | + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= | |
981 | + | |
916 | 982 | dir-glob@^3.0.1: |
917 | 983 | version "3.0.1" |
918 | 984 | resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" |
... | ... | @@ -962,6 +1028,11 @@ ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: |
962 | 1028 | dependencies: |
963 | 1029 | safe-buffer "^5.0.1" |
964 | 1030 | |
1031 | +ee-first@1.1.1: | |
1032 | + version "1.1.1" | |
1033 | + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" | |
1034 | + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= | |
1035 | + | |
965 | 1036 | emoji-regex@^7.0.1: |
966 | 1037 | version "7.0.3" |
967 | 1038 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" |
... | ... | @@ -977,6 +1048,11 @@ enabled@2.0.x: |
977 | 1048 | resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" |
978 | 1049 | integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== |
979 | 1050 | |
1051 | +encodeurl@~1.0.2: | |
1052 | + version "1.0.2" | |
1053 | + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" | |
1054 | + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= | |
1055 | + | |
980 | 1056 | end-of-stream@^1.0.0, end-of-stream@^1.1.0: |
981 | 1057 | version "1.4.4" |
982 | 1058 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" |
... | ... | @@ -994,6 +1070,11 @@ escape-goat@^2.0.0: |
994 | 1070 | resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" |
995 | 1071 | integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== |
996 | 1072 | |
1073 | +escape-html@~1.0.3: | |
1074 | + version "1.0.3" | |
1075 | + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" | |
1076 | + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= | |
1077 | + | |
997 | 1078 | escodegen@^1.14.1: |
998 | 1079 | version "1.14.3" |
999 | 1080 | resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" |
... | ... | @@ -1021,6 +1102,11 @@ esutils@^2.0.2: |
1021 | 1102 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" |
1022 | 1103 | integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== |
1023 | 1104 | |
1105 | +etag@~1.8.1: | |
1106 | + version "1.8.1" | |
1107 | + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" | |
1108 | + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= | |
1109 | + | |
1024 | 1110 | event-target-shim@^5.0.0: |
1025 | 1111 | version "5.0.1" |
1026 | 1112 | resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" |
... | ... | @@ -1041,6 +1127,42 @@ expand-template@^2.0.3: |
1041 | 1127 | resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" |
1042 | 1128 | integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== |
1043 | 1129 | |
1130 | +express@^4.17.1: | |
1131 | + version "4.17.1" | |
1132 | + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" | |
1133 | + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== | |
1134 | + dependencies: | |
1135 | + accepts "~1.3.7" | |
1136 | + array-flatten "1.1.1" | |
1137 | + body-parser "1.19.0" | |
1138 | + content-disposition "0.5.3" | |
1139 | + content-type "~1.0.4" | |
1140 | + cookie "0.4.0" | |
1141 | + cookie-signature "1.0.6" | |
1142 | + debug "2.6.9" | |
1143 | + depd "~1.1.2" | |
1144 | + encodeurl "~1.0.2" | |
1145 | + escape-html "~1.0.3" | |
1146 | + etag "~1.8.1" | |
1147 | + finalhandler "~1.1.2" | |
1148 | + fresh "0.5.2" | |
1149 | + merge-descriptors "1.0.1" | |
1150 | + methods "~1.1.2" | |
1151 | + on-finished "~2.3.0" | |
1152 | + parseurl "~1.3.3" | |
1153 | + path-to-regexp "0.1.7" | |
1154 | + proxy-addr "~2.0.5" | |
1155 | + qs "6.7.0" | |
1156 | + range-parser "~1.2.1" | |
1157 | + safe-buffer "5.1.2" | |
1158 | + send "0.17.1" | |
1159 | + serve-static "1.14.1" | |
1160 | + setprototypeof "1.1.1" | |
1161 | + statuses "~1.5.0" | |
1162 | + type-is "~1.6.18" | |
1163 | + utils-merge "1.0.1" | |
1164 | + vary "~1.1.2" | |
1165 | + | |
1044 | 1166 | extend@^3.0.2, extend@~3.0.2: |
1045 | 1167 | version "3.0.2" |
1046 | 1168 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" |
... | ... | @@ -1119,6 +1241,19 @@ fill-range@^7.0.1: |
1119 | 1241 | dependencies: |
1120 | 1242 | to-regex-range "^5.0.1" |
1121 | 1243 | |
1244 | +finalhandler@~1.1.2: | |
1245 | + version "1.1.2" | |
1246 | + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" | |
1247 | + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== | |
1248 | + dependencies: | |
1249 | + debug "2.6.9" | |
1250 | + encodeurl "~1.0.2" | |
1251 | + escape-html "~1.0.3" | |
1252 | + on-finished "~2.3.0" | |
1253 | + parseurl "~1.3.3" | |
1254 | + statuses "~1.5.0" | |
1255 | + unpipe "~1.0.0" | |
1256 | + | |
1122 | 1257 | find-up@^4.1.0: |
1123 | 1258 | version "4.1.0" |
1124 | 1259 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" |
... | ... | @@ -1155,6 +1290,16 @@ form-data@~2.3.2: |
1155 | 1290 | combined-stream "^1.0.6" |
1156 | 1291 | mime-types "^2.1.12" |
1157 | 1292 | |
1293 | +forwarded@0.2.0: | |
1294 | + version "0.2.0" | |
1295 | + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" | |
1296 | + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== | |
1297 | + | |
1298 | +fresh@0.5.2: | |
1299 | + version "0.5.2" | |
1300 | + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" | |
1301 | + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= | |
1302 | + | |
1158 | 1303 | from2@^2.3.0: |
1159 | 1304 | version "2.3.0" |
1160 | 1305 | resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" |
... | ... | @@ -1384,6 +1529,28 @@ http-cache-semantics@^4.0.0: |
1384 | 1529 | resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" |
1385 | 1530 | integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== |
1386 | 1531 | |
1532 | +http-errors@1.7.2: | |
1533 | + version "1.7.2" | |
1534 | + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" | |
1535 | + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== | |
1536 | + dependencies: | |
1537 | + depd "~1.1.2" | |
1538 | + inherits "2.0.3" | |
1539 | + setprototypeof "1.1.1" | |
1540 | + statuses ">= 1.5.0 < 2" | |
1541 | + toidentifier "1.0.0" | |
1542 | + | |
1543 | +http-errors@~1.7.2: | |
1544 | + version "1.7.3" | |
1545 | + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" | |
1546 | + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== | |
1547 | + dependencies: | |
1548 | + depd "~1.1.2" | |
1549 | + inherits "2.0.4" | |
1550 | + setprototypeof "1.1.1" | |
1551 | + statuses ">= 1.5.0 < 2" | |
1552 | + toidentifier "1.0.0" | |
1553 | + | |
1387 | 1554 | http-signature@~1.2.0: |
1388 | 1555 | version "1.2.0" |
1389 | 1556 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" |
... | ... | @@ -1401,6 +1568,13 @@ https-proxy-agent@^5.0.0: |
1401 | 1568 | agent-base "6" |
1402 | 1569 | debug "4" |
1403 | 1570 | |
1571 | +iconv-lite@0.4.24: | |
1572 | + version "0.4.24" | |
1573 | + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" | |
1574 | + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== | |
1575 | + dependencies: | |
1576 | + safer-buffer ">= 2.1.2 < 3" | |
1577 | + | |
1404 | 1578 | ieee754@1.1.13, ieee754@^1.1.4: |
1405 | 1579 | version "1.1.13" |
1406 | 1580 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" |
... | ... | @@ -1431,7 +1605,7 @@ inherits@2.0.3: |
1431 | 1605 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" |
1432 | 1606 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= |
1433 | 1607 | |
1434 | -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: | |
1608 | +inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: | |
1435 | 1609 | version "2.0.4" |
1436 | 1610 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" |
1437 | 1611 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== |
... | ... | @@ -1449,6 +1623,11 @@ into-stream@^5.1.1: |
1449 | 1623 | from2 "^2.3.0" |
1450 | 1624 | p-is-promise "^3.0.0" |
1451 | 1625 | |
1626 | +ipaddr.js@1.9.1: | |
1627 | + version "1.9.1" | |
1628 | + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" | |
1629 | + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== | |
1630 | + | |
1452 | 1631 | is-arrayish@^0.3.1: |
1453 | 1632 | version "0.3.2" |
1454 | 1633 | resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" |
... | ... | @@ -1764,11 +1943,26 @@ make-dir@^3.0.0: |
1764 | 1943 | dependencies: |
1765 | 1944 | semver "^6.0.0" |
1766 | 1945 | |
1946 | +media-typer@0.3.0: | |
1947 | + version "0.3.0" | |
1948 | + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" | |
1949 | + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= | |
1950 | + | |
1951 | +merge-descriptors@1.0.1: | |
1952 | + version "1.0.1" | |
1953 | + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" | |
1954 | + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= | |
1955 | + | |
1767 | 1956 | merge2@^1.3.0: |
1768 | 1957 | version "1.4.1" |
1769 | 1958 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" |
1770 | 1959 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== |
1771 | 1960 | |
1961 | +methods@~1.1.2: | |
1962 | + version "1.1.2" | |
1963 | + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" | |
1964 | + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= | |
1965 | + | |
1772 | 1966 | micromatch@^4.0.2: |
1773 | 1967 | version "4.0.2" |
1774 | 1968 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" |
... | ... | @@ -1782,6 +1976,11 @@ mime-db@1.44.0: |
1782 | 1976 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" |
1783 | 1977 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== |
1784 | 1978 | |
1979 | +mime-db@1.48.0: | |
1980 | + version "1.48.0" | |
1981 | + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" | |
1982 | + integrity sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ== | |
1983 | + | |
1785 | 1984 | mime-types@^2.1.12, mime-types@~2.1.19: |
1786 | 1985 | version "2.1.27" |
1787 | 1986 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" |
... | ... | @@ -1789,6 +1988,18 @@ mime-types@^2.1.12, mime-types@~2.1.19: |
1789 | 1988 | dependencies: |
1790 | 1989 | mime-db "1.44.0" |
1791 | 1990 | |
1991 | +mime-types@~2.1.24: | |
1992 | + version "2.1.31" | |
1993 | + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.31.tgz#a00d76b74317c61f9c2db2218b8e9f8e9c5c9e6b" | |
1994 | + integrity sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg== | |
1995 | + dependencies: | |
1996 | + mime-db "1.48.0" | |
1997 | + | |
1998 | +mime@1.6.0: | |
1999 | + version "1.6.0" | |
2000 | + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" | |
2001 | + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== | |
2002 | + | |
1792 | 2003 | mime@^2.2.0: |
1793 | 2004 | version "2.4.6" |
1794 | 2005 | resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" |
... | ... | @@ -1833,6 +2044,11 @@ ms@2.0.0: |
1833 | 2044 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" |
1834 | 2045 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= |
1835 | 2046 | |
2047 | +ms@2.1.1: | |
2048 | + version "2.1.1" | |
2049 | + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" | |
2050 | + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== | |
2051 | + | |
1836 | 2052 | ms@^2.1.1: |
1837 | 2053 | version "2.1.2" |
1838 | 2054 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" |
... | ... | @@ -1846,6 +2062,11 @@ multistream@^2.1.1: |
1846 | 2062 | inherits "^2.0.1" |
1847 | 2063 | readable-stream "^2.0.5" |
1848 | 2064 | |
2065 | +negotiator@0.6.2: | |
2066 | + version "0.6.2" | |
2067 | + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" | |
2068 | + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== | |
2069 | + | |
1849 | 2070 | node-fetch@^2.3.0, node-fetch@^2.6.0: |
1850 | 2071 | version "2.6.0" |
1851 | 2072 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" |
... | ... | @@ -1899,6 +2120,13 @@ object-hash@^2.0.1: |
1899 | 2120 | resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" |
1900 | 2121 | integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== |
1901 | 2122 | |
2123 | +on-finished@~2.3.0: | |
2124 | + version "2.3.0" | |
2125 | + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" | |
2126 | + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= | |
2127 | + dependencies: | |
2128 | + ee-first "1.1.1" | |
2129 | + | |
1902 | 2130 | once@^1.3.1, once@^1.4.0: |
1903 | 2131 | version "1.4.0" |
1904 | 2132 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" |
... | ... | @@ -1974,6 +2202,11 @@ package-json@^6.3.0: |
1974 | 2202 | registry-url "^5.0.0" |
1975 | 2203 | semver "^6.2.0" |
1976 | 2204 | |
2205 | +parseurl@~1.3.3: | |
2206 | + version "1.3.3" | |
2207 | + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" | |
2208 | + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== | |
2209 | + | |
1977 | 2210 | path-exists@^4.0.0: |
1978 | 2211 | version "4.0.0" |
1979 | 2212 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" |
... | ... | @@ -1984,6 +2217,11 @@ path-parse@^1.0.6: |
1984 | 2217 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" |
1985 | 2218 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== |
1986 | 2219 | |
2220 | +path-to-regexp@0.1.7: | |
2221 | + version "0.1.7" | |
2222 | + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" | |
2223 | + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= | |
2224 | + | |
1987 | 2225 | path-type@^4.0.0: |
1988 | 2226 | version "4.0.0" |
1989 | 2227 | resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" |
... | ... | @@ -2079,6 +2317,14 @@ protobufjs@^6.8.6, protobufjs@^6.9.0: |
2079 | 2317 | "@types/node" "^13.7.0" |
2080 | 2318 | long "^4.0.0" |
2081 | 2319 | |
2320 | +proxy-addr@~2.0.5: | |
2321 | + version "2.0.7" | |
2322 | + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" | |
2323 | + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== | |
2324 | + dependencies: | |
2325 | + forwarded "0.2.0" | |
2326 | + ipaddr.js "1.9.1" | |
2327 | + | |
2082 | 2328 | psl@^1.1.28, psl@^1.1.33: |
2083 | 2329 | version "1.8.0" |
2084 | 2330 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" |
... | ... | @@ -2114,6 +2360,11 @@ pupa@^2.0.1: |
2114 | 2360 | dependencies: |
2115 | 2361 | escape-goat "^2.0.0" |
2116 | 2362 | |
2363 | +qs@6.7.0: | |
2364 | + version "6.7.0" | |
2365 | + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" | |
2366 | + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== | |
2367 | + | |
2117 | 2368 | qs@~6.5.2: |
2118 | 2369 | version "6.5.2" |
2119 | 2370 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" |
... | ... | @@ -2129,6 +2380,21 @@ querystringify@^2.1.1: |
2129 | 2380 | resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" |
2130 | 2381 | integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== |
2131 | 2382 | |
2383 | +range-parser@~1.2.1: | |
2384 | + version "1.2.1" | |
2385 | + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" | |
2386 | + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== | |
2387 | + | |
2388 | +raw-body@2.4.0: | |
2389 | + version "2.4.0" | |
2390 | + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" | |
2391 | + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== | |
2392 | + dependencies: | |
2393 | + bytes "3.1.0" | |
2394 | + http-errors "1.7.2" | |
2395 | + iconv-lite "0.4.24" | |
2396 | + unpipe "1.0.0" | |
2397 | + | |
2132 | 2398 | rc@^1.2.8: |
2133 | 2399 | version "1.2.8" |
2134 | 2400 | resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" |
... | ... | @@ -2292,17 +2558,17 @@ run-parallel@^1.1.9: |
2292 | 2558 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" |
2293 | 2559 | integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== |
2294 | 2560 | |
2561 | +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2: | |
2562 | + version "5.1.2" | |
2563 | + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" | |
2564 | + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== | |
2565 | + | |
2295 | 2566 | safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: |
2296 | 2567 | version "5.2.1" |
2297 | 2568 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" |
2298 | 2569 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== |
2299 | 2570 | |
2300 | -safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2: | |
2301 | - version "5.1.2" | |
2302 | - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" | |
2303 | - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== | |
2304 | - | |
2305 | -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: | |
2571 | +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: | |
2306 | 2572 | version "2.1.2" |
2307 | 2573 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" |
2308 | 2574 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== |
... | ... | @@ -2339,11 +2605,45 @@ semver@^7.1.3: |
2339 | 2605 | resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" |
2340 | 2606 | integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== |
2341 | 2607 | |
2608 | +send@0.17.1: | |
2609 | + version "0.17.1" | |
2610 | + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" | |
2611 | + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== | |
2612 | + dependencies: | |
2613 | + debug "2.6.9" | |
2614 | + depd "~1.1.2" | |
2615 | + destroy "~1.0.4" | |
2616 | + encodeurl "~1.0.2" | |
2617 | + escape-html "~1.0.3" | |
2618 | + etag "~1.8.1" | |
2619 | + fresh "0.5.2" | |
2620 | + http-errors "~1.7.2" | |
2621 | + mime "1.6.0" | |
2622 | + ms "2.1.1" | |
2623 | + on-finished "~2.3.0" | |
2624 | + range-parser "~1.2.1" | |
2625 | + statuses "~1.5.0" | |
2626 | + | |
2627 | +serve-static@1.14.1: | |
2628 | + version "1.14.1" | |
2629 | + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" | |
2630 | + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== | |
2631 | + dependencies: | |
2632 | + encodeurl "~1.0.2" | |
2633 | + escape-html "~1.0.3" | |
2634 | + parseurl "~1.3.3" | |
2635 | + send "0.17.1" | |
2636 | + | |
2342 | 2637 | set-blocking@^2.0.0: |
2343 | 2638 | version "2.0.0" |
2344 | 2639 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" |
2345 | 2640 | integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= |
2346 | 2641 | |
2642 | +setprototypeof@1.1.1: | |
2643 | + version "1.1.1" | |
2644 | + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" | |
2645 | + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== | |
2646 | + | |
2347 | 2647 | signal-exit@^3.0.2: |
2348 | 2648 | version "3.0.3" |
2349 | 2649 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" |
... | ... | @@ -2391,6 +2691,11 @@ stack-trace@0.0.x: |
2391 | 2691 | resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" |
2392 | 2692 | integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= |
2393 | 2693 | |
2694 | +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: | |
2695 | + version "1.5.0" | |
2696 | + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" | |
2697 | + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= | |
2698 | + | |
2394 | 2699 | stream-browserify@^2.0.2: |
2395 | 2700 | version "2.0.2" |
2396 | 2701 | resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" |
... | ... | @@ -2513,6 +2818,11 @@ to-regex-range@^5.0.1: |
2513 | 2818 | dependencies: |
2514 | 2819 | is-number "^7.0.0" |
2515 | 2820 | |
2821 | +toidentifier@1.0.0: | |
2822 | + version "1.0.0" | |
2823 | + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" | |
2824 | + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== | |
2825 | + | |
2516 | 2826 | touch@^3.1.0: |
2517 | 2827 | version "3.1.0" |
2518 | 2828 | resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" |
... | ... | @@ -2581,6 +2891,14 @@ type-fest@^0.8.1: |
2581 | 2891 | resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" |
2582 | 2892 | integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== |
2583 | 2893 | |
2894 | +type-is@~1.6.17, type-is@~1.6.18: | |
2895 | + version "1.6.18" | |
2896 | + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" | |
2897 | + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== | |
2898 | + dependencies: | |
2899 | + media-typer "0.3.0" | |
2900 | + mime-types "~2.1.24" | |
2901 | + | |
2584 | 2902 | typedarray-to-buffer@^3.1.5: |
2585 | 2903 | version "3.1.5" |
2586 | 2904 | resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" |
... | ... | @@ -2636,6 +2954,11 @@ universalify@^1.0.0: |
2636 | 2954 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" |
2637 | 2955 | integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== |
2638 | 2956 | |
2957 | +unpipe@1.0.0, unpipe@~1.0.0: | |
2958 | + version "1.0.0" | |
2959 | + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" | |
2960 | + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= | |
2961 | + | |
2639 | 2962 | update-notifier@^4.0.0: |
2640 | 2963 | version "4.1.1" |
2641 | 2964 | resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.1.tgz#895fc8562bbe666179500f9f2cebac4f26323746" |
... | ... | @@ -2705,6 +3028,11 @@ util@^0.11.1: |
2705 | 3028 | dependencies: |
2706 | 3029 | inherits "2.0.3" |
2707 | 3030 | |
3031 | +utils-merge@1.0.1: | |
3032 | + version "1.0.1" | |
3033 | + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" | |
3034 | + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= | |
3035 | + | |
2708 | 3036 | uuid-parse@^1.1.0: |
2709 | 3037 | version "1.1.0" |
2710 | 3038 | resolved "https://registry.yarnpkg.com/uuid-parse/-/uuid-parse-1.1.0.tgz#7061c5a1384ae0e1f943c538094597e1b5f3a65b" |
... | ... | @@ -2735,6 +3063,11 @@ validator@^9.4.1: |
2735 | 3063 | resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" |
2736 | 3064 | integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== |
2737 | 3065 | |
3066 | +vary@~1.1.2: | |
3067 | + version "1.1.2" | |
3068 | + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" | |
3069 | + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= | |
3070 | + | |
2738 | 3071 | verror@1.10.0: |
2739 | 3072 | version "1.10.0" |
2740 | 3073 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" | ... | ... |
... | ... | @@ -1010,6 +1010,11 @@ |
1010 | 1010 | </exclusions> |
1011 | 1011 | </dependency> |
1012 | 1012 | <dependency> |
1013 | + <groupId>org.springframework</groupId> | |
1014 | + <artifactId>spring-core</artifactId> | |
1015 | + <version>${spring.version}</version> | |
1016 | + </dependency> | |
1017 | + <dependency> | |
1013 | 1018 | <groupId>org.springframework.boot</groupId> |
1014 | 1019 | <artifactId>spring-boot-starter-web</artifactId> |
1015 | 1020 | <version>${spring-boot.version}</version> | ... | ... |
... | ... | @@ -104,6 +104,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; |
104 | 104 | import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; |
105 | 105 | import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; |
106 | 106 | import org.thingsboard.server.common.data.oauth2.OAuth2Info; |
107 | +import org.thingsboard.server.common.data.oauth2.PlatformType; | |
107 | 108 | import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; |
108 | 109 | import org.thingsboard.server.common.data.ota.OtaPackageType; |
109 | 110 | import org.thingsboard.server.common.data.page.PageData; |
... | ... | @@ -1257,7 +1258,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { |
1257 | 1258 | params.put("deviceProfileId", deviceProfileId.getId().toString()); |
1258 | 1259 | |
1259 | 1260 | return restTemplate.exchange( |
1260 | - baseURL + "/api/devices/count/{otaPackageType}?deviceProfileId={deviceProfileId}", | |
1261 | + baseURL + "/api/devices/count/{otaPackageType}/{deviceProfileId}", | |
1261 | 1262 | HttpMethod.GET, |
1262 | 1263 | HttpEntity.EMPTY, |
1263 | 1264 | new ParameterizedTypeReference<Long>() { |
... | ... | @@ -1772,7 +1773,7 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { |
1772 | 1773 | }).getBody(); |
1773 | 1774 | } |
1774 | 1775 | |
1775 | - public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName) { | |
1776 | + public List<OAuth2ClientInfo> getOAuth2Clients(String pkgName, PlatformType platformType) { | |
1776 | 1777 | Map<String, String> params = new HashMap<>(); |
1777 | 1778 | StringBuilder urlBuilder = new StringBuilder(baseURL); |
1778 | 1779 | urlBuilder.append("/api/noauth/oauth2Clients"); |
... | ... | @@ -1780,6 +1781,15 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { |
1780 | 1781 | urlBuilder.append("?pkgName={pkgName}"); |
1781 | 1782 | params.put("pkgName", pkgName); |
1782 | 1783 | } |
1784 | + if (platformType != null) { | |
1785 | + if (pkgName != null) { | |
1786 | + urlBuilder.append("&"); | |
1787 | + } else { | |
1788 | + urlBuilder.append("?"); | |
1789 | + } | |
1790 | + urlBuilder.append("platform={platform}"); | |
1791 | + params.put("platform", platformType.name()); | |
1792 | + } | |
1783 | 1793 | return restTemplate.exchange( |
1784 | 1794 | urlBuilder.toString(), |
1785 | 1795 | HttpMethod.POST, | ... | ... |
... | ... | @@ -19,29 +19,22 @@ import com.fasterxml.jackson.databind.JsonNode; |
19 | 19 | import com.google.common.util.concurrent.ListenableFuture; |
20 | 20 | import org.thingsboard.server.common.msg.TbMsg; |
21 | 21 | |
22 | -import javax.script.ScriptException; | |
23 | 22 | import java.util.List; |
24 | 23 | import java.util.Set; |
25 | 24 | |
26 | 25 | public interface ScriptEngine { |
27 | 26 | |
28 | - List<TbMsg> executeUpdate(TbMsg msg) throws ScriptException; | |
29 | - | |
30 | 27 | ListenableFuture<List<TbMsg>> executeUpdateAsync(TbMsg msg); |
31 | 28 | |
32 | - TbMsg executeGenerate(TbMsg prevMsg) throws ScriptException; | |
33 | - | |
34 | - boolean executeFilter(TbMsg msg) throws ScriptException; | |
29 | + ListenableFuture<TbMsg> executeGenerateAsync(TbMsg prevMsg); | |
35 | 30 | |
36 | 31 | ListenableFuture<Boolean> executeFilterAsync(TbMsg msg); |
37 | 32 | |
38 | - Set<String> executeSwitch(TbMsg msg) throws ScriptException; | |
39 | - | |
40 | - JsonNode executeJson(TbMsg msg) throws ScriptException; | |
33 | + ListenableFuture<Set<String>> executeSwitchAsync(TbMsg msg); | |
41 | 34 | |
42 | - ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg) throws ScriptException; | |
35 | + ListenableFuture<JsonNode> executeJsonAsync(TbMsg msg); | |
43 | 36 | |
44 | - String executeToString(TbMsg msg) throws ScriptException; | |
37 | + ListenableFuture<String> executeToStringAsync(TbMsg msg); | |
45 | 38 | |
46 | 39 | void destroy(); |
47 | 40 | ... | ... |
... | ... | @@ -214,6 +214,10 @@ public interface TbContext { |
214 | 214 | |
215 | 215 | EdgeEventService getEdgeEventService(); |
216 | 216 | |
217 | + /** | |
218 | + * Js script executors call are completely asynchronous | |
219 | + * */ | |
220 | + @Deprecated | |
217 | 221 | ListeningExecutor getJsExecutor(); |
218 | 222 | |
219 | 223 | ListeningExecutor getMailExecutor(); | ... | ... |
... | ... | @@ -15,8 +15,11 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.rule.engine.action; |
17 | 17 | |
18 | +import com.google.common.util.concurrent.FutureCallback; | |
19 | +import com.google.common.util.concurrent.Futures; | |
20 | +import com.google.common.util.concurrent.MoreExecutors; | |
18 | 21 | import lombok.extern.slf4j.Slf4j; |
19 | -import org.thingsboard.common.util.ListeningExecutor; | |
22 | +import org.checkerframework.checker.nullness.qual.Nullable; | |
20 | 23 | import org.thingsboard.rule.engine.api.RuleNode; |
21 | 24 | import org.thingsboard.rule.engine.api.ScriptEngine; |
22 | 25 | import org.thingsboard.rule.engine.api.TbContext; |
... | ... | @@ -55,18 +58,21 @@ public class TbLogNode implements TbNode { |
55 | 58 | |
56 | 59 | @Override |
57 | 60 | public void onMsg(TbContext ctx, TbMsg msg) { |
58 | - ListeningExecutor jsExecutor = ctx.getJsExecutor(); | |
59 | 61 | ctx.logJsEvalRequest(); |
60 | - withCallback(jsExecutor.executeAsync(() -> jsEngine.executeToString(msg)), | |
61 | - toString -> { | |
62 | - ctx.logJsEvalResponse(); | |
63 | - log.info(toString); | |
64 | - ctx.tellSuccess(msg); | |
65 | - }, | |
66 | - t -> { | |
67 | - ctx.logJsEvalResponse(); | |
68 | - ctx.tellFailure(msg, t); | |
69 | - }); | |
62 | + Futures.addCallback(jsEngine.executeToStringAsync(msg), new FutureCallback<String>() { | |
63 | + @Override | |
64 | + public void onSuccess(@Nullable String result) { | |
65 | + ctx.logJsEvalResponse(); | |
66 | + log.info(result); | |
67 | + ctx.tellSuccess(msg); | |
68 | + } | |
69 | + | |
70 | + @Override | |
71 | + public void onFailure(Throwable t) { | |
72 | + ctx.logJsEvalResponse(); | |
73 | + ctx.tellFailure(msg, t); | |
74 | + } | |
75 | + }, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor | |
70 | 76 | } |
71 | 77 | |
72 | 78 | @Override | ... | ... |
... | ... | @@ -15,9 +15,12 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.rule.engine.debug; |
17 | 17 | |
18 | +import com.google.common.util.concurrent.Futures; | |
18 | 19 | import com.google.common.util.concurrent.ListenableFuture; |
20 | +import com.google.common.util.concurrent.MoreExecutors; | |
19 | 21 | import lombok.extern.slf4j.Slf4j; |
20 | 22 | import org.springframework.util.StringUtils; |
23 | +import org.thingsboard.common.util.TbStopWatch; | |
21 | 24 | import org.thingsboard.rule.engine.api.RuleNode; |
22 | 25 | import org.thingsboard.rule.engine.api.ScriptEngine; |
23 | 26 | import org.thingsboard.rule.engine.api.TbContext; |
... | ... | @@ -35,6 +38,7 @@ import org.thingsboard.server.common.msg.queue.ServiceQueue; |
35 | 38 | |
36 | 39 | import java.util.UUID; |
37 | 40 | import java.util.concurrent.TimeUnit; |
41 | +import java.util.concurrent.atomic.AtomicBoolean; | |
38 | 42 | |
39 | 43 | import static org.thingsboard.common.util.DonAsynchron.withCallback; |
40 | 44 | import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS; |
... | ... | @@ -64,10 +68,11 @@ public class TbMsgGeneratorNode implements TbNode { |
64 | 68 | private EntityId originatorId; |
65 | 69 | private UUID nextTickId; |
66 | 70 | private TbMsg prevMsg; |
67 | - private volatile boolean initialized; | |
71 | + private final AtomicBoolean initialized = new AtomicBoolean(false); | |
68 | 72 | |
69 | 73 | @Override |
70 | 74 | public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { |
75 | + log.trace("init generator with config {}", configuration); | |
71 | 76 | this.config = TbNodeUtils.convert(configuration, TbMsgGeneratorNodeConfiguration.class); |
72 | 77 | this.delay = TimeUnit.SECONDS.toMillis(config.getPeriodInSeconds()); |
73 | 78 | this.currentMsgCount = 0; |
... | ... | @@ -81,35 +86,39 @@ public class TbMsgGeneratorNode implements TbNode { |
81 | 86 | |
82 | 87 | @Override |
83 | 88 | public void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) { |
89 | + log.trace("onPartitionChangeMsg, PartitionChangeMsg {}, config {}", msg, config); | |
84 | 90 | updateGeneratorState(ctx); |
85 | 91 | } |
86 | 92 | |
87 | 93 | private void updateGeneratorState(TbContext ctx) { |
94 | + log.trace("updateGeneratorState, config {}", config); | |
88 | 95 | if (ctx.isLocalEntity(originatorId)) { |
89 | - if (!initialized) { | |
90 | - initialized = true; | |
96 | + if (initialized.compareAndSet(false, true)) { | |
91 | 97 | this.jsEngine = ctx.createJsScriptEngine(config.getJsScript(), "prevMsg", "prevMetadata", "prevMsgType"); |
92 | 98 | scheduleTickMsg(ctx); |
93 | 99 | } |
94 | - } else if (initialized) { | |
95 | - initialized = false; | |
100 | + } else if (initialized.compareAndSet(true, false)) { | |
96 | 101 | destroy(); |
97 | 102 | } |
98 | 103 | } |
99 | 104 | |
100 | 105 | @Override |
101 | 106 | public void onMsg(TbContext ctx, TbMsg msg) { |
102 | - if (initialized && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) { | |
107 | + log.trace("onMsg, config {}, msg {}", config, msg); | |
108 | + if (initialized.get() && msg.getType().equals(TB_MSG_GENERATOR_NODE_MSG) && msg.getId().equals(nextTickId)) { | |
109 | + TbStopWatch sw = TbStopWatch.startNew(); | |
103 | 110 | withCallback(generate(ctx, msg), |
104 | 111 | m -> { |
105 | - if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { | |
112 | + log.trace("onMsg onSuccess callback, took {}ms, config {}, msg {}", sw.stopAndGetTotalTimeMillis(), config, msg); | |
113 | + if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { | |
106 | 114 | ctx.enqueueForTellNext(m, SUCCESS); |
107 | 115 | scheduleTickMsg(ctx); |
108 | 116 | currentMsgCount++; |
109 | 117 | } |
110 | 118 | }, |
111 | 119 | t -> { |
112 | - if (initialized && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { | |
120 | + log.warn("onMsg onFailure callback, took {}ms, config {}, msg {}, exception {}", sw.stopAndGetTotalTimeMillis(), config, msg, t); | |
121 | + if (initialized.get() && (config.getMsgCount() == TbMsgGeneratorNodeConfiguration.UNLIMITED_MSG_COUNT || currentMsgCount < config.getMsgCount())) { | |
113 | 122 | ctx.tellFailure(msg, t); |
114 | 123 | scheduleTickMsg(ctx); |
115 | 124 | currentMsgCount++; |
... | ... | @@ -119,6 +128,7 @@ public class TbMsgGeneratorNode implements TbNode { |
119 | 128 | } |
120 | 129 | |
121 | 130 | private void scheduleTickMsg(TbContext ctx) { |
131 | + log.trace("scheduleTickMsg, config {}", config); | |
122 | 132 | long curTs = System.currentTimeMillis(); |
123 | 133 | if (lastScheduledTs == 0L) { |
124 | 134 | lastScheduledTs = curTs; |
... | ... | @@ -131,22 +141,26 @@ public class TbMsgGeneratorNode implements TbNode { |
131 | 141 | } |
132 | 142 | |
133 | 143 | private ListenableFuture<TbMsg> generate(TbContext ctx, TbMsg msg) { |
134 | - return ctx.getJsExecutor().executeAsync(() -> { | |
135 | - if (prevMsg == null) { | |
136 | - prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); | |
137 | - } | |
138 | - if (initialized) { | |
139 | - ctx.logJsEvalRequest(); | |
140 | - TbMsg generated = jsEngine.executeGenerate(prevMsg); | |
144 | + log.trace("generate, config {}", config); | |
145 | + if (prevMsg == null) { | |
146 | + prevMsg = ctx.newMsg(ServiceQueue.MAIN, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); | |
147 | + } | |
148 | + if (initialized.get()) { | |
149 | + ctx.logJsEvalRequest(); | |
150 | + return Futures.transformAsync(jsEngine.executeGenerateAsync(prevMsg), generated -> { | |
151 | + log.trace("generate process response, generated {}, config {}", generated, config); | |
141 | 152 | ctx.logJsEvalResponse(); |
142 | 153 | prevMsg = ctx.newMsg(ServiceQueue.MAIN, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); |
143 | - } | |
144 | - return prevMsg; | |
145 | - }); | |
154 | + return Futures.immediateFuture(prevMsg); | |
155 | + }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool | |
156 | + } | |
157 | + return Futures.immediateFuture(prevMsg); | |
158 | + | |
146 | 159 | } |
147 | 160 | |
148 | 161 | @Override |
149 | 162 | public void destroy() { |
163 | + log.trace("destroy, config {}", config); | |
150 | 164 | prevMsg = null; |
151 | 165 | if (jsEngine != null) { |
152 | 166 | jsEngine.destroy(); | ... | ... |
... | ... | @@ -15,7 +15,11 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.rule.engine.filter; |
17 | 17 | |
18 | +import com.google.common.util.concurrent.FutureCallback; | |
19 | +import com.google.common.util.concurrent.Futures; | |
20 | +import com.google.common.util.concurrent.MoreExecutors; | |
18 | 21 | import lombok.extern.slf4j.Slf4j; |
22 | +import org.checkerframework.checker.nullness.qual.Nullable; | |
19 | 23 | import org.thingsboard.common.util.ListeningExecutor; |
20 | 24 | import org.thingsboard.rule.engine.api.RuleNode; |
21 | 25 | import org.thingsboard.rule.engine.api.ScriptEngine; |
... | ... | @@ -29,8 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg; |
29 | 33 | |
30 | 34 | import java.util.Set; |
31 | 35 | |
32 | -import static org.thingsboard.common.util.DonAsynchron.withCallback; | |
33 | - | |
34 | 36 | @Slf4j |
35 | 37 | @RuleNode( |
36 | 38 | type = ComponentType.FILTER, |
... | ... | @@ -58,17 +60,20 @@ public class TbJsSwitchNode implements TbNode { |
58 | 60 | |
59 | 61 | @Override |
60 | 62 | public void onMsg(TbContext ctx, TbMsg msg) { |
61 | - ListeningExecutor jsExecutor = ctx.getJsExecutor(); | |
62 | 63 | ctx.logJsEvalRequest(); |
63 | - withCallback(jsExecutor.executeAsync(() -> jsEngine.executeSwitch(msg)), | |
64 | - result -> { | |
65 | - ctx.logJsEvalResponse(); | |
66 | - processSwitch(ctx, msg, result); | |
67 | - }, | |
68 | - t -> { | |
69 | - ctx.logJsEvalFailure(); | |
70 | - ctx.tellFailure(msg, t); | |
71 | - }, ctx.getDbCallbackExecutor()); | |
64 | + Futures.addCallback(jsEngine.executeSwitchAsync(msg), new FutureCallback<Set<String>>() { | |
65 | + @Override | |
66 | + public void onSuccess(@Nullable Set<String> result) { | |
67 | + ctx.logJsEvalResponse(); | |
68 | + processSwitch(ctx, msg, result); | |
69 | + } | |
70 | + | |
71 | + @Override | |
72 | + public void onFailure(Throwable t) { | |
73 | + ctx.logJsEvalFailure(); | |
74 | + ctx.tellFailure(msg, t); | |
75 | + } | |
76 | + }, MoreExecutors.directExecutor()); //usually runs in a callbackExecutor | |
72 | 77 | } |
73 | 78 | |
74 | 79 | private void processSwitch(TbContext ctx, TbMsg msg, Set<String> nextRelations) { | ... | ... |
... | ... | @@ -64,7 +64,7 @@ public class TbJsSwitchNodeTest { |
64 | 64 | private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); |
65 | 65 | |
66 | 66 | @Test |
67 | - public void multipleRoutesAreAllowed() throws TbNodeException, ScriptException { | |
67 | + public void multipleRoutesAreAllowed() throws TbNodeException { | |
68 | 68 | initWithScript(); |
69 | 69 | TbMsgMetaData metaData = new TbMsgMetaData(); |
70 | 70 | metaData.putValue("temp", "10"); |
... | ... | @@ -72,11 +72,9 @@ public class TbJsSwitchNodeTest { |
72 | 72 | String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; |
73 | 73 | |
74 | 74 | TbMsg msg = TbMsg.newMsg( "USER", null, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); |
75 | - mockJsExecutor(); | |
76 | - when(scriptEngine.executeSwitch(msg)).thenReturn(Sets.newHashSet("one", "three")); | |
75 | + when(scriptEngine.executeSwitchAsync(msg)).thenReturn(Futures.immediateFuture(Sets.newHashSet("one", "three"))); | |
77 | 76 | |
78 | 77 | node.onMsg(ctx, msg); |
79 | - verify(ctx).getJsExecutor(); | |
80 | 78 | verify(ctx).tellNext(msg, Sets.newHashSet("one", "three")); |
81 | 79 | } |
82 | 80 | |
... | ... | @@ -92,19 +90,6 @@ public class TbJsSwitchNodeTest { |
92 | 90 | node.init(ctx, nodeConfiguration); |
93 | 91 | } |
94 | 92 | |
95 | - @SuppressWarnings("unchecked") | |
96 | - private void mockJsExecutor() { | |
97 | - when(ctx.getJsExecutor()).thenReturn(executor); | |
98 | - doAnswer((Answer<ListenableFuture<Set<String>>>) invocationOnMock -> { | |
99 | - try { | |
100 | - Callable task = (Callable) (invocationOnMock.getArguments())[0]; | |
101 | - return Futures.immediateFuture((Set<String>) task.call()); | |
102 | - } catch (Throwable th) { | |
103 | - return Futures.immediateFailedFuture(th); | |
104 | - } | |
105 | - }).when(executor).executeAsync(ArgumentMatchers.any(Callable.class)); | |
106 | - } | |
107 | - | |
108 | 93 | private void verifyError(TbMsg msg, String message, Class expectedClass) { |
109 | 94 | ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); |
110 | 95 | verify(ctx).tellFailure(same(msg), captor.capture()); | ... | ... |
... | ... | @@ -133,7 +133,7 @@ export class OtaPackageService { |
133 | 133 | } |
134 | 134 | |
135 | 135 | public countUpdateDeviceAfterChangePackage(type: OtaUpdateType, entityId: EntityId, config?: RequestConfig): Observable<number> { |
136 | - return this.http.get<number>(`/api/devices/count/${type}?deviceProfileId=${entityId.id}`, defaultHttpOptionsFromConfig(config)); | |
136 | + return this.http.get<number>(`/api/devices/count/${type}/${entityId.id}`, defaultHttpOptionsFromConfig(config)); | |
137 | 137 | } |
138 | 138 | |
139 | 139 | public confirmDialogUpdatePackage(entity: BaseData<EntityId>&OtaPagesIds, | ... | ... |
... | ... | @@ -293,24 +293,24 @@ export class MenuService { |
293 | 293 | sections.push( |
294 | 294 | { |
295 | 295 | id: guid(), |
296 | + name: 'edge.edge-instances', | |
297 | + type: 'link', | |
298 | + path: '/edgeInstances', | |
299 | + icon: 'router' | |
300 | + }, | |
301 | + { | |
302 | + id: guid(), | |
296 | 303 | name: 'edge.management', |
297 | 304 | type: 'toggle', |
298 | - path: '/edges', | |
299 | - height: '80px', | |
300 | - icon: 'router', | |
305 | + path: '/edgeManagement', | |
306 | + height: '40px', | |
307 | + icon: 'settings_input_antenna', | |
301 | 308 | pages: [ |
302 | 309 | { |
303 | 310 | id: guid(), |
304 | - name: 'edge.edge-instances', | |
305 | - type: 'link', | |
306 | - path: '/edges', | |
307 | - icon: 'router' | |
308 | - }, | |
309 | - { | |
310 | - id: guid(), | |
311 | 311 | name: 'edge.rulechain-templates', |
312 | 312 | type: 'link', |
313 | - path: '/edges/ruleChains', | |
313 | + path: '/edgeManagement/ruleChains', | |
314 | 314 | icon: 'settings_ethernet' |
315 | 315 | } |
316 | 316 | ] |
... | ... | @@ -448,12 +448,12 @@ export class MenuService { |
448 | 448 | { |
449 | 449 | name: 'edge.edge-instances', |
450 | 450 | icon: 'router', |
451 | - path: '/edges' | |
451 | + path: '/edgeInstances' | |
452 | 452 | }, |
453 | 453 | { |
454 | 454 | name: 'edge.rulechain-templates', |
455 | 455 | icon: 'settings_ethernet', |
456 | - path: '/edges/ruleChains' | |
456 | + path: '/edgeManagement/ruleChains' | |
457 | 457 | } |
458 | 458 | ] |
459 | 459 | } |
... | ... | @@ -548,7 +548,7 @@ export class MenuService { |
548 | 548 | id: guid(), |
549 | 549 | name: 'edge.edge-instances', |
550 | 550 | type: 'link', |
551 | - path: '/edges', | |
551 | + path: '/edgeInstances', | |
552 | 552 | icon: 'router' |
553 | 553 | } |
554 | 554 | ); |
... | ... | @@ -606,8 +606,8 @@ export class MenuService { |
606 | 606 | places: [ |
607 | 607 | { |
608 | 608 | name: 'edge.edge-instances', |
609 | - icon: 'router', | |
610 | - path: '/edges' | |
609 | + icon: 'settings_input_antenna', | |
610 | + path: '/edgeInstances' | |
611 | 611 | } |
612 | 612 | ] |
613 | 613 | } | ... | ... |
... | ... | @@ -128,6 +128,7 @@ import { |
128 | 128 | DashboardImageDialogComponent, |
129 | 129 | DashboardImageDialogData, DashboardImageDialogResult |
130 | 130 | } from '@home/components/dashboard-page/dashboard-image-dialog.component'; |
131 | +import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; | |
131 | 132 | |
132 | 133 | // @dynamic |
133 | 134 | @Component({ |
... | ... | @@ -211,7 +212,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
211 | 212 | |
212 | 213 | addingLayoutCtx: DashboardPageLayoutContext; |
213 | 214 | |
214 | - logo = 'assets/logo_title_white.svg'; | |
215 | + private dashboardLogoCache: SafeUrl; | |
216 | + private defaultDashboardLogo = 'assets/logo_title_white.svg'; | |
215 | 217 | |
216 | 218 | dashboardCtx: DashboardContext = { |
217 | 219 | instanceId: this.utils.guid(), |
... | ... | @@ -312,7 +314,8 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
312 | 314 | private ngZone: NgZone, |
313 | 315 | private overlay: Overlay, |
314 | 316 | private viewContainerRef: ViewContainerRef, |
315 | - private cd: ChangeDetectorRef) { | |
317 | + private cd: ChangeDetectorRef, | |
318 | + private sanitizer: DomSanitizer) { | |
316 | 319 | super(store); |
317 | 320 | |
318 | 321 | } |
... | ... | @@ -413,6 +416,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
413 | 416 | private reset() { |
414 | 417 | this.dashboard = null; |
415 | 418 | this.dashboardConfiguration = null; |
419 | + this.dashboardLogoCache = undefined; | |
416 | 420 | this.prevDashboard = null; |
417 | 421 | |
418 | 422 | this.widgetEditMode = false; |
... | ... | @@ -570,8 +574,12 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
570 | 574 | } |
571 | 575 | } |
572 | 576 | |
573 | - public get dashboardLogo(): string { | |
574 | - return this.dashboard.configuration.settings.dashboardLogoUrl || this.logo; | |
577 | + public get dashboardLogo(): SafeUrl { | |
578 | + if (!this.dashboardLogoCache) { | |
579 | + const logo = this.dashboard.configuration.settings.dashboardLogoUrl || this.defaultDashboardLogo; | |
580 | + this.dashboardLogoCache = this.sanitizer.bypassSecurityTrustUrl(logo); | |
581 | + } | |
582 | + return this.dashboardLogoCache; | |
575 | 583 | } |
576 | 584 | |
577 | 585 | public showRightLayoutSwitch(): boolean { |
... | ... | @@ -702,6 +710,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
702 | 710 | }).afterClosed().subscribe((data) => { |
703 | 711 | if (data) { |
704 | 712 | this.dashboard.configuration.settings = data.settings; |
713 | + this.dashboardLogoCache = undefined; | |
705 | 714 | const newGridSettings = data.gridSettings; |
706 | 715 | if (newGridSettings) { |
707 | 716 | const layout = this.dashboard.configuration.states[layoutKeys.state].layouts[layoutKeys.layout]; |
... | ... | @@ -855,11 +864,13 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC |
855 | 864 | if (this.widgetEditMode) { |
856 | 865 | if (revert) { |
857 | 866 | this.dashboard = this.prevDashboard; |
867 | + this.dashboardLogoCache = undefined; | |
858 | 868 | } |
859 | 869 | } else { |
860 | 870 | this.resetHighlight(); |
861 | 871 | if (revert) { |
862 | 872 | this.dashboard = this.prevDashboard; |
873 | + this.dashboardLogoCache = undefined; | |
863 | 874 | this.dashboardConfiguration = this.dashboard.configuration; |
864 | 875 | this.dashboardCtx.dashboardTimewindow = this.dashboardConfiguration.timewindow; |
865 | 876 | this.entityAliasesUpdated(); | ... | ... |
... | ... | @@ -99,7 +99,6 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr |
99 | 99 | import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component'; |
100 | 100 | import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component'; |
101 | 101 | import { CoapDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/coap-device-profile-transport-configuration.component'; |
102 | -import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snmp-device-profile-transport-configuration.component'; | |
103 | 102 | import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component'; |
104 | 103 | import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component'; |
105 | 104 | import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component'; |
... | ... | @@ -143,6 +142,7 @@ import { SecurityConfigLwm2mComponent } from '@home/components/device/security-c |
143 | 142 | import { SecurityConfigLwm2mServerComponent } from '@home/components/device/security-config-lwm2m-server.component'; |
144 | 143 | import { DashboardImageDialogComponent } from '@home/components/dashboard-page/dashboard-image-dialog.component'; |
145 | 144 | import { WidgetContainerComponent } from '@home/components/widget/widget-container.component'; |
145 | +import { SnmpDeviceProfileTransportModule } from '@home/components/profile/device/snpm/snmp-device-profile-transport.module'; | |
146 | 146 | |
147 | 147 | @NgModule({ |
148 | 148 | declarations: |
... | ... | @@ -228,7 +228,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain |
228 | 228 | DefaultDeviceProfileTransportConfigurationComponent, |
229 | 229 | MqttDeviceProfileTransportConfigurationComponent, |
230 | 230 | CoapDeviceProfileTransportConfigurationComponent, |
231 | - SnmpDeviceProfileTransportConfigurationComponent, | |
232 | 231 | DeviceProfileTransportConfigurationComponent, |
233 | 232 | CreateAlarmRulesComponent, |
234 | 233 | AlarmRuleComponent, |
... | ... | @@ -272,6 +271,7 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain |
272 | 271 | SharedModule, |
273 | 272 | SharedHomeComponentsModule, |
274 | 273 | Lwm2mProfileComponentsModule, |
274 | + SnmpDeviceProfileTransportModule, | |
275 | 275 | StatesControllerModule |
276 | 276 | ], |
277 | 277 | exports: [ |
... | ... | @@ -339,7 +339,6 @@ import { WidgetContainerComponent } from '@home/components/widget/widget-contain |
339 | 339 | DefaultDeviceProfileTransportConfigurationComponent, |
340 | 340 | MqttDeviceProfileTransportConfigurationComponent, |
341 | 341 | CoapDeviceProfileTransportConfigurationComponent, |
342 | - SnmpDeviceProfileTransportConfigurationComponent, | |
343 | 342 | DeviceProfileTransportConfigurationComponent, |
344 | 343 | CreateAlarmRulesComponent, |
345 | 344 | AlarmRuleComponent, | ... | ... |
... | ... | @@ -89,7 +89,9 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu |
89 | 89 | if (configuration) { |
90 | 90 | delete configuration.type; |
91 | 91 | } |
92 | - this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); | |
92 | + setTimeout(() => { | |
93 | + this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); | |
94 | + }); | |
93 | 95 | } |
94 | 96 | |
95 | 97 | private updateModel() { | ... | ... |
ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html
deleted
100644 → 0
1 | -<!-- | |
2 | - | |
3 | - Copyright © 2016-2021 The Thingsboard Authors | |
4 | - | |
5 | - Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | - you may not use this file except in compliance with the License. | |
7 | - You may obtain a copy of the License at | |
8 | - | |
9 | - http://www.apache.org/licenses/LICENSE-2.0 | |
10 | - | |
11 | - Unless required by applicable law or agreed to in writing, software | |
12 | - distributed under the License is distributed on an "AS IS" BASIS, | |
13 | - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | - See the License for the specific language governing permissions and | |
15 | - limitations under the License. | |
16 | - | |
17 | ---> | |
18 | -<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding-bottom: 16px;"> | |
19 | - <tb-json-object-edit | |
20 | - required | |
21 | - formControlName="configuration"> | |
22 | - </tb-json-object-edit> | |
23 | -</form> |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2021 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<div fxLayout="column"> | |
19 | + <div *ngFor="let deviceProfileCommunication of communicationConfigFormArray().controls; let $index = index; | |
20 | + last as isLast;" fxLayout="row" fxLayoutAlign="start center" | |
21 | + fxLayoutGap="8px" class="scope-row" [formGroup]="deviceProfileCommunication"> | |
22 | + <div class="communication-config" fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start"> | |
23 | + <mat-form-field class="spec mat-block" floatLabel="always" hideRequiredMarker> | |
24 | + <mat-label translate>device-profile.snmp.scope</mat-label> | |
25 | + <mat-select formControlName="spec" required> | |
26 | + <mat-option *ngFor="let snmpSpecType of snmpSpecTypes" [value]="snmpSpecType" | |
27 | + [disabled]="isDisabledSeverity(snmpSpecType, $index)"> | |
28 | + {{ snmpSpecTypeTranslationMap.get(snmpSpecType) }} | |
29 | + </mat-option> | |
30 | + </mat-select> | |
31 | + <mat-error *ngIf="deviceProfileCommunication.get('spec').hasError('required')"> | |
32 | + {{ 'device-profile.snmp.scope-required' | translate }} | |
33 | + </mat-error> | |
34 | + </mat-form-field> | |
35 | + <mat-divider vertical></mat-divider> | |
36 | + <section fxFlex fxLayout="column"> | |
37 | + <mat-form-field *ngIf="isShowFrequency(deviceProfileCommunication.get('spec').value)"> | |
38 | + <mat-label translate>device-profile.snmp.querying-frequency</mat-label> | |
39 | + <input matInput formControlName="queryingFrequencyMs" type="number" min="0" required/> | |
40 | + <mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('required')"> | |
41 | + {{ 'device-profile.snmp.querying-frequency-required' | translate }} | |
42 | + </mat-error> | |
43 | + <mat-error *ngIf="deviceProfileCommunication.get('queryingFrequencyMs').hasError('pattern') || | |
44 | + deviceProfileCommunication.get('queryingFrequencyMs').hasError('min')"> | |
45 | + {{ 'device-profile.snmp.querying-frequency-invalid-format' | translate }} | |
46 | + </mat-error> | |
47 | + </mat-form-field> | |
48 | + <tb-snmp-device-profile-mapping formControlName="mappings"> | |
49 | + </tb-snmp-device-profile-mapping> | |
50 | + </section> | |
51 | + </div> | |
52 | + <button *ngIf="!disabled" | |
53 | + mat-icon-button color="primary" style="min-width: 40px;" | |
54 | + type="button" | |
55 | + (click)="removeCommunicationConfig($index)" | |
56 | + matTooltip="{{ 'action.remove' | translate }}" | |
57 | + matTooltipPosition="above"> | |
58 | + <mat-icon>remove_circle_outline</mat-icon> | |
59 | + </button> | |
60 | + </div> | |
61 | + <div *ngIf="!communicationConfigFormArray().controls.length && !disabled"> | |
62 | + <span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-communication-config</span> | |
63 | + </div> | |
64 | + <div *ngIf="!disabled && isAddEnabled"> | |
65 | + <button mat-stroked-button color="primary" | |
66 | + type="button" | |
67 | + (click)="addCommunicationConfig()"> | |
68 | + <mat-icon class="button-icon">add_circle_outline</mat-icon> | |
69 | + {{ 'device-profile.snmp.add-communication-config' | translate }} | |
70 | + </button> | |
71 | + </div> | |
72 | +</div> | ... | ... |
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 | +:host { | |
17 | + .communication-config { | |
18 | + border: 2px groove rgba(0, 0, 0, 0.25); | |
19 | + border-radius: 4px; | |
20 | + padding: 8px; | |
21 | + min-width: 0; | |
22 | + } | |
23 | + | |
24 | + .scope-row { | |
25 | + padding-bottom: 8px; | |
26 | + } | |
27 | + | |
28 | + .required-text { | |
29 | + margin: 16px 0 | |
30 | + } | |
31 | +} | |
32 | + | |
33 | +:host ::ng-deep { | |
34 | + .mat-form-field.spec { | |
35 | + .mat-form-field-infix { | |
36 | + width: 160px; | |
37 | + } | |
38 | + } | |
39 | + .button-icon{ | |
40 | + font-size: 20px; | |
41 | + width: 20px; | |
42 | + height: 20px; | |
43 | + } | |
44 | +} | ... | ... |
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 | + | |
17 | +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; | |
18 | +import { | |
19 | + AbstractControl, | |
20 | + ControlValueAccessor, | |
21 | + FormArray, | |
22 | + FormBuilder, | |
23 | + FormGroup, | |
24 | + NG_VALIDATORS, | |
25 | + NG_VALUE_ACCESSOR, | |
26 | + Validator, | |
27 | + Validators | |
28 | +} from '@angular/forms'; | |
29 | +import { SnmpCommunicationConfig, SnmpSpecType, SnmpSpecTypeTranslationMap } from '@shared/models/device.models'; | |
30 | +import { Subject, Subscription } from 'rxjs'; | |
31 | +import { isUndefinedOrNull } from '@core/utils'; | |
32 | +import { takeUntil } from 'rxjs/operators'; | |
33 | + | |
34 | +@Component({ | |
35 | + selector: 'tb-snmp-device-profile-communication-config', | |
36 | + templateUrl: './snmp-device-profile-communication-config.component.html', | |
37 | + styleUrls: ['./snmp-device-profile-communication-config.component.scss'], | |
38 | + providers: [ | |
39 | + { | |
40 | + provide: NG_VALUE_ACCESSOR, | |
41 | + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent), | |
42 | + multi: true | |
43 | + }, | |
44 | + { | |
45 | + provide: NG_VALIDATORS, | |
46 | + useExisting: forwardRef(() => SnmpDeviceProfileCommunicationConfigComponent), | |
47 | + multi: true | |
48 | + }] | |
49 | +}) | |
50 | +export class SnmpDeviceProfileCommunicationConfigComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { | |
51 | + | |
52 | + snmpSpecTypes = Object.values(SnmpSpecType); | |
53 | + snmpSpecTypeTranslationMap = SnmpSpecTypeTranslationMap; | |
54 | + | |
55 | + deviceProfileCommunicationConfig: FormGroup; | |
56 | + | |
57 | + @Input() | |
58 | + disabled: boolean; | |
59 | + | |
60 | + private usedSpecType: SnmpSpecType[] = []; | |
61 | + private valueChange$: Subscription = null; | |
62 | + private destroy$ = new Subject(); | |
63 | + private propagateChange = (v: any) => { }; | |
64 | + | |
65 | + constructor(private fb: FormBuilder) { } | |
66 | + | |
67 | + ngOnInit(): void { | |
68 | + this.deviceProfileCommunicationConfig = this.fb.group({ | |
69 | + communicationConfig: this.fb.array([]) | |
70 | + }); | |
71 | + } | |
72 | + | |
73 | + ngOnDestroy() { | |
74 | + if (this.valueChange$) { | |
75 | + this.valueChange$.unsubscribe(); | |
76 | + } | |
77 | + this.destroy$.next(); | |
78 | + this.destroy$.complete(); | |
79 | + } | |
80 | + | |
81 | + communicationConfigFormArray(): FormArray { | |
82 | + return this.deviceProfileCommunicationConfig.get('communicationConfig') as FormArray; | |
83 | + } | |
84 | + | |
85 | + registerOnChange(fn: any): void { | |
86 | + this.propagateChange = fn; | |
87 | + } | |
88 | + | |
89 | + registerOnTouched(fn: any): void { | |
90 | + } | |
91 | + | |
92 | + setDisabledState(isDisabled: boolean) { | |
93 | + this.disabled = isDisabled; | |
94 | + if (this.disabled) { | |
95 | + this.deviceProfileCommunicationConfig.disable({emitEvent: false}); | |
96 | + } else { | |
97 | + this.deviceProfileCommunicationConfig.enable({emitEvent: false}); | |
98 | + } | |
99 | + } | |
100 | + | |
101 | + writeValue(communicationConfig: SnmpCommunicationConfig[]) { | |
102 | + if (this.valueChange$) { | |
103 | + this.valueChange$.unsubscribe(); | |
104 | + } | |
105 | + const communicationConfigControl: Array<AbstractControl> = []; | |
106 | + if (communicationConfig) { | |
107 | + communicationConfig.forEach((config) => { | |
108 | + communicationConfigControl.push(this.createdFormGroup(config)); | |
109 | + }); | |
110 | + } | |
111 | + this.deviceProfileCommunicationConfig.setControl('communicationConfig', this.fb.array(communicationConfigControl)); | |
112 | + if (!communicationConfig || !communicationConfig.length) { | |
113 | + this.addCommunicationConfig(); | |
114 | + } | |
115 | + if (this.disabled) { | |
116 | + this.deviceProfileCommunicationConfig.disable({emitEvent: false}); | |
117 | + } else { | |
118 | + this.deviceProfileCommunicationConfig.enable({emitEvent: false}); | |
119 | + } | |
120 | + this.valueChange$ = this.deviceProfileCommunicationConfig.valueChanges.subscribe(() => { | |
121 | + this.updateModel(); | |
122 | + }); | |
123 | + this.updateUsedSpecType(); | |
124 | + if (!this.disabled && !this.deviceProfileCommunicationConfig.valid) { | |
125 | + this.updateModel(); | |
126 | + } | |
127 | + } | |
128 | + | |
129 | + public validate() { | |
130 | + return this.deviceProfileCommunicationConfig.valid && this.deviceProfileCommunicationConfig.value.communicationConfig.length ? null : { | |
131 | + communicationConfig: false | |
132 | + }; | |
133 | + } | |
134 | + | |
135 | + public removeCommunicationConfig(index: number) { | |
136 | + this.communicationConfigFormArray().removeAt(index); | |
137 | + } | |
138 | + | |
139 | + | |
140 | + get isAddEnabled(): boolean { | |
141 | + return this.communicationConfigFormArray().length !== Object.keys(SnmpSpecType).length; | |
142 | + } | |
143 | + | |
144 | + public addCommunicationConfig() { | |
145 | + this.communicationConfigFormArray().push(this.createdFormGroup()); | |
146 | + this.deviceProfileCommunicationConfig.updateValueAndValidity(); | |
147 | + if (!this.deviceProfileCommunicationConfig.valid) { | |
148 | + this.updateModel(); | |
149 | + } | |
150 | + } | |
151 | + | |
152 | + private getFirstUnusedSeverity(): SnmpSpecType { | |
153 | + for (const type of Object.values(SnmpSpecType)) { | |
154 | + if (this.usedSpecType.indexOf(type) === -1) { | |
155 | + return type; | |
156 | + } | |
157 | + } | |
158 | + return null; | |
159 | + } | |
160 | + | |
161 | + public isDisabledSeverity(type: SnmpSpecType, index: number): boolean { | |
162 | + const usedIndex = this.usedSpecType.indexOf(type); | |
163 | + return usedIndex > -1 && usedIndex !== index; | |
164 | + } | |
165 | + | |
166 | + public isShowFrequency(type: SnmpSpecType): boolean { | |
167 | + return type === SnmpSpecType.TELEMETRY_QUERYING || type === SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING; | |
168 | + } | |
169 | + | |
170 | + private updateUsedSpecType() { | |
171 | + this.usedSpecType = []; | |
172 | + const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value; | |
173 | + value.forEach((rule, index) => { | |
174 | + this.usedSpecType[index] = rule.spec; | |
175 | + }); | |
176 | + } | |
177 | + | |
178 | + private createdFormGroup(value?: SnmpCommunicationConfig): FormGroup { | |
179 | + if (isUndefinedOrNull(value)) { | |
180 | + value = { | |
181 | + spec: this.getFirstUnusedSeverity(), | |
182 | + queryingFrequencyMs: 5000, | |
183 | + mappings: null | |
184 | + }; | |
185 | + } | |
186 | + const form = this.fb.group({ | |
187 | + spec: [value.spec, Validators.required], | |
188 | + mappings: [value.mappings] | |
189 | + }); | |
190 | + if (this.isShowFrequency(value.spec)) { | |
191 | + form.addControl('queryingFrequencyMs', | |
192 | + this.fb.control(value.queryingFrequencyMs, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')])); | |
193 | + } | |
194 | + form.get('spec').valueChanges.pipe( | |
195 | + takeUntil(this.destroy$) | |
196 | + ).subscribe(spec => { | |
197 | + if (this.isShowFrequency(spec)) { | |
198 | + form.addControl('queryingFrequencyMs', | |
199 | + this.fb.control(5000, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')])); | |
200 | + } else { | |
201 | + form.removeControl('queryingFrequencyMs'); | |
202 | + } | |
203 | + }); | |
204 | + return form; | |
205 | + } | |
206 | + | |
207 | + private updateModel() { | |
208 | + const value: SnmpCommunicationConfig[] = this.deviceProfileCommunicationConfig.get('communicationConfig').value; | |
209 | + value.forEach(config => { | |
210 | + if (!this.isShowFrequency(config.spec)) { | |
211 | + delete config.queryingFrequencyMs; | |
212 | + } | |
213 | + }); | |
214 | + this.updateUsedSpecType(); | |
215 | + this.propagateChange(value); | |
216 | + } | |
217 | + | |
218 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2021 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<div fxFlex fxLayout="column" class="mapping-config"> | |
19 | + <div fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px" fxFlex="100"> | |
20 | + <div fxFlex fxLayout="row" fxLayoutGap="8px"> | |
21 | + <label fxFlex="26" class="tb-title no-padding" translate>device-profile.snmp.data-type</label> | |
22 | + <label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.data-key</label> | |
23 | + <label fxFlex="37" class="tb-title no-padding" translate>device-profile.snmp.oid</label> | |
24 | + <span style="min-width: 40px" [fxShow]="!disabled"></span> | |
25 | + </div> | |
26 | + </div> | |
27 | + <mat-divider></mat-divider> | |
28 | + <div *ngFor="let mappingConfig of mappingsConfigFormArray().controls; let $index = index; | |
29 | + last as isLast;" fxLayout="row" fxLayoutAlign="start center" | |
30 | + fxLayoutGap="8px" [formGroup]="mappingConfig" class="mapping-list"> | |
31 | + <div fxFlex fxLayout="row" fxLayoutGap="8px" fxLayoutAlign="start"> | |
32 | + <mat-form-field fxFlex="26" floatLabel="always" hideRequiredMarker> | |
33 | + <mat-label></mat-label> | |
34 | + <mat-select formControlName="dataType" required> | |
35 | + <mat-option *ngFor="let dataType of dataTypes" [value]="dataType"> | |
36 | + {{ dataTypesTranslationMap.get(dataType) | translate }} | |
37 | + </mat-option> | |
38 | + </mat-select> | |
39 | + <mat-error *ngIf="mappingConfig.get('dataType').hasError('required')"> | |
40 | + {{ 'device-profile.snmp.data-type-required' | translate }} | |
41 | + </mat-error> | |
42 | + </mat-form-field> | |
43 | + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37"> | |
44 | + <mat-label></mat-label> | |
45 | + <input matInput formControlName="key" required/> | |
46 | + <mat-error *ngIf="mappingConfig.get('key').hasError('required')"> | |
47 | + {{ 'device-profile.snmp.data-key-required' | translate }} | |
48 | + </mat-error> | |
49 | + </mat-form-field> | |
50 | + <mat-form-field floatLabel="always" hideRequiredMarker fxFlex="37"> | |
51 | + <mat-label></mat-label> | |
52 | + <input matInput formControlName="oid" required/> | |
53 | + <mat-error *ngIf="mappingConfig.get('oid').hasError('required')"> | |
54 | + {{ 'device-profile.snmp.oid-required' | translate }} | |
55 | + </mat-error> | |
56 | + <mat-error *ngIf="mappingConfig.get('oid').hasError('pattern')"> | |
57 | + {{ 'device-profile.snmp.oid-pattern' | translate }} | |
58 | + </mat-error> | |
59 | + </mat-form-field> | |
60 | + <button *ngIf="!disabled" | |
61 | + mat-icon-button color="primary" | |
62 | + type="button" | |
63 | + (click)="removeMappingConfig($index)" | |
64 | + matTooltip="{{ 'action.remove' | translate }}" | |
65 | + matTooltipPosition="above"> | |
66 | + <mat-icon>close</mat-icon> | |
67 | + </button> | |
68 | + </div> | |
69 | + </div> | |
70 | + <div *ngIf="!mappingsConfigFormArray().controls.length && !disabled"> | |
71 | + <span fxLayoutAlign="center center" class="tb-prompt required required-text" translate>device-profile.snmp.please-add-mapping-config</span> | |
72 | + </div> | |
73 | + <div *ngIf="!disabled"> | |
74 | + <button mat-stroked-button color="primary" | |
75 | + type="button" | |
76 | + (click)="addMappingConfig()"> | |
77 | + <mat-icon class="button-icon">add_circle_outline</mat-icon> | |
78 | + {{ 'device-profile.snmp.add-mapping' | translate }} | |
79 | + </button> | |
80 | + </div> | |
81 | +</div> | ... | ... |
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 | +:host { | |
17 | + .mapping-config { | |
18 | + min-width: 518px; | |
19 | + } | |
20 | + .mapping-list { | |
21 | + padding-bottom: 8px; | |
22 | + height: 46px; | |
23 | + } | |
24 | + | |
25 | + .required-text { | |
26 | + margin: 14px 0; | |
27 | + } | |
28 | +} | |
29 | + | |
30 | +:host ::ng-deep { | |
31 | + .mapping-list { | |
32 | + mat-form-field { | |
33 | + .mat-form-field-wrapper { | |
34 | + padding-bottom: 0; | |
35 | + .mat-form-field-infix { | |
36 | + border-top-width: 0.2em; | |
37 | + width: auto; | |
38 | + min-width: auto; | |
39 | + } | |
40 | + .mat-form-field-underline { | |
41 | + bottom: 0; | |
42 | + } | |
43 | + .mat-form-field-subscript-wrapper{ | |
44 | + margin-top: 1.8em; | |
45 | + } | |
46 | + } | |
47 | + } | |
48 | + } | |
49 | +} | ... | ... |
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-mapping.component.ts
0 → 100644
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 | + | |
17 | +import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; | |
18 | +import { | |
19 | + AbstractControl, | |
20 | + ControlValueAccessor, | |
21 | + FormArray, | |
22 | + FormBuilder, | |
23 | + FormGroup, | |
24 | + NG_VALIDATORS, | |
25 | + NG_VALUE_ACCESSOR, | |
26 | + ValidationErrors, | |
27 | + Validator, | |
28 | + Validators | |
29 | +} from '@angular/forms'; | |
30 | +import { SnmpMapping } from '@shared/models/device.models'; | |
31 | +import { Subscription } from 'rxjs'; | |
32 | +import { DataType, DataTypeTranslationMap } from '@shared/models/constants'; | |
33 | +import { isUndefinedOrNull } from '@core/utils'; | |
34 | + | |
35 | +@Component({ | |
36 | + selector: 'tb-snmp-device-profile-mapping', | |
37 | + templateUrl: './snmp-device-profile-mapping.component.html', | |
38 | + styleUrls: ['./snmp-device-profile-mapping.component.scss'], | |
39 | + providers: [ | |
40 | + { | |
41 | + provide: NG_VALUE_ACCESSOR, | |
42 | + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent), | |
43 | + multi: true | |
44 | + }, | |
45 | + { | |
46 | + provide: NG_VALIDATORS, | |
47 | + useExisting: forwardRef(() => SnmpDeviceProfileMappingComponent), | |
48 | + multi: true | |
49 | + }] | |
50 | +}) | |
51 | +export class SnmpDeviceProfileMappingComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { | |
52 | + | |
53 | + mappingsConfigForm: FormGroup; | |
54 | + | |
55 | + dataTypes = Object.values(DataType); | |
56 | + dataTypesTranslationMap = DataTypeTranslationMap; | |
57 | + | |
58 | + @Input() | |
59 | + disabled: boolean; | |
60 | + | |
61 | + private readonly oidPattern: RegExp = /^\.?([0-2])((\.0)|(\.[1-9][0-9]*))*$/; | |
62 | + | |
63 | + private valueChange$: Subscription = null; | |
64 | + private propagateChange = (v: any) => { }; | |
65 | + | |
66 | + constructor(private fb: FormBuilder) { } | |
67 | + | |
68 | + ngOnInit() { | |
69 | + this.mappingsConfigForm = this.fb.group({ | |
70 | + mappings: this.fb.array([]) | |
71 | + }); | |
72 | + } | |
73 | + | |
74 | + ngOnDestroy() { | |
75 | + if (this.valueChange$) { | |
76 | + this.valueChange$.unsubscribe(); | |
77 | + } | |
78 | + } | |
79 | + | |
80 | + registerOnChange(fn: any) { | |
81 | + this.propagateChange = fn; | |
82 | + } | |
83 | + | |
84 | + registerOnTouched(fn: any) { | |
85 | + } | |
86 | + | |
87 | + setDisabledState(isDisabled: boolean) { | |
88 | + this.disabled = isDisabled; | |
89 | + if (this.disabled) { | |
90 | + this.mappingsConfigForm.disable({emitEvent: false}); | |
91 | + } else { | |
92 | + this.mappingsConfigForm.enable({emitEvent: false}); | |
93 | + } | |
94 | + } | |
95 | + | |
96 | + validate(): ValidationErrors | null { | |
97 | + return this.mappingsConfigForm.valid && this.mappingsConfigForm.value.mappings.length ? null : { | |
98 | + mapping: false | |
99 | + }; | |
100 | + } | |
101 | + | |
102 | + writeValue(mappings: SnmpMapping[]) { | |
103 | + if (this.valueChange$) { | |
104 | + this.valueChange$.unsubscribe(); | |
105 | + } | |
106 | + const mappingsControl: Array<AbstractControl> = []; | |
107 | + if (mappings) { | |
108 | + mappings.forEach((config) => { | |
109 | + mappingsControl.push(this.createdFormGroup(config)); | |
110 | + }); | |
111 | + } | |
112 | + this.mappingsConfigForm.setControl('mappings', this.fb.array(mappingsControl)); | |
113 | + if (!mappings || !mappings.length) { | |
114 | + this.addMappingConfig(); | |
115 | + } | |
116 | + if (this.disabled) { | |
117 | + this.mappingsConfigForm.disable({emitEvent: false}); | |
118 | + } else { | |
119 | + this.mappingsConfigForm.enable({emitEvent: false}); | |
120 | + } | |
121 | + this.valueChange$ = this.mappingsConfigForm.valueChanges.subscribe(() => { | |
122 | + this.updateModel(); | |
123 | + }); | |
124 | + if (!this.disabled && !this.mappingsConfigForm.valid) { | |
125 | + this.updateModel(); | |
126 | + } | |
127 | + } | |
128 | + | |
129 | + mappingsConfigFormArray(): FormArray { | |
130 | + return this.mappingsConfigForm.get('mappings') as FormArray; | |
131 | + } | |
132 | + | |
133 | + public addMappingConfig() { | |
134 | + this.mappingsConfigFormArray().push(this.createdFormGroup()); | |
135 | + this.mappingsConfigForm.updateValueAndValidity(); | |
136 | + if (!this.mappingsConfigForm.valid) { | |
137 | + this.updateModel(); | |
138 | + } | |
139 | + } | |
140 | + | |
141 | + public removeMappingConfig(index: number) { | |
142 | + this.mappingsConfigFormArray().removeAt(index); | |
143 | + } | |
144 | + | |
145 | + private createdFormGroup(value?: SnmpMapping): FormGroup { | |
146 | + if (isUndefinedOrNull(value)) { | |
147 | + value = { | |
148 | + dataType: DataType.STRING, | |
149 | + key: '', | |
150 | + oid: '' | |
151 | + }; | |
152 | + } | |
153 | + return this.fb.group({ | |
154 | + dataType: [value.dataType, Validators.required], | |
155 | + key: [value.key, Validators.required], | |
156 | + oid: [value.oid, [Validators.required, Validators.pattern(this.oidPattern)]] | |
157 | + }); | |
158 | + } | |
159 | + | |
160 | + private updateModel() { | |
161 | + const value: SnmpMapping[] = this.mappingsConfigForm.get('mappings').value; | |
162 | + this.propagateChange(value); | |
163 | + } | |
164 | + | |
165 | +} | ... | ... |
1 | +<!-- | |
2 | + | |
3 | + Copyright © 2016-2021 The Thingsboard Authors | |
4 | + | |
5 | + Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | + you may not use this file except in compliance with the License. | |
7 | + You may obtain a copy of the License at | |
8 | + | |
9 | + http://www.apache.org/licenses/LICENSE-2.0 | |
10 | + | |
11 | + Unless required by applicable law or agreed to in writing, software | |
12 | + distributed under the License is distributed on an "AS IS" BASIS, | |
13 | + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | + See the License for the specific language governing permissions and | |
15 | + limitations under the License. | |
16 | + | |
17 | +--> | |
18 | +<form [formGroup]="snmpDeviceProfileTransportConfigurationFormGroup" style="padding: 8px 0 16px;"> | |
19 | + <section fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | |
20 | + <mat-form-field fxFlex> | |
21 | + <mat-label translate>device-profile.snmp.timeout-ms</mat-label> | |
22 | + <input matInput formControlName="timeoutMs" type="number" min="0" required/> | |
23 | + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('required')"> | |
24 | + {{ 'device-profile.snmp.timeout-ms-required' | translate }} | |
25 | + </mat-error> | |
26 | + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('pattern') || | |
27 | + snmpDeviceProfileTransportConfigurationFormGroup.get('timeoutMs').hasError('min')"> | |
28 | + {{ 'device-profile.snmp.timeout-ms-invalid-format' | translate }} | |
29 | + </mat-error> | |
30 | + </mat-form-field> | |
31 | + <mat-form-field fxFlex> | |
32 | + <mat-label translate>device-profile.snmp.retries</mat-label> | |
33 | + <input matInput formControlName="retries" type="number" min="0" required/> | |
34 | + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('required')"> | |
35 | + {{ 'device-profile.snmp.retries-required' | translate }} | |
36 | + </mat-error> | |
37 | + <mat-error *ngIf="snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('pattern') || | |
38 | + snmpDeviceProfileTransportConfigurationFormGroup.get('retries').hasError('min')"> | |
39 | + {{ 'device-profile.snmp.retries-invalid-format' | translate }} | |
40 | + </mat-error> | |
41 | + </mat-form-field> | |
42 | + </section> | |
43 | + <div class="tb-small" style="padding-bottom: 8px" translate>device-profile.snmp.communication-configs</div> | |
44 | + <tb-snmp-device-profile-communication-config formControlName="communicationConfigs"> | |
45 | + </tb-snmp-device-profile-communication-config> | |
46 | +</form> | ... | ... |
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component.ts
renamed from
ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts
... | ... | @@ -15,9 +15,16 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { Component, forwardRef, Input, OnDestroy, OnInit } from '@angular/core'; |
18 | -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | |
19 | -import { Store } from '@ngrx/store'; | |
20 | -import { AppState } from '@app/core/core.state'; | |
18 | +import { | |
19 | + ControlValueAccessor, | |
20 | + FormBuilder, | |
21 | + FormGroup, | |
22 | + NG_VALIDATORS, | |
23 | + NG_VALUE_ACCESSOR, | |
24 | + ValidationErrors, | |
25 | + Validator, | |
26 | + Validators | |
27 | +} from '@angular/forms'; | |
21 | 28 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
22 | 29 | import { |
23 | 30 | DeviceProfileTransportConfiguration, |
... | ... | @@ -40,19 +47,24 @@ export interface OidMappingConfiguration { |
40 | 47 | selector: 'tb-snmp-device-profile-transport-configuration', |
41 | 48 | templateUrl: './snmp-device-profile-transport-configuration.component.html', |
42 | 49 | styleUrls: [], |
43 | - providers: [{ | |
44 | - provide: NG_VALUE_ACCESSOR, | |
45 | - useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), | |
46 | - multi: true | |
47 | - }] | |
50 | + providers: [ | |
51 | + { | |
52 | + provide: NG_VALUE_ACCESSOR, | |
53 | + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), | |
54 | + multi: true | |
55 | + }, | |
56 | + { | |
57 | + provide: NG_VALIDATORS, | |
58 | + useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent), | |
59 | + multi: true | |
60 | + }] | |
48 | 61 | }) |
49 | -export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit, OnDestroy { | |
62 | +export class SnmpDeviceProfileTransportConfigurationComponent implements OnInit, OnDestroy, ControlValueAccessor, Validator { | |
50 | 63 | |
51 | 64 | snmpDeviceProfileTransportConfigurationFormGroup: FormGroup; |
52 | 65 | |
53 | 66 | private destroy$ = new Subject(); |
54 | 67 | private requiredValue: boolean; |
55 | - private configuration = []; | |
56 | 68 | |
57 | 69 | get required(): boolean { |
58 | 70 | return this.requiredValue; |
... | ... | @@ -69,12 +81,14 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control |
69 | 81 | private propagateChange = (v: any) => { |
70 | 82 | } |
71 | 83 | |
72 | - constructor(private store: Store<AppState>, private fb: FormBuilder) { | |
84 | + constructor(private fb: FormBuilder) { | |
73 | 85 | } |
74 | 86 | |
75 | 87 | ngOnInit(): void { |
76 | 88 | this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({ |
77 | - configuration: [null, Validators.required] | |
89 | + timeoutMs: [500, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], | |
90 | + retries: [0, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], | |
91 | + communicationConfigs: [null, Validators.required], | |
78 | 92 | }); |
79 | 93 | this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.pipe( |
80 | 94 | takeUntil(this.destroy$) |
... | ... | @@ -95,18 +109,33 @@ export class SnmpDeviceProfileTransportConfigurationComponent implements Control |
95 | 109 | registerOnTouched(fn: any): void { |
96 | 110 | } |
97 | 111 | |
112 | + setDisabledState(isDisabled: boolean) { | |
113 | + this.disabled = isDisabled; | |
114 | + if (this.disabled) { | |
115 | + this.snmpDeviceProfileTransportConfigurationFormGroup.disable({emitEvent: false}); | |
116 | + } else { | |
117 | + this.snmpDeviceProfileTransportConfigurationFormGroup.enable({emitEvent: false}); | |
118 | + } | |
119 | + } | |
120 | + | |
98 | 121 | writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void { |
99 | 122 | if (isDefinedAndNotNull(value)) { |
100 | - this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); | |
123 | + this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue(value, {emitEvent: !value.communicationConfigs}); | |
101 | 124 | } |
102 | 125 | } |
103 | 126 | |
104 | 127 | private updateModel() { |
105 | 128 | let configuration: DeviceProfileTransportConfiguration = null; |
106 | 129 | if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) { |
107 | - configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration; | |
130 | + configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue(); | |
108 | 131 | configuration.type = DeviceTransportType.SNMP; |
109 | 132 | } |
110 | 133 | this.propagateChange(configuration); |
111 | 134 | } |
135 | + | |
136 | + validate(): ValidationErrors | null { | |
137 | + return this.snmpDeviceProfileTransportConfigurationFormGroup.valid ? null : { | |
138 | + snmpDeviceProfileTransportConfiguration: false | |
139 | + }; | |
140 | + } | |
112 | 141 | } | ... | ... |
ui-ngx/src/app/modules/home/components/profile/device/snpm/snmp-device-profile-transport.module.ts
0 → 100644
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 | + | |
17 | +import { NgModule } from '@angular/core'; | |
18 | +import { SharedModule } from '@shared/shared.module'; | |
19 | +import { CommonModule } from '@angular/common'; | |
20 | +import { SnmpDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/snpm/snmp-device-profile-transport-configuration.component'; | |
21 | +import { SnmpDeviceProfileCommunicationConfigComponent } from './snmp-device-profile-communication-config.component'; | |
22 | +import { SnmpDeviceProfileMappingComponent } from './snmp-device-profile-mapping.component'; | |
23 | + | |
24 | +@NgModule({ | |
25 | + declarations: [ | |
26 | + SnmpDeviceProfileTransportConfigurationComponent, | |
27 | + SnmpDeviceProfileCommunicationConfigComponent, | |
28 | + SnmpDeviceProfileMappingComponent | |
29 | + ], | |
30 | + imports: [ | |
31 | + CommonModule, | |
32 | + SharedModule | |
33 | + ], | |
34 | + exports: [ | |
35 | + SnmpDeviceProfileTransportConfigurationComponent | |
36 | + ] | |
37 | +}) | |
38 | +export class SnmpDeviceProfileTransportModule { } | ... | ... |
... | ... | @@ -274,6 +274,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, |
274 | 274 | this.updateTitle(true); |
275 | 275 | this.alarmsDatasource.updateAlarms(); |
276 | 276 | this.clearCache(); |
277 | + this.ctx.detectChanges(); | |
277 | 278 | } |
278 | 279 | |
279 | 280 | public pageLinkSortDirection(): SortDirection { | ... | ... |
... | ... | @@ -443,7 +443,7 @@ export class EntitiesHierarchyWidgetComponent extends PageComponent implements O |
443 | 443 | const dataPageData = subscription.dataPages[0]; |
444 | 444 | const childNodes: HierarchyNavTreeNode[] = []; |
445 | 445 | datasourcesPageData.data.forEach((childDatasource, index) => { |
446 | - childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index])); | |
446 | + childNodes.push(this.datasourceToNode(childDatasource as HierarchyNodeDatasource, dataPageData.data[index], nodeCtx)); | |
447 | 447 | }); |
448 | 448 | nodeCtx.childrenNodesLoaded = true; |
449 | 449 | childrenNodesLoadCb(this.prepareNodes(childNodes)); | ... | ... |
... | ... | @@ -47,7 +47,6 @@ import { |
47 | 47 | isDefined, |
48 | 48 | isNumber, |
49 | 49 | isObject, |
50 | - isString, | |
51 | 50 | isUndefined |
52 | 51 | } from '@core/utils'; |
53 | 52 | import cssjs from '@core/css/css'; |
... | ... | @@ -236,6 +235,7 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni |
236 | 235 | this.updateTitle(true); |
237 | 236 | this.entityDatasource.dataUpdated(); |
238 | 237 | this.clearCache(); |
238 | + this.ctx.detectChanges(); | |
239 | 239 | } |
240 | 240 | |
241 | 241 | public pageLinkSortDirection(): SortDirection { | ... | ... |
... | ... | @@ -69,10 +69,10 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC |
69 | 69 | |
70 | 70 | this.config.cellActionDescriptors.push( |
71 | 71 | { |
72 | - name: this.translate.instant('resource.export'), | |
72 | + name: this.translate.instant('resource.download'), | |
73 | 73 | icon: 'file_download', |
74 | 74 | isEnabled: () => true, |
75 | - onAction: ($event, entity) => this.exportResource($event, entity) | |
75 | + onAction: ($event, entity) => this.downloadResource($event, entity) | |
76 | 76 | } |
77 | 77 | ); |
78 | 78 | |
... | ... | @@ -118,7 +118,7 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC |
118 | 118 | return this.config; |
119 | 119 | } |
120 | 120 | |
121 | - exportResource($event: Event, resource: ResourceInfo) { | |
121 | + downloadResource($event: Event, resource: ResourceInfo) { | |
122 | 122 | if ($event) { |
123 | 123 | $event.stopPropagation(); |
124 | 124 | } |
... | ... | @@ -127,8 +127,8 @@ export class ResourcesLibraryTableConfigResolver implements Resolve<EntityTableC |
127 | 127 | |
128 | 128 | onResourceAction(action: EntityAction<ResourceInfo>): boolean { |
129 | 129 | switch (action.action) { |
130 | - case 'uploadResource': | |
131 | - this.exportResource(action.event, action.entity); | |
130 | + case 'downloadResource': | |
131 | + this.downloadResource(action.event, action.entity); | |
132 | 132 | return true; |
133 | 133 | } |
134 | 134 | return false; | ... | ... |
... | ... | @@ -18,16 +18,26 @@ |
18 | 18 | <div class="tb-details-buttons" fxLayout.xs="column"> |
19 | 19 | <button mat-raised-button color="primary" fxFlex.xs |
20 | 20 | [disabled]="(isLoading$ | async)" |
21 | - (click)="onEntityAction($event, 'uploadResource')" | |
21 | + (click)="onEntityAction($event, 'downloadResource')" | |
22 | 22 | [fxShow]="!isEdit"> |
23 | - {{'resource.export' | translate }} | |
23 | + {{ 'resource.download' | translate }} | |
24 | 24 | </button> |
25 | 25 | <button mat-raised-button color="primary" fxFlex.xs |
26 | 26 | [disabled]="(isLoading$ | async)" |
27 | 27 | (click)="onEntityAction($event, 'delete')" |
28 | 28 | [fxShow]="!hideDelete() && !isEdit"> |
29 | - {{'resource.delete' | translate }} | |
29 | + {{ 'resource.delete' | translate }} | |
30 | 30 | </button> |
31 | + <div fxLayout="row" fxLayout.xs="column"> | |
32 | + <button mat-raised-button | |
33 | + ngxClipboard | |
34 | + (cbOnSuccess)="onResourceIdCopied()" | |
35 | + [cbContent]="entity?.id?.id" | |
36 | + [fxShow]="!isEdit"> | |
37 | + <mat-icon svgIcon="mdi:clipboard-arrow-left"></mat-icon> | |
38 | + <span translate>resource.copyId</span> | |
39 | + </button> | |
40 | + </div> | |
31 | 41 | </div> |
32 | 42 | <div class="mat-padding" fxLayout="column"> |
33 | 43 | <form [formGroup]="entityForm"> |
... | ... | @@ -47,7 +57,7 @@ |
47 | 57 | {{ 'resource.title-required' | translate }} |
48 | 58 | </mat-error> |
49 | 59 | </mat-form-field> |
50 | - <tb-file-input | |
60 | + <tb-file-input *ngIf="isAdd" | |
51 | 61 | formControlName="data" |
52 | 62 | required |
53 | 63 | [readAsBinary]="true" |
... | ... | @@ -59,6 +69,12 @@ |
59 | 69 | [existingFileName]="entityForm.get('fileName')?.value" |
60 | 70 | (fileNameChanged)="entityForm?.get('fileName').patchValue($event)"> |
61 | 71 | </tb-file-input> |
72 | + <div *ngIf="!isAdd" fxLayout="row" fxLayoutGap.gt-md="8px" fxLayoutGap.sm="8px" fxLayout.xs="column" fxLayout.md="column"> | |
73 | + <mat-form-field fxFlex> | |
74 | + <mat-label translate>resource.file-name</mat-label> | |
75 | + <input matInput formControlName="fileName" type="text"> | |
76 | + </mat-form-field> | |
77 | + </div> | |
62 | 78 | </fieldset> |
63 | 79 | </form> |
64 | 80 | </div> | ... | ... |
... | ... | @@ -30,6 +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 | 34 | |
34 | 35 | @Component({ |
35 | 36 | selector: 'tb-resources-library', |
... | ... | @@ -88,26 +89,29 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme |
88 | 89 | } |
89 | 90 | |
90 | 91 | buildForm(entity: Resource): FormGroup { |
91 | - return this.fb.group( | |
92 | + const form = this.fb.group( | |
92 | 93 | { |
94 | + title: [entity ? entity.title : '', []], | |
93 | 95 | resourceType: [{ |
94 | 96 | value: entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, |
95 | - disabled: this.isEdit | |
97 | + disabled: !this.isAdd | |
96 | 98 | }, [Validators.required]], |
97 | - data: [entity ? entity.data : null, [Validators.required]], | |
98 | 99 | fileName: [entity ? entity.fileName : null, [Validators.required]], |
99 | - title: [entity ? entity.title : '', []] | |
100 | 100 | } |
101 | 101 | ); |
102 | + if (this.isAdd) { | |
103 | + form.addControl('data', this.fb.control(null, Validators.required)); | |
104 | + } | |
105 | + return form | |
102 | 106 | } |
103 | 107 | |
104 | 108 | updateForm(entity: Resource) { |
105 | - this.entityForm.patchValue({resourceType: entity.resourceType}); | |
106 | 109 | if (this.isEdit) { |
107 | 110 | this.entityForm.get('resourceType').disable({emitEvent: false}); |
111 | + this.entityForm.get('fileName').disable({emitEvent: false}); | |
108 | 112 | } |
109 | 113 | this.entityForm.patchValue({ |
110 | - data: entity.data, | |
114 | + resourceType: entity.resourceType, | |
111 | 115 | fileName: entity.fileName, |
112 | 116 | title: entity.title |
113 | 117 | }); |
... | ... | @@ -132,4 +136,15 @@ export class ResourcesLibraryComponent extends EntityComponent<Resource> impleme |
132 | 136 | convertToBase64File(data: string): string { |
133 | 137 | return window.btoa(data); |
134 | 138 | } |
139 | + | |
140 | + onResourceIdCopied() { | |
141 | + this.store.dispatch(new ActionNotificationShow( | |
142 | + { | |
143 | + message: this.translate.instant('resource.idCopiedMessage'), | |
144 | + type: 'success', | |
145 | + duration: 750, | |
146 | + verticalPosition: 'bottom', | |
147 | + horizontalPosition: 'right' | |
148 | + })); | |
149 | + } | |
135 | 150 | } | ... | ... |
... | ... | @@ -169,7 +169,7 @@ export class CustomersTableConfigResolver implements Resolve<EntityTableConfig<C |
169 | 169 | if ($event) { |
170 | 170 | $event.stopPropagation(); |
171 | 171 | } |
172 | - this.router.navigateByUrl(`customers/${customer.id.id}/edges`); | |
172 | + this.router.navigateByUrl(`customers/${customer.id.id}/edgeInstances`); | |
173 | 173 | } |
174 | 174 | |
175 | 175 | onCustomerAction(action: EntityAction<Customer>): boolean { | ... | ... |
... | ... | @@ -362,7 +362,7 @@ export class DashboardsTableConfigResolver implements Resolve<EntityTableConfig< |
362 | 362 | if (this.config.componentsData.dashboardScope === 'customer') { |
363 | 363 | this.router.navigateByUrl(`customers/${this.config.componentsData.customerId}/dashboards/${dashboard.id.id}`); |
364 | 364 | } else if (this.config.componentsData.dashboardScope === 'edge') { |
365 | - this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/dashboards/${dashboard.id.id}`); | |
365 | + this.router.navigateByUrl(`edgeInstances/${this.config.componentsData.edgeId}/dashboards/${dashboard.id.id}`); | |
366 | 366 | } else { |
367 | 367 | this.router.navigateByUrl(`dashboards/${dashboard.id.id}`); |
368 | 368 | } | ... | ... |
... | ... | @@ -15,7 +15,16 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
18 | -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | |
18 | +import { | |
19 | + ControlValueAccessor, | |
20 | + FormBuilder, | |
21 | + FormGroup, | |
22 | + NG_VALIDATORS, | |
23 | + NG_VALUE_ACCESSOR, | |
24 | + ValidationErrors, | |
25 | + Validator, | |
26 | + Validators | |
27 | +} from '@angular/forms'; | |
19 | 28 | import { Store } from '@ngrx/store'; |
20 | 29 | import { AppState } from '@app/core/core.state'; |
21 | 30 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
... | ... | @@ -29,13 +38,20 @@ import { |
29 | 38 | selector: 'tb-device-data', |
30 | 39 | templateUrl: './device-data.component.html', |
31 | 40 | styleUrls: [], |
32 | - providers: [{ | |
33 | - provide: NG_VALUE_ACCESSOR, | |
34 | - useExisting: forwardRef(() => DeviceDataComponent), | |
35 | - multi: true | |
36 | - }] | |
41 | + providers: [ | |
42 | + { | |
43 | + provide: NG_VALUE_ACCESSOR, | |
44 | + useExisting: forwardRef(() => DeviceDataComponent), | |
45 | + multi: true | |
46 | + }, | |
47 | + { | |
48 | + provide: NG_VALIDATORS, | |
49 | + useExisting: forwardRef(() => DeviceDataComponent), | |
50 | + multi: true | |
51 | + }, | |
52 | + ] | |
37 | 53 | }) |
38 | -export class DeviceDataComponent implements ControlValueAccessor, OnInit { | |
54 | +export class DeviceDataComponent implements ControlValueAccessor, OnInit, Validator { | |
39 | 55 | |
40 | 56 | deviceDataFormGroup: FormGroup; |
41 | 57 | |
... | ... | @@ -97,6 +113,12 @@ export class DeviceDataComponent implements ControlValueAccessor, OnInit { |
97 | 113 | this.deviceDataFormGroup.patchValue({transportConfiguration: value?.transportConfiguration}, {emitEvent: false}); |
98 | 114 | } |
99 | 115 | |
116 | + validate(): ValidationErrors | null { | |
117 | + return this.deviceDataFormGroup.valid ? null : { | |
118 | + deviceDataForm: false | |
119 | + }; | |
120 | + } | |
121 | + | |
100 | 122 | private updateModel() { |
101 | 123 | let deviceData: DeviceData = null; |
102 | 124 | if (this.deviceDataFormGroup.valid) { | ... | ... |
... | ... | @@ -15,27 +15,39 @@ |
15 | 15 | /// |
16 | 16 | |
17 | 17 | import { Component, forwardRef, Input, OnInit } from '@angular/core'; |
18 | -import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators } from '@angular/forms'; | |
18 | +import { | |
19 | + ControlValueAccessor, | |
20 | + FormBuilder, | |
21 | + FormGroup, | |
22 | + NG_VALIDATORS, | |
23 | + NG_VALUE_ACCESSOR, | |
24 | + ValidationErrors, | |
25 | + Validator, | |
26 | + Validators | |
27 | +} from '@angular/forms'; | |
19 | 28 | import { Store } from '@ngrx/store'; |
20 | 29 | import { AppState } from '@app/core/core.state'; |
21 | 30 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; |
22 | -import { | |
23 | - DeviceTransportConfiguration, | |
24 | - DeviceTransportType | |
25 | -} from '@shared/models/device.models'; | |
31 | +import { DeviceTransportConfiguration, DeviceTransportType } from '@shared/models/device.models'; | |
26 | 32 | import { deepClone } from '@core/utils'; |
27 | 33 | |
28 | 34 | @Component({ |
29 | 35 | selector: 'tb-device-transport-configuration', |
30 | 36 | templateUrl: './device-transport-configuration.component.html', |
31 | 37 | styleUrls: [], |
32 | - providers: [{ | |
33 | - provide: NG_VALUE_ACCESSOR, | |
34 | - useExisting: forwardRef(() => DeviceTransportConfigurationComponent), | |
35 | - multi: true | |
36 | - }] | |
38 | + providers: [ | |
39 | + { | |
40 | + provide: NG_VALUE_ACCESSOR, | |
41 | + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), | |
42 | + multi: true | |
43 | + }, | |
44 | + { | |
45 | + provide: NG_VALIDATORS, | |
46 | + useExisting: forwardRef(() => DeviceTransportConfigurationComponent), | |
47 | + multi: true | |
48 | + }] | |
37 | 49 | }) |
38 | -export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { | |
50 | +export class DeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { | |
39 | 51 | |
40 | 52 | deviceTransportType = DeviceTransportType; |
41 | 53 | |
... | ... | @@ -92,7 +104,15 @@ export class DeviceTransportConfigurationComponent implements ControlValueAccess |
92 | 104 | if (configuration) { |
93 | 105 | delete configuration.type; |
94 | 106 | } |
95 | - this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); | |
107 | + setTimeout(() => { | |
108 | + this.deviceTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false}); | |
109 | + }, 0); | |
110 | + } | |
111 | + | |
112 | + validate(): ValidationErrors | null { | |
113 | + return this.deviceTransportConfigurationFormGroup.valid ? null : { | |
114 | + deviceTransportConfiguration: false | |
115 | + }; | |
96 | 116 | } |
97 | 117 | |
98 | 118 | private updateModel() { | ... | ... |
... | ... | @@ -15,10 +15,119 @@ |
15 | 15 | limitations under the License. |
16 | 16 | |
17 | 17 | --> |
18 | -<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;"> | |
19 | - <tb-json-object-edit | |
20 | - [required]="required" | |
21 | - label="{{ 'device-profile.transport-type-snmp-hint' | translate }}" | |
22 | - formControlName="configuration"> | |
23 | - </tb-json-object-edit> | |
18 | +<form [formGroup]="snmpDeviceTransportConfigurationFormGroup" style="padding-bottom: 16px;" fxLayoutGap="8px" fxLayout="column"> | |
19 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | |
20 | + <mat-form-field fxFlex> | |
21 | + <mat-label translate>device-profile.snmp.host</mat-label> | |
22 | + <input matInput formControlName="host" required> | |
23 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('host').hasError('required') || | |
24 | + snmpDeviceTransportConfigurationFormGroup.get('host').hasError('pattern')"> | |
25 | + {{ 'device-profile.snmp.host-required' | translate }} | |
26 | + </mat-error> | |
27 | + </mat-form-field> | |
28 | + <mat-form-field fxFlex> | |
29 | + <mat-label translate>device-profile.snmp.port</mat-label> | |
30 | + <input matInput formControlName="port" type="number" min="0" required> | |
31 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('port').hasError('required')"> | |
32 | + {{ 'device-profile.snmp.port-required' | translate }} | |
33 | + </mat-error> | |
34 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('port').hasError('pattern') || | |
35 | + snmpDeviceTransportConfigurationFormGroup.get('port').hasError('min')"> | |
36 | + {{ 'device-profile.snmp.port-format' | translate }} | |
37 | + </mat-error> | |
38 | + </mat-form-field> | |
39 | + </div> | |
40 | + <mat-form-field class="mat-block" floatLabel="always" hideRequiredMarker> | |
41 | + <mat-label translate>device-profile.snmp.protocol-version</mat-label> | |
42 | + <mat-select formControlName="protocolVersion" required> | |
43 | + <mat-option *ngFor="let snmpDeviceProtocolVersion of snmpDeviceProtocolVersions" [value]="snmpDeviceProtocolVersion"> | |
44 | + {{ snmpDeviceProtocolVersion | lowercase }} | |
45 | + </mat-option> | |
46 | + </mat-select> | |
47 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').hasError('required')"> | |
48 | + {{ 'device-profile.snmp.protocol-version-required' | translate }} | |
49 | + </mat-error> | |
50 | + </mat-form-field> | |
51 | + <section *ngIf="!isV3protocolVersion()"> | |
52 | + <mat-form-field class="mat-block"> | |
53 | + <mat-label translate>device-profile.snmp.community</mat-label> | |
54 | + <input matInput formControlName="community" required> | |
55 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('community').hasError('required') || | |
56 | + snmpDeviceTransportConfigurationFormGroup.get('community').hasError('pattern')"> | |
57 | + {{ 'device-profile.snmp.community-required' | translate }} | |
58 | + </mat-error> | |
59 | + </mat-form-field> | |
60 | + </section> | |
61 | + <section *ngIf="isV3protocolVersion()"> | |
62 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | |
63 | + <mat-form-field fxFlex> | |
64 | + <mat-label translate>device-profile.snmp.user-name</mat-label> | |
65 | + <input matInput formControlName="username" required> | |
66 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('username').hasError('required') || | |
67 | + snmpDeviceTransportConfigurationFormGroup.get('username').hasError('pattern')"> | |
68 | + {{ 'device-profile.snmp.user-name-required' | translate }} | |
69 | + </mat-error> | |
70 | + </mat-form-field> | |
71 | + <mat-form-field fxFlex> | |
72 | + <mat-label translate>device-profile.snmp.security-name</mat-label> | |
73 | + <input matInput formControlName="securityName" required> | |
74 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('required') || | |
75 | + snmpDeviceTransportConfigurationFormGroup.get('securityName').hasError('pattern')"> | |
76 | + {{ 'device-profile.snmp.security-name-required' | translate }} | |
77 | + </mat-error> | |
78 | + </mat-form-field> | |
79 | + </div> | |
80 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | |
81 | + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker> | |
82 | + <mat-label translate>device-profile.snmp.authentication-protocol</mat-label> | |
83 | + <mat-select formControlName="authenticationProtocol" required> | |
84 | + <mat-option *ngFor="let snmpAuthenticationProtocol of snmpAuthenticationProtocols" [value]="snmpAuthenticationProtocol"> | |
85 | + {{ snmpAuthenticationProtocolTranslation.get(snmpAuthenticationProtocol) }} | |
86 | + </mat-option> | |
87 | + </mat-select> | |
88 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').hasError('required')"> | |
89 | + {{ 'device-profile.snmp.authentication-protocol-required' | translate }} | |
90 | + </mat-error> | |
91 | + </mat-form-field> | |
92 | + <mat-form-field fxFlex> | |
93 | + <mat-label translate>device-profile.snmp.authentication-passphrase</mat-label> | |
94 | + <input matInput formControlName="authenticationPassphrase" required> | |
95 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('required') || | |
96 | + snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').hasError('pattern')"> | |
97 | + {{ 'device-profile.snmp.authentication-passphrase-required' | translate }} | |
98 | + </mat-error> | |
99 | + </mat-form-field> | |
100 | + </div> | |
101 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | |
102 | + <mat-form-field fxFlex floatLabel="always" hideRequiredMarker> | |
103 | + <mat-label translate>device-profile.snmp.privacy-protocol</mat-label> | |
104 | + <mat-select formControlName="privacyProtocol" required> | |
105 | + <mat-option *ngFor="let snmpPrivacyProtocol of snmpPrivacyProtocols" [value]="snmpPrivacyProtocol"> | |
106 | + {{ snmpPrivacyProtocolTranslation.get(snmpPrivacyProtocol) }} | |
107 | + </mat-option> | |
108 | + </mat-select> | |
109 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').hasError('required')"> | |
110 | + {{ 'device-profile.snmp.privacy-protocol-required' | translate }} | |
111 | + </mat-error> | |
112 | + </mat-form-field> | |
113 | + <mat-form-field fxFlex> | |
114 | + <mat-label translate>device-profile.snmp.privacy-passphrase</mat-label> | |
115 | + <input matInput formControlName="privacyPassphrase" required> | |
116 | + <mat-error *ngIf="snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('required') || | |
117 | + snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').hasError('pattern')"> | |
118 | + {{ 'device-profile.snmp.privacy-passphrase-required' | translate }} | |
119 | + </mat-error> | |
120 | + </mat-form-field> | |
121 | + </div> | |
122 | + <div fxLayout="row" fxLayoutGap="8px" fxLayout.xs="column"> | |
123 | + <mat-form-field fxFlex> | |
124 | + <mat-label translate>device-profile.snmp.context-name</mat-label> | |
125 | + <input matInput formControlName="contextName"> | |
126 | + </mat-form-field> | |
127 | + <mat-form-field fxFlex> | |
128 | + <mat-label translate>device-profile.snmp.engine-id</mat-label> | |
129 | + <input matInput formControlName="engineId"> | |
130 | + </mat-form-field> | |
131 | + </div> | |
132 | + </section> | |
24 | 133 | </form> | ... | ... |
... | ... | @@ -14,31 +14,57 @@ |
14 | 14 | /// limitations under the License. |
15 | 15 | /// |
16 | 16 | |
17 | -import {Component, forwardRef, Input, OnInit} from '@angular/core'; | |
18 | -import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms'; | |
19 | -import {Store} from '@ngrx/store'; | |
20 | -import {AppState} from '@app/core/core.state'; | |
21 | -import {coerceBooleanProperty} from '@angular/cdk/coercion'; | |
17 | +import { Component, forwardRef, Input, OnInit } from '@angular/core'; | |
18 | +import { | |
19 | + ControlValueAccessor, | |
20 | + FormBuilder, | |
21 | + FormGroup, | |
22 | + NG_VALIDATORS, | |
23 | + NG_VALUE_ACCESSOR, | |
24 | + ValidationErrors, | |
25 | + Validator, | |
26 | + Validators | |
27 | +} from '@angular/forms'; | |
28 | +import { Store } from '@ngrx/store'; | |
29 | +import { AppState } from '@app/core/core.state'; | |
30 | +import { coerceBooleanProperty } from '@angular/cdk/coercion'; | |
22 | 31 | import { |
23 | 32 | DeviceTransportConfiguration, |
24 | 33 | DeviceTransportType, |
25 | - SnmpDeviceTransportConfiguration | |
34 | + SnmpAuthenticationProtocol, | |
35 | + SnmpAuthenticationProtocolTranslationMap, | |
36 | + SnmpDeviceProtocolVersion, | |
37 | + SnmpDeviceTransportConfiguration, | |
38 | + SnmpPrivacyProtocol, | |
39 | + SnmpPrivacyProtocolTranslationMap | |
26 | 40 | } from '@shared/models/device.models'; |
41 | +import { isDefinedAndNotNull } from '@core/utils'; | |
27 | 42 | |
28 | 43 | @Component({ |
29 | 44 | selector: 'tb-snmp-device-transport-configuration', |
30 | 45 | templateUrl: './snmp-device-transport-configuration.component.html', |
31 | 46 | styleUrls: [], |
32 | - providers: [{ | |
33 | - provide: NG_VALUE_ACCESSOR, | |
34 | - useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), | |
35 | - multi: true | |
36 | - }] | |
47 | + providers: [ | |
48 | + { | |
49 | + provide: NG_VALUE_ACCESSOR, | |
50 | + useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), | |
51 | + multi: true | |
52 | + }, { | |
53 | + provide: NG_VALIDATORS, | |
54 | + useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent), | |
55 | + multi: true | |
56 | + }] | |
37 | 57 | }) |
38 | -export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit { | |
58 | +export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit, Validator { | |
39 | 59 | |
40 | 60 | snmpDeviceTransportConfigurationFormGroup: FormGroup; |
41 | 61 | |
62 | + snmpDeviceProtocolVersions = Object.values(SnmpDeviceProtocolVersion); | |
63 | + snmpAuthenticationProtocols = Object.values(SnmpAuthenticationProtocol); | |
64 | + snmpAuthenticationProtocolTranslation = SnmpAuthenticationProtocolTranslationMap; | |
65 | + snmpPrivacyProtocols = Object.values(SnmpPrivacyProtocol); | |
66 | + snmpPrivacyProtocolTranslation = SnmpPrivacyProtocolTranslationMap; | |
67 | + | |
42 | 68 | private requiredValue: boolean; |
43 | 69 | |
44 | 70 | get required(): boolean { |
... | ... | @@ -53,8 +79,7 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc |
53 | 79 | @Input() |
54 | 80 | disabled: boolean; |
55 | 81 | |
56 | - private propagateChange = (v: any) => { | |
57 | - }; | |
82 | + private propagateChange = (v: any) => { }; | |
58 | 83 | |
59 | 84 | constructor(private store: Store<AppState>, |
60 | 85 | private fb: FormBuilder) { |
... | ... | @@ -69,13 +94,33 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc |
69 | 94 | |
70 | 95 | ngOnInit() { |
71 | 96 | this.snmpDeviceTransportConfigurationFormGroup = this.fb.group({ |
72 | - configuration: [null, Validators.required] | |
97 | + host: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], | |
98 | + port: [null, [Validators.required, Validators.min(0), Validators.pattern('[0-9]*')]], | |
99 | + protocolVersion: [SnmpDeviceProtocolVersion.V2C, Validators.required], | |
100 | + community: ['public', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], | |
101 | + username: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], | |
102 | + securityName: ['public', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], | |
103 | + contextName: [null], | |
104 | + authenticationProtocol: [SnmpAuthenticationProtocol.SHA_512, Validators.required], | |
105 | + authenticationPassphrase: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], | |
106 | + privacyProtocol: [SnmpPrivacyProtocol.DES, Validators.required], | |
107 | + privacyPassphrase: ['', [Validators.required, Validators.pattern('(.|\\s)*\\S(.|\\s)*')]], | |
108 | + engineId: [''] | |
109 | + }); | |
110 | + this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').valueChanges.subscribe((protocol: SnmpDeviceProtocolVersion) => { | |
111 | + this.updateDisabledFormValue(protocol); | |
73 | 112 | }); |
74 | 113 | this.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => { |
75 | 114 | this.updateModel(); |
76 | 115 | }); |
77 | 116 | } |
78 | 117 | |
118 | + validate(): ValidationErrors | null { | |
119 | + return this.snmpDeviceTransportConfigurationFormGroup.valid ? null : { | |
120 | + snmpDeviceTransportConfiguration: false | |
121 | + }; | |
122 | + } | |
123 | + | |
79 | 124 | setDisabledState(isDisabled: boolean): void { |
80 | 125 | this.disabled = isDisabled; |
81 | 126 | if (this.disabled) { |
... | ... | @@ -86,13 +131,46 @@ export class SnmpDeviceTransportConfigurationComponent implements ControlValueAc |
86 | 131 | } |
87 | 132 | |
88 | 133 | writeValue(value: SnmpDeviceTransportConfiguration | null): void { |
89 | - this.snmpDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false}); | |
134 | + if (isDefinedAndNotNull(value)) { | |
135 | + this.snmpDeviceTransportConfigurationFormGroup.patchValue(value, {emitEvent: false}); | |
136 | + if (this.snmpDeviceTransportConfigurationFormGroup.enabled) { | |
137 | + this.updateDisabledFormValue(value.protocolVersion || SnmpDeviceProtocolVersion.V2C); | |
138 | + } | |
139 | + } | |
140 | + } | |
141 | + | |
142 | + isV3protocolVersion(): boolean { | |
143 | + return this.snmpDeviceTransportConfigurationFormGroup.get('protocolVersion').value === SnmpDeviceProtocolVersion.V3; | |
144 | + } | |
145 | + | |
146 | + private updateDisabledFormValue(protocol: SnmpDeviceProtocolVersion) { | |
147 | + if (protocol === SnmpDeviceProtocolVersion.V3) { | |
148 | + this.snmpDeviceTransportConfigurationFormGroup.get('community').disable({emitEvent: false}); | |
149 | + this.snmpDeviceTransportConfigurationFormGroup.get('username').enable({emitEvent: false}); | |
150 | + this.snmpDeviceTransportConfigurationFormGroup.get('securityName').enable({emitEvent: false}); | |
151 | + this.snmpDeviceTransportConfigurationFormGroup.get('contextName').enable({emitEvent: false}); | |
152 | + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').enable({emitEvent: false}); | |
153 | + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').enable({emitEvent: false}); | |
154 | + this.snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').enable({emitEvent: false}); | |
155 | + this.snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').enable({emitEvent: false}); | |
156 | + this.snmpDeviceTransportConfigurationFormGroup.get('engineId').enable({emitEvent: false}); | |
157 | + } else { | |
158 | + this.snmpDeviceTransportConfigurationFormGroup.get('community').enable({emitEvent: false}); | |
159 | + this.snmpDeviceTransportConfigurationFormGroup.get('username').disable({emitEvent: false}); | |
160 | + this.snmpDeviceTransportConfigurationFormGroup.get('securityName').disable({emitEvent: false}); | |
161 | + this.snmpDeviceTransportConfigurationFormGroup.get('contextName').disable({emitEvent: false}); | |
162 | + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationProtocol').disable({emitEvent: false}); | |
163 | + this.snmpDeviceTransportConfigurationFormGroup.get('authenticationPassphrase').disable({emitEvent: false}); | |
164 | + this.snmpDeviceTransportConfigurationFormGroup.get('privacyProtocol').disable({emitEvent: false}); | |
165 | + this.snmpDeviceTransportConfigurationFormGroup.get('privacyPassphrase').disable({emitEvent: false}); | |
166 | + this.snmpDeviceTransportConfigurationFormGroup.get('engineId').disable({emitEvent: false}); | |
167 | + } | |
90 | 168 | } |
91 | 169 | |
92 | 170 | private updateModel() { |
93 | 171 | let configuration: DeviceTransportConfiguration = null; |
94 | 172 | if (this.snmpDeviceTransportConfigurationFormGroup.valid) { |
95 | - configuration = this.snmpDeviceTransportConfigurationFormGroup.getRawValue().configuration; | |
173 | + configuration = this.snmpDeviceTransportConfigurationFormGroup.value; | |
96 | 174 | configuration.type = DeviceTransportType.SNMP; |
97 | 175 | } |
98 | 176 | this.propagateChange(configuration); | ... | ... |
... | ... | @@ -42,7 +42,7 @@ import { |
42 | 42 | |
43 | 43 | const routes: Routes = [ |
44 | 44 | { |
45 | - path: 'edges', | |
45 | + path: 'edgeInstances', | |
46 | 46 | data: { |
47 | 47 | breadcrumb: { |
48 | 48 | label: 'edge.edge-instances', |
... | ... | @@ -187,6 +187,24 @@ const routes: Routes = [ |
187 | 187 | } |
188 | 188 | } |
189 | 189 | ] |
190 | + } | |
191 | + ] | |
192 | + }, | |
193 | + { | |
194 | + path: 'edgeManagement', | |
195 | + data: { | |
196 | + breadcrumb: { | |
197 | + label: 'edge.management', | |
198 | + icon: 'settings_input_antenna' | |
199 | + } | |
200 | + }, | |
201 | + children: [ | |
202 | + { | |
203 | + path: '', | |
204 | + data: { | |
205 | + auth: [Authority.TENANT_ADMIN, Authority.CUSTOMER_USER], | |
206 | + redirectTo: '/edgeManagement/ruleChains' | |
207 | + } | |
190 | 208 | }, |
191 | 209 | { |
192 | 210 | path: 'ruleChains', | ... | ... |
... | ... | @@ -417,7 +417,7 @@ export class EdgesTableConfigResolver implements Resolve<EntityTableConfig<EdgeI |
417 | 417 | suffix = 'ruleChains'; |
418 | 418 | break; |
419 | 419 | } |
420 | - this.router.navigateByUrl(`edges/${edge.id.id}/${suffix}`); | |
420 | + this.router.navigateByUrl(`edgeInstances/${edge.id.id}/${suffix}`); | |
421 | 421 | } |
422 | 422 | |
423 | 423 | assignToCustomer($event: Event, edgesIds: Array<EdgeId>) { | ... | ... |
... | ... | @@ -152,7 +152,7 @@ export class RuleNodeDetailsComponent extends PageComponent implements OnInit, O |
152 | 152 | } |
153 | 153 | if (this.ruleNode.targetRuleChainId) { |
154 | 154 | if (this.ruleChainType === RuleChainType.EDGE) { |
155 | - this.router.navigateByUrl(`/edges/ruleChains/${this.ruleNode.targetRuleChainId}`); | |
155 | + this.router.navigateByUrl(`/edgeManagement/ruleChains/${this.ruleNode.targetRuleChainId}`); | |
156 | 156 | } else { |
157 | 157 | this.router.navigateByUrl(`/ruleChains/${this.ruleNode.targetRuleChainId}`); |
158 | 158 | } | ... | ... |
... | ... | @@ -1289,7 +1289,7 @@ export class RuleChainPageComponent extends PageComponent |
1289 | 1289 | if (this.ruleChainType !== RuleChainType.EDGE) { |
1290 | 1290 | this.router.navigateByUrl(`ruleChains/${this.ruleChain.id.id}`); |
1291 | 1291 | } else { |
1292 | - this.router.navigateByUrl(`edges/ruleChains/${this.ruleChain.id.id}`); | |
1292 | + this.router.navigateByUrl(`edgeManagement/ruleChains/${this.ruleChain.id.id}`); | |
1293 | 1293 | } |
1294 | 1294 | } else { |
1295 | 1295 | this.createRuleChainModel(); | ... | ... |
... | ... | @@ -285,7 +285,7 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig< |
285 | 285 | if (ruleChainImport) { |
286 | 286 | this.itembuffer.storeRuleChainImport(ruleChainImport); |
287 | 287 | if (this.config.componentsData.ruleChainScope === 'edges') { |
288 | - this.router.navigateByUrl(`edges/ruleChains/ruleChain/import`); | |
288 | + this.router.navigateByUrl(`edgeManagement/ruleChains/ruleChain/import`); | |
289 | 289 | } else { |
290 | 290 | this.router.navigateByUrl(`ruleChains/ruleChain/import`); |
291 | 291 | } |
... | ... | @@ -298,9 +298,9 @@ export class RuleChainsTableConfigResolver implements Resolve<EntityTableConfig< |
298 | 298 | $event.stopPropagation(); |
299 | 299 | } |
300 | 300 | if (this.config.componentsData.ruleChainScope === 'edges') { |
301 | - this.router.navigateByUrl(`edges/ruleChains/${ruleChain.id.id}`); | |
301 | + this.router.navigateByUrl(`edgeManagement/ruleChains/${ruleChain.id.id}`); | |
302 | 302 | } else if (this.config.componentsData.ruleChainScope === 'edge') { |
303 | - this.router.navigateByUrl(`edges/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`); | |
303 | + this.router.navigateByUrl(`edgeInstances/${this.config.componentsData.edgeId}/ruleChains/${ruleChain.id.id}`); | |
304 | 304 | } else { |
305 | 305 | this.router.navigateByUrl(`ruleChains/${ruleChain.id.id}`); |
306 | 306 | } | ... | ... |
... | ... | @@ -50,7 +50,7 @@ export class RuleNodeComponent extends FcNodeComponent implements OnInit { |
50 | 50 | } |
51 | 51 | if (node.targetRuleChainId) { |
52 | 52 | if (node.ruleChainType === RuleChainType.EDGE) { |
53 | - this.router.navigateByUrl(`/edges/ruleChains/${node.targetRuleChainId}`); | |
53 | + this.router.navigateByUrl(`/edgeManagement/ruleChains/${node.targetRuleChainId}`); | |
54 | 54 | } else { |
55 | 55 | this.router.navigateByUrl(`/ruleChains/${node.targetRuleChainId}`); |
56 | 56 | } | ... | ... |
... | ... | @@ -120,6 +120,7 @@ export const HelpLinks = { |
120 | 120 | entityViews: helpBaseUrl + '/docs/user-guide/ui/entity-views', |
121 | 121 | entitiesImport: helpBaseUrl + '/docs/user-guide/bulk-provisioning', |
122 | 122 | rulechains: helpBaseUrl + '/docs/user-guide/ui/rule-chains', |
123 | + resources: helpBaseUrl + '/docs/user-guide/ui/resources', | |
123 | 124 | dashboards: helpBaseUrl + '/docs/user-guide/ui/dashboards', |
124 | 125 | otaUpdates: helpBaseUrl + '/docs/user-guide/ui/ota-updates', |
125 | 126 | widgetsBundles: helpBaseUrl + '/docs/user-guide/ui/widget-library#bundles', |
... | ... | @@ -147,6 +148,22 @@ export enum ValueType { |
147 | 148 | JSON = 'JSON' |
148 | 149 | } |
149 | 150 | |
151 | +export enum DataType { | |
152 | + STRING = 'STRING', | |
153 | + LONG = 'LONG', | |
154 | + BOOLEAN = 'BOOLEAN', | |
155 | + DOUBLE = 'DOUBLE', | |
156 | + JSON = 'JSON' | |
157 | +} | |
158 | + | |
159 | +export const DataTypeTranslationMap = new Map([ | |
160 | + [DataType.STRING, 'value.string'], | |
161 | + [DataType.LONG, 'value.integer'], | |
162 | + [DataType.BOOLEAN, 'value.boolean'], | |
163 | + [DataType.DOUBLE, 'value.double'], | |
164 | + [DataType.JSON, 'value.json'] | |
165 | +]); | |
166 | + | |
150 | 167 | export const valueTypesMap = new Map<ValueType, ValueTypeData>( |
151 | 168 | [ |
152 | 169 | [ | ... | ... |
... | ... | @@ -29,6 +29,7 @@ import * as _moment from 'moment'; |
29 | 29 | import { AbstractControl, ValidationErrors } from '@angular/forms'; |
30 | 30 | import { OtaPackageId } from '@shared/models/id/ota-package-id'; |
31 | 31 | import { DashboardId } from '@shared/models/id/dashboard-id'; |
32 | +import { DataType } from '@shared/models/constants'; | |
32 | 33 | |
33 | 34 | export enum DeviceProfileType { |
34 | 35 | DEFAULT = 'DEFAULT', |
... | ... | @@ -257,7 +258,35 @@ export interface Lwm2mDeviceProfileTransportConfiguration { |
257 | 258 | } |
258 | 259 | |
259 | 260 | export interface SnmpDeviceProfileTransportConfiguration { |
260 | - [key: string]: any; | |
261 | + timeoutMs?: number; | |
262 | + retries?: number; | |
263 | + communicationConfigs?: SnmpCommunicationConfig[]; | |
264 | +} | |
265 | + | |
266 | +export enum SnmpSpecType { | |
267 | + TELEMETRY_QUERYING = 'TELEMETRY_QUERYING', | |
268 | + CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING', | |
269 | + SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING', | |
270 | + TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST' | |
271 | +} | |
272 | + | |
273 | +export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([ | |
274 | + [SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'], | |
275 | + [SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'], | |
276 | + [SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'], | |
277 | + [SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request'] | |
278 | +]); | |
279 | + | |
280 | +export interface SnmpCommunicationConfig { | |
281 | + spec: SnmpSpecType; | |
282 | + mappings: SnmpMapping[]; | |
283 | + queryingFrequencyMs?: number; | |
284 | +} | |
285 | + | |
286 | +export interface SnmpMapping { | |
287 | + oid: string; | |
288 | + key: string; | |
289 | + dataType: DataType; | |
261 | 290 | } |
262 | 291 | |
263 | 292 | export type DeviceProfileTransportConfigurations = DefaultDeviceProfileTransportConfiguration & |
... | ... | @@ -332,7 +361,11 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT |
332 | 361 | transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; |
333 | 362 | break; |
334 | 363 | case DeviceTransportType.SNMP: |
335 | - const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = {}; | |
364 | + const snmpTransportConfiguration: SnmpDeviceProfileTransportConfiguration = { | |
365 | + timeoutMs: 500, | |
366 | + retries: 0, | |
367 | + communicationConfigs: null | |
368 | + }; | |
336 | 369 | transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP}; |
337 | 370 | break; |
338 | 371 | } |
... | ... | @@ -361,7 +394,12 @@ export function createDeviceTransportConfiguration(type: DeviceTransportType): D |
361 | 394 | transportConfiguration = {...lwm2mTransportConfiguration, type: DeviceTransportType.LWM2M}; |
362 | 395 | break; |
363 | 396 | case DeviceTransportType.SNMP: |
364 | - const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = {}; | |
397 | + const snmpTransportConfiguration: SnmpDeviceTransportConfiguration = { | |
398 | + host: 'localhost', | |
399 | + port: 161, | |
400 | + protocolVersion: SnmpDeviceProtocolVersion.V2C, | |
401 | + community: 'public' | |
402 | + }; | |
365 | 403 | transportConfiguration = {...snmpTransportConfiguration, type: DeviceTransportType.SNMP}; |
366 | 404 | break; |
367 | 405 | } |
... | ... | @@ -539,8 +577,57 @@ export interface Lwm2mDeviceTransportConfiguration { |
539 | 577 | [key: string]: any; |
540 | 578 | } |
541 | 579 | |
580 | +export enum SnmpDeviceProtocolVersion { | |
581 | + V1 = 'V1', | |
582 | + V2C = 'V2C', | |
583 | + V3 = 'V3' | |
584 | +} | |
585 | + | |
586 | +export enum SnmpAuthenticationProtocol { | |
587 | + SHA_1 = 'SHA_1', | |
588 | + SHA_224 = 'SHA_224', | |
589 | + SHA_256 = 'SHA_256', | |
590 | + SHA_384 = 'SHA_384', | |
591 | + SHA_512 = 'SHA_512', | |
592 | + MD5 = 'MD%' | |
593 | +} | |
594 | + | |
595 | +export const SnmpAuthenticationProtocolTranslationMap = new Map<SnmpAuthenticationProtocol, string>([ | |
596 | + [SnmpAuthenticationProtocol.SHA_1, 'SHA-1'], | |
597 | + [SnmpAuthenticationProtocol.SHA_224, 'SHA-224'], | |
598 | + [SnmpAuthenticationProtocol.SHA_256, 'SHA-256'], | |
599 | + [SnmpAuthenticationProtocol.SHA_384, 'SHA-384'], | |
600 | + [SnmpAuthenticationProtocol.SHA_512, 'SHA-512'], | |
601 | + [SnmpAuthenticationProtocol.MD5, 'MD5'] | |
602 | +]); | |
603 | + | |
604 | +export enum SnmpPrivacyProtocol { | |
605 | + DES = 'DES', | |
606 | + AES_128 = 'AES_128', | |
607 | + AES_192 = 'AES_192', | |
608 | + AES_256 = 'AES_256' | |
609 | +} | |
610 | + | |
611 | +export const SnmpPrivacyProtocolTranslationMap = new Map<SnmpPrivacyProtocol, string>([ | |
612 | + [SnmpPrivacyProtocol.DES, 'DES'], | |
613 | + [SnmpPrivacyProtocol.AES_128, 'AES-128'], | |
614 | + [SnmpPrivacyProtocol.AES_192, 'AES-192'], | |
615 | + [SnmpPrivacyProtocol.AES_256, 'AES-256'], | |
616 | +]); | |
617 | + | |
542 | 618 | export interface SnmpDeviceTransportConfiguration { |
543 | - [key: string]: any; | |
619 | + host?: string; | |
620 | + port?: number; | |
621 | + protocolVersion?: SnmpDeviceProtocolVersion; | |
622 | + community?: string; | |
623 | + username?: string; | |
624 | + securityName?: string; | |
625 | + contextName?: string; | |
626 | + authenticationProtocol?: SnmpAuthenticationProtocol; | |
627 | + authenticationPassphrase?: string; | |
628 | + privacyProtocol?: SnmpPrivacyProtocol; | |
629 | + privacyPassphrase?: string; | |
630 | + engineId?: string; | |
544 | 631 | } |
545 | 632 | |
546 | 633 | export type DeviceTransportConfigurations = DefaultDeviceTransportConfiguration & | ... | ... |
... | ... | @@ -49,8 +49,10 @@ export interface DefaultTenantProfileConfiguration { |
49 | 49 | maxRuleNodeExecutionsPerMessage: number; |
50 | 50 | maxEmails: number; |
51 | 51 | maxSms: number; |
52 | + maxCreatedAlarms: number; | |
52 | 53 | |
53 | 54 | defaultStorageTtlDays: number; |
55 | + alarmsTtlDays: number; | |
54 | 56 | } |
55 | 57 | |
56 | 58 | export type TenantProfileConfigurations = DefaultTenantProfileConfiguration; |
... | ... | @@ -81,7 +83,9 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan |
81 | 83 | maxRuleNodeExecutionsPerMessage: 0, |
82 | 84 | maxEmails: 0, |
83 | 85 | maxSms: 0, |
84 | - defaultStorageTtlDays: 0 | |
86 | + maxCreatedAlarms: 0, | |
87 | + defaultStorageTtlDays: 0, | |
88 | + alarmsTtlDays: 0 | |
85 | 89 | }; |
86 | 90 | configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT}; |
87 | 91 | break; | ... | ... |
... | ... | @@ -1297,6 +1297,54 @@ |
1297 | 1297 | "sw-update-recourse": "Software update CoAP recourse", |
1298 | 1298 | "sw-update-recourse-required": "Software update CoAP recourse is required.", |
1299 | 1299 | "config-json-tab": "Json Config Profile Device" |
1300 | + }, | |
1301 | + "snmp": { | |
1302 | + "add-communication-config": "Add communication config", | |
1303 | + "add-mapping": "Add mapping", | |
1304 | + "authentication-passphrase": "Authentication passphrase", | |
1305 | + "authentication-passphrase-required": "Authentication passphrase is required.", | |
1306 | + "authentication-protocol": "Authentication protocol", | |
1307 | + "authentication-protocol-required": "Authentication protocol is required.", | |
1308 | + "communication-configs": "Communication configs", | |
1309 | + "community": "Community string", | |
1310 | + "community-required": "Community string is required.", | |
1311 | + "context-name": "Context name", | |
1312 | + "data-key": "Data key", | |
1313 | + "data-key-required": "Data key is required.", | |
1314 | + "data-type": "Data type", | |
1315 | + "data-type-required": "Data type is required.", | |
1316 | + "engine-id": "Engine ID", | |
1317 | + "host": "Host", | |
1318 | + "host-required": "Host is required.", | |
1319 | + "oid": "OID", | |
1320 | + "oid-pattern": "Invalid OID format.", | |
1321 | + "oid-required": "OID is required.", | |
1322 | + "please-add-communication-config": "Please add communication config", | |
1323 | + "please-add-mapping-config": "Please add mapping config", | |
1324 | + "port": "Port", | |
1325 | + "port-format": "Invalid port format.", | |
1326 | + "port-required": "Port is required.", | |
1327 | + "privacy-passphrase": "Privacy passphrase", | |
1328 | + "privacy-passphrase-required": "Privacy passphrase is required.", | |
1329 | + "privacy-protocol": "Privacy protocol", | |
1330 | + "privacy-protocol-required": "Privacy protocol is required.", | |
1331 | + "protocol-version": "Protocol version", | |
1332 | + "protocol-version-required": "Protocol version is required.", | |
1333 | + "querying-frequency": "Querying frequency, ms", | |
1334 | + "querying-frequency-invalid-format": "Querying frequency must be a positive integer.", | |
1335 | + "querying-frequency-required": "Querying frequency is required.", | |
1336 | + "retries": "Retries", | |
1337 | + "retries-invalid-format": "Retries must be a positive integer.", | |
1338 | + "retries-required": "Retries is required.", | |
1339 | + "scope": "Scope", | |
1340 | + "scope-required": "Scope is required.", | |
1341 | + "security-name": "Security name", | |
1342 | + "security-name-required": "Security name is required.", | |
1343 | + "timeout-ms": "Timeout, ms", | |
1344 | + "timeout-ms-invalid-format": "Timeout must be a positive integer.", | |
1345 | + "timeout-ms-required": "Timeout is required.", | |
1346 | + "user-name": "User name", | |
1347 | + "user-name-required": "User name is required." | |
1300 | 1348 | } |
1301 | 1349 | }, |
1302 | 1350 | "dialog": { |
... | ... | @@ -2309,20 +2357,23 @@ |
2309 | 2357 | }, |
2310 | 2358 | "resource": { |
2311 | 2359 | "add": "Add Resource", |
2360 | + "copyId": "Copy resource Id", | |
2312 | 2361 | "delete": "Delete resource", |
2313 | 2362 | "delete-resource-text": "Be careful, after the confirmation the resource will become unrecoverable.", |
2314 | 2363 | "delete-resource-title": "Are you sure you want to delete the resource '{{resourceTitle}}'?", |
2315 | 2364 | "delete-resources-action-title": "Delete { count, plural, 1 {1 resource} other {# resources} }", |
2316 | 2365 | "delete-resources-text": "Be careful, after the confirmation all selected resources will be removed.", |
2317 | 2366 | "delete-resources-title": "Are you sure you want to delete { count, plural, 1 {1 resource} other {# resources} }?", |
2367 | + "download": "Download resource", | |
2318 | 2368 | "drop-file": "Drop a resource file or click to select a file to upload.", |
2319 | 2369 | "empty": "Resource is empty", |
2320 | - "export": "Export resource", | |
2370 | + "file-name": "File name", | |
2371 | + "idCopiedMessage": "Resource Id has been copied to clipboard", | |
2321 | 2372 | "no-resource-matching": "No resource matching '{{widgetsBundle}}' were found.", |
2322 | 2373 | "no-resource-text": "No resources found", |
2323 | 2374 | "open-widgets-bundle": "Open widgets bundle", |
2324 | 2375 | "resource": "Resource", |
2325 | - "resource-library-details": "Resource library details", | |
2376 | + "resource-library-details": "Resource details", | |
2326 | 2377 | "resource-type": "Resource type", |
2327 | 2378 | "resources-library": "Resources library", |
2328 | 2379 | "search": "Search resources", | ... | ... |
... | ... | @@ -1994,7 +1994,6 @@ |
1994 | 1994 | "delete-resources-title": "确定要删除 { count, plural, 1 {# 个资源} other {# 个资源} }?", |
1995 | 1995 | "drop-file": "拖拽资源文件或单击以选择要上传的文件。", |
1996 | 1996 | "empty": "资源为空", |
1997 | - "export": "导出资源", | |
1998 | 1997 | "no-resource-matching": "找不到与 '{{widgetsBundle}}' 匹配的资源。", |
1999 | 1998 | "no-resource-text": "找不到资源", |
2000 | 1999 | "open-widgets-bundle": "打开部件库", | ... | ... |