Browse Source

merge with develop/3.5.2

pull/8786/head
ShvaykaD 3 years ago
parent
commit
e44a4693a5
  1. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  2. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
  3. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java
  4. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java
  5. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
  6. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  7. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  8. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java
  9. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java
  10. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  11. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  12. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java
  13. 2
      ui-ngx/src/app/modules/common/modules-map.ts
  14. 2
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  15. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html
  16. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  17. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  18. 24
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  19. 55
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  20. 4
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  21. 3
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  22. 20
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html
  23. 68
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts
  24. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  25. 2
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  26. 1
      ui-ngx/src/app/shared/components/public-api.ts
  27. 39
      ui-ngx/src/app/shared/components/unit-input.component.html
  28. 40
      ui-ngx/src/app/shared/components/unit-input.component.scss
  29. 148
      ui-ngx/src/app/shared/components/unit-input.component.ts
  30. 70
      ui-ngx/src/app/shared/models/unit.models.ts
  31. 5
      ui-ngx/src/app/shared/pipe/highlight.pipe.ts
  32. 3
      ui-ngx/src/app/shared/shared.module.ts
  33. 9
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  34. 79
      ui-ngx/src/form.scss

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.rule.engine.aws.sns;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
@ -70,6 +71,9 @@ public class TbSnsNode extends TbAbstractExternalNode {
this.snsClient = AmazonSNSClient.builder()
.withCredentials(credProvider)
.withRegion(this.config.getRegion())
.withClientConfiguration(new ClientConfiguration()
.withConnectionTimeout(10000)
.withRequestTimeout(5000))
.build();
} catch (Exception e) {
throw new TbNodeException(e);
@ -78,10 +82,10 @@ public class TbSnsNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
withCallback(publishMessageAsync(ctx, msg),
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t));
ackIfNeeded(ctx, msg);
t -> tellFailure(ctx, processException(ctx, tbMsg, t), t));
}
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.rule.engine.aws.sqs;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
@ -77,6 +78,9 @@ public class TbSqsNode extends TbAbstractExternalNode {
this.sqsClient = AmazonSQSClientBuilder.standard()
.withCredentials(credProvider)
.withRegion(this.config.getRegion())
.withClientConfiguration(new ClientConfiguration()
.withConnectionTimeout(10000)
.withRequestTimeout(5000))
.build();
} catch (Exception e) {
throw new TbNodeException(e);
@ -85,10 +89,10 @@ public class TbSqsNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg),
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(msg, t), t));
ackIfNeeded(ctx, msg);
t -> tellFailure(ctx, processException(tbMsg, t), t));
}
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java

@ -44,9 +44,12 @@ public abstract class TbAbstractExternalNode implements TbNode {
}
}
protected void ackIfNeeded(TbContext ctx, TbMsg msg) {
protected TbMsg ackIfNeeded(TbContext ctx, TbMsg msg) {
if (forceAck) {
ctx.ack(msg);
return msg.copyWithNewCtx();
} else {
return msg;
}
}

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java

@ -20,6 +20,7 @@ import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString;
@ -35,6 +36,7 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.threeten.bp.Duration;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@ -74,8 +76,8 @@ public class TbPubSubNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
msg = ackIfNeeded(ctx, msg);
publishMessage(ctx, msg);
ackIfNeeded(ctx, msg);
}
@Override
@ -133,8 +135,19 @@ public class TbPubSubNode extends TbAbstractExternalNode {
new ByteArrayInputStream(config.getServiceAccountKey().getBytes()));
CredentialsProvider credProvider = FixedCredentialsProvider.create(credentials);
var retrySettings = RetrySettings.newBuilder()
.setTotalTimeout(Duration.ofSeconds(10))
.setInitialRetryDelay(Duration.ofMillis(50))
.setRetryDelayMultiplier(1.1)
.setMaxRetryDelay(Duration.ofSeconds(2))
.setInitialRpcTimeout(Duration.ofSeconds(2))
.setRpcTimeoutMultiplier(1)
.setMaxRpcTimeout(Duration.ofSeconds(10))
.build();
return Publisher.newBuilder(topicName)
.setCredentialsProvider(credProvider)
.setRetrySettings(retrySettings)
.build();
}
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java

