Merge branch 'master' of github.com:thingsboard/thingsboard

This commit is contained in:
Andrii Shvaika 2021-11-08 14:38:34 +02:00
commit 1affc60ace
27 changed files with 212 additions and 41 deletions

View File

@ -55,7 +55,7 @@
"templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>", "templateHtml": "<tb-timeseries-table-widget \n [ctx]=\"ctx\">\n</tb-timeseries-table-widget>",
"templateCss": "", "templateCss": "",
"controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n }\n };\n}\n\nself.onDestroy = function() {\n}", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.timeseriesTableWidget.onDataUpdated();\n}\n\nself.typeParameters = function() {\n return {\n ignoreDataUpdateOnIntervalTick: true\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: 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 \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\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 \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"identifyDeviceSelector\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\n ]\n}", "settingsSchema": "{\n \"schema\": {\n \"type\": \"object\",\n \"title\": \"TimeseriesTableSettings\",\n \"properties\": {\n \"enableSearch\": {\n \"title\": \"Enable search\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyHeader\": {\n \"title\": \"Always display header\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"enableStickyAction\": {\n \"title\": \"Always display actions column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"reserveSpaceForHiddenAction\": {\n \"title\": \"Hidden cell button actions display mode\",\n \"type\": \"string\",\n \"default\": \"true\"\n },\n \"showTimestamp\": {\n \"title\": \"Display timestamp column\",\n \"type\": \"boolean\",\n \"default\": true\n },\n \"showMilliseconds\": {\n \"title\": \"Display timestamp milliseconds\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"displayPagination\": {\n \"title\": \"Display pagination\",\n \"type\": \"boolean\",\n \"default\": true\n }, \n \"useEntityLabel\": {\n \"title\": \"Use entity label in tab name\",\n \"type\": \"boolean\",\n \"default\": false\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 \"disableStickyHeader\": {\n \"title\": \"Disable sticky header\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"useRowStyleFunction\": {\n \"title\": \"Use row style function\",\n \"type\": \"boolean\",\n \"default\": false\n },\n \"rowStyleFunction\": {\n \"title\": \"Row style function: f(rowData, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"enableSearch\",\n \"enableStickyHeader\",\n \"enableStickyAction\",\n {\n \"key\": \"reserveSpaceForHiddenAction\",\n \"type\": \"rc-select\",\n \"multiple\": false,\n \"items\": [\n {\n \"value\": \"true\",\n \"label\": \"Show empty space instead of hidden cell button action\"\n },\n {\n \"value\": \"false\",\n \"label\": \"Don't reserve space for hidden action buttons\"\n }\n ]\n },\n \"showTimestamp\",\n \"showMilliseconds\",\n \"displayPagination\",\n \"useEntityLabel\",\n \"defaultPageSize\",\n \"hideEmptyLines\",\n \"useRowStyleFunction\",\n {\n \"key\": \"rowStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/row_style_fn\",\n \"condition\": \"model.useRowStyleFunction === true\"\n }\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, rowData, ctx)\",\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, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\n }\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, rowData, ctx)\",\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, ctx)\",\n \"type\": \"string\",\n \"default\": \"\"\n }\n },\n \"required\": []\n },\n \"form\": [\n \"useCellStyleFunction\",\n {\n \"key\": \"cellStyleFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_style_fn\",\n \"condition\": \"model.useCellStyleFunction === true\"\n },\n \"useCellContentFunction\",\n {\n \"key\": \"cellContentFunction\",\n \"type\": \"javascript\",\n \"helpId\": \"widget/lib/timeseries/cell_content_fn\",\n \"condition\": \"model.useCellContentFunction === true\"\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,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}" "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,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"widgetStyle\":{},\"actions\":{},\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\"}"
} }

