Commit adb71ecfe7b2a87e372d207837c769017f49d18a

Authored by Igor Kulikov
1 parent 73473e3b

Rule Node Configuration

Showing 35 changed files with 483 additions and 72 deletions
@@ -180,7 +180,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe @@ -180,7 +180,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
180 return scannedComponent; 180 return scannedComponent;
181 } 181 }
182 182
183 - private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws IOException { 183 + private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws Exception {
184 NodeDefinition nodeDefinition = new NodeDefinition(); 184 NodeDefinition nodeDefinition = new NodeDefinition();
185 nodeDefinition.setDetails(nodeAnnotation.nodeDetails()); 185 nodeDefinition.setDetails(nodeAnnotation.nodeDetails());
186 nodeDefinition.setDescription(nodeAnnotation.nodeDescription()); 186 nodeDefinition.setDescription(nodeAnnotation.nodeDescription());
@@ -188,9 +188,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe @@ -188,9 +188,10 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
188 nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled()); 188 nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled());
189 nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes()); 189 nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes());
190 nodeDefinition.setCustomRelations(nodeAnnotation.customRelations()); 190 nodeDefinition.setCustomRelations(nodeAnnotation.customRelations());
191 - String defaultConfigResourceName = nodeAnnotation.defaultConfigResource();  
192 - nodeDefinition.setDefaultConfiguration(mapper.readTree(  
193 - Resources.toString(Resources.getResource(defaultConfigResourceName), Charsets.UTF_8))); 191 + Class<? extends NodeConfiguration> configClazz = nodeAnnotation.configClazz();
  192 + NodeConfiguration config = configClazz.newInstance();
  193 + NodeConfiguration defaultConfiguration = config.defaultConfiguration();
  194 + nodeDefinition.setDefaultConfiguration(mapper.valueToTree(defaultConfiguration));
194 return nodeDefinition; 195 return nodeDefinition;
195 } 196 }
196 197
@@ -234,7 +234,7 @@ caffeine: @@ -234,7 +234,7 @@ caffeine:
234 specs: 234 specs:
235 relations: 235 relations:
236 timeToLiveInMinutes: 1440 236 timeToLiveInMinutes: 1440
237 - maxSize: 100000 237 + maxSize: 0
238 deviceCredentials: 238 deviceCredentials:
239 timeToLiveInMinutes: 1440 239 timeToLiveInMinutes: 1440
240 maxSize: 100000 240 maxSize: 100000
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.rule.engine.api;
  17 +
  18 +public interface NodeConfiguration {
  19 +
  20 + NodeConfiguration defaultConfiguration();
  21 +
  22 +}
