Browse Source

Merge branch 'master' into features/add_hide_empty_lines_options_on_timeseries_table_widget

pull/1698/head
Igor Kulikov 7 years ago
committed by GitHub
parent
commit
457fc4423a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      application/build.gradle
  2. 2
      application/src/main/data/json/system/widget_bundles/cards.json
  3. 34
      application/src/main/data/json/system/widget_bundles/maps.json
  4. 60
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  5. 2
      application/src/main/resources/thingsboard.yml
  6. 5
      application/src/main/scripts/control/deb/postinst
  7. 6
      application/src/main/scripts/control/deb/postrm
  8. 4
      application/src/main/scripts/control/deb/preinst
  9. 4
      application/src/main/scripts/control/deb/prerm
  10. 4
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  11. 1
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  12. 10
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  13. 13
      dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java
  14. 2
      dao/src/test/resources/cassandra-test.properties
  15. 4
      msa/tb-node/docker/Dockerfile
  16. 4
      msa/tb/docker-cassandra/Dockerfile
  17. 4
      msa/tb/docker-postgres/Dockerfile
  18. 4
      msa/tb/docker-tb/Dockerfile
  19. 2
      msa/transport/coap/docker/Dockerfile
  20. 2
      msa/transport/http/docker/Dockerfile
  21. 2
      msa/transport/mqtt/docker/Dockerfile
  22. 2
      pom.xml
  23. 15
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java
  24. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractAlarmNode.java
  25. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java
  26. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java
  27. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java
  28. 30
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
  29. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  30. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
  31. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
  32. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
  33. 8
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  34. 13
      ui/package-lock.json
  35. 1
      ui/package.json
  36. 2
      ui/src/app/components/dashboard.directive.js
  37. 14
      ui/src/app/components/dashboard.scss
  38. 124
      ui/src/app/components/dashboard.tpl.html
  39. 4
      ui/src/app/components/timewindow.tpl.html
  40. 5
      ui/src/app/components/widget/widget-config.directive.js
  41. 3
      ui/src/app/components/widget/widget-config.tpl.html
  42. 5
      ui/src/app/components/widget/widget.controller.js
  43. 1
      ui/src/app/dashboard/dashboard-toolbar.scss
  44. 2
      ui/src/app/dashboard/dashboard.tpl.html
  45. 5
      ui/src/app/locale/locale.constant-de_DE.json
  46. 1
      ui/src/app/locale/locale.constant-en_US.json
  47. 3
      ui/src/app/locale/locale.constant-es_ES.json
  48. 5
      ui/src/app/locale/locale.constant-fr_FR.json
  49. 1
      ui/src/app/locale/locale.constant-it_IT.json
  50. 96
      ui/src/app/locale/locale.constant-zh_CN.json
  51. 18
      ui/src/app/widget/lib/flot-widget.js
  52. 25
      ui/src/app/widget/lib/google-map.js
  53. 9
      ui/src/app/widget/lib/image-map.js
  54. 43
      ui/src/app/widget/lib/map-widget2.js
  55. 9
      ui/src/app/widget/lib/openstreet-map.js
  56. 28
      ui/src/app/widget/lib/tencent-map.js
  57. 26
      ui/src/app/widget/lib/timeseries-table-widget.js
  58. 17
      ui/src/app/widget/lib/timeseries-table-widget.tpl.html
  59. 154
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js
  60. 2
      ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html
  61. 2
      ui/src/scss/main.scss

7
application/build.gradle

@ -160,6 +160,13 @@ buildDeb {
user pkgName
permissionGroup pkgName
// Copy the system unit files
from("${buildDir}/control/${pkgName}.service") {
addParentDirs = false
fileMode 0644
into "/lib/systemd/system"
}
directory(pkgLogFolder, 0755)
link("/etc/init.d/${pkgName}", "${pkgInstallFolder}/bin/${pkgName}.jar")
link("${pkgInstallFolder}/bin/${pkgName}.yml", "${pkgInstallFolder}/conf/${pkgName}.yml")

2
application/src/main/data/json/system/widget_bundles/cards.json

@ -111,7 +111,7 @@
"resources": [],
"templateHtml": "<tb-timeseries-table-widget \n table-id=\"tableId\"\n ctx=\"ctx\">\n</tb-timeseries-table-widget>",
"templateCss": "",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.onDestroy = function() {\n}",
"controllerScript": "self.onInit = function() {\n var scope = self.ctx.$scope;\n var id = self.ctx.$scope.$injector.get('utils').guid();\n scope.tableId = \"table-\"+id;\n scope.ctx = self.ctx;\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.$broadcast('timeseries-table-data-updated', self.ctx.$scope.tableId);\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}",
"settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"defaultPageSize\": {\n \"title\": \"Default page size\",\n \"type\": \"number\",\n \"default\": 10\n },\n \"hideEmptyLines\": {\n \"title\": \"Hide empty lines\",\n \"type\": \"boolean\",\n \"default\": false\n }\n },\n \"required\": []\n },\n \"form\": [\n \"showTimestamp\",\n \"displayPagination\",\n \"defaultPageSize\",\n \"hideEmptyLines\"\n ]\n}",
"dataKeySettingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"DataKeySettings\",\n \"properties\": {\n \"useCellStyleFunction\": {\n \"title\": \"Use cell style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellStyleFunction\": {\n \"title\": \"Cell style function: f(value)\",\n \"type\": \"string\",\n \"default\": \"\"\n },\n \"useCellContentFunction\": {\n \"title\": \"Use cell content function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"cellContentFunction\": {\n \"title\": \"Cell content function: f(value, rowData, filter)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\"\n }\n ]\n}",
"defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature °C\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = (value + 60)/120 * 100;\\n var color = tinycolor.mix('blue', 'red', amount = percent);\\n color.setAlpha(.5);\\n return {\\n paddingLeft: '20px',\\n color: '#ffffff',\\n background: color.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\"},\"_hash\":0.8587686344902596,\"funcBody\":\"var value = prevValue + Math.random() * 40 - 20;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -60) {\\n\\tvalue = -60;\\n} else if (value > 60) {\\n\\tvalue = 60;\\n}\\nreturn value;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Humidity, %\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":true,\"cellStyleFunction\":\"if (value) {\\n var percent = value;\\n var backgroundColor = tinycolor('blue');\\n backgroundColor.setAlpha(value/100);\\n var color = 'blue';\\n if (value > 50) {\\n color = 'white';\\n }\\n \\n return {\\n paddingLeft: '20px',\\n color: color,\\n background: backgroundColor.toRgbString(),\\n fontSize: '18px'\\n };\\n} else {\\n return {};\\n}\",\"useCellContentFunction\":false},\"_hash\":0.12775350966079668,\"funcBody\":\"var value = prevValue + Math.random() * 20 - 10;\\nvar multiplier = Math.pow(10, 1 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < 5) {\\n\\tvalue = 5;\\n} else if (value > 100) {\\n\\tvalue = 100;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":60000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"showTimestamp\":true,\"displayPagination\":true,\"defaultPageSize\":10},\"title\":\"Timeseries table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{}}"

34
application/src/main/data/json/system/widget_bundles/maps.json

File diff suppressed because one or more lines are too long

60
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -17,6 +17,9 @@ package org.thingsboard.server.actors.ruleChain;
import akka.actor.ActorRef;
import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.channel.EventLoopGroup;
import org.springframework.util.StringUtils;
import org.thingsboard.rule.engine.api.ListeningExecutor;
@ -30,6 +33,11 @@ import org.thingsboard.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -38,9 +46,11 @@ import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.cluster.ServerType;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
@ -69,6 +79,8 @@ import java.util.function.Consumer;
*/
class DefaultTbContext implements TbContext {
public final static ObjectMapper mapper = new ObjectMapper();
private final ActorSystemContext mainCtx;
private final RuleNodeCtx nodeCtx;
@ -138,6 +150,48 @@ class DefaultTbContext implements TbContext {
return new TbMsg(origMsg.getId(), type, originator, metaData.copy(), origMsg.getDataType(), data, origMsg.getTransactionData(), origMsg.getRuleChainId(), origMsg.getRuleNodeId(), mainCtx.getQueuePartitionId());
}
@Override
public void sendTbMsgToRuleEngine(TbMsg msg) {
mainCtx.getActorService().onMsg(new SendToClusterMsg(msg.getOriginator(), new ServiceToRuleEngineMsg(getTenantId(), msg)));
}
public TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId) {
try {
ObjectNode entityNode = mapper.valueToTree(customer);
return new TbMsg(UUIDs.timeBased(), DataConstants.ENTITY_CREATED, customer.getId(), getActionMetaData(ruleNodeId), mapper.writeValueAsString(entityNode), null, null, 0L);
} catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process customer created msg: " + e);
}
}
public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) {
try {
ObjectNode entityNode = mapper.valueToTree(device);
return new TbMsg(UUIDs.timeBased(), DataConstants.ENTITY_CREATED, device.getId(), getActionMetaData(ruleNodeId), mapper.writeValueAsString(entityNode), null, null, 0L);
} catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process device created msg: " + e);
}
}
public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) {
try {
ObjectNode entityNode = mapper.valueToTree(asset);
return new TbMsg(UUIDs.timeBased(), DataConstants.ENTITY_CREATED, asset.getId(), getActionMetaData(ruleNodeId), mapper.writeValueAsString(entityNode), null, null, 0L);
} catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process asset created msg: " + e);
}
}
public TbMsg alarmCreatedMsg(Alarm alarm, RuleNodeId ruleNodeId) {
try {
ObjectNode entityNode = mapper.valueToTree(alarm);
return new TbMsg(UUIDs.timeBased(), DataConstants.ENTITY_CREATED, alarm.getId(), getActionMetaData(ruleNodeId), mapper.writeValueAsString(entityNode), null, null, 0L);
} catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process alarm created msg: " + e);
}
}
@Override
public RuleNodeId getSelfId() {
return nodeCtx.getSelf().getId();
@ -305,4 +359,10 @@ class DefaultTbContext implements TbContext {
return mainCtx.getCassandraBufferedRateExecutor();
}
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
metaData.putValue("ruleNodeId", ruleNodeId.toString());
return metaData;
}
}

