Skip to content

Commit 4f3ae86

Browse files
committed
Merge remote-tracking branch 'ce/master'
2 parents 5f9b7ba + 79bb2b0 commit 4f3ae86

66 files changed

Lines changed: 2782 additions & 249 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

application/src/main/data/json/system/widget_bundles/cards.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"cards.html_value_card",
2525
"cards.markdown_card",
2626
"cards.simple_card",
27-
"unread_notifications"
27+
"unread_notifications",
28+
"html_container"
2829
]
2930
}

application/src/main/data/json/system/widget_bundles/html_widgets.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"widgetTypeFqns": [
1212
"cards.html_card",
1313
"cards.html_value_card",
14-
"cards.markdown_card"
14+
"cards.markdown_card",
15+
"html_container"
1516
]
1617
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"fqn": "html_container",
3+
"name": "HTML Container",
4+
"deprecated": false,
5+
"image": "tb-image;/api/images/system/html-container.png",
6+
"description": "Configurable HTML, CSS and JavaScript widget with access to the Widget API via WidgetContext and the ability to load external resources or modules. Supports two modes: plain HTML (regular HTML/JS bound to the widget container) and Angular (Angular template with bound variables and functions). Use for custom complex visualizations or actions when system widgets are not enough.",
7+
"descriptor": {
8+
"type": "static",
9+
"sizeX": 9.5,
10+
"sizeY": 5.5,
11+
"resources": [],
12+
"templateHtml": "<tb-html-container-widget \n [ctx]=\"ctx\">\n</tb-html-container-widget>",
13+
"templateCss": "",
14+
"controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n",
15+
"settingsDirective": "tb-html-container-widget-settings",
16+
"hasBasicMode": true,
17+
"basicModeDirective": "tb-html-container-basic-config",
18+
"defaultConfig": "{\"datasources\":[],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"rgba(255, 255, 255, 0)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{\"type\":\"PLAIN\",\"html\":\"\",\"css\":\"\",\"js\":\"\",\"resources\":[]},\"title\":\"HTML Container\",\"dropShadow\":false,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\"\",\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"configMode\":\"basic\"}"
19+
},
20+
"resources": [
21+
{
22+
"link": "/api/images/system/html-container.png",
23+
"title": "\"HTML Container\" system widget image",
24+
"type": "IMAGE",
25+
"subType": "IMAGE",
26+
"fileName": "html-container.png",
27+
"publicResourceKey": "0CBg8htTwiFsclrm44sIp5pQNS4MM35l",
28+
"mediaType": "image/png",
29+
"data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAYAAABJ/yOpAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAOdEVYdFNvZnR3YXJlAEZpZ21hnrGWYwAADt9JREFUeAHtnU9oHccdx39OnCCVOrYcCESmCKxDITYkUB9aYkNb4tySQ0rsWxzowT600Bwa4h56yMW9tb05t6a3+Bjf7EAP8qEF6RCIAg2VSgtRoCVSHAfbKcbtfrrzy47Wu6NdvSe9Wfn7gZH2zZvdtzsz3/m785t9VrK/cE8W7vHCPWJiUhwI/2+ZmCS3C7dRuHt8QBzfKdwhkzgmzcHgxORAA09YqYn9iGOmcDcL96UJIe5bqQWE8iR/vlW4r0wIEYNIHvcm1X0TQsSgiUfU5xAigQQiRAIJRIgEEogQCSQQIRJIIEIkkECESCCBCJFAAhEigQQiRAIJRIgEEogQCfabEP1gecTZwh2N/FhcdKkl/MVwjrNSuCvhnOxRDSL6csE2i6Mv8+Eag0ACEX1AGDM2Olxj3gaAmliiD4ejY28qbcXl6Phc4WbD8TiEtuMMoQYhQs8XbqrHOVPhnFkTO8lG5LqEuWsDI3eBkMFpr3q7tYtIpqJzJBIxEjkLxMUxFX0+0eG841aJYtokEjECuQqkLg64XrgbHc5dDGEdiURsmxwF0iaO690v8UB4iURsi9wEMg5xtJ0nkYje5CSQcYqj7XyJZDRGmSCs87QNgFwEshPiaLuORLI9GBXsMkiS4nZ0fMoGMFn4qJU2eW/a5NhJcTir4b8nyGOFe7Zwn1hehqI9Dr62PDhduGOF+3E4jiF9PrN+ENexyDgm/Z+yMm1WLS8OTVoguyEOZwgiyU0gpM2cbZ71ZsKP9PmL9ccnDGleTQc/F8e87Uy6j8IhvYsltsMoM+IIY58NhEnXIJTcfy3cc1a9F+Yl/Lir29O2uZlwp3DvFG7N8iG3GgRIBwTxVPhMBj8W/Pu+sk6T6hWrag/gna5lK2tyNbEa2A2RDEEckJtAVoP70MqMHPcfpoJ/H162zc01XmT8wPIUB2TTxCKjEllx1V3P1NtlKOLIHTJw/CbDtI3GouUpik3k1AfZCZFIHONlnPGWvTggt076OEUicYiRyXEUaxwikTjEWMh1mLdNJF1mck+axCHGRM7zIHWRfFq4jzqct2SVGCQOMRK5TxS6SBhiJKN3maByUayaxCFGZAhGG8jg7/Q75f8iuWxiJ2GYt/4KShODMM7QhqyaiD6sR8e8R3cxHKcMx/E+V5NIZDhO7Dlotq7b6HCNFRsAEojoi/fvtov3JwfDnIlcOBicyIM51SBCJJBAhEgggQiRQAIRIoEEIkQCCUSIBBKIEAkkECESSCBCJJBAhEgggQiRIMfX3bG3xFqD+HVoXpe+Y82mZu6E//Vz/DwL/n6Nwe2TlymkE5t6Eqf1eHfD4Gsdw2dLjgJxe71vRn5sXI8dJdakk9HdwBqZnaW4HxfujD1o19f3474UrrFi+dl/HSKeRry2TloQ/+9bmTZvRP689XslET57hrZgylcJIgY3ogwIh5Ip3r/C9/QeTGk1INjO+ZqVhuRcFJgPfd5KuwGIghqdBVWI4WSDP+GzXxOScx9kJnJdoDo/YpXZUkSzaGLcYJmdNPG4pQD6g5U1OdshuKV2CqxfWymMlQb/QSyYyrkGuRAdxwnSBiXTQuGesbIqRyCUWNlv0jIwvB8Y9+V8ARU1OoJ51UoL7r6h6vUQ3v2v20AKr5wFEq9xvtAhPAlHlf9W4b6wMgHGsTxUbMabrPQlXCRYe/cO+WJwNHFft2qZ7kJwhD0brqMm1i5D9U01j+G4JRM7ARmbDP9i+EztTp+EESoy/svB3wUzFb53/5XIP3v2olUTSq8XrEyI+qaTsUnSwa2Nzog/Fu41K2sDYESK+EQ47P/oFjDpl9AZ3wjh3d/3BBkEWpOeD0Nbk942gDJlzTVEm3+uzMkulhiFtiH0uz39s0WvmgiRQAIRIoEEIkQCCUSIBBKIEAkkECESSCBCJJBAhEgggQiRQAIRIoEEIkQCCUSIBLyseMC0q1EuDOlN14eBA6pBhEhADXKrcDdN5ITSIw8OqQYRIoEEIkQCCUSIBBKIEAkkECESSCBCJJBAhEgggQiRQAIRIoEEIkQCCUSIBBKIEAkkECESSCBCJJBAhEgggQiRQAIRIoE20BHjgLX0x63cItp3wWVznbXgBrtXvQQiRgFhsDnncUsbnPDtoAcnFAlEbBffzrmLJZYTITz71g9m8054tHCHTEYCcsEz29eWN6cKd8Y2F7DsXMs+9X8OjqbVvcI9Fb5/rHDPWblV9z9tGBwaSg0yW7iT0Wc2p19sCPM9q9rAbTSdK7pDPL8UfWarZ7aFrjefiGcEw064bA19OPjTJGMv+xUbAEMRCCXriZpfnMlJhAvWvbpHSNojfXuci44RB/GY2r0W4fzeSpHMBj9qn9/aAHa93SvDvGT6PlYJ5619j2/RDvHs8bZuZc3RJZPTrHonnGPhGidsAGgeRPQhztRto1JT1lxYIZIPos/HbADs1VEsSrZPa360m4+b6AuZ/VRwccZfagl7IRxftgdrl+XgR7hZGwB7VSCUVvXSjQT9rBZGpCETN/XtmjrYLg7P+BzXRUKcr4cwDKbQ1Mp6bmSvCuTpBj8SipEVEmiwM7u7SNPAB8Lw2fGYujjMKnHVRRIf02RbbrheNuwVgdSr8pe3CP++lUOQop1YHBQqTPKtNoRrEofTJhLndHAr4frZFVx7pZPOkG+fUohE0V4c7dRHqxiB6isOp62ZFsOo4i8sw35JTjUIY+Px0GufeQratr8r3NFEGCYavZM+HVz24/ATostoFVBTx5magZEj4Xgt+m42hKWWoDDzPgzp7e9xkR7MsWQ1P5KTQEaZmzgazndIhHqiDmLcPRPiTJ96d+pqCEu6vWtlGhyJzqMp+3rhPg/HUH+LAQH6TDvXORX8siDnPkgslq1eHyFhTkefEUd9pEVNqu54XBGPqdLcJwBJK2qM+dr3NMvof6wnrsNvULP48DDzIxJIA0RULIqLibBbDdGeMTEKPlfRpVC5Y+n06NI3XLVM50dy6qT3eXltyUZH8yDt+Csh1Nzz9hCTk0CoVru8ZUu4eqnEeX06dh+ZOugp4n7HC7bzMG/ltVVWcyK59UF8rD1euulQ4i9b83AjzTNGP07a1v0Vwi6YSMEckb9aQg1Ck5VO9k4UKvzG2eizBLIF212rQca/amIcUBhds2rClRFAhIJw1hrCNmXqGdu6eebrfGaia2XTQQctuRVtIAZqYx8dJBO/1BCOAu1KOI6H1k9Y/6H17GbTJRCRwicJ6Ycc7hDe36vqOxKVepVlouwr3Fzh/mEiBw6G/znaCPDJ2PpkLqOP8agitc4xS7/V4Hi/ElHlOGgy980fkQUHrRKJmDxzWlEoRAIJRIgEEogQCSQQIRJIIEIkkECESCCBCJFAAhEigQQiRAIJRIgEEogQCSQQIRJIIEIkmKRAxrk/B9fquwaB8EM0BTRTc5N4Bpbj1uM7vqc9w6QWTBGJmPW5ZKOtIOM6Z62yEs7/y9btmqyzZkFQm2E0VsPdsfw2nSTe4ufDAEXTUmPWbrAu430bPyyTrS+1je+LdLhumS2f3Q65rCj0hTgsvtkInzFjeTf4swhnrSUcm5BeCtd5zcrEI8P43t1uRI7Ph8O1SNwr4b9ffzqEWbZqB6R1y3Ofb+49NpNEae5bPnjJ/oyVAnG7xfX4gPlwHt/dDceHrYoHi87rUlj4fbFYCkNwC+G68+E6cVzW09IS9+h5wZ8TpqPzreUe23637tfKpHa55eGopt16xvOF+8rK2oAdUFkHvT8cv164L62MgHo4IuRE8CMzs7LtEysj7s1w3g/Db5LwP7UykomYV6wS2U/C733fykyF/7NWRuS/g9sNuuxyS9ywNJVnd/vC3PfJ4P9LK3eZdQPUfM/z/DycTzji4l9WGoz+bvj+VDjnQLjedHQevzUf/JdCWH5rrXZfS+EcnuMHhftT4X5k5ZJddrnFCMSH1pzmbuT6llVphv/F8Dw8x89sc5rNhvtw49jz4RmWwm89G57nRSsLCtL8WM3vnrWTxS63NBG4URKNmydRl8MxD0pp9J6ViRGHwzQQAqO2IFLOhO9pUpyyqumxFL7nfDdyDbGpUkqoK1Y1/bAzS8RvWJ77evu2AUCTkqYMmd1LbZ6V5z4cviPsejjmGc9ZZfrIm6QUQMtReBfXuyEcme/kFveFUQfi+Ei4zt1wLd/xi0zsafd3K9PLbZSdDefcCPdxztJbVPwtXJfr/TccT4d7OBru9VJ079yTC+qadeyz5iAQEobmwMdW2bQi4ohYHt6r4NO1cN4kcoF5wiMOakXfTYrMgmgQyVbrnr3K3cq21qSpN7GAhPdapI4PYrj9qXinrVQzg/glTonjLnGCKG+H31qwqsNODeJxz38KLtLzvFUinInCbFjVhGrDw3pNB9509vXwZ6PrWfgdfpfCZM0qayyt7OYoFhn3fDh29XLjZFw2d6T08PaldwApDdxOVlM4qslz0bUoJUlUMoBHEiLbylavJ/7R6PfNhjPKRUZCHIvWXMoTH8TP5eCuWTe8j0KG7lKTes3F771olfHrq+F3KagQmzevfxPOI2297+K/G+8E5oVhk0h9Sze+9z3ZPf3es6qG5fqnwz3wu0esskTfym7WIEQM7c63rawSY3P4KP1O8Pft00hEmgxeIi7UwhEhPPhL4ZpuRJnEIFHYC/1iCEdJkSoBvWk1bVXTgMSmHfuF5WeJ8Xx0vBj95znfslIk3g4nLM0P4vXtEHbBuhnoI1NR+hM3PqDRpdDwtON3yKSvWmlBh3TDGjzNrXNWDRAsRs8VpxncCNdqM5K9Gp71V1Y9GwK9btXI2rpVgnvDqr0SO9mD3m2rJk1Ww7taEm8L56VHk/9WeFVvI9zXuBi3VZP6/W/3ecYxtzEzgt8oeWOmY7gm5ibRB7nb0a/rue4/ynXbwg/dwPW4nmccw9wbI/h1ue/U/iNdwjUiy4p7YDJL7Bx6F0uIBBKIEAkkECESSCBCJJBAhEgggQiRQAIRIoEEIkQCBHLfJBQh6jzif/5j5QISIUTFtwt3G4F8XrgnrBSJahLxsIMG/KXRjX3Bk3eyfL22mBxek98yMSnoctCqouK49z8r21UdWLc82QAAAABJRU5ErkJggg==",
30+
"public": true
31+
}
32+
],
33+
"scada": false,
34+
"tags": [
35+
"html",
36+
"css",
37+
"javascript",
38+
"custom",
39+
"script",
40+
"code",
41+
"container",
42+
"angular",
43+
"template",
44+
"external resources",
45+
"widget api",
46+
"advanced",
47+
"custom visualization",
48+
"custom action",
49+
"web",
50+
"markup"
51+
]
52+
}

