Artem Halushko 6 years ago
parent
commit
e7cf3be661
  1. 14
      application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java
  2. 3
      dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java
  3. 13
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java
  4. 12
      dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java
  5. 51
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/AbstractRelationInsertRepository.java
  6. 47
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/HsqlRelationInsertRepository.java
  7. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java
  8. 43
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/PsqlRelationInsertRepository.java
  9. 24
      dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java
  10. 9
      dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java
  11. 2
      ui/src/app/admin/admin.controller.js
  12. 43
      ui/src/app/widget/lib/CanvasDigitalGauge.js
  13. 251
      ui/src/app/widget/lib/canvas-digital-gauge.js

14
application/src/test/java/org/thingsboard/server/controller/BaseAdminControllerTest.java

@ -92,19 +92,7 @@ public abstract class BaseAdminControllerTest extends AbstractControllerTest {
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("is prohibited")));
}
@Test
public void testSaveAdminSettingsWithNewJsonStructure() throws Exception {
loginSysAdmin();
AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
JsonNode json = adminSettings.getJsonValue();
((ObjectNode) json).put("newKey", "my new value");
adminSettings.setJsonValue(json);
doPost("/api/admin/settings", adminSettings)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Provided json structure is different")));
}
@Test
public void testSendTestMail() throws Exception {
loginSysAdmin();

3
dao/src/main/java/org/thingsboard/server/dao/settings/AdminSettingsServiceImpl.java

@ -73,9 +73,6 @@ public class AdminSettingsServiceImpl implements AdminSettingsService {
if (!existentAdminSettings.getKey().equals(adminSettings.getKey())) {
throw new DataValidationException("Changing key of admin settings entry is prohibited!");
}
if (adminSettings.getKey().equals("mail")) {
validateJsonStructure(existentAdminSettings.getJsonValue(), adminSettings.getJsonValue());
}
}
}

13
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/AttributeKvRepository.java

@ -15,9 +15,11 @@
*/
package org.thingsboard.server.dao.sql.attributes;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey;
import org.thingsboard.server.dao.model.sql.AttributeKvEntity;
@ -34,5 +36,16 @@ public interface AttributeKvRepository extends CrudRepository<AttributeKvEntity,
List<AttributeKvEntity> findAllByEntityTypeAndEntityIdAndAttributeType(@Param("entityType") EntityType entityType,
@Param("entityId") String entityId,
@Param("attributeType") String attributeType);
@Transactional
@Modifying
@Query("DELETE FROM AttributeKvEntity a WHERE a.id.entityType = :entityType " +
"AND a.id.entityId = :entityId " +
"AND a.id.attributeType = :attributeType " +
"AND a.id.attributeKey = :attributeKey")
void delete(@Param("entityType") EntityType entityType,
@Param("entityId") String entityId,
@Param("attributeType") String attributeType,
@Param("attributeKey") String attributeKey);
}

12
dao/src/main/java/org/thingsboard/server/dao/sql/attributes/JpaAttributeDao.java

@ -138,16 +138,10 @@ public class JpaAttributeDao extends JpaAbstractDaoListeningExecutorService impl
@Override
public ListenableFuture<List<Void>> removeAll(TenantId tenantId, EntityId entityId, String attributeType, List<String> keys) {
List<AttributeKvEntity> entitiesToDelete = keys
.stream()
.map(key -> {
AttributeKvEntity entityToDelete = new AttributeKvEntity();
entityToDelete.setId(new AttributeKvCompositeKey(entityId.getEntityType(), fromTimeUUID(entityId.getId()), attributeType, key));
return entityToDelete;
}).collect(Collectors.toList());
return service.submit(() -> {
attributeKvRepository.deleteAll(entitiesToDelete);
keys.forEach(key ->
attributeKvRepository.delete(entityId.getEntityType(), UUIDConverter.fromTimeUUID(entityId.getId()), attributeType, key)
);
return null;
});
}