2
application/src/main/resources/thingsboard.yml

@ -166,6 +166,8 @@ cassandra:
ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
ts_key_value_ttl: "${TS_KV_TTL:0}"
events_ttl: "${TS_EVENTS_TTL:0}"
# Specify TTL of debug log in seconds. The current value corresponds to one week
debug_events_ttl: "${DEBUG_EVENTS_TTL:604800}"
buffer_size: "${CASSANDRA_QUERY_BUFFER_SIZE:200000}"
concurrent_limit: "${CASSANDRA_QUERY_CONCURRENT_LIMIT:1000}"
permit_max_wait_time: "${PERMIT_MAX_WAIT_TIME:120000}"

5
application/src/main/scripts/control/deb/postinst

@ -1,6 +1,9 @@
#!/bin/sh
set -e
chown -R ${pkg.name}: ${pkg.logFolder}
chown -R ${pkg.name}: ${pkg.installFolder}
update-rc.d ${pkg.name} defaults
systemctl --no-reload enable ${pkg.name}.service >/dev/null 2>&1 || :
exit 0

6
application/src/main/scripts/control/deb/postrm

@ -1,3 +1,7 @@
#!/bin/sh
update-rc.d -f ${pkg.name} remove
set -e
systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
exit 0

4
application/src/main/scripts/control/deb/preinst

@ -1,5 +1,7 @@
#!/bin/sh
set -e
if ! getent group ${pkg.name} >/dev/null; then
addgroup --system ${pkg.name}
fi
@ -16,3 +18,5 @@ if ! getent passwd ${pkg.name} >/dev/null; then
-gecos "Thingsboard application" \
${pkg.name}
fi
exit 0

4
application/src/main/scripts/control/deb/prerm

@ -1,5 +1,9 @@
#!/bin/sh
set -e
if [ -e /var/run/${pkg.name}/${pkg.name}.pid ]; then
service ${pkg.name} stop
fi
exit 0

4
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -20,7 +20,6 @@ package org.thingsboard.server.common.data;
*/
public class DataConstants {
public static final String SYSTEM = "SYSTEM";
public static final String TENANT = "TENANT";
public static final String CUSTOMER = "CUSTOMER";
public static final String DEVICE = "DEVICE";
@ -40,9 +39,6 @@ public class DataConstants {
public static final String DEBUG_RULE_NODE = "DEBUG_RULE_NODE";
public static final String DEBUG_RULE_CHAIN = "DEBUG_RULE_CHAIN";
public static final String ONEWAY = "ONEWAY";
public static final String TWOWAY = "TWOWAY";
public static final String IN = "IN";
public static final String OUT = "OUT";

1
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java

@ -24,7 +24,6 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@Data
@EqualsAndHashCode(callSuper = true)

10
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -172,8 +172,13 @@ public class GatewaySessionHandler {
if (!deviceEntry.getValue().isJsonArray()) {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
}
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray());
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
try {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray());
transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg));
} catch (Throwable e) {
UUID gatewayId = new UUID(gateway.getDeviceIdMSB(), gateway.getDeviceIdLSB());
log.warn("[{}][{}] Failed to convert telemetry: {}", gatewayId, deviceName, deviceEntry.getValue(), e);
}
}
@Override
@ -204,7 +209,6 @@ public class GatewaySessionHandler {
TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject());
transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg));
}
@Override
public void onFailure(Throwable t) {
log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t);

13
dao/src/main/java/org/thingsboard/server/dao/event/CassandraBaseEventDao.java