@ -113,25 +113,25 @@ public class TbKafkaNode extends TbAbstractExternalNode {
public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg);
String keyPattern = config.getKeyPattern();
var tbMsg = ackIfNeeded(ctx, msg);
try {
if (initError != null) {
ctx.tellFailure(msg, new RuntimeException("Failed to initialize Kafka rule node producer: " + initError.getMessage()));
ctx.tellFailure(tbMsg, new RuntimeException("Failed to initialize Kafka rule node producer: " + initError.getMessage()));
} else {
ctx.getExternalCallExecutor().executeAsync(() -> {
publish(
ctx,
msg,
tbMsg,
topic,
keyPattern == null || keyPattern.isEmpty()
? null
: TbNodeUtils.processPattern(config.getKeyPattern(), msg)
: TbNodeUtils.processPattern(config.getKeyPattern(), tbMsg)
);
return null;
});
}
ackIfNeeded(ctx, msg);
} catch (Exception e) {
ctx.tellFailure(msg, e);
ctx.tellFailure(tbMsg, e);
}
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java

@ -72,13 +72,13 @@ public class TbSendEmailNode extends TbAbstractExternalNode {
try {
validateType(msg.getType());
TbEmail email = getEmail(msg);
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(ctx.getMailExecutor().executeAsync(() -> {
sendEmail(ctx, msg, email);
sendEmail(ctx, tbMsg, email);
return null;
}),
ok -> tellSuccess(ctx, msg),
fail -> tellFailure(ctx, msg, fail));
ackIfNeeded(ctx, msg);
ok -> tellSuccess(ctx, tbMsg),
fail -> tellFailure(ctx, tbMsg, fail));
} catch (Exception ex) {
ctx.tellFailure(msg, ex);
}

8
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -80,16 +80,16 @@ public class TbMqttNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), msg);
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(msg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
var tbMsg = ackIfNeeded(ctx, msg);
this.mqttClient.publish(topic, Unpooled.wrappedBuffer(tbMsg.getData().getBytes(UTF8)), MqttQoS.AT_LEAST_ONCE, mqttNodeConfiguration.isRetainedMessage())
.addListener(future -> {
if (future.isSuccess()) {
tellSuccess(ctx, msg);
tellSuccess(ctx, tbMsg);
} else {
tellFailure(ctx, processException(msg, future.cause()), future.cause());
tellFailure(ctx, processException(tbMsg, future.cause()), future.cause());
}
}
);
ackIfNeeded(ctx, msg);
}
private TbMsg processException(TbMsg origMsg, Throwable e) {

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java

@ -71,16 +71,17 @@ public class TbNotificationNode extends TbAbstractExternalNode {
.originatorEntityId(ctx.getSelf().getRuleChainId())
.build();
var tbMsg = ackIfNeeded(ctx, msg);
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() ->
ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> {
TbMsgMetaData metaData = msg.getMetaData().copy();
TbMsgMetaData metaData = tbMsg.getMetaData().copy();
metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats));
tellSuccess(ctx, TbMsg.transformMsg(msg, metaData));
tellSuccess(ctx, TbMsg.transformMsg(tbMsg, metaData));
})),
r -> {
},
e -> tellFailure(ctx, msg, e));
ackIfNeeded(ctx, msg);
e -> tellFailure(ctx, tbMsg, e));
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java

@ -61,12 +61,12 @@ public class TbSlackNode extends TbAbstractExternalNode {
}
String message = TbNodeUtils.processPattern(config.getMessageTemplate(), msg);
var tbMsg = ackIfNeeded(ctx, msg);
DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> {
ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message);
}),
r -> tellSuccess(ctx, msg),
e -> tellFailure(ctx, msg, e));
ackIfNeeded(ctx, msg);
r -> tellSuccess(ctx, tbMsg),
e -> tellFailure(ctx, tbMsg, e));
}
}

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@ -50,7 +51,7 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
)
public class TbRabbitMqNode extends TbAbstractExternalNode {
private static final Charset UTF8 = Charset.forName("UTF-8");
private static final Charset UTF8 = StandardCharsets.UTF_8;
private static final String ERROR = "error";
@ -83,10 +84,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg),
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(msg, t), t));
ackIfNeeded(ctx, msg);
t -> tellFailure(ctx, processException(tbMsg, t), t));
}
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

4
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java

@ -59,10 +59,10 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
httpClient.processMessage(ctx, msg,
var tbMsg = ackIfNeeded(ctx, msg);
httpClient.processMessage(ctx, tbMsg,
m -> tellSuccess(ctx, m),
(m, t) -> tellFailure(ctx, m, t));
ackIfNeeded(ctx, msg);
}
@Override

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java