View File

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.AdminSettingsId;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel @ApiModel
@ -29,6 +30,7 @@ public class AdminSettings extends BaseData<AdminSettingsId> {
private static final long serialVersionUID = -7670322981725511892L; private static final long serialVersionUID = -7670322981725511892L;
@NoXss @NoXss
@Length(fieldName = "key")
private String key; private String key;
private transient JsonNode jsonValue; private transient JsonNode jsonValue;

View File

@ -68,10 +68,13 @@ public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo<OtaPackage
@NoXss @NoXss
@ApiModelProperty(position = 11, value = "OTA Package file name.", example = "fw_1.0", readOnly = true) @ApiModelProperty(position = 11, value = "OTA Package file name.", example = "fw_1.0", readOnly = true)
private String fileName; private String fileName;
@NoXss
@Length(fieldName = "contentType")
@ApiModelProperty(position = 12, value = "OTA Package content type.", example = "APPLICATION_OCTET_STREAM", readOnly = true) @ApiModelProperty(position = 12, value = "OTA Package content type.", example = "APPLICATION_OCTET_STREAM", readOnly = true)
private String contentType; private String contentType;
@ApiModelProperty(position = 13, value = "OTA Package checksum algorithm.", example = "CRC32", readOnly = true) @ApiModelProperty(position = 13, value = "OTA Package checksum algorithm.", example = "CRC32", readOnly = true)
private ChecksumAlgorithm checksumAlgorithm; private ChecksumAlgorithm checksumAlgorithm;
@Length(fieldName = "checksum", max = 1020)
@ApiModelProperty(position = 14, value = "OTA Package checksum.", example = "0xd87f7e0c", readOnly = true) @ApiModelProperty(position = 14, value = "OTA Package checksum.", example = "0xd87f7e0c", readOnly = true)
private String checksum; private String checksum;
@ApiModelProperty(position = 15, value = "OTA Package data size.", example = "8", readOnly = true) @ApiModelProperty(position = 15, value = "OTA Package data size.", example = "8", readOnly = true)

View File

@ -42,6 +42,8 @@ public class TbResourceInfo extends SearchTextBased<TbResourceId> implements Has
private String title; private String title;
@ApiModelProperty(position = 5, value = "Resource type.", example = "LWM2M_MODEL", readOnly = true) @ApiModelProperty(position = 5, value = "Resource type.", example = "LWM2M_MODEL", readOnly = true)
private ResourceType resourceType; private ResourceType resourceType;
@NoXss
@Length(fieldName = "resourceKey")
@ApiModelProperty(position = 6, value = "Resource key.", example = "19_1.0", readOnly = true) @ApiModelProperty(position = 6, value = "Resource key.", example = "19_1.0", readOnly = true)
private String resourceKey; private String resourceKey;
@ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", readOnly = true) @ApiModelProperty(position = 7, value = "Resource search text.", example = "19_1.0:binaryappdatacontainer", readOnly = true)

View File

@ -37,6 +37,7 @@ public class Tenant extends ContactBased<TenantId> implements HasTenantId {
@ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A") @ApiModelProperty(position = 3, value = "Title of the tenant", example = "Company A")
private String title; private String title;
@NoXss @NoXss
@Length(fieldName = "region")
@ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America") @ApiModelProperty(position = 5, value = "Geo region of the tenant", example = "North America")
private String region; private String region;

View File

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
@ApiModel @ApiModel
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ -41,15 +42,26 @@ public class Edge extends SearchTextBasedWithAdditionalInfo<EdgeId> implements H
private TenantId tenantId; private TenantId tenantId;
private CustomerId customerId; private CustomerId customerId;
private RuleChainId rootRuleChainId; private RuleChainId rootRuleChainId;
@NoXss
@Length(fieldName = "name") @Length(fieldName = "name")
private String name; private String name;
@NoXss
@Length(fieldName = "type") @Length(fieldName = "type")
private String type; private String type;
@NoXss
@Length(fieldName = "label") @Length(fieldName = "label")
private String label; private String label;
@NoXss
@Length(fieldName = "routingKey")
private String routingKey; private String routingKey;
@NoXss
@Length(fieldName = "secret")
private String secret; private String secret;
@NoXss
@Length(fieldName = "edgeLicenseKey", max = 30)
private String edgeLicenseKey; private String edgeLicenseKey;
@NoXss
@Length(fieldName = "cloudEndpoint")
private String cloudEndpoint; private String cloudEndpoint;
public Edge() { public Edge() {

View File

@ -21,6 +21,7 @@ import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import org.thingsboard.server.common.data.validation.Length;
@Builder(toBuilder = true) @Builder(toBuilder = true)
@EqualsAndHashCode @EqualsAndHashCode
@ -28,21 +29,27 @@ import lombok.ToString;
@ToString @ToString
@ApiModel @ApiModel
public class OAuth2BasicMapperConfig { public class OAuth2BasicMapperConfig {
@Length(fieldName = "emailAttributeKey", max = 31)
@ApiModelProperty(value = "Email attribute key of OAuth2 principal attributes. " + @ApiModelProperty(value = "Email attribute key of OAuth2 principal attributes. " +
"Must be specified for BASIC mapper type and cannot be specified for GITHUB type") "Must be specified for BASIC mapper type and cannot be specified for GITHUB type")
private final String emailAttributeKey; private final String emailAttributeKey;
@Length(fieldName = "firstNameAttributeKey", max = 31)
@ApiModelProperty(value = "First name attribute key") @ApiModelProperty(value = "First name attribute key")
private final String firstNameAttributeKey; private final String firstNameAttributeKey;
@Length(fieldName = "lastNameAttributeKey", max = 31)
@ApiModelProperty(value = "Last name attribute key") @ApiModelProperty(value = "Last name attribute key")
private final String lastNameAttributeKey; private final String lastNameAttributeKey;
@ApiModelProperty(value = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", required = true) @ApiModelProperty(value = "Tenant naming strategy. For DOMAIN type, domain for tenant name will be taken from the email (substring before '@')", required = true)
private final TenantNameStrategyType tenantNameStrategy; private final TenantNameStrategyType tenantNameStrategy;
@Length(fieldName = "tenantNamePattern")
@ApiModelProperty(value = "Tenant name pattern for CUSTOM naming strategy. " + @ApiModelProperty(value = "Tenant name pattern for CUSTOM naming strategy. " +
"OAuth2 attributes in the pattern can be used by enclosing attribute key in '%{' and '}'", example = "%{email}") "OAuth2 attributes in the pattern can be used by enclosing attribute key in '%{' and '}'", example = "%{email}")
private final String tenantNamePattern; private final String tenantNamePattern;
@Length(fieldName = "customerNamePattern")
@ApiModelProperty(value = "Customer name pattern. When creating a user on the first OAuth2 log in, if specified, " + @ApiModelProperty(value = "Customer name pattern. When creating a user on the first OAuth2 log in, if specified, " +
"customer name will be used to create or find existing customer in the platform and assign customerId to the user") "customer name will be used to create or find existing customer in the platform and assign customerId to the user")
private final String customerNamePattern; private final String customerNamePattern;
@Length(fieldName = "defaultDashboardName")
@ApiModelProperty(value = "Name of the tenant's dashboard to set as default dashboard for newly created user") @ApiModelProperty(value = "Name of the tenant's dashboard to set as default dashboard for newly created user")
private final String defaultDashboardName; private final String defaultDashboardName;
@ApiModelProperty(value = "Whether default dashboard should be open in full screen") @ApiModelProperty(value = "Whether default dashboard should be open in full screen")

View File

@ -24,7 +24,9 @@ import lombok.ToString;
import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo;
import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId;
import org.thingsboard.server.common.data.validation.Length;
import javax.validation.Valid;
import java.util.List; import java.util.List;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@ -34,30 +36,41 @@ import java.util.List;
@ApiModel @ApiModel
public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo<OAuth2ClientRegistrationTemplateId> implements HasName { public class OAuth2ClientRegistrationTemplate extends SearchTextBasedWithAdditionalInfo<OAuth2ClientRegistrationTemplateId> implements HasName {
@Length(fieldName = "providerId")
@ApiModelProperty(value = "OAuth2 provider identifier (e.g. its name)", required = true) @ApiModelProperty(value = "OAuth2 provider identifier (e.g. its name)", required = true)
private String providerId; private String providerId;
@Valid
@ApiModelProperty(value = "Default config for mapping OAuth2 log in response to platform entities") @ApiModelProperty(value = "Default config for mapping OAuth2 log in response to platform entities")
private OAuth2MapperConfig mapperConfig; private OAuth2MapperConfig mapperConfig;
@Length(fieldName = "authorizationUri")
@ApiModelProperty(value = "Default authorization URI of the OAuth2 provider") @ApiModelProperty(value = "Default authorization URI of the OAuth2 provider")
private String authorizationUri; private String authorizationUri;
@Length(fieldName = "accessTokenUri")
@ApiModelProperty(value = "Default access token URI of the OAuth2 provider") @ApiModelProperty(value = "Default access token URI of the OAuth2 provider")
private String accessTokenUri; private String accessTokenUri;
@ApiModelProperty(value = "Default OAuth scopes that will be requested from OAuth2 platform") @ApiModelProperty(value = "Default OAuth scopes that will be requested from OAuth2 platform")
private List<String> scope; private List<String> scope;
@Length(fieldName = "userInfoUri")
@ApiModelProperty(value = "Default user info URI of the OAuth2 provider") @ApiModelProperty(value = "Default user info URI of the OAuth2 provider")
private String userInfoUri; private String userInfoUri;
@Length(fieldName = "userNameAttributeName")
@ApiModelProperty(value = "Default name of the username attribute in OAuth2 provider log in response") @ApiModelProperty(value = "Default name of the username attribute in OAuth2 provider log in response")
private String userNameAttributeName; private String userNameAttributeName;
@Length(fieldName = "jwkSetUri")
@ApiModelProperty(value = "Default JSON Web Key URI of the OAuth2 provider") @ApiModelProperty(value = "Default JSON Web Key URI of the OAuth2 provider")
private String jwkSetUri; private String jwkSetUri;
@Length(fieldName = "clientAuthenticationMethod")
@ApiModelProperty(value = "Default client authentication method to use: 'BASIC' or 'POST'") @ApiModelProperty(value = "Default client authentication method to use: 'BASIC' or 'POST'")
private String clientAuthenticationMethod; private String clientAuthenticationMethod;
@ApiModelProperty(value = "Comment for OAuth2 provider") @ApiModelProperty(value = "Comment for OAuth2 provider")
private String comment; private String comment;
@Length(fieldName = "loginButtonIcon")
@ApiModelProperty(value = "Default log in button icon for OAuth2 provider") @ApiModelProperty(value = "Default log in button icon for OAuth2 provider")
private String loginButtonIcon; private String loginButtonIcon;
@Length(fieldName = "loginButtonLabel")
@ApiModelProperty(value = "Default OAuth2 provider label") @ApiModelProperty(value = "Default OAuth2 provider label")
private String loginButtonLabel; private String loginButtonLabel;
@Length(fieldName = "helpLink")
@ApiModelProperty(value = "Help link for OAuth2 provider") @ApiModelProperty(value = "Help link for OAuth2 provider")
private String helpLink; private String helpLink;

View File

@ -15,15 +15,22 @@
*/ */
package org.thingsboard.server.common.data.oauth2; package org.thingsboard.server.common.data.oauth2;
import lombok.*; import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.thingsboard.server.common.data.validation.Length;
@Builder(toBuilder = true) @Builder(toBuilder = true)
@EqualsAndHashCode @EqualsAndHashCode
@Data @Data
@ToString(exclude = {"password"}) @ToString(exclude = {"password"})
public class OAuth2CustomMapperConfig { public class OAuth2CustomMapperConfig {
@Length(fieldName = "url")
private final String url; private final String url;
@Length(fieldName = "username")
private final String username; private final String username;
@Length(fieldName = "password")
private final String password; private final String password;
private final boolean sendToken; private final boolean sendToken;
} }

View File

@ -21,6 +21,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
import javax.validation.Valid;
@Builder(toBuilder = true) @Builder(toBuilder = true)
@EqualsAndHashCode @EqualsAndHashCode
@Data @Data
@ -32,8 +34,10 @@ public class OAuth2MapperConfig {
private boolean activateUser; private boolean activateUser;
@ApiModelProperty(value = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", required = true) @ApiModelProperty(value = "Type of OAuth2 mapper. Depending on this param, different mapper config fields must be specified", required = true)
private MapperType type; private MapperType type;
@Valid
@ApiModelProperty(value = "Mapper config for BASIC and GITHUB mapper types") @ApiModelProperty(value = "Mapper config for BASIC and GITHUB mapper types")
private OAuth2BasicMapperConfig basic; private OAuth2BasicMapperConfig basic;
@Valid
@ApiModelProperty(value = "Mapper config for CUSTOM mapper type") @ApiModelProperty(value = "Mapper config for CUSTOM mapper type")
private OAuth2CustomMapperConfig custom; private OAuth2CustomMapperConfig custom;
} }

View File

@ -21,6 +21,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.*; import lombok.*;
import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.id.ComponentDescriptorId; import org.thingsboard.server.common.data.id.ComponentDescriptorId;
import org.thingsboard.server.common.data.validation.Length;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -35,12 +36,14 @@ public class ComponentDescriptor extends SearchTextBased<ComponentDescriptorId>
@Getter @Setter private ComponentType type; @Getter @Setter private ComponentType type;
@ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", readOnly = true, allowableValues = "TENANT", example = "TENANT") @ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", readOnly = true, allowableValues = "TENANT", example = "TENANT")
@Getter @Setter private ComponentScope scope; @Getter @Setter private ComponentScope scope;
@Length(fieldName = "name")
@ApiModelProperty(position = 5, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", readOnly = true, example = "Custom Rule Node") @ApiModelProperty(position = 5, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", readOnly = true, example = "Custom Rule Node")
@Getter @Setter private String name; @Getter @Setter private String name;
@ApiModelProperty(position = 6, value = "Full name of the Java class that implements the Rule Engine Node interface.", readOnly = true, example = "com.mycompany.CustomRuleNode") @ApiModelProperty(position = 6, value = "Full name of the Java class that implements the Rule Engine Node interface.", readOnly = true, example = "com.mycompany.CustomRuleNode")
@Getter @Setter private String clazz; @Getter @Setter private String clazz;
@ApiModelProperty(position = 7, value = "Complex JSON object that represents the Rule Node configuration.", readOnly = true) @ApiModelProperty(position = 7, value = "Complex JSON object that represents the Rule Node configuration.", readOnly = true)
@Getter @Setter private transient JsonNode configurationDescriptor; @Getter @Setter private transient JsonNode configurationDescriptor;
@Length(fieldName = "actions")
@ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", readOnly = true) @ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", readOnly = true)
@Getter @Setter private String actions; @Getter @Setter private String actions;

View File

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@Data @Data
@ -31,12 +32,15 @@ public class BaseWidgetType extends BaseData<WidgetTypeId> implements HasTenantI
@ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true) @ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true)
private TenantId tenantId; private TenantId tenantId;
@NoXss @NoXss
@Length(fieldName = "bundleAlias")
@ApiModelProperty(position = 4, value = "Reference to widget bundle", readOnly = true) @ApiModelProperty(position = 4, value = "Reference to widget bundle", readOnly = true)
private String bundleAlias; private String bundleAlias;
@NoXss @NoXss
@Length(fieldName = "alias")
@ApiModelProperty(position = 5, value = "Unique alias that is used in dashboards as a reference widget type", readOnly = true) @ApiModelProperty(position = 5, value = "Unique alias that is used in dashboards as a reference widget type", readOnly = true)
private String alias; private String alias;
@NoXss @NoXss
@Length(fieldName = "name")
@ApiModelProperty(position = 6, value = "Widget name used in search and UI", readOnly = true) @ApiModelProperty(position = 6, value = "Widget name used in search and UI", readOnly = true)
private String name; private String name;

View File

@ -19,15 +19,18 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
@Data @Data
@JsonPropertyOrder({ "alias", "name", "image", "description", "descriptor" }) @JsonPropertyOrder({ "alias", "name", "image", "description", "descriptor" })
public class WidgetTypeDetails extends WidgetType { public class WidgetTypeDetails extends WidgetType {
@NoXss
@ApiModelProperty(position = 8, value = "Base64 encoded thumbnail", readOnly = true) @ApiModelProperty(position = 8, value = "Base64 encoded thumbnail", readOnly = true)
private String image; private String image;
@NoXss @NoXss
@Length(fieldName = "description")
@ApiModelProperty(position = 9, value = "Description of the widget", readOnly = true) @ApiModelProperty(position = 9, value = "Description of the widget", readOnly = true)
private String description; private String description;

View File

@ -34,10 +34,9 @@ public class CoapEfentoCallback implements TransportServiceCallback<Void> {
@Override @Override
public void onSuccess(Void msg) { public void onSuccess(Void msg) {
//We respond only to confirmed requests in order to reduce battery consumption for Efento devices.
if (isConRequest()) { if (isConRequest()) {
Response response = new Response(onSuccessResponse); exchange.respond(new Response(onSuccessResponse));
response.setAcknowledged(true);
exchange.respond(response);
} }
} }

View File

@ -99,10 +99,9 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
break; break;
case DEVICE_INFO: case DEVICE_INFO:
case CONFIGURATION: case CONFIGURATION:
Response response = new Response(CoAP.ResponseCode.CREATED); //We respond only to confirmed requests in order to reduce battery consumption for Efento devices.
if (exchange.advanced().getRequest().isConfirmable()) { if (exchange.advanced().getRequest().isConfirmable()) {
response.setAcknowledged(true); exchange.respond(new Response(CoAP.ResponseCode.CREATED));
exchange.respond(response);
} }
break; break;
default: default:

View File

@ -157,7 +157,11 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple
private boolean deleteRelationIfExists(RelationCompositeKey key) { private boolean deleteRelationIfExists(RelationCompositeKey key) {
boolean relationExistsBeforeDelete = relationRepository.existsById(key); boolean relationExistsBeforeDelete = relationRepository.existsById(key);
if (relationExistsBeforeDelete) { if (relationExistsBeforeDelete) {
relationRepository.deleteById(key); try {
relationRepository.deleteById(key);
} catch (ConcurrencyFailureException e) {
log.debug("[{}] Concurrency exception while deleting relation", key, e);
}
} }
return relationExistsBeforeDelete; return relationExistsBeforeDelete;
} }

View File

@ -16,6 +16,8 @@
package org.thingsboard.server.dao.service; package org.thingsboard.server.dao.service;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
@ -31,6 +33,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -84,6 +87,23 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest {
Assert.assertTrue(relationService.deleteRelationAsync(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get()); Assert.assertTrue(relationService.deleteRelationAsync(SYSTEM_TENANT_ID, childId, subChildId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.COMMON).get());
} }
@Test
public void testDeleteRelationConcurrently() throws ExecutionException, InterruptedException {
AssetId parentId = new AssetId(Uuids.timeBased());
AssetId childId = new AssetId(Uuids.timeBased());
EntityRelation relationA = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE);
saveRelation(relationA);
List<ListenableFuture<Boolean>> futures = new ArrayList<>();
for (int i = 0; i < 2; i++) {
futures.add(relationService.deleteRelationAsync(SYSTEM_TENANT_ID, relationA));
}
List<Boolean> results = Futures.allAsList(futures).get();
Assert.assertTrue(results.contains(true));
}
@Test @Test
public void testDeleteEntityRelations() throws ExecutionException, InterruptedException { public void testDeleteEntityRelations() throws ExecutionException, InterruptedException {
AssetId parentId = new AssetId(Uuids.timeBased()); AssetId parentId = new AssetId(Uuids.timeBased());

View File

@ -55,6 +55,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
/** /**
* Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times * Represents an MqttClientImpl connected to a single MQTT server. Will try to keep the connection going at all times
@ -155,11 +156,14 @@ final class MqttClientImpl implements MqttClient {
if (callback != null) { if (callback != null) {
callback.connectionLost(e); callback.connectionLost(e);
} }
pendingSubscriptions.forEach((id, mqttPendingSubscription) -> mqttPendingSubscription.onChannelClosed());
pendingSubscriptions.clear(); pendingSubscriptions.clear();
serverSubscriptions.clear(); serverSubscriptions.clear();
subscriptions.clear(); subscriptions.clear();
pendingServerUnsubscribes.forEach((id, mqttPendingServerUnsubscribes) -> mqttPendingServerUnsubscribes.onChannelClosed());
pendingServerUnsubscribes.clear(); pendingServerUnsubscribes.clear();
qos2PendingIncomingPublishes.clear(); qos2PendingIncomingPublishes.clear();
pendingPublishes.forEach((id, mqttPendingPublish) -> mqttPendingPublish.onChannelClosed());
pendingPublishes.clear(); pendingPublishes.clear();
pendingSubscribeTopics.clear(); pendingSubscribeTopics.clear();
handlerToSubscribtion.clear(); handlerToSubscribtion.clear();
@ -366,19 +370,24 @@ final class MqttClientImpl implements MqttClient {
ChannelFuture channelFuture = this.sendAndFlushPacket(message); ChannelFuture channelFuture = this.sendAndFlushPacket(message);
if (channelFuture != null) { if (channelFuture != null) {
pendingPublish.setSent(true); channelFuture.addListener(result -> {
if (channelFuture.cause() != null) { pendingPublish.setSent(true);
future.setFailure(channelFuture.cause()); if (result.cause() != null) {
return future; pendingPublishes.remove(pendingPublish.getMessageId());
} future.setFailure(result.cause());
} } else {
if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) { if (pendingPublish.isSent() && pendingPublish.getQos() == MqttQoS.AT_MOST_ONCE) {
this.pendingPublishes.remove(pendingPublish.getMessageId()); pendingPublishes.remove(pendingPublish.getMessageId());
pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0 pendingPublish.getFuture().setSuccess(null); //We don't get an ACK for QOS 0
} else if (pendingPublish.isSent()) { } else if (pendingPublish.isSent()) {
pendingPublish.startPublishRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); pendingPublish.startPublishRetransmissionTimer(eventLoop.next(), MqttClientImpl.this::sendAndFlushPacket);
} else {
pendingPublishes.remove(pendingPublish.getMessageId());
}
}
});
} else { } else {
this.pendingPublishes.remove(pendingPublish.getMessageId()); pendingPublishes.remove(pendingPublish.getMessageId());
} }
return future; return future;
} }

View File

@ -24,7 +24,7 @@ import io.netty.util.concurrent.Promise;
import java.util.function.Consumer; import java.util.function.Consumer;
final class MqttPendingPublish { final class MqttPendingPublish{
private final int messageId; private final int messageId;
private final Promise<Void> future; private final Promise<Void> future;
@ -98,4 +98,9 @@ final class MqttPendingPublish {
void onPubcompReceived() { void onPubcompReceived() {
this.pubrelRetransmissionHandler.stop(); this.pubrelRetransmissionHandler.stop();
} }
void onChannelClosed(){
this.publishRetransmissionHandler.stop();
this.pubrelRetransmissionHandler.stop();
}
} }

View File

@ -23,7 +23,7 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
final class MqttPendingSubscription { final class MqttPendingSubscription{
private final Promise<Void> future; private final Promise<Void> future;
private final String topic; private final String topic;
@ -99,4 +99,8 @@ final class MqttPendingSubscription {
return once; return once;
} }
} }
void onChannelClosed(){
this.retransmissionHandler.stop();
}
} }

View File

@ -21,7 +21,7 @@ import io.netty.util.concurrent.Promise;
import java.util.function.Consumer; import java.util.function.Consumer;
final class MqttPendingUnsubscription { final class MqttPendingUnsubscription{
private final Promise<Void> future; private final Promise<Void> future;
private final String topic; private final String topic;
@ -52,4 +52,8 @@ final class MqttPendingUnsubscription {
void onUnsubackReceived(){ void onUnsubackReceived(){
this.retransmissionHandler.stop(); this.retransmissionHandler.stop();
} }
void onChannelClosed(){
this.retransmissionHandler.stop();
}
} }

View File

@ -79,6 +79,9 @@
<mat-error *ngIf="domainInfo.get('name').hasError('pattern')"> <mat-error *ngIf="domainInfo.get('name').hasError('pattern')">
{{ 'admin.error-verification-url' | translate }} {{ 'admin.error-verification-url' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="domainInfo.get('name').hasError('maxlength')">
{{ 'admin.domain-name-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-error *ngIf="domainInfo.hasError('unique')"> <mat-error *ngIf="domainInfo.hasError('unique')">
@ -246,6 +249,9 @@
<mat-error *ngIf="registration.get('clientId').hasError('required')"> <mat-error *ngIf="registration.get('clientId').hasError('required')">
{{ 'admin.oauth2.client-id-required' | translate }} {{ 'admin.oauth2.client-id-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="registration.get('clientId').hasError('maxlength')">
{{ 'admin.oauth2.client-id-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
@ -254,6 +260,9 @@
<mat-error *ngIf="registration.get('clientSecret').hasError('required')"> <mat-error *ngIf="registration.get('clientSecret').hasError('required')">
{{ 'admin.oauth2.client-secret-required' | translate }} {{ 'admin.oauth2.client-secret-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="registration.get('clientSecret').hasError('maxlength')">
{{ 'admin.oauth2.client-secret-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
@ -426,17 +435,29 @@
*ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('required')"> *ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('required')">
{{ 'admin.oauth2.email-attribute-key-required' | translate }} {{ 'admin.oauth2.email-attribute-key-required' | translate }}
</mat-error> </mat-error>
<mat-error
*ngIf="registration.get('mapperConfig.basic.emailAttributeKey').hasError('maxlength')">
{{ 'admin.oauth2.email-attribute-key-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.first-name-attribute-key</mat-label> <mat-label translate>admin.oauth2.first-name-attribute-key</mat-label>
<input matInput formControlName="firstNameAttributeKey"> <input matInput formControlName="firstNameAttributeKey">
<mat-error
*ngIf="registration.get('mapperConfig.basic.firstNameAttributeKey').hasError('maxlength')">
{{ 'admin.oauth2.first-name-attribute-key-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.last-name-attribute-key</mat-label> <mat-label translate>admin.oauth2.last-name-attribute-key</mat-label>
<input matInput formControlName="lastNameAttributeKey"> <input matInput formControlName="lastNameAttributeKey">
<mat-error
*ngIf="registration.get('mapperConfig.basic.lastNameAttributeKey').hasError('maxlength')">
{{ 'admin.oauth2.last-name-attribute-key-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
@ -460,18 +481,30 @@
*ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('required')"> *ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('required')">
{{ 'admin.oauth2.tenant-name-pattern-required' | translate }} {{ 'admin.oauth2.tenant-name-pattern-required' | translate }}
</mat-error> </mat-error>
<mat-error
*ngIf="registration.get('mapperConfig.basic.tenantNamePattern').hasError('maxlength')">
{{ 'admin.oauth2.tenant-name-pattern-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.customer-name-pattern</mat-label> <mat-label translate>admin.oauth2.customer-name-pattern</mat-label>
<input matInput formControlName="customerNamePattern"> <input matInput formControlName="customerNamePattern">
<mat-error
*ngIf="registration.get('mapperConfig.basic.customerNamePattern').hasError('maxlength')">
{{ 'admin.oauth2.customer-name-pattern-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.oauth2.default-dashboard-name</mat-label> <mat-label translate>admin.oauth2.default-dashboard-name</mat-label>
<input matInput formControlName="defaultDashboardName"> <input matInput formControlName="defaultDashboardName">
<mat-error
*ngIf="registration.get('mapperConfig.basic.defaultDashboardName').hasError('maxlength')">
{{ 'admin.oauth2.default-dashboard-name-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-checkbox fxFlex formControlName="alwaysFullScreen" class="checkbox-row"> <mat-checkbox fxFlex formControlName="alwaysFullScreen" class="checkbox-row">
@ -493,18 +526,28 @@
*ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')"> *ngIf="registration.get('mapperConfig.custom.url').hasError('pattern')">
{{ 'admin.oauth2.url-pattern' | translate }} {{ 'admin.oauth2.url-pattern' | translate }}
</mat-error> </mat-error>
<mat-error
*ngIf="registration.get('mapperConfig.custom.url').hasError('maxlength')">
{{ 'admin.oauth2.url-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px"> <div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>common.username</mat-label> <mat-label translate>common.username</mat-label>
<input matInput formControlName="username" autocomplete="new-username"> <input matInput formControlName="username" autocomplete="new-username">
<mat-error *ngIf="registration.get('mapperConfig.custom.username').hasError('maxlength')">
{{ 'admin.oauth2.username-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
<mat-form-field fxFlex class="mat-block"> <mat-form-field fxFlex class="mat-block">
<mat-label translate>common.password</mat-label> <mat-label translate>common.password</mat-label>
<input matInput type="password" formControlName="password" autocomplete="new-password"> <input matInput type="password" formControlName="password" autocomplete="new-password">
<tb-toggle-password matSuffix></tb-toggle-password> <tb-toggle-password matSuffix></tb-toggle-password>
<mat-error *ngIf="registration.get('mapperConfig.custom.password').hasError('maxlength')">
{{ 'admin.oauth2.password-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
</section> </section>

View File

@ -155,13 +155,18 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
tenantNamePattern = {value: null, disabled: true}; tenantNamePattern = {value: null, disabled: true};
} }
const basicGroup = this.fb.group({ const basicGroup = this.fb.group({
emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email', Validators.required], emailAttributeKey: [mapperConfigBasic?.emailAttributeKey ? mapperConfigBasic.emailAttributeKey : 'email',
firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : ''], [Validators.required, Validators.maxLength(31)]],
lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : ''], firstNameAttributeKey: [mapperConfigBasic?.firstNameAttributeKey ? mapperConfigBasic.firstNameAttributeKey : '',
Validators.maxLength(31)],
lastNameAttributeKey: [mapperConfigBasic?.lastNameAttributeKey ? mapperConfigBasic.lastNameAttributeKey : '',
Validators.maxLength(31)],
tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN], tenantNameStrategy: [mapperConfigBasic?.tenantNameStrategy ? mapperConfigBasic.tenantNameStrategy : TenantNameStrategy.DOMAIN],
tenantNamePattern: [tenantNamePattern, Validators.required], tenantNamePattern: [tenantNamePattern, [Validators.required, Validators.maxLength(255)]],
customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null], customerNamePattern: [mapperConfigBasic?.customerNamePattern ? mapperConfigBasic.customerNamePattern : null,
defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null], Validators.maxLength(255)],
defaultDashboardName: [mapperConfigBasic?.defaultDashboardName ? mapperConfigBasic.defaultDashboardName : null,
Validators.maxLength(255)],
alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false] alwaysFullScreen: [isDefinedAndNotNull(mapperConfigBasic?.alwaysFullScreen) ? mapperConfigBasic.alwaysFullScreen : false]
}); });
@ -178,9 +183,10 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): FormGroup { private formCustomGroup(mapperConfigCustom?: MapperConfigCustom): FormGroup {
return this.fb.group({ return this.fb.group({
url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null, [Validators.required, Validators.pattern(this.URL_REGEXP)]], url: [mapperConfigCustom?.url ? mapperConfigCustom.url : null,
username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null], [Validators.required, Validators.pattern(this.URL_REGEXP), Validators.maxLength(255)]],
password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null] username: [mapperConfigCustom?.username ? mapperConfigCustom.username : null, Validators.maxLength(255)],
password: [mapperConfigCustom?.password ? mapperConfigCustom.password : null, Validators.maxLength(255)]
}); });
} }
@ -266,7 +272,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
private buildDomainInfoForm(domainInfo?: OAuth2DomainInfo): FormGroup { private buildDomainInfoForm(domainInfo?: OAuth2DomainInfo): FormGroup {
return this.fb.group({ return this.fb.group({
name: [domainInfo ? domainInfo.name : this.window.location.hostname, [ name: [domainInfo ? domainInfo.name : this.window.location.hostname, [
Validators.required, Validators.required, Validators.maxLength(255),
Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]], Validators.pattern(this.DOMAIN_AND_PORT_REGEXP)]],
scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required] scheme: [domainInfo?.scheme ? domainInfo.scheme : DomainSchema.HTTPS, Validators.required]
}, {validators: this.uniqueDomainValidator}); }, {validators: this.uniqueDomainValidator});
@ -300,8 +306,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
platforms: [registration?.platforms ? registration.platforms : []], platforms: [registration?.platforms ? registration.platforms : []],
loginButtonLabel: [registration?.loginButtonLabel ? registration.loginButtonLabel : null, Validators.required], loginButtonLabel: [registration?.loginButtonLabel ? registration.loginButtonLabel : null, Validators.required],
loginButtonIcon: [registration?.loginButtonIcon ? registration.loginButtonIcon : null], loginButtonIcon: [registration?.loginButtonIcon ? registration.loginButtonIcon : null],
clientId: [registration?.clientId ? registration.clientId : '', Validators.required], clientId: [registration?.clientId ? registration.clientId : '', [Validators.required, Validators.maxLength(255)]],
clientSecret: [registration?.clientSecret ? registration.clientSecret : '', Validators.required], clientSecret: [registration?.clientSecret ? registration.clientSecret : '', [Validators.required, Validators.maxLength(2048)]],
accessTokenUri: [registration?.accessTokenUri ? registration.accessTokenUri : '', accessTokenUri: [registration?.accessTokenUri ? registration.accessTokenUri : '',
[Validators.required, [Validators.required,
Validators.pattern(this.URL_REGEXP)]], Validators.pattern(this.URL_REGEXP)]],

View File

@ -143,8 +143,8 @@
<mat-error *ngIf="entityForm.get('edgeLicenseKey').hasError('required')"> <mat-error *ngIf="entityForm.get('edgeLicenseKey').hasError('required')">
{{ 'edge.edge-license-key-required' | translate }} {{ 'edge.edge-license-key-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="entityForm.get('type').hasError('maxlength')"> <mat-error *ngIf="entityForm.get('edgeLicenseKey').hasError('maxlength')">
{{ 'edge.type-max-length' | translate }} {{ 'edge.edge-license-key-max-length' | translate }}
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
@ -156,6 +156,9 @@
<mat-error *ngIf="entityForm.get('cloudEndpoint').hasError('required')"> <mat-error *ngIf="entityForm.get('cloudEndpoint').hasError('required')">
{{ 'edge.cloud-endpoint-required' | translate }} {{ 'edge.cloud-endpoint-required' | translate }}
</mat-error> </mat-error>
<mat-error *ngIf="entityForm.get('cloudEndpoint').hasError('maxlength')">
{{ 'edge.cloud-endpoint-max-length' | translate }}
</mat-error>
</mat-form-field> </mat-form-field>
</div> </div>
</fieldset> </fieldset>

View File

@ -73,8 +73,8 @@ export class EdgeComponent extends EntityComponent<EdgeInfo> {
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]],
type: [entity?.type ? entity.type : 'default', [Validators.required, Validators.maxLength(255)]], type: [entity?.type ? entity.type : 'default', [Validators.required, Validators.maxLength(255)]],
label: [entity ? entity.label : '', Validators.maxLength(255)], label: [entity ? entity.label : '', Validators.maxLength(255)],
cloudEndpoint: [null, [Validators.required]], cloudEndpoint: [null, [Validators.required, Validators.maxLength(255)]],
edgeLicenseKey: ['', [Validators.required]], edgeLicenseKey: ['', [Validators.required, Validators.maxLength(30)]],
routingKey: this.fb.control({value: entity ? entity.routingKey : null, disabled: true}), routingKey: this.fb.control({value: entity ? entity.routingKey : null, disabled: true}),
secret: this.fb.control({value: entity ? entity.secret : null, disabled: true}), secret: this.fb.control({value: entity ? entity.secret : null, disabled: true}),
additionalInfo: this.fb.group( additionalInfo: this.fb.group(

View File

@ -21,7 +21,7 @@
<mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px" fxLayoutGap.lt-xl="8px"> <mat-toolbar class="mat-elevation-z1 tb-edit-toolbar mat-hue-3" fxLayoutGap="16px" fxLayoutGap.lt-xl="8px">
<mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title"> <mat-form-field floatLabel="always" hideRequiredMarker class="tb-widget-title">
<mat-label></mat-label> <mat-label></mat-label>
<input [disabled]="isReadOnly" matInput required <input [disabled]="isReadOnly" matInput required maxlength="255"
[(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true" [(ngModel)]="widget.widgetName" (ngModelChange)="isDirty = true"
placeholder="{{ 'widget.title' | translate }}"/> placeholder="{{ 'widget.title' | translate }}"/>
</mat-form-field> </mat-form-field>

View File

@ -158,6 +158,7 @@
"user-lockout-notification-email": "In case user account lockout, send notification to email", "user-lockout-notification-email": "In case user account lockout, send notification to email",
"domain-name": "Domain name", "domain-name": "Domain name",
"domain-name-unique": "Domain name and protocol need to unique.", "domain-name-unique": "Domain name and protocol need to unique.",
"domain-name-max-length": "Domain name should be less than 256",
"error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io", "error-verification-url": "A domain name shouldn't contain symbols '/' and ':'. Example: thingsboard.io",
"oauth2": { "oauth2": {
"access-token-uri": "Access token URI", "access-token-uri": "Access token URI",
@ -174,21 +175,28 @@
"client-authentication-method": "Client authentication method", "client-authentication-method": "Client authentication method",
"client-id": "Client ID", "client-id": "Client ID",
"client-id-required": "Client ID is required.", "client-id-required": "Client ID is required.",
"client-id-max-length": "Client ID should be less than 256",
"client-secret": "Client secret", "client-secret": "Client secret",
"client-secret-required": "Client secret is required.", "client-secret-required": "Client secret is required.",
"client-secret-max-length": "Client secret should be less than 2049",
"custom-setting": "Custom settings", "custom-setting": "Custom settings",
"customer-name-pattern": "Customer name pattern", "customer-name-pattern": "Customer name pattern",
"customer-name-pattern-max-length": "Customer name pattern should be less than 256",
"default-dashboard-name": "Default dashboard name", "default-dashboard-name": "Default dashboard name",
"default-dashboard-name-max-length": "Default dashboard name should be less than 256",
"delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.", "delete-domain-text": "Be careful, after the confirmation a domain and all provider data will be unavailable.",
"delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?", "delete-domain-title": "Are you sure you want to delete settings the domain '{{domainName}}'?",
"delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.", "delete-registration-text": "Be careful, after the confirmation a provider data will be unavailable.",
"delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?", "delete-registration-title": "Are you sure you want to delete the provider '{{name}}'?",
"email-attribute-key": "Email attribute key", "email-attribute-key": "Email attribute key",
"email-attribute-key-required": "Email attribute key is required.", "email-attribute-key-required": "Email attribute key is required.",
"email-attribute-key-max-length": "Email attribute key should be less than 32",
"first-name-attribute-key": "First name attribute key", "first-name-attribute-key": "First name attribute key",
"first-name-attribute-key-max-length": "First name attribute key should be less than 32",
"general": "General", "general": "General",
"jwk-set-uri": "JSON Web Key URI", "jwk-set-uri": "JSON Web Key URI",
"last-name-attribute-key": "Last name attribute key", "last-name-attribute-key": "Last name attribute key",
"last-name-attribute-key-max-length": "Last name attribute key should be less than 32",
"login-button-icon": "Login button icon", "login-button-icon": "Login button icon",
"login-button-label": "Provider label", "login-button-label": "Provider label",
"login-button-label-placeholder": "Login with $(Provider label)", "login-button-label-placeholder": "Login with $(Provider label)",
@ -197,6 +205,7 @@
"mapper": "Mapper", "mapper": "Mapper",
"new-domain": "New domain", "new-domain": "New domain",
"oauth2": "OAuth2", "oauth2": "OAuth2",
"password-max-length": "Password should be less than 256",
"redirect-uri-template": "Redirect URI template", "redirect-uri-template": "Redirect URI template",
"copy-redirect-uri": "Copy redirect URI", "copy-redirect-uri": "Copy redirect URI",
"registration-id": "Registration ID", "registration-id": "Registration ID",
@ -206,14 +215,17 @@
"scope-required": "Scope is required.", "scope-required": "Scope is required.",
"tenant-name-pattern": "Tenant name pattern", "tenant-name-pattern": "Tenant name pattern",
"tenant-name-pattern-required": "Tenant name pattern is required.", "tenant-name-pattern-required": "Tenant name pattern is required.",
"tenant-name-pattern-max-length": "Tenant name pattern ishould be less than 256",
"tenant-name-strategy": "Tenant name strategy", "tenant-name-strategy": "Tenant name strategy",
"type": "Mapper type", "type": "Mapper type",
"uri-pattern-error": "Invalid URI format.", "uri-pattern-error": "Invalid URI format.",
"url": "URL", "url": "URL",
"url-pattern": "Invalid URL format.", "url-pattern": "Invalid URL format.",
"url-required": "URL is required.", "url-required": "URL is required.",
"url-max-length": "URL should be less than 256",
"user-info-uri": "User info URI", "user-info-uri": "User info URI",
"user-info-uri-required": "User info URI is required.", "user-info-uri-required": "User info URI is required.",
"username-max-length": "User name should be less than 256",
"user-name-attribute-name": "User name attribute key", "user-name-attribute-name": "User name attribute key",
"user-name-attribute-name-required": "User name attribute key is required", "user-name-attribute-name-required": "User name attribute key is required",
"protocol": "Protocol", "protocol": "Protocol",
@ -1448,9 +1460,11 @@
"name-required": "Name is required.", "name-required": "Name is required.",
"edge-license-key": "Edge License Key", "edge-license-key": "Edge License Key",
"edge-license-key-required": "Edge License Key is required.", "edge-license-key-required": "Edge License Key is required.",
"edge-license-key-max-length": "Edge License Key should be less than 31",
"edge-license-key-hint": "To obtain your license please navigate to the <a href='https://thingsboard.io/pricing/?active=thingsboard-edge' target='_blank'>pricing page</a> and select the best license option for your case.", "edge-license-key-hint": "To obtain your license please navigate to the <a href='https://thingsboard.io/pricing/?active=thingsboard-edge' target='_blank'>pricing page</a> and select the best license option for your case.",
"cloud-endpoint": "Cloud Endpoint", "cloud-endpoint": "Cloud Endpoint",
"cloud-endpoint-required": "Cloud Endpoint is required.", "cloud-endpoint-required": "Cloud Endpoint is required.",
"cloud-endpoint-max-length": "Cloud Endpoint should be less than 256",
"cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard CE/PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.", "cloud-endpoint-hint": "Edge requires HTTP(s) access to Cloud (ThingsBoard CE/PE) to verify the license key. Please specify Cloud URL that Edge is able to connect to.",
"description": "Description", "description": "Description",
"details": "Details", "details": "Details",