@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EventId;
@ -69,6 +70,9 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
@Value("${cassandra.query.events_ttl:0}")
private int eventsTtl;
@Value("${cassandra.query.debug_events_ttl:0}")
private int debugEventsTtl;
@Override
public Event save(TenantId tenantId, Event event) {
try {
@ -188,11 +192,16 @@ public class CassandraBaseEventDao extends CassandraAbstractSearchTimeDao<EventE
.value(ModelConstants.EVENT_TYPE_PROPERTY, entity.getEventType())
.value(ModelConstants.EVENT_UID_PROPERTY, entity.getEventUid())
.value(ModelConstants.EVENT_BODY_PROPERTY, entity.getBody());
if (ifNotExists) {
insert = insert.ifNotExists();
}
if(ttl > 0){
insert.using(ttl(ttl));
int selectedTtl = (entity.getEventType().equals(DataConstants.DEBUG_RULE_NODE) ||
entity.getEventType().equals(DataConstants.DEBUG_RULE_CHAIN)) ? debugEventsTtl : ttl;
if (selectedTtl > 0) {
insert.using(ttl(selectedTtl));
}
ResultSetFuture resultSetFuture = executeAsyncWrite(tenantId, insert);
return Futures.transform(resultSetFuture, rs -> {

2
dao/src/test/resources/cassandra-test.properties

@ -48,6 +48,8 @@ cassandra.query.ts_key_value_partitioning=HOURS
cassandra.query.ts_key_value_ttl=0
cassandra.query.debug_events_ttl=604800
cassandra.query.max_limit_per_request=1000
cassandra.query.buffer_size=100000
cassandra.query.concurrent_limit=1000

4
msa/tb-node/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
COPY start-tb-node.sh ${pkg.name}.deb /tmp/
@ -23,6 +23,6 @@ RUN chmod a+x /tmp/*.sh \
RUN dpkg -i /tmp/${pkg.name}.deb
RUN update-rc.d ${pkg.name} disable
RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
CMD ["start-tb-node.sh"]

4
msa/tb/docker-cassandra/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
RUN apt-get update
RUN apt-get install -y curl nmap procps
@ -36,7 +36,7 @@ RUN chmod a+x /tmp/*.sh \
RUN dpkg -i /tmp/${pkg.name}.deb
RUN update-rc.d ${pkg.name} disable
RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
RUN mv /tmp/logback.xml ${pkg.installFolder}/conf \
&& mv /tmp/${pkg.name}.conf ${pkg.installFolder}/conf

4
msa/tb/docker-postgres/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
RUN apt-get update
RUN apt-get install -y postgresql postgresql-contrib
@ -34,7 +34,7 @@ RUN chmod a+x /tmp/*.sh \
RUN dpkg -i /tmp/${pkg.name}.deb
RUN update-rc.d ${pkg.name} disable
RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
RUN mv /tmp/logback.xml ${pkg.installFolder}/conf \
&& mv /tmp/${pkg.name}.conf ${pkg.installFolder}/conf

4
msa/tb/docker-tb/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
COPY logback.xml ${pkg.name}.conf start-db.sh stop-db.sh start-tb.sh upgrade-tb.sh install-tb.sh ${pkg.name}.deb /tmp/
@ -27,7 +27,7 @@ RUN chmod a+x /tmp/*.sh \
RUN dpkg -i /tmp/${pkg.name}.deb
RUN update-rc.d ${pkg.name} disable
RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
RUN mv /tmp/logback.xml ${pkg.installFolder}/conf \
&& mv /tmp/${pkg.name}.conf ${pkg.installFolder}/conf

2
msa/transport/coap/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
COPY start-tb-coap-transport.sh ${pkg.name}.deb /tmp/

2
msa/transport/http/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
COPY start-tb-http-transport.sh ${pkg.name}.deb /tmp/

2
msa/transport/mqtt/docker/Dockerfile

@ -14,7 +14,7 @@
# limitations under the License.
#
FROM openjdk:8-jdk
FROM thingsboard/openjdk8
COPY start-tb-mqtt-transport.sh ${pkg.name}.deb /tmp/

2
pom.xml

@ -47,7 +47,7 @@
<guava.version>21.0</guava.version>
<caffeine.version>2.6.1</caffeine.version>
<commons-lang3.version>3.4</commons-lang3.version>
<commons-validator.version>1.5.0</commons-validator.version>
<commons-validator.version>1.6</commons-validator.version>
<commons-io.version>2.5</commons-io.version>
<commons-csv.version>1.4</commons-csv.version>
<jackson.version>2.9.8</jackson.version>

15
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java

@ -16,6 +16,10 @@
package org.thingsboard.rule.engine.api;
import io.netty.channel.EventLoopGroup;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
@ -38,7 +42,6 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
/**
* Created by ashvayka on 13.01.18.
@ -59,10 +62,20 @@ public interface TbContext {
void updateSelf(RuleNode self);
void sendTbMsgToRuleEngine(TbMsg msg);
TbMsg newMsg(String type, EntityId originator, TbMsgMetaData metaData, String data);
TbMsg transformMsg(TbMsg origMsg, String type, EntityId originator, TbMsgMetaData metaData, String data);
TbMsg customerCreatedMsg(Customer customer, RuleNodeId ruleNodeId);
TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId);
TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId);
TbMsg alarmCreatedMsg(Alarm alarm, RuleNodeId ruleNodeId);
RuleNodeId getSelfId();
TenantId getTenantId();

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

@ -19,7 +19,11 @@ 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.rule.engine.api.ScriptEngine;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
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.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -56,6 +60,7 @@ public abstract class TbAbstractAlarmNode<C extends TbAbstractAlarmNodeConfigura
ctx.tellNext(msg, "False");
} else if (alarmResult.isCreated) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Created");
ctx.sendTbMsgToRuleEngine(ctx.alarmCreatedMsg(alarmResult.alarm, ctx.getSelfId()));
} else if (alarmResult.isUpdated) {
ctx.tellNext(toAlarmMsg(ctx, alarmResult, msg), "Updated");
} else if (alarmResult.isCleared) {

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java

@ -121,10 +121,13 @@ public abstract class TbAbstractCustomerActionNode<C extends TbAbstractCustomerA
Customer newCustomer = new Customer();
newCustomer.setTitle(key.getCustomerTitle());
newCustomer.setTenantId(ctx.getTenantId());
return Optional.of(service.saveCustomer(newCustomer).getId());
Customer savedCustomer = service.saveCustomer(newCustomer);
ctx.sendTbMsgToRuleEngine(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()));
return Optional.of(savedCustomer.getId());
}
return Optional.empty();
}
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java

@ -137,7 +137,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
}
}
protected String processPattern(TbMsg msg, String pattern){
protected String processPattern(TbMsg msg, String pattern) {
return TbNodeUtils.processPattern(pattern, msg.getMetaData());
}
@ -187,6 +187,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
newDevice.setType(entitykey.getType());
newDevice.setTenantId(ctx.getTenantId());
Device savedDevice = deviceService.saveDevice(newDevice);
ctx.sendTbMsgToRuleEngine(ctx.deviceCreatedMsg(savedDevice, ctx.getSelfId()));
targetEntity.setEntityId(savedDevice.getId());
}
break;
@ -201,6 +202,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
newAsset.setType(entitykey.getType());
newAsset.setTenantId(ctx.getTenantId());
Asset savedAsset = assetService.saveAsset(newAsset);
ctx.sendTbMsgToRuleEngine(ctx.assetCreatedMsg(savedAsset, ctx.getSelfId()));
targetEntity.setEntityId(savedAsset.getId());
}
break;
@ -214,6 +216,7 @@ public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationA
newCustomer.setTitle(entitykey.getEntityName());
newCustomer.setTenantId(ctx.getTenantId());
Customer savedCustomer = customerService.saveCustomer(newCustomer);
ctx.sendTbMsgToRuleEngine(ctx.customerCreatedMsg(savedCustomer, ctx.getSelfId()));
targetEntity.setEntityId(savedCustomer.getId());
}
break;

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java

@ -27,7 +27,7 @@ import org.thingsboard.server.common.msg.TbMsg;
type = ComponentType.FILTER,
name = "originator type switch",
configClazz = EmptyNodeConfiguration.class,
relationTypes = {"Device", "Asset", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
relationTypes = {"Device", "Asset", "Entity View", "Tenant", "Customer", "User", "Dashboard", "Rule chain", "Rule node"},
nodeDescription = "Route incoming messages by Message Originator Type",
nodeDetails = "Routes messages to chain according to the originator type ('Device', 'Asset', etc.).",
uiResources = {"static/rulenode/rulenode-core-config.js"},
@ -64,6 +64,9 @@ public class TbOriginatorTypeSwitchNode implements TbNode {
case DEVICE:
relationType = "Device";
break;
case ENTITY_VIEW:
relationType = "Entity View";
break;
case RULE_CHAIN:
relationType = "Rule chain";
break;

30
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java

@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -51,7 +52,7 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
try {
withCallback(
findEntityIdAsync(ctx, msg.getOriginator()),
findEntityIdAsync(ctx, msg),
entityId -> safePutAttributes(ctx, msg, entityId),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
} catch (Throwable th) {
@ -80,11 +81,18 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(ctx.getTenantId(), entityId, scope, keys);
return Futures.transform(latest, l -> {
l.forEach(r -> {
if (r.getValue() != null) {
msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString());
if (BooleanUtils.toBooleanDefaultIfNull(this.config.isTellFailureIfAbsent(), true)) {
if (r.getValue() != null) {
msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString());
} else {
throw new RuntimeException("[" + scope + "][" + r.getKey() + "] attribute value is not present in the DB!");
}
} else {
throw new RuntimeException("[" + scope + "][" + r.getKey() + "] attribute value is not present in the DB!");
if (r.getValue() != null) {
msg.getMetaData().putValue(prefix + r.getKey(), r.getValueAsString());
}
}
});
return null;
});
@ -97,10 +105,16 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, keys);
return Futures.transform(latest, l -> {
l.forEach(r -> {
if (r.getValue() != null) {
msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
if (BooleanUtils.toBooleanDefaultIfNull(this.config.isTellFailureIfAbsent(), true)) {
if (r.getValue() != null) {
msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
} else {
throw new RuntimeException("[" + r.getKey() + "] telemetry value is not present in the DB!");
}
} else {
throw new RuntimeException("[" + r.getKey() + "] telemetry value is not present in the DB!");
if (r.getValue() != null) {
msg.getMetaData().putValue(r.getKey(), r.getValueAsString());
}
}
});
return null;
@ -112,5 +126,5 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
}
protected abstract ListenableFuture<T> findEntityIdAsync(TbContext ctx, EntityId originator);
protected abstract ListenableFuture<T> findEntityIdAsync(TbContext ctx, TbMsg msg);
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java

@ -25,6 +25,7 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
/**
* Created by ashvayka on 19.01.18.
@ -48,7 +49,8 @@ public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttrib
}
@Override
protected ListenableFuture<EntityId> findEntityIdAsync(TbContext ctx, EntityId originator) {
return Futures.immediateFuture(originator);
protected ListenableFuture<EntityId> findEntityIdAsync(TbContext ctx, TbMsg msg) {
return Futures.immediateFuture(msg.getOriginator());
}
}

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java

@ -33,6 +33,8 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGet
private List<String> latestTsKeyNames;
private boolean tellFailureIfAbsent;
@Override
public TbGetAttributesNodeConfiguration defaultConfiguration() {
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
@ -40,6 +42,7 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGet
configuration.setSharedAttributeNames(Collections.emptyList());
configuration.setServerAttributeNames(Collections.emptyList());
configuration.setLatestTsKeyNames(Collections.emptyList());
configuration.setTellFailureIfAbsent(true);
return configuration;
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java

@ -26,6 +26,7 @@ import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
@ -46,7 +47,8 @@ public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDevice
}
@Override
protected ListenableFuture<DeviceId> findEntityIdAsync(TbContext ctx, EntityId originator) {
return EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, originator, config.getDeviceRelationsQuery());
protected ListenableFuture<DeviceId> findEntityIdAsync(TbContext ctx, TbMsg msg) {
return EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery());
}
}

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java

@ -34,6 +34,7 @@ public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfigu
configuration.setSharedAttributeNames(Collections.emptyList());
configuration.setServerAttributeNames(Collections.emptyList());
configuration.setLatestTsKeyNames(Collections.emptyList());
configuration.setTellFailureIfAbsent(true);
DeviceRelationsQuery deviceRelationsQuery = new DeviceRelationsQuery();
deviceRelationsQuery.setDirection(EntitySearchDirection.FROM);

8
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

13
ui/package-lock.json

@ -7755,11 +7755,24 @@
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz",
"integrity": "sha512-FYL1LGFdj6v+2Ifpw+AcFIuIOqjNggfoLUwuwQv6+3sS21Za7Wvapq+LhbSE4NDXrEj6eYnW3y7LsaBICpyXtw=="
},
"leaflet-polylinedecorator": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/leaflet-polylinedecorator/-/leaflet-polylinedecorator-1.6.0.tgz",
"integrity": "sha1-nvef0bUwLWe3Lv6Vmo7NJVPycmY=",
"requires": {
"leaflet-rotatedmarker": "^0.2.0"
}
},
"leaflet-providers": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/leaflet-providers/-/leaflet-providers-1.5.0.tgz",
"integrity": "sha512-btncloSyOHrgYNexoz2dRpCl+U9iDQME91RsOWQWNAD9jQUPAkq9mxuTvL/O9VOwrqcEtzhvuHBHIOacJAZDxQ=="
},
"leaflet-rotatedmarker": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/leaflet-rotatedmarker/-/leaflet-rotatedmarker-0.2.0.tgz",
"integrity": "sha1-RGf0n5jRv9VpWb2cZwUgPdJgEnc="
},
"less": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz",

1
ui/package.json

@ -63,6 +63,7 @@
"jstree": "^3.3.7",
"jstree-bootstrap-theme": "^1.0.1",
"leaflet": "^1.0.3",
"leaflet-polylinedecorator": "^1.6.0",
"leaflet-providers": "^1.1.17",
"material-ui": "^0.16.1",
"material-ui-number-input": "^5.0.16",

2
ui/src/app/components/dashboard.directive.js

@ -979,7 +979,7 @@ function DashboardController($scope, $rootScope, $element, $timeout, $mdMedia, $
function hasTimewindow(widget) {
if (widget.type === types.widgetType.timeseries.value || widget.type === types.widgetType.alarm.value) {
return angular.isDefined(widget.config.useDashboardTimewindow) ?
!widget.config.useDashboardTimewindow : false;
(!widget.config.useDashboardTimewindow && (angular.isUndefined(widget.config.displayTimewindow) || widget.config.displayTimewindow)) : false;
} else {
return false;
}

14
ui/src/app/components/dashboard.scss

@ -24,12 +24,19 @@ div.tb-widget {
transition: all .2s ease-in-out;
.tb-widget-title {
max-height: 60px;
.tb-widget-header{
display: flex;
align-items: start;
justify-content: space-between;
}
.tb-widget-title {
display: block;
max-height: 75px;
padding-top: 5px;
padding-left: 5px;
overflow: hidden;
text-overflow: ellipsis;
tb-timewindow {
font-size: 14px;
@ -38,9 +45,6 @@ div.tb-widget {
}
.tb-widget-actions {
position: absolute;
top: 8px;
right: 8px;
z-index: 19;
margin: 0;

124
ui/src/app/components/dashboard.tpl.html

@ -44,66 +44,68 @@
ng-click="vm.widgetClicked($event, widget)"
tb-contextmenu="vm.openWidgetContextMenu($event, widget, $mdOpenMousepointMenu)"
ng-style="vm.widgetStyle(widget)">
<div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitlePanel(widget)">
<div ng-if="vm.hasWidgetTitleTemplate(widget)" ng-include="vm.widgetTitleTemplate(widget)"></div>
<span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{vm.widgetTitle(widget)}}</span>
<tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
<div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
<md-button ng-repeat="action in vm.customWidgetHeaderActions(widget)"
aria-label="{{action.displayName}}"
ng-show="!vm.isEdit"
ng-click="action.onAction($event)"
class="md-icon-button">
<md-tooltip md-direction="{{vm.isWidgetExpanded ? 'bottom' : 'top'}}">
{{action.displayName}}
</md-tooltip>
<ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
</md-button>
<md-button ng-repeat="action in vm.widgetActions(widget)"
aria-label="{{ action.name | translate }}"
ng-show="!vm.isEdit && action.show"
ng-click="action.onAction($event)"
class="md-icon-button">
<md-tooltip md-direction="{{vm.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ action.name | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
</md-button>
<md-button id="expand-button"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="edit"></ng-md-icon>
</md-button>
<md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button"
ng-click="vm.exportWidget($event, widget)"
aria-label="{{ 'widget.export' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.export' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="file_download"></ng-md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="close"></ng-md-icon>
</md-button>
<div class="tb-widget-header">
<div class="tb-widget-title" layout="column" layout-align="center start" ng-show="vm.showWidgetTitlePanel(widget)">
<div ng-if="vm.hasWidgetTitleTemplate(widget)" ng-include="vm.widgetTitleTemplate(widget)"></div>
<span ng-show="vm.showWidgetTitle(widget)" ng-style="vm.widgetTitleStyle(widget)" class="md-subhead">{{vm.widgetTitle(widget)}}</span>
<tb-timewindow aggregation="{{vm.hasAggregation(widget)}}" ng-if="vm.hasTimewindow(widget)" ng-model="widget.config.timewindow"></tb-timewindow>
</div>
<div class="tb-widget-actions" layout="row" layout-align="start center" ng-show="vm.showWidgetActions(widget)" tb-mousedown="$event.stopPropagation()">
<md-button ng-repeat="action in vm.customWidgetHeaderActions(widget)"
aria-label="{{action.displayName}}"
ng-show="!vm.isEdit"
ng-click="action.onAction($event)"
class="md-icon-button">
<md-tooltip md-direction="{{vm.isWidgetExpanded ? 'bottom' : 'top'}}">
{{action.displayName}}
</md-tooltip>
<ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
</md-button>
<md-button ng-repeat="action in vm.widgetActions(widget)"
aria-label="{{ action.name | translate }}"
ng-show="!vm.isEdit && action.show"
ng-click="action.onAction($event)"
class="md-icon-button">
<md-tooltip md-direction="{{vm.isWidgetExpanded ? 'bottom' : 'top'}}">
{{ action.name | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="{{action.icon}}"></ng-md-icon>
</md-button>
<md-button id="expand-button"
ng-show="!vm.isEdit && vm.enableWidgetFullscreen(widget)"
aria-label="{{ 'fullscreen.fullscreen' | translate }}"
class="md-icon-button"></md-button>
<md-button ng-show="vm.isEditActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button"
ng-click="vm.editWidget($event, widget)"
aria-label="{{ 'widget.edit' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.edit' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="edit"></ng-md-icon>
</md-button>
<md-button ng-show="vm.isExportActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button"
ng-click="vm.exportWidget($event, widget)"
aria-label="{{ 'widget.export' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.export' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="file_download"></ng-md-icon>
</md-button>
<md-button ng-show="vm.isRemoveActionEnabled && !vm.isWidgetExpanded"
ng-disabled="vm.loading()"
class="md-icon-button"
ng-click="vm.removeWidget($event, widget)"
aria-label="{{ 'widget.remove' | translate }}">
<md-tooltip md-direction="top">
{{ 'widget.remove' | translate }}
</md-tooltip>
<ng-md-icon size="20" icon="close"></ng-md-icon>
</md-button>
</div>
</div>
<div flex layout="column" class="tb-widget-content">
<div flex tb-widget
@ -143,4 +145,4 @@
</md-button>
</md-menu-item>
</md-menu-content>
</md-menu>
</md-menu>

4
ui/src/app/components/timewindow.tpl.html

@ -16,7 +16,7 @@
-->
<section class="tb-timewindow" layout='row' layout-align="start center">
<md-button ng-if="direction === 'left'" ng-disabled="disabled" class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
<md-button ng-if="direction === 'left'" ng-disabled="disabled" class="md-icon-button" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
<md-tooltip md-direction="{{tooltipDirection}}">
{{ 'timewindow.edit' | translate }}
</md-tooltip>
@ -28,7 +28,7 @@
</md-tooltip>
{{model.displayValue}}
</span>
<md-button ng-if="direction === 'right'" ng-disabled="disabled" class="md-icon-button tb-md-32" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
<md-button ng-if="direction === 'right'" ng-disabled="disabled" class="md-icon-button" aria-label="{{ 'timewindow.edit' | translate }}" ng-click="openEditMode($event)">
<md-tooltip md-direction="{{tooltipDirection}}">
{{ 'timewindow.edit' | translate }}
</md-tooltip>

5
ui/src/app/components/widget/widget-config.directive.js

@ -124,6 +124,8 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
scope.decimals = config.decimals;
scope.useDashboardTimewindow = angular.isDefined(config.useDashboardTimewindow) ?
config.useDashboardTimewindow : true;
scope.displayTimewindow = angular.isDefined(config.displayTimewindow) ?
config.displayTimewindow : true;
scope.timewindow = config.timewindow;
scope.showLegend = angular.isDefined(config.showLegend) ?
config.showLegend : scope.widgetType === types.widgetType.timeseries.value;
@ -230,7 +232,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
};
scope.$watch('title + showTitle + dropShadow + enableFullscreen + backgroundColor + color + ' +
'padding + margin + widgetStyle + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + ' +
'padding + margin + widgetStyle + titleStyle + mobileOrder + mobileHeight + units + decimals + useDashboardTimewindow + displayTimewindow + ' +
'alarmSearchStatus + alarmsPollingInterval + showLegend', function () {
if (ngModelCtrl.$viewValue) {
var value = ngModelCtrl.$viewValue;
@ -257,6 +259,7 @@ function WidgetConfig($compile, $templateCache, $rootScope, $translate, $timeout
config.units = scope.units;
config.decimals = scope.decimals;
config.useDashboardTimewindow = scope.useDashboardTimewindow;
config.displayTimewindow = scope.displayTimewindow;
config.alarmSearchStatus = scope.alarmSearchStatus;
config.alarmsPollingInterval = scope.alarmsPollingInterval;
config.showLegend = scope.showLegend;

3
ui/src/app/components/widget/widget-config.tpl.html

@ -26,6 +26,9 @@
<md-checkbox flex aria-label="{{ 'widget-config.use-dashboard-timewindow' | translate }}"
ng-model="useDashboardTimewindow">{{ 'widget-config.use-dashboard-timewindow' | translate }}
</md-checkbox>
<md-checkbox ng-disabled="useDashboardTimewindow" flex aria-label="{{ 'widget-config.display-timewindow' | translate }}"
ng-model="displayTimewindow">{{ 'widget-config.display-timewindow' | translate }}
</md-checkbox>
<section flex layout="row" layout-align="start center" style="margin-bottom: 16px;">
<span ng-class="{'tb-disabled-label': useDashboardTimewindow}" translate style="padding-right: 8px;">widget-config.timewindow</span>
<tb-timewindow ng-disabled="useDashboardTimewindow" as-button="true" aggregation="{{ widgetType === types.widgetType.timeseries.value }}"

5
ui/src/app/components/widget/widget.controller.js

@ -287,6 +287,9 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
options.useDashboardTimewindow = angular.isDefined(widget.config.useDashboardTimewindow)
? widget.config.useDashboardTimewindow : true;
options.displayTimewindow = angular.isDefined(widget.config.displayTimewindow)
? widget.config.displayTimewindow : !options.useDashboardTimewindow;
options.timeWindowConfig = options.useDashboardTimewindow ? vm.dashboardTimewindow : widget.config.timewindow;
options.legendConfig = null;
@ -909,4 +912,4 @@ export default function WidgetController($scope, $state, $timeout, $window, $ele
}
/* eslint-enable angular/angularelement */
/* eslint-enable angular/angularelement */

1
ui/src/app/dashboard/dashboard-toolbar.scss

@ -166,6 +166,7 @@ tb-dashboard-toolbar {
}
md-select {
margin: 0;
pointer-events: all;
}

2
ui/src/app/dashboard/dashboard.tpl.html

@ -21,7 +21,7 @@
ng-class="{ 'tb-dashboard-toolbar-opened': vm.toolbarOpened, 'tb-dashboard-toolbar-closed': !vm.toolbarOpened }">
<tb-dashboard-toolbar ng-show="!vm.widgetEditMode" force-fullscreen="forceFullscreen"
toolbar-opened="vm.toolbarOpened" on-trigger-click="vm.openToolbar()">
<div class="tb-dashboard-action-panels" layout-gt-sm="row" layout-align-gt-sm="space-between center" layout="column" layout-align="center stretch">
<div class="tb-dashboard-action-panels" layout-sm="column" layout-xs="column" layout-align-gt-sm="space-between center" layout="row" layout-align="center stretch">
<div class="tb-dashboard-action-panel" flex-md="30" layout="row" layout-align-gt-sm="start center" layout-align="space-between center">
<md-button ng-show="vm.showCloseToolbar()" aria-label="close-toolbar" class="md-icon-button close-action" ng-click="vm.closeToolbar()">
<md-tooltip md-direction="bottom">

5
ui/src/app/locale/locale.constant-de_DE.json

@ -23,7 +23,7 @@
"remove": "Löschen",
"search": "Suche",
"clear-search": "Suchanfrage löschen",
"assign": "Zoordnen",
"assign": "Zuordnen",
"unassign": "Zuordnung aufheben",
"share": "Teilen",
"make-private": "Privat machen",
@ -1515,6 +1515,7 @@
"decimals": "Anzahl der Stellen nach dem Fließkomma",
"timewindow": "Zeitfenster",
"use-dashboard-timewindow": "Dashboard-Zeitfenster verwenden",
"display-timewindow": "Zeitfenster anzeigen",
"display-legend": "Legende anzeigen",
"datasources": "Datenquellen",
"maximum-datasources": "Maximal { count, plural, 1 {1 Datenquelle ist erlaubt} other {# Datenquellen sind erlaubt} }.",
@ -1640,4 +1641,4 @@
"uk_UA": "Ukrainisch"
}
}
}
}

1
ui/src/app/locale/locale.constant-en_US.json

@ -1526,6 +1526,7 @@
"decimals": "Number of digits after floating point",
"timewindow": "Timewindow",
"use-dashboard-timewindow": "Use dashboard timewindow",
"display-timewindow": "Display timewindow",
"display-legend": "Display legend",
"datasources": "Datasources",
"maximum-datasources": "Maximum { count, plural, 1 {1 datasource is allowed.} other {# datasources are allowed} }",

3
ui/src/app/locale/locale.constant-es_ES.json

@ -1515,6 +1515,7 @@
"decimals": "Número de dígitos después del punto flotante",
"timewindow": "Ventana de tiempo",
"use-dashboard-timewindow": "Utilizar ventana de tiempo del panel",
"display-timewindow": "Mostrar ventana de tiempo",
"display-legend": "Mostrar leyenda",
"datasources": "Orígenes de datos",
"maximum-datasources": "Máximo { count, plural, 1 {1 origen de datos permitido.} other {# origenes de datos permitidos} }",
@ -1640,4 +1641,4 @@
"uk_UA": "Ucraniano"
}
}
}
}

