Browse Source

Separate Alarm node to Create and Clear Alarm nodes.

pull/804/head
Igor Kulikov 8 years ago
parent
commit
2dec050a37
  1. 2
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  2. 3
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  3. 6
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  4. 2
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java
  5. 14
      extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
  6. 117
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  7. 26
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java
  8. 219
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java
  9. 80
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java
  10. 36
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java
  11. 106
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java
  12. 19
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java
  13. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java
  14. 6
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  15. 98
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java
  16. 4
      ui/src/app/alarm/alarm-table.directive.js
  17. 7
      ui/src/app/alarm/alarm-table.tpl.html
  18. 3
      ui/src/app/locale/locale.constant.js
  19. 2
      ui/src/app/rulechain/rulechain.controller.js
  20. 13
      ui/src/app/rulechain/rulechain.scss
  21. 11
      ui/src/app/rulechain/rulechain.tpl.html

2
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -93,7 +93,7 @@ public class AlarmController extends BaseController {
try {
AlarmId alarmId = new AlarmId(toUUID(strAlarmId));
checkAlarmId(alarmId);
alarmService.clearAlarm(alarmId, System.currentTimeMillis()).get();
alarmService.clearAlarm(alarmId, null, System.currentTimeMillis()).get();
} catch (Exception e) {
throw handleException(e);
}

3
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.alarm.*;
import org.thingsboard.server.common.data.id.EntityId;
@ -30,7 +31,7 @@ public interface AlarmService {
ListenableFuture<Boolean> ackAlarm(AlarmId alarmId, long ackTs);
ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long ackTs);
ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, JsonNode details, long ackTs);
ListenableFuture<Alarm> findAlarmByIdAsync(AlarmId alarmId);

6
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -16,6 +16,7 @@
package org.thingsboard.server.dao.alarm;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
@ -187,7 +188,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
}
@Override
public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, long clearTime) {
public ListenableFuture<Boolean> clearAlarm(AlarmId alarmId, JsonNode details, long clearTime) {
return getAndUpdate(alarmId, new Function<Alarm, Boolean>() {
@Nullable
@Override
@ -199,6 +200,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK;
alarm.setStatus(newStatus);
alarm.setClearTs(clearTime);
if (details != null) {
alarm.setDetails(details);
}
alarmDao.save(alarm);
updateRelations(alarm, oldStatus, newStatus);
return true;

2
dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java

@ -168,7 +168,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest {
Assert.assertNotNull(alarms.getData());
Assert.assertEquals(0, alarms.getData().size());
alarmService.clearAlarm(created.getId(), System.currentTimeMillis()).get();
alarmService.clearAlarm(created.getId(), null, System.currentTimeMillis()).get();
created = alarmService.findAlarmByIdAsync(created.getId()).get();
alarms = alarmService.findAlarms(AlarmQuery.builder()

14
extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java

@ -31,12 +31,14 @@ public class SubscriptionUpdate {
super();
this.subscriptionId = subscriptionId;
this.data = new TreeMap<>();
for (TsKvEntry tsEntry : data) {
List<Object> values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>());
Object[] value = new Object[2];
value[0] = tsEntry.getTs();
value[1] = tsEntry.getValueAsString();
values.add(value);
if (data != null) {
for (TsKvEntry tsEntry : data) {
List<Object> values = this.data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>());
Object[] value = new Object[2];
value[0] = tsEntry.getTs();
value[1] = tsEntry.getValueAsString();
values.add(value);
}
}
}

117
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java

@ -0,0 +1,117 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j
public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfiguration> implements TbNode {
static final String PREV_ALARM_DETAILS = "prevAlarmDetails";
static final String IS_NEW_ALARM = "isNewAlarm";
static final String IS_EXISTING_ALARM = "isExistingAlarm";
static final String IS_CLEARED_ALARM = "isClearedAlarm";
private final ObjectMapper mapper = new ObjectMapper();
protected C config;
private ScriptEngine buildDetailsJsEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadAlarmNodeConfig(configuration);
this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details");
}
protected abstract C loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(processAlarm(ctx, msg),
alarmResult -> {
if (alarmResult.alarm == null) {
ctx.tellNext(msg, "False");
} else if (alarmResult.isCreated) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Created");
} else if (alarmResult.isUpdated) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Updated");
} else if (alarmResult.isCleared) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Cleared");
}
},
t -> ctx.tellError(msg, t));
}
protected abstract ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg);
protected ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg, JsonNode previousDetails) {
return ctx.getJsExecutor().executeAsync(() -> {
TbMsg theMsg = msg;
if (previousDetails != null) {
TbMsgMetaData metaData = msg.getMetaData().copy();
metaData.putValue(PREV_ALARM_DETAILS, mapper.writeValueAsString(previousDetails));
theMsg = ctx.newMsg(msg.getType(), msg.getOriginator(), metaData, msg.getData());
}
return buildDetailsJsEngine.executeJson(theMsg);
});
}
private TbMsg toAlarmMsg(TbContext ctx, AlarmResult alarmResult, TbMsg originalMsg) {
JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
String data = jsonNodes.toString();
TbMsgMetaData metaData = originalMsg.getMetaData().copy();
if (alarmResult.isCreated) {
metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isUpdated) {
metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isCleared) {
metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
}
return ctx.transformMsg(originalMsg, "ALARM", originalMsg.getOriginator(), metaData, data);
}
@Override
public void destroy() {
if (buildDetailsJsEngine != null) {
buildDetailsJsEngine.destroy();
}
}
protected static class AlarmResult {
boolean isCreated;
boolean isUpdated;
boolean isCleared;
Alarm alarm;
AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
this.isCreated = isCreated;
this.isUpdated = isUpdated;
this.isCleared = isCleared;
this.alarm = alarm;
}
}
}