51
dao/src/main/java/org/thingsboard/server/dao/sql/relation/AbstractRelationInsertRepository.java

@ -0,0 +1,51 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.relation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.jpa.repository.Modifying;
import org.thingsboard.server.dao.model.sql.RelationEntity;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
@Slf4j
public abstract class AbstractRelationInsertRepository implements RelationInsertRepository {
@PersistenceContext
protected EntityManager entityManager;
protected Query getQuery(RelationEntity entity, String query) {
Query nativeQuery = entityManager.createNativeQuery(query, RelationEntity.class);
if (entity.getAdditionalInfo() == null) {
nativeQuery.setParameter("additionalInfo", null);
} else {
nativeQuery.setParameter("additionalInfo", entity.getAdditionalInfo().toString());
}
return nativeQuery
.setParameter("fromId", entity.getFromId())
.setParameter("fromType", entity.getFromType())
.setParameter("toId", entity.getToId())
.setParameter("toType", entity.getToType())
.setParameter("relationTypeGroup", entity.getRelationTypeGroup())
.setParameter("relationType", entity.getRelationType());
}
@Modifying
protected abstract RelationEntity processSaveOrUpdate(RelationEntity entity);
}

47
dao/src/main/java/org/thingsboard/server/dao/sql/relation/HsqlRelationInsertRepository.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.relation;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.RelationCompositeKey;
import org.thingsboard.server.dao.model.sql.RelationEntity;
import org.thingsboard.server.dao.util.HsqlDao;
import org.thingsboard.server.dao.util.SqlDao;
@HsqlDao
@SqlDao
@Repository
@Transactional
public class HsqlRelationInsertRepository extends AbstractRelationInsertRepository implements RelationInsertRepository {
private static final String INSERT_ON_CONFLICT_DO_UPDATE = "MERGE INTO relation USING (VALUES :fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) R " +
"(from_id, from_type, to_id, to_type, relation_type_group, relation_type, additional_info) " +
"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) " +
"WHEN MATCHED THEN UPDATE SET relation.additional_info = R.additional_info " +
"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)";
@Override
public RelationEntity saveOrUpdate(RelationEntity entity) {
return processSaveOrUpdate(entity);
}
@Override
protected RelationEntity processSaveOrUpdate(RelationEntity entity) {
getQuery(entity, INSERT_ON_CONFLICT_DO_UPDATE).executeUpdate();
return entityManager.find(RelationEntity.class, new RelationCompositeKey(entity.toData()));
}
}

7
dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java

@ -58,6 +58,9 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
@Autowired
private RelationRepository relationRepository;
@Autowired
private RelationInsertRepository relationInsertRepository;
@Override
public ListenableFuture<List<EntityRelation>> findAllByFrom(TenantId tenantId, EntityId from, RelationTypeGroup typeGroup) {
return service.submit(() -> DaoUtil.convertDataList(
@ -119,12 +122,12 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
@Override
public boolean saveRelation(TenantId tenantId, EntityRelation relation) {
return relationRepository.save(new RelationEntity(relation)) != null;
return relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null;
}
@Override
public ListenableFuture<Boolean> saveRelationAsync(TenantId tenantId, EntityRelation relation) {
return service.submit(() -> relationRepository.save(new RelationEntity(relation)) != null);
return service.submit(() -> relationInsertRepository.saveOrUpdate(new RelationEntity(relation)) != null);
}
@Override

43
dao/src/main/java/org/thingsboard/server/dao/sql/relation/PsqlRelationInsertRepository.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.relation;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.RelationEntity;
import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlDao;
@PsqlDao
@SqlDao
@Repository
@Transactional
public class PsqlRelationInsertRepository extends AbstractRelationInsertRepository implements RelationInsertRepository {
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)" +
" VALUES (:fromId, :fromType, :toId, :toType, :relationTypeGroup, :relationType, :additionalInfo) " +
"ON CONFLICT (from_id, from_type, relation_type_group, relation_type, to_id, to_type) DO UPDATE SET additional_info = :additionalInfo returning *";
@Override
public RelationEntity saveOrUpdate(RelationEntity entity) {
return processSaveOrUpdate(entity);
}
@Override
protected RelationEntity processSaveOrUpdate(RelationEntity entity) {
return (RelationEntity) getQuery(entity, INSERT_ON_CONFLICT_DO_UPDATE).getSingleResult();
}
}

24
dao/src/main/java/org/thingsboard/server/dao/sql/relation/RelationInsertRepository.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2020 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.dao.sql.relation;
import org.thingsboard.server.dao.model.sql.RelationEntity;
public interface RelationInsertRepository {
RelationEntity saveOrUpdate(RelationEntity entity);
}

9
dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java

@ -67,13 +67,4 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest {
adminSettings.setKey("newKey");
adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);
}
@Test(expected = DataValidationException.class)
public void testSaveAdminSettingsWithNewJsonStructure() {
AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail");
JsonNode json = adminSettings.getJsonValue();
((ObjectNode) json).put("newKey", "my new value");
adminSettings.setJsonValue(json);
adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings);
}
}

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

