Commit e7cf3be6619619083c74dfe2ba5c8bf1410db8a0

Authored by Artem Halushko
2 parents 6818c993 29429cb0
@@ -92,19 +92,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest { @@ -92,19 +92,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
92 .andExpect(status().isBadRequest()) 92 .andExpect(status().isBadRequest())
93 .andExpect(statusReason(containsString("is prohibited"))); 93 .andExpect(statusReason(containsString("is prohibited")));
94 } 94 }
95 -  
96 - @Test  
97 - public void testSaveAdminSettingsWithNewJsonStructure() throws Exception {  
98 - loginSysAdmin();  
99 - AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);  
100 - JsonNode json = adminSettings.getJsonValue();  
101 - ((ObjectNode) json).put("newKey", "my new value");  
102 - adminSettings.setJsonValue(json);  
103 - doPost("/api/admin/settings", adminSettings)  
104 - .andExpect(status().isBadRequest())  
105 - .andExpect(statusReason(containsString("Provided json structure is different")));  
106 - }  
107 - 95 +
108 @Test 96 @Test
109 public void testSendTestMail() throws Exception { 97 public void testSendTestMail() throws Exception {
110 loginSysAdmin(); 98 loginSysAdmin();
@@ -73,9 +73,6 @@ public class AdminSettingsServiceImpl implements AdminSettingsService { @@ -73,9 +73,6 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
73 if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) { 73 if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) {
74 throw new DataValidationException("Changing key of admin settings entry is prohibited!"); 74 throw new DataValidationException("Changing key of admin settings entry is prohibited!");
75 } 75 }
76 - if (adminSettings.getKey().equals("mail")) {  
77 - validateJsonStructure(existentAdminSettings.getJsonValue(), adminSettings.getJsonValue());  
78 - }  
79 } 76 }
80 } 77 }
81 78
@@ -15,9 +15,11 @@ @@ -15,9 +15,11 @@
15 */ 15 */
16 package org.thingsboard.server.dao.sql.attributes; 16 package org.thingsboard.server.dao.sql.attributes;
17 17
  18 +import org.springframework.data.jpa.repository.Modifying;
18 import org.springframework.data.jpa.repository.Query; 19 import org.springframework.data.jpa.repository.Query;
19 import org.springframework.data.repository.CrudRepository; 20 import org.springframework.data.repository.CrudRepository;
20 import org.springframework.data.repository.query.Param; 21 import org.springframework.data.repository.query.Param;
  22 +import org.springframework.transaction.annotation.Transactional;
21 import org.thingsboard.server.common.data.EntityType; 23 import org.thingsboard.server.common.data.EntityType;
22 import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; 24 import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey;
23 import org.thingsboard.server.dao.model.sql.AttributeKvEntity; 25 import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
@@ -34,5 +36,16 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity, @@ -34,5 +36,16 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity,
34 List<AttributeKvEntity> findAllByEntityTypeAndEntityIdAndAttributeType(@Param("entityType") EntityType entityType, 36 List<AttributeKvEntity> findAllByEntityTypeAndEntityIdAndAttributeType(@Param("entityType") EntityType entityType,
35 @Param("entityId") String entityId, 37 @Param("entityId") String entityId,
36 @Param("attributeType") String attributeType); 38 @Param("attributeType") String attributeType);
  39 +
  40 + @Transactional
  41 + @Modifying
  42 + @Query("DELETE FROM AttributeKvEntity a WHERE a.id.entityType = :entityType " +
  43 + "AND a.id.entityId = :entityId " +
  44 + "AND a.id.attributeType = :attributeType " +
  45 + "AND a.id.attributeKey = :attributeKey")
  46 + void delete(@Param("entityType") EntityType entityType,
  47 + @Param("entityId") String entityId,
  48 + @Param("attributeType") String attributeType,
  49 + @Param("attributeKey") String attributeKey);