@ -60,16 +60,16 @@ public class TbSendSmsNode extends TbAbstractExternalNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var tbMsg = ackIfNeeded(ctx, msg);
try {
withCallback(ctx.getSmsExecutor().executeAsync(() -> {
sendSms(ctx, msg);
sendSms(ctx, tbMsg);
return null;
}),
ok -> tellSuccess(ctx, msg),
fail -> tellFailure(ctx, msg, fail));
ackIfNeeded(ctx, msg);
ok -> tellSuccess(ctx, tbMsg),
fail -> tellFailure(ctx, tbMsg, fail));
} catch (Exception ex) {
ctx.tellFailure(msg, ex);
ctx.tellFailure(tbMsg, ex);
}
}

2
ui-ngx/src/app/modules/common/modules-map.ts

@ -180,6 +180,7 @@ import * as SlackConversationAutocompleteComponent from '@shared/components/slac
import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -480,6 +481,7 @@ class ModulesMap implements IModulesMap {
'@shared/components/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent,

2
ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html

@ -42,7 +42,7 @@
#entityAliasAutocomplete="matAutocomplete"
[displayWith]="displayEntityAliasFn">
<mat-option *ngFor="let entityAlias of filteredEntityAliases | async" [value]="entityAlias">
<span [innerHTML]="entityAlias.alias | highlight:searchText"></span>
<span [innerHTML]="entityAlias.alias | highlight:searchText:true:'ig'"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntityAliases | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()">

4
ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html

@ -51,9 +51,9 @@
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.units-short</div>
<tb-widget-units
<tb-unit-input
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div>

8
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<div [formGroup]="keyRowFormGroup" class="tb-data-key-row">
<div [formGroup]="keyRowFormGroup" class="tb-form-table-row tb-data-key-row">
<mat-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>
@ -148,16 +148,16 @@
</tb-color-input>
</div>
<div *ngIf="!hideUnits" class="tb-units-field">
<tb-widget-units *ngIf="displayUnitsOrDigits"
<tb-unit-input *ngIf="displayUnitsOrDigits"
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div *ngIf="!hideDecimals" class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic">
<input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
<div class="tb-data-keys-table-row-buttons">
<div class="tb-form-table-row-cell-buttons">
<button type="button"
mat-icon-button
(click)="editKey(true)"

27
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss

@ -14,12 +14,6 @@
* limitations under the License.
*/
.tb-data-key-row {
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
.mat-mdc-form-field.tb-inline-field.tb-key-field {
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
.mat-mdc-form-field-infix {
@ -48,26 +42,17 @@
}
.tb-color-field, .tb-units-field, .tb-decimals-field {
width: 60px;
display: flex;
flex-direction: row;
place-content: center;
align-items: center;
}
}
.tb-data-keys-table-row-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
&.tb-hidden {
visibility: hidden;
}
.tb-units-field {
width: 80px;
}
.tb-color-field, .tb-decimals-field {
width: 60px;
}
}

24
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html

@ -17,22 +17,22 @@
-->
<div class="tb-form-panel">
<div class="tb-form-panel-title">{{ panelTitle }}</div>
<div class="tb-data-keys-table">
<div class="tb-data-keys-header">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-data-keys-header-cell tb-color-header" translate>datakey.color</div>
<div *ngIf="!hideUnits" class="tb-data-keys-header-cell tb-units-header" translate>widget-config.units-short</div>
<div *ngIf="!hideDecimals" class="tb-data-keys-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-data-keys-header-cell tb-actions-header"></div>
<div class="tb-form-table">
<div class="tb-form-table-header">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-form-table-header-cell" fxFlex translate>datakey.key</div>
<div class="tb-form-table-header-cell" fxFlex translate>datakey.label</div>
<div *ngIf="!hideDataKeyColor" class="tb-form-table-header-cell tb-color-header" translate>datakey.color</div>
<div *ngIf="!hideUnits" class="tb-form-table-header-cell tb-units-header" translate>widget-config.units-short</div>
<div *ngIf="!hideDecimals" class="tb-form-table-header-cell tb-decimals-header" translate>widget-config.decimals-short</div>
<div class="tb-form-table-header-cell tb-actions-header"></div>
</div>
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-data-keys-body tb-drop-list"
<div *ngIf="keysFormArray().controls.length; else noKeys" class="tb-form-table-body tb-drop-list"
cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled"
class="tb-data-keys-table-row tb-draggable"
class="tb-form-table-row tb-draggable"
*ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex
[formControl]="keyControl"
@ -41,7 +41,7 @@
[entityAliasId]="entityAliasId"
(keyRemoved)="removeKey($index)">
</tb-data-key-row>
<div class="tb-data-keys-table-row-buttons">
<div class="tb-form-table-row-cell-buttons">
<button mat-icon-button
type="button"
cdkDragHandle

55
ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss

@ -13,52 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-data-keys-table {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 12px;
.tb-data-keys-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 12px;
padding-left: 12px;
.tb-data-keys-header-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
&.tb-source-header {
width: 140px;
}
&.tb-color-header, &.tb-units-header, &.tb-decimals-header {
width: 60px;
}
&.tb-actions-header {
width: 114px;
}
}
.tb-form-table-header-cell {
&.tb-source-header {
width: 140px;
}
.tb-data-keys-body {
display: flex;
flex-direction: column;
gap: 12px;
&.tb-units-header {
width: 80px;
}
.tb-prompt {
height: 38px;
&.tb-color-header, &.tb-decimals-header {
width: 60px;
}
&.tb-actions-header {
width: 114px;
}
}
.tb-data-keys-table-row {
height: 38px;
display: flex;
flex-direction: row;
background: #fff;
}

4
ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html

@ -48,9 +48,9 @@
<ng-container *ngIf="modelValue.type !== dataKeyTypes.alarm">
<div class="tb-form-row space-between" *ngIf="!hideDataKeyUnits">
<div translate>widget-config.units-short</div>
<tb-widget-units
<tb-unit-input
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div class="tb-form-row space-between" *ngIf="!hideDataKeyDecimals">
<div translate>widget-config.decimals-short</div>

3
ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts

@ -29,7 +29,6 @@ import { FilterSelectComponent } from '@home/components/filter/filter-select.com
import { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component';
import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component';
@NgModule({
declarations:
@ -44,7 +43,6 @@ import { WidgetUnitsComponent } from '@home/components/widget/config/widget-unit
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent
],
imports: [
@ -63,7 +61,6 @@ import { WidgetUnitsComponent } from '@home/components/widget/config/widget-unit
EntityAliasSelectComponent,
FilterSelectComponent,
TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent
]
})

20
ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html

@ -1,20 +0,0 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-form-field appearance="outline" class="tb-inline-field" subscriptSizing="dynamic">
<input matInput [formControl]="unitsFormControl" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>

68
ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts

@ -1,68 +0,0 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
@Component({
selector: 'tb-widget-units',
templateUrl: './widget-units.component.html',
styleUrls: [],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WidgetUnitsComponent),
multi: true
}
]
})
export class WidgetUnitsComponent implements ControlValueAccessor, OnInit {
@Input()
disabled: boolean;
unitsFormControl: FormControl;
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder) {
}
ngOnInit() {
this.unitsFormControl = this.fb.control('', []);
this.unitsFormControl.valueChanges.subscribe(val => this.propagateChange(val));
}
writeValue(units?: string): void {
this.unitsFormControl.patchValue(units, {emitEvent: false});
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.unitsFormControl.disable({emitEvent: false});
} else {
this.unitsFormControl.enable({emitEvent: false});
}
}
}

