Browse Source

Merge branch 'develop/3.5.2' into AD/imp/show-hide-action-on-mobile

pull/8894/head^2
Vladyslav 3 years ago
committed by GitHub
parent
commit
e48d97054a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  2. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java
  3. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java
  4. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/external/TbAbstractExternalNode.java
  5. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java
  6. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java
  7. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java
  8. 8
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  9. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java
  10. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java
  11. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java
  12. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java
  13. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java
  14. 3
      ui-ngx/src/app/core/services/dialog.service.ts
  15. 2142
      ui-ngx/src/app/core/services/material-icons-codepoints.raw
  16. 25
      ui-ngx/src/app/core/services/resources.service.ts
  17. 75
      ui-ngx/src/app/core/services/utils.service.ts
  18. 4
      ui-ngx/src/app/modules/common/modules-map.ts
  19. 2
      ui-ngx/src/app/modules/home/components/alias/entity-alias-select.component.html
  20. 4
      ui-ngx/src/app/modules/home/components/widget/config/basic/cards/simple-card-basic-config.component.html
  21. 8
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.html
  22. 27
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-key-row.component.scss
  23. 24
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.html
  24. 55
      ui-ngx/src/app/modules/home/components/widget/config/basic/common/data-keys-panel.component.scss
  25. 4
      ui-ngx/src/app/modules/home/components/widget/config/data-key-config.component.html
  26. 3
      ui-ngx/src/app/modules/home/components/widget/config/widget-config-components.module.ts
  27. 20
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.html
  28. 68
      ui-ngx/src/app/modules/home/components/widget/config/widget-units.component.ts
  29. 40
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  30. 46
      ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss
  31. 4
      ui-ngx/src/app/modules/home/components/widget/widget-config.component.html
  32. 71
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html
  33. 35
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss
  34. 56
      ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts
  35. 2
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  36. 12
      ui-ngx/src/app/shared/components/material-icon-select.component.html
  37. 31
      ui-ngx/src/app/shared/components/material-icon-select.component.scss
  38. 36
      ui-ngx/src/app/shared/components/material-icon-select.component.ts
  39. 65
      ui-ngx/src/app/shared/components/material-icons.component.html
  40. 61
      ui-ngx/src/app/shared/components/material-icons.component.scss
  41. 135
      ui-ngx/src/app/shared/components/material-icons.component.ts
  42. 20
      ui-ngx/src/app/shared/components/popover.component.ts
  43. 4
      ui-ngx/src/app/shared/components/popover.service.ts
  44. 2
      ui-ngx/src/app/shared/components/public-api.ts
  45. 39
      ui-ngx/src/app/shared/components/unit-input.component.html
  46. 40
      ui-ngx/src/app/shared/components/unit-input.component.scss
  47. 148
      ui-ngx/src/app/shared/components/unit-input.component.ts
  48. 72
      ui-ngx/src/app/shared/models/icon.models.ts
  49. 70
      ui-ngx/src/app/shared/models/unit.models.ts
  50. 5
      ui-ngx/src/app/shared/pipe/highlight.pipe.ts
  51. 6
      ui-ngx/src/app/shared/shared.module.ts
  52. BIN
      ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf
  53. 17
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  54. 6367
      ui-ngx/src/assets/metadata/material-icons.json
  55. 137
      ui-ngx/src/form.scss

30
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -19,9 +19,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.security.SecurityProperties; import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor; import org.springframework.security.config.annotation.ObjectPostProcessor;
@ -29,7 +31,6 @@ import org.springframework.security.config.annotation.authentication.builders.Au
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
@ -37,9 +38,11 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.header.writers.StaticHeadersWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter; import org.springframework.web.filter.CorsFilter;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
@ -119,6 +122,17 @@ public class ThingsboardSecurityConfiguration {
@Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter;
@Bean
protected FilterRegistrationBean<ShallowEtagHeaderFilter> buildEtagFilter() throws Exception {
ShallowEtagHeaderFilter etagFilter = new ShallowEtagHeaderFilter();
etagFilter.setWriteWeakETag(true);
FilterRegistrationBean<ShallowEtagHeaderFilter> filterRegistrationBean
= new FilterRegistrationBean<>( etagFilter);
filterRegistrationBean.addUrlPatterns("*.js","*.css","*.ico","/assets/*","/static/*");
filterRegistrationBean.setName("etagFilter");
return filterRegistrationBean;
}
@Bean @Bean
protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception { protected RestLoginProcessingFilter buildRestLoginProcessingFilter() throws Exception {
RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler); RestLoginProcessingFilter filter = new RestLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler);
@ -181,8 +195,18 @@ public class ThingsboardSecurityConfiguration {
private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver; private OAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver;
@Bean @Bean
public WebSecurityCustomizer webSecurityCustomizer() { @Order(0)
return (web) -> web.ignoring().antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"); SecurityFilterChain resources(HttpSecurity http) throws Exception {
http
.requestMatchers((matchers) -> matchers.antMatchers("/*.js","/*.css","/*.ico","/assets/**","/static/**"))
.headers().defaultsDisabled()
.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, public"))
.and()
.authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll())
.requestCache().disable()
.securityContext().disable()
.sessionManagement().disable();
return http.build();
} }
@Bean @Bean

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

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

@ -52,9 +52,12 @@ public abstract class TbAbstractExternalNode implements TbNode {
} }
} }
protected void ackIfNeeded(TbContext ctx, TbMsg msg) { protected TbMsg ackIfNeeded(TbContext ctx, TbMsg msg) {
if (forceAck) { if (forceAck) {
ctx.ack(msg); 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.core.ApiFutures;
import com.google.api.gax.core.CredentialsProvider; import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.FixedCredentialsProvider; import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.pubsub.v1.Publisher; import com.google.cloud.pubsub.v1.Publisher;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
@ -36,6 +37,7 @@ import org.thingsboard.rule.engine.external.TbAbstractExternalNode;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.threeten.bp.Duration;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -75,8 +77,8 @@ public class TbPubSubNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
msg = ackIfNeeded(ctx, msg);
publishMessage(ctx, msg); publishMessage(ctx, msg);
ackIfNeeded(ctx, msg);
} }
@Override @Override
@ -134,8 +136,19 @@ public class TbPubSubNode extends TbAbstractExternalNode {
new ByteArrayInputStream(config.getServiceAccountKey().getBytes())); new ByteArrayInputStream(config.getServiceAccountKey().getBytes()));
CredentialsProvider credProvider = FixedCredentialsProvider.create(credentials); 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) return Publisher.newBuilder(topicName)
.setCredentialsProvider(credProvider) .setCredentialsProvider(credProvider)
.setRetrySettings(retrySettings)
.build(); .build();
} }
} }

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