5
ui/src/app/locale/locale.constant-fr_FR.json

@ -1028,7 +1028,7 @@
"settings": "Paramètres de mise en page"
},
"legend": {
"avg": "avg",
"avg": "moy",
"max": "max",
"min": "min",
"position": "Position de la légende",
@ -1405,6 +1405,7 @@
"delete-action": "Supprimer l'action",
"delete-action-text": "Etes-vous sûr de vouloir supprimer l'action du widget nommé '{{actionName}}'?",
"delete-action-title": "Supprimer l'action du widget",
"display-timewindow": "Afficher fenêtre de temps",
"display-legend": "Afficher la légende",
"display-title": "Afficher le titre",
"drop-shadow": "Ombre portée",
@ -1523,4 +1524,4 @@
"widgets-bundle-required": "Un groupe de widgets est requis.",
"widgets-bundles": "Groupes de widgets"
}
}
}

1
ui/src/app/locale/locale.constant-it_IT.json

@ -1520,6 +1520,7 @@
"decimals": "Numero di cifre decimali",
"timewindow": "Intervallo temporale",
"use-dashboard-timewindow": "Usa intervallo temporale dashboard",
"display-timewindow": "Mostra intervallo temporale",
"display-legend": "Mostra legenda",
"datasources": "Sorgenti dei dati",
"maximum-datasources": "Massimo { count, plural, 1 {1 sorgente dati consentita.} other {# sorgenti dati consentite} }",

