Browse Source

Merge pull request #9337 from thingsboard/feature/widgets-bundle-pagination

Implement widgets bundles / widgets full search.
pull/9343/head
Igor Kulikov 2 years ago
committed by GitHub
parent
commit
d713ee62b9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      application/src/main/data/upgrade/3.6.0/schema_update.sql
  2. 54
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  3. 9
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  4. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  5. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java
  7. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java
  8. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java
  9. 16
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  10. 9
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java
  11. 4
      common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java
  12. 22
      common/data/src/main/java/org/thingsboard/server/common/data/widget/DeprecatedFilter.java
  13. 4
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java
  14. 7
      common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java
  15. 1
      common/edge-api/src/main/proto/edge.proto
  16. 5
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  17. 8
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java
  18. 34
      dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeInfoEntity.java
  19. 55
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java
  20. 49
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java
  21. 134
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java
  22. 31
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java
  23. 49
      dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java
  24. 45
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayType.java
  25. 119
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayTypeDescriptor.java
  26. 86
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArraySqlTypeDescriptor.java
  27. 335
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArrayUtil.java
  28. 64
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/ParameterizedParameterType.java
  29. 41
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayType.java
  30. 31
      dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayTypeDescriptor.java
  31. 16
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java
  32. 32
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java
  33. 4
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java
  34. 16
      dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java
  35. 1
      dao/src/main/resources/sql/schema-entities.sql
  36. 6
      dao/src/main/resources/sql/schema-views-and-functions.sql
  37. 8
      dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java
  38. 67
      dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java
  39. 137
      dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java
  40. 2
      ui-ngx/src/app/core/http/entity.service.ts
  41. 61
      ui-ngx/src/app/core/http/widget.service.ts
  42. 43
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html
  43. 13
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts
  44. 128
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html
  45. 126
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss
  46. 307
      ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts
  47. 53
      ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html
  48. 32
      ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.scss
  49. 103
      ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts
  50. 7
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  51. 250
      ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts
  52. 3
      ui-ngx/src/app/modules/home/models/widget-component.models.ts
  53. 5
      ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html
  54. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts
  55. 4
      ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html
  56. 2
      ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts
  57. 2
      ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts
  58. 8
      ui-ngx/src/app/shared/models/widget.models.ts
  59. 1
      ui-ngx/src/assets/locale/locale.constant-en_US.json

5
application/src/main/data/upgrade/3.6.0/schema_update.sql

@ -14,5 +14,8 @@
-- limitations under the License.
--
ALTER TABLE widget_type
ADD COLUMN IF NOT EXISTS tags text[];
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS tbel_exec varchar(32);
UPDATE api_usage_state SET tbel_exec = js_exec WHERE tbel_exec IS NULL;
UPDATE api_usage_state SET tbel_exec = js_exec WHERE tbel_exec IS NULL;

54
application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java

@ -28,6 +28,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
@ -46,6 +48,8 @@ import org.thingsboard.server.service.entitiy.widgets.type.TbWidgetTypeService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
@ -75,7 +79,11 @@ public class WidgetTypeController extends AutoCommitController {
private static final String WIDGET_TYPE_INFO_DESCRIPTION = "Widget Type Info is a lightweight object that represents Widget Type but does not contain the heavyweight widget descriptor JSON";
private static final String TENANT_ONLY_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether only tenant widget types should be returned";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether search widgets by description not only by name";
private static final String DEPRECATED_FILTER_ALLOWABLE_VALUES = "ALL, ACTUAL, DEPRECATED";
private static final String DEPRECATED_FILTER_PARAM_DESCRIPTION = "Optional string parameter indicating whether to include deprecated widgets";
private static final String UPDATE_EXISTING_BY_FQN_PARAM_DESCRIPTION = "Optional boolean parameter indicating whether to update existing widget type by FQN if present instead of creating new one";
private static final String WIDGET_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the widget type value";
private static final String WIDGET_TYPE_ALLOWABLE_VALUES = "timeseries, latest, control, alarm, static";
@ApiOperation(value = "Get Widget Type Details (getWidgetTypeById)",
notes = "Get the Widget Type Details based on the provided Widget Type Id. " + WIDGET_TYPE_DETAILS_DESCRIPTION + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@ -166,15 +174,22 @@ public class WidgetTypeController extends AutoCommitController {
@ApiParam(value = TENANT_ONLY_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean tenantOnly,
@ApiParam(value = FULL_SEARCH_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean fullSearch) throws ThingsboardException {
@RequestParam(required = false) Boolean fullSearch,
@ApiParam(value = DEPRECATED_FILTER_PARAM_DESCRIPTION, allowableValues = DEPRECATED_FILTER_ALLOWABLE_VALUES)
@RequestParam(required = false) String deprecatedFilter,
@ApiParam(value = WIDGET_TYPE_ARRAY_DESCRIPTION, allowableValues = WIDGET_TYPE_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List<String> widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
boolean fullSearchBool = fullSearch != null && fullSearch;
DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL;
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetTypeService.findSystemWidgetTypesByPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
} else {
if (tenantOnly != null && tenantOnly) {
return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
} else {
return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
return checkNotNull(widgetTypeService.findAllTenantWidgetTypesByTenantIdAndPageLink(getTenantId(), fullSearchBool, widgetTypeDeprecatedFilter, widgetTypes, pageLink));
}
}
}
@ -272,19 +287,40 @@ public class WidgetTypeController extends AutoCommitController {
tenantId = getCurrentUser().getTenantId();
}
WidgetsBundle widgetsBundle = checkNotNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(tenantId, bundleAlias));
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundle.getId()));
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundle.getId(), false, DeprecatedFilter.ALL,
null, new PageLink(1024))).getData();
}
@ApiOperation(value = "Get Widget Type Info objects (getBundleWidgetTypesInfos)",
notes = "Get the Widget Type Info objects based on the provided parameters. " + WIDGET_TYPE_INFO_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId"}, method = RequestMethod.GET)
@RequestMapping(value = "/widgetTypesInfos", params = {"widgetsBundleId", "pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public List<WidgetTypeInfo> getBundleWidgetTypesInfos(
public PageData<WidgetTypeInfo> getBundleWidgetTypesInfos(
@ApiParam(value = "Widget Bundle Id", required = true)
@RequestParam("widgetsBundleId") String strWidgetsBundleId) throws ThingsboardException {
@RequestParam("widgetsBundleId") String strWidgetsBundleId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = WIDGET_TYPE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = WIDGET_TYPE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = FULL_SEARCH_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean fullSearch,
@ApiParam(value = DEPRECATED_FILTER_PARAM_DESCRIPTION, allowableValues = DEPRECATED_FILTER_ALLOWABLE_VALUES)
@RequestParam(required = false) String deprecatedFilter,
@ApiParam(value = WIDGET_TYPE_ARRAY_DESCRIPTION, allowableValues = WIDGET_TYPE_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] widgetTypeList) throws ThingsboardException {
WidgetsBundleId widgetsBundleId = new WidgetsBundleId(toUUID(strWidgetsBundleId));
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundleId));
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
List<String> widgetTypes = widgetTypeList != null ? Arrays.asList(widgetTypeList) : Collections.emptyList();
DeprecatedFilter widgetTypeDeprecatedFilter = StringUtils.isNotEmpty(deprecatedFilter) ? DeprecatedFilter.valueOf(deprecatedFilter) : DeprecatedFilter.ALL;
return checkNotNull(widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(getTenantId(), widgetsBundleId, fullSearch != null && fullSearch,
widgetTypeDeprecatedFilter, widgetTypes, pageLink));
}
@ApiOperation(value = "Get Widget Type (getWidgetTypeByBundleAliasAndTypeAlias) (Deprecated)",

9
application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java

@ -68,6 +68,7 @@ public class WidgetsBundleController extends BaseController {
private final TbWidgetsBundleService tbWidgetsBundleService;
private static final String WIDGET_BUNDLE_DESCRIPTION = "Widget Bundle represents a group(bundle) of widgets. Widgets are grouped into bundle by type or use case. ";
private static final String FULL_SEARCH_PARAM_DESCRIPTION = "Optional boolean parameter indicating extended search of widget bundles by description and by name / description of related widget types";
@ApiOperation(value = "Get Widget Bundle (getWidgetsBundleById)",
notes = "Get the Widget Bundle based on the provided Widget Bundle Id. " + WIDGET_BUNDLE_DESCRIPTION + AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@ -183,13 +184,15 @@ public class WidgetsBundleController extends BaseController {
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@RequestParam(required = false) String sortOrder,
@ApiParam(value = FULL_SEARCH_PARAM_DESCRIPTION)
@RequestParam(required = false) Boolean fullSearch) throws ThingsboardException {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (Authority.SYS_ADMIN.equals(getCurrentUser().getAuthority())) {
return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), pageLink));
return checkNotNull(widgetsBundleService.findSystemWidgetsBundlesByPageLink(getTenantId(), fullSearch != null && fullSearch, pageLink));
} else {
TenantId tenantId = getCurrentUser().getTenantId();
return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink));
return checkNotNull(widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, fullSearch != null && fullSearch, pageLink));
}
}

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -262,7 +262,6 @@ public class ThingsboardInstallService {
databaseEntitiesUpgradeService.upgradeDatabase("3.5.1");
dataUpdateService.updateData("3.5.1");
systemDataLoaderService.updateDefaultNotificationConfigs();
break;
case "3.6.0":
log.info("Upgrading ThingsBoard from version 3.6.0 to 3.6.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.0");

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/WidgetTypeMsgConstructor.java

@ -24,6 +24,8 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.util.Arrays;
@Component
@TbCoreComponent
public class WidgetTypeMsgConstructor {
@ -59,6 +61,9 @@ public class WidgetTypeMsgConstructor {
builder.setDescription(widgetTypeDetails.getDescription());
}
builder.setDeprecated(widgetTypeDetails.isDeprecated());
if (widgetTypeDetails.getTags() != null) {
builder.addAllTags(Arrays.asList(widgetTypeDetails.getTags()));
}
return builder.build();
}

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetTypesEdgeEventFetcher.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@ -31,6 +32,6 @@ public class SystemWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF
@Override
protected PageData<WidgetTypeInfo> findWidgetTypes(TenantId tenantId, PageLink pageLink) {
return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, pageLink);
return widgetTypeService.findSystemWidgetTypesByPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink);
}
}

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/SystemWidgetsBundlesEdgeEventFetcher.java

@ -31,6 +31,6 @@ public class SystemWidgetsBundlesEdgeEventFetcher extends BaseWidgetsBundlesEdge
@Override
protected PageData<WidgetsBundle> findWidgetsBundles(TenantId tenantId, PageLink pageLink) {
return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, pageLink);
return widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
}
}

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/TenantWidgetTypesEdgeEventFetcher.java

@ -19,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@ -30,6 +31,6 @@ public class TenantWidgetTypesEdgeEventFetcher extends BaseWidgetTypesEdgeEventF
}
@Override
protected PageData<WidgetTypeInfo> findWidgetTypes(TenantId tenantId, PageLink pageLink) {
return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, pageLink);
return widgetTypeService.findTenantWidgetTypesByTenantIdAndPageLink(tenantId, false, DeprecatedFilter.ALL, null, pageLink);
}
}

16
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
@ -83,6 +84,8 @@ import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
@ -489,10 +492,15 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
public void deleteSystemWidgetBundle(String bundleAlias) throws Exception {
WidgetsBundle widgetsBundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, bundleAlias);
if (widgetsBundle != null) {
var widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
for (var widgetType : widgetTypes) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
PageData<WidgetTypeInfo> widgetTypes;
var pageLink = new PageLink(1024);
do {
widgetTypes = widgetTypeService.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID, widgetsBundle.getId(), false, DeprecatedFilter.ALL, null, pageLink);
for (var widgetType : widgetTypes.getData()) {
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetType.getId());
}
pageLink.nextPageLink();
} while (widgetTypes.hasNext());
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, widgetsBundle.getId());
}
}

9
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeService.java

@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
@ -39,17 +40,17 @@ public interface WidgetTypeService extends EntityDaoService {
void deleteWidgetType(TenantId tenantId, WidgetTypeId widgetTypeId);
PageData<WidgetTypeInfo> findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
PageData<WidgetTypeInfo> findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
List<WidgetType> findWidgetTypesByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId);
List<WidgetTypeDetails> findWidgetTypesDetailsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId);
List<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId);
PageData<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
List<String> findWidgetFqnsByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId);

4
common/dao-api/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleService.java

@ -34,13 +34,13 @@ public interface WidgetsBundleService extends EntityDaoService {
WidgetsBundle findWidgetsBundleByTenantIdAndAlias(TenantId tenantId, String alias);
PageData<WidgetsBundle> findSystemWidgetsBundlesByPageLink(TenantId tenantId, PageLink pageLink);
PageData<WidgetsBundle> findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
List<WidgetsBundle> findSystemWidgetsBundles(TenantId tenantId);
PageData<WidgetsBundle> findTenantWidgetsBundlesByTenantId(TenantId tenantId, PageLink pageLink);
PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink);
PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink);
List<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(TenantId tenantId);

22
common/data/src/main/java/org/thingsboard/server/common/data/widget/DeprecatedFilter.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2023 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.common.data.widget;
public enum DeprecatedFilter {
ALL,
ACTUAL,
DEPRECATED
}

4
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeDetails.java

@ -38,6 +38,9 @@ public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantI
@Length(fieldName = "description", max = 1024)
@ApiModelProperty(position = 10, value = "Description of the widget")
private String description;
@NoXss
@ApiModelProperty(position = 11, value = "Tags of the widget type")
private String[] tags;
@Getter
@Setter
@ -59,6 +62,7 @@ public class WidgetTypeDetails extends WidgetType implements HasName, HasTenantI
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
this.tags = widgetTypeDetails.getTags();
this.externalId = widgetTypeDetails.getExternalId();
}
}

7
common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetTypeInfo.java