@ -115,25 +115,25 @@ public class TbKafkaNode extends TbAbstractExternalNode {
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg); String topic = TbNodeUtils.processPattern(config.getTopicPattern(), msg);
String keyPattern = config.getKeyPattern(); String keyPattern = config.getKeyPattern();
var tbMsg = ackIfNeeded(ctx, msg);
try { try {
if (initError != null) { 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 { } else {
ctx.getExternalCallExecutor().executeAsync(() -> { ctx.getExternalCallExecutor().executeAsync(() -> {
publish( publish(
ctx, ctx,
msg, tbMsg,
topic, topic,
keyPattern == null || keyPattern.isEmpty() keyPattern == null || keyPattern.isEmpty()
? null ? null
: TbNodeUtils.processPattern(config.getKeyPattern(), msg) : TbNodeUtils.processPattern(config.getKeyPattern(), tbMsg)
); );
return null; return null;
}); });
} }
ackIfNeeded(ctx, msg);
} catch (Exception e) { } 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 { try {
validateType(msg.getType()); validateType(msg.getType());
TbEmail email = getEmail(msg); TbEmail email = getEmail(msg);
var tbMsg = ackIfNeeded(ctx, msg);
withCallback(ctx.getMailExecutor().executeAsync(() -> { withCallback(ctx.getMailExecutor().executeAsync(() -> {
sendEmail(ctx, msg, email); sendEmail(ctx, tbMsg, email);
return null; return null;
}), }),
ok -> tellSuccess(ctx, msg), ok -> tellSuccess(ctx, tbMsg),
fail -> tellFailure(ctx, msg, fail)); fail -> tellFailure(ctx, tbMsg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception ex) { } catch (Exception ex) {
ctx.tellFailure(msg, 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 @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
String topic = TbNodeUtils.processPattern(this.mqttNodeConfiguration.getTopicPattern(), 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 -> { .addListener(future -> {
if (future.isSuccess()) { if (future.isSuccess()) {
tellSuccess(ctx, msg); tellSuccess(ctx, tbMsg);
} else { } else {
tellFailure(ctx, processException(ctx, msg, future.cause()), future.cause()); tellFailure(ctx, processException(ctx, tbMsg, future.cause()), future.cause());
} }
} }
); );
ackIfNeeded(ctx, msg);
} }
private TbMsg processException(TbContext ctx, TbMsg origMsg, Throwable e) { private TbMsg processException(TbContext ctx, 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()) .originatorEntityId(ctx.getSelf().getRuleChainId())
.build(); .build();
var tbMsg = ackIfNeeded(ctx, msg);
DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() -> DonAsynchron.withCallback(ctx.getNotificationExecutor().executeAsync(() ->
ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> { ctx.getNotificationCenter().processNotificationRequest(ctx.getTenantId(), notificationRequest, stats -> {
TbMsgMetaData metaData = msg.getMetaData().copy(); TbMsgMetaData metaData = tbMsg.getMetaData().copy();
metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats)); metaData.putValue("notificationRequestResult", JacksonUtil.toString(stats));
tellSuccess(ctx, TbMsg.transformMsg(msg, metaData)); tellSuccess(ctx, TbMsg.transformMsg(tbMsg, metaData));
})), })),
r -> { r -> {
}, },
e -> tellFailure(ctx, msg, e)); e -> tellFailure(ctx, tbMsg, e));
ackIfNeeded(ctx, msg);
} }
} }

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); String message = TbNodeUtils.processPattern(config.getMessageTemplate(), msg);
var tbMsg = ackIfNeeded(ctx, msg);
DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> { DonAsynchron.withCallback(ctx.getExternalCallExecutor().executeAsync(() -> {
ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message); ctx.getSlackService().sendMessage(ctx.getTenantId(), token, config.getConversation().getId(), message);
}), }),
r -> tellSuccess(ctx, msg), r -> tellSuccess(ctx, tbMsg),
e -> tellFailure(ctx, msg, e)); e -> tellFailure(ctx, tbMsg, e));
ackIfNeeded(ctx, msg);
} }
} }

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

@ -84,10 +84,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(publishMessageAsync(ctx, msg), var tbMsg = ackIfNeeded(ctx, msg);
withCallback(publishMessageAsync(ctx, tbMsg),
m -> tellSuccess(ctx, m), m -> tellSuccess(ctx, m),
t -> tellFailure(ctx, processException(ctx, msg, t), t)); t -> tellFailure(ctx, processException(ctx, tbMsg, t), t));
ackIfNeeded(ctx, msg);
} }
private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) { private ListenableFuture<TbMsg> publishMessageAsync(TbContext ctx, TbMsg msg) {

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

@ -60,10 +60,10 @@ public class TbRestApiCallNode extends TbAbstractExternalNode {
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) { 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 -> tellSuccess(ctx, m),
(m, t) -> tellFailure(ctx, m, t)); (m, t) -> tellFailure(ctx, m, t));
ackIfNeeded(ctx, msg);
} }
@Override @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 @Override
public void onMsg(TbContext ctx, TbMsg msg) { public void onMsg(TbContext ctx, TbMsg msg) {
var tbMsg = ackIfNeeded(ctx, msg);
try { try {
withCallback(ctx.getSmsExecutor().executeAsync(() -> { withCallback(ctx.getSmsExecutor().executeAsync(() -> {
sendSms(ctx, msg); sendSms(ctx, tbMsg);
return null; return null;
}), }),
ok -> tellSuccess(ctx, msg), ok -> tellSuccess(ctx, tbMsg),
fail -> tellFailure(ctx, msg, fail)); fail -> tellFailure(ctx, tbMsg, fail));
ackIfNeeded(ctx, msg);
} catch (Exception ex) { } catch (Exception ex) {
ctx.tellFailure(msg, ex); ctx.tellFailure(tbMsg, ex);
} }
} }

3
ui-ngx/src/app/core/services/dialog.service.ts

@ -114,7 +114,8 @@ export class DialogService {
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
icon icon
} },
autoFocus: false
}).afterClosed(); }).afterClosed();
} }

2142
ui-ngx/src/app/core/services/material-icons-codepoints.raw

File diff suppressed because it is too large

25
ui-ngx/src/app/core/services/resources.service.ts