37 } 50 }
38 51
@@ -138,16 +138,10 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl @@ -138,16 +138,10 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
138 138
139 @Override 139 @Override
140 public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys) { 140 public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys) {
141 - List<AttributeKvEntity> entitiesToDelete = keys  
142 - .stream()  
143 - .map(key -> {  
144 - AttributeKvEntity entityToDelete = new AttributeKvEntity();  
145 - entityToDelete.setId(new AttributeKvCompositeKey(entityId.getEntityType(), fromTimeUUID(entityId.getId()), attributeType, key));  
146 - return entityToDelete;  
147 - }).collect(Collectors.toList());  
148 -  
149 return service.submit(() -> { 141 return service.submit(() -> {
150 - attributeKvRepository.deleteAll(entitiesToDelete); 142 + keys.forEach(key ->
  143 + attributeKvRepository.delete(entityId.getEntityType(), UUIDConverter.fromTimeUUID(entityId.getId()), attributeType, key)
  144 + );
151 return null; 145 return null;
152 }); 146 });
153 } 147 }
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.sql.relation;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.springframework.data.jpa.repository.Modifying;
  20 +import org.thingsboard.server.dao.model.sql.RelationEntity;
  21 +
  22 +import javax.persistence.EntityManager;
  23 +import javax.persistence.PersistenceContext;
  24 +import javax.persistence.Query;
  25 +
  26 +@Slf4j
  27 +public abstract class AbstractRelationInsertRepository implements RelationInsertRepository {
  28 +
  29 + @PersistenceContext
  30 + protected EntityManager entityManager;
  31 +
  32 + protected Query getQuery(RelationEntity entity, String query) {
  33 + Query nativeQuery = entityManager.createNativeQuery(query, RelationEntity.class);
  34 + if (entity.getAdditionalInfo() == null) {
  35 + nativeQuery.setParameter("additionalInfo", null);
  36 + } else {
  37 + nativeQuery.setParameter("additionalInfo", entity.getAdditionalInfo().toString());
  38 + }
  39 + return nativeQuery
  40 + .setParameter("fromId", entity.getFromId())
  41 + .setParameter("fromType", entity.getFromType())
  42 + .setParameter("toId", entity.getToId())
  43 + .setParameter("toType", entity.getToType())
  44 + .setParameter("relationTypeGroup", entity.getRelationTypeGroup())
  45 + .setParameter("relationType", entity.getRelationType());
  46 + }
  47 +
  48 + @Modifying
  49 + protected abstract RelationEntity processSaveOrUpdate(RelationEntity entity);
  50 +
  51 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.sql.relation;
  17 +
  18 +import org.springframework.stereotype.Repository;
  19 +import org.springframework.transaction.annotation.Transactional;
  20 +import org.thingsboard.server.dao.model.sql.RelationCompositeKey;
  21 +import org.thingsboard.server.dao.model.sql.RelationEntity;
  22 +import org.thingsboard.server.dao.util.HsqlDao;
  23 +import org.thingsboard.server.dao.util.SqlDao;
  24 +
  25 +@HsqlDao
  26 +@SqlDao
  27 +@Repository
  28 +@Transactional
  29 +public class HsqlRelationInsertRepository extends AbstractRelationInsertRepository implements RelationInsertRepository {
  30 +
  31 + private static final String INSERT_ON_CONFLICT_DO_UPDATE = "MERGE INTO relation USING (VALUES :fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) R " +
  32 + "(from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info) " +
  33 + "ON (relation.from_id = R.from_id AND relation.from_type = R.from_type AND relation.relation_type_group = R.relation_type_group AND relation.relation_type = R.relation_type AND relation.to_id = R.to_id AND relation.to_type = R.to_type) " +
  34 + "WHEN MATCHED THEN UPDATE SET relation.additional_info = R.additional_info " +
  35 + "WHEN NOT MATCHED THEN INSERT (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info) VALUES (R.from_id, R.from_type, R.to_id, R.to_type, R.relation_type_group, R.relation_type, R.additional_info)";
  36 +
  37 + @Override
  38 + public RelationEntity saveOrUpdate(RelationEntity entity) {
  39 + return processSaveOrUpdate(entity);
  40 + }
  41 +
  42 + @Override
  43 + protected RelationEntity processSaveOrUpdate(RelationEntity entity) {
  44 + getQuery(entity, INSERT_ON_CONFLICT_DO_UPDATE).executeUpdate();
  45 + return entityManager.find(RelationEntity.class, new RelationCompositeKey(entity.toData()));
  46 + }
  47 +}
@@ -58,6 +58,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple @@ -58,6 +58,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
58 @Autowired 58 @Autowired
59 private RelationRepository relationRepository; 59 private RelationRepository relationRepository;
60 60
  61 + @Autowired
  62 + private RelationInsertRepository relationInsertRepository;
  63 +
61 @Override 64 @Override
62 public ListenableFuture<List<EntityRelation>> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) { 65 public ListenableFuture<List<EntityRelation>> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) {
63 return service.submit(() -> DaoUtil.convertDataList( 66 return service.submit(() -> DaoUtil.convertDataList(
@@ -119,12 +122,12 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple @@ -119,12 +122,12 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
119 122
120 @Override 123 @Override
121 public boolean saveRelation(TenantId tenantId, EntityRelation relation) { 124 public boolean saveRelation(TenantId tenantId, EntityRelation relation) {
122 - return relationRepository.save(new RelationEntity(relation)) != null; 125 + return relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null;
123 } 126 }
124 127
125 @Override 128 @Override
126 public ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation) { 129 public ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation) {
127 - return service.submit(() -> relationRepository.save(new RelationEntity(relation)) != null); 130 + return service.submit(() -> relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null);
128 } 131 }
129 132
130 @Override 133 @Override
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.sql.relation;
  17 +
  18 +import org.springframework.stereotype.Repository;
  19 +import org.springframework.transaction.annotation.Transactional;
  20 +import org.thingsboard.server.dao.model.sql.RelationEntity;
  21 +import org.thingsboard.server.dao.util.PsqlDao;
  22 +import org.thingsboard.server.dao.util.SqlDao;
  23 +
  24 +@PsqlDao
  25 +@SqlDao
  26 +@Repository
  27 +@Transactional
  28 +public class PsqlRelationInsertRepository extends AbstractRelationInsertRepository implements RelationInsertRepository {
  29 +
  30 + private static final String INSERT_ON_CONFLICT_DO_UPDATE = "INSERT INTO relation (from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info)" +
  31 + " VALUES (:fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) " +
  32 + "ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = :additionalInfo returning *";
  33 +
  34 + @Override
  35 + public RelationEntity saveOrUpdate(RelationEntity entity) {
  36 + return processSaveOrUpdate(entity);
  37 + }
  38 +
  39 + @Override
  40 + protected RelationEntity processSaveOrUpdate(RelationEntity entity) {
  41 + return (RelationEntity) getQuery(entity, INSERT_ON_CONFLICT_DO_UPDATE).getSingleResult();
  42 + }
  43 +}
  1 +/**
  2 + * Copyright © 2016-2020 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.dao.sql.relation;
  17 +
  18 +import org.thingsboard.server.dao.model.sql.RelationEntity;
  19 +
  20 +public interface RelationInsertRepository {
  21 +
  22 + RelationEntity saveOrUpdate(RelationEntity entity);
  23 +
  24 +}
@@ -67,13 +67,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest { @@ -67,13 +67,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest {
67 adminSettings.setKey("newKey"); 67 adminSettings.setKey("newKey");
68 adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); 68 adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);
69 } 69 }
70 -  
71 - @Test(expected = DataValidationException.class)  
72 - public void testSaveAdminSettingsWithNewJsonStructure() {  
73 - AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail");  
74 - JsonNode json = adminSettings.getJsonValue();  
75 - ((ObjectNode) json).put("newKey", "my new value");  
76 - adminSettings.setJsonValue(json);  
77 - adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);  
78 - }  
79 } 70 }
@@ -25,7 +25,7 @@ export default function AdminController(adminService, toast, $scope, $rootScope, @@ -25,7 +25,7 @@ export default function AdminController(adminService, toast, $scope, $rootScope,
25 return protocol; 25 return protocol;
26 }); 26 });
27 27
28 - vm.tlsVersions = ['TLSv1.0', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3']; 28 + vm.tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
29 29
30 $translate('admin.test-mail-sent').then(function (translation) { 30 $translate('admin.test-mail-sent').then(function (translation) {
31 vm.testMailSent = translation; 31 vm.testMailSent = translation;
@@ -104,26 +104,32 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge { @@ -104,26 +104,32 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
104 } 104 }
105 105
106 var colorsCount = options.levelColors.length; 106 var colorsCount = options.levelColors.length;
107 - var inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1; 107 + const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
  108 + var isColorProperty = angular.isString(options.levelColors[0]);
  109 +
108 options.colorsRange = []; 110 options.colorsRange = [];
109 if (options.neonGlowBrightness) { 111 if (options.neonGlowBrightness) {
110 options.neonColorsRange = []; 112 options.neonColorsRange = [];
111 } 113 }
112 - for (var i = 0; i < options.levelColors.length; i++) {  
113 - var percentage = inc * i;  
114 - var tColor = tinycolor(options.levelColors[i]);  
115 - options.colorsRange[i] = {  
116 - pct: percentage,  
117 - color: tColor.toRgb(),  
118 - rgbString: tColor.toRgbString()  
119 - };  
120 - if (options.neonGlowBrightness) {  
121 - tColor = tinycolor(options.levelColors[i]).brighten(options.neonGlowBrightness);  
122 - options.neonColorsRange[i] = { 114 +
  115 + for (let i = 0; i < options.levelColors.length; i++) {
  116 + const levelColor = options.levelColors[i];
  117 + if (levelColor !== null) {
  118 + let percentage = isColorProperty ? inc * i : CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue);
  119 + let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color);
  120 + options.colorsRange.push({
123 pct: percentage, 121 pct: percentage,
124 color: tColor.toRgb(), 122 color: tColor.toRgb(),
125 rgbString: tColor.toRgbString() 123 rgbString: tColor.toRgbString()
126 - }; 124 + });
  125 + if (options.neonGlowBrightness) {
  126 + tColor = tinycolor(isColorProperty ? levelColor : levelColor.color).brighten(options.neonGlowBrightness);
  127 + options.neonColorsRange.push({
  128 + pct: percentage,
  129 + color: tColor.toRgb(),
  130 + rgbString: tColor.toRgbString()
  131 + });
  132 + }
127 } 133 }
128 } 134 }
129 135
@@ -137,6 +143,17 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge { @@ -137,6 +143,17 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
137 return canvasGauges.BaseGauge.configure(options); 143 return canvasGauges.BaseGauge.configure(options);
138 } 144 }
139 145
  146 + static normalizeValue (value, min, max) {
  147 + let normalValue = (value - min) / (max - min);
  148 + if (normalValue <= 0) {
  149 + return 0;
  150 + }
  151 + if (normalValue >= 1) {
  152 + return 1;
  153 + }
  154 + return normalValue;
  155 + }
  156 +
140 destroy() { 157 destroy() {
141 this.contextValueClone = null; 158 this.contextValueClone = null;
142 this.elementValueClone = null; 159 this.elementValueClone = null;
@@ -50,10 +50,16 @@ export default class TbCanvasDigitalGauge { @@ -50,10 +50,16 @@ export default class TbCanvasDigitalGauge {
50 this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75; 50 this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75;
51 this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString(); 51 this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString();
52 52
53 - if (!settings.levelColors || settings.levelColors.length <= 0) {  
54 - this.localSettings.levelColors = [keyColor]; 53 + this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false;
  54 + if (!settings.useFixedLevelColor) {
  55 + if (!settings.levelColors || settings.levelColors.length <= 0) {
  56 + this.localSettings.levelColors = [keyColor];
  57 + } else {
  58 + this.localSettings.levelColors = settings.levelColors.slice();
  59 + }
55 } else { 60 } else {
56 - this.localSettings.levelColors = settings.levelColors.slice(); 61 + this.localSettings.levelColors = [keyColor];
  62 + this.localSettings.fixedLevelColors = settings.fixedLevelColors || [];
57 } 63 }
58 64
59 this.localSettings.decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals : 65 this.localSettings.decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals :
@@ -191,15 +197,137 @@ export default class TbCanvasDigitalGauge { @@ -191,15 +197,137 @@ export default class TbCanvasDigitalGauge {
191 }; 197 };
192 198
193 this.gauge = new CanvasDigitalGauge(gaugeData).draw(); 199 this.gauge = new CanvasDigitalGauge(gaugeData).draw();
  200 + this.init();
  201 + }
  202 +
  203 + init() {
  204 + if (this.localSettings.useFixedLevelColor) {
  205 + if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) {
  206 + this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors);
  207 + this.updateLevelColors(this.localSettings.levelColors);
  208 + }
  209 + }
  210 + }
  211 +
  212 + settingLevelColorsSubscribe(options) {
  213 + let levelColorsDatasource = [];
  214 + let predefineLevelColors = [];
  215 +
  216 + function setLevelColor(levelSetting, color) {
  217 + if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) {
  218 + predefineLevelColors.push({
  219 + value: levelSetting.value,
  220 + color: color
  221 + })
  222 + } else if (levelSetting.entityAlias && levelSetting.attribute) {
  223 + let entityAliasId = this.ctx.aliasController.getEntityAliasId(levelSetting.entityAlias);
  224 + if (!entityAliasId) {
  225 + return;
  226 + }
  227 +
  228 + let datasource = levelColorsDatasource.filter((datasource) => {
  229 + return datasource.entityAliasId === entityAliasId;
  230 + })[0];
  231 +
  232 + let dataKey = {
  233 + type: this.ctx.$scope.$injector.get('types').dataKeyType.attribute,
  234 + name: levelSetting.attribute,
  235 + label: levelSetting.attribute,
  236 + settings: [{
  237 + color: color,
  238 + index: predefineLevelColors.length
  239 + }],
  240 + _hash: Math.random()
  241 + };
  242 +
  243 + if (datasource) {
  244 + let findDataKey = datasource.dataKeys.filter((dataKey) => {
  245 + return dataKey.name === levelSetting.attribute;
  246 + })[0];
  247 +
  248 + if (findDataKey) {
  249 + findDataKey.settings.push({
  250 + color: color,
  251 + index: predefineLevelColors.length
  252 + });
  253 + } else {
  254 + datasource.dataKeys.push(dataKey)
  255 + }
  256 + } else {
  257 + datasource = {
  258 + type: this.ctx.$scope.$injector.get('types').datasourceType.entity,
  259 + name: levelSetting.entityAlias,
  260 + aliasName: levelSetting.entityAlias,
  261 + entityAliasId: entityAliasId,
  262 + dataKeys: [dataKey]
  263 + };
  264 + levelColorsDatasource.push(datasource);
  265 + }
  266 +
  267 + predefineLevelColors.push(null);
  268 + }
  269 + }
  270 +
  271 + for (let i = 0; i < options.length; i++) {
  272 + let levelColor = options[i];
  273 + if (levelColor.from) {
  274 + setLevelColor.call(this, levelColor.from, levelColor.color);
  275 + }
  276 + if (levelColor.to) {
  277 + setLevelColor.call(this, levelColor.to, levelColor.color);
  278 + }
  279 + }
194 280
  281 + this.subscribeLevelColorsAttributes(levelColorsDatasource);
  282 +
  283 + return predefineLevelColors;
  284 + }
  285 +
  286 + updateLevelColors(levelColors) {
  287 + this.gauge.options.levelColors = levelColors;
  288 + this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options);
  289 + this.gauge.update();
  290 + }
  291 +
  292 + subscribeLevelColorsAttributes(datasources) {
  293 + let TbCanvasDigitalGauge = this;
  294 + let levelColorsSourcesSubscriptionOptions = {
  295 + datasources: datasources,
  296 + useDashboardTimewindow: false,
  297 + type: this.ctx.$scope.$injector.get('types').widgetType.latest.value,
  298 + callbacks: {
  299 + onDataUpdated: (subscription) => {
  300 + for (let i = 0; i < subscription.data.length; i++) {
  301 + let keyData = subscription.data[i];
  302 + if (keyData && keyData.data && keyData.data[0]) {
  303 + let attrValue = keyData.data[0][1];
  304 + if (isFinite(attrValue)) {
  305 + for (let i = 0; i < keyData.dataKey.settings.length; i++) {
  306 + let setting = keyData.dataKey.settings[i];
  307 + this.localSettings.levelColors[setting.index] = {
  308 + value: attrValue,
  309 + color: setting.color
  310 + };
  311 + }
  312 + }
  313 + }
  314 + }
  315 + this.updateLevelColors(this.localSettings.levelColors);
  316 + }
  317 + }
  318 + };
  319 + this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true).then(
  320 + (subscription) => {
  321 + TbCanvasDigitalGauge.levelColorSourcesSubscription = subscription;
  322 + }
  323 + );
195 } 324 }
196 325
197 update() { 326 update() {
198 if (this.ctx.data.length > 0) { 327 if (this.ctx.data.length > 0) {
199 var cellData = this.ctx.data[0]; 328 var cellData = this.ctx.data[0];
200 if (cellData.data.length > 0) { 329 if (cellData.data.length > 0) {
201 - var tvPair = cellData.data[cellData.data.length -  
202 - 1]; 330 + var tvPair = cellData.data[cellData.data.length - 1];
203 var timestamp; 331 var timestamp;
204 if (this.localSettings.showTimestamp) { 332 if (this.localSettings.showTimestamp) {
205 timestamp = tvPair[0]; 333 timestamp = tvPair[0];
@@ -325,6 +453,11 @@ export default class TbCanvasDigitalGauge { @@ -325,6 +453,11 @@ export default class TbCanvasDigitalGauge {
325 "type": "string", 453 "type": "string",
326 "default": null 454 "default": null
327 }, 455 },
  456 + "useFixedLevelColor": {
  457 + "title": "Use precise value for the color indicator",
  458 + "type": "boolean",
  459 + "default": false
  460 + },
328 "levelColors": { 461 "levelColors": {
329 "title": "Colors of indicator, from lower to upper", 462 "title": "Colors of indicator, from lower to upper",
330 "type": "array", 463 "type": "array",
@@ -333,6 +466,66 @@ export default class TbCanvasDigitalGauge { @@ -333,6 +466,66 @@ export default class TbCanvasDigitalGauge {
333 "type": "string" 466 "type": "string"
334 } 467 }
335 }, 468 },
  469 + "fixedLevelColors": {
  470 + "title": "The colors for the indicator using boundary values",
  471 + "type": "array",
  472 + "items": {
  473 + "title": "levelColor",
  474 + "type": "object",
  475 + "properties": {
  476 + "from": {
  477 + "title": "From",
  478 + "type": "object",
  479 + "properties": {
  480 + "valueSource": {
  481 + "title": "[From] Value source",
  482 + "type": "string",
  483 + "default": "predefinedValue"
  484 + },
  485 + "entityAlias": {
  486 + "title": "[From] Source entity alias",
  487 + "type": "string"
  488 + },
  489 + "attribute": {
  490 + "title": "[From] Source entity attribute",
  491 + "type": "string"
  492 + },
  493 + "value": {
  494 + "title": "[From] Value (if predefined value is selected)",
  495 + "type": "number"
  496 + }
  497 + }
  498 + },
  499 + "to": {
  500 + "title": "To",
  501 + "type": "object",
  502 + "properties": {
  503 + "valueSource": {
  504 + "title": "[To] Value source",
  505 + "type": "string",
  506 + "default": "predefinedValue"
  507 + },
  508 + "entityAlias": {
  509 + "title": "[To] Source entity alias",
  510 + "type": "string"
  511 + },
  512 + "attribute": {
  513 + "title": "[To] Source entity attribute",
  514 + "type": "string"
  515 + },
  516 + "value": {
  517 + "title": "[To] Value (if predefined value is selected)",
  518 + "type": "number"
  519 + }
  520 + }
  521 + },
  522 + "color": {
  523 + "title": "Color",
  524 + "type": "string"
  525 + }
  526 + }
  527 + }
  528 + },
336 "animation": { 529 "animation": {
337 "title": "Enable animation", 530 "title": "Enable animation",
338 "type": "boolean", 531 "type": "boolean",
@@ -521,8 +714,10 @@ export default class TbCanvasDigitalGauge { @@ -521,8 +714,10 @@ export default class TbCanvasDigitalGauge {
521 "key": "gaugeColor", 714 "key": "gaugeColor",
522 "type": "color" 715 "type": "color"
523 }, 716 },
  717 + "useFixedLevelColor",
524 { 718 {
525 "key": "levelColors", 719 "key": "levelColors",
  720 + "condition": "model.useFixedLevelColor !== true",
526 "items": [ 721 "items": [
527 { 722 {
528 "key": "levelColors[]", 723 "key": "levelColors[]",
@@ -530,6 +725,52 @@ export default class TbCanvasDigitalGauge { @@ -530,6 +725,52 @@ export default class TbCanvasDigitalGauge {
530 } 725 }
531 ] 726 ]
532 }, 727 },
  728 + {
  729 + "key": "fixedLevelColors",
  730 + "condition": "model.useFixedLevelColor === true",
  731 + "items": [
  732 + {
  733 + "key": "fixedLevelColors[].from.valueSource",
  734 + "type": "rc-select",
  735 + "multiple": false,
  736 + "items": [
  737 + {
  738 + "value": "predefinedValue",
  739 + "label": "Predefined value (Default)"
  740 + },
  741 + {
  742 + "value": "entityAttribute",
  743 + "label": "Value taken from entity attribute"
  744 + }
  745 + ]
  746 + },
  747 + "fixedLevelColors[].from.value",
  748 + "fixedLevelColors[].from.entityAlias",
  749 + "fixedLevelColors[].from.attribute",
  750 + {
  751 + "key": "fixedLevelColors[].to.valueSource",
  752 + "type": "rc-select",
  753 + "multiple": false,
  754 + "items": [
  755 + {
  756 + "value": "predefinedValue",
  757 + "label": "Predefined value (Default)"
  758 + },
  759 + {
  760 + "value": "entityAttribute",
  761 + "label": "Value taken from entity attribute"
  762 + }
  763 + ]
  764 + },
  765 + "fixedLevelColors[].to.value",
  766 + "fixedLevelColors[].to.entityAlias",
  767 + "fixedLevelColors[].to.attribute",
  768 + {
  769 + "key": "fixedLevelColors[].color",
  770 + "type": "color"
  771 + }
  772 + ]
  773 + },
533 "animation", 774 "animation",
534 "animationDuration", 775 "animationDuration",
535 { 776 {