26
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNodeConfiguration.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import lombok.Data;
@Data
public abstract class TbAbstractAlarmNodeConfiguration {
private String alarmType;
private String alarmDetailsBuildJs;
}

219
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNode.java

@ -1,219 +0,0 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.*;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import static org.thingsboard.rule.engine.DonAsynchron.withCallback;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "alarm", relationTypes = {"Created", "Updated", "Cleared", "False"},
configClazz = TbAlarmNodeConfiguration.class,
nodeDescription = "Create/Update/Clear Alarm",
nodeDetails = "isAlarm - JS function that verifies if Alarm should be CREATED for incoming message.\n" +
"isCleared - JS function that verifies if Alarm should be CLEARED for incoming message.\n" +
"Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
"Node output:\n" +
"If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm/isClearedAlarm' " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeAlarmConfig",
icon = "notifications_active"
)
public class TbAlarmNode implements TbNode {
static final String IS_NEW_ALARM = "isNewAlarm";
static final String IS_EXISTING_ALARM = "isExistingAlarm";
static final String IS_CLEARED_ALARM = "isClearedAlarm";
private final ObjectMapper mapper = new ObjectMapper();
private TbAlarmNodeConfiguration config;
private ScriptEngine createJsEngine;
private ScriptEngine clearJsEngine;
private ScriptEngine buildDetailsJsEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbAlarmNodeConfiguration.class);
this.createJsEngine = ctx.createJsScriptEngine(config.getCreateConditionJs(), "isAlarm");
this.clearJsEngine = ctx.createJsScriptEngine(config.getClearConditionJs(), "isCleared");
this.buildDetailsJsEngine = ctx.createJsScriptEngine(config.getAlarmDetailsBuildJs(), "Details");
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
ListeningExecutor jsExecutor = ctx.getJsExecutor();
ListenableFuture<Boolean> shouldCreate = jsExecutor.executeAsync(() -> createJsEngine.executeFilter(msg));
ListenableFuture<AlarmResult> transform = Futures.transform(shouldCreate, (AsyncFunction<Boolean, AlarmResult>) create -> {
if (create) {
return createOrUpdate(ctx, msg);
} else {
return checkForClearIfExist(ctx, msg);
}
}, ctx.getDbCallbackExecutor());
withCallback(transform,
alarmResult -> {
if (alarmResult.alarm == null) {
ctx.tellNext(msg, "False");
} else if (alarmResult.isCreated) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Created");
} else if (alarmResult.isUpdated) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Updated");
} else if (alarmResult.isCleared) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Cleared");
}
},
t -> ctx.tellError(msg, t));
}
private ListenableFuture<AlarmResult> createOrUpdate(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a == null || a.getStatus().isCleared()) {
return createNewAlarm(ctx, msg);
} else {
return updateAlarm(ctx, msg, a);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<AlarmResult> checkForClearIfExist(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a != null && !a.getStatus().isCleared()) {
return clearAlarm(ctx, msg, a);
}
return Futures.immediateFuture(new AlarmResult(false, false, false, null));
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg),
(Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
(Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
}
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg), (Function<JsonNode, Alarm>) details -> {
alarm.setSeverity(config.getSeverity());
alarm.setPropagate(config.isPropagate());
alarm.setDetails(details);
alarm.setEndTs(System.currentTimeMillis());
return ctx.getAlarmService().createOrUpdateAlarm(alarm);
}, ctx.getDbCallbackExecutor());
return Futures.transform(asyncUpdated, (Function<Alarm, AlarmResult>) a -> new AlarmResult(false, true, false, a));
}
private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Boolean> shouldClear = ctx.getJsExecutor().executeAsync(() -> clearJsEngine.executeFilter(msg));
return Futures.transform(shouldClear, (AsyncFunction<Boolean, AlarmResult>) clear -> {
if (clear) {
ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), System.currentTimeMillis());
return Futures.transform(clearFuture, (Function<Boolean, AlarmResult>) cleared -> {
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
return new AlarmResult(false, false, true, alarm);
});
}
return Futures.immediateFuture(new AlarmResult(false, false, false, null));
});
}
private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
return Alarm.builder()
.tenantId(tenantId)
.originator(msg.getOriginator())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(config.getSeverity())
.propagate(config.isPropagate())
.type(config.getAlarmType())
//todo-vp: alarm date should be taken from Message or current Time should be used?
// .startTs(System.currentTimeMillis())
// .endTs(System.currentTimeMillis())
.details(details)
.build();
}
private ListenableFuture<JsonNode> buildAlarmDetails(TbContext ctx, TbMsg msg) {
return ctx.getJsExecutor().executeAsync(() -> buildDetailsJsEngine.executeJson(msg));
}
private TbMsg toAlarmMsg(TbContext ctx, AlarmResult alarmResult, TbMsg originalMsg) {
JsonNode jsonNodes = mapper.valueToTree(alarmResult.alarm);
String data = jsonNodes.toString();
TbMsgMetaData metaData = originalMsg.getMetaData().copy();
if (alarmResult.isCreated) {
metaData.putValue(IS_NEW_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isUpdated) {
metaData.putValue(IS_EXISTING_ALARM, Boolean.TRUE.toString());
} else if (alarmResult.isCleared) {
metaData.putValue(IS_CLEARED_ALARM, Boolean.TRUE.toString());
}
return ctx.transformMsg(originalMsg, "ALARM", originalMsg.getOriginator(), metaData, data);
}
@Override
public void destroy() {
if (createJsEngine != null) {
createJsEngine.destroy();
}
if (clearJsEngine != null) {
clearJsEngine.destroy();
}
if (buildDetailsJsEngine != null) {
buildDetailsJsEngine.destroy();
}
}
private static class AlarmResult {
boolean isCreated;
boolean isUpdated;
boolean isCleared;
Alarm alarm;
AlarmResult(boolean isCreated, boolean isUpdated, boolean isCleared, Alarm alarm) {
this.isCreated = isCreated;
this.isUpdated = isUpdated;
this.isCleared = isCleared;
this.alarm = alarm;
}
}
}