@ -47,6 +47,7 @@ export interface ModulesWithFactories {
}) })
export class ResourcesService { export class ResourcesService {
private loadedJsonResources: { [url: string]: ReplaySubject<any> } = {};
private loadedResources: { [url: string]: ReplaySubject<void> } = {}; private loadedResources: { [url: string]: ReplaySubject<void> } = {};
private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {}; private loadedModules: { [url: string]: ReplaySubject<Type<any>[]> } = {};
private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {}; private loadedModulesAndFactories: { [url: string]: ReplaySubject<ModulesWithFactories> } = {};
@ -61,6 +62,30 @@ export class ResourcesService {
this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache()); this.store.pipe(select(selectIsAuthenticated)).subscribe(() => this.clearModulesCache());
} }
public loadJsonResource<T>(url: string, postProcess?: (data: T) => T): Observable<T> {
if (this.loadedJsonResources[url]) {
return this.loadedJsonResources[url].asObservable();
}
const subject = new ReplaySubject<any>();
this.loadedJsonResources[url] = subject;
this.http.get<T>(url).subscribe(
{
next: (o) => {
if (postProcess) {
o = postProcess(o);
}
this.loadedJsonResources[url].next(o);
this.loadedJsonResources[url].complete();
},
error: () => {
this.loadedJsonResources[url].error(new Error(`Unable to load ${url}`));
delete this.loadedJsonResources[url];
}
}
);
return subject.asObservable();
}
public loadResource(url: string): Observable<any> { public loadResource(url: string): Observable<any> {
if (this.loadedResources[url]) { if (this.loadedResources[url]) {
return this.loadedResources[url].asObservable(); return this.loadedResources[url].asObservable();

75
ui-ngx/src/app/core/services/utils.service.ts

@ -21,32 +21,31 @@ import { Inject, Injectable, NgZone } from '@angular/core';
import { WINDOW } from '@core/services/window.service'; import { WINDOW } from '@core/services/window.service';
import { ExceptionData } from '@app/shared/models/error.models'; import { ExceptionData } from '@app/shared/models/error.models';
import { import {
base64toObj,
base64toString,
baseUrl, baseUrl,
createLabelFromDatasource, createLabelFromDatasource,
deepClone, deepClone,
deleteNullProperties, deleteNullProperties,
guid, hashCode, guid,
hashCode,
isDefined, isDefined,
isDefinedAndNotNull, isDefinedAndNotNull,
isString, isString,
isUndefined, isUndefined,
objToBase64, objToBase64,
objToBase64URI, objToBase64URI
base64toString,
base64toObj
} from '@core/utils'; } from '@core/utils';
import { WindowMessage } from '@shared/models/window-message.model'; import { WindowMessage } from '@shared/models/window-message.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants'; import { customTranslationsPrefix, i18nPrefix } from '@app/shared/models/constants';
import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models'; import { DataKey, Datasource, DatasourceType, KeyInfo } from '@shared/models/widget.models';
import { EntityType } from '@shared/models/entity-type.models';
import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models'; import { DataKeyType } from '@app/shared/models/telemetry/telemetry.models';
import { alarmFields } from '@shared/models/alarm.models'; import { alarmFields, alarmSeverityTranslations, alarmStatusTranslations } from '@shared/models/alarm.models';
import { materialColors } from '@app/shared/models/material.models'; import { materialColors } from '@app/shared/models/material.models';
import { WidgetInfo } from '@home/models/widget-component.models'; import { WidgetInfo } from '@home/models/widget-component.models';
import jsonSchemaDefaults from 'json-schema-defaults'; import jsonSchemaDefaults from 'json-schema-defaults';
import materialIconsCodepoints from '!raw-loader!./material-icons-codepoints.raw'; import { Observable } from 'rxjs';
import { Observable, of, ReplaySubject } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators'; import { publishReplay, refCount } from 'rxjs/operators';
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { import {
@ -56,6 +55,8 @@ import {
TelemetryType TelemetryType
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { DatePipe } from '@angular/common';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g'); const i18nRegExp = new RegExp(`{${i18nPrefix}:[^{}]+}`, 'g');
@ -86,13 +87,6 @@ const defaultAlarmFields: Array<string> = [
alarmFields.status.keyName alarmFields.status.keyName
]; ];
const commonMaterialIcons: Array<string> = ['more_horiz', 'more_vert', 'open_in_new',
'visibility', 'play_arrow', 'arrow_back', 'arrow_downward',
'arrow_forward', 'arrow_upwards', 'close', 'refresh', 'menu', 'show_chart', 'multiline_chart', 'pie_chart', 'insert_chart', 'people',
'person', 'domain', 'devices_other', 'now_widgets', 'dashboards', 'map', 'pin_drop', 'my_location', 'extension', 'search',
'settings', 'notifications', 'notifications_active', 'info', 'info_outline', 'warning', 'list', 'file_download', 'import_export',
'share', 'add', 'edit', 'done', 'delete'];
// @dynamic // @dynamic
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -121,10 +115,9 @@ export class UtilsService {
defaultAlarmDataKeys: Array<DataKey> = []; defaultAlarmDataKeys: Array<DataKey> = [];
materialIcons: Array<string> = [];
constructor(@Inject(WINDOW) private window: Window, constructor(@Inject(WINDOW) private window: Window,
private zone: NgZone, private zone: NgZone,
private datePipe: DatePipe,
private translate: TranslateService) { private translate: TranslateService) {
let frame: Element = null; let frame: Element = null;
try { try {
@ -180,6 +173,27 @@ export class UtilsService {
return deepClone(this.defaultAlarmDataKeys); return deepClone(this.defaultAlarmDataKeys);
} }
public defaultAlarmFieldContent(key: DataKey | {name: string}, value: any): string {
if (isDefined(value)) {
const alarmField = alarmFields[key.name];
if (alarmField) {
if (alarmField.time) {
return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : '';
} else if (alarmField === alarmFields.severity) {
return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField === alarmFields.status) {
return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value;
} else if (alarmField === alarmFields.originatorType) {
return this.translate.instant(entityTypeTranslations.get(value).type);
} else if (alarmField.value === alarmFields.assignee.value) {
return '';
}
}
return value;
}
return '';
}
public generateObjectFromJsonSchema(schema: any): any { public generateObjectFromJsonSchema(schema: any): any {
const obj = jsonSchemaDefaults(schema); const obj = jsonSchemaDefaults(schema);
deleteNullProperties(obj); deleteNullProperties(obj);
@ -306,31 +320,6 @@ export class UtilsService {
return datasources; return datasources;
} }
public getMaterialIcons(): Observable<Array<string>> {
if (this.materialIcons.length) {
return of(this.materialIcons);
} else {
const materialIconsSubject = new ReplaySubject<Array<string>>();
this.zone.runOutsideAngular(() => {
const codepointsArray = materialIconsCodepoints
.split('\n')
.filter((codepoint) => codepoint && codepoint.length);
codepointsArray.forEach((codepoint) => {
const values = codepoint.split(' ');
if (values && values.length === 2) {
this.materialIcons.push(values[0]);
}
});
materialIconsSubject.next(this.materialIcons);
});
return materialIconsSubject.asObservable();
}
}
public getCommonMaterialIcons(): Array<string> {
return commonMaterialIcons;
}
public getMaterialColor(index: number) { public getMaterialColor(index: number) {
const colorIndex = index % materialColors.length; const colorIndex = index % materialColors.length;
return materialColors[colorIndex].value; return materialColors[colorIndex].value;
@ -411,7 +400,7 @@ export class UtilsService {
public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string { public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string {
if (str && str.length) { if (str && str.length) {
let hue = hashCode(str) % 360; const hue = hashCode(str) % 360;
return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`; return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`;
} }
} }

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

@ -180,6 +180,8 @@ import * as SlackConversationAutocompleteComponent from '@shared/components/slac
import * as StringItemsListComponent from '@shared/components/string-items-list.component'; import * as StringItemsListComponent from '@shared/components/string-items-list.component';
import * as ToggleHeaderComponent from '@shared/components/toggle-header.component'; import * as ToggleHeaderComponent from '@shared/components/toggle-header.component';
import * as ToggleSelectComponent from '@shared/components/toggle-select.component'; import * as ToggleSelectComponent from '@shared/components/toggle-select.component';
import * as UnitInputComponent from '@shared/components/unit-input.component';
import * as MaterialIconsComponent from '@shared/components/material-icons.component';
import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component'; import * as AddEntityDialogComponent from '@home/components/entity/add-entity-dialog.component';
import * as EntitiesTableComponent from '@home/components/entity/entities-table.component'; import * as EntitiesTableComponent from '@home/components/entity/entities-table.component';
@ -480,6 +482,8 @@ class ModulesMap implements IModulesMap {
'@shared/components/string-items-list.component': StringItemsListComponent, '@shared/components/string-items-list.component': StringItemsListComponent,
'@shared/components/toggle-header.component': ToggleHeaderComponent, '@shared/components/toggle-header.component': ToggleHeaderComponent,
'@shared/components/toggle-select.component': ToggleSelectComponent, '@shared/components/toggle-select.component': ToggleSelectComponent,
'@shared/components/unit-input.component': UnitInputComponent,
'@shared/components/material-icons.component': MaterialIconsComponent,
'@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent, '@home/components/entity/add-entity-dialog.component': AddEntityDialogComponent,
'@home/components/entity/entities-table.component': EntitiesTableComponent, '@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" #entityAliasAutocomplete="matAutocomplete"
[displayWith]="displayEntityAliasFn"> [displayWith]="displayEntityAliasFn">
<mat-option *ngFor="let entityAlias of filteredEntityAliases | async" [value]="entityAlias"> <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>
<mat-option *ngIf="!(filteredEntityAliases | async)?.length" [value]="null" class="tb-not-found"> <mat-option *ngIf="!(filteredEntityAliases | async)?.length" [value]="null" class="tb-not-found">
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> <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>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widget-config.units-short</div> <div translate>widget-config.units-short</div>
<tb-widget-units <tb-unit-input
formControlName="units"> formControlName="units">
</tb-widget-units> </tb-unit-input>
</div> </div>
<div class="tb-form-row space-between"> <div class="tb-form-row space-between">
<div translate>widget-config.decimals-short</div> <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. 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-form-field *ngIf="hasAdditionalLatestDataKeys" class="tb-inline-field tb-source-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="latest"> <mat-select formControlName="latest">
<mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option> <mat-option [value]="false">{{ 'datakey.timeseries' | translate }}</mat-option>
@ -148,16 +148,16 @@
</tb-color-input> </tb-color-input>
</div> </div>
<div *ngIf="!hideUnits" class="tb-units-field"> <div *ngIf="!hideUnits" class="tb-units-field">
<tb-widget-units *ngIf="displayUnitsOrDigits" <tb-unit-input *ngIf="displayUnitsOrDigits"
formControlName="units"> formControlName="units">
</tb-widget-units> </tb-unit-input>
</div> </div>
<div *ngIf="!hideDecimals" class="tb-decimals-field"> <div *ngIf="!hideDecimals" class="tb-decimals-field">
<mat-form-field *ngIf="displayUnitsOrDigits" appearance="outline" class="tb-inline-field number" subscriptSizing="dynamic"> <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 }}"> <input matInput formControlName="decimals" type="number" min="0" max="15" step="1" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="tb-data-keys-table-row-buttons"> <div class="tb-form-table-row-cell-buttons">
<button type="button" <button type="button"
mat-icon-button mat-icon-button
(click)="editKey(true)" (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. * limitations under the License.
*/ */
.tb-data-key-row { .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-form-field.tb-inline-field.tb-key-field {
.mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) { .mat-mdc-text-field-wrapper:not(.mdc-text-field--outlined) {
.mat-mdc-form-field-infix { .mat-mdc-form-field-infix {
@ -48,26 +42,17 @@
} }
.tb-color-field, .tb-units-field, .tb-decimals-field { .tb-color-field, .tb-units-field, .tb-decimals-field {
width: 60px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
place-content: center; place-content: center;
align-items: center; align-items: center;
} }
}
.tb-data-keys-table-row-buttons { .tb-units-field {
display: flex; width: 80px;
flex-direction: row; }
button.mat-mdc-icon-button.mat-mdc-button-base {
padding: 7px; .tb-color-field, .tb-decimals-field {
width: 38px; width: 60px;
height: 38px;
.mat-icon {
color: rgba(0, 0, 0, 0.38);
}
&.tb-hidden {
visibility: hidden;
}
} }
} }

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">
<div class="tb-form-panel-title">{{ panelTitle }}</div> <div class="tb-form-panel-title">{{ panelTitle }}</div>
<div class="tb-data-keys-table"> <div class="tb-form-table">
<div class="tb-data-keys-header"> <div class="tb-form-table-header">
<div *ngIf="hasAdditionalLatestDataKeys" class="tb-data-keys-header-cell tb-source-header" translate>datakey.source</div> <div *ngIf="hasAdditionalLatestDataKeys" class="tb-form-table-header-cell tb-source-header" translate>datakey.source</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.key</div> <div class="tb-form-table-header-cell" fxFlex translate>datakey.key</div>
<div class="tb-data-keys-header-cell" fxFlex translate>datakey.label</div> <div class="tb-form-table-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="!hideDataKeyColor" class="tb-form-table-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="!hideUnits" class="tb-form-table-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 *ngIf="!hideDecimals" class="tb-form-table-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-header-cell tb-actions-header"></div>
</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" cdkDropList cdkDropListOrientation="vertical"
[cdkDropListDisabled]="!dragEnabled" [cdkDropListDisabled]="!dragEnabled"
(cdkDropListDropped)="keyDrop($event)"> (cdkDropListDropped)="keyDrop($event)">
<div cdkDrag [cdkDragDisabled]="!dragEnabled" <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;"> *ngFor="let keyControl of keysFormArray().controls; trackBy: trackByKey; let $index = index;">
<tb-data-key-row fxFlex <tb-data-key-row fxFlex
[formControl]="keyControl" [formControl]="keyControl"
@ -41,7 +41,7 @@
[entityAliasId]="entityAliasId" [entityAliasId]="entityAliasId"
(keyRemoved)="removeKey($index)"> (keyRemoved)="removeKey($index)">
</tb-data-key-row> </tb-data-key-row>
<div class="tb-data-keys-table-row-buttons"> <div class="tb-form-table-row-cell-buttons">
<button mat-icon-button <button mat-icon-button
type="button" type="button"
cdkDragHandle 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 * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.tb-data-keys-table { .tb-form-table-header-cell {
border: 1px solid rgba(0, 0, 0, 0.12); &.tb-source-header {
border-radius: 6px; width: 140px;
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-data-keys-body { &.tb-units-header {
display: flex; width: 80px;
flex-direction: column;
gap: 12px;
} }
.tb-prompt { &.tb-color-header, &.tb-decimals-header {
height: 38px; 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"> <ng-container *ngIf="modelValue.type !== dataKeyTypes.alarm">
<div class="tb-form-row space-between" *ngIf="!hideDataKeyUnits"> <div class="tb-form-row space-between" *ngIf="!hideDataKeyUnits">
<div translate>widget-config.units-short</div> <div translate>widget-config.units-short</div>
<tb-widget-units <tb-unit-input
formControlName="units"> formControlName="units">
</tb-widget-units> </tb-unit-input>
</div> </div>
<div class="tb-form-row space-between" *ngIf="!hideDataKeyDecimals"> <div class="tb-form-row space-between" *ngIf="!hideDataKeyDecimals">
<div translate>widget-config.decimals-short</div> <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 { WidgetSettingsModule } from '@home/components/widget/lib/settings/widget-settings.module';
import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component'; import { WidgetSettingsComponent } from '@home/components/widget/config/widget-settings.component';
import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component'; import { TimewindowConfigPanelComponent } from '@home/components/widget/config/timewindow-config-panel.component';
import { WidgetUnitsComponent } from '@home/components/widget/config/widget-units.component';
@NgModule({ @NgModule({
declarations: declarations:
@ -44,7 +43,6 @@ import { WidgetUnitsComponent } from '@home/components/widget/config/widget-unit
EntityAliasSelectComponent, EntityAliasSelectComponent,
FilterSelectComponent, FilterSelectComponent,
TimewindowConfigPanelComponent, TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent WidgetSettingsComponent
], ],
imports: [ imports: [
@ -63,7 +61,6 @@ import { WidgetUnitsComponent } from '@home/components/widget/config/widget-unit
EntityAliasSelectComponent, EntityAliasSelectComponent,
FilterSelectComponent, FilterSelectComponent,
TimewindowConfigPanelComponent, TimewindowConfigPanelComponent,
WidgetUnitsComponent,
WidgetSettingsComponent 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});
}
}
}

40
ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts

@ -53,7 +53,6 @@ import { Direction } from '@shared/models/page/sort-order';
import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections'; import { CollectionViewer, DataSource, SelectionModel } from '@angular/cdk/collections';
import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, forkJoin, fromEvent, merge, Observable, Subscription } from 'rxjs';
import { emptyPageData, PageData } from '@shared/models/page/page-data'; import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { entityTypeTranslations } from '@shared/models/entity-type.models';
import { debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, take, tap } from 'rxjs/operators';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort'; import { MatSort, SortDirection } from '@angular/material/sort';
@ -93,15 +92,7 @@ import {
DisplayColumnsPanelComponent, DisplayColumnsPanelComponent,
DisplayColumnsPanelData DisplayColumnsPanelData
} from '@home/components/widget/lib/display-columns-panel.component'; } from '@home/components/widget/lib/display-columns-panel.component';
import { import { AlarmDataInfo, alarmFields, AlarmInfo, alarmSeverityColors, AlarmStatus } from '@shared/models/alarm.models';
AlarmDataInfo,
alarmFields,
AlarmInfo,
alarmSeverityColors,
alarmSeverityTranslations,
AlarmStatus,
alarmStatusTranslations
} from '@shared/models/alarm.models';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { import {
AlarmDetailsDialogComponent, AlarmDetailsDialogComponent,
@ -140,7 +131,6 @@ import {
AlarmFilterConfigData AlarmFilterConfigData
} from '@home/components/alarm/alarm-filter-config.component'; } from '@home/components/alarm/alarm-filter-config.component';
import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { getCurrentAuthUser } from '@core/auth/auth.selectors';
import { UserId } from '@shared/models/id/user-id';
interface AlarmsTableWidgetSettings extends TableWidgetSettings { interface AlarmsTableWidgetSettings extends TableWidgetSettings {
alarmsTitle: string; alarmsTitle: string;
@ -168,7 +158,8 @@ interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor {
templateUrl: './alarms-table-widget.component.html', templateUrl: './alarms-table-widget.component.html',
styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss'] styleUrls: ['./alarms-table-widget.component.scss', './table-widget.scss']
}) })
export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, AfterViewInit, OnDestroy { export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() @Input()
ctx: WidgetContext; ctx: WidgetContext;
@ -434,7 +425,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
keySettings.columnWidth = '120px'; keySettings.columnWidth = '120px';
} }
if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) { if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) {
keySettings.columnWidth = '120px' keySettings.columnWidth = '120px';
} }
} }
this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx');
@ -546,14 +537,12 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
overlayRef.dispose(); overlayRef.dispose();
}); });
const columns: DisplayColumn[] = this.columns.map(column => { const columns: DisplayColumn[] = this.columns.map(column => ({
return {
title: column.title, title: column.title,
def: column.def, def: column.def,
display: this.displayedColumns.indexOf(column.def) > -1, display: this.displayedColumns.indexOf(column.def) > -1,
selectable: this.columnSelectionAvailability[column.def] selectable: this.columnSelectionAvailability[column.def]
}; }));
});
const providers: StaticProvider[] = [ const providers: StaticProvider[] = [
{ {
@ -1013,7 +1002,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
data: { data: {
alarmId: alarm.id.id alarmId: alarm.id.id
} }
}).afterClosed() }).afterClosed();
} }
} }
@ -1021,20 +1010,7 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit,
if (isDefined(value)) { if (isDefined(value)) {
const alarmField = alarmFields[key.name]; const alarmField = alarmFields[key.name];
if (alarmField) { if (alarmField) {
if (alarmField.time) { return this.utils.defaultAlarmFieldContent(key, value);
return value ? this.datePipe.transform(value, 'yyyy-MM-dd HH:mm:ss') : '';
} else if (alarmField.value === alarmFields.severity.value) {
return this.translate.instant(alarmSeverityTranslations.get(value));
} else if (alarmField.value === alarmFields.status.value) {
return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value;
} else if (alarmField.value === alarmFields.originatorType.value) {
return this.translate.instant(entityTypeTranslations.get(value).type);
} else if (alarmField.value === alarmFields.assignee.value) {
return '';
}
else {
return value;
}
} }
const entityField = entityFields[key.name]; const entityField = entityFields[key.name];
if (entityField) { if (entityField) {

46
ui-ngx/src/app/modules/home/components/widget/lib/home-page/home-page-widget.scss

@ -67,50 +67,4 @@
color: inherit; color: inherit;
} }
} }
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
} }

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 class="tb-form-panel-title" translate>widget-config.data-settings</div>
<div *ngIf="displayUnitsConfig" class="tb-form-row space-between"> <div *ngIf="displayUnitsConfig" class="tb-form-row space-between">
<div translate>widget-config.units</div> <div translate>widget-config.units</div>
<tb-widget-units <tb-unit-input
formControlName="units"> formControlName="units">
</tb-widget-units> </tb-unit-input>
</div> </div>
<div *ngIf="displayUnitsConfig" class="tb-form-row space-between"> <div *ngIf="displayUnitsConfig" class="tb-form-row space-between">
<div translate>widget-config.decimals</div> <div translate>widget-config.decimals</div>

71
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.html

@ -15,63 +15,14 @@
limitations under the License. limitations under the License.
--> -->
<form class="tb-material-icons-dialog" style="min-width: 600px;"> <div mat-dialog-content>
<mat-toolbar fxLayout="row" color="primary"> <button class="tb-close-button"
<h2>{{ 'icon.select-icon' | translate }}</h2> mat-icon-button
<span fxFlex></span> (click)="cancel()"
<section fxLayout="row" fxLayoutAlign="start center" fxLayoutGap="8px"> type="button">
<mat-slide-toggle [formControl]="showAllControl"> <mat-icon>close</mat-icon>
</mat-slide-toggle> </button>
<label translate>icon.show-all</label> <tb-material-icons [selectedIcon]="selectedIcon"
</section> (iconSelected)="selectIcon($event)">
<button mat-icon-button </tb-material-icons>
(click)="cancel()" </div>
type="button">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-toolbar>
<mat-progress-bar color="warn" mode="indeterminate" *ngIf="isLoading$ | async">
</mat-progress-bar>
<div style="height: 4px;" *ngIf="!(isLoading$ | async)"></div>
<div class="tb-absolute-fill tb-icons-load" *ngIf="loadingIcons$ | async" fxLayout="column" fxLayoutAlign="center center">
<mat-spinner color="accent" mode="indeterminate" diameter="40"></mat-spinner>
</div>
<div mat-dialog-content>
<div class="mat-content mat-padding" fxLayout="column">
<fieldset [disabled]="(isLoading$ | async)">
<ng-template ngFor let-icon [ngForOf]="icons$ | async" let-last="last">
<ng-container #iconButtons>
<button *ngIf="icon === selectedIcon"
class="tb-select-icon-button"
mat-raised-button
color="primary"
(click)="selectIcon(icon)"
matTooltip="{{ icon }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon}}</mat-icon>
</button>
<button *ngIf="icon !== selectedIcon"
class="tb-select-icon-button"
mat-button
(click)="selectIcon(icon)"
matTooltip="{{ icon }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon}}</mat-icon>
</button>
</ng-container>
</ng-template>
</fieldset>
</div>
</div>
<div mat-dialog-actions fxLayout="row">
<span fxFlex></span>
<button mat-button
type="button"
[disabled]="(isLoading$ | async)"
(click)="cancel()">
{{ 'action.cancel' | translate }}
</button>
</div>
</form>

35
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.scss

@ -14,36 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
:host { :host {
.tb-material-icons-dialog { .tb-close-button {
position: relative; position: absolute;
} top: 6px;
.tb-icons-load { right: 6px;
top: 64px;
z-index: 3;
background: rgba(255, 255, 255, .75);
}
}
:host ::ng-deep {
.tb-material-icons-dialog {
button.mat-mdc-button-base.tb-select-icon-button {
width: 56px;
min-width: 56px;
height: 56px;
padding: 16px;
margin: 10px;
border: solid 1px #ffa500;
border-radius: 0;
line-height: 0;
display: inline-block;
vertical-align: baseline;
.mat-icon {
width: 24px;
margin: 0;
height: 24px;
vertical-align: initial;
font-size: 24px;
}
}
} }
} }

56
ui-ngx/src/app/shared/components/dialog/material-icons-dialog.component.ts

@ -14,16 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { AfterViewInit, Component, Inject, OnInit, QueryList, ViewChildren } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DialogComponent } from '@shared/components/dialog.component'; import { DialogComponent } from '@shared/components/dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { UntypedFormControl } from '@angular/forms';
import { merge, Observable, of } from 'rxjs';
import { delay, map, mapTo, mergeMap, share, startWith, tap } from 'rxjs/operators';
export interface MaterialIconsDialogData { export interface MaterialIconsDialogData {
icon: string; icon: string;
@ -35,64 +31,16 @@ export interface MaterialIconsDialogData {
providers: [], providers: [],
styleUrls: ['./material-icons-dialog.component.scss'] styleUrls: ['./material-icons-dialog.component.scss']
}) })
export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> export class MaterialIconsDialogComponent extends DialogComponent<MaterialIconsDialogComponent, string> {
implements OnInit, AfterViewInit {
@ViewChildren('iconButtons') iconButtons: QueryList<HTMLElement>;
selectedIcon: string; selectedIcon: string;
icons$: Observable<Array<string>>;
loadingIcons$: Observable<boolean>;
showAllControl: UntypedFormControl;
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
protected router: Router, protected router: Router,
@Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData, @Inject(MAT_DIALOG_DATA) public data: MaterialIconsDialogData,
private utils: UtilsService,
public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) { public dialogRef: MatDialogRef<MaterialIconsDialogComponent, string>) {
super(store, router, dialogRef); super(store, router, dialogRef);
this.selectedIcon = data.icon; this.selectedIcon = data.icon;
this.showAllControl = new UntypedFormControl(false);
}
ngOnInit(): void {
this.icons$ = this.showAllControl.valueChanges.pipe(
map((showAll) => {
return {firstTime: false, showAll};
}),
startWith<{firstTime: boolean, showAll: boolean}>({firstTime: true, showAll: false}),
mergeMap((data) => {
if (data.showAll) {
return this.utils.getMaterialIcons().pipe(delay(100));
} else {
const res = of(this.utils.getCommonMaterialIcons());
return data.firstTime ? res : res.pipe(delay(50));
}
}),
share()
);
}
ngAfterViewInit(): void {
this.loadingIcons$ = merge(
this.showAllControl.valueChanges.pipe(
mapTo(true),
),
this.iconButtons.changes.pipe(
delay(100),
mapTo( false),
)
).pipe(
tap((loadingIcons) => {
if (loadingIcons) {
this.showAllControl.disable({emitEvent: false});
} else {
this.showAllControl.enable({emitEvent: false});
}
}),
share()
);
} }
selectIcon(icon: string) { selectIcon(icon: string) {

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

@ -33,7 +33,7 @@
#entityAutocomplete="matAutocomplete" #entityAutocomplete="matAutocomplete"
[displayWith]="displayEntityFn"> [displayWith]="displayEntityFn">
<mat-option *ngFor="let entity of filteredEntities | async" [value]="entity"> <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>
<mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null"> <mat-option *ngIf="!(filteredEntities | async)?.length" [value]="null">
<span> <span>

12
ui-ngx/src/app/shared/components/material-icon-select.component.html

@ -29,7 +29,13 @@
</mat-form-field> </mat-form-field>
</div> </div>
<ng-template #boxInput> <ng-template #boxInput>
<mat-icon class="icon-box" [ngStyle]="color && !disabled ? { color: color } : {}" <button type="button"
[ngClass]="{'disabled': disabled}" mat-stroked-button
(click)="openIconDialog()">{{materialIconFormGroup.get('icon').value}}</mat-icon> class="icon-box"
[ngStyle]="color && !disabled ? { color: color } : {}"
[disabled]="disabled"
#matButton
(click)="openIconPopup($event, matButton)">
<mat-icon>{{materialIconFormGroup.get('icon').value}}</mat-icon>
</button>
</ng-template> </ng-template>

31
ui-ngx/src/app/shared/components/material-icon-select.component.scss

@ -22,20 +22,23 @@
border: solid 1px rgba(0, 0, 0, .27); border: solid 1px rgba(0, 0, 0, .27);
box-sizing: initial; box-sizing: initial;
} }
&.icon-box { }
border: 1px solid rgba(0, 0, 0, 0.12); }
border-radius: 4px;
cursor: pointer; :host ::ng-deep {
box-sizing: border-box; button.mat-mdc-button-base.icon-box {
padding: 8px; width: 40px;
height: 40px; min-width: 40px;
width: 40px; height: 40px;
font-size: 22px; padding: 7px;
vertical-align: middle; &:not(:disabled) {
&.disabled { color: rgba(0, 0, 0, 0.87);
cursor: initial; }
color: rgba(0, 0, 0, 0.38); > .mat-icon {
} width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
} }
} }
} }