@ -29,7 +29,10 @@ public class WidgetTypeInfo extends BaseWidgetType {
@ApiModelProperty(position = 9, value = "Description of the widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String description;
@NoXss
@ApiModelProperty(position = 10, value = "Type of the widget (timeseries, latest, control, alarm or static)", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@ApiModelProperty(position = 10, value = "Tags of the widget type", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String[] tags;
@NoXss
@ApiModelProperty(position = 11, value = "Type of the widget (timeseries, latest, control, alarm or static)", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String widgetType;
public WidgetTypeInfo() {
@ -48,6 +51,7 @@ public class WidgetTypeInfo extends BaseWidgetType {
super(widgetTypeInfo);
this.image = widgetTypeInfo.getImage();
this.description = widgetTypeInfo.getDescription();
this.tags = widgetTypeInfo.getTags();
this.widgetType = widgetTypeInfo.getWidgetType();
}
@ -55,6 +59,7 @@ public class WidgetTypeInfo extends BaseWidgetType {
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
this.tags = widgetTypeDetails.getTags();
if (widgetTypeDetails.getDescriptor() != null && widgetTypeDetails.getDescriptor().has("type")) {
this.widgetType = widgetTypeDetails.getDescriptor().get("type").asText();
} else {

1
common/edge-api/src/main/proto/edge.proto

@ -377,6 +377,7 @@ message WidgetTypeUpdateMsg {
optional string description = 10;
optional string fqn = 11;
bool deprecated = 12;
repeated string tags = 13;
}
message AdminSettingsUpdateMsg {

5
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -313,10 +313,15 @@ public class ModelConstants {
public static final String WIDGET_TYPE_NAME_PROPERTY = "name";
public static final String WIDGET_TYPE_IMAGE_PROPERTY = "image";
public static final String WIDGET_TYPE_DESCRIPTION_PROPERTY = "description";
public static final String WIDGET_TYPE_TAGS_PROPERTY = "tags";
public static final String WIDGET_TYPE_DESCRIPTOR_PROPERTY = "descriptor";
public static final String WIDGET_TYPE_DEPRECATED_PROPERTY = "deprecated";
public static final String WIDGET_TYPE_WIDGET_TYPE_PROPERTY = "widget_type";
public static final String WIDGET_TYPE_INFO_VIEW_TABLE_NAME = "widget_type_info_view";
/**
* Widgets bundle widget constants.
*/

8
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeDetailsEntity.java

@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import org.thingsboard.server.dao.util.mapping.StringArrayType;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -36,6 +37,7 @@ import java.util.UUID;
@EqualsAndHashCode(callSuper = true)
@Entity
@TypeDef(name = "json", typeClass = JsonStringType.class)
@TypeDef(name = "string-array", typeClass = StringArrayType.class)
@Table(name = ModelConstants.WIDGET_TYPE_TABLE_NAME)
public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetTypeDetails> {
@ -45,6 +47,10 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetType
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTION_PROPERTY)
private String description;
@Type(type="string-array")
@Column(name = ModelConstants.WIDGET_TYPE_TAGS_PROPERTY, columnDefinition = "text[]")
private String[] tags;
@Type(type="json")
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTOR_PROPERTY)
private JsonNode descriptor;
@ -60,6 +66,7 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetType
super(widgetTypeDetails);
this.image = widgetTypeDetails.getImage();
this.description = widgetTypeDetails.getDescription();
this.tags = widgetTypeDetails.getTags();
this.descriptor = widgetTypeDetails.getDescriptor();
if (widgetTypeDetails.getExternalId() != null) {
this.externalId = widgetTypeDetails.getExternalId().getId();
@ -72,6 +79,7 @@ public class WidgetTypeDetailsEntity extends AbstractWidgetTypeEntity<WidgetType
WidgetTypeDetails widgetTypeDetails = new WidgetTypeDetails(baseWidgetType);
widgetTypeDetails.setImage(image);
widgetTypeDetails.setDescription(description);
widgetTypeDetails.setTags(tags);
widgetTypeDetails.setDescriptor(descriptor);
if (externalId != null) {
widgetTypeDetails.setExternalId(new WidgetTypeId(externalId));

34
dao/src/main/java/org/thingsboard/server/dao/model/sql/WidgetTypeInfoEntity.java

@ -17,38 +17,50 @@ package org.thingsboard.server.dao.model.sql;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.StringArrayType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Data
@EqualsAndHashCode(callSuper = true)
@Entity
@Immutable
@TypeDef(name = "string-array", typeClass = StringArrayType.class)
@Table(name = ModelConstants.WIDGET_TYPE_INFO_VIEW_TABLE_NAME)
public final class WidgetTypeInfoEntity extends AbstractWidgetTypeEntity<WidgetTypeInfo> {
@Column(name = ModelConstants.WIDGET_TYPE_IMAGE_PROPERTY)
private String image;
@Column(name = ModelConstants.WIDGET_TYPE_DESCRIPTION_PROPERTY)
private String description;
@Type(type="string-array")
@Column(name = ModelConstants.WIDGET_TYPE_TAGS_PROPERTY, columnDefinition = "text[]")
private String[] tags;
@Column(name = ModelConstants.WIDGET_TYPE_WIDGET_TYPE_PROPERTY)
private String widgetType;
public WidgetTypeInfoEntity() {
super();
}
public WidgetTypeInfoEntity(WidgetTypeDetailsEntity widgetTypeDetailsEntity) {
super(widgetTypeDetailsEntity);
this.image = widgetTypeDetailsEntity.getImage();
this.description = widgetTypeDetailsEntity.getDescription();
if (widgetTypeDetailsEntity.getDescriptor() != null && widgetTypeDetailsEntity.getDescriptor().has("type")) {
this.widgetType = widgetTypeDetailsEntity.getDescriptor().get("type").asText();
} else {
this.widgetType = "";
}
}
@Override
public WidgetTypeInfo toData() {
BaseWidgetType baseWidgetType = super.toBaseWidgetType();
WidgetTypeInfo widgetTypeInfo = new WidgetTypeInfo(baseWidgetType);
widgetTypeInfo.setImage(image);
widgetTypeInfo.setDescription(description);
widgetTypeInfo.setTags(tags);
widgetTypeInfo.setWidgetType(widgetType);
return widgetTypeInfo;
}

55
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
@ -35,6 +36,7 @@ import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.util.SqlDao;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
@ -54,6 +56,9 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
@Autowired
private WidgetTypeRepository widgetTypeRepository;
@Autowired
private WidgetTypeInfoRepository widgetTypeInfoRepository;
@Autowired
private WidgetsBundleWidgetRepository widgetsBundleWidgetRepository;
@ -78,36 +83,57 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
}
@Override
public PageData<WidgetTypeInfo> findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
public PageData<WidgetTypeInfo> findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
return DaoUtil.toPageData(
widgetTypeRepository
widgetTypeInfoRepository
.findSystemWidgetTypes(
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
fullSearch,
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
widgetTypes == null ? Collections.emptyList() : widgetTypes,
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) {
public PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
return DaoUtil.toPageData(
widgetTypeRepository
widgetTypeInfoRepository
.findAllTenantWidgetTypesByTenantId(
tenantId,
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
fullSearch,
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
widgetTypes == null ? Collections.emptyList() : widgetTypes,
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) {
public PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
return DaoUtil.toPageData(
widgetTypeRepository
widgetTypeInfoRepository
.findTenantWidgetTypesByTenantId(
tenantId,
Objects.toString(pageLink.getTextSearch(), ""),
fullSearch,
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
widgetTypes == null ? Collections.emptyList() : widgetTypes,
DaoUtil.toPageable(pageLink)));
}
@ -122,8 +148,21 @@ public class JpaWidgetTypeDao extends JpaAbstractDao<WidgetTypeDetailsEntity, Wi
}
@Override
public List<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId) {
return DaoUtil.convertDataList(widgetTypeRepository.findWidgetTypesInfosByWidgetsBundleId(widgetsBundleId));
public PageData<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
boolean deprecatedFilterEnabled = !DeprecatedFilter.ALL.equals(deprecatedFilter);
boolean deprecatedFilterBool = DeprecatedFilter.DEPRECATED.equals(deprecatedFilter);
boolean widgetTypesEmpty = widgetTypes == null || widgetTypes.isEmpty();
return DaoUtil.toPageData(
widgetTypeInfoRepository
.findWidgetTypesInfosByWidgetsBundleId(
widgetsBundleId,
Objects.toString(pageLink.getTextSearch(), ""),
fullSearch,
deprecatedFilterEnabled,
deprecatedFilterBool,
widgetTypesEmpty,
widgetTypes == null ? Collections.emptyList() : widgetTypes,
DaoUtil.toPageable(pageLink)));
}
@Override

49
dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDao.java

@ -62,13 +62,22 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao<WidgetsBundleEntity, Wid
}
@Override
public PageData<WidgetsBundle> findSystemWidgetsBundles(TenantId tenantId, PageLink pageLink) {
return DaoUtil.toPageData(
widgetsBundleRepository
.findSystemWidgetsBundles(
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
public PageData<WidgetsBundle> findSystemWidgetsBundles(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
if (fullSearch) {
return DaoUtil.toPageData(
widgetsBundleRepository
.findSystemWidgetsBundlesFullSearch(
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
} else {
return DaoUtil.toPageData(
widgetsBundleRepository
.findSystemWidgetsBundles(
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
}
@Override
@ -82,14 +91,24 @@ public class JpaWidgetsBundleDao extends JpaAbstractDao<WidgetsBundleEntity, Wid
}
@Override
public PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink) {
return DaoUtil.toPageData(
widgetsBundleRepository
.findAllTenantWidgetsBundlesByTenantId(
tenantId,
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
public PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink) {
if (fullSearch) {
return DaoUtil.toPageData(
widgetsBundleRepository
.findAllTenantWidgetsBundlesByTenantIdFullSearch(
tenantId,
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
} else {
return DaoUtil.toPageData(
widgetsBundleRepository
.findAllTenantWidgetsBundlesByTenantId(
tenantId,
NULL_UUID,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
}
@Override

134
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java

@ -0,0 +1,134 @@
/**
* Copyright © 2016-2023 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.widget;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity;
import java.util.List;
import java.util.UUID;
public interface WidgetTypeInfoRepository extends JpaRepository<WidgetTypeInfoEntity, UUID> {
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id = :systemTenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))",
// "OR to_tsvector(lower(array_to_string(wti.tags, ' '))) @@ to_tsquery(lower(:searchText)))))",
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id = :systemTenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findSystemWidgetTypes(@Param("systemTenantId") UUID systemTenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id IN (:tenantId, :nullTenantId) " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))",
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id IN (:tenantId, :nullTenantId) " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findAllTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId,
@Param("nullTenantId") UUID nullTenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti WHERE wti.tenant_id = :tenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))",
countQuery = "SELECT count(*) FROM widget_type_info_view wti WHERE wti.tenant_id = :tenantId " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
@Query("SELECT wti FROM WidgetTypeInfoEntity wti, WidgetsBundleWidgetEntity wbw " +
"WHERE wbw.widgetsBundleId = :widgetsBundleId " +
"AND wbw.widgetTypeId = wti.id ORDER BY wbw.widgetTypeOrder")
List<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId);
@Query(nativeQuery = true,
value = "SELECT * FROM widget_type_info_view wti, widgets_bundle_widget wbw " +
"WHERE wbw.widgets_bundle_id = :widgetsBundleId " +
"AND wbw.widget_type_id = wti.id " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' ')))) " +
"ORDER BY wbw.widget_type_order",
countQuery = "SELECT count(*) FROM widget_type_info_view wti, widgets_bundle_widget wbw " +
"WHERE wbw.widgets_bundle_id = :widgetsBundleId " +
"AND wbw.widget_type_id = wti.id " +
"AND ((:deprecatedFilterEnabled) IS FALSE OR wti.deprecated = :deprecatedFilter) " +
"AND ((:widgetTypesEmpty) IS TRUE OR wti.widget_type IN (:widgetTypes)) " +
"AND (wti.name ILIKE CONCAT('%', :searchText, '%') " +
"OR ((:fullSearch) IS TRUE AND (wti.description ILIKE CONCAT('%', :searchText, '%') " +
"OR lower(wti.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:searchText), ' '))))"
)
Page<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
@Param("deprecatedFilterEnabled") boolean deprecatedFilterEnabled,
@Param("deprecatedFilter") boolean deprecatedFilter,
@Param("widgetTypesEmpty") boolean widgetTypesEmpty,
@Param("widgetTypes") List<String> widgetTypes,
Pageable pageable);
}

31
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeRepository.java

@ -24,8 +24,6 @@ import org.thingsboard.server.dao.ExportableEntityRepository;
import org.thingsboard.server.dao.model.sql.WidgetTypeDetailsEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeIdFqnEntity;
import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity;
import org.thingsboard.server.dao.model.sql.WidgetsBundleEntity;
import java.util.List;
import java.util.UUID;
@ -37,31 +35,6 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
boolean existsByTenantIdAndId(UUID tenantId, UUID id);
@Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId = :systemTenantId " +
"AND (LOWER(wtd.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
"OR ((:fullSearch) IS TRUE AND LOWER(wtd.description) LIKE LOWER(CONCAT('%', :searchText, '%'))))")
Page<WidgetTypeInfoEntity> findSystemWidgetTypes(@Param("systemTenantId") UUID systemTenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId IN (:tenantId, :nullTenantId) " +
"AND (LOWER(wtd.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
"OR ((:fullSearch) IS TRUE AND LOWER(wtd.description) LIKE LOWER(CONCAT('%', :searchText, '%'))))")
Page<WidgetTypeInfoEntity> findAllTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId,
@Param("nullTenantId") UUID nullTenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
Pageable pageable);
@Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId = :tenantId " +
"AND (LOWER(wtd.name) LIKE LOWER(CONCAT('%', :searchText, '%')) " +
"OR ((:fullSearch) IS TRUE AND LOWER(wtd.description) LIKE LOWER(CONCAT('%', :searchText, '%'))))")
Page<WidgetTypeInfoEntity> findTenantWidgetTypesByTenantId(@Param("tenantId") UUID tenantId,
@Param("searchText") String searchText,
@Param("fullSearch") boolean fullSearch,
Pageable pageable);
@Query("SELECT wtd FROM WidgetTypeDetailsEntity wtd WHERE wtd.tenantId = :tenantId " +
"AND LOWER(wtd.name) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<WidgetTypeDetailsEntity> findTenantWidgetTypeDetailsByTenantId(@Param("tenantId") UUID tenantId,
@ -78,10 +51,6 @@ public interface WidgetTypeRepository extends JpaRepository<WidgetTypeDetailsEnt
"AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder")
List<WidgetTypeDetailsEntity> findWidgetTypesDetailsByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId);
@Query("SELECT new org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity(wtd) FROM WidgetTypeDetailsEntity wtd, WidgetsBundleWidgetEntity wbw " +
"WHERE wbw.widgetsBundleId = :widgetsBundleId " +
"AND wbw.widgetTypeId = wtd.id ORDER BY wbw.widgetTypeOrder")
List<WidgetTypeInfoEntity> findWidgetTypesInfosByWidgetsBundleId(@Param("widgetsBundleId") UUID widgetsBundleId);
@Query("SELECT wtd.fqn FROM WidgetTypeDetailsEntity wtd, WidgetsBundleWidgetEntity wbw " +
"WHERE wbw.widgetsBundleId = :widgetsBundleId " +

49
dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetsBundleRepository.java

@ -33,11 +33,33 @@ public interface WidgetsBundleRepository extends JpaRepository<WidgetsBundleEnti
WidgetsBundleEntity findWidgetsBundleByTenantIdAndAlias(UUID tenantId, String alias);
@Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :systemTenantId " +
"AND LOWER(wb.title) LIKE LOWER(CONCAT('%', :searchText, '%'))")
"AND LOWER(wb.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<WidgetsBundleEntity> findSystemWidgetsBundles(@Param("systemTenantId") UUID systemTenantId,
@Param("searchText") String searchText,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query(nativeQuery = true,
value = "SELECT * FROM widgets_bundle wb WHERE wb.tenant_id = :systemTenantId " +
"AND (wb.title ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.id in (SELECT wbw.widgets_bundle_id FROM widgets_bundle_widget wbw, widget_type wtd " +
"WHERE wtd.id = wbw.widget_type_id " +
"AND (wtd.name ILIKE CONCAT('%', :textSearch, '%') " +
"OR wtd.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR lower(wtd.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:textSearch), ' '))))",
countQuery = "SELECT count(*) FROM widgets_bundle wb WHERE wb.tenant_id = :systemTenantId " +
"AND (wb.title ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.id in (SELECT wbw.widgets_bundle_id FROM widgets_bundle_widget wbw, widget_type wtd " +
"WHERE wtd.id = wbw.widget_type_id " +
"AND (wtd.name ILIKE CONCAT('%', :textSearch, '%') " +
"OR wtd.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR lower(wtd.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:textSearch), ' '))))"
)
Page<WidgetsBundleEntity> findSystemWidgetsBundlesFullSearch(@Param("systemTenantId") UUID systemTenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
@Query("SELECT wb FROM WidgetsBundleEntity wb WHERE wb.tenantId = :tenantId " +
"AND LOWER(wb.title) LIKE LOWER(CONCAT('%', :textSearch, '%'))")
Page<WidgetsBundleEntity> findTenantWidgetsBundlesByTenantId(@Param("tenantId") UUID tenantId,
@ -51,6 +73,29 @@ public interface WidgetsBundleRepository extends JpaRepository<WidgetsBundleEnti
@Param("textSearch") String textSearch,
Pageable pageable);
@Query(nativeQuery = true,
value = "SELECT * FROM widgets_bundle wb WHERE wb.tenant_id IN (:tenantId, :nullTenantId) " +
"AND (wb.title ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.id in (SELECT wbw.widgets_bundle_id FROM widgets_bundle_widget wbw, widget_type wtd " +
"WHERE wtd.id = wbw.widget_type_id " +
"AND (wtd.name ILIKE CONCAT('%', :textSearch, '%') " +
"OR wtd.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR lower(wtd.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:textSearch), ' '))))",
countQuery = "SELECT count(*) FROM widgets_bundle wb WHERE wb.tenant_id IN (:tenantId, :nullTenantId) " +
"AND (wb.title ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR wb.id in (SELECT wbw.widgets_bundle_id FROM widgets_bundle_widget wbw, widget_type wtd " +
"WHERE wtd.id = wbw.widget_type_id " +
"AND (wtd.name ILIKE CONCAT('%', :textSearch, '%') " +
"OR wtd.description ILIKE CONCAT('%', :textSearch, '%') " +
"OR lower(wtd.tags\\:\\:text)\\:\\:text[] && string_to_array(lower(:textSearch), ' '))))"
)
Page<WidgetsBundleEntity> findAllTenantWidgetsBundlesByTenantIdFullSearch(@Param("tenantId") UUID tenantId,
@Param("nullTenantId") UUID nullTenantId,
@Param("textSearch") String textSearch,
Pageable pageable);
WidgetsBundleEntity findFirstByTenantIdAndTitle(UUID tenantId, String title);
@Query("SELECT externalId FROM WidgetsBundleEntity WHERE id = :id")

45
dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayType.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2023 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.util.mapping;
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
import org.hibernate.usertype.DynamicParameterizedType;
import java.util.Properties;
public abstract class AbstractArrayType<T>
extends AbstractSingleColumnStandardBasicType<T>
implements DynamicParameterizedType {
public static final String SQL_ARRAY_TYPE = "sql_array_type";
public AbstractArrayType(AbstractArrayTypeDescriptor<T> arrayTypeDescriptor) {
super(
ArraySqlTypeDescriptor.INSTANCE,
arrayTypeDescriptor
);
}
@Override
protected boolean registerUnderJavaType() {
return true;
}
@Override
public void setParameterValues(Properties parameters) {
((AbstractArrayTypeDescriptor) getJavaTypeDescriptor()).setParameterValues(parameters);
}
}

119
dao/src/main/java/org/thingsboard/server/dao/util/mapping/AbstractArrayTypeDescriptor.java

@ -0,0 +1,119 @@
/**
* Copyright © 2016-2023 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.util.mapping;
import org.hibernate.HibernateException;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor;
import org.hibernate.type.descriptor.java.MutabilityPlan;
import org.hibernate.type.descriptor.java.MutableMutabilityPlan;
import org.hibernate.usertype.DynamicParameterizedType;
import java.sql.Array;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Properties;
import static org.thingsboard.server.dao.util.mapping.AbstractArrayType.SQL_ARRAY_TYPE;
public abstract class AbstractArrayTypeDescriptor<T>
extends AbstractTypeDescriptor<T> implements DynamicParameterizedType {
private Class<T> arrayObjectClass;
private String sqlArrayType;
public AbstractArrayTypeDescriptor(Class<T> arrayObjectClass) {
this(arrayObjectClass, (MutabilityPlan<T>) new MutableMutabilityPlan<Object>() {
@Override
protected T deepCopyNotNull(Object value) {
return ArrayUtil.deepCopy(value);
}
});
}
protected AbstractArrayTypeDescriptor(Class<T> arrayObjectClass, MutabilityPlan<T> mutableMutabilityPlan) {
super(arrayObjectClass, mutableMutabilityPlan);
this.arrayObjectClass = arrayObjectClass;
}
public Class<T> getArrayObjectClass() {
return arrayObjectClass;
}
public void setArrayObjectClass(Class<T> arrayObjectClass) {
this.arrayObjectClass = arrayObjectClass;
}
@Override
public void setParameterValues(Properties parameters) {
if (parameters.containsKey(PARAMETER_TYPE)) {
arrayObjectClass = ((ParameterType) parameters.get(PARAMETER_TYPE)).getReturnedClass();
}
sqlArrayType = parameters.getProperty(SQL_ARRAY_TYPE);
}
@Override
public boolean areEqual(T one, T another) {
if (one == another) {
return true;
}
if (one == null || another == null) {
return false;
}
return ArrayUtil.isEquals(one, another);
}
@Override
public String toString(T value) {
return Arrays.deepToString(ArrayUtil.wrapArray(value));
}
@Override
public T fromString(String string) {
return ArrayUtil.fromString(string, arrayObjectClass);
}
@Override
public String extractLoggableRepresentation(T value) {
return (value == null) ? "null" : toString(value);
}
@SuppressWarnings({"unchecked"})
@Override
public <X> X unwrap(T value, Class<X> type, WrapperOptions options) {
return (X) ArrayUtil.wrapArray(value);
}
@Override
public <X> T wrap(X value, WrapperOptions options) {
if (value instanceof Array) {
Array array = (Array) value;
try {
return ArrayUtil.unwrapArray((Object[]) array.getArray(), arrayObjectClass);
} catch (SQLException e) {
throw new HibernateException(
new IllegalArgumentException(e)
);
}
}
return (T) value;
}
protected String getSqlArrayType() {
return sqlArrayType;
}
}

86
dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArraySqlTypeDescriptor.java

@ -0,0 +1,86 @@
/**
* Copyright © 2016-2023 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.util.mapping;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
import org.hibernate.type.descriptor.sql.BasicBinder;
import org.hibernate.type.descriptor.sql.BasicExtractor;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
public class ArraySqlTypeDescriptor implements SqlTypeDescriptor {
public static final ArraySqlTypeDescriptor INSTANCE = new ArraySqlTypeDescriptor();
@Override
public int getSqlType() {
return Types.ARRAY;
}
@Override
public boolean canBeRemapped() {
return true;
}
@Override
public <X> ValueBinder<X> getBinder(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>(javaTypeDescriptor, this) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
AbstractArrayTypeDescriptor<Object> abstractArrayTypeDescriptor = (AbstractArrayTypeDescriptor<Object>) javaTypeDescriptor;
st.setArray(index, st.getConnection().createArrayOf(
abstractArrayTypeDescriptor.getSqlArrayType(),
abstractArrayTypeDescriptor.unwrap(value, Object[].class, options)
));
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
throw new UnsupportedOperationException("Binding by name is not supported!");
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(final JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>(javaTypeDescriptor, this) {
@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(rs.getArray(name), options);
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(statement.getArray(index), options);
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(statement.getArray(name), options);
}
};
}
}

335
dao/src/main/java/org/thingsboard/server/dao/util/mapping/ArrayUtil.java

@ -0,0 +1,335 @@
/**
* Copyright © 2016-2023 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.util.mapping;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class ArrayUtil {
public static <T> T deepCopy(Object originalArray) {
Class arrayClass = originalArray.getClass();
if (boolean[].class.equals(arrayClass)) {
boolean[] array = (boolean[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (byte[].class.equals(arrayClass)) {
byte[] array = (byte[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (short[].class.equals(arrayClass)) {
short[] array = (short[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (int[].class.equals(arrayClass)) {
int[] array = (int[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (long[].class.equals(arrayClass)) {
long[] array = (long[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (float[].class.equals(arrayClass)) {
float[] array = (float[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (double[].class.equals(arrayClass)) {
double[] array = (double[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else if (char[].class.equals(arrayClass)) {
char[] array = (char[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
} else {
Object[] array = (Object[]) originalArray;
return (T) Arrays.copyOf(array, array.length);
}
}
public static Object[] wrapArray(Object originalArray) {
Class arrayClass = originalArray.getClass();
if (boolean[].class.equals(arrayClass)) {
boolean[] fromArray = (boolean[]) originalArray;
Boolean[] array = new Boolean[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (byte[].class.equals(arrayClass)) {
byte[] fromArray = (byte[]) originalArray;
Byte[] array = new Byte[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (short[].class.equals(arrayClass)) {
short[] fromArray = (short[]) originalArray;
Short[] array = new Short[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (int[].class.equals(arrayClass)) {
int[] fromArray = (int[]) originalArray;
Integer[] array = new Integer[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (long[].class.equals(arrayClass)) {
long[] fromArray = (long[]) originalArray;
Long[] array = new Long[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (float[].class.equals(arrayClass)) {
float[] fromArray = (float[]) originalArray;
Float[] array = new Float[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (double[].class.equals(arrayClass)) {
double[] fromArray = (double[]) originalArray;
Double[] array = new Double[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (char[].class.equals(arrayClass)) {
char[] fromArray = (char[]) originalArray;
Character[] array = new Character[fromArray.length];
for (int i = 0; i < fromArray.length; i++) {
array[i] = fromArray[i];
}
return array;
} else if (originalArray instanceof Collection) {
return ((Collection) originalArray).toArray();
} else {
return (Object[]) originalArray;
}
}
public static <T> T unwrapArray(Object[] originalArray, Class<T> arrayClass) {
if (boolean[].class.equals(arrayClass)) {
boolean[] array = new boolean[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Boolean) originalArray[i] : Boolean.FALSE;
}
return (T) array;
} else if (byte[].class.equals(arrayClass)) {
byte[] array = new byte[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Byte) originalArray[i] : 0;
}
return (T) array;
} else if (short[].class.equals(arrayClass)) {
short[] array = new short[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Short) originalArray[i] : 0;
}
return (T) array;
} else if (int[].class.equals(arrayClass)) {
int[] array = new int[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Integer) originalArray[i] : 0;
}
return (T) array;
} else if (long[].class.equals(arrayClass)) {
long[] array = new long[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Long) originalArray[i] : 0L;
}
return (T) array;
} else if (float[].class.equals(arrayClass)) {
float[] array = new float[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? ((Number) originalArray[i]).floatValue() : 0f;
}
return (T) array;
} else if (double[].class.equals(arrayClass)) {
double[] array = new double[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Double) originalArray[i] : 0d;
}
return (T) array;
} else if (char[].class.equals(arrayClass)) {
char[] array = new char[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
array[i] = originalArray[i] != null ? (Character) originalArray[i] : 0;
}
return (T) array;
} else if (Enum[].class.isAssignableFrom(arrayClass)) {
T array = arrayClass.cast(Array.newInstance(arrayClass.getComponentType(), originalArray.length));
for (int i = 0; i < originalArray.length; i++) {
Object objectValue = originalArray[i];
if (objectValue != null) {
String stringValue = (objectValue instanceof String) ? (String) objectValue : String.valueOf(objectValue);
objectValue = Enum.valueOf((Class) arrayClass.getComponentType(), stringValue);
}
Array.set(array, i, objectValue);
}
return array;
} else if (java.time.LocalDate[].class.equals(arrayClass) && java.sql.Date[].class.equals(originalArray.getClass())) {
// special case because conversion is neither with ctor nor valueOf
Object[] array = (Object[]) Array.newInstance(java.time.LocalDate.class, originalArray.length);
for (int i = 0; i < array.length; ++i) {
array[i] = originalArray[i] != null ? ((java.sql.Date) originalArray[i]).toLocalDate() : null;
}
return (T) array;
} else if (java.time.LocalDateTime[].class.equals(arrayClass) && java.sql.Timestamp[].class.equals(originalArray.getClass())) {
// special case because conversion is neither with ctor nor valueOf
Object[] array = (Object[]) Array.newInstance(java.time.LocalDateTime.class, originalArray.length);
for (int i = 0; i < array.length; ++i) {
array[i] = originalArray[i] != null ? ((java.sql.Timestamp) originalArray[i]).toLocalDateTime() : null;
}
return (T) array;
} else if(arrayClass.getComponentType() != null && arrayClass.getComponentType().isArray()) {
int arrayLength = originalArray.length;
Object[] array = (Object[]) Array.newInstance(arrayClass.getComponentType(), arrayLength);
if (arrayLength > 0) {
for (int i = 0; i < originalArray.length; i++) {
array[i] = unwrapArray((Object[]) originalArray[i], arrayClass.getComponentType());
}
}
return (T) array;
} else {
if (arrayClass.isInstance(originalArray)) {
return (T) originalArray;
} else {
return (T) Arrays.copyOf(originalArray, originalArray.length, (Class) arrayClass);
}
}
}
public static <T> T fromString(String string, Class<T> arrayClass) {
String stringArray = string.replaceAll("[\\[\\]]", "");
String[] tokens = stringArray.split(",");
int length = tokens.length;
if (boolean[].class.equals(arrayClass)) {
boolean[] array = new boolean[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Boolean.valueOf(tokens[i]);
}
return (T) array;
} else if (byte[].class.equals(arrayClass)) {
byte[] array = new byte[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Byte.valueOf(tokens[i]);
}
return (T) array;
} else if (short[].class.equals(arrayClass)) {
short[] array = new short[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Short.valueOf(tokens[i]);
}
return (T) array;
} else if (int[].class.equals(arrayClass)) {
int[] array = new int[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Integer.valueOf(tokens[i]);
}
return (T) array;
} else if (long[].class.equals(arrayClass)) {
long[] array = new long[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Long.valueOf(tokens[i]);
}
return (T) array;
} else if (float[].class.equals(arrayClass)) {
float[] array = new float[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Float.valueOf(tokens[i]);
}
return (T) array;
} else if (double[].class.equals(arrayClass)) {
double[] array = new double[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = Double.valueOf(tokens[i]);
}
return (T) array;
} else if (char[].class.equals(arrayClass)) {
char[] array = new char[length];
for (int i = 0; i < tokens.length; i++) {
array[i] = tokens[i].length() > 0 ? tokens[i].charAt(0) : Character.MIN_VALUE;
}
return (T) array;
} else {
return (T) tokens;
}
}
public static boolean isEquals(Object firstArray, Object secondArray) {
if (firstArray.getClass() != secondArray.getClass()) {
return false;
}
Class arrayClass = firstArray.getClass();
if (boolean[].class.equals(arrayClass)) {
return Arrays.equals((boolean[]) firstArray, (boolean[]) secondArray);
} else if (byte[].class.equals(arrayClass)) {
return Arrays.equals((byte[]) firstArray, (byte[]) secondArray);
} else if (short[].class.equals(arrayClass)) {
return Arrays.equals((short[]) firstArray, (short[]) secondArray);
} else if (int[].class.equals(arrayClass)) {
return Arrays.equals((int[]) firstArray, (int[]) secondArray);
} else if (long[].class.equals(arrayClass)) {
return Arrays.equals((long[]) firstArray, (long[]) secondArray);
} else if (float[].class.equals(arrayClass)) {
return Arrays.equals((float[]) firstArray, (float[]) secondArray);
} else if (double[].class.equals(arrayClass)) {
return Arrays.equals((double[]) firstArray, (double[]) secondArray);
} else if (char[].class.equals(arrayClass)) {
return Arrays.equals((char[]) firstArray, (char[]) secondArray);
} else {
return Arrays.equals((Object[]) firstArray, (Object[]) secondArray);
}
}
public static <T> Class<T[]> toArrayClass(Class<T> arrayElementClass) {
if (boolean.class.equals(arrayElementClass)) {
return (Class) boolean[].class;
} else if (byte.class.equals(arrayElementClass)) {
return (Class) byte[].class;
} else if (short.class.equals(arrayElementClass)) {
return (Class) short[].class;
} else if (int.class.equals(arrayElementClass)) {
return (Class) int[].class;
} else if (long.class.equals(arrayElementClass)) {
return (Class) long[].class;
} else if (float.class.equals(arrayElementClass)) {
return (Class) float[].class;
} else if (double[].class.equals(arrayElementClass)) {
return (Class) double[].class;
} else if (char[].class.equals(arrayElementClass)) {
return (Class) char[].class;
} else {
Object array = Array.newInstance(arrayElementClass, 0);
return (Class<T[]>) array.getClass();
}
}
public static <T> List<T> asList(T[] array) {
List<T> list = new ArrayList<T>(array.length);
for (int i = 0; i < array.length; i++) {
list.add(i, array[i]);
}
return list;
}
}

64
dao/src/main/java/org/thingsboard/server/dao/util/mapping/ParameterizedParameterType.java

@ -0,0 +1,64 @@
/**
* Copyright © 2016-2023 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.util.mapping;
import org.hibernate.usertype.DynamicParameterizedType;
import java.lang.annotation.Annotation;
public class ParameterizedParameterType implements DynamicParameterizedType.ParameterType {
private final Class<?> clasz;
public ParameterizedParameterType(Class<?> clasz) {
this.clasz = clasz;
}
@Override
public Class getReturnedClass() {
return clasz;
}
@Override
public Annotation[] getAnnotationsMethod() {
return new Annotation[0];
}
@Override
public String getCatalog() {
throw new UnsupportedOperationException();
}
@Override
public String getSchema() {
throw new UnsupportedOperationException();
}
@Override
public String getTable() {
throw new UnsupportedOperationException();
}
@Override
public boolean isPrimaryKey() {
throw new UnsupportedOperationException();
}
@Override
public String[] getColumns() {
throw new UnsupportedOperationException();
}
}

41
dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayType.java

@ -0,0 +1,41 @@
/**
* Copyright © 2016-2023 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.util.mapping;
import org.hibernate.usertype.DynamicParameterizedType;
import java.util.Properties;
public class StringArrayType extends AbstractArrayType<String[]> {
public static final StringArrayType INSTANCE = new StringArrayType();
public StringArrayType() {
super(
new StringArrayTypeDescriptor()
);
}
public StringArrayType(Class arrayClass) {
this();
Properties parameters = new Properties();
parameters.put(DynamicParameterizedType.PARAMETER_TYPE, new ParameterizedParameterType(arrayClass));
setParameterValues(parameters);
}
public String getName() {
return "string-array";
}
}

31
dao/src/main/java/org/thingsboard/server/dao/util/mapping/StringArrayTypeDescriptor.java

@ -0,0 +1,31 @@
/**
* Copyright © 2016-2023 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.util.mapping;
public class StringArrayTypeDescriptor
extends AbstractArrayTypeDescriptor<String[]> {
public StringArrayTypeDescriptor() {
super(String[].class);
}
@Override
protected String getSqlArrayType() {
String sqlArrayType = super.getSqlArrayType();
return sqlArrayType != null ? sqlArrayType : "text";
}
}

16
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeDao.java

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
@ -53,11 +54,11 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails>, ExportableEntityD
boolean existsByTenantIdAndId(TenantId tenantId, UUID widgetTypeId);
PageData<WidgetTypeInfo> findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, PageLink pageLink);
PageData<WidgetTypeInfo> findSystemWidgetTypes(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink);
PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink);
PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantId(UUID tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
/**
* Find widget types by widgetsBundleId.
@ -77,14 +78,7 @@ public interface WidgetTypeDao extends Dao<WidgetTypeDetails>, ExportableEntityD
*/
List<WidgetTypeDetails> findWidgetTypesDetailsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId);
/**
* Find widget types infos by widgetsBundleId.
*
* @param tenantId the tenantId
* @param widgetsBundleId the widgets bundle id
* @return the list of widget types infos objects
*/
List<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId);
PageData<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink);
List<String> findWidgetFqnsByWidgetsBundleId(UUID tenantId, UUID widgetsBundleId);

32
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetTypeServiceImpl.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
@ -110,26 +111,28 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
}
@Override
public PageData<WidgetTypeInfo> findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}] pageLink [{}]", fullSearch, pageLink);
public PageData<WidgetTypeInfo> findSystemWidgetTypesByPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
log.trace("Executing findSystemWidgetTypesByPageLink, fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]", fullSearch, deprecatedFilter, widgetTypes, pageLink);
Validator.validatePageLink(pageLink);
return widgetTypeDao.findSystemWidgetTypes(tenantId, fullSearch, pageLink);
return widgetTypeDao.findSystemWidgetTypes(tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
}
@Override
public PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink);
public PageData<WidgetTypeInfo> findAllTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
log.trace("Executing findAllTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]",
tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validatePageLink(pageLink);
return widgetTypeDao.findAllTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, pageLink);
return widgetTypeDao.findAllTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink);
}
@Override
public PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink);
public PageData<WidgetTypeInfo> findTenantWidgetTypesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
log.trace("Executing findTenantWidgetTypesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]",
tenantId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validatePageLink(pageLink);
return widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, pageLink);
return widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink);
}
@Override
@ -150,11 +153,14 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
}
@Override
public List<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId) {
log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}]", tenantId, widgetsBundleId);
public PageData<WidgetTypeInfo> findWidgetTypesInfosByWidgetsBundleId(TenantId tenantId, WidgetsBundleId widgetsBundleId, boolean fullSearch,
DeprecatedFilter deprecatedFilter, List<String> widgetTypes, PageLink pageLink) {
log.trace("Executing findWidgetTypesInfosByWidgetsBundleId, tenantId [{}], widgetsBundleId [{}], fullSearch [{}], deprecatedFilter [{}], widgetTypes [{}], pageLink [{}]",
tenantId, widgetsBundleId, fullSearch, deprecatedFilter, widgetTypes, pageLink);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validateId(widgetsBundleId, INCORRECT_WIDGETS_BUNDLE_ID + widgetsBundleId);
return widgetTypeDao.findWidgetTypesInfosByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId());
Validator.validatePageLink(pageLink);
return widgetTypeDao.findWidgetTypesInfosByWidgetsBundleId(tenantId.getId(), widgetsBundleId.getId(), fullSearch, deprecatedFilter, widgetTypes, pageLink);
}
@Override
@ -230,7 +236,7 @@ public class WidgetTypeServiceImpl implements WidgetTypeService {
@Override
protected PageData<WidgetTypeInfo> findEntities(TenantId tenantId, TenantId id, PageLink pageLink) {
return widgetTypeDao.findTenantWidgetTypesByTenantId(id.getId(), false, pageLink);
return widgetTypeDao.findTenantWidgetTypesByTenantId(id.getId(), false, DeprecatedFilter.ALL, null, pageLink);
}
@Override

4
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleDao.java

@ -54,7 +54,7 @@ public interface WidgetsBundleDao extends Dao<WidgetsBundle>, ExportableEntityDa
* @param pageLink the page link
* @return the list of widgets bundles objects
*/
PageData<WidgetsBundle> findSystemWidgetsBundles(TenantId tenantId, PageLink pageLink);
PageData<WidgetsBundle> findSystemWidgetsBundles(TenantId tenantId, boolean fullSearch, PageLink pageLink);
/**
* Find tenant widgets bundles by tenantId and page link.
@ -72,7 +72,7 @@ public interface WidgetsBundleDao extends Dao<WidgetsBundle>, ExportableEntityDa
* @param pageLink the page link
* @return the list of widgets bundles objects
*/
PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(UUID tenantId, PageLink pageLink);
PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantId(UUID tenantId, boolean fullSearch, PageLink pageLink);
}

16
dao/src/main/java/org/thingsboard/server/dao/widget/WidgetsBundleServiceImpl.java

@ -104,10 +104,10 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
}
@Override
public PageData<WidgetsBundle> findSystemWidgetsBundlesByPageLink(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findSystemWidgetsBundles, pageLink [{}]", pageLink);
public PageData<WidgetsBundle> findSystemWidgetsBundlesByPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
log.trace("Executing findSystemWidgetsBundles, fullSearch [{}], pageLink [{}]", fullSearch, pageLink);
Validator.validatePageLink(pageLink);
return widgetsBundleDao.findSystemWidgetsBundles(tenantId, pageLink);
return widgetsBundleDao.findSystemWidgetsBundles(tenantId, fullSearch, pageLink);
}
@Override
@ -117,7 +117,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT);
PageData<WidgetsBundle> pageData;
do {
pageData = findSystemWidgetsBundlesByPageLink(tenantId, pageLink);
pageData = findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
widgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -135,11 +135,11 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
}
@Override
public PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, PageLink pageLink) {
log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], pageLink [{}]", tenantId, pageLink);
public PageData<WidgetsBundle> findAllTenantWidgetsBundlesByTenantIdAndPageLink(TenantId tenantId, boolean fullSearch, PageLink pageLink) {
log.trace("Executing findAllTenantWidgetsBundlesByTenantIdAndPageLink, tenantId [{}], fullSearch [{}], pageLink [{}]", tenantId, fullSearch, pageLink);
Validator.validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
Validator.validatePageLink(pageLink);
return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), pageLink);
return widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId.getId(), fullSearch, pageLink);
}
@Override
@ -150,7 +150,7 @@ public class WidgetsBundleServiceImpl implements WidgetsBundleService {
PageLink pageLink = new PageLink(DEFAULT_WIDGETS_BUNDLE_LIMIT);
PageData<WidgetsBundle> pageData;
do {
pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
pageData = findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
widgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();

1
dao/src/main/resources/sql/schema-entities.sql

@ -489,6 +489,7 @@ CREATE TABLE IF NOT EXISTS widget_type (
image varchar(1000000),
deprecated boolean NOT NULL DEFAULT false,
description varchar(1024),
tags text[],
external_id uuid,
CONSTRAINT uq_widget_type_fqn UNIQUE (tenant_id, fqn),
CONSTRAINT widget_type_external_id_unq_key UNIQUE (tenant_id, external_id)

6
dao/src/main/resources/sql/schema-views-and-functions.sql

@ -285,3 +285,9 @@ BEGIN
RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text;
END
$$;
DROP VIEW IF EXISTS widget_type_info_view CASCADE;
CREATE OR REPLACE VIEW widget_type_info_view AS
SELECT t.*
, COALESCE((t.descriptor::json->>'type')::text, '') as widget_type
FROM widget_type t;

8
dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java

@ -174,7 +174,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
PageLink pageLink = new PageLink(19);
PageData<WidgetsBundle> pageData = null;
do {
pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, pageLink);
pageData = widgetsBundleService.findSystemWidgetsBundlesByPageLink(tenantId, false, pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -297,7 +297,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
PageLink pageLink = new PageLink(17);
PageData<WidgetsBundle> pageData = null;
do {
pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -314,7 +314,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
loadedWidgetsBundles.clear();
pageLink = new PageLink(14);
do {
pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
@ -336,7 +336,7 @@ public class WidgetsBundleServiceTest extends AbstractServiceTest {
loadedWidgetsBundles.clear();
pageLink = new PageLink(18);
do {
pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, pageLink);
pageData = widgetsBundleService.findAllTenantWidgetsBundlesByTenantIdAndPageLink(tenantId, false, pageLink);
loadedWidgetsBundles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();

67
dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDaoTest.java

@ -20,10 +20,17 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.widget.BaseWidgetType;
import org.thingsboard.server.common.data.widget.DeprecatedFilter;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetTypeInfo;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
@ -31,7 +38,10 @@ import org.thingsboard.server.dao.widget.WidgetTypeDao;
import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@ -43,7 +53,7 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
final String BUNDLE_ALIAS = "BUNDLE_ALIAS";
final int WIDGET_TYPE_COUNT = 3;
List<WidgetType> widgetTypeList;
List<WidgetTypeDetails> widgetTypeList;
WidgetsBundle widgetsBundle;
@Autowired
@ -64,17 +74,24 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
this.widgetsBundle = widgetsBundleDao.save(TenantId.SYS_TENANT_ID, widgetsBundle);
for (int i = 0; i < WIDGET_TYPE_COUNT; i++) {
var widgetType = createAndSaveWidgetType(i);
var widgetType = createAndSaveWidgetType(TenantId.SYS_TENANT_ID, i);
widgetTypeList.add(widgetType);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(this.widgetsBundle.getId(), widgetType.getId(), i));
}
widgetTypeList.sort(Comparator.comparing(BaseWidgetType::getName));
}
WidgetType createAndSaveWidgetType(int number) {
WidgetTypeDetails createAndSaveWidgetType(TenantId tenantId, int number) {
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(TenantId.SYS_TENANT_ID);
widgetType.setTenantId(tenantId);
widgetType.setName("WIDGET_TYPE_" + number);
widgetType.setDescription("WIDGET_TYPE_DESCRIPTION" + number);
widgetType.setFqn("FQN_" + number);
var descriptor = JacksonUtil.newObjectNode();
descriptor.put("type", number % 2 == 0 ? "latest" : "static");
widgetType.setDescriptor(descriptor);
String[] tags = new String[]{"Tag1_"+number, "Tag2_"+number, "TEST_"+number};
widgetType.setTags(tags);
return widgetTypeDao.save(TenantId.SYS_TENANT_ID, widgetType);
}
@ -92,6 +109,48 @@ public class JpaWidgetTypeDaoTest extends AbstractJpaDaoTest {
assertEquals(WIDGET_TYPE_COUNT, widgetTypes.size());
}
@Test
public void testFindSystemWidgetTypes() {
PageData<WidgetTypeInfo> widgetTypes = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.singletonList("static"),
new PageLink(1024, 0, "TYPE_DESCRIPTION", new SortOrder("name")));
assertEquals(1, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(1)), widgetTypes.getData().get(0));
widgetTypes = widgetTypeDao.findSystemWidgetTypes(TenantId.SYS_TENANT_ID, true, DeprecatedFilter.ALL, Collections.emptyList(),
new PageLink(1024, 0, "hfgfd tag2_2 ghg", new SortOrder("name")));
assertEquals(1, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(2)), widgetTypes.getData().get(0));
}
@Test
public void testFindTenantWidgetTypesByTenantId() {
UUID tenantId = Uuids.timeBased();
for (int i = 0; i < WIDGET_TYPE_COUNT; i++) {
var widgetType = createAndSaveWidgetType(new TenantId(tenantId), i);
widgetTypeList.add(widgetType);
}
PageData<WidgetTypeInfo> widgetTypes = widgetTypeDao.findTenantWidgetTypesByTenantId(tenantId, true, DeprecatedFilter.ALL, null,
new PageLink(10, 0, "", new SortOrder("name")));
assertEquals(WIDGET_TYPE_COUNT, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(3)), widgetTypes.getData().get(0));
assertEquals(new WidgetTypeInfo(widgetTypeList.get(4)), widgetTypes.getData().get(1));
assertEquals(new WidgetTypeInfo(widgetTypeList.get(5)), widgetTypes.getData().get(2));
}
@Test
public void testFindByWidgetTypeInfosByBundleId() {
PageData<WidgetTypeInfo> widgetTypes = widgetTypeDao.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID.getId(), widgetsBundle.getUuidId(),true, DeprecatedFilter.ALL, Collections.singletonList("latest"),
new PageLink(1024, 0, "TYPE_DESCRIPTION", new SortOrder("name")));
assertEquals(2, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(0)), widgetTypes.getData().get(0));
assertEquals(new WidgetTypeInfo(widgetTypeList.get(2)), widgetTypes.getData().get(1));
widgetTypes = widgetTypeDao.findWidgetTypesInfosByWidgetsBundleId(TenantId.SYS_TENANT_ID.getId(), widgetsBundle.getUuidId(), true, DeprecatedFilter.ALL, Collections.emptyList(),
new PageLink(1024, 0, "hfgfd TEST_0 ghg", new SortOrder("name")));
assertEquals(1, widgetTypes.getData().size());
assertEquals(new WidgetTypeInfo(widgetTypeList.get(0)), widgetTypes.getData().get(0));
}
@Test
public void testFindByTenantIdAndFqn() {
WidgetType result = widgetTypeList.get(0);

137
dao/src/test/java/org/thingsboard/server/dao/sql/widget/JpaWidgetsBundleDaoTest.java

@ -17,18 +17,27 @@ package org.thingsboard.server.dao.sql.widget;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.common.data.widget.WidgetsBundleWidget;
import org.thingsboard.server.dao.AbstractJpaDaoTest;
import org.thingsboard.server.dao.widget.WidgetTypeDao;
import org.thingsboard.server.dao.widget.WidgetsBundleDao;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.junit.Assert.assertEquals;
@ -38,15 +47,27 @@ import static org.junit.Assert.assertEquals;
public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
List<WidgetsBundle> widgetsBundles;
List<WidgetType> widgetTypeList;
@Autowired
private WidgetsBundleDao widgetsBundleDao;
@Autowired
private WidgetTypeDao widgetTypeDao;
@Before
public void setUp() {
widgetTypeList = new ArrayList<>();
}
@After
public void tearDown() {
for (WidgetsBundle widgetsBundle : widgetsBundles) {
widgetsBundleDao.removeById(widgetsBundle.getTenantId(), widgetsBundle.getUuidId());
}
for (WidgetType widgetType : widgetTypeList) {
widgetTypeDao.removeById(TenantId.SYS_TENANT_ID, widgetType.getUuidId());
}
}
@Test
@ -72,14 +93,53 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
assertEquals(30, widgetsBundles.size());
// Get first page
PageLink pageLink = new PageLink(10, 0, "WB");
PageData<WidgetsBundle> widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, pageLink);
PageData<WidgetsBundle> widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink);
assertEquals(10, widgetsBundles1.getData().size());
// Get next page
pageLink = pageLink.nextPageLink();
PageData<WidgetsBundle> widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, pageLink);
PageData<WidgetsBundle> widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, false, pageLink);
assertEquals(10, widgetsBundles2.getData().size());
}
@Test
public void testFindSystemWidgetsBundlesFullSearch() {
createSystemWidgetBundles(30, "WB_");
widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID).stream().sorted(Comparator.comparing(WidgetsBundle::getTitle)).collect(Collectors.toList());
assertEquals(30, widgetsBundles.size());
var widgetType1 = createAndSaveWidgetType(TenantId.SYS_TENANT_ID,1, "Test widget type 1", "This is the widget type 1", new String[]{"tag1", "Tag2", "TEST_TAG"});
var widgetType2 = createAndSaveWidgetType(TenantId.SYS_TENANT_ID,2, "Test widget type 2", "This is the widget type 2", new String[]{"tag3", "Tag5", "TEST_Tag2"});
var widgetsBundle1 = widgetsBundles.get(10);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle1.getId(), widgetType1.getId(), 0));
var widgetsBundle2 = widgetsBundles.get(15);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle2.getId(), widgetType2.getId(), 0));
var widgetsBundle3 = widgetsBundles.get(28);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType1.getId(), 0));
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType2.getId(), 1));
PageLink pageLink = new PageLink(10, 0, "widget type 1", new SortOrder("title"));
PageData<WidgetsBundle> widgetsBundles1 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink);
assertEquals(2, widgetsBundles1.getData().size());
assertEquals(widgetsBundle1, widgetsBundles1.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles1.getData().get(1));
pageLink = new PageLink(10, 0, "Test widget type 2", new SortOrder("title"));
PageData<WidgetsBundle> widgetsBundles2 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink);
assertEquals(2, widgetsBundles2.getData().size());
assertEquals(widgetsBundle2, widgetsBundles2.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles2.getData().get(1));
pageLink = new PageLink(10, 0, "ppp Fd v TAG1 tt", new SortOrder("title"));
PageData<WidgetsBundle> widgetsBundles3 = widgetsBundleDao.findSystemWidgetsBundles(TenantId.SYS_TENANT_ID, true, pageLink);
assertEquals(2, widgetsBundles3.getData().size());
assertEquals(widgetsBundle1, widgetsBundles3.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles3.getData().get(1));
}
@Test
public void testFindWidgetsBundlesByTenantId() {
UUID tenantId1 = Uuids.timeBased();
@ -120,22 +180,71 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
assertEquals(100, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size());
PageLink pageLink = new PageLink(30, 0, "WB");
PageData<WidgetsBundle> widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink);
PageData<WidgetsBundle> widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
assertEquals(30, widgetsBundles1.getData().size());
pageLink = pageLink.nextPageLink();
PageData<WidgetsBundle> widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink);
PageData<WidgetsBundle> widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
assertEquals(30, widgetsBundles2.getData().size());
pageLink = pageLink.nextPageLink();
PageData<WidgetsBundle> widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink);
PageData<WidgetsBundle> widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
assertEquals(10, widgetsBundles3.getData().size());
pageLink = pageLink.nextPageLink();
PageData<WidgetsBundle> widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, pageLink);
PageData<WidgetsBundle> widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, false, pageLink);
assertEquals(0, widgetsBundles4.getData().size());
}
@Test
public void testFindAllWidgetsBundlesByTenantIdFullSearch() {
UUID tenantId1 = Uuids.timeBased();
UUID tenantId2 = Uuids.timeBased();
for (int i = 0; i < 10; i++) {
createWidgetBundles(5, tenantId1, "WB1_" + i + "_");
createWidgetBundles(3, tenantId2, "WB2_" + i + "_");
createSystemWidgetBundles(2, "WB_SYS_" + i + "_");
}
widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID).stream().sorted(Comparator.comparing(WidgetsBundle::getTitle)).collect(Collectors.toList());;
assertEquals(100, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size());
var widgetType1 = createAndSaveWidgetType(new TenantId(tenantId1), 1, "Test widget type 1", "This is the widget type 1", new String[]{"tag1", "Tag2", "TEST_TAG"});
var widgetType2 = createAndSaveWidgetType(new TenantId(tenantId2), 2, "Test widget type 2", "This is the widget type 2", new String[]{"tag3", "Tag5", "TEST_Tag2"});
var widgetsBundle1 = widgetsBundles.stream().filter(widgetsBundle -> widgetsBundle.getTenantId().getId().equals(tenantId1)).collect(Collectors.toList()).get(10);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle1.getId(), widgetType1.getId(), 0));
var widgetsBundle2 = widgetsBundles.stream().filter(widgetsBundle -> widgetsBundle.getTenantId().getId().equals(tenantId2)).collect(Collectors.toList()).get(15);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle2.getId(), widgetType2.getId(), 0));
var widgetsBundle3 = widgetsBundles.stream().filter(widgetsBundle -> widgetsBundle.getTenantId().getId().equals(tenantId2)).collect(Collectors.toList()).get(28);
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType1.getId(), 0));
widgetTypeDao.saveWidgetsBundleWidget(new WidgetsBundleWidget(widgetsBundle3.getId(), widgetType2.getId(), 1));
PageLink pageLink = new PageLink(10, 0, "widget type 1", new SortOrder("title"));
PageData<WidgetsBundle> widgetsBundles1 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
assertEquals(1, widgetsBundles1.getData().size());
assertEquals(widgetsBundle1, widgetsBundles1.getData().get(0));
pageLink = new PageLink(10, 0, "Test widget type 2", new SortOrder("title"));
PageData<WidgetsBundle> widgetsBundles2 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
assertEquals(0, widgetsBundles2.getData().size());
PageData<WidgetsBundle> widgetsBundles3 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId2, true, pageLink);
assertEquals(2, widgetsBundles3.getData().size());
assertEquals(widgetsBundle2, widgetsBundles3.getData().get(0));
assertEquals(widgetsBundle3, widgetsBundles3.getData().get(1));
pageLink = new PageLink(10, 0, "ttt Tag2 ffff hhhh", new SortOrder("title"));
PageData<WidgetsBundle> widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId1, true, pageLink);
assertEquals(1, widgetsBundles4.getData().size());
assertEquals(widgetsBundle1, widgetsBundles4.getData().get(0));
PageData<WidgetsBundle> widgetsBundles5 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId2, true, pageLink);
assertEquals(1, widgetsBundles5.getData().size());
assertEquals(widgetsBundle3, widgetsBundles5.getData().get(0));
}
@Test
public void testSearchTextNotFound() {
UUID tenantId = Uuids.timeBased();
@ -144,7 +253,7 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetsBundles = widgetsBundleDao.find(TenantId.SYS_TENANT_ID);
assertEquals(10, widgetsBundleDao.find(TenantId.SYS_TENANT_ID).size());
PageLink textPageLink = new PageLink(30, 0, "TEXT_NOT_FOUND");
PageData<WidgetsBundle> widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, textPageLink);
PageData<WidgetsBundle> widgetsBundles4 = widgetsBundleDao.findAllTenantWidgetsBundlesByTenantId(tenantId, false, textPageLink);
assertEquals(0, widgetsBundles4.getData().size());
}
@ -169,4 +278,16 @@ public class JpaWidgetsBundleDaoTest extends AbstractJpaDaoTest {
widgetsBundleDao.save(TenantId.SYS_TENANT_ID, widgetsBundle);
}
}
WidgetType createAndSaveWidgetType(TenantId tenantId, int number, String name, String description, String[] tags) {
WidgetTypeDetails widgetType = new WidgetTypeDetails();
widgetType.setTenantId(tenantId);
widgetType.setName(name);
widgetType.setDescription(description);
widgetType.setTags(tags);
widgetType.setFqn("FQN_" + number);
var saved = widgetTypeDao.save(TenantId.SYS_TENANT_ID, widgetType);
this.widgetTypeList.add(saved);
return saved;
}
}