80
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "clear alarm", relationTypes = {"Cleared", "False"},
configClazz = TbClearAlarmNodeConfiguration.class,
nodeDescription = "Clear Alarm",
nodeDetails =
"Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
"Node output:\n" +
"If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains 'isClearedAlarm' property " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeClearAlarmConfig",
icon = "notifications_active"
)
public class TbClearAlarmNode extends TbAbstractAlarmNode<TbClearAlarmNodeConfiguration> {
@Override
protected TbClearAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbClearAlarmNodeConfiguration.class);
}
@Override
protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a != null && !a.getStatus().isCleared()) {
return clearAlarm(ctx, msg, a);
}
return Futures.immediateFuture(new AlarmResult(false, false, false, null));
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<AlarmResult> clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<JsonNode> asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails());
return Futures.transform(asyncDetails, (AsyncFunction<JsonNode, AlarmResult>) details -> {
ListenableFuture<Boolean> clearFuture = ctx.getAlarmService().clearAlarm(alarm.getId(), details, System.currentTimeMillis());
return Futures.transform(clearFuture, (AsyncFunction<Boolean, AlarmResult>) cleared -> {
if (cleared && details != null) {
alarm.setDetails(details);
}
alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK);
return Futures.immediateFuture(new AlarmResult(false, false, true, alarm));
});
}, ctx.getDbCallbackExecutor());
}
}