@@ -35,15 +35,16 @@ public @interface RuleNode { @@ -35,15 +35,16 @@ public @interface RuleNode {
35 35
36 String nodeDetails(); 36 String nodeDetails();
37 37
  38 + Class<? extends NodeConfiguration> configClazz();
  39 +
38 boolean inEnabled() default true; 40 boolean inEnabled() default true;
39 41
40 boolean outEnabled() default true; 42 boolean outEnabled() default true;
41 43
42 ComponentScope scope() default ComponentScope.TENANT; 44 ComponentScope scope() default ComponentScope.TENANT;
43 45
44 - String defaultConfigResource() default "EmptyNodeConfig.json";  
45 -  
46 String[] relationTypes() default {"Success", "Failure"}; 46 String[] relationTypes() default {"Success", "Failure"};
47 47
48 boolean customRelations() default false; 48 boolean customRelations() default false;
  49 +
49 } 50 }
@@ -30,6 +30,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; @@ -30,6 +30,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
30 @RuleNode( 30 @RuleNode(
31 type = ComponentType.FILTER, 31 type = ComponentType.FILTER,
32 name = "script", relationTypes = {"True", "False", "Failure"}, 32 name = "script", relationTypes = {"True", "False", "Failure"},
  33 + configClazz = TbJsFilterNodeConfiguration.class,
33 nodeDescription = "Filter incoming messages using JS script", 34 nodeDescription = "Filter incoming messages using JS script",
34 nodeDetails = "Evaluate incoming Message with configured JS condition. " + 35 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
35 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." + 36 "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." +
@@ -16,9 +16,17 @@ @@ -16,9 +16,17 @@
16 package org.thingsboard.rule.engine.filter; 16 package org.thingsboard.rule.engine.filter;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
20 @Data 21 @Data
21 -public class TbJsFilterNodeConfiguration { 22 +public class TbJsFilterNodeConfiguration implements NodeConfiguration {
22 23
23 private String jsScript; 24 private String jsScript;
  25 +
  26 + @Override
  27 + public TbJsFilterNodeConfiguration defaultConfiguration() {
  28 + TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
  29 + configuration.setJsScript("msg.passed < 15 && msg.name === 'Vit' && meta.temp == 10 && msg.bigObj.prop == 42;");
  30 + return configuration;
  31 + }
24 } 32 }
@@ -31,6 +31,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback; @@ -31,6 +31,7 @@ import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
31 @RuleNode( 31 @RuleNode(
32 type = ComponentType.FILTER, 32 type = ComponentType.FILTER,
33 name = "switch", customRelations = true, 33 name = "switch", customRelations = true,
  34 + configClazz = TbJsSwitchNodeConfiguration.class,
34 nodeDescription = "Route incoming Message to one or multiple output chains", 35 nodeDescription = "Route incoming Message to one or multiple output chains",
35 nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + 36 nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " +
36 "If Array is empty - message not routed to next Node. " + 37 "If Array is empty - message not routed to next Node. " +
@@ -15,14 +15,29 @@ @@ -15,14 +15,29 @@
15 */ 15 */
16 package org.thingsboard.rule.engine.filter; 16 package org.thingsboard.rule.engine.filter;
17 17
  18 +import com.google.common.collect.Sets;
18 import lombok.Data; 19 import lombok.Data;
  20 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 21
20 import java.util.Set; 22 import java.util.Set;
21 23
22 @Data 24 @Data
23 -public class TbJsSwitchNodeConfiguration { 25 +public class TbJsSwitchNodeConfiguration implements NodeConfiguration {
24 26
25 private String jsScript; 27 private String jsScript;
26 private Set<String> allowedRelations; 28 private Set<String> allowedRelations;
27 private boolean routeToAllWithNoCheck; 29 private boolean routeToAllWithNoCheck;
  30 +
  31 + @Override
  32 + public TbJsSwitchNodeConfiguration defaultConfiguration() {
  33 + TbJsSwitchNodeConfiguration configuration = new TbJsSwitchNodeConfiguration();
  34 + configuration.setJsScript("function nextRelation(meta, msg) {\n" +
  35 + " return ['one','nine'];" +
  36 + "};\n" +
  37 + "\n" +
  38 + "nextRelation(meta, msg);");
  39 + configuration.setAllowedRelations(Sets.newHashSet("one", "two"));
  40 + configuration.setRouteToAllWithNoCheck(false);
  41 + return configuration;
  42 + }
28 } 43 }
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg; @@ -28,6 +28,7 @@ import org.thingsboard.server.common.msg.TbMsg;
28 @RuleNode( 28 @RuleNode(
29 type = ComponentType.FILTER, 29 type = ComponentType.FILTER,
30 name = "message type", 30 name = "message type",
  31 + configClazz = TbMsgTypeFilterNodeConfiguration.class,
31 nodeDescription = "Filter incoming messages by Message Type", 32 nodeDescription = "Filter incoming messages by Message Type",
32 nodeDetails = "Evaluate incoming Message with configured JS condition. " + 33 nodeDetails = "Evaluate incoming Message with configured JS condition. " +
33 "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.") 34 "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.")
@@ -16,15 +16,24 @@ @@ -16,15 +16,24 @@
16 package org.thingsboard.rule.engine.filter; 16 package org.thingsboard.rule.engine.filter;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
  21 +import java.util.Arrays;
  22 +import java.util.Collections;
20 import java.util.List; 23 import java.util.List;
21 24
22 /** 25 /**
23 * Created by ashvayka on 19.01.18. 26 * Created by ashvayka on 19.01.18.
24 */ 27 */
25 @Data 28 @Data
26 -public class TbMsgTypeFilterNodeConfiguration { 29 +public class TbMsgTypeFilterNodeConfiguration implements NodeConfiguration {
27 30
28 private List<String> messageTypes; 31 private List<String> messageTypes;
29 32
  33 + @Override
  34 + public TbMsgTypeFilterNodeConfiguration defaultConfiguration() {
  35 + TbMsgTypeFilterNodeConfiguration configuration = new TbMsgTypeFilterNodeConfiguration();
  36 + configuration.setMessageTypes(Arrays.asList("GET_ATTRIBUTES","POST_ATTRIBUTES","POST_TELEMETRY","RPC_REQUEST"));
  37 + return configuration;
  38 + }
30 } 39 }
@@ -38,6 +38,7 @@ import static org.thingsboard.server.common.data.DataConstants.*; @@ -38,6 +38,7 @@ import static org.thingsboard.server.common.data.DataConstants.*;
38 @Slf4j 38 @Slf4j
39 @RuleNode(type = ComponentType.ENRICHMENT, 39 @RuleNode(type = ComponentType.ENRICHMENT,
40 name = "originator attributes", 40 name = "originator attributes",
  41 + configClazz = TbGetAttributesNodeConfiguration.class,
41 nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata", 42 nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata",
42 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " + 43 nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message metadata " +
43 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + 44 "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " +
@@ -16,14 +16,16 @@ @@ -16,14 +16,16 @@
16 package org.thingsboard.rule.engine.metadata; 16 package org.thingsboard.rule.engine.metadata;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
  21 +import java.util.Collections;
20 import java.util.List; 22 import java.util.List;
21 23
22 /** 24 /**
23 * Created by ashvayka on 19.01.18. 25 * Created by ashvayka on 19.01.18.
24 */ 26 */
25 @Data 27 @Data
26 -public class TbGetAttributesNodeConfiguration { 28 +public class TbGetAttributesNodeConfiguration implements NodeConfiguration {
27 29
28 private List<String> clientAttributeNames; 30 private List<String> clientAttributeNames;
29 private List<String> sharedAttributeNames; 31 private List<String> sharedAttributeNames;
@@ -31,4 +33,13 @@ public class TbGetAttributesNodeConfiguration { @@ -31,4 +33,13 @@ public class TbGetAttributesNodeConfiguration {
31 33
32 private List<String> latestTsKeyNames; 34 private List<String> latestTsKeyNames;
33 35
  36 + @Override
  37 + public TbGetAttributesNodeConfiguration defaultConfiguration() {
  38 + TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
  39 + configuration.setClientAttributeNames(Collections.emptyList());
  40 + configuration.setSharedAttributeNames(Collections.emptyList());
  41 + configuration.setServerAttributeNames(Collections.emptyList());
  42 + configuration.setLatestTsKeyNames(Collections.emptyList());
  43 + return configuration;
  44 + }
34 } 45 }
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
26 @RuleNode( 26 @RuleNode(
27 type = ComponentType.ENRICHMENT, 27 type = ComponentType.ENRICHMENT,
28 name="customer attributes", 28 name="customer attributes",
  29 + configClazz = TbGetEntityAttrNodeConfiguration.class,
29 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", 30 nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
30 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + 31 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
31 "To access those attributes in other nodes this template can be used " + 32 "To access those attributes in other nodes this template can be used " +
@@ -16,13 +16,25 @@ @@ -16,13 +16,25 @@
16 package org.thingsboard.rule.engine.metadata; 16 package org.thingsboard.rule.engine.metadata;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
  21 +import java.util.HashMap;
20 import java.util.Map; 22 import java.util.Map;
21 import java.util.Optional; 23 import java.util.Optional;
22 24
23 @Data 25 @Data
24 -public class TbGetEntityAttrNodeConfiguration { 26 +public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration {
25 27
26 private Map<String, String> attrMapping; 28 private Map<String, String> attrMapping;
27 private boolean isTelemetry = false; 29 private boolean isTelemetry = false;
  30 +
  31 + @Override
  32 + public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
  33 + TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
  34 + Map<String, String> attrMapping = new HashMap<>();
  35 + attrMapping.putIfAbsent("temperature", "tempo");
  36 + configuration.setAttrMapping(attrMapping);
  37 + configuration.setTelemetry(true);
  38 + return configuration;
  39 + }
28 } 40 }
@@ -16,11 +16,28 @@ @@ -16,11 +16,28 @@
16 package org.thingsboard.rule.engine.metadata; 16 package org.thingsboard.rule.engine.metadata;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import org.thingsboard.server.common.data.relation.EntityRelation;
19 import org.thingsboard.server.common.data.relation.EntitySearchDirection; 21 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
20 22
  23 +import java.util.HashMap;
  24 +import java.util.Map;
  25 +
21 @Data 26 @Data
22 -public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration { 27 +public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
23 28
24 private String relationType; 29 private String relationType;
25 private EntitySearchDirection direction; 30 private EntitySearchDirection direction;
  31 +
  32 + @Override
  33 + public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
  34 + TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
  35 + Map<String, String> attrMapping = new HashMap<>();
  36 + attrMapping.putIfAbsent("temperature", "tempo");
  37 + configuration.setAttrMapping(attrMapping);
  38 + configuration.setTelemetry(true);
  39 + configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
  40 + configuration.setDirection(EntitySearchDirection.FROM);
  41 + return configuration;
  42 + }
26 } 43 }
@@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
26 @RuleNode( 26 @RuleNode(
27 type = ComponentType.ENRICHMENT, 27 type = ComponentType.ENRICHMENT,
28 name="related attributes", 28 name="related attributes",
  29 + configClazz = TbGetRelatedAttrNodeConfiguration.class,
29 nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata", 30 nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
30 nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + 31 nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
31 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + 32 "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
@@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType; @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.plugin.ComponentType;
28 @RuleNode( 28 @RuleNode(
29 type = ComponentType.ENRICHMENT, 29 type = ComponentType.ENRICHMENT,
30 name="tenant attributes", 30 name="tenant attributes",
  31 + configClazz = TbGetEntityAttrNodeConfiguration.class,
31 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", 32 nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
32 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + 33 nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
33 "To access those attributes in other nodes this template can be used " + 34 "To access those attributes in other nodes this template can be used " +
@@ -36,6 +36,7 @@ import java.util.HashSet; @@ -36,6 +36,7 @@ import java.util.HashSet;
36 @RuleNode( 36 @RuleNode(
37 type = ComponentType.TRANSFORMATION, 37 type = ComponentType.TRANSFORMATION,
38 name="change originator", 38 name="change originator",
  39 + configClazz = TbChangeOriginatorNodeConfiguration.class,
39 nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity", 40 nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity",
40 nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + 41 nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
41 "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ") 42 "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ")
@@ -16,12 +16,24 @@ @@ -16,12 +16,24 @@
16 package org.thingsboard.rule.engine.transform; 16 package org.thingsboard.rule.engine.transform;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
  20 +import org.thingsboard.server.common.data.relation.EntityRelation;
19 import org.thingsboard.server.common.data.relation.EntitySearchDirection; 21 import org.thingsboard.server.common.data.relation.EntitySearchDirection;
20 22
21 @Data 23 @Data
22 -public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration{ 24 +public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
23 25
24 private String originatorSource; 26 private String originatorSource;
25 private EntitySearchDirection direction; 27 private EntitySearchDirection direction;
26 private String relationType; 28 private String relationType;
  29 +
  30 + @Override
  31 + public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
  32 + TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
  33 + configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
  34 + configuration.setDirection(EntitySearchDirection.FROM);
  35 + configuration.setRelationType(EntityRelation.CONTAINS_TYPE);
  36 + configuration.setStartNewChain(false);
  37 + return configuration;
  38 + }
27 } 39 }
@@ -27,6 +27,7 @@ import javax.script.Bindings; @@ -27,6 +27,7 @@ import javax.script.Bindings;
27 @RuleNode( 27 @RuleNode(
28 type = ComponentType.TRANSFORMATION, 28 type = ComponentType.TRANSFORMATION,
29 name = "script", 29 name = "script",
  30 + configClazz = TbTransformMsgNodeConfiguration.class,
30 nodeDescription = "Change Message payload and Metadata using JavaScript", 31 nodeDescription = "Change Message payload and Metadata using JavaScript",
31 nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + 32 nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " +
32 "<code>meta</code> - is a Message metadata.<br/>" + 33 "<code>meta</code> - is a Message metadata.<br/>" +
@@ -16,9 +16,18 @@ @@ -16,9 +16,18 @@
16 package org.thingsboard.rule.engine.transform; 16 package org.thingsboard.rule.engine.transform;
17 17
18 import lombok.Data; 18 import lombok.Data;
  19 +import org.thingsboard.rule.engine.api.NodeConfiguration;
19 20
20 @Data 21 @Data
21 -public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration { 22 +public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
22 23
23 private String jsScript; 24 private String jsScript;
  25 +
  26 + @Override
  27 + public TbTransformMsgNodeConfiguration defaultConfiguration() {
  28 + TbTransformMsgNodeConfiguration configuration = new TbTransformMsgNodeConfiguration();
  29 + configuration.setStartNewChain(false);
  30 + configuration.setJsScript("msg.passed = msg.passed * meta.temp; msg.bigObj.newProp = 'Ukraine' ");
  31 + return configuration;
  32 + }
24 } 33 }
@@ -153,16 +153,21 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService) @@ -153,16 +153,21 @@ function RuleChainService($http, $q, $filter, types, componentDescriptorService)
153 return deferred.promise; 153 return deferred.promise;
154 } 154 }
155 155
156 - function getRuleNodeSupportedLinks(nodeType) { //eslint-disable-line  
157 - //TODO:  
158 - var deferred = $q.defer();  
159 - var linkLabels = [  
160 - { name: 'Success', custom: false },  
161 - { name: 'Fail', custom: false },  
162 - { name: 'Custom', custom: true },  
163 - ];  
164 - deferred.resolve(linkLabels);  
165 - return deferred.promise; 156 + function getRuleNodeSupportedLinks(component) {
  157 + var relationTypes = component.configurationDescriptor.nodeDefinition.relationTypes;
  158 + var customRelations = component.configurationDescriptor.nodeDefinition.customRelations;
  159 + var linkLabels = [];
  160 + for (var i=0;i<relationTypes.length;i++) {
  161 + linkLabels.push({
  162 + name: relationTypes[i], custom: false
  163 + });
  164 + }
  165 + if (customRelations) {
  166 + linkLabels.push(
  167 + { name: 'Custom', custom: true }
  168 + );
  169 + }
  170 + return linkLabels;
166 } 171 }
167 172
168 function getRuleNodeComponents() { 173 function getRuleNodeComponents() {
  1 +/*
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +import './json-object-edit.scss';
  17 +
  18 +import 'brace/ext/language_tools';
  19 +import 'brace/mode/json';
  20 +import 'ace-builds/src-min-noconflict/snippets/json';
  21 +
  22 +/* eslint-disable import/no-unresolved, import/default */
  23 +
  24 +import jsonObjectEditTemplate from './json-object-edit.tpl.html';
  25 +
  26 +/* eslint-enable import/no-unresolved, import/default */
  27 +
  28 +export default angular.module('thingsboard.directives.jsonObjectEdit', [])
  29 + .directive('tbJsonObjectEdit', JsonObjectEdit)
  30 + .name;
  31 +
  32 +/*@ngInject*/
  33 +function JsonObjectEdit($compile, $templateCache, toast, utils) {
  34 +
  35 + var linker = function (scope, element, attrs, ngModelCtrl) {
  36 + var template = $templateCache.get(jsonObjectEditTemplate);
  37 + element.html(template);
  38 +
  39 + scope.label = attrs.label;
  40 +
  41 + scope.objectValid = true;
  42 + scope.validationError = '';
  43 +
  44 + scope.json_editor;
  45 +
  46 + scope.onFullscreenChanged = function () {
  47 + updateEditorSize();
  48 + };
  49 +
  50 + function updateEditorSize() {
  51 + if (scope.json_editor) {
  52 + scope.json_editor.resize();
  53 + scope.json_editor.renderer.updateFull();
  54 + }
  55 + }
  56 +
  57 + scope.jsonEditorOptions = {
  58 + useWrapMode: true,
  59 + mode: 'json',
  60 + advanced: {
  61 + enableSnippets: true,
  62 + enableBasicAutocompletion: true,
  63 + enableLiveAutocompletion: true
  64 + },
  65 + onLoad: function (_ace) {
  66 + scope.json_editor = _ace;
  67 + scope.json_editor.session.on("change", function () {
  68 + scope.cleanupJsonErrors();
  69 + });
  70 + }
  71 + };
  72 +
  73 + scope.cleanupJsonErrors = function () {
  74 + toast.hide();
  75 + };
  76 +
  77 + scope.updateValidity = function () {
  78 + ngModelCtrl.$setValidity('objectValid', scope.objectValid);
  79 + };
  80 +
  81 + scope.$watch('contentBody', function (newVal, prevVal) {
  82 + if (!angular.equals(newVal, prevVal)) {
  83 + var object = scope.validate();
  84 + ngModelCtrl.$setViewValue(object);
  85 + scope.updateValidity();
  86 + }
  87 + });
  88 +
  89 + ngModelCtrl.$render = function () {
  90 + var object = ngModelCtrl.$viewValue;
  91 + var content = '';
  92 + try {
  93 + if (object) {
  94 + content = angular.toJson(object, true);
  95 + }
  96 + } catch (e) {
  97 + //
  98 + }
  99 + scope.contentBody = content;
  100 + };
  101 +
  102 + scope.showError = function (error) {
  103 + var toastParent = angular.element('#tb-json-panel', element);
  104 + toast.showError(error, toastParent, 'bottom left');
  105 + };
  106 +
  107 + scope.validate = function () {
  108 + if (!scope.contentBody || !scope.contentBody.length) {
  109 + if (scope.required) {
  110 + scope.validationError = 'Json object is required.';
  111 + scope.objectValid = false;
  112 + } else {
  113 + scope.validationError = '';
  114 + scope.objectValid = true;
  115 + }
  116 + return null;
  117 + } else {
  118 + try {
  119 + var object = angular.fromJson(scope.contentBody);
  120 + scope.validationError = '';
  121 + scope.objectValid = true;
  122 + return object;
  123 + } catch (e) {
  124 + var details = utils.parseException(e);
  125 + var errorInfo = 'Error:';
  126 + if (details.name) {
  127 + errorInfo += ' ' + details.name + ':';
  128 + }
  129 + if (details.message) {
  130 + errorInfo += ' ' + details.message;
  131 + }
  132 + scope.validationError = errorInfo;
  133 + scope.objectValid = false;
  134 + return null;
  135 + }
  136 + }
  137 + };
  138 +
  139 + scope.$on('form-submit', function () {
  140 + if (!scope.readonly) {
  141 + scope.cleanupJsonErrors();
  142 + if (!scope.objectValid) {
  143 + scope.showError(scope.validationError);
  144 + }
  145 + }
  146 + });
  147 +
  148 + scope.$on('update-ace-editor-size', function () {
  149 + updateEditorSize();
  150 + });
  151 +
  152 + $compile(element.contents())(scope);
  153 + }
  154 +
  155 + return {
  156 + restrict: "E",
  157 + require: "^ngModel",
  158 + scope: {
  159 + required:'=ngRequired',
  160 + readonly:'=ngReadonly',
  161 + fillHeight:'=?'
  162 + },
  163 + link: linker
  164 + };
  165 +}
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +tb-json-object-edit {
  17 + position: relative;
  18 + .fill-height {
  19 + height: 100%;
  20 + }
  21 +}
  22 +
  23 +.tb-json-object-panel {
  24 + margin-left: 15px;
  25 + border: 1px solid #C0C0C0;
  26 + height: 100%;
  27 + #tb-json-input {
  28 + min-width: 200px;
  29 + width: 100%;
  30 + height: 100%;
  31 + &:not(.fill-height) {
  32 + min-height: 200px;
  33 + }
  34 + }
  35 +}
  1 +<!--
  2 +
  3 + Copyright © 2016-2018 The Thingsboard Authors
  4 +
  5 + Licensed under the Apache License, Version 2.0 (the "License");
  6 + you may not use this file except in compliance with the License.
  7 + You may obtain a copy of the License at
  8 +
  9 + http://www.apache.org/licenses/LICENSE-2.0
  10 +
  11 + Unless required by applicable law or agreed to in writing, software
  12 + distributed under the License is distributed on an "AS IS" BASIS,
  13 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 + See the License for the specific language governing permissions and
  15 + limitations under the License.
  16 +
  17 +-->
  18 +<div style="background: #fff;" ng-class="{'fill-height': fillHeight}" tb-expand-fullscreen fullscreen-zindex="100" expand-button-id="expand-button" on-fullscreen-changed="onFullscreenChanged()" layout="column">
  19 + <div layout="row" layout-align="start center">
  20 + <label class="tb-title no-padding"
  21 + ng-class="{'tb-required': required,
  22 + 'tb-readonly': readonly,
  23 + 'tb-error': !objectValid}">{{ label }}</label>
  24 + <span flex></span>
  25 + <md-button id="expand-button" aria-label="Fullscreen" class="md-icon-button tb-md-32 tb-fullscreen-button-style"></md-button>
  26 + </div>
  27 + <div flex id="tb-json-panel" class="tb-json-object-panel" layout="column">
  28 + <div flex id="tb-json-input" ng-class="{'fill-height': fillHeight}"
  29 + ng-readonly="readonly"
  30 + ui-ace="jsonEditorOptions"
  31 + ng-model="contentBody">
  32 + </div>
  33 + </div>
  34 +</div>
@@ -29,6 +29,7 @@ import thingsboardNoAnimate from '../components/no-animate.directive'; @@ -29,6 +29,7 @@ import thingsboardNoAnimate from '../components/no-animate.directive';
29 import thingsboardOnFinishRender from '../components/finish-render.directive'; 29 import thingsboardOnFinishRender from '../components/finish-render.directive';
30 import thingsboardSideMenu from '../components/side-menu.directive'; 30 import thingsboardSideMenu from '../components/side-menu.directive';
31 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive'; 31 import thingsboardDashboardAutocomplete from '../components/dashboard-autocomplete.directive';
  32 +import thingsboardJsonObjectEdit from '../components/json-object-edit.directive';
32 33
33 import thingsboardUserMenu from './user-menu.directive'; 34 import thingsboardUserMenu from './user-menu.directive';
34 35
@@ -90,7 +91,8 @@ export default angular.module('thingsboard.home', [ @@ -90,7 +91,8 @@ export default angular.module('thingsboard.home', [
90 thingsboardNoAnimate, 91 thingsboardNoAnimate,
91 thingsboardOnFinishRender, 92 thingsboardOnFinishRender,
92 thingsboardSideMenu, 93 thingsboardSideMenu,
93 - thingsboardDashboardAutocomplete 94 + thingsboardDashboardAutocomplete,
  95 + thingsboardJsonObjectEdit
94 ]) 96 ])
95 .config(HomeRoutes) 97 .config(HomeRoutes)
96 .controller('HomeController', HomeController) 98 .controller('HomeController', HomeController)
@@ -1179,6 +1179,7 @@ export default angular.module('thingsboard.locale', []) @@ -1179,6 +1179,7 @@ export default angular.module('thingsboard.locale', [])
1179 "delete": "Delete rule node", 1179 "delete": "Delete rule node",
1180 "rulenode-details": "Rule node details", 1180 "rulenode-details": "Rule node details",
1181 "debug-mode": "Debug mode", 1181 "debug-mode": "Debug mode",
  1182 + "configuration": "Configuration",
1182 "link-details": "Rule node link details", 1183 "link-details": "Rule node link details",
1183 "add-link": "Add link", 1184 "add-link": "Add link",
1184 "link-label": "Link label", 1185 "link-label": "Link label",
@@ -151,6 +151,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -151,6 +151,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
151 }, 151 },
152 'mouseLeave': function () { 152 'mouseLeave': function () {
153 destroyTooltips(); 153 destroyTooltips();
  154 + },
  155 + 'mouseDown': function () {
  156 + destroyTooltips();
154 } 157 }
155 } 158 }
156 }; 159 };
@@ -226,16 +229,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -226,16 +229,12 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
226 edgeDoubleClick: function (event, edge) { 229 edgeDoubleClick: function (event, edge) {
227 var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source); 230 var sourceNode = vm.modelservice.nodes.getNodeByConnectorId(edge.source);
228 if (sourceNode.component.type != types.ruleNodeType.INPUT.value) { 231 if (sourceNode.component.type != types.ruleNodeType.INPUT.value) {
229 - ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(  
230 - (labels) => {  
231 - vm.isEditingRuleNode = false;  
232 - vm.editingRuleNode = null;  
233 - vm.editingRuleNodeLinkLabels = labels;  
234 - vm.isEditingRuleNodeLink = true;  
235 - vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);  
236 - vm.editingRuleNodeLink = angular.copy(edge);  
237 - }  
238 - ); 232 + vm.isEditingRuleNode = false;
  233 + vm.editingRuleNode = null;
  234 + vm.editingRuleNodeLinkLabels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
  235 + vm.isEditingRuleNodeLink = true;
  236 + vm.editingRuleNodeLinkIndex = vm.ruleChainModel.edges.indexOf(edge);
  237 + vm.editingRuleNodeLink = angular.copy(edge);
239 } 238 }
240 }, 239 },
241 nodeCallbacks: { 240 nodeCallbacks: {
@@ -267,16 +266,10 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -267,16 +266,10 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
267 deferred.resolve(edge); 266 deferred.resolve(edge);
268 } 267 }
269 } else { 268 } else {
270 - ruleChainService.getRuleNodeSupportedLinks(sourceNode.component.clazz).then(  
271 - (labels) => {  
272 - addRuleNodeLink(event, edge, labels).then(  
273 - (link) => {  
274 - deferred.resolve(link);  
275 - },  
276 - () => {  
277 - deferred.reject();  
278 - }  
279 - ); 269 + var labels = ruleChainService.getRuleNodeSupportedLinks(sourceNode.component);
  270 + addRuleNodeLink(event, edge, labels).then(
  271 + (link) => {
  272 + deferred.resolve(link);
280 }, 273 },
281 () => { 274 () => {
282 deferred.reject(); 275 deferred.reject();
@@ -309,24 +302,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -309,24 +302,19 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
309 y: 10+50*model.nodes.length, 302 y: 10+50*model.nodes.length,
310 connectors: [] 303 connectors: []
311 }; 304 };
312 - if (componentType == types.ruleNodeType.RULE_CHAIN.value) {  
313 - node.connectors.push(  
314 - {  
315 - type: flowchartConstants.leftConnectorType,  
316 - id: model.nodes.length  
317 - }  
318 - );  
319 - } else { 305 + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.inEnabled) {
320 node.connectors.push( 306 node.connectors.push(
321 { 307 {
322 type: flowchartConstants.leftConnectorType, 308 type: flowchartConstants.leftConnectorType,
323 - id: model.nodes.length*2 309 + id: model.nodes.length * 2
324 } 310 }
325 ); 311 );
  312 + }
  313 + if (ruleNodeComponent.configurationDescriptor.nodeDefinition.outEnabled) {
326 node.connectors.push( 314 node.connectors.push(
327 { 315 {
328 type: flowchartConstants.rightConnectorType, 316 type: flowchartConstants.rightConnectorType,
329 - id: model.nodes.length*2+1 317 + id: model.nodes.length * 2 + 1
330 } 318 }
331 ); 319 );
332 } 320 }
@@ -398,17 +386,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -398,17 +386,24 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
398 name: ruleNode.name, 386 name: ruleNode.name,
399 nodeClass: vm.types.ruleNodeType[component.type].nodeClass, 387 nodeClass: vm.types.ruleNodeType[component.type].nodeClass,
400 icon: vm.types.ruleNodeType[component.type].icon, 388 icon: vm.types.ruleNodeType[component.type].icon,
401 - connectors: [ 389 + connectors: []
  390 + };
  391 + if (component.configurationDescriptor.nodeDefinition.inEnabled) {
  392 + node.connectors.push(
402 { 393 {
403 type: flowchartConstants.leftConnectorType, 394 type: flowchartConstants.leftConnectorType,
404 id: vm.nextConnectorID++ 395 id: vm.nextConnectorID++
405 - }, 396 + }
  397 + );
  398 + }
  399 + if (component.configurationDescriptor.nodeDefinition.outEnabled) {
  400 + node.connectors.push(
406 { 401 {
407 type: flowchartConstants.rightConnectorType, 402 type: flowchartConstants.rightConnectorType,
408 id: vm.nextConnectorID++ 403 id: vm.nextConnectorID++
409 } 404 }
410 - ]  
411 - }; 405 + );
  406 + }