36
ui-ngx/src/app/shared/components/material-icon-select.component.ts

@ -14,15 +14,18 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import { PageComponent } from '@shared/components/page.component'; import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state'; import { AppState } from '@core/core.state';
import { ControlValueAccessor, UntypedFormBuilder, UntypedFormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { DialogService } from '@core/services/dialog.service'; import { DialogService } from '@core/services/dialog.service';
import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { TbPopoverService } from '@shared/components/popover.service';
import { MaterialIconsComponent } from '@shared/components/material-icons.component';
import { MatButton } from '@angular/material/button';
@Component({ @Component({
selector: 'tb-material-icon-select', selector: 'tb-material-icon-select',
@ -81,6 +84,9 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
constructor(protected store: Store<AppState>, constructor(protected store: Store<AppState>,
private dialogs: DialogService, private dialogs: DialogService,
private translate: TranslateService, private translate: TranslateService,
private popoverService: TbPopoverService,
private renderer: Renderer2,
private viewContainerRef: ViewContainerRef,
private fb: UntypedFormBuilder, private fb: UntypedFormBuilder,
private cd: ChangeDetectorRef) { private cd: ChangeDetectorRef) {
super(store); super(store);
@ -142,6 +148,32 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit
} }
} }
openIconPopup($event: Event, matButton: MatButton) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const materialIconsPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, MaterialIconsComponent, 'left', true, null,
{
selectedIcon: this.materialIconFormGroup.get('icon').value
},
{},
{}, {}, true);
materialIconsPopover.tbComponentRef.instance.popover = materialIconsPopover;
materialIconsPopover.tbComponentRef.instance.iconSelected.subscribe((icon) => {
materialIconsPopover.hide();
this.materialIconFormGroup.patchValue(
{icon}, {emitEvent: true}
);
this.cd.markForCheck();
});
}
}
clear() { clear() {
this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true});
this.cd.markForCheck(); this.cd.markForCheck();