application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,6 @@ private CalculatedFieldState createState(CalculatedFieldCtx ctx) {
485485

486486
private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) {
487487
state.setCtx(ctx, actorCtx);
488-
state.init(false);
489488

490489
if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isCfHasRelationPathQuerySource()) {
491490
GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state;
@@ -494,6 +493,7 @@ private void initState(CalculatedFieldState state, CalculatedFieldCtx ctx) {
494493

495494
Map<String, ArgumentEntry> arguments = fetchArguments(ctx);
496495
state.update(arguments, ctx);
496+
state.init(false);
497497

498498
state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize());
499499
states.put(ctx.getCfId(), state);

application/src/main/java/org/thingsboard/server/service/ai/AiChatModelServiceImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.fasterxml.jackson.core.io.JsonStringEncoder;
1919
import com.google.common.util.concurrent.FluentFuture;
20+
import com.google.common.util.concurrent.Futures;
2021
import dev.langchain4j.data.message.ChatMessage;
2122
import dev.langchain4j.data.message.Content;
2223
import dev.langchain4j.data.message.TextContent;
@@ -42,7 +43,12 @@ class AiChatModelServiceImpl implements AiChatModelService {
4243

4344
@Override
4445
public <C extends AiChatModelConfig<C>> FluentFuture<ChatResponse> sendChatRequestAsync(AiChatModelConfig<C> chatModelConfig, ChatRequest chatRequest) {
45-
ChatModel langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
46+
ChatModel langChainChatModel;
47+
try {
48+
langChainChatModel = chatModelConfig.configure(chatModelConfigurer);
49+
} catch (Throwable t) {
50+
return FluentFuture.from(Futures.immediateFailedFuture(t));
51+
}
4652
if (langChainChatModel.provider() == ModelProvider.GITHUB_MODELS) {
4753
chatRequest = prepareGithubChatRequest(chatRequest);
4854
}

application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel;
3838
import org.springframework.http.HttpHeaders;
3939
import org.springframework.stereotype.Component;
40+
import org.thingsboard.common.util.SsrfProtectionValidator;
4041
import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig;
4142
import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig;
4243
import org.thingsboard.server.common.data.ai.model.chat.AzureOpenAiChatModelConfig;
@@ -58,6 +59,7 @@
5859

5960
import java.io.ByteArrayInputStream;
6061
import java.io.IOException;
62+
import java.net.URI;
6163
import java.nio.charset.StandardCharsets;
6264
import java.time.Duration;
6365
import java.util.Base64;
@@ -69,6 +71,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur
6971

7072
@Override
7173
public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
74+
validateBaseUrl(chatModelConfig.providerConfig().baseUrl());
7275
return OpenAiChatModel.builder()
7376
.baseUrl(chatModelConfig.providerConfig().baseUrl())
7477
.apiKey(chatModelConfig.providerConfig().apiKey())
@@ -86,6 +89,7 @@ public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) {
8689
@Override
8790
public ChatModel configureChatModel(AzureOpenAiChatModelConfig chatModelConfig) {
8891
AzureOpenAiProviderConfig providerConfig = chatModelConfig.providerConfig();
92+
validateBaseUrl(providerConfig.endpoint());
8993
return AzureOpenAiChatModel.builder()
9094
.endpoint(providerConfig.endpoint())
9195
.serviceVersion(providerConfig.serviceVersion())
@@ -273,6 +277,7 @@ public ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig)
273277

274278
@Override
275279
public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
280+
validateBaseUrl(chatModelConfig.providerConfig().baseUrl());
276281
var builder = OllamaChatModel.builder()
277282
.baseUrl(chatModelConfig.providerConfig().baseUrl())
278283
.modelName(chatModelConfig.modelId())
@@ -300,6 +305,10 @@ public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) {
300305
return builder.build();
301306
}
302307

308+
private static void validateBaseUrl(String url) {
309+
SsrfProtectionValidator.validateUri(URI.create(url));
310+
}
311+
303312
private static Duration toDuration(Integer timeoutSeconds) {
304313
return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null;
305314
}

application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ private AlarmRuleState initRuleState(AlarmSeverity severity, AlarmRule rule, Ala
179179
ruleState.setActive(null);
180180
AlarmCondition condition = rule.getCondition();
181181
if (condition.hasSchedule() || (condition.getType() == AlarmConditionType.DURATION && !ruleState.isEmpty())) {
182+
ruleState.cancelDurationCheckFuture();
182183
reevalNeeded.set(true);
183184
}
184185
}

application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,7 @@ private void clearDurationConditionState() {
256256
firstEventTs = 0L;
257257
lastCheckTs = 0L;
258258
duration = 0L;
259-
if (durationCheckFuture != null) {
260-
durationCheckFuture.cancel(true);
261-
durationCheckFuture = null;
262-
}
259+
cancelDurationCheckFuture();
263260
}
264261

265262
public void setDurationCheckFuture(ScheduledFuture<?> durationCheckFuture) {
@@ -270,6 +267,13 @@ public void setDurationCheckFuture(ScheduledFuture<?> durationCheckFuture) {
270267
this.durationCheckFuture = durationCheckFuture;
271268
}
272269

270+
public void cancelDurationCheckFuture() {
271+
if (durationCheckFuture != null) {
272+
durationCheckFuture.cancel(true);
273+
durationCheckFuture = null;
274+
}
275+
}
276+
273277
public boolean isEmpty() {
274278
return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null;
275279
}

application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ public Path getWidgetTypesDir() {
135135
return Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_TYPES_DIR);
136136
}
137137

138+
public Path getWidgetBundlesDir() {
139+
return Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
140+
}
141+
138142
public String getDataDir() {
139143
if (!StringUtils.isEmpty(dataDir)) {
140144
if (!Paths.get(this.dataDir).toFile().isDirectory()) {
@@ -207,7 +211,7 @@ public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePat
207211
public void loadSystemWidgets() {
208212
log.info("Loading system widgets");
209213
Map<Path, JsonNode> widgetsBundlesMap = new HashMap<>();
210-
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
214+
Path widgetBundlesDir = getWidgetBundlesDir();
211215
try (Stream<Path> dirStream = listDir(widgetBundlesDir).filter(path -> path.toString().endsWith(JSON_EXT))) {
212216
dirStream.forEach(
213217
path -> {

0 commit comments

Comments
 (0)