Browse Source

Merge remote-tracking branch 'upstream/rc' into merge/05-05-26-1/rc

pull/15580/head
Vladyslav_Prykhodko 4 weeks ago
parent
commit
d8bad39a5c
  1. 2
      application/src/main/data/json/system/widget_types/html_container.json
  2. 1
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java
  3. 12
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmRuleState.java
  4. 2
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java
  5. 13
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java
  6. 51
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  7. 111
      common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportServiceTest.java
  8. 4
      dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java
  9. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java
  10. 25
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  11. 15
      ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.models.ts
  12. 162
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.html
  13. 114
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.scss
  14. 63
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.ts
  15. 120
      ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts
  16. 6
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts

2
application/src/main/data/json/system/widget_types/html_container.json

@ -11,7 +11,7 @@
"resources": [],
"templateHtml": "<tb-html-container-widget \n [ctx]=\"ctx\">\n</tb-html-container-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n \n}\n",
"controllerScript": "self.onInit = function() {\n \n}\n\nself.typeParameters = function() {\n return {\n previewWidth: '100%',\n previewHeight: '100%',\n overflowVisible: true\n };\n};\n",
"settingsDirective": "tb-html-container-widget-settings",
"hasBasicMode": true,
"basicModeDirective": "tb-html-container-basic-config",

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

@ -179,6 +179,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState {
ruleState.setActive(null);
AlarmCondition condition = rule.getCondition();
if (condition.hasSchedule() || (condition.getType() == AlarmConditionType.DURATION && !ruleState.isEmpty())) {
ruleState.cancelDurationCheckFuture();
reevalNeeded.set(true);
}
}

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

@ -256,10 +256,7 @@ public class AlarmRuleState {
firstEventTs = 0L;
lastCheckTs = 0L;
duration = 0L;
if (durationCheckFuture != null) {
durationCheckFuture.cancel(true);
durationCheckFuture = null;
}
cancelDurationCheckFuture();
}
public void setDurationCheckFuture(ScheduledFuture<?> durationCheckFuture) {
@ -270,6 +267,13 @@ public class AlarmRuleState {
this.durationCheckFuture = durationCheckFuture;
}
public void cancelDurationCheckFuture() {
if (durationCheckFuture != null) {
durationCheckFuture.cancel(true);
durationCheckFuture = null;
}
}
public boolean isEmpty() {
return eventCount == 0L && firstEventTs == 0L && lastCheckTs == 0L && durationCheckFuture == null;
}

2
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArg.java

@ -73,7 +73,7 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
throw new IllegalArgumentException("Rolling argument values are empty.");
}
double max = Double.MIN_VALUE;
double max = -Double.MAX_VALUE;
for (TbelCfTsDoubleVal value : values) {
double val = value.getValue();
if (!ignoreNaN && Double.isNaN(val)) {

13
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbelCfTsRollingArgTest.java

@ -57,6 +57,19 @@ public class TbelCfTsRollingArgTest {
assertThat(rollingArg.max(false)).isNaN();
}
@Test
void testMaxOverAllNegativeValues() {
TbelCfTsRollingArg negativeArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10),
List.of(
new TbelCfTsDoubleVal(ts - 10, -50.0),
new TbelCfTsDoubleVal(ts - 20, -100.0),
new TbelCfTsDoubleVal(ts - 30, -75.0)
)
);
assertThat(negativeArg.max()).isEqualTo(-50.0);
}
@Test
void testMin() {
assertThat(rollingArg.min()).isEqualTo(2.0);

51
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -78,22 +78,47 @@ public class MqttTransportService implements TbTransportService {
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.valueOf(leakDetectorLevel.toUpperCase()));
log.info("Starting MQTT transport...");
bossGroup = new NioEventLoopGroup(bossGroupThreadCount);
workerGroup = new NioEventLoopGroup(workerGroupThreadCount);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(context, false))
.childOption(ChannelOption.SO_KEEPALIVE, keepAlive);
serverChannel = b.bind(host, port).sync().channel();
if (sslEnabled) {
b = new ServerBootstrap();
try {
bossGroup = new NioEventLoopGroup(bossGroupThreadCount);
workerGroup = new NioEventLoopGroup(workerGroupThreadCount);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(context, true))
.childHandler(new MqttTransportServerInitializer(context, false))
.childOption(ChannelOption.SO_KEEPALIVE, keepAlive);
sslServerChannel = b.bind(sslHost, sslPort).sync().channel();
serverChannel = b.bind(host, port).sync().channel();
if (sslEnabled) {
b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(context, true))
.childOption(ChannelOption.SO_KEEPALIVE, keepAlive);
sslServerChannel = b.bind(sslHost, sslPort).sync().channel();
}
} catch (Exception e) {
log.error("Failed to start MQTT transport, releasing resources", e);
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
try {
if (serverChannel != null) {
serverChannel.close().sync();
}
if (sslServerChannel != null) {
sslServerChannel.close().sync();
}
} catch (Exception suppressed) {
e.addSuppressed(suppressed);
} finally {
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
}
throw e;
}
log.info("Mqtt transport started!");
}