412 nodes.push(node); 407 nodes.push(node);
413 vm.ruleChainModel.nodes.push(node); 408 vm.ruleChainModel.nodes.push(node);
414 } 409 }
@@ -590,6 +585,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -590,6 +585,9 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
590 } 585 }
591 586
592 function addRuleNode($event, ruleNode) { 587 function addRuleNode($event, ruleNode) {
  588 +
  589 + ruleNode.configuration = angular.copy(ruleNode.component.configurationDescriptor.nodeDefinition.defaultConfiguration);
  590 +
593 $mdDialog.show({ 591 $mdDialog.show({
594 controller: 'AddRuleNodeController', 592 controller: 'AddRuleNodeController',
595 controllerAs: 'vm', 593 controllerAs: 'vm',
@@ -601,13 +599,15 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, @@ -601,13 +599,15 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil,
601 }).then(function (ruleNode) { 599 }).then(function (ruleNode) {
602 ruleNode.id = vm.nextNodeID++; 600 ruleNode.id = vm.nextNodeID++;
603 ruleNode.connectors = []; 601 ruleNode.connectors = [];
604 - ruleNode.connectors.push(  
605 - {  
606 - id: vm.nextConnectorID++,  
607 - type: flowchartConstants.leftConnectorType  
608 - }  
609 - );  
610 - if (ruleNode.component.type != types.ruleNodeType.RULE_CHAIN.value) { 602 + if (ruleNode.component.configurationDescriptor.nodeDefinition.inEnabled) {
  603 + ruleNode.connectors.push(
  604 + {
  605 + id: vm.nextConnectorID++,
  606 + type: flowchartConstants.leftConnectorType
  607 + }
  608 + );
  609 + }
  610 + if (ruleNode.component.configurationDescriptor.nodeDefinition.outEnabled) {
611 ruleNode.connectors.push( 611 ruleNode.connectors.push(
612 { 612 {
613 id: vm.nextConnectorID++, 613 id: vm.nextConnectorID++,
@@ -38,6 +38,11 @@ @@ -38,6 +38,11 @@
38 ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }} 38 ng-model="ruleNode.debugMode">{{ 'rulenode.debug-mode' | translate }}
39 </md-checkbox> 39 </md-checkbox>
40 </md-input-container> 40 </md-input-container>
  41 + <tb-json-object-edit class="tb-rule-node-configuration-json" ng-model="ruleNode.configuration"
  42 + label="{{ 'rulenode.configuration' | translate }}"
  43 + ng-required="true"
  44 + fill-height="true">
  45 + </tb-json-object-edit>
41 <md-input-container class="md-block"> 46 <md-input-container class="md-block">
42 <label translate>rulenode.description</label> 47 <label translate>rulenode.description</label>
43 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea> 48 <textarea ng-model="ruleNode.additionalInfo.description" rows="2"></textarea>
@@ -14,6 +14,8 @@ @@ -14,6 +14,8 @@
14 * limitations under the License. 14 * limitations under the License.
15 */ 15 */
16 16
  17 +import './rulenode.scss';
  18 +
17 /* eslint-disable import/no-unresolved, import/default */ 19 /* eslint-disable import/no-unresolved, import/default */
18 20
19 import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html'; 21 import ruleNodeFieldsetTemplate from './rulenode-fieldset.tpl.html';
  1 +/**
  2 + * Copyright © 2016-2018 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +
  17 +.tb-rulenode {
  18 + tb-json-object-edit.tb-rule-node-configuration-json {
  19 + height: 300px;
  20 + display: block;
  21 + }
  22 +}
@@ -19,7 +19,7 @@ @@ -19,7 +19,7 @@
19 id="{{node.id}}" 19 id="{{node.id}}"
20 ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;" 20 ng-attr-style="position: absolute; top: {{ node.y }}px; left: {{ node.x }}px;"
21 ng-dblclick="callbacks.doubleClick($event, node)" 21 ng-dblclick="callbacks.doubleClick($event, node)"
22 - ng-mouseover="callbacks.mouseOver($event, node)" 22 + ng-mousedown="callbacks.mouseDown($event, node)"
23 ng-mouseenter="callbacks.mouseEnter($event, node)" 23 ng-mouseenter="callbacks.mouseEnter($event, node)"
24 ng-mouseleave="callbacks.mouseLeave($event, node)"> 24 ng-mouseleave="callbacks.mouseLeave($event, node)">
25 <div class="tb-rule-node {{node.nodeClass}}"> 25 <div class="tb-rule-node {{node.nodeClass}}">
@@ -203,6 +203,12 @@ md-sidenav { @@ -203,6 +203,12 @@ md-sidenav {
203 * THINGSBOARD SPECIFIC 203 * THINGSBOARD SPECIFIC
204 ***********************/ 204 ***********************/
205 205
  206 +$swift-ease-out-duration: 0.4s !default;
  207 +$swift-ease-out-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1) !default;
  208 +
  209 +$input-label-float-offset: 6px !default;
  210 +$input-label-float-scale: 0.75 !default;
  211 +
206 label { 212 label {
207 &.tb-title { 213 &.tb-title {
208 pointer-events: none; 214 pointer-events: none;
@@ -213,6 +219,18 @@ label { @@ -213,6 +219,18 @@ label {
213 &.no-padding { 219 &.no-padding {
214 padding-bottom: 0px; 220 padding-bottom: 0px;
215 } 221 }
  222 + &.tb-required:after {
  223 + content: ' *';
  224 + font-size: 13px;
  225 + vertical-align: top;
  226 + color: rgba(0,0,0,0.54);
  227 + }
  228 + &.tb-error {
  229 + color: rgb(221,44,0);
  230 + &.tb-required:after {
  231 + color: rgb(221,44,0);
  232 + }
  233 + }
216 } 234 }
217 } 235 }
218 236