Showing
18 changed files
with
173 additions
and
197 deletions
@@ -16,6 +16,8 @@ | @@ -16,6 +16,8 @@ | ||
16 | package org.thingsboard.server.service.component; | 16 | package org.thingsboard.server.service.component; |
17 | 17 | ||
18 | import com.fasterxml.jackson.databind.ObjectMapper; | 18 | import com.fasterxml.jackson.databind.ObjectMapper; |
19 | +import com.fasterxml.jackson.databind.node.ObjectNode; | ||
20 | +import com.fasterxml.jackson.databind.JsonNode; | ||
19 | import com.google.common.base.Charsets; | 21 | import com.google.common.base.Charsets; |
20 | import com.google.common.io.Resources; | 22 | import com.google.common.io.Resources; |
21 | import lombok.extern.slf4j.Slf4j; | 23 | import lombok.extern.slf4j.Slf4j; |
@@ -26,16 +28,14 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen | @@ -26,16 +28,14 @@ import org.springframework.context.annotation.ClassPathScanningCandidateComponen | ||
26 | import org.springframework.core.env.Environment; | 28 | import org.springframework.core.env.Environment; |
27 | import org.springframework.core.type.filter.AnnotationTypeFilter; | 29 | import org.springframework.core.type.filter.AnnotationTypeFilter; |
28 | import org.springframework.stereotype.Service; | 30 | import org.springframework.stereotype.Service; |
29 | -import org.thingsboard.rule.engine.api.ActionNode; | ||
30 | -import org.thingsboard.rule.engine.api.EnrichmentNode; | ||
31 | -import org.thingsboard.rule.engine.api.FilterNode; | ||
32 | -import org.thingsboard.rule.engine.api.TransformationNode; | 31 | +import org.thingsboard.rule.engine.api.*; |
33 | import org.thingsboard.server.common.data.plugin.ComponentDescriptor; | 32 | import org.thingsboard.server.common.data.plugin.ComponentDescriptor; |
34 | import org.thingsboard.server.common.data.plugin.ComponentType; | 33 | import org.thingsboard.server.common.data.plugin.ComponentType; |
35 | import org.thingsboard.server.dao.component.ComponentDescriptorService; | 34 | import org.thingsboard.server.dao.component.ComponentDescriptorService; |
36 | import org.thingsboard.server.extensions.api.component.*; | 35 | import org.thingsboard.server.extensions.api.component.*; |
37 | 36 | ||
38 | import javax.annotation.PostConstruct; | 37 | import javax.annotation.PostConstruct; |
38 | +import java.io.IOException; | ||
39 | import java.lang.annotation.Annotation; | 39 | import java.lang.annotation.Annotation; |
40 | import java.util.*; | 40 | import java.util.*; |
41 | import java.util.stream.Collectors; | 41 | import java.util.stream.Collectors; |
@@ -70,6 +70,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | @@ -70,6 +70,24 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | ||
70 | } | 70 | } |
71 | } | 71 | } |
72 | 72 | ||
73 | + private void registerRuleNodeComponents() { | ||
74 | + Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class); | ||
75 | + for (BeanDefinition def : ruleNodeBeanDefinitions) { | ||
76 | + try { | ||
77 | + String clazzName = def.getBeanClassName(); | ||
78 | + Class<?> clazz = Class.forName(clazzName); | ||
79 | + RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); | ||
80 | + ComponentType type = ruleNodeAnnotation.type(); | ||
81 | + ComponentDescriptor component = scanAndPersistComponent(def, type); | ||
82 | + components.put(component.getClazz(), component); | ||
83 | + componentsMap.computeIfAbsent(type, k -> new ArrayList<>()).add(component); | ||
84 | + } catch (Exception e) { | ||
85 | + log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e); | ||
86 | + throw new RuntimeException(e); | ||
87 | + } | ||
88 | + } | ||
89 | + } | ||
90 | + | ||
73 | private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) { | 91 | private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) { |
74 | List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type); | 92 | List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type); |
75 | componentsMap.put(type, components); | 93 | componentsMap.put(type, components); |
@@ -97,34 +115,25 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | @@ -97,34 +115,25 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | ||
97 | String descriptorResourceName; | 115 | String descriptorResourceName; |
98 | switch (type) { | 116 | switch (type) { |
99 | case ENRICHMENT: | 117 | case ENRICHMENT: |
100 | - EnrichmentNode enrichmentAnnotation = clazz.getAnnotation(EnrichmentNode.class); | ||
101 | - scannedComponent.setName(enrichmentAnnotation.name()); | ||
102 | - scannedComponent.setScope(enrichmentAnnotation.scope()); | ||
103 | - descriptorResourceName = enrichmentAnnotation.descriptor(); | ||
104 | - break; | ||
105 | case FILTER: | 118 | case FILTER: |
106 | - FilterNode filterAnnotation = clazz.getAnnotation(FilterNode.class); | ||
107 | - scannedComponent.setName(filterAnnotation.name()); | ||
108 | - scannedComponent.setScope(filterAnnotation.scope()); | ||
109 | - descriptorResourceName = filterAnnotation.descriptor(); | ||
110 | - break; | ||
111 | case TRANSFORMATION: | 119 | case TRANSFORMATION: |
112 | - TransformationNode trAnnotation = clazz.getAnnotation(TransformationNode.class); | ||
113 | - scannedComponent.setName(trAnnotation.name()); | ||
114 | - scannedComponent.setScope(trAnnotation.scope()); | ||
115 | - descriptorResourceName = trAnnotation.descriptor(); | ||
116 | - break; | ||
117 | case ACTION: | 120 | case ACTION: |
118 | - ActionNode actionAnnotation = clazz.getAnnotation(ActionNode.class); | ||
119 | - scannedComponent.setName(actionAnnotation.name()); | ||
120 | - scannedComponent.setScope(actionAnnotation.scope()); | ||
121 | - descriptorResourceName = actionAnnotation.descriptor(); | 121 | + RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); |
122 | + scannedComponent.setName(ruleNodeAnnotation.name()); | ||
123 | + scannedComponent.setScope(ruleNodeAnnotation.scope()); | ||
124 | + NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); | ||
125 | + ObjectNode configurationDescriptor = mapper.createObjectNode(); | ||
126 | + JsonNode node = mapper.valueToTree(nodeDefinition); | ||
127 | + configurationDescriptor.set("nodeDefinition", node); | ||
128 | + scannedComponent.setConfigurationDescriptor(configurationDescriptor); | ||
122 | break; | 129 | break; |
123 | case OLD_ACTION: | 130 | case OLD_ACTION: |
124 | Action oldActionAnnotation = clazz.getAnnotation(Action.class); | 131 | Action oldActionAnnotation = clazz.getAnnotation(Action.class); |
125 | scannedComponent.setName(oldActionAnnotation.name()); | 132 | scannedComponent.setName(oldActionAnnotation.name()); |
126 | scannedComponent.setScope(oldActionAnnotation.scope()); | 133 | scannedComponent.setScope(oldActionAnnotation.scope()); |
127 | descriptorResourceName = oldActionAnnotation.descriptor(); | 134 | descriptorResourceName = oldActionAnnotation.descriptor(); |
135 | + scannedComponent.setConfigurationDescriptor(mapper.readTree( | ||
136 | + Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8))); | ||
128 | break; | 137 | break; |
129 | case PLUGIN: | 138 | case PLUGIN: |
130 | Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class); | 139 | Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class); |
@@ -143,12 +152,12 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | @@ -143,12 +152,12 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | ||
143 | } | 152 | } |
144 | } | 153 | } |
145 | scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(Class::getName).collect(Collectors.joining(","))); | 154 | scannedComponent.setActions(Arrays.stream(pluginAnnotation.actions()).map(Class::getName).collect(Collectors.joining(","))); |
155 | + scannedComponent.setConfigurationDescriptor(mapper.readTree( | ||
156 | + Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8))); | ||
146 | break; | 157 | break; |
147 | default: | 158 | default: |
148 | throw new RuntimeException(type + " is not supported yet!"); | 159 | throw new RuntimeException(type + " is not supported yet!"); |
149 | } | 160 | } |
150 | - scannedComponent.setConfigurationDescriptor(mapper.readTree( | ||
151 | - Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8))); | ||
152 | scannedComponent.setClazz(clazzName); | 161 | scannedComponent.setClazz(clazzName); |
153 | log.info("Processing scanned component: {}", scannedComponent); | 162 | log.info("Processing scanned component: {}", scannedComponent); |
154 | } catch (Exception e) { | 163 | } catch (Exception e) { |
@@ -171,6 +180,20 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | @@ -171,6 +180,20 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | ||
171 | return scannedComponent; | 180 | return scannedComponent; |
172 | } | 181 | } |
173 | 182 | ||
183 | + private NodeDefinition prepareNodeDefinition(RuleNode nodeAnnotation) throws IOException { | ||
184 | + NodeDefinition nodeDefinition = new NodeDefinition(); | ||
185 | + nodeDefinition.setDetails(nodeAnnotation.nodeDetails()); | ||
186 | + nodeDefinition.setDescription(nodeAnnotation.nodeDescription()); | ||
187 | + nodeDefinition.setInEnabled(nodeAnnotation.inEnabled()); | ||
188 | + nodeDefinition.setOutEnabled(nodeAnnotation.outEnabled()); | ||
189 | + nodeDefinition.setRelationTypes(nodeAnnotation.relationTypes()); | ||
190 | + nodeDefinition.setCustomRelations(nodeAnnotation.customRelations()); | ||
191 | + String defaultConfigResourceName = nodeAnnotation.defaultConfigResource(); | ||
192 | + nodeDefinition.setDefaultConfiguration(mapper.readTree( | ||
193 | + Resources.toString(Resources.getResource(defaultConfigResourceName), Charsets.UTF_8))); | ||
194 | + return nodeDefinition; | ||
195 | + } | ||
196 | + | ||
174 | private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) { | 197 | private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) { |
175 | ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); | 198 | ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); |
176 | scanner.addIncludeFilter(new AnnotationTypeFilter(componentType)); | 199 | scanner.addIncludeFilter(new AnnotationTypeFilter(componentType)); |
@@ -183,13 +206,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | @@ -183,13 +206,8 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | ||
183 | 206 | ||
184 | @Override | 207 | @Override |
185 | public void discoverComponents() { | 208 | public void discoverComponents() { |
186 | - registerComponents(ComponentType.ENRICHMENT, EnrichmentNode.class); | ||
187 | - | ||
188 | - registerComponents(ComponentType.FILTER, FilterNode.class); | ||
189 | - | ||
190 | - registerComponents(ComponentType.TRANSFORMATION, TransformationNode.class); | ||
191 | 209 | ||
192 | - registerComponents(ComponentType.ACTION, ActionNode.class); | 210 | + registerRuleNodeComponents(); |
193 | 211 | ||
194 | registerComponents(ComponentType.OLD_ACTION, Action.class); | 212 | registerComponents(ComponentType.OLD_ACTION, Action.class); |
195 | 213 | ||
@@ -200,15 +218,19 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | @@ -200,15 +218,19 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe | ||
200 | 218 | ||
201 | @Override | 219 | @Override |
202 | public List<ComponentDescriptor> getComponents(ComponentType type) { | 220 | public List<ComponentDescriptor> getComponents(ComponentType type) { |
203 | - return Collections.unmodifiableList(componentsMap.get(type)); | 221 | + if (componentsMap.containsKey(type)) { |
222 | + return Collections.unmodifiableList(componentsMap.get(type)); | ||
223 | + } else { | ||
224 | + return Collections.emptyList(); | ||
225 | + } | ||
204 | } | 226 | } |
205 | 227 | ||
206 | @Override | 228 | @Override |
207 | public List<ComponentDescriptor> getComponents(Set<ComponentType> types) { | 229 | public List<ComponentDescriptor> getComponents(Set<ComponentType> types) { |
208 | List<ComponentDescriptor> result = new ArrayList<>(); | 230 | List<ComponentDescriptor> result = new ArrayList<>(); |
209 | - for (ComponentType type : types) { | 231 | + types.stream().filter(type -> componentsMap.containsKey(type)).forEach(type -> { |
210 | result.addAll(componentsMap.get(type)); | 232 | result.addAll(componentsMap.get(type)); |
211 | - } | 233 | + }); |
212 | return Collections.unmodifiableList(result); | 234 | return Collections.unmodifiableList(result); |
213 | } | 235 | } |
214 | 236 |
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/FilterNode.java
deleted
100644 → 0
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 | -import org.thingsboard.server.common.data.plugin.ComponentScope; | ||
19 | - | ||
20 | -import java.lang.annotation.ElementType; | ||
21 | -import java.lang.annotation.Retention; | ||
22 | -import java.lang.annotation.RetentionPolicy; | ||
23 | -import java.lang.annotation.Target; | ||
24 | - | ||
25 | -/** | ||
26 | - * @author Andrew Shvayka | ||
27 | - */ | ||
28 | -@Retention(RetentionPolicy.RUNTIME) | ||
29 | -@Target(ElementType.TYPE) | ||
30 | -public @interface FilterNode { | ||
31 | - | ||
32 | - String name(); | ||
33 | - | ||
34 | - String nodeDescription(); | ||
35 | - | ||
36 | - String nodeDetails(); | ||
37 | - | ||
38 | - boolean inEnabled() default true; | ||
39 | - | ||
40 | - boolean outEnabled() default true; | ||
41 | - | ||
42 | - ComponentScope scope() default ComponentScope.TENANT; | ||
43 | - | ||
44 | - String descriptor() default "EmptyNodeDescriptor.json"; | ||
45 | - | ||
46 | - String[] relationTypes() default {"Success", "Failure"}; | ||
47 | - | ||
48 | - boolean customRelations() default false; | ||
49 | - | ||
50 | -} |
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/NodeDefinition.java
renamed from
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/ActionNode.java
@@ -13,31 +13,21 @@ | @@ -13,31 +13,21 @@ | ||
13 | * See the License for the specific language governing permissions and | 13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | -package org.thingsboard.rule.engine.api; | ||
17 | - | ||
18 | -import org.thingsboard.server.common.data.plugin.ComponentScope; | ||
19 | -import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration; | ||
20 | - | ||
21 | -import java.lang.annotation.ElementType; | ||
22 | -import java.lang.annotation.Retention; | ||
23 | -import java.lang.annotation.RetentionPolicy; | ||
24 | -import java.lang.annotation.Target; | ||
25 | - | ||
26 | -/** | ||
27 | - * @author Andrew Shvayka | ||
28 | - */ | ||
29 | -@Retention(RetentionPolicy.RUNTIME) | ||
30 | -@Target(ElementType.TYPE) | ||
31 | -public @interface ActionNode { | ||
32 | 16 | ||
33 | - String name(); | ||
34 | - | ||
35 | - ComponentScope scope() default ComponentScope.TENANT; | 17 | +package org.thingsboard.rule.engine.api; |
36 | 18 | ||
37 | - String descriptor() default "EmptyNodeDescriptor.json"; | 19 | +import com.fasterxml.jackson.databind.JsonNode; |
20 | +import lombok.Data; | ||
38 | 21 | ||
39 | - String[] relationTypes() default {"Success","Failure"}; | 22 | +@Data |
23 | +public class NodeDefinition { | ||
40 | 24 | ||
41 | - boolean customRelations() default false; | 25 | + private String details; |
26 | + private String description; | ||
27 | + private boolean inEnabled; | ||
28 | + private boolean outEnabled; | ||
29 | + String[] relationTypes; | ||
30 | + boolean customRelations; | ||
31 | + JsonNode defaultConfiguration; | ||
42 | 32 | ||
43 | } | 33 | } |
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
renamed from
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/EnrichmentNode.java
@@ -16,18 +16,18 @@ | @@ -16,18 +16,18 @@ | ||
16 | package org.thingsboard.rule.engine.api; | 16 | package org.thingsboard.rule.engine.api; |
17 | 17 | ||
18 | import org.thingsboard.server.common.data.plugin.ComponentScope; | 18 | import org.thingsboard.server.common.data.plugin.ComponentScope; |
19 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
19 | 20 | ||
20 | import java.lang.annotation.ElementType; | 21 | import java.lang.annotation.ElementType; |
21 | import java.lang.annotation.Retention; | 22 | import java.lang.annotation.Retention; |
22 | import java.lang.annotation.RetentionPolicy; | 23 | import java.lang.annotation.RetentionPolicy; |
23 | import java.lang.annotation.Target; | 24 | import java.lang.annotation.Target; |
24 | 25 | ||
25 | -/** | ||
26 | - * @author Andrew Shvayka | ||
27 | - */ | ||
28 | @Retention(RetentionPolicy.RUNTIME) | 26 | @Retention(RetentionPolicy.RUNTIME) |
29 | @Target(ElementType.TYPE) | 27 | @Target(ElementType.TYPE) |
30 | -public @interface EnrichmentNode { | 28 | +public @interface RuleNode { |
29 | + | ||
30 | + ComponentType type(); | ||
31 | 31 | ||
32 | String name(); | 32 | String name(); |
33 | 33 | ||
@@ -41,7 +41,7 @@ public @interface EnrichmentNode { | @@ -41,7 +41,7 @@ public @interface EnrichmentNode { | ||
41 | 41 | ||
42 | ComponentScope scope() default ComponentScope.TENANT; | 42 | ComponentScope scope() default ComponentScope.TENANT; |
43 | 43 | ||
44 | - String descriptor() default "EmptyNodeDescriptor.json"; | 44 | + String defaultConfigResource() default "EmptyNodeConfig.json"; |
45 | 45 | ||
46 | String[] relationTypes() default {"Success", "Failure"}; | 46 | String[] relationTypes() default {"Success", "Failure"}; |
47 | 47 |
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TransformationNode.java
deleted
100644 → 0
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 | -import org.thingsboard.server.common.data.plugin.ComponentScope; | ||
19 | -import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration; | ||
20 | - | ||
21 | -import java.lang.annotation.ElementType; | ||
22 | -import java.lang.annotation.Retention; | ||
23 | -import java.lang.annotation.RetentionPolicy; | ||
24 | -import java.lang.annotation.Target; | ||
25 | - | ||
26 | -/** | ||
27 | - * @author Andrew Shvayka | ||
28 | - */ | ||
29 | -@Retention(RetentionPolicy.RUNTIME) | ||
30 | -@Target(ElementType.TYPE) | ||
31 | -public @interface TransformationNode { | ||
32 | - | ||
33 | - String name(); | ||
34 | - | ||
35 | - String nodeDescription(); | ||
36 | - | ||
37 | - String nodeDetails(); | ||
38 | - | ||
39 | - boolean inEnabled() default true; | ||
40 | - | ||
41 | - boolean outEnabled() default true; | ||
42 | - | ||
43 | - ComponentScope scope() default ComponentScope.TENANT; | ||
44 | - | ||
45 | - String descriptor() default "EmptyNodeDescriptor.json"; | ||
46 | - | ||
47 | - String[] relationTypes() default {"Success","Failure"}; | ||
48 | - | ||
49 | - boolean customRelations() default false; | ||
50 | - | ||
51 | -} |
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; | ||
19 | import org.thingsboard.rule.engine.TbNodeUtils; | 19 | import org.thingsboard.rule.engine.TbNodeUtils; |
20 | import org.thingsboard.rule.engine.api.*; | 20 | import org.thingsboard.rule.engine.api.*; |
21 | import org.thingsboard.rule.engine.js.NashornJsEngine; | 21 | import org.thingsboard.rule.engine.js.NashornJsEngine; |
22 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
22 | import org.thingsboard.server.common.msg.TbMsg; | 23 | import org.thingsboard.server.common.msg.TbMsg; |
23 | 24 | ||
24 | import javax.script.Bindings; | 25 | import javax.script.Bindings; |
@@ -26,12 +27,14 @@ import javax.script.Bindings; | @@ -26,12 +27,14 @@ import javax.script.Bindings; | ||
26 | import static org.thingsboard.rule.engine.DonAsynchron.withCallback; | 27 | import static org.thingsboard.rule.engine.DonAsynchron.withCallback; |
27 | 28 | ||
28 | @Slf4j | 29 | @Slf4j |
29 | -@FilterNode(name = "script", relationTypes = {"True", "False", "Failure"}, | 30 | +@RuleNode( |
31 | + type = ComponentType.FILTER, | ||
32 | + name = "script", relationTypes = {"True", "False", "Failure"}, | ||
30 | nodeDescription = "Filter incoming messages using JS script", | 33 | nodeDescription = "Filter incoming messages using JS script", |
31 | nodeDetails = "Evaluate incoming Message with configured JS condition. " + | 34 | nodeDetails = "Evaluate incoming Message with configured JS condition. " + |
32 | - "If 'True' - send Message via 'True' chain, otherwise 'False' chain is used." + | ||
33 | - "Message payload can be accessed via 'msg' property. For example 'msg.temperature < 10;'" + | ||
34 | - "Message metadata can be accessed via 'meta' property. For example 'meta.customerName === 'John';'") | 35 | + "If <b>True</b> - send Message via <b>True</b> chain, otherwise <b>False</b> chain is used." + |
36 | + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code>" + | ||
37 | + "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") | ||
35 | public class TbJsFilterNode implements TbNode { | 38 | public class TbJsFilterNode implements TbNode { |
36 | 39 | ||
37 | private TbJsFilterNodeConfiguration config; | 40 | private TbJsFilterNodeConfiguration config; |
@@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; | @@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; | ||
19 | import org.thingsboard.rule.engine.TbNodeUtils; | 19 | import org.thingsboard.rule.engine.TbNodeUtils; |
20 | import org.thingsboard.rule.engine.api.*; | 20 | import org.thingsboard.rule.engine.api.*; |
21 | import org.thingsboard.rule.engine.js.NashornJsEngine; | 21 | import org.thingsboard.rule.engine.js.NashornJsEngine; |
22 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
22 | import org.thingsboard.server.common.msg.TbMsg; | 23 | import org.thingsboard.server.common.msg.TbMsg; |
23 | 24 | ||
24 | import javax.script.Bindings; | 25 | import javax.script.Bindings; |
@@ -27,12 +28,14 @@ import java.util.Set; | @@ -27,12 +28,14 @@ import java.util.Set; | ||
27 | import static org.thingsboard.rule.engine.DonAsynchron.withCallback; | 28 | import static org.thingsboard.rule.engine.DonAsynchron.withCallback; |
28 | 29 | ||
29 | @Slf4j | 30 | @Slf4j |
30 | -@FilterNode(name = "switch", customRelations = true, | 31 | +@RuleNode( |
32 | + type = ComponentType.FILTER, | ||
33 | + name = "switch", customRelations = true, | ||
31 | nodeDescription = "Route incoming Message to one or multiple output chains", | 34 | nodeDescription = "Route incoming Message to one or multiple output chains", |
32 | nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + | 35 | nodeDetails = "Node executes configured JS script. Script should return array of next Chain names where Message should be routed. " + |
33 | "If Array is empty - message not routed to next Node. " + | 36 | "If Array is empty - message not routed to next Node. " + |
34 | - "Message payload can be accessed via 'msg' property. For example 'msg.temperature < 10;' " + | ||
35 | - "Message metadata can be accessed via 'meta' property. For example 'meta.customerName === 'John';' ") | 37 | + "Message payload can be accessed via <code>msg</code> property. For example <code>msg.temperature < 10;</code> " + |
38 | + "Message metadata can be accessed via <code>meta</code> property. For example <code>meta.customerName === 'John';</code>") | ||
36 | public class TbJsSwitchNode implements TbNode { | 39 | public class TbJsSwitchNode implements TbNode { |
37 | 40 | ||
38 | private TbJsSwitchNodeConfiguration config; | 41 | private TbJsSwitchNodeConfiguration config; |
@@ -18,16 +18,19 @@ package org.thingsboard.rule.engine.filter; | @@ -18,16 +18,19 @@ package org.thingsboard.rule.engine.filter; | ||
18 | import lombok.extern.slf4j.Slf4j; | 18 | import lombok.extern.slf4j.Slf4j; |
19 | import org.thingsboard.rule.engine.TbNodeUtils; | 19 | import org.thingsboard.rule.engine.TbNodeUtils; |
20 | import org.thingsboard.rule.engine.api.*; | 20 | import org.thingsboard.rule.engine.api.*; |
21 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
21 | import org.thingsboard.server.common.msg.TbMsg; | 22 | import org.thingsboard.server.common.msg.TbMsg; |
22 | 23 | ||
23 | /** | 24 | /** |
24 | * Created by ashvayka on 19.01.18. | 25 | * Created by ashvayka on 19.01.18. |
25 | */ | 26 | */ |
26 | @Slf4j | 27 | @Slf4j |
27 | -@FilterNode(name = "message type", | 28 | +@RuleNode( |
29 | + type = ComponentType.FILTER, | ||
30 | + name = "message type", | ||
28 | nodeDescription = "Filter incoming messages by Message Type", | 31 | nodeDescription = "Filter incoming messages by Message Type", |
29 | nodeDetails = "Evaluate incoming Message with configured JS condition. " + | 32 | nodeDetails = "Evaluate incoming Message with configured JS condition. " + |
30 | - "If incoming MessageType is expected - send Message via 'Success' chain, otherwise 'Failure' chain is used.") | 33 | + "If incoming MessageType is expected - send Message via <b>Success</b> chain, otherwise <b>Failure</b> chain is used.") |
31 | public class TbMsgTypeFilterNode implements TbNode { | 34 | public class TbMsgTypeFilterNode implements TbNode { |
32 | 35 | ||
33 | TbMsgTypeFilterNodeConfiguration config; | 36 | TbMsgTypeFilterNodeConfiguration config; |
@@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.TbNodeUtils; | @@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.TbNodeUtils; | ||
24 | import org.thingsboard.rule.engine.api.*; | 24 | import org.thingsboard.rule.engine.api.*; |
25 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; | 25 | import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
26 | import org.thingsboard.server.common.data.kv.TsKvEntry; | 26 | import org.thingsboard.server.common.data.kv.TsKvEntry; |
27 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
27 | import org.thingsboard.server.common.msg.TbMsg; | 28 | import org.thingsboard.server.common.msg.TbMsg; |
28 | 29 | ||
29 | import java.util.List; | 30 | import java.util.List; |
@@ -35,11 +36,12 @@ import static org.thingsboard.server.common.data.DataConstants.*; | @@ -35,11 +36,12 @@ import static org.thingsboard.server.common.data.DataConstants.*; | ||
35 | * Created by ashvayka on 19.01.18. | 36 | * Created by ashvayka on 19.01.18. |
36 | */ | 37 | */ |
37 | @Slf4j | 38 | @Slf4j |
38 | -@EnrichmentNode(name = "originator attributes", | ||
39 | - nodeDescription = "Add Message Originator Attributes or Latest Telemetry into Message Metadata", | ||
40 | - nodeDetails = "If Attributes enrichment configured, CLIENT/SHARED/SERVER attributes are added into Message metadata " + | ||
41 | - "with specific prefix: cs/shared/ss. To access those attributes in other nodes this template can be used " + | ||
42 | - "'meta.cs.temperature' or 'meta.shared.limit' " + | 39 | +@RuleNode(type = ComponentType.ENRICHMENT, |
40 | + name = "originator attributes", | ||
41 | + 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 | + "with specific prefix: <i>cs/shared/ss</i>. To access those attributes in other nodes this template can be used " + | ||
44 | + "<code>meta.cs.temperature</code> or <code>meta.shared.limit</code> " + | ||
43 | "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") | 45 | "If Latest Telemetry enrichment configured, latest telemetry added into metadata without prefix.") |
44 | public class TbGetAttributesNode implements TbNode { | 46 | public class TbGetAttributesNode implements TbNode { |
45 | 47 |
@@ -16,17 +16,20 @@ | @@ -16,17 +16,20 @@ | ||
16 | package org.thingsboard.rule.engine.metadata; | 16 | package org.thingsboard.rule.engine.metadata; |
17 | 17 | ||
18 | import com.google.common.util.concurrent.ListenableFuture; | 18 | import com.google.common.util.concurrent.ListenableFuture; |
19 | -import org.thingsboard.rule.engine.api.EnrichmentNode; | 19 | +import org.thingsboard.rule.engine.api.RuleNode; |
20 | import org.thingsboard.rule.engine.api.TbContext; | 20 | import org.thingsboard.rule.engine.api.TbContext; |
21 | import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; | 21 | import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; |
22 | import org.thingsboard.server.common.data.id.CustomerId; | 22 | import org.thingsboard.server.common.data.id.CustomerId; |
23 | import org.thingsboard.server.common.data.id.EntityId; | 23 | import org.thingsboard.server.common.data.id.EntityId; |
24 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
24 | 25 | ||
25 | -@EnrichmentNode(name="customer attributes", | 26 | +@RuleNode( |
27 | + type = ComponentType.ENRICHMENT, | ||
28 | + name="customer attributes", | ||
26 | nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", | 29 | nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata", |
27 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + | 30 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + |
28 | "To access those attributes in other nodes this template can be used " + | 31 | "To access those attributes in other nodes this template can be used " + |
29 | - "'meta.temperature'. If Latest Telemetry enrichment configured, latest telemetry added into metadata") | 32 | + "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") |
30 | public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { | 33 | public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> { |
31 | 34 | ||
32 | @Override | 35 | @Override |
@@ -17,22 +17,21 @@ package org.thingsboard.rule.engine.metadata; | @@ -17,22 +17,21 @@ package org.thingsboard.rule.engine.metadata; | ||
17 | 17 | ||
18 | import com.google.common.util.concurrent.ListenableFuture; | 18 | import com.google.common.util.concurrent.ListenableFuture; |
19 | import org.thingsboard.rule.engine.TbNodeUtils; | 19 | import org.thingsboard.rule.engine.TbNodeUtils; |
20 | -import org.thingsboard.rule.engine.api.TbContext; | ||
21 | -import org.thingsboard.rule.engine.api.TbNodeConfiguration; | ||
22 | -import org.thingsboard.rule.engine.api.TbNodeException; | ||
23 | -import org.thingsboard.rule.engine.api.TbNodeState; | ||
24 | -import org.thingsboard.rule.engine.api.EnrichmentNode; | 20 | +import org.thingsboard.rule.engine.api.*; |
25 | import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; | 21 | import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; |
26 | 22 | ||
27 | import org.thingsboard.server.common.data.id.EntityId; | 23 | import org.thingsboard.server.common.data.id.EntityId; |
24 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
28 | 25 | ||
29 | -@EnrichmentNode(name="related attributes", | 26 | +@RuleNode( |
27 | + type = ComponentType.ENRICHMENT, | ||
28 | + name="related attributes", | ||
30 | nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata", | 29 | nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata", |
31 | nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + | 30 | nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + |
32 | "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + | 31 | "If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " + |
33 | "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + | 32 | "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + |
34 | "To access those attributes in other nodes this template can be used " + | 33 | "To access those attributes in other nodes this template can be used " + |
35 | - "'meta.temperature'. If Latest Telemetry enrichment configured, latest telemetry added into metadata") | 34 | + "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") |
36 | public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { | 35 | public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> { |
37 | 36 | ||
38 | private TbGetRelatedAttrNodeConfiguration config; | 37 | private TbGetRelatedAttrNodeConfiguration config; |
@@ -17,18 +17,21 @@ package org.thingsboard.rule.engine.metadata; | @@ -17,18 +17,21 @@ package org.thingsboard.rule.engine.metadata; | ||
17 | 17 | ||
18 | import com.google.common.util.concurrent.ListenableFuture; | 18 | import com.google.common.util.concurrent.ListenableFuture; |
19 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
20 | -import org.thingsboard.rule.engine.api.EnrichmentNode; | 20 | +import org.thingsboard.rule.engine.api.RuleNode; |
21 | import org.thingsboard.rule.engine.api.TbContext; | 21 | import org.thingsboard.rule.engine.api.TbContext; |
22 | import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; | 22 | import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; |
23 | import org.thingsboard.server.common.data.id.EntityId; | 23 | import org.thingsboard.server.common.data.id.EntityId; |
24 | import org.thingsboard.server.common.data.id.TenantId; | 24 | import org.thingsboard.server.common.data.id.TenantId; |
25 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
25 | 26 | ||
26 | @Slf4j | 27 | @Slf4j |
27 | -@EnrichmentNode(name="tenant attributes", | 28 | +@RuleNode( |
29 | + type = ComponentType.ENRICHMENT, | ||
30 | + name="tenant attributes", | ||
28 | nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", | 31 | nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata", |
29 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + | 32 | nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " + |
30 | "To access those attributes in other nodes this template can be used " + | 33 | "To access those attributes in other nodes this template can be used " + |
31 | - "'meta.temperature'. If Latest Telemetry enrichment configured, latest telemetry added into metadata") | 34 | + "<code>meta.temperature</code>. If Latest Telemetry enrichment configured, latest telemetry added into metadata") |
32 | public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { | 35 | public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> { |
33 | 36 | ||
34 | @Override | 37 | @Override |
@@ -27,12 +27,15 @@ import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; | @@ -27,12 +27,15 @@ import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader; | ||
27 | import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; | 27 | import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader; |
28 | import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; | 28 | import org.thingsboard.rule.engine.util.EntitiesTenantIdAsyncLoader; |
29 | import org.thingsboard.server.common.data.id.EntityId; | 29 | import org.thingsboard.server.common.data.id.EntityId; |
30 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
30 | import org.thingsboard.server.common.msg.TbMsg; | 31 | import org.thingsboard.server.common.msg.TbMsg; |
31 | 32 | ||
32 | import java.util.HashSet; | 33 | import java.util.HashSet; |
33 | 34 | ||
34 | @Slf4j | 35 | @Slf4j |
35 | -@TransformationNode(name="change originator", | 36 | +@RuleNode( |
37 | + type = ComponentType.TRANSFORMATION, | ||
38 | + name="change originator", | ||
36 | nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity", | 39 | nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity", |
37 | nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + | 40 | nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + |
38 | "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ") | 41 | "If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded. ") |
@@ -19,15 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture; | @@ -19,15 +19,18 @@ import com.google.common.util.concurrent.ListenableFuture; | ||
19 | import org.thingsboard.rule.engine.TbNodeUtils; | 19 | import org.thingsboard.rule.engine.TbNodeUtils; |
20 | import org.thingsboard.rule.engine.api.*; | 20 | import org.thingsboard.rule.engine.api.*; |
21 | import org.thingsboard.rule.engine.js.NashornJsEngine; | 21 | import org.thingsboard.rule.engine.js.NashornJsEngine; |
22 | +import org.thingsboard.server.common.data.plugin.ComponentType; | ||
22 | import org.thingsboard.server.common.msg.TbMsg; | 23 | import org.thingsboard.server.common.msg.TbMsg; |
23 | 24 | ||
24 | import javax.script.Bindings; | 25 | import javax.script.Bindings; |
25 | 26 | ||
26 | -@TransformationNode(name = "script", | 27 | +@RuleNode( |
28 | + type = ComponentType.TRANSFORMATION, | ||
29 | + name = "script", | ||
27 | nodeDescription = "Change Message payload and Metadata using JavaScript", | 30 | nodeDescription = "Change Message payload and Metadata using JavaScript", |
28 | - nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside. " + | ||
29 | - "'meta' - is a Message metadata. " + | ||
30 | - "'msg' - is a Message payload. Any properties can be changed/removed/added in those objects.") | 31 | + nodeDetails = "JavaScript function recieve 2 input parameters that can be changed inside.<br/> " + |
32 | + "<code>meta</code> - is a Message metadata.<br/>" + | ||
33 | + "<code>msg</code> - is a Message payload.<br/>Any properties can be changed/removed/added in those objects.") | ||
31 | public class TbTransformMsgNode extends TbAbstractTransformNode { | 34 | public class TbTransformMsgNode extends TbAbstractTransformNode { |
32 | 35 | ||
33 | private TbTransformMsgNodeConfiguration config; | 36 | private TbTransformMsgNodeConfiguration config; |
@@ -460,8 +460,19 @@ export default angular.module('thingsboard.types', []) | @@ -460,8 +460,19 @@ export default angular.module('thingsboard.types', []) | ||
460 | ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION"], | 460 | ruleNodeTypeComponentTypes: ["FILTER", "ENRICHMENT", "TRANSFORMATION", "ACTION"], |
461 | ruleChainNodeComponent: { | 461 | ruleChainNodeComponent: { |
462 | type: 'RULE_CHAIN', | 462 | type: 'RULE_CHAIN', |
463 | - name: 'Rule chain', | ||
464 | - clazz: 'tb.internal.RuleChain' | 463 | + name: 'rule chain', |
464 | + clazz: 'tb.internal.RuleChain', | ||
465 | + configurationDescriptor: { | ||
466 | + nodeDefinition: { | ||
467 | + description: "Forwards incoming messages to specified Rule Chain", | ||
468 | + details: "Forwards incoming messages to specified Rule Chain", | ||
469 | + inEnabled: true, | ||
470 | + outEnabled: false, | ||
471 | + relationTypes: [], | ||
472 | + customRelations: false, | ||
473 | + defaultConfiguration: {} | ||
474 | + } | ||
475 | + } | ||
465 | }, | 476 | }, |
466 | inputNodeComponent: { | 477 | inputNodeComponent: { |
467 | type: 'INPUT', | 478 | type: 'INPUT', |
@@ -183,7 +183,8 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | @@ -183,7 +183,8 @@ export function RuleChainController($stateParams, $scope, $compile, $q, $mdUtil, | ||
183 | '<div class="tb-rule-node-tooltip">' + | 183 | '<div class="tb-rule-node-tooltip">' + |
184 | '<div id="tooltip-content" layout="column">' + | 184 | '<div id="tooltip-content" layout="column">' + |
185 | '<div class="tb-node-title">' + node.component.name + '</div>' + | 185 | '<div class="tb-node-title">' + node.component.name + '</div>' + |
186 | - '<div class="tb-node-description">' + 'Some description of node' + '</div>' + | 186 | + '<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' + |
187 | + '<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' + | ||
187 | '</div>' + | 188 | '</div>' + |
188 | '</div>' | 189 | '</div>' |
189 | ); | 190 | ); |
@@ -260,3 +260,32 @@ | @@ -260,3 +260,32 @@ | ||
260 | stroke-dashoffset: 500; | 260 | stroke-dashoffset: 500; |
261 | } | 261 | } |
262 | } | 262 | } |
263 | + | ||
264 | +.tb-rule-node-tooltip { | ||
265 | + font-size: 14px; | ||
266 | + width: 300px; | ||
267 | + color: #333; | ||
268 | + #tooltip-content { | ||
269 | + .tb-node-title { | ||
270 | + font-weight: 600; | ||
271 | + } | ||
272 | + .tb-node-description { | ||
273 | + font-style: italic; | ||
274 | + color: #555; | ||
275 | + } | ||
276 | + .tb-node-details { | ||
277 | + padding-top: 10px; | ||
278 | + padding-bottom: 10px; | ||
279 | + } | ||
280 | + code { | ||
281 | + padding: 0px 3px 2px 3px; | ||
282 | + margin: 1px; | ||
283 | + color: #AD1625; | ||
284 | + white-space: nowrap; | ||
285 | + background-color: #f7f7f9; | ||
286 | + border: 1px solid #e1e1e8; | ||
287 | + border-radius: 2px; | ||
288 | + font-size: 12px; | ||
289 | + } | ||
290 | + } | ||
291 | +} |