4
ui-ngx/src/app/modules/home/components/widget/widget-config.component.html

@ -267,9 +267,9 @@
<div class="tb-form-panel-title" translate>widget-config.data-settings</div>
<div *ngIf="displayUnitsConfig" class="tb-form-row space-between">
<div translate>widget-config.units</div>
<tb-widget-units
<tb-unit-input
formControlName="units">
</tb-widget-units>
</tb-unit-input>
</div>
<div *ngIf="displayUnitsConfig" class="tb-form-row space-between">
<div translate>widget-config.decimals</div>

2
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html

@ -33,7 +33,7 @@
#entityAutocomplete="matAutocomplete"
[displayWith]="displayEntityFn">
<mat-option *ngFor="let entity of filteredEntities | async" [value]="entity">
<span [innerHTML]="entity.name | highlight:searchText"></span>
<span [innerHTML]="entity.name | highlight:searchText:true:'ig'"></span>
</mat-option>
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null">
<span>

1
ui-ngx/src/app/shared/components/public-api.ts

@ -25,3 +25,4 @@ export * from './notification/template-autocomplete.component';
export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component';
export * from './toggle-select.component';
export * from './unit-input.component';

39
ui-ngx/src/app/shared/components/unit-input.component.html

@ -0,0 +1,39 @@
<!--
Copyright © 2016-2023 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.
-->
<mat-form-field appearance="outline" class="tb-inline-field" subscriptSizing="dynamic">
<input matInput #unitInput [formControl]="unitsFormControl"
placeholder="{{ 'widget-config.set' | translate }}"
(focusin)="onFocus()"
[matAutocomplete]="unitsAutocomplete">
<button *ngIf="unitsFormControl.value && !disabled"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clear()">
<mat-icon class="material-icons">close</mat-icon>
</button>
<mat-autocomplete
#unitsAutocomplete="matAutocomplete"
class="tb-autocomplete tb-unit-input-autocomplete"
panelWidth="fit-content"
[displayWith]="displayUnitFn">
<mat-option *ngFor="let unit of filteredUnits | async" [value]="unit">
<span class="tb-unit-name" fxFlex [innerHTML]="unit.name | highlight:searchText:true:'ig'"></span>
<span class="tb-unit-symbol" [innerHTML]="unit.symbol | highlight:searchText:true:'ig'"></span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