@ -25,7 +25,7 @@ export default function AdminController(adminService, toast, $scope, $rootScope,
return protocol;
});
vm.tlsVersions = ['TLSv1.0', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
vm.tlsVersions = ['TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3'];
$translate('admin.test-mail-sent').then(function (translation) {
vm.testMailSent = translation;

43
ui/src/app/widget/lib/CanvasDigitalGauge.js

@ -104,26 +104,32 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
}
var colorsCount = options.levelColors.length;
var inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
const inc = colorsCount > 1 ? (1 / (colorsCount - 1)) : 1;
var isColorProperty = angular.isString(options.levelColors[0]);
options.colorsRange = [];
if (options.neonGlowBrightness) {
options.neonColorsRange = [];
}
for (var i = 0; i < options.levelColors.length; i++) {
var percentage = inc * i;
var tColor = tinycolor(options.levelColors[i]);
options.colorsRange[i] = {
pct: percentage,
color: tColor.toRgb(),
rgbString: tColor.toRgbString()
};
if (options.neonGlowBrightness) {
tColor = tinycolor(options.levelColors[i]).brighten(options.neonGlowBrightness);
options.neonColorsRange[i] = {
for (let i = 0; i < options.levelColors.length; i++) {
const levelColor = options.levelColors[i];
if (levelColor !== null) {
let percentage = isColorProperty ? inc * i : CanvasDigitalGauge.normalizeValue(levelColor.value, options.minValue, options.maxValue);
let tColor = tinycolor(isColorProperty ? levelColor : levelColor.color);
options.colorsRange.push({
pct: percentage,
color: tColor.toRgb(),
rgbString: tColor.toRgbString()
};
});
if (options.neonGlowBrightness) {
tColor = tinycolor(isColorProperty ? levelColor : levelColor.color).brighten(options.neonGlowBrightness);
options.neonColorsRange.push({
pct: percentage,
color: tColor.toRgb(),
rgbString: tColor.toRgbString()
});
}
}
}
@ -137,6 +143,17 @@ export default class CanvasDigitalGauge extends canvasGauges.BaseGauge {
return canvasGauges.BaseGauge.configure(options);
}
static normalizeValue (value, min, max) {
let normalValue = (value - min) / (max - min);
if (normalValue <= 0) {
return 0;
}
if (normalValue >= 1) {
return 1;
}
return normalValue;
}
destroy() {
this.contextValueClone = null;
this.elementValueClone = null;

251
ui/src/app/widget/lib/canvas-digital-gauge.js

@ -50,10 +50,16 @@ export default class TbCanvasDigitalGauge {
this.localSettings.gaugeWidthScale = settings.gaugeWidthScale || 0.75;
this.localSettings.gaugeColor = settings.gaugeColor || tinycolor(keyColor).setAlpha(0.2).toRgbString();
if (!settings.levelColors || settings.levelColors.length <= 0) {
this.localSettings.levelColors = [keyColor];
this.localSettings.useFixedLevelColor = settings.useFixedLevelColor || false;
if (!settings.useFixedLevelColor) {
if (!settings.levelColors || settings.levelColors.length <= 0) {
this.localSettings.levelColors = [keyColor];
} else {
this.localSettings.levelColors = settings.levelColors.slice();
}
} else {
this.localSettings.levelColors = settings.levelColors.slice();
this.localSettings.levelColors = [keyColor];
this.localSettings.fixedLevelColors = settings.fixedLevelColors || [];
}
this.localSettings.decimals = angular.isDefined(dataKey.decimals) ? dataKey.decimals :
@ -191,15 +197,137 @@ export default class TbCanvasDigitalGauge {
};
this.gauge = new CanvasDigitalGauge(gaugeData).draw();
this.init();
}
init() {
if (this.localSettings.useFixedLevelColor) {
if (this.localSettings.fixedLevelColors && this.localSettings.fixedLevelColors.length > 0) {
this.localSettings.levelColors = this.settingLevelColorsSubscribe(this.localSettings.fixedLevelColors);
this.updateLevelColors(this.localSettings.levelColors);
}
}
}
settingLevelColorsSubscribe(options) {
let levelColorsDatasource = [];
let predefineLevelColors = [];
function setLevelColor(levelSetting, color) {
if (levelSetting.valueSource === 'predefinedValue' && isFinite(levelSetting.value)) {
predefineLevelColors.push({
value: levelSetting.value,
color: color
})
} else if (levelSetting.entityAlias && levelSetting.attribute) {
let entityAliasId = this.ctx.aliasController.getEntityAliasId(levelSetting.entityAlias);
if (!entityAliasId) {
return;
}
let datasource = levelColorsDatasource.filter((datasource) => {
return datasource.entityAliasId === entityAliasId;
})[0];
let dataKey = {
type: this.ctx.$scope.$injector.get('types').dataKeyType.attribute,
name: levelSetting.attribute,
label: levelSetting.attribute,
settings: [{
color: color,
index: predefineLevelColors.length
}],
_hash: Math.random()
};
if (datasource) {
let findDataKey = datasource.dataKeys.filter((dataKey) => {
return dataKey.name === levelSetting.attribute;
})[0];
if (findDataKey) {
findDataKey.settings.push({
color: color,
index: predefineLevelColors.length
});
} else {
datasource.dataKeys.push(dataKey)
}
} else {
datasource = {
type: this.ctx.$scope.$injector.get('types').datasourceType.entity,
name: levelSetting.entityAlias,
aliasName: levelSetting.entityAlias,
entityAliasId: entityAliasId,
dataKeys: [dataKey]
};
levelColorsDatasource.push(datasource);
}
predefineLevelColors.push(null);
}
}
for (let i = 0; i < options.length; i++) {
let levelColor = options[i];
if (levelColor.from) {
setLevelColor.call(this, levelColor.from, levelColor.color);
}
if (levelColor.to) {
setLevelColor.call(this, levelColor.to, levelColor.color);
}
}
this.subscribeLevelColorsAttributes(levelColorsDatasource);
return predefineLevelColors;
}
updateLevelColors(levelColors) {
this.gauge.options.levelColors = levelColors;
this.gauge.options = CanvasDigitalGauge.configure(this.gauge.options);
this.gauge.update();
}
subscribeLevelColorsAttributes(datasources) {
let TbCanvasDigitalGauge = this;
let levelColorsSourcesSubscriptionOptions = {
datasources: datasources,
useDashboardTimewindow: false,
type: this.ctx.$scope.$injector.get('types').widgetType.latest.value,
callbacks: {
onDataUpdated: (subscription) => {
for (let i = 0; i < subscription.data.length; i++) {
let keyData = subscription.data[i];
if (keyData && keyData.data && keyData.data[0]) {
let attrValue = keyData.data[0][1];
if (isFinite(attrValue)) {
for (let i = 0; i < keyData.dataKey.settings.length; i++) {
let setting = keyData.dataKey.settings[i];
this.localSettings.levelColors[setting.index] = {
value: attrValue,
color: setting.color
};
}
}
}
}
this.updateLevelColors(this.localSettings.levelColors);
}
}
};
this.ctx.subscriptionApi.createSubscription(levelColorsSourcesSubscriptionOptions, true).then(
(subscription) => {
TbCanvasDigitalGauge.levelColorSourcesSubscription = subscription;
}
);
}
update() {
if (this.ctx.data.length > 0) {
var cellData = this.ctx.data[0];
if (cellData.data.length > 0) {
var tvPair = cellData.data[cellData.data.length -
1];
var tvPair = cellData.data[cellData.data.length - 1];
var timestamp;
if (this.localSettings.showTimestamp) {
timestamp = tvPair[0];
@ -325,6 +453,11 @@ export default class TbCanvasDigitalGauge {
"type": "string",
"default": null
},
"useFixedLevelColor": {
"title": "Use precise value for the color indicator",
"type": "boolean",
"default": false
},
"levelColors": {
"title": "Colors of indicator, from lower to upper",
"type": "array",
@ -333,6 +466,66 @@ export default class TbCanvasDigitalGauge {
"type": "string"
}
},
"fixedLevelColors": {
"title": "The colors for the indicator using boundary values",
"type": "array",
"items": {
"title": "levelColor",
"type": "object",
"properties": {
"from": {
"title": "From",
"type": "object",
"properties": {
"valueSource": {
"title": "[From] Value source",
"type": "string",
"default": "predefinedValue"
},
"entityAlias": {
"title": "[From] Source entity alias",
"type": "string"
},
"attribute": {
"title": "[From] Source entity attribute",
"type": "string"
},
"value": {
"title": "[From] Value (if predefined value is selected)",
"type": "number"
}
}
},
"to": {
"title": "To",
"type": "object",
"properties": {
"valueSource": {
"title": "[To] Value source",
"type": "string",
"default": "predefinedValue"
},
"entityAlias": {
"title": "[To] Source entity alias",
"type": "string"
},
"attribute": {
"title": "[To] Source entity attribute",
"type": "string"
},
"value": {
"title": "[To] Value (if predefined value is selected)",
"type": "number"
}
}
},
"color": {
"title": "Color",
"type": "string"
}
}
}
},
"animation": {
"title": "Enable animation",
"type": "boolean",
@ -521,8 +714,10 @@ export default class TbCanvasDigitalGauge {
"key": "gaugeColor",
"type": "color"
},
"useFixedLevelColor",
{
"key": "levelColors",
"condition": "model.useFixedLevelColor !== true",
"items": [
{
"key": "levelColors[]",
@ -530,6 +725,52 @@ export default class TbCanvasDigitalGauge {
}
]
},
{
"key": "fixedLevelColors",
"condition": "model.useFixedLevelColor === true",
"items": [
{
"key": "fixedLevelColors[].from.valueSource",
"type": "rc-select",
"multiple": false,
"items": [
{
"value": "predefinedValue",
"label": "Predefined value (Default)"
},
{
"value": "entityAttribute",
"label": "Value taken from entity attribute"
}
]
},
"fixedLevelColors[].from.value",
"fixedLevelColors[].from.entityAlias",
"fixedLevelColors[].from.attribute",
{
"key": "fixedLevelColors[].to.valueSource",
"type": "rc-select",
"multiple": false,
"items": [
{
"value": "predefinedValue",
"label": "Predefined value (Default)"
},
{
"value": "entityAttribute",
"label": "Value taken from entity attribute"
}
]
},
"fixedLevelColors[].to.value",
"fixedLevelColors[].to.entityAlias",
"fixedLevelColors[].to.attribute",
{
"key": "fixedLevelColors[].color",
"type": "color"
}
]
},
"animation",
"animationDuration",
{

Loading…
Cancel
Save