65
ui-ngx/src/app/shared/components/material-icons.component.html

@ -0,0 +1,65 @@
<!--
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.
-->
<div class="tb-material-icons-panel">
<div class="tb-material-icons-title" translate>icon.icons</div>
<mat-form-field class="tb-material-icons-search tb-inline-field" appearance="outline" subscriptSizing="dynamic">
<mat-icon matPrefix>search</mat-icon>
<input matInput [formControl]="searchIconControl" placeholder="{{ 'icon.search-icon' | translate }}"/>
<button *ngIf="searchIconControl.value"
type="button"
matSuffix mat-icon-button aria-label="Clear"
(click)="clearSearch()">
<mat-icon class="material-icons">close</mat-icon>
</button>
</mat-form-field>
<cdk-virtual-scroll-viewport [fxShow]="!notFound" #iconsPanel
[itemSize]="iconsRowHeight" class="tb-material-icons-viewport" [ngStyle]="{width: iconsPanelWidth, height: iconsPanelHeight}">
<div *cdkVirtualFor="let iconRow of iconRows$ | async" class="tb-material-icons-row">
<ng-container *ngFor="let icon of iconRow">
<button *ngIf="icon.name === selectedIcon"
class="tb-select-icon-button"
mat-raised-button
color="primary"
(click)="selectIcon(icon)"
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon.name}}</mat-icon>
</button>
<button *ngIf="icon.name !== selectedIcon"
class="tb-select-icon-button"
mat-button
(click)="selectIcon(icon)"
matTooltip="{{ icon.displayName }}"
matTooltipPosition="above"
type="button">
<mat-icon>{{icon.name}}</mat-icon>
</button>
</ng-container>
</div>
</cdk-virtual-scroll-viewport>
<button *ngIf="!showAllSubject.value" class="tb-material-icons-show-more" mat-button color="primary" (click)="showAllSubject.next(true)">
{{ 'action.show-more' | translate }}
</button>
<ng-container *ngIf="notFound">
<div class="tb-no-data-available" [ngStyle]="{width: iconsPanelWidth}">
<div class="tb-no-data-bg"></div>
<div class="tb-no-data-text">{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}</div>
</div>
</ng-container>
</div>