40
ui-ngx/src/app/shared/components/unit-input.component.scss

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.tb-autocomplete.tb-unit-input-autocomplete {
.mat-mdc-option {
border-bottom: none;
.mdc-list-item__primary-text {
flex: 1;
display: flex;
flex-direction: row;
gap: 8px;
.tb-unit-name, .tb-unit-symbol {
font-size: 14px;
font-weight: 400;
line-height: 20px;
letter-spacing: 0.2px;
}
.tb-unit-symbol {
color: rgba(0, 0, 0, 0.38);
min-width: 22px;
text-align: end;
b {
color: rgba(0, 0, 0, 0.87);
}
}
}
}
}

148
ui-ngx/src/app/shared/components/unit-input.component.ts

@ -0,0 +1,148 @@
///
/// Copyright © 2016-2023 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, UntypedFormBuilder } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { searchUnits, Unit, unitBySymbol, units } from '@shared/models/unit.models';
import { map, mergeMap, startWith, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
@Component({
selector: 'tb-unit-input',
templateUrl: './unit-input.component.html',
styleUrls: ['./unit-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => UnitInputComponent),
multi: true
}
],
encapsulation: ViewEncapsulation.None
})
export class UnitInputComponent implements ControlValueAccessor, OnInit {
unitsFormControl: FormControl;
modelValue: string | null;
@Input()
disabled: boolean;
@ViewChild('unitInput', {static: true}) unitInput: ElementRef;
filteredUnits: Observable<Array<Unit | string>>;
searchText = '';
private dirty = false;
private translatedUnits: Array<Unit> = units.map(u => ({symbol: u.symbol,
name: this.translate.instant(u.name),
tags: u.tags}));
private propagateChange = (_val: any) => {};
constructor(private fb: UntypedFormBuilder,
private translate: TranslateService) {
}
ngOnInit() {
this.unitsFormControl = this.fb.control('', []);
this.filteredUnits = this.unitsFormControl.valueChanges
.pipe(
tap(value => {
this.updateView(value);
}),
startWith<string | Unit>(''),
map(value => (value as Unit)?.symbol ? (value as Unit).symbol : (value ? value as string : '')),
mergeMap(symbol => this.fetchUnits(symbol) )
);
}
writeValue(symbol?: string): void {
this.searchText = '';
this.modelValue = symbol;
let res: Unit | string = null;
if (symbol) {
const unit = unitBySymbol(symbol);
res = unit ? unit : symbol;
}
this.unitsFormControl.patchValue(res, {emitEvent: false});
this.dirty = true;
}
onFocus() {
if (this.dirty) {
this.unitsFormControl.updateValueAndValidity({onlySelf: true, emitEvent: true});
this.dirty = false;
}
}
updateView(value: Unit | string | null) {
const res: string = (value as Unit)?.symbol ? (value as Unit)?.symbol : (value as string);
if (this.modelValue !== res) {
this.modelValue = res;
this.propagateChange(this.modelValue);
}
}
displayUnitFn(unit?: Unit | string): string | undefined {
if (unit) {
if ((unit as Unit).symbol) {
return (unit as Unit).symbol;
} else {
return unit as string;
}
}
return undefined;
}
fetchUnits(searchText?: string): Observable<Array<Unit | string>> {
this.searchText = searchText;
const result = searchUnits(this.translatedUnits, searchText);
if (result.length) {
return of(result);
} else {
return of([]);
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
if (this.disabled) {
this.unitsFormControl.disable({emitEvent: false});
} else {
this.unitsFormControl.enable({emitEvent: false});
}
}
clear() {
this.unitsFormControl.patchValue(null, {emitEvent: true});
setTimeout(() => {
this.unitInput.nativeElement.blur();
this.unitInput.nativeElement.focus();
}, 0);
}
}

70
ui-ngx/src/app/shared/models/unit.models.ts

@ -0,0 +1,70 @@
///
/// Copyright © 2016-2023 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.
///
export interface Unit {
name: string;
symbol: string;
tags: string[];
}
export const units: Array<Unit> = [
{
name: 'unit.celsius',
symbol: '°C',
tags: ['temperature']
},
{
name: 'unit.kelvin',
symbol: 'K',
tags: ['temperature']
},
{
name: 'unit.fahrenheit',
symbol: '°F',
tags: ['temperature']
},
{
name: 'unit.percentage',
symbol: '%',
tags: ['percentage']
},
{
name: 'unit.second',
symbol: 's',
tags: ['time']
},
{
name: 'unit.minute',
symbol: 'min',
tags: ['time']
},
{
name: 'unit.hour',
symbol: 'h',
tags: ['time']
}
];
export const unitBySymbol = (symbol: string): Unit => units.find(u => u.symbol === symbol);
const searchUnitTags = (unit: Unit, searchText: string): boolean =>
!!unit.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase()));
export const searchUnits = (_units: Array<Unit>, searchText: string): Array<Unit> => _units.filter(
u => u.symbol.toUpperCase().includes(searchText.toUpperCase()) ||
u.name.toUpperCase().includes(searchText.toUpperCase()) ||
searchUnitTags(u, searchText)
);