2
ui-ngx/src/app/core/http/entity.service.ts

@ -416,7 +416,7 @@ export class EntityService {
break;
case EntityType.WIDGETS_BUNDLE:
pageLink.sortOrder.property = 'title';
entitiesObservable = this.widgetService.getWidgetBundles(pageLink, config);
entitiesObservable = this.widgetService.getWidgetBundles(pageLink, false, config);
break;
case EntityType.NOTIFICATION_TARGET:
pageLink.sortOrder.property = 'name';

61
ui-ngx/src/app/core/http/widget.service.ts

@ -23,6 +23,7 @@ import { PageData } from '@shared/models/page/page-data';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import {
BaseWidgetType,
DeprecatedFilter,
fullWidgetTypeFqn,
WidgetType,
widgetType,
@ -46,8 +47,6 @@ export class WidgetService {
private systemWidgetsBundles: Array<WidgetsBundle>;
private tenantWidgetsBundles: Array<WidgetsBundle>;
private widgetTypeInfosCache = new Map<string, Array<WidgetTypeInfo>>();
private widgetsInfoInMemoryCache = new Map<string, WidgetInfo>();
private loadWidgetsBundleCacheSubject: ReplaySubject<void>;
@ -86,8 +85,8 @@ export class WidgetService {
);
}
public getWidgetBundles(pageLink: PageLink, config?: RequestConfig): Observable<PageData<WidgetsBundle>> {
return this.http.get<PageData<WidgetsBundle>>(`/api/widgetsBundles${pageLink.toQuery()}`,
public getWidgetBundles(pageLink: PageLink, fullSearch = false, config?: RequestConfig): Observable<PageData<WidgetsBundle>> {
return this.http.get<PageData<WidgetsBundle>>(`/api/widgetsBundles${pageLink.toQuery()}&fullSearch=${fullSearch}`,
defaultHttpOptionsFromConfig(config));
}
@ -109,21 +108,13 @@ export class WidgetService {
public updateWidgetsBundleWidgetTypes(widgetsBundleId: string, widgetTypeIds: Array<string>,
config?: RequestConfig): Observable<void> {
return this.http.post<void>(`/api/widgetsBundle/${widgetsBundleId}/widgetTypes`, widgetTypeIds,
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.widgetTypeInfosCache.delete(widgetsBundleId);
})
);
defaultHttpOptionsFromConfig(config));
}
public updateWidgetsBundleWidgetFqns(widgetsBundleId: string, widgetTypeFqns: Array<string>,
config?: RequestConfig): Observable<void> {
return this.http.post<void>(`/api/widgetsBundle/${widgetsBundleId}/widgetTypeFqns`, widgetTypeFqns,
defaultHttpOptionsFromConfig(config)).pipe(
tap(() => {
this.widgetTypeInfosCache.delete(widgetsBundleId);
})
);
defaultHttpOptionsFromConfig(config));
}
public deleteWidgetsBundle(widgetsBundleId: string, config?: RequestConfig) {
@ -155,16 +146,27 @@ export class WidgetService {
defaultHttpOptionsFromConfig(config));
}
public getBundleWidgetTypeInfos(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<WidgetTypeInfo>> {
if (this.widgetTypeInfosCache.has(widgetsBundleId)) {
return of(this.widgetTypeInfosCache.get(widgetsBundleId));
} else {
return this.http.get<Array<WidgetTypeInfo>>(`/api/widgetTypesInfos?widgetsBundleId=${widgetsBundleId}`,
defaultHttpOptionsFromConfig(config)).pipe(
tap((res) => this.widgetTypeInfosCache.set(widgetsBundleId, res) )
);
public getBundleWidgetTypeInfosList(widgetsBundleId: string,
config?: RequestConfig): Observable<Array<WidgetTypeInfo>> {
return this.getBundleWidgetTypeInfos(new PageLink(1024), widgetsBundleId, false, DeprecatedFilter.ALL, null, config).pipe(
map((data) => data.data)
);
}
public getBundleWidgetTypeInfos(pageLink: PageLink,
widgetsBundleId: string,
fullSearch = false,
deprecatedFilter = DeprecatedFilter.ALL,
widgetTypes: Array<widgetType> = null,
config?: RequestConfig): Observable<PageData<WidgetTypeInfo>> {
let url =
`/api/widgetTypesInfos${pageLink.toQuery()}&widgetsBundleId=${widgetsBundleId}` +
`&fullSearch=${fullSearch}&deprecatedFilter=${deprecatedFilter}`;
if (widgetTypes && widgetTypes.length) {
url += `&widgetTypeList=${widgetTypes.join(',')}`;
}
return this.http.get<PageData<WidgetTypeInfo>>(url, defaultHttpOptionsFromConfig(config));
}
public getWidgetType(fullFqn: string, config?: RequestConfig): Observable<WidgetType> {
@ -224,10 +226,14 @@ export class WidgetService {
}
public getWidgetTypes(pageLink: PageLink, tenantOnly = false,
fullSearch = false, config?: RequestConfig): Observable<PageData<WidgetTypeInfo>> {
return this.http.get<PageData<WidgetTypeInfo>>(
`/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}`,
defaultHttpOptionsFromConfig(config));
fullSearch = false, deprecatedFilter = DeprecatedFilter.ALL, widgetTypes: Array<widgetType> = null,
config?: RequestConfig): Observable<PageData<WidgetTypeInfo>> {
let url =
`/api/widgetTypes${pageLink.toQuery()}&tenantOnly=${tenantOnly}&fullSearch=${fullSearch}&deprecatedFilter=${deprecatedFilter}`;
if (widgetTypes && widgetTypes.length) {
url += `&widgetTypeList=${widgetTypes.join(',')}`;
}
return this.http.get<PageData<WidgetTypeInfo>>(url, defaultHttpOptionsFromConfig(config));
}
public getWidgetTemplate(widgetTypeParam: widgetType,
@ -301,6 +307,5 @@ export class WidgetService {
this.systemWidgetsBundles = undefined;
this.tenantWidgetsBundles = undefined;
this.loadWidgetsBundleCacheSubject = undefined;
this.widgetTypeInfosCache.clear();
}
}

43
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.html

@ -386,7 +386,7 @@
</tb-details-panel>
<tb-details-panel *ngIf="!isAddingWidgetClosed && !widgetEditMode" fxFlex
headerTitle="{{ isAddingWidget ?
((dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets' ? ('widget.select-widget' | translate) :
((dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets' ? ('dashboard.select-widget-value' | translate: { title: ('widget.all-widgets' | translate) }) :
(!dashboardWidgetSelectComponent?.widgetsBundle ?
'widget.select-widgets-bundle' : 'dashboard.select-widget-value') | translate: dashboardWidgetSelectComponent?.widgetsBundle)) : ''
}}"
@ -396,32 +396,24 @@
[isEdit]="false"
backgroundColor="#cfd8dc"
(closeDetails)="onAddWidgetClosed()"
(closeSearch)="onCloseSearchBundle()">
<div class="prefix-title-buttons" [fxShow]="(isAddingWidget && dashboardWidgetSelectComponent?.widgetsBundle) ? true : false" style="height: 28px; margin-right: 12px">
(closeSearch)="dashboardWidgetSelectComponent.search = ''">
<div class="prefix-title-buttons" [fxShow]="!!(isAddingWidget && (dashboardWidgetSelectComponent?.widgetsBundle || dashboardWidgetSelectComponent?.selectWidgetMode === 'allWidgets'))" style="height: 28px; margin-right: 12px">
<button class="tb-mat-28" mat-icon-button type="button" (click)="clearSelectedWidgetBundle()">
<mat-icon>arrow_back</mat-icon>
</button>
</div>
<div class="search-pane" *ngIf="isAddingWidget" fxLayout="row">
<div class="search-pane" *ngIf="isAddingWidget && dashboardWidgetSelectComponent" fxLayout="row">
<tb-widgets-bundle-search fxFlex
[(ngModel)]="searchBundle"
placeholder="{{ ((!dashboardWidgetSelectComponent?.widgetsBundle && dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles')
? 'widgets-bundle.search' : 'widget.search') | translate }}"
(ngModelChange)="searchBundle = $event">
[(ngModel)]="dashboardWidgetSelectComponent.search"
placeholder="{{ ((!dashboardWidgetSelectComponent.widgetsBundle && dashboardWidgetSelectComponent.selectWidgetMode === 'bundles')
? 'widgets-bundle.search' : 'widget.search') | translate }}">
</tb-widgets-bundle-search>
</div>
<div class="details-buttons" *ngIf="isAddingWidget" fxLayout="row" fxLayoutAlign="start center">
<button mat-button type="button" (click)="importWidget($event)"
*ngIf="!dashboardWidgetSelectComponent?.widgetsBundle">
*ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle">
<mat-icon>file_upload</mat-icon>{{ 'dashboard.import-widget' | translate }}</button>
<button mat-icon-button type="button"
*ngIf="dashboardWidgetSelectComponent?.widgetTypes.size > 1"
(click)="editWidgetsTypesToDisplay($event)"
matTooltip="{{ 'widget.filter' | translate }}"
matTooltipPosition="above">
<mat-icon>filter_list</mat-icon>
</button>
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent && !dashboardWidgetSelectComponent.widgetsBundle"
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.selectWidgetMode === 'bundles' && !dashboardWidgetSelectComponent?.widgetsBundle"
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
@ -429,22 +421,27 @@
<tb-toggle-option value="bundles">{{ 'widgets-bundle.widgets-bundles' | translate }}</tb-toggle-option>
<tb-toggle-option value="allWidgets">{{ 'widget.all-widgets' | translate }}</tb-toggle-option>
</tb-toggle-select>
<button mat-icon-button type="button"
*ngIf="dashboardWidgetSelectComponent?.widgetTypes.size > 1"
(click)="editWidgetsTypesToDisplay($event)"
matTooltip="{{ 'widget.filter' | translate }}"
matTooltipPosition="above">
<mat-icon>filter_list</mat-icon>
</button>
<tb-toggle-select *ngIf="dashboardWidgetSelectComponent?.hasDeprecated"
appearance="fill-invert"
disablePagination
selectMediaBreakpoint="xs"
[(ngModel)]="dashboardWidgetSelectComponent.widgetsListMode">
<tb-toggle-option value="all">{{ 'widget.all' | translate }}</tb-toggle-option>
<tb-toggle-option value="actual">{{ 'widget.actual' | translate }}</tb-toggle-option>
<tb-toggle-option value="deprecated">{{ 'widget.deprecated' | translate }}</tb-toggle-option>
[(ngModel)]="dashboardWidgetSelectComponent.deprecatedFilter">
<tb-toggle-option value="ALL">{{ 'widget.all' | translate }}</tb-toggle-option>
<tb-toggle-option value="ACTUAL">{{ 'widget.actual' | translate }}</tb-toggle-option>
<tb-toggle-option value="DEPRECATED">{{ 'widget.deprecated' | translate }}</tb-toggle-option>
</tb-toggle-select>
</div>
<tb-dashboard-widget-select #dashboardWidgetSelect
*ngIf="isAddingWidget"
[aliasController]="dashboardCtx.aliasController"
[searchBundle]="searchBundle"
[filterWidgetTypes]="filterWidgetTypes"
(widgetsBundleSelected)="widgetBundleSelected()"
(widgetSelected)="addWidgetFromType($event)">
</tb-dashboard-widget-select>
</tb-details-panel>