61
ui-ngx/src/app/shared/components/material-icons.component.scss

@ -0,0 +1,61 @@
/**
* 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-material-icons-panel {
width: 100%;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
.tb-material-icons-title {
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.87);
}
.tb-material-icons-title, .tb-material-icons-search, .tb-material-icons-show-more {
width: 100%;
}
.tb-material-icons-viewport {
min-height: 144px;
}
.tb-material-icons-row {
display: flex;
flex-direction: row;
gap: 12px;
}
.tb-material-icons-row + .tb-material-icons-row {
margin-top: 12px;
}
.tb-no-data-available {
min-height: 144px;
}
button.mat-mdc-button-base.tb-select-icon-button {
width: 36px;
min-width: 36px;
height: 36px;
padding: 6px;
&:not(.mat-primary) {
color: rgba(0, 0, 0, 0.54);
}
> .mat-icon {
width: 24px;
height: 24px;
font-size: 24px;
margin: 0;
}
}
}

135
ui-ngx/src/app/shared/components/material-icons.component.ts

@ -0,0 +1,135 @@
///
/// 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 { PageComponent } from '@shared/components/page.component';
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { UntypedFormControl } from '@angular/forms';
import { BehaviorSubject, combineLatest, debounce, Observable, of, timer } from 'rxjs';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { getMaterialIcons, MaterialIcon } from '@shared/models/icon.models';
import { distinctUntilChanged, map, mergeMap, share, startWith, tap } from 'rxjs/operators';
import { ResourcesService } from '@core/services/resources.service';
import { TbPopoverComponent } from '@shared/components/popover.component';
import { BreakpointObserver } from '@angular/cdk/layout';
import { MediaBreakpoints } from '@shared/models/constants';
@Component({
selector: 'tb-material-icons',
templateUrl: './material-icons.component.html',
providers: [],
styleUrls: ['./material-icons.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class MaterialIconsComponent extends PageComponent implements OnInit {
@ViewChild('iconsPanel')
iconsPanel: CdkVirtualScrollViewport;
@Input()
selectedIcon: string;
@Input()
popover: TbPopoverComponent<MaterialIconsComponent>;
@Output()
iconSelected = new EventEmitter<string>();
iconRows$: Observable<MaterialIcon[][]>;
showAllSubject = new BehaviorSubject<boolean>(false);
searchIconControl: UntypedFormControl;
iconsRowHeight = 48;
iconsPanelHeight: string;
iconsPanelWidth: string;
notFound = false;
constructor(protected store: Store<AppState>,
private resourcesService: ResourcesService,
private breakpointObserver: BreakpointObserver,
private cd: ChangeDetectorRef) {
super(store);
this.searchIconControl = new UntypedFormControl('');
}
ngOnInit(): void {
const iconsRowSize = this.breakpointObserver.isMatched(MediaBreakpoints['lt-md']) ? 8 : 11;
this.calculatePanelSize(iconsRowSize);
const iconsRowSizeObservable = this.breakpointObserver
.observe(MediaBreakpoints['lt-md']).pipe(
map((state) => state.matches ? 8 : 11),
startWith(iconsRowSize),
);
this.iconRows$ = combineLatest({showAll: this.showAllSubject.asObservable(),
rowSize: iconsRowSizeObservable,
searchText: this.searchIconControl.valueChanges.pipe(
startWith(''),
debounce((searchText) => searchText ? timer(150) : of({})),
)}).pipe(
map((data) => {
if (data.searchText && !data.showAll) {
data.showAll = true;
this.showAllSubject.next(true);
}
return data;
}),
distinctUntilChanged((p, c) => c.showAll === p.showAll && c.searchText === p.searchText && c.rowSize === p.rowSize),
mergeMap((data) => getMaterialIcons(this.resourcesService, data.rowSize, data.showAll, data.searchText).pipe(
map(iconRows => ({iconRows, iconsRowSize: data.rowSize}))
)),
tap((data) => {
this.notFound = !data.iconRows.length;
this.calculatePanelSize(data.iconsRowSize, data.iconRows.length);
this.cd.markForCheck();
setTimeout(() => {
this.checkSize();
}, 0);
}),
map((data) => data.iconRows),
share()
);
}
clearSearch() {
this.searchIconControl.patchValue('', {emitEvent: true});
}
selectIcon(icon: MaterialIcon) {
this.iconSelected.emit(icon.name);
}
private calculatePanelSize(iconsRowSize: number, iconRows = 4) {
this.iconsPanelHeight = Math.min(iconRows * this.iconsRowHeight, 10 * this.iconsRowHeight) + 'px';
this.iconsPanelWidth = (iconsRowSize * 36 + (iconsRowSize - 1) * 12 + 6) + 'px';
}
private checkSize() {
this.iconsPanel?.checkViewportSize();
this.popover?.updatePosition();
}
}

20
ui-ngx/src/app/shared/components/popover.component.ts

@ -63,8 +63,10 @@ import { coerceBoolean } from '@shared/decorators/coercion';
export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null; export type TbPopoverTrigger = 'click' | 'focus' | 'hover' | null;
@Directive({ @Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[tb-popover]', selector: '[tb-popover]',
exportAs: 'tbPopover', exportAs: 'tbPopover',
// eslint-disable-next-line @angular-eslint/no-host-metadata-property
host: { host: {
'[class.tb-popover-open]': 'visible' '[class.tb-popover-open]': 'visible'
} }
@ -265,12 +267,20 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
} else if (delay > 0) { } else if (delay > 0) {
this.delayTimer = setTimeout(() => { this.delayTimer = setTimeout(() => {
this.delayTimer = undefined; this.delayTimer = undefined;
isEnter ? this.show() : this.hide(); if (isEnter) {
this.show();
} else {
this.hide();
}
}, delay * 1000); }, delay * 1000);
} else { } else {
// `isOrigin` is used due to the tooltip will not hide immediately // `isOrigin` is used due to the tooltip will not hide immediately
// (may caused by the fade-out animation). // (may caused by the fade-out animation).
isEnter && isOrigin ? this.show() : this.hide(); if (isEnter && isOrigin) {
this.show();
} else {
this.hide();
}
} }
} }
@ -345,15 +355,15 @@ export class TbPopoverDirective implements OnChanges, OnDestroy, AfterViewInit {
</ng-template> </ng-template>
` `
}) })
export class TbPopoverComponent implements OnDestroy, OnInit { export class TbPopoverComponent<T = any> implements OnDestroy, OnInit {
@ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay; @ViewChild('overlay', { static: false }) overlay!: CdkConnectedOverlay;
@ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>; @ViewChild('popoverRoot', { static: false }) popoverRoot!: ElementRef<HTMLElement>;
@ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>; @ViewChild('popover', { static: false }) popover!: ElementRef<HTMLElement>;
tbContent: string | TemplateRef<void> | null = null; tbContent: string | TemplateRef<void> | null = null;
tbComponentFactory: ComponentFactory<any> | null = null; tbComponentFactory: ComponentFactory<T> | null = null;
tbComponentRef: ComponentRef<any> | null = null; tbComponentRef: ComponentRef<T> | null = null;
tbComponentContext: any; tbComponentContext: any;
tbComponentInjector: Injector | null = null; tbComponentInjector: Injector | null = null;
tbComponentStyle: { [klass: string]: any } = {}; tbComponentStyle: { [klass: string]: any } = {};

4
ui-ngx/src/app/shared/components/popover.service.ts

@ -65,7 +65,7 @@ export class TbPopoverService {
displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef, displayPopover<T>(trigger: Element, renderer: Renderer2, hostView: ViewContainerRef,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true, componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', hideOnClickOutside = true,
injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any, injector?: Injector, context?: any, overlayStyle: any = {}, popoverStyle: any = {}, style?: any,
showCloseButton = true): TbPopoverComponent { showCloseButton = true): TbPopoverComponent<T> {
const componentRef = this.createPopoverRef(hostView); const componentRef = this.createPopoverRef(hostView);
return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside, return this.displayPopoverWithComponentRef(componentRef, trigger, renderer, componentType, preferredPlacement, hideOnClickOutside,
injector, context, overlayStyle, popoverStyle, style, showCloseButton); injector, context, overlayStyle, popoverStyle, style, showCloseButton);
@ -74,7 +74,7 @@ export class TbPopoverService {
displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2, displayPopoverWithComponentRef<T>(componentRef: ComponentRef<TbPopoverComponent>, trigger: Element, renderer: Renderer2,
componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top', componentType: Type<T>, preferredPlacement: PopoverPlacement = 'top',
hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {}, hideOnClickOutside = true, injector?: Injector, context?: any, overlayStyle: any = {},
popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent { popoverStyle: any = {}, style?: any, showCloseButton = true): TbPopoverComponent<T> {
const component = componentRef.instance; const component = componentRef.instance;
this.popoverWithTriggers.push({ this.popoverWithTriggers.push({
trigger, trigger,

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

@ -25,3 +25,5 @@ export * from './notification/template-autocomplete.component';
export * from './resource/resource-autocomplete.component'; export * from './resource/resource-autocomplete.component';
export * from './toggle-header.component'; export * from './toggle-header.component';
export * from './toggle-select.component'; export * from './toggle-select.component';
export * from './unit-input.component';
export * from './material-icons.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);
}
}

72
ui-ngx/src/app/shared/models/icon.models.ts

@ -0,0 +1,72 @@
///
/// 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 { ResourcesService } from '@core/services/resources.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { isNotEmptyStr } from '@core/utils';
export interface MaterialIcon {
name: string;
displayName?: string;
tags: string[];
}
export const iconByName = (icons: Array<MaterialIcon>, name: string): MaterialIcon => icons.find(i => i.name === name);
const searchIconTags = (icon: MaterialIcon, searchText: string): boolean =>
!!icon.tags.find(t => t.toUpperCase().includes(searchText.toUpperCase()));
const searchIcons = (_icons: Array<MaterialIcon>, searchText: string): Array<MaterialIcon> => _icons.filter(
i => i.name.toUpperCase().includes(searchText.toUpperCase()) ||
i.displayName.toUpperCase().includes(searchText.toUpperCase()) ||
searchIconTags(i, searchText)
);
const getCommonMaterialIcons = (icons: Array<MaterialIcon>, chunkSize: number): Array<MaterialIcon> => icons.slice(0, chunkSize * 4);
export const getMaterialIcons = (resourcesService: ResourcesService, chunkSize = 11,
all = false, searchText: string): Observable<MaterialIcon[][]> =>
resourcesService.loadJsonResource<Array<MaterialIcon>>('/assets/metadata/material-icons.json',
(icons) => {
for (const icon of icons) {
const words = icon.name.replace(/_/g, ' ').split(' ');
for (let i = 0; i < words.length; i++) {
words[i] = words[i].charAt(0).toUpperCase() + words[i].slice(1);
}
icon.displayName = words.join(' ');
}
return icons;
}
).pipe(
map((icons) => {
if (isNotEmptyStr(searchText)) {
return searchIcons(icons, searchText);
} else if (!all) {
return getCommonMaterialIcons(icons, chunkSize);
} else {
return icons;
}
}),
map((icons) => {
const iconChunks: MaterialIcon[][] = [];
for (let i = 0; i < icons.length; i += chunkSize) {
const chunk = icons.slice(i, i + chunkSize);
iconChunks.push(chunk);
}
return iconChunks;
})
);

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' }) @Pipe({ name: 'highlight' })
export class HighlightPipe implements PipeTransform { export class HighlightPipe implements PipeTransform {
transform(text: string, search): string { transform(text: string, search: string, includes = false, flags = 'i'): string {
const pattern = search const pattern = search
.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); .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; return search ? text.replace(regex, match => `<b>${match}</b>`) : text;
} }
} }

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

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

BIN
ui-ngx/src/assets/fonts/MaterialIcons-Regular.ttf

Binary file not shown.

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

@ -68,7 +68,8 @@
"less": "Less", "less": "Less",
"skip": "Skip", "skip": "Skip",
"send": "Send", "send": "Send",
"reset": "Reset" "reset": "Reset",
"show-more": "Show more"
}, },
"aggregation": { "aggregation": {
"aggregation": "Aggregation", "aggregation": "Aggregation",
@ -3860,6 +3861,15 @@
"just-now": "Just now", "just-now": "Just now",
"ago": "ago" "ago": "ago"
}, },
"unit": {
"celsius": "Celsius",
"kelvin": "Kelvin",
"fahrenheit": "Fahrenheit",
"percentage": "Percentage",
"second": "Second",
"minute": "Minute",
"hour": "Hour"
},
"user": { "user": {
"user": "User", "user": "User",
"users": "Users", "users": "Users",
@ -5493,9 +5503,12 @@
}, },
"icon": { "icon": {
"icon": "Icon", "icon": "Icon",
"icons": "Icons",
"select-icon": "Select icon", "select-icon": "Select icon",
"material-icons": "Material icons", "material-icons": "Material icons",
"show-all": "Show all icons" "show-all": "Show all icons",
"search-icon": "Search icon",
"no-icons-found": "No icons found for '{{iconSearch}}'"
}, },
"phone-input": { "phone-input": {
"phone-input-label": "Phone number", "phone-input-label": "Phone number",

6367
ui-ngx/src/assets/metadata/material-icons.json

File diff suppressed because it is too large

137
ui-ngx/src/form.scss

@ -13,6 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
@import './scss/constants';
.tb-default, .tb-dark { .tb-default, .tb-dark {
.tb-form-panel { .tb-form-panel {
box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04); box-shadow: 0 0 10px 6px rgba(11, 17, 51, 0.04);
@ -177,10 +180,22 @@
opacity: 0; opacity: 0;
} }
} }
&:not(.mat-mdc-form-field-has-icon-prefix) {
.mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) {
padding-left: 12px;
}
}
}
&: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 { .mat-mdc-text-field-wrapper {
&.mdc-text-field--outlined, &:not(.mdc-text-field--outlined) { &.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) { &:not(.mdc-text-field--focused):not(.mdc-text-field--disabled):not(:hover) {
.mdc-notched-outline__leading, .mdc-notched-outline__trailing { .mdc-notched-outline__leading, .mdc-notched-outline__trailing {
border-color: rgba(0, 0, 0, 0.12); border-color: rgba(0, 0, 0, 0.12);
@ -197,7 +212,7 @@
line-height: 20px; line-height: 20px;
} }
} }
.mat-mdc-form-field-icon-suffix { .mat-mdc-form-field-icon-prefix, .mat-mdc-form-field-icon-suffix {
height: 40px; height: 40px;
font-size: 14px; font-size: 14px;
line-height: 40px; line-height: 40px;
@ -233,7 +248,9 @@
} }
&.number { &.number {
.mat-mdc-text-field-wrapper { .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 { .mat-mdc-form-field-infix {
input.mdc-text-field__input[type=number]::-webkit-inner-spin-button, input.mdc-text-field__input[type=number]::-webkit-inner-spin-button,
input.mdc-text-field__input[type=number]::-webkit-outer-spin-button { input.mdc-text-field__input[type=number]::-webkit-outer-spin-button {
@ -261,4 +278,116 @@
} }
} }
} }
.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;
}
}
}
}
}
.tb-no-data-available {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.tb-no-data-bg {
margin: 10px;
position: relative;
flex: 1;
width: 100%;
max-height: 100px;
&:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #305680;
-webkit-mask-image: url(/assets/home/no_data_folder_bg.svg);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: contain;
-webkit-mask-position: center;
mask-image: url(/assets/home/no_data_folder_bg.svg);
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.tb-no-data-text {
font-weight: 500;
font-size: 14px;
line-height: 20px;
letter-spacing: 0.25px;
color: rgba(0, 0, 0, 0.54);
@media #{$mat-md-lg} {
font-size: 12px;
line-height: 16px;
}
}
} }

Loading…
Cancel
Save