5
ui-ngx/src/app/shared/pipe/highlight.pipe.ts

@ -18,11 +18,10 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform {
transform(text: string, search): string {
transform(text: string, search: string, includes = false, flags = 'i'): string {
const pattern = search
.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
const regex = new RegExp('^' + pattern, 'i');
const regex = new RegExp((!includes ? '^' : '') + pattern, flags);
return search ? text.replace(regex, match => `<b>${match}</b>`) : text;
}
}

3
ui-ngx/src/app/shared/shared.module.ts

@ -194,6 +194,7 @@ import { ShortNumberPipe } from '@shared/pipe/short-number.pipe';
import { ToggleHeaderComponent, ToggleOption } from '@shared/components/toggle-header.component';
import { RuleChainSelectComponent } from '@shared/components/rule-chain/rule-chain-select.component';
import { ToggleSelectComponent } from '@shared/components/toggle-select.component';
import { UnitInputComponent } from '@shared/components/unit-input.component';
export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) {
return markedOptionsService;
@ -367,6 +368,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleHeaderComponent,
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
RuleChainSelectComponent
],
imports: [
@ -597,6 +599,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService)
ToggleHeaderComponent,
ToggleOption,
ToggleSelectComponent,
UnitInputComponent,
RuleChainSelectComponent
]
})

9
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -3860,6 +3860,15 @@
"just-now": "Just now",
"ago": "ago"
},
"unit": {
"celsius": "Celsius",
"kelvin": "Kelvin",
"fahrenheit": "Fahrenheit",
"percentage": "Percentage",
"second": "Second",
"minute": "Minute",
"hour": "Hour"
},
"user": {
"user": "User",
"users": "Users",

79
ui-ngx/src/form.scss

@ -177,9 +177,15 @@
opacity: 0;
}
}
&:not(.mat-mdc-form-field-has-icon-suffix) {
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-right: 12px;
}
}
}
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-right: 12px;
padding-left: 12px;
&:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing {
@ -233,7 +239,9 @@
}
&.number {
.mat-mdc-text-field-wrapper {
padding-right: 4px;
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-right: 4px;
}
.mat-mdc-form-field-infix {
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button,
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button {
@ -261,4 +269,71 @@
}
}
}
.tb-form-table {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 12px;
.tb-form-table-header {
height: 48px;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex;
flex-direction: row;
place-content: center flex-start;
align-items: center;
gap: 12px;
padding-left: 12px;
&-cell {
font-weight: 400;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.54);
}
}
.tb-form-table-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.tb-prompt {
height: 38px;
}
.tb-form-table-row {
height: 38px;
display: flex;
flex-direction: row;
gap: 12px;
padding-left: 12px;
&.tb-draggable {
gap: 0;
padding-left: 0;
background: #fff;
}
&-cell-buttons {
display: flex;
flex-direction: row;
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px;
width: 38px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
&.tb-hidden {
visibility: hidden;
}
}
}
}
}
}

Loading…
Cancel
Save