13
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts

@ -230,7 +230,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
forceDashboardMobileMode = false;
isAddingWidget = false;
isAddingWidgetClosed = true;
searchBundle = '';
filterWidgetTypes: widgetType[] = null;
isToolbarOpened = false;
@ -1153,7 +1152,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
addWidgetFromType(widget: WidgetInfo) {
this.onAddWidgetClosed();
this.searchBundle = '';
this.widgetComponentService.getWidgetInfo(widget.typeFullFqn).subscribe(
(widgetTypeInfo) => {
const config: WidgetConfig = this.dashboardUtils.widgetConfigFromWidgetType(widgetTypeInfo);
@ -1426,13 +1424,10 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
return widgetContextActions;
}
widgetBundleSelected(){
this.searchBundle = '';
}
clearSelectedWidgetBundle() {
this.searchBundle = '';
this.dashboardWidgetSelectComponent.search = '';
this.dashboardWidgetSelectComponent.widgetsBundle = null;
this.dashboardWidgetSelectComponent.selectWidgetMode = 'bundles';
}
editWidgetsTypesToDisplay($event: Event) {
@ -1482,10 +1477,6 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC
this.cd.markForCheck();
}
onCloseSearchBundle() {
this.searchBundle = '';
}
public updateDashboardImage($event: Event) {
if ($event) {
$event.stopPropagation();

128
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.html

@ -15,79 +15,105 @@
limitations under the License.
-->
<div class="widget-select">
<div class="tb-dashboard-widget-select">
<ng-container *ngIf="selectWidgetMode === 'bundles'; else allWidgets">
<div *ngIf="widgetsBundle; else bundles">
<ng-container *ngTemplateOutlet="widgets; context:{ currentWidgets$: widgets$ }"></ng-container>
</div>
<ng-container *ngIf="widgetsBundle; else bundles">
<ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: widgetsFetchFunction, filter: widgetsFilter }"></ng-container>
</ng-container>
</ng-container>
<ng-template #allWidgets>
<ng-container *ngTemplateOutlet="widgets; context:{ currentWidgets$: allWidgets$ }"></ng-container>
<ng-container *ngTemplateOutlet="widgets; context:{ fetchFunction: allWidgetsFetchFunction, filter: allWidgetsFilter }"></ng-container>
</ng-template>
<ng-template #widgets let-currentWidgets$="currentWidgets$">
<div *ngIf="(currentWidgets$ | async)?.length; else loadingWidgets" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
<div *ngFor="let widget of currentWidgets$ | async" class="mat-card-container">
<mat-card appearance="raised" fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="onWidgetClicked($event, widget)">
<div class="preview-container" fxFlex="45">
<img class="preview" [src]="getPreviewImage(widget.image)" alt="{{ widget.title }}">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>{{widget.title}}<div *ngIf="widget.deprecated" class="tb-deprecated" translate>widget.deprecated</div></mat-card-title>
<mat-card-subtitle>{{ 'widget.' + widget.type | translate }}</mat-card-subtitle>
<mat-card-content *ngIf="widget.description">
{{ widget.description }}
</mat-card-content>
</div>
</mat-card>
</div>
</div>
</ng-template>
<ng-template #loadingWidgets>
<div *ngIf="loadingWidgets$ | async; else emptyBundle" fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill">
<ng-template #widgets let-fetchFunction="fetchFunction" let-filter="filter">
<tb-scroll-grid
[columns]="columns"
[fetchFunction]="fetchFunction"
[filter]="filter"
[itemCard]="widgetCard"
[loadingCell]="widgetLoadingCard"
[dataLoading]="loadingWidgets"
[noData]="noWidgets">
</tb-scroll-grid>
<ng-template #widgetCard let-item="item">
<mat-card class="tb-widget-preview-card" appearance="raised" fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="onWidgetClicked($event, item)">
<div class="preview-container" fxFlex="45">
<img class="preview" [src]="getPreviewImage(item.image)" alt="{{ item.title }}">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>{{item.name}}<div *ngIf="item.deprecated" class="tb-deprecated" translate>widget.deprecated</div></mat-card-title>
<mat-card-subtitle>{{ 'widget.' + item.widgetType | translate }}</mat-card-subtitle>
<mat-card-content *ngIf="item.description">
{{ item.description }}
</mat-card-content>
</div>
</mat-card>
</ng-template>
<ng-template #loadingWidgets>
<div fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill">
<span class="mat-headline-5" style="padding-bottom: 20px;">
{{ 'widget.loading-widgets' | translate }}
</span>
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #emptyBundle>
<span translate
style="display: flex;"
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #noWidgets>
<span style="display: flex;"
fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill">{{(selectWidgetMode === 'bundles' ? 'widgets-bundle.empty' : 'widget.no-widgets-text') | translate}}</span>
</ng-template>
</ng-template>
<ng-template #bundles>
<div *ngIf="(widgetsBundles$ | async)?.length; else loadingWidgetBundles" fxFlexFill fxLayoutGap="12px grid" fxLayout="row wrap">
<div *ngFor="let widgetsBundle of widgetsBundles$ | async" class="mat-card-container">
<mat-card appearance="raised" fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="selectBundle($event, widgetsBundle)">
<div class="preview-container" fxFlex="45">
<img class="preview" [src]=getPreviewImage(widgetsBundle.image) alt="{{ widgetsBundle.title }}">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>{{ widgetsBundle.title }}</mat-card-title>
<mat-card-subtitle *ngIf="isSystem(widgetsBundle)" translate>widgets-bundle.system</mat-card-subtitle>
<mat-card-content *ngIf="widgetsBundle.description">
{{ widgetsBundle.description }}
</mat-card-content>
</div>
</mat-card>
</div>
</div>
<tb-scroll-grid
[columns]="columns"
[fetchFunction]="widgetBundlesFetchFunction"
[filter]="widgetsBundleFilter"
[itemCard]="widgetsBundleCard"
[loadingCell]="widgetLoadingCard"
[dataLoading]="loadingWidgetBundles"
[noData]="noWidgetBundles">
</tb-scroll-grid>
<ng-template #widgetsBundleCard let-item="item">
<mat-card class="tb-widget-preview-card" appearance="raised" fxFlexFill fxLayout="row" fxLayoutGap="16px" (click)="selectBundle($event, item)">
<div class="preview-container" fxFlex="45">
<img class="preview" [src]=getPreviewImage(item.image) alt="{{ item.title }}">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>{{ item.title }}</mat-card-title>
<mat-card-subtitle *ngIf="isSystem(item)" translate>widgets-bundle.system</mat-card-subtitle>
<mat-card-content *ngIf="item.description">
{{ item.description }}
</mat-card-content>
</div>
</mat-card>
</ng-template>
<ng-template #loadingWidgetBundles>
<div *ngIf="loadingWidgetBundles$ | async; else noWidgetBundles" fxLayout="column"
<div fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill">
<span class="mat-headline-5" style="padding-bottom: 20px;">
{{ 'widgets-bundle.loading-widgets-bundles' | translate }}
</span>
<mat-spinner strokeWidth="5"></mat-spinner>
</div>
<ng-template #noWidgetBundles>
</ng-template>
<ng-template #noWidgetBundles>
<span translate
style="display: flex;"
fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill">widgets-bundle.no-widgets-bundles-text</span>
</ng-template>
</ng-template>
</ng-template>
<ng-template #widgetLoadingCard>
<mat-card class="tb-widget-preview-card loading-cell" appearance="raised" fxLayout="row" fxLayoutGap="16px">
<div class="preview-container" fxFlex="45">
</div>
<div fxFlex fxLayout="column">
<mat-card-title>
</mat-card-title>
<mat-card-content>
</mat-card-content>
</div>
</mat-card>
</ng-template>
</div>

126
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.scss

@ -13,83 +13,89 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@import '../../../../../scss/constants';
:host{
min-width: 100%;
min-height: 100%;
position: absolute;
.tb-dashboard-widget-select {
background-color: #cfd8dc;
position: absolute;
inset: 0;
.widget-select {
padding: 12px 0 12px 12px;
background-color: #cfd8dc;
.mat-card-container {
flex: 0 0 100%;
max-width: 100%;
.mat-mdc-card.tb-widget-preview-card {
cursor: pointer;
transition: box-shadow 0.2s;
padding: 16px;
&:hover {
box-shadow: 0 2px 6px 6px rgb(0 0 0 / 20%), 0 1px 4px 2px rgb(0 0 0 / 14%), 0 1px 6px 0 rgb(0 0 0 / 12%)
}
&:hover {
.mat-mdc-card {
box-shadow: 0 2px 6px 6px rgb(0 0 0 / 20%), 0 1px 4px 2px rgb(0 0 0 / 14%), 0 1px 6px 0 rgb(0 0 0 / 12%)
}
&.loading-cell {
height: 100%;
min-height: 200px;
.mat-mdc-card-title {
height: 32px;
margin-bottom: 16px;
}
.preview-container {
height: 80%;
margin: auto 0;
}
.mat-mdc-card {
cursor: pointer;
transition: box-shadow 0.2s;
padding: 16px;
.mat-mdc-card-content {
height: 100%;
margin: 0;
}
.preview-container {
text-align: center;
margin: auto 0;
}
.preview-container, .mat-mdc-card-title, .mat-mdc-card-content {
background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);
border-radius: 5px;
background-size: 200% 100%;
animation: 1s shine linear infinite;
}
}
.preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.preview-container {
text-align: center;
margin: auto 0;
}
.mat-mdc-card-title {
font-size: 20px;
line-height: normal;
margin-bottom: 8px;
.tb-deprecated {
font-size: 14px;
color: rgba(209, 39, 48, 0.87);
}
}
.preview {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.mat-mdc-card-subtitle {
margin-bottom: 16px;
margin-top: -4px;
line-height: normal;
font-weight: normal;
}
.mat-mdc-card-title {
font-size: 20px;
line-height: normal;
margin-bottom: 8px;
.mat-mdc-card-content {
font-size: 14px;
line-height: 18px;
color: #666;
margin-bottom: 16px;
padding: 0;
}
.tb-deprecated {
font-size: 14px;
color: rgba(209, 39, 48, 0.87);
}
}
@media #{$mat-gt-xs} {
.mat-card-container {
flex: 0 0 50%;
max-width: 50%;
}
.mat-mdc-card-subtitle {
margin-bottom: 16px;
margin-top: -4px;
line-height: normal;
font-weight: normal;
}
@media screen and (min-width: 2000px) {
.mat-card-container {
flex: 0 0 33.333333%;
max-width: 33.333333%;
}
.mat-mdc-card-content {
font-size: 14px;
line-height: 18px;
color: #666;
margin-bottom: 16px;
padding: 0;
}
}
}
@keyframes shine {
to {
background-position-x: -200%;
}
}

307
ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-widget-select.component.ts

@ -14,74 +14,68 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
import { WidgetsBundle } from '@shared/models/widgets-bundle.model';
import { IAliasController } from '@core/api/widget-api.models';
import { NULL_UUID } from '@shared/models/id/has-uuid';
import { WidgetService } from '@core/http/widget.service';
import { fullWidgetTypeFqn, WidgetInfo, widgetType, WidgetTypeInfo } from '@shared/models/widget.models';
import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Observable, of, ReplaySubject } from 'rxjs';
import {
DeprecatedFilter,
fullWidgetTypeFqn,
WidgetInfo,
widgetType,
WidgetTypeInfo
} from '@shared/models/widget.models';
import { debounceTime, distinctUntilChanged, map, skip } from 'rxjs/operators';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { isDefinedAndNotNull } from '@core/utils';
import { isDefinedAndNotNull, isObject } from '@core/utils';
import { PageLink } from '@shared/models/page/page-link';
import { Direction } from '@shared/models/page/sort-order';
type widgetsListMode = 'all' | 'actual' | 'deprecated';
import { GridEntitiesFetchFunction, ScrollGridColumns } from '@home/models/datasource/scroll-grid-datasource';
type selectWidgetMode = 'bundles' | 'allWidgets';
interface WidgetsFilter {
search: string;
filter: widgetType[];
deprecatedFilter: DeprecatedFilter;
}
interface BundleWidgetsFilter extends WidgetsFilter {
widgetsBundleId: string;
}
@Component({
selector: 'tb-dashboard-widget-select',
templateUrl: './dashboard-widget-select.component.html',
styleUrls: ['./dashboard-widget-select.component.scss']
styleUrls: ['./dashboard-widget-select.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class DashboardWidgetSelectComponent implements OnInit {
private search$ = new BehaviorSubject<string>('');
private searchSubject = new BehaviorSubject<string>('');
private search$ = this.searchSubject.asObservable().pipe(
debounceTime(150));
private filterWidgetTypes$ = new BehaviorSubject<Array<widgetType>>(null);
private widgetsListMode$ = new BehaviorSubject<widgetsListMode>('actual');
private deprecatedFilter$ = new BehaviorSubject<DeprecatedFilter>(DeprecatedFilter.ACTUAL);
private selectWidgetMode$ = new BehaviorSubject<selectWidgetMode>('bundles');
private widgetsInfo: Observable<Array<WidgetInfo>>;
private widgetsBundleValue: WidgetsBundle;
private widgetsBundle$ = new BehaviorSubject<WidgetsBundle>(null);
widgetTypes = new Set<widgetType>();
hasDeprecated = false;
allWidgets$: Observable<Array<WidgetInfo>>;
widgets$: Observable<Array<WidgetInfo>>;
loadingWidgetsSubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
loadingWidgets$ = this.loadingWidgetsSubject.pipe(
share()
);
widgetsBundles$: Observable<Array<WidgetsBundle>>;
loadingWidgetBundlesSubject: BehaviorSubject<boolean> = new BehaviorSubject(true);
loadingWidgetBundles$ = this.loadingWidgetBundlesSubject.pipe(
share()
);
set widgetsBundle(widgetBundle: WidgetsBundle) {
if (this.widgetsBundleValue !== widgetBundle) {
this.widgetsBundleValue = widgetBundle;
if (widgetBundle === null) {
this.widgetTypes.clear();
this.hasDeprecated = false;
}
this.filterWidgetTypes$.next(null);
this.widgetsListMode$.next('actual');
this.widgetsInfo = null;
}
}
get widgetsBundle(): WidgetsBundle {
return this.widgetsBundleValue;
}
@Input()
aliasController: IAliasController;
@Input()
set searchBundle(search: string) {
this.search$.next(search);
set search(search: string) {
this.searchSubject.next(search);
}
get search(): string {
return this.searchSubject.value;
}
@Input()
@ -95,7 +89,18 @@ export class DashboardWidgetSelectComponent implements OnInit {
@Input()
set selectWidgetMode(mode: selectWidgetMode) {
this.selectWidgetMode$.next(mode);
if (this.selectWidgetMode$.value !== mode) {
if (mode === 'bundles' && this.widgetsBundle$.value === null) {
this.widgetTypes.clear();
this.hasDeprecated = false;
} else {
this.widgetTypes = new Set<widgetType>(Object.keys(widgetType).map(t => t as widgetType));
this.hasDeprecated = true;
}
this.filterWidgetTypes$.next(null);
this.deprecatedFilter$.next(DeprecatedFilter.ACTUAL);
this.selectWidgetMode$.next(mode);
}
}
get selectWidgetMode(): selectWidgetMode {
@ -103,77 +108,121 @@ export class DashboardWidgetSelectComponent implements OnInit {
}
@Input()
set widgetsListMode(mode: widgetsListMode) {
this.widgetsListMode$.next(mode);
set deprecatedFilter(filter: DeprecatedFilter) {
this.deprecatedFilter$.next(filter);
}
get deprecatedFilter(): DeprecatedFilter {
return this.deprecatedFilter$.value;
}
set widgetsBundle(widgetBundle: WidgetsBundle) {
if (this.widgetsBundle$.value !== widgetBundle) {
if (widgetBundle === null && this.selectWidgetMode$.value !== 'allWidgets') {
this.widgetTypes.clear();
this.hasDeprecated = false;
} else {
this.widgetTypes = new Set<widgetType>(Object.keys(widgetType).map(t => t as widgetType));
this.hasDeprecated = true;
}
this.filterWidgetTypes$.next(null);
this.deprecatedFilter$.next(DeprecatedFilter.ACTUAL);
this.widgetsBundle$.next(widgetBundle);
}
}
get widgetsListMode(): widgetsListMode {
return this.widgetsListMode$.value;
get widgetsBundle(): WidgetsBundle {
return this.widgetsBundle$.value;
}
@Output()
widgetSelected: EventEmitter<WidgetInfo> = new EventEmitter<WidgetInfo>();
@Output()
widgetsBundleSelected: EventEmitter<WidgetsBundle> = new EventEmitter<WidgetsBundle>();
columns: ScrollGridColumns = {
columns: 1,
breakpoints: {
'screen and (min-width: 2000px)': 3,
'screen and (min-width: 600px)': 2
}
};
widgetBundlesFetchFunction: GridEntitiesFetchFunction<WidgetsBundle, string>;
allWidgetsFetchFunction: GridEntitiesFetchFunction<WidgetTypeInfo, WidgetsFilter>;
widgetsFetchFunction: GridEntitiesFetchFunction<WidgetTypeInfo, BundleWidgetsFilter>;
widgetsBundleFilter = '';
allWidgetsFilter: WidgetsFilter = {search: '', filter: null, deprecatedFilter: DeprecatedFilter.ACTUAL};
widgetsFilter: BundleWidgetsFilter = {search: '', filter: null, deprecatedFilter: DeprecatedFilter.ACTUAL, widgetsBundleId: null};
constructor(private widgetsService: WidgetService,
private sanitizer: DomSanitizer,
private cd: ChangeDetectorRef) {
this.widgetsBundles$ = combineLatest([this.search$.asObservable(), this.selectWidgetMode$.asObservable()]).pipe(
distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
switchMap(search => this.fetchWidgetBundle(...search))
private cd: ChangeDetectorRef,
private sanitizer: DomSanitizer) {
this.widgetBundlesFetchFunction = (pageSize, page, filter) => {
const pageLink = new PageLink(pageSize, page, filter, {
property: 'title',
direction: Direction.ASC
});
return this.widgetsService.getWidgetBundles(pageLink, true);
};
this.allWidgetsFetchFunction = (pageSize, page, filter) => {
const pageLink = new PageLink(pageSize, page, filter.search, {
property: 'name',
direction: Direction.ASC
});
return this.widgetsService.getWidgetTypes(pageLink, false, true, filter.deprecatedFilter, filter.filter);
};
this.widgetsFetchFunction = (pageSize, page, filter) => {
const pageLink = new PageLink(pageSize, page, filter.search, {
property: 'name',
direction: Direction.ASC
});
return this.widgetsService.getBundleWidgetTypeInfos(pageLink, filter.widgetsBundleId,
true, filter.deprecatedFilter, filter.filter);
};
this.search$.pipe(
distinctUntilChanged(),
skip(1)
).subscribe(
(search) => {
this.widgetsBundleFilter = search;
this.cd.markForCheck();
}
);
this.allWidgets$ = combineLatest([this.search$.asObservable().pipe(
debounceTime(150)
), this.selectWidgetMode$.asObservable()]).pipe(
distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
switchMap(search => this.fetchAllWidgets(...search)),
share({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: false })
combineLatest({search: this.search$, filter: this.filterWidgetTypes$.asObservable(),
deprecatedFilter: this.deprecatedFilter$.asObservable()}).pipe(
distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
skip(1)
).subscribe(
(filter) => {
this.allWidgetsFilter = filter;
this.cd.markForCheck();
}
);
this.widgets$ = combineLatest([this.search$.asObservable(), this.filterWidgetTypes$.asObservable(), this.widgetsListMode$]).pipe(
combineLatest({search: this.search$, widgetsBundleId: this.widgetsBundle$.pipe(map(wb => wb !== null ? wb.id.id : null)),
filter: this.filterWidgetTypes$.asObservable(), deprecatedFilter: this.deprecatedFilter$.asObservable()}).pipe(
distinctUntilChanged((oldValue, newValue) => JSON.stringify(oldValue) === JSON.stringify(newValue)),
switchMap(search => this.fetchWidgets(...search))
skip(1)
).subscribe(
(filter) => {
if (filter.widgetsBundleId) {
this.widgetsFilter = filter;
this.cd.markForCheck();
}
}
);
}
ngOnInit(): void {
}
private getWidgets(): Observable<Array<WidgetInfo>> {
if (!this.widgetsInfo) {
if (this.widgetsBundle !== null) {
this.loadingWidgetsSubject.next(true);
this.widgetsInfo = this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).pipe(
map(widgets => {
const widgetTypes = new Set<widgetType>();
const hasDeprecated = widgets.some(w => w.deprecated);
const widgetInfos = widgets.map((widgetTypeInfo) => {
widgetTypes.add(widgetTypeInfo.widgetType);
return this.toWidgetInfo(widgetTypeInfo);
}
);
setTimeout(() => {
this.widgetTypes = widgetTypes;
this.hasDeprecated = hasDeprecated;
this.cd.markForCheck();
});
return widgetInfos;
}),
tap(() => {
this.loadingWidgetsSubject.next(false);
}),
share({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: false })
);
} else {
this.widgetsInfo = of([]);
}
}
return this.widgetsInfo;
}
onWidgetClicked($event: Event, widget: WidgetInfo): void {
this.widgetSelected.emit(widget);
onWidgetClicked($event: Event, widget: WidgetTypeInfo): void {
this.widgetSelected.emit(this.toWidgetInfo(widget));
}
isSystem(item: WidgetsBundle): boolean {
@ -183,8 +232,10 @@ export class DashboardWidgetSelectComponent implements OnInit {
selectBundle($event: Event, bundle: WidgetsBundle) {
$event.preventDefault();
this.widgetsBundle = bundle;
this.search$.next('');
this.widgetsBundleSelected.emit(bundle);
if (bundle.title?.toLowerCase().includes(this.search.toLowerCase()) ||
bundle.description?.toLowerCase().includes(this.search.toLowerCase())) {
this.searchSubject.next('');
}
}
getPreviewImage(imageUrl: string | null): SafeUrl | string {
@ -194,62 +245,8 @@ export class DashboardWidgetSelectComponent implements OnInit {
return '/assets/widget-preview-empty.svg';
}
private getWidgetsBundles(): Observable<Array<WidgetsBundle>> {
return this.widgetsService.getAllWidgetsBundles().pipe(
tap(() => this.loadingWidgetBundlesSubject.next(false)),
share({ connector: () => new ReplaySubject(1), resetOnError: false, resetOnComplete: false, resetOnRefCountZero: false })
);
}
private fetchWidgetBundle(search: string, mode: selectWidgetMode): Observable<Array<WidgetsBundle>> {
if (mode === 'bundles') {
return this.getWidgetsBundles().pipe(
map(bundles => search ? bundles.filter(
bundle => (
bundle.title?.toLowerCase().includes(search.toLowerCase()) ||
bundle.description?.toLowerCase().includes(search.toLowerCase())
)) : bundles
)
);
} else {
return of([]);
}
}
private fetchWidgets(search: string, filter: widgetType[], listMode: widgetsListMode): Observable<Array<WidgetInfo>> {
return this.getWidgets().pipe(
map(widgets => (listMode && listMode !== 'all') ?
widgets.filter((widget) => listMode === 'actual' ? !widget.deprecated : widget.deprecated) : widgets),
map(widgets => filter ? widgets.filter((widget) => filter.includes(widget.type)) : widgets),
map(widgets => search ? widgets.filter(
widget => (
widget.title?.toLowerCase().includes(search.toLowerCase()) ||
widget.description?.toLowerCase().includes(search.toLowerCase())
)) : widgets
)
);
}
private fetchAllWidgets(search: string, mode: selectWidgetMode): Observable<Array<WidgetInfo>> {
if (mode === 'allWidgets') {
const pageLink = new PageLink(1024, 0, search, {
property: 'name',
direction: Direction.ASC
});
return this.getAllWidgets(pageLink);
} else {
return of([]);
}
}
private getAllWidgets(pageLink: PageLink): Observable<Array<WidgetInfo>> {
this.loadingWidgetsSubject.next(true);
return this.widgetsService.getWidgetTypes(pageLink, false, true).pipe(
map(data => data.data.map(w => this.toWidgetInfo(w))),
tap(() => {
this.loadingWidgetsSubject.next(false);
})
);
isObject(value: any): boolean {
return isObject(value);
}
private toWidgetInfo(widgetTypeInfo: WidgetTypeInfo): WidgetInfo {

53
ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.html

@ -0,0 +1,53 @@
<!--
Copyright © 2016-2023 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.
-->
<cdk-virtual-scroll-viewport #viewport class="tb-scroll-grid-viewport" [itemSize]="itemSize" appendOnly>
<ng-container *cdkVirtualFor="let itemsRow of dataSource">
<div *ngIf="itemsRow" class="tb-scroll-grid-items-row" [style]="{gap: gap+'px'}">
<div *ngFor="let item of itemsRow" class="tb-scroll-grid-item-container">
<ng-container *ngIf="item === 'loadingCell'">
<ng-container *ngTemplateOutlet="loadingCell ? loadingCell : defaultLoadingCell"></ng-container>
</ng-container>
<ng-container *ngIf="isObject(item)">
<ng-container *ngTemplateOutlet="itemCard; context:{ item: item }"></ng-container>
</ng-container>
</div>
</div>
</ng-container>
</cdk-virtual-scroll-viewport>
<ng-container *ngIf="dataSource.isEmpty">
<ng-container *ngTemplateOutlet="loadingItems"></ng-container>
</ng-container>
<ng-template #loadingItems>
<ng-container *ngIf="dataSource.initialDataLoading; else emptyData">
<ng-container *ngTemplateOutlet="dataLoading ? dataLoading : defaultDataLoading"></ng-container>
</ng-container>
</ng-template>
<ng-template #emptyData>
<ng-container *ngTemplateOutlet="noData"></ng-container>
</ng-template>
<ng-template #defaultLoadingCell>
<div fxLayout="column" fxLayoutAlign="center center" [style]="{minHeight: itemSize + 'px'}">
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>
<ng-template #defaultDataLoading>
<div fxLayout="column"
fxLayoutAlign="center center" class="tb-absolute-fill">
<mat-spinner color="accent" strokeWidth="5"></mat-spinner>
</div>
</ng-template>

32
ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.scss

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2023 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.
*/
.tb-scroll-grid-viewport {
height: 100%;
.cdk-virtual-scroll-content-wrapper {
display: flex;
flex-direction: column;
}
.cdk-virtual-scroll-spacer {
height: auto !important;
}
.tb-scroll-grid-items-row {
display: flex;
flex-direction: row;
}
.tb-scroll-grid-item-container {
flex: 1;
}
}

103
ui-ngx/src/app/modules/home/components/grid/scroll-grid.component.ts

@ -0,0 +1,103 @@
///
/// Copyright © 2016-2023 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.
///
import {
AfterViewInit,
Component,
Input,
OnChanges,
OnInit,
Renderer2,
SimpleChanges,
TemplateRef,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import {
GridEntitiesFetchFunction,
ScrollGridColumns,
ScrollGridDatasource
} from '@home/models/datasource/scroll-grid-datasource';
import { BreakpointObserver } from '@angular/cdk/layout';
import { isObject } from '@app/core/utils';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
@Component({
selector: 'tb-scroll-grid',
templateUrl: './scroll-grid.component.html',
styleUrls: ['./scroll-grid.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ScrollGridComponent<T, F> implements OnInit, AfterViewInit, OnChanges {
@ViewChild('viewport')
viewport: CdkVirtualScrollViewport;
@Input()
columns: ScrollGridColumns = {columns: 1};
@Input()
fetchFunction: GridEntitiesFetchFunction<T, F>;
@Input()
filter: F;
@Input()
itemSize = 200;
@Input()
gap = 12;
@Input()
itemCard: TemplateRef<{item: T}>;
@Input()
loadingCell: TemplateRef<any>;
@Input()
dataLoading: TemplateRef<any>;
@Input()
noData: TemplateRef<any>;
dataSource: ScrollGridDatasource<T, F>;
constructor(private breakpointObserver: BreakpointObserver,
private renderer: Renderer2) {
}
ngOnInit(): void {
this.dataSource = new ScrollGridDatasource<T, F>(this.breakpointObserver, this.columns, this.fetchFunction, this.filter);
}
ngAfterViewInit() {
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'gap', this.gap + 'px');
this.renderer.setStyle(this.viewport._contentWrapper.nativeElement, 'padding', this.gap + 'px');
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (!change.firstChange && change.currentValue !== change.previousValue && propName === 'filter') {
this.dataSource.updateFilter(this.filter);
}
}
}
isObject(value: any): boolean {
return isObject(value);
}
}

7
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -181,6 +181,7 @@ import { DeleteTimeseriesPanelComponent } from '@home/components/attribute/delet
import {
ExportWidgetsBundleDialogComponent
} from '@home/components/import-export/export-widgets-bundle-dialog.component';
import { ScrollGridComponent } from '@home/components/grid/scroll-grid.component';
@NgModule({
declarations:
@ -325,7 +326,8 @@ import {
RateLimitsComponent,
RateLimitsTextComponent,
RateLimitsDetailsDialogComponent,
SendNotificationButtonComponent
SendNotificationButtonComponent,
ScrollGridComponent
],
imports: [
CommonModule,
@ -463,7 +465,8 @@ import {
RateLimitsComponent,
RateLimitsTextComponent,
RateLimitsDetailsDialogComponent,
SendNotificationButtonComponent
SendNotificationButtonComponent,
ScrollGridComponent
],
providers: [
WidgetComponentService,

250
ui-ngx/src/app/modules/home/models/datasource/scroll-grid-datasource.ts

@ -0,0 +1,250 @@
///
/// Copyright © 2016-2023 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.
///
import { DataSource, ListRange } from '@angular/cdk/collections';
import { CdkVirtualForOf, CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { BehaviorSubject, Observable, of, Subscription } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { emptyPageData, PageData } from '@shared/models/page/page-data';
import { BreakpointObserver } from '@angular/cdk/layout';
export type GridEntitiesFetchFunction<T, F> = (pageSize: number, page: number, filter: F) => Observable<PageData<T>>;
export type GridCellType = 'emptyCell' | 'loadingCell';
export interface ScrollGridColumns {
columns: number;
breakpoints?: {[breakpoint: string]: number};
}
export class ScrollGridDatasource<T, F> extends DataSource<(T | GridCellType)[]> {
public initialDataLoading = true;
private _data: T[] = [];
private _rows: (T | GridCellType)[][] = Array.from<T[]>({length: 100000});
private _hasNext = true;
private _columns: number;
private _viewport: CdkVirtualScrollViewport;
private _pendingRange: ListRange = null;
private _fetchingData = false;
private _fetchSubscription: Subscription;
private _totalElements = 0;
private _dataStream: BehaviorSubject<(T | GridCellType)[][]>;
private _subscription: Subscription;
constructor(private breakpointObserver: BreakpointObserver,
private columns: ScrollGridColumns,
private fetchFunction: GridEntitiesFetchFunction<T, F>,
private filter: F) {
super();
}
connect(collectionViewer: CdkVirtualForOf<(T | GridCellType)[]>): Observable<(T | GridCellType)[][]> {
this._viewport = (collectionViewer as any)._viewport;
this._init();
if (this.columns.breakpoints) {
const breakpoints = Object.keys(this.columns.breakpoints);
this._subscription.add(this.breakpointObserver.observe(breakpoints).subscribe(
() => {
this._columnsChanged(this._detectColumns());
}
));
}
this._subscription.add(
collectionViewer.viewChange.subscribe(range => this._fetchDataFromRange(range))
);
return this._dataStream;
}
disconnect(): void {
this._reset();
this._subscription.unsubscribe();
}
get isEmpty(): boolean {
return !this._data.length;
}
get active(): boolean {
return !!this._subscription && !this._subscription.closed;
}
public updateFilter(filter: F) {
this.filter = filter;
if (this.active) {
const prevLength = this._rows.length;
this._reset();
const dataLengthChanged = prevLength !== this._rows.length;
const range = this._viewport.getRenderedRange();
if (dataLengthChanged) {
// Force recalculate new range
if (range.start === 0) {
range.start = 1;
}
this._viewport.appendOnly = false;
}
const scrollOffset = this._viewport.measureScrollOffset();
if (scrollOffset > 0) {
this._viewport.scrollToOffset(0);
}
this._dataUpdated();
this._viewport.appendOnly = true;
if (!dataLengthChanged) {
this._fetchDataFromRange(range);
}
}
}
private _detectColumns(): number {
let columns = this.columns.columns;
if (this.columns.breakpoints) {
for (const breakpont of Object.keys(this.columns.breakpoints)) {
if (this.breakpointObserver.isMatched(breakpont)) {
columns = this.columns.breakpoints[breakpont];
break;
}
}
}
return columns;
}
private _init() {
this._subscription = new Subscription();
this._columns = this._detectColumns();
if (this._dataStream) {
this._dataStream.complete();
}
this._dataStream = new BehaviorSubject(this._rows);
}
private _reset() {
this._data = [];
this._totalElements = 0;
this.initialDataLoading = true;
this._rows = Array.from<T[]>({length: 100000});
this._hasNext = true;
this._pendingRange = null;
this._fetchingData = false;
if (this._fetchSubscription) {
this._fetchSubscription.unsubscribe();
}
}
private _columnsChanged(columns: number) {
if (this._columns !== columns) {
const fetchData = columns > this._columns;
this._columns = columns;
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000;
this._rows = Array.from<T[]>({length: rowsLength});
this._dataUpdated();
if (fetchData && this._hasNext) {
this._fetchDataFromRange(this._viewport.getRenderedRange());
}
}
}
private _fetchDataFromRange(range: ListRange) {
if (this._hasNext) {
if (this._fetchingData) {
this._pendingRange = range;
} else {
const endIndex = (range.end + 1) * this._columns;
if (endIndex > this._data.length) {
const startIndex = this._data.length;
const minPageSize = endIndex - startIndex;
const maxPageSize = minPageSize * 2;
let pageSize = minPageSize;
let page = Math.floor(startIndex / pageSize);
while (startIndex % pageSize !== 0 && pageSize <= maxPageSize) {
if (((page + 1) * pageSize) > endIndex) {
break;
}
pageSize++;
page = Math.floor(startIndex / pageSize);
}
const offset = startIndex % pageSize;
this._fetchData(offset, pageSize, page);
}
}
}
}
private _fetchData(offset: number, pageSize: number, page: number) {
this._fetchingData = true;
this._fetchSubscription = this.fetchFunction(pageSize, page, this.filter).pipe(
catchError(() => of(emptyPageData<T>()))
).subscribe(
(data) => {
this._hasNext = data.hasNext;
if (data.data.length > offset) {
for (let i = offset; i < data.data.length; i++) {
this._data.push(data.data[i]);
}
}
this._totalElements = data.totalElements;
const rowsLength = this._totalElements ? Math.ceil(this._totalElements / this._columns) : 100000;
this._rows = Array.from<T[]>({length: rowsLength});
this._dataUpdated();
this.initialDataLoading = false;
this._fetchingData = false;
if (this._pendingRange) {
const range = this._pendingRange;
this._pendingRange = null;
this._fetchDataFromRange(range);
}
}
);
}
private _dataUpdated() {
for (let index = 0; index < this._data.length; index++) {
const row = Math.floor(index / this._columns);
const col = index % this._columns;
if (!this._rows[row]) {
this._rows[row] = [];
}
this._rows[row][col] = this._data[index];
}
this._fillGridCells();
this._dataStream.next(this._rows);
}
private _fillGridCells() {
if (this._totalElements) {
const startIndex = this._data.length;
const endIndex = this._rows.length * this._columns;
for (let index = startIndex; index < endIndex; index++) {
const row = Math.floor(index / this._columns);
const col = index % this._columns;
const cellType: GridCellType = index < this._totalElements ? 'loadingCell' : 'emptyCell';
if (!this._rows[row]) {
this._rows[row] = [];
}
this._rows[row][col] = cellType;
}
}
}
}

3
ui-ngx/src/app/modules/home/models/widget-component.models.ts

@ -516,6 +516,7 @@ export interface WidgetInfo extends WidgetTypeDescriptor, WidgetControllerDescri
typeLatestDataKeySettingsSchema?: string | any;
image?: string;
description?: string;
tags?: string[];
componentType?: Type<IDynamicWidgetComponent>;
componentModuleRef?: NgModuleRef<DynamicComponentModule>;
}
@ -632,6 +633,7 @@ export const detailsToWidgetInfo = (widgetTypeDetailsEntity: WidgetTypeDetails):
const widgetInfo = toWidgetInfo(widgetTypeDetailsEntity);
widgetInfo.image = widgetTypeDetailsEntity.image;
widgetInfo.description = widgetTypeDetailsEntity.description;
widgetInfo.tags = widgetTypeDetailsEntity.tags;
return widgetInfo;
};
@ -672,6 +674,7 @@ export const toWidgetTypeDetails = (widgetInfo: WidgetInfo, id: WidgetTypeId, te
return {
...widgetTypeEntity,
description: widgetInfo.description,
tags: widgetInfo.tags,
image: widgetInfo.image
};
};

5
ui-ngx/src/app/modules/home/pages/widget/widget-editor.component.html

@ -253,6 +253,11 @@
rows="2" maxlength="255"></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/255</mat-hint>
</mat-form-field>
<tb-string-items-list
label="{{ 'widget.tags' | translate }}"
[(ngModel)]="widget.tags"
(ngModelChange)="isDirty = true">
</tb-string-items-list>
<mat-slide-toggle class="mat-block" style="padding-bottom: 16px;"
[(ngModel)]="widget.deprecated"
(ngModelChange)="isDirty = true">

2
ui-ngx/src/app/modules/home/pages/widget/widget-library-routing.module.ts

@ -58,7 +58,7 @@ export class WidgetsBundleWidgetsResolver implements Resolve<Array<WidgetTypeInf
resolve(route: ActivatedRouteSnapshot): Observable<Array<WidgetTypeInfo>> {
const widgetsBundleId = route.params.widgetsBundleId;
return this.widgetsService.getBundleWidgetTypeInfos(widgetsBundleId);
return this.widgetsService.getBundleWidgetTypeInfosList(widgetsBundleId);
}
}

4
ui-ngx/src/app/modules/home/pages/widget/widget-type.component.html

@ -58,6 +58,10 @@
<textarea matInput formControlName="description" rows="2" maxlength="1024" #descriptionInput></textarea>
<mat-hint align="end">{{descriptionInput.value?.length || 0}}/1024</mat-hint>
</mat-form-field>
<tb-string-items-list
label="{{ 'widget.tags' | translate }}"
formControlName="tags">
</tb-string-items-list>
<mat-slide-toggle formControlName="deprecated">
{{ 'widget.deprecated' | translate }}
</mat-slide-toggle>

2
ui-ngx/src/app/modules/home/pages/widget/widget-type.component.ts

@ -52,6 +52,7 @@ export class WidgetTypeComponent extends EntityComponent<WidgetTypeDetails> {
name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]],
image: [entity ? entity.image : ''],
description: [entity ? entity.description : '', Validators.maxLength(1024)],
tags: [entity ? entity.tags : []],
deprecated: [entity ? entity.deprecated : false]
}
);
@ -62,6 +63,7 @@ export class WidgetTypeComponent extends EntityComponent<WidgetTypeDetails> {
name: entity.name,
image: entity.image,
description: entity.description,
tags: entity.tags,
deprecated: entity.deprecated
});
}

2
ui-ngx/src/app/modules/home/pages/widget/widgets-bundle-widgets.component.ts

@ -152,7 +152,7 @@ export class WidgetsBundleWidgetsComponent extends PageComponent implements OnIn
cancel() {
if (this.isDirty) {
this.widgetsService.getBundleWidgetTypeInfos(this.widgetsBundle.id.id).subscribe(
this.widgetsService.getBundleWidgetTypeInfosList(this.widgetsBundle.id.id).subscribe(
(widgets) => {
this.widgets = [...widgets];
this.isDirty = false;

8
ui-ngx/src/app/shared/models/widget.models.ts

@ -232,12 +232,20 @@ export interface WidgetType extends BaseWidgetType {
export interface WidgetTypeInfo extends BaseWidgetType {
image: string;
description: string;
tags: string[];
widgetType: widgetType;
}
export interface WidgetTypeDetails extends WidgetType, ExportableEntity<WidgetTypeId> {
image: string;
description: string;
tags: string[];
}
export enum DeprecatedFilter {
ALL = 'ALL',
ACTUAL = 'ACTUAL',
DEPRECATED = 'DEPRECATED'
}
export enum LegendDirection {

1
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -4789,6 +4789,7 @@
"latest-datakey-settings-schema": "Latest data key settings schema",
"widget-settings": "Widget settings",
"description": "Description",
"tags": "Tags",
"image-preview": "Image preview",
"settings-form-selector": "Settings form selector",
"data-key-settings-form-selector": "Data key settings form selector",

Loading…
Cancel
Save