96
ui/src/app/locale/locale.constant-zh_CN.json

@ -740,6 +740,102 @@
"no-entities-prompt": "没有找到实体",
"no-data": "无数据"
},
"entity-view": {
"entity-view": "实体视图",
"entity-view-required": "实体视图必填。",
"entity-views": "实体视图",
"management": "实体视图管理",
"view-entity-views": "查看实体视图",
"entity-view-alias": "实体视图别名",
"aliases": "实体视图别名",
"no-alias-matching": "'{{alias}}' 没有找到。",
"no-aliases-found": "找不到别名。",
"no-key-matching": "'{{key}}' 没有找到。",
"no-keys-found": "找不到密钥。",
"create-new-alias": "创建一个新的别名!",
"create-new-key": "创建一个新的密钥!",
"duplicate-alias-error": "找到重复别名 '{{alias}}'.<br>实体视图别名在仪表板中必须是唯一的。",
"configure-alias": "配置 '{{alias}}' 别名",
"no-entity-views-matching": "没有实体视图匹配 '{{entity}}' 被找到。",
"alias": "别名",
"alias-required": "视图实体别名必填。",
"remove-alias": "移除视图实体别名",
"add-alias": "添加视图实体别名",
"name-starts-with": "实体视图名称前缀",
"entity-view-list": "实体视图列表",
"use-entity-view-name-filter": "使用过滤器",
"entity-view-list-empty": "未选择任何实体视图。",
"entity-view-name-filter-required": "实体视图名称过滤器必填。",
"entity-view-name-filter-no-entity-view-matched": "没有实体视图名称前缀 '{{entityView}}' 被找到。",
"add": "添加视图实体",
"assign-to-customer": "分配给客户",
"assign-entity-view-to-customer": "将实体视图分配给客户",
"assign-entity-view-to-customer-text": "请选择要分配给客户的实体视图",
"no-entity-views-text": "找不到实体视图",
"assign-to-customer-text": "请选择客户以分配实体视图",
"entity-view-details": "实体视图详细信息",
"add-entity-view-text": "添加新的实体视图",
"delete": "删除实体视图",
"assign-entity-views": "分配实体视图",
"assign-entity-views-text": "分配 { count, plural, 1 {1 实体视图} other {# 实体视图} } 给客户",
"delete-entity-views": "移除实体视图",
"unassign-from-customer": "取消分配客户",
"unassign-entity-views": "取消分配实体视图",
"unassign-entity-views-action-title": "从客户处取消分配 { count, plural, 1 {1 实体视图} other {# 实体视图} }",
"assign-new-entity-view": "分配新的实体视图",
"delete-entity-view-title": "您确定要删除实体视图 '{{entityViewName}}'吗?",
"delete-entity-view-text": "请注意,在确认后实体视图和所有相关数据将变得不可恢复。",
"delete-entity-views-title": "你确定要删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }吗?",
"delete-entity-views-action-title": "删除 { count, plural, 1 {1 实体视图} other {# 实体视图} }",
"delete-entity-views-text": "请注意,在确认后将删除所有选定的实体视图,并且所有相关数据将变为不可恢复。",
"unassign-entity-view-title": "您确定要取消分配实体视图 '{{entityViewName}}'吗?",
"unassign-entity-view-text": "确认后,实体视图将被取消分配,客户将无法访问。",
"unassign-entity-view": "取消分配实体视图",
"unassign-entity-views-title": "你确定要取消分配 { count, plural, 1 {1 实体视图} other {# 实体视图} }吗?",
"unassign-entity-views-text": "确认后,所有选定的实体视图都将被取消分配,客户将无法访问。",
"entity-view-type": "实体视图类型",
"entity-view-type-required": "实体视图类型必填。",
"select-entity-view-type": "选择实体视图类型",
"enter-entity-view-type": "输入实体视图类型",
"any-entity-view": "任何实体视图",
"no-entity-view-types-matching": "没有找到匹配 '{{entitySubtype}}' 的实体视图类型。",
"entity-view-type-list-empty": "未选择任何实体视图类型。",
"entity-view-types": "实体视图类型",
"name": "名称",
"name-required": "名称必填。",
"description": "描述",
"events": "事件",
"details": "详情",
"copyId": "复制实体视图ID",
"assignedToCustomer": "分配给客户",
"unable-entity-view-device-alias-title": "无法删除实体视图别名",
"unable-entity-view-device-alias-text": "设备别名 '{{entityViewAlias}}' 无法删除,因为它由以下小部件使用:<br/>{{widgetsList}}",
"select-entity-view": "选择实体视图",
"make-public": "将实体视图公开",
"make-private": "将实体视图设为私有",
"start-date": "开始日期",
"start-ts": "开始时间",
"end-date": "结束日期",
"end-ts": "结束时间",
"date-limits": "日期范围",
"client-attributes": "客户属性",
"shared-attributes": "共享属性",
"server-attributes": "服务器属性",
"timeseries": "时间序列",
"client-attributes-placeholder": "客户属性",
"shared-attributes-placeholder": "共享属性",
"server-attributes-placeholder": "服务器属性",
"timeseries-placeholder": "时间序列",
"target-entity": "目标实体",
"attributes-propagation": "属性传播",
"attributes-propagation-hint": "每次保存或更新此实体视图时,实体视图将自动从目标实体复制指定的属性。出于性能原因,目标实体属性不会在每次属性更改时传播到实体视图。通过在规则链中配置“copy to view”规则节点,并将“Post attributes”和“Attributes Updated”消息链接到新的规则节点,可以启用自动传播。",
"timeseries-data": "时间序列数据",
"timeseries-data-hint": "配置实体视图可访问目标实体的时间序列数据键。此时间序列数据是只读的。",
"make-public-entity-view-title": "您确定要将实体视图 '{{entityViewName}}' 设为公开吗?",
"make-public-entity-view-text": "确认后,设备及其所有数据将被设为公开并可被其他人访问。",
"make-private-entity-view-title": "您确定要将实体视图 '{{entityViewName}}' 设为私有吗?",
"make-private-entity-view-text": "确认后,设备及其所有数据将被设为私有,不被其他人访问。"
},
"event": {
"event-type": "事件类型",
"type-error": "错误",

18
ui/src/app/widget/lib/flot-widget.js

@ -535,7 +535,11 @@ export default class TbFlot {
yaxis.tickUnits = units;
yaxis.tickDecimals = tickDecimals;
yaxis.tickSize = tickSize;
yaxis.alignTicksWithAxis = position == "right" ? 1 : null;
if (position === "right" && tickSize === null) {
yaxis.alignTicksWithAxis = 1;
} else {
yaxis.alignTicksWithAxis = null;
}
yaxis.position = position;
yaxis.keysInfo = [];
@ -938,11 +942,6 @@ export default class TbFlot {
"type": "string",
"default": null
},
"titleAngle": {
"title": "Axis title's angle in degrees",
"type": "number",
"default": 0
},
"color": {
"title": "Ticks color",
"type": "string",
@ -975,11 +974,6 @@ export default class TbFlot {
"type": "string",
"default": null
},
"titleAngle": {
"title": "Axis title's angle in degrees",
"type": "number",
"default": 0
},
"color": {
"title": "Ticks color",
"type": "string",
@ -1048,7 +1042,6 @@ export default class TbFlot {
"items": [
"xaxis.showLabels",
"xaxis.title",
"xaxis.titleAngle",
{
"key": "xaxis.color",
"type": "color"
@ -1064,7 +1057,6 @@ export default class TbFlot {
"yaxis.tickSize",
"yaxis.showLabels",
"yaxis.title",
"yaxis.titleAngle",
{
"key": "yaxis.color",
"type": "color"

25
ui/src/app/widget/lib/google-map.js

@ -266,14 +266,23 @@ export default class TbGoogleMap {
content: ''
});
var map = this;
marker.addListener('click', function() {
if (settings.autocloseTooltip) {
map.tooltips.forEach((tooltip) => {
tooltip.popup.close();
});
}
popup.open(this.map, marker);
});
if (settings.displayTooltipAction == 'hover') {
marker.addListener('mouseover', function () {
popup.open(this.map, marker);
});
marker.addListener('mouseout', function () {
popup.close();
});
} else {
marker.addListener('click', function() {
if (settings.autocloseTooltip) {
map.tooltips.forEach((tooltip) => {
tooltip.popup.close();
});
}
popup.open(this.map, marker);
});
}
this.tooltips.push( {
markerArgs: markerArgs,
popup: popup,

9
ui/src/app/widget/lib/image-map.js

@ -354,6 +354,15 @@ export default class TbImageMap {
var popup = L.popup();
popup.setContent('');
marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false});
if (settings.displayTooltipAction == 'hover') {
marker.off('click');
marker.on('mouseover', function () {
this.openPopup();
});
marker.on('mouseout', function () {
this.closePopup();
});
}
this.tooltips.push( {
markerArgs: markerArgs,
popup: popup,

43
ui/src/app/widget/lib/map-widget2.js

@ -156,8 +156,9 @@ export default class TbMapWidgetV2 {
this.locationSettings.showLabel = this.ctx.settings.showLabel !== false;
this.locationSettings.displayTooltip = this.ctx.settings.showTooltip !== false;
this.locationSettings.displayTooltipAction = this.ctx.settings.showTooltipAction && this.ctx.settings.showTooltipAction.length ? this.ctx.settings.showTooltipAction : "click";
this.locationSettings.autocloseTooltip = this.ctx.settings.autocloseTooltip !== false;
this.locationSettings.showPolygon = this.ctx.settings.showPolygon !== false;
this.locationSettings.showPolygon = this.ctx.settings.showPolygon === true;
this.locationSettings.labelColor = this.ctx.widgetConfig.color || '#000000';
this.locationSettings.label = this.ctx.settings.label || "${entityName}";
this.locationSettings.color = this.ctx.settings.color ? tinycolor(this.ctx.settings.color).toHexString() : "#FE7569";
@ -978,6 +979,11 @@ const commonMapSettingsSchema =
"type": "boolean",
"default": true
},
"showTooltipAction": {
"title": "Action for displaying the tooltip",
"type": "string",
"default": "click"
},
"autocloseTooltip": {
"title": "Auto-close tooltips",
"type": "boolean",
@ -1095,6 +1101,21 @@ const commonMapSettingsSchema =
"type": "javascript"
},
"showTooltip",
{
"key": "showTooltipAction",
"type": "rc-select",
"multiple": false,
"items": [
{
"value": "click",
"label": "Show tooltip on click (Default)"
},
{
"value": "hover",
"label": "Show tooltip on hover"
}
]
},
"autocloseTooltip",
{
"key": "tooltipPattern",
@ -1235,6 +1256,11 @@ const imageMapSettingsSchema =
"type": "boolean",
"default": true
},
"showTooltipAction": {
"title": "Action for displaying the tooltip",
"type": "string",
"default": "click"
},
"autocloseTooltip": {
"title": "Auto-close tooltips",
"type": "boolean",
@ -1329,6 +1355,21 @@ const imageMapSettingsSchema =
"type": "javascript"
},
"showTooltip",
{
"key": "showTooltipAction",
"type": "rc-select",
"multiple": false,
"items": [
{
"value": "click",
"label": "Show tooltip on click (Default)"
},
{
"value": "hover",
"label": "Show tooltip on hover"
}
]
},
"autocloseTooltip",
{
"key": "tooltipPattern",

9
ui/src/app/widget/lib/openstreet-map.js

@ -168,6 +168,15 @@ export default class TbOpenStreetMap {
var popup = L.popup();
popup.setContent('');
marker.bindPopup(popup, {autoClose: settings.autocloseTooltip, closeOnClick: false});
if (settings.displayTooltipAction == 'hover') {
marker.off('click');
marker.on('mouseover', function () {
this.openPopup();
});
marker.on('mouseout', function () {
this.closePopup();
});
}
this.tooltips.push({
markerArgs: markerArgs,
popup: popup,

28
ui/src/app/widget/lib/tencent-map.js

@ -278,15 +278,25 @@ export default class TbTencentMap {
map: this.map
});
var map = this;
qq.maps.event.addListener(marker, 'click', function () {
if (settings.autocloseTooltip) {
map.tooltips.forEach((tooltip) => {
tooltip.popup.close();
});
}
popup.open();
popup.setPosition(marker);
});
if (settings.displayTooltipAction == 'hover') {
qq.maps.event.addListener(marker, 'mouseover', function () {
popup.open();
popup.setPosition(marker);
});
qq.maps.event.addListener(marker, 'mouseout', function () {
popup.close();
});
} else {
qq.maps.event.addListener(marker, 'click', function () {
if (settings.autocloseTooltip) {
map.tooltips.forEach((tooltip) => {
tooltip.popup.close();
});
}
popup.open();
popup.setPosition(marker);
});
}
map.tooltips.push({
markerArgs: markerArgs,
popup: popup,

26
ui/src/app/widget/lib/timeseries-table-widget.js

@ -58,6 +58,9 @@ function TimeseriesTableWidgetController($element, $scope, $filter, $timeout, ty
vm.enterFilterMode = enterFilterMode;
vm.exitFilterMode = exitFilterMode;
vm.onRowClick = onRowClick;
vm.onActionButtonClick = onActionButtonClick;
vm.actionCellDescriptors = [];
function enterFilterMode () {
vm.query.search = '';
@ -93,6 +96,7 @@ function TimeseriesTableWidgetController($element, $scope, $filter, $timeout, ty
function initialize() {
vm.ctx.widgetActions = [ vm.searchAction ];
vm.actionCellDescriptors = vm.ctx.actionsApi.getActionDescriptors('actionCellButton');
vm.showTimestamp = vm.settings.showTimestamp !== false;
var origColor = vm.widgetConfig.color || 'rgba(0, 0, 0, 0.87)';
var defaultColor = tinycolor(origColor);
@ -179,6 +183,28 @@ function TimeseriesTableWidgetController($element, $scope, $filter, $timeout, ty
updatePage(source);
}
function onRowClick($event, row) {
if ($event) {
$event.stopPropagation();
}
var descriptors = vm.ctx.actionsApi.getActionDescriptors('rowClick');
if (descriptors.length) {
var entityId = vm.ctx.activeEntityInfo.entityId;
var entityName = vm.ctx.activeEntityInfo.entityName;
vm.ctx.actionsApi.handleWidgetAction($event, descriptors[0], entityId, entityName, row);
}
}
function onActionButtonClick($event, row, actionDescriptor) {
if ($event) {
$event.stopPropagation();
}
var entityId = vm.ctx.activeEntityInfo.entityId;
var entityName = vm.ctx.activeEntityInfo.entityName;
vm.ctx.actionsApi.handleWidgetAction($event, actionDescriptor, entityId, entityName, row);
}
vm.cellStyle = function(source, index, value) {
var style = {};
if (index > 0) {

17
ui/src/app/widget/lib/timeseries-table-widget.tpl.html

@ -60,13 +60,26 @@
</thead>
<tbody md-body>
<tr md-row ng-repeat="row in source.ts.data track by $index">
<tr md-row ng-repeat="row in source.ts.data track by $index" ng-click="vm.onRowClick($event, row)">
<td ng-show="$index > 0 || ($index === 0 && vm.showTimestamp)"
md-cell
ng-repeat="d in row track by $index"
ng-style="vm.cellStyle(source, $index, d)"
ng-bind-html="vm.cellContent(source, $index, row, d)"
></td>
<td md-cell class="tb-action-cell"
ng-style="{minWidth: vm.actionCellDescriptors.length*36+'px',
maxWidth: vm.actionCellDescriptors.length*36+'px',
width: vm.actionCellDescriptors.length*36+'px'}">
<md-button class="md-icon-button" ng-repeat="actionDescriptor in vm.actionCellDescriptors"
aria-label="{{ actionDescriptor.displayName }}"
ng-click="vm.onActionButtonClick($event, row, actionDescriptor)" ng-disabled="$root.loading">
<md-icon aria-label="{{ actionDescriptor.displayName }}" class="material-icons">{{actionDescriptor.icon}}</md-icon>
<md-tooltip md-direction="top">
{{ actionDescriptor.displayName }}
</md-tooltip>
</md-button>
</td>
</tr>
</tbody>
</table>
@ -86,4 +99,4 @@
md-page-select>
</md-table-pagination>
</div>
</div>
</div>

154
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.js

@ -17,6 +17,7 @@ import './trip-animation-widget.scss';
import template from "./trip-animation-widget.tpl.html";
import TbOpenStreetMap from '../openstreet-map';
import L from 'leaflet';
import 'leaflet-polylinedecorator'
import tinycolor from "tinycolor2";
import {fillPatternWithActions, isNumber, padValue, processPattern} from "../widget-utils";
@ -24,7 +25,6 @@ import {fillPatternWithActions, isNumber, padValue, processPattern} from "../wid
// save these original methods before they are overwritten
var proto_initIcon = L.Marker.prototype._initIcon;
var proto_setPos = L.Marker.prototype._setPos;
var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
L.Marker.addInitHook(function () {
@ -199,23 +199,45 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
vm.moveNext = function () {
vm.stopPlay();
moveInc(1);
}
if (vm.staticSettings.usePointAsAnchor) {
let newIndex = vm.maxTime;
for (let index = vm.index+1; index < vm.maxTime; index++) {
if (vm.trips.some(function (trip) {
return trip.timeRange[index].hasAnchor;
})) {
newIndex = index;
break;
}
}
moveToIndex(newIndex);
} else moveInc(1);
};
vm.movePrev = function () {
vm.stopPlay();
moveInc(-1);
}
if (vm.staticSettings.usePointAsAnchor) {
let newIndex = vm.minTime;
for (let index = vm.index-1; index > vm.minTime; index--) {
if (vm.trips.some(function (trip) {
return trip.timeRange[index].hasAnchor;
})) {
newIndex = index;
break;
}
}
moveToIndex(newIndex);
} else moveInc(-1);
};
vm.moveStart = function () {
vm.stopPlay();
moveToIndex(vm.minTime);
}
};
vm.moveEnd = function () {
vm.stopPlay();
moveToIndex(vm.maxTime);
}
};
vm.stopPlay = function () {
if (vm.isPlaying) {
@ -280,6 +302,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
let staticSettings = {};
vm.staticSettings = staticSettings;
//Calculate General Settings
staticSettings.normalizationStep = vm.ctx.settings.normalizationStep || 1000;
staticSettings.buttonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.54).toRgbString();
staticSettings.disabledButtonColor = tinycolor(vm.widgetConfig.color).setAlpha(0.3).toRgbString();
staticSettings.polygonColor = tinycolor(vm.ctx.settings.polygonColor).toHexString();
@ -301,8 +324,19 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
staticSettings.showTooltip = false;
staticSettings.label = vm.ctx.settings.label || "${entityName}";
staticSettings.useLabelFunction = vm.ctx.settings.useLabelFunction || false;
staticSettings.autocloseTooltip = vm.ctx.settings.autocloseTooltip || false;
staticSettings.pointTooltipOnRightPanel = vm.ctx.settings.pointTooltipOnRightPanel || false;
staticSettings.usePointAsAnchor = vm.ctx.settings.usePointAsAnchor || false;
staticSettings.showLabel = vm.ctx.settings.showLabel || false;
staticSettings.useTooltipFunction = vm.ctx.settings.useTooltipFunction || false;
staticSettings.usePolylineDecorator = vm.ctx.settings.usePolylineDecorator || false;
staticSettings.useDecoratorCustomColor = vm.ctx.settings.useDecoratorCustomColor || false;
staticSettings.decoratorCustomColor = tinycolor(vm.ctx.settings.decoratorCustomColor).toHexString();
staticSettings.decoratorSymbol = vm.ctx.settings.decoratorSymbol || "arrowHead";
staticSettings.decoratorSymbolSize = vm.ctx.settings.decoratorSymbolSize || 10;
staticSettings.decoratorOffset = vm.ctx.settings.decoratorOffset || "20px";
staticSettings.endDecoratorOffset = vm.ctx.settings.endDecoratorOffset || "20px";
staticSettings.decoratorRepeat = vm.ctx.settings.decoratorRepeat || "20px";
staticSettings.tooltipPattern = vm.ctx.settings.tooltipPattern || "<span style=\"font-size: 26px; color: #666; font-weight: bold;\">${entityName}</span>\n" +
"<br/>\n" +
"<span style=\"font-size: 12px; color: #666; font-weight: bold;\">Time:</span><span style=\"font-size: 12px;\"> ${formattedTs}</span>\n" +
@ -347,6 +381,10 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
staticSettings.colorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.colorFunction);
}
if (staticSettings.usePointAsAnchor && angular.isDefined(vm.ctx.settings.pointAsAnchorFunction)) {
staticSettings.pointAsAnchorFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.pointAsAnchorFunction);
}
if (staticSettings.usePolygonTooltipFunction && angular.isDefined(vm.ctx.settings.polygonTooltipFunction)) {
staticSettings.polygonTooltipFunction = new Function('data, dsData, dsIndex', vm.ctx.settings.polygonTooltipFunction);
}
@ -501,6 +539,27 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
return tooltip;
}
function calculatePointTooltip(trip, index) {
let tooltip = '';
if (vm.staticSettings.displayTooltip) {
let tooltipReplaceInfo;
let tooltipText = vm.staticSettings.tooltipPattern;
if (vm.staticSettings.useTooltipFunction && angular.isDefined(vm.staticSettings.tooltipFunction)) {
try {
tooltipText = vm.staticSettings.tooltipFunction(vm.ctx.data, trip.timeRange[index], trip.dSIndex);
} catch (e) {
tooltipText = null;
}
}
tooltipText = vm.utils.createLabelFromDatasource(trip.dataSource, tooltipText);
tooltipReplaceInfo = processPattern(tooltipText, vm.ctx.datasources, trip.dSIndex);
tooltip = fillPattern(tooltipText, tooltipReplaceInfo, trip.timeRange[index]);
tooltip = fillPatternWithActions(tooltip, 'onTooltipAction', null);
}
return tooltip;
}
function calculateColor(trip) {
let color = vm.staticSettings.pathColor;
let colorFn;
@ -582,6 +641,10 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
trip.polyline.remove();
delete trip.polyline;
}
if (trip.polylineDecorator) {
trip.polylineDecorator.remove();
delete trip.polylineDecorator;
}
if (trip.polygon) {
trip.polygon.remove();
delete trip.polygon;
@ -595,7 +658,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
});
vm.initBounds = true;
}
let normalizedTimeRange = createNormalizedTime(vm.data, 1000);
let normalizedTimeRange = createNormalizedTime(vm.data, vm.staticSettings.normalizationStep);
createNormalizedTrips(normalizedTimeRange, vm.datasources);
createTripsOnMap(apply);
if (vm.initBounds && !vm.initTrips) {
@ -667,7 +730,7 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
}
}
vm.maxTime = normalizedArray.length - 1;
vm.minTime = vm.maxTime > 1 ? 1 : 0;
//vm.minTime = vm.maxTime > 1 ? 1 : 0;
if (vm.index < vm.minTime) {
vm.index = vm.minTime;
} else if (vm.index > vm.maxTime) {
@ -761,6 +824,13 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
}
}
function createPointPopup(point, index, trip) {
let popup = L.popup();
popup.setContent(calculatePointTooltip(trip, index));
point.bindPopup(popup, {autoClose: vm.staticSettings.autocloseTooltip, closeOnClick: false});
return popup;
}
function createTripsOnMap(apply) {
if (vm.trips.length > 0) {
vm.trips.forEach(function (trip) {
@ -768,17 +838,47 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
if (trip.timeRange.length > 0 && trip.latLngs.every(el => angular.isDefined(el))) {
if (vm.staticSettings.showPoints) {
trip.points = [];
trip.latLngs.forEach(function (latLng) {
let point = L.circleMarker(latLng, {
color: trip.settings.pointColor,
radius: trip.settings.pointSize
}).addTo(vm.map.map);
trip.points.push(point);
trip.timeRange.forEach(function (tRange, index) {
if (tRange && tRange.latLng
&& (!vm.staticSettings.usePointAsAnchor || vm.staticSettings.pointAsAnchorFunction(vm.ctx.data, tRange, trip.dSIndex))) {
let point = L.circleMarker(tRange.latLng, {
color: trip.settings.pointColor,
radius: trip.settings.pointSize
}).addTo(vm.map.map);
if (vm.staticSettings.pointTooltipOnRightPanel) {
point.popup = createPointPopup(point, index, trip);
} else {
point.on('click', function () {
showHidePointTooltip(calculatePointTooltip(trip, index), index);
});
}
if (vm.staticSettings.usePointAsAnchor) tRange.hasAnchor = true;
trip.points.push(point);
}
});
}
if (angular.isUndefined(trip.marker)) {
trip.polyline = vm.map.createPolyline(trip.latLngs, trip.settings);
if (vm.staticSettings.usePolylineDecorator) {
trip.polylineDecorator = L.polylineDecorator(trip.polyline, {
patterns: [
{
offset: vm.staticSettings.decoratorOffset,
endOffset: vm.staticSettings.endDecoratorOffset,
repeat: vm.staticSettings.decoratorRepeat,
symbol: L.Symbol[vm.staticSettings.decoratorSymbol]({
pixelSize: vm.staticSettings.decoratorSymbolSize,
polygon: false,
pathOptions: {
color: vm.staticSettings.useDecoratorCustomColor ? vm.staticSettings.decoratorCustomColor : trip.settings.color,
stroke: true}
})
}
],
interactive: false,
}).addTo(vm.map.map);
}
}
@ -859,23 +959,43 @@ function tripAnimationController($document, $scope, $log, $http, $timeout, $filt
if (vm.staticSettings.displayTooltip) {
if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'marker')) {
vm.staticSettings.showTooltip = true;
vm.staticSettings.tooltipMarker = 'marker';
} else {
vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
}
vm.staticSettings.tooltipMarker = 'marker';
}
if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.tooltipText;
}
function showHidePointTooltip(text, index) {
if (vm.staticSettings.displayTooltip) {
if (vm.staticSettings.tooltipMarker && vm.staticSettings.tooltipMarker.includes('point')) {
if (vm.staticSettings.tooltipMarker === 'point' + index) {
vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
} else {
vm.staticSettings.showTooltip = true;
vm.mainTooltip = $sce.trustAsHtml(text);
vm.staticSettings.tooltipMarker = 'point' + index;
}
} else {
vm.staticSettings.showTooltip = true;
vm.mainTooltip = $sce.trustAsHtml(text);
vm.staticSettings.tooltipMarker = 'point' + index;
}
}
}
function showHidePolygonTooltip(trip) {
if (vm.staticSettings.displayTooltip) {
if (vm.staticSettings.showTooltip && trip && (vm.activeTripIndex !== trip.dSIndex || vm.staticSettings.tooltipMarker !== 'polygon')) {
vm.staticSettings.showTooltip = true;
vm.staticSettings.tooltipMarker = 'polygon';
} else {
vm.staticSettings.showTooltip = !vm.staticSettings.showTooltip;
}
vm.staticSettings.tooltipMarker = 'polygon';
}
if (trip && vm.activeTripIndex !== trip.dSIndex) vm.activeTripIndex = trip.dSIndex;
vm.mainTooltip = vm.trips[vm.activeTripIndex].settings.polygonTooltipText;
}
}

2
ui/src/app/widget/lib/tripAnimation/trip-animation-widget.tpl.html

@ -27,7 +27,7 @@
<ng-md-icon icon="info_outline"></ng-md-icon>
</md-button>
</div>
<div class="trip-animation-tooltip md-whiteframe-z4" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'trip-animation-tooltip-hidden':''" ng-bind-html="vm.staticSettings.tooltipMarker === 'polygon' ? vm.trips[vm.activeTripIndex].settings.polygonTooltipText : vm.trips[vm.activeTripIndex].settings.tooltipText"
<div class="trip-animation-tooltip md-whiteframe-z4" layout="column" ng-class="!vm.staticSettings.showTooltip ? 'trip-animation-tooltip-hidden':''" ng-bind-html="vm.mainTooltip"
ng-style="{'background-color': vm.staticSettings.tooltipColor, 'opacity': vm.staticSettings.tooltipOpacity, 'color': vm.staticSettings.tooltipFontColor}">
</div>
</div>

2
ui/src/scss/main.scss

@ -647,7 +647,7 @@ section.tb-top-header-buttons {
.tb-header-buttons .tb-btn-header {
position: relative !important;
display: inline-block !important;
display: inline-block;
animation: tbMoveFromTopFade .3s ease both;
}

Loading…
Cancel
Save