36
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNodeConfiguration.java

@ -0,0 +1,36 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@Data
public class TbClearAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbClearAlarmNodeConfiguration> {
@Override
public TbClearAlarmNodeConfiguration defaultConfiguration() {
TbClearAlarmNodeConfiguration configuration = new TbClearAlarmNodeConfiguration();
configuration.setAlarmDetailsBuildJs("var details = {};\n" +
"if (metadata.prevAlarmDetails) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
"}\n" +
"return details;");
configuration.setAlarmType("General Alarm");
return configuration;
}
}

106
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java

@ -0,0 +1,106 @@
/**
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.action;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.base.Function;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.TbNodeUtils;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "create alarm", relationTypes = {"Created", "Updated", "False"},
configClazz = TbCreateAlarmNodeConfiguration.class,
nodeDescription = "Create or Update Alarm",
nodeDetails =
"Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" +
"Node output:\n" +
"If alarm was not created, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'matadata' will contains one of those properties 'isNewAlarm/isExistingAlarm' " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>" +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeCreateAlarmConfig",
icon = "notifications_active"
)
public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
@Override
protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
}
@Override
protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
return Futures.transform(latest, (AsyncFunction<Alarm, AlarmResult>) a -> {
if (a == null || a.getStatus().isCleared()) {
return createNewAlarm(ctx, msg);
} else {
return updateAlarm(ctx, msg, a);
}
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
(Function<JsonNode, Alarm>) details -> buildAlarm(msg, details, ctx.getTenantId()));
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
(Function<Alarm, Alarm>) alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
return Futures.transform(asyncCreated, (Function<Alarm, AlarmResult>) alarm -> new AlarmResult(true, false, false, alarm));
}
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
alarm.setSeverity(config.getSeverity());
alarm.setPropagate(config.isPropagate());
alarm.setDetails(details);
alarm.setEndTs(System.currentTimeMillis());
return ctx.getAlarmService().createOrUpdateAlarm(alarm);
}, ctx.getDbCallbackExecutor());
return Futures.transform(asyncUpdated, (Function<Alarm, AlarmResult>) a -> new AlarmResult(false, true, false, a));
}
private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) {
return Alarm.builder()
.tenantId(tenantId)
.originator(msg.getOriginator())
.status(AlarmStatus.ACTIVE_UNACK)
.severity(config.getSeverity())
.propagate(config.isPropagate())
.type(config.getAlarmType())
//todo-vp: alarm date should be taken from Message or current Time should be used?
// .startTs(System.currentTimeMillis())
// .endTs(System.currentTimeMillis())
.details(details)
.build();
}
}

19
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmNodeConfiguration.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNodeConfiguration.java

@ -20,22 +20,19 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@Data
public class TbAlarmNodeConfiguration implements NodeConfiguration {
public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfiguration implements NodeConfiguration<TbCreateAlarmNodeConfiguration> {
private String createConditionJs;
private String clearConditionJs;
private String alarmDetailsBuildJs;
private String alarmType;
private AlarmSeverity severity;
private boolean propagate;
@Override
public TbAlarmNodeConfiguration defaultConfiguration() {
TbAlarmNodeConfiguration configuration = new TbAlarmNodeConfiguration();
configuration.setCreateConditionJs("return 'incoming message = ' + msg + meta;");
configuration.setClearConditionJs("return 'incoming message = ' + msg + meta;");
configuration.setAlarmDetailsBuildJs("return 'incoming message = ' + msg + meta;");
public TbCreateAlarmNodeConfiguration defaultConfiguration() {
TbCreateAlarmNodeConfiguration configuration = new TbCreateAlarmNodeConfiguration();
configuration.setAlarmDetailsBuildJs("var details = {};\n" +
"if (metadata.prevAlarmDetails) {\n" +
" details = JSON.parse(metadata.prevAlarmDetails);\n" +
"}\n"+
"return details;");
configuration.setAlarmType("General Alarm");
configuration.setSeverity(AlarmSeverity.CRITICAL);
configuration.setPropagate(false);

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNodeConfiguration.java

@ -26,7 +26,7 @@ public class TbJsFilterNodeConfiguration implements NodeConfiguration<TbJsFilter
@Override
public TbJsFilterNodeConfiguration defaultConfiguration() {
TbJsFilterNodeConfiguration configuration = new TbJsFilterNodeConfiguration();
configuration.setJsScript("return msg.passed < 15 && msg.name === 'Vit' && metadata.temp == 10 && msg.bigObj.prop == 42;");
configuration.setJsScript("return msg.temperature > 20;");
return configuration;
}
}

6
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

98
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.action;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -45,7 +46,7 @@ import java.util.concurrent.Callable;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
import static org.thingsboard.rule.engine.action.TbAlarmNode.*;
import static org.thingsboard.rule.engine.action.TbAbstractAlarmNode.*;
import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL;
import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING;
import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
@ -53,7 +54,7 @@ import static org.thingsboard.server.common.data.alarm.AlarmStatus.*;
@RunWith(MockitoJUnitRunner.class)
public class TbAlarmNodeTest {
private TbAlarmNode node;
private TbAbstractAlarmNode node;
@Mock
private TbContext ctx;
@ -62,10 +63,6 @@ public class TbAlarmNodeTest {
@Mock
private AlarmService alarmService;
@Mock
private ScriptEngine createJs;
@Mock
private ScriptEngine clearJs;
@Mock
private ScriptEngine detailsJs;
@ -100,11 +97,10 @@ public class TbAlarmNodeTest {
@Test
public void newAlarmCanBeCreated() throws ScriptException, IOException {
initWithScript();
initWithCreateAlarmScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
@ -140,38 +136,15 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class));
}
@Test
public void shouldCreateScriptThrowsException() throws ScriptException {
initWithScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
when(createJs.executeFilter(msg)).thenThrow(new NotImplementedException("message"));
node.onMsg(ctx, msg);
verifyError(msg, "message", NotImplementedException.class);
verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
verify(ctx).createJsScriptEngine("DETAILS", "Details");
verify(ctx).getJsExecutor();
verify(ctx).getDbCallbackExecutor();
verifyNoMoreInteractions(ctx, alarmService, clearJs, detailsJs);
verify(executor, times(1)).executeAsync(any(Callable.class));
}
@Test
public void buildDetailsThrowsException() throws ScriptException, IOException {
initWithScript();
initWithCreateAlarmScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenThrow(new NotImplementedException("message"));
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null));
@ -179,27 +152,24 @@ public class TbAlarmNodeTest {
verifyError(msg, "message", NotImplementedException.class);
verify(ctx).createJsScriptEngine("CREATE", "isAlarm");
verify(ctx).createJsScriptEngine("CLEAR", "isCleared");
verify(ctx).createJsScriptEngine("DETAILS", "Details");
verify(ctx, times(2)).getJsExecutor();
verify(ctx, times(1)).getJsExecutor();
verify(ctx).getAlarmService();
verify(ctx, times(3)).getDbCallbackExecutor();
verify(ctx, times(2)).getDbCallbackExecutor();
verify(ctx).getTenantId();
verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType");
verifyNoMoreInteractions(ctx, alarmService, clearJs);
verifyNoMoreInteractions(ctx, alarmService);
}
@Test
public void ifAlarmClearedCreateNew() throws ScriptException, IOException {
initWithScript();
initWithCreateAlarmScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build();
when(createJs.executeFilter(msg)).thenReturn(true);
when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm));
@ -236,20 +206,18 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class));
verify(executor, times(1)).executeAsync(any(Callable.class));
}
@Test
public void alarmCanBeUpdated() throws ScriptException, IOException {
initWithScript();
initWithCreateAlarmScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
long oldEndDate = System.currentTimeMillis();
Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
when(createJs.executeFilter(msg)).thenReturn(true);
when(clearJs.executeFilter(msg)).thenReturn(false);
when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
@ -287,23 +255,21 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm);
verify(executor, times(2)).executeAsync(any(Callable.class));
verify(executor, times(1)).executeAsync(any(Callable.class));
}
@Test
public void alarmCanBeCleared() throws ScriptException, IOException {
initWithScript();
initWithClearAlarmScript();
metaData.putValue("key", "value");
TbMsg msg = new TbMsg(UUIDs.timeBased(), "USER", originator, metaData, rawJson, ruleChainId, ruleNodeId, 0L);
long oldEndDate = System.currentTimeMillis();
Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build();
when(createJs.executeFilter(msg)).thenReturn(false);
when(clearJs.executeFilter(msg)).thenReturn(true);
// when(detailsJs.executeJson(msg)).thenReturn(null);
when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm));
when(alarmService.clearAlarm(eq(activeAlarm.getId()), anyLong())).thenReturn(Futures.immediateFuture(true));
when(alarmService.clearAlarm(eq(activeAlarm.getId()), org.mockito.Mockito.any(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true));
// doAnswer((Answer<Alarm>) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm);
node.onMsg(ctx, msg);
@ -338,20 +304,16 @@ public class TbAlarmNodeTest {
assertEquals(expectedAlarm, actualAlarm);
}
private void initWithScript() {
private void initWithCreateAlarmScript() {
try {
TbAlarmNodeConfiguration config = new TbAlarmNodeConfiguration();
TbCreateAlarmNodeConfiguration config = new TbCreateAlarmNodeConfiguration();
config.setPropagate(true);
config.setSeverity(CRITICAL);
config.setAlarmType("SomeType");
config.setCreateConditionJs("CREATE");
config.setClearConditionJs("CLEAR");
config.setAlarmDetailsBuildJs("DETAILS");
ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("CREATE", "isAlarm")).thenReturn(createJs);
when(ctx.createJsScriptEngine("CLEAR", "isCleared")).thenReturn(clearJs);
when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs);
when(ctx.getTenantId()).thenReturn(tenantId);
@ -361,7 +323,31 @@ public class TbAlarmNodeTest {
mockJsExecutor();
node = new TbAlarmNode();
node = new TbCreateAlarmNode();
node.init(ctx, nodeConfiguration);
} catch (TbNodeException ex) {
throw new IllegalStateException(ex);
}
}
private void initWithClearAlarmScript() {
try {
TbClearAlarmNodeConfiguration config = new TbClearAlarmNodeConfiguration();
config.setAlarmType("SomeType");
config.setAlarmDetailsBuildJs("DETAILS");
ObjectMapper mapper = new ObjectMapper();
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config));
when(ctx.createJsScriptEngine("DETAILS", "Details")).thenReturn(detailsJs);
when(ctx.getTenantId()).thenReturn(tenantId);
when(ctx.getJsExecutor()).thenReturn(executor);
when(ctx.getAlarmService()).thenReturn(alarmService);
when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
mockJsExecutor();
node = new TbClearAlarmNode();
node.init(ctx, nodeConfiguration);
} catch (TbNodeException ex) {
throw new IllegalStateException(ex);

4
ui/src/app/alarm/alarm-table.directive.js

@ -42,7 +42,7 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
history: {
timewindowMs: 24 * 60 * 60 * 1000 // 1 day
}
}
};
scope.topIndex = 0;
@ -98,6 +98,8 @@ export default function AlarmTableDirective($compile, $templateCache, $rootScope
}
};
scope.reload = reload;
scope.$watch("entityId", function(newVal, prevVal) {
if (newVal && !angular.equals(newVal, prevVal)) {
resetFilter();

7
ui/src/app/alarm/alarm-table.tpl.html

@ -26,6 +26,13 @@
</md-select>
</md-input-container>
<tb-timewindow flex ng-model="timewindow" history-only as-button="true"></tb-timewindow>
<md-button ng-disabled="$root.loading"
class="md-icon-button" ng-click="reload()">
<md-icon>refresh</md-icon>
<md-tooltip md-direction="top">
{{ 'action.refresh' | translate }}
</md-tooltip>
</md-button>
</section>
<div flex layout="column" class="tb-alarm-container md-whiteframe-z1">
<md-list flex layout="column" class="tb-alarm-table">

3
ui/src/app/locale/locale.constant.js

@ -1242,7 +1242,8 @@ export default angular.module('thingsboard.locale', [])
"metadata": "Metadata",
"metadata-required": "Metadata entries can't be empty.",
"output": "Output",
"test": "Test"
"test": "Test",
"help": "Help"
},
"rule-plugin": {
"management": "Rules and plugins management"

2
ui/src/app/rulechain/rulechain.controller.js

@ -489,7 +489,7 @@ export function RuleChainController($state, $scope, $compile, $q, $mdUtil, $time
function displayNodeDescriptionTooltip(event, node) {
displayTooltip(event,
'<div class="tb-rule-node-tooltip">' +
'<div id="tooltip-content" layout="column">' +
'<div id="tb-node-content" layout="column">' +
'<div class="tb-node-title">' + node.component.name + '</div>' +
'<div class="tb-node-description">' + node.component.configurationDescriptor.nodeDefinition.description + '</div>' +
'<div class="tb-node-details">' + node.component.configurationDescriptor.nodeDefinition.details + '</div>' +

13
ui/src/app/rulechain/rulechain.scss

@ -431,10 +431,17 @@
}
}
.tb-rule-node-tooltip, .tb-rule-node-help {
color: #333;
}
.tb-rule-node-tooltip {
font-size: 14px;
width: 300px;
color: #333;
}
.tb-rule-node-help {
font-size: 16px;
}
.tb-rule-node-error-tooltip {
@ -442,8 +449,8 @@
color: #ea0d0d;
}
.tb-rule-node-tooltip, .tb-rule-node-error-tooltip {
#tooltip-content {
.tb-rule-node-tooltip, .tb-rule-node-error-tooltip, .tb-rule-node-help {
#tb-node-content {
.tb-node-title {
font-weight: 600;
}

11
ui/src/app/rulechain/rulechain.tpl.html

@ -178,6 +178,17 @@
default-event-type="{{vm.types.debugEventType.debugRuleNode.value}}">
</tb-event-table>
</md-tab>
<md-tab label="{{ 'rulenode.help' | translate }}">
<div class="tb-rule-node-help">
<div id="tb-node-content" class="md-padding" layout="column">
<div class="tb-node-title">{{vm.editingRuleNode.component.name}}</div>
<div>&nbsp;</div>
<div class="tb-node-description">{{vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.description}}</div>
<div>&nbsp;</div>
<div class="tb-node-details" ng-bind-html="vm.editingRuleNode.component.configurationDescriptor.nodeDefinition.details"></div>
</div>
</div>
</md-tab>
</md-tabs>
</tb-details-sidenav>
<tb-details-sidenav class="tb-rulenode-link-details-sidenav"

Loading…
Cancel
Save