111
common/transport/mqtt/src/test/java/org/thingsboard/server/transport/mqtt/MqttTransportServiceTest.java

@ -0,0 +1,111 @@
/**
* Copyright © 2016-2026 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.transport.mqtt;
import io.netty.channel.Channel;
import io.netty.channel.EventLoopGroup;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
import static org.mockito.Mockito.mock;
public class MqttTransportServiceTest {
private static final String HOST = "127.0.0.1";
private MqttTransportService service;
private ServerSocket occupiedSocket;
private int occupiedPort;
@BeforeEach
public void setUp() throws Exception {
occupiedSocket = new ServerSocket(0, 50, InetAddress.getByName(HOST));
occupiedPort = occupiedSocket.getLocalPort();
service = new MqttTransportService();
ReflectionTestUtils.setField(service, "host", HOST);
ReflectionTestUtils.setField(service, "port", occupiedPort);
ReflectionTestUtils.setField(service, "sslEnabled", false);
ReflectionTestUtils.setField(service, "sslHost", HOST);
ReflectionTestUtils.setField(service, "sslPort", 0);
ReflectionTestUtils.setField(service, "leakDetectorLevel", "DISABLED");
ReflectionTestUtils.setField(service, "bossGroupThreadCount", 1);
ReflectionTestUtils.setField(service, "workerGroupThreadCount", 1);
ReflectionTestUtils.setField(service, "keepAlive", true);
ReflectionTestUtils.setField(service, "context", mock(MqttTransportContext.class));
}
@AfterEach
public void tearDown() throws Exception {
if (occupiedSocket != null && !occupiedSocket.isClosed()) {
occupiedSocket.close();
}
}
@Test
public void whenPlainBindFails_thenInitThrowsAndReleasesNettyResources() {
assertThatThrownBy(() -> service.init())
.isInstanceOf(BindException.class);
EventLoopGroup boss = (EventLoopGroup) ReflectionTestUtils.getField(service, "bossGroup");
EventLoopGroup worker = (EventLoopGroup) ReflectionTestUtils.getField(service, "workerGroup");
assertThat(boss).isNotNull();
assertThat(worker).isNotNull();
assertThat(boss.isShuttingDown()).isTrue();
assertThat(worker.isShuttingDown()).isTrue();
await().atMost(30, TimeUnit.SECONDS).until(boss::isTerminated);
await().atMost(30, TimeUnit.SECONDS).until(worker::isTerminated);
}
@Test
public void whenSslBindFailsAfterPlainBound_thenInitThrowsAndClosesPlainChannelAndReleasesNettyResources() {
ReflectionTestUtils.setField(service, "port", 0);
ReflectionTestUtils.setField(service, "sslEnabled", true);
ReflectionTestUtils.setField(service, "sslPort", occupiedPort);
assertThatThrownBy(() -> service.init())
.isInstanceOf(BindException.class);
Channel serverChannel = (Channel) ReflectionTestUtils.getField(service, "serverChannel");
Channel sslServerChannel = (Channel) ReflectionTestUtils.getField(service, "sslServerChannel");
EventLoopGroup boss = (EventLoopGroup) ReflectionTestUtils.getField(service, "bossGroup");
EventLoopGroup worker = (EventLoopGroup) ReflectionTestUtils.getField(service, "workerGroup");
assertThat(serverChannel).isNotNull();
assertThat(sslServerChannel).isNull();
assertThat(boss).isNotNull();
assertThat(worker).isNotNull();
await().atMost(10, TimeUnit.SECONDS).until(() -> !serverChannel.isOpen());
assertThat(boss.isShuttingDown()).isTrue();
assertThat(worker.isShuttingDown()).isTrue();
await().atMost(30, TimeUnit.SECONDS).until(boss::isTerminated);
await().atMost(30, TimeUnit.SECONDS).until(worker::isTerminated);
}
}

4
dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java

@ -61,8 +61,10 @@ public interface TsKvRepository extends JpaRepository<TsKvEntity, TsKvCompositeK
@Param("startTs") long startTs,
@Param("endTs") long endTs);
// -1.7976931348623157E308 = -Double.MAX_VALUE — the most negative finite double, used as a "less than any value" sentinel for MAX.
// Double.MIN_VALUE is +4.9E-324 (smallest positive), which would beat any negative real value and corrupt MAX.
@Query("SELECT new TsKvEntity(MAX(COALESCE(tskv.longValue, -9223372036854775807)), " +
"MAX(COALESCE(tskv.doubleValue, java.lang.Double.MIN_VALUE)), " +
"MAX(COALESCE(tskv.doubleValue, -1.7976931348623157E308)), " +
"SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " +
"SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " +
"'MAX', MAX(tskv.ts)) FROM TsKvEntity tskv " +

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/AggregatePartitionsFunction.java

@ -277,7 +277,7 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu
private Optional<TsKvEntry> processMinOrMaxResult(AggregationResult aggResult) {
if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) {
if (aggResult.hasDouble) {
double currentD = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.dValue).orElse(Double.MAX_VALUE) : Optional.ofNullable(aggResult.dValue).orElse(Double.MIN_VALUE);
double currentD = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.dValue).orElse(Double.MAX_VALUE) : Optional.ofNullable(aggResult.dValue).orElse(-Double.MAX_VALUE);
double currentL = aggregation == Aggregation.MIN ? Optional.ofNullable(aggResult.lValue).orElse(Long.MAX_VALUE) : Optional.ofNullable(aggResult.lValue).orElse(Long.MIN_VALUE);
return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.MIN ? Math.min(currentD, currentL) : Math.max(currentD, currentL))));
} else {

25
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -710,6 +710,31 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
assertEquals(java.util.Optional.of(2L), list.get(2).getLongValue());
}
@Test
public void testFindDeviceMaxAggregationOverNegativeMixedLongAndDoubleTsData() throws Exception {
save(deviceId, 5000, -100L);
save(deviceId, 15000, -50.0);
List<TsKvEntry> list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
60000, 60000, 1, Aggregation.MAX))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(1, list.size());
assertEquals(java.util.Optional.of(-50.0), list.get(0).getDoubleValue());
}
@Test
public void testFindDeviceMaxAggregationOverAllNegativeDoubleTsData() throws Exception {
save(deviceId, 5000, -50.0);
save(deviceId, 15000, -100.0);
save(deviceId, 25000, -75.0);
List<TsKvEntry> list = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery(LONG_KEY, 0,
60000, 60000, 1, Aggregation.MAX))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(1, list.size());
assertEquals(java.util.Optional.of(-50.0), list.get(0).getDoubleValue());
}
@Test
public void testSaveTs_RemoveTs_AndSaveTsAgain() throws Exception {
save(deviceId, 2000000L, 95);

15
ui-ngx/src/app/modules/home/components/widget/lib/html/html-container-widget.models.ts

@ -51,13 +51,18 @@ const containerFunctionCompletions: TbEditorCompletions = {
type: widgetContextCompletions.ctx.type,
description: widgetContextCompletions.ctx.description,
children: widgetContextCompletions.ctx.children
},
}
}
};
export const AngularContainerFunctionEditorCompleter = new TbEditorCompleter(containerFunctionCompletions);
export const HTMLContainerFunctionEditorCompleter = new TbEditorCompleter(
{...containerFunctionCompletions,
container: {
meta: 'argument',
type: 'HTMLElement',
description: 'Container element of the widget'
},
}
};
}}
);
export const ContainerFunctionEditorCompleter = new TbEditorCompleter(containerFunctionCompletions);

162
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.html

@ -17,82 +17,112 @@
-->
<ng-container [formGroup]="htmlContainerSettingsForm">
<div class="tb-form-panel no-padding no-border relative h-full">
<div class="flex flex-row items-center gap-4">
<div class="tb-form-panel-title" translate>widgets.html-container.container-type</div>
<div class="flex flex-row items-center">
<tb-toggle-select formControlName="type">
<tb-toggle-option [value]="HtmlContainerWidgetType.PLAIN">{{ 'widgets.html-container.type-plain' | translate }}</tb-toggle-option>
<tb-toggle-option [value]="HtmlContainerWidgetType.ANGULAR">{{ 'widgets.html-container.type-angular' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<mat-tab-group [mat-stretch-tabs]="false" selectedIndex="3" class="flex-1">
<mat-tab #resourceTab="matTab">
<ng-template mat-tab-label>
<div [matBadge]="resourcesFormArray.length" [matBadgeHidden]="resourceTab.isActive || !resourcesFormArray.length"
matBadgeSize="small">{{ 'widgets.html-container.resources' | translate }}</div>
</ng-template>
<div class="flex flex-col gap-2 pt-4">
@if (resourcesFormArray.length) {
@for (resourceControl of resourcesControls; track resourceControl; let i = $index) {
<div class="tb-form-row no-border no-padding" [formGroup]="resourceControl">
<tb-resource-autocomplete class="flex-1"
formControlName="url"
inlineField
hideRequiredMarker required
[allowAutocomplete]="resourceControl.get('isModule').value && htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
@if (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR) {
<mat-checkbox formControlName="isModule">
{{ 'widget.resource-is-extension' | translate }}
</mat-checkbox>
<div class="tb-html-container-settings-panel flex flex-1 flex-col" tb-fullscreen [fullscreen]="fullscreen">
<div class="tb-action-expand-button flex flex-row items-center justify-end">
<button mat-stroked-button
matTooltip="{{ 'widget.toggle-fullscreen' | translate }}"
matTooltipPosition="above"
(click)="toggleFullScreen()">
<mat-icon>{{ fullscreen ? 'fullscreen_exit' : 'fullscreen' }}</mat-icon>
<span>{{ (fullscreen ? 'fullscreen.exit' : 'fullscreen.fullscreen') | translate }}</span>
</button>
</div>
<div class="flex flex-1">
<mat-tab-group #leftPanel
[mat-stretch-tabs]="fullscreen"
[selectedIndex]="fullscreen ? 2 : 3"
[animationDuration]="tabsAnimationDuration"
[disablePagination]="fullscreen"
class="tb-content"
[class.flex-1]="!fullscreen">
<mat-tab #resourceTab="matTab">
<ng-template mat-tab-label>
<div [matBadge]="resourcesFormArray.length" [matBadgeHidden]="resourceTab.isActive || !resourcesFormArray.length"
matBadgeSize="small">{{ 'widgets.html-container.resources' | translate }}</div>
</ng-template>
<div class="flex flex-col gap-2 pt-4" [class.px-2]="fullscreen">
@if (resourcesFormArray.length) {
@for (resourceControl of resourcesControls; track resourceControl; let i = $index) {
<div class="tb-form-row no-border no-padding" [formGroup]="resourceControl">
<tb-resource-autocomplete class="flex-1"
formControlName="url"
inlineField
hideRequiredMarker required
[allowAutocomplete]="resourceControl.get('isModule').value && htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR"
placeholder="{{ 'widget.resource-url' | translate }}">
</tb-resource-autocomplete>
@if (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR) {
<mat-checkbox formControlName="isModule">
{{ 'widget.resource-is-extension' | translate }}
</mat-checkbox>
}
<button mat-icon-button color="primary"
(click)="removeResource(i)"
matTooltip="{{'widget.remove-resource' | translate}}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
</button>
</div>
}
<button mat-icon-button color="primary"
(click)="removeResource(i)"
matTooltip="{{'widget.remove-resource' | translate}}"
} @else {
<span translate
class="tb-prompt flex items-center justify-center">widgets.html-container.no-resources</span>
}
<div>
<button mat-raised-button color="primary"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<mat-icon>delete</mat-icon>
<span translate>action.add</span>
</button>
</div>
}
} @else {
<span translate
class="tb-prompt flex items-center justify-center">widgets.html-container.no-resources</span>
</div>
</mat-tab>
<mat-tab label="{{ 'widgets.html-container.css' | translate }}">
<tb-css class="flex-1"
[fillHeight]="true"
formControlName="css"
label="{{ 'widgets.html-container.css' | translate }}">
</tb-css>
</mat-tab>
<mat-tab label="{{ (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? 'widgets.html-container.angular-html-template' : 'widgets.html-container.html') | translate }}">
<tb-html class="flex-1"
[fillHeight]="true"
formControlName="html"
label="{{ (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? 'widgets.html-container.angular-html-template' : 'widgets.html-container.html') | translate }}">
</tb-html>
</mat-tab>
@if (!fullscreen) {
<mat-tab label="{{ 'widgets.html-container.java-script' | translate }}">
<ng-container *ngTemplateOutlet="javascript"></ng-container>
</mat-tab>
}
</mat-tab-group>
<div #rightPanel class="tb-content flex" [class.!hidden]="!fullscreen">
@if (fullscreen) {
<ng-container *ngTemplateOutlet="javascript"></ng-container>
}
<div>
<button mat-raised-button color="primary"
(click)="addResource()"
matTooltip="{{'widget.add-resource' | translate}}"
matTooltipPosition="above">
<span translate>action.add</span>
</button>
</div>
</div>
</mat-tab>
<mat-tab label="{{ (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? 'widgets.html-container.angular-html-template' : 'widgets.html-container.html') | translate }}">
<tb-html class="flex-1"
[fillHeight]="true"
formControlName="html"
label="{{ (htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? 'widgets.html-container.angular-html-template' : 'widgets.html-container.html') | translate }}">
</tb-html>
</mat-tab>
<mat-tab label="{{ 'widgets.html-container.css' | translate }}">
<tb-css class="flex-1"
[fillHeight]="true"
formControlName="css"
label="{{ 'widgets.html-container.css' | translate }}">
</tb-css>
</mat-tab>
<mat-tab label="{{ 'widgets.html-container.java-script' | translate }}">
<tb-js-func class="flex-1"
[fillHeight]="true"
formControlName="js"
[globalVariables]="functionScopeVariables"
[editorCompleter]="containerFunctionEditorCompleter"
[functionArgs]="htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? ['ctx'] : ['ctx', 'container']"
withModules
functionTitle="{{ 'widgets.html-container.js-function' | translate }}">
</tb-js-func>
</mat-tab>
</mat-tab-group>
</div>
</div>
</div>
</ng-container>
<ng-template #javascript>
<ng-container [formGroup]="htmlContainerSettingsForm">
<tb-js-func class="flex-1"
[fillHeight]="true"
formControlName="js"
[globalVariables]="functionScopeVariables"
[editorCompleter]="containerFunctionEditorCompleter"
[functionArgs]="htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR ? ['ctx'] : ['ctx', 'container']"
withModules
functionTitle="{{ 'widgets.html-container.js-function' | translate }}">
</tb-js-func>
</ng-container>
</ng-template>

114
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.scss

@ -14,15 +14,115 @@
* limitations under the License.
*/
:host {
&.tb-html-container-settings {
.tb-html-container-settings {
height: 100%;
}
.tb-html-container-settings .tb-html-container-settings-panel, .tb-html-container-settings-panel {
position: relative;
background: #fff;
.mat-mdc-tab-body-wrapper {
position: relative;
top: 0;
flex: 1;
}
.tb-action-expand-button {
position: absolute;
top: 4px;
right: 0;
z-index: 2;
}
.gutter {
display: none;
background-color: #eee;
background-repeat: no-repeat;
background-position: 50%;
&.gutter-horizontal {
cursor: col-resize;
background-image: url("../../../../../../../../../assets/split.js/grips/vertical.png");
}
}
.tb-js-func {
&:not(.tb-fullscreen) {
&.tb-hide-brackets {
padding-bottom: 0;
}
}
}
.tb-html {
position: relative;
&:not(.tb-fullscreen) {
padding-bottom: 0;
}
.tb-html-toolbar {
position: absolute;
top: 0;
right: 8px;
z-index: 8;
.tb-title {
display: none;
}
}
.tb-html-content-panel {
border-top: none;
height: 100%;
}
}
.tb-css {
position: relative;
&:not(.tb-fullscreen) {
.tb-css-content-panel {
margin: 0;
}
}
.tb-css-toolbar {
position: absolute;
top: 0;
right: 8px;
z-index: 8;
.tb-title {
display: none;
}
}
.tb-css-content-panel {
border-top: none;
height: 100%;
::ng-deep {
.mat-mdc-tab-body-wrapper {
position: relative;
top: 0;
flex: 1;
}
}
&.tb-fullscreen {
padding: 8px;
gap: 8px;
.tb-action-expand-button {
position: relative;
top: 0;
right: 0;
}
.gutter {
display: block;
}
.tb-content {
border: 1px solid #c0c0c0;
.tb-html {
.tb-html-content-panel {
border: none;
}
}
.tb-css {
.tb-css-content-panel {
border: none;
}
}
.tb-js-func {
padding-top: 8px;
.tb-js-func-toolbar {
padding: 0 5px;
}
.tb-js-func-panel {
border-left: none;
border-right: none;
border-bottom: none;
}
}
}
}
}

63
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/html/html-container-settings.component.ts

@ -14,7 +14,18 @@
/// limitations under the License.
///
import { Component, DestroyRef, forwardRef, HostBinding, Input, OnInit } from '@angular/core';
import {
AfterViewInit,
Component,
DestroyRef,
ElementRef,
forwardRef,
HostBinding,
Input,
OnInit,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { WidgetResource } from '@shared/models/widget.models';
import {
ControlValueAccessor,
@ -28,7 +39,8 @@ import {
Validators
} from '@angular/forms';
import {
ContainerFunctionEditorCompleter,
AngularContainerFunctionEditorCompleter,
HTMLContainerFunctionEditorCompleter,
HtmlContainerWidgetSettings,
HtmlContainerWidgetType
} from '@home/components/widget/lib/html/html-container-widget.models';
@ -52,23 +64,39 @@ import { WidgetService } from '@core/http/widget.service';
multi: true,
}
],
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class HtmlContainerSettingsComponent implements OnInit, ControlValueAccessor, Validator {
export class HtmlContainerSettingsComponent implements OnInit, AfterViewInit, ControlValueAccessor, Validator {
HtmlContainerWidgetType = HtmlContainerWidgetType;
functionScopeVariables = this.widgetService.getWidgetScopeVariables();
containerFunctionEditorCompleter = ContainerFunctionEditorCompleter;
get containerFunctionEditorCompleter() {
return this.htmlContainerSettingsForm.get('type').value === HtmlContainerWidgetType.ANGULAR
? AngularContainerFunctionEditorCompleter
: HTMLContainerFunctionEditorCompleter;
}
@HostBinding('class')
hostClass = 'tb-html-container-settings';
@ViewChild('leftPanel', { read: ElementRef })
leftPanelElmRef!: ElementRef;
@ViewChild('rightPanel', { read: ElementRef })
rightPanelElmRef!: ElementRef;
@Input()
disabled: boolean;
fullscreen = false;
tabsAnimationDuration = '500ms';
htmlContainerSettingsForm: UntypedFormGroup;
private modelValue: HtmlContainerWidgetSettings;
constructor(private fb: UntypedFormBuilder,
@ -102,11 +130,26 @@ export class HtmlContainerSettingsComponent implements OnInit, ControlValueAcces
});
}
ngAfterViewInit(): void {
if (this.leftPanelElmRef && this.rightPanelElmRef) {
this.initSplitLayout(this.leftPanelElmRef.nativeElement,
this.rightPanelElmRef.nativeElement);
}
}
private initSplitLayout(leftPanel: any, rightPanel: any) {
Split([leftPanel, rightPanel], {
sizes: [50, 50],
gutterSize: 8,
cursor: 'col-resize'
});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(_fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
@ -150,7 +193,15 @@ export class HtmlContainerSettingsComponent implements OnInit, ControlValueAcces
this.resourcesFormArray.removeAt(index);
}
private propagateChange = (v: any) => { };
toggleFullScreen(): void {
this.fullscreen = !this.fullscreen;
this.tabsAnimationDuration = '0ms';
setTimeout(() => {
this.tabsAnimationDuration = '500ms';
});
}
private propagateChange = (_v: any) => { };
private updateModel() {
this.modelValue = this.htmlContainerSettingsForm.value;

120
ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts

@ -27,8 +27,10 @@ import {
import {
additionalMapDataSourcesToDatasources,
BaseMapSettings,
latestMapDataLayerTypes,
MapDataLayerSettings,
MapDataLayerType,
mapDataLayerTypes,
MapDataSourceSettings,
mapDataSourceSettingsToDatasource,
MapType
@ -46,30 +48,21 @@ interface MapDataLayerDsInfo extends AliasFilterPair {
type ExportDataSourceInfo = {[dataLayerIndex: number]: MapDataLayerDsInfo};
interface MapDatasourcesInfo {
trips?: ExportDataSourceInfo;
markers?: ExportDataSourceInfo;
polygons?: ExportDataSourceInfo;
circles?: ExportDataSourceInfo;
type MapDatasourcesInfo = {
[K in MapDataLayerType]?: ExportDataSourceInfo;
} & {
additionalDataSources?: ExportDataSourceInfo;
}
};
export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
testWidget(widget: Widget): boolean {
if (widget?.config?.settings) {
const settings = widget.config.settings;
if (settings.mapType && [MapType.image, MapType.geoMap].includes(settings.mapType)) {
if (settings.trips && Array.isArray(settings.trips)) {
return true;
}
if (settings.markers && Array.isArray(settings.markers)) {
return true;
}
if (settings.polygons && Array.isArray(settings.polygons)) {
return true;
}
if (settings.circles && Array.isArray(settings.circles)) {
return true;
for (const layerType of mapDataLayerTypes) {
if (Array.isArray(settings[layerType])) {
return true;
}
}
}
}
@ -78,17 +71,11 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
prepareExportInfo(dashboard: Dashboard, widget: Widget): MapDatasourcesInfo {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
const info: MapDatasourcesInfo = {};
if (settings.trips?.length) {
info.trips = prepareExportDataSourcesInfo(dashboard, settings.trips);
}
if (settings.markers?.length) {
info.markers = prepareExportDataSourcesInfo(dashboard, settings.markers);
}
if (settings.polygons?.length) {
info.polygons = prepareExportDataSourcesInfo(dashboard, settings.polygons);
}
if (settings.circles?.length) {
info.circles = prepareExportDataSourcesInfo(dashboard, settings.circles);
for (const layerType of mapDataLayerTypes) {
const dataLayerSettings = settings[layerType];
if (dataLayerSettings?.length) {
info[layerType] = prepareExportDataSourcesInfo(dashboard, dataLayerSettings);
}
}
if (settings.additionalDataSources?.length) {
info.additionalDataSources = prepareExportDataSourcesInfo(dashboard, settings.additionalDataSources);
@ -96,59 +83,36 @@ export const MapModelDefinition: WidgetModelDefinition<MapDatasourcesInfo> = {
return info;
},
updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: MapDatasourcesInfo): void {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
if (info?.trips) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.trips, info.trips);
}
if (info?.markers) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.markers, info.markers);
}
if (info?.polygons) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.polygons, info.polygons);
}
if (info?.circles) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.circles, info.circles);
}
if (info?.additionalDataSources) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources);
if (info && Object.keys(info).length) {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
for (const layerType of mapDataLayerTypes) {
const layerInfo = info[layerType];
const dataLayerSettings = settings[layerType];
if (layerInfo && dataLayerSettings) {
updateMapDatasourceFromExportInfo(entityAliases, filters, dataLayerSettings, layerInfo);
}
}
if (info.additionalDataSources) {
updateMapDatasourceFromExportInfo(entityAliases, filters, settings.additionalDataSources, info.additionalDataSources);
}
}
},
datasources(widget: Widget): Datasource[] {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
const datasources: Datasource[] = [];
if (settings.trips?.length) {
datasources.push(...getMapDataLayersDatasources(settings.trips));
}
if (settings.markers?.length) {
datasources.push(...getMapDataLayersDatasources(settings.markers));
}
if (settings.polygons?.length) {
datasources.push(...getMapDataLayersDatasources(settings.polygons));
}
if (settings.circles?.length) {
datasources.push(...getMapDataLayersDatasources(settings.circles));
}
if (settings.additionalDataSources?.length) {
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));
}
return datasources;
return getMapDataLayersDatasources(widget.config.settings as BaseMapSettings, mapDataLayerTypes);
},
hasTimewindow(widget: Widget): boolean {
const settings: BaseMapSettings = widget.config.settings as BaseMapSettings;
if (settings.trips?.length) {
const timeSeriesDataLayerTypes = mapDataLayerTypes.filter(t => !latestMapDataLayerTypes.includes(t));
if (timeSeriesDataLayerTypes.some(layerType => settings[layerType]?.length)) {
return true;
} else {
const datasources: Datasource[] = getMapLatestDataLayersDatasources(settings);
return datasourcesHasAggregation(datasources);
}
return datasourcesHasAggregation(getMapDataLayersDatasources(settings, latestMapDataLayerTypes, true));
},
datasourcesHasAggregation(widget: Widget): boolean {
const datasources: Datasource[] = getMapLatestDataLayersDatasources(widget.config.settings as BaseMapSettings);
return datasourcesHasAggregation(datasources);
return datasourcesHasAggregation(getMapDataLayersDatasources(widget.config.settings as BaseMapSettings, latestMapDataLayerTypes, true));
},
datasourcesHasOnlyComparisonAggregation(widget: Widget): boolean {
const datasources: Datasource[] = getMapLatestDataLayersDatasources(widget.config.settings as BaseMapSettings);
return datasourcesHasOnlyComparisonAggregation(datasources);
return datasourcesHasOnlyComparisonAggregation(getMapDataLayersDatasources(widget.config.settings as BaseMapSettings, latestMapDataLayerTypes, true));
}
};
@ -236,7 +200,7 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource
}
}
const getMapDataLayersDatasources = (settings: MapDataLayerSettings[],
const getMapDataLayerDatasources = (settings: MapDataLayerSettings[],
includeDataKeys = false, dataLayerType?: MapDataLayerType): Datasource[] => {
const datasources: Datasource[] = [];
settings.forEach((dsSettings) => {
@ -255,16 +219,14 @@ const getMapDataLayersDatasources = (settings: MapDataLayerSettings[],
return datasources;
};
const getMapLatestDataLayersDatasources = (settings: BaseMapSettings): Datasource[] => {
const getMapDataLayersDatasources = (settings: BaseMapSettings,
layerTypes: readonly MapDataLayerType[], includeDataKeys = false): Datasource[] => {
const datasources: Datasource[] = [];
if (settings.markers?.length) {
datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers'));
}
if (settings.polygons?.length) {
datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons'));
}
if (settings.circles?.length) {
datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles'));
for (const layerType of layerTypes) {
const dataLayerSettings = settings[layerType];
if (dataLayerSettings?.length) {
datasources.push(...getMapDataLayerDatasources(dataLayerSettings, includeDataKeys, layerType));
}
}
if (settings.additionalDataSources?.length) {
datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources));

6
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -200,9 +200,11 @@ export const defaultBaseDataLayerSettings = (mapType: MapType): Partial<MapDataL
}
})
export type MapDataLayerType = 'trips' | 'markers' | 'polygons' | 'circles' | 'polylines';
export const mapDataLayerTypes = ['trips', 'markers', 'polygons', 'circles', 'polylines'] as const;
export const mapDataLayerTypes: MapDataLayerType[] = ['trips', 'markers', 'polygons', 'circles', 'polylines'];
export type MapDataLayerType = typeof mapDataLayerTypes[number];
export const latestMapDataLayerTypes: MapDataLayerType[] = ['markers', 'polygons', 'circles', 'polylines'];
export const mapDataLayerValid = (dataLayer: MapDataLayerSettings, type: MapDataLayerType): boolean => {
if (!dataLayer.dsType || ![DatasourceType.function, DatasourceType.device, DatasourceType.entity].includes(dataLayer.dsType)) {

Loading…
Cancel
Save